Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,15 +363,6 @@ impl JSValue {
pub fn is_error(self) -> bool {
self.is_cell() && self.js_type() == JSType::ErrorInstance
}
/// `JSValue.isJSXElement(globalObject)`. Checks via the
/// global's `Symbol.for("react.element")` / `Symbol.for("react.transitional.element")`
/// for `$$typeof`; may invoke a user getter and throw.
pub fn is_jsx_element(self, global: &JSGlobalObject) -> JsResult<bool> {
unsafe extern "C" {
safe fn JSC__JSValue__isJSXElement(this: JSValue, global: &JSGlobalObject) -> bool;
}
host_fn::from_js_host_call_generic(global, || JSC__JSValue__isJSXElement(self, global))
}
/// `JSValue.isAggregateError(globalObject)`.
#[inline]
pub fn is_aggregate_error(self, global: &JSGlobalObject) -> bool {
Expand Down
81 changes: 34 additions & 47 deletions src/jsc/bindings/JSBakeResponse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@

namespace Bun {

extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructForSSR(JSC::JSGlobalObject*, JSC::CallFrame*, int*, JSC::EncodedJSValue);
extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructForSSR(JSC::JSGlobalObject*, JSC::CallFrame*, bool isJSX, JSC::EncodedJSValue);
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructError(JSC::JSGlobalObject*, JSC::CallFrame*);
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructJSON(JSC::JSGlobalObject*, JSC::CallFrame*);
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructRender(JSC::JSGlobalObject*, JSC::CallFrame*);
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructRedirect(JSC::JSGlobalObject*, JSC::CallFrame*);
extern JSC_CALLCONV size_t Response__estimatedSize(void* ptr);

bool isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject)
static bool isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject)
{

auto* zigGlobal = static_cast<Zig::GlobalObject*>(globalObject);
Expand Down Expand Up @@ -61,11 +61,6 @@ bool isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObjec
return false;
}

extern "C" bool JSC__JSValue__isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject)
{
return isJSXElement(JSValue0, globalObject);
}

