Files
bun.sh/src/bake/hmr-runtime-server.ts
Zack Radisic a89e61fcaa ssg 3 (#22138)
### 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>
2025-09-30 05:26:32 -07:00

199 lines
5.9 KiB
TypeScript

// This file is the entrypoint to the hot-module-reloading runtime.
// On the server, communication is established with `server_exports`.
import type { Bake } from "bun";
import "./debug";
import { loadExports, replaceModules, serverManifest, ssrManifest } from "./hmr-module";
// import { AsyncLocalStorage } from "node:async_hooks";
const { AsyncLocalStorage } = require("node:async_hooks");
if (typeof IS_BUN_DEVELOPMENT !== "boolean") {
throw new Error("DCE is configured incorrectly");
}
export type RequestContext = {
responseOptions: ResponseInit;
streaming: boolean;
streamingStarted?: boolean;
renderAbort?: (path: string, params: Record<string, any> | null) => never;
};
// Create the AsyncLocalStorage instance for propagating response options
const responseOptionsALS = new AsyncLocalStorage();
let asyncLocalStorageWasSet = false;
interface Exports {
handleRequest: (
req: Request,
routerTypeMain: Id,
routeModules: Id[],
clientEntryUrl: string,
styles: string[],
params: Record<string, string> | null,
setAsyncLocalStorage: Function,
bundleNewRoute: (req: Request, path: string) => [number, Promise<void> | undefined],
newRouteParams: (
req: Request,
routeBundleIndex: number,
url: string,
) => {
routerTypeMain: Id;
routeModules: Id[];
clientEntryUrl: string;
styles: string[];
params: Record<string, string> | null;
},
) => any;
registerUpdate: (
modules: any,
componentManifestAdd: null | string[],
componentManifestDelete: null | string[],
) => void;
}
declare let server_exports: Exports;
server_exports = {
async handleRequest(
req,
routerTypeMain,
routeModules,
clientEntryUrl,
styles,
params,
setAsyncLocalStorage,
bundleNewRoute,
newRouteParams,
) {
if (!asyncLocalStorageWasSet) {
asyncLocalStorageWasSet = true;
setAsyncLocalStorage(responseOptionsALS);
}
while (true) {
if (IS_BUN_DEVELOPMENT && process.env.BUN_DEBUG_BAKE_JS) {
console.log("handleRequest", {
routeModules,
clientEntryUrl,
styles,
params,
});
}
const exports = await loadExports<Bake.ServerEntryPoint>(routerTypeMain);
const serverRenderer = exports.render;
if (!serverRenderer) {
throw new Error('Framework server entrypoint is missing a "render" export.');
}
if (typeof serverRenderer !== "function") {
throw new Error('Framework server entrypoint\'s "render" export is not a function.');
}
const [pageModule, ...layouts] = await Promise.all(routeModules.map(loadExports));
let requestWithCookies = req;
let storeValue: RequestContext = {
responseOptions: {},
streaming: pageModule.streaming ?? false,
};
try {
// Run the renderer inside the AsyncLocalStorage context
// This allows Response constructors to access the stored options
const response = await responseOptionsALS.run(storeValue, async () => {
return await serverRenderer(
requestWithCookies,
{
styles: styles,
modules: [clientEntryUrl],
layouts,
pageModule,
modulepreload: [],
params,
// Pass request in metadata when mode is 'ssr'
request: pageModule.mode === "ssr" ? requestWithCookies : undefined,
},
responseOptionsALS,
);
});
if (!(response instanceof Response)) {
throw $ERR_SSR_RESPONSE_EXPECTED(`Server-side request handler was expected to return a Response object.`);
}
return response;
} catch (error) {
// For `Response.render(...)`/`Response.redirect(...)` we throw the
// response to stop React from rendering
if (error instanceof Response) {
const resp = error;
// Handle `Response.render(...)`
if (resp.status !== 302) {
const newUrl = resp.headers.get("location");
if (!newUrl) {
throw new Error("Response.render(...) was expected to have a Location header");
}
const [routeBundleIndex, promise] = bundleNewRoute(req, newUrl);
if (promise) await promise;
if (req.signal.aborted) return new Response("");
const newArgs = newRouteParams(req, routeBundleIndex, newUrl);
routerTypeMain = newArgs.routerTypeMain;
routeModules = newArgs.routeModules;
clientEntryUrl = newArgs.clientEntryUrl;
styles = newArgs.styles;
params = newArgs.params;
continue;
}
// `Response.redirect(...)` or others, just return it
return resp;
}
throw error;
}
}
},
async registerUpdate(modules, componentManifestAdd, componentManifestDelete) {
replaceModules(modules);
if (componentManifestAdd) {
for (const uid of componentManifestAdd) {
try {
const exports = await loadExports<{}>(uid);
const client = {};
for (const exportName of Object.keys(exports)) {
serverManifest[uid + "#" + exportName] = {
id: uid,
name: exportName,
chunks: [],
};
client[exportName] = {
specifier: "ssr:" + uid,
name: exportName,
};
}
ssrManifest[uid] = client;
} catch (err) {
console.log(err);
}
}
}
if (componentManifestDelete) {
for (const fileName of componentManifestDelete) {
const client = ssrManifest[fileName];
for (const exportName in client) {
delete serverManifest[`${fileName}#${exportName}`];
}
delete ssrManifest[fileName];
}
}
},
} satisfies Exports;