nukejs 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/bundle.ts"],
4
- "sourcesContent": ["/**\r\n * bundle.ts \u2014 NukeJS Client Runtime\r\n *\r\n * This file is compiled by esbuild into /__n.js and served to every page.\r\n * It provides:\r\n *\r\n * initRuntime(data) \u2014 called once per page load to hydrate\r\n * \"use client\" components and wire up SPA nav\r\n * setupLocationChangeMonitor() \u2014 patches history.pushState/replaceState so\r\n * SPA navigation fires a 'locationchange' event\r\n *\r\n * Hydration model (partial hydration):\r\n * - The server renders the full page to HTML, wrapping each client component\r\n * in a <span data-hydrate-id=\"cc_\u2026\" data-hydrate-props=\"\u2026\"> marker.\r\n * - initRuntime loads the matching JS bundle for each marker and calls\r\n * hydrateRoot() on it, letting React take over just that subtree.\r\n * - Props serialized by the server may include nested React elements\r\n * (serialized as { __re: 'html'|'client', \u2026 }), which are reconstructed\r\n * back into React.createElement calls before mounting.\r\n *\r\n * SPA navigation:\r\n * - Link clicks / programmatic navigation dispatch a 'locationchange' event.\r\n * - The handler fetches the target URL as HTML, diffs the #app container,\r\n * unmounts the old React roots, and re-hydrates the new ones.\r\n * - HMR navigations add ?__hmr=1 so the server skips client-SSR (faster).\r\n *\r\n * Head tag management:\r\n * - The SSR renderer wraps every useHtml()-generated <meta>, <link>, <style>,\r\n * and <script> tag in <!--n-head-->\u2026<!--/n-head--> sentinel comments.\r\n * - On each navigation the client diffs the live sentinel block against the\r\n * incoming one by fingerprint, adding new tags and removing gone ones.\r\n * Tags shared between pages (e.g. a layout stylesheet) are left untouched\r\n * so there is no removal/re-insertion flash.\r\n * - New tags are always inserted before <!--/n-head--> so they stay inside\r\n * the tracked block and remain visible to the diff on subsequent navigations.\r\n */\r\n\r\n// \u2500\u2500\u2500 History patch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Patches history.pushState and history.replaceState to fire a custom\r\n * 'locationchange' event on window. Also listens to 'popstate' for\r\n * back/forward navigation.\r\n *\r\n * Called after initRuntime sets up the navigation listener so there is no\r\n * race between the event firing and the listener being registered.\r\n */\r\nexport function setupLocationChangeMonitor(): void {\r\n const originalPushState = window.history.pushState.bind(window.history);\r\n const originalReplaceState = window.history.replaceState.bind(window.history);\r\n\r\n const dispatch = (href?: any) =>\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href } }));\r\n\r\n window.history.pushState = function (...args) {\r\n originalPushState(...args);\r\n dispatch(args[2]); // args[2] is the URL\r\n };\r\n\r\n window.history.replaceState = function (...args) {\r\n originalReplaceState(...args);\r\n dispatch(args[2]);\r\n };\r\n\r\n // Back/forward navigation via the browser's native UI.\r\n window.addEventListener('popstate', () => dispatch(window.location.pathname));\r\n}\r\n\r\n// \u2500\u2500\u2500 Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ntype ClientDebugLevel = 'silent' | 'error' | 'info' | 'verbose';\r\n\r\n/**\r\n * Returns a thin logger whose methods are no-ops unless `level` allows them.\r\n * The server embeds the active debug level in the __n_data JSON blob so the\r\n * client respects the same setting as the server.\r\n */\r\nfunction makeLogger(level: ClientDebugLevel) {\r\n return {\r\n verbose: (...a: any[]) => { if (level === 'verbose') console.log(...a); },\r\n info: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.log(...a); },\r\n warn: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.warn(...a); },\r\n error: (...a: any[]) => { if (level !== 'silent') console.error(...a); },\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Serialized node types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** The wire format for React elements embedded in hydration props. */\r\ntype SerializedNode =\r\n | null\r\n | undefined\r\n | string\r\n | number\r\n | boolean\r\n | SerializedNode[]\r\n | { __re: 'html'; tag: string; props: Record<string, any> }\r\n | { __re: 'client'; componentId: string; props: Record<string, any> }\r\n | Record<string, any>;\r\n\r\ntype ModuleMap = Map<string, any>; // componentId \u2192 default export\r\n\r\n// \u2500\u2500\u2500 Prop reconstruction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively turns the server's serialized node tree back into real React\r\n * elements so they can be passed as props to hydrated components.\r\n *\r\n * The server serializes JSX passed as props (e.g. `<Button icon={<Icon />}>`)\r\n * into a JSON-safe format. This function reverses that process.\r\n */\r\nasync function reconstructElement(node: SerializedNode, mods: ModuleMap): Promise<any> {\r\n if (node === null || node === undefined) return node;\r\n if (typeof node !== 'object') return node; // primitive \u2014 pass through\r\n\r\n if (Array.isArray(node)) {\r\n const items = await Promise.all(node.map(n => reconstructElement(n, mods)));\r\n // Add index-based keys to React elements in the array to avoid the\r\n // \"Each child in a list should have a unique key prop\" warning.\r\n const React = await import('react');\r\n return items.map((el, i) =>\r\n el && typeof el === 'object' && el.$$typeof\r\n ? React.default.cloneElement(el, { key: el.key ?? i })\r\n : el,\r\n );\r\n }\r\n\r\n // Client component \u2014 look up the loaded module by ID.\r\n if ((node as any).__re === 'client') {\r\n const n = node as { __re: 'client'; componentId: string; props: Record<string, any> };\r\n const Comp = mods.get(n.componentId);\r\n if (!Comp) return null;\r\n const React = await import('react');\r\n return React.default.createElement(Comp, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Native HTML element (e.g. <div>, <span>).\r\n if ((node as any).__re === 'html') {\r\n const n = node as { __re: 'html'; tag: string; props: Record<string, any> };\r\n const React = await import('react');\r\n return React.default.createElement(n.tag, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Plain object \u2014 pass through as-is.\r\n return node;\r\n}\r\n\r\n/** Reconstructs every value in a props object, handling nested serialized nodes. */\r\nasync function reconstructProps(\r\n props: Record<string, any> | null | undefined,\r\n mods: ModuleMap,\r\n): Promise<Record<string, any>> {\r\n if (!props || typeof props !== 'object' || Array.isArray(props))\r\n return reconstructElement(props as any, mods);\r\n\r\n const out: Record<string, any> = {};\r\n for (const [k, v] of Object.entries(props))\r\n out[k] = await reconstructElement(v, mods);\r\n return out;\r\n}\r\n\r\n// \u2500\u2500\u2500 Module loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Dynamically imports each client component bundle from /__client-component/.\r\n * All fetches are issued in parallel; failures are logged but do not abort\r\n * the rest of the hydration pass.\r\n *\r\n * @param bust Optional cache-busting suffix appended as `?t=<bust>`.\r\n * Used during HMR navigation to bypass the module cache.\r\n */\r\nasync function loadModules(\r\n ids: string[],\r\n log: ReturnType<typeof makeLogger>,\r\n bust = '',\r\n): Promise<ModuleMap> {\r\n const mods: ModuleMap = new Map();\r\n await Promise.all(\r\n ids.map(async (id) => {\r\n try {\r\n const url = `/__client-component/${id}.js` + (bust ? `?t=${bust}` : '');\r\n const m = await import(url);\r\n mods.set(id, m.default);\r\n log.verbose('\u2713 Loaded:', id);\r\n } catch (err) {\r\n log.error('\u2717 Load failed:', id, err);\r\n }\r\n }),\r\n );\r\n return mods;\r\n}\r\n\r\n// \u2500\u2500\u2500 Root mounting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** All active React roots \u2014 tracked so they can be unmounted before navigation. */\r\ntype ReactRoot = { unmount(): void };\r\nconst activeRoots: ReactRoot[] = [];\r\n\r\n/**\r\n * Finds every `[data-hydrate-id]` span in the document and calls hydrateRoot()\r\n * on it. hydrateRoot reconciles React's virtual DOM against the existing server\r\n * HTML without discarding it, which avoids a visible flash on both initial load\r\n * and SPA navigation (where we set innerHTML to fresh SSR output before calling\r\n * mountNodes).\r\n *\r\n * Nested markers are skipped \u2014 the parent's React tree owns its children.\r\n */\r\nasync function mountNodes(\r\n mods: ModuleMap,\r\n log: ReturnType<typeof makeLogger>,\r\n): Promise<void> {\r\n const { hydrateRoot, createRoot } = await import('react-dom/client');\r\n const React = await import('react');\r\n\r\n const nodes = document.querySelectorAll<HTMLElement>('[data-hydrate-id]');\r\n log.verbose('Found', nodes.length, 'hydration point(s)');\r\n\r\n for (const node of nodes) {\r\n // Skip nested markers \u2014 the outer component owns its children.\r\n if (node.parentElement?.closest('[data-hydrate-id]')) continue;\r\n\r\n const id = node.getAttribute('data-hydrate-id')!;\r\n const Comp = mods.get(id);\r\n if (!Comp) { log.warn('No module for', id); continue; }\r\n\r\n let rawProps: Record<string, any> = {};\r\n try {\r\n rawProps = JSON.parse(node.getAttribute('data-hydrate-props') || '{}');\r\n } catch (e) {\r\n log.error('Props parse error for', id, e);\r\n }\r\n\r\n try {\r\n const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));\r\n\r\n // hydrateRoot reconciles against existing server HTML (initial page load).\r\n // createRoot renders fresh when the span is empty (HMR path \u2014 server sent\r\n // skipClientSSR=true so the span has no pre-rendered content to reconcile).\r\n let root: ReactRoot;\r\n if (node.innerHTML.trim()) {\r\n root = hydrateRoot(node, element);\r\n } else {\r\n const r = createRoot(node);\r\n r.render(element);\r\n root = r;\r\n }\r\n\r\n activeRoots.push(root);\r\n log.verbose('\u2713 Mounted:', id);\r\n } catch (err) {\r\n log.error('\u2717 Mount failed:', id, err);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag sync \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Walks a <head> element and returns every Element node that lives between\r\n * the <!--n-head--> and <!--/n-head--> sentinel comments, plus the closing\r\n * comment node itself (used as the insertion anchor).\r\n *\r\n * The SSR renderer emits these sentinels around every useHtml()-generated tag\r\n * so the client can manage exactly that set without touching permanent tags\r\n * (charset, viewport, importmap, runtime <script>).\r\n */\r\nfunction headBlock(head: HTMLHeadElement): { nodes: Element[]; closeComment: Comment | null } {\r\n const nodes: Element[] = [];\r\n let closeComment: Comment | null = null;\r\n let inside = false;\r\n\r\n for (const child of Array.from(head.childNodes)) {\r\n if (child.nodeType === Node.COMMENT_NODE) {\r\n const text = (child as Comment).data.trim();\r\n if (text === 'n-head') { inside = true; continue; }\r\n if (text === '/n-head') { closeComment = child as Comment; inside = false; continue; }\r\n }\r\n if (inside && child.nodeType === Node.ELEMENT_NODE)\r\n nodes.push(child as Element);\r\n }\r\n\r\n return { nodes, closeComment };\r\n}\r\n\r\n/** Stable key for an Element: tag name + sorted attribute list (name=value pairs). */\r\nfunction fingerprint(el: Element): string {\r\n return el.tagName + '|' + Array.from(el.attributes)\r\n .sort((a, b) => a.name.localeCompare(b.name))\r\n .map(a => `${a.name}=${a.value}`)\r\n .join('&');\r\n}\r\n\r\n/**\r\n * Diffs the live <!--n-head--> block against the incoming document's block and\r\n * applies the minimal set of DOM mutations:\r\n *\r\n * - Non-script tags (meta, link, style): fingerprint-diffed so shared layout\r\n * tags are left untouched (avoids stylesheet flash on navigation).\r\n * - Script tags: always removed and re-inserted as fresh elements so the\r\n * browser re-executes them and re-fetches any changed src file.\r\n * (Fingerprint diffing silently skips re-execution when src is unchanged.)\r\n *\r\n * If the live head has no sentinel block yet (e.g. initial page had no useHtml\r\n * tags), both sentinel comments are created on the fly.\r\n */\r\nfunction syncHeadTags(doc: Document): void {\r\n const live = headBlock(document.head);\r\n const next = headBlock(doc.head);\r\n\r\n // Ensure we have an anchor to insert before.\r\n let anchor = live.closeComment;\r\n if (!anchor) {\r\n document.head.appendChild(document.createComment('n-head'));\r\n anchor = document.createComment('/n-head');\r\n document.head.appendChild(anchor);\r\n }\r\n\r\n // \u2500\u2500 Scripts: always replace \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Remove all live script tags and re-insert fresh ones so the browser\r\n // executes them. src gets cache-busted so the latest file is fetched.\r\n for (const el of live.nodes)\r\n if (el.tagName === 'SCRIPT') el.remove();\r\n\r\n for (const el of next.nodes) {\r\n if (el.tagName === 'SCRIPT')\r\n document.head.insertBefore(cloneScriptForExecution(el), anchor);\r\n }\r\n\r\n // \u2500\u2500 Everything else: fingerprint diff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const liveMap = new Map<string, Element>();\r\n for (const el of live.nodes) if (el.tagName !== 'SCRIPT') liveMap.set(fingerprint(el), el);\r\n\r\n const nextMap = new Map<string, Element>();\r\n for (const el of next.nodes) if (el.tagName !== 'SCRIPT') nextMap.set(fingerprint(el), el);\r\n\r\n for (const [fp, el] of nextMap)\r\n if (!liveMap.has(fp)) document.head.insertBefore(el, anchor);\r\n\r\n for (const [fp, el] of liveMap)\r\n if (!nextMap.has(fp)) el.remove();\r\n}\r\n\r\n/**\r\n * Walks a <body> element and returns every Element node that lives between\r\n * the <!--n-body-scripts--> and <!--/n-body-scripts--> sentinel comments,\r\n * plus the closing comment node used as the insertion anchor.\r\n *\r\n * The SSR renderer emits these sentinels around every useHtml() body script\r\n * so the client can manage exactly that set without touching permanent nodes.\r\n */\r\nfunction bodyScriptsBlock(body: HTMLBodyElement | Element): { nodes: Element[]; closeComment: Comment | null } {\r\n const nodes: Element[] = [];\r\n let closeComment: Comment | null = null;\r\n let inside = false;\r\n\r\n for (const child of Array.from(body.childNodes)) {\r\n if (child.nodeType === Node.COMMENT_NODE) {\r\n const text = (child as Comment).data.trim();\r\n if (text === 'n-body-scripts') { inside = true; continue; }\r\n if (text === '/n-body-scripts') { closeComment = child as Comment; inside = false; continue; }\r\n }\r\n if (inside && child.nodeType === Node.ELEMENT_NODE)\r\n nodes.push(child as Element);\r\n }\r\n\r\n return { nodes, closeComment };\r\n}\r\n\r\n/**\r\n * Creates a fresh <script> element from a parsed source element so the browser\r\n * actually executes it when inserted into the live document.\r\n *\r\n * Why: browsers only execute a <script> that is *created and inserted* into\r\n * the live document. Nodes moved from a DOMParser document are auto-adopted\r\n * but their script is silently skipped. Cloning via createElement is required.\r\n *\r\n * Cache-busting: src-based scripts get a ?t=<timestamp> query appended so the\r\n * browser always fetches the latest version from the server on HMR updates,\r\n * bypassing the module/response cache.\r\n */\r\nfunction cloneScriptForExecution(src: Element): HTMLScriptElement {\r\n const el = document.createElement('script');\r\n for (const { name, value } of Array.from(src.attributes)) {\r\n if (name === 'src') {\r\n // Append a timestamp to force the browser to re-fetch the script file.\r\n const url = new URL(value, location.href);\r\n url.searchParams.set('t', String(Date.now()));\r\n el.setAttribute('src', url.toString());\r\n } else {\r\n el.setAttribute(name, value);\r\n }\r\n }\r\n // Copy inline content (for content-based scripts).\r\n if (src.textContent) el.textContent = src.textContent;\r\n return el;\r\n}\r\n\r\n/**\r\n * Replaces all body scripts in the <!--n-body-scripts--> sentinel block with\r\n * fresh elements from the incoming document.\r\n *\r\n * Unlike syncHeadTags (which diffs by fingerprint to avoid removing shared\r\n * stylesheets), body scripts must ALWAYS be removed and re-inserted so that:\r\n * - File changes picked up by HMR are actually executed by the browser.\r\n * - src-based scripts are cache-busted so the browser re-fetches them.\r\n *\r\n * Fingerprint diffing would silently skip re-execution of any script whose\r\n * src/attributes haven't changed, even if the file contents changed on disk.\r\n */\r\nfunction syncBodyScripts(doc: Document): void {\r\n const live = bodyScriptsBlock(document.body);\r\n const next = bodyScriptsBlock(doc.body);\r\n\r\n // Always remove every existing body script \u2014 never leave stale ones.\r\n for (const el of live.nodes) el.remove();\r\n\r\n // Ensure we have a sentinel anchor to insert before.\r\n let anchor = live.closeComment;\r\n if (!anchor) {\r\n document.body.appendChild(document.createComment('n-body-scripts'));\r\n anchor = document.createComment('/n-body-scripts');\r\n document.body.appendChild(anchor);\r\n }\r\n\r\n // Insert every script from the incoming document as a brand-new element\r\n // so the browser executes it. src gets a timestamp to bust any cache.\r\n for (const el of next.nodes)\r\n document.body.insertBefore(cloneScriptForExecution(el), anchor);\r\n}\r\n\r\n\r\n\r\n/**\r\n * Syncs attributes from a parsed element onto the live document element.\r\n * Adds/updates attributes present in `next` and removes any that were set\r\n * on `live` but are absent in `next` (clears stale htmlAttrs/bodyAttrs).\r\n */\r\nfunction syncAttrs(live: Element, next: Element): void {\r\n for (const { name, value } of Array.from(next.attributes))\r\n live.setAttribute(name, value);\r\n for (const { name } of Array.from(live.attributes))\r\n if (!next.hasAttribute(name)) live.removeAttribute(name);\r\n}\r\n\r\n/**\r\n * Listens for 'locationchange' events and performs a soft navigation:\r\n *\r\n * 1. Fetch the target URL as HTML (?__hmr=1 skips client-SSR for HMR speed).\r\n * 2. Parse the response with DOMParser.\r\n * 3. Apply all visual DOM changes first (head tags, html/body attrs, #app\r\n * innerHTML, title, __n_data) so the new content is painted before React\r\n * cleanup effects run \u2014 prevents a useHtml restore from briefly undoing\r\n * the new document state.\r\n * 4. Unmount old React roots (runs cleanup effects against the already-updated DOM).\r\n * 5. Re-hydrate new client component markers.\r\n * 6. Scroll to top.\r\n *\r\n * Falls back to a full page reload if anything goes wrong.\r\n */\r\nfunction setupNavigation(log: ReturnType<typeof makeLogger>): void {\r\n window.addEventListener('locationchange', async ({ detail: { href, hmr } }: any) => {\r\n try {\r\n const fetchUrl = hmr\r\n ? href + (href.includes('?') ? '&' : '?') + '__hmr=1'\r\n : href;\r\n\r\n const response = await fetch(fetchUrl, { headers: { Accept: 'text/html' } });\r\n if (!response.ok) {\r\n log.error('Navigation fetch failed:', response.status);\r\n return;\r\n }\r\n\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(await response.text(), 'text/html');\r\n const newApp = doc.getElementById('app');\r\n const currApp = document.getElementById('app');\r\n if (!newApp || !currApp) return;\r\n\r\n // \u2500\u2500 Visual update \u2014 all DOM mutations before React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Styles must be in place before new content appears to avoid an unstyled\r\n // flash. Unmounting runs useEffect cleanups (including useHtml restores)\r\n // which would temporarily revert document state if done first.\r\n\r\n // 1. Head tags \u2014 diff-based sync preserves shared layout tags untouched.\r\n syncHeadTags(doc);\r\n\r\n // 2. Body scripts (position='body') \u2014 diff-based sync mirrors head tag logic.\r\n syncBodyScripts(doc);\r\n\r\n // 3. <html> and <body> attributes (lang, class, style, etc.).\r\n syncAttrs(document.documentElement, doc.documentElement);\r\n syncAttrs(document.body, doc.body);\r\n\r\n // 4. Page content.\r\n currApp.innerHTML = newApp.innerHTML;\r\n\r\n // 5. <title>.\r\n const newTitle = doc.querySelector('title');\r\n if (newTitle) document.title = newTitle.textContent ?? '';\r\n\r\n // 6. Runtime data blob \u2014 must come after innerHTML swap so the new\r\n // __n_data element is part of the live document.\r\n const newDataEl = doc.getElementById('__n_data');\r\n const currDataEl = document.getElementById('__n_data');\r\n if (newDataEl && currDataEl) currDataEl.textContent = newDataEl.textContent;\r\n\r\n // \u2500\u2500 React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Unmount after the visual update. Cleanup effects now run against an\r\n // already-updated document, so there is nothing left to visually undo.\r\n activeRoots.splice(0).forEach(r => r.unmount());\r\n\r\n // \u2500\u2500 Re-hydration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const navData = JSON.parse(currDataEl?.textContent ?? '{}') as RuntimeData;\r\n log.info('\uD83D\uDD04 Route \u2192', href, '\u2014 mounting', navData.hydrateIds?.length ?? 0, 'component(s)');\r\n\r\n const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));\r\n await mountNodes(mods, log);\r\n\r\n window.scrollTo(0, 0);\r\n log.info('\uD83C\uDF89 Navigation complete:', href);\r\n } catch (err) {\r\n log.error('Navigation error, falling back to full reload:', err);\r\n window.location.href = href;\r\n }\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Shape of the JSON blob embedded as #__n_data in every SSR page. */\r\nexport interface RuntimeData {\r\n /** IDs of client components actually rendered on this page (subset of allIds). */\r\n hydrateIds: string[];\r\n /** All client component IDs reachable from this page, including layouts.\r\n * Pre-loaded so SPA navigations to related pages feel instant. */\r\n allIds: string[];\r\n url: string;\r\n params: Record<string, any>;\r\n debug: ClientDebugLevel;\r\n}\r\n\r\n/**\r\n * Bootstraps the NukeJS client runtime.\r\n *\r\n * Called once per page load from the inline <script type=\"module\"> injected\r\n * by the SSR renderer:\r\n *\r\n * ```js\r\n * const { initRuntime } = await import('nukejs');\r\n * const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n * await initRuntime(data);\r\n * ```\r\n *\r\n * Order of operations:\r\n * 1. Create the logger at the configured debug level.\r\n * 2. Wire up SPA navigation listener.\r\n * 3. Load all client component bundles in parallel.\r\n * 4. Hydrate every [data-hydrate-id] node.\r\n * 5. Patch history.pushState/replaceState so Link clicks trigger navigation.\r\n */\r\nexport async function initRuntime(data: RuntimeData): Promise<void> {\r\n const log = makeLogger(data.debug ?? 'silent');\r\n\r\n log.info('\uD83D\uDE80 Partial hydration:', data.hydrateIds.length, 'root component(s)');\r\n\r\n // Set up navigation first so any 'locationchange' fired during hydration\r\n // is captured (e.g. a redirect side-effect inside a component).\r\n setupNavigation(log);\r\n\r\n // Load all component bundles (not just hydrateIds) so SPA navigations to\r\n // related pages can mount their components without an extra network round-trip.\r\n const mods = await loadModules(data.allIds, log);\r\n await mountNodes(mods, log);\r\n\r\n log.info('\uD83C\uDF89 Done!');\r\n\r\n // Patch history last so pushState calls during hydration don't trigger a\r\n // navigation before roots are ready.\r\n setupLocationChangeMonitor();\r\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\r\n * bundle.ts \u2014 NukeJS Client Runtime\r\n *\r\n * This file is compiled by esbuild into /__n.js and served to every page.\r\n * It provides:\r\n *\r\n * initRuntime(data) \u2014 called once per page load to hydrate\r\n * \"use client\" components and wire up SPA nav\r\n * setupLocationChangeMonitor() \u2014 patches history.pushState/replaceState so\r\n * SPA navigation fires a 'locationchange' event\r\n *\r\n * Hydration model (partial hydration):\r\n * - The server renders the full page to HTML, wrapping each client component\r\n * in a <span data-hydrate-id=\"cc_\u2026\" data-hydrate-props=\"\u2026\"> marker.\r\n * - initRuntime loads the matching JS bundle for each marker and calls\r\n * hydrateRoot() on it, letting React take over just that subtree.\r\n * - Props serialized by the server may include nested React elements\r\n * (serialized as { __re: 'html'|'client', \u2026 }), which are reconstructed\r\n * back into React.createElement calls before mounting.\r\n *\r\n * SPA navigation:\r\n * - Link clicks / programmatic navigation dispatch a 'locationchange' event.\r\n * - The handler fetches the target URL as HTML, diffs the #app container,\r\n * unmounts the old React roots, and re-hydrates the new ones.\r\n * - HMR navigations add ?__hmr=1 so the server skips client-SSR (faster).\r\n *\r\n * Head tag management:\r\n * - The SSR renderer wraps every useHtml()-generated <meta>, <link>, <style>,\r\n * and <script> tag in <!--n-head-->\u2026<!--/n-head--> sentinel comments.\r\n * - On each navigation the client diffs the live sentinel block against the\r\n * incoming one by fingerprint, adding new tags and removing gone ones.\r\n * Tags shared between pages (e.g. a layout stylesheet) are left untouched\r\n * so there is no removal/re-insertion flash.\r\n * - New tags are always inserted before <!--/n-head--> so they stay inside\r\n * the tracked block and remain visible to the diff on subsequent navigations.\r\n */\r\n\r\n// \u2500\u2500\u2500 History patch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Patches history.pushState and history.replaceState to fire a custom\r\n * 'locationchange' event on window. Also listens to 'popstate' for\r\n * back/forward navigation.\r\n *\r\n * Called after initRuntime sets up the navigation listener so there is no\r\n * race between the event firing and the listener being registered.\r\n */\r\nexport function setupLocationChangeMonitor(): void {\r\n const originalPushState = window.history.pushState.bind(window.history);\r\n const originalReplaceState = window.history.replaceState.bind(window.history);\r\n\r\n const dispatch = (href?: any) =>\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href } }));\r\n\r\n window.history.pushState = function (...args) {\r\n originalPushState(...args);\r\n dispatch(args[2]); // args[2] is the URL\r\n };\r\n\r\n window.history.replaceState = function (...args) {\r\n originalReplaceState(...args);\r\n dispatch(args[2]);\r\n };\r\n\r\n // Back/forward navigation via the browser's native UI.\r\n window.addEventListener('popstate', () => dispatch(window.location.pathname));\r\n}\r\n\r\n// \u2500\u2500\u2500 Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ntype ClientDebugLevel = 'silent' | 'error' | 'info' | 'verbose';\r\n\r\n/**\r\n * Returns a thin logger whose methods are no-ops unless `level` allows them.\r\n * The server embeds the active debug level in the __n_data JSON blob so the\r\n * client respects the same setting as the server.\r\n */\r\nfunction makeLogger(level: ClientDebugLevel) {\r\n return {\r\n verbose: (...a: any[]) => { if (level === 'verbose') console.log(...a); },\r\n info: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.log(...a); },\r\n warn: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.warn(...a); },\r\n error: (...a: any[]) => { if (level !== 'silent') console.error(...a); },\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Serialized node types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** The wire format for React elements embedded in hydration props. */\r\ntype SerializedNode =\r\n | null\r\n | undefined\r\n | string\r\n | number\r\n | boolean\r\n | SerializedNode[]\r\n | { __re: 'html'; tag: string; props: Record<string, any> }\r\n | { __re: 'client'; componentId: string; props: Record<string, any> }\r\n | Record<string, any>;\r\n\r\ntype ModuleMap = Map<string, any>; // componentId \u2192 default export\r\n\r\n// \u2500\u2500\u2500 Prop reconstruction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively turns the server's serialized node tree back into real React\r\n * elements so they can be passed as props to hydrated components.\r\n *\r\n * The server serializes JSX passed as props (e.g. `<Button icon={<Icon />}>`)\r\n * into a JSON-safe format. This function reverses that process.\r\n */\r\nasync function reconstructElement(node: SerializedNode, mods: ModuleMap): Promise<any> {\r\n if (node === null || node === undefined) return node;\r\n if (typeof node !== 'object') return node; // primitive \u2014 pass through\r\n\r\n if (Array.isArray(node)) {\r\n const items = await Promise.all(node.map(n => reconstructElement(n, mods)));\r\n // Add index-based keys to React elements in the array to avoid the\r\n // \"Each child in a list should have a unique key prop\" warning.\r\n const React = await import('react');\r\n return items.map((el, i) =>\r\n el && typeof el === 'object' && el.$$typeof\r\n ? React.default.cloneElement(el, { key: el.key ?? i })\r\n : el,\r\n );\r\n }\r\n\r\n // Client component \u2014 look up the loaded module by ID.\r\n if ((node as any).__re === 'client') {\r\n const n = node as { __re: 'client'; componentId: string; props: Record<string, any> };\r\n const Comp = mods.get(n.componentId);\r\n if (!Comp) return null;\r\n const React = await import('react');\r\n return React.default.createElement(Comp, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Native HTML element (e.g. <div>, <span>).\r\n if ((node as any).__re === 'html') {\r\n const n = node as { __re: 'html'; tag: string; props: Record<string, any> };\r\n const React = await import('react');\r\n return React.default.createElement(n.tag, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Plain object \u2014 pass through as-is.\r\n return node;\r\n}\r\n\r\n/** Reconstructs every value in a props object, handling nested serialized nodes. */\r\nasync function reconstructProps(\r\n props: Record<string, any> | null | undefined,\r\n mods: ModuleMap,\r\n): Promise<Record<string, any>> {\r\n if (!props || typeof props !== 'object' || Array.isArray(props))\r\n return reconstructElement(props as any, mods);\r\n\r\n const out: Record<string, any> = {};\r\n for (const [k, v] of Object.entries(props))\r\n out[k] = await reconstructElement(v, mods);\r\n return out;\r\n}\r\n\r\n// \u2500\u2500\u2500 Module loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Dynamically imports each client component bundle from /__client-component/.\r\n * All fetches are issued in parallel; failures are logged but do not abort\r\n * the rest of the hydration pass.\r\n *\r\n * @param bust Optional cache-busting suffix appended as `?t=<bust>`.\r\n * Used during HMR navigation to bypass the module cache.\r\n */\r\nasync function loadModules(\r\n ids: string[],\r\n log: ReturnType<typeof makeLogger>,\r\n bust = '',\r\n): Promise<ModuleMap> {\r\n const mods: ModuleMap = new Map();\r\n await Promise.all(\r\n ids.map(async (id) => {\r\n try {\r\n const url = `/__client-component/${id}.js` + (bust ? `?t=${bust}` : '');\r\n const m = await import(url);\r\n mods.set(id, m.default);\r\n log.verbose('\u2713 Loaded:', id);\r\n } catch (err) {\r\n log.error('\u2717 Load failed:', id, err);\r\n }\r\n }),\r\n );\r\n return mods;\r\n}\r\n\r\n// \u2500\u2500\u2500 Root mounting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** All active React roots \u2014 tracked so they can be unmounted before navigation. */\r\ntype ReactRoot = { unmount(): void };\r\nconst activeRoots: ReactRoot[] = [];\r\n\r\n/**\r\n * Finds every `[data-hydrate-id]` span in the document and calls hydrateRoot()\r\n * on it. hydrateRoot reconciles React's virtual DOM against the existing server\r\n * HTML without discarding it, which avoids a visible flash on both initial load\r\n * and SPA navigation (where we set innerHTML to fresh SSR output before calling\r\n * mountNodes).\r\n *\r\n * Nested markers are skipped \u2014 the parent's React tree owns its children.\r\n */\r\nasync function mountNodes(\r\n mods: ModuleMap,\r\n log: ReturnType<typeof makeLogger>,\r\n): Promise<void> {\r\n const { hydrateRoot, createRoot } = await import('react-dom/client');\r\n const React = await import('react');\r\n\r\n const nodes = document.querySelectorAll<HTMLElement>('[data-hydrate-id]');\r\n log.verbose('Found', nodes.length, 'hydration point(s)');\r\n\r\n for (const node of nodes) {\r\n // Skip nested markers \u2014 the outer component owns its children.\r\n if (node.parentElement?.closest('[data-hydrate-id]')) continue;\r\n\r\n const id = node.getAttribute('data-hydrate-id')!;\r\n const Comp = mods.get(id);\r\n if (!Comp) { log.warn('No module for', id); continue; }\r\n\r\n let rawProps: Record<string, any> = {};\r\n try {\r\n rawProps = JSON.parse(node.getAttribute('data-hydrate-props') || '{}');\r\n } catch (e) {\r\n log.error('Props parse error for', id, e);\r\n }\r\n\r\n try {\r\n const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));\r\n\r\n // hydrateRoot reconciles against existing server HTML (initial page load).\r\n // createRoot renders fresh when the span is empty (HMR path \u2014 server sent\r\n // skipClientSSR=true so the span has no pre-rendered content to reconcile).\r\n let root: ReactRoot;\r\n if (node.innerHTML.trim()) {\r\n root = hydrateRoot(node, element);\r\n } else {\r\n const r = createRoot(node);\r\n r.render(element);\r\n root = r;\r\n }\r\n\r\n activeRoots.push(root);\r\n log.verbose('\u2713 Mounted:', id);\r\n } catch (err) {\r\n log.error('\u2717 Mount failed:', id, err);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag sync \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Walks a <head> element and returns every Element node that lives between\r\n * the <!--n-head--> and <!--/n-head--> sentinel comments, plus the closing\r\n * comment node itself (used as the insertion anchor).\r\n *\r\n * The SSR renderer emits these sentinels around every useHtml()-generated tag\r\n * so the client can manage exactly that set without touching permanent tags\r\n * (charset, viewport, importmap, runtime <script>).\r\n */\r\nfunction headBlock(head: HTMLHeadElement): { nodes: Element[]; closeComment: Comment | null } {\r\n const nodes: Element[] = [];\r\n let closeComment: Comment | null = null;\r\n let inside = false;\r\n\r\n for (const child of Array.from(head.childNodes)) {\r\n if (child.nodeType === Node.COMMENT_NODE) {\r\n const text = (child as Comment).data.trim();\r\n if (text === 'n-head') { inside = true; continue; }\r\n if (text === '/n-head') { closeComment = child as Comment; inside = false; continue; }\r\n }\r\n if (inside && child.nodeType === Node.ELEMENT_NODE)\r\n nodes.push(child as Element);\r\n }\r\n\r\n return { nodes, closeComment };\r\n}\r\n\r\n/** Stable key for an Element: tag name + sorted attribute list (name=value pairs). */\r\nfunction fingerprint(el: Element): string {\r\n return el.tagName + '|' + Array.from(el.attributes)\r\n .sort((a, b) => a.name.localeCompare(b.name))\r\n .map(a => `${a.name}=${a.value}`)\r\n .join('&');\r\n}\r\n\r\n/**\r\n * Diffs the live <!--n-head--> block against the incoming document's block and\r\n * applies the minimal set of DOM mutations:\r\n *\r\n * - Non-script tags (meta, link, style): fingerprint-diffed so shared layout\r\n * tags are left untouched (avoids stylesheet flash on navigation).\r\n * - Script tags: always removed and re-inserted as fresh elements so the\r\n * browser re-executes them and re-fetches any changed src file.\r\n * (Fingerprint diffing silently skips re-execution when src is unchanged.)\r\n *\r\n * If the live head has no sentinel block yet (e.g. initial page had no useHtml\r\n * tags), both sentinel comments are created on the fly.\r\n */\r\nfunction syncHeadTags(doc: Document): void {\r\n const live = headBlock(document.head);\r\n const next = headBlock(doc.head);\r\n\r\n // Ensure we have an anchor to insert before.\r\n let anchor = live.closeComment;\r\n if (!anchor) {\r\n document.head.appendChild(document.createComment('n-head'));\r\n anchor = document.createComment('/n-head');\r\n document.head.appendChild(anchor);\r\n }\r\n\r\n // \u2500\u2500 Scripts: always replace \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Remove all live script tags and re-insert fresh ones so the browser\r\n // executes them. src gets cache-busted so the latest file is fetched.\r\n for (const el of live.nodes)\r\n if (el.tagName === 'SCRIPT') el.remove();\r\n\r\n for (const el of next.nodes) {\r\n if (el.tagName === 'SCRIPT')\r\n document.head.insertBefore(cloneScriptForExecution(el), anchor);\r\n }\r\n\r\n // \u2500\u2500 Everything else: fingerprint diff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const liveMap = new Map<string, Element>();\r\n for (const el of live.nodes) if (el.tagName !== 'SCRIPT') liveMap.set(fingerprint(el), el);\r\n\r\n const nextMap = new Map<string, Element>();\r\n for (const el of next.nodes) if (el.tagName !== 'SCRIPT') nextMap.set(fingerprint(el), el);\r\n\r\n for (const [fp, el] of nextMap)\r\n if (!liveMap.has(fp)) document.head.insertBefore(el, anchor);\r\n\r\n for (const [fp, el] of liveMap)\r\n if (!nextMap.has(fp)) el.remove();\r\n}\r\n\r\n/**\r\n * Walks a <body> element and returns every Element node that lives between\r\n * the <!--n-body-scripts--> and <!--/n-body-scripts--> sentinel comments,\r\n * plus the closing comment node used as the insertion anchor.\r\n *\r\n * The SSR renderer emits these sentinels around every useHtml() body script\r\n * so the client can manage exactly that set without touching permanent nodes.\r\n */\r\nfunction bodyScriptsBlock(body: HTMLBodyElement | Element): { nodes: Element[]; closeComment: Comment | null } {\r\n const nodes: Element[] = [];\r\n let closeComment: Comment | null = null;\r\n let inside = false;\r\n\r\n for (const child of Array.from(body.childNodes)) {\r\n if (child.nodeType === Node.COMMENT_NODE) {\r\n const text = (child as Comment).data.trim();\r\n if (text === 'n-body-scripts') { inside = true; continue; }\r\n if (text === '/n-body-scripts') { closeComment = child as Comment; inside = false; continue; }\r\n }\r\n if (inside && child.nodeType === Node.ELEMENT_NODE)\r\n nodes.push(child as Element);\r\n }\r\n\r\n return { nodes, closeComment };\r\n}\r\n\r\n/**\r\n * Creates a fresh <script> element from a parsed source element so the browser\r\n * actually executes it when inserted into the live document.\r\n *\r\n * Why: browsers only execute a <script> that is *created and inserted* into\r\n * the live document. Nodes moved from a DOMParser document are auto-adopted\r\n * but their script is silently skipped. Cloning via createElement is required.\r\n *\r\n * Cache-busting: src-based scripts get a ?t=<timestamp> query appended so the\r\n * browser always fetches the latest version from the server on HMR updates,\r\n * bypassing the module/response cache.\r\n */\r\nfunction cloneScriptForExecution(src: Element): HTMLScriptElement {\r\n const el = document.createElement('script');\r\n for (const { name, value } of Array.from(src.attributes)) {\r\n if (name === 'src') {\r\n // Append a timestamp to force the browser to re-fetch the script file.\r\n const url = new URL(value, location.href);\r\n url.searchParams.set('t', String(Date.now()));\r\n el.setAttribute('src', url.toString());\r\n } else {\r\n el.setAttribute(name, value);\r\n }\r\n }\r\n // Copy inline content (for content-based scripts).\r\n if (src.textContent) el.textContent = src.textContent;\r\n return el;\r\n}\r\n\r\n/**\r\n * Replaces all body scripts in the <!--n-body-scripts--> sentinel block with\r\n * fresh elements from the incoming document.\r\n *\r\n * Unlike syncHeadTags (which diffs by fingerprint to avoid removing shared\r\n * stylesheets), body scripts must ALWAYS be removed and re-inserted so that:\r\n * - File changes picked up by HMR are actually executed by the browser.\r\n * - src-based scripts are cache-busted so the browser re-fetches them.\r\n *\r\n * Fingerprint diffing would silently skip re-execution of any script whose\r\n * src/attributes haven't changed, even if the file contents changed on disk.\r\n */\r\nfunction syncBodyScripts(doc: Document): void {\r\n const live = bodyScriptsBlock(document.body);\r\n const next = bodyScriptsBlock(doc.body);\r\n\r\n // Always remove every existing body script \u2014 never leave stale ones.\r\n for (const el of live.nodes) el.remove();\r\n\r\n // Ensure we have a sentinel anchor to insert before.\r\n let anchor = live.closeComment;\r\n if (!anchor) {\r\n document.body.appendChild(document.createComment('n-body-scripts'));\r\n anchor = document.createComment('/n-body-scripts');\r\n document.body.appendChild(anchor);\r\n }\r\n\r\n // Insert every script from the incoming document as a brand-new element\r\n // so the browser executes it. src gets a timestamp to bust any cache.\r\n for (const el of next.nodes)\r\n document.body.insertBefore(cloneScriptForExecution(el), anchor);\r\n}\r\n\r\n\r\n\r\n/**\r\n * Syncs attributes from a parsed element onto the live document element.\r\n * Adds/updates attributes present in `next` and removes any that were set\r\n * on `live` but are absent in `next` (clears stale htmlAttrs/bodyAttrs).\r\n */\r\nfunction syncAttrs(live: Element, next: Element): void {\r\n for (const { name, value } of Array.from(next.attributes))\r\n live.setAttribute(name, value);\r\n for (const { name } of Array.from(live.attributes))\r\n if (!next.hasAttribute(name)) live.removeAttribute(name);\r\n}\r\n\r\n/**\r\n * Listens for 'locationchange' events and performs a soft navigation:\r\n *\r\n * 1. Fetch the target URL as HTML (?__hmr=1 skips client-SSR for HMR speed).\r\n * 2. Parse the response with DOMParser.\r\n * 3. Apply all visual DOM changes first (head tags, html/body attrs, #app\r\n * innerHTML, title, __n_data) so the new content is painted before React\r\n * cleanup effects run \u2014 prevents a useHtml restore from briefly undoing\r\n * the new document state.\r\n * 4. Unmount old React roots (runs cleanup effects against the already-updated DOM).\r\n * 5. Re-hydrate new client component markers.\r\n * 6. Scroll to top.\r\n *\r\n * Falls back to a full page reload if anything goes wrong.\r\n */\r\nfunction setupNavigation(log: ReturnType<typeof makeLogger>): void {\r\n window.addEventListener('locationchange', async ({ detail: { href, hmr } }: any) => {\r\n try {\r\n const fetchUrl = hmr\r\n ? href + (href.includes('?') ? '&' : '?') + '__hmr=1'\r\n : href;\r\n\r\n const response = await fetch(fetchUrl, { headers: { Accept: 'text/html' } });\r\n if (!response.ok) {\r\n log.error('Navigation fetch failed:', response.status);\r\n return;\r\n }\r\n\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(await response.text(), 'text/html');\r\n const newApp = doc.getElementById('app');\r\n const currApp = document.getElementById('app');\r\n if (!newApp || !currApp) return;\r\n\r\n // \u2500\u2500 Visual update \u2014 all DOM mutations before React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Styles must be in place before new content appears to avoid an unstyled\r\n // flash. Unmounting runs useEffect cleanups (including useHtml restores)\r\n // which would temporarily revert document state if done first.\r\n\r\n // 1. Head tags \u2014 diff-based sync preserves shared layout tags untouched.\r\n syncHeadTags(doc);\r\n\r\n // 2. Body scripts (position='body') \u2014 diff-based sync mirrors head tag logic.\r\n syncBodyScripts(doc);\r\n\r\n // 3. <html> and <body> attributes (lang, class, style, etc.).\r\n syncAttrs(document.documentElement, doc.documentElement);\r\n syncAttrs(document.body, doc.body);\r\n\r\n // 4. Page content.\r\n currApp.innerHTML = newApp.innerHTML;\r\n\r\n // 5. <title>.\r\n const newTitle = doc.querySelector('title');\r\n if (newTitle) document.title = newTitle.textContent ?? '';\r\n\r\n // 6. Runtime data blob \u2014 must come after innerHTML swap so the new\r\n // __n_data element is part of the live document.\r\n const newDataEl = doc.getElementById('__n_data');\r\n const currDataEl = document.getElementById('__n_data');\r\n if (newDataEl && currDataEl) currDataEl.textContent = newDataEl.textContent;\r\n\r\n // \u2500\u2500 React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Unmount after the visual update. Cleanup effects now run against an\r\n // already-updated document, so there is nothing left to visually undo.\r\n activeRoots.splice(0).forEach(r => r.unmount());\r\n\r\n // \u2500\u2500 Re-hydration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const navData = JSON.parse(currDataEl?.textContent ?? '{}') as RuntimeData;\r\n log.info('\uD83D\uDD04 Route \u2192', href, '\u2014 mounting', navData.hydrateIds?.length ?? 0, 'component(s)');\r\n\r\n const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));\r\n await mountNodes(mods, log);\r\n\r\n window.scrollTo(0, 0);\r\n log.info('\uD83C\uDF89 Navigation complete:', href);\r\n } catch (err) {\r\n log.error('Navigation error, falling back to full reload:', err);\r\n window.location.href = href;\r\n }\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Shape of the JSON blob embedded as #__n_data in every SSR page. */\r\nexport interface RuntimeData {\r\n /** IDs of client components actually rendered on this page (subset of allIds). */\r\n hydrateIds: string[];\r\n /** All client component IDs reachable from this page, including layouts.\r\n * Pre-loaded so SPA navigations to related pages feel instant. */\r\n allIds: string[];\r\n url: string;\r\n params: Record<string, any>;\r\n /** Query string parameters parsed from the URL. Multi-value keys are arrays. */\r\n query: Record<string, string | string[]>;\r\n /**\r\n * Safe subset of the incoming request headers (cookie, authorization, and\r\n * proxy-authorization are stripped before embedding in the HTML document).\r\n */\r\n headers: Record<string, string>;\r\n debug: ClientDebugLevel;\r\n}\r\n\r\n/**\r\n * Bootstraps the NukeJS client runtime.\r\n *\r\n * Called once per page load from the inline <script type=\"module\"> injected\r\n * by the SSR renderer:\r\n *\r\n * ```js\r\n * const { initRuntime } = await import('nukejs');\r\n * const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n * await initRuntime(data);\r\n * ```\r\n *\r\n * Order of operations:\r\n * 1. Create the logger at the configured debug level.\r\n * 2. Wire up SPA navigation listener.\r\n * 3. Load all client component bundles in parallel.\r\n * 4. Hydrate every [data-hydrate-id] node.\r\n * 5. Patch history.pushState/replaceState so Link clicks trigger navigation.\r\n */\r\nexport async function initRuntime(data: RuntimeData): Promise<void> {\r\n const log = makeLogger(data.debug ?? 'silent');\r\n\r\n log.info('\uD83D\uDE80 Partial hydration:', data.hydrateIds.length, 'root component(s)');\r\n\r\n // Set up navigation first so any 'locationchange' fired during hydration\r\n // is captured (e.g. a redirect side-effect inside a component).\r\n setupNavigation(log);\r\n\r\n // Load all component bundles (not just hydrateIds) so SPA navigations to\r\n // related pages can mount their components without an extra network round-trip.\r\n const mods = await loadModules(data.allIds, log);\r\n await mountNodes(mods, log);\r\n\r\n log.info('\uD83C\uDF89 Done!');\r\n\r\n // Patch history last so pushState calls during hydration don't trigger a\r\n // navigation before roots are ready.\r\n setupLocationChangeMonitor();\r\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/hmr-bundle.ts"],
4
- "sourcesContent": ["/**\r\n * hmr-bundle.ts \u2014 HMR Client Script\r\n *\r\n * This file is compiled on-demand by middleware.ts and served to the browser\r\n * as /__hmr.js (injected into every dev-mode page as a module script).\r\n *\r\n * It opens an EventSource connection to /__hmr and reacts to three message\r\n * types from the server:\r\n *\r\n * 'reload' \u2014 A page or stylesheet changed.\r\n * url === '*' \u2192 reload stylesheets in-place (no flicker)\r\n * url === window.location.pathname \u2192 soft-navigate the current page\r\n *\r\n * 'replace' \u2014 A component/utility changed.\r\n * Re-navigate the current page so SSR picks up the new code.\r\n *\r\n * 'restart' \u2014 The server is restarting (config or middleware changed).\r\n * Close the SSE connection and poll /__hmr_ping until the\r\n * server is back, then hard-reload the page.\r\n *\r\n * The same reconnect polling is used when the SSE connection drops unexpectedly\r\n * (e.g. the dev server crashed).\r\n */\r\n\r\nimport { log } from './logger';\r\n\r\n// \u2500\u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Opens the SSE connection and starts listening for HMR events. */\r\nexport default function hmr(): void {\r\n const es = new EventSource('/__hmr');\r\n\r\n es.onopen = () => {\r\n log.info('[HMR] Connected');\r\n };\r\n\r\n es.onerror = () => {\r\n // Connection dropped without a restart message (e.g. crash or network\r\n // blip). Close cleanly and poll until the server is back.\r\n es.close();\r\n waitForReconnect();\r\n };\r\n\r\n es.onmessage = async (event) => {\r\n try {\r\n const msg = JSON.parse(event.data);\r\n\r\n if (msg.type === 'restart') {\r\n log.info('[HMR] Server restarting \u2014 waiting to reconnect...');\r\n es.close();\r\n waitForReconnect();\r\n return;\r\n }\r\n\r\n if (msg.type === 'reload') {\r\n if (msg.url === '*') {\r\n // CSS / global style change \u2014 bust stylesheet hrefs in-place.\r\n // This avoids a full page reload and its associated FOUC.\r\n reloadStylesheets();\r\n return;\r\n }\r\n // A specific page changed \u2014 only navigate if we're on that page.\r\n if (patternMatchesPathname(msg.url, window.location.pathname)) {\r\n log.info('[HMR] Page changed:', msg.url);\r\n navigate(window.location.pathname + window.location.search);\r\n }\r\n return;\r\n }\r\n\r\n if (msg.type === 'replace') {\r\n // A shared component or utility changed. The current page might use\r\n // it, so we re-navigate to pick up the latest server render.\r\n log.info('[HMR] Component changed:', msg.component);\r\n navigate(window.location.pathname + window.location.search);\r\n return;\r\n }\r\n } catch (err) {\r\n log.error('[HMR] Message parse error:', err);\r\n }\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Soft navigation helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Triggers a soft (SPA-style) navigation via the locationchange event that\r\n * bundle.ts listens to. Adds `hmr: true` in the detail so the navigation\r\n * handler appends `?__hmr=1`, which tells SSR to skip client-component\r\n * renderToString (faster HMR round-trips).\r\n */\r\nfunction navigate(href: string): void {\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href, hmr: true } }));\r\n}\r\n\r\n// \u2500\u2500\u2500 Dynamic route pattern matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns true when `pathname` matches the route `pattern` emitted by the\r\n * server. Patterns use the file-system conventions:\r\n * [param] \u2192 any single non-slash segment\r\n * [...slug] \u2192 one or more segments\r\n * [[...slug]] \u2192 zero or more segments\r\n * [[param]] \u2192 zero or one segment\r\n *\r\n * Each segment is classified before any escaping so that bracket characters\r\n * in param names are never mistaken for regex metacharacters.\r\n */\r\nfunction patternMatchesPathname(pattern: string, pathname: string): boolean {\r\n // Normalise trailing slashes so /a/ matches pattern /a and vice versa.\r\n const normPattern = pattern.length > 1 ? pattern.replace(/\\/+$/, '') : pattern;\r\n const normPathname = pathname.length > 1 ? pathname.replace(/\\/+$/, '') : pathname;\r\n const segments = normPattern.replace(/^\\//, '').split('/');\r\n const regexParts = segments.map(seg => {\r\n if (/^\\[\\[\\.\\.\\..+\\]\\]$/.test(seg)) return '(?:\\/.*)?' ; // [[...x]] optional catch-all\r\n if (/^\\[\\.\\.\\./.test(seg)) return '(?:\\/.+)' ; // [...x] required catch-all\r\n if (/^\\[\\[/.test(seg)) return '(?:\\/[^/]*)?' ;// [[x]] optional single\r\n if (/^\\[/.test(seg)) return '\\/[^/]+' ; // [x] required single\r\n return '\\/' + seg.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&'); // static \u2014 escape metacharacters\r\n });\r\n return new RegExp('^' + regexParts.join('') + '$').test(normPathname);\r\n}\r\n\r\n// \u2500\u2500\u2500 Reconnect polling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Polls /__hmr_ping at `intervalMs` until the server responds with a 200\r\n * (meaning it's back up), then triggers a full page reload to pick up any\r\n * changes that happened during the downtime.\r\n *\r\n * Gives up after `maxAttempts` (default ~15 seconds at 500 ms intervals).\r\n */\r\nfunction waitForReconnect(intervalMs = 3000, maxAttempts = 10): void {\r\n let attempts = 0;\r\n\r\n const id = setInterval(async () => {\r\n attempts++;\r\n try {\r\n const res = await fetch('/__hmr_ping', { cache: 'no-store' });\r\n if (res.ok) {\r\n clearInterval(id);\r\n log.info('[HMR] Server back \u2014 reloading');\r\n window.location.reload();\r\n }\r\n } catch {\r\n // Server still down \u2014 keep polling silently.\r\n }\r\n\r\n if (attempts >= maxAttempts) {\r\n clearInterval(id);\r\n log.error('[HMR] Server did not come back after restart');\r\n }\r\n }, intervalMs);\r\n}\r\n\r\n// \u2500\u2500\u2500 Stylesheet cache-buster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Appends a `?t=<timestamp>` query to every `<link rel=\"stylesheet\">` href.\r\n * The browser treats the new URL as a different resource and re-fetches it,\r\n * updating styles without a page reload or visible flash.\r\n */\r\nfunction reloadStylesheets(): void {\r\n const links = document.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]');\r\n log.info(`[HMR] CSS changed \u2014 reloading ${links.length} stylesheet(s)`);\r\n links.forEach(link => {\r\n const url = new URL(link.href);\r\n url.searchParams.set('t', String(Date.now()));\r\n link.href = url.toString();\r\n });\r\n}\r\n\r\n// Auto-start when this module is loaded.\r\nhmr();"],
4
+ "sourcesContent": ["/**\r\n * hmr-bundle.ts \u2014 HMR Client Script\r\n *\r\n * This file is compiled on-demand by middleware.ts and served to the browser\r\n * as /__hmr.js (injected into every dev-mode page as a module script).\r\n *\r\n * It opens an EventSource connection to /__hmr and reacts to three message\r\n * types from the server:\r\n *\r\n * 'reload' \u2014 A page or stylesheet changed.\r\n * url === '*' \u2192 reload stylesheets in-place (no flicker)\r\n * url === window.location.pathname \u2192 soft-navigate the current page\r\n *\r\n * 'replace' \u2014 A component/utility changed.\r\n * Re-navigate the current page so SSR picks up the new code.\r\n *\r\n * 'restart' \u2014 The server is restarting (config or middleware changed).\r\n * Close the SSE connection and poll /__hmr_ping until the\r\n * server is back, then hard-reload the page.\r\n *\r\n * The same reconnect polling is used when the SSE connection drops unexpectedly\r\n * (e.g. the dev server crashed).\r\n */\r\n\r\nimport { log } from './logger';\r\n\r\n// \u2500\u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Opens the SSE connection and starts listening for HMR events. */\r\nexport default function hmr(): void {\r\n const es = new EventSource('/__hmr');\r\n\r\n es.onopen = () => {\r\n log.info('[HMR] Connected');\r\n };\r\n\r\n es.onerror = () => {\r\n // Connection dropped without a restart message (e.g. crash or network\r\n // blip). Close cleanly and poll until the server is back.\r\n es.close();\r\n waitForReconnect();\r\n };\r\n\r\n es.onmessage = async (event) => {\r\n try {\r\n const msg = JSON.parse(event.data);\r\n\r\n if (msg.type === 'restart') {\r\n log.info('[HMR] Server restarting \u2014 waiting to reconnect...');\r\n es.close();\r\n waitForReconnect();\r\n return;\r\n }\r\n\r\n if (msg.type === 'reload') {\r\n if (msg.url === '*') {\r\n // CSS / global style change \u2014 bust stylesheet hrefs in-place.\r\n // This avoids a full page reload and its associated FOUC.\r\n reloadStylesheets();\r\n return;\r\n }\r\n // A specific page changed \u2014 only navigate if we're on that page.\r\n if (patternMatchesPathname(msg.url, window.location.pathname)) {\r\n log.info('[HMR] Page changed:', msg.url);\r\n navigate(window.location.pathname + window.location.search);\r\n }\r\n return;\r\n }\r\n\r\n if (msg.type === 'replace') {\r\n // A shared component or utility changed. The current page might use\r\n // it, so we re-navigate to pick up the latest server render.\r\n log.info('[HMR] Component changed:', msg.component);\r\n navigate(window.location.pathname + window.location.search);\r\n return;\r\n }\r\n } catch (err) {\r\n log.error('[HMR] Message parse error:', err);\r\n }\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Soft navigation helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Triggers a soft (SPA-style) navigation via the locationchange event that\r\n * bundle.ts listens to. Adds `hmr: true` in the detail so the navigation\r\n * handler appends `?__hmr=1`, which tells SSR to skip client-component\r\n * renderToString (faster HMR round-trips).\r\n */\r\nfunction navigate(href: string): void {\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href, hmr: true } }));\r\n}\r\n\r\n// \u2500\u2500\u2500 Dynamic route pattern matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns true when `pathname` matches the route `pattern` emitted by the\r\n * server. Patterns use the file-system conventions:\r\n * [param] \u2192 any single non-slash segment\r\n * [...slug] \u2192 one or more segments\r\n * [[...slug]] \u2192 zero or more segments\r\n * [[param]] \u2192 zero or one segment\r\n *\r\n * Each segment is classified before any escaping so that bracket characters\r\n * in param names are never mistaken for regex metacharacters.\r\n */\r\nfunction patternMatchesPathname(pattern: string, pathname: string): boolean {\r\n // Normalise trailing slashes so /a/ matches pattern /a and vice versa.\r\n const normPattern = pattern.length > 1 ? pattern.replace(/\\/+$/, '') : pattern;\r\n const normPathname = pathname.length > 1 ? pathname.replace(/\\/+$/, '') : pathname;\r\n const segments = normPattern.replace(/^\\//, '').split('/');\r\n const regexParts = segments.map(seg => {\r\n if (/^\\[\\[\\.\\.\\..+\\]\\]$/.test(seg)) return '(?:\\/.*)?' ; // [[...x]] optional catch-all\r\n if (/^\\[\\.\\.\\./.test(seg)) return '(?:\\/.+)' ; // [...x] required catch-all\r\n if (/^\\[\\[/.test(seg)) return '(?:\\/[^/]*)?' ;// [[x]] optional single\r\n if (/^\\[/.test(seg)) return '\\/[^/]+' ; // [x] required single\r\n return '\\/' + seg.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&'); // static \u2014 escape metacharacters\r\n });\r\n return new RegExp('^' + regexParts.join('') + '$').test(normPathname);\r\n}\r\n\r\n// \u2500\u2500\u2500 Reconnect polling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Polls /__hmr_ping at `intervalMs` until the server responds with a 200\r\n * (meaning it's back up), then triggers a full page reload to pick up any\r\n * changes that happened during the downtime.\r\n *\r\n * Gives up after `maxAttempts` (default ~30 seconds at 3000 ms intervals).\r\n */\r\nfunction waitForReconnect(intervalMs = 3000, maxAttempts = 10): void {\r\n let attempts = 0;\r\n\r\n const id = setInterval(async () => {\r\n attempts++;\r\n try {\r\n const res = await fetch('/__hmr_ping', { cache: 'no-store' });\r\n if (res.ok) {\r\n clearInterval(id);\r\n log.info('[HMR] Server back \u2014 reloading');\r\n window.location.reload();\r\n }\r\n } catch {\r\n // Server still down \u2014 keep polling silently.\r\n }\r\n\r\n if (attempts >= maxAttempts) {\r\n clearInterval(id);\r\n log.error('[HMR] Server did not come back after restart');\r\n }\r\n }, intervalMs);\r\n}\r\n\r\n// \u2500\u2500\u2500 Stylesheet cache-buster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Appends a `?t=<timestamp>` query to every `<link rel=\"stylesheet\">` href.\r\n * The browser treats the new URL as a different resource and re-fetches it,\r\n * updating styles without a page reload or visible flash.\r\n */\r\nfunction reloadStylesheets(): void {\r\n const links = document.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]');\r\n log.info(`[HMR] CSS changed \u2014 reloading ${links.length} stylesheet(s)`);\r\n links.forEach(link => {\r\n const url = new URL(link.href);\r\n url.searchParams.set('t', String(Date.now()));\r\n link.href = url.toString();\r\n });\r\n}\r\n\r\n// Auto-start when this module is loaded.\r\nhmr();"],
5
5
  "mappings": "AAwBA,SAAS,WAAW;AAKL,SAAR,MAA6B;AAClC,QAAM,KAAK,IAAI,YAAY,QAAQ;AAEnC,KAAG,SAAS,MAAM;AAChB,QAAI,KAAK,iBAAiB;AAAA,EAC5B;AAEA,KAAG,UAAU,MAAM;AAGjB,OAAG,MAAM;AACT,qBAAiB;AAAA,EACnB;AAEA,KAAG,YAAY,OAAO,UAAU;AAC9B,QAAI;AACF,YAAM,MAAM,KAAK,MAAM,MAAM,IAAI;AAEjC,UAAI,IAAI,SAAS,WAAW;AAC1B,YAAI,KAAK,wDAAmD;AAC5D,WAAG,MAAM;AACT,yBAAiB;AACjB;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,UAAU;AACzB,YAAI,IAAI,QAAQ,KAAK;AAGnB,4BAAkB;AAClB;AAAA,QACF;AAEA,YAAI,uBAAuB,IAAI,KAAK,OAAO,SAAS,QAAQ,GAAG;AAC7D,cAAI,KAAK,uBAAuB,IAAI,GAAG;AACvC,mBAAS,OAAO,SAAS,WAAW,OAAO,SAAS,MAAM;AAAA,QAC5D;AACA;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,WAAW;AAG1B,YAAI,KAAK,4BAA4B,IAAI,SAAS;AAClD,iBAAS,OAAO,SAAS,WAAW,OAAO,SAAS,MAAM;AAC1D;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,MAAM,8BAA8B,GAAG;AAAA,IAC7C;AAAA,EACF;AACF;AAUA,SAAS,SAAS,MAAoB;AACpC,SAAO,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,EAAE,MAAM,KAAK,KAAK,EAAE,CAAC,CAAC;AACzF;AAeA,SAAS,uBAAuB,SAAiB,UAA2B;AAE1E,QAAM,cAAe,QAAQ,SAAU,IAAI,QAAQ,QAAQ,QAAQ,EAAE,IAAK;AAC1E,QAAM,eAAe,SAAS,SAAS,IAAI,SAAS,QAAQ,QAAQ,EAAE,IAAI;AAC1E,QAAM,WAAa,YAAY,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG;AAC3D,QAAM,aAAa,SAAS,IAAI,SAAO;AACrC,QAAI,qBAAqB,KAAK,GAAG,EAAG,QAAO;AAC3C,QAAI,YAAY,KAAK,GAAG,EAAc,QAAO;AAC7C,QAAI,QAAQ,KAAK,GAAG,EAAoB,QAAO;AAC/C,QAAI,MAAM,KAAK,GAAG,EAAsB,QAAO;AAC/C,WAAO,MAAO,IAAI,QAAQ,sBAAsB,MAAM;AAAA,EACxD,CAAC;AACD,SAAO,IAAI,OAAO,MAAM,WAAW,KAAK,EAAE,IAAI,GAAG,EAAE,KAAK,YAAY;AACtE;AAWA,SAAS,iBAAiB,aAAa,KAAM,cAAc,IAAU;AACnE,MAAI,WAAW;AAEf,QAAM,KAAK,YAAY,YAAY;AACjC;AACA,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,eAAe,EAAE,OAAO,WAAW,CAAC;AAC5D,UAAI,IAAI,IAAI;AACV,sBAAc,EAAE;AAChB,YAAI,KAAK,oCAA+B;AACxC,eAAO,SAAS,OAAO;AAAA,MACzB;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,QAAI,YAAY,aAAa;AAC3B,oBAAc,EAAE;AAChB,UAAI,MAAM,8CAA8C;AAAA,IAC1D;AAAA,EACF,GAAG,UAAU;AACf;AASA,SAAS,oBAA0B;AACjC,QAAM,QAAQ,SAAS,iBAAkC,wBAAwB;AACjF,MAAI,KAAK,sCAAiC,MAAM,MAAM,gBAAgB;AACtE,QAAM,QAAQ,UAAQ;AACpB,UAAM,MAAM,IAAI,IAAI,KAAK,IAAI;AAC7B,QAAI,aAAa,IAAI,KAAK,OAAO,KAAK,IAAI,CAAC,CAAC;AAC5C,SAAK,OAAO,IAAI,SAAS;AAAA,EAC3B,CAAC;AACH;AAGA,IAAI;",
