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] 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 f6a44973e0..7d9b1dfeff 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 07ec7b442c..5b8b028010 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 caf3a8cd7a..b7adac66e0 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 1ccf68d282..d4e4502d4d 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 92a00c9916..79edbe5aa6 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 3b6c914eb2..b467e24d5e 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 d20a431ab8..1476a3a90b 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 f12f7fb846..d7dd697247 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 f9ddef9a06..5ad525b275 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 0000000000..89c12fe757 --- /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; + }; +}