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