6
6
  "names": []
7
7
  }
package/dist/hmr.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/hmr.ts"],
4
- "sourcesContent": ["/**\r\n * hmr.ts \u2014 Server-Side HMR (Hot Module Replacement) Engine\r\n *\r\n * Manages the set of connected SSE clients and broadcasts change payloads to\r\n * them when source files are modified.\r\n *\r\n * The HMR protocol uses three message types:\r\n *\r\n * { type: 'reload', url: '/path' } \u2014 A page file changed. The browser\r\n * re-fetches and swaps that URL if it\r\n * is currently active.\r\n * url: '*' means CSS changed \u2014 only\r\n * stylesheets are reloaded in-place.\r\n *\r\n * { type: 'replace', component: 'X' } \u2014 A non-page file changed (shared\r\n * component, util, etc.). The browser\r\n * re-fetches the current page to pick\r\n * up the new version.\r\n *\r\n * { type: 'restart' } \u2014 The server is about to restart\r\n * (middleware.ts or nuke.config.ts\r\n * changed). The client polls\r\n * /__hmr_ping until it gets a 200.\r\n *\r\n * File change \u2192 payload mapping:\r\n * pages/** \u2192 reload (URL derived from the file path)\r\n * *.css / *.scss \u2026 \u2192 reload (url: '*' triggers stylesheet cache-bust)\r\n * anything else \u2192 replace (component name used for logging only)\r\n *\r\n * Debouncing:\r\n * Editors often emit multiple fs events for a single save. Changes are\r\n * debounced per filename with a 100 ms window so each save produces exactly\r\n * one broadcast.\r\n */\r\n\r\nimport { ServerResponse } from 'http';\r\nimport { existsSync, watch } from 'fs';\r\nimport path from 'path';\r\nimport { log } from './logger';\r\nimport { invalidateComponentCache } from './component-analyzer';\r\n\r\n// \u2500\u2500\u2500 SSE client registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * All currently connected SSE clients (long-lived ServerResponse objects).\r\n * Exported so middleware.ts can register new connections.\r\n */\r\nexport const hmrClients = new Set<ServerResponse>();\r\n\r\n// \u2500\u2500\u2500 Broadcast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Sends a JSON payload to every connected SSE client.\r\n * Clients that have disconnected are silently removed from the set.\r\n */\r\nfunction broadcastHmr(payload: object): void {\r\n const data = `data: ${JSON.stringify(payload)}\\n\\n`;\r\n for (const client of hmrClients) {\r\n try {\r\n client.write(data);\r\n } catch {\r\n // Write failed \u2014 client disconnected without closing cleanly.\r\n hmrClients.delete(client);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Payload builder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts a relative filename (as emitted by fs.watch) to the URL the browser\r\n * should re-fetch.\r\n *\r\n * Input examples:\r\n * 'pages/index.tsx' \u2192 '/'\r\n * 'pages/about/index.tsx' \u2192 '/about'\r\n * 'pages/blog/[slug].tsx' \u2192 '/blog/[slug]' (dynamic segment preserved)\r\n */\r\nfunction pageFileToUrl(filename: string): string {\r\n // Strip the leading 'pages/' prefix that watchDir was called with.\r\n const withoutPages = filename.slice('pages/'.length);\r\n const withoutExt = withoutPages.replace(/\\.(tsx|ts)$/, '');\r\n\r\n // 'index' and 'layout' at any level map to the directory URL.\r\n // e.g. 'users/layout' \u2192 '/users', 'layout' \u2192 '/', 'users/index' \u2192 '/users'\r\n const url = withoutExt === 'index' || withoutExt === 'layout'\r\n ? '/'\r\n : '/' + withoutExt\r\n .replace(/\\/index$/, '')\r\n .replace(/\\/layout$/, '')\r\n .replace(/\\\\/g, '/');\r\n\r\n return url;\r\n}\r\n\r\n/**\r\n * Determines the appropriate HMR message for a changed file.\r\n *\r\n * Routing logic:\r\n * - Paths under pages/ \u2192 `reload` with the derived URL\r\n * - CSS/Sass/Less files \u2192 `reload` with url='*' (stylesheet cache-bust)\r\n * - Everything else \u2192 `replace` with the component base name\r\n */\r\nfunction buildPayload(filename: string): object {\r\n const normalized = filename.replace(/\\\\/g, '/');\r\n\r\n if (normalized.startsWith('pages/')) {\r\n const url = pageFileToUrl(normalized);\r\n return { type: 'reload', url };\r\n }\r\n\r\n const ext = path.extname(filename).toLowerCase();\r\n if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') {\r\n return { type: 'reload', url: '*' };\r\n }\r\n\r\n // Generic component/util change \u2014 browser re-renders the current page.\r\n const componentName = path.basename(filename, path.extname(filename));\r\n return { type: 'replace', component: componentName };\r\n}\r\n\r\n// \u2500\u2500\u2500 File watcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Per-filename debounce timers. */\r\nconst pending = new Map<string, NodeJS.Timeout>();\r\n\r\n/**\r\n * Recursively watches `dir` and broadcasts an HMR message whenever a file\r\n * changes. Changes are debounced per file with a 100 ms window.\r\n *\r\n * @param dir Absolute path to watch.\r\n * @param label Short label for log messages (e.g. 'App', 'Server').\r\n */\r\nexport function watchDir(dir: string, label: string): void {\r\n if (!existsSync(dir)) return;\r\n\r\n watch(dir, { recursive: true }, (_event, filename) => {\r\n if (!filename) return;\r\n\r\n // Debounce: cancel any pending timer for this file and restart it.\r\n if (pending.has(filename)) clearTimeout(pending.get(filename)!);\r\n\r\n const timeout = setTimeout(() => {\r\n const payload = buildPayload(filename);\r\n log.info(`[HMR] ${label} changed: ${filename}`, JSON.stringify(payload));\r\n\r\n // Evict this file from the component-analysis cache so the next SSR\r\n // render re-analyses it (catches \"use client\" or import graph changes).\r\n if (dir) invalidateComponentCache(path.resolve(dir, filename));\r\n\r\n broadcastHmr(payload);\r\n pending.delete(filename);\r\n }, 100);\r\n\r\n pending.set(filename, timeout);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Restart broadcast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Sends a 'restart' message to all SSE clients, then waits 120 ms to give\r\n * them time to receive it before the process exits.\r\n *\r\n * Called by app.ts before `process.exit(75)` when a config file changes.\r\n */\r\nexport function broadcastRestart(): Promise<void> {\r\n broadcastHmr({ type: 'restart' });\r\n return new Promise<void>(resolve => setTimeout(resolve, 120));\r\n}"],
5
- "mappings": "AAoCA,SAAS,YAAY,aAAa;AAClC,OAAO,UAAU;AACjB,SAAS,WAAW;AACpB,SAAS,gCAAgC;AAQlC,MAAM,aAAa,oBAAI,IAAoB;AAQlD,SAAS,aAAa,SAAuB;AAC3C,QAAM,OAAO,SAAS,KAAK,UAAU,OAAO,CAAC;AAAA;AAAA;AAC7C,aAAW,UAAU,YAAY;AAC/B,QAAI;AACF,aAAO,MAAM,IAAI;AAAA,IACnB,QAAQ;AAEN,iBAAW,OAAO,MAAM;AAAA,IAC1B;AAAA,EACF;AACF;AAaA,SAAS,cAAc,UAA0B;AAE/C,QAAM,eAAe,SAAS,MAAM,SAAS,MAAM;AACnD,QAAM,aAAe,aAAa,QAAQ,eAAe,EAAE;AAI3D,QAAM,MAAM,eAAe,WAAW,eAAe,WACjD,MACA,MAAM,WACH,QAAQ,YAAY,EAAE,EACtB,QAAQ,aAAa,EAAE,EACvB,QAAQ,OAAO,GAAG;AAEzB,SAAO;AACT;AAUA,SAAS,aAAa,UAA0B;AAC9C,QAAM,aAAa,SAAS,QAAQ,OAAO,GAAG;AAE9C,MAAI,WAAW,WAAW,QAAQ,GAAG;AACnC,UAAM,MAAM,cAAc,UAAU;AACpC,WAAO,EAAE,MAAM,UAAU,IAAI;AAAA,EAC/B;AAEA,QAAM,MAAM,KAAK,QAAQ,QAAQ,EAAE,YAAY;AAC/C,MAAI,QAAQ,UAAU,QAAQ,WAAW,QAAQ,WAAW,QAAQ,SAAS;AAC3E,WAAO,EAAE,MAAM,UAAU,KAAK,IAAI;AAAA,EACpC;AAGA,QAAM,gBAAgB,KAAK,SAAS,UAAU,KAAK,QAAQ,QAAQ,CAAC;AACpE,SAAO,EAAE,MAAM,WAAW,WAAW,cAAc;AACrD;AAKA,MAAM,UAAU,oBAAI,IAA4B;AASzC,SAAS,SAAS,KAAa,OAAqB;AACzD,MAAI,CAAC,WAAW,GAAG,EAAG;AAEtB,QAAM,KAAK,EAAE,WAAW,KAAK,GAAG,CAAC,QAAQ,aAAa;AACpD,QAAI,CAAC,SAAU;AAGf,QAAI,QAAQ,IAAI,QAAQ,EAAG,cAAa,QAAQ,IAAI,QAAQ,CAAE;AAE9D,UAAM,UAAU,WAAW,MAAM;AAC/B,YAAM,UAAU,aAAa,QAAQ;AACrC,UAAI,KAAK,SAAS,KAAK,aAAa,QAAQ,IAAI,KAAK,UAAU,OAAO,CAAC;AAIvE,UAAI,IAAK,0BAAyB,KAAK,QAAQ,KAAK,QAAQ,CAAC;AAE7D,mBAAa,OAAO;AACpB,cAAQ,OAAO,QAAQ;AAAA,IACzB,GAAG,GAAG;AAEN,YAAQ,IAAI,UAAU,OAAO;AAAA,EAC/B,CAAC;AACH;AAUO,SAAS,mBAAkC;AAChD,eAAa,EAAE,MAAM,UAAU,CAAC;AAChC,SAAO,IAAI,QAAc,aAAW,WAAW,SAAS,GAAG,CAAC;AAC9D;",
4
+ "sourcesContent": ["/**\r\n * hmr.ts \u2014 Server-Side HMR (Hot Module Replacement) Engine\r\n *\r\n * Manages the set of connected SSE clients and broadcasts change payloads to\r\n * them when source files are modified.\r\n *\r\n * The HMR protocol uses three message types:\r\n *\r\n * { type: 'reload', url: '/path' } \u2014 A page file changed. The browser\r\n * re-fetches and swaps that URL if it\r\n * is currently active.\r\n * url: '*' means CSS changed \u2014 only\r\n * stylesheets are reloaded in-place.\r\n *\r\n * { type: 'replace', component: 'X' } \u2014 A non-page file changed (shared\r\n * component, util, etc.). The browser\r\n * re-fetches the current page to pick\r\n * up the new version.\r\n *\r\n * { type: 'restart' } \u2014 The server is about to restart\r\n * (middleware.ts or nuke.config.ts\r\n * changed). The client polls\r\n * /__hmr_ping until it gets a 200.\r\n *\r\n * File change \u2192 payload mapping:\r\n * pages/** \u2192 reload (URL derived from the file path)\r\n * *.css / *.scss \u2026 \u2192 reload (url: '*' triggers stylesheet cache-bust)\r\n * anything else \u2192 replace (component name used for logging only)\r\n *\r\n * Debouncing:\r\n * Editors often emit multiple fs events for a single save. Changes are\r\n * debounced per filename with a 100 ms window so each save produces exactly\r\n * one broadcast.\r\n */\r\n\r\nimport { ServerResponse } from 'http';\r\nimport { existsSync, watch } from 'fs';\r\nimport path from 'path';\r\nimport { log } from './logger';\r\nimport { invalidateComponentCache } from './component-analyzer';\r\n\r\n// \u2500\u2500\u2500 SSE client registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * All currently connected SSE clients (long-lived ServerResponse objects).\r\n * Exported so middleware.ts can register new connections.\r\n */\r\nexport const hmrClients = new Set<ServerResponse>();\r\n\r\n// \u2500\u2500\u2500 Broadcast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Sends a JSON payload to every connected SSE client.\r\n * Clients that have disconnected are silently removed from the set.\r\n */\r\nfunction broadcastHmr(payload: object): void {\r\n const data = `data: ${JSON.stringify(payload)}\\n\\n`;\r\n for (const client of hmrClients) {\r\n try {\r\n client.write(data);\r\n } catch {\r\n // Write failed \u2014 client disconnected without closing cleanly.\r\n hmrClients.delete(client);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Payload builder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts a relative filename (as emitted by fs.watch) to the URL the browser\r\n * should re-fetch.\r\n *\r\n * Input examples:\r\n * 'pages/index.tsx' \u2192 '/'\r\n * 'pages/about/index.tsx' \u2192 '/about'\r\n * 'pages/blog/[slug].tsx' \u2192 '/blog/[slug]' (dynamic segment preserved)\r\n */\r\nfunction pageFileToUrl(filename: string): string {\r\n // watchDir is called with ./app, so filenames are relative to that dir,\r\n // e.g. 'pages/about/index.tsx'. Strip the 'pages/' prefix to get the URL path.\r\n const withoutPages = filename.slice('pages/'.length);\r\n const withoutExt = withoutPages.replace(/\\.(tsx|ts)$/, '');\r\n\r\n // 'index' and 'layout' at any level map to the directory URL.\r\n // e.g. 'users/layout' \u2192 '/users', 'layout' \u2192 '/', 'users/index' \u2192 '/users'\r\n const url = withoutExt === 'index' || withoutExt === 'layout'\r\n ? '/'\r\n : '/' + withoutExt\r\n .replace(/\\/index$/, '')\r\n .replace(/\\/layout$/, '')\r\n .replace(/\\\\/g, '/');\r\n\r\n return url;\r\n}\r\n\r\n/**\r\n * Determines the appropriate HMR message for a changed file.\r\n *\r\n * Routing logic:\r\n * - Paths under pages/ \u2192 `reload` with the derived URL\r\n * - CSS/Sass/Less files \u2192 `reload` with url='*' (stylesheet cache-bust)\r\n * - Everything else \u2192 `replace` with the component base name\r\n */\r\nfunction buildPayload(filename: string): object {\r\n const normalized = filename.replace(/\\\\/g, '/');\r\n\r\n if (normalized.startsWith('pages/')) {\r\n const url = pageFileToUrl(normalized);\r\n return { type: 'reload', url };\r\n }\r\n\r\n const ext = path.extname(filename).toLowerCase();\r\n if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') {\r\n return { type: 'reload', url: '*' };\r\n }\r\n\r\n // Generic component/util change \u2014 browser re-renders the current page.\r\n const componentName = path.basename(filename, path.extname(filename));\r\n return { type: 'replace', component: componentName };\r\n}\r\n\r\n// \u2500\u2500\u2500 File watcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Per-filename debounce timers. */\r\nconst pending = new Map<string, NodeJS.Timeout>();\r\n\r\n/**\r\n * Recursively watches `dir` and broadcasts an HMR message whenever a file\r\n * changes. Changes are debounced per file with a 100 ms window.\r\n *\r\n * @param dir Absolute path to watch.\r\n * @param label Short label for log messages (e.g. 'App', 'Server').\r\n */\r\nexport function watchDir(dir: string, label: string): void {\r\n if (!existsSync(dir)) return;\r\n\r\n watch(dir, { recursive: true }, (_event, filename) => {\r\n if (!filename) return;\r\n\r\n // Debounce: cancel any pending timer for this file and restart it.\r\n if (pending.has(filename)) clearTimeout(pending.get(filename)!);\r\n\r\n const timeout = setTimeout(() => {\r\n const payload = buildPayload(filename);\r\n log.info(`[HMR] ${label} changed: ${filename}`, JSON.stringify(payload));\r\n\r\n // Evict this file from the component-analysis cache so the next SSR\r\n // render re-analyses it (catches \"use client\" or import graph changes).\r\n if (dir) invalidateComponentCache(path.resolve(dir, filename));\r\n\r\n broadcastHmr(payload);\r\n pending.delete(filename);\r\n }, 100);\r\n\r\n pending.set(filename, timeout);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Restart broadcast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Sends a 'restart' message to all SSE clients, then waits 120 ms to give\r\n * them time to receive it before the process exits.\r\n *\r\n * Called by app.ts before `process.exit(75)` when a config file changes.\r\n */\r\nexport function broadcastRestart(): Promise<void> {\r\n broadcastHmr({ type: 'restart' });\r\n return new Promise<void>(resolve => setTimeout(resolve, 120));\r\n}"],
5
+ "mappings": "AAoCA,SAAS,YAAY,aAAa;AAClC,OAAO,UAAU;AACjB,SAAS,WAAW;AACpB,SAAS,gCAAgC;AAQlC,MAAM,aAAa,oBAAI,IAAoB;AAQlD,SAAS,aAAa,SAAuB;AAC3C,QAAM,OAAO,SAAS,KAAK,UAAU,OAAO,CAAC;AAAA;AAAA;AAC7C,aAAW,UAAU,YAAY;AAC/B,QAAI;AACF,aAAO,MAAM,IAAI;AAAA,IACnB,QAAQ;AAEN,iBAAW,OAAO,MAAM;AAAA,IAC1B;AAAA,EACF;AACF;AAaA,SAAS,cAAc,UAA0B;AAG/C,QAAM,eAAe,SAAS,MAAM,SAAS,MAAM;AACnD,QAAM,aAAe,aAAa,QAAQ,eAAe,EAAE;AAI3D,QAAM,MAAM,eAAe,WAAW,eAAe,WACjD,MACA,MAAM,WACH,QAAQ,YAAY,EAAE,EACtB,QAAQ,aAAa,EAAE,EACvB,QAAQ,OAAO,GAAG;AAEzB,SAAO;AACT;AAUA,SAAS,aAAa,UAA0B;AAC9C,QAAM,aAAa,SAAS,QAAQ,OAAO,GAAG;AAE9C,MAAI,WAAW,WAAW,QAAQ,GAAG;AACnC,UAAM,MAAM,cAAc,UAAU;AACpC,WAAO,EAAE,MAAM,UAAU,IAAI;AAAA,EAC/B;AAEA,QAAM,MAAM,KAAK,QAAQ,QAAQ,EAAE,YAAY;AAC/C,MAAI,QAAQ,UAAU,QAAQ,WAAW,QAAQ,WAAW,QAAQ,SAAS;AAC3E,WAAO,EAAE,MAAM,UAAU,KAAK,IAAI;AAAA,EACpC;AAGA,QAAM,gBAAgB,KAAK,SAAS,UAAU,KAAK,QAAQ,QAAQ,CAAC;AACpE,SAAO,EAAE,MAAM,WAAW,WAAW,cAAc;AACrD;AAKA,MAAM,UAAU,oBAAI,IAA4B;AASzC,SAAS,SAAS,KAAa,OAAqB;AACzD,MAAI,CAAC,WAAW,GAAG,EAAG;AAEtB,QAAM,KAAK,EAAE,WAAW,KAAK,GAAG,CAAC,QAAQ,aAAa;AACpD,QAAI,CAAC,SAAU;AAGf,QAAI,QAAQ,IAAI,QAAQ,EAAG,cAAa,QAAQ,IAAI,QAAQ,CAAE;AAE9D,UAAM,UAAU,WAAW,MAAM;AAC/B,YAAM,UAAU,aAAa,QAAQ;AACrC,UAAI,KAAK,SAAS,KAAK,aAAa,QAAQ,IAAI,KAAK,UAAU,OAAO,CAAC;AAIvE,UAAI,IAAK,0BAAyB,KAAK,QAAQ,KAAK,QAAQ,CAAC;AAE7D,mBAAa,OAAO;AACpB,cAAQ,OAAO,QAAQ;AAAA,IACzB,GAAG,GAAG;AAEN,YAAQ,IAAI,UAAU,OAAO;AAAA,EAC/B,CAAC;AACH;AAUO,SAAS,mBAAkC;AAChD,eAAa,EAAE,MAAM,UAAU,CAAC;AAChC,SAAO,IAAI,QAAc,aAAW,WAAW,SAAS,GAAG,CAAC;AAC9D;",
6
6
  "names": []
