From abc52da7bb19647ff3bfba59508ac57aa37135b2 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:31:51 -0700 Subject: [PATCH 001/130] add check for React.useState --- src/js_parser.zig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/js_parser.zig b/src/js_parser.zig index 6bc4f2d803e3..7b97405ad7fe 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -17528,7 +17528,16 @@ fn NewParser_( // Why? Because we *don't* want to check for uses of // `useState` _inside_ React, and we know React uses // commonjs so it will never be `.e_import_identifier`. - e_.target.data == .e_import_identifier) { + check_for_usestate: { + if (e_.target.data == .e_import_identifier) break :check_for_usestate true; + // Also check for `React.useState(...)` + if (e_.target.data == .e_dot and e_.target.data.e_dot.target.data == .e_import_identifier) { + const id = e_.target.data.e_dot.target.data.e_import_identifier; + const name = p.symbols.items[id.ref.innerIndex()].original_name; + break :check_for_usestate bun.strings.eqlComptime(name, "React"); + } + break :check_for_usestate false; + }) { bun.assert(p.options.features.server_components.isServerSide()); if (bun.strings.eqlComptime(original_name, "useState")) { p.log.addError( From 381848cd69fe0a4242f4a88a9b1f6358b0376fbf Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:40:46 -0700 Subject: [PATCH 002/130] WIP less noisy errors --- src/bake/bun-framework-react/server.tsx | 8 +++++++- src/bake/bun-framework-react/ssr.tsx | 3 ++- src/js/builtins/Bake.ts | 10 ++++++++++ src/js_parser.zig | 4 +++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index ed4a6c1809f6..5957768b0ebd 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -128,7 +128,13 @@ export async function prerender(meta: Bake.RouteMetadata) { let rscChunks: Array = [int.buffer as ArrayBuffer, meta.styles.join("\n")]; rscPayload.on("data", chunk => rscChunks.push(chunk)); - const html = await renderToStaticHtml(rscPayload, meta.modules); + let html; + try { + html = await renderToStaticHtml(rscPayload, meta.modules); + } catch (err) { + console.error("ah fuck"); + return undefined; + } const rsc = new Blob(rscChunks, { type: "text/x-component" }); return { diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index 11fa09170369..7bf06193a477 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -284,7 +284,8 @@ class StaticRscInjectionStream extends EventEmitter { } destroy(error) { - console.error(error); + // We don't need to console.error here as react does ti itself + // console.error(error); this.reject(error); } } diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index 8b48b607070e..7dca2f9c4f7b 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -62,6 +62,9 @@ export async function renderRoutesForProdStatic( pageModule, params, } satisfies Bake.RouteMetadata); + if (results === undefined) { + return true; + } if (results == null) { throw new Error(`Route ${JSON.stringify(sourceRouteFiles[i])} cannot be pre-rendered to a static page.`); } @@ -151,6 +154,13 @@ export async function renderRoutesForProdStatic( } } } else { + // for (const params of paramGetter.pages) { + // const failed = await callRouteGenerator(type, noClient, i, layouts, pageModule, params); + // if (failed) { + // return; + // } + // } + // TODO: error signal here? await Promise.all( paramGetter.pages.map(params => { callRouteGenerator(type, noClient, i, layouts, pageModule, params); diff --git a/src/js_parser.zig b/src/js_parser.zig index 7b97405ad7fe..5a3d521d0aaf 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -17539,7 +17539,9 @@ fn NewParser_( break :check_for_usestate false; }) { bun.assert(p.options.features.server_components.isServerSide()); - if (bun.strings.eqlComptime(original_name, "useState")) { + if (!bun.strings.startsWith(p.source.path.pretty, "node_modules") and + bun.strings.eqlComptime(original_name, "useState")) + { p.log.addError( p.source, expr.loc, From b92555e09972c48ecdc1fb424f1cc1ddb7325d88 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:35:23 -0700 Subject: [PATCH 003/130] better error message --- src/bake/production.zig | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/bake/production.zig b/src/bake/production.zig index a5dffd479f54..880b2b4e2d17 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -184,11 +184,33 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa const default = BakeGetDefaultExportFromModule(vm.global, config_entry_point_string.toJS(vm.global)); if (!default.isObject()) { - Output.panic("TODO: print this error better, default export is not an object", .{}); + Output.panic( + \\Your config file's default export must be an object. + \\ + \\Example: + \\ export default { + \\ app: { + \\ framework: "react", + \\ } + \\ } + \\ + \\Learn more at https://bun.com/docs/ssg + , .{}); } const app = try default.get(vm.global, "app") orelse { - Output.panic("TODO: print this error better, default export needs an 'app' object", .{}); + Output.panic( + \\Your config file's default export must contain an "app" property. + \\ + \\Example: + \\ export default { + \\ app: { + \\ framework: "react", + \\ } + \\ } + \\ + \\Learn more at https://bun.com/docs/ssg + , .{}); }; break :config try bake.UserOptions.fromJS(app, vm.global); @@ -409,7 +431,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa }, .asset => {}, .bytecode => {}, - .sourcemap => @panic("TODO: register source map"), + .sourcemap => {}, } }, } From e992b804c81b1f8fd8bdb5e983e92277943c7427 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:47:55 -0700 Subject: [PATCH 004/130] fix --- src/bake/production.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bake/production.zig b/src/bake/production.zig index 880b2b4e2d17..c8a9efbc8486 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -184,7 +184,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa const default = BakeGetDefaultExportFromModule(vm.global, config_entry_point_string.toJS(vm.global)); if (!default.isObject()) { - Output.panic( + return global.throwInvalidArguments( \\Your config file's default export must be an object. \\ \\Example: @@ -199,7 +199,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa } const app = try default.get(vm.global, "app") orelse { - Output.panic( + return global.throwInvalidArguments( \\Your config file's default export must contain an "app" property. \\ \\Example: From c57be8dcdb90024292e61e82db94063bc8d5640e Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sun, 27 Jul 2025 00:57:29 -0700 Subject: [PATCH 005/130] extra option --- src/bake.zig | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/bake.zig b/src/bake.zig index ac5f5c299790..c7cf6de531dd 100644 --- a/src/bake.zig +++ b/src/bake.zig @@ -27,9 +27,6 @@ pub const UserOptions = struct { /// Currently, this function must run at the top of the event loop. pub fn fromJS(config: JSValue, global: *jsc.JSGlobalObject) !UserOptions { - if (!config.isObject()) { - return global.throwInvalidArguments("'" ++ api_name ++ "' is not an object", .{}); - } var arena = std.heap.ArenaAllocator.init(bun.default_allocator); errdefer arena.deinit(); const alloc = arena.allocator(); @@ -38,6 +35,38 @@ pub const UserOptions = struct { errdefer allocations.free(); var bundler_options = SplitBundlerOptions.empty; + if (!config.isObject()) { + // Allow users to do `export default { app: 'react' }` for convenience + if (config.isString()) { + const bunstr = try config.toBunString(global); + defer bunstr.deref(); + const utf8_string = bunstr.toUTF8(bun.default_allocator); + defer utf8_string.deinit(); + + if (bun.strings.eql(utf8_string.byteSlice(), "react")) { + const root = bun.getcwdAlloc(alloc) catch |err| switch (err) { + error.OutOfMemory => { + return global.throwOutOfMemory(); + }, + else => { + return global.throwError(err, "while querying current working directory"); + }, + }; + + const framework = try Framework.react(alloc); + + return UserOptions{ + .arena = arena, + .allocations = allocations, + .root = try alloc.dupeZ(u8, root), + .framework = framework, + .bundler_options = bundler_options, + }; + } + } + return global.throwInvalidArguments("'" ++ api_name ++ "' is not an object", .{}); + } + if (try config.getOptional(global, "bundlerOptions", JSValue)) |js_options| { if (try js_options.getOptional(global, "server", JSValue)) |server_options| { bundler_options.server = try BuildConfigSubset.fromJS(global, server_options); From 68be15361aeff1579b6c941161d99757c661b16d Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sun, 27 Jul 2025 01:19:53 -0700 Subject: [PATCH 006/130] fix use after free --- src/bun.js/api/server/RequestContext.zig | 5 ++++- src/bun.js/bindings/ZigStackFrame.zig | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/bun.js/api/server/RequestContext.zig b/src/bun.js/api/server/RequestContext.zig index 6bcab2874ddb..17bccf6d0984 100644 --- a/src/bun.js/api/server/RequestContext.zig +++ b/src/bun.js/api/server/RequestContext.zig @@ -1943,7 +1943,10 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, var vm: *jsc.VirtualMachine = this.server.?.vm; const globalThis = this.server.?.globalThis; if (comptime debug_mode) { - var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(this.allocator); + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(allocator); defer exception_list.deinit(); const prev_exception_list = vm.onUnhandledRejectionExceptionList; vm.onUnhandledRejectionExceptionList = &exception_list; diff --git a/src/bun.js/bindings/ZigStackFrame.zig b/src/bun.js/bindings/ZigStackFrame.zig index fdf933d7a096..0edf8479c8fd 100644 --- a/src/bun.js/bindings/ZigStackFrame.zig +++ b/src/bun.js/bindings/ZigStackFrame.zig @@ -20,7 +20,7 @@ pub const ZigStackFrame = extern struct { if (!this.function_name.isEmpty()) { var slicer = this.function_name.toUTF8(allocator); defer slicer.deinit(); - frame.function_name = (try slicer.clone(allocator)).slice(); + frame.function_name = try allocator.dupe(u8, slicer.slice()); } if (!this.source_url.isEmpty()) { From 740cdaba3d030dab39ca16a178c680f1236816e9 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:31:01 -0700 Subject: [PATCH 007/130] - fix catch-all routes not working in dev server - fix crash - fix require bug --- src/bake/DevServer.zig | 2 +- src/bake/hmr-module.ts | 5 ++ src/bun.js/api/server/RequestContext.zig | 21 ++++---- src/bun.js/bindings/BunString.cpp | 37 ++++++++++++++ src/bun.js/bindings/JSValue.zig | 7 +++ src/bun.js/bindings/ZigStackFrame.zig | 3 +- src/js_printer.zig | 3 ++ test/bake/dev/ssg-pages-router.test.ts | 63 ++++++++++++++++++++++++ 8 files changed, 128 insertions(+), 13 deletions(-) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 9664eb885891..798471de0055 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -1232,7 +1232,7 @@ fn onFrameworkRequestWithBundle( const value_str = bun.String.cloneUTF8(param.value); defer value_str.deref(); - obj.put(global, key_str, value_str.toJS(global)); + _ = try obj.putBunStringOneOrArray(global, &key_str, value_str.toJS(global)); } break :blk obj; } else JSValue.null; diff --git a/src/bake/hmr-module.ts b/src/bake/hmr-module.ts index 8c8b666eb327..1e3d6e511c58 100644 --- a/src/bake/hmr-module.ts +++ b/src/bake/hmr-module.ts @@ -115,6 +115,11 @@ export class HMRModule { : null; } + // Module Ids are pre-resolved by the bundler + requireResolve(id: Id): Id { + return id; + } + require(id: Id) { try { const mod = loadModuleSync(id, true, this); diff --git a/src/bun.js/api/server/RequestContext.zig b/src/bun.js/api/server/RequestContext.zig index 17bccf6d0984..98172cad9270 100644 --- a/src/bun.js/api/server/RequestContext.zig +++ b/src/bun.js/api/server/RequestContext.zig @@ -337,6 +337,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, pub fn renderDefaultError( this: *RequestContext, + arena_allocator: std.mem.Allocator, log: *logger.Log, err: anyerror, exceptions: []Api.JsException, @@ -351,12 +352,10 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, } } - const allocator = this.allocator; - - const fallback_container = allocator.create(Api.FallbackMessageContainer) catch unreachable; - defer allocator.destroy(fallback_container); + const fallback_container = arena_allocator.create(Api.FallbackMessageContainer) catch unreachable; + defer arena_allocator.destroy(fallback_container); fallback_container.* = Api.FallbackMessageContainer{ - .message = std.fmt.allocPrint(allocator, comptime Output.prettyFmt(fmt, false), args) catch unreachable, + .message = std.fmt.allocPrint(arena_allocator, comptime Output.prettyFmt(fmt, false), args) catch unreachable, .router = null, .reason = .fetch_event_handler, .cwd = VirtualMachine.get().transpiler.fs.top_level_dir, @@ -364,18 +363,19 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, .code = @as(u16, @truncate(@intFromError(err))), .name = @errorName(err), .exceptions = exceptions, - .build = log.toAPI(allocator) catch unreachable, + .build = log.toAPI(arena_allocator) catch unreachable, }, }; if (comptime fmt.len > 0) Output.prettyErrorln(fmt, args); Output.flush(); - var bb = std.ArrayList(u8).init(allocator); + // Explicitly use `this.allocator` and *not* the arena + var bb = std.ArrayList(u8).init(this.allocator); const bb_writer = bb.writer(); Fallback.renderBackend( - allocator, + arena_allocator, fallback_container, @TypeOf(bb_writer), bb_writer, @@ -1943,7 +1943,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, var vm: *jsc.VirtualMachine = this.server.?.vm; const globalThis = this.server.?.globalThis; if (comptime debug_mode) { - var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + var arena = std.heap.ArenaAllocator.init(this.allocator); defer arena.deinit(); const allocator = arena.allocator(); var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(allocator); @@ -1954,9 +1954,10 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, vm.onUnhandledRejectionExceptionList = prev_exception_list; this.renderDefaultError( + allocator, vm.log, error.ExceptionOcurred, - exception_list.toOwnedSlice() catch @panic("TODO"), + exception_list.items, "{s} - {} failed", .{ @as(string, @tagName(this.method)), this.ensurePathname() }, ); diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index e35e456dc0ac..396eef817df4 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -744,6 +744,43 @@ extern "C" [[ZIG_EXPORT(nothrow)]] void Bun__WTFStringImpl__ensureHash(WTF::Stri str->hash(); } +extern "C" JSC::EncodedJSValue JSC__JSValue__upsertBunStringArray( + JSC::EncodedJSValue encodedTarget, + JSC::JSGlobalObject* global, + const BunString* key, + JSC::EncodedJSValue encodedValue) +{ + auto scope = DECLARE_THROW_SCOPE(global->vm()); + JSC::JSObject* target = JSC::JSValue::decode(encodedTarget).getObject(); + RETURN_IF_EXCEPTION(scope, {}); + JSC::JSValue newValue = JSC::JSValue::decode(encodedValue); + auto& vm = global->vm(); + WTF::String str = key->tag == BunStringTag::Empty ? WTF::emptyString() : key->toWTFString(); + Identifier id = Identifier::fromString(vm, str); + auto existingValue = target->getIfPropertyExists(global, id); + RETURN_IF_EXCEPTION(scope, {}); + + if (!existingValue.isEmpty()) { + // If existing value is already an array, push to it + if (existingValue.isObject() && existingValue.getObject()->inherits()) { + JSC::JSArray* array = jsCast(existingValue.getObject()); + array->push(global, newValue); + } else { + // Create new array with both values + JSC::JSArray* array = JSC::constructEmptyArray(global, nullptr, 2); + array->putDirectIndex(global, 0, existingValue); + array->putDirectIndex(global, 1, newValue); + target->putDirect(vm, id, array, 0); + } + } else { + // No existing value, just put the new value directly + target->putDirect(vm, id, newValue, 0); + } + + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + extern "C" void JSC__JSValue__putBunString( JSC::EncodedJSValue encodedTarget, JSC::JSGlobalObject* global, diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index c3d3ad518ac8..039270668f9b 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -317,6 +317,13 @@ pub const JSValue = enum(i64) { JSC__JSValue__putBunString(value, global, key, result); } + extern "c" fn JSC__JSValue__upsertBunStringArray(value: JSValue, global: *JSGlobalObject, key: *const bun.String, result: jsc.JSValue) JSValue; + + /// Put key/val pair into `obj`. If `key` is already present on the object, create an array for the values. + pub fn putBunStringOneOrArray(obj: JSValue, global: *JSGlobalObject, key: *const bun.String, value: jsc.JSValue) bun.JSError!JSValue { + return fromJSHostCall(global, @src(), JSC__JSValue__upsertBunStringArray, .{ obj, global, key, value }); + } + pub fn put(value: JSValue, global: *JSGlobalObject, key: anytype, result: jsc.JSValue) void { const Key = @TypeOf(key); if (comptime @typeInfo(Key) == .pointer) { diff --git a/src/bun.js/bindings/ZigStackFrame.zig b/src/bun.js/bindings/ZigStackFrame.zig index 0edf8479c8fd..8b9a153d1393 100644 --- a/src/bun.js/bindings/ZigStackFrame.zig +++ b/src/bun.js/bindings/ZigStackFrame.zig @@ -19,8 +19,7 @@ pub const ZigStackFrame = extern struct { var frame: api.StackFrame = comptime std.mem.zeroes(api.StackFrame); if (!this.function_name.isEmpty()) { var slicer = this.function_name.toUTF8(allocator); - defer slicer.deinit(); - frame.function_name = try allocator.dupe(u8, slicer.slice()); + frame.function_name = (try slicer.cloneIfNeeded(allocator)).slice(); } if (!this.source_url.isEmpty()) { diff --git a/src/js_printer.zig b/src/js_printer.zig index 5d304f8def20..738ca549ed27 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -2330,6 +2330,9 @@ fn NewPrinter( if (p.options.require_ref) |require_ref| { p.printSymbol(require_ref); p.print(".resolve"); + } else if (p.options.module_type == .internal_bake_dev) { + p.printSymbol(p.options.hmr_ref); + p.print(".requireResolve"); } else { p.print("require.resolve"); } diff --git a/test/bake/dev/ssg-pages-router.test.ts b/test/bake/dev/ssg-pages-router.test.ts index ff592b4d680a..f8b2fbe8731f 100644 --- a/test/bake/dev/ssg-pages-router.test.ts +++ b/test/bake/dev/ssg-pages-router.test.ts @@ -324,3 +324,66 @@ export { Markdoc as default };`, expect(await c1.elemText("h1")).toBe("Welcome to SSG"); }, }); + +devTest("SSG pages router - catch-all routes [...slug]", { + framework: "react", + files: { + "pages/[...slug].tsx": ` + const CatchAllPage: Bun.SSGPage = ({ params }) => { + return ( +
+

Catch-all Route

+

{JSON.stringify(params)}

+
    + {params.slug && Array.isArray(params.slug) ? ( + params.slug.map((segment, index) => ( +
  • {segment}
  • + )) + ) : ( +
  • No slug array
  • + )} +
+
+ ); + }; + + export default CatchAllPage; + + export const getStaticPaths: Bun.GetStaticPaths = async () => { + return { + paths: [ + { params: { slug: ["docs"] } }, + { params: { slug: ["docs", "getting-started"] } }, + { params: { slug: ["docs", "api", "reference"] } }, + { params: { slug: ["blog", "2024", "january", "new-features"] } }, + ], + }; + }; + `, + }, + async test(dev) { + // Test single segment + await using c1 = await dev.client("/docs"); + expect(await c1.elemText("h1")).toBe("Catch-all Route"); + expect(await c1.elemText("#params")).toBe('{"slug":"docs"}'); + expect(await c1.elemsText("li")).toEqual(["No slug array"]); + + // Test two segments + await using c2 = await dev.client("/docs/getting-started"); + expect(await c2.elemText("h1")).toBe("Catch-all Route"); + expect(await c2.elemText("#params")).toBe('{"slug":["docs","getting-started"]}'); + expect(await c2.elemsText("li")).toEqual(["docs", "getting-started"]); + + // Test three segments + await using c3 = await dev.client("/docs/api/reference"); + expect(await c3.elemText("h1")).toBe("Catch-all Route"); + expect(await c3.elemText("#params")).toBe('{"slug":["docs","api","reference"]}'); + expect(await c3.elemsText("li")).toEqual(["docs", "api", "reference"]); + + // Test four segments + await using c4 = await dev.client("/blog/2024/january/new-features"); + expect(await c4.elemText("h1")).toBe("Catch-all Route"); + expect(await c4.elemText("#params")).toBe('{"slug":["blog","2024","january","new-features"]}'); + expect(await c4.elemsText("li")).toEqual(["blog", "2024", "january", "new-features"]); + }, +}); From b10fda04876760b6b18a775d0862ffb425a16a22 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:21:39 -0700 Subject: [PATCH 008/130] allow `app` configuration form `Bun.serve(...)` --- src/bun.js/api/BunObject.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index b7f593046d65..1dd8237903a6 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1014,7 +1014,7 @@ pub fn serve(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.J &config, &args, .{ - .allow_bake_config = bun.FeatureFlags.bake() and callframe.isFromBunMain(globalObject.vm()), + .allow_bake_config = bun.FeatureFlags.bake(), .is_fetch_required = true, .has_user_routes = false, }, From 24485fb4328862585fb9c945ab9700e4f92ce550 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:24:53 -0700 Subject: [PATCH 009/130] WIP --- src/bake/DevServer.zig | 21 ++- src/bake/DevServer/IncrementalGraph.zig | 158 ++++++++++++----- src/css/css_parser.zig | 3 +- src/sourcemap/sourcemap.zig | 224 ++++++++++++++++++++++-- 4 files changed, 345 insertions(+), 61 deletions(-) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 938704e88d73..f71ab7b0fedc 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2237,7 +2237,26 @@ pub fn finalizeBundle( // Load all new chunks into the server runtime. if (!dev.frontend_only and dev.server_graph.current_chunk_len > 0) { - const server_bundle = try dev.server_graph.takeJSBundle(&.{ .kind = .hmr_chunk }); + // Generate a script_id for server bundles + // Use high bit set to distinguish from client bundles, and include generation + // FIXME: Claude, this is highly sus + const server_script_id = SourceMapStore.Key.init((1 << 63) | @as(u64, dev.generation)); + + // Register the source map for server bundle + switch (try dev.source_maps.putOrIncrementRefCount(server_script_id, 1)) { + .uninitialized => |entry| { + errdefer dev.source_maps.unref(server_script_id); + var arena = std.heap.ArenaAllocator.init(dev.allocator); + defer arena.deinit(); + try dev.server_graph.takeSourceMap(arena.allocator(), dev.allocator, entry); + }, + .shared => {}, + } + + const server_bundle = try dev.server_graph.takeJSBundle(&.{ + .kind = .hmr_chunk, + .script_id = server_script_id, + }); defer dev.allocator.free(server_bundle); const server_modules = c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle)) catch |err| { diff --git a/src/bake/DevServer/IncrementalGraph.zig b/src/bake/DevServer/IncrementalGraph.zig index 7459c657f7f7..b4b7ed83170f 100644 --- a/src/bake/DevServer/IncrementalGraph.zig +++ b/src/bake/DevServer/IncrementalGraph.zig @@ -76,6 +76,12 @@ pub fn IncrementalGraph(side: bake.Side) type { .server => void, }, + /// Source maps for server chunks + current_chunk_source_maps: if (side == .server) ArrayListUnmanaged(PackedMap.RefOrEmpty) else void = if (side == .server) .empty, + + /// File indices for server chunks to track which file each chunk comes from + current_chunk_file_indices: if (side == .server) ArrayListUnmanaged(FileIndex) else void = if (side == .server) .empty, + pub const empty: @This() = .{ .bundled_files = .empty, .stale_files = .empty, @@ -89,6 +95,8 @@ pub fn IncrementalGraph(side: bake.Side) type { .current_chunk_parts = .empty, .current_css_files = if (side == .client) .empty, + .current_chunk_source_maps = if (side == .server) .empty else {}, + .current_chunk_file_indices = if (side == .server) .empty else {}, }; pub const File = switch (side) { @@ -314,6 +322,13 @@ pub fn IncrementalGraph(side: bake.Side) type { .current_chunk_len = {}, .current_chunk_parts = g.current_chunk_parts.deinit(allocator), .current_css_files = if (side == .client) g.current_css_files.deinit(allocator), + .current_chunk_source_maps = if (side == .server) { + for (g.current_chunk_source_maps.items) |source_map| { + source_map.deref(&g.owner().*); + } + g.current_chunk_source_maps.deinit(allocator); + }, + .current_chunk_file_indices = if (side == .server) g.current_chunk_file_indices.deinit(allocator), }; } @@ -346,6 +361,14 @@ pub fn IncrementalGraph(side: bake.Side) type { .empty => {}, } } + } else if (side == .server) { + graph += DevServer.memoryCostArrayList(g.current_chunk_source_maps); + graph += DevServer.memoryCostArrayList(g.current_chunk_file_indices); + for (g.current_chunk_source_maps.items) |source_map| { + if (source_map == .ref) { + source_maps += source_map.ref.data.memoryCostWithDedupe(new_dedupe_bits); + } + } } return .{ .graph = graph, @@ -576,10 +599,18 @@ pub fn IncrementalGraph(side: bake.Side) type { if (content == .js) { try g.current_chunk_parts.append(dev.allocator, content.js.code); g.current_chunk_len += content.js.code.len; + + // Track the file index for this chunk + try g.current_chunk_file_indices.append(dev.allocator, file_index); + + // Store the source map instead of freeing it if (content.js.source_map) |source_map| { - var take = source_map.chunk.buffer; - take.deinit(); - dev.allocator.free(source_map.escaped_source); + const packed_map = PackedMap.newNonEmpty(source_map.chunk, source_map.escaped_source); + try g.current_chunk_source_maps.append(dev.allocator, .{ + .ref = packed_map, + }); + } else { + try g.current_chunk_source_maps.append(dev.allocator, PackedMap.RefOrEmpty.blank_empty); } } }, @@ -1546,7 +1577,12 @@ pub fn IncrementalGraph(side: bake.Side) type { g.owner().graph_safety_lock.assertLocked(); g.current_chunk_len = 0; g.current_chunk_parts.clearRetainingCapacity(); - if (side == .client) g.current_css_files.clearRetainingCapacity(); + if (side == .client) { + g.current_css_files.clearRetainingCapacity(); + } else if (side == .server) { + g.current_chunk_source_maps.clearRetainingCapacity(); + g.current_chunk_file_indices.clearRetainingCapacity(); + } } const TakeJSBundleOptions = switch (side) { @@ -1559,6 +1595,7 @@ pub fn IncrementalGraph(side: bake.Side) type { }, .server => struct { kind: ChunkKind, + script_id: SourceMapStore.Key, }, }; @@ -1650,11 +1687,14 @@ pub fn IncrementalGraph(side: bake.Side) type { .server => try w.writeAll("})"), }, } - if (side == .client) { - try w.writeAll("\n//# sourceMappingURL=" ++ DevServer.client_prefix ++ "/"); - try w.writeAll(&std.fmt.bytesToHex(std.mem.asBytes(&options.script_id), .lower)); - try w.writeAll(".js.map\n"); + // Append source map URL for both client and server bundles + try w.writeAll("\n//# sourceMappingURL="); + switch (side) { + .client => try w.writeAll(DevServer.client_prefix ++ "/"), + .server => try w.writeAll("bake://server.map/"), } + try w.writeAll(&std.fmt.bytesToHex(std.mem.asBytes(&options.script_id), .lower)); + try w.writeAll(".js.map\n"); break :end end_list.items; }; @@ -1700,47 +1740,77 @@ pub fn IncrementalGraph(side: bake.Side) type { /// Uses `arena` as a temporary allocator, fills in all fields of `out` except ref_count pub fn takeSourceMap(g: *@This(), arena: std.mem.Allocator, gpa: Allocator, out: *SourceMapStore.Entry) bun.OOM!void { - if (side == .server) @compileError("not implemented"); - const paths = g.bundled_files.keys(); - const files = g.bundled_files.values(); - // This buffer is temporary, holding the quoted source paths, joined with commas. - var source_map_strings = std.ArrayList(u8).init(arena); - defer source_map_strings.deinit(); + switch (side) { + .client => { + const files = g.bundled_files.values(); - const dev = g.owner(); - dev.relative_path_buf_lock.lock(); - defer dev.relative_path_buf_lock.unlock(); - - const buf = bun.path_buffer_pool.get(); - defer bun.path_buffer_pool.put(buf); - - var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len); - errdefer file_paths.deinit(gpa); - var contained_maps: bun.MultiArrayList(PackedMap.RefOrEmpty) = .empty; - try contained_maps.ensureTotalCapacity(gpa, g.current_chunk_parts.items.len); - errdefer contained_maps.deinit(gpa); - - var overlapping_memory_cost: u32 = 0; - - for (g.current_chunk_parts.items) |file_index| { - file_paths.appendAssumeCapacity(paths[file_index.get()]); - const source_map = files[file_index.get()].sourceMap(); - contained_maps.appendAssumeCapacity(source_map.dupeRef()); - if (source_map == .ref) { - overlapping_memory_cost += @intCast(source_map.ref.data.memoryCost()); - } - } + // This buffer is temporary, holding the quoted source paths, joined with commas. + var source_map_strings = std.ArrayList(u8).init(arena); + defer source_map_strings.deinit(); - overlapping_memory_cost += @intCast(contained_maps.memoryCost() + DevServer.memoryCostSlice(file_paths.items)); + const dev = g.owner(); + dev.relative_path_buf_lock.lock(); + defer dev.relative_path_buf_lock.unlock(); + + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); + + var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len); + errdefer file_paths.deinit(gpa); + var contained_maps: bun.MultiArrayList(PackedMap.RefOrEmpty) = .empty; + try contained_maps.ensureTotalCapacity(gpa, g.current_chunk_parts.items.len); + errdefer contained_maps.deinit(gpa); + + var overlapping_memory_cost: u32 = 0; + + for (g.current_chunk_parts.items) |file_index| { + file_paths.appendAssumeCapacity(paths[file_index.get()]); + const source_map = files[file_index.get()].sourceMap(); + contained_maps.appendAssumeCapacity(source_map.dupeRef()); + if (source_map == .ref) { + overlapping_memory_cost += @intCast(source_map.ref.data.memoryCost()); + } + } - out.* = .{ - .ref_count = out.ref_count, - .paths = file_paths.items, - .files = contained_maps, - .overlapping_memory_cost = overlapping_memory_cost, - }; + overlapping_memory_cost += @intCast(contained_maps.memoryCost() + DevServer.memoryCostSlice(file_paths.items)); + + out.* = .{ + .ref_count = out.ref_count, + .paths = file_paths.items, + .files = contained_maps, + .overlapping_memory_cost = overlapping_memory_cost, + }; + }, + .server => { + var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len); + errdefer file_paths.deinit(gpa); + var contained_maps: bun.MultiArrayList(PackedMap.RefOrEmpty) = .empty; + try contained_maps.ensureTotalCapacity(gpa, g.current_chunk_parts.items.len); + errdefer contained_maps.deinit(gpa); + + var overlapping_memory_cost: u32 = 0; + + // For server, we use the tracked file indices to get the correct paths + for (g.current_chunk_file_indices.items, g.current_chunk_source_maps.items) |file_index, source_map| { + file_paths.appendAssumeCapacity(paths[file_index.get()]); + contained_maps.appendAssumeCapacity(source_map.dupeRef()); + if (source_map == .ref) { + overlapping_memory_cost += @intCast(source_map.ref.data.memoryCost()); + } + } + + overlapping_memory_cost += @intCast(contained_maps.memoryCost() + DevServer.memoryCostSlice(file_paths.items)); + + out.* = .{ + .ref_count = out.ref_count, + .paths = file_paths.items, + .files = contained_maps, + .overlapping_memory_cost = overlapping_memory_cost, + }; + }, + } } fn disconnectAndDeleteFile(g: *@This(), file_index: FileIndex) void { diff --git a/src/css/css_parser.zig b/src/css/css_parser.zig index 53b75645cc61..fc26bf225697 100644 --- a/src/css/css_parser.zig +++ b/src/css/css_parser.zig @@ -5890,7 +5890,8 @@ const Tokenizer = struct { } pub inline fn sliceFrom(this: *Tokenizer, start: usize) []const u8 { - return this.src[start..this.position]; + const position = this.getPosition(); + return this.src[start..position]; } }; diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 20e3096910bf..15cb72feb5df 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -1020,9 +1020,10 @@ pub const SourceMapLoadHint = enum(u2) { is_external_map, }; +/// Always returns UTF-8 fn findSourceMappingURL(comptime T: type, source: []const T, alloc: std.mem.Allocator) ?bun.jsc.ZigString.Slice { const needle = comptime bun.strings.literal(T, "\n//# sourceMappingURL="); - const found = bun.strings.indexOfT(T, source, needle) orelse return null; + const found = std.mem.lastIndexOf(T, source, needle) orelse return null; const end = std.mem.indexOfScalarPos(T, source, found + needle.len, '\n') orelse source.len; const url = std.mem.trimRight(T, source[found + needle.len .. end], &.{ ' ', '\r' }); return switch (T) { @@ -1035,6 +1036,189 @@ fn findSourceMappingURL(comptime T: type, source: []const T, alloc: std.mem.Allo }; } +fn findSourceMappingURLNah(comptime T: type, source: []const T, alloc: std.mem.Allocator) ?bun.jsc.ZigString.Slice { + // According to the spec, we need to find the LAST valid sourceMappingURL + // We need to handle both //# and //@ prefixes, and also /* */ comments + var last_url: ?bun.jsc.ZigString.Slice = null; + var i: usize = 0; + + const solidus = comptime bun.strings.literal(T, "/")[0]; + const asterisk = comptime bun.strings.literal(T, "*")[0]; + const newline = comptime bun.strings.literal(T, "\n")[0]; + const carriage_return = comptime bun.strings.literal(T, "\r")[0]; + + // Line terminators as per ECMAScript spec + // Note: For UTF-8, these would be multi-byte sequences, so we only check them in UTF-16 + const line_separator: T = if (T == u16) 0x2028 else newline; + const paragraph_separator: T = if (T == u16) 0x2029 else newline; + + while (i < source.len) { + // Skip to next potential comment + const slash_pos = std.mem.indexOfScalarPos(T, source, i, solidus) orelse break; + i = slash_pos + 1; + + if (i >= source.len) break; + + const next_char = source[i]; + + // Handle single-line comment // + if (next_char == solidus) { + i += 1; + const comment_start = i; + + // Find end of line + var line_end = source.len; + var j = comment_start; + while (j < source.len) : (j += 1) { + const c = source[j]; + if (c == newline or c == carriage_return or + (T == u16 and (c == line_separator or c == paragraph_separator))) + { + line_end = j; + break; + } + } + + const comment = source[comment_start..line_end]; + if (matchSourceMappingURL(T, comment, alloc)) |url| { + // Free previous URL if any + if (last_url) |prev| prev.deinit(); + last_url = url; + } + + i = line_end; + } + // Handle multi-line comment /* */ + else if (next_char == asterisk) { + i += 1; + const comment_start = i; + + // Find closing */ + var found_end = false; + while (i + 1 < source.len) : (i += 1) { + if (source[i] == asterisk and source[i + 1] == solidus) { + const comment = source[comment_start..i]; + if (matchSourceMappingURL(T, comment, alloc)) |url| { + // Free previous URL if any + if (last_url) |prev| prev.deinit(); + last_url = url; + } + i += 2; + found_end = true; + break; + } + } + + if (!found_end) { + // Unclosed comment - ignore rest of file + break; + } + } + // Not a comment - check if it's whitespace + else { + // Back up to check the character before the slash + const before_slash = slash_pos; + if (before_slash > 0) { + var j = before_slash - 1; + // Check backwards for non-whitespace on this line + while (j > 0) : (j -%= 1) { + const c = source[j]; + if (c == newline or c == carriage_return or + (T == u16 and (c == line_separator or c == paragraph_separator))) + { + // Hit line boundary, this slash starts the line (after whitespace) + break; + } + if (!isWhitespace(T, c)) { + // Non-whitespace found - reset last_url per spec + if (last_url) |prev| { + prev.deinit(); + last_url = null; + } + break; + } + if (j == 0) break; + } + } + } + } + + return last_url; +} + +// Helper function to match sourceMappingURL pattern in a comment +fn matchSourceMappingURL(comptime T: type, comment: []const T, alloc: std.mem.Allocator) ?bun.jsc.ZigString.Slice { + // Pattern: ^[@#]\s*sourceMappingURL=(\S*?)\s*$ + var i: usize = 0; + + // Skip leading whitespace + while (i < comment.len and isWhitespace(T, comment[i])) : (i += 1) {} + + if (i >= comment.len) return null; + + // Check for @ or # prefix + const at_sign = comptime bun.strings.literal(T, "@")[0]; + const hash = comptime bun.strings.literal(T, "#")[0]; + + if (comment[i] != at_sign and comment[i] != hash) return null; + i += 1; + + // Skip whitespace after prefix + while (i < comment.len and isWhitespace(T, comment[i])) : (i += 1) {} + + // Check for "sourceMappingURL=" + const mapping_text = comptime bun.strings.literal(T, "sourceMappingURL="); + if (i + mapping_text.len > comment.len) return null; + + const text_part = comment[i .. i + mapping_text.len]; + if (!std.mem.eql(T, text_part, mapping_text)) return null; + + i += mapping_text.len; + + // Find the URL (non-whitespace characters) + const url_start = i; + while (i < comment.len and !isWhitespace(T, comment[i])) : (i += 1) {} + + if (url_start == i) return null; // Empty URL + + const url = comment[url_start..i]; + + // Verify rest is only whitespace + while (i < comment.len) : (i += 1) { + if (!isWhitespace(T, comment[i])) return null; + } + + // Return the URL as a ZigString.Slice + return switch (T) { + u8 => bun.jsc.ZigString.Slice.fromUTF8NeverFree(url), + u16 => bun.jsc.ZigString.Slice.init( + alloc, + bun.strings.toUTF8Alloc(alloc, url) catch bun.outOfMemory(), + ), + else => @compileError("Not Supported"), + }; +} + +// Helper to check if a character is whitespace +fn isWhitespace(comptime T: type, char: T) bool { + return switch (char) { + '\t', '\n', '\r', ' ', 0x0B, 0x0C => true, + else => { + if (T == u16) { + return switch (char) { + 0xA0, // non-breaking space + 0xFEFF, // BOM + 0x2028, // line separator + 0x2029, // paragraph separator + => true, + else => false, + }; + } + return false; + }, + }; +} + /// The last two arguments to this specify loading hints pub fn getSourceMapImpl( comptime SourceProviderKind: type, @@ -1065,24 +1249,34 @@ pub fn getSourceMapImpl( defer source.deref(); bun.assert(source.tag == .ZigString); - const found_url = (if (source.is8Bit()) - findSourceMappingURL(u8, source.latin1(), allocator) - else - findSourceMappingURL(u16, source.utf16(), allocator)) orelse - break :try_inline; + const maybe_found_url = found_url: { + if (source.is8Bit()) + break :found_url findSourceMappingURL(u8, source.latin1(), allocator); + + break :found_url findSourceMappingURL(u16, source.utf16(), allocator); + }; + + const found_url = maybe_found_url orelse break :try_inline; defer found_url.deinit(); + if (bun.strings.hasPrefixComptime( + found_url.slice(), + "bake://server.map", + )) {} + + const parsed = parseUrl( + bun.default_allocator, + allocator, + found_url.slice(), + result, + ) catch |err| { + inline_err = err; + break :try_inline; + }; + break :parsed .{ .is_inline_map, - parseUrl( - bun.default_allocator, - allocator, - found_url.slice(), - result, - ) catch |err| { - inline_err = err; - break :try_inline; - }, + parsed, }; } From a952b4200e665629dafa037159f6befd284b0a98 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:58:50 -0700 Subject: [PATCH 010/130] fix that --- src/bake/BakeSourceProvider.cpp | 28 +++++++++++ src/bake/DevServer.zig | 63 ++++++++++++++++++------- src/bake/DevServer/IncrementalGraph.zig | 11 ++--- src/bake/DevServer/SourceMapStore.zig | 35 ++++++++------ src/bake/DevServerSourceProvider.cpp | 15 ++++++ src/bake/DevServerSourceProvider.h | 58 +++++++++++++++++++++++ src/bun.js/SavedSourceMap.zig | 33 +++++++++++++ src/bun.js/virtual_machine_exports.zig | 8 ++++ src/sourcemap/sourcemap.zig | 61 +++++++++++++++++++++++- 9 files changed, 273 insertions(+), 39 deletions(-) create mode 100644 src/bake/DevServerSourceProvider.cpp create mode 100644 src/bake/DevServerSourceProvider.h diff --git a/src/bake/BakeSourceProvider.cpp b/src/bake/BakeSourceProvider.cpp index 4534ddd3e82d..d2111891ab40 100644 --- a/src/bake/BakeSourceProvider.cpp +++ b/src/bake/BakeSourceProvider.cpp @@ -1,5 +1,6 @@ // clang-format off #include "BakeSourceProvider.h" +#include "DevServerSourceProvider.h" #include "BakeGlobalObject.h" #include "JavaScriptCore/CallData.h" #include "JavaScriptCore/Completion.h" @@ -78,6 +79,33 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatch(GlobalObject* global, BunS return JSC::JSValue::encode(result); } +extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, BunString sourceMapJSON) { + JSC::VM&vm = global->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + String string = "bake://server.patch.js"_s; + JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(string)); + + // Use DevServerSourceProvider with the source map JSON + auto provider = DevServerSourceProvider::create( + global, + source.toWTFString(), + sourceMapJSON.toWTFString(), + origin, + WTFMove(string), + WTF::TextPosition(), + JSC::SourceProviderSourceType::Program + ); + + JSC::SourceCode sourceCode = JSC::SourceCode(provider); + + JSC::JSValue result = vm.interpreter.executeProgram(sourceCode, global, global); + RETURN_IF_EXCEPTION(scope, {}); + + RELEASE_ASSERT(result); + return JSC::JSValue::encode(result); +} + extern "C" JSC::EncodedJSValue BakeGetModuleNamespace( JSC::JSGlobalObject* global, JSC::JSValue keyValue diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index f71ab7b0fedc..c5b3794a2260 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -844,6 +844,7 @@ fn onJsRequest(dev: *DevServer, req: *Request, resp: AnyResponse) void { arena.allocator(), source_id.kind, dev.allocator, + .client, ) catch bun.outOfMemory(); const response = StaticRoute.initFromAnyBlob(&.fromOwnedSlice(dev.allocator, json_bytes), .{ .server = dev.server, @@ -2239,19 +2240,37 @@ pub fn finalizeBundle( if (!dev.frontend_only and dev.server_graph.current_chunk_len > 0) { // Generate a script_id for server bundles // Use high bit set to distinguish from client bundles, and include generation - // FIXME: Claude, this is highly sus const server_script_id = SourceMapStore.Key.init((1 << 63) | @as(u64, dev.generation)); - // Register the source map for server bundle - switch (try dev.source_maps.putOrIncrementRefCount(server_script_id, 1)) { - .uninitialized => |entry| { - errdefer dev.source_maps.unref(server_script_id); - var arena = std.heap.ArenaAllocator.init(dev.allocator); - defer arena.deinit(); - try dev.server_graph.takeSourceMap(arena.allocator(), dev.allocator, entry); - }, - .shared => {}, - } + // Get the source map if available and render to JSON + const source_map_json = if (dev.server_graph.current_chunk_source_maps.items.len > 0) json: { + // Create a temporary source map entry to render + var source_map_entry = SourceMapStore.Entry{ + .ref_count = 1, + .paths = &.{}, + .files = .empty, + .overlapping_memory_cost = 0, + }; + + // Fill the source map entry + var arena = std.heap.ArenaAllocator.init(dev.allocator); + defer arena.deinit(); + try dev.server_graph.takeSourceMap(arena.allocator(), dev.allocator, &source_map_entry); + defer { + source_map_entry.ref_count = 0; + source_map_entry.deinit(dev); + } + + const json_data = try source_map_entry.renderJSON( + dev, + arena.allocator(), + .hmr_chunk, + dev.allocator, + .server, + ); + break :json json_data; + } else null; + defer if (source_map_json) |json| dev.allocator.free(json); const server_bundle = try dev.server_graph.takeJSBundle(&.{ .kind = .hmr_chunk, @@ -2259,11 +2278,18 @@ pub fn finalizeBundle( }); defer dev.allocator.free(server_bundle); - const server_modules = c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle)) catch |err| { - // No user code has been evaluated yet, since everything is to - // be wrapped in a function clousure. This means that the likely - // error is going to be a syntax error, or other mistake in the - // bundler. + const server_modules = if (source_map_json) |json| blk: { + const json_string = bun.String.cloneUTF8(json); + defer json_string.deref(); + break :blk c.BakeLoadServerHmrPatchWithSourceMap(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle), json_string) catch |err| { + // No user code has been evaluated yet, since everything is to + // be wrapped in a function clousure. This means that the likely + // error is going to be a syntax error, or other mistake in the + // bundler. + dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err)); + @panic("Error thrown while evaluating server code. This is always a bug in the bundler."); + }; + } else c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle)) catch |err| { dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err)); @panic("Error thrown while evaluating server code. This is always a bug in the bundler."); }; @@ -3573,6 +3599,11 @@ const c = struct { return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code }); } + fn BakeLoadServerHmrPatchWithSourceMap(global: *jsc.JSGlobalObject, code: bun.String, source_map_json: bun.String) bun.JSError!JSValue { + const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, bun.String) callconv(.c) JSValue, .{ .name = "BakeLoadServerHmrPatchWithSourceMap" }).*; + return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, source_map_json }); + } + fn BakeLoadInitialServerCode(global: *jsc.JSGlobalObject, code: bun.String, separate_ssr_graph: bool) bun.JSError!JSValue { const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, bool) callconv(.c) JSValue, .{ .name = "BakeLoadInitialServerCode" }).*; return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, separate_ssr_graph }); diff --git a/src/bake/DevServer/IncrementalGraph.zig b/src/bake/DevServer/IncrementalGraph.zig index b4b7ed83170f..95d4bff85475 100644 --- a/src/bake/DevServer/IncrementalGraph.zig +++ b/src/bake/DevServer/IncrementalGraph.zig @@ -1687,14 +1687,11 @@ pub fn IncrementalGraph(side: bake.Side) type { .server => try w.writeAll("})"), }, } - // Append source map URL for both client and server bundles - try w.writeAll("\n//# sourceMappingURL="); - switch (side) { - .client => try w.writeAll(DevServer.client_prefix ++ "/"), - .server => try w.writeAll("bake://server.map/"), + if (side == .client) { + try w.writeAll("\n//# sourceMappingURL=" ++ DevServer.client_prefix ++ "/"); + try w.writeAll(&std.fmt.bytesToHex(std.mem.asBytes(&options.script_id), .lower)); + try w.writeAll(".js.map\n"); } - try w.writeAll(&std.fmt.bytesToHex(std.mem.asBytes(&options.script_id), .lower)); - try w.writeAll(".js.map\n"); break :end end_list.items; }; diff --git a/src/bake/DevServer/SourceMapStore.zig b/src/bake/DevServer/SourceMapStore.zig index 0a8a1cd99261..8cbac3c127ce 100644 --- a/src/bake/DevServer/SourceMapStore.zig +++ b/src/bake/DevServer/SourceMapStore.zig @@ -75,11 +75,11 @@ pub const Entry = struct { pub fn renderMappings(map: Entry, kind: ChunkKind, arena: Allocator, gpa: Allocator) ![]u8 { var j: StringJoiner = .{ .allocator = arena }; j.pushStatic("AAAA"); - try joinVLQ(&map, kind, &j, arena); + try joinVLQ(&map, kind, &j, arena, .client); return j.done(gpa); } - pub fn renderJSON(map: *const Entry, dev: *DevServer, arena: Allocator, kind: ChunkKind, gpa: Allocator) ![]u8 { + pub fn renderJSON(map: *const Entry, dev: *DevServer, arena: Allocator, kind: ChunkKind, gpa: Allocator, side: bake.Side) ![]u8 { const map_files = map.files.slice(); const paths = map.paths; @@ -177,14 +177,14 @@ pub const Entry = struct { j.pushStatic( \\],"names":[],"mappings":"AAAA ); - try joinVLQ(map, kind, &j, arena); + try joinVLQ(map, kind, &j, arena, side); const json_bytes = try j.doneWithEnd(gpa, "\"}"); errdefer @compileError("last try should be the final alloc"); if (bun.FeatureFlags.bake_debugging_features) if (dev.dump_dir) |dump_dir| { - const rel_path_escaped = "latest_chunk.js.map"; - dumpBundle(dump_dir, .client, rel_path_escaped, json_bytes, false) catch |err| { + const rel_path_escaped = if (side == .client) "latest_chunk.js.map" else "latest_hmr.js.map"; + dumpBundle(dump_dir, if (side == .client) .client else .server, rel_path_escaped, json_bytes, false) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); Output.warn("Could not dump bundle: {}", .{err}); }; @@ -193,14 +193,9 @@ pub const Entry = struct { return json_bytes; } - fn joinVLQ(map: *const Entry, kind: ChunkKind, j: *StringJoiner, arena: Allocator) !void { + fn joinVLQ(map: *const Entry, kind: ChunkKind, j: *StringJoiner, arena: Allocator, side: bake.Side) !void { const map_files = map.files.slice(); - const runtime: bake.HmrRuntime = switch (kind) { - .initial_response => bun.bake.getHmrRuntime(.client), - .hmr_chunk => comptime .init("self[Symbol.for(\"bun:hmr\")]({\n"), - }; - var prev_end_state: SourceMap.SourceMapState = .{ .generated_line = 0, .generated_column = 0, @@ -209,8 +204,20 @@ pub const Entry = struct { .original_column = 0, }; - // +2 because the magic fairy in my dreams said it would align the source maps. - var lines_between: u32 = runtime.line_count + 2; + var lines_between: u32 = lines_between: { + if (side == .client) { + const runtime: bake.HmrRuntime = switch (kind) { + .initial_response => bun.bake.getHmrRuntime(.client), + .hmr_chunk => comptime .init("self[Symbol.for(\"bun:hmr\")]({\n"), + }; + // +2 because the magic fairy in my dreams said it would align the source maps. + // TODO: why the fuck is this 2? + const lines_between: u32 = runtime.line_count + 2; + break :lines_between lines_between; + } + + break :lines_between 0; + }; // Join all of the mappings together. for (map_files.items(.tags), map_files.items(.data), 1..) |tag, chunk, source_index| switch (tag) { @@ -226,7 +233,7 @@ pub const Entry = struct { continue; }, .ref => { - const content = chunk.ref.data; + const content: *PackedMap = chunk.ref.data; const start_state: SourceMap.SourceMapState = .{ .source_index = @intCast(source_index), .generated_line = @intCast(lines_between), diff --git a/src/bake/DevServerSourceProvider.cpp b/src/bake/DevServerSourceProvider.cpp new file mode 100644 index 000000000000..2f1c61f7e857 --- /dev/null +++ b/src/bake/DevServerSourceProvider.cpp @@ -0,0 +1,15 @@ +#include "DevServerSourceProvider.h" +#include "BunBuiltinNames.h" +#include "BunString.h" + +// The Zig implementation will be provided to handle registration +extern "C" void Bun__addDevServerSourceProvider(void* bun_vm, Bake::DevServerSourceProvider* opaque_source_provider, BunString* specifier); + +// Export functions for Zig to access DevServerSourceProvider +extern "C" BunString DevServerSourceProvider__getSourceSlice(Bake::DevServerSourceProvider* provider) { + return Bun::toStringView(provider->source()); +} + +extern "C" BunString DevServerSourceProvider__getSourceMapJSON(Bake::DevServerSourceProvider* provider) { + return Bun::toStringView(provider->sourceMapJSON()); +} \ No newline at end of file diff --git a/src/bake/DevServerSourceProvider.h b/src/bake/DevServerSourceProvider.h new file mode 100644 index 000000000000..187010d7fc29 --- /dev/null +++ b/src/bake/DevServerSourceProvider.h @@ -0,0 +1,58 @@ +#pragma once +#include "root.h" +#include "headers-handwritten.h" +#include "JavaScriptCore/SourceOrigin.h" +#include "ZigGlobalObject.h" + +namespace Bake { + +class DevServerSourceProvider; + +// Function to be implemented in Zig to register the source provider +extern "C" void Bun__addDevServerSourceProvider(void* bun_vm, DevServerSourceProvider* opaque_source_provider, BunString* specifier); + +class DevServerSourceProvider final : public JSC::StringSourceProvider { +public: + static Ref create( + JSC::JSGlobalObject* globalObject, + const String& source, + const String& sourceMapJSON, + const JSC::SourceOrigin& sourceOrigin, + String&& sourceURL, + const TextPosition& startPosition, + JSC::SourceProviderSourceType sourceType) + { + auto provider = adoptRef(*new DevServerSourceProvider(source, sourceMapJSON, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType)); + auto* zigGlobalObject = jsCast<::Zig::GlobalObject*>(globalObject); + auto specifier = Bun::toString(provider->sourceURL()); + Bun__addDevServerSourceProvider(zigGlobalObject->bunVM(), provider.ptr(), &specifier); + return provider; + } + + // TODO: This should be ZigString so we can have a UTF-8 string and not need + // to do conversions + const String& sourceMapJSON() const { return m_sourceMapJSON; } + +private: + DevServerSourceProvider( + const String& source, + const String& sourceMapJSON, + const JSC::SourceOrigin& sourceOrigin, + String&& sourceURL, + const TextPosition& startPosition, + JSC::SourceProviderSourceType sourceType) + : StringSourceProvider( + source, + sourceOrigin, + JSC::SourceTaintedOrigin::Untainted, + WTFMove(sourceURL), + startPosition, + sourceType) + , m_sourceMapJSON(sourceMapJSON) + { + } + + String m_sourceMapJSON; +}; + +} // namespace Bake diff --git a/src/bun.js/SavedSourceMap.zig b/src/bun.js/SavedSourceMap.zig index 68fcdb75e92e..86bd34fe9ca8 100644 --- a/src/bun.js/SavedSourceMap.zig +++ b/src/bun.js/SavedSourceMap.zig @@ -88,6 +88,7 @@ pub const Value = bun.TaggedPointerUnion(.{ SavedMappings, SourceProviderMap, BakeSourceProvider, + DevServerSourceProvider, }); pub const MissingSourceMapNoteInfo = struct { @@ -108,6 +109,10 @@ pub fn putBakeSourceProvider(this: *SavedSourceMap, opaque_source_provider: *Bak this.putValue(path, Value.init(opaque_source_provider)) catch bun.outOfMemory(); } +pub fn putDevServerSourceProvider(this: *SavedSourceMap, opaque_source_provider: *DevServerSourceProvider, path: []const u8) void { + this.putValue(path, Value.init(opaque_source_provider)) catch bun.outOfMemory(); +} + pub fn putZigSourceProvider(this: *SavedSourceMap, opaque_source_provider: *anyopaque, path: []const u8) void { const source_provider: *SourceProviderMap = @ptrCast(opaque_source_provider); this.putValue(path, Value.init(source_provider)) catch bun.outOfMemory(); @@ -279,6 +284,33 @@ fn getWithContent( MissingSourceMapNoteInfo.path = storage; return .{}; }, + @field(Value.Tag, @typeName(DevServerSourceProvider)) => { + // TODO: This is a copy-paste of above branch + const ptr: *DevServerSourceProvider = Value.from(mapping.value_ptr.*).as(DevServerSourceProvider); + this.unlock(); + + // Do not lock the mutex while we're parsing JSON! + if (ptr.getSourceMap(path, .none, hint)) |parse| { + if (parse.map) |map| { + map.ref(); + // The mutex is not locked. We have to check the hash table again. + this.putValue(path, Value.init(map)) catch bun.outOfMemory(); + + return parse; + } + } + + this.lock(); + defer this.unlock(); + // does not have a valid source map. let's not try again + _ = this.map.remove(hash); + + // Store path for a user note. + const storage = MissingSourceMapNoteInfo.storage[0..path.len]; + @memcpy(storage, path); + MissingSourceMapNoteInfo.path = storage; + return .{}; + }, else => { if (Environment.allow_assert) { @panic("Corrupt pointer tag"); @@ -333,5 +365,6 @@ const logger = bun.logger; const SourceMap = bun.sourcemap; const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; +const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider; const ParsedSourceMap = SourceMap.ParsedSourceMap; const SourceProviderMap = SourceMap.SourceProviderMap; diff --git a/src/bun.js/virtual_machine_exports.zig b/src/bun.js/virtual_machine_exports.zig index 41fa44cafc5a..9873b029ffbe 100644 --- a/src/bun.js/virtual_machine_exports.zig +++ b/src/bun.js/virtual_machine_exports.zig @@ -179,6 +179,13 @@ export fn Bun__addBakeSourceProviderSourceMap(vm: *VirtualMachine, opaque_source vm.source_mappings.putBakeSourceProvider(@as(*BakeSourceProvider, @ptrCast(opaque_source_provider)), slice.slice()); } +export fn Bun__addDevServerSourceProvider(vm: *VirtualMachine, opaque_source_provider: *anyopaque, specifier: *bun.String) void { + var sfb = std.heap.stackFallback(4096, bun.default_allocator); + const slice = specifier.toUTF8(sfb.get()); + defer slice.deinit(); + vm.source_mappings.putDevServerSourceProvider(@as(*DevServerSourceProvider, @ptrCast(opaque_source_provider)), slice.slice()); +} + export fn Bun__addSourceProviderSourceMap(vm: *VirtualMachine, opaque_source_provider: *anyopaque, specifier: *bun.String) void { var sfb = std.heap.stackFallback(4096, bun.default_allocator); const slice = specifier.toUTF8(sfb.get()); @@ -215,6 +222,7 @@ const std = @import("std"); const bun = @import("bun"); const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; +const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider; const PluginRunner = bun.transpiler.PluginRunner; const jsc = bun.jsc; diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 15cb72feb5df..34685524bc6e 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -887,15 +887,17 @@ pub const ParsedSourceMap = struct { is_standalone_module_graph: bool = false, - const SourceProviderKind = enum(u1) { zig, bake }; + const SourceProviderKind = enum(u2) { zig, bake, dev_server }; const AnySourceProvider = union(enum) { zig: *SourceProviderMap, bake: *BakeSourceProvider, + dev_server: *DevServerSourceProvider, pub fn ptr(this: AnySourceProvider) *anyopaque { return switch (this) { .zig => @ptrCast(this.zig), .bake => @ptrCast(this.bake), + .dev_server => @ptrCast(this.dev_server), }; } @@ -908,6 +910,7 @@ pub const ParsedSourceMap = struct { return switch (this) { .zig => this.zig.getSourceMap(source_filename, load_hint, result), .bake => this.bake.getSourceMap(source_filename, load_hint, result), + .dev_server => this.dev_server.getSourceMap(source_filename, load_hint, result), }; } }; @@ -915,7 +918,7 @@ pub const ParsedSourceMap = struct { const SourceContentPtr = packed struct(u64) { load_hint: SourceMapLoadHint, kind: SourceProviderKind, - data: u61, + data: u60, pub const none: SourceContentPtr = .{ .load_hint = .none, .kind = .zig, .data = 0 }; @@ -927,10 +930,15 @@ pub const ParsedSourceMap = struct { return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .bake }; } + fn fromDevServerProvider(p: *DevServerSourceProvider) SourceContentPtr { + return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .dev_server }; + } + pub fn provider(sc: SourceContentPtr) ?AnySourceProvider { switch (sc.kind) { .zig => return .{ .zig = @ptrFromInt(sc.data) }, .bake => return .{ .bake = @ptrFromInt(sc.data) }, + .dev_server => return .{ .dev_server = @ptrFromInt(sc.data) }, } } }; @@ -1282,6 +1290,27 @@ pub fn getSourceMapImpl( // try to load a .map file if (load_hint != .is_inline_map) try_external: { + if (comptime SourceProviderKind == DevServerSourceProvider) { + // For DevServerSourceProvider, get the source map JSON directly + const json_string = provider.getSourceMapJSON(); + defer json_string.deref(); + + // Convert to UTF-8 slice + const json_data = json_string.toUTF8(allocator); + defer json_data.deinit(); + + // Parse the JSON source map + break :parsed .{ + .is_external_map, + parseJSON( + bun.default_allocator, + allocator, + json_data.slice(), + result, + ) catch return null, + }; + } + if (comptime SourceProviderKind == BakeSourceProvider) fallback_to_normal: { const global = bun.jsc.VirtualMachine.get().global; // If we're using bake's production build the global object will @@ -1435,6 +1464,34 @@ pub const BakeSourceProvider = opaque { } }; +pub const DevServerSourceProvider = opaque { + extern fn DevServerSourceProvider__getSourceSlice(*DevServerSourceProvider) bun.String; + extern fn DevServerSourceProvider__getSourceMapJSON(*DevServerSourceProvider) bun.String; + + pub const getSourceSlice = DevServerSourceProvider__getSourceSlice; + pub const getSourceMapJSON = DevServerSourceProvider__getSourceMapJSON; + + pub fn toSourceContentPtr(this: *DevServerSourceProvider) ParsedSourceMap.SourceContentPtr { + return ParsedSourceMap.SourceContentPtr.fromDevServerProvider(this); + } + + /// The last two arguments to this specify loading hints + pub fn getSourceMap( + provider: *DevServerSourceProvider, + source_filename: []const u8, + load_hint: SourceMap.SourceMapLoadHint, + result: SourceMap.ParseUrlResultHint, + ) ?SourceMap.ParseUrl { + return getSourceMapImpl( + DevServerSourceProvider, + provider, + source_filename, + load_hint, + result, + ); + } +}; + pub const LineColumnOffset = struct { lines: i32 = 0, columns: i32 = 0, From 3a1b12ee61d310afd5033e206b986e919b5476cd Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:55:52 -0700 Subject: [PATCH 011/130] no need to percent encode or add "file://" to server-side sourcemaps --- src/bake/BakeSourceProvider.cpp | 5 +- src/bake/DevServer.zig | 21 ++++---- src/bake/DevServer/SourceMapStore.zig | 34 +++++++++--- src/bake/DevServerSourceProvider.cpp | 10 ++-- src/bake/DevServerSourceProvider.h | 76 ++++++++++++++++++++++++--- src/sourcemap/sourcemap.zig | 30 ++++++----- 6 files changed, 135 insertions(+), 41 deletions(-) diff --git a/src/bake/BakeSourceProvider.cpp b/src/bake/BakeSourceProvider.cpp index d2111891ab40..8efa3d8d3475 100644 --- a/src/bake/BakeSourceProvider.cpp +++ b/src/bake/BakeSourceProvider.cpp @@ -79,7 +79,7 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatch(GlobalObject* global, BunS return JSC::JSValue::encode(result); } -extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, BunString sourceMapJSON) { +extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, const char* sourceMapJSONPtr, size_t sourceMapJSONLength) { JSC::VM&vm = global->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -90,7 +90,8 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* auto provider = DevServerSourceProvider::create( global, source.toWTFString(), - sourceMapJSON.toWTFString(), + sourceMapJSONPtr, + sourceMapJSONLength, origin, WTFMove(string), WTF::TextPosition(), diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index c5b3794a2260..6d713edabe5f 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2243,7 +2243,7 @@ pub fn finalizeBundle( const server_script_id = SourceMapStore.Key.init((1 << 63) | @as(u64, dev.generation)); // Get the source map if available and render to JSON - const source_map_json = if (dev.server_graph.current_chunk_source_maps.items.len > 0) json: { + var source_map_json = if (dev.server_graph.current_chunk_source_maps.items.len > 0) json: { // Create a temporary source map entry to render var source_map_entry = SourceMapStore.Entry{ .ref_count = 1, @@ -2270,7 +2270,7 @@ pub fn finalizeBundle( ); break :json json_data; } else null; - defer if (source_map_json) |json| dev.allocator.free(json); + defer if (source_map_json) |json| bun.default_allocator.free(json); const server_bundle = try dev.server_graph.takeJSBundle(&.{ .kind = .hmr_chunk, @@ -2278,10 +2278,13 @@ pub fn finalizeBundle( }); defer dev.allocator.free(server_bundle); - const server_modules = if (source_map_json) |json| blk: { - const json_string = bun.String.cloneUTF8(json); - defer json_string.deref(); - break :blk c.BakeLoadServerHmrPatchWithSourceMap(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle), json_string) catch |err| { + const server_modules = if (bun.take(&source_map_json)) |json| blk: { + break :blk c.BakeLoadServerHmrPatchWithSourceMap( + @ptrCast(dev.vm.global), + bun.String.cloneLatin1(server_bundle), + json.ptr, + json.len, + ) catch |err| { // No user code has been evaluated yet, since everything is to // be wrapped in a function clousure. This means that the likely // error is going to be a syntax error, or other mistake in the @@ -3599,9 +3602,9 @@ const c = struct { return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code }); } - fn BakeLoadServerHmrPatchWithSourceMap(global: *jsc.JSGlobalObject, code: bun.String, source_map_json: bun.String) bun.JSError!JSValue { - const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, bun.String) callconv(.c) JSValue, .{ .name = "BakeLoadServerHmrPatchWithSourceMap" }).*; - return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, source_map_json }); + fn BakeLoadServerHmrPatchWithSourceMap(global: *jsc.JSGlobalObject, code: bun.String, source_map_json_ptr: [*]const u8, source_map_json_len: usize) bun.JSError!JSValue { + const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, [*]const u8, usize) callconv(.c) JSValue, .{ .name = "BakeLoadServerHmrPatchWithSourceMap" }).*; + return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, source_map_json_ptr, source_map_json_len }); } fn BakeLoadInitialServerCode(global: *jsc.JSGlobalObject, code: bun.String, separate_ssr_graph: bool) bun.JSError!JSValue { diff --git a/src/bake/DevServer/SourceMapStore.zig b/src/bake/DevServer/SourceMapStore.zig index 8cbac3c127ce..32229688dd57 100644 --- a/src/bake/DevServer/SourceMapStore.zig +++ b/src/bake/DevServer/SourceMapStore.zig @@ -108,13 +108,21 @@ pub const Entry = struct { if (std.fs.path.isAbsolute(path)) { const is_windows_drive_path = Environment.isWindows and path[0] != '/'; - try source_map_strings.appendSlice(if (is_windows_drive_path) - "\"file:///" - else - "\"file://"); + + // On the client we prefix the sourcemap path with "file://" and + // percent encode it + if (side == .client) { + try source_map_strings.appendSlice(if (is_windows_drive_path) + "\"file:///" + else + "\"file://"); + } else { + try source_map_strings.append('"'); + } + if (Environment.isWindows and !is_windows_drive_path) { // UNC namespace -> file://server/share/path.ext - bun.strings.percentEncodeWrite( + encodeSourceMapPath( if (path.len > 2 and path[0] == '/' and path[1] == '/') path[2..] else @@ -129,7 +137,7 @@ pub const Entry = struct { // -> file:///path/to/file.js // windows drive letter paths have the extra slash added // -> file:///C:/path/to/file.js - bun.strings.percentEncodeWrite(path, &source_map_strings) catch |err| switch (err) { + encodeSourceMapPath(side, path, &source_map_strings) catch |err| switch (err) { error.IncompleteUTF8 => @panic("Unexpected: asset with incomplete UTF-8 as file path"), error.OutOfMemory => |e| return e, }; @@ -193,6 +201,20 @@ pub const Entry = struct { return json_bytes; } + fn encodeSourceMapPath( + side: bake.Side, + utf8_input: []const u8, + writer: *std.ArrayList(u8), + ) error{ OutOfMemory, IncompleteUTF8 }!void { + // On the client, percent encode everything so it works in the browser + if (side == .client) { + return bun.strings.percentEncodeWrite(utf8_input, writer); + } + + // On the server, we don't need to do anything + try writer.appendSlice(utf8_input); + } + fn joinVLQ(map: *const Entry, kind: ChunkKind, j: *StringJoiner, arena: Allocator, side: bake.Side) !void { const map_files = map.files.slice(); diff --git a/src/bake/DevServerSourceProvider.cpp b/src/bake/DevServerSourceProvider.cpp index 2f1c61f7e857..366766438455 100644 --- a/src/bake/DevServerSourceProvider.cpp +++ b/src/bake/DevServerSourceProvider.cpp @@ -6,10 +6,12 @@ extern "C" void Bun__addDevServerSourceProvider(void* bun_vm, Bake::DevServerSourceProvider* opaque_source_provider, BunString* specifier); // Export functions for Zig to access DevServerSourceProvider -extern "C" BunString DevServerSourceProvider__getSourceSlice(Bake::DevServerSourceProvider* provider) { +extern "C" BunString DevServerSourceProvider__getSourceSlice(Bake::DevServerSourceProvider* provider) +{ return Bun::toStringView(provider->source()); } -extern "C" BunString DevServerSourceProvider__getSourceMapJSON(Bake::DevServerSourceProvider* provider) { - return Bun::toStringView(provider->sourceMapJSON()); -} \ No newline at end of file +extern "C" Bake::SourceMapData DevServerSourceProvider__getSourceMapJSON(Bake::DevServerSourceProvider* provider) +{ + return provider->sourceMapJSON(); +} diff --git a/src/bake/DevServerSourceProvider.h b/src/bake/DevServerSourceProvider.h index 187010d7fc29..6d1bd47f1d58 100644 --- a/src/bake/DevServerSourceProvider.h +++ b/src/bake/DevServerSourceProvider.h @@ -3,11 +3,68 @@ #include "headers-handwritten.h" #include "JavaScriptCore/SourceOrigin.h" #include "ZigGlobalObject.h" +#include namespace Bake { class DevServerSourceProvider; +class SourceMapJSONString { +public: + SourceMapJSONString(const char* ptr, size_t length) + : m_ptr(ptr) + , m_length(length) + { + } + + ~SourceMapJSONString() + { + if (m_ptr) { + mi_free(const_cast(m_ptr)); + } + } + + // Delete copy constructor and assignment operator to prevent double free + SourceMapJSONString(const SourceMapJSONString&) = delete; + SourceMapJSONString& operator=(const SourceMapJSONString&) = delete; + + // Move constructor and assignment + SourceMapJSONString(SourceMapJSONString&& other) noexcept + : m_ptr(other.m_ptr) + , m_length(other.m_length) + { + other.m_ptr = nullptr; + other.m_length = 0; + } + + SourceMapJSONString& operator=(SourceMapJSONString&& other) noexcept + { + if (this != &other) { + if (m_ptr) { + mi_free(const_cast(m_ptr)); + } + m_ptr = other.m_ptr; + m_length = other.m_length; + other.m_ptr = nullptr; + other.m_length = 0; + } + return *this; + } + + const char* ptr() const { return m_ptr; } + size_t length() const { return m_length; } + +private: + const char* m_ptr; + size_t m_length; +}; + +// Struct to return source map data to Zig +struct SourceMapData { + const char* ptr; + size_t length; +}; + // Function to be implemented in Zig to register the source provider extern "C" void Bun__addDevServerSourceProvider(void* bun_vm, DevServerSourceProvider* opaque_source_provider, BunString* specifier); @@ -16,27 +73,30 @@ class DevServerSourceProvider final : public JSC::StringSourceProvider { static Ref create( JSC::JSGlobalObject* globalObject, const String& source, - const String& sourceMapJSON, + const char* sourceMapJSONPtr, + size_t sourceMapJSONLength, const JSC::SourceOrigin& sourceOrigin, String&& sourceURL, const TextPosition& startPosition, JSC::SourceProviderSourceType sourceType) { - auto provider = adoptRef(*new DevServerSourceProvider(source, sourceMapJSON, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType)); + auto provider = adoptRef(*new DevServerSourceProvider(source, sourceMapJSONPtr, sourceMapJSONLength, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType)); auto* zigGlobalObject = jsCast<::Zig::GlobalObject*>(globalObject); auto specifier = Bun::toString(provider->sourceURL()); Bun__addDevServerSourceProvider(zigGlobalObject->bunVM(), provider.ptr(), &specifier); return provider; } - // TODO: This should be ZigString so we can have a UTF-8 string and not need - // to do conversions - const String& sourceMapJSON() const { return m_sourceMapJSON; } + SourceMapData sourceMapJSON() const + { + return SourceMapData { m_sourceMapJSON.ptr(), m_sourceMapJSON.length() }; + } private: DevServerSourceProvider( const String& source, - const String& sourceMapJSON, + const char* sourceMapJSONPtr, + size_t sourceMapJSONLength, const JSC::SourceOrigin& sourceOrigin, String&& sourceURL, const TextPosition& startPosition, @@ -48,11 +108,11 @@ class DevServerSourceProvider final : public JSC::StringSourceProvider { WTFMove(sourceURL), startPosition, sourceType) - , m_sourceMapJSON(sourceMapJSON) + , m_sourceMapJSON(sourceMapJSONPtr, sourceMapJSONLength) { } - String m_sourceMapJSON; + SourceMapJSONString m_sourceMapJSON; }; } // namespace Bake diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 34685524bc6e..a26481b63b2b 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -1292,25 +1292,26 @@ pub fn getSourceMapImpl( if (load_hint != .is_inline_map) try_external: { if (comptime SourceProviderKind == DevServerSourceProvider) { // For DevServerSourceProvider, get the source map JSON directly - const json_string = provider.getSourceMapJSON(); - defer json_string.deref(); - - // Convert to UTF-8 slice - const json_data = json_string.toUTF8(allocator); - defer json_data.deinit(); - + const source_map_data = provider.getSourceMapJSON(); + + if (source_map_data.length == 0) { + break :try_external; + } + + const json_slice = source_map_data.ptr[0..source_map_data.length]; + // Parse the JSON source map break :parsed .{ .is_external_map, parseJSON( bun.default_allocator, allocator, - json_data.slice(), + json_slice, result, ) catch return null, }; } - + if (comptime SourceProviderKind == BakeSourceProvider) fallback_to_normal: { const global = bun.jsc.VirtualMachine.get().global; // If we're using bake's production build the global object will @@ -1465,12 +1466,17 @@ pub const BakeSourceProvider = opaque { }; pub const DevServerSourceProvider = opaque { + pub const SourceMapData = extern struct { + ptr: [*]const u8, + length: usize, + }; + extern fn DevServerSourceProvider__getSourceSlice(*DevServerSourceProvider) bun.String; - extern fn DevServerSourceProvider__getSourceMapJSON(*DevServerSourceProvider) bun.String; - + extern fn DevServerSourceProvider__getSourceMapJSON(*DevServerSourceProvider) SourceMapData; + pub const getSourceSlice = DevServerSourceProvider__getSourceSlice; pub const getSourceMapJSON = DevServerSourceProvider__getSourceMapJSON; - + pub fn toSourceContentPtr(this: *DevServerSourceProvider) ParsedSourceMap.SourceContentPtr { return ParsedSourceMap.SourceContentPtr.fromDevServerProvider(this); } From f556fc987c4ff0026e90adc9ce5f3d8e324838b4 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:56:09 -0700 Subject: [PATCH 012/130] test --- test/bake/dev/server-sourcemap.test.ts | 150 +++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 test/bake/dev/server-sourcemap.test.ts diff --git a/test/bake/dev/server-sourcemap.test.ts b/test/bake/dev/server-sourcemap.test.ts new file mode 100644 index 000000000000..884046c372d7 --- /dev/null +++ b/test/bake/dev/server-sourcemap.test.ts @@ -0,0 +1,150 @@ +import { expect } from "bun:test"; +import { devTest } from "../bake-harness"; + +devTest("server-side source maps show correct error lines", { + files: { + "pages/[...slug].tsx": `export default async function MyPage(params) { + myFunc(); + return

{JSON.stringify(params)}

; +} + +function myFunc() { + throw new Error("Test error for source maps!"); +} + +export async function getStaticPaths() { + return { + paths: [ + { + params: { + slug: ["test-error"], + }, + }, + ], + }; +}`, + }, + framework: "react", + async test(dev) { + // Make a request that will trigger the error + await dev.fetch("/test-error").catch(() => {}); + + // Give it a moment to process the error + await Bun.sleep(1000); + + // The output we saw shows the stack trace with correct source mapping + // We need to check that the error shows the right file:line:column + const lines = dev.output.lines.join("\n"); + + // Check that we got the error + expect(lines).toContain("Test error for source maps!"); + + // Check that the stack trace shows correct file and line numbers + // The source maps are working if we see the correct patterns + // We need to check for the patterns because ANSI codes might be embedded + const hasCorrectThrowLine = lines.includes("myFunc") && lines.includes("7") && lines.includes("9"); + const hasCorrectCallLine = lines.includes("MyPage") && lines.includes("2") && lines.includes("3"); + const hasCorrectFileName = lines.includes("/pages/[...slug].tsx"); + + expect(hasCorrectThrowLine).toBe(true); + expect(hasCorrectCallLine).toBe(true); + expect(hasCorrectFileName).toBe(true); + }, + timeoutMultiplier: 2, // Give more time for the test +}); + +devTest("server-side source maps work with HMR updates", { + files: { + "pages/error-page.tsx": `export default function ErrorPage() { + return
Initial content
; +} + +export async function getStaticPaths() { + return { + paths: [{ params: {} }], + }; +}`, + }, + framework: "react", + async test(dev) { + // First fetch should work + const response1 = await dev.fetch("/error-page"); + expect(response1.status).toBe(200); + expect(await response1.text()).toContain("Initial content"); + + // Update the file to throw an error + await dev.write("pages/error-page.tsx", `export default function ErrorPage() { + throwError(); + return
Updated content
; +} + +function throwError() { + throw new Error("HMR error test"); +} + +export async function getStaticPaths() { + return { + paths: [{ params: {} }], + }; +}`); + + // Wait for the rebuild + await dev.waitForHmr(); + + // Second fetch should error + await dev.fetch("/error-page").catch(() => {}); + + // Wait for error output + await dev.output.waitForLine(/HMR error test/); + + // Check source map points to correct lines after HMR + const lines = dev.output.lines.join("\n"); + const hasCorrectThrowLine = lines.includes("throwError") && lines.includes("7") && lines.includes("9"); + const hasCorrectCallLine = lines.includes("ErrorPage") && lines.includes("2") && lines.includes("3"); + + expect(hasCorrectThrowLine).toBe(true); + expect(hasCorrectCallLine).toBe(true); + }, +}); + +devTest("server-side source maps handle nested imports", { + files: { + "pages/nested.tsx": `import { doSomething } from "../lib/utils"; + +export default function NestedPage() { + const result = doSomething(); + return
{result}
; +} + +export async function getStaticPaths() { + return { + paths: [{ params: {} }], + }; +}`, + "lib/utils.ts": `export function doSomething() { + return helperFunction(); +} + +function helperFunction() { + throw new Error("Nested error"); +}`, + }, + framework: "react", + async test(dev) { + // Make request that triggers error + await dev.fetch("/nested").catch(() => {}); + + // Wait for error output + await dev.output.waitForLine(/Nested error/); + + // Check that stack trace shows both files with correct lines + const lines = dev.output.lines.join("\n"); + const hasUtilsThrowLine = lines.includes("helperFunction") && lines.includes("6") && lines.includes("9"); + const hasUtilsCallLine = lines.includes("doSomething") && lines.includes("2"); + const hasPageCallLine = lines.includes("NestedPage") && lines.includes("4"); + + expect(hasUtilsThrowLine).toBe(true); + expect(hasUtilsCallLine).toBe(true); + expect(hasPageCallLine).toBe(true); + }, +}); \ No newline at end of file From 2ab962bf6be102a35548b651986799bf68b1e4fd Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:32:34 -0700 Subject: [PATCH 013/130] small stuff --- src/bake/DevServer.zig | 26 ++++++++++++------------- src/bake/bun-framework-react/server.tsx | 2 +- src/bun.js/api/server.zig | 22 +++++++++++++++------ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 6d713edabe5f..f8a4f230b7c2 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -1184,7 +1184,7 @@ fn onFrameworkRequestWithBundle( const route_bundle = dev.routeBundlePtr(route_bundle_index); assert(route_bundle.data == .framework); - const bundle = &route_bundle.data.framework; + const framework_bundle = &route_bundle.data.framework; // Extract route params by re-matching the URL var params: FrameworkRouter.MatchedParams = undefined; @@ -1241,9 +1241,9 @@ fn onFrameworkRequestWithBundle( const server_request_callback = dev.server_fetch_function_callback.get() orelse unreachable; // did not initialize server code - const router_type = dev.router.typePtr(dev.router.routePtr(bundle.route_index).type); + const router_type = dev.router.typePtr(dev.router.routePtr(framework_bundle.route_index).type); - dev.server.?.onRequestFromSaved( + dev.server.?.onSavedRequest( req, resp, server_request_callback, @@ -1258,17 +1258,17 @@ fn onFrameworkRequestWithBundle( break :str str; }, // routeModules - bundle.cached_module_list.get() orelse arr: { + framework_bundle.cached_module_list.get() orelse arr: { const global = dev.vm.global; const keys = dev.server_graph.bundled_files.keys(); var n: usize = 1; - var route = dev.router.routePtr(bundle.route_index); + var route = dev.router.routePtr(framework_bundle.route_index); while (true) { if (route.file_layout != .none) n += 1; route = dev.router.routePtr(route.parent.unwrap() orelse break); } const arr = try JSValue.createEmptyArray(global, n); - route = dev.router.routePtr(bundle.route_index); + route = dev.router.routePtr(framework_bundle.route_index); var route_name = bun.String.cloneUTF8(dev.relativePath(keys[fromOpaqueFileId(.server, route.file_page.unwrap().?).get()])); try arr.putIndex(global, 0, route_name.transferToJS(global)); dev.releaseRelativePathBuf(); @@ -1282,11 +1282,11 @@ fn onFrameworkRequestWithBundle( } route = dev.router.routePtr(route.parent.unwrap() orelse break); } - bundle.cached_module_list = .create(arr, global); + framework_bundle.cached_module_list = .create(arr, global); break :arr arr; }, // clientId - bundle.cached_client_bundle_url.get() orelse str: { + framework_bundle.cached_client_bundle_url.get() orelse str: { const bundle_index: u32 = route_bundle_index.get(); const generation: u32 = route_bundle.client_script_generation; const str = bun.String.createFormat(client_prefix ++ "/route-{}{}.js", .{ @@ -1295,13 +1295,13 @@ fn onFrameworkRequestWithBundle( }) catch bun.outOfMemory(); defer str.deref(); const js = str.toJS(dev.vm.global); - bundle.cached_client_bundle_url = .create(js, dev.vm.global); + framework_bundle.cached_client_bundle_url = .create(js, dev.vm.global); break :str js; }, // styles - bundle.cached_css_file_array.get() orelse arr: { + framework_bundle.cached_css_file_array.get() orelse arr: { const js = dev.generateCssJSArray(route_bundle) catch bun.outOfMemory(); - bundle.cached_css_file_array = .create(js, dev.vm.global); + framework_bundle.cached_css_file_array = .create(js, dev.vm.global); break :arr js; }, // params @@ -1471,7 +1471,7 @@ fn generateJavaScriptCodeForHTMLFile( pub fn onJsRequestWithBundle(dev: *DevServer, bundle_index: RouteBundle.Index, resp: AnyResponse, method: bun.http.Method) void { const route_bundle = dev.routeBundlePtr(bundle_index); - const blob = route_bundle.client_bundle orelse generate: { + const client_bundle = route_bundle.client_bundle orelse generate: { const payload = dev.generateClientBundle(route_bundle) catch bun.outOfMemory(); errdefer dev.allocator.free(payload); route_bundle.client_bundle = StaticRoute.initFromAnyBlob( @@ -1484,7 +1484,7 @@ pub fn onJsRequestWithBundle(dev: *DevServer, bundle_index: RouteBundle.Index, r break :generate route_bundle.client_bundle.?; }; dev.source_maps.addWeakRef(route_bundle.sourceMapId()); - blob.onWithMethod(method, resp); + client_bundle.onWithMethod(method, resp); } pub fn onSrcRequest(dev: *DevServer, req: *uws.Request, resp: anytype) void { diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index ed4a6c1809f6..fbc13637f815 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -106,7 +106,7 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis } // The RSC payload is rendered into HTML - return new Response(await renderToHtml(rscPayload, meta.modules, signal), { + return new Response(renderToHtml(rscPayload, meta.modules, signal), { headers: { "Content-Type": "text/html; charset=utf8", }, diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index ac5d0453a98f..7357f8a35aa8 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2086,7 +2086,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d this.handleRequest(&should_deinit_context, prepared, req, response_value); } - pub fn onRequestFromSaved( + pub fn onSavedRequest( this: *ThisServer, req: SavedRequest.Union, resp: *App.Response, @@ -2874,7 +2874,17 @@ pub const SavedRequest = struct { } pub const Union = union(enum) { + /// Direct pointer to a µWebSockets request that is still on the stack. + /// Used for synchronous request handling where the request can be processed + /// immediately within the current call frame. This avoids heap allocation + /// and is more efficient for simple, fast operations. stack: *uws.Request, + + /// A heap-allocated copy of the request data that persists beyond the + /// initial request handler. Used when request processing needs to be + /// deferred (e.g., async operations, waiting for framework initialization). + /// Contains strong references to JavaScript objects and all context needed + /// to complete the request later. saved: bun.jsc.API.SavedRequest, }; }; @@ -3147,7 +3157,7 @@ pub const AnyServer = struct { }; } - pub fn onRequestFromSaved( + pub fn onSavedRequest( this: AnyServer, req: SavedRequest.Union, resp: uws.AnyResponse, @@ -3156,10 +3166,10 @@ pub const AnyServer = struct { extra_args: [extra_arg_count]JSValue, ) void { return switch (this.ptr.tag()) { - Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequestFromSaved(req, resp.TCP, callback, extra_arg_count, extra_args), - Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onRequestFromSaved(req, resp.SSL, callback, extra_arg_count, extra_args), - Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequestFromSaved(req, resp.TCP, callback, extra_arg_count, extra_args), - Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onRequestFromSaved(req, resp.SSL, callback, extra_arg_count, extra_args), + Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onSavedRequest(req, resp.TCP, callback, extra_arg_count, extra_args), + Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onSavedRequest(req, resp.SSL, callback, extra_arg_count, extra_args), + Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onSavedRequest(req, resp.TCP, callback, extra_arg_count, extra_args), + Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onSavedRequest(req, resp.SSL, callback, extra_arg_count, extra_args), else => bun.unreachablePanic("Invalid pointer tag", .{}), }; } From 4c6b296a7c37875dd367e97288b87338d8d4f910 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:04:54 -0700 Subject: [PATCH 014/130] okie dokie --- src/bake/bun-framework-react/server.tsx | 126 ++++++++++++++---------- src/bake/bun-framework-react/ssr.tsx | 25 ++++- 2 files changed, 99 insertions(+), 52 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index fbc13637f815..55bbc051d700 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -55,62 +55,86 @@ function component(mod: any, params: Record | null) { // `server.tsx` exports a function to be used for handling user routes. It takes // in the Request object, the route's module, and extra route metadata. export async function render(request: Request, meta: Bake.RouteMetadata): Promise { - // The framework generally has two rendering modes. - // - Standard browser navigation - // - Client-side navigation - // - // For React, this means calling `renderToReadableStream` to generate the RSC - // payload, but only generate HTML for the former of these rendering modes. - // This is signaled by `client.tsx` via the `Accept` header. - const skipSSR = request.headers.get("Accept")?.includes("text/x-component"); - - // Do not render tags if the request is skipping SSR. - const page = getPage(meta, skipSSR ? [] : meta.styles); - - // TODO: write a lightweight version of PassThrough - const rscPayload = new PassThrough(); - - if (skipSSR) { - // "client.tsx" reads the start of the response to determine the - // CSS files to load. The styles are loaded before the new page - // is presented, to avoid a flash of unstyled content. - const int = Buffer.allocUnsafe(4); - const str = meta.styles.join("\n"); - int.writeUInt32LE(str.length, 0); - rscPayload.write(int); - rscPayload.write(str); - } + try { + // The framework generally has two rendering modes. + // - Standard browser navigation + // - Client-side navigation + // + // For React, this means calling `renderToReadableStream` to generate the RSC + // payload, but only generate HTML for the former of these rendering modes. + // This is signaled by `client.tsx` via the `Accept` header. + const skipSSR = request.headers.get("Accept")?.includes("text/x-component"); + + // Do not render tags if the request is skipping SSR. + const page = getPage(meta, skipSSR ? [] : meta.styles); + + // TODO: write a lightweight version of PassThrough + const rscPayload = new PassThrough(); + + if (skipSSR) { + // "client.tsx" reads the start of the response to determine the + // CSS files to load. The styles are loaded before the new page + // is presented, to avoid a flash of unstyled content. + const int = Buffer.allocUnsafe(4); + const str = meta.styles.join("\n"); + int.writeUInt32LE(str.length, 0); + rscPayload.write(int); + rscPayload.write(str); + } - // This renders Server Components to a ReadableStream "RSC Payload" - let pipe; - const signal: MiniAbortSignal = { aborted: false, abort: null! }; - ({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, { - onError: err => { + // This renders Server Components to a ReadableStream "RSC Payload" + let pipe; + const signal: MiniAbortSignal = { aborted: false, abort: null! }; + ({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, { + onError: err => { + if (signal.aborted) return; + console.error(err); + // Mark as aborted and call the abort function + signal.aborted = true; + signal.abort(); + // For PassThrough streams, we need to properly close them + // when an error occurs during React rendering + process.nextTick(() => { + // Emit error event first so listeners can handle it + if (!rscPayload.destroyed) { + rscPayload.emit("error", err); + rscPayload.destroy(); // Then destroy it + } + }); + }, + filterStackFrame: () => false, + })); + pipe(rscPayload); + + rscPayload.on("error", err => { if (signal.aborted) return; console.error(err); - }, - filterStackFrame: () => false, - })); - pipe(rscPayload); - - rscPayload.on("error", err => { - if (signal.aborted) return; - console.error(err); - }); - - if (skipSSR) { - return new Response(rscPayload as any, { - status: 200, - headers: { "Content-Type": "text/x-component" }, }); - } - // The RSC payload is rendered into HTML - return new Response(renderToHtml(rscPayload, meta.modules, signal), { - headers: { - "Content-Type": "text/html; charset=utf8", - }, - }); + if (skipSSR) { + return new Response(rscPayload as any, { + status: 200, + headers: { "Content-Type": "text/x-component" }, + }); + } + + // The RSC payload is rendered into HTML + return new Response(renderToHtml(rscPayload, meta.modules, signal), { + headers: { + "Content-Type": "text/html; charset=utf8", + }, + }); + } catch (error) { + console.error("Error during React server rendering:", error); + // Return a proper error response instead of hanging + return new Response( + `

