- Server functions can return not_found: true to render the 404 page with status 404. - Page components and window.__PAGE_PROPS__ (and HMR updates) only get props, query, url and error. response_options, redirect, cache settings and res_transform stay on the server, so cookies and private headers no longer end up in the HTML. BunextPageProps is narrowed to match (breaking for code that read server-only fields from component props). - response_options headers go through a Headers object, so repeated headers such as several Set-Cookie survive. - Redirects: status_code wins over permanent; response_options headers and res_transform apply to redirects too. - Gzip for compressible text responses; compress: false to turn it off. - Files the router matches but never bundles (.server.ts, __root, excluded paths and directories) return 404 instead of looping through dev rebuild retries. - Missing public files return 404 rather than 403, so dotted page paths like /users/john.doe fall through to pages again. - importFresh detects rewrites within the filesystem's mtime granularity. - README: self-hosted React (not esm.sh), snake_case option names, static file serving, cache headers, the new options. Co-Authored-By: Claude Opus 5.5 <[email protected]>
123 lines
4.7 KiB
JavaScript
123 lines
4.7 KiB
JavaScript
import grabClientPageProps from "../../utils/grab-client-page-props";
|
|
import _ from "lodash";
|
|
import grabPageComponent from "./web-pages/grab-page-component";
|
|
/**
|
|
* # Push a client-only HMR update
|
|
* Sends the new client bundle to every controller whose page is in
|
|
* `changed_pages`, without re-running server functions. The payload carries
|
|
* no `page_props`, so the browser keeps its current props and only swaps the
|
|
* hydration script/CSS. Used right after the hydration build so the browser
|
|
* doesn't wait for the SSR rebuild.
|
|
*/
|
|
export function pushClientUpdate(changed_pages) {
|
|
if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
|
|
return;
|
|
}
|
|
const changed_set = new Set(changed_pages);
|
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
|
const local_path = controller?.target_map?.local_path;
|
|
if (!controller || !local_path || !changed_set.has(local_path)) {
|
|
continue;
|
|
}
|
|
const target_artifact = global.BUNEXT_BUNDLER_CTX_MAP[local_path];
|
|
const payload = target_artifact?.path
|
|
? { target_map: target_artifact }
|
|
: { reload: true };
|
|
try {
|
|
controller.controller.enqueue(`event: update\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
if (target_artifact?.path) {
|
|
controller.target_map = target_artifact;
|
|
}
|
|
}
|
|
catch {
|
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
export default async function serverPostBuildFn(params) {
|
|
if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
|
|
return;
|
|
}
|
|
const changed_set = params?.changed_pages && params.changed_pages.length > 0
|
|
? new Set(params.changed_pages)
|
|
: undefined;
|
|
const reload_payload = { reload: true };
|
|
const reload_enqueue = `event: update\ndata: ${JSON.stringify(reload_payload)}\n\n`;
|
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
|
if (!controller) {
|
|
continue;
|
|
}
|
|
if (!controller.target_map?.local_path) {
|
|
continue;
|
|
}
|
|
// Only touch controllers whose page actually changed.
|
|
if (changed_set && !changed_set.has(controller.target_map.local_path)) {
|
|
continue;
|
|
}
|
|
if (params?.reload_all_controllers) {
|
|
try {
|
|
controller.controller.enqueue(reload_enqueue);
|
|
}
|
|
catch {
|
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
|
}
|
|
continue;
|
|
}
|
|
const target_artifact = global.BUNEXT_BUNDLER_CTX_MAP[controller.target_map.local_path];
|
|
if (!target_artifact?.local_path) {
|
|
try {
|
|
controller.controller.enqueue(reload_enqueue);
|
|
}
|
|
catch {
|
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
|
}
|
|
continue;
|
|
}
|
|
// Re-render with this tab's own URL and cookie, never another
|
|
// client's request.
|
|
const mock_req = new Request(controller.page_url);
|
|
if (controller.page_cookie) {
|
|
mock_req.headers.set("cookie", controller.page_cookie);
|
|
}
|
|
const page_component = await grabPageComponent({
|
|
req: mock_req,
|
|
return_server_res_only: true,
|
|
is_hydration: true,
|
|
// This runs inside serverPostBuildFn (post-build of fullRebuild);
|
|
// letting it trigger another rebuild/retry would cascade.
|
|
no_retry: true,
|
|
});
|
|
if (page_component instanceof Response) {
|
|
continue;
|
|
}
|
|
const { serverRes } = page_component || {};
|
|
const final_artifact = {
|
|
// `page_cookie` holds the raw Cookie header (incl. HttpOnly
|
|
// cookies), so it must never be sent back to page JS.
|
|
..._.omit(controller, ["controller", "page_cookie", "page_url"]),
|
|
target_map: target_artifact,
|
|
};
|
|
if (!target_artifact) {
|
|
delete final_artifact.target_map;
|
|
}
|
|
// Always replace so prior error props cannot linger
|
|
final_artifact.page_props = grabClientPageProps(serverRes);
|
|
try {
|
|
let final_data = {};
|
|
if (global.BUNEXT_ROOT_FILE_UPDATED) {
|
|
final_data = reload_payload;
|
|
}
|
|
else {
|
|
final_data = final_artifact;
|
|
}
|
|
controller.controller.enqueue(`event: update\ndata: ${JSON.stringify(final_data)}\n\n`);
|
|
global.BUNEXT_ROOT_FILE_UPDATED = false;
|
|
}
|
|
catch {
|
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
|
}
|
|
}
|
|
}
|