7
7
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { useHtml } from './use-html';
2
2
  export type { HtmlOptions, TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag, } from './use-html';
3
3
  export { default as useRouter } from './use-router';
4
+ export { useRequest } from './use-request';
5
+ export type { RequestContext } from './use-request';
6
+ export { normaliseHeaders, sanitiseHeaders } from './request-store';
4
7
  export { default as Link } from './Link';
5
8
  export { setupLocationChangeMonitor, initRuntime } from './bundle';
6
9
  export type { RuntimeData } from './bundle';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { useHtml } from "./use-html.js";
2
2
  import { default as default2 } from "./use-router.js";
3
+ import { useRequest } from "./use-request.js";
4
+ import { normaliseHeaders, sanitiseHeaders } from "./request-store.js";
3
5
  import { default as default3 } from "./Link.js";
4
6
  import { setupLocationChangeMonitor, initRuntime } from "./bundle.js";
5
7
  import { escapeHtml } from "./utils.js";
@@ -12,9 +14,12 @@ export {
12
14
  getDebugLevel,
13
15
  initRuntime,
14
16
  log,
17
+ normaliseHeaders,
18
+ sanitiseHeaders,
15
19
  setDebugLevel,
16
20
  setupLocationChangeMonitor,
17
21
  useHtml,
22
+ useRequest,
18
23
  default2 as useRouter
19
24
  };