extern JSC_CALLCONV JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponse__createForSSR(Zig::GlobalObject* globalObject, void* ptr, uint8_t kind)
{
Structure* structure = globalObject->bakeAdditions().JSBakeResponseStructure(globalObject);
Expand Down Expand Up @@ -159,6 +154,36 @@ void JSBakeResponse::visitChildrenImpl(JSCell* cell, Visitor& visitor)

DEFINE_VISIT_CHILDREN(JSBakeResponse);

static ALWAYS_INLINE JSC::EncodedJSValue constructBakeResponse(Zig::GlobalObject* globalObject, JSC::CallFrame* callFrame, JSC::Structure* structure)
{
JSC::VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

JSValue body = callFrame->argument(0);
bool isJSX = isJSXElement(JSValue::encode(body), globalObject);
RETURN_IF_EXCEPTION(scope, {});

JSBakeResponse* instance = JSBakeResponse::create(vm, globalObject, structure, nullptr);

void* ptr = BakeResponseClass__constructForSSR(globalObject, callFrame, isJSX, JSValue::encode(instance));
if (scope.exception()) [[unlikely]] {
ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new Response() allocated memory without checking for exceptions.");
return JSValue::encode(JSC::jsUndefined());
}

instance->m_ctx = ptr;

if (isJSX) {
instance->wrapInnerComponent(globalObject, vm, body, callFrame->argument(1));
RETURN_IF_EXCEPTION(scope, {});
}

auto size = Response__estimatedSize(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);

RELEASE_AND_RETURN(scope, JSValue::encode(instance));
}

class JSBakeResponseConstructor final : public JSC::InternalFunction {
public:
using Base = JSC::InternalFunction;
Expand Down Expand Up @@ -192,51 +217,13 @@ class JSBakeResponseConstructor final : public JSC::InternalFunction {
RETURN_IF_EXCEPTION(scope, {});
}

JSBakeResponse* instance = JSBakeResponse::create(vm, globalObject, structure, nullptr);

int arg_was_jsx = 0;
void* ptr = BakeResponseClass__constructForSSR(globalObject, callFrame, &arg_was_jsx, JSValue::encode(instance));
if (scope.exception()) [[unlikely]] {
ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new Response() allocated memory without checking for exceptions.");
return JSValue::encode(JSC::jsUndefined());
}

instance->m_ctx = ptr;

if (arg_was_jsx == 1 && callFrame->argumentCount() > 0) {
JSValue arg = callFrame->argument(0);
JSValue responseOptions = callFrame->argumentCount() > 1 ? callFrame->argument(1) : JSC::jsUndefined();
instance->wrapInnerComponent(globalObject, vm, arg, responseOptions);
}

auto size = Response__estimatedSize(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);

auto value = JSValue::encode(instance);
RELEASE_AND_RETURN(scope, value);
RELEASE_AND_RETURN(scope, constructBakeResponse(globalObject, callFrame, structure));
}

static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame)
{
Zig::GlobalObject* globalObject = static_cast<Zig::GlobalObject*>(lexicalGlobalObject);
JSC::VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

Structure* structure = globalObject->bakeAdditions().JSBakeResponseStructure(globalObject);
JSBakeResponse* instance = JSBakeResponse::create(vm, globalObject, structure, nullptr);

void* ptr = BakeResponseClass__constructForSSR(globalObject, callFrame, nullptr, JSValue::encode(instance));
if (scope.exception()) [[unlikely]] {
ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new Response() allocated memory without checking for exceptions.");
return JSValue::encode(JSC::jsUndefined());
}

instance->m_ctx = ptr;

auto size = Response__estimatedSize(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);

RELEASE_AND_RETURN(scope, JSValue::encode(instance));
return constructBakeResponse(globalObject, callFrame, globalObject->bakeAdditions().JSBakeResponseStructure(globalObject));
}

template<typename CellType, JSC::SubspaceAccess>
Expand Down
32 changes: 12 additions & 20 deletions src/runtime/webcore/BakeResponse.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use core::ffi::{c_int, c_void};
use core::ffi::c_void;

