diff options
Diffstat (limited to 'src/bun.js/api')
-rw-r--r-- | src/bun.js/api/FFI.h | 252 | ||||
-rw-r--r-- | src/bun.js/api/bun.zig | 3007 | ||||
-rw-r--r-- | src/bun.js/api/ffi.zig | 1436 | ||||
-rw-r--r-- | src/bun.js/api/html_rewriter.zig | 1886 | ||||
-rw-r--r-- | src/bun.js/api/libtcc1.a.macos-aarch64 | bin | 0 -> 29988 bytes | |||
-rw-r--r-- | src/bun.js/api/libtcc1.c | 606 | ||||
-rw-r--r-- | src/bun.js/api/router.zig | 541 | ||||
-rw-r--r-- | src/bun.js/api/server.zig | 1844 | ||||
-rw-r--r-- | src/bun.js/api/transpiler.zig | 1304 |
9 files changed, 10876 insertions, 0 deletions
diff --git a/src/bun.js/api/FFI.h b/src/bun.js/api/FFI.h new file mode 100644 index 000000000..e17e15d30 --- /dev/null +++ b/src/bun.js/api/FFI.h @@ -0,0 +1,252 @@ +// This file is part of Bun! +// You can find the original source: +// https://github.com/Jarred-Sumner/bun/blob/main/src/bun.js/api/FFI.h#L2 +// +// clang-format off +// This file is only compatible with 64 bit CPUs +// It must be kept in sync with JSCJSValue.h +// https://github.com/Jarred-Sumner/WebKit/blob/72c2052b781cbfd4af867ae79ac9de460e392fba/Source/JavaScriptCore/runtime/JSCJSValue.h#L455-L458 +#ifdef IS_CALLBACK +#define INJECT_BEFORE int c = 500; // This is a callback, so we need to inject code before the call +#endif +#define IS_BIG_ENDIAN 0 +#define USE_JSVALUE64 1 +#define USE_JSVALUE32_64 0 + + +// /* 7.18.1.1 Exact-width integer types */ +typedef unsigned char uint8_t; +typedef signed char int8_t; +typedef short int16_t; +typedef unsigned short uint16_t; +typedef int int32_t; +typedef unsigned int uint32_t; +typedef long long int64_t; +typedef unsigned long long uint64_t; +typedef unsigned long long size_t; +typedef long intptr_t; +typedef uint64_t uintptr_t; +typedef _Bool bool; + +#define true 1 +#define false 0 + + +#ifdef INJECT_BEFORE +// #include <stdint.h> +#endif +// #include <tcclib.h> + +// This value is 2^49, used to encode doubles such that the encoded value will +// begin with a 15-bit pattern within the range 0x0002..0xFFFC. +#define DoubleEncodeOffsetBit 49 +#define DoubleEncodeOffset (1ll << DoubleEncodeOffsetBit) +#define OtherTag 0x2 +#define BoolTag 0x4 +#define UndefinedTag 0x8 +#define TagValueFalse (OtherTag | BoolTag | false) +#define TagValueTrue (OtherTag | BoolTag | true) +#define TagValueUndefined (OtherTag | UndefinedTag) +#define TagValueNull (OtherTag) +#define NotCellMask NumberTag | OtherTag + +#define MAX_INT32 2147483648 +#define MAX_INT52 9007199254740991 + +// If all bits in the mask are set, this indicates an integer number, +// if any but not all are set this value is a double precision number. +#define NumberTag 0xfffe000000000000ll + +typedef void* JSCell; + +typedef union EncodedJSValue { + int64_t asInt64; + +#if USE_JSVALUE64 + JSCell *ptr; +#endif + +#if IS_BIG_ENDIAN + struct { + int32_t tag; + int32_t payload; + } asBits; +#else + struct { + int32_t payload; + int32_t tag; + } asBits; +#endif + + void* asPtr; + double asDouble; +} EncodedJSValue; + +EncodedJSValue ValueUndefined = { TagValueUndefined }; +EncodedJSValue ValueTrue = { TagValueTrue }; + +typedef void* JSContext; + +// Bun_FFI_PointerOffsetToArgumentsList is injected into the build +// The value is generated in `make sizegen` +// The value is 6. +// On ARM64_32, the value is something else but it really doesn't matter for our case +// However, I don't want this to subtly break amidst future upgrades to JavaScriptCore +#define LOAD_ARGUMENTS_FROM_CALL_FRAME \ + int64_t *argsPtr = (int64_t*)((size_t*)callFrame + Bun_FFI_PointerOffsetToArgumentsList) + + +#ifdef IS_CALLBACK +extern int64_t bun_call(JSContext, void* func, void* thisValue, size_t len, const EncodedJSValue args[], void* exception); +JSContext cachedJSContext; +void* cachedCallbackFunction; +#endif + +static bool JSVALUE_IS_CELL(EncodedJSValue val) __attribute__((__always_inline__)); +static bool JSVALUE_IS_INT32(EncodedJSValue val) __attribute__((__always_inline__)); +static bool JSVALUE_IS_NUMBER(EncodedJSValue val) __attribute__((__always_inline__)); + +static uint64_t JSVALUE_TO_UINT64(void* globalObject, EncodedJSValue value) __attribute__((__always_inline__)); +static int64_t JSVALUE_TO_INT64(EncodedJSValue value) __attribute__((__always_inline__)); +uint64_t JSVALUE_TO_UINT64_SLOW(void* globalObject, EncodedJSValue value); +int64_t JSVALUE_TO_INT64_SLOW(EncodedJSValue value); + +EncodedJSValue UINT64_TO_JSVALUE_SLOW(void* globalObject, uint64_t val); +EncodedJSValue INT64_TO_JSVALUE_SLOW(void* globalObject, int64_t val); +static EncodedJSValue UINT64_TO_JSVALUE(void* globalObject, uint64_t val) __attribute__((__always_inline__)); +static EncodedJSValue INT64_TO_JSVALUE(void* globalObject, int64_t val) __attribute__((__always_inline__)); + + +static EncodedJSValue INT32_TO_JSVALUE(int32_t val) __attribute__((__always_inline__)); +static EncodedJSValue DOUBLE_TO_JSVALUE(double val) __attribute__((__always_inline__)); +static EncodedJSValue FLOAT_TO_JSVALUE(float val) __attribute__((__always_inline__)); +static EncodedJSValue BOOLEAN_TO_JSVALUE(bool val) __attribute__((__always_inline__)); +static EncodedJSValue PTR_TO_JSVALUE(void* ptr) __attribute__((__always_inline__)); + +static void* JSVALUE_TO_PTR(EncodedJSValue val) __attribute__((__always_inline__)); +static int32_t JSVALUE_TO_INT32(EncodedJSValue val) __attribute__((__always_inline__)); +static float JSVALUE_TO_FLOAT(EncodedJSValue val) __attribute__((__always_inline__)); +static double JSVALUE_TO_DOUBLE(EncodedJSValue val) __attribute__((__always_inline__)); +static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__)); + +static bool JSVALUE_IS_CELL(EncodedJSValue val) { + return !(val.asInt64 & NotCellMask); +} + +static bool JSVALUE_IS_INT32(EncodedJSValue val) { + return (val.asInt64 & NumberTag) == NumberTag; +} + +static bool JSVALUE_IS_NUMBER(EncodedJSValue val) { + return val.asInt64 & NumberTag; +} + + +static void* JSVALUE_TO_PTR(EncodedJSValue val) { + // must be a double + return (void*)(val.asInt64 - DoubleEncodeOffset); +} + +static EncodedJSValue PTR_TO_JSVALUE(void* ptr) { + EncodedJSValue val; + val.asInt64 = (int64_t)ptr + DoubleEncodeOffset; + return val; +} + +static int32_t JSVALUE_TO_INT32(EncodedJSValue val) { + return val.asInt64; +} + +static EncodedJSValue INT32_TO_JSVALUE(int32_t val) { + EncodedJSValue res; + res.asInt64 = NumberTag | (uint32_t)val; + return res; +} + + +static EncodedJSValue DOUBLE_TO_JSVALUE(double val) { + EncodedJSValue res; + res.asDouble = val; + res.asInt64 += DoubleEncodeOffset; + return res; +} + +static EncodedJSValue FLOAT_TO_JSVALUE(float val) { + return DOUBLE_TO_JSVALUE((double)val); +} + +static EncodedJSValue BOOLEAN_TO_JSVALUE(bool val) { + EncodedJSValue res; + res.asInt64 = val ? TagValueTrue : TagValueFalse; + return res; +} + + +static double JSVALUE_TO_DOUBLE(EncodedJSValue val) { + val.asInt64 -= DoubleEncodeOffset; + return val.asDouble; +} + +static float JSVALUE_TO_FLOAT(EncodedJSValue val) { + return (float)JSVALUE_TO_DOUBLE(val); +} + +static bool JSVALUE_TO_BOOL(EncodedJSValue val) { + return val.asInt64 == TagValueTrue; +} + + +static uint64_t JSVALUE_TO_UINT64(void* globalObject, EncodedJSValue value) { + if (JSVALUE_IS_INT32(value)) { + return (uint64_t)JSVALUE_TO_INT32(value); + } + + if (JSVALUE_IS_NUMBER(value)) { + return (uint64_t)JSVALUE_TO_DOUBLE(value); + } + + return JSVALUE_TO_UINT64_SLOW(globalObject, value); +} +static int64_t JSVALUE_TO_INT64(EncodedJSValue value) { + if (JSVALUE_IS_INT32(value)) { + return (int64_t)JSVALUE_TO_INT32(value); + } + + if (JSVALUE_IS_NUMBER(value)) { + return (int64_t)JSVALUE_TO_DOUBLE(value); + } + + return JSVALUE_TO_INT64_SLOW(value); +} + +static EncodedJSValue UINT64_TO_JSVALUE(void* globalObject, uint64_t val) { + if (val < MAX_INT32) { + return INT32_TO_JSVALUE((int32_t)val); + } + + if (val < MAX_INT52) { + return DOUBLE_TO_JSVALUE((double)val); + } + + return UINT64_TO_JSVALUE_SLOW(globalObject, val); +} + +static EncodedJSValue INT64_TO_JSVALUE(void* globalObject, int64_t val) { + if (val >= -MAX_INT32 && val <= MAX_INT32) { + return INT32_TO_JSVALUE((int32_t)val); + } + + if (val >= -MAX_INT52 && val <= MAX_INT52) { + return DOUBLE_TO_JSVALUE((double)val); + } + + return INT64_TO_JSVALUE_SLOW(globalObject, val); +} + +#ifndef IS_CALLBACK +void* JSFunctionCall(void* globalObject, void* callFrame); + +#endif + + +// --- Generated Code --- diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig new file mode 100644 index 000000000..c5831ed59 --- /dev/null +++ b/src/bun.js/api/bun.zig @@ -0,0 +1,3007 @@ +const Bun = @This(); +const default_allocator = @import("../../global.zig").default_allocator; +const bun = @import("../../global.zig"); +const Environment = bun.Environment; +const NetworkThread = @import("http").NetworkThread; +const Global = bun.Global; +const strings = bun.strings; +const string = bun.string; +const Output = @import("../../global.zig").Output; +const MutableString = @import("../../global.zig").MutableString; +const std = @import("std"); +const Allocator = std.mem.Allocator; +const IdentityContext = @import("../../identity_context.zig").IdentityContext; +const Fs = @import("../../fs.zig"); +const Resolver = @import("../../resolver/resolver.zig"); +const ast = @import("../../import_record.zig"); +const NodeModuleBundle = @import("../../node_module_bundle.zig").NodeModuleBundle; +const MacroEntryPoint = @import("../../bundler.zig").MacroEntryPoint; +const logger = @import("../../logger.zig"); +const Api = @import("../../api/schema.zig").Api; +const options = @import("../../options.zig"); +const Bundler = @import("../../bundler.zig").Bundler; +const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint; +const js_printer = @import("../../js_printer.zig"); +const js_parser = @import("../../js_parser.zig"); +const js_ast = @import("../../js_ast.zig"); +const hash_map = @import("../../hash_map.zig"); +const http = @import("../../http.zig"); +const NodeFallbackModules = @import("../../node_fallbacks.zig"); +const ImportKind = ast.ImportKind; +const Analytics = @import("../../analytics/analytics_thread.zig"); +const ZigString = @import("../../jsc.zig").ZigString; +const Runtime = @import("../../runtime.zig"); +const Router = @import("./router.zig"); +const ImportRecord = ast.ImportRecord; +const DotEnv = @import("../../env_loader.zig"); +const ParseResult = @import("../../bundler.zig").ParseResult; +const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON; +const MacroRemap = @import("../../resolver/package_json.zig").MacroMap; +const WebCore = @import("../../jsc.zig").WebCore; +const Request = WebCore.Request; +const Response = WebCore.Response; +const Headers = WebCore.Headers; +const Fetch = WebCore.Fetch; +const FetchEvent = WebCore.FetchEvent; +const js = @import("../../jsc.zig").C; +const JSC = @import("../../jsc.zig"); +const JSError = @import("../base.zig").JSError; +const d = @import("../base.zig").d; +const MarkedArrayBuffer = @import("../base.zig").MarkedArrayBuffer; +const getAllocator = @import("../base.zig").getAllocator; +const JSValue = @import("../../jsc.zig").JSValue; +const NewClass = @import("../base.zig").NewClass; +const Microtask = @import("../../jsc.zig").Microtask; +const JSGlobalObject = @import("../../jsc.zig").JSGlobalObject; +const ExceptionValueRef = @import("../../jsc.zig").ExceptionValueRef; +const JSPrivateDataPtr = @import("../../jsc.zig").JSPrivateDataPtr; +const ZigConsoleClient = @import("../../jsc.zig").ZigConsoleClient; +const Node = @import("../../jsc.zig").Node; +const ZigException = @import("../../jsc.zig").ZigException; +const ZigStackTrace = @import("../../jsc.zig").ZigStackTrace; +const ErrorableResolvedSource = @import("../../jsc.zig").ErrorableResolvedSource; +const ResolvedSource = @import("../../jsc.zig").ResolvedSource; +const JSPromise = @import("../../jsc.zig").JSPromise; +const JSInternalPromise = @import("../../jsc.zig").JSInternalPromise; +const JSModuleLoader = @import("../../jsc.zig").JSModuleLoader; +const JSPromiseRejectionOperation = @import("../../jsc.zig").JSPromiseRejectionOperation; +const Exception = @import("../../jsc.zig").Exception; +const ErrorableZigString = @import("../../jsc.zig").ErrorableZigString; +const ZigGlobalObject = @import("../../jsc.zig").ZigGlobalObject; +const VM = @import("../../jsc.zig").VM; +const JSFunction = @import("../../jsc.zig").JSFunction; +const Config = @import("../config.zig"); +const URL = @import("../../url.zig").URL; +const Transpiler = @import("./transpiler.zig"); +const VirtualMachine = @import("../javascript.zig").VirtualMachine; +const IOTask = JSC.IOTask; +const zlib = @import("../../zlib.zig"); + +const is_bindgen = JSC.is_bindgen; +const max_addressible_memory = std.math.maxInt(u56); + +threadlocal var css_imports_list_strings: [512]ZigString = undefined; +threadlocal var css_imports_list: [512]Api.StringPointer = undefined; +threadlocal var css_imports_list_tail: u16 = 0; +threadlocal var css_imports_buf: std.ArrayList(u8) = undefined; +threadlocal var css_imports_buf_loaded: bool = false; + +threadlocal var routes_list_strings: [1024]ZigString = undefined; + +pub fn onImportCSS( + resolve_result: *const Resolver.Result, + import_record: *ImportRecord, + origin: URL, +) void { + if (!css_imports_buf_loaded) { + css_imports_buf = std.ArrayList(u8).initCapacity( + VirtualMachine.vm.allocator, + import_record.path.text.len, + ) catch unreachable; + css_imports_buf_loaded = true; + } + + var writer = css_imports_buf.writer(); + const offset = css_imports_buf.items.len; + css_imports_list[css_imports_list_tail] = .{ + .offset = @truncate(u32, offset), + .length = 0, + }; + getPublicPath(resolve_result.path_pair.primary.text, origin, @TypeOf(writer), writer); + const length = css_imports_buf.items.len - offset; + css_imports_list[css_imports_list_tail].length = @truncate(u32, length); + css_imports_list_tail += 1; +} + +pub fn flushCSSImports() void { + if (css_imports_buf_loaded) { + css_imports_buf.clearRetainingCapacity(); + css_imports_list_tail = 0; + } +} + +pub fn getCSSImports() []ZigString { + var i: u16 = 0; + const tail = css_imports_list_tail; + while (i < tail) : (i += 1) { + ZigString.fromStringPointer(css_imports_list[i], css_imports_buf.items, &css_imports_list_strings[i]); + } + return css_imports_list_strings[0..tail]; +} + +pub fn inspect( + // this + _: void, + ctx: js.JSContextRef, + // function + _: js.JSObjectRef, + // thisObject + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + if (arguments.len == 0) + return ZigString.Empty.toValue(ctx.ptr()).asObjectRef(); + + for (arguments) |arg| { + JSC.C.JSValueProtect(ctx, arg); + } + defer { + for (arguments) |arg| { + JSC.C.JSValueUnprotect(ctx, arg); + } + } + + // very stable memory address + var array = MutableString.init(getAllocator(ctx), 0) catch unreachable; + var buffered_writer_ = MutableString.BufferedWriter{ .context = &array }; + var buffered_writer = &buffered_writer_; + + var writer = buffered_writer.writer(); + const Writer = @TypeOf(writer); + // we buffer this because it'll almost always be < 4096 + // when it's under 4096, we want to avoid the dynamic allocation + ZigConsoleClient.format( + .Debug, + ctx.ptr(), + @ptrCast([*]const JSValue, arguments.ptr), + arguments.len, + Writer, + Writer, + writer, + false, + false, + false, + ); + buffered_writer.flush() catch { + return JSC.C.JSValueMakeUndefined(ctx); + }; + + // we are going to always clone to keep things simple for now + // the common case here will be stack-allocated, so it should be fine + var out = ZigString.init(array.toOwnedSliceLeaky()).withEncoding(); + const ret = out.toValueGC(ctx); + array.deinit(); + return ret.asObjectRef(); + + // // when it's a small thing, rely on GC to manage the memory + // if (writer.context.pos < 2048 and array.list.items.len == 0) { + // var slice = writer.context.buffer[0..writer.context.pos]; + // if (slice.len == 0) { + // return ZigString.Empty.toValue(ctx.ptr()).asObjectRef(); + // } + + // var zig_str = + // return zig_str.toValueGC(ctx.ptr()).asObjectRef(); + // } + + // // when it's a big thing, we will manage it + // { + // writer.context.flush() catch {}; + // var slice = writer.context.context.toOwnedSlice(); + + // var zig_str = ZigString.init(slice).withEncoding(); + // if (!zig_str.isUTF8()) { + // return zig_str.toExternalValue(ctx.ptr()).asObjectRef(); + // } else { + // return zig_str.toValueGC(ctx.ptr()).asObjectRef(); + // } + // } +} + +pub fn registerMacro( + // this + _: void, + ctx: js.JSContextRef, + // function + _: js.JSObjectRef, + // thisObject + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + if (arguments.len != 2 or !js.JSValueIsNumber(ctx, arguments[0])) { + JSError(getAllocator(ctx), "Internal error registering macros: invalid args", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + // TODO: make this faster + const id = @truncate(i32, @floatToInt(i64, js.JSValueToNumber(ctx, arguments[0], exception))); + if (id == -1 or id == 0) { + JSError(getAllocator(ctx), "Internal error registering macros: invalid id", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + + if (!js.JSValueIsObject(ctx, arguments[1]) or !js.JSObjectIsFunction(ctx, arguments[1])) { + JSError(getAllocator(ctx), "Macro must be a function. Received: {s}", .{@tagName(js.JSValueGetType(ctx, arguments[1]))}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + + var get_or_put_result = VirtualMachine.vm.macros.getOrPut(id) catch unreachable; + if (get_or_put_result.found_existing) { + js.JSValueUnprotect(ctx, get_or_put_result.value_ptr.*); + } + + js.JSValueProtect(ctx, arguments[1]); + get_or_put_result.value_ptr.* = arguments[1]; + + return js.JSValueMakeUndefined(ctx); +} + +pub fn getCWD( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return ZigString.init(VirtualMachine.vm.bundler.fs.top_level_dir).toValue(ctx.ptr()).asRef(); +} + +pub fn getOrigin( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return ZigString.init(VirtualMachine.vm.origin.origin).toValue(ctx.ptr()).asRef(); +} + +pub fn getStdin( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("BunSTDIN")); + if (existing.isEmpty()) { + var rare_data = JSC.VirtualMachine.vm.rareData(); + var store = rare_data.stdin(); + var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; + blob.* = JSC.WebCore.Blob.initWithStore(store, ctx.ptr()); + + return ctx.ptr().putCachedObject( + &ZigString.init("BunSTDIN"), + JSC.JSValue.fromRef(JSC.WebCore.Blob.Class.make(ctx, blob)), + ).asObjectRef(); + } + + return existing.asObjectRef(); +} + +pub fn getStderr( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("BunSTDERR")); + if (existing.isEmpty()) { + var rare_data = JSC.VirtualMachine.vm.rareData(); + var store = rare_data.stderr(); + var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; + blob.* = JSC.WebCore.Blob.initWithStore(store, ctx.ptr()); + + return ctx.ptr().putCachedObject( + &ZigString.init("BunSTDERR"), + JSC.JSValue.fromRef(JSC.WebCore.Blob.Class.make(ctx, blob)), + ).asObjectRef(); + } + + return existing.asObjectRef(); +} + +pub fn getStdout( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("BunSTDOUT")); + if (existing.isEmpty()) { + var rare_data = JSC.VirtualMachine.vm.rareData(); + var store = rare_data.stdout(); + var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; + blob.* = JSC.WebCore.Blob.initWithStore(store, ctx.ptr()); + + return ctx.ptr().putCachedObject( + &ZigString.init("BunSTDOUT"), + JSC.JSValue.fromRef(JSC.WebCore.Blob.Class.make(ctx, blob)), + ).asObjectRef(); + } + + return existing.asObjectRef(); +} + +pub fn enableANSIColors( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return js.JSValueMakeBoolean(ctx, Output.enable_ansi_colors); +} +pub fn getMain( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return ZigString.init(VirtualMachine.vm.main).toValue(ctx.ptr()).asRef(); +} + +pub fn getAssetPrefix( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return ZigString.init(VirtualMachine.vm.bundler.options.routes.asset_prefix_path).toValue(ctx.ptr()).asRef(); +} + +pub fn getArgv( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + if (comptime Environment.isWindows) { + @compileError("argv not supported on windows"); + } + + var argv_list = std.heap.stackFallback(128, getAllocator(ctx)); + var allocator = argv_list.get(); + var argv = allocator.alloc(ZigString, std.os.argv.len) catch unreachable; + defer if (argv.len > 128) allocator.free(argv); + for (std.os.argv) |arg, i| { + argv[i] = ZigString.init(std.mem.span(arg)); + } + + return JSValue.createStringArray(ctx.ptr(), argv.ptr, argv.len, true).asObjectRef(); +} + +pub fn getRoutesDir( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + if (!VirtualMachine.vm.bundler.options.routes.routes_enabled or VirtualMachine.vm.bundler.options.routes.dir.len == 0) { + return js.JSValueMakeUndefined(ctx); + } + + return ZigString.init(VirtualMachine.vm.bundler.options.routes.dir).toValue(ctx.ptr()).asRef(); +} + +pub fn getFilePath(ctx: js.JSContextRef, arguments: []const js.JSValueRef, buf: []u8, exception: js.ExceptionRef) ?string { + if (arguments.len != 1) { + JSError(getAllocator(ctx), "Expected a file path as a string or an array of strings to be part of a file path.", .{}, ctx, exception); + return null; + } + + const value = arguments[0]; + if (js.JSValueIsString(ctx, value)) { + var out = ZigString.Empty; + JSValue.toZigString(JSValue.fromRef(value), &out, ctx.ptr()); + var out_slice = out.slice(); + + // The dots are kind of unnecessary. They'll be normalized. + if (out.len == 0 or @ptrToInt(out.ptr) == 0 or std.mem.eql(u8, out_slice, ".") or std.mem.eql(u8, out_slice, "..") or std.mem.eql(u8, out_slice, "../")) { + JSError(getAllocator(ctx), "Expected a file path as a string or an array of strings to be part of a file path.", .{}, ctx, exception); + return null; + } + + var parts = [_]string{out_slice}; + // This does the equivalent of Node's path.normalize(path.join(cwd, out_slice)) + var res = VirtualMachine.vm.bundler.fs.absBuf(&parts, buf); + + return res; + } else if (js.JSValueIsArray(ctx, value)) { + var temp_strings_list: [32]string = undefined; + var temp_strings_list_len: u8 = 0; + defer { + for (temp_strings_list[0..temp_strings_list_len]) |_, i| { + temp_strings_list[i] = ""; + } + } + + var iter = JSValue.fromRef(value).arrayIterator(ctx.ptr()); + while (iter.next()) |item| { + if (temp_strings_list_len >= temp_strings_list.len) { + break; + } + + if (!item.isString()) { + JSError(getAllocator(ctx), "Expected a file path as a string or an array of strings to be part of a file path.", .{}, ctx, exception); + return null; + } + + var out = ZigString.Empty; + JSValue.toZigString(item, &out, ctx.ptr()); + const out_slice = out.slice(); + + temp_strings_list[temp_strings_list_len] = out_slice; + // The dots are kind of unnecessary. They'll be normalized. + if (out.len == 0 or @ptrToInt(out.ptr) == 0 or std.mem.eql(u8, out_slice, ".") or std.mem.eql(u8, out_slice, "..") or std.mem.eql(u8, out_slice, "../")) { + JSError(getAllocator(ctx), "Expected a file path as a string or an array of strings to be part of a file path.", .{}, ctx, exception); + return null; + } + temp_strings_list_len += 1; + } + + if (temp_strings_list_len == 0) { + JSError(getAllocator(ctx), "Expected a file path as a string or an array of strings to be part of a file path.", .{}, ctx, exception); + return null; + } + + return VirtualMachine.vm.bundler.fs.absBuf(temp_strings_list[0..temp_strings_list_len], buf); + } else { + JSError(getAllocator(ctx), "Expected a file path as a string or an array of strings to be part of a file path.", .{}, ctx, exception); + return null; + } +} + +pub fn getImportedStyles( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + defer flushCSSImports(); + const styles = getCSSImports(); + if (styles.len == 0) { + return js.JSObjectMakeArray(ctx, 0, null, null); + } + + return JSValue.createStringArray(ctx.ptr(), styles.ptr, styles.len, true).asRef(); +} + +pub fn newPath( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + args: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + const is_windows = args.len == 1 and JSValue.fromRef(args[0]).toBoolean(); + return Node.Path.create(ctx.ptr(), is_windows).asObjectRef(); +} + +pub fn readFileAsStringCallback( + ctx: js.JSContextRef, + buf_z: [:0]const u8, + exception: js.ExceptionRef, +) js.JSValueRef { + const path = buf_z.ptr[0..buf_z.len]; + var file = std.fs.cwd().openFileZ(buf_z, .{ .mode = .read_only }) catch |err| { + JSError(getAllocator(ctx), "Opening file {s} for path: \"{s}\"", .{ @errorName(err), path }, ctx, exception); + return js.JSValueMakeUndefined(ctx); + }; + + defer file.close(); + + const stat = file.stat() catch |err| { + JSError(getAllocator(ctx), "Getting file size {s} for \"{s}\"", .{ @errorName(err), path }, ctx, exception); + return js.JSValueMakeUndefined(ctx); + }; + + if (stat.kind != .File) { + JSError(getAllocator(ctx), "Can't read a {s} as a string (\"{s}\")", .{ @tagName(stat.kind), path }, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + + var contents_buf = VirtualMachine.vm.allocator.alloc(u8, stat.size + 2) catch unreachable; // OOM + defer VirtualMachine.vm.allocator.free(contents_buf); + const contents_len = file.readAll(contents_buf) catch |err| { + JSError(getAllocator(ctx), "{s} reading file (\"{s}\")", .{ @errorName(err), path }, ctx, exception); + return js.JSValueMakeUndefined(ctx); + }; + + contents_buf[contents_len] = 0; + + // Very slow to do it this way. We're copying the string twice. + // But it's important that this string is garbage collected instead of manually managed. + // We can't really recycle this one. + // TODO: use external string + return js.JSValueMakeString(ctx, js.JSStringCreateWithUTF8CString(contents_buf.ptr)); +} + +pub fn readFileAsBytesCallback( + ctx: js.JSContextRef, + buf_z: [:0]const u8, + exception: js.ExceptionRef, +) js.JSValueRef { + const path = buf_z.ptr[0..buf_z.len]; + + var file = std.fs.cwd().openFileZ(buf_z, .{ .mode = .read_only }) catch |err| { + JSError(getAllocator(ctx), "Opening file {s} for path: \"{s}\"", .{ @errorName(err), path }, ctx, exception); + return js.JSValueMakeUndefined(ctx); + }; + + defer file.close(); + + const stat = file.stat() catch |err| { + JSError(getAllocator(ctx), "Getting file size {s} for \"{s}\"", .{ @errorName(err), path }, ctx, exception); + return js.JSValueMakeUndefined(ctx); + }; + + if (stat.kind != .File) { + JSError(getAllocator(ctx), "Can't read a {s} as a string (\"{s}\")", .{ @tagName(stat.kind), path }, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + + var contents_buf = VirtualMachine.vm.allocator.alloc(u8, stat.size + 2) catch unreachable; // OOM + errdefer VirtualMachine.vm.allocator.free(contents_buf); + const contents_len = file.readAll(contents_buf) catch |err| { + JSError(getAllocator(ctx), "{s} reading file (\"{s}\")", .{ @errorName(err), path }, ctx, exception); + return js.JSValueMakeUndefined(ctx); + }; + + contents_buf[contents_len] = 0; + + var marked_array_buffer = VirtualMachine.vm.allocator.create(MarkedArrayBuffer) catch unreachable; + marked_array_buffer.* = MarkedArrayBuffer.fromBytes( + contents_buf[0..contents_len], + VirtualMachine.vm.allocator, + .Uint8Array, + ); + + return marked_array_buffer.toJSObjectRef(ctx, exception); +} + +pub fn getRouteFiles( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + if (VirtualMachine.vm.bundler.router == null) return js.JSObjectMakeArray(ctx, 0, null, null); + + const router = &VirtualMachine.vm.bundler.router.?; + const list = router.getPublicPaths() catch unreachable; + + for (routes_list_strings[0..@minimum(list.len, routes_list_strings.len)]) |_, i| { + routes_list_strings[i] = ZigString.init(list[i]); + } + + const ref = JSValue.createStringArray(ctx.ptr(), &routes_list_strings, list.len, true).asRef(); + return ref; +} + +pub fn getRouteNames( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + if (VirtualMachine.vm.bundler.router == null) return js.JSObjectMakeArray(ctx, 0, null, null); + + const router = &VirtualMachine.vm.bundler.router.?; + const list = router.getNames() catch unreachable; + + for (routes_list_strings[0..@minimum(list.len, routes_list_strings.len)]) |_, i| { + routes_list_strings[i] = ZigString.init(list[i]); + } + + const ref = JSValue.createStringArray(ctx.ptr(), &routes_list_strings, list.len, true).asRef(); + return ref; +} + +const Editor = @import("../../open.zig").Editor; +pub fn openInEditor( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + args: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + var edit = &VirtualMachine.vm.rareData().editor_context; + + var arguments = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), args); + defer arguments.deinit(); + var path: string = ""; + var editor_choice: ?Editor = null; + var line: ?string = null; + var column: ?string = null; + + if (arguments.nextEat()) |file_path_| { + path = file_path_.toSlice(ctx.ptr(), bun.default_allocator).slice(); + } + + if (arguments.nextEat()) |opts| { + if (!opts.isUndefinedOrNull()) { + if (opts.getTruthy(ctx.ptr(), "editor")) |editor_val| { + var sliced = editor_val.toSlice(ctx.ptr(), bun.default_allocator); + var prev_name = edit.name; + + if (!strings.eqlLong(prev_name, sliced.slice(), true)) { + var prev = edit.*; + edit.name = sliced.slice(); + edit.detectEditor(VirtualMachine.vm.bundler.env); + editor_choice = edit.editor; + if (editor_choice == null) { + edit.* = prev; + JSError(getAllocator(ctx), "Could not find editor \"{s}\"", .{sliced.slice()}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } else if (edit.name.ptr == edit.path.ptr) { + edit.name = bun.default_allocator.dupe(u8, edit.path) catch unreachable; + edit.path = edit.path; + } + } + } + + if (opts.getTruthy(ctx.ptr(), "line")) |line_| { + line = line_.toSlice(ctx.ptr(), bun.default_allocator).slice(); + } + + if (opts.getTruthy(ctx.ptr(), "column")) |column_| { + column = column_.toSlice(ctx.ptr(), bun.default_allocator).slice(); + } + } + } + + const editor = editor_choice orelse edit.editor orelse brk: { + edit.autoDetectEditor(VirtualMachine.vm.bundler.env); + if (edit.editor == null) { + JSC.JSError(bun.default_allocator, "Failed to auto-detect editor", .{}, ctx, exception); + return null; + } + + break :brk edit.editor.?; + }; + + if (path.len == 0) { + JSError(getAllocator(ctx), "No file path specified", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + + editor.open(edit.path, path, line, column, bun.default_allocator) catch |err| { + JSC.JSError(bun.default_allocator, "Opening editor failed {s}", .{@errorName(err)}, ctx, exception); + return null; + }; + + return JSC.JSValue.jsUndefined().asObjectRef(); +} + +pub fn readFileAsBytes( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path = getFilePath(ctx, arguments, &buf, exception) orelse return null; + buf[path.len] = 0; + + const buf_z: [:0]const u8 = buf[0..path.len :0]; + const result = readFileAsBytesCallback(ctx, buf_z, exception); + return result; +} + +pub fn readFileAsString( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path = getFilePath(ctx, arguments, &buf, exception) orelse return null; + buf[path.len] = 0; + + const buf_z: [:0]const u8 = buf[0..path.len :0]; + const result = readFileAsStringCallback(ctx, buf_z, exception); + return result; +} + +pub fn getPublicPath(to: string, origin: URL, comptime Writer: type, writer: Writer) void { + const relative_path = VirtualMachine.vm.bundler.fs.relativeTo(to); + if (origin.isAbsolute()) { + if (strings.hasPrefix(relative_path, "..") or strings.hasPrefix(relative_path, "./")) { + writer.writeAll(origin.origin) catch return; + writer.writeAll("/abs:") catch return; + if (std.fs.path.isAbsolute(to)) { + writer.writeAll(to) catch return; + } else { + writer.writeAll(VirtualMachine.vm.bundler.fs.abs(&[_]string{to})) catch return; + } + } else { + origin.joinWrite( + Writer, + writer, + VirtualMachine.vm.bundler.options.routes.asset_prefix_path, + "", + relative_path, + "", + ) catch return; + } + } else { + writer.writeAll(std.mem.trimLeft(u8, relative_path, "/")) catch unreachable; + } +} + +pub fn sleepSync( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + if (js.JSValueIsNumber(ctx, arguments[0])) { + const seconds = JSValue.fromRef(arguments[0]).asNumber(); + if (seconds > 0 and std.math.isFinite(seconds)) std.time.sleep(@floatToInt(u64, seconds * 1000) * std.time.ns_per_ms); + } + + return js.JSValueMakeUndefined(ctx); +} + +pub fn createNodeFS( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + return Node.NodeFSBindings.make( + ctx, + VirtualMachine.vm.nodeFS(), + ); +} + +pub fn generateHeapSnapshot( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + return ctx.ptr().generateHeapSnapshot().asObjectRef(); +} + +pub fn runGC( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + // it should only force cleanup on thread exit + + Global.mimalloc_cleanup(false); + + return ctx.ptr().vm().runGC(arguments.len > 0 and JSValue.fromRef(arguments[0]).toBoolean()).asRef(); +} + +pub fn shrink( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + ctx.ptr().vm().shrinkFootprint(); + return JSValue.jsUndefined().asRef(); +} + +fn doResolve( + ctx: js.JSContextRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) ?JSC.JSValue { + var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); + defer args.deinit(); + const specifier = args.protectEatNext() orelse { + JSC.throwInvalidArguments("Expected a specifier and a from path", .{}, ctx, exception); + return null; + }; + + if (specifier.isUndefinedOrNull()) { + JSC.throwInvalidArguments("specifier must be a string", .{}, ctx, exception); + return null; + } + + const from = args.protectEatNext() orelse { + JSC.throwInvalidArguments("Expected a from path", .{}, ctx, exception); + return null; + }; + + if (from.isUndefinedOrNull()) { + JSC.throwInvalidArguments("from must be a string", .{}, ctx, exception); + return null; + } + + return doResolveWithArgs(ctx, specifier.getZigString(ctx.ptr()), from.getZigString(ctx.ptr()), exception, false); +} + +fn doResolveWithArgs( + ctx: js.JSContextRef, + specifier: ZigString, + from: ZigString, + exception: js.ExceptionRef, + comptime is_file_path: bool, +) ?JSC.JSValue { + var errorable: ErrorableZigString = undefined; + + if (comptime is_file_path) { + VirtualMachine.resolveFilePathForAPI( + &errorable, + ctx.ptr(), + specifier, + from, + ); + } else { + VirtualMachine.resolveForAPI( + &errorable, + ctx.ptr(), + specifier, + from, + ); + } + + if (!errorable.success) { + exception.* = bun.cast(JSC.JSValueRef, errorable.result.err.ptr.?); + return null; + } + + return errorable.result.value.toValue(ctx.ptr()); +} + +pub fn resolveSync( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + const value = doResolve(ctx, arguments, exception) orelse return null; + return value.asObjectRef(); +} + +pub fn resolve( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + const value = doResolve(ctx, arguments, exception) orelse { + var exception_value = exception.*.?; + exception.* = null; + return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), JSC.JSValue.fromRef(exception_value)).asObjectRef(); + }; + return JSC.JSPromise.resolvedPromiseValue(ctx.ptr(), value).asObjectRef(); +} + +export fn Bun__resolve( + global: *JSGlobalObject, + specifier: JSValue, + source: JSValue, +) JSC.JSValue { + var exception_ = [1]JSC.JSValueRef{null}; + var exception = &exception_; + const value = doResolveWithArgs(global.ref(), specifier.getZigString(global), source.getZigString(global), exception, true) orelse { + return JSC.JSPromise.rejectedPromiseValue(global, JSC.JSValue.fromRef(exception[0])); + }; + return JSC.JSPromise.resolvedPromiseValue(global, value); +} + +export fn Bun__resolveSync( + global: *JSGlobalObject, + specifier: JSValue, + source: JSValue, +) JSC.JSValue { + var exception_ = [1]JSC.JSValueRef{null}; + var exception = &exception_; + return doResolveWithArgs(global.ref(), specifier.getZigString(global), source.getZigString(global), exception, true) orelse { + return JSC.JSValue.fromRef(exception[0]); + }; +} + +comptime { + if (!is_bindgen) { + _ = Bun__resolve; + _ = Bun__resolveSync; + } +} + +pub fn readAllStdinSync( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + var stack = std.heap.stackFallback(2048, getAllocator(ctx)); + var allocator = stack.get(); + + var stdin = std.io.getStdIn(); + var result = stdin.readToEndAlloc(allocator, std.math.maxInt(u32)) catch |err| { + JSError(undefined, "{s} reading stdin", .{@errorName(err)}, ctx, exception); + return null; + }; + var out = ZigString.init(result); + out.detectEncoding(); + return out.toValueGC(ctx.ptr()).asObjectRef(); +} + +var public_path_temp_str: [bun.MAX_PATH_BYTES]u8 = undefined; + +pub fn getPublicPathJS( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSValueRef { + var zig_str: ZigString = ZigString.Empty; + JSValue.toZigString(JSValue.fromRef(arguments[0]), &zig_str, ctx.ptr()); + + const to = zig_str.slice(); + + var stream = std.io.fixedBufferStream(&public_path_temp_str); + var writer = stream.writer(); + getPublicPath(to, VirtualMachine.vm.origin, @TypeOf(&writer), &writer); + + return ZigString.init(stream.buffer[0..stream.pos]).toValueGC(ctx.ptr()).asObjectRef(); +} + +pub const Class = NewClass( + void, + .{ + .name = "Bun", + .read_only = true, + }, + .{ + .match = .{ + .rfn = Router.match, + .ts = Router.match_type_definition, + }, + .sleepSync = .{ + .rfn = sleepSync, + }, + .fetch = .{ + .rfn = Fetch.call, + .ts = d.ts{}, + }, + .getImportedStyles = .{ + .rfn = Bun.getImportedStyles, + .ts = d.ts{ + .name = "getImportedStyles", + .@"return" = "string[]", + }, + }, + .inspect = .{ + .rfn = Bun.inspect, + .ts = d.ts{ + .name = "inspect", + .@"return" = "string", + }, + }, + .getRouteFiles = .{ + .rfn = Bun.getRouteFiles, + .ts = d.ts{ + .name = "getRouteFiles", + .@"return" = "string[]", + }, + }, + ._Path = .{ + .rfn = Bun.newPath, + .ts = d.ts{}, + }, + .getRouteNames = .{ + .rfn = Bun.getRouteNames, + .ts = d.ts{ + .name = "getRouteNames", + .@"return" = "string[]", + }, + }, + .readFile = .{ + .rfn = Bun.readFileAsString, + .ts = d.ts{ + .name = "readFile", + .@"return" = "string", + }, + }, + .resolveSync = .{ + .rfn = Bun.resolveSync, + .ts = d.ts{ + .name = "resolveSync", + .@"return" = "string", + }, + }, + .resolve = .{ + .rfn = Bun.resolve, + .ts = d.ts{ + .name = "resolve", + .@"return" = "string", + }, + }, + .readFileBytes = .{ + .rfn = Bun.readFileAsBytes, + .ts = d.ts{ + .name = "readFile", + .@"return" = "Uint8Array", + }, + }, + .getPublicPath = .{ + .rfn = Bun.getPublicPathJS, + .ts = d.ts{ + .name = "getPublicPath", + .@"return" = "string", + }, + }, + .registerMacro = .{ + .rfn = Bun.registerMacro, + .ts = d.ts{ + .name = "registerMacro", + .@"return" = "undefined", + }, + .enumerable = false, + }, + .fs = .{ + .rfn = Bun.createNodeFS, + .ts = d.ts{}, + .enumerable = false, + }, + .jest = .{ + .rfn = @import("../test/jest.zig").Jest.call, + .ts = d.ts{}, + .enumerable = false, + }, + .gc = .{ + .rfn = Bun.runGC, + .ts = d.ts{}, + }, + .allocUnsafe = .{ + .rfn = Bun.allocUnsafe, + .ts = .{}, + }, + .mmap = .{ + .rfn = Bun.mmapFile, + .ts = .{}, + }, + .generateHeapSnapshot = .{ + .rfn = Bun.generateHeapSnapshot, + .ts = d.ts{}, + }, + .shrink = .{ + .rfn = Bun.shrink, + .ts = d.ts{}, + }, + .openInEditor = .{ + .rfn = Bun.openInEditor, + .ts = d.ts{}, + }, + .readAllStdinSync = .{ + .rfn = Bun.readAllStdinSync, + .ts = d.ts{}, + }, + .serve = .{ + .rfn = Bun.serve, + .ts = d.ts{}, + }, + .file = .{ + .rfn = JSC.WebCore.Blob.constructFile, + .ts = d.ts{}, + }, + .write = .{ + .rfn = JSC.WebCore.Blob.writeFile, + .ts = d.ts{}, + }, + .sha = .{ + .rfn = JSC.wrapWithHasContainer(Crypto.SHA512_256, "hash", false, false, true), + }, + .nanoseconds = .{ + .rfn = nanoseconds, + }, + .gzipSync = .{ + .rfn = JSC.wrapWithHasContainer(JSZlib, "gzipSync", false, false, true), + }, + .deflateSync = .{ + .rfn = JSC.wrapWithHasContainer(JSZlib, "deflateSync", false, false, true), + }, + .gunzipSync = .{ + .rfn = JSC.wrapWithHasContainer(JSZlib, "gunzipSync", false, false, true), + }, + .inflateSync = .{ + .rfn = JSC.wrapWithHasContainer(JSZlib, "inflateSync", false, false, true), + }, + }, + .{ + .main = .{ + .get = getMain, + .ts = d.ts{ .name = "main", .@"return" = "string" }, + }, + .cwd = .{ + .get = getCWD, + .ts = d.ts{ .name = "cwd", .@"return" = "string" }, + }, + .origin = .{ + .get = getOrigin, + .ts = d.ts{ .name = "origin", .@"return" = "string" }, + }, + .stdin = .{ + .get = getStdin, + }, + .stdout = .{ + .get = getStdout, + }, + .stderr = .{ + .get = getStderr, + }, + .routesDir = .{ + .get = getRoutesDir, + .ts = d.ts{ .name = "routesDir", .@"return" = "string" }, + }, + .assetPrefix = .{ + .get = getAssetPrefix, + .ts = d.ts{ .name = "assetPrefix", .@"return" = "string" }, + }, + .argv = .{ + .get = getArgv, + .ts = d.ts{ .name = "argv", .@"return" = "string[]" }, + }, + .env = .{ + .get = EnvironmentVariables.getter, + }, + + .enableANSIColors = .{ + .get = enableANSIColors, + }, + .Transpiler = .{ + .get = getTranspilerConstructor, + .ts = d.ts{ .name = "Transpiler", .@"return" = "Transpiler.prototype" }, + }, + .hash = .{ + .get = getHashObject, + }, + .TOML = .{ + .get = getTOMLObject, + .ts = d.ts{ .name = "TOML", .@"return" = "TOML.prototype" }, + }, + .unsafe = .{ + .get = getUnsafe, + }, + + .SHA1 = .{ + .get = Crypto.SHA1.getter, + }, + .MD5 = .{ + .get = Crypto.MD5.getter, + }, + .MD4 = .{ + .get = Crypto.MD4.getter, + }, + .SHA224 = .{ + .get = Crypto.SHA224.getter, + }, + .SHA512 = .{ + .get = Crypto.SHA512.getter, + }, + .SHA384 = .{ + .get = Crypto.SHA384.getter, + }, + .SHA256 = .{ + .get = Crypto.SHA256.getter, + }, + .SHA512_256 = .{ + .get = Crypto.SHA512_256.getter, + }, + .FFI = .{ + .get = FFI.getter, + }, + }, +); + +pub const Crypto = struct { + const Hashers = @import("../../sha.zig"); + + fn CryptoHasher(comptime Hasher: type, comptime name: [:0]const u8, cached_constructor_name: []const u8) type { + return struct { + hashing: Hasher = Hasher{}, + + pub fn byteLength( + _: void, + _: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + return JSC.JSValue.jsNumber(@as(u16, Hasher.digest)).asObjectRef(); + } + + pub fn byteLength2( + _: *@This(), + _: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + return JSC.JSValue.jsNumber(@as(u16, Hasher.digest)).asObjectRef(); + } + + pub const Constructor = JSC.NewConstructor( + @This(), + .{ + .hash = .{ + .rfn = JSC.wrapWithHasContainer(@This(), "hash", false, false, true), + }, + .constructor = .{ .rfn = constructor }, + }, + .{ + .byteLength = .{ + .get = byteLength, + }, + }, + ); + + pub const Class = JSC.NewClass( + @This(), + .{ + .name = name, + }, + .{ + .update = .{ + .rfn = JSC.wrapSync(@This(), "update"), + }, + .digest = .{ + .rfn = JSC.wrapSync(@This(), "digest"), + }, + .finalize = finalize, + }, + .{ + .byteLength = .{ + .get = byteLength2, + }, + }, + ); + + fn hashToEncoding( + globalThis: *JSGlobalObject, + input: JSC.Node.StringOrBuffer, + encoding: JSC.Node.Encoding, + exception: JSC.C.ExceptionRef, + ) JSC.JSValue { + var output_digest_buf: Hasher.Digest = undefined; + + Hasher.hash(input.slice(), &output_digest_buf, JSC.VirtualMachine.vm.rareData().boringEngine()); + + return encoding.encodeWithSize(globalThis, Hasher.digest, &output_digest_buf, exception); + } + + fn hashToBytes( + globalThis: *JSGlobalObject, + input: JSC.Node.StringOrBuffer, + output: ?JSC.ArrayBuffer, + exception: JSC.C.ExceptionRef, + ) JSC.JSValue { + var output_digest_buf: Hasher.Digest = undefined; + var output_digest_slice: *Hasher.Digest = &output_digest_buf; + if (output) |output_buf| { + var bytes = output_buf.byteSlice(); + if (bytes.len < Hasher.digest) { + JSC.JSError( + bun.default_allocator, + comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), + .{}, + globalThis.ref(), + exception, + ); + return JSC.JSValue.zero; + } + output_digest_slice = bytes[0..Hasher.digest]; + } + + Hasher.hash(input.slice(), output_digest_slice, JSC.VirtualMachine.vm.rareData().boringEngine()); + + if (output) |output_buf| { + return output_buf.value; + } else { + var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, output_digest_slice) catch unreachable, .Uint8Array); + return array_buffer_out.toJSUnchecked(globalThis.ref(), exception); + } + } + + pub fn hash( + globalThis: *JSGlobalObject, + input: JSC.Node.StringOrBuffer, + output: ?JSC.Node.StringOrBuffer, + exception: JSC.C.ExceptionRef, + ) JSC.JSValue { + if (output) |string_or_buffer| { + switch (string_or_buffer) { + .string => |str| { + const encoding = JSC.Node.Encoding.from(str) orelse { + JSC.JSError( + bun.default_allocator, + "Unknown encoding", + .{}, + globalThis.ref(), + exception, + ); + return JSC.JSValue.zero; + }; + + return hashToEncoding(globalThis, input, encoding, exception); + }, + .buffer => |buffer| { + return hashToBytes(globalThis, input, buffer.buffer, exception); + }, + } + } else { + return hashToBytes(globalThis, input, null, exception); + } + } + + pub fn constructor( + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSObjectRef { + var this = bun.default_allocator.create(@This()) catch { + JSC.JSError(bun.default_allocator, "Failed to create new object", .{}, ctx, exception); + return null; + }; + + this.* = .{ .hashing = Hasher.init() }; + return @This().Class.make(ctx, this); + } + + pub fn getter( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init(cached_constructor_name)); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init(cached_constructor_name), + JSC.JSValue.fromRef(@This().Constructor.constructor(ctx)), + ).asObjectRef(); + } + + return existing.asObjectRef(); + } + + pub fn update(this: *@This(), thisObj: JSC.C.JSObjectRef, buffer: JSC.Node.StringOrBuffer) JSC.JSValue { + this.hashing.update(buffer.slice()); + return JSC.JSValue.c(thisObj); + } + + pub fn digest( + this: *@This(), + globalThis: *JSGlobalObject, + output: ?JSC.Node.StringOrBuffer, + exception: JSC.C.ExceptionRef, + ) JSC.JSValue { + if (output) |string_or_buffer| { + switch (string_or_buffer) { + .string => |str| { + const encoding = JSC.Node.Encoding.from(str) orelse { + JSC.JSError( + bun.default_allocator, + "Unknown encoding", + .{}, + globalThis.ref(), + exception, + ); + return JSC.JSValue.zero; + }; + + return this.digestToEncoding(globalThis, exception, encoding); + }, + .buffer => |buffer| { + return this.digestToBytes( + globalThis, + exception, + buffer.buffer, + ); + }, + } + } else { + return this.digestToBytes(globalThis, exception, null); + } + } + + fn digestToBytes(this: *@This(), globalThis: *JSGlobalObject, exception: JSC.C.ExceptionRef, output: ?JSC.ArrayBuffer) JSC.JSValue { + var output_digest_buf: Hasher.Digest = undefined; + var output_digest_slice: *Hasher.Digest = &output_digest_buf; + if (output) |output_buf| { + var bytes = output_buf.byteSlice(); + if (bytes.len < Hasher.digest) { + JSC.JSError( + bun.default_allocator, + comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{@as(usize, Hasher.digest)}), + .{}, + globalThis.ref(), + exception, + ); + return JSC.JSValue.zero; + } + output_digest_slice = bytes[0..Hasher.digest]; + } else { + output_digest_buf = comptime brk: { + var bytes: Hasher.Digest = undefined; + var i: usize = 0; + while (i < Hasher.digest) { + bytes[i] = 0; + i += 1; + } + break :brk bytes; + }; + } + + this.hashing.final(output_digest_slice); + + if (output) |output_buf| { + return output_buf.value; + } else { + var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, &output_digest_buf) catch unreachable, .Uint8Array); + return array_buffer_out.toJSUnchecked(globalThis.ref(), exception); + } + } + + fn digestToEncoding(this: *@This(), globalThis: *JSGlobalObject, exception: JSC.C.ExceptionRef, encoding: JSC.Node.Encoding) JSC.JSValue { + var output_digest_buf: Hasher.Digest = comptime brk: { + var bytes: Hasher.Digest = undefined; + var i: usize = 0; + while (i < Hasher.digest) { + bytes[i] = 0; + i += 1; + } + break :brk bytes; + }; + + var output_digest_slice: *Hasher.Digest = &output_digest_buf; + + this.hashing.final(output_digest_slice); + + return encoding.encodeWithSize(globalThis, Hasher.digest, output_digest_slice, exception); + } + + pub fn finalize(this: *@This()) void { + VirtualMachine.vm.allocator.destroy(this); + } + }; + } + + pub const SHA1 = CryptoHasher(Hashers.SHA1, "SHA1", "Bun_Crypto_SHA1"); + pub const MD5 = CryptoHasher(Hashers.MD5, "MD5", "Bun_Crypto_MD5"); + pub const MD4 = CryptoHasher(Hashers.MD4, "MD4", "Bun_Crypto_MD4"); + pub const SHA224 = CryptoHasher(Hashers.SHA224, "SHA224", "Bun_Crypto_SHA224"); + pub const SHA512 = CryptoHasher(Hashers.SHA512, "SHA512", "Bun_Crypto_SHA512"); + pub const SHA384 = CryptoHasher(Hashers.SHA384, "SHA384", "Bun_Crypto_SHA384"); + pub const SHA256 = CryptoHasher(Hashers.SHA256, "SHA256", "Bun_Crypto_SHA256"); + pub const SHA512_256 = CryptoHasher(Hashers.SHA512_256, "SHA512_256", "Bun_Crypto_SHA512_256"); + pub const MD5_SHA1 = CryptoHasher(Hashers.MD5_SHA1, "MD5_SHA1", "Bun_Crypto_MD5_SHA1"); +}; + +pub fn nanoseconds( + _: void, + _: JSC.C.JSContextRef, + _: JSC.C.JSObjectRef, + _: JSC.C.JSObjectRef, + _: []const JSC.C.JSValueRef, + _: JSC.C.ExceptionRef, +) JSC.C.JSValueRef { + const ns = JSC.VirtualMachine.vm.origin_timer.read(); + return JSC.JSValue.jsNumberFromUint64(ns).asObjectRef(); +} + +pub fn serve( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); + var config = JSC.API.ServerConfig.fromJS(ctx.ptr(), &args, exception); + if (exception.* != null) { + return null; + } + + // Listen happens on the next tick! + // This is so we can return a Server object + if (config.ssl_config != null) { + if (config.development) { + var server = JSC.API.DebugSSLServer.init(config, ctx.ptr()); + server.listen(); + if (!server.thisObject.isEmpty()) { + exception.* = server.thisObject.asObjectRef(); + server.thisObject = JSC.JSValue.zero; + server.deinit(); + return null; + } + var obj = JSC.API.DebugSSLServer.Class.make(ctx, server); + JSC.C.JSValueProtect(ctx, obj); + server.thisObject = JSValue.c(obj); + return obj; + } else { + var server = JSC.API.SSLServer.init(config, ctx.ptr()); + server.listen(); + if (!server.thisObject.isEmpty()) { + exception.* = server.thisObject.asObjectRef(); + server.thisObject = JSC.JSValue.zero; + server.deinit(); + return null; + } + var obj = JSC.API.SSLServer.Class.make(ctx, server); + JSC.C.JSValueProtect(ctx, obj); + server.thisObject = JSValue.c(obj); + return obj; + } + } else { + if (config.development) { + var server = JSC.API.DebugServer.init(config, ctx.ptr()); + server.listen(); + if (!server.thisObject.isEmpty()) { + exception.* = server.thisObject.asObjectRef(); + server.thisObject = JSC.JSValue.zero; + server.deinit(); + return null; + } + var obj = JSC.API.DebugServer.Class.make(ctx, server); + JSC.C.JSValueProtect(ctx, obj); + server.thisObject = JSValue.c(obj); + return obj; + } else { + var server = JSC.API.Server.init(config, ctx.ptr()); + server.listen(); + if (!server.thisObject.isEmpty()) { + exception.* = server.thisObject.asObjectRef(); + server.thisObject = JSC.JSValue.zero; + server.deinit(); + return null; + } + var obj = JSC.API.Server.Class.make(ctx, server); + JSC.C.JSValueProtect(ctx, obj); + server.thisObject = JSValue.c(obj); + return obj; + } + } + + unreachable; +} + +pub export fn Bun__escapeHTML( + globalObject: *JSGlobalObject, + callframe: *JSC.CallFrame, +) JSC.JSValue { + const arguments = callframe.arguments(); + if (arguments.len < 1) { + return ZigString.Empty.toValue(globalObject); + } + + const input_value = arguments[0]; + const zig_str = input_value.getZigString(globalObject); + if (zig_str.len == 0) + return ZigString.Empty.toValue(globalObject); + + if (zig_str.is16Bit()) { + const input_slice = zig_str.utf16SliceAligned(); + const escaped = strings.escapeHTMLForUTF16Input(globalObject.bunVM().allocator, input_slice) catch { + globalObject.vm().throwError(globalObject, ZigString.init("Out of memory").toValue(globalObject)); + return JSC.JSValue.jsUndefined(); + }; + + switch (escaped) { + .static => |val| { + return ZigString.init(val).toValue(globalObject); + }, + .original => return input_value, + .allocated => |escaped_html| { + if (comptime Environment.allow_assert) { + // assert that re-encoding the string produces the same result + std.debug.assert( + std.mem.eql( + u16, + (strings.toUTF16Alloc(bun.default_allocator, strings.toUTF8Alloc(bun.default_allocator, escaped_html) catch unreachable, false) catch unreachable).?, + escaped_html, + ), + ); + + // assert we do not allocate a new string unnecessarily + std.debug.assert( + !std.mem.eql( + u16, + input_slice, + escaped_html, + ), + ); + + // the output should always be longer than the input + std.debug.assert(escaped_html.len > input_slice.len); + } + + return ZigString.from16(escaped_html.ptr, escaped_html.len).toExternalValue(globalObject); + }, + } + } else { + const input_slice = zig_str.slice(); + const escaped = strings.escapeHTMLForLatin1Input(globalObject.bunVM().allocator, input_slice) catch { + globalObject.vm().throwError(globalObject, ZigString.init("Out of memory").toValue(globalObject)); + return JSC.JSValue.jsUndefined(); + }; + + switch (escaped) { + .static => |val| { + return ZigString.init(val).toValue(globalObject); + }, + .original => return input_value, + .allocated => |escaped_html| { + if (comptime Environment.allow_assert) { + // the output should always be longer than the input + std.debug.assert(escaped_html.len > input_slice.len); + + // assert we do not allocate a new string unnecessarily + std.debug.assert( + !std.mem.eql( + u8, + input_slice, + escaped_html, + ), + ); + } + + return ZigString.init(escaped_html).toExternalValue(globalObject); + }, + } + } +} + +comptime { + if (!JSC.is_bindgen) { + _ = Bun__escapeHTML; + } +} + +pub fn allocUnsafe( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); + + const length = @intCast( + usize, + @minimum( + @maximum(1, (args.nextEat() orelse JSC.JSValue.jsNumber(@as(i32, 1))).toInt32()), + std.math.maxInt(i32), + ), + ); + var bytes = bun.default_allocator.alloc(u8, length) catch { + JSC.JSError(bun.default_allocator, "OOM! Out of memory", .{}, ctx, exception); + return null; + }; + + return JSC.MarkedArrayBuffer.fromBytes( + bytes, + bun.default_allocator, + .Uint8Array, + ).toJSObjectRef(ctx, null); +} + +pub fn mmapFile( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSValueRef { + var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); + + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path = getFilePath(ctx, arguments[0..@minimum(1, arguments.len)], &buf, exception) orelse return null; + args.eat(); + + buf[path.len] = 0; + + const buf_z: [:0]const u8 = buf[0..path.len :0]; + + const sync_flags: u32 = if (@hasDecl(std.os.MAP, "SYNC")) std.os.MAP.SYNC | std.os.MAP.SHARED_VALIDATE else 0; + const file_flags: u32 = if (@hasDecl(std.os.MAP, "FILE")) std.os.MAP.FILE else 0; + + // Conforming applications must specify either MAP_PRIVATE or MAP_SHARED. + var offset: usize = 0; + var flags = file_flags; + var map_size: ?usize = null; + + if (args.nextEat()) |opts| { + const sync = opts.get(ctx.ptr(), "sync") orelse JSC.JSValue.jsBoolean(false); + const shared = opts.get(ctx.ptr(), "shared") orelse JSC.JSValue.jsBoolean(true); + flags |= @as(u32, if (sync.toBoolean()) sync_flags else 0); + flags |= @as(u32, if (shared.toBoolean()) std.os.MAP.SHARED else std.os.MAP.PRIVATE); + + if (opts.get(ctx.ptr(), "size")) |value| { + map_size = @intCast(usize, value.toInt64()); + } + + if (opts.get(ctx.ptr(), "offset")) |value| { + offset = @intCast(usize, value.toInt64()); + offset = std.mem.alignBackwardAnyAlign(offset, std.mem.page_size); + } + } else { + flags |= std.os.MAP.SHARED; + } + + const map = switch (JSC.Node.Syscall.mmapFile(buf_z, flags, map_size, offset)) { + .result => |map| map, + + .err => |err| { + exception.* = err.toJS(ctx); + return null; + }, + }; + + return JSC.C.JSObjectMakeTypedArrayWithBytesNoCopy(ctx, JSC.C.JSTypedArrayType.kJSTypedArrayTypeUint8Array, @ptrCast(?*anyopaque, map.ptr), map.len, struct { + pub fn x(ptr: ?*anyopaque, size: ?*anyopaque) callconv(.C) void { + _ = JSC.Node.Syscall.munmap(@ptrCast([*]align(std.mem.page_size) u8, @alignCast(std.mem.page_size, ptr))[0..@ptrToInt(size)]); + } + }.x, @intToPtr(?*anyopaque, map.len), exception); +} + +pub fn getTranspilerConstructor( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("BunTranspiler")); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init("BunTranspiler"), + JSC.JSValue.fromRef(Transpiler.Constructor.constructor(ctx)), + ).asObjectRef(); + } + + return existing.asObjectRef(); +} + +pub fn getHashObject( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("BunHash")); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init("BunHash"), + JSC.JSValue.fromRef(JSC.C.JSObjectMake(ctx, Hash.Class.get().*, null)), + ).asObjectRef(); + } + + return existing.asObjectRef(); +} + +pub const Hash = struct { + pub const Class = NewClass( + void, + .{ + .name = "Hash", + }, + .{ + .call = .{ + .rfn = call, + }, + .wyhash = .{ + .rfn = hashWrap(std.hash.Wyhash).hash, + }, + .adler32 = .{ + .rfn = hashWrap(std.hash.Adler32).hash, + }, + .crc32 = .{ + .rfn = hashWrap(std.hash.Crc32).hash, + }, + .cityHash32 = .{ + .rfn = hashWrap(std.hash.CityHash32).hash, + }, + .cityHash64 = .{ + .rfn = hashWrap(std.hash.CityHash64).hash, + }, + .murmur32v2 = .{ + .rfn = hashWrap(std.hash.murmur.Murmur2_32).hash, + }, + .murmur32v3 = .{ + .rfn = hashWrap(std.hash.murmur.Murmur3_32).hash, + }, + .murmur64v2 = .{ + .rfn = hashWrap(std.hash.murmur.Murmur2_64).hash, + }, + }, + .{}, + ); + + pub fn call( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSObjectRef { + return hashWrap(std.hash.Wyhash).hash(void{}, ctx, null, null, arguments, exception); + } + fn hashWrap(comptime Hasher: anytype) type { + return struct { + pub fn hash( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSValueRef { + var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); + var input: []const u8 = ""; + var input_slice = ZigString.Slice.empty; + defer input_slice.deinit(); + if (args.nextEat()) |arg| { + if (arg.as(JSC.WebCore.Blob)) |blob| { + // TODO: files + input = blob.sharedView(); + } else { + switch (arg.jsTypeLoose()) { + .ArrayBuffer, .Int8Array, .Uint8Array, .Uint8ClampedArray, .Int16Array, .Uint16Array, .Int32Array, .Uint32Array, .Float32Array, .Float64Array, .BigInt64Array, .BigUint64Array, .DataView => { + var array_buffer = arg.asArrayBuffer(ctx.ptr()) orelse { + JSC.throwInvalidArguments("ArrayBuffer conversion error", .{}, ctx, exception); + return null; + }; + input = array_buffer.slice(); + }, + else => { + input_slice = arg.toSlice(ctx.ptr(), bun.default_allocator); + input = input_slice.slice(); + }, + } + } + } + + // std.hash has inconsistent interfaces + // + const Function = if (@hasDecl(Hasher, "hashWithSeed")) Hasher.hashWithSeed else Hasher.hash; + var function_args: std.meta.ArgsTuple(@TypeOf(Function)) = undefined; + if (comptime std.meta.fields(std.meta.ArgsTuple(@TypeOf(Function))).len == 1) { + return JSC.JSValue.jsNumber(Function(input)).asObjectRef(); + } else { + var seed: u64 = 0; + if (args.nextEat()) |arg| { + if (arg.isNumber()) { + seed = arg.toU32(); + } + } + if (comptime std.meta.trait.isNumber(@TypeOf(function_args[0]))) { + function_args[0] = @intCast(@TypeOf(function_args[0]), seed); + function_args[1] = input; + } else { + function_args[1] = @intCast(@TypeOf(function_args[1]), seed); + function_args[0] = input; + } + + const value = @call(.{}, Function, function_args); + + if (@TypeOf(value) == u32) { + return JSC.JSValue.jsNumber(@bitCast(i32, value)).asObjectRef(); + } + return JSC.JSValue.jsNumber(value).asObjectRef(); + } + } + }; + } +}; + +pub fn getTOMLObject( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("TOML")); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init("TOML"), + JSValue.fromRef(js.JSObjectMake(ctx, TOML.Class.get().?[0], null)), + ).asObjectRef(); + } + + return existing.asObjectRef(); +} + +pub fn getUnsafe( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("Unsafe")); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init("Unsafe"), + JSValue.fromRef(js.JSObjectMake(ctx, Unsafe.Class.get().?[0], null)), + ).asObjectRef(); + } + + return existing.asObjectRef(); +} + +pub const Unsafe = struct { + pub const Class = NewClass( + void, + .{ .name = "Unsafe", .read_only = true }, + .{ + .segfault = .{ + .rfn = __debug__doSegfault, + }, + .arrayBufferToString = .{ + .rfn = arrayBufferToString, + }, + }, + .{}, + ); + + // For testing the segfault handler + pub fn __debug__doSegfault( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, + ) js.JSValueRef { + _ = ctx; + const Reporter = @import("../../report.zig"); + Reporter.globalError(error.SegfaultTest); + } + + pub fn arrayBufferToString( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + args: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSValueRef { + const array_buffer = JSC.ArrayBuffer.fromTypedArray(ctx, JSC.JSValue.fromRef(args[0]), exception); + switch (array_buffer.typed_array_type) { + .Uint16Array, .Int16Array => { + var zig_str = ZigString.init(""); + zig_str.ptr = @ptrCast([*]const u8, @alignCast(@alignOf([*]align(1) const u16), array_buffer.ptr)); + zig_str.len = array_buffer.len; + zig_str.markUTF16(); + // the deinitializer for string causes segfaults + // if we don't clone it + return ZigString.toValueGC(&zig_str, ctx.ptr()).asObjectRef(); + }, + else => { + // the deinitializer for string causes segfaults + // if we don't clone it + return ZigString.init(array_buffer.slice()).toValueGC(ctx.ptr()).asObjectRef(); + }, + } + } +}; + +// pub const Lockfile = struct { +// const BunLockfile = @import("../../install/install.zig").Lockfile; +// lockfile: *BunLockfile, + +// pub const RefCountedLockfile = bun.RefCount(Lockfile, true); + +// pub const StaticClass = NewClass( +// void, +// .{ +// .name = "Lockfile", +// .read_only = true, +// }, +// .{ +// .load = .{ +// .rfn = BunLockfile.load, +// }, +// }, +// .{}, +// ); + +// pub const Class = NewClass( +// RefCountedLockfile, +// .{ +// .name = "Lockfile", +// .read_only = true, +// }, +// .{ +// .findPackagesByName = .{ +// .rfn = BunLockfile.load, +// }, +// .dependencies = .{ +// .rfn = BunLockfile.load, +// }, +// }, +// .{}, +// ); + +// pub fn deinit(this: *Lockfile) void { +// this.lockfile.deinit(); +// } + +// pub fn load( +// // this +// _: void, +// ctx: js.JSContextRef, +// // function +// _: js.JSObjectRef, +// // thisObject +// _: js.JSObjectRef, +// arguments: []const js.JSValueRef, +// exception: js.ExceptionRef, +// ) js.JSValueRef { +// if (arguments.len == 0) { +// JSError(undefined, "Expected file path string or buffer", .{}, ctx, exception); +// return null; +// } + +// var lockfile: *BunLockfile = getAllocator(ctx).create(BunLockfile) catch return JSValue.jsUndefined().asRef(); + +// var log = logger.Log.init(default_allocator); +// var args_slice = @ptrCast([*]const JSValue, arguments.ptr)[0..arguments.len]; + +// var arguments_slice = Node.ArgumentsSlice.init(args_slice); +// var path_or_buffer = Node.PathLike.fromJS(ctx, &arguments_slice, exception) orelse { +// getAllocator(ctx).destroy(lockfile); +// JSError(undefined, "Expected file path string or buffer", .{}, ctx, exception); +// return null; +// }; + +// const load_from_disk_result = switch (path_or_buffer) { +// Node.PathLike.Tag.string => lockfile.loadFromDisk(getAllocator(ctx), &log, path_or_buffer.string), +// Node.PathLike.Tag.buffer => lockfile.loadFromBytes(getAllocator(ctx), path_or_buffer.buffer.slice(), &log), +// else => { +// getAllocator(ctx).destroy(lockfile); +// JSError(undefined, "Expected file path string or buffer", .{}, ctx, exception); +// return null; +// }, +// }; + +// switch (load_from_disk_result) { +// .err => |cause| { +// defer getAllocator(ctx).destroy(lockfile); +// switch (cause.step) { +// .open_file => { +// JSError(undefined, "error opening lockfile: {s}", .{ +// @errorName(cause.value), +// }, ctx, exception); +// return null; +// }, +// .parse_file => { +// JSError(undefined, "error parsing lockfile: {s}", .{ +// @errorName(cause.value), +// }, ctx, exception); +// return null; +// }, +// .read_file => { +// JSError(undefined, "error reading lockfile: {s}", .{ +// @errorName(cause.value), +// }, ctx, exception); +// return null; +// }, +// } +// }, +// .ok => {}, +// } +// } +// }; + +pub const TOML = struct { + const TOMLParser = @import("../../toml/toml_parser.zig").TOML; + pub const Class = NewClass( + void, + .{ + .name = "TOML", + .read_only = true, + }, + .{ + .parse = .{ + .rfn = TOML.parse, + }, + }, + .{}, + ); + + pub fn parse( + // this + _: void, + ctx: js.JSContextRef, + // function + _: js.JSObjectRef, + // thisObject + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSValueRef { + var arena = std.heap.ArenaAllocator.init(getAllocator(ctx)); + var allocator = arena.allocator(); + defer arena.deinit(); + var log = logger.Log.init(default_allocator); + var input_str = ZigString.init(""); + JSValue.fromRef(arguments[0]).toZigString(&input_str, ctx.ptr()); + var needs_deinit = false; + var input = input_str.slice(); + if (input_str.is16Bit()) { + input = std.fmt.allocPrint(allocator, "{}", .{input_str}) catch unreachable; + needs_deinit = true; + } + var source = logger.Source.initPathString("input.toml", input); + var parse_result = TOMLParser.parse(&source, &log, allocator) catch { + exception.* = log.toJS(ctx.ptr(), default_allocator, "Failed to parse toml").asObjectRef(); + return null; + }; + + // for now... + var buffer_writer = try js_printer.BufferWriter.init(allocator); + var writer = js_printer.BufferPrinter.init(buffer_writer); + _ = js_printer.printJSON(*js_printer.BufferPrinter, &writer, parse_result, &source) catch { + exception.* = log.toJS(ctx.ptr(), default_allocator, "Failed to print toml").asObjectRef(); + return null; + }; + + var slice = writer.ctx.buffer.toOwnedSliceLeaky(); + var out = ZigString.init(slice); + + const out_value = js.JSValueMakeFromJSONString(ctx, out.toJSStringRef()); + return out_value; + } +}; + +pub const Timer = struct { + last_id: i32 = 0, + warned: bool = false, + active: u32 = 0, + timeouts: TimeoutMap = TimeoutMap{}, + + const TimeoutMap = std.AutoArrayHashMapUnmanaged(i32, *Timeout); + + pub fn getNextID() callconv(.C) i32 { + VirtualMachine.vm.timer.last_id += 1; + return VirtualMachine.vm.timer.last_id; + } + + pub const Timeout = struct { + id: i32 = 0, + callback: JSValue, + interval: i32 = 0, + completion: NetworkThread.Completion = undefined, + repeat: bool = false, + io_task: ?*TimeoutTask = null, + cancelled: bool = false, + + pub const TimeoutTask = IOTask(Timeout); + + pub fn run(this: *Timeout, _task: *TimeoutTask) void { + this.io_task = _task; + NetworkThread.global.pool.io.?.timeout( + *Timeout, + this, + onCallback, + &this.completion, + std.time.ns_per_ms * @intCast( + u63, + @maximum( + this.interval, + 1, + ), + ), + ); + } + + pub fn onCallback(this: *Timeout, _: *NetworkThread.Completion, _: NetworkThread.AsyncIO.TimeoutError!void) void { + this.io_task.?.onFinish(); + } + + pub fn then(this: *Timeout, global: *JSGlobalObject) void { + if (comptime JSC.is_bindgen) + unreachable; + + if (!this.cancelled) { + if (this.repeat) { + this.io_task.?.deinit(); + var task = Timeout.TimeoutTask.createOnJSThread(VirtualMachine.vm.allocator, global, this) catch unreachable; + this.io_task = task; + task.schedule(); + } + + _ = JSC.C.JSObjectCallAsFunction(global.ref(), this.callback.asObjectRef(), null, 0, null, null); + + if (this.repeat) + return; + } + + this.clear(global); + } + + pub fn clear(this: *Timeout, global: *JSGlobalObject) void { + if (comptime JSC.is_bindgen) + unreachable; + + this.cancelled = true; + JSC.C.JSValueUnprotect(global.ref(), this.callback.asObjectRef()); + _ = VirtualMachine.vm.timer.timeouts.swapRemove(this.id); + if (this.io_task) |task| { + task.deinit(); + } + VirtualMachine.vm.allocator.destroy(this); + VirtualMachine.vm.timer.active -|= 1; + VirtualMachine.vm.active_tasks -|= 1; + } + }; + + fn set( + id: i32, + globalThis: *JSGlobalObject, + callback: JSValue, + countdown: JSValue, + repeat: bool, + ) !void { + if (comptime is_bindgen) unreachable; + var timeout = try VirtualMachine.vm.allocator.create(Timeout); + js.JSValueProtect(globalThis.ref(), callback.asObjectRef()); + timeout.* = Timeout{ .id = id, .callback = callback, .interval = countdown.toInt32(), .repeat = repeat }; + var task = try Timeout.TimeoutTask.createOnJSThread(VirtualMachine.vm.allocator, globalThis, timeout); + VirtualMachine.vm.timer.timeouts.put(VirtualMachine.vm.allocator, id, timeout) catch unreachable; + VirtualMachine.vm.timer.active +|= 1; + VirtualMachine.vm.active_tasks +|= 1; + task.schedule(); + } + + pub fn setTimeout( + globalThis: *JSGlobalObject, + callback: JSValue, + countdown: JSValue, + ) callconv(.C) JSValue { + if (comptime is_bindgen) unreachable; + const id = VirtualMachine.vm.timer.last_id; + VirtualMachine.vm.timer.last_id +%= 1; + + Timer.set(id, globalThis, callback, countdown, false) catch + return JSValue.jsUndefined(); + + return JSValue.jsNumberWithType(i32, id); + } + pub fn setInterval( + globalThis: *JSGlobalObject, + callback: JSValue, + countdown: JSValue, + ) callconv(.C) JSValue { + if (comptime is_bindgen) unreachable; + const id = VirtualMachine.vm.timer.last_id; + VirtualMachine.vm.timer.last_id +%= 1; + + Timer.set(id, globalThis, callback, countdown, true) catch + return JSValue.jsUndefined(); + + return JSValue.jsNumberWithType(i32, id); + } + + pub fn clearTimer(id: JSValue, _: *JSGlobalObject) void { + if (comptime is_bindgen) unreachable; + var timer: *Timeout = VirtualMachine.vm.timer.timeouts.get(id.toInt32()) orelse return; + timer.cancelled = true; + } + + pub fn clearTimeout( + globalThis: *JSGlobalObject, + id: JSValue, + ) callconv(.C) JSValue { + if (comptime is_bindgen) unreachable; + Timer.clearTimer(id, globalThis); + return JSValue.jsUndefined(); + } + pub fn clearInterval( + globalThis: *JSGlobalObject, + id: JSValue, + ) callconv(.C) JSValue { + if (comptime is_bindgen) unreachable; + Timer.clearTimer(id, globalThis); + return JSValue.jsUndefined(); + } + + const Shimmer = @import("../bindings/shimmer.zig").Shimmer; + + pub const shim = Shimmer("Bun", "Timer", @This()); + pub const name = "Bun__Timer"; + pub const include = ""; + pub const namespace = shim.namespace; + + pub const Export = shim.exportFunctions(.{ + .@"setTimeout" = setTimeout, + .@"setInterval" = setInterval, + .@"clearTimeout" = clearTimeout, + .@"clearInterval" = clearInterval, + .@"getNextID" = getNextID, + }); + + comptime { + if (!JSC.is_bindgen) { + @export(setTimeout, .{ .name = Export[0].symbol_name }); + @export(setInterval, .{ .name = Export[1].symbol_name }); + @export(clearTimeout, .{ .name = Export[2].symbol_name }); + @export(clearInterval, .{ .name = Export[3].symbol_name }); + @export(getNextID, .{ .name = Export[4].symbol_name }); + } + } +}; + +pub const FFI = struct { + pub const Class = NewClass( + void, + .{ + .name = "FFI", + }, + .{ + .viewSource = .{ + .rfn = JSC.wrapWithHasContainer(JSC.FFI, "print", false, false, true), + }, + .dlopen = .{ + .rfn = JSC.wrapWithHasContainer(JSC.FFI, "open", false, false, true), + }, + .callback = .{ + .rfn = JSC.wrapWithHasContainer(JSC.FFI, "callback", false, false, false), + }, + .linkSymbols = .{ + .rfn = JSC.wrapWithHasContainer(JSC.FFI, "linkSymbols", false, false, false), + }, + .ptr = .{ + .rfn = JSC.wrapWithHasContainer(@This(), "ptr", false, false, true), + }, + .toBuffer = .{ + .rfn = JSC.wrapWithHasContainer(@This(), "toBuffer", false, false, true), + }, + .toArrayBuffer = .{ + .rfn = JSC.wrapWithHasContainer(@This(), "toArrayBuffer", false, false, true), + }, + }, + .{ + .CString = .{ + .get = UnsafeCString.getter, + }, + }, + ); + + pub fn ptr( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + ) JSValue { + if (value.isEmpty()) { + return JSC.JSValue.jsNull(); + } + + const array_buffer = value.asArrayBuffer(globalThis) orelse { + return JSC.toInvalidArguments("Expected ArrayBufferView", .{}, globalThis.ref()); + }; + + if (array_buffer.len == 0) { + return JSC.toInvalidArguments("ArrayBufferView must have a length > 0. A pointer to empty memory doesn't work", .{}, globalThis.ref()); + } + + var addr: usize = @ptrToInt(array_buffer.ptr); + + if (byteOffset) |off| { + if (!off.isEmptyOrUndefinedOrNull()) { + if (!off.isNumber()) { + return JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis.ref()); + } + } + + const bytei64 = off.toInt64(); + if (bytei64 < 0) { + addr -|= @intCast(usize, bytei64 * -1); + } else { + addr += @intCast(usize, bytei64); + } + + if (addr > @ptrToInt(array_buffer.ptr) + @as(usize, array_buffer.byte_len)) { + return JSC.toInvalidArguments("byteOffset out of bounds", .{}, globalThis.ref()); + } + } + + if (addr > max_addressible_memory) { + return JSC.toInvalidArguments("Pointer is outside max addressible memory, which usually means a bug in your program.", .{}, globalThis.ref()); + } + + if (addr == 0) { + return JSC.toInvalidArguments("Pointer must not be 0", .{}, globalThis.ref()); + } + + if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { + return JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis.ref()); + } + + return JSC.JSValue.fromPtrAddress(addr); + } + + const ValueOrError = union(enum) { + err: JSValue, + slice: []u8, + }; + + pub fn getPtrSlice(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, byteLength: ?JSValue) ValueOrError { + if (!value.isNumber()) { + return .{ .err = JSC.toInvalidArguments("ptr must be a number.", .{}, globalThis.ref()) }; + } + + const num = value.asNumber(); + if (num == 0) { + return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis.ref()) }; + } + + if (!std.math.isFinite(num)) { + return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis.ref()) }; + } + + var addr = @bitCast(usize, num); + + if (byteOffset) |byte_off| { + if (byte_off.isNumber()) { + const off = byte_off.toInt64(); + if (off < 0) { + addr -|= @intCast(usize, off * -1); + } else { + addr +|= @intCast(usize, off); + } + + if (addr == 0) { + return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis.ref()) }; + } + + if (!std.math.isFinite(byte_off.asNumber())) { + return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis.ref()) }; + } + } else if (!byte_off.isEmptyOrUndefinedOrNull()) { + // do nothing + } else { + return .{ .err = JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis.ref()) }; + } + } + + if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { + return .{ .err = JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis.ref()) }; + } + + if (byteLength) |valueLength| { + if (!valueLength.isEmptyOrUndefinedOrNull()) { + if (!valueLength.isNumber()) { + return .{ .err = JSC.toInvalidArguments("length must be a number.", .{}, globalThis.ref()) }; + } + + if (valueLength.asNumber() == 0.0) { + return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis.ref()) }; + } + + const length_i = valueLength.toInt64(); + if (length_i < 0) { + return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis.ref()) }; + } + + if (length_i > max_addressible_memory) { + return .{ .err = JSC.toInvalidArguments("length exceeds max addressable memory. This usually means a bug in your code.", .{}, globalThis.ref()) }; + } + + const length = @intCast(usize, length_i); + return .{ .slice = @intToPtr([*]u8, addr)[0..length] }; + } + } + + return .{ .slice = bun.span(@intToPtr([*:0]u8, addr)) }; + } + + pub fn toArrayBuffer( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + valueLength: ?JSValue, + ) JSC.JSValue { + switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { + .err => |erro| { + return erro; + }, + .slice => |slice| { + return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis.ref(), null, null, null); + }, + } + } + + pub fn toBuffer( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + valueLength: ?JSValue, + ) JSC.JSValue { + switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { + .err => |erro| { + return erro; + }, + .slice => |slice| { + return JSC.JSValue.createBuffer(globalThis, slice, null); + }, + } + } + + pub fn toCStringBuffer( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + valueLength: ?JSValue, + ) JSC.JSValue { + switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { + .err => |erro| { + return erro; + }, + .slice => |slice| { + return JSC.JSValue.createBuffer(globalThis, slice, null); + }, + } + } + + pub fn getter( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("FFI")); + if (existing.isEmpty()) { + var prototype = JSC.C.JSObjectMake(ctx, FFI.Class.get().?[0], null); + var base = JSC.C.JSObjectMake(ctx, null, null); + JSC.C.JSObjectSetPrototype(ctx, base, prototype); + return ctx.ptr().putCachedObject( + &ZigString.init("FFI"), + JSValue.fromRef(base), + ).asObjectRef(); + } + + return existing.asObjectRef(); + } +}; + +pub const UnsafeCString = struct { + pub fn constructor( + ctx: js.JSContextRef, + _: js.JSObjectRef, + len: usize, + args: [*c]const js.JSValueRef, + exception: js.ExceptionRef, + ) callconv(.C) js.JSObjectRef { + if (len == 0) { + JSC.throwInvalidArguments("Expected a ptr", .{}, ctx, exception); + return null; + } + + return newCString(ctx.ptr(), JSC.JSValue.fromRef(args[0]), if (len > 1) JSC.JSValue.fromRef(args[1]) else null, if (len > 2) JSC.JSValue.fromRef(args[2]) else null).asObjectRef(); + } + + pub fn newCString(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, lengthValue: ?JSValue) JSC.JSValue { + switch (FFI.getPtrSlice(globalThis, value, byteOffset, lengthValue)) { + .err => |err| { + return err; + }, + .slice => |slice| { + return WebCore.Encoder.toString(slice.ptr, slice.len, globalThis, .utf8); + }, + } + } + + pub fn getter( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("UnsafeCString")); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init("UnsafeCString"), + JSValue.fromRef(JSC.C.JSObjectMakeConstructor(ctx, null, constructor)), + ).asObjectRef(); + } + + return existing.asObjectRef(); + } +}; + +/// EnvironmentVariables is runtime defined. +/// Also, you can't iterate over process.env normally since it only exists at build-time otherwise +// This is aliased to Bun.env +pub const EnvironmentVariables = struct { + pub const Class = NewClass( + void, + .{ + .name = "DotEnv", + .read_only = true, + }, + .{ + .getProperty = .{ + .rfn = getProperty, + }, + .setProperty = .{ + .rfn = setProperty, + }, + .deleteProperty = .{ + .rfn = deleteProperty, + }, + .convertToType = .{ .rfn = convertToType }, + .hasProperty = .{ + .rfn = hasProperty, + }, + .getPropertyNames = .{ + .rfn = getPropertyNames, + }, + .toJSON = .{ + .rfn = toJSON, + .name = "toJSON", + }, + }, + .{}, + ); + + pub fn getter( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("Bun.env")); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init("Bun.env"), + JSValue.fromRef(js.JSObjectMake(ctx, EnvironmentVariables.Class.get().*, null)), + ).asObjectRef(); + } + + return existing.asObjectRef(); + } + + pub const BooleanString = struct { + pub const @"true": string = "true"; + pub const @"false": string = "false"; + }; + + pub fn getProperty( + ctx: js.JSContextRef, + _: js.JSObjectRef, + propertyName: js.JSStringRef, + _: js.ExceptionRef, + ) callconv(.C) js.JSValueRef { + const len = js.JSStringGetLength(propertyName); + var ptr = js.JSStringGetCharacters8Ptr(propertyName); + var name = ptr[0..len]; + if (VirtualMachine.vm.bundler.env.map.get(name)) |value| { + return ZigString.toRef(value, ctx.ptr()); + } + + if (Output.enable_ansi_colors) { + // https://github.com/chalk/supports-color/blob/main/index.js + if (strings.eqlComptime(name, "FORCE_COLOR")) { + return ZigString.toRef(BooleanString.@"true", ctx.ptr()); + } + } + + return js.JSValueMakeUndefined(ctx); + } + + pub fn toJSON( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, + ) js.JSValueRef { + var map = VirtualMachine.vm.bundler.env.map.map; + var keys = map.keys(); + var values = map.values(); + const StackFallback = std.heap.StackFallbackAllocator(32 * 2 * @sizeOf(ZigString)); + var stack = StackFallback{ + .buffer = undefined, + .fallback_allocator = bun.default_allocator, + .fixed_buffer_allocator = undefined, + }; + var allocator = stack.get(); + var key_strings_ = allocator.alloc(ZigString, keys.len * 2) catch unreachable; + var key_strings = key_strings_[0..keys.len]; + var value_strings = key_strings_[keys.len..]; + + for (keys) |key, i| { + key_strings[i] = ZigString.init(key); + key_strings[i].detectEncoding(); + value_strings[i] = ZigString.init(values[i]); + value_strings[i].detectEncoding(); + } + + var result = JSValue.fromEntries(ctx.ptr(), key_strings.ptr, value_strings.ptr, keys.len, false).asObjectRef(); + allocator.free(key_strings_); + return result; + // } + // ZigConsoleClient.Formatter.format(this: *Formatter, result: Tag.Result, comptime Writer: type, writer: Writer, value: JSValue, globalThis: *JSGlobalObject, comptime enable_ansi_colors: bool) + } + + pub fn deleteProperty( + _: js.JSContextRef, + _: js.JSObjectRef, + propertyName: js.JSStringRef, + _: js.ExceptionRef, + ) callconv(.C) bool { + const len = js.JSStringGetLength(propertyName); + var ptr = js.JSStringGetCharacters8Ptr(propertyName); + var name = ptr[0..len]; + _ = VirtualMachine.vm.bundler.env.map.map.swapRemove(name); + return true; + } + + pub fn setProperty( + ctx: js.JSContextRef, + _: js.JSObjectRef, + propertyName: js.JSStringRef, + value: js.JSValueRef, + exception: js.ExceptionRef, + ) callconv(.C) bool { + const len = js.JSStringGetLength(propertyName); + var ptr = js.JSStringGetCharacters8Ptr(propertyName); + var name = ptr[0..len]; + var val = ZigString.init(""); + JSValue.fromRef(value).toZigString(&val, ctx.ptr()); + if (exception.* != null) return false; + var result = std.fmt.allocPrint(VirtualMachine.vm.allocator, "{}", .{val}) catch unreachable; + VirtualMachine.vm.bundler.env.map.put(name, result) catch unreachable; + + return true; + } + + pub fn hasProperty( + _: js.JSContextRef, + _: js.JSObjectRef, + propertyName: js.JSStringRef, + ) callconv(.C) bool { + const len = js.JSStringGetLength(propertyName); + const ptr = js.JSStringGetCharacters8Ptr(propertyName); + const name = ptr[0..len]; + return VirtualMachine.vm.bundler.env.map.get(name) != null or (Output.enable_ansi_colors and strings.eqlComptime(name, "FORCE_COLOR")); + } + + pub fn convertToType(ctx: js.JSContextRef, obj: js.JSObjectRef, kind: js.JSType, exception: js.ExceptionRef) callconv(.C) js.JSValueRef { + _ = ctx; + _ = obj; + _ = kind; + _ = exception; + return obj; + } + + pub fn getPropertyNames( + _: js.JSContextRef, + _: js.JSObjectRef, + props: js.JSPropertyNameAccumulatorRef, + ) callconv(.C) void { + var iter = VirtualMachine.vm.bundler.env.map.iter(); + + while (iter.next()) |item| { + const str = item.key_ptr.*; + js.JSPropertyNameAccumulatorAddName(props, js.JSStringCreateStatic(str.ptr, str.len)); + } + } +}; + +export fn Bun__reportError(_: *JSGlobalObject, err: JSC.JSValue) void { + JSC.VirtualMachine.vm.defaultErrorHandler(err, null); +} + +comptime { + if (!is_bindgen) { + _ = Bun__reportError; + } +} + +pub const JSZlib = struct { + export fn reader_deallocator(_: ?*anyopaque, ctx: ?*anyopaque) void { + var reader: *zlib.ZlibReaderArrayList = bun.cast(*zlib.ZlibReaderArrayList, ctx.?); + reader.list.deinit(reader.allocator); + reader.deinit(); + } + + export fn compressor_deallocator(_: ?*anyopaque, ctx: ?*anyopaque) void { + var compressor: *zlib.ZlibCompressorArrayList = bun.cast(*zlib.ZlibCompressorArrayList, ctx.?); + compressor.list.deinit(compressor.allocator); + compressor.deinit(); + } + + pub fn gzipSync( + globalThis: *JSGlobalObject, + buffer: JSC.Node.StringOrBuffer, + options_val_: ?JSValue, + ) JSValue { + return gzipOrDeflateSync(globalThis, buffer, options_val_, true); + } + + pub fn deflateSync( + globalThis: *JSGlobalObject, + buffer: JSC.Node.StringOrBuffer, + options_val_: ?JSValue, + ) JSValue { + return gzipOrDeflateSync(globalThis, buffer, options_val_, false); + } + + pub fn gzipOrDeflateSync( + globalThis: *JSGlobalObject, + buffer: JSC.Node.StringOrBuffer, + options_val_: ?JSValue, + is_gzip: bool, + ) JSValue { + var opts = zlib.Options{ .gzip = is_gzip }; + if (options_val_) |options_val| { + if (options_val.isObject()) { + if (options_val.get(globalThis, "windowBits")) |window| { + opts.windowBits = window.toInt32(); + } + + if (options_val.get(globalThis, "level")) |level| { + opts.level = level.toInt32(); + } + + if (options_val.get(globalThis, "memLevel")) |memLevel| { + opts.memLevel = memLevel.toInt32(); + } + + if (options_val.get(globalThis, "strategy")) |strategy| { + opts.strategy = strategy.toInt32(); + } + } + } + + var compressed = buffer.slice(); + const allocator = JSC.VirtualMachine.vm.allocator; + var list = std.ArrayListUnmanaged(u8).initCapacity(allocator, if (compressed.len > 512) compressed.len else 32) catch unreachable; + var reader = zlib.ZlibCompressorArrayList.init(compressed, &list, allocator, opts) catch |err| { + if (err == error.InvalidArgument) { + return JSC.toInvalidArguments("Invalid buffer", .{}, globalThis.ref()); + } + + return JSC.toInvalidArguments("Unexpected", .{}, globalThis.ref()); + }; + + reader.readAll() catch { + defer reader.deinit(); + if (reader.errorMessage()) |msg| { + return ZigString.init(msg).toErrorInstance(globalThis); + } + return ZigString.init("Zlib returned an error").toErrorInstance(globalThis); + }; + reader.list = .{ .items = reader.list.toOwnedSlice(allocator) }; + reader.list.capacity = reader.list.items.len; + reader.list_ptr = &reader.list; + + var array_buffer = JSC.ArrayBuffer.fromBytes(reader.list.items, .Uint8Array); + return array_buffer.toJSWithContext(globalThis.ref(), reader, reader_deallocator, null); + } + + pub fn inflateSync( + globalThis: *JSGlobalObject, + buffer: JSC.Node.StringOrBuffer, + ) JSValue { + var compressed = buffer.slice(); + const allocator = JSC.VirtualMachine.vm.allocator; + var list = std.ArrayListUnmanaged(u8).initCapacity(allocator, if (compressed.len > 512) compressed.len else 32) catch unreachable; + var reader = zlib.ZlibReaderArrayList.initWithOptions(compressed, &list, allocator, .{ + .windowBits = -15, + }) catch |err| { + if (err == error.InvalidArgument) { + return JSC.toInvalidArguments("Invalid buffer", .{}, globalThis.ref()); + } + + return JSC.toInvalidArguments("Unexpected", .{}, globalThis.ref()); + }; + + reader.readAll() catch { + defer reader.deinit(); + if (reader.errorMessage()) |msg| { + return ZigString.init(msg).toErrorInstance(globalThis); + } + return ZigString.init("Zlib returned an error").toErrorInstance(globalThis); + }; + reader.list = .{ .items = reader.list.toOwnedSlice(allocator) }; + reader.list.capacity = reader.list.items.len; + reader.list_ptr = &reader.list; + + var array_buffer = JSC.ArrayBuffer.fromBytes(reader.list.items, .Uint8Array); + return array_buffer.toJSWithContext(globalThis.ref(), reader, reader_deallocator, null); + } + + pub fn gunzipSync( + globalThis: *JSGlobalObject, + buffer: JSC.Node.StringOrBuffer, + ) JSValue { + var compressed = buffer.slice(); + const allocator = JSC.VirtualMachine.vm.allocator; + var list = std.ArrayListUnmanaged(u8).initCapacity(allocator, if (compressed.len > 512) compressed.len else 32) catch unreachable; + var reader = zlib.ZlibReaderArrayList.init(compressed, &list, allocator) catch |err| { + if (err == error.InvalidArgument) { + return JSC.toInvalidArguments("Invalid buffer", .{}, globalThis.ref()); + } + + return JSC.toInvalidArguments("Unexpected", .{}, globalThis.ref()); + }; + + reader.readAll() catch { + defer reader.deinit(); + if (reader.errorMessage()) |msg| { + return ZigString.init(msg).toErrorInstance(globalThis); + } + return ZigString.init("Zlib returned an error").toErrorInstance(globalThis); + }; + reader.list = .{ .items = reader.list.toOwnedSlice(allocator) }; + reader.list.capacity = reader.list.items.len; + reader.list_ptr = &reader.list; + + var array_buffer = JSC.ArrayBuffer.fromBytes(reader.list.items, .Uint8Array); + return array_buffer.toJSWithContext(globalThis.ref(), reader, reader_deallocator, null); + } +}; diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig new file mode 100644 index 000000000..b4db32e4a --- /dev/null +++ b/src/bun.js/api/ffi.zig @@ -0,0 +1,1436 @@ +const Bun = @This(); +const default_allocator = @import("../../global.zig").default_allocator; +const bun = @import("../../global.zig"); +const Environment = bun.Environment; +const NetworkThread = @import("http").NetworkThread; +const Global = bun.Global; +const strings = bun.strings; +const string = bun.string; +const Output = @import("../../global.zig").Output; +const MutableString = @import("../../global.zig").MutableString; +const std = @import("std"); +const Allocator = std.mem.Allocator; +const IdentityContext = @import("../../identity_context.zig").IdentityContext; +const Fs = @import("../../fs.zig"); +const Resolver = @import("../../resolver/resolver.zig"); +const ast = @import("../../import_record.zig"); +const NodeModuleBundle = @import("../../node_module_bundle.zig").NodeModuleBundle; +const MacroEntryPoint = @import("../../bundler.zig").MacroEntryPoint; +const logger = @import("../../logger.zig"); +const Api = @import("../../api/schema.zig").Api; +const options = @import("../../options.zig"); +const Bundler = @import("../../bundler.zig").Bundler; +const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint; +const js_printer = @import("../../js_printer.zig"); +const js_parser = @import("../../js_parser.zig"); +const js_ast = @import("../../js_ast.zig"); +const hash_map = @import("../../hash_map.zig"); +const http = @import("../../http.zig"); +const NodeFallbackModules = @import("../../node_fallbacks.zig"); +const ImportKind = ast.ImportKind; +const Analytics = @import("../../analytics/analytics_thread.zig"); +const ZigString = @import("../../jsc.zig").ZigString; +const Runtime = @import("../../runtime.zig"); +const Router = @import("./router.zig"); +const ImportRecord = ast.ImportRecord; +const DotEnv = @import("../../env_loader.zig"); +const ParseResult = @import("../../bundler.zig").ParseResult; +const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON; +const MacroRemap = @import("../../resolver/package_json.zig").MacroMap; +const WebCore = @import("../../jsc.zig").WebCore; +const Request = WebCore.Request; +const Response = WebCore.Response; +const Headers = WebCore.Headers; +const Fetch = WebCore.Fetch; +const FetchEvent = WebCore.FetchEvent; +const js = @import("../../jsc.zig").C; +const JSC = @import("../../jsc.zig"); +const JSError = @import("../base.zig").JSError; +const d = @import("../base.zig").d; +const MarkedArrayBuffer = @import("../base.zig").MarkedArrayBuffer; +const getAllocator = @import("../base.zig").getAllocator; +const JSValue = @import("../../jsc.zig").JSValue; +const NewClass = @import("../base.zig").NewClass; +const Microtask = @import("../../jsc.zig").Microtask; +const JSGlobalObject = @import("../../jsc.zig").JSGlobalObject; +const ExceptionValueRef = @import("../../jsc.zig").ExceptionValueRef; +const JSPrivateDataPtr = @import("../../jsc.zig").JSPrivateDataPtr; +const ZigConsoleClient = @import("../../jsc.zig").ZigConsoleClient; +const Node = @import("../../jsc.zig").Node; +const ZigException = @import("../../jsc.zig").ZigException; +const ZigStackTrace = @import("../../jsc.zig").ZigStackTrace; +const ErrorableResolvedSource = @import("../../jsc.zig").ErrorableResolvedSource; +const ResolvedSource = @import("../../jsc.zig").ResolvedSource; +const JSPromise = @import("../../jsc.zig").JSPromise; +const JSInternalPromise = @import("../../jsc.zig").JSInternalPromise; +const JSModuleLoader = @import("../../jsc.zig").JSModuleLoader; +const JSPromiseRejectionOperation = @import("../../jsc.zig").JSPromiseRejectionOperation; +const Exception = @import("../../jsc.zig").Exception; +const ErrorableZigString = @import("../../jsc.zig").ErrorableZigString; +const ZigGlobalObject = @import("../../jsc.zig").ZigGlobalObject; +const VM = @import("../../jsc.zig").VM; +const JSFunction = @import("../../jsc.zig").JSFunction; +const Config = @import("../config.zig"); +const URL = @import("../../url.zig").URL; +const Transpiler = @import("./transpiler.zig"); +const VirtualMachine = @import("../javascript.zig").VirtualMachine; +const IOTask = JSC.IOTask; +const ComptimeStringMap = @import("../../comptime_string_map.zig").ComptimeStringMap; + +const TCC = @import("../../tcc.zig"); + +pub const FFI = struct { + dylib: ?std.DynLib = null, + functions: std.StringArrayHashMapUnmanaged(Function) = .{}, + closed: bool = false, + + pub const Class = JSC.NewClass( + FFI, + .{ .name = "class" }, + .{ .call = JSC.wrapWithHasContainer(FFI, "close", false, true, true) }, + .{}, + ); + + pub fn callback(globalThis: *JSGlobalObject, interface: JSC.JSValue, js_callback: JSC.JSValue) JSValue { + if (!interface.isObject()) { + return JSC.toInvalidArguments("Expected object", .{}, globalThis.ref()); + } + + if (js_callback.isEmptyOrUndefinedOrNull() or !js_callback.isCallable(globalThis.vm())) { + return JSC.toInvalidArguments("Expected callback function", .{}, globalThis.ref()); + } + + const allocator = VirtualMachine.vm.allocator; + var function: Function = .{}; + var func = &function; + + if (generateSymbolForFunction(globalThis, allocator, interface, func) catch ZigString.init("Out of memory").toErrorInstance(globalThis)) |val| { + return val; + } + + // TODO: WeakRefHandle that automatically frees it? + JSC.C.JSValueProtect(globalThis.ref(), js_callback.asObjectRef()); + func.base_name = ""; + + func.compileCallback(allocator, globalThis, js_callback.asObjectRef().?) catch return ZigString.init("Out of memory").toErrorInstance(globalThis); + switch (func.step) { + .failed => |err| { + JSC.C.JSValueUnprotect(globalThis.ref(), js_callback.asObjectRef()); + const message = ZigString.init(err.msg).toErrorInstance(globalThis); + + func.deinit(allocator); + + return message; + }, + .pending => { + JSC.C.JSValueUnprotect(globalThis.ref(), js_callback.asObjectRef()); + func.deinit(allocator); + return ZigString.init("Failed to compile, but not sure why. Please report this bug").toErrorInstance(globalThis); + }, + .compiled => { + var function_ = bun.default_allocator.create(Function) catch unreachable; + function_.* = func.*; + return JSC.JSValue.jsNumber(@bitCast(f64, @as(usize, @ptrToInt(function_.step.compiled.ptr)))); + }, + } + } + + pub fn close(this: *FFI) JSValue { + if (this.closed) { + return JSC.JSValue.jsUndefined(); + } + this.closed = true; + if (this.dylib) |*dylib| { + dylib.close(); + this.dylib = null; + } + + const allocator = VirtualMachine.vm.allocator; + + for (this.functions.values()) |*val| { + val.deinit(allocator); + } + this.functions.deinit(allocator); + + return JSC.JSValue.jsUndefined(); + } + + pub fn printCallback(global: *JSGlobalObject, object: JSC.JSValue) JSValue { + const allocator = VirtualMachine.vm.allocator; + + if (object.isEmptyOrUndefinedOrNull() or !object.isObject()) { + return JSC.toInvalidArguments("Expected an object", .{}, global.ref()); + } + + var function: Function = .{}; + if (generateSymbolForFunction(global, allocator, object, &function) catch ZigString.init("Out of memory").toErrorInstance(global)) |val| { + return val; + } + + var arraylist = std.ArrayList(u8).init(allocator); + defer arraylist.deinit(); + var writer = arraylist.writer(); + + function.base_name = "my_callback_function"; + + function.printCallbackSourceCode(&writer) catch { + return ZigString.init("Error while printing code").toErrorInstance(global); + }; + return ZigString.init(arraylist.items).toValueGC(global); + } + + pub fn print(global: *JSGlobalObject, object: JSC.JSValue, is_callback_val: ?JSC.JSValue) JSValue { + const allocator = VirtualMachine.vm.allocator; + if (is_callback_val) |is_callback| { + if (is_callback.toBoolean()) { + return printCallback(global, object); + } + } + + if (object.isEmptyOrUndefinedOrNull() or !object.isObject()) { + return JSC.toInvalidArguments("Expected an options object with symbol names", .{}, global.ref()); + } + + var symbols = std.StringArrayHashMapUnmanaged(Function){}; + if (generateSymbols(global, &symbols, object) catch JSC.JSValue.zero) |val| { + // an error while validating symbols + for (symbols.keys()) |key| { + allocator.free(bun.constStrToU8(key)); + } + symbols.clearAndFree(allocator); + return val; + } + + var zig_strings = allocator.alloc(ZigString, symbols.count()) catch unreachable; + for (symbols.values()) |*function, i| { + var arraylist = std.ArrayList(u8).init(allocator); + var writer = arraylist.writer(); + function.printSourceCode(&writer) catch { + // an error while generating source code + for (symbols.keys()) |key| { + allocator.free(bun.constStrToU8(key)); + } + for (zig_strings) |zig_string| { + allocator.free(bun.constStrToU8(zig_string.slice())); + } + for (symbols.values()) |*function_| { + function_.arg_types.deinit(allocator); + } + + symbols.clearAndFree(allocator); + return ZigString.init("Error while printing code").toErrorInstance(global); + }; + zig_strings[i] = ZigString.init(arraylist.items); + } + + const ret = JSC.JSValue.createStringArray(global, zig_strings.ptr, zig_strings.len, true); + + for (symbols.keys()) |key| { + allocator.free(bun.constStrToU8(key)); + } + for (zig_strings) |zig_string| { + allocator.free(bun.constStrToU8(zig_string.slice())); + } + for (symbols.values()) |*function_| { + function_.arg_types.deinit(allocator); + if (function_.step == .compiled) { + allocator.free(function_.step.compiled.buf); + } + } + symbols.clearAndFree(allocator); + + return ret; + } + + // pub fn dlcompile(global: *JSGlobalObject, object: JSC.JSValue) JSValue { + // const allocator = VirtualMachine.vm.allocator; + + // if (object.isEmptyOrUndefinedOrNull() or !object.isObject()) { + // return JSC.toInvalidArguments("Expected an options object with symbol names", .{}, global.ref()); + // } + + // var symbols = std.StringArrayHashMapUnmanaged(Function){}; + // if (generateSymbols(global, &symbols, object) catch JSC.JSValue.zero) |val| { + // // an error while validating symbols + // for (symbols.keys()) |key| { + // allocator.free(bun.constStrToU8(key)); + // } + // symbols.clearAndFree(allocator); + // return val; + // } + + // } + + pub fn open(global: *JSGlobalObject, name_str: ZigString, object: JSC.JSValue) JSC.JSValue { + const allocator = VirtualMachine.vm.allocator; + var name_slice = name_str.toSlice(allocator); + defer name_slice.deinit(); + + if (name_slice.len == 0) { + return JSC.toInvalidArguments("Invalid library name", .{}, global.ref()); + } + + if (object.isEmptyOrUndefinedOrNull() or !object.isObject()) { + return JSC.toInvalidArguments("Expected an options object with symbol names", .{}, global.ref()); + } + + const name = name_slice.sliceZ(); + var symbols = std.StringArrayHashMapUnmanaged(Function){}; + if (generateSymbols(global, &symbols, object) catch JSC.JSValue.zero) |val| { + // an error while validating symbols + for (symbols.keys()) |key| { + allocator.free(bun.constStrToU8(key)); + } + symbols.clearAndFree(allocator); + return val; + } + if (symbols.count() == 0) { + return JSC.toInvalidArguments("Expected at least one symbol", .{}, global.ref()); + } + + var dylib = std.DynLib.open(name) catch { + return JSC.toInvalidArguments("Failed to open library", .{}, global.ref()); + }; + + var obj = JSC.JSValue.c(JSC.C.JSObjectMake(global.ref(), null, null)); + JSC.C.JSValueProtect(global.ref(), obj.asObjectRef()); + defer JSC.C.JSValueUnprotect(global.ref(), obj.asObjectRef()); + for (symbols.values()) |*function| { + const function_name = function.base_name.?; + + // optional if the user passed "ptr" + if (function.symbol_from_dynamic_library == null) { + var resolved_symbol = dylib.lookup(*anyopaque, function_name) orelse { + const ret = JSC.toInvalidArguments("Symbol \"{s}\" not found in \"{s}\"", .{ std.mem.span(function_name), name_slice.slice() }, global.ref()); + for (symbols.values()) |*value| { + allocator.free(bun.constStrToU8(std.mem.span(value.base_name.?))); + value.arg_types.clearAndFree(allocator); + } + symbols.clearAndFree(allocator); + dylib.close(); + return ret; + }; + + function.symbol_from_dynamic_library = resolved_symbol; + } + + function.compile(allocator) catch |err| { + const ret = JSC.toInvalidArguments("{s} when compiling symbol \"{s}\" in \"{s}\"", .{ + std.mem.span(@errorName(err)), + std.mem.span(function_name), + name_slice.slice(), + }, global.ref()); + for (symbols.values()) |*value| { + allocator.free(bun.constStrToU8(std.mem.span(value.base_name.?))); + value.arg_types.clearAndFree(allocator); + } + symbols.clearAndFree(allocator); + dylib.close(); + return ret; + }; + switch (function.step) { + .failed => |err| { + for (symbols.values()) |*value| { + allocator.free(bun.constStrToU8(std.mem.span(value.base_name.?))); + value.arg_types.clearAndFree(allocator); + } + + const res = ZigString.init(err.msg).toErrorInstance(global); + function.deinit(allocator); + symbols.clearAndFree(allocator); + dylib.close(); + return res; + }, + .pending => { + for (symbols.values()) |*value| { + allocator.free(bun.constStrToU8(std.mem.span(value.base_name.?))); + value.arg_types.clearAndFree(allocator); + } + symbols.clearAndFree(allocator); + dylib.close(); + return ZigString.init("Failed to compile (nothing happend!)").toErrorInstance(global); + }, + .compiled => |compiled| { + var cb = JSC.NewFunctionPtr( + global, + &ZigString.init(std.mem.span(function_name)), + @intCast(u32, function.arg_types.items.len), + compiled.ptr, + ); + + obj.put(global, &ZigString.init(std.mem.span(function_name)), JSC.JSValue.cast(cb)); + }, + } + } + + var lib = allocator.create(FFI) catch unreachable; + lib.* = .{ + .dylib = dylib, + .functions = symbols, + }; + + var close_object = JSC.JSValue.c(Class.make(global.ref(), lib)); + + return JSC.JSValue.createObject2(global, &ZigString.init("close"), &ZigString.init("symbols"), close_object, obj); + } + + pub fn linkSymbols(global: *JSGlobalObject, object: JSC.JSValue) JSC.JSValue { + const allocator = VirtualMachine.vm.allocator; + + if (object.isEmptyOrUndefinedOrNull() or !object.isObject()) { + return JSC.toInvalidArguments("Expected an options object with symbol names", .{}, global.ref()); + } + + var symbols = std.StringArrayHashMapUnmanaged(Function){}; + if (generateSymbols(global, &symbols, object) catch JSC.JSValue.zero) |val| { + // an error while validating symbols + for (symbols.keys()) |key| { + allocator.free(bun.constStrToU8(key)); + } + symbols.clearAndFree(allocator); + return val; + } + if (symbols.count() == 0) { + return JSC.toInvalidArguments("Expected at least one symbol", .{}, global.ref()); + } + + var obj = JSC.JSValue.c(JSC.C.JSObjectMake(global.ref(), null, null)); + JSC.C.JSValueProtect(global.ref(), obj.asObjectRef()); + defer JSC.C.JSValueUnprotect(global.ref(), obj.asObjectRef()); + for (symbols.values()) |*function| { + const function_name = function.base_name.?; + + if (function.symbol_from_dynamic_library == null) { + const ret = JSC.toInvalidArguments("Symbol for \"{s}\" not found", .{std.mem.span(function_name)}, global.ref()); + for (symbols.values()) |*value| { + allocator.free(bun.constStrToU8(std.mem.span(value.base_name.?))); + value.arg_types.clearAndFree(allocator); + } + symbols.clearAndFree(allocator); + return ret; + } + + function.compile(allocator) catch |err| { + const ret = JSC.toInvalidArguments("{s} when compiling symbol \"{s}\"", .{ + std.mem.span(@errorName(err)), + std.mem.span(function_name), + }, global.ref()); + for (symbols.values()) |*value| { + allocator.free(bun.constStrToU8(std.mem.span(value.base_name.?))); + value.arg_types.clearAndFree(allocator); + } + symbols.clearAndFree(allocator); + return ret; + }; + switch (function.step) { + .failed => |err| { + for (symbols.values()) |*value| { + allocator.free(bun.constStrToU8(std.mem.span(value.base_name.?))); + value.arg_types.clearAndFree(allocator); + } + + const res = ZigString.init(err.msg).toErrorInstance(global); + function.deinit(allocator); + symbols.clearAndFree(allocator); + return res; + }, + .pending => { + for (symbols.values()) |*value| { + allocator.free(bun.constStrToU8(std.mem.span(value.base_name.?))); + value.arg_types.clearAndFree(allocator); + } + symbols.clearAndFree(allocator); + return ZigString.init("Failed to compile (nothing happend!)").toErrorInstance(global); + }, + .compiled => |compiled| { + var cb = JSC.NewFunctionPtr( + global, + &ZigString.init(std.mem.span(function_name)), + @intCast(u32, function.arg_types.items.len), + compiled.ptr, + ); + + obj.put(global, &ZigString.init(std.mem.span(function_name)), JSC.JSValue.cast(cb)); + }, + } + } + + var lib = allocator.create(FFI) catch unreachable; + lib.* = .{ + .dylib = null, + .functions = symbols, + }; + + var close_object = JSC.JSValue.c(Class.make(global.ref(), lib)); + + return JSC.JSValue.createObject2(global, &ZigString.init("close"), &ZigString.init("symbols"), close_object, obj); + } + pub fn generateSymbolForFunction(global: *JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, function: *Function) !?JSValue { + var abi_types = std.ArrayListUnmanaged(ABIType){}; + + if (value.get(global, "args")) |args| { + if (args.isEmptyOrUndefinedOrNull() or !args.jsType().isArray()) { + return ZigString.init("Expected an object with \"args\" as an array").toErrorInstance(global); + } + + var array = args.arrayIterator(global); + + try abi_types.ensureTotalCapacityPrecise(allocator, array.len); + while (array.next()) |val| { + if (val.isEmptyOrUndefinedOrNull()) { + abi_types.clearAndFree(allocator); + return ZigString.init("param must be a string (type name) or number").toErrorInstance(global); + } + + if (val.isAnyInt()) { + const int = val.toInt32(); + switch (int) { + 0...14 => { + abi_types.appendAssumeCapacity(@intToEnum(ABIType, int)); + continue; + }, + else => { + abi_types.clearAndFree(allocator); + return ZigString.init("invalid ABI type").toErrorInstance(global); + }, + } + } + + if (!val.jsType().isStringLike()) { + abi_types.clearAndFree(allocator); + return ZigString.init("param must be a string (type name) or number").toErrorInstance(global); + } + + var type_name = val.toSlice(global, allocator); + defer type_name.deinit(); + abi_types.appendAssumeCapacity(ABIType.label.get(type_name.slice()) orelse { + abi_types.clearAndFree(allocator); + return JSC.toTypeError(JSC.Node.ErrorCode.ERR_INVALID_ARG_VALUE, "Unknown type {s}", .{type_name.slice()}, global.ref()); + }); + } + } + // var function + var return_type = ABIType.@"void"; + + if (value.get(global, "returns")) |ret_value| brk: { + if (ret_value.isAnyInt()) { + const int = ret_value.toInt32(); + switch (int) { + 0...14 => { + return_type = @intToEnum(ABIType, int); + break :brk; + }, + else => { + abi_types.clearAndFree(allocator); + return ZigString.init("invalid ABI type").toErrorInstance(global); + }, + } + } + + var ret_slice = ret_value.toSlice(global, allocator); + defer ret_slice.deinit(); + return_type = ABIType.label.get(ret_slice.slice()) orelse { + abi_types.clearAndFree(allocator); + return JSC.toTypeError(JSC.Node.ErrorCode.ERR_INVALID_ARG_VALUE, "Unknown return type {s}", .{ret_slice.slice()}, global.ref()); + }; + } + + function.* = Function{ + .base_name = null, + .arg_types = abi_types, + .return_type = return_type, + }; + + if (value.get(global, "ptr")) |ptr| { + if (ptr.isNumber()) { + const num = @bitCast(usize, ptr.asNumber()); + if (num > 0) + function.symbol_from_dynamic_library = @intToPtr(*anyopaque, num); + } else { + const num = ptr.toUInt64NoTruncate(); + if (num > 0) { + function.symbol_from_dynamic_library = @intToPtr(*anyopaque, num); + } + } + } + + return null; + } + pub fn generateSymbols(global: *JSGlobalObject, symbols: *std.StringArrayHashMapUnmanaged(Function), object: JSC.JSValue) !?JSValue { + const allocator = VirtualMachine.vm.allocator; + + var keys = JSC.C.JSObjectCopyPropertyNames(global.ref(), object.asObjectRef()); + defer JSC.C.JSPropertyNameArrayRelease(keys); + const count = JSC.C.JSPropertyNameArrayGetCount(keys); + + try symbols.ensureTotalCapacity(allocator, count); + + var i: usize = 0; + while (i < count) : (i += 1) { + var property_name_ref = JSC.C.JSPropertyNameArrayGetNameAtIndex(keys, i); + defer JSC.C.JSStringRelease(property_name_ref); + const len = JSC.C.JSStringGetLength(property_name_ref); + if (len == 0) continue; + var prop = JSC.C.JSStringGetCharacters8Ptr(property_name_ref)[0..len]; + + var value = JSC.JSValue.c(JSC.C.JSObjectGetProperty(global.ref(), object.asObjectRef(), property_name_ref, null)); + if (value.isEmptyOrUndefinedOrNull()) { + return JSC.toTypeError(JSC.Node.ErrorCode.ERR_INVALID_ARG_VALUE, "Expected an object for key \"{s}\"", .{prop}, global.ref()); + } + + var function: Function = .{}; + if (try generateSymbolForFunction(global, allocator, value, &function)) |val| { + return val; + } + function.base_name = try allocator.dupeZ(u8, prop); + + symbols.putAssumeCapacity(std.mem.span(function.base_name.?), function); + } + + return null; + } + + pub const Function = struct { + symbol_from_dynamic_library: ?*anyopaque = null, + base_name: ?[:0]const u8 = null, + state: ?*TCC.TCCState = null, + + return_type: ABIType = ABIType.@"void", + arg_types: std.ArrayListUnmanaged(ABIType) = .{}, + step: Step = Step{ .pending = {} }, + + pub var lib_dirZ: [*:0]const u8 = ""; + + pub fn deinit(val: *Function, allocator: std.mem.Allocator) void { + if (val.base_name) |base_name| { + if (std.mem.span(base_name).len > 0) { + allocator.free(bun.constStrToU8(std.mem.span(base_name))); + } + } + + val.arg_types.clearAndFree(allocator); + + if (val.state) |state| { + TCC.tcc_delete(state); + val.state = null; + } + + if (val.step == .compiled) { + // allocator.free(val.step.compiled.buf); + if (val.step.compiled.js_function) |js_function| { + JSC.C.JSValueUnprotect(@ptrCast(JSC.C.JSContextRef, val.step.compiled.js_context.?), @ptrCast(JSC.C.JSObjectRef, js_function)); + } + } + + if (val.step == .failed and val.step.failed.allocated) { + allocator.free(val.step.failed.msg); + } + } + + pub const Step = union(enum) { + pending: void, + compiled: struct { + ptr: *anyopaque, + buf: []u8, + js_function: ?*anyopaque = null, + js_context: ?*anyopaque = null, + }, + failed: struct { + msg: []const u8, + allocated: bool = false, + }, + }; + + const FFI_HEADER: string = @embedFile("./FFI.h"); + pub inline fn ffiHeader() string { + if (comptime Environment.isDebug) { + var dirpath = std.fs.path.dirname(@src().file).?; + var env = std.process.getEnvMap(default_allocator) catch unreachable; + + const dir = std.mem.replaceOwned( + u8, + default_allocator, + dirpath, + "jarred", + env.get("USER").?, + ) catch unreachable; + var runtime_path = std.fs.path.join(default_allocator, &[_]string{ dir, "FFI.h" }) catch unreachable; + const file = std.fs.openFileAbsolute(runtime_path, .{}) catch @panic("Missing bun/src/bun.js/api/FFI.h."); + defer file.close(); + return file.readToEndAlloc(default_allocator, (file.stat() catch unreachable).size) catch unreachable; + } else { + return FFI_HEADER; + } + } + + pub fn handleTCCError(ctx: ?*anyopaque, message: [*c]const u8) callconv(.C) void { + var this = bun.cast(*Function, ctx.?); + var msg = std.mem.span(message); + if (msg.len > 0) { + var offset: usize = 0; + // the message we get from TCC sometimes has garbage in it + // i think because we're doing in-memory compilation + while (offset < msg.len) : (offset += 1) { + if (msg[offset] > 0x20 and msg[offset] < 0x7f) break; + } + msg = msg[offset..]; + } + + this.step = .{ .failed = .{ .msg = VirtualMachine.vm.allocator.dupe(u8, msg) catch unreachable, .allocated = true } }; + } + + extern fn pthread_jit_write_protect_np(enable: bool) callconv(.C) void; + + const MyFunctionSStructWorkAround = struct { + JSVALUE_TO_INT64: fn (JSValue0: JSC.JSValue) callconv(.C) i64, + JSVALUE_TO_UINT64: fn (JSValue0: JSC.JSValue) callconv(.C) u64, + INT64_TO_JSVALUE: fn (arg0: [*c]JSC.JSGlobalObject, arg1: i64) callconv(.C) JSC.JSValue, + UINT64_TO_JSVALUE: fn (arg0: [*c]JSC.JSGlobalObject, arg1: u64) callconv(.C) JSC.JSValue, + bun_call: *const @TypeOf(JSC.C.JSObjectCallAsFunction), + }; + const headers = @import("../bindings/headers.zig"); + + var workaround: MyFunctionSStructWorkAround = .{ + .JSVALUE_TO_INT64 = headers.JSC__JSValue__toInt64, + .JSVALUE_TO_UINT64 = headers.JSC__JSValue__toUInt64NoTruncate, + .INT64_TO_JSVALUE = headers.JSC__JSValue__fromInt64NoTruncate, + .UINT64_TO_JSVALUE = headers.JSC__JSValue__fromUInt64NoTruncate, + .bun_call = &JSC.C.JSObjectCallAsFunction, + }; + + const tcc_options = "-std=c11 -nostdlib -Wl,--export-all-symbols" ++ if (Environment.isDebug) " -g" else ""; + + pub fn compile( + this: *Function, + allocator: std.mem.Allocator, + ) !void { + var source_code = std.ArrayList(u8).init(allocator); + var source_code_writer = source_code.writer(); + try this.printSourceCode(&source_code_writer); + + try source_code.append(0); + defer source_code.deinit(); + + var state = TCC.tcc_new() orelse return error.TCCMissing; + TCC.tcc_set_options(state, tcc_options); + // addSharedLibPaths(state); + TCC.tcc_set_error_func(state, this, handleTCCError); + this.state = state; + defer { + if (this.step == .failed) { + TCC.tcc_delete(state); + this.state = null; + } + } + + _ = TCC.tcc_set_output_type(state, TCC.TCC_OUTPUT_MEMORY); + const Sizes = @import("../bindings/sizes.zig"); + + var symbol_buf: [256]u8 = undefined; + TCC.tcc_define_symbol( + state, + "Bun_FFI_PointerOffsetToArgumentsList", + std.fmt.bufPrintZ(&symbol_buf, "{d}", .{Sizes.Bun_FFI_PointerOffsetToArgumentsList}) catch unreachable, + ); + CompilerRT.define(state); + + // TCC.tcc_define_symbol( + // state, + // "Bun_FFI_PointerOffsetToArgumentsCount", + // std.fmt.bufPrintZ(symbol_buf[8..], "{d}", .{Bun_FFI_PointerOffsetToArgumentsCount}) catch unreachable, + // ); + + const compilation_result = TCC.tcc_compile_string( + state, + source_code.items.ptr, + ); + // did tcc report an error? + if (this.step == .failed) { + return; + } + + // did tcc report failure but never called the error callback? + if (compilation_result == -1) { + this.step = .{ .failed = .{ .msg = "tcc returned -1, which means it failed" } }; + return; + } + CompilerRT.inject(state); + _ = TCC.tcc_add_symbol(state, this.base_name.?, this.symbol_from_dynamic_library.?); + + if (this.step == .failed) { + return; + } + + var relocation_size = TCC.tcc_relocate(state, null); + if (this.step == .failed) { + return; + } + + if (relocation_size < 0) { + this.step = .{ .failed = .{ .msg = "tcc_relocate returned a negative value" } }; + return; + } + + var bytes: []u8 = try allocator.rawAlloc(@intCast(usize, relocation_size), 16, 16, 0); + defer { + if (this.step == .failed) { + allocator.free(bytes); + } + } + + if (comptime Environment.isAarch64 and Environment.isMac) { + pthread_jit_write_protect_np(false); + } + _ = TCC.tcc_relocate(state, bytes.ptr); + if (comptime Environment.isAarch64 and Environment.isMac) { + pthread_jit_write_protect_np(true); + } + + var symbol = TCC.tcc_get_symbol(state, "JSFunctionCall") orelse { + this.step = .{ .failed = .{ .msg = "missing generated symbol in source code" } }; + + return; + }; + + this.step = .{ + .compiled = .{ + .ptr = symbol, + .buf = bytes, + }, + }; + return; + } + const CompilerRT = struct { + noinline fn memset( + dest: [*]u8, + c: u8, + byte_count: usize, + ) callconv(.C) void { + @memset(dest, c, byte_count); + } + + noinline fn memcpy( + noalias dest: [*]u8, + noalias source: [*]const u8, + byte_count: usize, + ) callconv(.C) void { + @memcpy(dest, source, byte_count); + } + + pub fn define(state: *TCC.TCCState) void { + if (comptime Environment.isX64) { + _ = TCC.tcc_define_symbol(state, "NEEDS_COMPILER_RT_FUNCTIONS", "1"); + // there + _ = TCC.tcc_compile_string(state, @embedFile(("libtcc1.c"))); + } + } + + pub fn inject(state: *TCC.TCCState) void { + _ = TCC.tcc_add_symbol(state, "memset", &memset); + _ = TCC.tcc_add_symbol(state, "memcpy", &memcpy); + + _ = TCC.tcc_add_symbol( + state, + "JSVALUE_TO_INT64_SLOW", + workaround.JSVALUE_TO_INT64, + ); + _ = TCC.tcc_add_symbol( + state, + "JSVALUE_TO_UINT64_SLOW", + workaround.JSVALUE_TO_UINT64, + ); + std.mem.doNotOptimizeAway(headers.JSC__JSValue__toUInt64NoTruncate); + std.mem.doNotOptimizeAway(headers.JSC__JSValue__toInt64); + std.mem.doNotOptimizeAway(headers.JSC__JSValue__fromInt64NoTruncate); + std.mem.doNotOptimizeAway(headers.JSC__JSValue__fromUInt64NoTruncate); + _ = TCC.tcc_add_symbol( + state, + "INT64_TO_JSVALUE_SLOW", + workaround.INT64_TO_JSVALUE, + ); + _ = TCC.tcc_add_symbol( + state, + "UINT64_TO_JSVALUE_SLOW", + workaround.UINT64_TO_JSVALUE, + ); + } + }; + + pub fn compileCallback( + this: *Function, + allocator: std.mem.Allocator, + js_context: *anyopaque, + js_function: *anyopaque, + ) !void { + Output.debug("welcome", .{}); + var source_code = std.ArrayList(u8).init(allocator); + var source_code_writer = source_code.writer(); + try this.printCallbackSourceCode(&source_code_writer); + Output.debug("helllooo", .{}); + try source_code.append(0); + // defer source_code.deinit(); + var state = TCC.tcc_new() orelse return error.TCCMissing; + TCC.tcc_set_options(state, tcc_options); + TCC.tcc_set_error_func(state, this, handleTCCError); + this.state = state; + defer { + if (this.step == .failed) { + TCC.tcc_delete(state); + this.state = null; + } + } + + _ = TCC.tcc_set_output_type(state, TCC.TCC_OUTPUT_MEMORY); + + CompilerRT.define(state); + + const compilation_result = TCC.tcc_compile_string( + state, + source_code.items.ptr, + ); + Output.debug("compile", .{}); + // did tcc report an error? + if (this.step == .failed) { + return; + } + + // did tcc report failure but never called the error callback? + if (compilation_result == -1) { + this.step = .{ .failed = .{ .msg = "tcc returned -1, which means it failed" } }; + + return; + } + + CompilerRT.inject(state); + Output.debug("here", .{}); + _ = TCC.tcc_add_symbol(state, "bun_call", workaround.bun_call.*); + _ = TCC.tcc_add_symbol(state, "cachedJSContext", js_context); + _ = TCC.tcc_add_symbol(state, "cachedCallbackFunction", js_function); + + var relocation_size = TCC.tcc_relocate(state, null); + if (relocation_size == 0) return; + var bytes: []u8 = try allocator.rawAlloc(@intCast(usize, relocation_size), 16, 16, 0); + defer { + if (this.step == .failed) { + allocator.free(bytes); + } + } + + if (comptime Environment.isAarch64 and Environment.isMac) { + pthread_jit_write_protect_np(false); + } + _ = TCC.tcc_relocate(state, bytes.ptr); + if (comptime Environment.isAarch64 and Environment.isMac) { + pthread_jit_write_protect_np(true); + } + + var symbol = TCC.tcc_get_symbol(state, "my_callback_function") orelse { + this.step = .{ .failed = .{ .msg = "missing generated symbol in source code" } }; + + return; + }; + + this.step = .{ + .compiled = .{ + .ptr = symbol, + .buf = bytes, + .js_function = js_function, + .js_context = js_context, + }, + }; + } + + pub fn printSourceCode( + this: *Function, + writer: anytype, + ) !void { + if (this.arg_types.items.len > 0) { + try writer.writeAll("#define HAS_ARGUMENTS\n"); + } + + brk: { + if (this.return_type.isFloatingPoint()) { + try writer.writeAll("#define USES_FLOAT 1\n"); + break :brk; + } + + for (this.arg_types.items) |arg| { + // conditionally include math.h + if (arg.isFloatingPoint()) { + try writer.writeAll("#define USES_FLOAT 1\n"); + break; + } + } + } + + if (comptime Environment.isRelease) { + try writer.writeAll(std.mem.span(FFI_HEADER)); + } else { + try writer.writeAll(ffiHeader()); + } + + // -- Generate the FFI function symbol + try writer.writeAll("/* --- The Function To Call */\n"); + try this.return_type.typename(writer); + try writer.writeAll(" "); + try writer.writeAll(std.mem.span(this.base_name.?)); + try writer.writeAll("("); + var first = true; + for (this.arg_types.items) |arg, i| { + if (!first) { + try writer.writeAll(", "); + } + first = false; + try arg.typename(writer); + try writer.print(" arg{d}", .{i}); + } + try writer.writeAll( + \\); + \\ + \\ + \\/* ---- Your Wrapper Function ---- */ + \\void* JSFunctionCall(void* globalObject, void* callFrame) { + \\ + ); + + if (this.arg_types.items.len > 0) { + try writer.writeAll( + \\ LOAD_ARGUMENTS_FROM_CALL_FRAME; + \\ + ); + for (this.arg_types.items) |arg, i| { + if (arg.needsACastInC()) { + if (i < this.arg_types.items.len - 1) { + try writer.print( + \\ EncodedJSValue arg{d}; + \\ arg{d}.asInt64 = *argsPtr++; + \\ + , + .{ + i, + i, + }, + ); + } else { + try writer.print( + \\ EncodedJSValue arg{d}; + \\ arg{d}.asInt64 = *argsPtr; + \\ + , + .{ + i, + i, + }, + ); + } + } else { + if (i < this.arg_types.items.len - 1) { + try writer.print( + \\ int64_t arg{d} = *argsPtr++; + \\ + , + .{ + i, + }, + ); + } else { + try writer.print( + \\ int64_t arg{d} = *argsPtr; + \\ + , + .{ + i, + }, + ); + } + } + } + } + + // try writer.writeAll( + // "(JSContext ctx, void* function, void* thisObject, size_t argumentCount, const EncodedJSValue arguments[], void* exception);\n\n", + // ); + + var arg_buf: [512]u8 = undefined; + + try writer.writeAll(" "); + if (!(this.return_type == .void)) { + try this.return_type.typename(writer); + try writer.writeAll(" return_value = "); + } + try writer.print("{s}(", .{std.mem.span(this.base_name.?)}); + first = true; + arg_buf[0..3].* = "arg".*; + for (this.arg_types.items) |arg, i| { + if (!first) { + try writer.writeAll(", "); + } + first = false; + + try writer.writeAll(" "); + const lengthBuf = std.fmt.bufPrintIntToSlice(arg_buf["arg".len..], i, 10, .lower, .{}); + const argName = arg_buf[0 .. 3 + lengthBuf.len]; + if (arg.needsACastInC()) { + try writer.print("{}", .{arg.toC(argName)}); + } else { + try writer.writeAll(argName); + } + } + try writer.writeAll(");\n"); + + if (!first) try writer.writeAll("\n"); + + try writer.writeAll(" "); + + try writer.writeAll("return "); + + if (!(this.return_type == .void)) { + try writer.print("{}.asPtr", .{this.return_type.toJS("return_value")}); + } else { + try writer.writeAll("ValueUndefined.asPtr"); + } + + try writer.writeAll(";\n}\n\n"); + } + + pub fn printCallbackSourceCode( + this: *Function, + writer: anytype, + ) !void { + try writer.writeAll("#define IS_CALLBACK 1\n"); + + brk: { + if (this.return_type.isFloatingPoint()) { + try writer.writeAll("#define USES_FLOAT 1\n"); + break :brk; + } + + for (this.arg_types.items) |arg| { + // conditionally include math.h + if (arg.isFloatingPoint()) { + try writer.writeAll("#define USES_FLOAT 1\n"); + break; + } + } + } + + if (comptime Environment.isRelease) { + try writer.writeAll(std.mem.span(FFI_HEADER)); + } else { + try writer.writeAll(ffiHeader()); + } + + // -- Generate the FFI function symbol + try writer.writeAll("\n \n/* --- The Callback Function */\n"); + try writer.writeAll("/* --- The Callback Function */\n"); + try this.return_type.typename(writer); + try writer.writeAll(" my_callback_function"); + try writer.writeAll("("); + var first = true; + for (this.arg_types.items) |arg, i| { + if (!first) { + try writer.writeAll(", "); + } + first = false; + try arg.typename(writer); + try writer.print(" arg{d}", .{i}); + } + try writer.writeAll(");\n\n"); + + first = true; + try this.return_type.typename(writer); + + try writer.writeAll(" my_callback_function"); + try writer.writeAll("("); + for (this.arg_types.items) |arg, i| { + if (!first) { + try writer.writeAll(", "); + } + first = false; + try arg.typename(writer); + try writer.print(" arg{d}", .{i}); + } + try writer.writeAll(") {\n"); + + if (comptime Environment.isDebug) { + try writer.writeAll("#ifdef INJECT_BEFORE\n"); + try writer.writeAll("INJECT_BEFORE;\n"); + try writer.writeAll("#endif\n"); + } + + first = true; + + if (this.arg_types.items.len > 0) { + try writer.print(" EncodedJSValue arguments[{d}] = {{\n", .{this.arg_types.items.len}); + + var arg_buf: [512]u8 = undefined; + arg_buf[0.."arg".len].* = "arg".*; + for (this.arg_types.items) |arg, i| { + const printed = std.fmt.bufPrintIntToSlice(arg_buf["arg".len..], i, 10, .lower, .{}); + const arg_name = arg_buf[0 .. "arg".len + printed.len]; + try writer.print(" {}", .{arg.toJS(arg_name)}); + if (i < this.arg_types.items.len - 1) { + try writer.writeAll(",\n"); + } + } + try writer.writeAll("\n };\n"); + } else { + try writer.writeAll(" EncodedJSValue arguments[1] = {{0}};\n"); + } + + try writer.writeAll(" "); + if (!(this.return_type == .void)) { + try writer.writeAll("EncodedJSValue return_value = {"); + } + // JSC.C.JSObjectCallAsFunction( + // ctx, + // object, + // thisObject, + // argumentCount, + // arguments, + // exception, + // ); + try writer.writeAll("bun_call(cachedJSContext, cachedCallbackFunction, (void*)0, "); + if (this.arg_types.items.len > 0) { + try writer.print("{d}, &arguments[0], (void*)0)", .{this.arg_types.items.len}); + } else { + try writer.writeAll("0, &arguments[0], (void*)0)"); + } + + if (this.return_type != .void) { + try writer.print("}};\n return {}", .{this.return_type.toC("return_value")}); + } + + try writer.writeAll(";\n}\n\n"); + } + }; + + pub const ABIType = enum(i32) { + char = 0, + + int8_t = 1, + uint8_t = 2, + + int16_t = 3, + uint16_t = 4, + + int32_t = 5, + uint32_t = 6, + + int64_t = 7, + uint64_t = 8, + + double = 9, + float = 10, + + bool = 11, + + ptr = 12, + + @"void" = 13, + + cstring = 14, + + i64_fast = 15, + u64_fast = 16, + + /// Types that we can directly pass through as an `int64_t` + pub fn needsACastInC(this: ABIType) bool { + return switch (this) { + .char, .int8_t, .uint8_t, .int16_t, .uint16_t, .int32_t, .uint32_t => false, + else => true, + }; + } + + const map = .{ + .{ "bool", ABIType.bool }, + .{ "c_int", ABIType.int32_t }, + .{ "c_uint", ABIType.uint32_t }, + .{ "char", ABIType.char }, + .{ "char*", ABIType.ptr }, + .{ "double", ABIType.double }, + .{ "f32", ABIType.float }, + .{ "f64", ABIType.double }, + .{ "float", ABIType.float }, + .{ "i16", ABIType.int16_t }, + .{ "i32", ABIType.int32_t }, + .{ "i64", ABIType.int64_t }, + .{ "i8", ABIType.int8_t }, + .{ "int", ABIType.int32_t }, + .{ "int16_t", ABIType.int16_t }, + .{ "int32_t", ABIType.int32_t }, + .{ "int64_t", ABIType.int64_t }, + .{ "int8_t", ABIType.int8_t }, + .{ "isize", ABIType.int64_t }, + .{ "u16", ABIType.uint16_t }, + .{ "u32", ABIType.uint32_t }, + .{ "u64", ABIType.uint64_t }, + .{ "u8", ABIType.uint8_t }, + .{ "uint16_t", ABIType.uint16_t }, + .{ "uint32_t", ABIType.uint32_t }, + .{ "uint64_t", ABIType.uint64_t }, + .{ "uint8_t", ABIType.uint8_t }, + .{ "usize", ABIType.uint64_t }, + .{ "void*", ABIType.ptr }, + .{ "ptr", ABIType.ptr }, + .{ "pointer", ABIType.ptr }, + .{ "void", ABIType.@"void" }, + .{ "cstring", ABIType.@"cstring" }, + .{ "i64_fast", ABIType.i64_fast }, + .{ "u64_fast", ABIType.u64_fast }, + }; + pub const label = ComptimeStringMap(ABIType, map); + const EnumMapFormatter = struct { + name: []const u8, + entry: ABIType, + pub fn format(self: EnumMapFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll("['"); + // these are not all valid identifiers + try writer.writeAll(self.name); + try writer.writeAll("']:"); + try std.fmt.formatInt(@enumToInt(self.entry), 10, .lower, .{}, writer); + try writer.writeAll(",'"); + try std.fmt.formatInt(@enumToInt(self.entry), 10, .lower, .{}, writer); + try writer.writeAll("':"); + try std.fmt.formatInt(@enumToInt(self.entry), 10, .lower, .{}, writer); + } + }; + pub const map_to_js_object = brk: { + var count: usize = 2; + for (map) |item, i| { + var fmt = EnumMapFormatter{ .name = item.@"0", .entry = item.@"1" }; + count += std.fmt.count("{}", .{fmt}); + count += @boolToInt(i > 0); + } + + var buf: [count]u8 = undefined; + buf[0] = '{'; + buf[buf.len - 1] = '}'; + var end: usize = 1; + for (map) |item, i| { + var fmt = EnumMapFormatter{ .name = item.@"0", .entry = item.@"1" }; + if (i > 0) { + buf[end] = ','; + end += 1; + } + end += (std.fmt.bufPrint(buf[end..], "{}", .{fmt}) catch unreachable).len; + } + + break :brk buf; + }; + + pub fn isFloatingPoint(this: ABIType) bool { + return switch (this) { + .double, .float => true, + else => false, + }; + } + + const ToCFormatter = struct { + symbol: string, + tag: ABIType, + + pub fn format(self: ToCFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (self.tag) { + .void => {}, + .bool => { + try writer.print("JSVALUE_TO_BOOL({s})", .{self.symbol}); + }, + .char, .int8_t, .uint8_t, .int16_t, .uint16_t, .int32_t, .uint32_t => { + try writer.print("JSVALUE_TO_INT32({s})", .{self.symbol}); + }, + .i64_fast, .int64_t => { + try writer.print("JSVALUE_TO_INT64({s})", .{self.symbol}); + }, + .u64_fast, .uint64_t => { + try writer.print("JSVALUE_TO_UINT64(globalObject, {s})", .{self.symbol}); + }, + .cstring, .ptr => { + try writer.print("JSVALUE_TO_PTR({s})", .{self.symbol}); + }, + .double => { + try writer.print("JSVALUE_TO_DOUBLE({s})", .{self.symbol}); + }, + .float => { + try writer.print("JSVALUE_TO_FLOAT({s})", .{self.symbol}); + }, + } + } + }; + + const ToJSFormatter = struct { + symbol: []const u8, + tag: ABIType, + + pub fn format(self: ToJSFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (self.tag) { + .void => {}, + .bool => { + try writer.print("BOOLEAN_TO_JSVALUE({s})", .{self.symbol}); + }, + .char, .int8_t, .uint8_t, .int16_t, .uint16_t, .int32_t, .uint32_t => { + try writer.print("INT32_TO_JSVALUE({s})", .{self.symbol}); + }, + .i64_fast => { + try writer.print("INT64_TO_JSVALUE(globalObject, {s})", .{self.symbol}); + }, + .int64_t => { + try writer.print("INT64_TO_JSVALUE_SLOW(globalObject, {s})", .{self.symbol}); + }, + .u64_fast => { + try writer.print("UINT64_TO_JSVALUE(globalObject, {s})", .{self.symbol}); + }, + .uint64_t => { + try writer.print("UINT64_TO_JSVALUE_SLOW(globalObject, {s})", .{self.symbol}); + }, + .cstring, .ptr => { + try writer.print("PTR_TO_JSVALUE({s})", .{self.symbol}); + }, + .double => { + try writer.print("DOUBLE_TO_JSVALUE({s})", .{self.symbol}); + }, + .float => { + try writer.print("FLOAT_TO_JSVALUE({s})", .{self.symbol}); + }, + } + } + }; + + pub fn toC(this: ABIType, symbol: string) ToCFormatter { + return ToCFormatter{ .tag = this, .symbol = symbol }; + } + + pub fn toJS( + this: ABIType, + symbol: string, + ) ToJSFormatter { + return ToJSFormatter{ + .tag = this, + .symbol = symbol, + }; + } + + pub fn typename(this: ABIType, writer: anytype) !void { + try writer.writeAll(this.typenameLabel()); + } + + pub fn typenameLabel(this: ABIType) []const u8 { + return switch (this) { + .cstring, .ptr => "void*", + .bool => "bool", + .int8_t => "int8_t", + .uint8_t => "uint8_t", + .int16_t => "int16_t", + .uint16_t => "uint16_t", + .int32_t => "int32_t", + .uint32_t => "uint32_t", + .i64_fast, .int64_t => "int64_t", + .u64_fast, .uint64_t => "uint64_t", + .double => "double", + .float => "float", + .char => "char", + .void => "void", + }; + } + }; +}; diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig new file mode 100644 index 000000000..fc91c76ad --- /dev/null +++ b/src/bun.js/api/html_rewriter.zig @@ -0,0 +1,1886 @@ +const std = @import("std"); +const Api = @import("../../api/schema.zig").Api; +const FilesystemRouter = @import("../../router.zig"); +const http = @import("../../http.zig"); +const JavaScript = @import("../javascript.zig"); +const QueryStringMap = @import("../../url.zig").QueryStringMap; +const CombinedScanner = @import("../../url.zig").CombinedScanner; +const bun = @import("../../global.zig"); +const string = bun.string; +const JSC = @import("../../jsc.zig"); +const js = JSC.C; +const WebCore = @import("../webcore/response.zig"); +const Router = @This(); +const Bundler = @import("../../bundler.zig"); +const VirtualMachine = JavaScript.VirtualMachine; +const ScriptSrcStream = std.io.FixedBufferStream([]u8); +const ZigString = JSC.ZigString; +const Fs = @import("../../fs.zig"); +const Base = @import("../base.zig"); +const getAllocator = Base.getAllocator; +const JSObject = JSC.JSObject; +const JSError = Base.JSError; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const strings = @import("strings"); +const NewClass = Base.NewClass; +const To = Base.To; +const Request = WebCore.Request; +const d = Base.d; +const FetchEvent = WebCore.FetchEvent; +const Response = WebCore.Response; +const LOLHTML = @import("lolhtml"); + +const SelectorMap = std.ArrayListUnmanaged(*LOLHTML.HTMLSelector); +pub const LOLHTMLContext = struct { + selectors: SelectorMap = .{}, + element_handlers: std.ArrayListUnmanaged(*ElementHandler) = .{}, + document_handlers: std.ArrayListUnmanaged(*DocumentHandler) = .{}, + + pub fn deinit(this: *LOLHTMLContext, allocator: std.mem.Allocator) void { + for (this.selectors.items) |selector| { + selector.deinit(); + } + this.selectors.deinit(allocator); + this.selectors = .{}; + + for (this.element_handlers.items) |handler| { + handler.deinit(); + } + this.element_handlers.deinit(allocator); + this.element_handlers = .{}; + + for (this.document_handlers.items) |handler| { + handler.deinit(); + } + this.document_handlers.deinit(allocator); + this.document_handlers = .{}; + } +}; +pub const HTMLRewriter = struct { + builder: *LOLHTML.HTMLRewriter.Builder, + context: LOLHTMLContext, + + pub const Constructor = JSC.NewConstructor(HTMLRewriter, .{ .constructor = constructor }, .{}); + + pub const Class = NewClass( + HTMLRewriter, + .{ .name = "HTMLRewriter" }, + .{ + .finalize = finalize, + .on = .{ + .rfn = wrap(HTMLRewriter, "on"), + }, + .onDocument = .{ + .rfn = wrap(HTMLRewriter, "onDocument"), + }, + .transform = .{ + .rfn = wrap(HTMLRewriter, "transform"), + }, + }, + .{}, + ); + + pub fn constructor( + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, + ) js.JSObjectRef { + var rewriter = bun.default_allocator.create(HTMLRewriter) catch unreachable; + rewriter.* = HTMLRewriter{ + .builder = LOLHTML.HTMLRewriter.Builder.init(), + .context = .{}, + }; + return HTMLRewriter.Class.make(ctx, rewriter); + } + + pub fn on( + this: *HTMLRewriter, + global: *JSGlobalObject, + selector_name: ZigString, + thisObject: JSC.C.JSObjectRef, + listener: JSValue, + exception: JSC.C.ExceptionRef, + ) JSValue { + var selector_slice = std.fmt.allocPrint(bun.default_allocator, "{}", .{selector_name}) catch unreachable; + + var selector = LOLHTML.HTMLSelector.parse(selector_slice) catch + return throwLOLHTMLError(global); + var handler_ = ElementHandler.init(global, listener, exception); + if (exception.* != null) { + selector.deinit(); + return JSValue.fromRef(exception.*); + } + var handler = getAllocator(global.ref()).create(ElementHandler) catch unreachable; + handler.* = handler_; + + this.builder.addElementContentHandlers( + selector, + + ElementHandler, + ElementHandler.onElement, + if (handler.onElementCallback != null) + handler + else + null, + + ElementHandler, + ElementHandler.onComment, + if (handler.onCommentCallback != null) + handler + else + null, + + ElementHandler, + ElementHandler.onText, + if (handler.onTextCallback != null) + handler + else + null, + ) catch { + selector.deinit(); + return throwLOLHTMLError(global); + }; + + this.context.selectors.append(bun.default_allocator, selector) catch unreachable; + this.context.element_handlers.append(bun.default_allocator, handler) catch unreachable; + return JSValue.fromRef(thisObject); + } + + pub fn onDocument( + this: *HTMLRewriter, + global: *JSGlobalObject, + listener: JSValue, + thisObject: JSC.C.JSObjectRef, + exception: JSC.C.ExceptionRef, + ) JSValue { + var handler_ = DocumentHandler.init(global, listener, exception); + if (exception.* != null) { + return JSValue.fromRef(exception.*); + } + + var handler = getAllocator(global.ref()).create(DocumentHandler) catch unreachable; + handler.* = handler_; + + this.builder.addDocumentContentHandlers( + DocumentHandler, + DocumentHandler.onDocType, + if (handler.onDocTypeCallback != null) + handler + else + null, + + DocumentHandler, + DocumentHandler.onComment, + if (handler.onCommentCallback != null) + handler + else + null, + + DocumentHandler, + DocumentHandler.onText, + if (handler.onTextCallback != null) + handler + else + null, + + DocumentHandler, + DocumentHandler.onEnd, + if (handler.onEndCallback != null) + handler + else + null, + ) catch { + return throwLOLHTMLError(global); + }; + + this.context.document_handlers.append(bun.default_allocator, handler) catch unreachable; + return JSValue.fromRef(thisObject); + } + + pub fn finalize(this: *HTMLRewriter) void { + this.finalizeWithoutDestroy(); + bun.default_allocator.destroy(this); + } + + pub fn finalizeWithoutDestroy(this: *HTMLRewriter) void { + this.context.deinit(bun.default_allocator); + } + + pub fn beginTransform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { + const new_context = this.context; + this.context = .{}; + return BufferOutputSink.init(new_context, global, response, this.builder); + } + + pub fn returnEmptyResponse(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { + var result = bun.default_allocator.create(Response) catch unreachable; + + response.cloneInto(result, getAllocator(global.ref()), global); + this.finalizeWithoutDestroy(); + return JSValue.fromRef(Response.makeMaybePooled(global.ref(), result)); + } + + pub fn transform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { + var input = response.body.slice(); + + if (input.len == 0 and !(response.body.value == .Blob and response.body.value.Blob.needsToReadFile())) { + return this.returnEmptyResponse(global, response); + } + + return this.beginTransform(global, response); + } + + pub const HTMLRewriterLoader = struct { + rewriter: *LOLHTML.HTMLRewriter, + finalized: bool = false, + context: LOLHTMLContext, + chunk_size: usize = 0, + failed: bool = false, + output: JSC.WebCore.Sink, + signal: JSC.WebCore.Signal = .{}, + backpressure: std.fifo.LinearFifo(u8, .Dynamic) = std.fifo.LinearFifo(u8, .Dynamic).init(bun.default_allocator), + + pub fn finalize(this: *HTMLRewriterLoader) void { + if (this.finalized) return; + this.rewriter.deinit(); + this.backpressure.deinit(); + this.backpressure = std.fifo.LinearFifo(u8, .Dynamic).init(bun.default_allocator); + this.finalized = true; + } + + pub fn fail(this: *HTMLRewriterLoader, err: JSC.Node.Syscall.Error) void { + this.signal.close(err); + this.output.end(err); + this.failed = true; + this.finalize(); + } + + pub fn connect(this: *HTMLRewriterLoader, signal: JSC.WebCore.Signal) void { + this.signal = signal; + } + + pub fn writeToDestination(this: *HTMLRewriterLoader, bytes: []const u8) void { + if (this.backpressure.count > 0) { + this.backpressure.write(bytes) catch { + this.fail(JSC.Node.Syscall.Error.oom); + this.finalize(); + }; + return; + } + + const write_result = this.output.write(.{ .temporary = bun.ByteList.init(bytes) }); + + switch (write_result) { + .err => |err| { + this.fail(err); + }, + .owned_and_done, .temporary_and_done, .into_array_and_done => { + this.done(); + }, + .pending => |pending| { + pending.applyBackpressure(bun.default_allocator, &this.output, pending, bytes); + }, + .into_array, .owned, .temporary => { + this.signal.ready(if (this.chunk_size > 0) this.chunk_size else null, null); + }, + } + } + + pub fn done( + this: *HTMLRewriterLoader, + ) void { + this.output.end(null); + this.signal.close(null); + this.finalize(); + } + + pub fn setup( + this: *HTMLRewriterLoader, + builder: *LOLHTML.HTMLRewriter.Builder, + context: LOLHTMLContext, + size_hint: ?usize, + output: JSC.WebCore.Sink, + ) ?[]const u8 { + for (context.document_handlers.items) |doc| { + doc.ctx = this; + } + for (context.element_handlers.items) |doc| { + doc.ctx = this; + } + + const chunk_size = @maximum(size_hint orelse 16384, 1024); + this.rewriter = builder.build( + .UTF8, + .{ + .preallocated_parsing_buffer_size = chunk_size, + .max_allowed_memory_usage = std.math.maxInt(u32), + }, + false, + HTMLRewriterLoader, + this, + HTMLRewriterLoader.writeToDestination, + HTMLRewriterLoader.done, + ) catch { + output.end(); + return LOLHTML.HTMLString.lastError().slice(); + }; + + this.chunk_size = chunk_size; + this.context = context; + this.output = output; + + return null; + } + + pub fn sink(this: *HTMLRewriterLoader) JSC.WebCore.Sink { + return JSC.WebCore.Sink.init(this); + } + + fn writeBytes(this: *HTMLRewriterLoader, bytes: bun.ByteList, comptime deinit_: bool) ?JSC.Node.Syscall.Error { + this.rewriter.write(bytes.slice()) catch { + return JSC.Node.Syscall.Error{ + .errno = 1, + // TODO: make this a union + .path = bun.default_allocator.dupe(u8, LOLHTML.HTMLString.lastError().slice()) catch unreachable, + }; + }; + if (comptime deinit_) bytes.listManaged(bun.default_allocator).deinit(); + return null; + } + + pub fn write(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable { + switch (data) { + .owned => |bytes| { + if (this.writeBytes(bytes, true)) |err| { + return .{ .err = err }; + } + return .{ .owned = bytes.len }; + }, + .owned_and_done => |bytes| { + if (this.writeBytes(bytes, true)) |err| { + return .{ .err = err }; + } + return .{ .owned_and_done = bytes.len }; + }, + .temporary_and_done => |bytes| { + if (this.writeBytes(bytes, false)) |err| { + return .{ .err = err }; + } + return .{ .temporary_and_done = bytes.len }; + }, + .temporary => |bytes| { + if (this.writeBytes(bytes, false)) |err| { + return .{ .err = err }; + } + return .{ .temporary = bytes.len }; + }, + else => unreachable, + } + } + + pub fn writeUTF16(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable { + return JSC.WebCore.Sink.UTF8Fallback.writeUTF16(HTMLRewriterLoader, this, data, write); + } + + pub fn writeLatin1(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable { + return JSC.WebCore.Sink.UTF8Fallback.writeLatin1(HTMLRewriterLoader, this, data, write); + } + }; + + pub const BufferOutputSink = struct { + global: *JSGlobalObject, + bytes: bun.MutableString, + rewriter: *LOLHTML.HTMLRewriter, + context: LOLHTMLContext, + response: *Response, + input: JSC.WebCore.Blob = undefined, + pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue { + var result = bun.default_allocator.create(Response) catch unreachable; + var sink = bun.default_allocator.create(BufferOutputSink) catch unreachable; + sink.* = BufferOutputSink{ + .global = global, + .bytes = bun.MutableString.initEmpty(bun.default_allocator), + .rewriter = undefined, + .context = context, + .response = result, + }; + + for (sink.context.document_handlers.items) |doc| { + doc.ctx = sink; + } + for (sink.context.element_handlers.items) |doc| { + doc.ctx = sink; + } + + sink.rewriter = builder.build( + .UTF8, + .{ + .preallocated_parsing_buffer_size = @maximum(original.body.len(), 1024), + .max_allowed_memory_usage = std.math.maxInt(u32), + }, + false, + BufferOutputSink, + sink, + BufferOutputSink.write, + BufferOutputSink.done, + ) catch { + sink.deinit(); + bun.default_allocator.destroy(result); + + return throwLOLHTMLError(global); + }; + + result.* = Response{ + .allocator = bun.default_allocator, + .body = .{ + .init = .{ + .status_code = 200, + }, + .value = .{ + .Locked = .{ + .global = global, + .task = sink, + }, + }, + }, + }; + + result.body.init.headers = original.body.init.headers; + result.body.init.method = original.body.init.method; + result.body.init.status_code = original.body.init.status_code; + + result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable; + result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable; + + var input: JSC.WebCore.Blob = original.body.value.use(); + + const is_pending = input.needsToReadFile(); + defer if (!is_pending) input.detach(); + + if (is_pending) { + input.doReadFileInternal(*BufferOutputSink, sink, onFinishedLoading, global); + } else if (sink.runOutputSink(input.sharedView(), false, false)) |error_value| { + return error_value; + } + + // Hold off on cloning until we're actually done. + + return JSC.JSValue.fromRef( + Response.makeMaybePooled(sink.global.ref(), sink.response), + ); + } + + pub fn onFinishedLoading(sink: *BufferOutputSink, bytes: JSC.WebCore.Blob.Store.ReadFile.ResultType) void { + switch (bytes) { + .err => |err| { + if (sink.response.body.value == .Locked and @ptrToInt(sink.response.body.value.Locked.task) == @ptrToInt(sink) and + sink.response.body.value.Locked.promise == null) + { + sink.response.body.value = .{ .Empty = .{} }; + // is there a pending promise? + // we will need to reject it + } else if (sink.response.body.value == .Locked and @ptrToInt(sink.response.body.value.Locked.task) == @ptrToInt(sink) and + sink.response.body.value.Locked.promise != null) + { + sink.response.body.value.Locked.callback = null; + sink.response.body.value.Locked.task = null; + } + + sink.response.body.value.toErrorInstance(err.toErrorInstance(sink.global), sink.global); + sink.rewriter.end() catch {}; + sink.deinit(); + return; + }, + .result => |data| { + _ = sink.runOutputSink(data.buf, true, data.is_temporary); + }, + } + } + + pub fn runOutputSink( + sink: *BufferOutputSink, + bytes: []const u8, + is_async: bool, + free_bytes_on_end: bool, + ) ?JSValue { + defer if (free_bytes_on_end) + bun.default_allocator.free(bun.constStrToU8(bytes)); + + sink.bytes.growBy(bytes.len) catch unreachable; + var global = sink.global; + var response = sink.response; + + sink.rewriter.write(bytes) catch { + sink.deinit(); + bun.default_allocator.destroy(sink); + + if (is_async) { + response.body.value.toErrorInstance(throwLOLHTMLError(global), global); + + return null; + } else { + return throwLOLHTMLError(global); + } + }; + + sink.rewriter.end() catch { + if (!is_async) response.finalize(); + sink.response = undefined; + sink.deinit(); + + if (is_async) { + response.body.value.toErrorInstance(throwLOLHTMLError(global), global); + return null; + } else { + return throwLOLHTMLError(global); + } + }; + + return null; + } + + pub const Sync = enum { suspended, pending, done }; + + pub fn done(this: *BufferOutputSink) void { + var prev_value = this.response.body.value; + var bytes = this.bytes.toOwnedSliceLeaky(); + this.response.body.value = .{ + .Blob = JSC.WebCore.Blob.init(bytes, this.bytes.allocator, this.global), + }; + prev_value.resolve( + &this.response.body.value, + this.global, + ); + } + + pub fn write(this: *BufferOutputSink, bytes: []const u8) void { + this.bytes.append(bytes) catch unreachable; + } + + pub fn deinit(this: *BufferOutputSink) void { + this.bytes.deinit(); + + this.context.deinit(bun.default_allocator); + } + }; + + // pub const StreamOutputSink = struct { + // global: *JSGlobalObject, + // rewriter: *LOLHTML.HTMLRewriter, + // context: LOLHTMLContext, + // response: *Response, + // input: JSC.WebCore.Blob = undefined, + // pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue { + // var result = bun.default_allocator.create(Response) catch unreachable; + // var sink = bun.default_allocator.create(StreamOutputSink) catch unreachable; + // sink.* = StreamOutputSink{ + // .global = global, + // .rewriter = undefined, + // .context = context, + // .response = result, + // }; + + // for (sink.context.document_handlers.items) |doc| { + // doc.ctx = sink; + // } + // for (sink.context.element_handlers.items) |doc| { + // doc.ctx = sink; + // } + + // sink.rewriter = builder.build( + // .UTF8, + // .{ + // .preallocated_parsing_buffer_size = @maximum(original.body.len(), 1024), + // .max_allowed_memory_usage = std.math.maxInt(u32), + // }, + // false, + // StreamOutputSink, + // sink, + // StreamOutputSink.write, + // StreamOutputSink.done, + // ) catch { + // sink.deinit(); + // bun.default_allocator.destroy(result); + + // return throwLOLHTMLError(global); + // }; + + // result.* = Response{ + // .allocator = bun.default_allocator, + // .body = .{ + // .init = .{ + // .status_code = 200, + // }, + // .value = .{ + // .Locked = .{ + // .global = global, + // .task = sink, + // }, + // }, + // }, + // }; + + // result.body.init.headers = original.body.init.headers; + // result.body.init.method = original.body.init.method; + // result.body.init.status_code = original.body.init.status_code; + + // result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable; + // result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable; + + // var input: JSC.WebCore.Blob = original.body.value.use(); + + // const is_pending = input.needsToReadFile(); + // defer if (!is_pending) input.detach(); + + // if (is_pending) { + // input.doReadFileInternal(*StreamOutputSink, sink, onFinishedLoading, global); + // } else if (sink.runOutputSink(input.sharedView(), false, false)) |error_value| { + // return error_value; + // } + + // // Hold off on cloning until we're actually done. + + // return JSC.JSValue.fromRef( + // Response.makeMaybePooled(sink.global.ref(), sink.response), + // ); + // } + + // pub fn runOutputSink( + // sink: *StreamOutputSink, + // bytes: []const u8, + // is_async: bool, + // free_bytes_on_end: bool, + // ) ?JSValue { + // defer if (free_bytes_on_end) + // bun.default_allocator.free(bun.constStrToU8(bytes)); + + // return null; + // } + + // pub const Sync = enum { suspended, pending, done }; + + // pub fn done(this: *StreamOutputSink) void { + // var prev_value = this.response.body.value; + // var bytes = this.bytes.toOwnedSliceLeaky(); + // this.response.body.value = .{ + // .Blob = JSC.WebCore.Blob.init(bytes, this.bytes.allocator, this.global), + // }; + // prev_value.resolve( + // &this.response.body.value, + // this.global, + // ); + // } + + // pub fn write(this: *StreamOutputSink, bytes: []const u8) void { + // this.bytes.append(bytes) catch unreachable; + // } + + // pub fn deinit(this: *StreamOutputSink) void { + // this.bytes.deinit(); + + // this.context.deinit(bun.default_allocator); + // } + // }; +}; + +const DocumentHandler = struct { + onDocTypeCallback: ?JSValue = null, + onCommentCallback: ?JSValue = null, + onTextCallback: ?JSValue = null, + onEndCallback: ?JSValue = null, + thisObject: JSValue, + global: *JSGlobalObject, + ctx: ?*HTMLRewriter.BufferOutputSink = null, + + pub const onDocType = HandlerCallback( + DocumentHandler, + DocType, + LOLHTML.DocType, + "doctype", + "onDocTypeCallback", + ); + pub const onComment = HandlerCallback( + DocumentHandler, + Comment, + LOLHTML.Comment, + "comment", + "onCommentCallback", + ); + pub const onText = HandlerCallback( + DocumentHandler, + TextChunk, + LOLHTML.TextChunk, + "text_chunk", + "onTextCallback", + ); + pub const onEnd = HandlerCallback( + DocumentHandler, + DocEnd, + LOLHTML.DocEnd, + "doc_end", + "onEndCallback", + ); + + pub fn init(global: *JSGlobalObject, thisObject: JSValue, exception: JSC.C.ExceptionRef) DocumentHandler { + var handler = DocumentHandler{ + .thisObject = thisObject, + .global = global, + }; + + switch (thisObject.jsType()) { + .Object, .ProxyObject, .Cell, .FinalObject => {}, + else => |kind| { + JSC.throwInvalidArguments( + "Expected object but received {s}", + .{std.mem.span(@tagName(kind))}, + global.ref(), + exception, + ); + return undefined; + }, + } + + if (thisObject.get(global, "doctype")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("doctype must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onDocTypeCallback = val; + } + + if (thisObject.get(global, "comments")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("comments must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onCommentCallback = val; + } + + if (thisObject.get(global, "text")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("text must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onTextCallback = val; + } + + if (thisObject.get(global, "end")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("end must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onEndCallback = val; + } + + JSC.C.JSValueProtect(global.ref(), thisObject.asObjectRef()); + return handler; + } + + pub fn deinit(this: *DocumentHandler) void { + if (this.onDocTypeCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onDocTypeCallback = null; + } + + if (this.onCommentCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onCommentCallback = null; + } + + if (this.onTextCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onTextCallback = null; + } + + if (this.onEndCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onEndCallback = null; + } + + JSC.C.JSValueUnprotect(this.global.ref(), this.thisObject.asObjectRef()); + } +}; + +fn HandlerCallback( + comptime HandlerType: type, + comptime ZigType: type, + comptime LOLHTMLType: type, + comptime field_name: string, + comptime callback_name: string, +) (fn (*HandlerType, *LOLHTMLType) bool) { + return struct { + pub fn callback(this: *HandlerType, value: *LOLHTMLType) bool { + if (comptime JSC.is_bindgen) + unreachable; + var zig_element = bun.default_allocator.create(ZigType) catch unreachable; + @field(zig_element, field_name) = value; + // At the end of this scope, the value is no longer valid + var args = [1]JSC.C.JSObjectRef{ + ZigType.Class.make(this.global.ref(), zig_element), + }; + var result = JSC.C.JSObjectCallAsFunctionReturnValue( + this.global.ref(), + @field(this, callback_name).?.asObjectRef(), + if (comptime @hasField(HandlerType, "thisObject")) + @field(this, "thisObject").asObjectRef() + else + null, + 1, + &args, + ); + var promise_: ?*JSC.JSInternalPromise = null; + while (!result.isUndefinedOrNull()) { + if (result.isError() or result.isAggregateError(this.global)) { + @field(zig_element, field_name) = null; + return true; + } + + var promise = promise_ orelse JSC.JSInternalPromise.resolvedPromise(this.global, result); + promise_ = promise; + JavaScript.VirtualMachine.vm.event_loop.waitForPromise(promise); + + switch (promise.status(this.global.vm())) { + JSC.JSPromise.Status.Pending => unreachable, + JSC.JSPromise.Status.Rejected => { + JavaScript.VirtualMachine.vm.defaultErrorHandler(promise.result(this.global.vm()), null); + @field(zig_element, field_name) = null; + return false; + }, + JSC.JSPromise.Status.Fulfilled => { + result = promise.result(this.global.vm()); + break; + }, + } + + break; + } + @field(zig_element, field_name) = null; + return false; + } + }.callback; +} + +const ElementHandler = struct { + onElementCallback: ?JSValue = null, + onCommentCallback: ?JSValue = null, + onTextCallback: ?JSValue = null, + thisObject: JSValue, + global: *JSGlobalObject, + ctx: ?*HTMLRewriter.BufferOutputSink = null, + + pub fn init(global: *JSGlobalObject, thisObject: JSValue, exception: JSC.C.ExceptionRef) ElementHandler { + var handler = ElementHandler{ + .thisObject = thisObject, + .global = global, + }; + + switch (thisObject.jsType()) { + .Object, .ProxyObject, .Cell, .FinalObject => {}, + else => |kind| { + JSC.throwInvalidArguments( + "Expected object but received {s}", + .{std.mem.span(@tagName(kind))}, + global.ref(), + exception, + ); + return undefined; + }, + } + + if (thisObject.get(global, "element")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("element must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onElementCallback = val; + } + + if (thisObject.get(global, "comments")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("comments must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onCommentCallback = val; + } + + if (thisObject.get(global, "text")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("text must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onTextCallback = val; + } + + JSC.C.JSValueProtect(global.ref(), thisObject.asObjectRef()); + return handler; + } + + pub fn deinit(this: *ElementHandler) void { + if (this.onElementCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onElementCallback = null; + } + + if (this.onCommentCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onCommentCallback = null; + } + + if (this.onTextCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onTextCallback = null; + } + + JSC.C.JSValueUnprotect(this.global.ref(), this.thisObject.asObjectRef()); + } + + pub fn onElement(this: *ElementHandler, value: *LOLHTML.Element) bool { + return HandlerCallback( + ElementHandler, + Element, + LOLHTML.Element, + "element", + "onElementCallback", + )(this, value); + } + + pub const onComment = HandlerCallback( + ElementHandler, + Comment, + LOLHTML.Comment, + "comment", + "onCommentCallback", + ); + + pub const onText = HandlerCallback( + ElementHandler, + TextChunk, + LOLHTML.TextChunk, + "text_chunk", + "onTextCallback", + ); +}; + +pub const ContentOptions = struct { + html: bool = false, +}; + +const getterWrap = JSC.getterWrap; +const setterWrap = JSC.setterWrap; +const wrap = JSC.wrapAsync; + +pub fn free_html_writer_string(_: ?*anyopaque, ptr: ?*anyopaque, len: usize) callconv(.C) void { + var str = LOLHTML.HTMLString{ .ptr = bun.cast([*]const u8, ptr.?), .len = len }; + str.deinit(); +} + +fn throwLOLHTMLError(global: *JSGlobalObject) JSValue { + var err = LOLHTML.HTMLString.lastError(); + return ZigString.init(err.slice()).toErrorInstance(global); +} + +fn htmlStringValue(input: LOLHTML.HTMLString, globalObject: *JSGlobalObject) JSValue { + var str = ZigString.init( + input.slice(), + ); + str.detectEncoding(); + + return str.toExternalValueWithCallback( + globalObject, + free_html_writer_string, + ); +} + +pub const TextChunk = struct { + text_chunk: ?*LOLHTML.TextChunk = null, + + pub const Class = NewClass( + TextChunk, + .{ .name = "TextChunk" }, + .{ + .before = .{ + .rfn = wrap(TextChunk, "before"), + }, + .after = .{ + .rfn = wrap(TextChunk, "after"), + }, + + .replace = .{ + .rfn = wrap(TextChunk, "replace"), + }, + + .remove = .{ + .rfn = wrap(TextChunk, "remove"), + }, + .finalize = finalize, + }, + .{ + .removed = .{ + .get = getterWrap(TextChunk, "removed"), + }, + .text = .{ + .get = getterWrap(TextChunk, "getText"), + }, + }, + ); + + fn contentHandler(this: *TextChunk, comptime Callback: (fn (*LOLHTML.TextChunk, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.text_chunk == null) + return JSC.JSValue.jsUndefined(); + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.text_chunk.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + pub fn before( + this: *TextChunk, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.TextChunk.before, thisObject, globalObject, content, contentOptions); + } + + pub fn after( + this: *TextChunk, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.TextChunk.after, thisObject, globalObject, content, contentOptions); + } + + pub fn replace( + this: *TextChunk, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.TextChunk.replace, thisObject, globalObject, content, contentOptions); + } + + pub fn remove(this: *TextChunk, thisObject: js.JSObjectRef) JSValue { + if (this.text_chunk == null) + return JSC.JSValue.jsUndefined(); + this.text_chunk.?.remove(); + return JSValue.fromRef(thisObject); + } + + pub fn getText(this: *TextChunk, global: *JSGlobalObject) JSValue { + if (this.text_chunk == null) + return JSC.JSValue.jsUndefined(); + return ZigString.init(this.text_chunk.?.getContent().slice()).withEncoding().toValue(global); + } + + pub fn removed(this: *TextChunk, _: *JSGlobalObject) JSValue { + return JSC.JSValue.jsBoolean(this.text_chunk.?.isRemoved()); + } + + pub fn finalize(this: *TextChunk) void { + this.text_chunk = null; + bun.default_allocator.destroy(this); + } +}; + +pub const DocType = struct { + doctype: ?*LOLHTML.DocType = null, + + pub fn finalize(this: *DocType) void { + this.doctype = null; + bun.default_allocator.destroy(this); + } + + pub const Class = NewClass( + DocType, + .{ + .name = "DocType", + }, + .{ + .finalize = finalize, + }, + .{ + .name = .{ + .get = getterWrap(DocType, "name"), + }, + .systemId = .{ + .get = getterWrap(DocType, "systemId"), + }, + + .publicId = .{ + .get = getterWrap(DocType, "publicId"), + }, + }, + ); + + /// The doctype name. + pub fn name(this: *DocType, global: *JSGlobalObject) JSValue { + if (this.doctype == null) + return JSC.JSValue.jsUndefined(); + const str = this.doctype.?.getName().slice(); + if (str.len == 0) + return JSValue.jsNull(); + return ZigString.init(str).toValue(global); + } + + pub fn systemId(this: *DocType, global: *JSGlobalObject) JSValue { + if (this.doctype == null) + return JSC.JSValue.jsUndefined(); + + const str = this.doctype.?.getSystemId().slice(); + if (str.len == 0) + return JSValue.jsNull(); + return ZigString.init(str).toValue(global); + } + + pub fn publicId(this: *DocType, global: *JSGlobalObject) JSValue { + if (this.doctype == null) + return JSC.JSValue.jsUndefined(); + + const str = this.doctype.?.getPublicId().slice(); + if (str.len == 0) + return JSValue.jsNull(); + return ZigString.init(str).toValue(global); + } +}; + +pub const DocEnd = struct { + doc_end: ?*LOLHTML.DocEnd, + + pub fn finalize(this: *DocEnd) void { + this.doc_end = null; + bun.default_allocator.destroy(this); + } + + pub const Class = NewClass( + DocEnd, + .{ .name = "DocEnd" }, + .{ + .finalize = finalize, + .append = .{ + .rfn = wrap(DocEnd, "append"), + }, + }, + .{}, + ); + + fn contentHandler(this: *DocEnd, comptime Callback: (fn (*LOLHTML.DocEnd, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.doc_end == null) + return JSValue.jsNull(); + + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.doc_end.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + pub fn append( + this: *DocEnd, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.DocEnd.append, thisObject, globalObject, content, contentOptions); + } +}; + +pub const Comment = struct { + comment: ?*LOLHTML.Comment = null, + + pub fn finalize(this: *Comment) void { + this.comment = null; + bun.default_allocator.destroy(this); + } + + pub const Class = NewClass( + Comment, + .{ .name = "Comment" }, + .{ + .before = .{ + .rfn = wrap(Comment, "before"), + }, + .after = .{ + .rfn = wrap(Comment, "after"), + }, + + .replace = .{ + .rfn = wrap(Comment, "replace"), + }, + + .remove = .{ + .rfn = wrap(Comment, "remove"), + }, + .finalize = finalize, + }, + .{ + .removed = .{ + .get = getterWrap(Comment, "removed"), + }, + .text = .{ + .get = getterWrap(Comment, "getText"), + .set = setterWrap(Comment, "setText"), + }, + }, + ); + + fn contentHandler(this: *Comment, comptime Callback: (fn (*LOLHTML.Comment, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.comment == null) + return JSValue.jsNull(); + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.comment.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + pub fn before( + this: *Comment, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.Comment.before, thisObject, globalObject, content, contentOptions); + } + + pub fn after( + this: *Comment, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.Comment.after, thisObject, globalObject, content, contentOptions); + } + + pub fn replace( + this: *Comment, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.Comment.replace, thisObject, globalObject, content, contentOptions); + } + + pub fn remove(this: *Comment, thisObject: js.JSObjectRef) JSValue { + if (this.comment == null) + return JSValue.jsNull(); + this.comment.?.remove(); + return JSValue.fromRef(thisObject); + } + + pub fn getText(this: *Comment, global: *JSGlobalObject) JSValue { + if (this.comment == null) + return JSValue.jsNull(); + return ZigString.init(this.comment.?.getText().slice()).withEncoding().toValue(global); + } + + pub fn setText( + this: *Comment, + value: JSValue, + exception: JSC.C.ExceptionRef, + global: *JSGlobalObject, + ) void { + if (this.comment == null) + return; + var text = value.toSlice(global, bun.default_allocator); + defer text.deinit(); + this.comment.?.setText(text.slice()) catch { + exception.* = throwLOLHTMLError(global).asObjectRef(); + }; + } + + pub fn removed(this: *Comment, _: *JSGlobalObject) JSValue { + if (this.comment == null) + return JSC.JSValue.jsUndefined(); + return JSC.JSValue.jsBoolean(this.comment.?.isRemoved()); + } +}; + +pub const EndTag = struct { + end_tag: ?*LOLHTML.EndTag, + + pub fn finalize(this: *EndTag) void { + this.end_tag = null; + bun.default_allocator.destroy(this); + } + + pub const Handler = struct { + callback: ?JSC.JSValue, + global: *JSGlobalObject, + + pub const onEndTag = HandlerCallback( + Handler, + EndTag, + LOLHTML.EndTag, + "end_tag", + "callback", + ); + + pub const onEndTagHandler = LOLHTML.DirectiveHandler(LOLHTML.EndTag, Handler, onEndTag); + }; + + pub const Class = NewClass( + EndTag, + .{ .name = "EndTag" }, + .{ + .before = .{ + .rfn = wrap(EndTag, "before"), + }, + .after = .{ + .rfn = wrap(EndTag, "after"), + }, + + .remove = .{ + .rfn = wrap(EndTag, "remove"), + }, + .finalize = finalize, + }, + .{ + .name = .{ + .get = getterWrap(EndTag, "getName"), + .set = setterWrap(EndTag, "setName"), + }, + }, + ); + + fn contentHandler(this: *EndTag, comptime Callback: (fn (*LOLHTML.EndTag, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.end_tag == null) + return JSValue.jsNull(); + + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.end_tag.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + pub fn before( + this: *EndTag, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.EndTag.before, thisObject, globalObject, content, contentOptions); + } + + pub fn after( + this: *EndTag, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.EndTag.after, thisObject, globalObject, content, contentOptions); + } + + pub fn replace( + this: *EndTag, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.EndTag.replace, thisObject, globalObject, content, contentOptions); + } + + pub fn remove(this: *EndTag, thisObject: js.JSObjectRef) JSValue { + if (this.end_tag == null) + return JSC.JSValue.jsUndefined(); + + this.end_tag.?.remove(); + return JSValue.fromRef(thisObject); + } + + pub fn getName(this: *EndTag, global: *JSGlobalObject) JSValue { + if (this.end_tag == null) + return JSC.JSValue.jsUndefined(); + + return ZigString.init(this.end_tag.?.getName().slice()).withEncoding().toValue(global); + } + + pub fn setName( + this: *EndTag, + value: JSValue, + exception: JSC.C.ExceptionRef, + global: *JSGlobalObject, + ) void { + if (this.end_tag == null) + return; + var text = value.toSlice(global, bun.default_allocator); + defer text.deinit(); + this.end_tag.?.setName(text.slice()) catch { + exception.* = throwLOLHTMLError(global).asObjectRef(); + }; + } +}; + +pub const AttributeIterator = struct { + iterator: ?*LOLHTML.Attribute.Iterator = null, + + const attribute_iterator_path: string = "file:///bun-vfs/lolhtml/AttributeIterator.js"; + const attribute_iterator_code: string = + \\"use strict"; + \\ + \\class AttributeIterator { + \\ constructor(internal) { + \\ this.#iterator = internal; + \\ } + \\ + \\ #iterator; + \\ + \\ [Symbol.iterator]() { + \\ return this; + \\ } + \\ + \\ next() { + \\ if (this.#iterator === null) + \\ return {done: true}; + \\ var value = this.#iterator.next(); + \\ if (!value) { + \\ this.#iterator = null; + \\ return {done: true}; + \\ } + \\ return {done: false, value: value}; + \\ } + \\} + \\ + \\return new AttributeIterator(internal1); + ; + threadlocal var attribute_iterator_class: JSC.C.JSObjectRef = undefined; + threadlocal var attribute_iterator_loaded: bool = false; + + pub fn getAttributeIteratorJSClass(global: *JSGlobalObject) JSValue { + if (attribute_iterator_loaded) + return JSC.JSValue.fromRef(attribute_iterator_class); + attribute_iterator_loaded = true; + var exception_ptr: ?[*]JSC.JSValueRef = null; + var name = JSC.C.JSStringCreateStatic("AttributeIteratorGetter", "AttributeIteratorGetter".len); + var param_name = JSC.C.JSStringCreateStatic("internal1", "internal1".len); + var attribute_iterator_class_ = JSC.C.JSObjectMakeFunction( + global.ref(), + name, + 1, + &[_]JSC.C.JSStringRef{param_name}, + JSC.C.JSStringCreateStatic(attribute_iterator_code.ptr, attribute_iterator_code.len), + JSC.C.JSStringCreateStatic(attribute_iterator_path.ptr, attribute_iterator_path.len), + 0, + exception_ptr, + ); + JSC.C.JSValueProtect(global.ref(), attribute_iterator_class_); + attribute_iterator_class = attribute_iterator_class_; + return JSC.JSValue.fromRef(attribute_iterator_class); + } + + pub fn finalize(this: *AttributeIterator) void { + if (this.iterator) |iter| { + iter.deinit(); + this.iterator = null; + } + bun.default_allocator.destroy(this); + } + + pub const Class = NewClass( + AttributeIterator, + .{ .name = "AttributeIterator" }, + .{ + .next = .{ + .rfn = wrap(AttributeIterator, "next"), + }, + .finalize = finalize, + }, + .{}, + ); + + const value_ = ZigString.init("value"); + const done_ = ZigString.init("done"); + pub fn next( + this: *AttributeIterator, + globalObject: *JSGlobalObject, + ) JSValue { + if (this.iterator == null) { + return JSC.JSValue.jsNull(); + } + + var attribute = this.iterator.?.next() orelse { + this.iterator.?.deinit(); + this.iterator = null; + return JSC.JSValue.jsNull(); + }; + + // TODO: don't clone here + const value = attribute.value(); + const name = attribute.name(); + defer name.deinit(); + defer value.deinit(); + + var strs = [2]ZigString{ + ZigString.init(name.slice()), + ZigString.init(value.slice()), + }; + + var valid_strs: []ZigString = strs[0..2]; + + var array = JSC.JSValue.createStringArray( + globalObject, + valid_strs.ptr, + valid_strs.len, + true, + ); + + return array; + } +}; +pub const Element = struct { + element: ?*LOLHTML.Element = null, + + pub const Class = NewClass( + Element, + .{ .name = "Element" }, + .{ + .getAttribute = .{ + .rfn = wrap(Element, "getAttribute"), + }, + .hasAttribute = .{ + .rfn = wrap(Element, "hasAttribute"), + }, + .setAttribute = .{ + .rfn = wrap(Element, "setAttribute"), + }, + .removeAttribute = .{ + .rfn = wrap(Element, "removeAttribute"), + }, + .before = .{ + .rfn = wrap(Element, "before"), + }, + .after = .{ + .rfn = wrap(Element, "after"), + }, + .prepend = .{ + .rfn = wrap(Element, "prepend"), + }, + .append = .{ + .rfn = wrap(Element, "append"), + }, + .replace = .{ + .rfn = wrap(Element, "replace"), + }, + .setInnerContent = .{ + .rfn = wrap(Element, "setInnerContent"), + }, + .remove = .{ + .rfn = wrap(Element, "remove"), + }, + .removeAndKeepContent = .{ + .rfn = wrap(Element, "removeAndKeepContent"), + }, + .onEndTag = .{ + .rfn = wrap(Element, "onEndTag"), + }, + .finalize = finalize, + }, + .{ + .tagName = .{ + .get = getterWrap(Element, "getTagName"), + .set = setterWrap(Element, "setTagName"), + }, + .removed = .{ + .get = getterWrap(Element, "getRemoved"), + }, + .namespaceURI = .{ + .get = getterWrap(Element, "getNamespaceURI"), + }, + .attributes = .{ + .get = getterWrap(Element, "getAttributes"), + }, + }, + ); + + pub fn finalize(this: *Element) void { + this.element = null; + bun.default_allocator.destroy(this); + } + + pub fn onEndTag( + this: *Element, + globalObject: *JSGlobalObject, + function: JSValue, + thisObject: JSC.C.JSObjectRef, + ) JSValue { + if (this.element == null) + return JSValue.jsNull(); + if (function.isUndefinedOrNull() or !function.isCallable(globalObject.vm())) { + return ZigString.init("Expected a function").withEncoding().toValue(globalObject); + } + + var end_tag_handler = bun.default_allocator.create(EndTag.Handler) catch unreachable; + end_tag_handler.* = .{ .global = globalObject, .callback = function }; + + this.element.?.onEndTag(EndTag.Handler.onEndTagHandler, end_tag_handler) catch { + bun.default_allocator.destroy(end_tag_handler); + return throwLOLHTMLError(globalObject); + }; + + JSC.C.JSValueProtect(globalObject.ref(), function.asObjectRef()); + return JSValue.fromRef(thisObject); + } + + // // fn wrap(comptime name: string) + + /// Returns the value for a given attribute name: ZigString on the element, or null if it is not found. + pub fn getAttribute(this: *Element, globalObject: *JSGlobalObject, name: ZigString) JSValue { + if (this.element == null) + return JSValue.jsNull(); + + var slice = name.toSlice(bun.default_allocator); + defer slice.deinit(); + var attr = this.element.?.getAttribute(slice.slice()).slice(); + + if (attr.len == 0) + return JSC.JSValue.jsNull(); + + var str = ZigString.init( + attr, + ); + + return str.toExternalValueWithCallback( + globalObject, + free_html_writer_string, + ); + } + + /// Returns a boolean indicating whether an attribute exists on the element. + pub fn hasAttribute(this: *Element, global: *JSGlobalObject, name: ZigString) JSValue { + if (this.element == null) + return JSValue.jsBoolean(false); + + var slice = name.toSlice(bun.default_allocator); + defer slice.deinit(); + return JSValue.jsBoolean(this.element.?.hasAttribute(slice.slice()) catch return throwLOLHTMLError(global)); + } + + /// Sets an attribute to a provided value, creating the attribute if it does not exist. + pub fn setAttribute(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, name_: ZigString, value_: ZigString) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + var name_slice = name_.toSlice(bun.default_allocator); + defer name_slice.deinit(); + + var value_slice = value_.toSlice(bun.default_allocator); + defer value_slice.deinit(); + this.element.?.setAttribute(name_slice.slice(), value_slice.slice()) catch return throwLOLHTMLError(globalObject); + return JSValue.fromRef(thisObject); + } + + /// Removes the attribute. + pub fn removeAttribute(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, name: ZigString) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + var name_slice = name.toSlice(bun.default_allocator); + defer name_slice.deinit(); + + this.element.?.removeAttribute( + name_slice.slice(), + ) catch return throwLOLHTMLError(globalObject); + return JSValue.fromRef(thisObject); + } + + fn contentHandler(this: *Element, comptime Callback: (fn (*LOLHTML.Element, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.element.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + /// Inserts content before the element. + pub fn before(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.before, + thisObject, + globalObject, + content, + contentOptions, + ); + } + + /// Inserts content right after the element. + pub fn after(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.after, + thisObject, + globalObject, + content, + contentOptions, + ); + } + + /// Inserts content right after the start tag of the element. + pub fn prepend(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.prepend, + thisObject, + globalObject, + content, + contentOptions, + ); + } + + /// Inserts content right before the end tag of the element. + pub fn append(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.append, + thisObject, + globalObject, + content, + contentOptions, + ); + } + + /// Removes the element and inserts content in place of it. + pub fn replace(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.replace, + thisObject, + globalObject, + content, + contentOptions, + ); + } + + /// Replaces content of the element. + pub fn setInnerContent(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.setInnerContent, + thisObject, + globalObject, + content, + contentOptions, + ); + } + + /// Removes the element with all its content. + pub fn remove(this: *Element, thisObject: js.JSObjectRef) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + this.element.?.remove(); + return JSValue.fromRef(thisObject); + } + + /// Removes the start tag and end tag of the element but keeps its inner content intact. + pub fn removeAndKeepContent(this: *Element, thisObject: js.JSObjectRef) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + this.element.?.removeAndKeepContent(); + return JSValue.fromRef(thisObject); + } + + pub fn getTagName(this: *Element, globalObject: *JSGlobalObject) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + return htmlStringValue(this.element.?.tagName(), globalObject); + } + + pub fn setTagName(this: *Element, value: JSValue, exception: JSC.C.ExceptionRef, global: *JSGlobalObject) void { + if (this.element == null) + return; + + var text = value.toSlice(global, bun.default_allocator); + defer text.deinit(); + + this.element.?.setTagName(text.slice()) catch { + exception.* = throwLOLHTMLError(global).asObjectRef(); + }; + } + + pub fn getRemoved(this: *Element, _: *JSGlobalObject) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + return JSC.JSValue.jsBoolean(this.element.?.isRemoved()); + } + + pub fn getNamespaceURI(this: *Element, globalObject: *JSGlobalObject) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + return ZigString.init(std.mem.span(this.element.?.namespaceURI())).toValue(globalObject); + } + + pub fn getAttributes(this: *Element, globalObject: *JSGlobalObject) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + var iter = this.element.?.attributes() orelse return throwLOLHTMLError(globalObject); + var attr_iter = bun.default_allocator.create(AttributeIterator) catch unreachable; + attr_iter.* = .{ .iterator = iter }; + var attr = AttributeIterator.Class.make(globalObject.ref(), attr_iter); + JSC.C.JSValueProtect(globalObject.ref(), attr); + defer JSC.C.JSValueUnprotect(globalObject.ref(), attr); + return JSC.JSValue.fromRef( + JSC.C.JSObjectCallAsFunction( + globalObject.ref(), + AttributeIterator.getAttributeIteratorJSClass(globalObject).asObjectRef(), + null, + 1, + @ptrCast([*]JSC.C.JSObjectRef, &attr), + null, + ), + ); + } +}; diff --git a/src/bun.js/api/libtcc1.a.macos-aarch64 b/src/bun.js/api/libtcc1.a.macos-aarch64 Binary files differnew file mode 100644 index 000000000..60696b611 --- /dev/null +++ b/src/bun.js/api/libtcc1.a.macos-aarch64 diff --git a/src/bun.js/api/libtcc1.c b/src/bun.js/api/libtcc1.c new file mode 100644 index 000000000..38750b825 --- /dev/null +++ b/src/bun.js/api/libtcc1.c @@ -0,0 +1,606 @@ +/* TCC runtime library. + Parts of this code are (c) 2002 Fabrice Bellard + + Copyright (C) 1987, 1988, 1992, 1994, 1995 Free Software Foundation, Inc. + +This file is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 2, or (at your option) any +later version. + +In addition to the permissions in the GNU General Public License, the +Free Software Foundation gives you unlimited permission to link the +compiled version of this file into combinations with other programs, +and to distribute those combinations without any restriction coming +from the use of this file. (The General Public License restrictions +do apply in other respects; for example, they cover modification of +the file, and distribution when not linked into a combine +executable.) + +This file is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; see the file COPYING. If not, write to +the Free Software Foundation, 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +*/ + +#define W_TYPE_SIZE 32 +#define BITS_PER_UNIT 8 + +typedef int Wtype; +typedef unsigned int UWtype; +typedef unsigned int USItype; +typedef long long DWtype; +typedef unsigned long long UDWtype; + +struct DWstruct { + Wtype low, high; +}; + +typedef union +{ + struct DWstruct s; + DWtype ll; +} DWunion; + +typedef long double XFtype; +#define WORD_SIZE (sizeof (Wtype) * BITS_PER_UNIT) +#define HIGH_WORD_COEFF (((UDWtype) 1) << WORD_SIZE) + +/* the following deal with IEEE single-precision numbers */ +#define EXCESS 126 +#define SIGNBIT 0x80000000 +#define HIDDEN (1 << 23) +#define SIGN(fp) ((fp) & SIGNBIT) +#define EXP(fp) (((fp) >> 23) & 0xFF) +#define MANT(fp) (((fp) & 0x7FFFFF) | HIDDEN) +#define PACK(s,e,m) ((s) | ((e) << 23) | (m)) + +/* the following deal with IEEE double-precision numbers */ +#define EXCESSD 1022 +#define HIDDEND (1 << 20) +#define EXPD(fp) (((fp.l.upper) >> 20) & 0x7FF) +#define SIGND(fp) ((fp.l.upper) & SIGNBIT) +#define MANTD(fp) (((((fp.l.upper) & 0xFFFFF) | HIDDEND) << 10) | \ + (fp.l.lower >> 22)) +#define HIDDEND_LL ((long long)1 << 52) +#define MANTD_LL(fp) ((fp.ll & (HIDDEND_LL-1)) | HIDDEND_LL) +#define PACKD_LL(s,e,m) (((long long)((s)+((e)<<20))<<32)|(m)) + +/* the following deal with x86 long double-precision numbers */ +#define EXCESSLD 16382 +#define EXPLD(fp) (fp.l.upper & 0x7fff) +#define SIGNLD(fp) ((fp.l.upper) & 0x8000) + +/* only for x86 */ +union ldouble_long { + long double ld; + struct { + unsigned long long lower; + unsigned short upper; + } l; +}; + +union double_long { + double d; +#if 1 + struct { + unsigned int lower; + int upper; + } l; +#else + struct { + int upper; + unsigned int lower; + } l; +#endif + long long ll; +}; + +union float_long { + float f; + long l; +}; + +/* XXX: we don't support several builtin supports for now */ +#ifndef __x86_64__ + +/* XXX: use gcc/tcc intrinsic ? */ +#if defined(__i386__) +#define sub_ddmmss(sh, sl, ah, al, bh, bl) \ + __asm__ ("subl %5,%1\n\tsbbl %3,%0" \ + : "=r" ((USItype) (sh)), \ + "=&r" ((USItype) (sl)) \ + : "0" ((USItype) (ah)), \ + "g" ((USItype) (bh)), \ + "1" ((USItype) (al)), \ + "g" ((USItype) (bl))) +#define umul_ppmm(w1, w0, u, v) \ + __asm__ ("mull %3" \ + : "=a" ((USItype) (w0)), \ + "=d" ((USItype) (w1)) \ + : "%0" ((USItype) (u)), \ + "rm" ((USItype) (v))) +#define udiv_qrnnd(q, r, n1, n0, dv) \ + __asm__ ("divl %4" \ + : "=a" ((USItype) (q)), \ + "=d" ((USItype) (r)) \ + : "0" ((USItype) (n0)), \ + "1" ((USItype) (n1)), \ + "rm" ((USItype) (dv))) +#define count_leading_zeros(count, x) \ + do { \ + USItype __cbtmp; \ + __asm__ ("bsrl %1,%0" \ + : "=r" (__cbtmp) : "rm" ((USItype) (x))); \ + (count) = __cbtmp ^ 31; \ + } while (0) +#else +#error unsupported CPU type +#endif + +/* most of this code is taken from libgcc2.c from gcc */ + +static UDWtype __udivmoddi4 (UDWtype n, UDWtype d, UDWtype *rp) +{ + DWunion ww; + DWunion nn, dd; + DWunion rr; + UWtype d0, d1, n0, n1, n2; + UWtype q0, q1; + UWtype b, bm; + + nn.ll = n; + dd.ll = d; + + d0 = dd.s.low; + d1 = dd.s.high; + n0 = nn.s.low; + n1 = nn.s.high; + +#if !UDIV_NEEDS_NORMALIZATION + if (d1 == 0) + { + if (d0 > n1) + { + /* 0q = nn / 0D */ + + udiv_qrnnd (q0, n0, n1, n0, d0); + q1 = 0; + + /* Remainder in n0. */ + } + else + { + /* qq = NN / 0d */ + + if (d0 == 0) + d0 = 1 / d0; /* Divide intentionally by zero. */ + + udiv_qrnnd (q1, n1, 0, n1, d0); + udiv_qrnnd (q0, n0, n1, n0, d0); + + /* Remainder in n0. */ + } + + if (rp != 0) + { + rr.s.low = n0; + rr.s.high = 0; + *rp = rr.ll; + } + } + +#else /* UDIV_NEEDS_NORMALIZATION */ + + if (d1 == 0) + { + if (d0 > n1) + { + /* 0q = nn / 0D */ + + count_leading_zeros (bm, d0); + + if (bm != 0) + { + /* Normalize, i.e. make the most significant bit of the + denominator set. */ + + d0 = d0 << bm; + n1 = (n1 << bm) | (n0 >> (W_TYPE_SIZE - bm)); + n0 = n0 << bm; + } + + udiv_qrnnd (q0, n0, n1, n0, d0); + q1 = 0; + + /* Remainder in n0 >> bm. */ + } + else + { + /* qq = NN / 0d */ + + if (d0 == 0) + d0 = 1 / d0; /* Divide intentionally by zero. */ + + count_leading_zeros (bm, d0); + + if (bm == 0) + { + /* From (n1 >= d0) /\ (the most significant bit of d0 is set), + conclude (the most significant bit of n1 is set) /\ (the + leading quotient digit q1 = 1). + + This special case is necessary, not an optimization. + (Shifts counts of W_TYPE_SIZE are undefined.) */ + + n1 -= d0; + q1 = 1; + } + else + { + /* Normalize. */ + + b = W_TYPE_SIZE - bm; + + d0 = d0 << bm; + n2 = n1 >> b; + n1 = (n1 << bm) | (n0 >> b); + n0 = n0 << bm; + + udiv_qrnnd (q1, n1, n2, n1, d0); + } + + /* n1 != d0... */ + + udiv_qrnnd (q0, n0, n1, n0, d0); + + /* Remainder in n0 >> bm. */ + } + + if (rp != 0) + { + rr.s.low = n0 >> bm; + rr.s.high = 0; + *rp = rr.ll; + } + } +#endif /* UDIV_NEEDS_NORMALIZATION */ + + else + { + if (d1 > n1) + { + /* 00 = nn / DD */ + + q0 = 0; + q1 = 0; + + /* Remainder in n1n0. */ + if (rp != 0) + { + rr.s.low = n0; + rr.s.high = n1; + *rp = rr.ll; + } + } + else + { + /* 0q = NN / dd */ + + count_leading_zeros (bm, d1); + if (bm == 0) + { + /* From (n1 >= d1) /\ (the most significant bit of d1 is set), + conclude (the most significant bit of n1 is set) /\ (the + quotient digit q0 = 0 or 1). + + This special case is necessary, not an optimization. */ + + /* The condition on the next line takes advantage of that + n1 >= d1 (true due to program flow). */ + if (n1 > d1 || n0 >= d0) + { + q0 = 1; + sub_ddmmss (n1, n0, n1, n0, d1, d0); + } + else + q0 = 0; + + q1 = 0; + + if (rp != 0) + { + rr.s.low = n0; + rr.s.high = n1; + *rp = rr.ll; + } + } + else + { + UWtype m1, m0; + /* Normalize. */ + + b = W_TYPE_SIZE - bm; + + d1 = (d1 << bm) | (d0 >> b); + d0 = d0 << bm; + n2 = n1 >> b; + n1 = (n1 << bm) | (n0 >> b); + n0 = n0 << bm; + + udiv_qrnnd (q0, n1, n2, n1, d1); + umul_ppmm (m1, m0, q0, d0); + + if (m1 > n1 || (m1 == n1 && m0 > n0)) + { + q0--; + sub_ddmmss (m1, m0, m1, m0, d1, d0); + } + + q1 = 0; + + /* Remainder in (n1n0 - m1m0) >> bm. */ + if (rp != 0) + { + sub_ddmmss (n1, n0, n1, n0, m1, m0); + rr.s.low = (n1 << b) | (n0 >> bm); + rr.s.high = n1 >> bm; + *rp = rr.ll; + } + } + } + } + + ww.s.low = q0; + ww.s.high = q1; + return ww.ll; +} + +#define __negdi2(a) (-(a)) + +long long __divdi3(long long u, long long v) +{ + int c = 0; + DWunion uu, vv; + DWtype w; + + uu.ll = u; + vv.ll = v; + + if (uu.s.high < 0) { + c = ~c; + uu.ll = __negdi2 (uu.ll); + } + if (vv.s.high < 0) { + c = ~c; + vv.ll = __negdi2 (vv.ll); + } + w = __udivmoddi4 (uu.ll, vv.ll, (UDWtype *) 0); + if (c) + w = __negdi2 (w); + return w; +} + +long long __moddi3(long long u, long long v) +{ + int c = 0; + DWunion uu, vv; + DWtype w; + + uu.ll = u; + vv.ll = v; + + if (uu.s.high < 0) { + c = ~c; + uu.ll = __negdi2 (uu.ll); + } + if (vv.s.high < 0) + vv.ll = __negdi2 (vv.ll); + + __udivmoddi4 (uu.ll, vv.ll, (UDWtype *) &w); + if (c) + w = __negdi2 (w); + return w; +} + +unsigned long long __udivdi3(unsigned long long u, unsigned long long v) +{ + return __udivmoddi4 (u, v, (UDWtype *) 0); +} + +unsigned long long __umoddi3(unsigned long long u, unsigned long long v) +{ + UDWtype w; + + __udivmoddi4 (u, v, &w); + return w; +} + +/* XXX: fix tcc's code generator to do this instead */ +long long __ashrdi3(long long a, int b) +{ +#ifdef __TINYC__ + DWunion u; + u.ll = a; + if (b >= 32) { + u.s.low = u.s.high >> (b - 32); + u.s.high = u.s.high >> 31; + } else if (b != 0) { + u.s.low = ((unsigned)u.s.low >> b) | (u.s.high << (32 - b)); + u.s.high = u.s.high >> b; + } + return u.ll; +#else + return a >> b; +#endif +} + +/* XXX: fix tcc's code generator to do this instead */ +unsigned long long __lshrdi3(unsigned long long a, int b) +{ +#ifdef __TINYC__ + DWunion u; + u.ll = a; + if (b >= 32) { + u.s.low = (unsigned)u.s.high >> (b - 32); + u.s.high = 0; + } else if (b != 0) { + u.s.low = ((unsigned)u.s.low >> b) | (u.s.high << (32 - b)); + u.s.high = (unsigned)u.s.high >> b; + } + return u.ll; +#else + return a >> b; +#endif +} + +/* XXX: fix tcc's code generator to do this instead */ +long long __ashldi3(long long a, int b) +{ +#ifdef __TINYC__ + DWunion u; + u.ll = a; + if (b >= 32) { + u.s.high = (unsigned)u.s.low << (b - 32); + u.s.low = 0; + } else if (b != 0) { + u.s.high = ((unsigned)u.s.high << b) | ((unsigned)u.s.low >> (32 - b)); + u.s.low = (unsigned)u.s.low << b; + } + return u.ll; +#else + return a << b; +#endif +} + +#if defined(__i386__) +/* FPU control word for rounding to nearest mode */ +unsigned short __tcc_fpu_control = 0x137f; +/* FPU control word for round to zero mode for int conversion */ +unsigned short __tcc_int_fpu_control = 0x137f | 0x0c00; +#endif + +#endif /* !__x86_64__ */ + +/* XXX: fix tcc's code generator to do this instead */ +float __floatundisf(unsigned long long a) +{ + DWunion uu; + XFtype r; + + uu.ll = a; + if (uu.s.high >= 0) { + return (float)uu.ll; + } else { + r = (XFtype)uu.ll; + r += 18446744073709551616.0; + return (float)r; + } +} + +double __floatundidf(unsigned long long a) +{ + DWunion uu; + XFtype r; + + uu.ll = a; + if (uu.s.high >= 0) { + return (double)uu.ll; + } else { + r = (XFtype)uu.ll; + r += 18446744073709551616.0; + return (double)r; + } +} + +long double __floatundixf(unsigned long long a) +{ + DWunion uu; + XFtype r; + + uu.ll = a; + if (uu.s.high >= 0) { + return (long double)uu.ll; + } else { + r = (XFtype)uu.ll; + r += 18446744073709551616.0; + return (long double)r; + } +} + +unsigned long long __fixunssfdi (float a1) +{ + register union float_long fl1; + register int exp; + register unsigned long l; + + fl1.f = a1; + + if (fl1.l == 0) + return (0); + + exp = EXP (fl1.l) - EXCESS - 24; + + l = MANT(fl1.l); + if (exp >= 41) + return (unsigned long long)-1; + else if (exp >= 0) + return (unsigned long long)l << exp; + else if (exp >= -23) + return l >> -exp; + else + return 0; +} + +unsigned long long __fixunsdfdi (double a1) +{ + register union double_long dl1; + register int exp; + register unsigned long long l; + + dl1.d = a1; + + if (dl1.ll == 0) + return (0); + + exp = EXPD (dl1) - EXCESSD - 53; + + l = MANTD_LL(dl1); + + if (exp >= 12) + return (unsigned long long)-1; + else if (exp >= 0) + return l << exp; + else if (exp >= -52) + return l >> -exp; + else + return 0; +} + +unsigned long long __fixunsxfdi (long double a1) +{ + register union ldouble_long dl1; + register int exp; + register unsigned long long l; + + dl1.ld = a1; + + if (dl1.l.lower == 0 && dl1.l.upper == 0) + return (0); + + exp = EXPLD (dl1) - EXCESSLD - 64; + + l = dl1.l.lower; + + if (exp > 0) + return (unsigned long long)-1; + else if (exp >= -63) + return l >> -exp; + else + return 0; +} diff --git a/src/bun.js/api/router.zig b/src/bun.js/api/router.zig new file mode 100644 index 000000000..847e7a756 --- /dev/null +++ b/src/bun.js/api/router.zig @@ -0,0 +1,541 @@ +const std = @import("std"); +const Api = @import("../../api/schema.zig").Api; +const FilesystemRouter = @import("../../router.zig"); +const http = @import("../../http.zig"); +const JavaScript = @import("../javascript.zig"); +const QueryStringMap = @import("../../url.zig").QueryStringMap; +const CombinedScanner = @import("../../url.zig").CombinedScanner; +const bun = @import("../../global.zig"); +const string = bun.string; +const JSC = @import("../../jsc.zig"); +const js = JSC.C; +const WebCore = @import("../webcore/response.zig"); +const Router = @This(); +const Bundler = @import("../../bundler.zig"); +const VirtualMachine = JavaScript.VirtualMachine; +const ScriptSrcStream = std.io.FixedBufferStream([]u8); +const ZigString = JSC.ZigString; +const Fs = @import("../../fs.zig"); +const Base = @import("../base.zig"); +const getAllocator = Base.getAllocator; +const JSObject = JSC.JSObject; +const JSError = Base.JSError; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const strings = @import("strings"); +const NewClass = Base.NewClass; +const To = Base.To; +const Request = WebCore.Request; +const d = Base.d; +const FetchEvent = WebCore.FetchEvent; +const URLPath = @import("../../http/url_path.zig"); +const URL = @import("../../url.zig").URL; +route: *const FilesystemRouter.Match, +route_holder: FilesystemRouter.Match = undefined, +needs_deinit: bool = false, +query_string_map: ?QueryStringMap = null, +param_map: ?QueryStringMap = null, +params_list_holder: FilesystemRouter.Param.List = .{}, + +pub fn importRoute( + this: *Router, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + _: []const js.JSValueRef, + _: js.ExceptionRef, +) js.JSObjectRef { + const prom = JSC.JSModuleLoader.loadAndEvaluateModule(ctx.ptr(), &ZigString.init(this.route.file_path)); + + VirtualMachine.vm.tick(); + + return prom.result(ctx.ptr().vm()).asRef(); +} + +pub fn match( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSObjectRef { + if (arguments.len == 0) { + JSError(getAllocator(ctx), "Expected string, FetchEvent, or Request but there were no arguments", .{}, ctx, exception); + return null; + } + + const arg: JSC.JSValue = brk: { + if (FetchEvent.Class.isLoaded()) { + if (JSValue.as(JSValue.fromRef(arguments[0]), FetchEvent)) |fetch_event| { + if (fetch_event.request_context != null) { + return matchFetchEvent(ctx, fetch_event, exception); + } + + // When disconencted, we still have a copy of the request data in here + break :brk JSC.JSValue.fromRef(fetch_event.getRequest(ctx, null, null, null)); + } + } + break :brk JSC.JSValue.fromRef(arguments[0]); + }; + + var router = JavaScript.VirtualMachine.vm.bundler.router orelse { + JSError(getAllocator(ctx), "Bun.match needs a framework configured with routes", .{}, ctx, exception); + return null; + }; + + var path_: ?ZigString.Slice = null; + var pathname: string = ""; + defer { + if (path_) |path| { + path.deinit(); + } + } + + if (arg.isString()) { + var path_string = arg.getZigString(ctx.ptr()); + path_ = path_string.toSlice(bun.default_allocator); + var url = URL.parse(path_.?.slice()); + pathname = url.pathname; + } else if (arg.as(Request)) |req| { + var path_string = req.url; + path_ = path_string.toSlice(bun.default_allocator); + var url = URL.parse(path_.?.slice()); + pathname = url.pathname; + } + + if (path_ == null) { + JSError(getAllocator(ctx), "Expected string, FetchEvent, or Request", .{}, ctx, exception); + return null; + } + + const url_path = URLPath.parse(path_.?.slice()) catch { + JSError(getAllocator(ctx), "Could not parse URL path", .{}, ctx, exception); + return null; + }; + + var match_params_fallback = std.heap.stackFallback(1024, bun.default_allocator); + var match_params_allocator = match_params_fallback.get(); + var match_params = FilesystemRouter.Param.List{}; + match_params.ensureTotalCapacity(match_params_allocator, 16) catch unreachable; + var prev_allocator = router.routes.allocator; + router.routes.allocator = match_params_allocator; + defer router.routes.allocator = prev_allocator; + if (router.routes.matchPage("", url_path, &match_params)) |matched| { + var match_ = matched; + var params_list = match_.params.clone(bun.default_allocator) catch unreachable; + var instance = getAllocator(ctx).create(Router) catch unreachable; + + instance.* = Router{ + .route_holder = match_, + .route = undefined, + }; + instance.params_list_holder = params_list; + instance.route = &instance.route_holder; + instance.route_holder.params = &instance.params_list_holder; + + return Instance.make(ctx, instance); + } + // router.routes.matchPage + + return JSC.JSValue.jsNull().asObjectRef(); +} + +fn matchRequest( + ctx: js.JSContextRef, + request: *const Request, + _: js.ExceptionRef, +) js.JSObjectRef { + return createRouteObject(ctx, request.request_context); +} + +fn matchFetchEvent( + ctx: js.JSContextRef, + fetch_event: *const FetchEvent, + _: js.ExceptionRef, +) js.JSObjectRef { + return createRouteObject(ctx, fetch_event.request_context.?); +} + +fn createRouteObject(ctx: js.JSContextRef, req: *const http.RequestContext) js.JSValueRef { + const route = &(req.matched_route orelse { + return js.JSValueMakeNull(ctx); + }); + + return createRouteObjectFromMatch(ctx, route); +} + +fn createRouteObjectFromMatch( + ctx: js.JSContextRef, + route: *const FilesystemRouter.Match, +) js.JSValueRef { + var router = getAllocator(ctx).create(Router) catch unreachable; + router.* = Router{ + .route = route, + }; + + return Instance.make(ctx, router); +} + +pub const match_type_definition = &[_]d.ts{ + .{ + .tsdoc = "Match a {@link https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent FetchEvent} to a `Route` from the local filesystem. Returns `null` if there is no match.", + .args = &[_]d.ts.arg{ + .{ + .name = "event", + .@"return" = "FetchEvent", + }, + }, + .@"return" = "Route | null", + }, + .{ + .tsdoc = "Match a `pathname` to a `Route` from the local filesystem. Returns `null` if there is no match.", + .args = &[_]d.ts.arg{ + .{ + .name = "pathname", + .@"return" = "string", + }, + }, + .@"return" = "Route | null", + }, + .{ + .tsdoc = "Match a {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Request} to a `Route` from the local filesystem. Returns `null` if there is no match.", + .args = &[_]d.ts.arg{ + .{ + .name = "request", + .@"return" = "Request", + }, + }, + .@"return" = "Route | null", + }, +}; + +pub const Instance = NewClass( + Router, + .{ + .name = "Route", + .read_only = true, + .ts = .{ + .class = d.ts.class{ + .tsdoc = + \\Route matched from the filesystem. + , + }, + }, + }, + .{ + .finalize = finalize, + .import = .{ + .rfn = importRoute, + .ts = d.ts{ + .@"return" = "Object", + .tsdoc = + \\Synchronously load & evaluate the file corresponding to the route. Returns the exports of the route. This is similar to `await import(route.filepath)`, except it's synchronous. It is recommended to use this function instead of `import`. + , + }, + }, + }, + .{ + .pathname = .{ + .get = getPathname, + .ro = true, + .ts = d.ts{ + .@"return" = "string", + .@"tsdoc" = "URL path as appears in a web browser's address bar", + }, + }, + + .filePath = .{ + .get = getFilePath, + .ro = true, + .ts = d.ts{ + .@"return" = "string", + .tsdoc = + \\Project-relative filesystem path to the route file. + , + }, + }, + .scriptSrc = .{ + .get = getScriptSrc, + .ro = true, + .ts = d.ts{ + .@"return" = "string", + .tsdoc = + \\src attribute of the script tag that loads the route. + , + }, + }, + .kind = .{ + .get = getKind, + .ro = true, + .ts = d.ts{ + .@"return" = "\"exact\" | \"dynamic\" | \"catch-all\" | \"optional-catch-all\"", + }, + }, + .name = .{ + .get = getRoute, + .ro = true, + .ts = d.ts{ + .@"return" = "string", + .tsdoc = + \\Route name + \\@example + \\`"blog/posts/[id]"` + \\`"blog/posts/[id]/[[...slug]]"` + \\`"blog"` + , + }, + }, + .query = .{ + .get = getQuery, + .ro = true, + .ts = d.ts{ + .@"return" = "Record<string, string | string[]>", + .tsdoc = + \\Route parameters & parsed query string values as a key-value object + \\ + \\@example + \\```js + \\console.assert(router.query.id === "123"); + \\console.assert(router.pathname === "/blog/posts/123"); + \\console.assert(router.route === "blog/posts/[id]"); + \\``` + , + }, + }, + .params = .{ + .get = getParams, + .ro = true, + .ts = d.ts{ + .@"return" = "Record<string, string | string[]>", + .tsdoc = + \\Route parameters as a key-value object + \\ + \\@example + \\```js + \\console.assert(router.query.id === "123"); + \\console.assert(router.pathname === "/blog/posts/123"); + \\console.assert(router.route === "blog/posts/[id]"); + \\``` + , + }, + }, + }, +); + +pub fn getFilePath( + this: *Router, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return ZigString.init(this.route.file_path) + .withEncoding() + .toValueGC(ctx.ptr()).asRef(); +} + +pub fn finalize( + this: *Router, +) void { + if (this.query_string_map) |*map| { + map.deinit(); + } + + if (this.needs_deinit) { + this.params_list_holder.deinit(bun.default_allocator); + this.params_list_holder = .{}; + this.needs_deinit = false; + } + + bun.default_allocator.destroy(this); +} + +pub fn getPathname( + this: *Router, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return ZigString.init(this.route.pathname) + .withEncoding() + .toValueGC(ctx.ptr()).asRef(); +} + +pub fn getRoute( + this: *Router, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return ZigString.init(this.route.name) + .withEncoding() + .toValueGC(ctx.ptr()).asRef(); +} + +const KindEnum = struct { + pub const exact = "exact"; + pub const catch_all = "catch-all"; + pub const optional_catch_all = "optional-catch-all"; + pub const dynamic = "dynamic"; + + // this is kinda stupid it should maybe just store it + pub fn init(name: string) ZigString { + if (strings.contains(name, "[[...")) { + return ZigString.init(optional_catch_all); + } else if (strings.contains(name, "[...")) { + return ZigString.init(catch_all); + } else if (strings.contains(name, "[")) { + return ZigString.init(dynamic); + } else { + return ZigString.init(exact); + } + } +}; + +pub fn getKind( + this: *Router, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + return KindEnum.init(this.route.name).toValue(ctx.ptr()).asRef(); +} + +threadlocal var query_string_values_buf: [256]string = undefined; +threadlocal var query_string_value_refs_buf: [256]ZigString = undefined; +pub fn createQueryObject(ctx: js.JSContextRef, map: *QueryStringMap, _: js.ExceptionRef) callconv(.C) js.JSValueRef { + const QueryObjectCreator = struct { + query: *QueryStringMap, + pub fn create(this: *@This(), obj: *JSObject, global: *JSGlobalObject) void { + var iter = this.query.iter(); + var str: ZigString = undefined; + while (iter.next(&query_string_values_buf)) |entry| { + str = ZigString.init(entry.name); + + std.debug.assert(entry.values.len > 0); + if (entry.values.len > 1) { + var values = query_string_value_refs_buf[0..entry.values.len]; + for (entry.values) |value, i| { + values[i] = ZigString.init(value); + } + obj.putRecord(global, &str, values.ptr, values.len); + } else { + query_string_value_refs_buf[0] = ZigString.init(entry.values[0]); + + obj.putRecord(global, &str, &query_string_value_refs_buf, 1); + } + } + } + }; + + var creator = QueryObjectCreator{ .query = map }; + + var value = JSObject.createWithInitializer(QueryObjectCreator, &creator, ctx.ptr(), map.getNameCount()); + + return value.asRef(); +} + +pub fn getScriptSrcString( + comptime Writer: type, + writer: Writer, + file_path: string, + client_framework_enabled: bool, +) void { + var entry_point_tempbuf: [bun.MAX_PATH_BYTES]u8 = undefined; + // We don't store the framework config including the client parts in the server + // instead, we just store a boolean saying whether we should generate this whenever the script is requested + // this is kind of bad. we should consider instead a way to inline the contents of the script. + if (client_framework_enabled) { + JSC.API.Bun.getPublicPath( + Bundler.ClientEntryPoint.generateEntryPointPath( + &entry_point_tempbuf, + Fs.PathName.init(file_path), + ), + VirtualMachine.vm.origin, + Writer, + writer, + ); + } else { + JSC.API.Bun.getPublicPath(file_path, VirtualMachine.vm.origin, Writer, writer); + } +} + +pub fn getScriptSrc( + this: *Router, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef { + var script_src_buffer = std.ArrayList(u8).init(bun.default_allocator); + + var writer = script_src_buffer.writer(); + getScriptSrcString(@TypeOf(&writer), &writer, this.route.file_path, this.route.client_framework_enabled); + + return ZigString.init(script_src_buffer.toOwnedSlice()).toExternalValue(ctx.ptr()).asObjectRef(); +} + +pub fn getParams( + this: *Router, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSStringRef, + exception: js.ExceptionRef, +) js.JSValueRef { + if (this.param_map == null) { + if (this.route.params.len > 0) { + if (QueryStringMap.initWithScanner(getAllocator(ctx), CombinedScanner.init( + "", + this.route.pathnameWithoutLeadingSlash(), + this.route.name, + this.route.params, + ))) |map| { + this.param_map = map; + } else |_| {} + } + } + + // If it's still null, there are no params + if (this.param_map) |*map| { + return createQueryObject(ctx, map, exception); + } else { + return JSValue.createEmptyObject(ctx.ptr(), 0).asRef(); + } +} + +pub fn getQuery( + this: *Router, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSStringRef, + exception: js.ExceptionRef, +) js.JSValueRef { + if (this.query_string_map == null) { + if (this.route.params.len > 0) { + if (QueryStringMap.initWithScanner(getAllocator(ctx), CombinedScanner.init( + this.route.query_string, + this.route.pathnameWithoutLeadingSlash(), + this.route.name, + + this.route.params, + ))) |map| { + this.query_string_map = map; + } else |_| {} + } else if (this.route.query_string.len > 0) { + if (QueryStringMap.init(getAllocator(ctx), this.route.query_string)) |map| { + this.query_string_map = map; + } else |_| {} + } + } + + // If it's still null, the query string has no names. + if (this.query_string_map) |*map| { + return createQueryObject(ctx, map, exception); + } else { + return JSValue.createEmptyObject(ctx.ptr(), 0).asRef(); + } +} diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig new file mode 100644 index 000000000..cb3c4387a --- /dev/null +++ b/src/bun.js/api/server.zig @@ -0,0 +1,1844 @@ +const Bun = @This(); +const default_allocator = @import("../../global.zig").default_allocator; +const bun = @import("../../global.zig"); +const Environment = bun.Environment; +const NetworkThread = @import("http").NetworkThread; +const Global = bun.Global; +const strings = bun.strings; +const string = bun.string; +const Output = @import("../../global.zig").Output; +const MutableString = @import("../../global.zig").MutableString; +const std = @import("std"); +const Allocator = std.mem.Allocator; +const IdentityContext = @import("../../identity_context.zig").IdentityContext; +const Fs = @import("../../fs.zig"); +const Resolver = @import("../../resolver/resolver.zig"); +const ast = @import("../../import_record.zig"); +const NodeModuleBundle = @import("../../node_module_bundle.zig").NodeModuleBundle; +const MacroEntryPoint = @import("../../bundler.zig").MacroEntryPoint; +const logger = @import("../../logger.zig"); +const Api = @import("../../api/schema.zig").Api; +const options = @import("../../options.zig"); +const Bundler = @import("../../bundler.zig").Bundler; +const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint; +const js_printer = @import("../../js_printer.zig"); +const js_parser = @import("../../js_parser.zig"); +const js_ast = @import("../../js_ast.zig"); +const hash_map = @import("../../hash_map.zig"); +const http = @import("../../http.zig"); +const NodeFallbackModules = @import("../../node_fallbacks.zig"); +const ImportKind = ast.ImportKind; +const Analytics = @import("../../analytics/analytics_thread.zig"); +const ZigString = @import("../../jsc.zig").ZigString; +const Runtime = @import("../../runtime.zig"); +const Router = @import("./router.zig"); +const ImportRecord = ast.ImportRecord; +const DotEnv = @import("../../env_loader.zig"); +const ParseResult = @import("../../bundler.zig").ParseResult; +const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON; +const MacroRemap = @import("../../resolver/package_json.zig").MacroMap; +const WebCore = @import("../../jsc.zig").WebCore; +const Request = WebCore.Request; +const Response = WebCore.Response; +const Headers = WebCore.Headers; +const Fetch = WebCore.Fetch; +const HTTP = @import("http"); +const FetchEvent = WebCore.FetchEvent; +const js = @import("../../jsc.zig").C; +const JSC = @import("../../jsc.zig"); +const JSError = @import("../base.zig").JSError; +const MarkedArrayBuffer = @import("../base.zig").MarkedArrayBuffer; +const getAllocator = @import("../base.zig").getAllocator; +const JSValue = @import("../../jsc.zig").JSValue; +const NewClass = @import("../base.zig").NewClass; +const Microtask = @import("../../jsc.zig").Microtask; +const JSGlobalObject = @import("../../jsc.zig").JSGlobalObject; +const ExceptionValueRef = @import("../../jsc.zig").ExceptionValueRef; +const JSPrivateDataPtr = @import("../../jsc.zig").JSPrivateDataPtr; +const ZigConsoleClient = @import("../../jsc.zig").ZigConsoleClient; +const Node = @import("../../jsc.zig").Node; +const ZigException = @import("../../jsc.zig").ZigException; +const ZigStackTrace = @import("../../jsc.zig").ZigStackTrace; +const ErrorableResolvedSource = @import("../../jsc.zig").ErrorableResolvedSource; +const ResolvedSource = @import("../../jsc.zig").ResolvedSource; +const JSPromise = @import("../../jsc.zig").JSPromise; +const JSInternalPromise = @import("../../jsc.zig").JSInternalPromise; +const JSModuleLoader = @import("../../jsc.zig").JSModuleLoader; +const JSPromiseRejectionOperation = @import("../../jsc.zig").JSPromiseRejectionOperation; +const Exception = @import("../../jsc.zig").Exception; +const ErrorableZigString = @import("../../jsc.zig").ErrorableZigString; +const ZigGlobalObject = @import("../../jsc.zig").ZigGlobalObject; +const VM = @import("../../jsc.zig").VM; +const JSFunction = @import("../../jsc.zig").JSFunction; +const Config = @import("../config.zig"); +const URL = @import("../../url.zig").URL; +const Transpiler = @import("./transpiler.zig"); +const VirtualMachine = @import("../javascript.zig").VirtualMachine; +const IOTask = JSC.IOTask; +const is_bindgen = JSC.is_bindgen; +const uws = @import("uws"); +const Fallback = Runtime.Fallback; +const MimeType = HTTP.MimeType; +const Blob = JSC.WebCore.Blob; +const BoringSSL = @import("boringssl"); +const Arena = @import("../../mimalloc_arena.zig").Arena; +const SendfileContext = struct { + fd: i32, + socket_fd: i32 = 0, + remain: Blob.SizeType = 0, + offset: Blob.SizeType = 0, + has_listener: bool = false, + has_set_on_writable: bool = false, + auto_close: bool = false, +}; +const DateTime = @import("datetime"); +const linux = std.os.linux; + +pub const ServerConfig = struct { + port: u16 = 0, + hostname: [*:0]const u8 = "0.0.0.0", + + // TODO: use webkit URL parser instead of bun's + base_url: URL = URL{}, + base_uri: string = "", + + ssl_config: ?SSLConfig = null, + max_request_body_size: usize = 1024 * 1024 * 128, + development: bool = false, + + onError: JSC.JSValue = JSC.JSValue.zero, + onRequest: JSC.JSValue = JSC.JSValue.zero, + + pub const SSLConfig = struct { + server_name: [*c]const u8 = null, + + key_file_name: [*c]const u8 = null, + cert_file_name: [*c]const u8 = null, + + ca_file_name: [*c]const u8 = null, + dh_params_file_name: [*c]const u8 = null, + + passphrase: [*c]const u8 = null, + low_memory_mode: bool = false, + + pub fn deinit(this: *SSLConfig) void { + const fields = .{ + "server_name", + "key_file_name", + "cert_file_name", + "ca_file_name", + "dh_params_file_name", + "passphrase", + }; + + inline for (fields) |field| { + const slice = std.mem.span(@field(this, field)); + if (slice.len > 0) { + bun.default_allocator.free(slice); + } + } + } + + const zero = SSLConfig{}; + + pub fn inJS(global: *JSC.JSGlobalObject, obj: JSC.JSValue, exception: JSC.C.ExceptionRef) ?SSLConfig { + var result = zero; + var any = false; + + // Required + if (obj.getTruthy(global, "keyFile")) |key_file_name| { + var sliced = key_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.key_file_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + if (std.os.system.access(result.key_file_name, std.os.F_OK) != 0) { + JSC.throwInvalidArguments("Unable to access keyFile path", .{}, global.ref(), exception); + result.deinit(); + + return null; + } + any = true; + } + } + if (obj.getTruthy(global, "certFile")) |cert_file_name| { + var sliced = cert_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.cert_file_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + if (std.os.system.access(result.cert_file_name, std.os.F_OK) != 0) { + JSC.throwInvalidArguments("Unable to access certFile path", .{}, global.ref(), exception); + result.deinit(); + return null; + } + any = true; + } + } + + // Optional + if (any) { + if (obj.getTruthy(global, "serverName")) |key_file_name| { + var sliced = key_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.server_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + } + } + + if (obj.getTruthy(global, "caFile")) |ca_file_name| { + var sliced = ca_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.ca_file_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + if (std.os.system.access(result.ca_file_name, std.os.F_OK) != 0) { + JSC.throwInvalidArguments("Invalid caFile path", .{}, global.ref(), exception); + result.deinit(); + return null; + } + } + } + if (obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| { + var sliced = dh_params_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.dh_params_file_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + if (std.os.system.access(result.dh_params_file_name, std.os.F_OK) != 0) { + JSC.throwInvalidArguments("Invalid dhParamsFile path", .{}, global.ref(), exception); + result.deinit(); + return null; + } + } + } + + if (obj.getTruthy(global, "passphrase")) |passphrase| { + var sliced = passphrase.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.passphrase = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + } + } + + if (obj.get(global, "lowMemoryMode")) |low_memory_mode| { + result.low_memory_mode = low_memory_mode.toBoolean(); + any = true; + } + } + + if (!any) + return null; + return result; + } + + pub fn fromJS(global: *JSC.JSGlobalObject, arguments: *JSC.Node.ArgumentsSlice, exception: JSC.C.ExceptionRef) ?SSLConfig { + if (arguments.next()) |arg| { + return SSLConfig.inJS(global, arg, exception); + } + + return null; + } + }; + + pub fn fromJS(global: *JSC.JSGlobalObject, arguments: *JSC.Node.ArgumentsSlice, exception: JSC.C.ExceptionRef) ServerConfig { + var env = arguments.vm.bundler.env; + + var args = ServerConfig{ + .port = 3000, + .hostname = "0.0.0.0", + .development = true, + }; + var has_hostname = false; + if (strings.eqlComptime(env.get("NODE_ENV") orelse "", "production")) { + args.development = false; + } + + if (arguments.vm.bundler.options.production) { + args.development = false; + } + + const PORT_ENV = .{ "PORT", "BUN_PORT", "NODE_PORT" }; + + inline for (PORT_ENV) |PORT| { + if (env.get(PORT)) |port| { + if (std.fmt.parseInt(u16, port, 10)) |_port| { + args.port = _port; + } else |_| {} + } + } + + if (arguments.vm.bundler.options.transform_options.port) |port| { + args.port = port; + } + + if (arguments.vm.bundler.options.transform_options.origin) |origin| { + args.base_uri = origin; + } + + if (arguments.next()) |arg| { + if (arg.isUndefinedOrNull() or !arg.isObject()) { + JSC.throwInvalidArguments("Bun.serve expects an object", .{}, global.ref(), exception); + return args; + } + + if (arg.getTruthy(global, "port")) |port_| { + args.port = @intCast(u16, @minimum(@maximum(0, port_.toInt32()), std.math.maxInt(u16))); + } + + if (arg.getTruthy(global, "baseURI")) |baseURI| { + var sliced = baseURI.toSlice(global, bun.default_allocator); + + if (sliced.len > 0) { + defer sliced.deinit(); + args.base_uri = bun.default_allocator.dupe(u8, sliced.slice()) catch unreachable; + } + } + + if (arg.getTruthy(global, "hostname") orelse arg.getTruthy(global, "host")) |host| { + const host_str = host.toSlice( + global, + bun.default_allocator, + ); + if (host_str.len > 0) { + args.hostname = bun.default_allocator.dupeZ(u8, host_str.slice()) catch unreachable; + has_hostname = true; + } + } + + if (arg.get(global, "development")) |dev| { + args.development = dev.toBoolean(); + } + + if (SSLConfig.fromJS(global, arguments, exception)) |ssl_config| { + args.ssl_config = ssl_config; + } + + if (exception.* != null) { + return args; + } + + if (arg.getTruthy(global, "maxRequestBodySize")) |max_request_body_size| { + args.max_request_body_size = @intCast(u64, @maximum(0, max_request_body_size.toInt64())); + } + + if (arg.getTruthy(global, "error")) |onError| { + if (!onError.isCallable(global.vm())) { + JSC.throwInvalidArguments("Expected error to be a function", .{}, global.ref(), exception); + if (args.ssl_config) |*conf| { + conf.deinit(); + } + return args; + } + JSC.C.JSValueProtect(global.ref(), onError.asObjectRef()); + args.onError = onError; + } + + if (arg.getTruthy(global, "fetch")) |onRequest| { + if (!onRequest.isCallable(global.vm())) { + JSC.throwInvalidArguments("Expected fetch() to be a function", .{}, global.ref(), exception); + return args; + } + JSC.C.JSValueProtect(global.ref(), onRequest.asObjectRef()); + args.onRequest = onRequest; + } else { + JSC.throwInvalidArguments("Expected fetch() to be a function", .{}, global.ref(), exception); + if (args.ssl_config) |*conf| { + conf.deinit(); + } + return args; + } + } + + if (args.port == 0) { + JSC.throwInvalidArguments("Invalid port: must be > 0", .{}, global.ref(), exception); + } + + if (args.base_uri.len > 0) { + args.base_url = URL.parse(args.base_uri); + if (args.base_url.hostname.len == 0) { + JSC.throwInvalidArguments("baseURI must have a hostname", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + if (!strings.isAllASCII(args.base_uri)) { + JSC.throwInvalidArguments("Unicode baseURI must already be encoded for now.\nnew URL(baseuRI).toString() should do the trick.", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + if (args.base_url.protocol.len == 0) { + const protocol: string = if (args.ssl_config != null) "https" else "http"; + + args.base_uri = (if ((args.port == 80 and args.ssl_config == null) or (args.port == 443 and args.ssl_config != null)) + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}/{s}", .{ + protocol, + args.base_url.hostname, + strings.trimLeadingChar(args.base_url.pathname, '/'), + }) + else + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}:{d}/{s}", .{ + protocol, + args.base_url.hostname, + args.port, + strings.trimLeadingChar(args.base_url.pathname, '/'), + })) catch unreachable; + + args.base_url = URL.parse(args.base_uri); + } + } else { + const hostname: string = + if (has_hostname and std.mem.span(args.hostname).len > 0) std.mem.span(args.hostname) else "localhost"; + const protocol: string = if (args.ssl_config != null) "https" else "http"; + + args.base_uri = (if ((args.port == 80 and args.ssl_config == null) or (args.port == 443 and args.ssl_config != null)) + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}/", .{ + protocol, + hostname, + }) + else + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}:{d}/", .{ protocol, hostname, args.port })) catch unreachable; + + if (!strings.isAllASCII(hostname)) { + JSC.throwInvalidArguments("Unicode hostnames must already be encoded for now.\nnew URL(input).hostname should do the trick.", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + args.base_url = URL.parse(args.base_uri); + } + + // I don't think there's a case where this can happen + // but let's check anyway, just in case + if (args.base_url.hostname.len == 0) { + JSC.throwInvalidArguments("baseURI must have a hostname", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + if (args.base_url.username.len > 0 or args.base_url.password.len > 0) { + JSC.throwInvalidArguments("baseURI can't have a username or password", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + return args; + } +}; + +pub fn NewRequestContextStackAllocator(comptime RequestContext: type, comptime count: usize) type { + // Pre-allocate up to 2048 requests + // use a bitset to track which ones are used + return struct { + buf: [count]RequestContext = undefined, + unused: Set = undefined, + fallback_allocator: std.mem.Allocator = undefined, + + pub const Set = std.bit_set.ArrayBitSet(usize, count); + + pub fn get(this: *@This()) std.mem.Allocator { + this.unused = Set.initFull(); + return std.mem.Allocator.init(this, alloc, resize, free); + } + + fn alloc(self: *@This(), a: usize, b: u29, c: u29, d: usize) ![]u8 { + if (self.unused.findFirstSet()) |i| { + self.unused.unset(i); + return std.mem.asBytes(&self.buf[i]); + } + + return try self.fallback_allocator.rawAlloc(a, b, c, d); + } + + fn resize( + _: *@This(), + _: []u8, + _: u29, + _: usize, + _: u29, + _: usize, + ) ?usize { + unreachable; + } + + fn sliceContainsSlice(container: []u8, slice: []u8) bool { + return @ptrToInt(slice.ptr) >= @ptrToInt(container.ptr) and + (@ptrToInt(slice.ptr) + slice.len) <= (@ptrToInt(container.ptr) + container.len); + } + + fn free( + self: *@This(), + buf: []u8, + buf_align: u29, + return_address: usize, + ) void { + _ = buf_align; + _ = return_address; + const bytes = std.mem.asBytes(&self.buf); + if (sliceContainsSlice(bytes, buf)) { + const index = if (bytes[0..buf.len].ptr != buf.ptr) + (@ptrToInt(buf.ptr) - @ptrToInt(bytes)) / @sizeOf(RequestContext) + else + @as(usize, 0); + + if (comptime Environment.allow_assert) { + std.debug.assert(@intToPtr(*RequestContext, @ptrToInt(buf.ptr)) == &self.buf[index]); + std.debug.assert(!self.unused.isSet(index)); + } + + self.unused.set(index); + } else { + self.fallback_allocator.rawFree(buf, buf_align, return_address); + } + } + }; +} + +// This is defined separately partially to work-around an LLVM debugger bug. +fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comptime ThisServer: type) type { + return struct { + const RequestContext = @This(); + const App = uws.NewApp(ssl_enabled); + pub threadlocal var pool: ?*RequestContext.RequestContextStackAllocator = null; + pub threadlocal var pool_allocator: std.mem.Allocator = undefined; + + pub const RequestContextStackAllocator = NewRequestContextStackAllocator(RequestContext, 2048); + pub const name = "HTTPRequestContext" ++ (if (debug_mode) "Debug" else "") ++ (if (ThisServer.ssl_enabled) "TLS" else ""); + pub const shim = JSC.Shimmer("Bun", name, @This()); + + server: *ThisServer, + resp: *App.Response, + /// thread-local default heap allocator + /// this prevents an extra pthread_getspecific() call which shows up in profiling + allocator: std.mem.Allocator, + req: *uws.Request, + url: string, + method: HTTP.Method, + aborted: bool = false, + finalized: bun.DebugOnly(bool) = bun.DebugOnlyDefault(false), + + /// We can only safely free once the request body promise is finalized + /// and the response is rejected + pending_promises_for_abort: u8 = 0, + + has_marked_complete: bool = false, + response_jsvalue: JSC.JSValue = JSC.JSValue.zero, + response_ptr: ?*JSC.WebCore.Response = null, + blob: JSC.WebCore.Blob = JSC.WebCore.Blob{}, + promise: ?*JSC.JSValue = null, + response_headers: ?*JSC.FetchHeaders = null, + has_abort_handler: bool = false, + has_sendfile_ctx: bool = false, + has_called_error_handler: bool = false, + needs_content_length: bool = false, + sendfile: SendfileContext = undefined, + request_js_object: JSC.C.JSObjectRef = null, + request_body_buf: std.ArrayListUnmanaged(u8) = .{}, + + /// Used either for temporary blob data or fallback + /// When the response body is a temporary value + response_buf_owned: std.ArrayListUnmanaged(u8) = .{}, + + // TODO: support builtin compression + const can_sendfile = !ssl_enabled; + + pub const thenables = shim.thenables(.{ + PromiseHandler, + }); + + pub const lazy_static_functions = thenables; + pub const Export = lazy_static_functions; + + const PromiseHandler = JSC.Thenable(RequestContext, onResolve, onReject); + + pub fn setAbortHandler(this: *RequestContext) void { + if (this.has_abort_handler) return; + this.has_abort_handler = true; + this.resp.onAborted(*RequestContext, RequestContext.onAbort, this); + } + + pub fn onResolve( + ctx: *RequestContext, + _: *JSC.JSGlobalObject, + arguments: []const JSC.JSValue, + ) void { + if (ctx.aborted) { + ctx.finalizeForAbort(); + return; + } + + if (arguments.len == 0) { + ctx.renderMissing(); + return; + } + + handleResolve(ctx, arguments[0]); + } + + fn handleResolve(ctx: *RequestContext, value: JSC.JSValue) void { + if (value.isEmptyOrUndefinedOrNull()) { + ctx.renderMissing(); + return; + } + + var response = value.as(JSC.WebCore.Response) orelse { + Output.prettyErrorln("Expected a Response object", .{}); + Output.flush(); + ctx.renderMissing(); + return; + }; + ctx.response_jsvalue = value; + JSC.C.JSValueProtect(ctx.server.globalThis.ref(), value.asObjectRef()); + + ctx.render(response); + } + + pub fn finalizeForAbort(this: *RequestContext) void { + this.pending_promises_for_abort -|= 1; + if (this.pending_promises_for_abort == 0) this.finalize(); + } + + pub fn onReject( + ctx: *RequestContext, + _: *JSC.JSGlobalObject, + arguments: []const JSC.JSValue, + ) void { + if (ctx.aborted) { + ctx.finalizeForAbort(); + return; + } + handleReject(ctx, if (arguments.len > 0) arguments[0] else JSC.JSValue.jsUndefined()); + } + + fn handleReject(ctx: *RequestContext, value: JSC.JSValue) void { + ctx.runErrorHandler( + value, + ); + + if (ctx.aborted) { + ctx.finalizeForAbort(); + return; + } + if (!ctx.resp.hasResponded()) { + ctx.renderMissing(); + } + } + + pub fn renderMissing(ctx: *RequestContext) void { + if (comptime !debug_mode) { + ctx.resp.writeStatus("204 No Content"); + ctx.resp.endWithoutBody(); + ctx.finalize(); + } else { + ctx.resp.writeStatus("200 OK"); + ctx.resp.end("Welcome to Bun! To get started, return a Response object.", false); + ctx.finalize(); + } + } + + pub fn renderDefaultError( + this: *RequestContext, + log: *logger.Log, + err: anyerror, + exceptions: []Api.JsException, + comptime fmt: string, + args: anytype, + ) void { + this.resp.writeStatus("500 Internal Server Error"); + this.resp.writeHeader("content-type", MimeType.html.value); + + const allocator = this.allocator; + + var fallback_container = allocator.create(Api.FallbackMessageContainer) catch unreachable; + defer allocator.destroy(fallback_container); + fallback_container.* = Api.FallbackMessageContainer{ + .message = std.fmt.allocPrint(allocator, comptime Output.prettyFmt(fmt, false), args) catch unreachable, + .router = null, + .reason = .fetch_event_handler, + .cwd = VirtualMachine.vm.bundler.fs.top_level_dir, + .problems = Api.Problems{ + .code = @truncate(u16, @errorToInt(err)), + .name = @errorName(err), + .exceptions = exceptions, + .build = log.toAPI(allocator) catch unreachable, + }, + }; + + if (comptime fmt.len > 0) Output.prettyErrorln(fmt, args); + Output.flush(); + + var bb = std.ArrayList(u8).init(allocator); + var bb_writer = bb.writer(); + + Fallback.renderBackend( + allocator, + fallback_container, + @TypeOf(bb_writer), + bb_writer, + ) catch unreachable; + if (this.resp.tryEnd(bb.items, bb.items.len)) { + bb.clearAndFree(); + this.finalizeWithoutDeinit(); + return; + } + + this.response_buf_owned = std.ArrayListUnmanaged(u8){ .items = bb.items, .capacity = bb.capacity }; + this.renderResponseBuffer(); + } + + pub fn renderResponseBuffer(this: *RequestContext) void { + this.resp.onWritable(*RequestContext, onWritableResponseBuffer, this); + } + + pub fn onWritableResponseBuffer(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool { + std.debug.assert(this.resp == resp); + if (this.aborted) { + this.finalizeForAbort(); + return false; + } + return this.sendWritableBytes(this.response_buf_owned.items, write_offset, resp); + } + + pub fn create(this: *RequestContext, server: *ThisServer, req: *uws.Request, resp: *App.Response) void { + this.* = .{ + .allocator = server.allocator, + .resp = resp, + .req = req, + // this memory is owned by the Request object + .url = strings.append(this.allocator, server.base_url_string_for_joining, req.url()) catch + @panic("Out of memory while joining the URL path?"), + .method = HTTP.Method.which(req.method()) orelse .GET, + .server = server, + }; + } + + pub fn isDeadRequest(this: *RequestContext) bool { + if (this.pending_promises_for_abort > 0) return false; + + if (this.promise != null) { + return false; + } + + if (this.request_js_object) |obj| { + if (obj.value().as(Request)) |req| { + if (req.body == .Locked) { + return false; + } + } + } + + return true; + } + + pub fn onAbort(this: *RequestContext, resp: *App.Response) void { + std.debug.assert(this.resp == resp); + std.debug.assert(!this.aborted); + this.aborted = true; + + // if we can, free the request now. + if (this.isDeadRequest()) { + this.finalizeWithoutDeinit(); + this.markComplete(); + this.deinit(); + } else { + this.pending_promises_for_abort = 0; + + // if we cannot, we have to reject pending promises + // first, we reject the request body promise + if (this.request_js_object != null) { + var request_js = this.request_js_object.?.value(); + request_js.ensureStillAlive(); + + this.request_js_object = null; + defer request_js.ensureStillAlive(); + defer JSC.C.JSValueUnprotect(this.server.globalThis.ref(), request_js.asObjectRef()); + // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object + // but we received nothing or the connection was aborted + if (request_js.as(Request)) |req| { + // the promise is pending + if (req.body == .Locked and (req.body.Locked.action != .none or req.body.Locked.promise != null)) { + this.pending_promises_for_abort += 1; + req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); + } + req.uws_request = null; + } + } + + // then, we reject the response promise + if (this.promise) |promise| { + this.pending_promises_for_abort += 1; + this.promise = null; + promise.asPromise().?.reject(this.server.globalThis, JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis)); + } + + if (this.pending_promises_for_abort > 0) { + this.server.vm.tick(); + } + } + } + + pub fn markComplete(this: *RequestContext) void { + if (!this.has_marked_complete) this.server.onRequestComplete(); + this.has_marked_complete = true; + } + + // This function may be called multiple times + // so it's important that we can safely do that + pub fn finalizeWithoutDeinit(this: *RequestContext) void { + this.blob.detach(); + + if (comptime Environment.allow_assert) { + std.debug.assert(!this.finalized); + this.finalized = true; + } + + if (!this.response_jsvalue.isEmpty()) { + this.server.response_objects_pool.push(this.server.globalThis, this.response_jsvalue); + this.response_jsvalue = JSC.JSValue.zero; + } + + if (this.request_js_object != null) { + var request_js = this.request_js_object.?.value(); + request_js.ensureStillAlive(); + + this.request_js_object = null; + defer request_js.ensureStillAlive(); + defer JSC.C.JSValueUnprotect(this.server.globalThis.ref(), request_js.asObjectRef()); + // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object + // but we received nothing or the connection was aborted + if (request_js.as(Request)) |req| { + // the promise is pending + if (req.body == .Locked and req.body.Locked.action != .none and req.body.Locked.promise != null) { + req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); + } + req.uws_request = null; + } + } + + if (this.promise) |promise| { + this.promise = null; + + if (promise.asInternalPromise()) |prom| { + prom.rejectAsHandled(this.server.globalThis, (JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis))); + } else if (promise.asPromise()) |prom| { + prom.rejectAsHandled(this.server.globalThis, (JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis))); + } + JSC.C.JSValueUnprotect(this.server.globalThis.ref(), promise.asObjectRef()); + } + + if (this.response_headers != null) { + this.response_headers.?.deref(); + this.response_headers = null; + } + } + pub fn finalize(this: *RequestContext) void { + this.finalizeWithoutDeinit(); + this.markComplete(); + this.deinit(); + } + + pub fn deinit(this: *RequestContext) void { + if (comptime Environment.allow_assert) + std.debug.assert(this.finalized); + + if (comptime Environment.allow_assert) + std.debug.assert(this.has_marked_complete); + + var server = this.server; + this.request_body_buf.clearAndFree(this.allocator); + this.response_buf_owned.clearAndFree(this.allocator); + + server.request_pool_allocator.destroy(this); + } + + fn writeHeaders( + this: *RequestContext, + headers: *JSC.FetchHeaders, + ) void { + headers.remove(&ZigString.init("content-length")); + headers.remove(&ZigString.init("transfer-encoding")); + if (!ssl_enabled) headers.remove(&ZigString.init("strict-transport-security")); + headers.toUWSResponse(ssl_enabled, this.resp); + } + + pub fn writeStatus(this: *RequestContext, status: u16) void { + var status_text_buf: [48]u8 = undefined; + + if (status == 302) { + this.resp.writeStatus("302 Found"); + } else { + this.resp.writeStatus(std.fmt.bufPrint(&status_text_buf, "{d} HM", .{status}) catch unreachable); + } + } + + fn cleanupAndFinalizeAfterSendfile(this: *RequestContext) void { + this.resp.setWriteOffset(this.sendfile.offset); + this.resp.endWithoutBody(); + // use node syscall so that we don't segfault on BADF + if (this.sendfile.auto_close) + _ = JSC.Node.Syscall.close(this.sendfile.fd); + this.sendfile = undefined; + this.finalize(); + } + const separator: string = "\r\n"; + const separator_iovec = [1]std.os.iovec_const{.{ + .iov_base = separator.ptr, + .iov_len = separator.len, + }}; + + pub fn onSendfile(this: *RequestContext) bool { + if (this.aborted) { + this.cleanupAndFinalizeAfterSendfile(); + return false; + } + + const adjusted_count_temporary = @minimum(@as(u64, this.sendfile.remain), @as(u63, std.math.maxInt(u63))); + // TODO we should not need this int cast; improve the return type of `@minimum` + const adjusted_count = @intCast(u63, adjusted_count_temporary); + + if (Environment.isLinux) { + var signed_offset = @intCast(i64, this.sendfile.offset); + const start = this.sendfile.offset; + const val = + // this does the syscall directly, without libc + linux.sendfile(this.sendfile.socket_fd, this.sendfile.fd, &signed_offset, this.sendfile.remain); + this.sendfile.offset = @intCast(Blob.SizeType, signed_offset); + + const errcode = linux.getErrno(val); + + this.sendfile.remain -= @intCast(Blob.SizeType, this.sendfile.offset - start); + + if (errcode != .SUCCESS or this.aborted or this.sendfile.remain == 0 or val == 0) { + if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE) { + Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); + Output.flush(); + } + this.cleanupAndFinalizeAfterSendfile(); + return errcode != .SUCCESS; + } + } else { + var sbytes: std.os.off_t = adjusted_count; + const signed_offset = @bitCast(i64, @as(u64, this.sendfile.offset)); + + const errcode = std.c.getErrno(std.c.sendfile( + this.sendfile.fd, + this.sendfile.socket_fd, + + signed_offset, + &sbytes, + null, + 0, + )); + const wrote = @intCast(Blob.SizeType, sbytes); + this.sendfile.offset += wrote; + this.sendfile.remain -= wrote; + if (errcode != .AGAIN or this.aborted or this.sendfile.remain == 0 or sbytes == 0) { + if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE) { + Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); + Output.flush(); + } + this.cleanupAndFinalizeAfterSendfile(); + return errcode == .SUCCESS; + } + } + + if (!this.sendfile.has_set_on_writable) { + this.sendfile.has_set_on_writable = true; + this.resp.onWritable(*RequestContext, onWritableSendfile, this); + } + + this.setAbortHandler(); + this.resp.markNeedsMore(); + + return true; + } + + pub fn onWritableBytes(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool { + std.debug.assert(this.resp == resp); + if (this.aborted) { + this.finalizeForAbort(); + return false; + } + + var bytes = this.blob.sharedView(); + return this.sendWritableBytes(bytes, write_offset, resp); + } + + pub fn sendWritableBytes(this: *RequestContext, bytes_: []const u8, write_offset: c_ulong, resp: *App.Response) bool { + std.debug.assert(this.resp == resp); + + var bytes = bytes_[@minimum(bytes_.len, @truncate(usize, write_offset))..]; + if (resp.tryEnd(bytes, bytes_.len)) { + this.finalize(); + return true; + } else { + this.resp.onWritable(*RequestContext, onWritableBytes, this); + return true; + } + } + + pub fn onWritableSendfile(this: *RequestContext, _: c_ulong, _: *App.Response) callconv(.C) bool { + return this.onSendfile(); + } + + // We tried open() in another thread for this + // it was not faster due to the mountain of syscalls + pub fn renderSendFile(this: *RequestContext, blob: JSC.WebCore.Blob) void { + this.blob = blob; + const file = &this.blob.store.?.data.file; + var file_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const auto_close = file.pathlike != .fd; + const fd = if (!auto_close) + file.pathlike.fd + else switch (JSC.Node.Syscall.open(file.pathlike.path.sliceZ(&file_buf), std.os.O.RDONLY | std.os.O.NONBLOCK | std.os.O.CLOEXEC, 0)) { + .result => |_fd| _fd, + .err => |err| return this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toSystemError().toErrorInstance( + this.server.globalThis, + )), + }; + + // stat only blocks if the target is a file descriptor + const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) { + .result => |result| result, + .err => |err| { + this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toSystemError().toErrorInstance( + this.server.globalThis, + )); + if (auto_close) { + _ = JSC.Node.Syscall.close(fd); + } + return; + }, + }; + + if (Environment.isMac) { + if (!std.os.S.ISREG(stat.mode)) { + if (auto_close) { + _ = JSC.Node.Syscall.close(fd); + } + + var err = JSC.Node.Syscall.Error{ + .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(std.os.E.INVAL)), + .path = file.pathlike.path.slice(), + .syscall = .sendfile, + }; + var sys = err.toSystemError(); + sys.message = ZigString.init("MacOS does not support sending non-regular files"); + this.runErrorHandler(sys.toErrorInstance( + this.server.globalThis, + )); + return; + } + } + + if (Environment.isLinux) { + if (!(std.os.S.ISREG(stat.mode) or std.os.S.ISFIFO(stat.mode))) { + if (auto_close) { + _ = JSC.Node.Syscall.close(fd); + } + + var err = JSC.Node.Syscall.Error{ + .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(std.os.E.INVAL)), + .path = file.pathlike.path.slice(), + .syscall = .sendfile, + }; + var sys = err.toSystemError(); + sys.message = ZigString.init("File must be regular or FIFO"); + this.runErrorHandler(sys.toErrorInstance( + this.server.globalThis, + )); + return; + } + } + + this.blob.size = @intCast(Blob.SizeType, stat.size); + this.needs_content_length = true; + + this.sendfile = .{ + .fd = fd, + .remain = this.blob.size, + .auto_close = auto_close, + .socket_fd = if (!this.aborted) this.resp.getNativeHandle() else -999, + }; + + this.resp.runCorked(*RequestContext, renderMetadataAndNewline, this); + + if (this.blob.size == 0) { + this.cleanupAndFinalizeAfterSendfile(); + return; + } + + _ = this.onSendfile(); + } + + pub fn renderMetadataAndNewline(this: *RequestContext) void { + this.renderMetadata(); + this.resp.prepareForSendfile(); + } + + pub fn doSendfile(this: *RequestContext, blob: Blob) void { + if (this.aborted) { + this.finalizeForAbort(); + return; + } + + if (this.has_sendfile_ctx) return; + + this.has_sendfile_ctx = true; + + if (comptime can_sendfile) { + return this.renderSendFile(blob); + } + + this.setAbortHandler(); + this.blob.doReadFileInternal(*RequestContext, this, onReadFile, this.server.globalThis); + } + + pub fn onReadFile(this: *RequestContext, result: Blob.Store.ReadFile.ResultType) void { + if (this.aborted) { + this.finalizeForAbort(); + return; + } + + if (result == .err) { + this.runErrorHandler(result.err.toErrorInstance(this.server.globalThis)); + return; + } + + const is_temporary = result.result.is_temporary; + if (!is_temporary) { + this.blob.resolveSize(); + this.doRenderBlob(); + } else { + this.blob.size = @truncate(Blob.SizeType, result.result.buf.len); + this.response_buf_owned = .{ .items = result.result.buf, .capacity = result.result.buf.len }; + this.renderResponseBuffer(); + } + } + + pub fn doRenderWithBodyLocked(this: *anyopaque, value: *JSC.WebCore.Body.Value) void { + doRenderWithBody(bun.cast(*RequestContext, this), value); + } + + pub fn doRenderWithBody(this: *RequestContext, value: *JSC.WebCore.Body.Value) void { + switch (value.*) { + .Error => { + const err = value.Error; + _ = value.use(); + if (this.aborted) { + this.finalizeForAbort(); + return; + } + this.runErrorHandler(err); + return; + }, + .Blob => { + this.blob = value.use(); + + if (this.aborted) { + this.finalizeForAbort(); + return; + } + + if (this.blob.needsToReadFile()) { + this.req.setYield(false); + if (!this.has_sendfile_ctx) + this.doSendfile(this.blob); + return; + } + }, + // TODO: this needs to support streaming! + .Locked => |*lock| { + lock.callback = doRenderWithBodyLocked; + lock.task = this; + return; + }, + else => {}, + } + + this.doRenderBlob(); + } + + pub fn doRenderBlob(this: *RequestContext) void { + if (this.has_abort_handler) + this.resp.runCorked(*RequestContext, renderMetadata, this) + else + this.renderMetadata(); + + this.renderBytes(); + } + + pub fn doRender(this: *RequestContext) void { + if (this.aborted) { + this.finalizeForAbort(); + return; + } + var response = this.response_ptr.?; + this.doRenderWithBody(&response.body.value); + } + + pub fn renderProductionError(this: *RequestContext, status: u16) void { + switch (status) { + 404 => { + this.resp.writeStatus("404 Not Found"); + this.resp.endWithoutBody(); + }, + else => { + this.resp.writeStatus("500 Internal Server Error"); + this.resp.writeHeader("content-type", "text/plain"); + this.resp.end("Something went wrong!", true); + }, + } + + this.finalize(); + } + + pub fn runErrorHandler( + this: *RequestContext, + value: JSC.JSValue, + ) void { + runErrorHandlerWithStatusCode(this, value, 500); + } + + pub fn runErrorHandlerWithStatusCode( + this: *RequestContext, + value: JSC.JSValue, + status: u16, + ) void { + JSC.markBinding(); + if (this.resp.hasResponded()) return; + + var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(this.allocator); + defer exception_list.deinit(); + if (!this.server.config.onError.isEmpty() and !this.has_called_error_handler) { + this.has_called_error_handler = true; + var args = [_]JSC.C.JSValueRef{value.asObjectRef()}; + const result = JSC.C.JSObjectCallAsFunctionReturnValue(this.server.globalThis.ref(), this.server.config.onError.asObjectRef(), this.server.thisObject.asObjectRef(), 1, &args); + + if (!result.isEmptyOrUndefinedOrNull()) { + if (result.isError() or result.isAggregateError(this.server.globalThis)) { + this.runErrorHandler(result); + return; + } else if (result.as(Response)) |response| { + this.render(response); + return; + } + } + } + + if (comptime debug_mode) { + JSC.VirtualMachine.vm.defaultErrorHandler(value, &exception_list); + + this.renderDefaultError( + JSC.VirtualMachine.vm.log, + error.ExceptionOcurred, + exception_list.toOwnedSlice(), + "<r><red>{s}<r> - <b>{s}<r> failed", + .{ std.mem.span(@tagName(this.method)), this.url }, + ); + } else { + if (status != 404) + JSC.VirtualMachine.vm.defaultErrorHandler(value, &exception_list); + this.renderProductionError(status); + } + JSC.VirtualMachine.vm.log.reset(); + return; + } + + pub fn renderMetadata(this: *RequestContext) void { + var response: *JSC.WebCore.Response = this.response_ptr.?; + var status = response.statusCode(); + const size = this.blob.size; + status = if (status == 200 and size == 0) + 204 + else + status; + + this.writeStatus(status); + var needs_content_type = true; + const content_type: MimeType = brk: { + if (response.body.init.headers) |headers_| { + if (headers_.get("content-type")) |content| { + needs_content_type = false; + break :brk MimeType.init(content); + } + } + break :brk if (this.blob.content_type.len > 0) + MimeType.init(this.blob.content_type) + else if (MimeType.sniff(this.blob.sharedView())) |content| + content + else if (this.blob.is_all_ascii orelse false) + MimeType.text + else + MimeType.other; + }; + + var has_content_disposition = false; + + if (response.body.init.headers) |headers_| { + this.writeHeaders(headers_); + has_content_disposition = headers_.has(&ZigString.init("content-disposition")); + response.body.init.headers = null; + headers_.deref(); + } + + if (needs_content_type) { + this.resp.writeHeader("content-type", content_type.value); + } + + // automatically include the filename when: + // 1. Bun.file("foo") + // 2. The content-disposition header is not present + if (!has_content_disposition and content_type.category.autosetFilename()) { + if (this.blob.store) |store| { + if (store.data == .file) { + if (store.data.file.pathlike == .path) { + const basename = std.fs.path.basename(store.data.file.pathlike.path.slice()); + if (basename.len > 0) { + var filename_buf: [1024]u8 = undefined; + + this.resp.writeHeader( + "content-disposition", + std.fmt.bufPrint(&filename_buf, "filename=\"{s}\"", .{basename[0..@minimum(basename.len, 1024 - 32)]}) catch "", + ); + } + } + } + } + } + + if (this.needs_content_length) { + this.resp.writeHeaderInt("content-length", size); + this.needs_content_length = false; + } + } + + pub fn renderBytes(this: *RequestContext) void { + const bytes = this.blob.sharedView(); + + if (!this.resp.tryEnd( + bytes, + bytes.len, + )) { + this.resp.onWritable(*RequestContext, onWritableBytes, this); + return; + } + + this.finalize(); + } + + pub fn render(this: *RequestContext, response: *JSC.WebCore.Response) void { + this.response_ptr = response; + + this.doRender(); + } + + pub fn resolveRequestBody(this: *RequestContext) void { + if (this.aborted) { + this.finalizeForAbort(); + return; + } + + if (JSC.JSValue.fromRef(this.request_js_object).as(Request)) |req| { + var bytes = this.request_body_buf.toOwnedSlice(this.allocator); + var old = req.body; + req.body = .{ + .Blob = if (bytes.len > 0) + Blob.init(bytes, this.allocator, this.server.globalThis) + else + Blob.initEmpty(this.server.globalThis), + }; + old.resolve(&req.body, this.server.globalThis); + VirtualMachine.vm.tick(); + return; + } + } + + pub fn onBodyChunk(this: *RequestContext, resp: *App.Response, chunk: []const u8, last: bool) void { + std.debug.assert(this.resp == resp); + + if (this.aborted) return; + this.request_body_buf.appendSlice(this.allocator, chunk) catch @panic("Out of memory while allocating request body"); + if (last) { + if (JSC.JSValue.fromRef(this.request_js_object).as(Request) != null) { + uws.Loop.get().?.nextTick(*RequestContext, this, resolveRequestBody); + } else { + this.request_body_buf.deinit(this.allocator); + this.request_body_buf = .{}; + } + } + } + + pub fn onPull(this: *RequestContext) void { + if (this.req.header("content-length")) |content_length| { + const len = std.fmt.parseInt(usize, content_length, 10) catch 0; + if (len == 0) { + if (JSC.JSValue.fromRef(this.request_js_object).as(Request)) |req| { + var old = req.body; + old.Locked.callback = null; + req.body = .{ .Empty = .{} }; + old.resolve(&req.body, this.server.globalThis); + VirtualMachine.vm.tick(); + return; + } + } + + if (len >= this.server.config.max_request_body_size) { + if (JSC.JSValue.fromRef(this.request_js_object).as(Request)) |req| { + var old = req.body; + old.Locked.callback = null; + req.body = .{ .Empty = .{} }; + old.toError(error.RequestBodyTooLarge, this.server.globalThis); + VirtualMachine.vm.tick(); + return; + } + + this.resp.writeStatus("413 Request Entity Too Large"); + this.resp.endWithoutBody(); + this.finalize(); + return; + } + + this.request_body_buf.ensureTotalCapacityPrecise(this.allocator, len) catch @panic("Out of memory while allocating request body buffer"); + } + this.setAbortHandler(); + + this.resp.onData(*RequestContext, onBodyChunk, this); + } + + pub fn onPullCallback(this: *anyopaque) void { + onPull(bun.cast(*RequestContext, this)); + } + + comptime { + if (!JSC.is_bindgen) { + @export(PromiseHandler.resolve, .{ + .name = Export[0].symbol_name, + }); + @export(PromiseHandler.reject, .{ + .name = Export[1].symbol_name, + }); + } + } + }; +} + +pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { + return struct { + pub const ssl_enabled = ssl_enabled_; + const debug_mode = debug_mode_; + + const ThisServer = @This(); + pub const RequestContext = NewRequestContext(ssl_enabled, debug_mode, @This()); + + pub const App = uws.NewApp(ssl_enabled); + + listener: ?*App.ListenSocket = null, + thisObject: JSC.JSValue = JSC.JSValue.zero, + app: *App = undefined, + vm: *JSC.VirtualMachine = undefined, + globalThis: *JSGlobalObject, + base_url_string_for_joining: string = "", + response_objects_pool: JSC.WebCore.Response.Pool = JSC.WebCore.Response.Pool{}, + config: ServerConfig = ServerConfig{}, + pending_requests: usize = 0, + request_pool_allocator: std.mem.Allocator = undefined, + has_js_deinited: bool = false, + listen_callback: JSC.AnyTask = undefined, + allocator: std.mem.Allocator, + + pub const Class = JSC.NewClass( + ThisServer, + .{ .name = "Server" }, + .{ + .stop = .{ + .rfn = JSC.wrapSync(ThisServer, "stopFromJS"), + }, + .finalize = .{ + .rfn = finalize, + }, + }, + .{ + .port = .{ + .get = JSC.getterWrap(ThisServer, "getPort"), + }, + .hostname = .{ + .get = JSC.getterWrap(ThisServer, "getHostname"), + }, + .development = .{ + .get = JSC.getterWrap(ThisServer, "getDevelopment"), + }, + .pendingRequests = .{ + .get = JSC.getterWrap(ThisServer, "getPendingRequests"), + }, + }, + ); + + pub fn stopFromJS(this: *ThisServer) JSC.JSValue { + if (this.listener != null) { + JSC.C.JSValueUnprotect(this.globalThis.ref(), this.thisObject.asObjectRef()); + this.thisObject = JSC.JSValue.jsUndefined(); + this.stop(); + } + + return JSC.JSValue.jsUndefined(); + } + + pub fn getPort(this: *ThisServer) JSC.JSValue { + return JSC.JSValue.jsNumber(this.config.port); + } + + pub fn getPendingRequests(this: *ThisServer) JSC.JSValue { + return JSC.JSValue.jsNumber(@intCast(i32, @truncate(u31, this.pending_requests))); + } + + pub fn getHostname(this: *ThisServer, globalThis: *JSGlobalObject) JSC.JSValue { + return ZigString.init(this.config.base_uri).toValue(globalThis); + } + + pub fn getDevelopment( + _: *ThisServer, + ) JSC.JSValue { + return JSC.JSValue.jsBoolean(debug_mode); + } + + pub fn onRequestComplete(this: *ThisServer) void { + this.pending_requests -= 1; + this.deinitIfWeCan(); + } + + pub fn finalize(this: *ThisServer) void { + this.has_js_deinited = true; + this.deinitIfWeCan(); + } + + pub fn deinitIfWeCan(this: *ThisServer) void { + if (this.pending_requests == 0 and this.listener == null and this.has_js_deinited) + this.deinit(); + } + + pub fn stop(this: *ThisServer) void { + if (this.listener) |listener| { + listener.close(); + this.listener = null; + this.vm.disable_run_us_loop = false; + } + + this.deinitIfWeCan(); + } + + pub fn deinit(this: *ThisServer) void { + if (this.vm.response_objects_pool) |pool| { + if (pool == &this.response_objects_pool) { + this.vm.response_objects_pool = null; + } + } + + this.app.destroy(); + const allocator = this.allocator; + allocator.destroy(this); + } + + pub fn init(config: ServerConfig, globalThis: *JSGlobalObject) *ThisServer { + var server = bun.default_allocator.create(ThisServer) catch @panic("Out of memory!"); + server.* = .{ + .globalThis = globalThis, + .config = config, + .base_url_string_for_joining = strings.trim(config.base_url.href, "/"), + .vm = JSC.VirtualMachine.vm, + .allocator = Arena.getThreadlocalDefault(), + }; + if (RequestContext.pool == null) { + RequestContext.pool = server.allocator.create(RequestContext.RequestContextStackAllocator) catch @panic("Out of memory!"); + RequestContext.pool.?.* = .{ + .fallback_allocator = server.allocator, + }; + server.request_pool_allocator = RequestContext.pool.?.get(); + RequestContext.pool_allocator = server.request_pool_allocator; + } else { + server.request_pool_allocator = RequestContext.pool_allocator; + } + + return server; + } + + noinline fn onListenFailed(this: *ThisServer) void { + var zig_str: ZigString = ZigString.init("Failed to start server"); + if (comptime ssl_enabled) { + var output_buf: [4096]u8 = undefined; + output_buf[0] = 0; + var written: usize = 0; + var ssl_error = BoringSSL.ERR_get_error(); + while (ssl_error != 0 and written < output_buf.len) : (ssl_error = BoringSSL.ERR_get_error()) { + if (written > 0) { + output_buf[written] = '\n'; + written += 1; + } + + if (BoringSSL.ERR_reason_error_string( + ssl_error, + )) |reason_ptr| { + const reason = std.mem.span(reason_ptr); + if (reason.len == 0) { + break; + } + @memcpy(output_buf[written..].ptr, reason.ptr, reason.len); + written += reason.len; + } + + if (BoringSSL.ERR_func_error_string( + ssl_error, + )) |reason_ptr| { + const reason = std.mem.span(reason_ptr); + if (reason.len > 0) { + output_buf[written..][0.." via ".len].* = " via ".*; + written += " via ".len; + @memcpy(output_buf[written..].ptr, reason.ptr, reason.len); + written += reason.len; + } + } + + if (BoringSSL.ERR_lib_error_string( + ssl_error, + )) |reason_ptr| { + const reason = std.mem.span(reason_ptr); + if (reason.len > 0) { + output_buf[written..][0] = ' '; + written += 1; + @memcpy(output_buf[written..].ptr, reason.ptr, reason.len); + written += reason.len; + } + } + } + + if (written > 0) { + var message = output_buf[0..written]; + zig_str = ZigString.init(std.fmt.allocPrint(bun.default_allocator, "OpenSSL {s}", .{message}) catch unreachable); + zig_str.withEncoding().mark(); + } + } + // store the exception in here + this.thisObject = zig_str.toErrorInstance(this.globalThis); + return; + } + + pub fn onListen(this: *ThisServer, socket: ?*App.ListenSocket, _: uws.uws_app_listen_config_t) void { + if (socket == null) { + return this.onListenFailed(); + } + + this.listener = socket; + const needs_post_handler = this.vm.uws_event_loop == null; + this.vm.uws_event_loop = uws.Loop.get(); + this.vm.response_objects_pool = &this.response_objects_pool; + this.listen_callback = JSC.AnyTask.New(ThisServer, run).init(this); + this.vm.eventLoop().enqueueTask(JSC.Task.init(&this.listen_callback)); + if (needs_post_handler) { + _ = this.vm.uws_event_loop.?.addPostHandler(*JSC.EventLoop, this.vm.eventLoop(), JSC.EventLoop.tick); + } + } + + pub fn run(this: *ThisServer) void { + // this.app.addServerName(hostname_pattern: [*:0]const u8) + + // we do not increment the reference count here + // uWS manages running the loop, so it is unnecessary + // this.vm.us_loop_reference_count +|= 1; + this.vm.disable_run_us_loop = true; + + this.app.run(); + } + + pub fn onBunInfoRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { + JSC.markBinding(); + this.pending_requests += 1; + defer this.pending_requests -= 1; + req.setYield(false); + var stack_fallback = std.heap.stackFallback(8096, this.allocator); + var allocator = stack_fallback.get(); + + var buffer_writer = js_printer.BufferWriter.init(allocator) catch unreachable; + var writer = js_printer.BufferPrinter.init(buffer_writer); + defer writer.ctx.buffer.deinit(); + var source = logger.Source.initEmptyFile("info.json"); + _ = js_printer.printJSON( + *js_printer.BufferPrinter, + &writer, + bun.Global.BunInfo.generate(*Bundler, &JSC.VirtualMachine.vm.bundler, allocator) catch unreachable, + &source, + ) catch unreachable; + + resp.writeStatus("200 OK"); + resp.writeHeader("Content-Type", MimeType.json.value); + resp.writeHeader("Cache-Control", "public, max-age=3600"); + resp.writeHeaderInt("Age", 0); + const buffer = writer.ctx.written; + resp.end(buffer, false); + } + + pub fn onSrcRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { + JSC.markBinding(); + this.pending_requests += 1; + defer this.pending_requests -= 1; + req.setYield(false); + if (req.header("open-in-editor") == null) { + resp.writeStatus("501 Not Implemented"); + resp.end("Viewing source without opening in editor is not implemented yet!", false); + return; + } + + var ctx = &JSC.VirtualMachine.vm.rareData().editor_context; + ctx.autoDetectEditor(JSC.VirtualMachine.vm.bundler.env); + var line: ?string = req.header("editor-line"); + var column: ?string = req.header("editor-column"); + + if (ctx.editor) |editor| { + resp.writeStatus("200 Opened"); + resp.end("Opened in editor", false); + var url = req.url()["/src:".len..]; + if (strings.indexOfChar(url, ':')) |colon| { + url = url[0..colon]; + } + editor.open(ctx.path, url, line, column, this.allocator) catch Output.prettyErrorln("Failed to open editor", .{}); + } else { + resp.writeStatus("500 Missing Editor :("); + resp.end("Please set your editor in bunfig.toml", false); + } + } + + pub fn onRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { + JSC.markBinding(); + this.pending_requests += 1; + var vm = this.vm; + req.setYield(false); + var ctx = this.request_pool_allocator.create(RequestContext) catch @panic("ran out of memory"); + ctx.create(this, req, resp); + + var request_object = this.allocator.create(JSC.WebCore.Request) catch unreachable; + request_object.* = .{ + .url = JSC.ZigString.init(ctx.url), + .method = ctx.method, + .uws_request = req, + .body = .{ + .Locked = .{ + .task = ctx, + .global = this.globalThis, + .onPull = RequestContext.onPullCallback, + }, + }, + }; + request_object.url.mark(); + // We keep the Request object alive for the duration of the request so that we can remove the pointer to the UWS request object. + var args = [_]JSC.C.JSValueRef{JSC.WebCore.Request.Class.make(this.globalThis.ref(), request_object)}; + ctx.request_js_object = args[0]; + JSC.C.JSValueProtect(this.globalThis.ref(), args[0]); + const response_value = JSC.C.JSObjectCallAsFunctionReturnValue(this.globalThis.ref(), this.config.onRequest.asObjectRef(), this.thisObject.asObjectRef(), 1, &args); + + if (ctx.aborted) { + ctx.finalizeForAbort(); + return; + } + if (response_value.isEmptyOrUndefinedOrNull() and !ctx.resp.hasResponded()) { + ctx.renderMissing(); + return; + } + + if (response_value.isError() or response_value.isAggregateError(this.globalThis) or response_value.isException(this.globalThis.vm())) { + ctx.runErrorHandler(response_value); + return; + } + + if (response_value.as(JSC.WebCore.Response)) |response| { + JSC.C.JSValueProtect(this.globalThis.ref(), response_value.asObjectRef()); + ctx.response_jsvalue = response_value; + + ctx.render(response); + return; + } + + var wait_for_promise = false; + + if (response_value.asPromise()) |promise| { + // If we immediately have the value available, we can skip the extra event loop tick + switch (promise.status(vm.global.vm())) { + .Pending => {}, + .Fulfilled => { + ctx.handleResolve(promise.result(vm.global.vm())); + return; + }, + .Rejected => { + ctx.handleReject(promise.result(vm.global.vm())); + return; + }, + } + wait_for_promise = true; + // I don't think this case should happen + // But I'm uncertain + } else if (response_value.asInternalPromise()) |promise| { + switch (promise.status(vm.global.vm())) { + .Pending => {}, + .Fulfilled => { + ctx.handleResolve(promise.result(vm.global.vm())); + return; + }, + .Rejected => { + ctx.handleReject(promise.result(vm.global.vm())); + return; + }, + } + wait_for_promise = true; + } + + if (wait_for_promise) { + ctx.setAbortHandler(); + + RequestContext.PromiseHandler.then(ctx, response_value, this.globalThis); + return; + } + + // The user returned something that wasn't a promise or a promise with a response + if (!ctx.resp.hasResponded()) ctx.renderMissing(); + } + + pub fn listen(this: *ThisServer) void { + if (ssl_enabled) { + BoringSSL.load(); + const ssl_config = this.config.ssl_config orelse @panic("Assertion failure: ssl_config"); + this.app = App.create(.{ + .key_file_name = ssl_config.key_file_name, + .cert_file_name = ssl_config.cert_file_name, + .passphrase = ssl_config.passphrase, + .dh_params_file_name = ssl_config.dh_params_file_name, + .ca_file_name = ssl_config.ca_file_name, + .ssl_prefer_low_memory_usage = @as(c_int, @boolToInt(ssl_config.low_memory_mode)), + }); + + if (ssl_config.server_name != null and std.mem.span(ssl_config.server_name).len > 0) { + this.app.addServerName(ssl_config.server_name); + } + } else { + this.app = App.create(.{}); + } + + this.app.any("/*", *ThisServer, this, onRequest); + + if (comptime debug_mode) { + this.app.get("/bun:info", *ThisServer, this, onBunInfoRequest); + this.app.get("/src:/*", *ThisServer, this, onSrcRequest); + } + + this.app.listenWithConfig(*ThisServer, this, onListen, .{ + .port = this.config.port, + .host = this.config.hostname, + .options = 0, + }); + } + }; +} + +pub const Server = NewServer(false, false); +pub const SSLServer = NewServer(true, false); +pub const DebugServer = NewServer(false, true); +pub const DebugSSLServer = NewServer(true, true); diff --git a/src/bun.js/api/transpiler.zig b/src/bun.js/api/transpiler.zig new file mode 100644 index 000000000..6d3f3f6fd --- /dev/null +++ b/src/bun.js/api/transpiler.zig @@ -0,0 +1,1304 @@ +const std = @import("std"); +const Api = @import("../../api/schema.zig").Api; +const FilesystemRouter = @import("../../router.zig"); +const http = @import("../../http.zig"); +const JavaScript = @import("../javascript.zig"); +const QueryStringMap = @import("../../url.zig").QueryStringMap; +const CombinedScanner = @import("../../url.zig").CombinedScanner; +const bun = @import("../../global.zig"); +const string = bun.string; +const JSC = @import("../../jsc.zig"); +const js = JSC.C; +const WebCore = @import("../webcore/response.zig"); +const Bundler = @import("../../bundler.zig"); +const options = @import("../../options.zig"); +const VirtualMachine = JavaScript.VirtualMachine; +const ScriptSrcStream = std.io.FixedBufferStream([]u8); +const ZigString = JSC.ZigString; +const Fs = @import("../../fs.zig"); +const Base = @import("../base.zig"); +const getAllocator = Base.getAllocator; +const JSObject = JSC.JSObject; +const JSError = Base.JSError; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const strings = @import("strings"); +const NewClass = Base.NewClass; +const To = Base.To; +const Request = WebCore.Request; +const d = Base.d; +const FetchEvent = WebCore.FetchEvent; +const MacroMap = @import("../../resolver/package_json.zig").MacroMap; +const TSConfigJSON = @import("../../resolver/tsconfig_json.zig").TSConfigJSON; +const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON; +const logger = @import("../../logger.zig"); +const Loader = options.Loader; +const Platform = options.Platform; +const JSAst = @import("../../js_ast.zig"); +const Transpiler = @This(); +const JSParser = @import("../../js_parser.zig"); +const JSPrinter = @import("../../js_printer.zig"); +const ScanPassResult = JSParser.ScanPassResult; +const Mimalloc = @import("../../mimalloc_arena.zig"); +const Runtime = @import("../../runtime.zig").Runtime; +const JSLexer = @import("../../js_lexer.zig"); +const Expr = JSAst.Expr; + +bundler: Bundler.Bundler, +arena: std.heap.ArenaAllocator, +transpiler_options: TranspilerOptions, +scan_pass_result: ScanPassResult, +buffer_writer: ?JSPrinter.BufferWriter = null, + +pub const Class = NewClass( + Transpiler, + .{ .name = "Transpiler" }, + .{ + .scanImports = .{ + .rfn = scanImports, + }, + .scan = .{ + .rfn = scan, + }, + .transform = .{ + .rfn = transform, + }, + .transformSync = .{ + .rfn = transformSync, + }, + // .resolve = .{ + // .rfn = resolve, + // }, + // .buildSync = .{ + // .rfn = buildSync, + // }, + .finalize = finalize, + }, + .{}, +); + +pub const Constructor = JSC.NewConstructor( + @This(), + .{ + .constructor = .{ .rfn = constructor }, + }, + .{}, +); + +const default_transform_options: Api.TransformOptions = brk: { + var opts = std.mem.zeroes(Api.TransformOptions); + opts.disable_hmr = true; + opts.platform = Api.Platform.browser; + opts.serve = false; + break :brk opts; +}; + +const TranspilerOptions = struct { + transform: Api.TransformOptions = default_transform_options, + default_loader: options.Loader = options.Loader.jsx, + macro_map: MacroMap = MacroMap{}, + tsconfig: ?*TSConfigJSON = null, + tsconfig_buf: []const u8 = "", + macros_buf: []const u8 = "", + log: logger.Log, + runtime: Runtime.Features = Runtime.Features{ .top_level_await = true }, + tree_shaking: bool = false, + trim_unused_imports: ?bool = null, +}; + +// Mimalloc gets unstable if we try to move this to a different thread +// threadlocal var transform_buffer: bun.MutableString = undefined; +// threadlocal var transform_buffer_loaded: bool = false; + +// This is going to be hard to not leak +pub const TransformTask = struct { + input_code: ZigString = ZigString.init(""), + protected_input_value: JSC.JSValue = @intToEnum(JSC.JSValue, 0), + output_code: ZigString = ZigString.init(""), + bundler: Bundler.Bundler = undefined, + log: logger.Log, + err: ?anyerror = null, + macro_map: MacroMap = MacroMap{}, + tsconfig: ?*TSConfigJSON = null, + loader: Loader, + global: *JSGlobalObject, + replace_exports: Runtime.Features.ReplaceableExport.Map = .{}, + + pub const AsyncTransformTask = JSC.ConcurrentPromiseTask(TransformTask); + pub const AsyncTransformEventLoopTask = AsyncTransformTask.EventLoopTask; + + pub fn create(transpiler: *Transpiler, protected_input_value: JSC.C.JSValueRef, globalThis: *JSGlobalObject, input_code: ZigString, loader: Loader) !*AsyncTransformTask { + var transform_task = try bun.default_allocator.create(TransformTask); + transform_task.* = .{ + .input_code = input_code, + .protected_input_value = if (protected_input_value != null) JSC.JSValue.fromRef(protected_input_value) else @intToEnum(JSC.JSValue, 0), + .bundler = undefined, + .global = globalThis, + .macro_map = transpiler.transpiler_options.macro_map, + .tsconfig = transpiler.transpiler_options.tsconfig, + .log = logger.Log.init(bun.default_allocator), + .loader = loader, + .replace_exports = transpiler.transpiler_options.runtime.replace_exports, + }; + transform_task.bundler = transpiler.bundler; + transform_task.bundler.linker.resolver = &transform_task.bundler.resolver; + + transform_task.bundler.setLog(&transform_task.log); + transform_task.bundler.setAllocator(bun.default_allocator); + return try AsyncTransformTask.createOnJSThread(bun.default_allocator, globalThis, transform_task); + } + + pub fn run(this: *TransformTask) void { + const name = this.loader.stdinName(); + const source = logger.Source.initPathString(name, this.input_code.slice()); + + JSAst.Stmt.Data.Store.create(bun.default_allocator); + JSAst.Expr.Data.Store.create(bun.default_allocator); + + var arena = Mimalloc.Arena.init() catch unreachable; + + const allocator = arena.allocator(); + + defer { + JSAst.Stmt.Data.Store.reset(); + JSAst.Expr.Data.Store.reset(); + arena.deinit(); + } + + this.bundler.setAllocator(allocator); + const jsx = if (this.tsconfig != null) + this.tsconfig.?.mergeJSX(this.bundler.options.jsx) + else + this.bundler.options.jsx; + + const parse_options = Bundler.Bundler.ParseOptions{ + .allocator = allocator, + .macro_remappings = this.macro_map, + .dirname_fd = 0, + .file_descriptor = null, + .loader = this.loader, + .jsx = jsx, + .path = source.path, + .virtual_source = &source, + .replace_exports = this.replace_exports, + // .allocator = this. + }; + + const parse_result = this.bundler.parse(parse_options, null) orelse { + this.err = error.ParseError; + return; + }; + + if (parse_result.empty) { + this.output_code = ZigString.init(""); + return; + } + + var global_allocator = arena.backingAllocator(); + var buffer_writer = JSPrinter.BufferWriter.init(global_allocator) catch |err| { + this.err = err; + return; + }; + buffer_writer.buffer.list.ensureTotalCapacity(global_allocator, 512) catch unreachable; + buffer_writer.reset(); + + // defer { + // transform_buffer = buffer_writer.buffer; + // } + + var printer = JSPrinter.BufferPrinter.init(buffer_writer); + const printed = this.bundler.print(parse_result, @TypeOf(&printer), &printer, .esm_ascii) catch |err| { + this.err = err; + return; + }; + + if (printed > 0) { + buffer_writer = printer.ctx; + buffer_writer.buffer.list.items = buffer_writer.written; + + var output = JSC.ZigString.init(buffer_writer.written); + output.mark(); + this.output_code = output; + } else { + this.output_code = ZigString.init(""); + } + } + + pub fn then(this: *TransformTask, promise: *JSC.JSInternalPromise) void { + if (this.log.hasAny() or this.err != null) { + const error_value: JSValue = brk: { + if (this.err) |err| { + if (!this.log.hasAny()) { + break :brk JSC.JSValue.fromRef(JSC.BuildError.create( + this.global, + bun.default_allocator, + logger.Msg{ + .data = logger.Data{ .text = std.mem.span(@errorName(err)) }, + }, + )); + } + } + + break :brk this.log.toJS(this.global, bun.default_allocator, "Transform failed"); + }; + + promise.reject(this.global, error_value); + return; + } + + finish(this.output_code, this.global, promise); + + if (@enumToInt(this.protected_input_value) != 0) { + this.protected_input_value = @intToEnum(JSC.JSValue, 0); + } + this.deinit(); + } + + noinline fn finish(code: ZigString, global: *JSGlobalObject, promise: *JSC.JSInternalPromise) void { + promise.resolve(global, code.toValueGC(global)); + } + + pub fn deinit(this: *TransformTask) void { + var should_cleanup = false; + defer if (should_cleanup) bun.Global.mimalloc_cleanup(false); + + this.log.deinit(); + if (this.input_code.isGloballyAllocated()) { + this.input_code.deinitGlobal(); + } + + if (this.output_code.isGloballyAllocated()) { + should_cleanup = this.output_code.len > 512_000; + this.output_code.deinitGlobal(); + } + + bun.default_allocator.destroy(this); + } +}; + +fn exportReplacementValue(value: JSValue, globalThis: *JSGlobalObject) ?JSAst.Expr { + if (value.isBoolean()) { + return Expr{ + .data = .{ + .e_boolean = .{ + .value = value.toBoolean(), + }, + }, + .loc = logger.Loc.Empty, + }; + } + + if (value.isNumber()) { + return Expr{ + .data = .{ + .e_number = .{ .value = value.asNumber() }, + }, + .loc = logger.Loc.Empty, + }; + } + + if (value.isNull()) { + return Expr{ + .data = .{ + .e_null = .{}, + }, + .loc = logger.Loc.Empty, + }; + } + + if (value.isUndefined()) { + return Expr{ + .data = .{ + .e_undefined = .{}, + }, + .loc = logger.Loc.Empty, + }; + } + + if (value.isString()) { + var str = JSAst.E.String{ + .data = std.fmt.allocPrint(bun.default_allocator, "{}", .{value.getZigString(globalThis)}) catch unreachable, + }; + var out = bun.default_allocator.create(JSAst.E.String) catch unreachable; + out.* = str; + return Expr{ + .data = .{ + .e_string = out, + }, + .loc = logger.Loc.Empty, + }; + } + + return null; +} + +fn transformOptionsFromJSC(ctx: JSC.C.JSContextRef, temp_allocator: std.mem.Allocator, args: *JSC.Node.ArgumentsSlice, exception: JSC.C.ExceptionRef) !TranspilerOptions { + var globalThis = ctx.ptr(); + const object = args.next() orelse return TranspilerOptions{ .log = logger.Log.init(temp_allocator) }; + if (object.isUndefinedOrNull()) return TranspilerOptions{ .log = logger.Log.init(temp_allocator) }; + + args.eat(); + var allocator = args.arena.allocator(); + + var transpiler = TranspilerOptions{ + .default_loader = .jsx, + .transform = default_transform_options, + .log = logger.Log.init(allocator), + }; + transpiler.log.level = .warn; + + if (!object.isObject()) { + JSC.throwInvalidArguments("Expected an object", .{}, ctx, exception); + return transpiler; + } + + if (object.getIfPropertyExists(ctx.ptr(), "define")) |define| { + define: { + if (define.isUndefinedOrNull()) { + break :define; + } + + if (!define.isObject()) { + JSC.throwInvalidArguments("define must be an object", .{}, ctx, exception); + return transpiler; + } + + var array = JSC.C.JSObjectCopyPropertyNames(globalThis.ref(), define.asObjectRef()); + defer JSC.C.JSPropertyNameArrayRelease(array); + const count = JSC.C.JSPropertyNameArrayGetCount(array); + // cannot be a temporary because it may be loaded on different threads. + var map_entries = allocator.alloc([]u8, count * 2) catch unreachable; + var names = map_entries[0..count]; + + var values = map_entries[count..]; + + var i: usize = 0; + while (i < count) : (i += 1) { + var property_name_ref = JSC.C.JSPropertyNameArrayGetNameAtIndex( + array, + i, + ); + defer JSC.C.JSStringRelease(property_name_ref); + const prop: []const u8 = JSC.C.JSStringGetCharacters8Ptr(property_name_ref)[0..JSC.C.JSStringGetLength(property_name_ref)]; + const property_value: JSC.JSValue = JSC.JSValue.fromRef( + JSC.C.JSObjectGetProperty( + globalThis.ref(), + define.asObjectRef(), + property_name_ref, + null, + ), + ); + const value_type = property_value.jsType(); + + if (!value_type.isStringLike()) { + JSC.throwInvalidArguments("define \"{s}\" must be a JSON string", .{prop}, ctx, exception); + return transpiler; + } + names[i] = allocator.dupe(u8, prop) catch unreachable; + var val = JSC.ZigString.init(""); + property_value.toZigString(&val, globalThis); + if (val.len == 0) { + val = JSC.ZigString.init("\"\""); + } + values[i] = std.fmt.allocPrint(allocator, "{}", .{val}) catch unreachable; + } + transpiler.transform.define = Api.StringMap{ + .keys = names, + .values = values, + }; + } + } + + if (object.get(globalThis, "external")) |external| { + external: { + if (external.isUndefinedOrNull()) break :external; + + const toplevel_type = external.jsType(); + if (toplevel_type.isStringLike()) { + var zig_str = JSC.ZigString.init(""); + external.toZigString(&zig_str, globalThis); + if (zig_str.len == 0) break :external; + var single_external = allocator.alloc(string, 1) catch unreachable; + single_external[0] = std.fmt.allocPrint(allocator, "{}", .{external}) catch unreachable; + transpiler.transform.external = single_external; + } else if (toplevel_type.isArray()) { + const count = external.getLengthOfArray(globalThis); + if (count == 0) break :external; + + var externals = allocator.alloc(string, count) catch unreachable; + var iter = external.arrayIterator(globalThis); + var i: usize = 0; + while (iter.next()) |entry| { + if (!entry.jsType().isStringLike()) { + JSC.throwInvalidArguments("external must be a string or string[]", .{}, ctx, exception); + return transpiler; + } + + var zig_str = JSC.ZigString.init(""); + entry.toZigString(&zig_str, globalThis); + if (zig_str.len == 0) continue; + externals[i] = std.fmt.allocPrint(allocator, "{}", .{external}) catch unreachable; + i += 1; + } + + transpiler.transform.external = externals[0..i]; + } else { + JSC.throwInvalidArguments("external must be a string or string[]", .{}, ctx, exception); + return transpiler; + } + } + } + + if (object.get(globalThis, "loader")) |loader| { + if (Loader.fromJS(globalThis, loader, exception)) |resolved| { + if (!resolved.isJavaScriptLike()) { + JSC.throwInvalidArguments("only JavaScript-like loaders supported for now", .{}, ctx, exception); + return transpiler; + } + + transpiler.default_loader = resolved; + } + + if (exception.* != null) { + return transpiler; + } + } + + if (object.get(globalThis, "platform")) |platform| { + if (Platform.fromJS(globalThis, platform, exception)) |resolved| { + transpiler.transform.platform = resolved.toAPI(); + } + + if (exception.* != null) { + return transpiler; + } + } + + if (object.get(globalThis, "tsconfig")) |tsconfig| { + tsconfig: { + if (tsconfig.isUndefinedOrNull()) break :tsconfig; + const kind = tsconfig.jsType(); + var out = JSC.ZigString.init(""); + + if (kind.isArray()) { + JSC.throwInvalidArguments("tsconfig must be a string or object", .{}, ctx, exception); + return transpiler; + } + + if (!kind.isStringLike()) { + tsconfig.jsonStringify(globalThis, 0, &out); + } else { + tsconfig.toZigString(&out, globalThis); + } + + if (out.len == 0) break :tsconfig; + transpiler.tsconfig_buf = std.fmt.allocPrint(allocator, "{}", .{out}) catch unreachable; + + // TODO: JSC -> Ast conversion + if (TSConfigJSON.parse( + allocator, + &transpiler.log, + logger.Source.initPathString("tsconfig.json", transpiler.tsconfig_buf), + &VirtualMachine.vm.bundler.resolver.caches.json, + true, + ) catch null) |parsed_tsconfig| { + transpiler.tsconfig = parsed_tsconfig; + } + } + } + + transpiler.runtime.allow_runtime = false; + + if (object.getIfPropertyExists(globalThis, "macro")) |macros| { + macros: { + if (macros.isUndefinedOrNull()) break :macros; + const kind = macros.jsType(); + const is_object = kind.isObject(); + if (!(kind.isStringLike() or is_object)) { + JSC.throwInvalidArguments("macro must be an object", .{}, ctx, exception); + return transpiler; + } + + var out: ZigString = ZigString.init(""); + // TODO: write a converter between JSC types and Bun AST types + if (is_object) { + macros.jsonStringify(globalThis, 0, &out); + } else { + macros.toZigString(&out, globalThis); + } + + if (out.len == 0) break :macros; + transpiler.macros_buf = std.fmt.allocPrint(allocator, "{}", .{out}) catch unreachable; + const source = logger.Source.initPathString("macros.json", transpiler.macros_buf); + const json = (VirtualMachine.vm.bundler.resolver.caches.json.parseJSON( + &transpiler.log, + source, + allocator, + ) catch null) orelse break :macros; + transpiler.macro_map = PackageJSON.parseMacrosJSON(allocator, json, &transpiler.log, &source); + } + } + + if (object.get(globalThis, "autoImportJSX")) |flag| { + transpiler.runtime.auto_import_jsx = flag.toBoolean(); + } + + if (object.get(globalThis, "allowBunRuntime")) |flag| { + transpiler.runtime.allow_runtime = flag.toBoolean(); + } + + if (object.get(globalThis, "jsxOptimizationInline")) |flag| { + transpiler.runtime.jsx_optimization_inline = flag.toBoolean(); + } + + if (object.get(globalThis, "jsxOptimizationHoist")) |flag| { + transpiler.runtime.jsx_optimization_hoist = flag.toBoolean(); + + if (!transpiler.runtime.jsx_optimization_inline and transpiler.runtime.jsx_optimization_hoist) { + JSC.throwInvalidArguments("jsxOptimizationHoist requires jsxOptimizationInline", .{}, ctx, exception); + return transpiler; + } + } + + if (object.get(globalThis, "sourcemap")) |flag| { + if (flag.isBoolean() or flag.isUndefinedOrNull()) { + if (flag.toBoolean()) { + transpiler.transform.source_map = Api.SourceMapMode.external; + } else { + transpiler.transform.source_map = Api.SourceMapMode.inline_into_file; + } + } else { + var sourcemap = flag.toSlice(globalThis, allocator); + if (options.SourceMapOption.map.get(sourcemap.slice())) |source| { + transpiler.transform.source_map = source.toAPI(); + } else { + JSC.throwInvalidArguments("sourcemap must be one of \"inline\", \"external\", or \"none\"", .{}, ctx, exception); + return transpiler; + } + } + } + + var tree_shaking: ?bool = null; + if (object.get(globalThis, "treeShaking")) |treeShaking| { + tree_shaking = treeShaking.toBoolean(); + } + + var trim_unused_imports: ?bool = null; + if (object.get(globalThis, "trimUnusedImports")) |trimUnusedImports| { + trim_unused_imports = trimUnusedImports.toBoolean(); + } + + if (object.getTruthy(globalThis, "exports")) |exports| { + if (!exports.isObject()) { + JSC.throwInvalidArguments("exports must be an object", .{}, ctx, exception); + return transpiler; + } + + var replacements = Runtime.Features.ReplaceableExport.Map{}; + errdefer replacements.clearAndFree(bun.default_allocator); + + if (exports.getTruthy(globalThis, "eliminate")) |eliminate| { + if (!eliminate.jsType().isArray()) { + JSC.throwInvalidArguments("exports.eliminate must be an array", .{}, ctx, exception); + return transpiler; + } + + var total_name_buf_len: u32 = 0; + var string_count: u32 = 0; + var iter = JSC.JSArrayIterator.init(eliminate, globalThis); + { + var length_iter = iter; + while (length_iter.next()) |value| { + if (value.isString()) { + const length = value.getLengthOfArray(globalThis); + string_count += @as(u32, @boolToInt(length > 0)); + total_name_buf_len += length; + } + } + } + + if (total_name_buf_len > 0) { + var buf = try std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, total_name_buf_len); + try replacements.ensureUnusedCapacity(bun.default_allocator, string_count); + { + var length_iter = iter; + while (length_iter.next()) |value| { + if (!value.isString()) continue; + var str = value.getZigString(globalThis); + if (str.len == 0) continue; + const name = std.fmt.bufPrint(buf.items.ptr[buf.items.len..buf.capacity], "{}", .{str}) catch { + JSC.throwInvalidArguments("Error reading exports.eliminate. TODO: utf-16", .{}, ctx, exception); + return transpiler; + }; + buf.items.len += name.len; + if (name.len > 0) { + replacements.putAssumeCapacity(name, .{ .delete = .{} }); + } + } + } + } + } + + if (exports.getTruthy(globalThis, "replace")) |replace| { + if (!replace.isObject()) { + JSC.throwInvalidArguments("replace must be an object", .{}, ctx, exception); + return transpiler; + } + + var total_name_buf_len: usize = 0; + + var array = js.JSObjectCopyPropertyNames(ctx, replace.asObjectRef()); + defer js.JSPropertyNameArrayRelease(array); + const property_names_count = @intCast(u32, js.JSPropertyNameArrayGetCount(array)); + var iter = JSC.JSPropertyNameIterator{ + .array = array, + .count = @intCast(u32, property_names_count), + }; + + { + var key_iter = iter; + while (key_iter.next()) |item| { + total_name_buf_len += JSC.C.JSStringGetLength(item); + } + } + + if (total_name_buf_len > 0) { + var total_name_buf = try std.ArrayList(u8).initCapacity(bun.default_allocator, total_name_buf_len); + errdefer total_name_buf.clearAndFree(); + + try replacements.ensureUnusedCapacity(bun.default_allocator, property_names_count); + defer { + if (exception.* != null) { + total_name_buf.clearAndFree(); + replacements.clearAndFree(bun.default_allocator); + } + } + + while (iter.next()) |item| { + const start = total_name_buf.items.len; + total_name_buf.items.len += @maximum( + // this returns a null terminated string + JSC.C.JSStringGetUTF8CString(item, total_name_buf.items.ptr + start, total_name_buf.capacity - start), + 1, + ) - 1; + JSC.C.JSStringRelease(item); + const key = total_name_buf.items[start..total_name_buf.items.len]; + // if somehow the string is empty, skip it + if (key.len == 0) + continue; + + const value = replace.get(globalThis, key).?; + if (value.isEmpty()) continue; + + if (!JSLexer.isIdentifier(key)) { + JSC.throwInvalidArguments("\"{s}\" is not a valid ECMAScript identifier", .{key}, ctx, exception); + total_name_buf.deinit(); + return transpiler; + } + + var entry = replacements.getOrPutAssumeCapacity(key); + + if (exportReplacementValue(value, globalThis)) |expr| { + entry.value_ptr.* = .{ .replace = expr }; + continue; + } + + if (value.isObject() and value.getLengthOfArray(ctx.ptr()) == 2) { + const replacementValue = JSC.JSObject.getIndex(value, globalThis, 1); + if (exportReplacementValue(replacementValue, globalThis)) |to_replace| { + const replacementKey = JSC.JSObject.getIndex(value, globalThis, 0); + var slice = (try replacementKey.toSlice(globalThis, bun.default_allocator).cloneIfNeeded()); + var replacement_name = slice.slice(); + + if (!JSLexer.isIdentifier(replacement_name)) { + JSC.throwInvalidArguments("\"{s}\" is not a valid ECMAScript identifier", .{replacement_name}, ctx, exception); + total_name_buf.deinit(); + slice.deinit(); + return transpiler; + } + + entry.value_ptr.* = .{ + .inject = .{ + .name = replacement_name, + .value = to_replace, + }, + }; + continue; + } + } + + JSC.throwInvalidArguments("exports.replace values can only be string, null, undefined, number or boolean", .{}, ctx, exception); + return transpiler; + } + } + } + + tree_shaking = tree_shaking orelse (replacements.count() > 0); + transpiler.runtime.replace_exports = replacements; + } + + transpiler.tree_shaking = tree_shaking orelse false; + transpiler.trim_unused_imports = trim_unused_imports orelse transpiler.tree_shaking; + + return transpiler; +} + +pub fn constructor( + ctx: js.JSContextRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) js.JSObjectRef { + var temp = std.heap.ArenaAllocator.init(getAllocator(ctx)); + var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); + defer temp.deinit(); + const transpiler_options: TranspilerOptions = if (arguments.len > 0) + transformOptionsFromJSC(ctx, temp.allocator(), &args, exception) catch { + JSC.throwInvalidArguments("Failed to create transpiler", .{}, ctx, exception); + return null; + } + else + TranspilerOptions{ .log = logger.Log.init(getAllocator(ctx)) }; + + if (exception.* != null) { + return null; + } + + if ((transpiler_options.log.warnings + transpiler_options.log.errors) > 0) { + var out_exception = transpiler_options.log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to create transpiler"); + exception.* = out_exception.asObjectRef(); + return null; + } + + var log = getAllocator(ctx).create(logger.Log) catch unreachable; + log.* = transpiler_options.log; + var bundler = Bundler.Bundler.init( + getAllocator(ctx), + log, + transpiler_options.transform, + null, + JavaScript.VirtualMachine.vm.bundler.env, + ) catch |err| { + if ((log.warnings + log.errors) > 0) { + var out_exception = log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to create transpiler"); + exception.* = out_exception.asObjectRef(); + return null; + } + + JSC.throwInvalidArguments("Error creating transpiler: {s}", .{@errorName(err)}, ctx, exception); + return null; + }; + + bundler.configureLinkerWithAutoJSX(false); + bundler.options.env.behavior = .disable; + bundler.configureDefines() catch |err| { + if ((log.warnings + log.errors) > 0) { + var out_exception = log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to load define"); + exception.* = out_exception.asObjectRef(); + return null; + } + + JSC.throwInvalidArguments("Failed to load define: {s}", .{@errorName(err)}, ctx, exception); + return null; + }; + + if (transpiler_options.macro_map.count() > 0) { + bundler.options.macro_remap = transpiler_options.macro_map; + } + + bundler.options.tree_shaking = transpiler_options.tree_shaking; + bundler.options.trim_unused_imports = transpiler_options.trim_unused_imports; + bundler.options.allow_runtime = transpiler_options.runtime.allow_runtime; + bundler.options.auto_import_jsx = transpiler_options.runtime.auto_import_jsx; + bundler.options.hot_module_reloading = transpiler_options.runtime.hot_module_reloading; + bundler.options.jsx.supports_fast_refresh = bundler.options.hot_module_reloading and + bundler.options.allow_runtime and transpiler_options.runtime.react_fast_refresh; + + var transpiler = getAllocator(ctx).create(Transpiler) catch unreachable; + transpiler.* = Transpiler{ + .transpiler_options = transpiler_options, + .bundler = bundler, + .arena = args.arena, + .scan_pass_result = ScanPassResult.init(getAllocator(ctx)), + }; + + return Class.make(ctx, transpiler); +} + +pub fn finalize( + this: *Transpiler, +) void { + this.bundler.log.deinit(); + this.scan_pass_result.named_imports.deinit(); + this.scan_pass_result.import_records.deinit(); + this.scan_pass_result.used_symbols.deinit(); + if (this.buffer_writer != null) { + this.buffer_writer.?.buffer.deinit(); + } + + // bun.default_allocator.free(this.transpiler_options.tsconfig_buf); + // bun.default_allocator.free(this.transpiler_options.macros_buf); + this.arena.deinit(); +} + +fn getParseResult(this: *Transpiler, allocator: std.mem.Allocator, code: []const u8, loader: ?Loader, macro_js_ctx: JSValue) ?Bundler.ParseResult { + const name = this.transpiler_options.default_loader.stdinName(); + const source = logger.Source.initPathString(name, code); + + const jsx = if (this.transpiler_options.tsconfig != null) + this.transpiler_options.tsconfig.?.mergeJSX(this.bundler.options.jsx) + else + this.bundler.options.jsx; + + const parse_options = Bundler.Bundler.ParseOptions{ + .allocator = allocator, + .macro_remappings = this.transpiler_options.macro_map, + .dirname_fd = 0, + .file_descriptor = null, + .loader = loader orelse this.transpiler_options.default_loader, + .jsx = jsx, + .path = source.path, + .virtual_source = &source, + .replace_exports = this.transpiler_options.runtime.replace_exports, + .macro_js_ctx = macro_js_ctx, + // .allocator = this. + }; + + var parse_result = this.bundler.parse(parse_options, null); + + // necessary because we don't run the linker + if (parse_result) |*res| { + for (res.ast.import_records) |*import| { + if (import.kind.isCommonJS()) { + import.wrap_with_to_module = true; + import.module_id = @truncate(u32, std.hash.Wyhash.hash(0, import.path.pretty)); + } + } + } + + return parse_result; +} + +pub fn scan( + this: *Transpiler, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) JSC.C.JSObjectRef { + var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); + defer args.arena.deinit(); + const code_arg = args.next() orelse { + JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); + return null; + }; + + const code_holder = JSC.Node.StringOrBuffer.fromJS(ctx.ptr(), args.arena.allocator(), code_arg, exception) orelse { + if (exception.* == null) JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); + return null; + }; + + const code = code_holder.slice(); + args.eat(); + const loader: ?Loader = brk: { + if (args.next()) |arg| { + args.eat(); + break :brk Loader.fromJS(ctx.ptr(), arg, exception); + } + + break :brk null; + }; + + if (exception.* != null) return null; + + var arena = Mimalloc.Arena.init() catch unreachable; + var prev_allocator = this.bundler.allocator; + this.bundler.setAllocator(arena.allocator()); + var log = logger.Log.init(arena.backingAllocator()); + defer log.deinit(); + this.bundler.setLog(&log); + defer { + this.bundler.setLog(&this.transpiler_options.log); + this.bundler.setAllocator(prev_allocator); + arena.deinit(); + } + + defer { + JSAst.Stmt.Data.Store.reset(); + JSAst.Expr.Data.Store.reset(); + } + + const parse_result = getParseResult(this, arena.allocator(), code, loader, JSC.JSValue.zero) orelse { + if ((this.bundler.log.warnings + this.bundler.log.errors) > 0) { + var out_exception = this.bundler.log.toJS(ctx.ptr(), getAllocator(ctx), "Parse error"); + exception.* = out_exception.asObjectRef(); + return null; + } + + JSC.throwInvalidArguments("Failed to parse", .{}, ctx, exception); + return null; + }; + + if ((this.bundler.log.warnings + this.bundler.log.errors) > 0) { + var out_exception = this.bundler.log.toJS(ctx.ptr(), getAllocator(ctx), "Parse error"); + exception.* = out_exception.asObjectRef(); + return null; + } + + const exports_label = JSC.ZigString.init("exports"); + const imports_label = JSC.ZigString.init("imports"); + const named_imports_value = namedImportsToJS( + ctx.ptr(), + parse_result.ast.import_records, + exception, + ); + if (exception.* != null) return null; + var named_exports_value = namedExportsToJS( + ctx.ptr(), + parse_result.ast.named_exports, + ); + return JSC.JSValue.createObject2(ctx.ptr(), &imports_label, &exports_label, named_imports_value, named_exports_value).asObjectRef(); +} + +// pub fn build( +// this: *Transpiler, +// ctx: js.JSContextRef, +// _: js.JSObjectRef, +// _: js.JSObjectRef, +// arguments: []const js.JSValueRef, +// exception: js.ExceptionRef, +// ) JSC.C.JSObjectRef {} + +pub fn transform( + this: *Transpiler, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) JSC.C.JSObjectRef { + var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); + defer args.arena.deinit(); + const code_arg = args.next() orelse { + JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); + return null; + }; + + const code_holder = JSC.Node.StringOrBuffer.fromJS(ctx.ptr(), this.arena.allocator(), code_arg, exception) orelse { + if (exception.* == null) JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); + return null; + }; + + const code = code_holder.slice(); + args.eat(); + const loader: ?Loader = brk: { + if (args.next()) |arg| { + args.eat(); + break :brk Loader.fromJS(ctx.ptr(), arg, exception); + } + + break :brk null; + }; + + if (exception.* != null) return null; + if (code_holder == .string) { + JSC.C.JSValueProtect(ctx, arguments[0]); + } + + var task = TransformTask.create(this, if (code_holder == .string) arguments[0] else null, ctx.ptr(), ZigString.init(code), loader orelse this.transpiler_options.default_loader) catch return null; + task.schedule(); + return task.promise.asObjectRef(); +} + +pub fn transformSync( + this: *Transpiler, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) JSC.C.JSObjectRef { + var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); + defer args.arena.deinit(); + const code_arg = args.next() orelse { + JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); + return null; + }; + + var arena = Mimalloc.Arena.init() catch unreachable; + defer arena.deinit(); + const code_holder = JSC.Node.StringOrBuffer.fromJS(ctx.ptr(), arena.allocator(), code_arg, exception) orelse { + if (exception.* == null) JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); + return null; + }; + + const code = code_holder.slice(); + JSC.JSValue.c(arguments[0]).ensureStillAlive(); + defer JSC.JSValue.c(arguments[0]).ensureStillAlive(); + + args.eat(); + var js_ctx_value: JSC.JSValue = JSC.JSValue.zero; + const loader: ?Loader = brk: { + if (args.next()) |arg| { + args.eat(); + if (arg.isNumber() or arg.isString()) { + break :brk Loader.fromJS(ctx.ptr(), arg, exception); + } + + if (arg.isObject()) { + js_ctx_value = arg; + break :brk null; + } + } + + break :brk null; + }; + + if (args.nextEat()) |arg| { + if (arg.isObject()) { + js_ctx_value = arg; + } else { + JSC.throwInvalidArguments("Expected a Loader or object", .{}, ctx, exception); + return null; + } + } + if (!js_ctx_value.isEmpty()) { + js_ctx_value.ensureStillAlive(); + } + + defer { + if (!js_ctx_value.isEmpty()) { + js_ctx_value.ensureStillAlive(); + } + } + + if (exception.* != null) return null; + + JSAst.Stmt.Data.Store.reset(); + JSAst.Expr.Data.Store.reset(); + defer { + JSAst.Stmt.Data.Store.reset(); + JSAst.Expr.Data.Store.reset(); + } + + var prev_bundler = this.bundler; + this.bundler.setAllocator(arena.allocator()); + this.bundler.macro_context = null; + var log = logger.Log.init(arena.backingAllocator()); + this.bundler.setLog(&log); + + defer { + this.bundler = prev_bundler; + } + + var parse_result = getParseResult( + this, + arena.allocator(), + code, + loader, + js_ctx_value, + ) orelse { + if ((this.bundler.log.warnings + this.bundler.log.errors) > 0) { + var out_exception = this.bundler.log.toJS(ctx.ptr(), getAllocator(ctx), "Parse error"); + exception.* = out_exception.asObjectRef(); + return null; + } + + JSC.throwInvalidArguments("Failed to parse", .{}, ctx, exception); + return null; + }; + + if ((this.bundler.log.warnings + this.bundler.log.errors) > 0) { + var out_exception = this.bundler.log.toJS(ctx.ptr(), getAllocator(ctx), "Parse error"); + exception.* = out_exception.asObjectRef(); + return null; + } + + var buffer_writer = this.buffer_writer orelse brk: { + var writer = JSPrinter.BufferWriter.init(arena.backingAllocator()) catch { + JSC.throwInvalidArguments("Failed to create BufferWriter", .{}, ctx, exception); + return null; + }; + + writer.buffer.growIfNeeded(code.len) catch unreachable; + writer.buffer.list.expandToCapacity(); + break :brk writer; + }; + + defer { + this.buffer_writer = buffer_writer; + } + + buffer_writer.reset(); + var printer = JSPrinter.BufferPrinter.init(buffer_writer); + _ = this.bundler.print(parse_result, @TypeOf(&printer), &printer, .esm_ascii) catch |err| { + JSC.JSError(bun.default_allocator, "Failed to print code: {s}", .{@errorName(err)}, ctx, exception); + + return null; + }; + + // TODO: benchmark if pooling this way is faster or moving is faster + buffer_writer = printer.ctx; + var out = JSC.ZigString.init(buffer_writer.written); + out.mark(); + + return out.toValueGC(ctx.ptr()).asObjectRef(); +} + +fn namedExportsToJS(global: *JSGlobalObject, named_exports: JSAst.Ast.NamedExports) JSC.JSValue { + if (named_exports.count() == 0) + return JSC.JSValue.fromRef(JSC.C.JSObjectMakeArray(global.ref(), 0, null, null)); + + var named_exports_iter = named_exports.iterator(); + var stack_fallback = std.heap.stackFallback(@sizeOf(JSC.ZigString) * 32, getAllocator(global.ref())); + var allocator = stack_fallback.get(); + var names = allocator.alloc( + JSC.ZigString, + named_exports.count(), + ) catch unreachable; + defer allocator.free(names); + var i: usize = 0; + while (named_exports_iter.next()) |entry| { + names[i] = JSC.ZigString.init(entry.key_ptr.*); + i += 1; + } + JSC.ZigString.sortAsc(names[0..i]); + return JSC.JSValue.createStringArray(global, names.ptr, names.len, true); +} + +const ImportRecord = @import("../../import_record.zig").ImportRecord; + +fn namedImportsToJS( + global: *JSGlobalObject, + import_records: []const ImportRecord, + exception: JSC.C.ExceptionRef, +) JSC.JSValue { + var stack_fallback = std.heap.stackFallback(@sizeOf(JSC.C.JSObjectRef) * 32, getAllocator(global.ref())); + var allocator = stack_fallback.get(); + + var i: usize = 0; + const path_label = JSC.ZigString.init("path"); + const kind_label = JSC.ZigString.init("kind"); + var array_items = allocator.alloc( + JSC.C.JSValueRef, + import_records.len, + ) catch unreachable; + defer allocator.free(array_items); + + for (import_records) |record| { + if (record.is_internal) continue; + + const path = JSC.ZigString.init(record.path.text).toValueGC(global); + const kind = JSC.ZigString.init(record.kind.label()).toValue(global); + array_items[i] = JSC.JSValue.createObject2(global, &path_label, &kind_label, path, kind).asObjectRef(); + i += 1; + } + + return JSC.JSValue.fromRef(JSC.C.JSObjectMakeArray(global.ref(), i, array_items.ptr, exception)); +} + +pub fn scanImports( + this: *Transpiler, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, +) JSC.C.JSObjectRef { + var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); + const code_arg = args.next() orelse { + JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); + return null; + }; + + const code_holder = JSC.Node.StringOrBuffer.fromJS(ctx.ptr(), args.arena.allocator(), code_arg, exception) orelse { + if (exception.* == null) JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); + return null; + }; + args.eat(); + const code = code_holder.slice(); + + var loader: Loader = this.transpiler_options.default_loader; + if (args.next()) |arg| { + if (Loader.fromJS(ctx.ptr(), arg, exception)) |_loader| { + loader = _loader; + } + args.eat(); + } + + if (!loader.isJavaScriptLike()) { + JSC.throwInvalidArguments("Only JavaScript-like files support this fast path", .{}, ctx, exception); + return null; + } + + if (exception.* != null) return null; + + var arena = Mimalloc.Arena.init() catch unreachable; + var prev_allocator = this.bundler.allocator; + this.bundler.setAllocator(arena.allocator()); + var log = logger.Log.init(arena.backingAllocator()); + defer log.deinit(); + this.bundler.setLog(&log); + defer { + this.bundler.setLog(&this.transpiler_options.log); + this.bundler.setAllocator(prev_allocator); + arena.deinit(); + } + + const source = logger.Source.initPathString(loader.stdinName(), code); + var bundler = &this.bundler; + const jsx = if (this.transpiler_options.tsconfig != null) + this.transpiler_options.tsconfig.?.mergeJSX(this.bundler.options.jsx) + else + this.bundler.options.jsx; + + var opts = JSParser.Parser.Options.init(jsx, loader); + if (this.bundler.macro_context == null) { + this.bundler.macro_context = JSAst.Macro.MacroContext.init(&this.bundler); + } + opts.macro_context = &this.bundler.macro_context.?; + + JSAst.Stmt.Data.Store.reset(); + JSAst.Expr.Data.Store.reset(); + + defer { + JSAst.Stmt.Data.Store.reset(); + JSAst.Expr.Data.Store.reset(); + } + + bundler.resolver.caches.js.scan( + bundler.allocator, + &this.scan_pass_result, + opts, + bundler.options.define, + &log, + &source, + ) catch |err| { + defer this.scan_pass_result.reset(); + if ((log.warnings + log.errors) > 0) { + var out_exception = log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to scan imports"); + exception.* = out_exception.asObjectRef(); + return null; + } + + JSC.throwInvalidArguments("Failed to scan imports: {s}", .{@errorName(err)}, ctx, exception); + return null; + }; + + defer this.scan_pass_result.reset(); + + if ((log.warnings + log.errors) > 0) { + var out_exception = log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to scan imports"); + exception.* = out_exception.asObjectRef(); + return null; + } + + const named_imports_value = namedImportsToJS( + ctx.ptr(), + this.scan_pass_result.import_records.items, + exception, + ); + if (exception.* != null) return null; + return named_imports_value.asObjectRef(); +} |