20
25
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["// \u2500\u2500\u2500 Client-side hooks & components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { useHtml } from './use-html';\r\nexport type {\r\n HtmlOptions,\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './use-html';\r\n\r\nexport { default as useRouter } from './use-router';\r\n\r\nexport { default as Link } from './Link';\r\n\r\n// \u2500\u2500\u2500 Client runtime (browser bootstrap) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { setupLocationChangeMonitor, initRuntime } from './bundle';\r\nexport type { RuntimeData } from './bundle';\r\n\r\n// \u2500\u2500\u2500 Shared utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { escapeHtml } from './utils';\r\n\r\nexport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nexport type { DebugLevel } from './logger';"],
5
- "mappings": "AACA,SAAS,eAAe;AAYxB,SAAoB,WAAXA,gBAA4B;AAErC,SAAoB,WAAXA,gBAAuB;AAGhC,SAAS,4BAA4B,mBAAmB;AAIxD,SAAS,kBAAkB;AAE3B,SAAS,MAAM,GAAG,KAAK,eAAe,qBAAqB;",
4
+ "sourcesContent": ["// \u2500\u2500\u2500 Client-side hooks & components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { useHtml } from './use-html';\r\nexport type {\r\n HtmlOptions,\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './use-html';\r\n\r\nexport { default as useRouter } from './use-router';\r\n\r\nexport { useRequest } from './use-request';\r\nexport type { RequestContext } from './use-request';\r\n\r\nexport { normaliseHeaders, sanitiseHeaders } from './request-store';\r\n\r\nexport { default as Link } from './Link';\r\n\r\n// \u2500\u2500\u2500 Client runtime (browser bootstrap) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { setupLocationChangeMonitor, initRuntime } from './bundle';\r\nexport type { RuntimeData } from './bundle';\r\n\r\n// \u2500\u2500\u2500 Shared utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { escapeHtml } from './utils';\r\n\r\nexport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nexport type { DebugLevel } from './logger';"],
5
+ "mappings": "AACA,SAAS,eAAe;AAYxB,SAAoB,WAAXA,gBAA4B;AAErC,SAAS,kBAAkB;AAG3B,SAAS,kBAAkB,uBAAuB;AAElD,SAAoB,WAAXA,gBAAuB;AAGhC,SAAS,4BAA4B,mBAAmB;AAIxD,SAAS,kBAAkB;AAE3B,SAAS,MAAM,GAAG,KAAK,eAAe,qBAAqB;",
6
6
  "names": ["default"]
