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()) {