500 - Server Error

${error instanceof Error ? error.message : "Unknown error"}
`, + { + status: 500, + headers: { "Content-Type": "text/html; charset=utf8" }, + }, + ); + } } // When a production build is performed, pre-rendering is invoked here. If this diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index 11fa09170369..d197d7224539 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -56,6 +56,12 @@ export function renderToHtml( // with `use`, and then returning the parsed React component for the UI. const Root: any = () => React.use(promise); + // If the signal is already aborted, we should not proceed + if (signal.aborted) { + controller.close(); + return Promise.resolve(); + } + // `renderToPipeableStream` is what actually generates HTML. // Here is where React is told what script tags to inject. let pipe: (stream: any) => void; @@ -64,6 +70,13 @@ export function renderToHtml( onError(error) { if (!signal.aborted) { console.error(error); + // Abort the rendering and close the stream + signal.aborted = true; + abort(); + if (signal.abort) signal.abort(); + if (stream) { + stream.controller.close(); + } } }, })); @@ -133,19 +146,29 @@ class RscInjectionStream extends EventEmitter { /** Resolved when all data is written */ finished: Promise; finalize: () => void; + reject: (err: any) => void; constructor(rscPayload: Readable, controller: ReadableStreamDirectController) { super(); this.controller = controller; - const { resolve, promise } = Promise.withResolvers(); + const { resolve, promise, reject } = Promise.withResolvers(); this.finished = promise; this.finalize = resolve; + this.reject = reject; rscPayload.on("data", this.writeRscData.bind(this)); rscPayload.on("end", () => { this.rscHasEnded = true; }); + rscPayload.on("error", err => { + console.error("RSC payload stream error in RscInjectionStream:", err); + this.rscHasEnded = true; + // Close the controller + controller.close(); + // Reject the promise instead of resolving it + this.reject(err); + }); } write(data: Uint8Array) { From 1b9b686772cf32cb7c3d38a691bc3980b44e4fdf Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 5 Aug 2025 20:22:09 -0700 Subject: [PATCH 015/130] fix compile errors from merge --- src/bake/DevServer/IncrementalGraph.zig | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/bake/DevServer/IncrementalGraph.zig b/src/bake/DevServer/IncrementalGraph.zig index 96228cad697a..69f57b1f29aa 100644 --- a/src/bake/DevServer/IncrementalGraph.zig +++ b/src/bake/DevServer/IncrementalGraph.zig @@ -511,7 +511,7 @@ pub fn IncrementalGraph(side: bake.Side) type { .js => |js| { // Insert new source map or patch existing empty source map. const source_map: PackedMap.RefOrEmpty = brk: { - if (js.source_map) |source_map| { + if (js.source_map) |*source_map| { bun.debugAssert(!flags.is_html_route); // suspect behind #17956 if (source_map.chunk.buffer.len() > 0) { flags.source_map_state = .ref; @@ -624,14 +624,14 @@ pub fn IncrementalGraph(side: bake.Side) type { } } } else { - if (content.js.source_map) |source_map| { - const packed_map = PackedMap.newNonEmpty(source_map.chunk, source_map.escaped_source); + if (content.js.source_map) |source_map| append_empty: { + const packed_map = PackedMap.newNonEmpty(source_map.chunk, source_map.escaped_source orelse break :append_empty); try g.current_chunk_source_maps.append(dev.allocator, .{ .ref = packed_map, }); - } else { - try g.current_chunk_source_maps.append(dev.allocator, PackedMap.RefOrEmpty.blank_empty); + return; } + try g.current_chunk_source_maps.append(dev.allocator, PackedMap.RefOrEmpty.blank_empty); } } }, @@ -1779,7 +1779,7 @@ pub fn IncrementalGraph(side: bake.Side) type { }; /// Uses `arena` as a temporary allocator, fills in all fields of `out` except ref_count - pub fn takeSourceMap(g: *@This(), arena: std.mem.Allocator, gpa: Allocator, out: *SourceMapStore.Entry) bun.OOM!void { + pub fn takeSourceMap(g: *@This(), _: std.mem.Allocator, gpa: Allocator, out: *SourceMapStore.Entry) bun.OOM!void { const paths = g.bundled_files.keys(); switch (side) { @@ -1789,9 +1789,6 @@ pub fn IncrementalGraph(side: bake.Side) type { const buf = bun.path_buffer_pool.get(); defer bun.path_buffer_pool.put(buf); - const buf = bun.path_buffer_pool.get(); - defer bun.path_buffer_pool.put(buf); - var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len); errdefer file_paths.deinit(gpa); var contained_maps: bun.MultiArrayList(PackedMap.RefOrEmpty) = .empty; From 160625c37ca9b2aeb03df6a7e1087f8637a84182 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 6 Aug 2025 18:17:46 -0700 Subject: [PATCH 016/130] remove debugging stuff --- src/bake/bun-framework-react/server.tsx | 138 +++++++++++------------- src/bake/bun-framework-react/ssr.tsx | 1 - 2 files changed, 63 insertions(+), 76 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 55bbc051d700..292413e31e14 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -55,86 +55,74 @@ function component(mod: any, params: Record | null) { // `server.tsx` exports a function to be used for handling user routes. It takes // in the Request object, the route's module, and extra route metadata. export async function render(request: Request, meta: Bake.RouteMetadata): Promise { - try { - // The framework generally has two rendering modes. - // - Standard browser navigation - // - Client-side navigation - // - // For React, this means calling `renderToReadableStream` to generate the RSC - // payload, but only generate HTML for the former of these rendering modes. - // This is signaled by `client.tsx` via the `Accept` header. - const skipSSR = request.headers.get("Accept")?.includes("text/x-component"); - - // Do not render tags if the request is skipping SSR. - const page = getPage(meta, skipSSR ? [] : meta.styles); - - // TODO: write a lightweight version of PassThrough - const rscPayload = new PassThrough(); - - if (skipSSR) { - // "client.tsx" reads the start of the response to determine the - // CSS files to load. The styles are loaded before the new page - // is presented, to avoid a flash of unstyled content. - const int = Buffer.allocUnsafe(4); - const str = meta.styles.join("\n"); - int.writeUInt32LE(str.length, 0); - rscPayload.write(int); - rscPayload.write(str); - } + // The framework generally has two rendering modes. + // - Standard browser navigation + // - Client-side navigation + // + // For React, this means calling `renderToReadableStream` to generate the RSC + // payload, but only generate HTML for the former of these rendering modes. + // This is signaled by `client.tsx` via the `Accept` header. + const skipSSR = request.headers.get("Accept")?.includes("text/x-component"); + + // Do not render tags if the request is skipping SSR. + const page = getPage(meta, skipSSR ? [] : meta.styles); + + // TODO: write a lightweight version of PassThrough + const rscPayload = new PassThrough(); + + if (skipSSR) { + // "client.tsx" reads the start of the response to determine the + // CSS files to load. The styles are loaded before the new page + // is presented, to avoid a flash of unstyled content. + const int = Buffer.allocUnsafe(4); + const str = meta.styles.join("\n"); + int.writeUInt32LE(str.length, 0); + rscPayload.write(int); + rscPayload.write(str); + } - // This renders Server Components to a ReadableStream "RSC Payload" - let pipe; - const signal: MiniAbortSignal = { aborted: false, abort: null! }; - ({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, { - onError: err => { - if (signal.aborted) return; - console.error(err); - // Mark as aborted and call the abort function - signal.aborted = true; - signal.abort(); - // For PassThrough streams, we need to properly close them - // when an error occurs during React rendering - process.nextTick(() => { - // Emit error event first so listeners can handle it - if (!rscPayload.destroyed) { - rscPayload.emit("error", err); - rscPayload.destroy(); // Then destroy it - } - }); - }, - filterStackFrame: () => false, - })); - pipe(rscPayload); - - rscPayload.on("error", err => { + // This renders Server Components to a ReadableStream "RSC Payload" + let pipe; + const signal: MiniAbortSignal = { aborted: false, abort: null! }; + ({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, { + onError: err => { if (signal.aborted) return; - console.error(err); - }); - if (skipSSR) { - return new Response(rscPayload as any, { - status: 200, - headers: { "Content-Type": "text/x-component" }, - }); - } - - // The RSC payload is rendered into HTML - return new Response(renderToHtml(rscPayload, meta.modules, signal), { - headers: { - "Content-Type": "text/html; charset=utf8", - }, + // Mark as aborted and call the abort function + signal.aborted = true; + signal.abort(); + // For PassThrough streams, we need to properly close them + // when an error occurs during React rendering + // process.nextTick(() => { + // // Emit error event first so listeners can handle it + // if (!rscPayload.destroyed) { + // rscPayload.emit("error", err); + // rscPayload.destroy(); // Then destroy it + // } + // }); + }, + filterStackFrame: () => false, + })); + pipe(rscPayload); + + rscPayload.on("error", err => { + if (signal.aborted) return; + console.error(err); + }); + + if (skipSSR) { + return new Response(rscPayload as any, { + status: 200, + headers: { "Content-Type": "text/x-component" }, }); - } catch (error) { - console.error("Error during React server rendering:", error); - // Return a proper error response instead of hanging - return new Response( - `

