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] 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 3fa14d4497..07ec7b442c 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 f8b090353a..789e1c6068 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 30db947fee..d9a1e3acfc 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 7cbada9a94..64a38b53ff 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 0b9a4df64a..92a00c9916 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 9205857e71..2c38dd25a2 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5884,12 +5884,152 @@ restart: 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 2453ca8825..2230c21833 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()) {