This commit is contained in:
Zack Radisic
2025-08-18 22:23:30 -07:00
parent 20e4c094ac
commit cfdeb42023
7 changed files with 194 additions and 43 deletions

View File

@@ -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

View File

@@ -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 <link> 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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -5884,12 +5884,152 @@ restart:
JSC__JSValue__forEachPropertyImpl<false>(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<true>(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();

View File

@@ -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(<jsx> ... </jsx>, { ... }`
// 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()) {