use crate::webcore::Response;
use crate::webcore::response::{HeadersRef, Init};
Expand Down Expand Up @@ -61,12 +61,10 @@ bun_jsc::jsc_host_abi! {
pub(crate) unsafe fn BakeResponseClass__constructForSSR(
global_object: &JSGlobalObject,
call_frame: &CallFrame,
bake_ssr_has_jsx: *mut c_int,
is_jsx: bool,
js_this: JSValue,
) -> *mut c_void {
// SAFETY: caller (C++) guarantees `bake_ssr_has_jsx` is a valid, exclusive out-pointer for the call.
let bake_ssr_has_jsx = unsafe { &mut *bake_ssr_has_jsx };
match constructor(global_object, call_frame, bake_ssr_has_jsx, js_this) {
match constructor(global_object, call_frame, is_jsx, js_this) {
Ok(response) => response.cast::<c_void>(),
Err(JsError::Thrown) => core::ptr::null_mut(),
Err(JsError::Terminated) => {
Expand All @@ -85,25 +83,19 @@ bun_jsc::jsc_host_abi! {
fn constructor(
global_this: &JSGlobalObject,
callframe: &CallFrame,
bake_ssr_has_jsx: &mut c_int,
is_jsx: bool,
js_this: JSValue,
) -> JsResult<*mut Response> {
let arguments: [JSValue; 2] = callframe.arguments_as_array::<2>();

// Allow `return new Response(<jsx> ... </jsx>, { ... }`
// inside of a react component
if !arguments[0].is_undefined_or_null() && arguments[0].is_object() {
*bake_ssr_has_jsx = 0;
if arguments[0].is_jsx_element(global_this)? {
let vm = global_this.bun_vm().as_mut();
if let Some(async_local_storage) = vm.get_dev_server_async_local_storage()? {
assert_streaming_disabled(
global_this,
async_local_storage,
b"new Response(<jsx />, { ... })",
)?;
}
*bake_ssr_has_jsx = 1;
if is_jsx {
let vm = global_this.bun_vm().as_mut();
if let Some(async_local_storage) = vm.get_dev_server_async_local_storage()? {
assert_streaming_disabled(
global_this,
async_local_storage,
b"new Response(<jsx />, { ... })",
)?;
}
}

Expand Down
53 changes: 53 additions & 0 deletions test/bake/dev/react-response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,32 @@ devTest("new Response with JSX and custom headers", {
},
});

devTest("Response with JSX called without new", {
framework: "react",
files: {
"pages/index.tsx": `
export const streaming = false;
export const mode = "ssr";

export default async function IndexPage() {
return Response(<h1>Hello World</h1>, {
status: 201,
headers: {
"X-Custom-Header": "test-value"
}
});
}
`,
},
async test(dev) {
const response = await dev.fetch("/");
expect(response.status).toBe(201);
expect(response.headers.get("X-Custom-Header")).toBe("test-value");
const text = await response.text();
expect(text).toContain("<h1>Hello World</h1>");
},
});

// Test case 5: new Response with JSX when streaming = true (should error)
devTest("new Response with JSX when streaming = true should error", {
framework: "react",
Expand Down Expand Up @@ -133,6 +159,33 @@ devTest("new Response with JSX when streaming = true should error", {
},
});

devTest("Response with JSX called without new when streaming = true should error", {
framework: "react",
files: {
"pages/index.tsx": `
export const streaming = true;
export const mode = "ssr";

export default async function IndexPage() {
return Response(<h1>Hello World</h1>, {
status: 201,
headers: {
"X-Custom-Header": "test-value"
}
});
}
`,
},
async test(dev) {
const response = await dev.fetch("/");
const text = await response.text();
const fallback_message_container = getFallbackMessageContainer(text);
expect(fallback_message_container.problems?.exceptions[0].message).toContain(
'"new Response(<jsx />, { ... })" is not available when `export const streaming = true`',
);
},
});

// Test case 6: Response.redirect() - content matching
devTest("Response.redirect() - content matching", {
framework: "react",
Expand Down
38 changes: 38 additions & 0 deletions test/bake/dev/response-to-bake-response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,42 @@ describe.concurrent('the Response of "bun:app" outside the dev server', () => {
`);
expect(result).toEqual({ stdout: "201 true\n", stderr: "", exitCode: 0 });
});

test("Response() without new returns a Response", async () => {
const result = await run(`
for (const body of ["text", { a: 1 }]) {
const response = Response(body, { status: 201 });
console.log(response instanceof Response, response.status, await response.text());
}
`);
expect(result).toEqual({ stdout: "true 201 text\ntrue 201 [object Object]\n", stderr: "", exitCode: 0 });
});

test("Response(<jsx />) without new returns the element as a component", async () => {
const result = await run(`
const element = { $$typeof: Symbol.for("react.transitional.element"), type: "div", key: null, props: {} };
const response = Response(element, { status: 201 });
console.log(response instanceof Response, response.status, response.type() === element);
`);
expect(result).toEqual({ stdout: "true 201 true\n", stderr: "", exitCode: 0 });
});

test("an error from the $$typeof getter of the body propagates", async () => {
const result = await run(`
const body = {
get $$typeof() {
throw new Error("from the getter");
},
};
for (const construct of [() => new Response(body), () => Response(body)]) {
try {
construct();
console.log("no error");
} catch (error) {
console.log(error.message);
}
}
`);
expect(result).toEqual({ stdout: "from the getter\nfrom the getter\n", stderr: "", exitCode: 0 });
});
});
Loading