7
7
  }
@@ -13,16 +13,17 @@
13
13
  * scripts: [{ src: '/analytics.js', defer: true }],
14
14
  * };
15
15
  */
16
- export interface ScriptTag {
17
- src?: string;
18
- content?: string;
19
- type?: string;
20
- defer?: boolean;
21
- async?: boolean;
22
- }
16
+ import type { ScriptTag } from './html-store';
17
+ export type { ScriptTag };
18
+ /**
19
+ * StyleTag for the legacy metadata API.
20
+ * Extends html-store's StyleTag with `href` for external stylesheet links,
21
+ * which the metadata API supported but html-store's <style>-only type does not.
22
+ */
23
23
  export interface StyleTag {
24
24
  href?: string;
25
25
  content?: string;
26
+ media?: string;
26
27
  }
27
28
  export interface Metadata {
28
29
  title?: string;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/metadata.ts"],
4
- "sourcesContent": ["/**\r\n * metadata.ts \u2014 Legacy Metadata Helpers\r\n *\r\n * @deprecated Use the `useHtml()` hook instead.\r\n *\r\n * This module provided an earlier metadata API where pages exported a\r\n * `metadata` object alongside their default component. It is retained for\r\n * backwards compatibility but new projects should use useHtml().\r\n *\r\n * Example (old pattern):\r\n * export const metadata = {\r\n * title: 'My Page',\r\n * scripts: [{ src: '/analytics.js', defer: true }],\r\n * };\r\n */\r\n\r\nimport { pathToFileURL } from 'url';\r\nimport { escapeHtml } from './utils';\r\n\r\nexport interface ScriptTag {\r\n src?: string;\r\n content?: string;\r\n type?: string;\r\n defer?: boolean;\r\n async?: boolean;\r\n}\r\n\r\nexport interface StyleTag {\r\n href?: string;\r\n content?: string;\r\n}\r\n\r\nexport interface Metadata {\r\n title?: string;\r\n scripts?: ScriptTag[];\r\n styles?: StyleTag[];\r\n}\r\n\r\n/**\r\n * Dynamically imports a page/layout module and returns its exported `metadata`\r\n * object, or an empty object if none is found or the import fails.\r\n */\r\nexport async function loadMetadata(filePath: string): Promise<Metadata> {\r\n try {\r\n const mod = await import(pathToFileURL(filePath).href);\r\n return (mod.metadata as Metadata) ?? {};\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\n/**\r\n * Merges metadata from an array of modules in render order (outermost layout\r\n * first, page last).\r\n *\r\n * Merge strategy:\r\n * title \u2014 last non-empty value wins (page overrides layout)\r\n * scripts \u2014 concatenated in order\r\n * styles \u2014 concatenated in order\r\n */\r\nexport function mergeMetadata(ordered: Metadata[]): Required<Metadata> {\r\n const result: Required<Metadata> = { title: '', scripts: [], styles: [] };\r\n for (const m of ordered) {\r\n if (m.title) result.title = m.title;\r\n if (m.scripts?.length) result.scripts.push(...m.scripts);\r\n if (m.styles?.length) result.styles.push(...m.styles);\r\n }\r\n return result;\r\n}\r\n\r\n/** Renders a ScriptTag to an HTML string. */\r\nexport function renderScriptTag(s: ScriptTag): string {\r\n if (s.src) {\r\n const attrs = [\r\n `src=\"${escapeHtml(s.src)}\"`,\r\n s.type ? `type=\"${escapeHtml(s.type)}\"` : '',\r\n s.defer ? 'defer' : '',\r\n s.async ? 'async' : '',\r\n ].filter(Boolean).join(' ');\r\n return `<script ${attrs}></script>`;\r\n }\r\n const typeAttr = s.type ? ` type=\"${escapeHtml(s.type)}\"` : '';\r\n return `<script${typeAttr}>${s.content ?? ''}</script>`;\r\n}\r\n\r\n/** Renders a StyleTag to an HTML string. */\r\nexport function renderStyleTag(s: StyleTag): string {\r\n if (s.href) return `<link rel=\"stylesheet\" href=\"${escapeHtml(s.href)}\" />`;\r\n return `<style>${s.content ?? ''}</style>`;\r\n}\r\n"],
5
- "mappings": "AAgBA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAyB3B,eAAsB,aAAa,UAAqC;AACtE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,cAAc,QAAQ,EAAE;AACjD,WAAQ,IAAI,YAAyB,CAAC;AAAA,EACxC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAWO,SAAS,cAAc,SAAyC;AACrE,QAAM,SAA6B,EAAE,OAAO,IAAI,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE;AACxE,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,MAAgB,QAAO,QAAQ,EAAE;AACvC,QAAI,EAAE,SAAS,OAAQ,QAAO,QAAQ,KAAK,GAAG,EAAE,OAAO;AACvD,QAAI,EAAE,QAAQ,OAAS,QAAO,OAAO,KAAK,GAAG,EAAE,MAAM;AAAA,EACvD;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,GAAsB;AACpD,MAAI,EAAE,KAAK;AACT,UAAM,QAAQ;AAAA,MACZ,QAAQ,WAAW,EAAE,GAAG,CAAC;AAAA,MACzB,EAAE,OAAQ,SAAS,WAAW,EAAE,IAAI,CAAC,MAAM;AAAA,MAC3C,EAAE,QAAQ,UAAW;AAAA,MACrB,EAAE,QAAQ,UAAW;AAAA,IACvB,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAC1B,WAAO,WAAW,KAAK;AAAA,EACzB;AACA,QAAM,WAAW,EAAE,OAAO,UAAU,WAAW,EAAE,IAAI,CAAC,MAAM;AAC5D,SAAO,UAAU,QAAQ,IAAI,EAAE,WAAW,EAAE;AAC9C;AAGO,SAAS,eAAe,GAAqB;AAClD,MAAI,EAAE,KAAM,QAAO,gCAAgC,WAAW,EAAE,IAAI,CAAC;AACrE,SAAO,UAAU,EAAE,WAAW,EAAE;AAClC;",
4
+ "sourcesContent": ["/**\r\n * metadata.ts \u2014 Legacy Metadata Helpers\r\n *\r\n * @deprecated Use the `useHtml()` hook instead.\r\n *\r\n * This module provided an earlier metadata API where pages exported a\r\n * `metadata` object alongside their default component. It is retained for\r\n * backwards compatibility but new projects should use useHtml().\r\n *\r\n * Example (old pattern):\r\n * export const metadata = {\r\n * title: 'My Page',\r\n * scripts: [{ src: '/analytics.js', defer: true }],\r\n * };\r\n */\r\n\r\nimport { pathToFileURL } from 'url';\r\nimport { escapeHtml } from './utils';\r\n// ScriptTag is fully compatible with html-store's definition.\r\nimport type { ScriptTag } from './html-store';\r\nexport type { ScriptTag };\r\n\r\n/**\r\n * StyleTag for the legacy metadata API.\r\n * Extends html-store's StyleTag with `href` for external stylesheet links,\r\n * which the metadata API supported but html-store's <style>-only type does not.\r\n */\r\nexport interface StyleTag {\r\n href?: string;\r\n content?: string;\r\n media?: string;\r\n}\r\n\r\nexport interface Metadata {\r\n title?: string;\r\n scripts?: ScriptTag[];\r\n styles?: StyleTag[];\r\n}\r\n\r\n/**\r\n * Dynamically imports a page/layout module and returns its exported `metadata`\r\n * object, or an empty object if none is found or the import fails.\r\n */\r\nexport async function loadMetadata(filePath: string): Promise<Metadata> {\r\n try {\r\n const mod = await import(pathToFileURL(filePath).href);\r\n return (mod.metadata as Metadata) ?? {};\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\n/**\r\n * Merges metadata from an array of modules in render order (outermost layout\r\n * first, page last).\r\n *\r\n * Merge strategy:\r\n * title \u2014 last non-empty value wins (page overrides layout)\r\n * scripts \u2014 concatenated in order\r\n * styles \u2014 concatenated in order\r\n */\r\nexport function mergeMetadata(ordered: Metadata[]): Required<Metadata> {\r\n const result: Required<Metadata> = { title: '', scripts: [], styles: [] };\r\n for (const m of ordered) {\r\n if (m.title) result.title = m.title;\r\n if (m.scripts?.length) result.scripts.push(...m.scripts);\r\n if (m.styles?.length) result.styles.push(...m.styles);\r\n }\r\n return result;\r\n}\r\n\r\n/** Renders a ScriptTag to an HTML string. */\r\nexport function renderScriptTag(s: ScriptTag): string {\r\n if (s.src) {\r\n const attrs = [\r\n `src=\"${escapeHtml(s.src)}\"`,\r\n s.type ? `type=\"${escapeHtml(s.type)}\"` : '',\r\n s.defer ? 'defer' : '',\r\n s.async ? 'async' : '',\r\n ].filter(Boolean).join(' ');\r\n return `<script ${attrs}></script>`;\r\n }\r\n const typeAttr = s.type ? ` type=\"${escapeHtml(s.type)}\"` : '';\r\n return `<script${typeAttr}>${s.content ?? ''}</script>`;\r\n}\r\n\r\n/** Renders a StyleTag to an HTML string. */\r\nexport function renderStyleTag(s: StyleTag): string {\r\n if (s.href) return `<link rel=\"stylesheet\" href=\"${escapeHtml(s.href)}\" />`;\r\n return `<style>${s.content ?? ''}</style>`;\r\n}"],
5
+ "mappings": "AAgBA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AA0B3B,eAAsB,aAAa,UAAqC;AACtE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,cAAc,QAAQ,EAAE;AACjD,WAAQ,IAAI,YAAyB,CAAC;AAAA,EACxC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAWO,SAAS,cAAc,SAAyC;AACrE,QAAM,SAA6B,EAAE,OAAO,IAAI,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE;AACxE,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,MAAO,QAAO,QAAQ,EAAE;AAC9B,QAAI,EAAE,SAAS,OAAQ,QAAO,QAAQ,KAAK,GAAG,EAAE,OAAO;AACvD,QAAI,EAAE,QAAQ,OAAQ,QAAO,OAAO,KAAK,GAAG,EAAE,MAAM;AAAA,EACtD;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,GAAsB;AACpD,MAAI,EAAE,KAAK;AACT,UAAM,QAAQ;AAAA,MACZ,QAAQ,WAAW,EAAE,GAAG,CAAC;AAAA,MACzB,EAAE,OAAO,SAAS,WAAW,EAAE,IAAI,CAAC,MAAM;AAAA,MAC1C,EAAE,QAAQ,UAAU;AAAA,MACpB,EAAE,QAAQ,UAAU;AAAA,IACtB,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAC1B,WAAO,WAAW,KAAK;AAAA,EACzB;AACA,QAAM,WAAW,EAAE,OAAO,UAAU,WAAW,EAAE,IAAI,CAAC,MAAM;AAC5D,SAAO,UAAU,QAAQ,IAAI,EAAE,WAAW,EAAE;AAC9C;AAGO,SAAS,eAAe,GAAqB;AAClD,MAAI,EAAE,KAAM,QAAO,gCAAgC,WAAW,EAAE,IAAI,CAAC;AACrE,SAAO,UAAU,EAAE,WAAW,EAAE;AAClC;",
6
6
  "names": []
7
7
  }
