mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
### What does this PR do?
Fixes a crash related to the dev server overwriting the uws user context
pointer when setting abort callback.
Adds support for `return new Response(<jsx />, { ... })` and `return
Response.render(...)` and `return Response.redirect(...)`:
- Created a `SSRResponse` class to handle this (see
`JSBakeResponse.{h,cpp}`)
- `SSRResponse` is designed to "fake" being a React component
- This is done in JSBakeResponse::create inside of
src/bun.js/bindings/JSBakeResponse.cpp
- And `src/js/builtins/BakeSSRResponse.ts` defines a `wrapComponent`
function which wraps
the passed in component (when doing `new Response(<jsx />, ...)`). It
does
this to throw an error (in redirect()/render() case) or return the
component.
- Created a `BakeAdditionsToGlobal` struct which contains some
properties
needed for this
- Added some of the properties we need to fake to BunBuiltinNames.h
(e.g.
`$$typeof`), the rationale behind this is that we couldn't use
`structure->addPropertyTransition` because JSBakeResponse is not a final
JSObject.
- When bake and server-side, bundler rewrites `Response ->
Bun.SSRResponse` (see `src/ast/P.zig` and `src/ast/visitExpr.zig`)
- Created a new WebCore body variant (`Render: struct { path: []const u8
}`)
- Created when `return Response.render(...)`
- When handled, it re-invokes dev server to render the new path
Enables server-side sourcemaps for the dev server:
- New source providers for server-side:
(`DevServerSourceProvider.{h,cpp}`)
- IncrementalGraph and SourceMapStore are updated to support this
There are numerous other stuff:
- allow `app` configuration from Bun.serve(...)
- fix errors stopping dev server
- fix use after free related to in
RequestContext.finishRunningErrorHandler
- Request.cookies
- Make `"use client";` components work
- Fix some bugs using `require(...)` in dev server
- Fix catch-all routes not working in the dev server
- Updates `findSourceMappingURL(...)` to use `std.mem.lastIndexOf(...)`
because
the sourcemap that should be used is the last one anyway
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
248 lines
8.5 KiB
TypeScript
248 lines
8.5 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
|
import path from "node:path";
|
|
|
|
test("Response -> import { Response } from 'bun:app' transform in server components", async () => {
|
|
const dir = tempDirWithFiles("response-transform", {
|
|
"server-component.js": `
|
|
export const mode = "ssr";
|
|
export const streaming = false;
|
|
|
|
export default async function ServerPage({ request }) {
|
|
// Response should be imported from 'bun:app'
|
|
const response1 = new Response("Hello", { status: 200 });
|
|
|
|
// Response.redirect should work with imported Response
|
|
if (!request.userId) {
|
|
return Response.redirect("/login");
|
|
}
|
|
|
|
// Response.render should work with imported Response
|
|
if (request.page === "404") {
|
|
return Response.render("/404");
|
|
}
|
|
|
|
// Response in string content should also be transformed
|
|
return new Response("Hello from server", { status: 200 });
|
|
}
|
|
`,
|
|
"client-component.js": `
|
|
"use client";
|
|
|
|
export default function ClientPage() {
|
|
// Response should NOT be transformed in client components
|
|
const response = new Response("Client", { status: 200 });
|
|
return "Client Component";
|
|
}
|
|
`,
|
|
});
|
|
|
|
// Build with server components enabled for server-side
|
|
const serverResult =
|
|
await Bun.$`${bunExe()} build ${path.join(dir, "server-component.js")} --target=bun --server-components`
|
|
.env(bunEnv)
|
|
.text();
|
|
|
|
// Check that Response import was added from 'bun:app'
|
|
expect(serverResult).toContain('import { Response } from "bun:app"');
|
|
// Response is transformed to import_bun_app.Response
|
|
expect(serverResult).toContain("new import_bun_app.Response");
|
|
expect(serverResult).toContain("import_bun_app.Response.redirect");
|
|
expect(serverResult).toContain("import_bun_app.Response.render");
|
|
|
|
// Build client component (should not have the transform)
|
|
const clientResult = await Bun.$`${bunExe()} build ${path.join(dir, "client-component.js")} --target=browser`
|
|
.env(bunEnv)
|
|
.text();
|
|
|
|
// Check that Response import was NOT added in client component
|
|
expect(clientResult).not.toContain('import { Response } from "bun:app"');
|
|
expect(clientResult).toContain("new Response");
|
|
});
|
|
|
|
test("Response import is added for global Response in various contexts", async () => {
|
|
const dir = tempDirWithFiles("response-contexts", {
|
|
"server.js": `
|
|
export const mode = "ssr";
|
|
|
|
export default function Page() {
|
|
// As constructor
|
|
const r1 = new Response();
|
|
|
|
// As type check
|
|
if (obj instanceof Response) {
|
|
console.log("is response");
|
|
}
|
|
|
|
// As property access
|
|
const status = Response.prototype.status;
|
|
|
|
// As method call
|
|
const json = Response.json({ data: true });
|
|
|
|
// In destructuring (should not transform if it's a binding)
|
|
const { Response: LocalResponse } = imports;
|
|
|
|
return r1;
|
|
}
|
|
`,
|
|
});
|
|
|
|
const result = await Bun.$`${bunExe()} build ${path.join(dir, "server.js")} --target=bun --server-components`
|
|
.env(bunEnv)
|
|
.text();
|
|
|
|
// Check that import was added
|
|
expect(result).toContain('import { Response } from "bun:app"');
|
|
// Response is transformed to import_bun_app.Response
|
|
expect(result).toContain("new import_bun_app.Response");
|
|
expect(result).toContain("instanceof import_bun_app.Response");
|
|
expect(result).toContain("import_bun_app.Response.prototype.status");
|
|
expect(result).toContain("import_bun_app.Response.json");
|
|
});
|
|
|
|
test("Response import is not added when Response is already imported or shadowed", async () => {
|
|
const dir = tempDirWithFiles("response-shadowing", {
|
|
"server.js": `
|
|
export const mode = "ssr";
|
|
|
|
// Import shadowing Response
|
|
import { Response } from "./custom-response";
|
|
|
|
export default function Page() {
|
|
// Should use the imported Response, not transform to Bun.SSRResponse
|
|
const r = new Response();
|
|
return r;
|
|
}
|
|
`,
|
|
"server2.js": `
|
|
export const mode = "ssr";
|
|
|
|
export default function Page() {
|
|
// Local variable shadowing Response
|
|
const Response = CustomResponse;
|
|
|
|
// Should use the local Response, not transform
|
|
const r = new Response();
|
|
return r;
|
|
}
|
|
|
|
export function inner() {
|
|
// But here it should transform since it's not shadowed
|
|
return new Response();
|
|
}
|
|
`,
|
|
"custom-response.ts": `
|
|
export class Response {
|
|
constructor() {
|
|
this.custom = true;
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
const result1 = await Bun.$`${bunExe()} build ${path.join(dir, "server.js")} --target=bun --server-components`
|
|
.env(bunEnv)
|
|
.text();
|
|
|
|
// When Response is already imported from another source, no bun:app import should be added
|
|
expect(result1).not.toContain('import { Response } from "bun:app"');
|
|
|
|
const result2 = await Bun.$`${bunExe()} build ${path.join(dir, "server2.js")} --target=bun --server-components`
|
|
.env(bunEnv)
|
|
.text();
|
|
|
|
// Should preserve local variable
|
|
expect(result2).toContain("return new CustomResponse");
|
|
// The file should have the import added for the inner function
|
|
expect(result2).toContain('import { Response } from "bun:app"');
|
|
});
|
|
|
|
test("Response import is NOT added in client components", async () => {
|
|
const dir = tempDirWithFiles("client-no-transform", {
|
|
"client-component.js": `
|
|
"use client";
|
|
|
|
// Response should NOT be transformed to Bun.SSRResponse in client components
|
|
const response = new Response("Client data", {
|
|
status: 200,
|
|
headers: { "Content-Type": "text/plain" }
|
|
});
|
|
|
|
// Response.json should remain Response.json
|
|
const jsonResponse = Response.json({ data: "test" });
|
|
|
|
// instanceof Response should remain as-is
|
|
if (response instanceof Response) {
|
|
console.log("Is a Response");
|
|
}
|
|
|
|
// Response.redirect should remain Response.redirect
|
|
const redirect = Response.redirect("/new-page");
|
|
|
|
export default response;
|
|
`,
|
|
"server-component.js": `
|
|
export const mode = "ssr";
|
|
|
|
// Response should be imported from 'bun:app' in server component
|
|
const serverResponse = new Response("Server", { status: 200 });
|
|
|
|
// Response static methods should work with imported Response
|
|
const json = Response.json({ server: true });
|
|
|
|
export default serverResponse;
|
|
`,
|
|
});
|
|
|
|
// Test 1: Client component - Response should NOT be transformed
|
|
const clientResult = await Bun.$`${bunExe()} build ${path.join(dir, "client-component.js")} --target=browser`
|
|
.env(bunEnv as any)
|
|
.text();
|
|
|
|
// Verify Response import is NOT added in client components
|
|
expect(clientResult).not.toContain('import { Response } from "bun:app"');
|
|
expect(clientResult).toContain("new Response");
|
|
expect(clientResult).toContain("Response.json");
|
|
expect(clientResult).toContain("instanceof Response");
|
|
expect(clientResult).toContain("Response.redirect");
|
|
|
|
// Test 2: Server component - Response SHOULD be transformed
|
|
const serverResult =
|
|
await Bun.$`${bunExe()} build ${path.join(dir, "server-component.js")} --target=bun --server-components`
|
|
.env(bunEnv as any)
|
|
.text();
|
|
|
|
// Server component should have import from bun:app
|
|
expect(serverResult).toContain('import { Response } from "bun:app"');
|
|
expect(serverResult).toContain("new import_bun_app.Response");
|
|
});
|
|
|
|
test("Response import is added when Response is global, but not when shadowed", async () => {
|
|
const dir = tempDirWithFiles("response-shadowing", {
|
|
"server-component.js": `
|
|
export const mode = "ssr";
|
|
|
|
export function inner() {
|
|
const Response = 'ooga booga!';
|
|
const foo = new Response('test', { status: 200 });
|
|
return foo;
|
|
}
|
|
|
|
export const lmao = new Response()
|
|
`,
|
|
});
|
|
|
|
const serverResult =
|
|
await Bun.$`${bunExe()} build ${path.join(dir, "server-component.js")} --target=bun --server-components`
|
|
.env(bunEnv as any)
|
|
.text();
|
|
|
|
// Import should be added for the global Response usage
|
|
expect(serverResult).toContain('import { Response } from "bun:app"');
|
|
// Local shadowed Response should not be affected
|
|
expect(serverResult).toContain('new "ooga booga!"');
|
|
// Global Response is transformed to import_bun_app.Response
|
|
expect(serverResult).toContain("var lmao = new import_bun_app.Response");
|
|
});
|