{"version":3,"file":"stream-Cs9yOnvR.cjs","names":[],"sources":["../../src/ssr/stream.ts"],"sourcesContent":["import type { NixTemplate } from \"@deijose/nix-js\";\nimport { renderToString } from \"../render/render-to-string.js\";\nimport { documentShell } from \"../build/document-shell.js\";\nimport type { PageRoute, ScannedRoutes } from \"../router/route-scanner.js\";\nimport type { BuildConfig } from \"../build/build.js\";\nimport type { PageDataLoad } from \"../types.js\";\nimport { matchRoute } from \"./match.js\";\nimport { renderPage } from \"./render.js\";\n\nexport interface StreamingPageOptions {\n  route: PageRoute;\n  params: Record<string, string | string[]>;\n  searchParams: URLSearchParams;\n  config: Pick<BuildConfig, \"lang\" | \"clientEntry\">;\n  importer?: (path: string) => Promise<unknown>;\n  actions?: Record<string, string[]>;\n  request?: Request;\n}\n\nconst defaultImport = (path: string) => import(path);\n\n/** Builds the concrete URL path for a route pattern given matched params. */\nfunction buildConcretePath(\n  routePath: string,\n  params: Record<string, string | string[]>,\n): string {\n  return routePath.replace(/:([a-zA-Z0-9_]+)(\\*)?/g, (_m, name: string, catchAll?: string) => {\n    const value = params[name];\n    if (value === undefined || value === null) return \"\";\n    return catchAll ? (Array.isArray(value) ? value.join(\"/\") : String(value)) : String(value);\n  });\n}\n\nfunction streamingScript(page: string, search: string): string {\n  const src = `\n    async function __nixJsStreamRender() {\n      try {\n        const url = \"/__nix-js/render?page=\" + encodeURIComponent(${JSON.stringify(page)}) + \"&search=\" + encodeURIComponent(${JSON.stringify(search)});\n        const res = await fetch(url);\n        if (!res.ok) throw new Error(\"Streaming render failed: \" + res.status);\n        const html = await res.text();\n        const app = document.getElementById(\"app\");\n        if (app) app.innerHTML = html;\n        document.dispatchEvent(new CustomEvent(\"nix-js:rendered\"));\n      } catch (err) {\n        console.error(\"[nix-js-kit] streaming render failed\", err);\n      }\n    }\n    __nixJsStreamRender();\n  `;\n  return `<script type=\"module\">${src}</script>`;\n}\n\n/**\n * Render a page shell that shows the loading boundary while the real content\n * is fetched and injected by the client.\n */\nexport async function renderStreamingPage(options: StreamingPageOptions): Promise<string> {\n  const { route, params, searchParams, config, importer = defaultImport, actions } = options;\n  if (!route.loadingPath) {\n    throw new Error(\"Cannot stream a page without a loading.ts boundary\");\n  }\n\n  const { default: Loading } = (await importer(route.loadingPath)) as {\n    default: () => NixTemplate;\n  };\n\n  const loadingBody = await renderToString(() => Loading());\n  const concretePath = buildConcretePath(route.path, params);\n  const body = `<div id=\"nix-js-loading\">${loadingBody}</div>${streamingScript(concretePath, searchParams.toString())}`;\n\n  // Apply <html> attributes and head scripts (e.g. data-theme and the no-flash\n  // theme script) from the root layout loader so the shell paints correctly\n  // before the real content arrives.\n  const htmlAttributes: Record<string, string> = {};\n  const headScripts: string[] = [];\n  const headLinks: string[] = [];\n  if (route.layouts.length > 0) {\n    const rootLayout = route.layouts[0];\n    const dataPath = rootLayout.replace(/layout\\.ts$/, \"layout.data.ts\");\n    if (dataPath !== rootLayout) {\n      try {\n        const mod = (await importer(dataPath)) as { load?: PageDataLoad };\n        const layoutData = mod.load ? await mod.load({ params, searchParams, request: options.request }) : undefined;\n        if (layoutData && typeof layoutData === \"object\") {\n          const attrs = (layoutData as { htmlAttributes?: Record<string, string> }).htmlAttributes;\n          if (attrs) Object.assign(htmlAttributes, attrs);\n          const scripts = (layoutData as { headScripts?: string[] }).headScripts;\n          if (Array.isArray(scripts)) headScripts.push(...scripts);\n          const links = (layoutData as { headLinks?: string[] }).headLinks;\n          if (Array.isArray(links)) headLinks.push(...links);\n        }\n      } catch {\n        // The root layout loader is optional; ignore failures here.\n      }\n    }\n  }\n\n  return documentShell({\n    title: \"Loading...\",\n    lang: config.lang,\n    body,\n    data: { __nix_js_streaming: true, page: route.path },\n    actions,\n    htmlAttributes,\n    headScripts,\n    headLinks,\n    clientEntry: config.clientEntry,\n  });\n}\n\nexport interface RenderPageBodyOptions {\n  routes: ScannedRoutes;\n  pathname: string;\n  searchParams: URLSearchParams;\n  config: Pick<BuildConfig, \"lang\" | \"clientEntry\">;\n  actions?: Record<string, string[]>;\n  importer?: (path: string) => Promise<unknown>;\n  request?: Request;\n}\n\nexport interface RenderPageBodyResult {\n  /** Inner HTML body for the page (without the document shell). */\n  body: string;\n  /** Page title extracted from the rendered shell. */\n  title: string;\n  /** Full rendered document shell (used for ISR caching). */\n  fullHtml?: string;\n  /** `Set-Cookie` value that clears a consumed action error cookie. */\n  clearActionErrorCookie?: string;\n  /** `<head>` tags (title, meta, OG, twitter) for the SPA router to merge. */\n  head?: string;\n  /** First-class Response when a loader threw one (A-22). */\n  response?: Response;\n}\n\n/** Thrown by `renderPageBody` when the requested path has no matching route. */\nexport class RouteNotFoundError extends Error {\n  constructor(pathname: string) {\n    super(`No route found for ${pathname}`);\n    this.name = \"RouteNotFoundError\";\n  }\n}\n\n/**\n * Render only the inner HTML body for a page. Used by the streaming endpoint\n * to inject the real content into the shell.\n */\nexport async function renderPageBody(options: RenderPageBodyOptions): Promise<RenderPageBodyResult> {\n  const { routes, pathname, searchParams, config, actions, importer = defaultImport, request } = options;\n  const match = matchRoute(pathname, routes.pages);\n  if (!match) {\n    throw new RouteNotFoundError(pathname);\n  }\n\n  const result = await renderPage({\n    route: match.route,\n    params: match.params,\n    searchParams,\n    config,\n    actions,\n    importer,\n    request,\n  });\n\n  // If a loader threw a Response (redirect, 404, etc.), propagate it (A-22).\n  if (result.response) {\n    return {\n      body: \"\",\n      title: \"\",\n      response: result.response,\n    };\n  }\n\n  const bodyMatch = result.html.match(/<div id=\"app\">([\\s\\S]*)<\\/div>\\s*(<script|$)/);\n  const body = bodyMatch ? bodyMatch[1].trim() : result.html;\n  const titleMatch = result.html.match(/<title[^>]*>([^<]*)<\\/title>/);\n  return {\n    body,\n    title: titleMatch ? titleMatch[1] : result.resolvedTitle ?? \"\",\n    fullHtml: result.html,\n    clearActionErrorCookie: result.clearActionErrorCookie,\n    head: result.head,\n  };\n}\n"],"mappings":"oKAmBM,EAAiB,GAAiB,OAAO,GAG/C,SAAS,EACP,EACA,EACQ,CACR,OAAO,EAAU,QAAQ,0BAA2B,EAAI,EAAc,IAAsB,CAC1F,IAAM,EAAQ,EAAO,GAErB,OADI,GAAiC,KAAa,GAC3C,GAAY,MAAM,QAAQ,CAAK,EAAI,EAAM,KAAK,GAAG,EAAI,OAAO,CAAK,CAC1E,CAAC,CACH,CAEA,SAAS,EAAgB,EAAc,EAAwB,CAiB7D,MAAO,yBAAyB;;;oEAbkC,KAAK,UAAU,CAAI,EAAE,sCAAsC,KAAK,UAAU,CAAM,EAAE;;;;;;;;;;;;IAahH,WACtC,CAMA,eAAsB,EAAoB,EAAgD,CACxF,GAAM,CAAE,QAAO,SAAQ,eAAc,SAAQ,WAAW,EAAe,WAAY,EACnF,GAAI,CAAC,EAAM,YACT,MAAU,MAAM,oDAAoD,EAGtE,GAAM,CAAE,QAAS,GAAa,MAAM,EAAS,EAAM,WAAW,EAMxD,EAAO,4BAA4B,MAFf,EAAA,MAAqB,EAAQ,CAAC,EAEH,QAAQ,EADxC,EAAkB,EAAM,KAAM,CAC0B,EAAc,EAAa,SAAS,CAAC,IAK5G,EAAyC,CAAC,EAC1C,EAAwB,CAAC,EACzB,EAAsB,CAAC,EAC7B,GAAI,EAAM,QAAQ,OAAS,EAAG,CAC5B,IAAM,EAAa,EAAM,QAAQ,GAC3B,EAAW,EAAW,QAAQ,cAAe,gBAAgB,EACnE,GAAI,IAAa,EACf,GAAI,CACF,IAAM,EAAO,MAAM,EAAS,CAAQ,EAC9B,EAAa,EAAI,KAAO,MAAM,EAAI,KAAK,CAAE,SAAQ,eAAc,QAAS,EAAQ,OAAQ,CAAC,EAAI,IAAA,GACnG,GAAI,GAAc,OAAO,GAAe,SAAU,CAChD,IAAM,EAAS,EAA2D,eACtE,GAAO,OAAO,OAAO,EAAgB,CAAK,EAC9C,IAAM,EAAW,EAA0C,YACvD,MAAM,QAAQ,CAAO,GAAG,EAAY,KAAK,GAAG,CAAO,EACvD,IAAM,EAAS,EAAwC,UACnD,MAAM,QAAQ,CAAK,GAAG,EAAU,KAAK,GAAG,CAAK,CACnD,CACF,MAAQ,CAER,CAEJ,CAEA,OAAO,EAAA,EAAc,CACnB,MAAO,aACP,KAAM,EAAO,KACb,OACA,KAAM,CAAE,mBAAoB,GAAM,KAAM,EAAM,IAAK,EACnD,UACA,iBACA,cACA,YACA,YAAa,EAAO,WACtB,CAAC,CACH,CA4BA,IAAa,EAAb,cAAwC,KAAM,CAC5C,YAAY,EAAkB,CAC5B,MAAM,sBAAsB,GAAU,EACtC,KAAK,KAAO,oBACd,CACF,EAMA,eAAsB,EAAe,EAA+D,CAClG,GAAM,CAAE,SAAQ,WAAU,eAAc,SAAQ,UAAS,WAAW,EAAe,WAAY,EACzF,EAAQ,EAAA,EAAW,EAAU,EAAO,KAAK,EAC/C,GAAI,CAAC,EACH,MAAM,IAAI,EAAmB,CAAQ,EAGvC,IAAM,EAAS,MAAM,EAAA,EAAW,CAC9B,MAAO,EAAM,MACb,OAAQ,EAAM,OACd,eACA,SACA,UACA,WACA,SACF,CAAC,EAGD,GAAI,EAAO,SACT,MAAO,CACL,KAAM,GACN,MAAO,GACP,SAAU,EAAO,QACnB,EAGF,IAAM,EAAY,EAAO,KAAK,MAAM,8CAA8C,EAC5E,EAAO,EAAY,EAAU,EAAE,CAAC,KAAK,EAAI,EAAO,KAChD,EAAa,EAAO,KAAK,MAAM,8BAA8B,EACnE,MAAO,CACL,OACA,MAAO,EAAa,EAAW,GAAK,EAAO,eAAiB,GAC5D,SAAU,EAAO,KACjB,uBAAwB,EAAO,uBAC/B,KAAM,EAAO,IACf,CACF"}