package/dist/renderer.js CHANGED
@@ -4,6 +4,35 @@ import { renderToString } from "react-dom/server";
4
4
  import { log } from "./logger.js";
5
5
  import { getComponentCache } from "./component-analyzer.js";
6
6
  import { escapeHtml } from "./utils.js";
7
+ function isWrapperAttr(key) {
8
+ return key === "className" || key === "style" || key === "id" || key.startsWith("data-") || key.startsWith("aria-");
9
+ }
10
+ function splitWrapperAttrs(props) {
11
+ const wrapperAttrs = {};
12
+ const componentProps = {};
13
+ for (const [key, value] of Object.entries(props || {})) {
14
+ if (isWrapperAttr(key)) wrapperAttrs[key] = value;
15
+ else componentProps[key] = value;
16
+ }
17
+ return { wrapperAttrs, componentProps };
18
+ }
19
+ function buildWrapperAttrString(attrs) {
20
+ const parts = Object.entries(attrs).map(([key, value]) => {
21
+ if (key === "className") key = "class";
22
+ if (key === "style" && typeof value === "object") {
23
+ const css = Object.entries(value).map(([k, v]) => {
24
+ const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
25
+ const safeVal = String(v).replace(/[<>"'`\\]/g, "");
26
+ return `${prop}:${safeVal}`;
27
+ }).join(";");
28
+ return `style="${css}"`;
29
+ }
30
+ if (typeof value === "boolean") return value ? key : "";
31
+ if (value == null) return "";
32
+ return `${key}="${escapeHtml(String(value))}"`;
33
+ }).filter(Boolean);
34
+ return parts.length ? " " + parts.join(" ") : "";
35
+ }
7
36
  async function renderElementToHtml(element, ctx) {
8
37
  if (element === null || element === void 0 || typeof element === "boolean") return "";
9
38
  if (typeof element === "string" || typeof element === "number")
@@ -54,10 +83,12 @@ async function renderFunctionComponent(type, props, ctx) {
54
83
  if (!info.exportedName || type.name !== info.exportedName) continue;
55
84
  try {
56
85
  ctx.hydrated.add(id);
57
- const serializedProps = serializePropsForHydration(props, ctx.registry);
86
+ const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
87
+ const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
88
+ const serializedProps = serializePropsForHydration(componentProps, ctx.registry);
58
89
  log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);
59
- const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, props));
60
- return `<span data-hydrate-id="${id}" data-hydrate-props="${escapeHtml(
90
+ const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, componentProps));
91
+ return `<span data-hydrate-id="${id}"${wrapperAttrStr} data-hydrate-props="${escapeHtml(
61
92
  JSON.stringify(serializedProps)
62
93
  )}">${html}</span>`;