500 - Server Error

${error instanceof Error ? error.message : "Unknown error"}
`, - { - status: 500, - headers: { "Content-Type": "text/html; charset=utf8" }, - }, - ); } + + // The RSC payload is rendered into HTML + return new Response(renderToHtml(rscPayload, meta.modules, signal), { + headers: { + "Content-Type": "text/html; charset=utf8", + }, + }); } // When a production build is performed, pre-rendering is invoked here. If this diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index d197d7224539..6d6746265069 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -162,7 +162,6 @@ class RscInjectionStream extends EventEmitter { this.rscHasEnded = true; }); rscPayload.on("error", err => { - console.error("RSC payload stream error in RscInjectionStream:", err); this.rscHasEnded = true; // Close the controller controller.close(); From 47d6e161fed8a4f69e0ea07e315bfc0ad30855b3 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:10:05 -0700 Subject: [PATCH 017/130] fix that --- cmake/sources/CxxSources.txt | 1 + src/bake/bun-framework-react/server.tsx | 9 ++++++--- src/bake/bun-framework-react/ssr.tsx | 12 ++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cmake/sources/CxxSources.txt b/cmake/sources/CxxSources.txt index ddc959fa372c..30c3a4025c17 100644 --- a/cmake/sources/CxxSources.txt +++ b/cmake/sources/CxxSources.txt @@ -3,6 +3,7 @@ packages/bun-usockets/src/crypto/sni_tree.cpp src/bake/BakeGlobalObject.cpp src/bake/BakeProduction.cpp src/bake/BakeSourceProvider.cpp +src/bake/DevServerSourceProvider.cpp src/bun.js/bindings/ActiveDOMCallback.cpp src/bun.js/bindings/AsymmetricKeyValue.cpp src/bun.js/bindings/AsyncContextFrame.cpp diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 292413e31e14..a4b9b7927cd7 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -86,11 +86,13 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis const signal: MiniAbortSignal = { aborted: false, abort: null! }; ({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, { onError: err => { + // console.error("onError renderToPipeableStream", !!signal.aborted); if (signal.aborted) return; // Mark as aborted and call the abort function - signal.aborted = true; - signal.abort(); + signal.aborted = err; + signal.abort(err); + rscPayload.destroy(err); // For PassThrough streams, we need to properly close them // when an error occurs during React rendering // process.nextTick(() => { @@ -106,8 +108,9 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis pipe(rscPayload); rscPayload.on("error", err => { + // console.error("rscPayload.on('error')", !!signal.aborted); if (signal.aborted) return; - console.error(err); + // console.error(err); }); if (skipSSR) { diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index 6d6746265069..d375deaef564 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -58,8 +58,8 @@ export function renderToHtml( // If the signal is already aborted, we should not proceed if (signal.aborted) { - controller.close(); - return Promise.resolve(); + controller.close(signal.aborted); + return Promise.reject(signal.aborted); } // `renderToPipeableStream` is what actually generates HTML. @@ -69,7 +69,7 @@ export function renderToHtml( bootstrapModules, onError(error) { if (!signal.aborted) { - console.error(error); + // console.error(error); // Abort the rendering and close the stream signal.aborted = true; abort(); @@ -87,8 +87,8 @@ export function renderToHtml( // Promise resolved after all data is combined. return stream.finished; }, - cancel() { - signal.aborted = true; + cancel(err) { + // console.error("renderToHtml.cancel"); signal.abort(); abort?.(); }, @@ -306,7 +306,7 @@ class StaticRscInjectionStream extends EventEmitter { } destroy(error) { - console.error(error); + // console.error(error); this.reject(error); } } From 0f74fafc5968a2959fd6ce238ef92983140b7516 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:24:03 -0700 Subject: [PATCH 018/130] cleanup --- src/bake/bun-framework-react/server.tsx | 20 +++----------------- src/bake/bun-framework-react/ssr.tsx | 8 +++++--- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index a4b9b7927cd7..64e09554969e 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -83,7 +83,7 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis // This renders Server Components to a ReadableStream "RSC Payload" let pipe; - const signal: MiniAbortSignal = { aborted: false, abort: null! }; + const signal: MiniAbortSignal = { aborted: undefined, abort: null! }; ({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, { onError: err => { // console.error("onError renderToPipeableStream", !!signal.aborted); @@ -91,28 +91,14 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis // Mark as aborted and call the abort function signal.aborted = err; + // @ts-expect-error signal.abort(err); rscPayload.destroy(err); - // For PassThrough streams, we need to properly close them - // when an error occurs during React rendering - // process.nextTick(() => { - // // Emit error event first so listeners can handle it - // if (!rscPayload.destroyed) { - // rscPayload.emit("error", err); - // rscPayload.destroy(); // Then destroy it - // } - // }); }, filterStackFrame: () => false, })); pipe(rscPayload); - rscPayload.on("error", err => { - // console.error("rscPayload.on('error')", !!signal.aborted); - if (signal.aborted) return; - // console.error(err); - }); - if (skipSSR) { return new Response(rscPayload as any, { status: 200, @@ -199,7 +185,7 @@ export const contentTypeToStaticFile = { /** Instead of using AbortController, this is used */ export interface MiniAbortSignal { - aborted: boolean; + aborted: Error | undefined; /** Caller must set `aborted` to true before calling. */ abort: () => void; } diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index d375deaef564..da2995fd545b 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -71,7 +71,7 @@ export function renderToHtml( if (!signal.aborted) { // console.error(error); // Abort the rendering and close the stream - signal.aborted = true; + signal.aborted = error; abort(); if (signal.abort) signal.abort(); if (stream) { @@ -88,8 +88,10 @@ export function renderToHtml( return stream.finished; }, cancel(err) { - // console.error("renderToHtml.cancel"); - signal.abort(); + if (!signal.aborted) { + signal.aborted = err; + signal.abort(err); + } abort?.(); }, } as Bun.DirectUnderlyingSource as any); From c8773c5e30dada3e3dfadff7bc0375fc8044c0dc Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:04:58 -0700 Subject: [PATCH 019/130] error modal on ssr error --- src/bun.js/api/server/RequestContext.zig | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/bun.js/api/server/RequestContext.zig b/src/bun.js/api/server/RequestContext.zig index 0fab727adaf0..30995fecb2c6 100644 --- a/src/bun.js/api/server/RequestContext.zig +++ b/src/bun.js/api/server/RequestContext.zig @@ -1675,6 +1675,55 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(req.allocator); defer exception_list.deinit(); server.vm.runErrorHandler(err, &exception_list); + + // Render the error fallback HTML page like renderDefaultError does + if (!req.flags.has_written_status) { + req.flags.has_written_status = true; + if (req.resp) |resp| { + resp.writeStatus("500 Internal Server Error"); + resp.writeHeader("content-type", MimeType.html.value); + } + } + + const allocator = req.allocator; + const fallback_container = allocator.create(Api.FallbackMessageContainer) catch unreachable; + defer allocator.destroy(fallback_container); + + // Create error message for the stream rejection + const error_message = "Stream error during server-side rendering"; + + fallback_container.* = Api.FallbackMessageContainer{ + .message = allocator.dupe(u8, error_message) catch unreachable, + .router = null, + .reason = .fetch_event_handler, + .cwd = server.vm.transpiler.fs.top_level_dir, + .problems = Api.Problems{ + .code = 500, + .name = "StreamError", + .exceptions = exception_list.items, + .build = .{ + .msgs = &.{}, + }, + }, + }; + + var bb = std.ArrayList(u8).init(allocator); + defer bb.clearAndFree(); + const bb_writer = bb.writer(); + + Fallback.renderBackend( + allocator, + fallback_container, + @TypeOf(bb_writer), + bb_writer, + ) catch unreachable; + + if (req.resp) |resp| { + _ = resp.write(bb.items); + } + + req.endStream(req.shouldCloseConnection()); + return; } } } From dfa93a8ede800343ee23d83cf63e7d7e7a58de29 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:18:19 -0700 Subject: [PATCH 020/130] small --- src/bake/bun-framework-react/ssr.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index da2995fd545b..c549cd774c8b 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -92,7 +92,7 @@ export function renderToHtml( signal.aborted = err; signal.abort(err); } - abort?.(); + abort?.(err); }, } as Bun.DirectUnderlyingSource as any); } From 78a2ae44aa4b50cba595b5378724247ced4199d6 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:30:44 -0700 Subject: [PATCH 021/130] move change back --- src/ast/visitExpr.zig | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ast/visitExpr.zig b/src/ast/visitExpr.zig index 7c508a81e9c1..7fe130e7f9e1 100644 --- a/src/ast/visitExpr.zig +++ b/src/ast/visitExpr.zig @@ -1439,9 +1439,20 @@ pub fn VisitExpr( // Why? Because we *don't* want to check for uses of // `useState` _inside_ React, and we know React uses // commonjs so it will never be `.e_import_identifier`. - e_.target.data == .e_import_identifier) { + check_for_usestate: { + if (e_.target.data == .e_import_identifier) break :check_for_usestate true; + // Also check for `React.useState(...)` + if (e_.target.data == .e_dot and e_.target.data.e_dot.target.data == .e_import_identifier) { + const id = e_.target.data.e_dot.target.data.e_import_identifier; + const name = p.symbols.items[id.ref.innerIndex()].original_name; + break :check_for_usestate bun.strings.eqlComptime(name, "React"); + } + break :check_for_usestate false; + }) { bun.assert(p.options.features.server_components.isServerSide()); - if (bun.strings.eqlComptime(original_name, "useState")) { + if (!bun.strings.startsWith(p.source.path.pretty, "node_modules") and + bun.strings.eqlComptime(original_name, "useState")) + { p.log.addError( p.source, expr.loc, From 59a4d0697b1b38f82c836a8d7748ca75d977ebfe Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:31:27 -0700 Subject: [PATCH 022/130] fix --- src/allocators/AllocationScope.zig | 6 ++++++ src/bake/DevServer.zig | 6 +++++- src/bake/DevServer/IncrementalGraph.zig | 12 +++++++++++- src/bake/DevServer/SourceMapStore.zig | 1 + 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/allocators/AllocationScope.zig b/src/allocators/AllocationScope.zig index a37e3fa555eb..e31f8b04925a 100644 --- a/src/allocators/AllocationScope.zig +++ b/src/allocators/AllocationScope.zig @@ -252,6 +252,12 @@ pub inline fn downcast(a: Allocator) ?*AllocationScope { null; } +pub fn leakSlice(scope: *AllocationScope, memory: anytype) void { + if (comptime !enabled) return; + _ = @typeInfo(@TypeOf(memory)).pointer; + bun.assert(!scope.trackExternalFree(memory, null)); +} + const std = @import("std"); const Allocator = std.mem.Allocator; diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index e132eee1f3d7..f332f86035fa 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2308,9 +2308,13 @@ pub fn finalizeBundle( defer dev.allocator.free(server_bundle); const server_modules = if (bun.take(&source_map_json)) |json| blk: { + // This memory will be owned by the `DevServerSourceProvider` in C++ + // from here on out + dev.allocation_scope.leakSlice(json); + break :blk c.BakeLoadServerHmrPatchWithSourceMap( @ptrCast(dev.vm.global), - bun.String.cloneLatin1(server_bundle), + bun.String.cloneUTF8(server_bundle), json.ptr, json.len, ) catch |err| { diff --git a/src/bake/DevServer/IncrementalGraph.zig b/src/bake/DevServer/IncrementalGraph.zig index 0eb68801ab9b..b0c8c79afc50 100644 --- a/src/bake/DevServer/IncrementalGraph.zig +++ b/src/bake/DevServer/IncrementalGraph.zig @@ -631,7 +631,17 @@ pub fn IncrementalGraph(side: bake.Side) type { }); return; } - try g.current_chunk_source_maps.append(dev.allocator, PackedMap.RefOrEmpty.blank_empty); + + // Must precompute this. Otherwise, source maps won't have + // the info needed to concatenate VLQ mappings. + const count: u32 = @intCast(bun.strings.countChar(content.js.code, '\n')); + try g.current_chunk_source_maps.append(dev.allocator, PackedMap.RefOrEmpty{ + .empty = .{ + .line_count = .init(count), + // TODO: not sure if this is correct + .html_bundle_route_index = .none, + }, + }); } } }, diff --git a/src/bake/DevServer/SourceMapStore.zig b/src/bake/DevServer/SourceMapStore.zig index b4905cd0aab9..65d506be19de 100644 --- a/src/bake/DevServer/SourceMapStore.zig +++ b/src/bake/DevServer/SourceMapStore.zig @@ -120,6 +120,7 @@ pub const Entry = struct { if (Environment.isWindows and !is_windows_drive_path) { // UNC namespace -> file://server/share/path.ext encodeSourceMapPath( + side, if (path.len > 2 and path[0] == '/' and path[1] == '/') path[2..] else From 20e4c094ac7f6ab0f94b62383067894e7dacfa68 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:13:31 -0700 Subject: [PATCH 023/130] support `streaming = false | true= --- src/bake/bun-framework-react/server.tsx | 47 ++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 789e1c606892..f8b090353acc 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -64,6 +64,9 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis // This is signaled by `client.tsx` via the `Accept` header. const skipSSR = request.headers.get("Accept")?.includes("text/x-component"); + // Check if the page module has a streaming export, default to false + const streaming = meta.pageModule.streaming ?? false; + // Do not render tags if the request is skipping SSR. const page = getPage(meta, skipSSR ? [] : meta.styles); @@ -107,11 +110,45 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis } // The RSC payload is rendered into HTML - return new Response(renderToHtml(rscPayload, meta.modules, signal), { - headers: { - "Content-Type": "text/html; charset=utf8", - }, - }); + if (streaming) { + // Stream the response as before + return new Response(renderToHtml(rscPayload, meta.modules, signal), { + headers: { + "Content-Type": "text/html; charset=utf8", + }, + }); + } else { + // TODO: this seems shitty + // Buffer the entire response and return it all at once + const htmlStream = renderToHtml(rscPayload, meta.modules, signal); + const chunks: Uint8Array[] = []; + const reader = htmlStream.getReader(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + + // Combine all chunks into a single response + const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return new Response(result, { + headers: { + "Content-Type": "text/html; charset=utf8", + }, + }); + } } // When a production build is performed, pre-rendering is invoked here. If this From cfdeb4202333e17aaf3fcb9ef54f0e6fe53751d5 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:23:30 -0700 Subject: [PATCH 024/130] WIP --- src/bake/DevServer.zig | 5 + src/bake/bun-framework-react/server.tsx | 47 +------- src/bun.js/VirtualMachine.zig | 10 ++ src/bun.js/bindings/JSGlobalObject.zig | 4 + src/bun.js/bindings/JSValue.zig | 15 +++ src/bun.js/bindings/bindings.cpp | 140 ++++++++++++++++++++++++ src/bun.js/webcore/Response.zig | 16 ++- 7 files changed, 194 insertions(+), 43 deletions(-) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 3fa14d449769..07ec7b442c56 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2307,6 +2307,11 @@ pub fn finalizeBundle( }); defer dev.allocator.free(server_bundle); + // TODO: is this the best place to set this? Would it be better to + // transpile the server modules to replace `new Response(...)` with `new + // ResponseBake(...)`?? + dev.vm.setAllowJSXInResponseConstructor(true); + const server_modules = if (bun.take(&source_map_json)) |json| blk: { // This memory will be owned by the `DevServerSourceProvider` in C++ // from here on out diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index f8b090353acc..789e1c606892 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -64,9 +64,6 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis // This is signaled by `client.tsx` via the `Accept` header. const skipSSR = request.headers.get("Accept")?.includes("text/x-component"); - // Check if the page module has a streaming export, default to false - const streaming = meta.pageModule.streaming ?? false; - // Do not render tags if the request is skipping SSR. const page = getPage(meta, skipSSR ? [] : meta.styles); @@ -110,45 +107,11 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis } // The RSC payload is rendered into HTML - if (streaming) { - // Stream the response as before - return new Response(renderToHtml(rscPayload, meta.modules, signal), { - headers: { - "Content-Type": "text/html; charset=utf8", - }, - }); - } else { - // TODO: this seems shitty - // Buffer the entire response and return it all at once - const htmlStream = renderToHtml(rscPayload, meta.modules, signal); - const chunks: Uint8Array[] = []; - const reader = htmlStream.getReader(); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - } - } finally { - reader.releaseLock(); - } - - // Combine all chunks into a single response - const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); - const result = new Uint8Array(totalLength); - let offset = 0; - for (const chunk of chunks) { - result.set(chunk, offset); - offset += chunk.length; - } - - return new Response(result, { - headers: { - "Content-Type": "text/html; charset=utf8", - }, - }); - } + return new Response(renderToHtml(rscPayload, meta.modules, signal), { + headers: { + "Content-Type": "text/html; charset=utf8", + }, + }); } // When a production build is performed, pre-rendering is invoked here. If this diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 30db947fee4d..d9a1e3acfc9d 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -196,6 +196,16 @@ has_mutated_built_in_extensions: u32 = 0, initial_script_execution_context_identifier: i32, +allow_jsx_in_response_constructor: bool = false, + +pub fn setAllowJSXInResponseConstructor(this: *VirtualMachine, value: bool) void { + this.allow_jsx_in_response_constructor = value; +} + +pub fn allowJSXInResponseConstructor(this: *VirtualMachine) bool { + return this.allow_jsx_in_response_constructor; +} + pub const ProcessAutoKiller = @import("./ProcessAutoKiller.zig"); pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSGlobalObject, JSValue) void; diff --git a/src/bun.js/bindings/JSGlobalObject.zig b/src/bun.js/bindings/JSGlobalObject.zig index 7cbada9a9412..64a38b53ff0f 100644 --- a/src/bun.js/bindings/JSGlobalObject.zig +++ b/src/bun.js/bindings/JSGlobalObject.zig @@ -12,6 +12,10 @@ pub const JSGlobalObject = opaque { return error.JSError; } + pub fn allowJSXInResponseConstructor(this: *JSGlobalObject) bool { + return this.bunVM().allowJSXInResponseConstructor(); + } + extern fn JSGlobalObject__createOutOfMemoryError(this: *JSGlobalObject) JSValue; pub fn createOutOfMemoryError(this: *JSGlobalObject) JSValue { return JSGlobalObject__createOutOfMemoryError(this); diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 0b9a4df64ae0..92a00c9916a7 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -45,6 +45,21 @@ pub const JSValue = enum(i64) { return jsc.JSObject.getIndex(this, globalThis, i); } + extern fn JSC__JSValue__isJSXElement(JSValue, *JSGlobalObject) bool; + pub fn isJSXElement(this: JSValue, globalThis: *jsc.JSGlobalObject) JSError!bool { + return bun.jsc.fromJSHostCallGeneric( + globalThis, + @src(), + JSC__JSValue__isJSXElement, + .{ this, globalThis }, + ); + } + + extern fn JSC__JSValue__transformToReactElement(responseValue: JSValue, componentValue: JSValue, globalObject: *JSGlobalObject) void; + pub fn transformToReactElement(responseValue: JSValue, componentValue: JSValue, globalThis: *jsc.JSGlobalObject) void { + JSC__JSValue__transformToReactElement(responseValue, componentValue, globalThis); + } + extern fn JSC__JSValue__getDirectIndex(JSValue, *JSGlobalObject, u32) JSValue; pub fn getDirectIndex(this: JSValue, globalThis: *JSGlobalObject, i: u32) JSValue { return JSC__JSValue__getDirectIndex(this, globalThis, i); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 9205857e7118..2c38dd25a258 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5884,12 +5884,152 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: JSC__JSValue__forEachPropertyImpl(JSValue0, globalObject, arg2, iter); } +extern "C" bool JSC__JSValue__isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject) +{ + + auto& vm = JSC::getVM(globalObject); + + // React does this: + // export const REACT_LEGACY_ELEMENT_TYPE: symbol = Symbol.for('react.element'); + // export const REACT_ELEMENT_TYPE: symbol = renameElementSymbol + // ? Symbol.for('react.transitional.element') + // : REACT_LEGACY_ELEMENT_TYPE; + + // TODO: cache these, i cri everytim + auto react_legacy_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.element"_s)); + auto react_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.transitional.element"_s)); + + JSC::JSValue value = JSC::JSValue::decode(JSValue0); + + // If it's a function (React component), call it to get the element + if (value.isCallable()) { + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::JSObject* function = value.getObject(); + JSC::CallData callData = JSC::getCallData(function); + JSC::MarkedArgumentBuffer args; + + // Call the component function with no arguments + JSC::JSValue result = JSC::call(globalObject, function, callData, JSC::jsUndefined(), args); + RETURN_IF_EXCEPTION(scope, false); + + // Now check if the result is a JSX element + if (!result.isObject()) { + return false; + } + + JSC::JSObject* resultObject = result.getObject(); + auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s); + JSC::JSValue typeofValue = resultObject->get(globalObject, typeofProperty); + RETURN_IF_EXCEPTION(scope, false); + + if (typeofValue.isSymbol() && (typeofValue == react_legacy_element_symbol || typeofValue == react_element_symbol)) { + return true; + } + } + // If it's already an object, check directly for $$typeof + else if (value.isObject()) { + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::JSObject* object = value.getObject(); + auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s); + JSC::JSValue typeofValue = object->get(globalObject, typeofProperty); + RETURN_IF_EXCEPTION(scope, false); + + if (typeofValue.isSymbol() && (typeofValue == react_legacy_element_symbol || typeofValue == react_element_symbol)) { + return true; + } + } + + return false; +} + +extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue responseValue, JSC::EncodedJSValue componentValue, JSC::JSGlobalObject* globalObject) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_CATCH_SCOPE(vm); + + JSC::JSValue response = JSC::JSValue::decode(responseValue); + JSC::JSValue component = JSC::JSValue::decode(componentValue); + + if (!response.isObject()) { + return; + } + + JSC::JSObject* responseObject = response.getObject(); + + // Get the React element symbol - same as in isJSXElement + // For now, use the transitional element symbol (React 19+) + // TODO: check for legacy symbol as fallback + auto react_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.transitional.element"_s)); + JSC::JSValue symbolToUse = react_element_symbol; + + // Set $$typeof property + auto typeofIdentifier = JSC::Identifier::fromString(vm, "$$typeof"_s); + responseObject->putDirect(vm, typeofIdentifier, symbolToUse, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + + // Set type property to the component if it's a function, otherwise keep the JSX element as-is + auto typeIdentifier = JSC::Identifier::fromString(vm, "type"_s); + if (component.isCallable()) { + responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } else if (component.isObject()) { + // If it's already a JSX element, extract its type + JSC::JSObject* componentObject = component.getObject(); + JSC::JSValue typeValue = componentObject->get(globalObject, typeIdentifier); + if (!scope.exception() && !typeValue.isUndefined()) { + responseObject->putDirect(vm, typeIdentifier, typeValue, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } + } + + // Set key property to null + auto keyIdentifier = JSC::Identifier::fromString(vm, "key"_s); + responseObject->putDirect(vm, keyIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + + // Set props property to empty object (or extract from component if it's an element) + auto propsIdentifier = JSC::Identifier::fromString(vm, "props"_s); + if (component.isObject()) { + JSC::JSObject* componentObject = component.getObject(); + JSC::JSValue propsValue = componentObject->get(globalObject, propsIdentifier); + if (!scope.exception() && !propsValue.isUndefined()) { + responseObject->putDirect(vm, propsIdentifier, propsValue, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } else { + responseObject->putDirect(vm, propsIdentifier, JSC::constructEmptyObject(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } + } else { + responseObject->putDirect(vm, propsIdentifier, JSC::constructEmptyObject(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } + + // Add _store object for dev mode + auto storeIdentifier = JSC::Identifier::fromString(vm, "_store"_s); + JSC::JSObject* storeObject = JSC::constructEmptyObject(globalObject); + + // Add validated property to _store + auto validatedIdentifier = JSC::Identifier::fromString(vm, "validated"_s); + storeObject->putDirect(vm, validatedIdentifier, JSC::jsNumber(0), 0); + + responseObject->putDirect(vm, storeIdentifier, storeObject, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + + // Add debug properties (all set to null) + auto ownerIdentifier = JSC::Identifier::fromString(vm, "_owner"_s); + responseObject->putDirect(vm, ownerIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + + auto debugInfoIdentifier = JSC::Identifier::fromString(vm, "_debugInfo"_s); + responseObject->putDirect(vm, debugInfoIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + + auto debugStackIdentifier = JSC::Identifier::fromString(vm, "_debugStack"_s); + responseObject->putDirect(vm, debugStackIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + + auto debugTaskIdentifier = JSC::Identifier::fromString(vm, "_debugTask"_s); + responseObject->putDirect(vm, debugTaskIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); +} + extern "C" void JSC__JSValue__forEachPropertyNonIndexed(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)(JSC::JSGlobalObject* arg0, void* ctx, ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) { JSC__JSValue__forEachPropertyImpl(JSValue0, globalObject, arg2, iter); } [[ZIG_EXPORT(check_slow)]] void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)([[ZIG_NONNULL]] JSC::JSGlobalObject* arg0, void* ctx, [[ZIG_NONNULL]] ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) + { JSC::JSValue value = JSC::JSValue::decode(JSValue0); JSC::JSObject* object = value.getObject(); diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 2453ca8825c6..2230c21833d9 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -199,8 +199,12 @@ pub fn getURL( pub fn getResponseType( this: *Response, + this_value: jsc.JSValue, globalThis: *jsc.JSGlobalObject, ) jsc.JSValue { + if (js.gc.jsxElement.get(this_value)) |jsx_element| { + return jsx_element; + } if (this.init.status_code < 200) { return bun.String.static("error").toJS(globalThis); } @@ -303,6 +307,7 @@ pub fn cloneValue( } pub fn clone(this: *Response, globalThis: *JSGlobalObject) bun.JSError!*Response { + // TODO: handle clone for jsxElement for bake? return bun.new(Response, try this.cloneValue(globalThis)); } @@ -519,7 +524,7 @@ pub fn constructError( return response.toJS(globalThis); } -pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*Response { +pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, this_value: jsc.JSValue) bun.JSError!*Response { const arguments = callframe.argumentsAsArray(2); if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) { @@ -554,6 +559,15 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b return bun.new(Response, response); } } + + // Special case for bake: allow `return new Response( ... , { ... }` + // inside of a react component + if (globalThis.allowJSXInResponseConstructor() and try arguments[0].isJSXElement(globalThis)) { + // Store the JSX element for later retrieval + js.gc.jsxElement.set(this_value, globalThis, arguments[0]); + // Transform the Response object to look like a React element + JSValue.transformToReactElement(this_value, arguments[0], globalThis); + } } var init: Init = (brk: { if (arguments[1].isUndefinedOrNull()) { From 87487468f32f55d22f664857df56bcf576f2f55f Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:07:31 -0700 Subject: [PATCH 025/130] wip --- src/bake/bun-framework-react/server.tsx | 47 +++++++++++++++-- src/bun.js/bindings/bindings.cpp | 67 +++++-------------------- src/bun.js/webcore/Response.zig | 12 +++-- src/bun.js/webcore/response.classes.ts | 3 ++ 4 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 789e1c606892..213b0f8d5805 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -64,6 +64,9 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis // This is signaled by `client.tsx` via the `Accept` header. const skipSSR = request.headers.get("Accept")?.includes("text/x-component"); + // Check if the page module has a streaming export, default to false + const streaming = meta.pageModule.streaming ?? false; + // Do not render tags if the request is skipping SSR. const page = getPage(meta, skipSSR ? [] : meta.styles); @@ -107,11 +110,45 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis } // The RSC payload is rendered into HTML - return new Response(renderToHtml(rscPayload, meta.modules, signal), { - headers: { - "Content-Type": "text/html; charset=utf8", - }, - }); + if (streaming) { + // Stream the response as before + return new Response(renderToHtml(rscPayload, meta.modules, signal), { + headers: { + "Content-Type": "text/html; charset=utf8", + }, + }); + } else { + // TODO: this seems shitty + // Buffer the entire response and return it all at once + const htmlStream = renderToHtml(rscPayload, meta.modules, signal); + const chunks: Uint8Array[] = []; + const reader = htmlStream.getReader(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + + // Combine all chunks into a single response + const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return new Response(result, { + headers: { + "Content-Type": "text/html; charset=utf8", + }, + }); + } } // When a production build is performed, pre-rendering is invoked here. If this diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 2c38dd25a258..aaedc135931d 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5901,40 +5901,16 @@ extern "C" bool JSC__JSValue__isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JS JSC::JSValue value = JSC::JSValue::decode(JSValue0); - // If it's a function (React component), call it to get the element - if (value.isCallable()) { - auto scope = DECLARE_THROW_SCOPE(vm); - - JSC::JSObject* function = value.getObject(); - JSC::CallData callData = JSC::getCallData(function); - JSC::MarkedArgumentBuffer args; - - // Call the component function with no arguments - JSC::JSValue result = JSC::call(globalObject, function, callData, JSC::jsUndefined(), args); - RETURN_IF_EXCEPTION(scope, false); - - // Now check if the result is a JSX element - if (!result.isObject()) { - return false; - } - - JSC::JSObject* resultObject = result.getObject(); - auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s); - JSC::JSValue typeofValue = resultObject->get(globalObject, typeofProperty); - RETURN_IF_EXCEPTION(scope, false); - - if (typeofValue.isSymbol() && (typeofValue == react_legacy_element_symbol || typeofValue == react_element_symbol)) { - return true; - } - } - // If it's already an object, check directly for $$typeof - else if (value.isObject()) { + // TODO: primitive values (strings, numbers, booleans, null, undefined) are also valid + if (value.isObject()) { auto scope = DECLARE_THROW_SCOPE(vm); JSC::JSObject* object = value.getObject(); auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s); JSC::JSValue typeofValue = object->get(globalObject, typeofProperty); RETURN_IF_EXCEPTION(scope, false); + + printf("IS SYMBOL: %s\n", typeofValue.isSymbol() ? "true" : "false"); if (typeofValue.isSymbol() && (typeofValue == react_legacy_element_symbol || typeofValue == react_element_symbol)) { return true; @@ -5962,42 +5938,25 @@ extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue respon // For now, use the transitional element symbol (React 19+) // TODO: check for legacy symbol as fallback auto react_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.transitional.element"_s)); - JSC::JSValue symbolToUse = react_element_symbol; - // Set $$typeof property + // Transform the Response object itself into a React element + // The component parameter is what will be stored in the 'type' field + + // Set $$typeof property to mark this as a React element auto typeofIdentifier = JSC::Identifier::fromString(vm, "$$typeof"_s); - responseObject->putDirect(vm, typeofIdentifier, symbolToUse, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + responseObject->putDirect(vm, typeofIdentifier, react_element_symbol, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - // Set type property to the component if it's a function, otherwise keep the JSX element as-is + // Set type property - this is the component/element that was passed auto typeIdentifier = JSC::Identifier::fromString(vm, "type"_s); - if (component.isCallable()) { - responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - } else if (component.isObject()) { - // If it's already a JSX element, extract its type - JSC::JSObject* componentObject = component.getObject(); - JSC::JSValue typeValue = componentObject->get(globalObject, typeIdentifier); - if (!scope.exception() && !typeValue.isUndefined()) { - responseObject->putDirect(vm, typeIdentifier, typeValue, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - } - } + responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); // Set key property to null auto keyIdentifier = JSC::Identifier::fromString(vm, "key"_s); responseObject->putDirect(vm, keyIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - // Set props property to empty object (or extract from component if it's an element) + // Set props property to empty object auto propsIdentifier = JSC::Identifier::fromString(vm, "props"_s); - if (component.isObject()) { - JSC::JSObject* componentObject = component.getObject(); - JSC::JSValue propsValue = componentObject->get(globalObject, propsIdentifier); - if (!scope.exception() && !propsValue.isUndefined()) { - responseObject->putDirect(vm, propsIdentifier, propsValue, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - } else { - responseObject->putDirect(vm, propsIdentifier, JSC::constructEmptyObject(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - } - } else { - responseObject->putDirect(vm, propsIdentifier, JSC::constructEmptyObject(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - } + responseObject->putDirect(vm, propsIdentifier, JSC::constructEmptyObject(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); // Add _store object for dev mode auto storeIdentifier = JSC::Identifier::fromString(vm, "_store"_s); diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 2230c21833d9..d9a7daef7f22 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -562,11 +562,13 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, t // Special case for bake: allow `return new Response( ... , { ... }` // inside of a react component - if (globalThis.allowJSXInResponseConstructor() and try arguments[0].isJSXElement(globalThis)) { - // Store the JSX element for later retrieval - js.gc.jsxElement.set(this_value, globalThis, arguments[0]); - // Transform the Response object to look like a React element - JSValue.transformToReactElement(this_value, arguments[0], globalThis); + if (globalThis.allowJSXInResponseConstructor()) { + const arg = arguments[0]; + // Check if it's a JSX element (object with $$typeof) + if (try arg.isJSXElement(globalThis)) { + js.gc.jsxElement.set(this_value, globalThis, arg); + JSValue.transformToReactElement(this_value, arg, globalThis); + } } } var init: Init = (brk: { diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index 06cc53375de1..f12f7fb8461e 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -102,6 +102,7 @@ export default [ type: { getter: "getResponseType", + this: true, }, headers: { getter: "getHeaders", @@ -124,6 +125,8 @@ export default [ getter: "getBodyUsed", }, }, + values: ["jsxElement"], + constructNeedsThis: true, }), define({ name: "Blob", From b9da6b71f98475a9da8616ce37979143dba758d6 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:36:34 -0700 Subject: [PATCH 026/130] stupid hack --- src/bun.js/bindings/bindings.cpp | 39 ++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index aaedc135931d..3b6c914eb273 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -48,6 +48,7 @@ #include "JavaScriptCore/JSONObject.h" #include "JavaScriptCore/JSObject.h" #include "JavaScriptCore/JSSet.h" +#include "JavaScriptCore/Strong.h" #include "JavaScriptCore/JSSetIterator.h" #include "JavaScriptCore/JSString.h" #include "JavaScriptCore/ProxyObject.h" @@ -5946,9 +5947,43 @@ extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue respon auto typeofIdentifier = JSC::Identifier::fromString(vm, "$$typeof"_s); responseObject->putDirect(vm, typeofIdentifier, react_element_symbol, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - // Set type property - this is the component/element that was passed + // Set type property auto typeIdentifier = JSC::Identifier::fromString(vm, "type"_s); - responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + + // TODO: this is stupid + // Check if the component is a JSX element (has $$typeof property) + // If it is, we need to wrap it in a function for React to work properly + // because `` is already the result of calling Component() + if (component.isObject()) { + JSC::JSObject* componentObject = component.getObject(); + auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s); + JSC::JSValue typeofValue = componentObject->get(globalObject, typeofProperty); + + if (!scope.exception() && typeofValue.isSymbol()) { + // It's a JSX element - wrap it in a function that returns it + // This creates: () => component + // We need to protect the component value from GC + JSC::Strong strongComponent(vm, component); + + auto* wrapperFunction = JSC::JSNativeStdFunction::create( + vm, + globalObject, + 0, // arity + String(), // name + [strongComponent](JSC::JSGlobalObject*, JSC::CallFrame*) -> JSC::EncodedJSValue { + return JSC::JSValue::encode(strongComponent.get()); + } + ); + + responseObject->putDirect(vm, typeIdentifier, wrapperFunction, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } else { + // It's not a JSX element, use it directly + responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } + } else { + // It's not an object (could be a function or primitive), use it directly + responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } // Set key property to null auto keyIdentifier = JSC::Identifier::fromString(vm, "key"_s); From b4fdf41ea52a553132372af1337f419c22be4f57 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:18:28 -0700 Subject: [PATCH 027/130] WIP --- cmake/sources/CxxSources.txt | 1 + src/bake/bun-framework-react/server.tsx | 14 +-- src/bake/hmr-runtime-server.ts | 34 +++++-- src/bun.js/VirtualMachine.zig | 48 +++++++++- .../bindings/webcore/ResponseHelpers.cpp | 90 +++++++++++++++++++ src/bun.js/webcore/Response.zig | 20 ++++- 6 files changed, 191 insertions(+), 16 deletions(-) create mode 100644 src/bun.js/bindings/webcore/ResponseHelpers.cpp diff --git a/cmake/sources/CxxSources.txt b/cmake/sources/CxxSources.txt index c86b620511f4..b6c9ef948432 100644 --- a/cmake/sources/CxxSources.txt +++ b/cmake/sources/CxxSources.txt @@ -392,6 +392,7 @@ src/bun.js/bindings/webcore/ReadableStreamDefaultController.cpp src/bun.js/bindings/webcore/ReadableStreamSink.cpp src/bun.js/bindings/webcore/ReadableStreamSource.cpp src/bun.js/bindings/webcore/ResourceTiming.cpp +src/bun.js/bindings/webcore/ResponseHelpers.cpp src/bun.js/bindings/webcore/RFC7230.cpp src/bun.js/bindings/webcore/SerializedScriptValue.cpp src/bun.js/bindings/webcore/ServerTiming.cpp diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 213b0f8d5805..caf3a8cd7a7f 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -3,6 +3,7 @@ import { renderToHtml, renderToStaticHtml } from "bun-framework-react/ssr.tsx" w import { serverManifest } from "bun:bake/server"; import { PassThrough } from "node:stream"; import { renderToPipeableStream } from "react-server-dom-bun/server.node.unbundled.js"; +import type { AsyncLocalStorage } from "node:async_hooks"; function assertReactComponent(Component: any) { if (typeof Component !== "function") { @@ -53,8 +54,8 @@ function component(mod: any, params: Record | null) { } // `server.tsx` exports a function to be used for handling user routes. It takes -// in the Request object, the route's module, and extra route metadata. -export async function render(request: Request, meta: Bake.RouteMetadata): Promise { +// in the Request object, the route's module, extra route metadata, and the AsyncLocalStorage instance. +export async function render(request: Request, meta: Bake.RouteMetadata, als?: AsyncLocalStorage): Promise { // The framework generally has two rendering modes. // - Standard browser navigation // - Client-side navigation @@ -106,6 +107,7 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis return new Response(rscPayload as any, { status: 200, headers: { "Content-Type": "text/x-component" }, + ...(als?.getStore() || {}), }); } @@ -116,6 +118,7 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis headers: { "Content-Type": "text/html; charset=utf8", }, + ...(als?.getStore() || {}), }); } else { // TODO: this seems shitty @@ -123,7 +126,7 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis const htmlStream = renderToHtml(rscPayload, meta.modules, signal); const chunks: Uint8Array[] = []; const reader = htmlStream.getReader(); - + try { while (true) { const { done, value } = await reader.read(); @@ -133,7 +136,7 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis } finally { reader.releaseLock(); } - + // Combine all chunks into a single response const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Uint8Array(totalLength); @@ -142,11 +145,12 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis result.set(chunk, offset); offset += chunk.length; } - + return new Response(result, { headers: { "Content-Type": "text/html; charset=utf8", }, + ...(als?.getStore() || {}), }); } } diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 68ed6cb455cb..1ccf68d28249 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -8,6 +8,21 @@ if (typeof IS_BUN_DEVELOPMENT !== "boolean") { throw new Error("DCE is configured incorrectly"); } +// Dynamic import of AsyncLocalStorage to work with bundling +// The require is wrapped in eval to prevent bundler from trying to resolve it at bundle time +const AsyncLocalStorage = eval('require("node:async_hooks").AsyncLocalStorage'); + +// Create the AsyncLocalStorage instance for propagating response options +const responseOptionsALS = new AsyncLocalStorage(); + +// Store reference to the AsyncLocalStorage in the VM +// This will be accessed from Zig code +const setDevServerAsyncLocalStorage = $newZigFunction("bun.js/VirtualMachine.zig", "setDevServerAsyncLocalStorage", 2); +const getDevServerAsyncLocalStorage = $newZigFunction("bun.js/VirtualMachine.zig", "getDevServerAsyncLocalStorage", 1); + +// Set the AsyncLocalStorage instance in the VM +setDevServerAsyncLocalStorage(responseOptionsALS); + interface Exports { handleRequest: ( req: Request, @@ -48,13 +63,18 @@ server_exports = { } const [pageModule, ...layouts] = await Promise.all(routeModules.map(loadExports)); - const response = await serverRenderer(req, { - styles: styles, - modules: [clientEntryUrl], - layouts, - pageModule, - modulepreload: [], - params, + + // Run the renderer inside the AsyncLocalStorage context + // This allows Response constructors to access the stored options + const response = await responseOptionsALS.run({}, async () => { + return await serverRenderer(req, { + styles: styles, + modules: [clientEntryUrl], + layouts, + pageModule, + modulepreload: [], + params, + }, responseOptionsALS); }); if (!(response instanceof Response)) { diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index d9a1e3acfc9d..e9cf282bf431 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -196,14 +196,56 @@ has_mutated_built_in_extensions: u32 = 0, initial_script_execution_context_identifier: i32, -allow_jsx_in_response_constructor: bool = false, +dev_server_async_local_storage: jsc.Strong.Optional = .empty, pub fn setAllowJSXInResponseConstructor(this: *VirtualMachine, value: bool) void { - this.allow_jsx_in_response_constructor = value; + // When enabled, we create an AsyncLocalStorage instance + // When disabled, we clear it + if (value) { + // We'll set this from JavaScript when we create the AsyncLocalStorage instance + // For now, just keep track of the flag internally + } else { + this.dev_server_async_local_storage.deinit(); + } } pub fn allowJSXInResponseConstructor(this: *VirtualMachine) bool { - return this.allow_jsx_in_response_constructor; + // Check if the AsyncLocalStorage instance exists + return this.dev_server_async_local_storage.has(); +} + +pub fn getDevServerAsyncLocalStorage(this: *VirtualMachine) ?jsc.JSValue { + return this.dev_server_async_local_storage.get(); +} + +pub fn setDevServerAsyncLocalStorage(this: *VirtualMachine, global: *jsc.JSGlobalObject, value: jsc.JSValue) void { + if (value == .zero) { + this.dev_server_async_local_storage.deinit(); + } else if (this.dev_server_async_local_storage.has()) { + this.dev_server_async_local_storage.set(global, value); + } else { + this.dev_server_async_local_storage = jsc.Strong.Optional.create(value, global); + } +} + +// JavaScript binding to set the AsyncLocalStorage instance +pub export fn VirtualMachine__setDevServerAsyncLocalStorage(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) jsc.JSValue { + const arguments = callframe.arguments_old(1).slice(); + const vm = global.bunVM(); + + if (arguments.len < 1) { + _ = global.throwInvalidArguments("setDevServerAsyncLocalStorage expects 1 argument", .{}) catch {}; + return .zero; + } + + vm.setDevServerAsyncLocalStorage(global, arguments[0]); + return .js_undefined; +} + +// JavaScript binding to get the AsyncLocalStorage instance +pub export fn VirtualMachine__getDevServerAsyncLocalStorage(global: *jsc.JSGlobalObject, _: *jsc.CallFrame) jsc.JSValue { + const vm = global.bunVM(); + return vm.getDevServerAsyncLocalStorage() orelse .js_undefined; } pub const ProcessAutoKiller = @import("./ProcessAutoKiller.zig"); diff --git a/src/bun.js/bindings/webcore/ResponseHelpers.cpp b/src/bun.js/bindings/webcore/ResponseHelpers.cpp new file mode 100644 index 000000000000..d8718f7be4ea --- /dev/null +++ b/src/bun.js/bindings/webcore/ResponseHelpers.cpp @@ -0,0 +1,90 @@ +#include "config.h" +#include "JavaScriptCore/JSObject.h" +#include "JavaScriptCore/ObjectConstructor.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include "ZigGlobalObject.h" + +namespace Bun { + +using namespace JSC; + +// Helper function to merge AsyncLocalStorage context into Response init options +// This modifies initOptions in place by adding properties from alsStore that don't exist in initOptions +extern "C" void Response__mergeAsyncLocalStorageOptions( + JSC::JSGlobalObject* globalObject, + JSC::EncodedJSValue alsStoreValue, + JSC::EncodedJSValue initOptionsValue) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue alsStore = JSValue::decode(alsStoreValue); + JSValue initOptions = JSValue::decode(initOptionsValue); + + // Both must be objects + if (!alsStore.isObject() || !initOptions.isObject()) { + return; + } + + JSObject* alsStoreObject = asObject(alsStore); + JSObject* initOptionsObject = asObject(initOptions); + + // Get properties from alsStore + PropertyNameArray properties(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); + alsStoreObject->getOwnPropertyNames(alsStoreObject, globalObject, properties, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(scope, ); + + // Copy properties from alsStore to initOptions (only if they don't already exist) + for (auto& propertyName : properties) { + // Check if initOptions already has this property + PropertySlot checkSlot(initOptionsObject, PropertySlot::InternalMethodType::Get); + if (!initOptionsObject->getOwnPropertySlot(initOptionsObject, globalObject, propertyName, checkSlot)) { + // Property doesn't exist in initOptions, copy it from alsStore + PropertySlot slot(alsStoreObject, PropertySlot::InternalMethodType::Get); + if (alsStoreObject->getOwnPropertySlot(alsStoreObject, globalObject, propertyName, slot)) { + JSValue value = slot.getValue(globalObject, propertyName); + RETURN_IF_EXCEPTION(scope, ); + initOptionsObject->putDirect(vm, propertyName, value); + } + } + } +} + +// Helper function to get the store from AsyncLocalStorage instance +extern "C" JSC::EncodedJSValue Response__getAsyncLocalStorageStore( + JSC::JSGlobalObject* globalObject, + JSC::EncodedJSValue alsValue) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue als = JSValue::decode(alsValue); + if (!als.isObject()) { + return JSValue::encode(jsUndefined()); + } + + JSObject* alsObject = asObject(als); + + // Call the getStore() method + Identifier getStoreId = Identifier::fromString(vm, "getStore"_s); + PropertySlot slot(alsObject, PropertySlot::InternalMethodType::Get); + if (!alsObject->getPropertySlot(globalObject, getStoreId, slot)) { + return JSValue::encode(jsUndefined()); + } + + JSValue getStoreFunction = slot.getValue(globalObject, getStoreId); + RETURN_IF_EXCEPTION(scope, {}); + + if (!getStoreFunction.isCallable()) { + return JSValue::encode(jsUndefined()); + } + + CallData callData = getCallData(getStoreFunction); + MarkedArgumentBuffer args; + JSValue result = call(globalObject, getStoreFunction, callData, alsObject, args); + RETURN_IF_EXCEPTION(scope, {}); + + return JSValue::encode(result); +} + +} // namespace Bun diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index d9a7daef7f22..d20a431ab8cb 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -1,5 +1,9 @@ const Response = @This(); +// C++ helper functions for AsyncLocalStorage integration +extern fn Response__getAsyncLocalStorageStore(global: *JSGlobalObject, als: JSValue) JSValue; +extern fn Response__mergeAsyncLocalStorageOptions(global: *JSGlobalObject, alsStore: JSValue, initOptions: JSValue) void; + const ResponseMixin = BodyMixin(@This()); pub const js = jsc.Codegen.JSResponse; // NOTE: toJS is overridden @@ -525,7 +529,7 @@ pub fn constructError( } pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, this_value: jsc.JSValue) bun.JSError!*Response { - const arguments = callframe.argumentsAsArray(2); + var arguments = callframe.argumentsAsArray(2); if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) { if (arguments[0].as(Blob)) |blob| { @@ -569,6 +573,20 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, t js.gc.jsxElement.set(this_value, globalThis, arg); JSValue.transformToReactElement(this_value, arg, globalThis); } + + // Get the AsyncLocalStorage instance from the VM + const vm = globalThis.bunVM(); + if (arguments[1].isObject()) { + if (vm.getDevServerAsyncLocalStorage()) |als| { + // Get the store from the AsyncLocalStorage instance + const store = try bun.jsc.fromJSHostCall(globalThis, @src(), Response__getAsyncLocalStorageStore, .{ globalThis, als }); + if (store != .zero and store.isObject()) { + // Merge properties from alsStore into initOptions (initOptions takes precedence) + // This modifies arguments[1] in place + try bun.jsc.fromJSHostCallGeneric(globalThis, @src(), Response__mergeAsyncLocalStorageOptions, .{ globalThis, store, arguments[1] }); + } + } + } } } var init: Init = (brk: { From 5b6344cf3c1ea4ddcb46ffb4116e5218fb7b8470 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 19 Aug 2025 19:29:25 -0700 Subject: [PATCH 028/130] make it work --- cmake/sources/JavaScriptSources.txt | 1 + src/bake/DevServer.zig | 35 +++++++++- src/bake/bun-framework-react/server.tsx | 11 +++- src/bake/hmr-runtime-server.ts | 49 ++++++++------ src/bun.js/bindings/JSValue.zig | 5 ++ src/bun.js/bindings/bindings.cpp | 87 +++++++++++++++++++++---- src/bun.js/webcore/Response.zig | 59 +++++++++++------ src/bun.js/webcore/response.classes.ts | 2 - src/codegen/bake-codegen.ts | 1 + src/js/builtins/BakeSSRResponse.ts | 12 ++++ 10 files changed, 203 insertions(+), 59 deletions(-) create mode 100644 src/js/builtins/BakeSSRResponse.ts diff --git a/cmake/sources/JavaScriptSources.txt b/cmake/sources/JavaScriptSources.txt index f6a44973e028..7d9b1dfefffb 100644 --- a/cmake/sources/JavaScriptSources.txt +++ b/cmake/sources/JavaScriptSources.txt @@ -1,5 +1,6 @@ src/js/builtins.d.ts src/js/builtins/Bake.ts +src/js/builtins/BakeSSRResponse.ts src/js/builtins/BundlerPlugin.ts src/js/builtins/ByteLengthQueuingStrategy.ts src/js/builtins/CommonJS.ts diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 07ec7b442c56..5b8b02801021 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -1241,11 +1241,40 @@ fn onFrameworkRequestWithBundle( const router_type = dev.router.typePtr(dev.router.routePtr(framework_bundle.route_index).type); + // Wrapper functions for AsyncLocalStorage that match JSHostFnZig signature + const SetAsyncLocalStorageWrapper = struct { + pub fn call(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { + return VirtualMachine.VirtualMachine__setDevServerAsyncLocalStorage(global, callframe); + } + }; + + const GetAsyncLocalStorageWrapper = struct { + pub fn call(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { + return VirtualMachine.VirtualMachine__getDevServerAsyncLocalStorage(global, callframe); + } + }; + + // Create the setter and getter functions for AsyncLocalStorage + const setAsyncLocalStorage = jsc.JSFunction.create( + dev.vm.global, + "setDevServerAsyncLocalStorage", + SetAsyncLocalStorageWrapper.call, + 1, + .{} + ); + const getAsyncLocalStorage = jsc.JSFunction.create( + dev.vm.global, + "getDevServerAsyncLocalStorage", + GetAsyncLocalStorageWrapper.call, + 0, + .{} + ); + dev.server.?.onSavedRequest( req, resp, server_request_callback, - 5, + 7, .{ // routerTypeMain router_type.server_file_string.get() orelse str: { @@ -1312,6 +1341,10 @@ fn onFrameworkRequestWithBundle( }, // params params_js_value, + // setDevServerAsyncLocalStorage function + setAsyncLocalStorage, + // getDevServerAsyncLocalStorage function + getAsyncLocalStorage, }, ); } diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index caf3a8cd7a7f..b7adac66e0fb 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -55,7 +55,11 @@ function component(mod: any, params: Record | null) { // `server.tsx` exports a function to be used for handling user routes. It takes // in the Request object, the route's module, extra route metadata, and the AsyncLocalStorage instance. -export async function render(request: Request, meta: Bake.RouteMetadata, als?: AsyncLocalStorage): Promise { +export async function render( + request: Request, + meta: Bake.RouteMetadata, + als?: AsyncLocalStorage, +): Promise { // The framework generally has two rendering modes. // - Standard browser navigation // - Client-side navigation @@ -146,11 +150,14 @@ export async function render(request: Request, meta: Bake.RouteMetadata, als?: A offset += chunk.length; } + const { headers, ...response_options } = als?.getStore() ?? { headers: {} }; + return new Response(result, { headers: { "Content-Type": "text/html; charset=utf8", + ...response_options.headers, }, - ...(als?.getStore() || {}), + ...response_options, }); } } diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 1ccf68d28249..d4e4502d4d5d 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -3,25 +3,19 @@ import type { Bake } from "bun"; import "./debug"; import { loadExports, replaceModules, serverManifest, ssrManifest } from "./hmr-module"; +// import { AsyncLocalStorage } from "node:async_hooks"; +const { AsyncLocalStorage } = require("node:async_hooks"); if (typeof IS_BUN_DEVELOPMENT !== "boolean") { throw new Error("DCE is configured incorrectly"); } -// Dynamic import of AsyncLocalStorage to work with bundling -// The require is wrapped in eval to prevent bundler from trying to resolve it at bundle time -const AsyncLocalStorage = eval('require("node:async_hooks").AsyncLocalStorage'); - // Create the AsyncLocalStorage instance for propagating response options const responseOptionsALS = new AsyncLocalStorage(); -// Store reference to the AsyncLocalStorage in the VM -// This will be accessed from Zig code -const setDevServerAsyncLocalStorage = $newZigFunction("bun.js/VirtualMachine.zig", "setDevServerAsyncLocalStorage", 2); -const getDevServerAsyncLocalStorage = $newZigFunction("bun.js/VirtualMachine.zig", "getDevServerAsyncLocalStorage", 1); - -// Set the AsyncLocalStorage instance in the VM -setDevServerAsyncLocalStorage(responseOptionsALS); +// These will be set by the handleRequest function +let setDevServerAsyncLocalStorage: Function | null = null; +let getDevServerAsyncLocalStorage: Function | null = null; interface Exports { handleRequest: ( @@ -31,6 +25,8 @@ interface Exports { clientEntryUrl: string, styles: string[], params: Record | null, + setAsyncLocalStorage: Function, + getAsyncLocalStorage: Function, ) => any; registerUpdate: ( modules: any, @@ -41,7 +37,14 @@ interface Exports { declare let server_exports: Exports; server_exports = { - async handleRequest(req, routerTypeMain, routeModules, clientEntryUrl, styles, params) { + async handleRequest(req, routerTypeMain, routeModules, clientEntryUrl, styles, params, setAsyncLocalStorage, getAsyncLocalStorage) { + // Store the functions for later use + setDevServerAsyncLocalStorage = setAsyncLocalStorage; + getDevServerAsyncLocalStorage = getAsyncLocalStorage; + + // Set the AsyncLocalStorage instance in the VM + setAsyncLocalStorage(responseOptionsALS); + if (IS_BUN_DEVELOPMENT && process.env.BUN_DEBUG_BAKE_JS) { console.log("handleRequest", { routeModules, @@ -63,18 +66,22 @@ server_exports = { } const [pageModule, ...layouts] = await Promise.all(routeModules.map(loadExports)); - + // Run the renderer inside the AsyncLocalStorage context // This allows Response constructors to access the stored options const response = await responseOptionsALS.run({}, async () => { - return await serverRenderer(req, { - styles: styles, - modules: [clientEntryUrl], - layouts, - pageModule, - modulepreload: [], - params, - }, responseOptionsALS); + return await serverRenderer( + req, + { + styles: styles, + modules: [clientEntryUrl], + layouts, + pageModule, + modulepreload: [], + params, + }, + responseOptionsALS, + ); }); if (!(response instanceof Response)) { diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 92a00c9916a7..79edbe5aa693 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -59,6 +59,11 @@ pub const JSValue = enum(i64) { pub fn transformToReactElement(responseValue: JSValue, componentValue: JSValue, globalThis: *jsc.JSGlobalObject) void { JSC__JSValue__transformToReactElement(responseValue, componentValue, globalThis); } + + extern fn JSC__JSValue__transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalObject: *JSGlobalObject) void; + pub fn transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalThis: *jsc.JSGlobalObject) void { + JSC__JSValue__transformToReactElementWithOptions(responseValue, componentValue, responseOptions, globalThis); + } extern fn JSC__JSValue__getDirectIndex(JSValue, *JSGlobalObject, u32) JSValue; pub fn getDirectIndex(this: JSValue, globalThis: *JSGlobalObject, i: u32) JSValue { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 3b6c914eb273..b467e24d5eab 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -20,6 +20,7 @@ #include "BunClientData.h" #include "GCDefferalContext.h" +#include "WebCoreJSBuiltins.h" #include "JavaScriptCore/AggregateError.h" #include "JavaScriptCore/BytecodeIndex.h" @@ -5921,13 +5922,23 @@ extern "C" bool JSC__JSValue__isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JS return false; } +// Forward declaration +extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSValue responseValue, JSC::EncodedJSValue componentValue, JSC::EncodedJSValue responseOptionsValue, JSC::JSGlobalObject* globalObject); + extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue responseValue, JSC::EncodedJSValue componentValue, JSC::JSGlobalObject* globalObject) +{ + // Call the version with options, passing undefined for options + JSC__JSValue__transformToReactElementWithOptions(responseValue, componentValue, JSC::JSValue::encode(JSC::jsUndefined()), globalObject); +} + +extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSValue responseValue, JSC::EncodedJSValue componentValue, JSC::EncodedJSValue responseOptionsValue, JSC::JSGlobalObject* globalObject) { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_CATCH_SCOPE(vm); JSC::JSValue response = JSC::JSValue::decode(responseValue); JSC::JSValue component = JSC::JSValue::decode(componentValue); + JSC::JSValue responseOptions = JSC::JSValue::decode(responseOptionsValue); if (!response.isObject()) { return; @@ -5940,6 +5951,16 @@ extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue respon // TODO: check for legacy symbol as fallback auto react_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.transitional.element"_s)); + // If we have response options, we need to wrap the component + // For now, we'll store the response options directly on the response object + // The actual wrapping with AsyncLocalStorage update will happen when rendered + if (!responseOptions.isUndefinedOrNull() && responseOptions.isObject()) { + // Store the response options on the response object for later use + // This will be accessed when the component is rendered + auto responseOptionsIdentifier = JSC::Identifier::fromString(vm, "__responseOptions"_s); + responseObject->putDirect(vm, responseOptionsIdentifier, responseOptions, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } + // Transform the Response object itself into a React element // The component parameter is what will be stored in the 'type' field @@ -5961,21 +5982,61 @@ extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue respon if (!scope.exception() && typeofValue.isSymbol()) { // It's a JSX element - wrap it in a function that returns it - // This creates: () => component - // We need to protect the component value from GC - JSC::Strong strongComponent(vm, component); + // If we have response options, use the BakeSSRResponse builtin to wrap the component + // so it can update AsyncLocalStorage when rendered - auto* wrapperFunction = JSC::JSNativeStdFunction::create( - vm, - globalObject, - 0, // arity - String(), // name - [strongComponent](JSC::JSGlobalObject*, JSC::CallFrame*) -> JSC::EncodedJSValue { - return JSC::JSValue::encode(strongComponent.get()); + if (!responseOptions.isUndefinedOrNull() && responseOptions.isObject()) { + // Use the BakeSSRResponse builtin's wrapComponent function + // This will create a wrapper that updates AsyncLocalStorage before returning the component + + // Create the wrapComponent function from the BakeSSRResponse builtin + // The pattern is: CodeGenerator + // So for BakeSSRResponse.ts with export function wrapComponent: + JSC::JSFunction* wrapComponentFn = JSC::JSFunction::create(vm, globalObject, bakeSSRResponseWrapComponentCodeGenerator(vm), globalObject); + + // Call wrapComponent(component, responseOptions) + JSC::MarkedArgumentBuffer args; + args.append(component); + args.append(responseOptions); + + // Call the wrapComponent function + auto callData = JSC::getCallData(wrapComponentFn); + JSC::JSValue wrappedComponent = JSC::call(globalObject, wrapComponentFn, callData, JSC::jsUndefined(), args); + + if (!scope.exception() && !wrappedComponent.isUndefinedOrNull()) { + responseObject->putDirect(vm, typeIdentifier, wrappedComponent, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } else { + // Fall back to simple wrapper if there was an exception or undefined result + scope.clearException(); + JSC::Strong strongComponent(vm, component); + auto* wrapperFunction = JSC::JSNativeStdFunction::create( + vm, + globalObject, + 0, // arity + String(), // name + [strongComponent](JSC::JSGlobalObject* execGlobalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue { + return JSC::JSValue::encode(strongComponent.get()); + } + ); + + responseObject->putDirect(vm, typeIdentifier, wrapperFunction, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); } - ); - - responseObject->putDirect(vm, typeIdentifier, wrapperFunction, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } else { + // No response options - create a simple wrapper + JSC::Strong strongComponent(vm, component); + + auto* wrapperFunction = JSC::JSNativeStdFunction::create( + vm, + globalObject, + 0, // arity + String(), // name + [strongComponent](JSC::JSGlobalObject* execGlobalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue { + return JSC::JSValue::encode(strongComponent.get()); + } + ); + + responseObject->putDirect(vm, typeIdentifier, wrapperFunction, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } } else { // It's not a JSX element, use it directly responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index d20a431ab8cb..1476a3a90ba4 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -4,6 +4,41 @@ const Response = @This(); extern fn Response__getAsyncLocalStorageStore(global: *JSGlobalObject, als: JSValue) JSValue; extern fn Response__mergeAsyncLocalStorageOptions(global: *JSGlobalObject, alsStore: JSValue, initOptions: JSValue) void; +// Zig function to update AsyncLocalStorage with response options +pub fn bunUpdateAsyncLocalStorage(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { + const arguments = callframe.arguments_old(1).slice(); + const vm = global.bunVM(); + + if (arguments.len < 1) { + return .js_undefined; + } + + const responseOptions = arguments[0]; + if (!responseOptions.isObject()) { + return .js_undefined; + } + + // Get the AsyncLocalStorage instance from the VM + if (vm.getDevServerAsyncLocalStorage()) |als| { + // Get the current store + const store = bun.jsc.fromJSHostCall(global, @src(), Response__getAsyncLocalStorageStore, .{ global, als }) catch .zero; + if (store != .zero and store.isObject()) { + // Merge the response options into the store + bun.jsc.fromJSHostCallGeneric(global, @src(), Response__mergeAsyncLocalStorageOptions, .{ global, responseOptions, store }) catch {}; + } else { + // If no store exists, we need to call enterWith on the AsyncLocalStorage + // to set the new store + if (try als.get(global, "enterWith")) |enter_with_value| { + if (enter_with_value.isCallable()) { + _ = enter_with_value.call(global, als, &.{responseOptions}) catch .zero; + } + } + } + } + + return .js_undefined; +} + const ResponseMixin = BodyMixin(@This()); pub const js = jsc.Codegen.JSResponse; // NOTE: toJS is overridden @@ -203,12 +238,8 @@ pub fn getURL( pub fn getResponseType( this: *Response, - this_value: jsc.JSValue, globalThis: *jsc.JSGlobalObject, ) jsc.JSValue { - if (js.gc.jsxElement.get(this_value)) |jsx_element| { - return jsx_element; - } if (this.init.status_code < 200) { return bun.String.static("error").toJS(globalThis); } @@ -570,22 +601,10 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, t const arg = arguments[0]; // Check if it's a JSX element (object with $$typeof) if (try arg.isJSXElement(globalThis)) { - js.gc.jsxElement.set(this_value, globalThis, arg); - JSValue.transformToReactElement(this_value, arg, globalThis); - } - - // Get the AsyncLocalStorage instance from the VM - const vm = globalThis.bunVM(); - if (arguments[1].isObject()) { - if (vm.getDevServerAsyncLocalStorage()) |als| { - // Get the store from the AsyncLocalStorage instance - const store = try bun.jsc.fromJSHostCall(globalThis, @src(), Response__getAsyncLocalStorageStore, .{ globalThis, als }); - if (store != .zero and store.isObject()) { - // Merge properties from alsStore into initOptions (initOptions takes precedence) - // This modifies arguments[1] in place - try bun.jsc.fromJSHostCallGeneric(globalThis, @src(), Response__mergeAsyncLocalStorageOptions, .{ globalThis, store, arguments[1] }); - } - } + // Pass the response options (arguments[1]) to transformToReactElement + // so it can store them for later use when the component is rendered + const responseOptions = if (arguments[1].isObject()) arguments[1] else .js_undefined; + JSValue.transformToReactElementWithOptions(this_value, arg, responseOptions, globalThis); } } } diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index f12f7fb8461e..d7dd697247d5 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -102,7 +102,6 @@ export default [ type: { getter: "getResponseType", - this: true, }, headers: { getter: "getHeaders", @@ -125,7 +124,6 @@ export default [ getter: "getBodyUsed", }, }, - values: ["jsxElement"], constructNeedsThis: true, }), define({ diff --git a/src/codegen/bake-codegen.ts b/src/codegen/bake-codegen.ts index f9ddef9a06f0..5ad525b27547 100644 --- a/src/codegen/bake-codegen.ts +++ b/src/codegen/bake-codegen.ts @@ -93,6 +93,7 @@ async function run() { entrypoints: [generated_entrypoint], minify: !debug, drop: debug ? [] : ["DEBUG"], + target: side === "server" ? "bun" : "browser", }); if (!result.success) throw new AggregateError(result.logs); assert(result.outputs.length === 1, "must bundle to a single file"); diff --git a/src/js/builtins/BakeSSRResponse.ts b/src/js/builtins/BakeSSRResponse.ts new file mode 100644 index 000000000000..89c12fe757cf --- /dev/null +++ b/src/js/builtins/BakeSSRResponse.ts @@ -0,0 +1,12 @@ +// Used to make a Response fake being a component +// When this is called, it will render the component and then update async local +// storage with the options of the Response +export function wrapComponent(strongComponent, responseOptions) { + const bunUpdateAsyncLocalStorage = $newZigFunction("bun.js/webcore/Response.zig", "bunUpdateAsyncLocalStorage", 2); + + return () => { + // Update the AsyncLocalStorage with the response options + bunUpdateAsyncLocalStorage(responseOptions); + return strongComponent; + }; +} From bfe30411797989322e4061ee113e7c22607c100a Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 19 Aug 2025 19:50:10 -0700 Subject: [PATCH 029/130] cookie + request --- src/bake/bun-framework-react/server.tsx | 13 +++++++++--- src/bake/hmr-runtime-server.ts | 27 +++++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index b7adac66e0fb..4039ef62d220 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -13,8 +13,8 @@ function assertReactComponent(Component: any) { } // This function converts the route information into a React component tree. -function getPage(meta: Bake.RouteMetadata, styles: readonly string[]) { - let route = component(meta.pageModule, meta.params); +function getPage(meta: Bake.RouteMetadata & { request?: Request }, styles: readonly string[]) { + let route = component(meta.pageModule, meta.params, meta.request); for (const layout of meta.layouts) { const Layout = layout.default; if (import.meta.env.DEV) assertReactComponent(Layout); @@ -36,7 +36,7 @@ function getPage(meta: Bake.RouteMetadata, styles: readonly string[]) { ); } -function component(mod: any, params: Record | null) { +function component(mod: any, params: Record | null, request?: Request) { const Page = mod.default; let props = {}; if (import.meta.env.DEV) assertReactComponent(Page); @@ -50,6 +50,12 @@ function component(mod: any, params: Record | null) { props = method(); } + console.log("REQUEST", mod.mode, request); + // Pass request prop if mode is 'ssr' + if (mod.mode === "ssr" && request) { + props.request = request; + } + return ; } @@ -155,6 +161,7 @@ export async function render( return new Response(result, { headers: { "Content-Type": "text/html; charset=utf8", + "Set-Cookie": request.cookies.toSetCookieHeaders(), ...response_options.headers, }, ...response_options, diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index d4e4502d4d5d..9e78058ccfc0 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -37,14 +37,23 @@ interface Exports { declare let server_exports: Exports; server_exports = { - async handleRequest(req, routerTypeMain, routeModules, clientEntryUrl, styles, params, setAsyncLocalStorage, getAsyncLocalStorage) { + async handleRequest( + req, + routerTypeMain, + routeModules, + clientEntryUrl, + styles, + params, + setAsyncLocalStorage, + getAsyncLocalStorage, + ) { // Store the functions for later use setDevServerAsyncLocalStorage = setAsyncLocalStorage; getDevServerAsyncLocalStorage = getAsyncLocalStorage; - + // Set the AsyncLocalStorage instance in the VM setAsyncLocalStorage(responseOptionsALS); - + if (IS_BUN_DEVELOPMENT && process.env.BUN_DEBUG_BAKE_JS) { console.log("handleRequest", { routeModules, @@ -67,11 +76,19 @@ server_exports = { const [pageModule, ...layouts] = await Promise.all(routeModules.map(loadExports)); + // Add cookies to request when mode is 'ssr' + let requestWithCookies = req; + if (pageModule.mode === "ssr") { + requestWithCookies.cookies = req.cookies || new Bun.CookieMap(req.headers.get("Cookie") || ""); + } + + console.log("PAGE MODULE MODE", pageModule.mode); + // Run the renderer inside the AsyncLocalStorage context // This allows Response constructors to access the stored options const response = await responseOptionsALS.run({}, async () => { return await serverRenderer( - req, + requestWithCookies, { styles: styles, modules: [clientEntryUrl], @@ -79,6 +96,8 @@ server_exports = { pageModule, modulepreload: [], params, + // Pass request in metadata when mode is 'ssr' + request: pageModule.mode === "ssr" ? requestWithCookies : undefined, }, responseOptionsALS, ); From c1f0ce277df966e2ee70abbd7629f18426f54c41 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 19 Aug 2025 19:54:31 -0700 Subject: [PATCH 030/130] comment that cursed shit --- src/bun.js/bindings/bindings.cpp | 93 +++++++++++++++++++------------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index b467e24d5eab..7105818a5fb6 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5902,23 +5902,21 @@ extern "C" bool JSC__JSValue__isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JS auto react_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.transitional.element"_s)); JSC::JSValue value = JSC::JSValue::decode(JSValue0); - + // TODO: primitive values (strings, numbers, booleans, null, undefined) are also valid if (value.isObject()) { auto scope = DECLARE_THROW_SCOPE(vm); - + JSC::JSObject* object = value.getObject(); auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s); JSC::JSValue typeofValue = object->get(globalObject, typeofProperty); RETURN_IF_EXCEPTION(scope, false); - printf("IS SYMBOL: %s\n", typeofValue.isSymbol() ? "true" : "false"); - if (typeofValue.isSymbol() && (typeofValue == react_legacy_element_symbol || typeofValue == react_element_symbol)) { return true; } } - + return false; } @@ -5931,26 +5929,50 @@ extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue respon JSC__JSValue__transformToReactElementWithOptions(responseValue, componentValue, JSC::JSValue::encode(JSC::jsUndefined()), globalObject); } +/// What this function does is make a Response object pretend to be a React +/// component. To do this we have to put a couple properties on it: +/// +/// ```ts +/// response["$$typeof"] = REACT_ELEMENT_TYPE; +/// response.type = Component; +/// response.key = null; +/// response.props = {}; + +/// // Add the _store object that React expects in dev mode +/// response._store = {}; +/// Object.defineProperty(response._store, 'validated', { +/// configurable: false, +/// enumerable: false, +/// writable: true, +/// value: 0 // or 1 to mark it as already validated +/// }); + +/// // Add debug properties expected in dev mode +/// response._owner = null; +/// response._debugInfo = null; +/// response._debugStack = null; // or new Error() if you want a stack trace +/// response._debugTask = null; +/// ``` extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSValue responseValue, JSC::EncodedJSValue componentValue, JSC::EncodedJSValue responseOptionsValue, JSC::JSGlobalObject* globalObject) { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_CATCH_SCOPE(vm); - + JSC::JSValue response = JSC::JSValue::decode(responseValue); JSC::JSValue component = JSC::JSValue::decode(componentValue); JSC::JSValue responseOptions = JSC::JSValue::decode(responseOptionsValue); - + if (!response.isObject()) { return; } - + JSC::JSObject* responseObject = response.getObject(); - + // Get the React element symbol - same as in isJSXElement // For now, use the transitional element symbol (React 19+) // TODO: check for legacy symbol as fallback auto react_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.transitional.element"_s)); - + // If we have response options, we need to wrap the component // For now, we'll store the response options directly on the response object // The actual wrapping with AsyncLocalStorage update will happen when rendered @@ -5960,17 +5982,17 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV auto responseOptionsIdentifier = JSC::Identifier::fromString(vm, "__responseOptions"_s); responseObject->putDirect(vm, responseOptionsIdentifier, responseOptions, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); } - + // Transform the Response object itself into a React element // The component parameter is what will be stored in the 'type' field - + // Set $$typeof property to mark this as a React element auto typeofIdentifier = JSC::Identifier::fromString(vm, "$$typeof"_s); responseObject->putDirect(vm, typeofIdentifier, react_element_symbol, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - + // Set type property auto typeIdentifier = JSC::Identifier::fromString(vm, "type"_s); - + // TODO: this is stupid // Check if the component is a JSX element (has $$typeof property) // If it is, we need to wrap it in a function for React to work properly @@ -5979,30 +6001,30 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV JSC::JSObject* componentObject = component.getObject(); auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s); JSC::JSValue typeofValue = componentObject->get(globalObject, typeofProperty); - + if (!scope.exception() && typeofValue.isSymbol()) { // It's a JSX element - wrap it in a function that returns it // If we have response options, use the BakeSSRResponse builtin to wrap the component // so it can update AsyncLocalStorage when rendered - + if (!responseOptions.isUndefinedOrNull() && responseOptions.isObject()) { // Use the BakeSSRResponse builtin's wrapComponent function // This will create a wrapper that updates AsyncLocalStorage before returning the component - + // Create the wrapComponent function from the BakeSSRResponse builtin // The pattern is: CodeGenerator // So for BakeSSRResponse.ts with export function wrapComponent: JSC::JSFunction* wrapComponentFn = JSC::JSFunction::create(vm, globalObject, bakeSSRResponseWrapComponentCodeGenerator(vm), globalObject); - + // Call wrapComponent(component, responseOptions) JSC::MarkedArgumentBuffer args; args.append(component); args.append(responseOptions); - + // Call the wrapComponent function auto callData = JSC::getCallData(wrapComponentFn); JSC::JSValue wrappedComponent = JSC::call(globalObject, wrapComponentFn, callData, JSC::jsUndefined(), args); - + if (!scope.exception() && !wrappedComponent.isUndefinedOrNull()) { responseObject->putDirect(vm, typeIdentifier, wrappedComponent, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); } else { @@ -6016,15 +6038,14 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV String(), // name [strongComponent](JSC::JSGlobalObject* execGlobalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue { return JSC::JSValue::encode(strongComponent.get()); - } - ); - + }); + responseObject->putDirect(vm, typeIdentifier, wrapperFunction, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); } } else { // No response options - create a simple wrapper JSC::Strong strongComponent(vm, component); - + auto* wrapperFunction = JSC::JSNativeStdFunction::create( vm, globalObject, @@ -6032,9 +6053,8 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV String(), // name [strongComponent](JSC::JSGlobalObject* execGlobalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue { return JSC::JSValue::encode(strongComponent.get()); - } - ); - + }); + responseObject->putDirect(vm, typeIdentifier, wrapperFunction, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); } } else { @@ -6045,35 +6065,36 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV // It's not an object (could be a function or primitive), use it directly responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); } - + // Set key property to null auto keyIdentifier = JSC::Identifier::fromString(vm, "key"_s); responseObject->putDirect(vm, keyIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - + // Set props property to empty object auto propsIdentifier = JSC::Identifier::fromString(vm, "props"_s); responseObject->putDirect(vm, propsIdentifier, JSC::constructEmptyObject(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - + // Add _store object for dev mode auto storeIdentifier = JSC::Identifier::fromString(vm, "_store"_s); JSC::JSObject* storeObject = JSC::constructEmptyObject(globalObject); - + // Add validated property to _store auto validatedIdentifier = JSC::Identifier::fromString(vm, "validated"_s); storeObject->putDirect(vm, validatedIdentifier, JSC::jsNumber(0), 0); - + responseObject->putDirect(vm, storeIdentifier, storeObject, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - + // Add debug properties (all set to null) auto ownerIdentifier = JSC::Identifier::fromString(vm, "_owner"_s); responseObject->putDirect(vm, ownerIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - + auto debugInfoIdentifier = JSC::Identifier::fromString(vm, "_debugInfo"_s); responseObject->putDirect(vm, debugInfoIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - + auto debugStackIdentifier = JSC::Identifier::fromString(vm, "_debugStack"_s); + // TODO: we should put an error here if we want a stack trace apparently responseObject->putDirect(vm, debugStackIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - + auto debugTaskIdentifier = JSC::Identifier::fromString(vm, "_debugTask"_s); responseObject->putDirect(vm, debugTaskIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); } From 3760407908272befd534ec2014c1c96acdbe17cb Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 19 Aug 2025 20:07:10 -0700 Subject: [PATCH 031/130] stuff --- src/bake/DevServer.zig | 19 ++++--------------- src/bake/hmr-runtime-server.ts | 11 +---------- src/bun.js/bindings/bindings.cpp | 2 ++ 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 5b8b02801021..fcf110a36469 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -1241,13 +1241,14 @@ fn onFrameworkRequestWithBundle( const router_type = dev.router.typePtr(dev.router.routePtr(framework_bundle.route_index).type); + // FIXME: We should not create these on every single request // Wrapper functions for AsyncLocalStorage that match JSHostFnZig signature const SetAsyncLocalStorageWrapper = struct { pub fn call(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { return VirtualMachine.VirtualMachine__setDevServerAsyncLocalStorage(global, callframe); } }; - + const GetAsyncLocalStorageWrapper = struct { pub fn call(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { return VirtualMachine.VirtualMachine__getDevServerAsyncLocalStorage(global, callframe); @@ -1255,20 +1256,8 @@ fn onFrameworkRequestWithBundle( }; // Create the setter and getter functions for AsyncLocalStorage - const setAsyncLocalStorage = jsc.JSFunction.create( - dev.vm.global, - "setDevServerAsyncLocalStorage", - SetAsyncLocalStorageWrapper.call, - 1, - .{} - ); - const getAsyncLocalStorage = jsc.JSFunction.create( - dev.vm.global, - "getDevServerAsyncLocalStorage", - GetAsyncLocalStorageWrapper.call, - 0, - .{} - ); + const setAsyncLocalStorage = jsc.JSFunction.create(dev.vm.global, "setDevServerAsyncLocalStorage", SetAsyncLocalStorageWrapper.call, 1, .{}); + const getAsyncLocalStorage = jsc.JSFunction.create(dev.vm.global, "getDevServerAsyncLocalStorage", GetAsyncLocalStorageWrapper.call, 0, .{}); dev.server.?.onSavedRequest( req, diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 9e78058ccfc0..ead37cf69898 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -13,10 +13,6 @@ if (typeof IS_BUN_DEVELOPMENT !== "boolean") { // Create the AsyncLocalStorage instance for propagating response options const responseOptionsALS = new AsyncLocalStorage(); -// These will be set by the handleRequest function -let setDevServerAsyncLocalStorage: Function | null = null; -let getDevServerAsyncLocalStorage: Function | null = null; - interface Exports { handleRequest: ( req: Request, @@ -47,10 +43,7 @@ server_exports = { setAsyncLocalStorage, getAsyncLocalStorage, ) { - // Store the functions for later use - setDevServerAsyncLocalStorage = setAsyncLocalStorage; - getDevServerAsyncLocalStorage = getAsyncLocalStorage; - + // FIXME: We should only have to do this once // Set the AsyncLocalStorage instance in the VM setAsyncLocalStorage(responseOptionsALS); @@ -82,8 +75,6 @@ server_exports = { requestWithCookies.cookies = req.cookies || new Bun.CookieMap(req.headers.get("Cookie") || ""); } - console.log("PAGE MODULE MODE", pageModule.mode); - // Run the renderer inside the AsyncLocalStorage context // This allows Response constructors to access the stored options const response = await responseOptionsALS.run({}, async () => { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 7105818a5fb6..b8e764d1b6ae 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5929,6 +5929,8 @@ extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue respon JSC__JSValue__transformToReactElementWithOptions(responseValue, componentValue, JSC::JSValue::encode(JSC::jsUndefined()), globalObject); } +/// TODO: This could just be a builtin function and be 10x less lines of code why is it in C++ +/// /// What this function does is make a Response object pretend to be a React /// component. To do this we have to put a couple properties on it: /// From 33447ef2dbaac711c67e7b59d3b1c8628147bad3 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:00:46 -0700 Subject: [PATCH 032/130] that was way more complicated then need be --- src/bake/bun-framework-react/server.tsx | 19 ++++++--- src/bake/hmr-runtime-server.ts | 6 +++ src/bun.js/webcore/Response.zig | 56 ++++++++++++++----------- src/bun.js/webcore/response.classes.ts | 3 ++ src/js/builtins/BakeSSRResponse.ts | 10 ++++- 5 files changed, 61 insertions(+), 33 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 4039ef62d220..8d8cd882b933 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -4,6 +4,7 @@ import { serverManifest } from "bun:bake/server"; import { PassThrough } from "node:stream"; import { renderToPipeableStream } from "react-server-dom-bun/server.node.unbundled.js"; import type { AsyncLocalStorage } from "node:async_hooks"; +import type { RequestContext } from "../hmr-runtime-server"; function assertReactComponent(Component: any) { if (typeof Component !== "function") { @@ -50,7 +51,6 @@ function component(mod: any, params: Record | null, request?: Re props = method(); } - console.log("REQUEST", mod.mode, request); // Pass request prop if mode is 'ssr' if (mod.mode === "ssr" && request) { props.request = request; @@ -64,7 +64,7 @@ function component(mod: any, params: Record | null, request?: Re export async function render( request: Request, meta: Bake.RouteMetadata, - als?: AsyncLocalStorage, + als?: AsyncLocalStorage, ): Promise { // The framework generally has two rendering modes. // - Standard browser navigation @@ -114,21 +114,27 @@ export async function render( pipe(rscPayload); if (skipSSR) { + const responseOptions = als?.getStore()?.responseOptions || {}; return new Response(rscPayload as any, { status: 200, headers: { "Content-Type": "text/x-component" }, - ...(als?.getStore() || {}), + ...responseOptions, }); } // The RSC payload is rendered into HTML if (streaming) { + const responseOptions = als?.getStore()?.responseOptions || {}; + if (als) { + const state = als.getStore(); + if (state) state.streamingStarted = true; + } // Stream the response as before return new Response(renderToHtml(rscPayload, meta.modules, signal), { headers: { "Content-Type": "text/html; charset=utf8", }, - ...(als?.getStore() || {}), + ...responseOptions, }); } else { // TODO: this seems shitty @@ -156,13 +162,14 @@ export async function render( offset += chunk.length; } - const { headers, ...response_options } = als?.getStore() ?? { headers: {} }; + const opts = als?.getStore()?.responseOptions ?? { headers: {} }; + const { headers, ...response_options } = opts; return new Response(result, { headers: { "Content-Type": "text/html; charset=utf8", "Set-Cookie": request.cookies.toSetCookieHeaders(), - ...response_options.headers, + ...headers, }, ...response_options, }); diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index ead37cf69898..11ddb0c961e3 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -10,6 +10,12 @@ if (typeof IS_BUN_DEVELOPMENT !== "boolean") { throw new Error("DCE is configured incorrectly"); } +export type RequestContext = { + responseOptions: ResponseInit; + streamingStarted?: boolean; + abortNonStreaming: (path: string, params: Record | null) => {}; +}; + // Create the AsyncLocalStorage instance for propagating response options const responseOptionsALS = new AsyncLocalStorage(); diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 1476a3a90ba4..bbcbe95aa16e 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -5,35 +5,12 @@ extern fn Response__getAsyncLocalStorageStore(global: *JSGlobalObject, als: JSVa extern fn Response__mergeAsyncLocalStorageOptions(global: *JSGlobalObject, alsStore: JSValue, initOptions: JSValue) void; // Zig function to update AsyncLocalStorage with response options -pub fn bunUpdateAsyncLocalStorage(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { - const arguments = callframe.arguments_old(1).slice(); +pub fn bakeGetAsyncLocalStorage(global: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue { const vm = global.bunVM(); - if (arguments.len < 1) { - return .js_undefined; - } - - const responseOptions = arguments[0]; - if (!responseOptions.isObject()) { - return .js_undefined; - } - // Get the AsyncLocalStorage instance from the VM if (vm.getDevServerAsyncLocalStorage()) |als| { - // Get the current store - const store = bun.jsc.fromJSHostCall(global, @src(), Response__getAsyncLocalStorageStore, .{ global, als }) catch .zero; - if (store != .zero and store.isObject()) { - // Merge the response options into the store - bun.jsc.fromJSHostCallGeneric(global, @src(), Response__mergeAsyncLocalStorageOptions, .{ global, responseOptions, store }) catch {}; - } else { - // If no store exists, we need to call enterWith on the AsyncLocalStorage - // to set the new store - if (try als.get(global, "enterWith")) |enter_with_value| { - if (enter_with_value.isCallable()) { - _ = enter_with_value.call(global, als, &.{responseOptions}) catch .zero; - } - } - } + return als; } return .js_undefined; @@ -540,6 +517,35 @@ pub fn constructRedirect( return ptr.toJS(globalThis); } + +pub fn constructRender( + globalThis: *jsc.JSGlobalObject, + callframe: *jsc.CallFrame, +) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2); + const vm = globalThis.bunVM(); + + // Check if dev_server_async_local_storage is set + if (!vm.dev_server_async_local_storage.has()) { + return globalThis.throwInvalidArguments("Response.render() is only available in the Bun dev server", .{}); + } + + // For now, just stub - we'll implement the actual logic later + // We expect two arguments: path and params + if (arguments.len < 1) { + return globalThis.throwInvalidArguments("Response.render() requires at least a path argument", .{}); + } + + const path_arg = arguments.ptr[0]; + if (!path_arg.isString()) { + return globalThis.throwInvalidArguments("Response.render() path must be a string", .{}); + } + + // TODO: Implement actual render logic + // For now, return an error response indicating this is not yet implemented + return globalThis.throwTODO("Response.render() is not yet fully implemented"); +} + pub fn constructError( globalThis: *jsc.JSGlobalObject, _: *jsc.CallFrame, diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index d7dd697247d5..d2a788742c36 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -83,6 +83,9 @@ export default [ error: { fn: "constructError", }, + render: { + fn: "constructRender", + }, }, proto: { url: { diff --git a/src/js/builtins/BakeSSRResponse.ts b/src/js/builtins/BakeSSRResponse.ts index 89c12fe757cf..a39d93e8ab2d 100644 --- a/src/js/builtins/BakeSSRResponse.ts +++ b/src/js/builtins/BakeSSRResponse.ts @@ -2,11 +2,17 @@ // When this is called, it will render the component and then update async local // storage with the options of the Response export function wrapComponent(strongComponent, responseOptions) { - const bunUpdateAsyncLocalStorage = $newZigFunction("bun.js/webcore/Response.zig", "bunUpdateAsyncLocalStorage", 2); + const bakeGetAsyncLocalStorage = $newZigFunction("bun.js/webcore/Response.zig", "bakeGetAsyncLocalStorage", 0); return () => { // Update the AsyncLocalStorage with the response options - bunUpdateAsyncLocalStorage(responseOptions); + const async_local_storage = bakeGetAsyncLocalStorage(); + if (async_local_storage) { + const store = async_local_storage.getStore(); + if (store) { + store.responseOptions = responseOptions; + } + } return strongComponent; }; } From 3906407e5dc89bb7b2605af6a4b2ccd424d4fa04 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:07:39 -0700 Subject: [PATCH 033/130] stuff --- src/bake/bun-framework-react/server.tsx | 14 +++++++++----- src/bake/hmr-runtime-server.ts | 8 ++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 8d8cd882b933..009a8ae5758f 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -137,18 +137,22 @@ export async function render( ...responseOptions, }); } else { - // TODO: this seems shitty + // TODO: this is bad and could be done way better + // Buffer the entire response and return it all at once const htmlStream = renderToHtml(rscPayload, meta.modules, signal); const chunks: Uint8Array[] = []; const reader = htmlStream.getReader(); try { - while (true) { + let keepGoing = true; + do { const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - } + keepGoing = !done; + if (!done) { + chunks.push(value); + } + } while (keepGoing); } finally { reader.releaseLock(); } diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 11ddb0c961e3..1b69284a7105 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -13,7 +13,7 @@ if (typeof IS_BUN_DEVELOPMENT !== "boolean") { export type RequestContext = { responseOptions: ResponseInit; streamingStarted?: boolean; - abortNonStreaming: (path: string, params: Record | null) => {}; + abortNonStreaming?: (path: string, params: Record | null) => {}; }; // Create the AsyncLocalStorage instance for propagating response options @@ -81,9 +81,13 @@ server_exports = { requestWithCookies.cookies = req.cookies || new Bun.CookieMap(req.headers.get("Cookie") || ""); } + let storeValue: RequestContext = { + responseOptions: {}, + }; + // Run the renderer inside the AsyncLocalStorage context // This allows Response constructors to access the stored options - const response = await responseOptionsALS.run({}, async () => { + const response = await responseOptionsALS.run(storeValue, async () => { return await serverRenderer( requestWithCookies, { From ab3566627ded001257721dd438e65dcb88b56b7e Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:11:07 -0700 Subject: [PATCH 034/130] comment --- src/bake/bun-framework-react/server.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 009a8ae5758f..015a3147ab56 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -137,7 +137,8 @@ export async function render( ...responseOptions, }); } else { - // TODO: this is bad and could be done way better + // FIXME: this is bad and could be done way better + // FIXME: why are we even doing stream stuff is `streaming=false`, is there a way to do RSC without stream // Buffer the entire response and return it all at once const htmlStream = renderToHtml(rscPayload, meta.modules, signal); From 3fb0a824cb9c46f094f41c13db69f233a6bb3426 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:07:19 -0700 Subject: [PATCH 035/130] wip --- src/bake/DevServer.zig | 41 +++++++++-- src/bake/bun-framework-react/server.tsx | 33 ++++++++- src/bake/bun-framework-react/ssr.tsx | 1 - src/bake/hmr-runtime-server.ts | 81 +++++++++++++++------ src/bun.js/VirtualMachine.zig | 6 +- src/bun.js/api/server/AnyRequestContext.zig | 26 +++++++ src/bun.js/api/server/RequestContext.zig | 10 +++ src/bun.js/bindings/bindings.cpp | 1 + src/bun.js/webcore/Response.zig | 39 ++++++++-- 9 files changed, 196 insertions(+), 42 deletions(-) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index fcf110a36469..2ce05c1366c6 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -1081,20 +1081,30 @@ fn deferRequest( resp: AnyResponse, ) !void { const deferred = dev.deferred_request_pool.get(); + debug.log("DeferredRequest(0x{x}).init", .{@intFromPtr(&deferred.data)}); const method = bun.http.Method.which(req.method()) orelse .POST; deferred.data = .{ .route_bundle_index = route_bundle_index, .dev = dev, .ref_count = .init(), .handler = switch (kind) { - .bundled_html_page => .{ .bundled_html_page = .{ .response = resp, .method = method } }, - .server_handler => .{ - .server_handler = dev.server.?.prepareAndSaveJsRequestContext(req, resp, dev.vm.global, method) orelse return, + .bundled_html_page => brk: { + resp.onAborted(*DeferredRequest, DeferredRequest.onAbort, &deferred.data); + break :brk .{ .bundled_html_page = .{ .response = resp, .method = method } }; + }, + .server_handler => brk: { + const server_handler = dev.server.?.prepareAndSaveJsRequestContext(req, resp, dev.vm.global, method) orelse { + dev.deferred_request_pool.put(deferred); + return; + }; + server_handler.ctx.setAbortCallback(DeferredRequest.onAbortWrapper, &deferred.data); + break :brk .{ + .server_handler = server_handler, + }; }, }, }; deferred.data.ref(); - resp.onAborted(*DeferredRequest, DeferredRequest.onAbort, &deferred.data); requests_array.prepend(deferred); } @@ -1562,7 +1572,11 @@ pub const DeferredRequest = struct { pub const List = std.SinglyLinkedList(DeferredRequest); pub const Node = List.Node; - const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinitImpl, .{}); + const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinitImpl, .{ + .debug_name = "DeferredRequest", + }); + + const debugLog = bun.Output.Scoped("DlogeferredRequest", .hidden).log; route_bundle_index: RouteBundle.Index, handler: Handler, @@ -1595,8 +1609,18 @@ pub const DeferredRequest = struct { }; }; - fn onAbort(this: *DeferredRequest, resp: AnyResponse) void { - _ = resp; + fn onAbortWrapper(this: *anyopaque) void { + const self: *DeferredRequest = @alignCast(@ptrCast(this)); + self.onAbortImpl(); + } + + fn onAbort(this: *DeferredRequest, _: AnyResponse) void { + this.onAbortImpl(); + } + + fn onAbortImpl(this: *DeferredRequest) void { + debugLog("DeferredRequest(0x{x}) onAbort", .{@intFromPtr(this)}); + this.abort(); assert(this.handler == .aborted); } @@ -1607,6 +1631,7 @@ pub const DeferredRequest = struct { /// such as for bundling failures or aborting the server. /// Does not free the underlying `DeferredRequest.Node` fn deinitImpl(this: *DeferredRequest) void { + debugLog("DeferredRequest(0x{x}) deinitImpl", .{@intFromPtr(this)}); this.ref_count.assertNoRefs(); defer this.dev.deferred_request_pool.put(@fieldParentPtr("data", this)); @@ -1618,11 +1643,11 @@ pub const DeferredRequest = struct { /// Deinitializes state by aborting the connection. fn abort(this: *DeferredRequest) void { + debugLog("DeferredRequest(0x{x}) abort", .{@intFromPtr(this)}); var handler = this.handler; this.handler = .aborted; switch (handler) { .server_handler => |*saved| { - saved.ctx.onAbort(saved.response); saved.js_request.deinit(); }, .bundled_html_page => |r| { diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 015a3147ab56..4fbf0bb47aff 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -140,6 +140,22 @@ export async function render( // FIXME: this is bad and could be done way better // FIXME: why are we even doing stream stuff is `streaming=false`, is there a way to do RSC without stream + // Set up the render abort handler for non-streaming mode + if (als) { + const store = als.getStore(); + if (store) { + store.renderAbort = (path: string, params: Record | null) => { + // Create the abort error + const abortError = new (globalThis as any).RenderAbortError(path, params); + // Abort the current render + signal.aborted = abortError; + signal.abort(abortError); + rscPayload.destroy(abortError); + throw abortError; + }; + } + } + // Buffer the entire response and return it all at once const htmlStream = renderToHtml(rscPayload, meta.modules, signal); const chunks: Uint8Array[] = []; @@ -149,6 +165,21 @@ export async function render( let keepGoing = true; do { const { done, value } = await reader.read(); + + // Check if the render was aborted with an error + if (signal.aborted) { + // If it's a RenderAbortError, re-throw it to be handled upstream + if (signal.aborted instanceof (globalThis as any).RenderAbortError) { + throw signal.aborted; + } + // For some reason in react-server-dom the `stream.on("error")` + // handler creates a new Error??? + if (signal.aborted.message !== "Connection closed.") { + // For other errors, we can handle them here or re-throw + throw signal.aborted; + } + } + keepGoing = !done; if (!done) { chunks.push(value); @@ -200,7 +231,7 @@ export async function prerender(meta: Bake.RouteMetadata) { try { html = await renderToStaticHtml(rscPayload, meta.modules); } catch (err) { - console.error("ah fuck"); + //console.error("ah fuck"); return undefined; } const rsc = new Blob(rscChunks, { type: "text/x-component" }); diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index 6e1d98aa020f..1a40dc57bde9 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -69,7 +69,6 @@ export function renderToHtml( bootstrapModules, onError(error) { if (!signal.aborted) { - // console.error(error); // Abort the rendering and close the stream signal.aborted = error; abort(); diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 1b69284a7105..c3c0999b31f8 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -13,12 +13,26 @@ if (typeof IS_BUN_DEVELOPMENT !== "boolean") { export type RequestContext = { responseOptions: ResponseInit; streamingStarted?: boolean; - abortNonStreaming?: (path: string, params: Record | null) => {}; + renderAbort?: (path: string, params: Record | null) => never; }; // Create the AsyncLocalStorage instance for propagating response options const responseOptionsALS = new AsyncLocalStorage(); +/// Created when the user does `return Response.render(...)` +class RenderAbortError extends Error { + constructor( + public path: string, + public params: Record | null, + ) { + super("Response.render() called"); + this.name = "RenderAbortError"; + } +} + +// Make RenderAbortError globally available for other modules +(globalThis as any).RenderAbortError = RenderAbortError; + interface Exports { handleRequest: ( req: Request, @@ -85,30 +99,49 @@ server_exports = { responseOptions: {}, }; - // Run the renderer inside the AsyncLocalStorage context - // This allows Response constructors to access the stored options - const response = await responseOptionsALS.run(storeValue, async () => { - return await serverRenderer( - requestWithCookies, - { - styles: styles, - modules: [clientEntryUrl], - layouts, - pageModule, - modulepreload: [], - params, - // Pass request in metadata when mode is 'ssr' - request: pageModule.mode === "ssr" ? requestWithCookies : undefined, - }, - responseOptionsALS, - ); - }); - - if (!(response instanceof Response)) { - throw new Error(`Server-side request handler was expected to return a Response object.`); - } + try { + // Run the renderer inside the AsyncLocalStorage context + // This allows Response constructors to access the stored options + const response = await responseOptionsALS.run(storeValue, async () => { + return await serverRenderer( + requestWithCookies, + { + styles: styles, + modules: [clientEntryUrl], + layouts, + pageModule, + modulepreload: [], + params, + // Pass request in metadata when mode is 'ssr' + request: pageModule.mode === "ssr" ? requestWithCookies : undefined, + }, + responseOptionsALS, + ); + }); + + if (!(response instanceof Response)) { + throw new Error(`Server-side request handler was expected to return a Response object.`); + } - return response; + return response; + } catch (error) { + // Handle Response.render() aborts + if (error instanceof RenderAbortError) { + // TODO: Implement route resolution to get the new route modules + // For now, we'll need to get this information from the native side + // The native code will need to resolve the route and call handleRequest again + + // Store the render error info so native code can access it + (globalThis as any).__lastRenderAbort = { + path: error.path, + params: error.params, + }; + + // Re-throw to let native code handle the recursive call + throw error; + } + throw error; + } }, async registerUpdate(modules, componentManifestAdd, componentManifestDelete) { replaceModules(modules); diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index e9cf282bf431..e541ad7fbf87 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -232,17 +232,17 @@ pub fn setDevServerAsyncLocalStorage(this: *VirtualMachine, global: *jsc.JSGloba pub export fn VirtualMachine__setDevServerAsyncLocalStorage(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) jsc.JSValue { const arguments = callframe.arguments_old(1).slice(); const vm = global.bunVM(); - + if (arguments.len < 1) { _ = global.throwInvalidArguments("setDevServerAsyncLocalStorage expects 1 argument", .{}) catch {}; return .zero; } - + vm.setDevServerAsyncLocalStorage(global, arguments[0]); return .js_undefined; } -// JavaScript binding to get the AsyncLocalStorage instance +// JavaScript binding to get the AsyncLocalStorage instance pub export fn VirtualMachine__getDevServerAsyncLocalStorage(global: *jsc.JSGlobalObject, _: *jsc.CallFrame) jsc.JSValue { const vm = global.bunVM(); return vm.getDevServerAsyncLocalStorage() orelse .js_undefined; diff --git a/src/bun.js/api/server/AnyRequestContext.zig b/src/bun.js/api/server/AnyRequestContext.zig index 78c92144a274..51c3448b5ea5 100644 --- a/src/bun.js/api/server/AnyRequestContext.zig +++ b/src/bun.js/api/server/AnyRequestContext.zig @@ -18,6 +18,32 @@ pub fn init(request_ctx: anytype) AnyRequestContext { return .{ .tagged_pointer = Pointer.init(request_ctx) }; } +pub fn setAbortCallback(self: AnyRequestContext, cb: *const fn (this: *anyopaque) void, data: *anyopaque) void { + if (self.tagged_pointer.isNull()) { + return; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + self.tagged_pointer.as(HTTPServer.RequestContext).onAbortCb = cb; + self.tagged_pointer.as(HTTPServer.RequestContext).onAbortData = data; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + self.tagged_pointer.as(HTTPSServer.RequestContext).onAbortCb = cb; + self.tagged_pointer.as(HTTPSServer.RequestContext).onAbortData = data; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPServer.RequestContext).onAbortCb = cb; + self.tagged_pointer.as(DebugHTTPServer.RequestContext).onAbortData = data; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPSServer.RequestContext).onAbortCb = cb; + self.tagged_pointer.as(DebugHTTPSServer.RequestContext).onAbortData = data; + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + pub fn memoryCost(self: AnyRequestContext) usize { if (self.tagged_pointer.isNull()) { return 0; diff --git a/src/bun.js/api/server/RequestContext.zig b/src/bun.js/api/server/RequestContext.zig index a885cd45c485..2cddfe21f4bb 100644 --- a/src/bun.js/api/server/RequestContext.zig +++ b/src/bun.js/api/server/RequestContext.zig @@ -55,6 +55,9 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, /// Defer finalization until after the request handler task is completed? defer_deinit_until_callback_completes: ?*bool = null, + onAbortCb: ?*const fn (this: *anyopaque) void = null, + onAbortData: ?*anyopaque = null, + // TODO: support builtin compression const can_sendfile = !ssl_enabled and !Environment.isWindows; @@ -201,6 +204,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, /// destroy RequestContext, should be only called by deref or if defer_deinit_until_callback_completes is ref is set to true pub fn deinit(this: *RequestContext) void { + ctxLog("deinit", .{}); this.detachResponse(); this.endRequestStreamingAndDrain(); // TODO: has_marked_complete is doing something? @@ -436,6 +440,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, } pub fn end(this: *RequestContext, data: []const u8, closeConnection: bool) void { + ctxLog("end", .{}); if (this.resp) |resp| { defer this.deref(); @@ -461,6 +466,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, } pub fn endWithoutBody(this: *RequestContext, closeConnection: bool) void { + ctxLog("endWithoutBody", .{}); if (this.resp) |resp| { defer this.deref(); @@ -556,11 +562,15 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, } pub fn onAbort(this: *RequestContext, resp: *App.Response) void { + ctxLog("onAbort", .{}); assert(this.resp == resp); assert(!this.flags.aborted); assert(this.server != null); // mark request as aborted this.flags.aborted = true; + if (this.onAbortData != null and this.onAbortCb != null) { + this.onAbortCb.?(this.onAbortData.?); + } this.detachResponse(); var any_js_calls = false; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index b8e764d1b6ae..f7648e6bfa2f 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5930,6 +5930,7 @@ extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue respon } /// TODO: This could just be a builtin function and be 10x less lines of code why is it in C++ +/// TODO: this should actually just be a special Response sub-class and the transpiler rewrites the code to use this /// /// What this function does is make a Response object pretend to be a React /// component. To do this we have to put a couple properties on it: diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index bbcbe95aa16e..193641686214 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -530,8 +530,7 @@ pub fn constructRender( return globalThis.throwInvalidArguments("Response.render() is only available in the Bun dev server", .{}); } - // For now, just stub - we'll implement the actual logic later - // We expect two arguments: path and params + // Validate arguments if (arguments.len < 1) { return globalThis.throwInvalidArguments("Response.render() requires at least a path argument", .{}); } @@ -541,9 +540,39 @@ pub fn constructRender( return globalThis.throwInvalidArguments("Response.render() path must be a string", .{}); } - // TODO: Implement actual render logic - // For now, return an error response indicating this is not yet implemented - return globalThis.throwTODO("Response.render() is not yet fully implemented"); + // Get params if provided (second argument) + const params_arg = if (arguments.len >= 2) arguments.ptr[1] else JSValue.jsNull(); + + // Get the AsyncLocalStorage instance + const als = vm.dev_server_async_local_storage.get() orelse { + return globalThis.throwInvalidArguments("Response.render() AsyncLocalStorage not found", .{}); + }; + + // Call als.getStore() to get the current store + const getStore_fn = try als.get(globalThis, "getStore") orelse { + return globalThis.throwInvalidArguments("Response.render() getStore method not found", .{}); + }; + + const store = try getStore_fn.call(globalThis, als, &.{}); + + if (store.isUndefinedOrNull()) { + return globalThis.throwInvalidArguments("Response.render() no active context", .{}); + } + + // Get the renderAbort function from the store + const renderAbort = try store.get(globalThis, "renderAbort") orelse { + return globalThis.throwInvalidArguments("Response.render() is only available in non-streaming mode", .{}); + }; + + if (!renderAbort.isCallable()) { + return globalThis.throwInvalidArguments("Response.render() is only available in non-streaming mode", .{}); + } + + // Call renderAbort(path, params) - this will throw and abort the render + _ = try renderAbort.call(globalThis, store, &.{ path_arg, params_arg }); + + // This should not be reached as renderAbort should throw + return .js_undefined; } pub fn constructError( From f0d4fa8b63e96338acdda5cd863c0d9c00f7aee3 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:36:55 -0700 Subject: [PATCH 036/130] support `return Response.render(...)` --- src/bake/DevServer.zig | 59 ++++++++++++--- src/bake/hmr-runtime-server.ts | 5 +- src/bun.js/api/bun/spawn/stdio.zig | 4 ++ src/bun.js/api/server/RequestContext.zig | 92 +++++++++++++++++++++++- src/bun.js/bindings/bindings.cpp | 51 ++++++++++++- src/bun.js/webcore/Blob.zig | 2 + src/bun.js/webcore/Body.zig | 11 +++ src/bun.js/webcore/Response.zig | 68 +++++++++++------- src/bun.js/webcore/blob/write_file.zig | 1 + src/js/builtins/BakeSSRResponse.ts | 24 ++++++- 10 files changed, 273 insertions(+), 44 deletions(-) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 2ce05c1366c6..4336a32265c4 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -951,7 +951,7 @@ fn ensureRouteIsBundled( dev: *DevServer, route_bundle_index: RouteBundle.Index, kind: DeferredRequest.Handler.Kind, - req: *Request, + req: ReqOrSaved, resp: AnyResponse, ) bun.JSError!void { assert(dev.magic == .valid); @@ -1066,23 +1066,35 @@ fn ensureRouteIsBundled( ); }, .loaded => switch (kind) { - .server_handler => try dev.onFrameworkRequestWithBundle(route_bundle_index, .{ .stack = req }, resp), - .bundled_html_page => dev.onHtmlRequestWithBundle(route_bundle_index, resp, bun.http.Method.which(req.method()) orelse .POST), + .server_handler => try dev.onFrameworkRequestWithBundle(route_bundle_index, if (req == .req) .{ .stack = req.req } else .{ .saved = req.saved }, resp), + .bundled_html_page => dev.onHtmlRequestWithBundle(route_bundle_index, resp, req.method()), }, } } +const ReqOrSaved = union(enum) { + req: *Request, + saved: bun.jsc.API.SavedRequest, + + pub fn method(this: *const @This()) bun.http.Method { + return switch (this.*) { + .req => |req| bun.http.Method.which(req.method()) orelse .POST, + .saved => |saved| saved.request.method, + }; + } +}; + fn deferRequest( dev: *DevServer, requests_array: *DeferredRequest.List, route_bundle_index: RouteBundle.Index, kind: DeferredRequest.Handler.Kind, - req: *Request, + req: ReqOrSaved, resp: AnyResponse, ) !void { const deferred = dev.deferred_request_pool.get(); debug.log("DeferredRequest(0x{x}).init", .{@intFromPtr(&deferred.data)}); - const method = bun.http.Method.which(req.method()) orelse .POST; + const method = req.method(); deferred.data = .{ .route_bundle_index = route_bundle_index, .dev = dev, @@ -1093,9 +1105,12 @@ fn deferRequest( break :brk .{ .bundled_html_page = .{ .response = resp, .method = method } }; }, .server_handler => brk: { - const server_handler = dev.server.?.prepareAndSaveJsRequestContext(req, resp, dev.vm.global, method) orelse { - dev.deferred_request_pool.put(deferred); - return; + const server_handler = switch (req) { + .req => |r| dev.server.?.prepareAndSaveJsRequestContext(r, resp, dev.vm.global, method) orelse { + dev.deferred_request_pool.put(deferred); + return; + }, + .saved => |saved| saved, }; server_handler.ctx.setAbortCallback(DeferredRequest.onAbortWrapper, &deferred.data); break :brk .{ @@ -2923,7 +2938,7 @@ fn onRequest(dev: *DevServer, req: *Request, resp: anytype) void { dev.ensureRouteIsBundled( dev.getOrPutRouteBundle(.{ .framework = route_index }) catch bun.outOfMemory(), .server_handler, - req, + .{ .req = req }, AnyResponse.init(resp), ) catch bun.outOfMemory(); return; @@ -2938,7 +2953,31 @@ fn onRequest(dev: *DevServer, req: *Request, resp: anytype) void { } pub fn respondForHTMLBundle(dev: *DevServer, html: *HTMLBundle.HTMLBundleRoute, req: *uws.Request, resp: AnyResponse) !void { - try dev.ensureRouteIsBundled(try dev.getOrPutRouteBundle(.{ .html = html }), .bundled_html_page, req, resp); + try dev.ensureRouteIsBundled(try dev.getOrPutRouteBundle(.{ .html = html }), .bundled_html_page, .{ .req = req }, resp); +} + +// TODO: path params +pub fn handleRenderRedirect( + dev: *DevServer, + saved_request: bun.jsc.API.SavedRequest, + render_path: []const u8, + resp: AnyResponse, +) !void { + // Match the render path against the router + var params: FrameworkRouter.MatchedParams = undefined; + if (dev.router.matchSlow(render_path, ¶ms)) |route_index| { + // Found a matching route, bundle it and handle the request + dev.ensureRouteIsBundled( + dev.getOrPutRouteBundle(.{ .framework = route_index }) catch bun.outOfMemory(), + .server_handler, + .{ .saved = saved_request }, + resp, + ) catch bun.outOfMemory(); + return; + } + + // No matching route found - render 404 + sendBuiltInNotFound(resp); } fn getOrPutRouteBundle(dev: *DevServer, route: RouteBundle.UnresolvedIndex) !RouteBundle.Index { diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index c3c0999b31f8..d2489a459a86 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -24,6 +24,7 @@ class RenderAbortError extends Error { constructor( public path: string, public params: Record | null, + public response: Response, ) { super("Response.render() called"); this.name = "RenderAbortError"; @@ -137,8 +138,8 @@ server_exports = { params: error.params, }; - // Re-throw to let native code handle the recursive call - throw error; + // return it so the Zig code can handle it and re-render new route + return error.response; } throw error; } diff --git a/src/bun.js/api/bun/spawn/stdio.zig b/src/bun.js/api/bun/spawn/stdio.zig index e11696cd2cf2..508cb36ef37d 100644 --- a/src/bun.js/api/bun/spawn/stdio.zig +++ b/src/bun.js/api/bun/spawn/stdio.zig @@ -297,6 +297,10 @@ pub const Stdio = union(enum) { }, .Blob, .WTFStringImpl, .InternalBlob => unreachable, // handled above. + .Render => { + // Render bodies should not be used as stdio + return globalThis.throwInvalidArguments("Response.render() result cannot be used as stdio", .{}); + }, .Locked => { if (is_sync) { return globalThis.throwInvalidArguments("ReadableStream cannot be used in sync mode", .{}); diff --git a/src/bun.js/api/server/RequestContext.zig b/src/bun.js/api/server/RequestContext.zig index 2cddfe21f4bb..2e0a97f70aa7 100644 --- a/src/bun.js/api/server/RequestContext.zig +++ b/src/bun.js/api/server/RequestContext.zig @@ -186,7 +186,23 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, // 5 - is the only reference of the context // 6 - is not waiting for request body // 7 - did not call sendfile - return this.resp != null and !this.flags.aborted and !this.flags.has_marked_complete and !this.flags.has_marked_pending and this.ref_count == 1 and !this.flags.is_waiting_for_request_body and !this.flags.has_sendfile_ctx; + ctxLog("RequestContext(0x{x}).shouldRenderMissing {s} {s} {s} {s} {s} {s} {s}", .{ + @intFromPtr(this), + if (this.resp != null) "has response" else "no response", + if (this.flags.aborted) "aborted" else "not aborted", + if (this.flags.has_marked_complete) "marked complete" else "not marked complete", + if (this.flags.has_marked_pending) "marked pending" else "not marked pending", + if (this.ref_count == 1) "only reference" else "not only reference", + if (this.flags.is_waiting_for_request_body) "waiting for request body" else "not waiting for request body", + if (this.flags.has_sendfile_ctx) "has sendfile context" else "no sendfile context", + }); + return this.resp != null and + !this.flags.aborted and + !this.flags.has_marked_complete and + !this.flags.has_marked_pending and + this.ref_count == 1 and + !this.flags.is_waiting_for_request_body and + !this.flags.has_sendfile_ctx; } pub fn isDeadRequest(this: *RequestContext) bool { @@ -1449,6 +1465,12 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, resp.writeHeaderInt("content-length", 0); this.endWithoutBody(this.shouldCloseConnection()); }, + .Render => { + // Render should have been handled elsewhere, this is unexpected + this.renderMetadata(); + resp.writeHeaderInt("content-length", 0); + this.endWithoutBody(this.shouldCloseConnection()); + }, } } @@ -1862,6 +1884,74 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, return; }, + .Render => |render_body| { + // Handle Response.render() case - need to call DevServer with new path + defer bun.default_allocator.free(render_body.path); + + if (this.server) |server| { + if (@hasField(@TypeOf(server.*), "dev_server")) { + if (server.dev_server) |dev_server_| { + const dev_server: *bun.bake.DevServer = dev_server_; + // Use the current response from the RequestContext + const response = if (this.resp) |resp| + bun.uws.AnyResponse.init(resp) + else { + this.renderMissing(); + return; + }; + + const signal = if (this.signal) |signal| signal.ref() else null; + + const resp = this.resp; + this.detachResponse(); + + const new_request_ctx = server.request_pool_allocator.tryGet() catch bun.outOfMemory(); + new_request_ctx.* = .{ + .allocator = server.allocator, + .resp = resp, + .req = this.req, + .method = this.method, + .server = server, + .defer_deinit_until_callback_completes = null, + .signal = signal, + }; + + const url = bun.String.cloneUTF8(render_body.path); + const body: jsc.WebCore.Body.Value = .{ .Null = {} }; + + const new_request = Request.new(.{ + .method = .GET, // TODO: use the correct one + .request_context = AnyRequestContext.init(new_request_ctx), + .https = ssl_enabled, + .signal = if (signal) |s| s.ref() else null, + .body = server.vm.initRequestBodyValue(body) catch bun.outOfMemory(), + .url = url, + }); + + new_request_ctx.request_weakref = .initRef(new_request); + + server.onPendingRequest(); + // Call DevServer with the render path + dev_server.handleRenderRedirect(bun.jsc.API.SavedRequest{ + .js_request = .create(new_request.toJS(server.globalThis), server.globalThis), + .ctx = AnyRequestContext.init(new_request_ctx), + .request = new_request, + .response = bun.uws.AnyResponse.init(resp.?), + }, render_body.path, response) catch { + // On error, render missing + this.renderMissing(); + return; + }; + + return; + } + } + } + + // Fallback to rendering missing + this.renderMissing(); + return; + }, else => {}, } diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index f7648e6bfa2f..09beca1a20cd 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5996,11 +5996,56 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV // Set type property auto typeIdentifier = JSC::Identifier::fromString(vm, "type"_s); + // TODO: this is fucking awful + // Check if this is a render redirect (Response.render() case) + // We detect this by checking if component is the special marker string "__bun_render_redirect__" + bool isRenderRedirect = false; + if (component.isString()) { + JSC::JSString* componentString = component.toString(globalObject); + if (componentString) { + String componentStr = componentString->value(globalObject); + if (componentStr == "__bun_render_redirect__"_s) { + isRenderRedirect = true; + } + } + } + + // Handle Response.render() case - use BakeSSRResponse builtin to create wrapper + if (isRenderRedirect) { + // Get the render path and params from the response object + auto renderPathIdentifier = JSC::Identifier::fromString(vm, "__renderPath"_s); + auto renderParamsIdentifier = JSC::Identifier::fromString(vm, "__renderParams"_s); + + JSC::JSValue renderPath = responseObject->get(globalObject, renderPathIdentifier); + JSC::JSValue renderParams = responseObject->get(globalObject, renderParamsIdentifier); + + // Use the BakeSSRResponse builtin's wrapComponent function + JSC::JSFunction* wrapComponentFn = JSC::JSFunction::create(vm, globalObject, bakeSSRResponseWrapComponentCodeGenerator(vm), globalObject); + + // Call wrapComponent(path, params, true, responseObject) where true indicates this is a render redirect + JSC::MarkedArgumentBuffer args; + args.append(renderPath); + args.append(renderParams); + args.append(JSC::jsBoolean(true)); // This is a render redirect + args.append(response); // Pass the Response object + + // Call the wrapComponent function + auto callData = JSC::getCallData(wrapComponentFn); + JSC::JSValue wrappedComponent = JSC::call(globalObject, wrapComponentFn, callData, JSC::jsUndefined(), args); + + if (!scope.exception() && !wrappedComponent.isUndefinedOrNull()) { + responseObject->putDirect(vm, typeIdentifier, wrappedComponent, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } else { + // If there was an error, clear it and set type to null + scope.clearException(); + responseObject->putDirect(vm, typeIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } + } // TODO: this is stupid // Check if the component is a JSX element (has $$typeof property) // If it is, we need to wrap it in a function for React to work properly // because `` is already the result of calling Component() - if (component.isObject()) { + else if (component.isObject()) { JSC::JSObject* componentObject = component.getObject(); auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s); JSC::JSValue typeofValue = componentObject->get(globalObject, typeofProperty); @@ -6019,10 +6064,12 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV // So for BakeSSRResponse.ts with export function wrapComponent: JSC::JSFunction* wrapComponentFn = JSC::JSFunction::create(vm, globalObject, bakeSSRResponseWrapComponentCodeGenerator(vm), globalObject); - // Call wrapComponent(component, responseOptions) + // Call wrapComponent(component, responseOptions, false, undefined) JSC::MarkedArgumentBuffer args; args.append(component); args.append(responseOptions); + args.append(JSC::jsBoolean(false)); // Not a render redirect + args.append(JSC::jsUndefined()); // No response object for regular case // Call the wrapComponent function auto callData = JSC::getCallData(wrapComponentFn); diff --git a/src/bun.js/webcore/Blob.zig b/src/bun.js/webcore/Blob.zig index 363be1758ff3..82d92fbb8a79 100644 --- a/src/bun.js/webcore/Blob.zig +++ b/src/bun.js/webcore/Blob.zig @@ -1322,6 +1322,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr .Empty, .Blob, .Null, + .Render, => { break :brk response.body.use(); }, @@ -1383,6 +1384,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr .Empty, .Blob, .Null, + .Render, => { break :brk request.body.value.use(); }, diff --git a/src/bun.js/webcore/Body.zig b/src/bun.js/webcore/Body.zig index e22184331483..6be3a8469567 100644 --- a/src/bun.js/webcore/Body.zig +++ b/src/bun.js/webcore/Body.zig @@ -268,6 +268,9 @@ pub const Value = union(Tag) { Empty, Error: ValueError, Null, + Render: struct { + path: []const u8, + }, // We may not have all the data yet // So we can't know for sure if it's empty or not @@ -280,6 +283,7 @@ pub const Value = union(Tag) { .Blob => this.Blob.size == 0, .WTFStringImpl => this.WTFStringImpl.length() == 0, .Error, .Locked => false, + .Render => false, }; } @@ -440,6 +444,7 @@ pub const Value = union(Tag) { Empty, Error, Null, + Render, }; // pub const empty = Value{ .Empty = {} }; @@ -457,6 +462,10 @@ pub const Value = union(Tag) { .Null => { return JSValue.null; }, + .Render => { + // Render variant cannot be converted to a stream + return JSValue.null; + }, .InternalBlob, .Blob, .WTFStringImpl => { var blob = this.use(); defer blob.detach(); @@ -751,6 +760,7 @@ pub const Value = union(Tag) { .InternalBlob => this.InternalBlob.sliceConst(), .WTFStringImpl => if (this.WTFStringImpl.canUseAsUTF8()) this.WTFStringImpl.latin1Slice() else "", // .InlineBlob => this.InlineBlob.sliceConst(), + .Render => "", else => "", }; } @@ -1396,6 +1406,7 @@ pub const ValueBufferer = struct { .WTFStringImpl, .InternalBlob, .Blob, + .Render, => { // toBlobIfPossible checks for WTFString needing a conversion. var input = value.useAsAnyBlobAllowNonUTF8String(); diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 193641686214..4d2dc20a3eea 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -30,6 +30,8 @@ redirected: bool = false, /// In the server we use a flag response_protected to protect/unprotect the response ref_count: u32 = 1, +is_jsx: bool = false, + // We must report a consistent value for this reported_estimated_size: usize = 0, @@ -120,7 +122,7 @@ pub export fn jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer(globa var any_blob = body.useAsAnyBlob(); return any_blob.toArrayBufferTransfer(globalObject) catch return .zero; }, - .Error, .Locked => return .js_undefined, + .Error, .Locked, .Render => return .js_undefined, } } @@ -540,39 +542,50 @@ pub fn constructRender( return globalThis.throwInvalidArguments("Response.render() path must be a string", .{}); } - // Get params if provided (second argument) - const params_arg = if (arguments.len >= 2) arguments.ptr[1] else JSValue.jsNull(); + // Get the path string + const path_str = try path_arg.toSlice(globalThis, bun.default_allocator); - // Get the AsyncLocalStorage instance - const als = vm.dev_server_async_local_storage.get() orelse { - return globalThis.throwInvalidArguments("Response.render() AsyncLocalStorage not found", .{}); + // Duplicate the path string so it persists + const path_copy = bun.default_allocator.dupe(u8, path_str.slice()) catch { + path_str.deinit(); + return globalThis.throwOutOfMemory(); }; + path_str.deinit(); - // Call als.getStore() to get the current store - const getStore_fn = try als.get(globalThis, "getStore") orelse { - return globalThis.throwInvalidArguments("Response.render() getStore method not found", .{}); - }; - - const store = try getStore_fn.call(globalThis, als, &.{}); + // Create a Response with Render body + var response = bun.new(Response, Response{ + .body = Body{ + .value = .{ + .Render = .{ + .path = path_copy, + }, + }, + }, + .init = Response.Init{ + .status_code = 200, + }, + }); - if (store.isUndefinedOrNull()) { - return globalThis.throwInvalidArguments("Response.render() no active context", .{}); - } + const response_js = response.toJS(globalThis); + response_js.ensureStillAlive(); - // Get the renderAbort function from the store - const renderAbort = try store.get(globalThis, "renderAbort") orelse { - return globalThis.throwInvalidArguments("Response.render() is only available in non-streaming mode", .{}); - }; + // Store the render path and params on the response for later use + // When React tries to render this component, we'll check for these and throw RenderAbortError + response_js.put(globalThis, "__renderPath", path_arg); + const params_arg = if (arguments.len >= 2) arguments.ptr[1] else JSValue.jsNull(); + response_js.put(globalThis, "__renderParams", params_arg); - if (!renderAbort.isCallable()) { - return globalThis.throwInvalidArguments("Response.render() is only available in non-streaming mode", .{}); - } + // TODO: this is terrible + // Create a simple wrapper function that will be called by React + // This needs to be handled specially in transformToReactElementWithOptions + // We'll pass a special marker as the component to indicate this is a render redirect + const render_marker = ZigString.init("__bun_render_redirect__").toJS(globalThis); - // Call renderAbort(path, params) - this will throw and abort the render - _ = try renderAbort.call(globalThis, store, &.{ path_arg, params_arg }); + // Transform the Response to act as a React element + // The C++ code will need to check for this special marker + JSValue.transformToReactElementWithOptions(response_js, render_marker, params_arg, globalThis); - // This should not be reached as renderAbort should throw - return .js_undefined; + return response_js; } pub fn constructError( @@ -597,6 +610,7 @@ pub fn constructError( pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, this_value: jsc.JSValue) bun.JSError!*Response { var arguments = callframe.argumentsAsArray(2); + var is_jsx = false; if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) { if (arguments[0].as(Blob)) |blob| { if (blob.isS3()) { @@ -640,6 +654,7 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, t // so it can store them for later use when the component is rendered const responseOptions = if (arguments[1].isObject()) arguments[1] else .js_undefined; JSValue.transformToReactElementWithOptions(this_value, arg, responseOptions, globalThis); + is_jsx = true; } } } @@ -681,6 +696,7 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, t var response = bun.new(Response, Response{ .body = body, .init = init, + .is_jsx = is_jsx, }); if (response.body.value == .Blob and diff --git a/src/bun.js/webcore/blob/write_file.zig b/src/bun.js/webcore/blob/write_file.zig index e72f49e32fc3..988bb682d669 100644 --- a/src/bun.js/webcore/blob/write_file.zig +++ b/src/bun.js/webcore/blob/write_file.zig @@ -689,6 +689,7 @@ pub const WriteFileWaitFromLockedValueTask = struct { .Null, .Empty, .Blob, + .Render, => { var blob = value.use(); // TODO: this should be one promise not two! diff --git a/src/js/builtins/BakeSSRResponse.ts b/src/js/builtins/BakeSSRResponse.ts index a39d93e8ab2d..fd03e4d12a3f 100644 --- a/src/js/builtins/BakeSSRResponse.ts +++ b/src/js/builtins/BakeSSRResponse.ts @@ -1,11 +1,29 @@ // Used to make a Response fake being a component // When this is called, it will render the component and then update async local // storage with the options of the Response -export function wrapComponent(strongComponent, responseOptions) { +// For Response.render(), we pass the response as strongComponent and need a 4th parameter +export function wrapComponent(strongComponent, responseOptions, isRenderRedirect, responseObject) { const bakeGetAsyncLocalStorage = $newZigFunction("bun.js/webcore/Response.zig", "bakeGetAsyncLocalStorage", 0); - return () => { - // Update the AsyncLocalStorage with the response options + return function() { + // For Response.render(), we need to throw a RenderAbortError + if (isRenderRedirect) { + // strongComponent is the path string, responseOptions is params, responseObject is the Response + // We need to get the RenderAbortError from the global + const RenderAbortError = globalThis.RenderAbortError; + if (RenderAbortError) { + throw new RenderAbortError(strongComponent, responseOptions, responseObject); + } + // Fallback if RenderAbortError is not available + const error = new Error("Response.render() called"); + error.name = "RenderAbortError"; + error.path = strongComponent; + error.params = responseOptions; + error.response = responseObject; + throw error; + } + + // For new Response(, {}), update AsyncLocalStorage const async_local_storage = bakeGetAsyncLocalStorage(); if (async_local_storage) { const store = async_local_storage.getStore(); From 59f12d30b30eba153d038e54ed6b5dda494319e7 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:33:32 -0700 Subject: [PATCH 037/130] response redirect --- src/bake/hmr-runtime-server.ts | 20 ++++++++++++++++- src/bun.js/bindings/bindings.cpp | 35 ++++++++++++++++++++++++++++-- src/bun.js/webcore/Response.zig | 17 ++++++++++++++- src/js/builtins/BakeSSRResponse.ts | 17 ++++++++++++++- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index d2489a459a86..7fc2e00af814 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -31,8 +31,19 @@ class RenderAbortError extends Error { } } -// Make RenderAbortError globally available for other modules +/// Created when the user does `return Response.redirect(...)` +class RedirectAbortError extends Error { + constructor( + public response: Response, + ) { + super("Response.redirect() called"); + this.name = "RedirectAbortError"; + } +} + +// Make RenderAbortError and RedirectAbortError globally available for other modules (globalThis as any).RenderAbortError = RenderAbortError; +(globalThis as any).RedirectAbortError = RedirectAbortError; interface Exports { handleRequest: ( @@ -141,6 +152,13 @@ server_exports = { // return it so the Zig code can handle it and re-render new route return error.response; } + + // Handle Response.redirect() aborts + if (error instanceof RedirectAbortError) { + // Return the redirect response directly + return error.response; + } + throw error; } }, diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 09beca1a20cd..4d6f1c8c1495 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5997,15 +5997,18 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV auto typeIdentifier = JSC::Identifier::fromString(vm, "type"_s); // TODO: this is fucking awful - // Check if this is a render redirect (Response.render() case) - // We detect this by checking if component is the special marker string "__bun_render_redirect__" + // Check if this is a render redirect (Response.render() case) or regular redirect (Response.redirect() case) + // We detect this by checking if component is the special marker string "__bun_render_redirect__" or "__bun_redirect__" bool isRenderRedirect = false; + bool isRedirect = false; if (component.isString()) { JSC::JSString* componentString = component.toString(globalObject); if (componentString) { String componentStr = componentString->value(globalObject); if (componentStr == "__bun_render_redirect__"_s) { isRenderRedirect = true; + } else if (componentStr == "__bun_redirect__"_s) { + isRedirect = true; } } } @@ -6041,6 +6044,34 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV responseObject->putDirect(vm, typeIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); } } + // Handle Response.redirect() case - use BakeSSRResponse builtin to create wrapper + else if (isRedirect) { + // Use the BakeSSRResponse builtin's wrapComponent function + JSC::JSFunction* wrapComponentFn = JSC::JSFunction::create(vm, globalObject, bakeSSRResponseWrapComponentCodeGenerator(vm), globalObject); + + // Call wrapComponent(undefined, undefined, "redirect", responseObject) + // For redirect, we pass "redirect" as the third parameter and the Response object as the fourth + JSC::MarkedArgumentBuffer args; + args.append(JSC::jsUndefined()); // No component for redirect + args.append(JSC::jsUndefined()); // No response options for redirect + + // Pass "redirect" string as the third parameter to indicate this is a redirect + // The responseOptions parameter from transformToReactElementWithOptions contains "redirect" + args.append(responseOptions); // This should be the "redirect" string + args.append(response); // Pass the Response object + + // Call the wrapComponent function + auto callData = JSC::getCallData(wrapComponentFn); + JSC::JSValue wrappedComponent = JSC::call(globalObject, wrapComponentFn, callData, JSC::jsUndefined(), args); + + if (!scope.exception() && !wrappedComponent.isUndefinedOrNull()) { + responseObject->putDirect(vm, typeIdentifier, wrappedComponent, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } else { + // If there was an error, clear it and set type to null + scope.clearException(); + responseObject->putDirect(vm, typeIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + } + } // TODO: this is stupid // Check if the component is a JSX element (has $$typeof property) // If it is, we need to wrap it in a function for React to work properly diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 4d2dc20a3eea..609db50a5cb8 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -516,8 +516,23 @@ pub fn constructRedirect( var headers_ref = response.init.headers.?; try headers_ref.put(.Location, url_string_slice.slice(), globalThis); const ptr = bun.new(Response, response); + + const response_js = ptr.toJS(globalThis); + + // Check if dev_server_async_local_storage is set (indicating we're in Bun dev server) + const vm = globalThis.bunVM(); + if (vm.dev_server_async_local_storage.has()) { + // Mark this as a redirect Response that should be handled specially + // when used in a React component + const redirect_marker = ZigString.init("__bun_redirect__").toJS(globalThis); + + // Transform the Response to act as a React element with special redirect handling + // Pass "redirect" as the third parameter to indicate this is a redirect + const redirect_flag = ZigString.init("redirect").toJS(globalThis); + JSValue.transformToReactElementWithOptions(response_js, redirect_marker, redirect_flag, globalThis); + } - return ptr.toJS(globalThis); + return response_js; } pub fn constructRender( diff --git a/src/js/builtins/BakeSSRResponse.ts b/src/js/builtins/BakeSSRResponse.ts index fd03e4d12a3f..0935e3140c05 100644 --- a/src/js/builtins/BakeSSRResponse.ts +++ b/src/js/builtins/BakeSSRResponse.ts @@ -2,12 +2,27 @@ // When this is called, it will render the component and then update async local // storage with the options of the Response // For Response.render(), we pass the response as strongComponent and need a 4th parameter +// For Response.redirect(), isRenderRedirect will be "redirect" instead of true export function wrapComponent(strongComponent, responseOptions, isRenderRedirect, responseObject) { const bakeGetAsyncLocalStorage = $newZigFunction("bun.js/webcore/Response.zig", "bakeGetAsyncLocalStorage", 0); return function() { + // For Response.redirect(), we need to throw a RedirectAbortError + if (isRenderRedirect === "redirect") { + // responseObject is the Response from Response.redirect() + const RedirectAbortError = globalThis.RedirectAbortError; + if (RedirectAbortError) { + throw new RedirectAbortError(responseObject); + } + // Fallback if RedirectAbortError is not available + const error = new Error("Response.redirect() called"); + error.name = "RedirectAbortError"; + error.response = responseObject; + throw error; + } + // For Response.render(), we need to throw a RenderAbortError - if (isRenderRedirect) { + if (isRenderRedirect === true || isRenderRedirect === "render") { // strongComponent is the path string, responseOptions is params, responseObject is the Response // We need to get the RenderAbortError from the global const RenderAbortError = globalThis.RenderAbortError; From c343aca21e01080b78362b797f3d50a7e2e99661 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 01:33:55 +0000 Subject: [PATCH 038/130] [autofix.ci] apply automated fixes --- src/bake/bun-framework-react/server.tsx | 2 +- src/bake/hmr-runtime-server.ts | 8 ++---- src/bun.js/bindings/JSValue.zig | 2 +- src/bun.js/bindings/bindings.cpp | 4 +-- src/bun.js/virtual_machine_exports.zig | 5 ++-- src/bun.js/webcore/Response.zig | 6 ++-- src/js/builtins/BakeSSRResponse.ts | 6 ++-- test/bake/dev/server-sourcemap.test.ts | 37 +++++++++++++------------ 8 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 4fbf0bb47aff..eaa254838c2d 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -1,9 +1,9 @@ import type { Bake } from "bun"; import { renderToHtml, renderToStaticHtml } from "bun-framework-react/ssr.tsx" with { bunBakeGraph: "ssr" }; import { serverManifest } from "bun:bake/server"; +import type { AsyncLocalStorage } from "node:async_hooks"; import { PassThrough } from "node:stream"; import { renderToPipeableStream } from "react-server-dom-bun/server.node.unbundled.js"; -import type { AsyncLocalStorage } from "node:async_hooks"; import type { RequestContext } from "../hmr-runtime-server"; function assertReactComponent(Component: any) { diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 7fc2e00af814..64bfd4b31ba1 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -33,9 +33,7 @@ class RenderAbortError extends Error { /// Created when the user does `return Response.redirect(...)` class RedirectAbortError extends Error { - constructor( - public response: Response, - ) { + constructor(public response: Response) { super("Response.redirect() called"); this.name = "RedirectAbortError"; } @@ -152,13 +150,13 @@ server_exports = { // return it so the Zig code can handle it and re-render new route return error.response; } - + // Handle Response.redirect() aborts if (error instanceof RedirectAbortError) { // Return the redirect response directly return error.response; } - + throw error; } }, diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 2999b0448c6b..51f2688a1177 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -66,7 +66,7 @@ pub const JSValue = enum(i64) { pub fn transformToReactElement(responseValue: JSValue, componentValue: JSValue, globalThis: *jsc.JSGlobalObject) void { JSC__JSValue__transformToReactElement(responseValue, componentValue, globalThis); } - + extern fn JSC__JSValue__transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalObject: *JSGlobalObject) void; pub fn transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalThis: *jsc.JSGlobalObject) void { JSC__JSValue__transformToReactElementWithOptions(responseValue, componentValue, responseOptions, globalThis); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 0311a763b23c..68fa2d91e1c3 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -6056,10 +6056,10 @@ extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSV JSC::MarkedArgumentBuffer args; args.append(JSC::jsUndefined()); // No component for redirect args.append(JSC::jsUndefined()); // No response options for redirect - + // Pass "redirect" string as the third parameter to indicate this is a redirect // The responseOptions parameter from transformToReactElementWithOptions contains "redirect" - args.append(responseOptions); // This should be the "redirect" string + args.append(responseOptions); // This should be the "redirect" string args.append(response); // Pass the Response object // Call the wrapComponent function diff --git a/src/bun.js/virtual_machine_exports.zig b/src/bun.js/virtual_machine_exports.zig index f7f1520fe0b3..0232a574cf65 100644 --- a/src/bun.js/virtual_machine_exports.zig +++ b/src/bun.js/virtual_machine_exports.zig @@ -215,11 +215,12 @@ const IPC = @import("./ipc.zig"); const std = @import("std"); const bun = @import("bun"); -const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; -const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider; const PluginRunner = bun.transpiler.PluginRunner; const jsc = bun.jsc; const JSGlobalObject = jsc.JSGlobalObject; const JSValue = jsc.JSValue; const VirtualMachine = jsc.VirtualMachine; + +const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; +const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider; diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 609db50a5cb8..60d937425ee9 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -516,16 +516,16 @@ pub fn constructRedirect( var headers_ref = response.init.headers.?; try headers_ref.put(.Location, url_string_slice.slice(), globalThis); const ptr = bun.new(Response, response); - + const response_js = ptr.toJS(globalThis); - + // Check if dev_server_async_local_storage is set (indicating we're in Bun dev server) const vm = globalThis.bunVM(); if (vm.dev_server_async_local_storage.has()) { // Mark this as a redirect Response that should be handled specially // when used in a React component const redirect_marker = ZigString.init("__bun_redirect__").toJS(globalThis); - + // Transform the Response to act as a React element with special redirect handling // Pass "redirect" as the third parameter to indicate this is a redirect const redirect_flag = ZigString.init("redirect").toJS(globalThis); diff --git a/src/js/builtins/BakeSSRResponse.ts b/src/js/builtins/BakeSSRResponse.ts index 0935e3140c05..1b6de3553d6c 100644 --- a/src/js/builtins/BakeSSRResponse.ts +++ b/src/js/builtins/BakeSSRResponse.ts @@ -6,7 +6,7 @@ export function wrapComponent(strongComponent, responseOptions, isRenderRedirect, responseObject) { const bakeGetAsyncLocalStorage = $newZigFunction("bun.js/webcore/Response.zig", "bakeGetAsyncLocalStorage", 0); - return function() { + return function () { // For Response.redirect(), we need to throw a RedirectAbortError if (isRenderRedirect === "redirect") { // responseObject is the Response from Response.redirect() @@ -20,7 +20,7 @@ export function wrapComponent(strongComponent, responseOptions, isRenderRedirect error.response = responseObject; throw error; } - + // For Response.render(), we need to throw a RenderAbortError if (isRenderRedirect === true || isRenderRedirect === "render") { // strongComponent is the path string, responseOptions is params, responseObject is the Response @@ -37,7 +37,7 @@ export function wrapComponent(strongComponent, responseOptions, isRenderRedirect error.response = responseObject; throw error; } - + // For new Response(, {}), update AsyncLocalStorage const async_local_storage = bakeGetAsyncLocalStorage(); if (async_local_storage) { diff --git a/test/bake/dev/server-sourcemap.test.ts b/test/bake/dev/server-sourcemap.test.ts index 884046c372d7..2d2cad65d906 100644 --- a/test/bake/dev/server-sourcemap.test.ts +++ b/test/bake/dev/server-sourcemap.test.ts @@ -28,24 +28,24 @@ export async function getStaticPaths() { async test(dev) { // Make a request that will trigger the error await dev.fetch("/test-error").catch(() => {}); - + // Give it a moment to process the error await Bun.sleep(1000); - + // The output we saw shows the stack trace with correct source mapping // We need to check that the error shows the right file:line:column const lines = dev.output.lines.join("\n"); - + // Check that we got the error expect(lines).toContain("Test error for source maps!"); - + // Check that the stack trace shows correct file and line numbers // The source maps are working if we see the correct patterns // We need to check for the patterns because ANSI codes might be embedded const hasCorrectThrowLine = lines.includes("myFunc") && lines.includes("7") && lines.includes("9"); const hasCorrectCallLine = lines.includes("MyPage") && lines.includes("2") && lines.includes("3"); const hasCorrectFileName = lines.includes("/pages/[...slug].tsx"); - + expect(hasCorrectThrowLine).toBe(true); expect(hasCorrectCallLine).toBe(true); expect(hasCorrectFileName).toBe(true); @@ -71,9 +71,11 @@ export async function getStaticPaths() { const response1 = await dev.fetch("/error-page"); expect(response1.status).toBe(200); expect(await response1.text()).toContain("Initial content"); - + // Update the file to throw an error - await dev.write("pages/error-page.tsx", `export default function ErrorPage() { + await dev.write( + "pages/error-page.tsx", + `export default function ErrorPage() { throwError(); return
Updated content
; } @@ -86,22 +88,23 @@ export async function getStaticPaths() { return { paths: [{ params: {} }], }; -}`); - +}`, + ); + // Wait for the rebuild await dev.waitForHmr(); - + // Second fetch should error await dev.fetch("/error-page").catch(() => {}); - + // Wait for error output await dev.output.waitForLine(/HMR error test/); - + // Check source map points to correct lines after HMR const lines = dev.output.lines.join("\n"); const hasCorrectThrowLine = lines.includes("throwError") && lines.includes("7") && lines.includes("9"); const hasCorrectCallLine = lines.includes("ErrorPage") && lines.includes("2") && lines.includes("3"); - + expect(hasCorrectThrowLine).toBe(true); expect(hasCorrectCallLine).toBe(true); }, @@ -133,18 +136,18 @@ function helperFunction() { async test(dev) { // Make request that triggers error await dev.fetch("/nested").catch(() => {}); - + // Wait for error output await dev.output.waitForLine(/Nested error/); - + // Check that stack trace shows both files with correct lines const lines = dev.output.lines.join("\n"); const hasUtilsThrowLine = lines.includes("helperFunction") && lines.includes("6") && lines.includes("9"); const hasUtilsCallLine = lines.includes("doSomething") && lines.includes("2"); const hasPageCallLine = lines.includes("NestedPage") && lines.includes("4"); - + expect(hasUtilsThrowLine).toBe(true); expect(hasUtilsCallLine).toBe(true); expect(hasPageCallLine).toBe(true); }, -}); \ No newline at end of file +}); From 0fb277a56e9c4c0d84576bab73b2b28963594844 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:12:34 -0700 Subject: [PATCH 039/130] fix more compile errors --- src/allocators/AllocationScope.zig | 2 +- src/bake/DevServer.zig | 13 ++-- src/bake/DevServer/IncrementalGraph.zig | 88 +++++++++++++++++++------ 3 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/allocators/AllocationScope.zig b/src/allocators/AllocationScope.zig index edf3d188681a..aea8b26c84db 100644 --- a/src/allocators/AllocationScope.zig +++ b/src/allocators/AllocationScope.zig @@ -280,7 +280,7 @@ pub inline fn downcast(a: Allocator) ?Self { null; } -pub fn leakSlice(scope: *AllocationScope, memory: anytype) void { +pub fn leakSlice(scope: *Self, memory: anytype) void { if (comptime !enabled) return; _ = @typeInfo(@TypeOf(memory)).pointer; bun.assert(!scope.trackExternalFree(memory, null)); diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 7444869490e7..ce48676d4446 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -840,7 +840,7 @@ fn onJsRequest(dev: *DevServer, req: *Request, resp: AnyResponse) void { dev, arena.allocator(), source_id.kind, - dev.allocator, + dev.allocator(), .client, ) catch bun.outOfMemory(); const response = StaticRoute.initFromAnyBlob(&.fromOwnedSlice(dev.allocator(), json_bytes), .{ @@ -2341,22 +2341,23 @@ pub fn finalizeBundle( .paths = &.{}, .files = .empty, .overlapping_memory_cost = 0, + .dev_allocator = dev.dev_allocator(), }; // Fill the source map entry - var arena = std.heap.ArenaAllocator.init(dev.allocator); + var arena = std.heap.ArenaAllocator.init(dev.allocator()); defer arena.deinit(); - try dev.server_graph.takeSourceMap(arena.allocator(), dev.allocator, &source_map_entry); + try dev.server_graph.takeSourceMap(arena.allocator(), dev.allocator(), &source_map_entry); defer { source_map_entry.ref_count = 0; - source_map_entry.deinit(dev); + source_map_entry.deinit(); } const json_data = try source_map_entry.renderJSON( dev, arena.allocator(), .hmr_chunk, - dev.allocator, + dev.allocator(), .server, ); break :json json_data; @@ -2367,7 +2368,7 @@ pub fn finalizeBundle( .kind = .hmr_chunk, .script_id = server_script_id, }); - defer dev.allocator.free(server_bundle); + defer dev.allocator().free(server_bundle); // TODO: is this the best place to set this? Would it be better to // transpile the server modules to replace `new Response(...)` with `new diff --git a/src/bake/DevServer/IncrementalGraph.zig b/src/bake/DevServer/IncrementalGraph.zig index 8f355d51c8fe..e41d0e13e8be 100644 --- a/src/bake/DevServer/IncrementalGraph.zig +++ b/src/bake/DevServer/IncrementalGraph.zig @@ -281,7 +281,7 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { }, /// Source maps for server chunks - current_chunk_source_maps: if (side == .server) ArrayListUnmanaged(PackedMap.RefOrEmpty) else void = if (side == .server) .empty, + current_chunk_source_maps: if (side == .server) ArrayListUnmanaged(PackedMap.Shared) else void = if (side == .server) .empty, /// File indices for server chunks to track which file each chunk comes from current_chunk_file_indices: if (side == .server) ArrayListUnmanaged(FileIndex) else void = if (side == .server) .empty, @@ -389,12 +389,12 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { .current_css_files = if (comptime side == .client) g.current_css_files.deinit(alloc), .current_chunk_source_maps = if (side == .server) { for (g.current_chunk_source_maps.items) |source_map| { - source_map.deref(&g.owner().*); + source_map.deinit(); } g.current_chunk_source_maps.deinit(alloc); }, .current_chunk_file_indices = if (side == .server) g.current_chunk_file_indices.deinit(alloc), - }; + }); } const MemoryCost = struct { @@ -429,9 +429,7 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { graph += DevServer.memoryCostArrayList(g.current_chunk_source_maps); graph += DevServer.memoryCostArrayList(g.current_chunk_file_indices); for (g.current_chunk_source_maps.items) |source_map| { - if (source_map == .ref) { - source_maps += source_map.ref.data.memoryCostWithDedupe(new_dedupe_bits); - } + source_maps += source_map.memoryCost(); } } return .{ @@ -466,7 +464,7 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { g: *Self, ctx: *HotUpdateContext, index: bun.ast.Index, - content: union(enum) { + _content: union(enum) { js: struct { code: JsCode, source_map: ?struct { @@ -478,6 +476,7 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { }, is_ssr_graph: bool, ) !void { + var content = _content; const dev = g.owner(); dev.graph_safety_lock.assertLocked(); @@ -658,7 +657,7 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { g.current_chunk_len += content.js.code.len; // Track the file index for this chunk - try g.current_chunk_file_indices.append(dev.allocator, file_index); + try g.current_chunk_file_indices.append(dev.allocator(), file_index); // TODO: we probably want to store SSR chunks but not // server chunks, but not 100% sure @@ -672,10 +671,11 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { } } } else { - if (content.js.source_map) |source_map| append_empty: { - const packed_map = PackedMap.newNonEmpty(source_map.chunk, source_map.escaped_source orelse break :append_empty); - try g.current_chunk_source_maps.append(dev.allocator, .{ - .ref = packed_map, + if (content.js.source_map) |*source_map| append_empty: { + const escaped_source = source_map.escaped_source.take() orelse break :append_empty; + const packed_map = PackedMap.newNonEmpty(source_map.chunk, escaped_source); + try g.current_chunk_source_maps.append(dev.allocator(), .{ + .some = packed_map, }); return; } @@ -683,12 +683,8 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { // Must precompute this. Otherwise, source maps won't have // the info needed to concatenate VLQ mappings. const count: u32 = @intCast(bun.strings.countChar(content.js.code, '\n')); - try g.current_chunk_source_maps.append(dev.allocator, PackedMap.RefOrEmpty{ - .empty = .{ - .line_count = .init(count), - // TODO: not sure if this is correct - .html_bundle_route_index = .none, - }, + try g.current_chunk_source_maps.append(dev.allocator(), PackedMap.Shared{ + .line_count = .init(count), }); } } @@ -1824,10 +1820,62 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { const buf = bun.path_buffer_pool.get(); defer bun.path_buffer_pool.put(buf); + + var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len); + errdefer file_paths.deinit(gpa); + var contained_maps: bun.MultiArrayList(PackedMap.Shared) = .empty; + try contained_maps.ensureTotalCapacity(gpa, g.current_chunk_parts.items.len); + errdefer contained_maps.deinit(gpa); + + var overlapping_memory_cost: usize = 0; + + for (g.current_chunk_parts.items) |file_index| { + file_paths.appendAssumeCapacity(paths[file_index.get()]); + const source_map = files[file_index.get()].unpack().source_map.clone(); + if (source_map.get()) |map| { + overlapping_memory_cost += map.memoryCost(); + } + contained_maps.appendAssumeCapacity(source_map); + } + + overlapping_memory_cost += contained_maps.memoryCost() + DevServer.memoryCostSlice(file_paths.items); + + const ref_count = out.ref_count; + out.* = .{ + .dev_allocator = g.dev_allocator(), + .ref_count = ref_count, + .paths = file_paths.items, + .files = contained_maps, + .overlapping_memory_cost = @intCast(overlapping_memory_cost), + }; }, - .server => {}, - } + .server => { + var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len); + errdefer file_paths.deinit(gpa); + var contained_maps: bun.MultiArrayList(PackedMap.Shared) = .empty; + try contained_maps.ensureTotalCapacity(gpa, g.current_chunk_parts.items.len); + errdefer contained_maps.deinit(gpa); + + var overlapping_memory_cost: u32 = 0; + + // For server, we use the tracked file indices to get the correct paths + for (g.current_chunk_file_indices.items, g.current_chunk_source_maps.items) |file_index, source_map| { + file_paths.appendAssumeCapacity(paths[file_index.get()]); + contained_maps.appendAssumeCapacity(source_map.clone()); + overlapping_memory_cost += @intCast(source_map.memoryCost()); + } + overlapping_memory_cost += @intCast(contained_maps.memoryCost() + DevServer.memoryCostSlice(file_paths.items)); + + out.* = .{ + .dev_allocator = g.dev_allocator(), + .ref_count = out.ref_count, + .paths = file_paths.items, + .files = contained_maps, + .overlapping_memory_cost = overlapping_memory_cost, + }; + }, + } } fn disconnectAndDeleteFile(g: *Self, file_index: FileIndex) void { From cb9c45c26c2b7909e49e85a3c36e37e5d25c5d59 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:56:02 -0700 Subject: [PATCH 040/130] always use dev.allocator() to allocate sourcemaps we honestly should remove this dev.allocator() and just use leak sanitizer --- src/bundler/linker_context/generateCompileResultForJSChunk.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bundler/linker_context/generateCompileResultForJSChunk.zig b/src/bundler/linker_context/generateCompileResultForJSChunk.zig index 06767fa06fca..22e294746e87 100644 --- a/src/bundler/linker_context/generateCompileResultForJSChunk.zig +++ b/src/bundler/linker_context/generateCompileResultForJSChunk.zig @@ -32,8 +32,7 @@ fn generateCompileResultForJSChunkImpl(worker: *ThreadPool.Worker, c: *LinkerCon // as it must outlive the bundle task. const allocator = blk: { const dev = c.dev_server orelse break :blk default_allocator; - const graph = c.parse_graph.ast.items(.target)[part_range.source_index.get()].bakeGraph(); - break :blk if (graph == .client) dev.allocator() else default_allocator; + break :blk dev.allocator(); }; var arena = &worker.temporary_arena; From 842503ecb1c9ed9568f506421d283a07a7e67c3a Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:39:41 -0700 Subject: [PATCH 041/130] fix errors --- src/bake/bun-framework-react/server.tsx | 7 ++----- src/bake/bun-framework-react/ssr.tsx | 13 ++++++++++--- src/bake/hmr-runtime-server.ts | 11 +---------- src/js/builtins/BakeSSRResponse.ts | 12 ++---------- 4 files changed, 15 insertions(+), 28 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index eaa254838c2d..f70596c74333 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -100,7 +100,6 @@ export async function render( const signal: MiniAbortSignal = { aborted: undefined, abort: null! }; ({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, { onError: err => { - // console.error("onError renderToPipeableStream", !!signal.aborted); if (signal.aborted) return; // Mark as aborted and call the abort function @@ -144,6 +143,7 @@ export async function render( if (als) { const store = als.getStore(); if (store) { + /* store.renderAbort = (path: string, params: Record | null) => { // Create the abort error const abortError = new (globalThis as any).RenderAbortError(path, params); @@ -153,6 +153,7 @@ export async function render( rscPayload.destroy(abortError); throw abortError; }; + */ } } @@ -168,10 +169,6 @@ export async function render( // Check if the render was aborted with an error if (signal.aborted) { - // If it's a RenderAbortError, re-throw it to be handled upstream - if (signal.aborted instanceof (globalThis as any).RenderAbortError) { - throw signal.aborted; - } // For some reason in react-server-dom the `stream.on("error")` // handler creates a new Error??? if (signal.aborted.message !== "Connection closed.") { diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index 1a40dc57bde9..d9e423f9776b 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -84,7 +84,14 @@ export function renderToHtml( pipe(stream); // Promise resolved after all data is combined. - return stream.finished; + // + // We need to catch this or otherwise it results in unhandled rejection, I + // think this is a problem with `type: "direct"` as it does not happen + // when that line is commented out. + // + // This is fine because the actual error will come in cancel or onError callback elsewhere + return stream.finished.catch(() => {}); + // return stream.finished; }, cancel(err) { if (!signal.aborted) { @@ -255,9 +262,9 @@ class RscInjectionStream extends EventEmitter { // Ignore flush requests from React. Bun will automatically flush when reasonable. } - destroy() {} + destroy(e) {} - end() {} + end(e) {} } class StaticRscInjectionStream extends EventEmitter { diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 64bfd4b31ba1..7c402d2b61f1 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -137,16 +137,6 @@ server_exports = { } catch (error) { // Handle Response.render() aborts if (error instanceof RenderAbortError) { - // TODO: Implement route resolution to get the new route modules - // For now, we'll need to get this information from the native side - // The native code will need to resolve the route and call handleRequest again - - // Store the render error info so native code can access it - (globalThis as any).__lastRenderAbort = { - path: error.path, - params: error.params, - }; - // return it so the Zig code can handle it and re-render new route return error.response; } @@ -157,6 +147,7 @@ server_exports = { return error.response; } + //return error; throw error; } }, diff --git a/src/js/builtins/BakeSSRResponse.ts b/src/js/builtins/BakeSSRResponse.ts index 1b6de3553d6c..ccc33df0d361 100644 --- a/src/js/builtins/BakeSSRResponse.ts +++ b/src/js/builtins/BakeSSRResponse.ts @@ -15,10 +15,7 @@ export function wrapComponent(strongComponent, responseOptions, isRenderRedirect throw new RedirectAbortError(responseObject); } // Fallback if RedirectAbortError is not available - const error = new Error("Response.redirect() called"); - error.name = "RedirectAbortError"; - error.response = responseObject; - throw error; + throw new Error("RedirectAbortError not available this is a bug"); } // For Response.render(), we need to throw a RenderAbortError @@ -30,12 +27,7 @@ export function wrapComponent(strongComponent, responseOptions, isRenderRedirect throw new RenderAbortError(strongComponent, responseOptions, responseObject); } // Fallback if RenderAbortError is not available - const error = new Error("Response.render() called"); - error.name = "RenderAbortError"; - error.path = strongComponent; - error.params = responseOptions; - error.response = responseObject; - throw error; + throw new Error("RenderAbortError not available this is a bug"); } // For new Response(, {}), update AsyncLocalStorage From 19b9c4a850c362b12e45079f2721fca70a9b1632 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:26:06 -0700 Subject: [PATCH 042/130] Properly error when using functions not available when streaming = true --- src/bake/hmr-runtime-server.ts | 5 +++-- src/bun.js/bindings/JSValue.zig | 4 ++-- src/bun.js/webcore/Response.zig | 24 ++++++++++++++++++------ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 7c402d2b61f1..3c12e6280196 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -12,6 +12,7 @@ if (typeof IS_BUN_DEVELOPMENT !== "boolean") { export type RequestContext = { responseOptions: ResponseInit; + streaming: boolean; streamingStarted?: boolean; renderAbort?: (path: string, params: Record | null) => never; }; @@ -73,7 +74,7 @@ server_exports = { setAsyncLocalStorage, getAsyncLocalStorage, ) { - // FIXME: We should only have to do this once + // FIXME: We should only have to create an AsyncLocalStorage instance once // Set the AsyncLocalStorage instance in the VM setAsyncLocalStorage(responseOptionsALS); @@ -107,6 +108,7 @@ server_exports = { let storeValue: RequestContext = { responseOptions: {}, + streaming: pageModule.streaming ?? false, }; try { @@ -147,7 +149,6 @@ server_exports = { return error.response; } - //return error; throw error; } }, diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 51f2688a1177..e8060b940f95 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -68,8 +68,8 @@ pub const JSValue = enum(i64) { } extern fn JSC__JSValue__transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalObject: *JSGlobalObject) void; - pub fn transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalThis: *jsc.JSGlobalObject) void { - JSC__JSValue__transformToReactElementWithOptions(responseValue, componentValue, responseOptions, globalThis); + pub fn transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalThis: *jsc.JSGlobalObject) JSError!void { + return bun.jsc.fromJSHostCallGeneric(globalThis, @src(), JSC__JSValue__transformToReactElementWithOptions, .{ responseValue, componentValue, responseOptions, globalThis }); } extern fn JSC__JSValue__getDirectIndex(JSValue, *JSGlobalObject, u32) JSValue; diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 60d937425ee9..1c11f6c4c122 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -521,7 +521,7 @@ pub fn constructRedirect( // Check if dev_server_async_local_storage is set (indicating we're in Bun dev server) const vm = globalThis.bunVM(); - if (vm.dev_server_async_local_storage.has()) { + if (vm.dev_server_async_local_storage.get()) |async_local_storage| { // Mark this as a redirect Response that should be handled specially // when used in a React component const redirect_marker = ZigString.init("__bun_redirect__").toJS(globalThis); @@ -529,7 +529,8 @@ pub fn constructRedirect( // Transform the Response to act as a React element with special redirect handling // Pass "redirect" as the third parameter to indicate this is a redirect const redirect_flag = ZigString.init("redirect").toJS(globalThis); - JSValue.transformToReactElementWithOptions(response_js, redirect_marker, redirect_flag, globalThis); + try checkStreamingDisabled(globalThis, async_local_storage, "Response.redirect"); + try JSValue.transformToReactElementWithOptions(response_js, redirect_marker, redirect_flag, globalThis); } return response_js; @@ -543,9 +544,11 @@ pub fn constructRender( const vm = globalThis.bunVM(); // Check if dev_server_async_local_storage is set - if (!vm.dev_server_async_local_storage.has()) { + const async_local_storage = vm.dev_server_async_local_storage.get() orelse { return globalThis.throwInvalidArguments("Response.render() is only available in the Bun dev server", .{}); - } + }; + + try checkStreamingDisabled(globalThis, async_local_storage, "Response.render"); // Validate arguments if (arguments.len < 1) { @@ -598,7 +601,7 @@ pub fn constructRender( // Transform the Response to act as a React element // The C++ code will need to check for this special marker - JSValue.transformToReactElementWithOptions(response_js, render_marker, params_arg, globalThis); + try JSValue.transformToReactElementWithOptions(response_js, render_marker, params_arg, globalThis); return response_js; } @@ -668,7 +671,7 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, t // Pass the response options (arguments[1]) to transformToReactElement // so it can store them for later use when the component is rendered const responseOptions = if (arguments[1].isObject()) arguments[1] else .js_undefined; - JSValue.transformToReactElementWithOptions(this_value, arg, responseOptions, globalThis); + try JSValue.transformToReactElementWithOptions(this_value, arg, responseOptions, globalThis); is_jsx = true; } } @@ -855,6 +858,15 @@ inline fn emptyWithStatus(_: *jsc.JSGlobalObject, status: u16) Response { }); } +fn checkStreamingDisabled(globalThis: *jsc.JSGlobalObject, async_local_storage: JSValue, desired_function: []const u8) bun.JSError!void { + if (async_local_storage.isEmptyOrUndefinedOrNull() or !async_local_storage.isObject()) return globalThis.throwInvalidArguments("store value must be an object", .{}); + const getStoreFn = (try async_local_storage.getPropertyValue(globalThis, "getStore")) orelse return globalThis.throwInvalidArguments("store value must have a \"getStore\" field", .{}); + const store_value = try getStoreFn.call(globalThis, async_local_storage, &.{}); + const streaming_val = (try store_value.getPropertyValue(globalThis, "streaming")) orelse return globalThis.throwInvalidArguments("store value must have a \"streaming\" field", .{}); + if (!streaming_val.isBoolean()) return globalThis.throwInvalidArguments("\"streaming\" fied must be a boolean", .{}); + if (streaming_val.asBoolean()) return globalThis.throwInvalidArguments("\"{s}\" is not available when `export const streaming = true`", .{desired_function}); +} + /// https://developer.mozilla.org/en-US/docs/Web/API/Headers // TODO: move to http.zig. this has nothing to do with jsc or WebCore From 03d1e48004f83a4cfe3d07820e18e168dc98d889 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:45:17 -0700 Subject: [PATCH 043/130] holy moly fix all the leaks --- src/bake/DevServer/IncrementalGraph.zig | 45 ++++++++++++------- src/bake/DevServer/PackedMap.zig | 4 +- .../generateCompileResultForJSChunk.zig | 4 +- src/js_printer.zig | 4 +- src/string/MutableString.zig | 6 +++ 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/bake/DevServer/IncrementalGraph.zig b/src/bake/DevServer/IncrementalGraph.zig index e41d0e13e8be..8e62077129a2 100644 --- a/src/bake/DevServer/IncrementalGraph.zig +++ b/src/bake/DevServer/IncrementalGraph.zig @@ -269,9 +269,8 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { /// All part contents current_chunk_parts: ArrayListUnmanaged(switch (side) { .client => FileIndex, - // These slices do not outlive the bundler, and must - // be joined before its arena is deinitialized. - .server => []const u8, + // This memory is allocated by the dev server allocator + .server => bun.ptr.ScopedOwned([]const u8), }), /// Asset IDs, which can be printed as hex in '/_bun/asset/{hash}.css' @@ -385,7 +384,12 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { .edges = g.edges.deinit(alloc), .edges_free_list = g.edges_free_list.deinit(alloc), .current_chunk_len = {}, - .current_chunk_parts = g.current_chunk_parts.deinit(alloc), + .current_chunk_parts = { + if (comptime side == .server) { + for (g.current_chunk_parts.items) |part| part.deinit(); + } + g.current_chunk_parts.deinit(alloc); + }, .current_css_files = if (comptime side == .client) g.current_css_files.deinit(alloc), .current_chunk_source_maps = if (side == .server) { for (g.current_chunk_source_maps.items) |source_map| { @@ -567,7 +571,7 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { var escaped_source = source_map.escaped_source; if (chunk.buffer.len() > 0) { break :blk .{ .some = PackedMap.newNonEmpty( - chunk, + &chunk, escaped_source.take().?, ) }; } @@ -653,7 +657,13 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { } } if (content == .js) { - try g.current_chunk_parts.append(dev.allocator(), content.js.code); + try g.current_chunk_parts.append( + dev.allocator(), + bun.ptr.ScopedOwned([]const u8).fromRawOwned( + content.js.code, + if (comptime bun.Environment.enableAllocScopes) dev.allocation_scope else null, + ), + ); g.current_chunk_len += content.js.code.len; // Track the file index for this chunk @@ -663,17 +673,16 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { // server chunks, but not 100% sure const should_immediately_free_sourcemap = false; if (should_immediately_free_sourcemap) { - if (content.js.source_map) |source_map| { - var take = source_map.chunk.buffer; - take.deinit(); - if (source_map.escaped_source) |escaped_source| { - bun.default_allocator.free(escaped_source); - } - } + @compileError("Not implemented the codepath to free the sourcemap"); } else { if (content.js.source_map) |*source_map| append_empty: { + defer source_map.chunk.deinit(); + defer source_map.escaped_source.deinit(); const escaped_source = source_map.escaped_source.take() orelse break :append_empty; - const packed_map = PackedMap.newNonEmpty(source_map.chunk, escaped_source); + const packed_map = PackedMap.newNonEmpty( + &source_map.chunk, + escaped_source, + ); try g.current_chunk_source_maps.append(dev.allocator(), .{ .some = packed_map, }); @@ -1648,14 +1657,18 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { pub fn reset(g: *Self) void { g.owner().graph_safety_lock.assertLocked(); g.current_chunk_len = 0; - g.current_chunk_parts.clearRetainingCapacity(); if (comptime side == .client) { g.current_css_files.clearRetainingCapacity(); } else if (comptime side == .server) { + for (g.current_chunk_parts.items) |part| part.deinit(); + + for (g.current_chunk_source_maps.items) |sourcemap| sourcemap.deinit(); g.current_chunk_source_maps.clearRetainingCapacity(); g.current_chunk_file_indices.clearRetainingCapacity(); } + + g.current_chunk_parts.clearRetainingCapacity(); } const TakeJSBundleOptions = switch (side) { @@ -1784,7 +1797,7 @@ pub fn IncrementalGraph(comptime side: bake.Side) type { // entry is an index into files .client => files[entry.get()].unpack().jsCode().?, // entry is the '[]const u8' itself - .server => entry, + .server => entry.get(), }); } list.appendSliceAssumeCapacity(end); diff --git a/src/bake/DevServer/PackedMap.zig b/src/bake/DevServer/PackedMap.zig index 1626f3cf25da..72b39b4e8a1f 100644 --- a/src/bake/DevServer/PackedMap.zig +++ b/src/bake/DevServer/PackedMap.zig @@ -19,8 +19,8 @@ end_state: struct { original_column: i32, }, -pub fn newNonEmpty(chunk: SourceMap.Chunk, escaped_source: Owned([]u8)) bun.ptr.Shared(*Self) { - var buffer = chunk.buffer; +pub fn newNonEmpty(chunk: *SourceMap.Chunk, escaped_source: Owned([]u8)) bun.ptr.Shared(*Self) { + var buffer = &chunk.buffer; assert(!buffer.isEmpty()); return .new(.{ .vlq_ = .fromDynamic(buffer.toDynamicOwned()), diff --git a/src/bundler/linker_context/generateCompileResultForJSChunk.zig b/src/bundler/linker_context/generateCompileResultForJSChunk.zig index 22e294746e87..d70e2fd291f1 100644 --- a/src/bundler/linker_context/generateCompileResultForJSChunk.zig +++ b/src/bundler/linker_context/generateCompileResultForJSChunk.zig @@ -28,8 +28,8 @@ fn generateCompileResultForJSChunkImpl(worker: *ThreadPool.Worker, c: *LinkerCon const trace = bun.perf.trace("Bundler.generateCodeForFileInChunkJS"); defer trace.end(); - // Client bundles for Bake must be globally allocated, - // as it must outlive the bundle task. + // Client and server bundles for Bake must be globally allocated, as they + // must outlive the bundle task. const allocator = blk: { const dev = c.dev_server orelse break :blk default_allocator; break :blk dev.allocator(); diff --git a/src/js_printer.zig b/src/js_printer.zig index 14fce20cfbc6..8757b795f03e 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -6007,11 +6007,11 @@ pub fn printWithWriterAndPlatform( break :brk chunk; } else null; - var buffer = printer.writer.takeBuffer(); + var buffer: MutableString = printer.writer.takeBuffer(); return .{ .result = .{ - .code = buffer.toOwnedSlice(), + .code = buffer.takeSlice(), .source_map = source_map, }, }; diff --git a/src/string/MutableString.zig b/src/string/MutableString.zig index cae69222cf2a..c6f44bbee385 100644 --- a/src/string/MutableString.zig +++ b/src/string/MutableString.zig @@ -240,6 +240,12 @@ pub inline fn lenI(self: *MutableString) i32 { return @as(i32, @intCast(self.list.items.len)); } +pub fn takeSlice(self: *MutableString) []u8 { + const out = self.list.items; + self.list = .{}; + return out; +} + pub fn toOwnedSlice(self: *MutableString) []u8 { return bun.handleOom(self.list.toOwnedSlice(self.allocator)); } From c5fc729fde8c89114a4ae12fc06ff1574cb3e6d9 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:18:14 -0700 Subject: [PATCH 044/130] fix tests --- src/bake/bun-framework-react/server.tsx | 5 +- src/bake/hmr-runtime-server.ts | 9 +++- src/bun.js/webcore/Response.zig | 13 +++-- test/bun.lock | 63 +++++++++++++++++++++++++ test/package.json | 1 + 5 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index f70596c74333..2a354d2b275b 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -198,10 +198,13 @@ export async function render( const opts = als?.getStore()?.responseOptions ?? { headers: {} }; const { headers, ...response_options } = opts; + const cookies = meta.pageModule.mode === "ssr" ? { "Set-Cookie": request.cookies.toSetCookieHeaders() } : {}; + return new Response(result, { headers: { "Content-Type": "text/html; charset=utf8", - "Set-Cookie": request.cookies.toSetCookieHeaders(), + // TODO: merge cookies and cookies inside of headers + ...cookies, ...headers, }, ...response_options, diff --git a/src/bake/hmr-runtime-server.ts b/src/bake/hmr-runtime-server.ts index 3c12e6280196..72411530dfbd 100644 --- a/src/bake/hmr-runtime-server.ts +++ b/src/bake/hmr-runtime-server.ts @@ -100,10 +100,17 @@ server_exports = { const [pageModule, ...layouts] = await Promise.all(routeModules.map(loadExports)); + const mode = pageModule.mode; // Add cookies to request when mode is 'ssr' let requestWithCookies = req; - if (pageModule.mode === "ssr") { + if (mode === "ssr") { requestWithCookies.cookies = req.cookies || new Bun.CookieMap(req.headers.get("Cookie") || ""); + } else { + Object.defineProperty(requestWithCookies, "cookies", { + get: () => { + throw new Error('"request.cookies" is only available when "export const mode = \'ssr\'"'); + }, + }); } let storeValue: RequestContext = { diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 1c11f6c4c122..3a06ff59943e 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -529,7 +529,7 @@ pub fn constructRedirect( // Transform the Response to act as a React element with special redirect handling // Pass "redirect" as the third parameter to indicate this is a redirect const redirect_flag = ZigString.init("redirect").toJS(globalThis); - try checkStreamingDisabled(globalThis, async_local_storage, "Response.redirect"); + try assertStreamingDisabled(globalThis, async_local_storage, "Response.redirect"); try JSValue.transformToReactElementWithOptions(response_js, redirect_marker, redirect_flag, globalThis); } @@ -548,7 +548,7 @@ pub fn constructRender( return globalThis.throwInvalidArguments("Response.render() is only available in the Bun dev server", .{}); }; - try checkStreamingDisabled(globalThis, async_local_storage, "Response.render"); + try assertStreamingDisabled(globalThis, async_local_storage, "Response.render"); // Validate arguments if (arguments.len < 1) { @@ -668,6 +668,11 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, t const arg = arguments[0]; // Check if it's a JSX element (object with $$typeof) if (try arg.isJSXElement(globalThis)) { + const vm = globalThis.bunVM(); + if (vm.dev_server_async_local_storage.get()) |async_local_storage| { + try assertStreamingDisabled(globalThis, async_local_storage, "new Response(, { ... })"); + } + // Pass the response options (arguments[1]) to transformToReactElement // so it can store them for later use when the component is rendered const responseOptions = if (arguments[1].isObject()) arguments[1] else .js_undefined; @@ -858,13 +863,13 @@ inline fn emptyWithStatus(_: *jsc.JSGlobalObject, status: u16) Response { }); } -fn checkStreamingDisabled(globalThis: *jsc.JSGlobalObject, async_local_storage: JSValue, desired_function: []const u8) bun.JSError!void { +fn assertStreamingDisabled(globalThis: *jsc.JSGlobalObject, async_local_storage: JSValue, display_function: []const u8) bun.JSError!void { if (async_local_storage.isEmptyOrUndefinedOrNull() or !async_local_storage.isObject()) return globalThis.throwInvalidArguments("store value must be an object", .{}); const getStoreFn = (try async_local_storage.getPropertyValue(globalThis, "getStore")) orelse return globalThis.throwInvalidArguments("store value must have a \"getStore\" field", .{}); const store_value = try getStoreFn.call(globalThis, async_local_storage, &.{}); const streaming_val = (try store_value.getPropertyValue(globalThis, "streaming")) orelse return globalThis.throwInvalidArguments("store value must have a \"streaming\" field", .{}); if (!streaming_val.isBoolean()) return globalThis.throwInvalidArguments("\"streaming\" fied must be a boolean", .{}); - if (streaming_val.asBoolean()) return globalThis.throwInvalidArguments("\"{s}\" is not available when `export const streaming = true`", .{desired_function}); + if (streaming_val.asBoolean()) return globalThis.throwInvalidArguments("\"{s}\" is not available when `export const streaming = true`", .{display_function}); } /// https://developer.mozilla.org/en-US/docs/Web/API/Headers diff --git a/test/bun.lock b/test/bun.lock index d5dc61d49f02..d64ec1e96789 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -63,6 +63,7 @@ "node-gyp": "10.0.1", "nodemailer": "6.9.3", "p-queue": "8.1.0", + "peechy": "^0.4.310", "pg": "8.11.1", "pg-connection-string": "2.6.1", "pg-gateway": "0.3.0-beta.4", @@ -1020,10 +1021,14 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], "caniuse-lite": ["caniuse-lite@1.0.30001636", "", {}, "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg=="], + "capital-case": ["capital-case@1.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A=="], + "case": ["case@1.6.3", "", {}, "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ=="], "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], @@ -1034,6 +1039,8 @@ "chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + "change-case": ["change-case@4.1.2", "", { "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", "constant-case": "^3.0.4", "dot-case": "^3.0.4", "header-case": "^2.0.4", "no-case": "^3.0.4", "param-case": "^3.0.4", "pascal-case": "^3.1.2", "path-case": "^3.0.4", "sentence-case": "^3.0.4", "snake-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], @@ -1110,6 +1117,8 @@ "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], + "constant-case": ["constant-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case": "^2.0.2" } }, "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ=="], + "constructs": ["constructs@10.3.0", "", {}, "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ=="], "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], @@ -1204,6 +1213,8 @@ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -1474,6 +1485,8 @@ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "header-case": ["header-case@2.0.4", "", { "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" } }, "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q=="], + "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], @@ -1720,6 +1733,8 @@ "lowdb": ["lowdb@1.0.0", "", { "dependencies": { "graceful-fs": "^4.1.3", "is-promise": "^2.1.0", "lodash": "4", "pify": "^3.0.0", "steno": "^0.4.1" } }, "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ=="], + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + "lru-cache": ["lru-cache@8.0.5", "", {}, "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -1904,6 +1919,8 @@ "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], "node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], @@ -1984,6 +2001,8 @@ "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="], @@ -2002,6 +2021,10 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + + "path-case": ["path-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -2018,6 +2041,8 @@ "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + "peechy": ["peechy@0.4.310", "", { "dependencies": { "change-case": "^4.1.2" }, "bin": { "peechy": "cli.js" } }, "sha512-PoCEQKanz2KaUmH9bhhWhy2NH+bsDM37xiPYPfJOD/xvQnXelv4IqO1qXFDle31drsQLiSbwszpu3jaeArg3AQ=="], + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], "peek-stream": ["peek-stream@1.1.3", "", { "dependencies": { "buffer-from": "^1.0.0", "duplexify": "^3.5.0", "through2": "^2.0.3" } }, "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA=="], @@ -2270,6 +2295,8 @@ "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + "sentence-case": ["sentence-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg=="], + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], @@ -2318,6 +2345,8 @@ "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + "socket.io": ["socket.io@4.7.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.5.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw=="], "socket.io-adapter": ["socket.io-adapter@2.5.5", "", { "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg=="], @@ -2558,6 +2587,10 @@ "update-browserslist-db": ["update-browserslist-db@1.0.16", "", { "dependencies": { "escalade": "^3.1.2", "picocolors": "^1.0.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ=="], + "upper-case": ["upper-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg=="], + + "upper-case-first": ["upper-case-first@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], @@ -2960,8 +2993,14 @@ "cacache/minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + "camel-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "capital-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "change-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "chromium-bidi/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -2984,6 +3023,8 @@ "concordance/semver": ["semver@7.6.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="], + "constant-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], @@ -2992,6 +3033,8 @@ "debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "dot-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "duckdb/node-gyp": ["node-gyp@9.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ=="], "duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -3060,6 +3103,8 @@ "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + "header-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "http-proxy-agent/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="], "https-proxy-agent/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="], @@ -3086,6 +3131,8 @@ "jsonwebtoken/semver": ["semver@7.6.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="], + "lower-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "make-fetch-happen/proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], "micromark/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], @@ -3124,6 +3171,8 @@ "nise/path-to-regexp": ["path-to-regexp@1.8.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA=="], + "no-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "node-gyp/semver": ["semver@7.6.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="], @@ -3134,10 +3183,16 @@ "pac-proxy-agent/socks-proxy-agent": ["socks-proxy-agent@8.0.4", "", { "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw=="], + "param-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], + "pascal-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "path-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "path-scurry/lru-cache": ["lru-cache@10.2.2", "", {}, "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ=="], "peek-stream/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="], @@ -3206,12 +3261,16 @@ "send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "sentence-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "serve-static/send": ["send@0.18.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg=="], "sharp/semver": ["semver@7.6.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="], "sinon/diff": ["diff@3.5.0", "", {}, "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="], + "snake-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "socket.io/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="], "socket.io-adapter/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="], @@ -3290,6 +3349,10 @@ "unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "upper-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "upper-case-first/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "v8-heapsnapshot/@types/node": ["@types/node@18.19.69", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ECPdY1nlaiO/Y6GUnwgtAAhLNaQ53AyIVz+eILxpEo5OvuqE6yWkqWBIb5dU0DqhKQtMeny+FBD3PK6lm7L5xQ=="], "verdaccio/@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-goDVOH4e8xMUxjHybJpi5HwIecVFqzJ9jeNFrRUgtUUn0PtFuNMHgxOeqDKRVboZhc5HK90yed8URK/1O6VsUw=="], diff --git a/test/package.json b/test/package.json index 8b302e35dcaf..8d448facf5fa 100644 --- a/test/package.json +++ b/test/package.json @@ -68,6 +68,7 @@ "node-gyp": "10.0.1", "nodemailer": "6.9.3", "p-queue": "8.1.0", + "peechy": "^0.4.310", "pg": "8.11.1", "pg-connection-string": "2.6.1", "pg-gateway": "0.3.0-beta.4", From 21266f526310f8203053edb617164986cd9acd87 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:09:02 -0700 Subject: [PATCH 045/130] fix compile error --- src/bake/DevServer.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index ce48676d4446..2e2cb92b54b6 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2378,7 +2378,7 @@ pub fn finalizeBundle( const server_modules = if (bun.take(&source_map_json)) |json| blk: { // This memory will be owned by the `DevServerSourceProvider` in C++ // from here on out - dev.allocation_scope.leakSlice(json); + if (comptime AllocationScope.enabled) dev.allocation_scope.leakSlice(json); break :blk c.BakeLoadServerHmrPatchWithSourceMap( @ptrCast(dev.vm.global), From aa33b11a7ad33ba6aa35e71780747b44f885bbd2 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:18:49 -0700 Subject: [PATCH 046/130] Forgot to commit test file --- test/bake/dev/response-error-handling.test.ts | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 test/bake/dev/response-error-handling.test.ts diff --git a/test/bake/dev/response-error-handling.test.ts b/test/bake/dev/response-error-handling.test.ts new file mode 100644 index 000000000000..c3b0c298a262 --- /dev/null +++ b/test/bake/dev/response-error-handling.test.ts @@ -0,0 +1,226 @@ +import { decodeFallbackMessageContainer } from "../../../src/api/schema"; +import { devTest } from "../bake-harness"; +import { expect } from "bun:test"; +import { ByteBuffer } from "peechy"; + +function getFallbackMessageContainer(text: string) { + const regex = /\s*\