@pyreon/server 0.3.1 → 0.4.0

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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"96b528cd-1","name":"html.ts"},{"uid":"96b528cd-3","name":"handler.ts"},{"uid":"96b528cd-5","name":"island.ts"},{"uid":"96b528cd-7","name":"ssg.ts"},{"uid":"96b528cd-9","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"96b528cd-1":{"renderedLength":1378,"gzipLength":733,"brotliLength":0,"metaUid":"96b528cd-0"},"96b528cd-3":{"renderedLength":2783,"gzipLength":1221,"brotliLength":0,"metaUid":"96b528cd-2"},"96b528cd-5":{"renderedLength":1444,"gzipLength":673,"brotliLength":0,"metaUid":"96b528cd-4"},"96b528cd-7":{"renderedLength":2806,"gzipLength":1214,"brotliLength":0,"metaUid":"96b528cd-6"},"96b528cd-9":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"96b528cd-8"}},"nodeMetas":{"96b528cd-0":{"id":"/src/html.ts","moduleParts":{"index.js":"96b528cd-1"},"imported":[],"importedBy":[{"uid":"96b528cd-8"},{"uid":"96b528cd-2"}]},"96b528cd-2":{"id":"/src/handler.ts","moduleParts":{"index.js":"96b528cd-3"},"imported":[{"uid":"96b528cd-10"},{"uid":"96b528cd-11"},{"uid":"96b528cd-12"},{"uid":"96b528cd-13"},{"uid":"96b528cd-0"}],"importedBy":[{"uid":"96b528cd-8"}]},"96b528cd-4":{"id":"/src/island.ts","moduleParts":{"index.js":"96b528cd-5"},"imported":[{"uid":"96b528cd-10"}],"importedBy":[{"uid":"96b528cd-8"}]},"96b528cd-6":{"id":"/src/ssg.ts","moduleParts":{"index.js":"96b528cd-7"},"imported":[{"uid":"96b528cd-14"},{"uid":"96b528cd-15"}],"importedBy":[{"uid":"96b528cd-8"}]},"96b528cd-8":{"id":"/src/index.ts","moduleParts":{"index.js":"96b528cd-9"},"imported":[{"uid":"96b528cd-2"},{"uid":"96b528cd-0"},{"uid":"96b528cd-4"},{"uid":"96b528cd-6"}],"importedBy":[],"isEntry":true},"96b528cd-10":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"96b528cd-2"},{"uid":"96b528cd-4"}]},"96b528cd-11":{"id":"@pyreon/head","moduleParts":{},"imported":[],"importedBy":[{"uid":"96b528cd-2"}]},"96b528cd-12":{"id":"@pyreon/router","moduleParts":{},"imported":[],"importedBy":[{"uid":"96b528cd-2"}]},"96b528cd-13":{"id":"@pyreon/runtime-server","moduleParts":{},"imported":[],"importedBy":[{"uid":"96b528cd-2"}]},"96b528cd-14":{"id":"node:fs/promises","moduleParts":{},"imported":[],"importedBy":[{"uid":"96b528cd-6"}]},"96b528cd-15":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"96b528cd-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"c9e26545-1","name":"html.ts"},{"uid":"c9e26545-3","name":"handler.ts"},{"uid":"c9e26545-5","name":"island.ts"},{"uid":"c9e26545-7","name":"ssg.ts"},{"uid":"c9e26545-9","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"c9e26545-1":{"renderedLength":2797,"gzipLength":1166,"brotliLength":0,"metaUid":"c9e26545-0"},"c9e26545-3":{"renderedLength":2691,"gzipLength":1189,"brotliLength":0,"metaUid":"c9e26545-2"},"c9e26545-5":{"renderedLength":1444,"gzipLength":673,"brotliLength":0,"metaUid":"c9e26545-4"},"c9e26545-7":{"renderedLength":2806,"gzipLength":1214,"brotliLength":0,"metaUid":"c9e26545-6"},"c9e26545-9":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"c9e26545-8"}},"nodeMetas":{"c9e26545-0":{"id":"/src/html.ts","moduleParts":{"index.js":"c9e26545-1"},"imported":[],"importedBy":[{"uid":"c9e26545-8"},{"uid":"c9e26545-2"}]},"c9e26545-2":{"id":"/src/handler.ts","moduleParts":{"index.js":"c9e26545-3"},"imported":[{"uid":"c9e26545-10"},{"uid":"c9e26545-11"},{"uid":"c9e26545-12"},{"uid":"c9e26545-13"},{"uid":"c9e26545-0"}],"importedBy":[{"uid":"c9e26545-8"}]},"c9e26545-4":{"id":"/src/island.ts","moduleParts":{"index.js":"c9e26545-5"},"imported":[{"uid":"c9e26545-10"}],"importedBy":[{"uid":"c9e26545-8"}]},"c9e26545-6":{"id":"/src/ssg.ts","moduleParts":{"index.js":"c9e26545-7"},"imported":[{"uid":"c9e26545-14"},{"uid":"c9e26545-15"}],"importedBy":[{"uid":"c9e26545-8"}]},"c9e26545-8":{"id":"/src/index.ts","moduleParts":{"index.js":"c9e26545-9"},"imported":[{"uid":"c9e26545-2"},{"uid":"c9e26545-0"},{"uid":"c9e26545-4"},{"uid":"c9e26545-6"}],"importedBy":[],"isEntry":true},"c9e26545-10":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"c9e26545-2"},{"uid":"c9e26545-4"}]},"c9e26545-11":{"id":"@pyreon/head","moduleParts":{},"imported":[],"importedBy":[{"uid":"c9e26545-2"}]},"c9e26545-12":{"id":"@pyreon/router","moduleParts":{},"imported":[],"importedBy":[{"uid":"c9e26545-2"}]},"c9e26545-13":{"id":"@pyreon/runtime-server","moduleParts":{},"imported":[],"importedBy":[{"uid":"c9e26545-2"}]},"c9e26545-14":{"id":"node:fs/promises","moduleParts":{},"imported":[],"importedBy":[{"uid":"c9e26545-6"}]},"c9e26545-15":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"c9e26545-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -26,9 +26,31 @@ const DEFAULT_TEMPLATE = `<!DOCTYPE html>
26
26
  <!--pyreon-scripts-->
27
27
  </body>
28
28
  </html>`;
29
+ function compileTemplate(template) {
30
+ if (!template.includes("<!--pyreon-app-->")) throw new Error("[pyreon/server] Template must contain <!--pyreon-app--> placeholder");
31
+ const [beforeHead, afterHead] = splitOnce(template, "<!--pyreon-head-->");
32
+ const [betweenHeadApp, afterApp] = splitOnce(afterHead, "<!--pyreon-app-->");
33
+ const [betweenAppScripts, afterScripts] = splitOnce(afterApp, "<!--pyreon-scripts-->");
34
+ return { parts: [
35
+ beforeHead,
36
+ betweenHeadApp,
37
+ betweenAppScripts,
38
+ afterScripts
39
+ ] };
40
+ }
41
+ function splitOnce(str, delimiter) {
42
+ const idx = str.indexOf(delimiter);
43
+ if (idx === -1) return [str, ""];
44
+ return [str.slice(0, idx), str.slice(idx + delimiter.length)];
45
+ }
29
46
  function processTemplate(template, data) {
30
47
  return template.replace("<!--pyreon-head-->", data.head).replace("<!--pyreon-app-->", data.app).replace("<!--pyreon-scripts-->", data.scripts);
31
48
  }
49
+ /** Fast path using a pre-compiled template */
50
+ function processCompiledTemplate(compiled, data) {
51
+ const [p0, p1, p2, p3] = compiled.parts;
52
+ return p0 + data.head + p1 + data.app + p2 + data.scripts + p3;
53
+ }
32
54
  /**
33
55
  * Build the script tags for client hydration.
34
56
  *
@@ -45,11 +67,22 @@ function buildScripts(clientEntry, loaderData) {
45
67
  parts.push(`<script type="module" src="${clientEntry}"><\/script>`);
46
68
  return parts.join("\n ");
47
69
  }
70
+ /** Pre-build the static client entry script tag (invariant across requests) */
71
+ function buildClientEntryTag(clientEntry) {
72
+ return `<script type="module" src="${clientEntry}"><\/script>`;
73
+ }
74
+ /** Fast path: build scripts with a pre-built client entry tag */
75
+ function buildScriptsFast(clientEntryTag, loaderData) {
76
+ if (loaderData && Object.keys(loaderData).length > 0) return `<script>window.__PYREON_LOADER_DATA__=${JSON.stringify(loaderData).replace(/<\//g, "<\\/")}<\/script>\n ${clientEntryTag}`;
77
+ return clientEntryTag;
78
+ }
48
79
 
49
80
  //#endregion
50
81
  //#region src/handler.ts
51
82
  function createHandler(options) {
52
83
  const { App, routes, template = DEFAULT_TEMPLATE, clientEntry = "/src/entry-client.ts", middleware = [], mode = "string" } = options;
84
+ const compiled = compileTemplate(template);
85
+ const clientEntryTag = buildClientEntryTag(clientEntry);
53
86
  return async function handler(req) {
54
87
  const url = new URL(req.url);
55
88
  const path = url.pathname + url.search;
@@ -73,12 +106,12 @@ function createHandler(options) {
73
106
  try {
74
107
  await prefetchLoaderData(router, path);
75
108
  const app = h(RouterProvider, { router }, h(App, null));
76
- if (mode === "stream") return renderStreamResponse(app, router, template, clientEntry, ctx.headers);
109
+ if (mode === "stream") return renderStreamResponse(app, router, compiled, clientEntryTag, ctx.headers);
77
110
  const { html: appHtml, head } = await renderWithHead(app);
78
- const fullHtml = processTemplate(template, {
111
+ const fullHtml = processCompiledTemplate(compiled, {
79
112
  head,
80
113
  app: appHtml,
81
- scripts: buildScripts(clientEntry, serializeLoaderData(router))
114
+ scripts: buildScriptsFast(clientEntryTag, serializeLoaderData(router))
82
115
  });
83
116
  return new Response(fullHtml, {
84
117
  status: 200,
@@ -99,12 +132,11 @@ function createHandler(options) {
99
132
  * Head tags from the initial synchronous render are included in the shell.
100
133
  * Suspense boundaries resolve out-of-order via inline <template> + swap scripts.
101
134
  */
102
- async function renderStreamResponse(app, router, template, clientEntry, extraHeaders) {
103
- const scripts = buildScripts(clientEntry, serializeLoaderData(router));
104
- const [beforeApp, afterApp] = template.split("<!--pyreon-app-->");
105
- if (!beforeApp || afterApp === void 0) throw new Error("[pyreon/server] Template must contain <!--pyreon-app--> placeholder");
106
- const shellHead = beforeApp.replace("<!--pyreon-head-->", "");
107
- const shellTail = afterApp.replace("<!--pyreon-scripts-->", scripts);
135
+ async function renderStreamResponse(app, router, compiled, clientEntryTag, extraHeaders) {
136
+ const scripts = buildScriptsFast(clientEntryTag, serializeLoaderData(router));
137
+ const [p0, p1, p2, p3] = compiled.parts;
138
+ const shellHead = p0 + p1;
139
+ const shellTail = p2 + scripts + p3;
108
140
  const reader = renderToStream(app).getReader();
109
141
  const stream = new ReadableStream({ async start(controller) {
110
142
  const encoder = new TextEncoder();
@@ -290,5 +322,5 @@ function resolveOutputPath(outDir, path) {
290
322
  }
291
323
 
292
324
  //#endregion
293
- export { DEFAULT_TEMPLATE, buildScripts, createHandler, island, prerender, processTemplate };
325
+ export { DEFAULT_TEMPLATE, buildScripts, compileTemplate, createHandler, island, prerender, processCompiledTemplate, processTemplate };
294
326
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/html.ts","../src/handler.ts","../src/island.ts","../src/ssg.ts"],"sourcesContent":["/**\n * HTML template processing for SSR/SSG.\n *\n * Templates use comment placeholders:\n * <!--pyreon-head--> — replaced with <head> tags (title, meta, link, etc.)\n * <!--pyreon-app--> — replaced with rendered application HTML\n * <!--pyreon-scripts--> — replaced with client entry script + inline loader data\n */\n\nexport const DEFAULT_TEMPLATE = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <!--pyreon-head-->\n</head>\n<body>\n <div id=\"app\"><!--pyreon-app--></div>\n <!--pyreon-scripts-->\n</body>\n</html>`\n\nexport interface TemplateData {\n head: string\n app: string\n scripts: string\n}\n\nexport function processTemplate(template: string, data: TemplateData): string {\n return template\n .replace(\"<!--pyreon-head-->\", data.head)\n .replace(\"<!--pyreon-app-->\", data.app)\n .replace(\"<!--pyreon-scripts-->\", data.scripts)\n}\n\n/**\n * Build the script tags for client hydration.\n *\n * Emits:\n * 1. Inline script with serialized loader data (if any)\n * 2. Module script tag pointing to the client entry\n */\nexport function buildScripts(\n clientEntry: string,\n loaderData: Record<string, unknown> | null,\n): string {\n const parts: string[] = []\n\n if (loaderData && Object.keys(loaderData).length > 0) {\n // Escape </script> inside JSON to prevent premature tag close\n const json = JSON.stringify(loaderData).replace(/<\\//g, \"<\\\\/\")\n parts.push(`<script>window.__PYREON_LOADER_DATA__=${json}</script>`)\n }\n\n parts.push(`<script type=\"module\" src=\"${clientEntry}\"></script>`)\n\n return parts.join(\"\\n \")\n}\n","/**\n * SSR request handler.\n *\n * Creates a Web-standard `(Request) => Promise<Response>` handler that:\n * 1. Runs middleware (auth, redirects, headers, etc.)\n * 2. Creates a per-request router with the matched URL\n * 3. Prefetches loader data for matched routes\n * 4. Renders the app to HTML with head tag collection\n * 5. Injects everything into an HTML template\n * 6. Returns a Response\n *\n * Compatible with Bun.serve, Deno.serve, Cloudflare Workers,\n * Express (via adapter), and any Web-standard server.\n *\n * @example\n * import { createHandler } from \"@pyreon/server\"\n *\n * const handler = createHandler({\n * App,\n * routes,\n * template: await Bun.file(\"index.html\").text(),\n * })\n *\n * Bun.serve({ fetch: handler })\n */\n\nimport type { ComponentFn } from \"@pyreon/core\"\nimport { h } from \"@pyreon/core\"\nimport { renderWithHead } from \"@pyreon/head\"\nimport {\n createRouter,\n prefetchLoaderData,\n type RouteRecord,\n RouterProvider,\n serializeLoaderData,\n} from \"@pyreon/router\"\nimport { renderToStream, runWithRequestContext } from \"@pyreon/runtime-server\"\nimport { buildScripts, DEFAULT_TEMPLATE, processTemplate } from \"./html\"\nimport type { Middleware, MiddlewareContext } from \"./middleware\"\n\nexport interface HandlerOptions {\n /** Root application component */\n App: ComponentFn\n /** Route definitions */\n routes: RouteRecord[]\n /**\n * HTML template with placeholders:\n * <!--pyreon-head--> — head tags (title, meta, link, etc.)\n * <!--pyreon-app--> — rendered app HTML\n * <!--pyreon-scripts--> — client entry + loader data\n *\n * Defaults to a minimal HTML5 template.\n */\n template?: string\n /** Path to the client entry module (default: \"/src/entry-client.ts\") */\n clientEntry?: string\n /** Middleware chain — runs before rendering */\n middleware?: Middleware[]\n /**\n * Rendering mode:\n * \"string\" (default) — full renderToString, complete HTML in one response\n * \"stream\" — progressive streaming via renderToStream (Suspense out-of-order)\n */\n mode?: \"string\" | \"stream\"\n}\n\nexport function createHandler(options: HandlerOptions): (req: Request) => Promise<Response> {\n const {\n App,\n routes,\n template = DEFAULT_TEMPLATE,\n clientEntry = \"/src/entry-client.ts\",\n middleware = [],\n mode = \"string\",\n } = options\n\n return async function handler(req: Request): Promise<Response> {\n const url = new URL(req.url)\n const path = url.pathname + url.search\n\n // ── Middleware pipeline ────────────────────────────────────────────────────\n const ctx: MiddlewareContext = {\n req,\n url,\n path,\n headers: new Headers({ \"Content-Type\": \"text/html; charset=utf-8\" }),\n locals: {},\n }\n\n for (const mw of middleware) {\n const result = await mw(ctx)\n if (result instanceof Response) return result\n }\n\n // ── Per-request router ────────────────────────────────────────────────────\n const router = createRouter({ routes, mode: \"history\", url: path })\n\n return runWithRequestContext(async () => {\n try {\n // Pre-run loaders so data is available during render\n await prefetchLoaderData(router as never, path)\n\n // Build the VNode tree\n const app = h(RouterProvider, { router }, h(App, null))\n\n if (mode === \"stream\") {\n return renderStreamResponse(app, router, template, clientEntry, ctx.headers)\n }\n\n // ── String mode (default) ─────────────────────────────────────────────\n const { html: appHtml, head } = await renderWithHead(app)\n const loaderData = serializeLoaderData(router as never)\n const scripts = buildScripts(clientEntry, loaderData)\n const fullHtml = processTemplate(template, { head, app: appHtml, scripts })\n\n return new Response(fullHtml, { status: 200, headers: ctx.headers })\n } catch (_err) {\n return new Response(\"Internal Server Error\", {\n status: 500,\n headers: { \"Content-Type\": \"text/plain\" },\n })\n }\n })\n }\n}\n\n/**\n * Streaming mode: shell is emitted immediately, app content streams progressively.\n *\n * Head tags from the initial synchronous render are included in the shell.\n * Suspense boundaries resolve out-of-order via inline <template> + swap scripts.\n */\nasync function renderStreamResponse(\n app: ReturnType<typeof h>,\n router: ReturnType<typeof createRouter>,\n template: string,\n clientEntry: string,\n extraHeaders: Headers,\n): Promise<Response> {\n const loaderData = serializeLoaderData(router as never)\n const scripts = buildScripts(clientEntry, loaderData)\n\n // Split template around <!--pyreon-app-->\n const [beforeApp, afterApp] = template.split(\"<!--pyreon-app-->\")\n if (!beforeApp || afterApp === undefined) {\n throw new Error(\"[pyreon/server] Template must contain <!--pyreon-app--> placeholder\")\n }\n\n // Replace other placeholders in shell parts\n const shellHead = beforeApp.replace(\"<!--pyreon-head-->\", \"\")\n const shellTail = afterApp.replace(\"<!--pyreon-scripts-->\", scripts)\n\n const appStream = renderToStream(app)\n const reader = appStream.getReader()\n\n const stream = new ReadableStream<Uint8Array>({\n async start(controller) {\n const encoder = new TextEncoder()\n const push = (s: string) => controller.enqueue(encoder.encode(s))\n\n try {\n push(shellHead)\n\n // Stream app content\n let done = false\n while (!done) {\n const result = await reader.read()\n done = result.done\n if (result.value) push(result.value)\n }\n\n push(shellTail)\n } catch (_err) {\n // Emit an inline error indicator — status code is already sent (200)\n push(`<script>console.error(\"[pyreon/server] Stream render failed\")</script>`)\n push(shellTail)\n } finally {\n controller.close()\n }\n },\n })\n\n return new Response(stream, {\n status: 200,\n headers: extraHeaders,\n })\n}\n","/**\n * Island architecture — partial hydration for content-heavy sites.\n *\n * Islands are interactive components embedded in otherwise-static HTML.\n * Only island components ship JavaScript to the client — the rest of the\n * page stays as zero-JS server-rendered HTML.\n *\n * ## Server side\n *\n * `island()` wraps an async component import and returns a ComponentFn.\n * During SSR, it renders the component output inside a `<pyreon-island>` element\n * with serialized props, so the client knows what to hydrate.\n *\n * ```tsx\n * import { island } from \"@pyreon/server\"\n *\n * const Counter = island(() => import(\"./Counter\"), { name: \"Counter\" })\n * const Search = island(() => import(\"./Search\"), { name: \"Search\" })\n *\n * function Page() {\n * return <div>\n * <h1>Static heading (no JS)</h1>\n * <Counter initial={5} /> // hydrated on client\n * <p>Static paragraph</p>\n * <Search /> // hydrated on client\n * </div>\n * }\n * ```\n *\n * ## Client side\n *\n * Use `hydrateIslands()` from `@pyreon/server/client` to hydrate all islands\n * on the page. Only the island components' JavaScript is loaded.\n *\n * ```ts\n * // entry-client.ts (island mode)\n * import { hydrateIslands } from \"@pyreon/server/client\"\n *\n * hydrateIslands({\n * Counter: () => import(\"./Counter\"),\n * Search: () => import(\"./Search\"),\n * })\n * ```\n *\n * ## Hydration strategies\n *\n * Control when an island hydrates via the `hydrate` option:\n * - \"load\" (default) — hydrate immediately on page load\n * - \"idle\" — hydrate when the browser is idle (requestIdleCallback)\n * - \"visible\" — hydrate when the island scrolls into the viewport\n * - \"media(query)\" — hydrate when a media query matches\n * - \"never\" — never hydrate (render-only, no client JS)\n */\n\nimport type { ComponentFn, Props, VNode } from \"@pyreon/core\"\nimport { h } from \"@pyreon/core\"\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport type HydrationStrategy = \"load\" | \"idle\" | \"visible\" | \"never\" | `media(${string})`\n\nexport interface IslandOptions {\n /** Unique name — must match the key in the client-side hydrateIslands() registry */\n name: string\n /** When to hydrate on the client (default: \"load\") */\n hydrate?: HydrationStrategy\n}\n\nexport interface IslandMeta {\n readonly __island: true\n readonly name: string\n readonly hydrate: HydrationStrategy\n}\n\n// ─── Server-side island factory ──────────────────────────────────────────────\n\n/**\n * Create an island component.\n *\n * Returns an async ComponentFn that:\n * 1. Resolves the dynamic import\n * 2. Renders the component to VNodes\n * 3. Wraps the output in `<pyreon-island>` with serialized props + hydration strategy\n */\nexport function island<P extends Props = Props>(\n loader: () => Promise<{ default: ComponentFn<P> } | ComponentFn<P>>,\n options: IslandOptions,\n): ComponentFn<P> & IslandMeta {\n const { name, hydrate = \"load\" } = options\n\n const IslandWrapper = async function IslandWrapper(props: P): Promise<VNode | null> {\n const mod = await loader()\n const Comp = typeof mod === \"function\" ? mod : mod.default\n const serializedProps = serializeIslandProps(props)\n\n return h(\n \"pyreon-island\",\n {\n \"data-component\": name,\n \"data-props\": serializedProps,\n \"data-hydrate\": hydrate,\n },\n h(Comp, props),\n )\n }\n\n // Attach metadata so the Vite plugin can detect islands for code-splitting\n const wrapper = IslandWrapper as unknown as ComponentFn<P> & IslandMeta\n Object.defineProperties(wrapper, {\n __island: { value: true, enumerable: true },\n name: { value: name, enumerable: true, writable: false, configurable: true },\n hydrate: { value: hydrate, enumerable: true },\n })\n\n return wrapper\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Serialize component props to a JSON string for embedding in HTML attributes.\n * Strips non-serializable values (functions, symbols, children).\n */\nfunction serializeIslandProps(props: Record<string, unknown>): string {\n const clean: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n // Skip non-serializable or internal props\n if (key === \"children\") continue\n if (typeof value === \"function\") continue\n if (typeof value === \"symbol\") continue\n if (value === undefined) continue\n clean[key] = value\n }\n // The SSR renderer's renderProp() already applies escapeHtml() to attribute\n // values, so the JSON is safe to embed in HTML attributes without double-escaping.\n return JSON.stringify(clean)\n}\n","/**\n * Static Site Generation — pre-render routes to HTML files at build time.\n *\n * @example\n * // ssg.ts (run with: bun run ssg.ts)\n * import { createHandler } from \"@pyreon/server\"\n * import { prerender } from \"@pyreon/server\"\n * import { App } from \"./src/App\"\n * import { routes } from \"./src/routes\"\n *\n * const handler = createHandler({ App, routes })\n *\n * await prerender({\n * handler,\n * paths: [\"/\", \"/about\", \"/blog\", \"/blog/hello-world\"],\n * outDir: \"dist\",\n * })\n *\n * @example\n * // Dynamic paths from a CMS or filesystem\n * await prerender({\n * handler,\n * paths: async () => {\n * const posts = await fetchAllPosts()\n * return [\"/\", \"/about\", ...posts.map(p => `/blog/${p.slug}`)]\n * },\n * outDir: \"dist\",\n * })\n */\n\nimport { mkdir, writeFile } from \"node:fs/promises\"\nimport { dirname, join, resolve } from \"node:path\"\n\nexport interface PrerenderOptions {\n /** SSR handler created by createHandler() */\n handler: (req: Request) => Promise<Response>\n /** Routes to pre-render — array of URL paths or async function that returns them */\n paths: string[] | (() => string[] | Promise<string[]>)\n /** Output directory for the generated HTML files */\n outDir: string\n /** Origin for constructing full URLs (default: \"http://localhost\") */\n origin?: string\n /**\n * Called after each page is rendered — use for logging or progress tracking.\n * Return false to skip writing this page.\n */\n // biome-ignore lint/suspicious/noConfusingVoidType: void is intentional\n onPage?: (path: string, html: string) => void | boolean | Promise<void | boolean>\n}\n\nexport interface PrerenderResult {\n /** Number of pages generated */\n pages: number\n /** Paths that failed to render */\n errors: { path: string; error: unknown }[]\n /** Total elapsed time in milliseconds */\n elapsed: number\n}\n\n/**\n * Pre-render a list of routes to static HTML files.\n *\n * For each path:\n * 1. Constructs a Request for the path\n * 2. Calls the SSR handler to render to HTML\n * 3. Writes the HTML to `outDir/<path>/index.html`\n *\n * The root path \"/\" becomes `outDir/index.html`.\n * Paths like \"/about\" become `outDir/about/index.html`.\n */\nexport async function prerender(options: PrerenderOptions): Promise<PrerenderResult> {\n const { handler, outDir, origin = \"http://localhost\", onPage } = options\n\n const start = Date.now()\n\n // Resolve paths (may be async)\n const paths = typeof options.paths === \"function\" ? await options.paths() : options.paths\n\n let pages = 0\n const errors: PrerenderResult[\"errors\"] = []\n\n async function renderPage(path: string): Promise<void> {\n const url = new URL(path, origin)\n const req = new Request(url.href)\n const res = await Promise.race([\n handler(req),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`Prerender timeout for \"${path}\" (30s)`)), 30_000),\n ),\n ])\n\n if (!res.ok) {\n errors.push({ path, error: new Error(`HTTP ${res.status}`) })\n return\n }\n\n const html = await res.text()\n\n if (onPage) {\n const result = await onPage(path, html)\n if (result === false) return\n }\n\n const filePath = resolveOutputPath(outDir, path)\n\n const resolvedOut = resolve(outDir)\n if (!resolve(filePath).startsWith(resolvedOut)) {\n errors.push({ path, error: new Error(`Path traversal detected: \"${path}\"`) })\n return\n }\n\n await mkdir(dirname(filePath), { recursive: true })\n await writeFile(filePath, html, \"utf-8\")\n pages++\n }\n\n // Process paths concurrently (batch of 10 to avoid overwhelming)\n const BATCH_SIZE = 10\n for (let i = 0; i < paths.length; i += BATCH_SIZE) {\n const batch = paths.slice(i, i + BATCH_SIZE)\n await Promise.all(\n batch.map(async (path) => {\n try {\n await renderPage(path)\n } catch (error) {\n errors.push({ path, error })\n }\n }),\n )\n }\n\n return {\n pages,\n errors,\n elapsed: Date.now() - start,\n }\n}\n\nfunction resolveOutputPath(outDir: string, path: string): string {\n if (path === \"/\") return join(outDir, \"index.html\")\n if (path.endsWith(\".html\")) return join(outDir, path)\n return join(outDir, path, \"index.html\")\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/html.ts","../src/handler.ts","../src/island.ts","../src/ssg.ts"],"sourcesContent":["/**\n * HTML template processing for SSR/SSG.\n *\n * Templates use comment placeholders:\n * <!--pyreon-head--> — replaced with <head> tags (title, meta, link, etc.)\n * <!--pyreon-app--> — replaced with rendered application HTML\n * <!--pyreon-scripts--> — replaced with client entry script + inline loader data\n */\n\nexport const DEFAULT_TEMPLATE = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <!--pyreon-head-->\n</head>\n<body>\n <div id=\"app\"><!--pyreon-app--></div>\n <!--pyreon-scripts-->\n</body>\n</html>`\n\nexport interface TemplateData {\n head: string\n app: string\n scripts: string\n}\n\n/**\n * Pre-compiled template — splits the template string once so that\n * each request only concatenates 6 parts instead of scanning 3x with `.replace()`.\n */\nexport interface CompiledTemplate {\n /** [before-head, between-head-app, between-app-scripts, after-scripts] */\n parts: [string, string, string, string]\n}\n\nexport function compileTemplate(template: string): CompiledTemplate {\n if (!template.includes(\"<!--pyreon-app-->\")) {\n throw new Error(\"[pyreon/server] Template must contain <!--pyreon-app--> placeholder\")\n }\n const [beforeHead, afterHead] = splitOnce(template, \"<!--pyreon-head-->\")\n const [betweenHeadApp, afterApp] = splitOnce(afterHead, \"<!--pyreon-app-->\")\n const [betweenAppScripts, afterScripts] = splitOnce(afterApp, \"<!--pyreon-scripts-->\")\n return { parts: [beforeHead, betweenHeadApp, betweenAppScripts, afterScripts] }\n}\n\nfunction splitOnce(str: string, delimiter: string): [string, string] {\n const idx = str.indexOf(delimiter)\n if (idx === -1) return [str, \"\"]\n return [str.slice(0, idx), str.slice(idx + delimiter.length)]\n}\n\nexport function processTemplate(template: string, data: TemplateData): string {\n return template\n .replace(\"<!--pyreon-head-->\", data.head)\n .replace(\"<!--pyreon-app-->\", data.app)\n .replace(\"<!--pyreon-scripts-->\", data.scripts)\n}\n\n/** Fast path using a pre-compiled template */\nexport function processCompiledTemplate(compiled: CompiledTemplate, data: TemplateData): string {\n const [p0, p1, p2, p3] = compiled.parts\n return p0 + data.head + p1 + data.app + p2 + data.scripts + p3\n}\n\n/**\n * Build the script tags for client hydration.\n *\n * Emits:\n * 1. Inline script with serialized loader data (if any)\n * 2. Module script tag pointing to the client entry\n */\nexport function buildScripts(\n clientEntry: string,\n loaderData: Record<string, unknown> | null,\n): string {\n const parts: string[] = []\n\n if (loaderData && Object.keys(loaderData).length > 0) {\n // Escape </script> inside JSON to prevent premature tag close\n const json = JSON.stringify(loaderData).replace(/<\\//g, \"<\\\\/\")\n parts.push(`<script>window.__PYREON_LOADER_DATA__=${json}</script>`)\n }\n\n parts.push(`<script type=\"module\" src=\"${clientEntry}\"></script>`)\n\n return parts.join(\"\\n \")\n}\n\n/** Pre-build the static client entry script tag (invariant across requests) */\nexport function buildClientEntryTag(clientEntry: string): string {\n return `<script type=\"module\" src=\"${clientEntry}\"></script>`\n}\n\n/** Fast path: build scripts with a pre-built client entry tag */\nexport function buildScriptsFast(\n clientEntryTag: string,\n loaderData: Record<string, unknown> | null,\n): string {\n if (loaderData && Object.keys(loaderData).length > 0) {\n const json = JSON.stringify(loaderData).replace(/<\\//g, \"<\\\\/\")\n return `<script>window.__PYREON_LOADER_DATA__=${json}</script>\\n ${clientEntryTag}`\n }\n return clientEntryTag\n}\n","/**\n * SSR request handler.\n *\n * Creates a Web-standard `(Request) => Promise<Response>` handler that:\n * 1. Runs middleware (auth, redirects, headers, etc.)\n * 2. Creates a per-request router with the matched URL\n * 3. Prefetches loader data for matched routes\n * 4. Renders the app to HTML with head tag collection\n * 5. Injects everything into an HTML template\n * 6. Returns a Response\n *\n * Compatible with Bun.serve, Deno.serve, Cloudflare Workers,\n * Express (via adapter), and any Web-standard server.\n *\n * @example\n * import { createHandler } from \"@pyreon/server\"\n *\n * const handler = createHandler({\n * App,\n * routes,\n * template: await Bun.file(\"index.html\").text(),\n * })\n *\n * Bun.serve({ fetch: handler })\n */\n\nimport type { ComponentFn } from \"@pyreon/core\"\nimport { h } from \"@pyreon/core\"\nimport { renderWithHead } from \"@pyreon/head\"\nimport {\n createRouter,\n prefetchLoaderData,\n type RouteRecord,\n RouterProvider,\n serializeLoaderData,\n} from \"@pyreon/router\"\nimport { renderToStream, runWithRequestContext } from \"@pyreon/runtime-server\"\nimport {\n buildClientEntryTag,\n buildScriptsFast,\n type CompiledTemplate,\n compileTemplate,\n DEFAULT_TEMPLATE,\n processCompiledTemplate,\n} from \"./html\"\nimport type { Middleware, MiddlewareContext } from \"./middleware\"\n\nexport interface HandlerOptions {\n /** Root application component */\n App: ComponentFn\n /** Route definitions */\n routes: RouteRecord[]\n /**\n * HTML template with placeholders:\n * <!--pyreon-head--> — head tags (title, meta, link, etc.)\n * <!--pyreon-app--> — rendered app HTML\n * <!--pyreon-scripts--> — client entry + loader data\n *\n * Defaults to a minimal HTML5 template.\n */\n template?: string\n /** Path to the client entry module (default: \"/src/entry-client.ts\") */\n clientEntry?: string\n /** Middleware chain — runs before rendering */\n middleware?: Middleware[]\n /**\n * Rendering mode:\n * \"string\" (default) — full renderToString, complete HTML in one response\n * \"stream\" — progressive streaming via renderToStream (Suspense out-of-order)\n */\n mode?: \"string\" | \"stream\"\n}\n\nexport function createHandler(options: HandlerOptions): (req: Request) => Promise<Response> {\n const {\n App,\n routes,\n template = DEFAULT_TEMPLATE,\n clientEntry = \"/src/entry-client.ts\",\n middleware = [],\n mode = \"string\",\n } = options\n\n // Pre-compile once at handler creation — avoids 3x string scan per request\n const compiled = compileTemplate(template)\n const clientEntryTag = buildClientEntryTag(clientEntry)\n\n return async function handler(req: Request): Promise<Response> {\n const url = new URL(req.url)\n const path = url.pathname + url.search\n\n // ── Middleware pipeline ────────────────────────────────────────────────────\n const ctx: MiddlewareContext = {\n req,\n url,\n path,\n headers: new Headers({ \"Content-Type\": \"text/html; charset=utf-8\" }),\n locals: {},\n }\n\n for (const mw of middleware) {\n const result = await mw(ctx)\n if (result instanceof Response) return result\n }\n\n // ── Per-request router ────────────────────────────────────────────────────\n const router = createRouter({ routes, mode: \"history\", url: path })\n\n return runWithRequestContext(async () => {\n try {\n // Pre-run loaders so data is available during render\n await prefetchLoaderData(router as never, path)\n\n // Build the VNode tree\n const app = h(RouterProvider, { router }, h(App, null))\n\n if (mode === \"stream\") {\n return renderStreamResponse(app, router, compiled, clientEntryTag, ctx.headers)\n }\n\n // ── String mode (default) ─────────────────────────────────────────────\n const { html: appHtml, head } = await renderWithHead(app)\n const loaderData = serializeLoaderData(router as never)\n const scripts = buildScriptsFast(clientEntryTag, loaderData)\n const fullHtml = processCompiledTemplate(compiled, { head, app: appHtml, scripts })\n\n return new Response(fullHtml, { status: 200, headers: ctx.headers })\n } catch (_err) {\n return new Response(\"Internal Server Error\", {\n status: 500,\n headers: { \"Content-Type\": \"text/plain\" },\n })\n }\n })\n }\n}\n\n/**\n * Streaming mode: shell is emitted immediately, app content streams progressively.\n *\n * Head tags from the initial synchronous render are included in the shell.\n * Suspense boundaries resolve out-of-order via inline <template> + swap scripts.\n */\nasync function renderStreamResponse(\n app: ReturnType<typeof h>,\n router: ReturnType<typeof createRouter>,\n compiled: CompiledTemplate,\n clientEntryTag: string,\n extraHeaders: Headers,\n): Promise<Response> {\n const loaderData = serializeLoaderData(router as never)\n const scripts = buildScriptsFast(clientEntryTag, loaderData)\n\n // Use pre-split parts: [before-head, between-head-app, between-app-scripts, after-scripts]\n const [p0, p1, p2, p3] = compiled.parts\n const shellHead = p0 + p1\n const shellTail = p2 + scripts + p3\n\n const appStream = renderToStream(app)\n const reader = appStream.getReader()\n\n const stream = new ReadableStream<Uint8Array>({\n async start(controller) {\n const encoder = new TextEncoder()\n const push = (s: string) => controller.enqueue(encoder.encode(s))\n\n try {\n push(shellHead)\n\n // Stream app content\n let done = false\n while (!done) {\n const result = await reader.read()\n done = result.done\n if (result.value) push(result.value)\n }\n\n push(shellTail)\n } catch (_err) {\n // Emit an inline error indicator — status code is already sent (200)\n push(`<script>console.error(\"[pyreon/server] Stream render failed\")</script>`)\n push(shellTail)\n } finally {\n controller.close()\n }\n },\n })\n\n return new Response(stream, {\n status: 200,\n headers: extraHeaders,\n })\n}\n","/**\n * Island architecture — partial hydration for content-heavy sites.\n *\n * Islands are interactive components embedded in otherwise-static HTML.\n * Only island components ship JavaScript to the client — the rest of the\n * page stays as zero-JS server-rendered HTML.\n *\n * ## Server side\n *\n * `island()` wraps an async component import and returns a ComponentFn.\n * During SSR, it renders the component output inside a `<pyreon-island>` element\n * with serialized props, so the client knows what to hydrate.\n *\n * ```tsx\n * import { island } from \"@pyreon/server\"\n *\n * const Counter = island(() => import(\"./Counter\"), { name: \"Counter\" })\n * const Search = island(() => import(\"./Search\"), { name: \"Search\" })\n *\n * function Page() {\n * return <div>\n * <h1>Static heading (no JS)</h1>\n * <Counter initial={5} /> // hydrated on client\n * <p>Static paragraph</p>\n * <Search /> // hydrated on client\n * </div>\n * }\n * ```\n *\n * ## Client side\n *\n * Use `hydrateIslands()` from `@pyreon/server/client` to hydrate all islands\n * on the page. Only the island components' JavaScript is loaded.\n *\n * ```ts\n * // entry-client.ts (island mode)\n * import { hydrateIslands } from \"@pyreon/server/client\"\n *\n * hydrateIslands({\n * Counter: () => import(\"./Counter\"),\n * Search: () => import(\"./Search\"),\n * })\n * ```\n *\n * ## Hydration strategies\n *\n * Control when an island hydrates via the `hydrate` option:\n * - \"load\" (default) — hydrate immediately on page load\n * - \"idle\" — hydrate when the browser is idle (requestIdleCallback)\n * - \"visible\" — hydrate when the island scrolls into the viewport\n * - \"media(query)\" — hydrate when a media query matches\n * - \"never\" — never hydrate (render-only, no client JS)\n */\n\nimport type { ComponentFn, Props, VNode } from \"@pyreon/core\"\nimport { h } from \"@pyreon/core\"\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport type HydrationStrategy = \"load\" | \"idle\" | \"visible\" | \"never\" | `media(${string})`\n\nexport interface IslandOptions {\n /** Unique name — must match the key in the client-side hydrateIslands() registry */\n name: string\n /** When to hydrate on the client (default: \"load\") */\n hydrate?: HydrationStrategy\n}\n\nexport interface IslandMeta {\n readonly __island: true\n readonly name: string\n readonly hydrate: HydrationStrategy\n}\n\n// ─── Server-side island factory ──────────────────────────────────────────────\n\n/**\n * Create an island component.\n *\n * Returns an async ComponentFn that:\n * 1. Resolves the dynamic import\n * 2. Renders the component to VNodes\n * 3. Wraps the output in `<pyreon-island>` with serialized props + hydration strategy\n */\nexport function island<P extends Props = Props>(\n loader: () => Promise<{ default: ComponentFn<P> } | ComponentFn<P>>,\n options: IslandOptions,\n): ComponentFn<P> & IslandMeta {\n const { name, hydrate = \"load\" } = options\n\n const IslandWrapper = async function IslandWrapper(props: P): Promise<VNode | null> {\n const mod = await loader()\n const Comp = typeof mod === \"function\" ? mod : mod.default\n const serializedProps = serializeIslandProps(props)\n\n return h(\n \"pyreon-island\",\n {\n \"data-component\": name,\n \"data-props\": serializedProps,\n \"data-hydrate\": hydrate,\n },\n h(Comp, props),\n )\n }\n\n // Attach metadata so the Vite plugin can detect islands for code-splitting\n const wrapper = IslandWrapper as unknown as ComponentFn<P> & IslandMeta\n Object.defineProperties(wrapper, {\n __island: { value: true, enumerable: true },\n name: { value: name, enumerable: true, writable: false, configurable: true },\n hydrate: { value: hydrate, enumerable: true },\n })\n\n return wrapper\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Serialize component props to a JSON string for embedding in HTML attributes.\n * Strips non-serializable values (functions, symbols, children).\n */\nfunction serializeIslandProps(props: Record<string, unknown>): string {\n const clean: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n // Skip non-serializable or internal props\n if (key === \"children\") continue\n if (typeof value === \"function\") continue\n if (typeof value === \"symbol\") continue\n if (value === undefined) continue\n clean[key] = value\n }\n // The SSR renderer's renderProp() already applies escapeHtml() to attribute\n // values, so the JSON is safe to embed in HTML attributes without double-escaping.\n return JSON.stringify(clean)\n}\n","/**\n * Static Site Generation — pre-render routes to HTML files at build time.\n *\n * @example\n * // ssg.ts (run with: bun run ssg.ts)\n * import { createHandler } from \"@pyreon/server\"\n * import { prerender } from \"@pyreon/server\"\n * import { App } from \"./src/App\"\n * import { routes } from \"./src/routes\"\n *\n * const handler = createHandler({ App, routes })\n *\n * await prerender({\n * handler,\n * paths: [\"/\", \"/about\", \"/blog\", \"/blog/hello-world\"],\n * outDir: \"dist\",\n * })\n *\n * @example\n * // Dynamic paths from a CMS or filesystem\n * await prerender({\n * handler,\n * paths: async () => {\n * const posts = await fetchAllPosts()\n * return [\"/\", \"/about\", ...posts.map(p => `/blog/${p.slug}`)]\n * },\n * outDir: \"dist\",\n * })\n */\n\nimport { mkdir, writeFile } from \"node:fs/promises\"\nimport { dirname, join, resolve } from \"node:path\"\n\nexport interface PrerenderOptions {\n /** SSR handler created by createHandler() */\n handler: (req: Request) => Promise<Response>\n /** Routes to pre-render — array of URL paths or async function that returns them */\n paths: string[] | (() => string[] | Promise<string[]>)\n /** Output directory for the generated HTML files */\n outDir: string\n /** Origin for constructing full URLs (default: \"http://localhost\") */\n origin?: string\n /**\n * Called after each page is rendered — use for logging or progress tracking.\n * Return false to skip writing this page.\n */\n // biome-ignore lint/suspicious/noConfusingVoidType: void is intentional\n onPage?: (path: string, html: string) => void | boolean | Promise<void | boolean>\n}\n\nexport interface PrerenderResult {\n /** Number of pages generated */\n pages: number\n /** Paths that failed to render */\n errors: { path: string; error: unknown }[]\n /** Total elapsed time in milliseconds */\n elapsed: number\n}\n\n/**\n * Pre-render a list of routes to static HTML files.\n *\n * For each path:\n * 1. Constructs a Request for the path\n * 2. Calls the SSR handler to render to HTML\n * 3. Writes the HTML to `outDir/<path>/index.html`\n *\n * The root path \"/\" becomes `outDir/index.html`.\n * Paths like \"/about\" become `outDir/about/index.html`.\n */\nexport async function prerender(options: PrerenderOptions): Promise<PrerenderResult> {\n const { handler, outDir, origin = \"http://localhost\", onPage } = options\n\n const start = Date.now()\n\n // Resolve paths (may be async)\n const paths = typeof options.paths === \"function\" ? await options.paths() : options.paths\n\n let pages = 0\n const errors: PrerenderResult[\"errors\"] = []\n\n async function renderPage(path: string): Promise<void> {\n const url = new URL(path, origin)\n const req = new Request(url.href)\n const res = await Promise.race([\n handler(req),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`Prerender timeout for \"${path}\" (30s)`)), 30_000),\n ),\n ])\n\n if (!res.ok) {\n errors.push({ path, error: new Error(`HTTP ${res.status}`) })\n return\n }\n\n const html = await res.text()\n\n if (onPage) {\n const result = await onPage(path, html)\n if (result === false) return\n }\n\n const filePath = resolveOutputPath(outDir, path)\n\n const resolvedOut = resolve(outDir)\n if (!resolve(filePath).startsWith(resolvedOut)) {\n errors.push({ path, error: new Error(`Path traversal detected: \"${path}\"`) })\n return\n }\n\n await mkdir(dirname(filePath), { recursive: true })\n await writeFile(filePath, html, \"utf-8\")\n pages++\n }\n\n // Process paths concurrently (batch of 10 to avoid overwhelming)\n const BATCH_SIZE = 10\n for (let i = 0; i < paths.length; i += BATCH_SIZE) {\n const batch = paths.slice(i, i + BATCH_SIZE)\n await Promise.all(\n batch.map(async (path) => {\n try {\n await renderPage(path)\n } catch (error) {\n errors.push({ path, error })\n }\n }),\n )\n }\n\n return {\n pages,\n errors,\n elapsed: Date.now() - start,\n }\n}\n\nfunction resolveOutputPath(outDir: string, path: string): string {\n if (path === \"/\") return join(outDir, \"index.html\")\n if (path.endsWith(\".html\")) return join(outDir, path)\n return join(outDir, path, \"index.html\")\n}\n"],"mappings":"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"}
@@ -15,9 +15,28 @@ import { dirname, join, resolve } from "node:path";
15
15
  * <!--pyreon-scripts--> — replaced with client entry script + inline loader data
16
16
  */
17
17
 
18
+ function compileTemplate(template) {
19
+ if (!template.includes("<!--pyreon-app-->")) throw new Error("[pyreon/server] Template must contain <!--pyreon-app--> placeholder");
20
+ const [beforeHead, afterHead] = splitOnce(template, "<!--pyreon-head-->");
21
+ const [betweenHeadApp, afterApp] = splitOnce(afterHead, "<!--pyreon-app-->");
22
+ const [betweenAppScripts, afterScripts] = splitOnce(afterApp, "<!--pyreon-scripts-->");
23
+ return {
24
+ parts: [beforeHead, betweenHeadApp, betweenAppScripts, afterScripts]
25
+ };
26
+ }
27
+ function splitOnce(str, delimiter) {
28
+ const idx = str.indexOf(delimiter);
29
+ if (idx === -1) return [str, ""];
30
+ return [str.slice(0, idx), str.slice(idx + delimiter.length)];
31
+ }
18
32
  function processTemplate(template, data) {
19
33
  return template.replace("<!--pyreon-head-->", data.head).replace("<!--pyreon-app-->", data.app).replace("<!--pyreon-scripts-->", data.scripts);
20
34
  }
35
+ /** Fast path using a pre-compiled template */
36
+ function processCompiledTemplate(compiled, data) {
37
+ const [p0, p1, p2, p3] = compiled.parts;
38
+ return p0 + data.head + p1 + data.app + p2 + data.scripts + p3;
39
+ }
21
40
  /**
22
41
  * Build the script tags for client hydration.
23
42
  *
@@ -34,6 +53,15 @@ function buildScripts(clientEntry, loaderData) {
34
53
  parts.push(`<script type="module" src="${clientEntry}"><\/script>`);
35
54
  return parts.join("\n ");
36
55
  }
56
+ /** Pre-build the static client entry script tag (invariant across requests) */
57
+ function buildClientEntryTag(clientEntry) {
58
+ return `<script type="module" src="${clientEntry}"><\/script>`;
59
+ }
60
+ /** Fast path: build scripts with a pre-built client entry tag */
61
+ function buildScriptsFast(clientEntryTag, loaderData) {
62
+ if (loaderData && Object.keys(loaderData).length > 0) return `<script>window.__PYREON_LOADER_DATA__=${JSON.stringify(loaderData).replace(/<\//g, "<\\/")}<\/script>\n ${clientEntryTag}`;
63
+ return clientEntryTag;
64
+ }
37
65
 
38
66
  //#endregion
39
67
  //#region src/handler.ts
@@ -46,6 +74,8 @@ function createHandler(options) {
46
74
  middleware = [],
47
75
  mode = "string"
48
76
  } = options;
77
+ const compiled = compileTemplate(template);
78
+ const clientEntryTag = buildClientEntryTag(clientEntry);
49
79
  return async function handler(req) {
50
80
  const url = new URL(req.url);
51
81
  const path = url.pathname + url.search;
@@ -73,15 +103,15 @@ function createHandler(options) {
73
103
  const app = h(RouterProvider, {
74
104
  router
75
105
  }, h(App, null));
76
- if (mode === "stream") return renderStreamResponse(app, router, template, clientEntry, ctx.headers);
106
+ if (mode === "stream") return renderStreamResponse(app, router, compiled, clientEntryTag, ctx.headers);
77
107
  const {
78
108
  html: appHtml,
79
109
  head
80
110
  } = await renderWithHead(app);
81
- const fullHtml = processTemplate(template, {
111
+ const fullHtml = processCompiledTemplate(compiled, {
82
112
  head,
83
113
  app: appHtml,
84
- scripts: buildScripts(clientEntry, serializeLoaderData(router))
114
+ scripts: buildScriptsFast(clientEntryTag, serializeLoaderData(router))
85
115
  });
86
116
  return new Response(fullHtml, {
87
117
  status: 200,
@@ -104,12 +134,11 @@ function createHandler(options) {
104
134
  * Head tags from the initial synchronous render are included in the shell.
105
135
  * Suspense boundaries resolve out-of-order via inline <template> + swap scripts.
106
136
  */
107
- async function renderStreamResponse(app, router, template, clientEntry, extraHeaders) {
108
- const scripts = buildScripts(clientEntry, serializeLoaderData(router));
109
- const [beforeApp, afterApp] = template.split("<!--pyreon-app-->");
110
- if (!beforeApp || afterApp === void 0) throw new Error("[pyreon/server] Template must contain <!--pyreon-app--> placeholder");
111
- const shellHead = beforeApp.replace("<!--pyreon-head-->", "");
112
- const shellTail = afterApp.replace("<!--pyreon-scripts-->", scripts);
137
+ async function renderStreamResponse(app, router, compiled, clientEntryTag, extraHeaders) {
138
+ const scripts = buildScriptsFast(clientEntryTag, serializeLoaderData(router));
139
+ const [p0, p1, p2, p3] = compiled.parts;
140
+ const shellHead = p0 + p1;
141
+ const shellTail = p2 + scripts + p3;
113
142
  const reader = renderToStream(app).getReader();
114
143
  const stream = new ReadableStream({
115
144
  async start(controller) {
@@ -307,5 +336,5 @@ function resolveOutputPath(outDir, path) {
307
336
  }
308
337
 
309
338
  //#endregion
310
- export { DEFAULT_TEMPLATE, buildScripts, createHandler, island, prerender, processTemplate };
339
+ export { DEFAULT_TEMPLATE, buildScripts, compileTemplate, createHandler, island, prerender, processCompiledTemplate, processTemplate };
311
340
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/html.ts","../../src/handler.ts","../../src/island.ts","../../src/ssg.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/html.ts","../../src/handler.ts","../../src/island.ts","../../src/ssg.ts"],"mappings":"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"}
@@ -82,7 +82,18 @@ interface TemplateData {
82
82
  app: string;
83
83
  scripts: string;
84
84
  }
85
+ /**
86
+ * Pre-compiled template — splits the template string once so that
87
+ * each request only concatenates 6 parts instead of scanning 3x with `.replace()`.
88
+ */
89
+ interface CompiledTemplate {
90
+ /** [before-head, between-head-app, between-app-scripts, after-scripts] */
91
+ parts: [string, string, string, string];
92
+ }
93
+ declare function compileTemplate(template: string): CompiledTemplate;
85
94
  declare function processTemplate(template: string, data: TemplateData): string;
95
+ /** Fast path using a pre-compiled template */
96
+ declare function processCompiledTemplate(compiled: CompiledTemplate, data: TemplateData): string;
86
97
  /**
87
98
  * Build the script tags for client hydration.
88
99
  *
@@ -186,5 +197,5 @@ interface PrerenderResult {
186
197
  */
187
198
  declare function prerender(options: PrerenderOptions): Promise<PrerenderResult>;
188
199
  //#endregion
189
- export { DEFAULT_TEMPLATE, type HandlerOptions, type HydrationStrategy, type IslandMeta, type IslandOptions, type Middleware, type MiddlewareContext, type PrerenderOptions, type PrerenderResult, type TemplateData, buildScripts, createHandler, island, prerender, processTemplate };
200
+ export { type CompiledTemplate, DEFAULT_TEMPLATE, type HandlerOptions, type HydrationStrategy, type IslandMeta, type IslandOptions, type Middleware, type MiddlewareContext, type PrerenderOptions, type PrerenderResult, type TemplateData, buildScripts, compileTemplate, createHandler, island, prerender, processCompiledTemplate, processTemplate };
190
201
  //# sourceMappingURL=index2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../src/middleware.ts","../../src/handler.ts","../../src/html.ts","../../src/island.ts","../../src/ssg.ts"],"mappings":";;;;;;;;AAqBA;;;;;;;;;;;;;;;;UAAiB,iBAAA;EAUP;EARR,GAAA,EAAK,OAAA;EAQS;EANd,GAAA,EAAK,GAAA;EAae;EAXpB,IAAA;EAW6B;EAT7B,OAAA,EAAS,OAAA;EASoE;EAP7E,MAAA,EAAQ,MAAA;AAAA;;;;KAOE,UAAA,IAAc,GAAA,EAAK,iBAAA,KAAsB,QAAA,UAAkB,OAAA,CAAQ,QAAA;;;UCE9D,cAAA;EDF8D;ECI7E,GAAA,EAAK,WAAA;EDJuE;ECM5E,MAAA,EAAQ,WAAA;EDNqB;;;;;;;;ECe7B,QAAA;;EAEA,WAAA;EAf6B;EAiB7B,UAAA,GAAa,UAAA;EAfR;;;;;EAqBL,IAAA;AAAA;AAAA,iBAGc,aAAA,CAAc,OAAA,EAAS,cAAA,IAAkB,GAAA,EAAK,OAAA,KAAY,OAAA,CAAQ,QAAA;;;;;;;AD7ClF;;;;cEZa,gBAAA;AAAA,UAaI,YAAA;EACf,IAAA;EACA,GAAA;EACA,OAAA;AAAA;AAAA,iBAGc,eAAA,CAAgB,QAAA,UAAkB,IAAA,EAAM,YAAA;;;;;;;;iBAcxC,YAAA,CACd,WAAA,UACA,UAAA,EAAY,MAAA;;;KCeF,iBAAA;AAAA,UAEK,aAAA;EFNf;EEQA,IAAA;EFNa;EEQb,OAAA,GAAU,iBAAA;AAAA;AAAA,UAGK,UAAA;EAAA,SACN,QAAA;EAAA,SACA,IAAA;EAAA,SACA,OAAA,EAAS,iBAAA;AAAA;;;;;;;;;iBAaJ,MAAA,WAAiB,KAAA,GAAQ,KAAA,CAAA,CACvC,MAAA,QAAc,OAAA;EAAU,OAAA,EAAS,WAAA,CAAY,CAAA;AAAA,IAAO,WAAA,CAAY,CAAA,IAChE,OAAA,EAAS,aAAA,GACR,WAAA,CAAY,CAAA,IAAK,UAAA;;;;;;;AHlEpB;;;;;;;;;;;;;;;;;;;;AAiBA;;;;;UILiB,gBAAA;EJKsD;EIHrE,OAAA,GAAU,GAAA,EAAK,OAAA,KAAY,OAAA,CAAQ,QAAA;EJGyC;EID5E,KAAA,+BAAoC,OAAA;EJCZ;EICxB,MAAA;EJDqE;EIGrE,MAAA;EJHqF;;;;EISrF,MAAA,IAAU,IAAA,UAAc,IAAA,8BAAkC,OAAA;AAAA;AAAA,UAG3C,eAAA;;EAEf,KAAA;EHRQ;EGUR,MAAA;IAAU,IAAA;IAAc,KAAA;EAAA;EHZnB;EGcL,OAAA;AAAA;;;;;;;;AHUF;;;;iBGIsB,SAAA,CAAU,OAAA,EAAS,gBAAA,GAAmB,OAAA,CAAQ,eAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../src/middleware.ts","../../src/handler.ts","../../src/html.ts","../../src/island.ts","../../src/ssg.ts"],"mappings":";;;;;;;;AAqBA;;;;;;;;;;;;;;;;UAAiB,iBAAA;EAUP;EARR,GAAA,EAAK,OAAA;EAQS;EANd,GAAA,EAAK,GAAA;EAae;EAXpB,IAAA;EAW6B;EAT7B,OAAA,EAAS,OAAA;EASoE;EAP7E,MAAA,EAAQ,MAAA;AAAA;;;;KAOE,UAAA,IAAc,GAAA,EAAK,iBAAA,KAAsB,QAAA,UAAkB,OAAA,CAAQ,QAAA;;;UCS9D,cAAA;EDT8D;ECW7E,GAAA,EAAK,WAAA;EDXuE;ECa5E,MAAA,EAAQ,WAAA;EDbqB;;;;;;;;ECsB7B,QAAA;;EAEA,WAAA;EAf6B;EAiB7B,UAAA,GAAa,UAAA;EAfR;;;;;EAqBL,IAAA;AAAA;AAAA,iBAGc,aAAA,CAAc,OAAA,EAAS,cAAA,IAAkB,GAAA,EAAK,OAAA,KAAY,OAAA,CAAQ,QAAA;;;;;;;ADpDlF;;;;cEZa,gBAAA;AAAA,UAaI,YAAA;EACf,IAAA;EACA,GAAA;EACA,OAAA;AAAA;;;;;UAOe,gBAAA;EFHN;EEKT,KAAA;AAAA;AAAA,iBAGc,eAAA,CAAgB,QAAA,WAAmB,gBAAA;AAAA,iBAgBnC,eAAA,CAAgB,QAAA,UAAkB,IAAA,EAAM,YAAA;AFfxD;AAAA,iBEuBgB,uBAAA,CAAwB,QAAA,EAAU,gBAAA,EAAkB,IAAA,EAAM,YAAA;;;;;;;;iBAY1D,YAAA,CACd,WAAA,UACA,UAAA,EAAY,MAAA;;;KChBF,iBAAA;AAAA,UAEK,aAAA;EFCf;EECA,IAAA;EFCa;EECb,OAAA,GAAU,iBAAA;AAAA;AAAA,UAGK,UAAA;EAAA,SACN,QAAA;EAAA,SACA,IAAA;EAAA,SACA,OAAA,EAAS,iBAAA;AAAA;;;;;;;;;iBAaJ,MAAA,WAAiB,KAAA,GAAQ,KAAA,CAAA,CACvC,MAAA,QAAc,OAAA;EAAU,OAAA,EAAS,WAAA,CAAY,CAAA;AAAA,IAAO,WAAA,CAAY,CAAA,IAChE,OAAA,EAAS,aAAA,GACR,WAAA,CAAY,CAAA,IAAK,UAAA;;;;;;;AHlEpB;;;;;;;;;;;;;;;;;;;;AAiBA;;;;;UILiB,gBAAA;EJKsD;EIHrE,OAAA,GAAU,GAAA,EAAK,OAAA,KAAY,OAAA,CAAQ,QAAA;EJGyC;EID5E,KAAA,+BAAoC,OAAA;EJCZ;EICxB,MAAA;EJDqE;EIGrE,MAAA;EJHqF;;;;EISrF,MAAA,IAAU,IAAA,UAAc,IAAA,8BAAkC,OAAA;AAAA;AAAA,UAG3C,eAAA;;EAEf,KAAA;EHDQ;EGGR,MAAA;IAAU,IAAA;IAAc,KAAA;EAAA;EHLnB;EGOL,OAAA;AAAA;;;;;;;;AHiBF;;;;iBGHsB,SAAA,CAAU,OAAA,EAAS,gBAAA,GAAmB,OAAA,CAAQ,eAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/server",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "SSR handler, SSG prerender, and island architecture for Pyreon",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,12 +44,12 @@
44
44
  "prepublishOnly": "bun run build"
45
45
  },
46
46
  "dependencies": {
47
- "@pyreon/core": "^0.3.1",
48
- "@pyreon/head": "^0.3.1",
49
- "@pyreon/reactivity": "^0.3.1",
50
- "@pyreon/router": "^0.3.1",
51
- "@pyreon/runtime-dom": "^0.3.1",
52
- "@pyreon/runtime-server": "^0.3.1"
47
+ "@pyreon/core": "^0.4.0",
48
+ "@pyreon/head": "^0.4.0",
49
+ "@pyreon/reactivity": "^0.4.0",
50
+ "@pyreon/router": "^0.4.0",
51
+ "@pyreon/runtime-dom": "^0.4.0",
52
+ "@pyreon/runtime-server": "^0.4.0"
53
53
  },
54
54
  "publishConfig": {
55
55
  "access": "public"
package/src/handler.ts CHANGED
@@ -35,7 +35,14 @@ import {
35
35
  serializeLoaderData,
36
36
  } from "@pyreon/router"
37
37
  import { renderToStream, runWithRequestContext } from "@pyreon/runtime-server"
38
- import { buildScripts, DEFAULT_TEMPLATE, processTemplate } from "./html"
38
+ import {
39
+ buildClientEntryTag,
40
+ buildScriptsFast,
41
+ type CompiledTemplate,
42
+ compileTemplate,
43
+ DEFAULT_TEMPLATE,
44
+ processCompiledTemplate,
45
+ } from "./html"
39
46
  import type { Middleware, MiddlewareContext } from "./middleware"
40
47
 
41
48
  export interface HandlerOptions {
@@ -74,6 +81,10 @@ export function createHandler(options: HandlerOptions): (req: Request) => Promis
74
81
  mode = "string",
75
82
  } = options
76
83
 
84
+ // Pre-compile once at handler creation — avoids 3x string scan per request
85
+ const compiled = compileTemplate(template)
86
+ const clientEntryTag = buildClientEntryTag(clientEntry)
87
+
77
88
  return async function handler(req: Request): Promise<Response> {
78
89
  const url = new URL(req.url)
79
90
  const path = url.pathname + url.search
@@ -104,14 +115,14 @@ export function createHandler(options: HandlerOptions): (req: Request) => Promis
104
115
  const app = h(RouterProvider, { router }, h(App, null))
105
116
 
106
117
  if (mode === "stream") {
107
- return renderStreamResponse(app, router, template, clientEntry, ctx.headers)
118
+ return renderStreamResponse(app, router, compiled, clientEntryTag, ctx.headers)
108
119
  }
109
120
 
110
121
  // ── String mode (default) ─────────────────────────────────────────────
111
122
  const { html: appHtml, head } = await renderWithHead(app)
112
123
  const loaderData = serializeLoaderData(router as never)
113
- const scripts = buildScripts(clientEntry, loaderData)
114
- const fullHtml = processTemplate(template, { head, app: appHtml, scripts })
124
+ const scripts = buildScriptsFast(clientEntryTag, loaderData)
125
+ const fullHtml = processCompiledTemplate(compiled, { head, app: appHtml, scripts })
115
126
 
116
127
  return new Response(fullHtml, { status: 200, headers: ctx.headers })
117
128
  } catch (_err) {
@@ -133,22 +144,17 @@ export function createHandler(options: HandlerOptions): (req: Request) => Promis
133
144
  async function renderStreamResponse(
134
145
  app: ReturnType<typeof h>,
135
146
  router: ReturnType<typeof createRouter>,
136
- template: string,
137
- clientEntry: string,
147
+ compiled: CompiledTemplate,
148
+ clientEntryTag: string,
138
149
  extraHeaders: Headers,
139
150
  ): Promise<Response> {
140
151
  const loaderData = serializeLoaderData(router as never)
141
- const scripts = buildScripts(clientEntry, loaderData)
142
-
143
- // Split template around <!--pyreon-app-->
144
- const [beforeApp, afterApp] = template.split("<!--pyreon-app-->")
145
- if (!beforeApp || afterApp === undefined) {
146
- throw new Error("[pyreon/server] Template must contain <!--pyreon-app--> placeholder")
147
- }
152
+ const scripts = buildScriptsFast(clientEntryTag, loaderData)
148
153
 
149
- // Replace other placeholders in shell parts
150
- const shellHead = beforeApp.replace("<!--pyreon-head-->", "")
151
- const shellTail = afterApp.replace("<!--pyreon-scripts-->", scripts)
154
+ // Use pre-split parts: [before-head, between-head-app, between-app-scripts, after-scripts]
155
+ const [p0, p1, p2, p3] = compiled.parts
156
+ const shellHead = p0 + p1
157
+ const shellTail = p2 + scripts + p3
152
158
 
153
159
  const appStream = renderToStream(app)
154
160
  const reader = appStream.getReader()
package/src/html.ts CHANGED
@@ -26,6 +26,31 @@ export interface TemplateData {
26
26
  scripts: string
27
27
  }
28
28
 
29
+ /**
30
+ * Pre-compiled template — splits the template string once so that
31
+ * each request only concatenates 6 parts instead of scanning 3x with `.replace()`.
32
+ */
33
+ export interface CompiledTemplate {
34
+ /** [before-head, between-head-app, between-app-scripts, after-scripts] */
35
+ parts: [string, string, string, string]
36
+ }
37
+
38
+ export function compileTemplate(template: string): CompiledTemplate {
39
+ if (!template.includes("<!--pyreon-app-->")) {
40
+ throw new Error("[pyreon/server] Template must contain <!--pyreon-app--> placeholder")
41
+ }
42
+ const [beforeHead, afterHead] = splitOnce(template, "<!--pyreon-head-->")
43
+ const [betweenHeadApp, afterApp] = splitOnce(afterHead, "<!--pyreon-app-->")
44
+ const [betweenAppScripts, afterScripts] = splitOnce(afterApp, "<!--pyreon-scripts-->")
45
+ return { parts: [beforeHead, betweenHeadApp, betweenAppScripts, afterScripts] }
46
+ }
47
+
48
+ function splitOnce(str: string, delimiter: string): [string, string] {
49
+ const idx = str.indexOf(delimiter)
50
+ if (idx === -1) return [str, ""]
51
+ return [str.slice(0, idx), str.slice(idx + delimiter.length)]
52
+ }
53
+
29
54
  export function processTemplate(template: string, data: TemplateData): string {
30
55
  return template
31
56
  .replace("<!--pyreon-head-->", data.head)
@@ -33,6 +58,12 @@ export function processTemplate(template: string, data: TemplateData): string {
33
58
  .replace("<!--pyreon-scripts-->", data.scripts)
34
59
  }
35
60
 
61
+ /** Fast path using a pre-compiled template */
62
+ export function processCompiledTemplate(compiled: CompiledTemplate, data: TemplateData): string {
63
+ const [p0, p1, p2, p3] = compiled.parts
64
+ return p0 + data.head + p1 + data.app + p2 + data.scripts + p3
65
+ }
66
+
36
67
  /**
37
68
  * Build the script tags for client hydration.
38
69
  *
@@ -56,3 +87,20 @@ export function buildScripts(
56
87
 
57
88
  return parts.join("\n ")
58
89
  }
90
+
91
+ /** Pre-build the static client entry script tag (invariant across requests) */
92
+ export function buildClientEntryTag(clientEntry: string): string {
93
+ return `<script type="module" src="${clientEntry}"></script>`
94
+ }
95
+
96
+ /** Fast path: build scripts with a pre-built client entry tag */
97
+ export function buildScriptsFast(
98
+ clientEntryTag: string,
99
+ loaderData: Record<string, unknown> | null,
100
+ ): string {
101
+ if (loaderData && Object.keys(loaderData).length > 0) {
102
+ const json = JSON.stringify(loaderData).replace(/<\//g, "<\\/")
103
+ return `<script>window.__PYREON_LOADER_DATA__=${json}</script>\n ${clientEntryTag}`
104
+ }
105
+ return clientEntryTag
106
+ }
package/src/index.ts CHANGED
@@ -55,9 +55,15 @@
55
55
  export type { HandlerOptions } from "./handler"
56
56
  // SSR handler
57
57
  export { createHandler } from "./handler"
58
- export type { TemplateData } from "./html"
58
+ export type { CompiledTemplate, TemplateData } from "./html"
59
59
  // HTML template
60
- export { buildScripts, DEFAULT_TEMPLATE, processTemplate } from "./html"
60
+ export {
61
+ buildScripts,
62
+ compileTemplate,
63
+ DEFAULT_TEMPLATE,
64
+ processCompiledTemplate,
65
+ processTemplate,
66
+ } from "./html"
61
67
  export type { HydrationStrategy, IslandMeta, IslandOptions } from "./island"
62
68
  // Islands
63
69
  export { island } from "./island"
@@ -189,13 +189,12 @@ describe("createHandler — stream mode", () => {
189
189
  expect(html).toContain('src="/dist/app.js"')
190
190
  })
191
191
 
192
- test("stream mode template without <!--pyreon-app--> throws", async () => {
192
+ test("stream mode template without <!--pyreon-app--> throws", () => {
193
193
  const badTemplate = "<html><!--pyreon-head--><!--pyreon-scripts--></html>"
194
- const handler = createHandler({ App: Home, routes, mode: "stream", template: badTemplate })
195
- // The stream rendering should throw because template has no <!--pyreon-app-->
196
- await expect(handler(new Request("http://localhost/"))).rejects.toThrow(
197
- "Template must contain <!--pyreon-app-->",
198
- )
194
+ // Template validation happens at createHandler time (compile-time, not per-request)
195
+ expect(() =>
196
+ createHandler({ App: Home, routes, mode: "stream", template: badTemplate }),
197
+ ).toThrow("Template must contain <!--pyreon-app-->")
199
198
  })
200
199
 
201
200
  test("stream mode includes middleware-set headers", async () => {