63
94
  } catch (err) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/renderer.ts"],
4
- "sourcesContent": ["/**\r\n * renderer.ts \u2014 Dev-Mode Async SSR Renderer\r\n *\r\n * Implements a recursive async renderer used in `nuke dev` to convert a React\r\n * element tree into an HTML string. It is a lighter alternative to\r\n * react-dom/server.renderToString that:\r\n *\r\n * - Supports async server components (components that return Promises).\r\n * - Emits <span data-hydrate-id=\"\u2026\"> markers for \"use client\" boundaries\r\n * instead of trying to render them server-side without their browser APIs.\r\n * - Serializes props passed to client components into the marker's\r\n * data-hydrate-props attribute so the browser can reconstruct them.\r\n *\r\n * In production (nuke build), the equivalent renderer is inlined into each\r\n * page's standalone bundle by build-common.ts (makePageAdapterSource).\r\n *\r\n * RenderContext:\r\n * registry \u2014 Map<id, filePath> of all client components for this page.\r\n * Populated by component-analyzer.ts before rendering.\r\n * hydrated \u2014 Set<id> populated during render; used to tell the browser\r\n * which components to hydrate on this specific request.\r\n * skipClientSSR \u2014 When true (HMR request), client components emit an empty\r\n * marker instead of running renderToString (faster dev reload).\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement, Fragment } from 'react';\r\nimport { renderToString } from 'react-dom/server';\r\nimport { log } from './logger';\r\nimport { getComponentCache } from './component-analyzer';\r\nimport { escapeHtml } from './utils';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface RenderContext {\r\n /** id \u2192 absolute file path for every client component reachable from this page. */\r\n registry: Map<string, string>;\r\n /** Populated during render: IDs of client components actually encountered. */\r\n hydrated: Set<string>;\r\n /** When true, skip renderToString for client components (faster HMR). */\r\n skipClientSSR?: boolean;\r\n}\r\n\r\n// \u2500\u2500\u2500 Top-level renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively renders a React element (or primitive) to an HTML string.\r\n *\r\n * Handles:\r\n * null / undefined / boolean \u2192 ''\r\n * string / number \u2192 HTML-escaped text\r\n * array \u2192 rendered in parallel, joined\r\n * Fragment \u2192 renders children directly\r\n * HTML element string \u2192 renderHtmlElement()\r\n * function component \u2192 renderFunctionComponent()\r\n */\r\nexport async function renderElementToHtml(\r\n element: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n if (element === null || element === undefined || typeof element === 'boolean') return '';\r\n if (typeof element === 'string' || typeof element === 'number')\r\n return escapeHtml(String(element));\r\n\r\n if (Array.isArray(element)) {\r\n const parts = await Promise.all(element.map(e => renderElementToHtml(e, ctx)));\r\n return parts.join('');\r\n }\r\n\r\n if (!element.type) return '';\r\n\r\n const { type, props } = element;\r\n\r\n if (type === Fragment) return renderElementToHtml(props.children, ctx);\r\n if (typeof type === 'string') return renderHtmlElement(type, props, ctx);\r\n if (typeof type === 'function') return renderFunctionComponent(type, props, ctx);\r\n\r\n return '';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML element renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a native HTML element (e.g. `<div className=\"foo\">`).\r\n *\r\n * Attribute conversion:\r\n * className \u2192 class\r\n * htmlFor \u2192 for\r\n * style \u2192 converted from camelCase object to CSS string\r\n * boolean \u2192 omitted when false, rendered as name-only attribute when true\r\n * dangerouslySetInnerHTML \u2192 inner HTML set verbatim (no escaping)\r\n *\r\n * Void elements (img, br, input, etc.) are self-closed.\r\n */\r\nasync function renderHtmlElement(\r\n type: string,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const { children, ...attributes } = (props || {}) as Record<string, any>;\r\n\r\n const attrs = Object.entries(attributes as Record<string, any>)\r\n .map(([key, value]) => {\r\n // React prop name \u2192 HTML attribute name.\r\n if (key === 'className') key = 'class';\r\n if (key === 'htmlFor') key = 'for';\r\n if (key === 'dangerouslySetInnerHTML') return ''; // handled separately below\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n\r\n // camelCase style object \u2192 \"prop:value;\u2026\" CSS string.\r\n if (key === 'style' && typeof value === 'object') {\r\n const styleStr = Object.entries(value)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n // Strip characters that could break out of the attribute value.\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${styleStr}\"`;\r\n }\r\n\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const attrStr = attrs ? ` ${attrs}` : '';\r\n\r\n if (props?.dangerouslySetInnerHTML) {\r\n return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;\r\n }\r\n\r\n // Void elements cannot have children.\r\n if (['img', 'br', 'hr', 'input', 'meta', 'link'].includes(type)) {\r\n return `<${type}${attrStr} />`;\r\n }\r\n\r\n const childrenHtml = children ? await renderElementToHtml(children, ctx) : '';\r\n return `<${type}${attrStr}>${childrenHtml}</${type}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Function component renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a function (or class) component.\r\n *\r\n * Client boundary detection:\r\n * The component cache maps file paths to ComponentInfo. We match the\r\n * component's function name against the default export of each registered\r\n * client file to determine whether this component is a client boundary.\r\n *\r\n * If it is, we emit a hydration marker and optionally run renderToString\r\n * to produce the initial HTML inside the marker (skipped when skipClientSSR\r\n * is set, e.g. during HMR navigation).\r\n *\r\n * Class components:\r\n * Instantiated via `new type(props)` and their render() method called.\r\n *\r\n * Async components:\r\n * Awaited if the return value is a Promise (standard server component pattern).\r\n */\r\nasync function renderFunctionComponent(\r\n type: Function,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const componentCache = getComponentCache();\r\n\r\n // Check whether this component function is a registered client component.\r\n for (const [id, filePath] of ctx.registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n\r\n // Match by default export function name (cached \u2014 handles both source and\r\n // esbuild-compiled formats; see component-analyzer.getExportedDefaultName).\r\n if (!info.exportedName || type.name !== info.exportedName) continue;\r\n\r\n // This is a client boundary.\r\n try {\r\n ctx.hydrated.add(id);\r\n const serializedProps = serializePropsForHydration(props, ctx.registry);\r\n log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);\r\n\r\n // Optionally SSR the component so the initial HTML is meaningful\r\n // (improves perceived performance and avoids layout shift).\r\n const html = ctx.skipClientSSR\r\n ? ''\r\n : renderToString(createElement(type as React.ComponentType<any>, props));\r\n\r\n return `<span data-hydrate-id=\"${id}\" data-hydrate-props=\"${escapeHtml(\r\n JSON.stringify(serializedProps),\r\n )}\">${html}</span>`;\r\n } catch (err) {\r\n log.error('Error rendering client component:', err);\r\n return `<div style=\"color:red\">Error rendering client component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n }\r\n\r\n // Server component \u2014 call it and recurse into the result.\r\n try {\r\n const result = type(props);\r\n const resolved = result?.then ? await result : result;\r\n return renderElementToHtml(resolved, ctx);\r\n } catch (err) {\r\n log.error('Error rendering component:', err);\r\n return `<div style=\"color:red\">Error rendering component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Prop serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts props into a JSON-serializable form for the data-hydrate-props\r\n * attribute. React elements inside props are serialized to a tagged object\r\n * format ({ __re: 'html'|'client', \u2026 }) that the browser's reconstructElement\r\n * function (in bundle.ts) can turn back into real React elements.\r\n *\r\n * Functions are dropped (cannot be serialized).\r\n */\r\nfunction serializePropsForHydration(\r\n props: any,\r\n registry: Map<string, string>,\r\n): any {\r\n if (!props || typeof props !== 'object') return props;\r\n const out: any = {};\r\n for (const [key, value] of Object.entries(props as Record<string, any>)) {\r\n const s = serializeValue(value, registry);\r\n if (s !== undefined) out[key] = s;\r\n }\r\n return out;\r\n}\r\n\r\nfunction serializeValue(value: any, registry: Map<string, string>): any {\r\n if (value === null || value === undefined) return value;\r\n if (typeof value === 'function') return undefined; // not serializable\r\n if (typeof value !== 'object') return value;\r\n if (Array.isArray(value))\r\n return value.map(v => serializeValue(v, registry)).filter(v => v !== undefined);\r\n if ((value as any).$$typeof)\r\n return serializeReactElement(value, registry);\r\n\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeValue(v, registry);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Serializes a React element to its wire format:\r\n * Native element \u2192 { __re: 'html', tag, props }\r\n * Client component \u2192 { __re: 'client', componentId, props }\r\n * Server component \u2192 undefined (cannot be serialized)\r\n */\r\nfunction serializeReactElement(element: any, registry: Map<string, string>): any {\r\n const { type, props } = element;\r\n\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializePropsForHydration(props, registry) };\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const componentCache = getComponentCache();\r\n for (const [id, filePath] of registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n if (info.exportedName && type.name === info.exportedName) {\r\n return {\r\n __re: 'client',\r\n componentId: id,\r\n props: serializePropsForHydration(props, registry),\r\n };\r\n }\r\n }\r\n }\r\n\r\n return undefined; // Server component \u2014 not serializable\r\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\r\n * renderer.ts \u2014 Dev-Mode Async SSR Renderer\r\n *\r\n * Implements a recursive async renderer used in `nuke dev` to convert a React\r\n * element tree into an HTML string. It is a lighter alternative to\r\n * react-dom/server.renderToString that:\r\n *\r\n * - Supports async server components (components that return Promises).\r\n * - Emits <span data-hydrate-id=\"\u2026\"> markers for \"use client\" boundaries\r\n * instead of trying to render them server-side without their browser APIs.\r\n * - Serializes props passed to client components into the marker's\r\n * data-hydrate-props attribute so the browser can reconstruct them.\r\n *\r\n * In production (nuke build), the equivalent renderer is inlined into each\r\n * page's standalone bundle by build-common.ts (makePageAdapterSource).\r\n *\r\n * RenderContext:\r\n * registry \u2014 Map<id, filePath> of all client components for this page.\r\n * Populated by component-analyzer.ts before rendering.\r\n * hydrated \u2014 Set<id> populated during render; used to tell the browser\r\n * which components to hydrate on this specific request.\r\n * skipClientSSR \u2014 When true (HMR request), client components emit an empty\r\n * marker instead of running renderToString (faster dev reload).\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement, Fragment } from 'react';\r\nimport { renderToString } from 'react-dom/server';\r\nimport { log } from './logger';\r\nimport { getComponentCache } from './component-analyzer';\r\nimport { escapeHtml } from './utils';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface RenderContext {\r\n /** id \u2192 absolute file path for every client component reachable from this page. */\r\n registry: Map<string, string>;\r\n /** Populated during render: IDs of client components actually encountered. */\r\n hydrated: Set<string>;\r\n /** When true, skip renderToString for client components (faster HMR). */\r\n skipClientSSR?: boolean;\r\n}\r\n\r\n// \u2500\u2500\u2500 Wrapper attribute helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Attributes that belong on the hydration <span> wrapper rather than being\r\n * forwarded to the inner client component. Includes className, style, id,\r\n * and any data-* / aria-* attributes.\r\n */\r\nfunction isWrapperAttr(key: string): boolean {\r\n return (\r\n key === 'className' ||\r\n key === 'style' ||\r\n key === 'id' ||\r\n key.startsWith('data-') ||\r\n key.startsWith('aria-')\r\n );\r\n}\r\n\r\n/**\r\n * Splits props into two bags:\r\n * wrapperAttrs \u2014 keys destined for the <span> (className, style, id, data-*, aria-*)\r\n * componentProps \u2014 everything else, forwarded to the actual component\r\n */\r\nfunction splitWrapperAttrs(props: any): {\r\n wrapperAttrs: Record<string, any>;\r\n componentProps: Record<string, any>;\r\n} {\r\n const wrapperAttrs: Record<string, any> = {};\r\n const componentProps: Record<string, any> = {};\r\n for (const [key, value] of Object.entries((props || {}) as Record<string, any>)) {\r\n if (isWrapperAttr(key)) wrapperAttrs[key] = value;\r\n else componentProps[key] = value;\r\n }\r\n return { wrapperAttrs, componentProps };\r\n}\r\n\r\n/**\r\n * Converts a wrapper-attrs bag into an HTML attribute string (leading space\r\n * included when non-empty) suitable for direct interpolation into a tag.\r\n *\r\n * className \u2192 class\r\n * style obj \u2192 \"prop:value;\u2026\" CSS string\r\n */\r\nfunction buildWrapperAttrString(attrs: Record<string, any>): string {\r\n const parts = Object.entries(attrs)\r\n .map(([key, value]) => {\r\n if (key === 'className') key = 'class';\r\n\r\n if (key === 'style' && typeof value === 'object') {\r\n const css = Object.entries(value as Record<string, any>)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${css}\"`;\r\n }\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n if (value == null) return '';\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean);\r\n\r\n return parts.length ? ' ' + parts.join(' ') : '';\r\n}\r\n\r\n// \u2500\u2500\u2500 Top-level renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively renders a React element (or primitive) to an HTML string.\r\n *\r\n * Handles:\r\n * null / undefined / boolean \u2192 ''\r\n * string / number \u2192 HTML-escaped text\r\n * array \u2192 rendered in parallel, joined\r\n * Fragment \u2192 renders children directly\r\n * HTML element string \u2192 renderHtmlElement()\r\n * function component \u2192 renderFunctionComponent()\r\n */\r\nexport async function renderElementToHtml(\r\n element: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n if (element === null || element === undefined || typeof element === 'boolean') return '';\r\n if (typeof element === 'string' || typeof element === 'number')\r\n return escapeHtml(String(element));\r\n\r\n if (Array.isArray(element)) {\r\n const parts = await Promise.all(element.map(e => renderElementToHtml(e, ctx)));\r\n return parts.join('');\r\n }\r\n\r\n if (!element.type) return '';\r\n\r\n const { type, props } = element;\r\n\r\n if (type === Fragment) return renderElementToHtml(props.children, ctx);\r\n if (typeof type === 'string') return renderHtmlElement(type, props, ctx);\r\n if (typeof type === 'function') return renderFunctionComponent(type, props, ctx);\r\n\r\n return '';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML element renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a native HTML element (e.g. `<div className=\"foo\">`).\r\n *\r\n * Attribute conversion:\r\n * className \u2192 class\r\n * htmlFor \u2192 for\r\n * style \u2192 converted from camelCase object to CSS string\r\n * boolean \u2192 omitted when false, rendered as name-only attribute when true\r\n * dangerouslySetInnerHTML \u2192 inner HTML set verbatim (no escaping)\r\n *\r\n * Void elements (img, br, input, etc.) are self-closed.\r\n */\r\nasync function renderHtmlElement(\r\n type: string,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const { children, ...attributes } = (props || {}) as Record<string, any>;\r\n\r\n const attrs = Object.entries(attributes as Record<string, any>)\r\n .map(([key, value]) => {\r\n // React prop name \u2192 HTML attribute name.\r\n if (key === 'className') key = 'class';\r\n if (key === 'htmlFor') key = 'for';\r\n if (key === 'dangerouslySetInnerHTML') return ''; // handled separately below\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n\r\n // camelCase style object \u2192 \"prop:value;\u2026\" CSS string.\r\n if (key === 'style' && typeof value === 'object') {\r\n const styleStr = Object.entries(value)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n // Strip characters that could break out of the attribute value.\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${styleStr}\"`;\r\n }\r\n\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const attrStr = attrs ? ` ${attrs}` : '';\r\n\r\n if (props?.dangerouslySetInnerHTML) {\r\n return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;\r\n }\r\n\r\n // Void elements cannot have children.\r\n if (['img', 'br', 'hr', 'input', 'meta', 'link'].includes(type)) {\r\n return `<${type}${attrStr} />`;\r\n }\r\n\r\n const childrenHtml = children ? await renderElementToHtml(children, ctx) : '';\r\n return `<${type}${attrStr}>${childrenHtml}</${type}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Function component renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a function (or class) component.\r\n *\r\n * Client boundary detection:\r\n * The component cache maps file paths to ComponentInfo. We match the\r\n * component's function name against the default export of each registered\r\n * client file to determine whether this component is a client boundary.\r\n *\r\n * If it is, we emit a hydration marker and optionally run renderToString\r\n * to produce the initial HTML inside the marker (skipped when skipClientSSR\r\n * is set, e.g. during HMR navigation).\r\n *\r\n * Class components:\r\n * Instantiated via `new type(props)` and their render() method called.\r\n *\r\n * Async components:\r\n * Awaited if the return value is a Promise (standard server component pattern).\r\n */\r\nasync function renderFunctionComponent(\r\n type: Function,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const componentCache = getComponentCache();\r\n\r\n // Check whether this component function is a registered client component.\r\n for (const [id, filePath] of ctx.registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n\r\n // Match by default export function name (cached \u2014 handles both source and\r\n // esbuild-compiled formats; see component-analyzer.getExportedDefaultName).\r\n if (!info.exportedName || type.name !== info.exportedName) continue;\r\n\r\n // This is a client boundary.\r\n try {\r\n ctx.hydrated.add(id);\r\n\r\n // Split props: wrapper attrs go on the <span>, the rest reach the component.\r\n const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);\r\n const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);\r\n const serializedProps = serializePropsForHydration(componentProps, ctx.registry);\r\n log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);\r\n\r\n // Optionally SSR the component so the initial HTML is meaningful\r\n // (improves perceived performance and avoids layout shift).\r\n const html = ctx.skipClientSSR\r\n ? ''\r\n : renderToString(createElement(type as React.ComponentType<any>, componentProps));\r\n\r\n return `<span data-hydrate-id=\"${id}\"${wrapperAttrStr} data-hydrate-props=\"${escapeHtml(\r\n JSON.stringify(serializedProps),\r\n )}\">${html}</span>`;\r\n } catch (err) {\r\n log.error('Error rendering client component:', err);\r\n return `<div style=\"color:red\">Error rendering client component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n }\r\n\r\n // Server component \u2014 call it and recurse into the result.\r\n try {\r\n const result = type(props);\r\n const resolved = result?.then ? await result : result;\r\n return renderElementToHtml(resolved, ctx);\r\n } catch (err) {\r\n log.error('Error rendering component:', err);\r\n return `<div style=\"color:red\">Error rendering component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Prop serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts props into a JSON-serializable form for the data-hydrate-props\r\n * attribute. React elements inside props are serialized to a tagged object\r\n * format ({ __re: 'html'|'client', \u2026 }) that the browser's reconstructElement\r\n * function (in bundle.ts) can turn back into real React elements.\r\n *\r\n * Functions are dropped (cannot be serialized).\r\n */\r\nfunction serializePropsForHydration(\r\n props: any,\r\n registry: Map<string, string>,\r\n): any {\r\n if (!props || typeof props !== 'object') return props;\r\n const out: any = {};\r\n for (const [key, value] of Object.entries(props as Record<string, any>)) {\r\n const s = serializeValue(value, registry);\r\n if (s !== undefined) out[key] = s;\r\n }\r\n return out;\r\n}\r\n\r\nfunction serializeValue(value: any, registry: Map<string, string>): any {\r\n if (value === null || value === undefined) return value;\r\n if (typeof value === 'function') return undefined; // not serializable\r\n if (typeof value !== 'object') return value;\r\n if (Array.isArray(value))\r\n return value.map(v => serializeValue(v, registry)).filter(v => v !== undefined);\r\n if ((value as any).$$typeof)\r\n return serializeReactElement(value, registry);\r\n\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeValue(v, registry);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Serializes a React element to its wire format:\r\n * Native element \u2192 { __re: 'html', tag, props }\r\n * Client component \u2192 { __re: 'client', componentId, props }\r\n * Server component \u2192 undefined (cannot be serialized)\r\n */\r\nfunction serializeReactElement(element: any, registry: Map<string, string>): any {\r\n const { type, props } = element;\r\n\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializePropsForHydration(props, registry) };\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const componentCache = getComponentCache();\r\n for (const [id, filePath] of registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n if (info.exportedName && type.name === info.exportedName) {\r\n return {\r\n __re: 'client',\r\n componentId: id,\r\n props: serializePropsForHydration(props, registry),\r\n };\r\n }\r\n }\r\n }\r\n\r\n return undefined; // Server component \u2014 not serializable\r\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }