@pyreon/server 0.11.5 → 0.11.6

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.
package/README.md CHANGED
@@ -13,14 +13,14 @@ bun add @pyreon/server
13
13
  Create a request handler that renders your Pyreon app on the server:
14
14
 
15
15
  ```ts
16
- import { createHandler } from "@pyreon/server"
17
- import { App } from "./App"
18
- import { routes } from "./routes"
16
+ import { createHandler } from '@pyreon/server'
17
+ import { App } from './App'
18
+ import { routes } from './routes'
19
19
 
20
20
  const handler = createHandler({
21
21
  App,
22
22
  routes,
23
- template: await Bun.file("index.html").text(),
23
+ template: await Bun.file('index.html').text(),
24
24
  })
25
25
 
26
26
  Bun.serve({ fetch: handler, port: 3000 })
@@ -31,13 +31,13 @@ Bun.serve({ fetch: handler, port: 3000 })
31
31
  Pre-render pages to static HTML files:
32
32
 
33
33
  ```ts
34
- import { createHandler, prerender } from "@pyreon/server"
34
+ import { createHandler, prerender } from '@pyreon/server'
35
35
 
36
36
  const handler = createHandler({ App, routes })
37
37
  const result = await prerender({
38
38
  handler,
39
- paths: ["/", "/about", "/blog"],
40
- outDir: "dist",
39
+ paths: ['/', '/about', '/blog'],
40
+ outDir: 'dist',
41
41
  })
42
42
  console.log(`Generated ${result.pages} pages in ${result.elapsed}ms`)
43
43
  ```
@@ -48,25 +48,25 @@ Render mostly-static pages with interactive islands that hydrate independently:
48
48
 
49
49
  ```ts
50
50
  // Server
51
- import { island } from "@pyreon/server"
51
+ import { island } from '@pyreon/server'
52
52
 
53
- const Counter = island(() => import("./Counter"), {
54
- name: "Counter",
55
- hydrate: "visible", // load | idle | visible | media(query) | never
53
+ const Counter = island(() => import('./Counter'), {
54
+ name: 'Counter',
55
+ hydrate: 'visible', // load | idle | visible | media(query) | never
56
56
  })
57
57
  ```
58
58
 
59
59
  ```ts
60
60
  // Client entry
61
- import { startClient, hydrateIslands } from "@pyreon/server/client"
61
+ import { startClient, hydrateIslands } from '@pyreon/server/client'
62
62
 
63
63
  // Full app hydration
64
- startClient({ App, routes, container: "#app" })
64
+ startClient({ App, routes, container: '#app' })
65
65
 
66
66
  // Or island-only hydration
67
67
  hydrateIslands({
68
- Counter: () => import("./Counter"),
69
- Search: () => import("./Search"),
68
+ Counter: () => import('./Counter'),
69
+ Search: () => import('./Search'),
70
70
  })
71
71
  ```
72
72
 
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * Client-side entry helpers for Pyreon SSR/SSG apps.\n *\n * ## Full app hydration\n *\n * ```ts\n * // entry-client.ts\n * import { startClient } from \"@pyreon/server/client\"\n * import { App } from \"./App\"\n * import { routes } from \"./routes\"\n *\n * startClient({ App, routes })\n * ```\n *\n * ## Island hydration (partial)\n *\n * ```ts\n * // entry-client.ts\n * import { hydrateIslands } from \"@pyreon/server/client\"\n *\n * hydrateIslands({\n * Counter: () => import(\"./Counter\"),\n * Search: () => import(\"./Search\"),\n * })\n * ```\n */\n\nimport type { ComponentFn } from \"@pyreon/core\"\nimport { h } from \"@pyreon/core\"\nimport { createRouter, hydrateLoaderData, type RouteRecord, RouterProvider } from \"@pyreon/router\"\nimport { hydrateRoot, mount } from \"@pyreon/runtime-dom\"\nimport type { HydrationStrategy } from \"./island\"\n\n// ─── Full app hydration ──────────────────────────────────────────────────────\n\nexport interface StartClientOptions {\n /** Root application component */\n App: ComponentFn\n /** Route definitions (same as server) */\n routes: RouteRecord[]\n /** CSS selector or element for the app container (default: \"#app\") */\n container?: string | Element\n}\n\n/**\n * Hydrate a server-rendered Pyreon app on the client.\n *\n * Handles:\n * - Router creation (history mode)\n * - Loader data hydration from `window.__PYREON_LOADER_DATA__`\n * - Hydration if container has SSR content, fresh mount otherwise\n *\n * Returns a cleanup function that unmounts the app.\n */\nexport function startClient(options: StartClientOptions): () => void {\n const { App, routes, container = \"#app\" } = options\n\n const el = typeof container === \"string\" ? document.querySelector(container) : container\n\n if (!el) {\n throw new Error(`[pyreon/client] Container \"${container}\" not found`)\n }\n\n // Create client-side router (history mode to match SSR)\n const router = createRouter({ routes, mode: \"history\" })\n\n // Hydrate loader data from SSR (avoids re-fetching on initial render)\n const loaderData = (window as unknown as Record<string, unknown>).__PYREON_LOADER_DATA__\n if (loaderData && typeof loaderData === \"object\") {\n hydrateLoaderData(router as never, loaderData as Record<string, unknown>)\n }\n\n // Build app tree\n const app = h(RouterProvider, { router }, h(App, null))\n\n // Hydrate if container has SSR content, mount fresh otherwise\n if (el.childNodes.length > 0) {\n return hydrateRoot(el, app)\n }\n return mount(app, el as HTMLElement)\n}\n\n// ─── Island hydration ────────────────────────────────────────────────────────\n\ntype IslandLoader = () => Promise<{ default: ComponentFn } | ComponentFn>\n\n/**\n * Hydrate all `<pyreon-island>` elements on the page.\n *\n * Only loads JavaScript for components that are actually present in the HTML.\n * Respects hydration strategies (load, idle, visible, media, never).\n *\n * @example\n * hydrateIslands({\n * Counter: () => import(\"./Counter\"),\n * Search: () => import(\"./Search\"),\n * })\n */\n/**\n * Hydrate all `<pyreon-island>` elements on the page.\n * Returns a cleanup function that disconnects any pending observers/listeners.\n */\nexport function hydrateIslands(registry: Record<string, IslandLoader>): () => void {\n const islands = document.querySelectorAll(\"pyreon-island\")\n const cleanups: (() => void)[] = []\n\n for (const el of islands) {\n const componentId = el.getAttribute(\"data-component\")\n if (!componentId) continue\n\n const loader = registry[componentId]\n if (!loader) {\n console.warn(`No loader registered for island \"${componentId}\"`)\n continue\n }\n\n const strategy = (el.getAttribute(\"data-hydrate\") ?? \"load\") as HydrationStrategy\n const propsJson = el.getAttribute(\"data-props\") ?? \"{}\"\n\n const cleanup = scheduleHydration(el as HTMLElement, loader, propsJson, strategy)\n if (cleanup) cleanups.push(cleanup)\n }\n\n return () => {\n for (const fn of cleanups) fn()\n }\n}\n\nfunction scheduleHydration(\n el: HTMLElement,\n loader: IslandLoader,\n propsJson: string,\n strategy: HydrationStrategy,\n): (() => void) | null {\n let cancelled = false\n const hydrate = () => {\n if (!cancelled) hydrateIsland(el, loader, propsJson)\n }\n\n switch (strategy) {\n case \"load\":\n hydrate()\n return null\n\n case \"idle\": {\n if (\"requestIdleCallback\" in window) {\n const id = requestIdleCallback(hydrate)\n return () => {\n cancelled = true\n cancelIdleCallback(id)\n }\n }\n const id = setTimeout(hydrate, 200)\n return () => {\n cancelled = true\n clearTimeout(id)\n }\n }\n\n case \"visible\":\n return observeVisibility(el, hydrate)\n\n case \"never\":\n return null\n\n default:\n // media(query)\n if (strategy.startsWith(\"media(\")) {\n const query = strategy.slice(6, -1)\n const mql = window.matchMedia(query)\n if (mql.matches) {\n hydrate()\n return null\n }\n const onChange = (e: MediaQueryListEvent) => {\n if (e.matches) {\n mql.removeEventListener(\"change\", onChange)\n hydrate()\n }\n }\n mql.addEventListener(\"change\", onChange)\n return () => {\n cancelled = true\n mql.removeEventListener(\"change\", onChange)\n }\n }\n hydrate()\n return null\n }\n}\n\nasync function hydrateIsland(\n el: HTMLElement,\n loader: IslandLoader,\n propsJson: string,\n): Promise<void> {\n const name = el.getAttribute(\"data-component\") ?? \"unknown\"\n try {\n let props: Record<string, unknown>\n try {\n props = JSON.parse(propsJson)\n if (typeof props !== \"object\" || props === null || Array.isArray(props)) {\n throw new TypeError(\"Expected object\")\n }\n } catch (parseErr) {\n console.error(`Invalid island props JSON for \"${name}\"`, parseErr)\n return\n }\n\n const mod = await loader()\n const Comp = typeof mod === \"function\" ? mod : mod.default\n hydrateRoot(el, h(Comp, props))\n } catch (err) {\n console.error(`Failed to hydrate island \"${name}\"`, err)\n }\n}\n\nfunction observeVisibility(el: HTMLElement, callback: () => void): (() => void) | null {\n if (!(\"IntersectionObserver\" in window)) {\n callback()\n return null\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n observer.disconnect()\n callback()\n return\n }\n }\n },\n { rootMargin: \"200px\" },\n )\n\n observer.observe(el)\n return () => observer.disconnect()\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * Client-side entry helpers for Pyreon SSR/SSG apps.\n *\n * ## Full app hydration\n *\n * ```ts\n * // entry-client.ts\n * import { startClient } from \"@pyreon/server/client\"\n * import { App } from \"./App\"\n * import { routes } from \"./routes\"\n *\n * startClient({ App, routes })\n * ```\n *\n * ## Island hydration (partial)\n *\n * ```ts\n * // entry-client.ts\n * import { hydrateIslands } from \"@pyreon/server/client\"\n *\n * hydrateIslands({\n * Counter: () => import(\"./Counter\"),\n * Search: () => import(\"./Search\"),\n * })\n * ```\n */\n\nimport type { ComponentFn } from '@pyreon/core'\nimport { h } from '@pyreon/core'\nimport { createRouter, hydrateLoaderData, type RouteRecord, RouterProvider } from '@pyreon/router'\nimport { hydrateRoot, mount } from '@pyreon/runtime-dom'\nimport type { HydrationStrategy } from './island'\n\n// ─── Full app hydration ──────────────────────────────────────────────────────\n\nexport interface StartClientOptions {\n /** Root application component */\n App: ComponentFn\n /** Route definitions (same as server) */\n routes: RouteRecord[]\n /** CSS selector or element for the app container (default: \"#app\") */\n container?: string | Element\n}\n\n/**\n * Hydrate a server-rendered Pyreon app on the client.\n *\n * Handles:\n * - Router creation (history mode)\n * - Loader data hydration from `window.__PYREON_LOADER_DATA__`\n * - Hydration if container has SSR content, fresh mount otherwise\n *\n * Returns a cleanup function that unmounts the app.\n */\nexport function startClient(options: StartClientOptions): () => void {\n const { App, routes, container = '#app' } = options\n\n const el = typeof container === 'string' ? document.querySelector(container) : container\n\n if (!el) {\n throw new Error(`[pyreon/client] Container \"${container}\" not found`)\n }\n\n // Create client-side router (history mode to match SSR)\n const router = createRouter({ routes, mode: 'history' })\n\n // Hydrate loader data from SSR (avoids re-fetching on initial render)\n const loaderData = (window as unknown as Record<string, unknown>).__PYREON_LOADER_DATA__\n if (loaderData && typeof loaderData === 'object') {\n hydrateLoaderData(router as never, loaderData as Record<string, unknown>)\n }\n\n // Build app tree\n const app = h(RouterProvider, { router }, h(App, null))\n\n // Hydrate if container has SSR content, mount fresh otherwise\n if (el.childNodes.length > 0) {\n return hydrateRoot(el, app)\n }\n return mount(app, el as HTMLElement)\n}\n\n// ─── Island hydration ────────────────────────────────────────────────────────\n\ntype IslandLoader = () => Promise<{ default: ComponentFn } | ComponentFn>\n\n/**\n * Hydrate all `<pyreon-island>` elements on the page.\n *\n * Only loads JavaScript for components that are actually present in the HTML.\n * Respects hydration strategies (load, idle, visible, media, never).\n *\n * @example\n * hydrateIslands({\n * Counter: () => import(\"./Counter\"),\n * Search: () => import(\"./Search\"),\n * })\n */\n/**\n * Hydrate all `<pyreon-island>` elements on the page.\n * Returns a cleanup function that disconnects any pending observers/listeners.\n */\nexport function hydrateIslands(registry: Record<string, IslandLoader>): () => void {\n const islands = document.querySelectorAll('pyreon-island')\n const cleanups: (() => void)[] = []\n\n for (const el of islands) {\n const componentId = el.getAttribute('data-component')\n if (!componentId) continue\n\n const loader = registry[componentId]\n if (!loader) {\n console.warn(`No loader registered for island \"${componentId}\"`)\n continue\n }\n\n const strategy = (el.getAttribute('data-hydrate') ?? 'load') as HydrationStrategy\n const propsJson = el.getAttribute('data-props') ?? '{}'\n\n const cleanup = scheduleHydration(el as HTMLElement, loader, propsJson, strategy)\n if (cleanup) cleanups.push(cleanup)\n }\n\n return () => {\n for (const fn of cleanups) fn()\n }\n}\n\nfunction scheduleHydration(\n el: HTMLElement,\n loader: IslandLoader,\n propsJson: string,\n strategy: HydrationStrategy,\n): (() => void) | null {\n let cancelled = false\n const hydrate = () => {\n if (!cancelled) hydrateIsland(el, loader, propsJson)\n }\n\n switch (strategy) {\n case 'load':\n hydrate()\n return null\n\n case 'idle': {\n if ('requestIdleCallback' in window) {\n const id = requestIdleCallback(hydrate)\n return () => {\n cancelled = true\n cancelIdleCallback(id)\n }\n }\n const id = setTimeout(hydrate, 200)\n return () => {\n cancelled = true\n clearTimeout(id)\n }\n }\n\n case 'visible':\n return observeVisibility(el, hydrate)\n\n case 'never':\n return null\n\n default:\n // media(query)\n if (strategy.startsWith('media(')) {\n const query = strategy.slice(6, -1)\n const mql = window.matchMedia(query)\n if (mql.matches) {\n hydrate()\n return null\n }\n const onChange = (e: MediaQueryListEvent) => {\n if (e.matches) {\n mql.removeEventListener('change', onChange)\n hydrate()\n }\n }\n mql.addEventListener('change', onChange)\n return () => {\n cancelled = true\n mql.removeEventListener('change', onChange)\n }\n }\n hydrate()\n return null\n }\n}\n\nasync function hydrateIsland(\n el: HTMLElement,\n loader: IslandLoader,\n propsJson: string,\n): Promise<void> {\n const name = el.getAttribute('data-component') ?? 'unknown'\n try {\n let props: Record<string, unknown>\n try {\n props = JSON.parse(propsJson)\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n throw new TypeError('Expected object')\n }\n } catch (parseErr) {\n console.error(`Invalid island props JSON for \"${name}\"`, parseErr)\n return\n }\n\n const mod = await loader()\n const Comp = typeof mod === 'function' ? mod : mod.default\n hydrateRoot(el, h(Comp, props))\n } catch (err) {\n console.error(`Failed to hydrate island \"${name}\"`, err)\n }\n}\n\nfunction observeVisibility(el: HTMLElement, callback: () => void): (() => void) | null {\n if (!('IntersectionObserver' in window)) {\n callback()\n return null\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n observer.disconnect()\n callback()\n return\n }\n }\n },\n { rootMargin: '200px' },\n )\n\n observer.observe(el)\n return () => observer.disconnect()\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,SAAgB,YAAY,SAAyC;CACnE,MAAM,EAAE,KAAK,QAAQ,YAAY,WAAW;CAE5C,MAAM,KAAK,OAAO,cAAc,WAAW,SAAS,cAAc,UAAU,GAAG;AAE/E,KAAI,CAAC,GACH,OAAM,IAAI,MAAM,8BAA8B,UAAU,aAAa;CAIvE,MAAM,SAAS,aAAa;EAAE;EAAQ,MAAM;EAAW,CAAC;CAGxD,MAAM,aAAc,OAA8C;AAClE,KAAI,cAAc,OAAO,eAAe,SACtC,mBAAkB,QAAiB,WAAsC;CAI3E,MAAM,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,KAAK,KAAK,CAAC;AAGvD,KAAI,GAAG,WAAW,SAAS,EACzB,QAAO,YAAY,IAAI,IAAI;AAE7B,QAAO,MAAM,KAAK,GAAkB;;;;;;;;;;;;;;;;;;AAuBtC,SAAgB,eAAe,UAAoD;CACjF,MAAM,UAAU,SAAS,iBAAiB,gBAAgB;CAC1D,MAAM,WAA2B,EAAE;AAEnC,MAAK,MAAM,MAAM,SAAS;EACxB,MAAM,cAAc,GAAG,aAAa,iBAAiB;AACrD,MAAI,CAAC,YAAa;EAElB,MAAM,SAAS,SAAS;AACxB,MAAI,CAAC,QAAQ;AACX,WAAQ,KAAK,oCAAoC,YAAY,GAAG;AAChE;;EAGF,MAAM,WAAY,GAAG,aAAa,eAAe,IAAI;EAGrD,MAAM,UAAU,kBAAkB,IAAmB,QAFnC,GAAG,aAAa,aAAa,IAAI,MAEqB,SAAS;AACjF,MAAI,QAAS,UAAS,KAAK,QAAQ;;AAGrC,cAAa;AACX,OAAK,MAAM,MAAM,SAAU,KAAI;;;AAInC,SAAS,kBACP,IACA,QACA,WACA,UACqB;CACrB,IAAI,YAAY;CAChB,MAAM,gBAAgB;AACpB,MAAI,CAAC,UAAW,eAAc,IAAI,QAAQ,UAAU;;AAGtD,SAAQ,UAAR;EACE,KAAK;AACH,YAAS;AACT,UAAO;EAET,KAAK,QAAQ;AACX,OAAI,yBAAyB,QAAQ;IACnC,MAAM,KAAK,oBAAoB,QAAQ;AACvC,iBAAa;AACX,iBAAY;AACZ,wBAAmB,GAAG;;;GAG1B,MAAM,KAAK,WAAW,SAAS,IAAI;AACnC,gBAAa;AACX,gBAAY;AACZ,iBAAa,GAAG;;;EAIpB,KAAK,UACH,QAAO,kBAAkB,IAAI,QAAQ;EAEvC,KAAK,QACH,QAAO;EAET;AAEE,OAAI,SAAS,WAAW,SAAS,EAAE;IACjC,MAAM,QAAQ,SAAS,MAAM,GAAG,GAAG;IACnC,MAAM,MAAM,OAAO,WAAW,MAAM;AACpC,QAAI,IAAI,SAAS;AACf,cAAS;AACT,YAAO;;IAET,MAAM,YAAY,MAA2B;AAC3C,SAAI,EAAE,SAAS;AACb,UAAI,oBAAoB,UAAU,SAAS;AAC3C,eAAS;;;AAGb,QAAI,iBAAiB,UAAU,SAAS;AACxC,iBAAa;AACX,iBAAY;AACZ,SAAI,oBAAoB,UAAU,SAAS;;;AAG/C,YAAS;AACT,UAAO;;;AAIb,eAAe,cACb,IACA,QACA,WACe;CACf,MAAM,OAAO,GAAG,aAAa,iBAAiB,IAAI;AAClD,KAAI;EACF,IAAI;AACJ,MAAI;AACF,WAAQ,KAAK,MAAM,UAAU;AAC7B,OAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,MAAM,CACrE,OAAM,IAAI,UAAU,kBAAkB;WAEjC,UAAU;AACjB,WAAQ,MAAM,kCAAkC,KAAK,IAAI,SAAS;AAClE;;EAGF,MAAM,MAAM,MAAM,QAAQ;AAE1B,cAAY,IAAI,EADH,OAAO,QAAQ,aAAa,MAAM,IAAI,SAC3B,MAAM,CAAC;UACxB,KAAK;AACZ,UAAQ,MAAM,6BAA6B,KAAK,IAAI,IAAI;;;AAI5D,SAAS,kBAAkB,IAAiB,UAA2C;AACrF,KAAI,EAAE,0BAA0B,SAAS;AACvC,YAAU;AACV,SAAO;;CAGT,MAAM,WAAW,IAAI,sBAClB,YAAY;AACX,OAAK,MAAM,SAAS,QAClB,KAAI,MAAM,gBAAgB;AACxB,YAAS,YAAY;AACrB,aAAU;AACV;;IAIN,EAAE,YAAY,SAAS,CACxB;AAED,UAAS,QAAQ,GAAG;AACpB,cAAa,SAAS,YAAY"}
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../head/lib/ssr.js","../src/html.ts","../src/handler.ts","../src/island.ts","../src/ssg.ts"],"sourcesContent":["import { createContext, h, pushContext } from \"@pyreon/core\";\nimport { renderToString } from \"@pyreon/runtime-server\";\n\n//#region src/context.ts\nfunction createHeadContext() {\n\tconst map = /* @__PURE__ */ new Map();\n\tlet dirty = true;\n\tlet cachedTags = [];\n\tlet cachedTitleTemplate;\n\tlet cachedHtmlAttrs = {};\n\tlet cachedBodyAttrs = {};\n\tfunction rebuild() {\n\t\tif (!dirty) return;\n\t\tdirty = false;\n\t\tconst keyed = /* @__PURE__ */ new Map();\n\t\tconst unkeyed = [];\n\t\tlet titleTemplate;\n\t\tconst htmlAttrs = {};\n\t\tconst bodyAttrs = {};\n\t\tfor (const entry of map.values()) {\n\t\t\tfor (const tag of entry.tags) if (tag.key) keyed.set(tag.key, tag);\n\t\t\telse unkeyed.push(tag);\n\t\t\tif (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;\n\t\t\tif (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs);\n\t\t\tif (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs);\n\t\t}\n\t\tcachedTags = [...keyed.values(), ...unkeyed];\n\t\tcachedTitleTemplate = titleTemplate;\n\t\tcachedHtmlAttrs = htmlAttrs;\n\t\tcachedBodyAttrs = bodyAttrs;\n\t}\n\treturn {\n\t\tadd(id, entry) {\n\t\t\tmap.set(id, entry);\n\t\t\tdirty = true;\n\t\t},\n\t\tremove(id) {\n\t\t\tmap.delete(id);\n\t\t\tdirty = true;\n\t\t},\n\t\tresolve() {\n\t\t\trebuild();\n\t\t\treturn cachedTags;\n\t\t},\n\t\tresolveTitleTemplate() {\n\t\t\trebuild();\n\t\t\treturn cachedTitleTemplate;\n\t\t},\n\t\tresolveHtmlAttrs() {\n\t\t\trebuild();\n\t\t\treturn cachedHtmlAttrs;\n\t\t},\n\t\tresolveBodyAttrs() {\n\t\t\trebuild();\n\t\t\treturn cachedBodyAttrs;\n\t\t}\n\t};\n}\nconst HeadContext = createContext(null);\n\n//#endregion\n//#region src/ssr.ts\nconst VOID_TAGS = new Set([\n\t\"meta\",\n\t\"link\",\n\t\"base\"\n]);\nasync function renderWithHead(app) {\n\tconst ctx = createHeadContext();\n\tfunction HeadInjector() {\n\t\tpushContext(new Map([[HeadContext.id, ctx]]));\n\t\treturn app;\n\t}\n\tconst html = await renderToString(h(HeadInjector, null));\n\tconst titleTemplate = ctx.resolveTitleTemplate();\n\treturn {\n\t\thtml,\n\t\thead: ctx.resolve().map((tag) => serializeTag(tag, titleTemplate)).join(\"\\n \"),\n\t\thtmlAttrs: ctx.resolveHtmlAttrs(),\n\t\tbodyAttrs: ctx.resolveBodyAttrs()\n\t};\n}\nfunction serializeTag(tag, titleTemplate) {\n\tif (tag.tag === \"title\") {\n\t\tconst raw = tag.children || \"\";\n\t\treturn `<title>${esc(titleTemplate ? typeof titleTemplate === \"function\" ? titleTemplate(raw) : titleTemplate.replace(/%s/g, raw) : raw)}</title>`;\n\t}\n\tconst props = tag.props;\n\tconst attrs = props ? Object.entries(props).map(([k, v]) => `${k}=\"${esc(v)}\"`).join(\" \") : \"\";\n\tconst open = attrs ? `<${tag.tag} ${attrs}` : `<${tag.tag}`;\n\tif (VOID_TAGS.has(tag.tag)) return `${open} />`;\n\treturn `${open}>${(tag.children || \"\").replace(/<\\/(script|style|noscript)/gi, \"<\\\\/$1\").replace(/<!--/g, \"<\\\\!--\")}</${tag.tag}>`;\n}\nconst ESC_RE = /[&<>\"]/g;\nconst ESC_MAP = {\n\t\"&\": \"&amp;\",\n\t\"<\": \"&lt;\",\n\t\">\": \"&gt;\",\n\t\"\\\"\": \"&quot;\"\n};\nfunction esc(s) {\n\treturn ESC_RE.test(s) ? s.replace(ESC_RE, (ch) => ESC_MAP[ch]) : s;\n}\n\n//#endregion\nexport { renderWithHead };\n//# sourceMappingURL=ssr.js.map","/**\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/ssr\"\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\nconst __DEV__ = typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\"\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 if (__DEV__) {\n console.error(\"[Pyreon Server] SSR render failed:\", err)\n }\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 if (__DEV__) {\n console.error(\"[Pyreon Server] Stream render failed:\", err)\n }\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":["../../head/lib/ssr.js","../src/html.ts","../src/handler.ts","../src/island.ts","../src/ssg.ts"],"sourcesContent":["import { createContext, h, pushContext } from \"@pyreon/core\";\nimport { renderToString } from \"@pyreon/runtime-server\";\n\n//#region src/context.ts\nfunction createHeadContext() {\n\tconst map = /* @__PURE__ */ new Map();\n\tlet dirty = true;\n\tlet cachedTags = [];\n\tlet cachedTitleTemplate;\n\tlet cachedHtmlAttrs = {};\n\tlet cachedBodyAttrs = {};\n\tfunction rebuild() {\n\t\tif (!dirty) return;\n\t\tdirty = false;\n\t\tconst keyed = /* @__PURE__ */ new Map();\n\t\tconst unkeyed = [];\n\t\tlet titleTemplate;\n\t\tconst htmlAttrs = {};\n\t\tconst bodyAttrs = {};\n\t\tfor (const entry of map.values()) {\n\t\t\tfor (const tag of entry.tags) if (tag.key) keyed.set(tag.key, tag);\n\t\t\telse unkeyed.push(tag);\n\t\t\tif (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;\n\t\t\tif (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs);\n\t\t\tif (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs);\n\t\t}\n\t\tcachedTags = [...keyed.values(), ...unkeyed];\n\t\tcachedTitleTemplate = titleTemplate;\n\t\tcachedHtmlAttrs = htmlAttrs;\n\t\tcachedBodyAttrs = bodyAttrs;\n\t}\n\treturn {\n\t\tadd(id, entry) {\n\t\t\tmap.set(id, entry);\n\t\t\tdirty = true;\n\t\t},\n\t\tremove(id) {\n\t\t\tmap.delete(id);\n\t\t\tdirty = true;\n\t\t},\n\t\tresolve() {\n\t\t\trebuild();\n\t\t\treturn cachedTags;\n\t\t},\n\t\tresolveTitleTemplate() {\n\t\t\trebuild();\n\t\t\treturn cachedTitleTemplate;\n\t\t},\n\t\tresolveHtmlAttrs() {\n\t\t\trebuild();\n\t\t\treturn cachedHtmlAttrs;\n\t\t},\n\t\tresolveBodyAttrs() {\n\t\t\trebuild();\n\t\t\treturn cachedBodyAttrs;\n\t\t}\n\t};\n}\nconst HeadContext = createContext(null);\n\n//#endregion\n//#region src/ssr.ts\nconst VOID_TAGS = new Set([\n\t\"meta\",\n\t\"link\",\n\t\"base\"\n]);\nasync function renderWithHead(app) {\n\tconst ctx = createHeadContext();\n\tfunction HeadInjector() {\n\t\tpushContext(new Map([[HeadContext.id, ctx]]));\n\t\treturn app;\n\t}\n\tconst html = await renderToString(h(HeadInjector, null));\n\tconst titleTemplate = ctx.resolveTitleTemplate();\n\treturn {\n\t\thtml,\n\t\thead: ctx.resolve().map((tag) => serializeTag(tag, titleTemplate)).join(\"\\n \"),\n\t\thtmlAttrs: ctx.resolveHtmlAttrs(),\n\t\tbodyAttrs: ctx.resolveBodyAttrs()\n\t};\n}\nfunction serializeTag(tag, titleTemplate) {\n\tif (tag.tag === \"title\") {\n\t\tconst raw = tag.children || \"\";\n\t\treturn `<title>${esc(titleTemplate ? typeof titleTemplate === \"function\" ? titleTemplate(raw) : titleTemplate.replace(/%s/g, raw) : raw)}</title>`;\n\t}\n\tconst props = tag.props;\n\tconst attrs = props ? Object.entries(props).map(([k, v]) => `${k}=\"${esc(v)}\"`).join(\" \") : \"\";\n\tconst open = attrs ? `<${tag.tag} ${attrs}` : `<${tag.tag}`;\n\tif (VOID_TAGS.has(tag.tag)) return `${open} />`;\n\treturn `${open}>${(tag.children || \"\").replace(/<\\/(script|style|noscript)/gi, \"<\\\\/$1\").replace(/<!--/g, \"<\\\\!--\")}</${tag.tag}>`;\n}\nconst ESC_RE = /[&<>\"]/g;\nconst ESC_MAP = {\n\t\"&\": \"&amp;\",\n\t\"<\": \"&lt;\",\n\t\">\": \"&gt;\",\n\t\"\\\"\": \"&quot;\"\n};\nfunction esc(s) {\n\treturn ESC_RE.test(s) ? s.replace(ESC_RE, (ch) => ESC_MAP[ch]) : s;\n}\n\n//#endregion\nexport { renderWithHead };\n//# sourceMappingURL=ssr.js.map","/**\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/ssr'\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\nconst __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\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 if (__DEV__) {\n console.error('[Pyreon Server] SSR render failed:', err)\n }\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 if (__DEV__) {\n console.error('[Pyreon Server] Stream render failed:', err)\n }\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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"}
@@ -63,7 +63,7 @@ interface HandlerOptions {
63
63
  * "string" (default) — full renderToString, complete HTML in one response
64
64
  * "stream" — progressive streaming via renderToStream (Suspense out-of-order)
65
65
  */
66
- mode?: "string" | "stream";
66
+ mode?: 'string' | 'stream';
67
67
  }
68
68
  declare function createHandler(options: HandlerOptions): (req: Request) => Promise<Response>;
69
69
  //#endregion
@@ -104,7 +104,7 @@ declare function processCompiledTemplate(compiled: CompiledTemplate, data: Templ
104
104
  declare function buildScripts(clientEntry: string, loaderData: Record<string, unknown> | null): string;
105
105
  //#endregion
106
106
  //#region src/island.d.ts
107
- type HydrationStrategy = "load" | "idle" | "visible" | "never" | `media(${string})`;
107
+ type HydrationStrategy = 'load' | 'idle' | 'visible' | 'never' | `media(${string})`;
108
108
  interface IslandOptions {
109
109
  /** Unique name — must match the key in the client-side hydrateIslands() registry */
110
110
  name: string;
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@pyreon/server",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "description": "SSR handler, SSG prerender, and island architecture for Pyreon",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/server#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/core/server"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/server#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
15
  "files": [
16
16
  "lib",
17
17
  "src",
18
18
  "README.md",
19
19
  "LICENSE"
20
20
  ],
21
- "sideEffects": false,
22
21
  "type": "module",
22
+ "sideEffects": false,
23
23
  "main": "./lib/index.js",
24
24
  "module": "./lib/index.js",
25
25
  "types": "./lib/types/index.d.ts",
@@ -35,23 +35,23 @@
35
35
  "types": "./lib/types/client.d.ts"
36
36
  }
37
37
  },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
38
41
  "scripts": {
39
42
  "build": "vl_rolldown_build",
40
43
  "dev": "vl_rolldown_build-watch",
41
44
  "test": "vitest run",
42
45
  "typecheck": "tsc --noEmit",
43
- "lint": "biome check .",
46
+ "lint": "oxlint .",
44
47
  "prepublishOnly": "bun run build"
45
48
  },
46
49
  "dependencies": {
47
- "@pyreon/core": "^0.11.5",
48
- "@pyreon/head": "^0.11.5",
49
- "@pyreon/reactivity": "^0.11.5",
50
- "@pyreon/router": "^0.11.5",
51
- "@pyreon/runtime-dom": "^0.11.5",
52
- "@pyreon/runtime-server": "^0.11.5"
53
- },
54
- "publishConfig": {
55
- "access": "public"
50
+ "@pyreon/core": "^0.11.6",
51
+ "@pyreon/head": "^0.11.6",
52
+ "@pyreon/reactivity": "^0.11.6",
53
+ "@pyreon/router": "^0.11.6",
54
+ "@pyreon/runtime-dom": "^0.11.6",
55
+ "@pyreon/runtime-server": "^0.11.6"
56
56
  }
57
57
  }
package/src/client.ts CHANGED
@@ -25,11 +25,11 @@
25
25
  * ```
26
26
  */
27
27
 
28
- import type { ComponentFn } from "@pyreon/core"
29
- import { h } from "@pyreon/core"
30
- import { createRouter, hydrateLoaderData, type RouteRecord, RouterProvider } from "@pyreon/router"
31
- import { hydrateRoot, mount } from "@pyreon/runtime-dom"
32
- import type { HydrationStrategy } from "./island"
28
+ import type { ComponentFn } from '@pyreon/core'
29
+ import { h } from '@pyreon/core'
30
+ import { createRouter, hydrateLoaderData, type RouteRecord, RouterProvider } from '@pyreon/router'
31
+ import { hydrateRoot, mount } from '@pyreon/runtime-dom'
32
+ import type { HydrationStrategy } from './island'
33
33
 
34
34
  // ─── Full app hydration ──────────────────────────────────────────────────────
35
35
 
@@ -53,20 +53,20 @@ export interface StartClientOptions {
53
53
  * Returns a cleanup function that unmounts the app.
54
54
  */
55
55
  export function startClient(options: StartClientOptions): () => void {
56
- const { App, routes, container = "#app" } = options
56
+ const { App, routes, container = '#app' } = options
57
57
 
58
- const el = typeof container === "string" ? document.querySelector(container) : container
58
+ const el = typeof container === 'string' ? document.querySelector(container) : container
59
59
 
60
60
  if (!el) {
61
61
  throw new Error(`[pyreon/client] Container "${container}" not found`)
62
62
  }
63
63
 
64
64
  // Create client-side router (history mode to match SSR)
65
- const router = createRouter({ routes, mode: "history" })
65
+ const router = createRouter({ routes, mode: 'history' })
66
66
 
67
67
  // Hydrate loader data from SSR (avoids re-fetching on initial render)
68
68
  const loaderData = (window as unknown as Record<string, unknown>).__PYREON_LOADER_DATA__
69
- if (loaderData && typeof loaderData === "object") {
69
+ if (loaderData && typeof loaderData === 'object') {
70
70
  hydrateLoaderData(router as never, loaderData as Record<string, unknown>)
71
71
  }
72
72
 
@@ -101,11 +101,11 @@ type IslandLoader = () => Promise<{ default: ComponentFn } | ComponentFn>
101
101
  * Returns a cleanup function that disconnects any pending observers/listeners.
102
102
  */
103
103
  export function hydrateIslands(registry: Record<string, IslandLoader>): () => void {
104
- const islands = document.querySelectorAll("pyreon-island")
104
+ const islands = document.querySelectorAll('pyreon-island')
105
105
  const cleanups: (() => void)[] = []
106
106
 
107
107
  for (const el of islands) {
108
- const componentId = el.getAttribute("data-component")
108
+ const componentId = el.getAttribute('data-component')
109
109
  if (!componentId) continue
110
110
 
111
111
  const loader = registry[componentId]
@@ -114,8 +114,8 @@ export function hydrateIslands(registry: Record<string, IslandLoader>): () => vo
114
114
  continue
115
115
  }
116
116
 
117
- const strategy = (el.getAttribute("data-hydrate") ?? "load") as HydrationStrategy
118
- const propsJson = el.getAttribute("data-props") ?? "{}"
117
+ const strategy = (el.getAttribute('data-hydrate') ?? 'load') as HydrationStrategy
118
+ const propsJson = el.getAttribute('data-props') ?? '{}'
119
119
 
120
120
  const cleanup = scheduleHydration(el as HTMLElement, loader, propsJson, strategy)
121
121
  if (cleanup) cleanups.push(cleanup)
@@ -138,12 +138,12 @@ function scheduleHydration(
138
138
  }
139
139
 
140
140
  switch (strategy) {
141
- case "load":
141
+ case 'load':
142
142
  hydrate()
143
143
  return null
144
144
 
145
- case "idle": {
146
- if ("requestIdleCallback" in window) {
145
+ case 'idle': {
146
+ if ('requestIdleCallback' in window) {
147
147
  const id = requestIdleCallback(hydrate)
148
148
  return () => {
149
149
  cancelled = true
@@ -157,15 +157,15 @@ function scheduleHydration(
157
157
  }
158
158
  }
159
159
 
160
- case "visible":
160
+ case 'visible':
161
161
  return observeVisibility(el, hydrate)
162
162
 
163
- case "never":
163
+ case 'never':
164
164
  return null
165
165
 
166
166
  default:
167
167
  // media(query)
168
- if (strategy.startsWith("media(")) {
168
+ if (strategy.startsWith('media(')) {
169
169
  const query = strategy.slice(6, -1)
170
170
  const mql = window.matchMedia(query)
171
171
  if (mql.matches) {
@@ -174,14 +174,14 @@ function scheduleHydration(
174
174
  }
175
175
  const onChange = (e: MediaQueryListEvent) => {
176
176
  if (e.matches) {
177
- mql.removeEventListener("change", onChange)
177
+ mql.removeEventListener('change', onChange)
178
178
  hydrate()
179
179
  }
180
180
  }
181
- mql.addEventListener("change", onChange)
181
+ mql.addEventListener('change', onChange)
182
182
  return () => {
183
183
  cancelled = true
184
- mql.removeEventListener("change", onChange)
184
+ mql.removeEventListener('change', onChange)
185
185
  }
186
186
  }
187
187
  hydrate()
@@ -194,13 +194,13 @@ async function hydrateIsland(
194
194
  loader: IslandLoader,
195
195
  propsJson: string,
196
196
  ): Promise<void> {
197
- const name = el.getAttribute("data-component") ?? "unknown"
197
+ const name = el.getAttribute('data-component') ?? 'unknown'
198
198
  try {
199
199
  let props: Record<string, unknown>
200
200
  try {
201
201
  props = JSON.parse(propsJson)
202
- if (typeof props !== "object" || props === null || Array.isArray(props)) {
203
- throw new TypeError("Expected object")
202
+ if (typeof props !== 'object' || props === null || Array.isArray(props)) {
203
+ throw new TypeError('Expected object')
204
204
  }
205
205
  } catch (parseErr) {
206
206
  console.error(`Invalid island props JSON for "${name}"`, parseErr)
@@ -208,7 +208,7 @@ async function hydrateIsland(
208
208
  }
209
209
 
210
210
  const mod = await loader()
211
- const Comp = typeof mod === "function" ? mod : mod.default
211
+ const Comp = typeof mod === 'function' ? mod : mod.default
212
212
  hydrateRoot(el, h(Comp, props))
213
213
  } catch (err) {
214
214
  console.error(`Failed to hydrate island "${name}"`, err)
@@ -216,7 +216,7 @@ async function hydrateIsland(
216
216
  }
217
217
 
218
218
  function observeVisibility(el: HTMLElement, callback: () => void): (() => void) | null {
219
- if (!("IntersectionObserver" in window)) {
219
+ if (!('IntersectionObserver' in window)) {
220
220
  callback()
221
221
  return null
222
222
  }
@@ -231,7 +231,7 @@ function observeVisibility(el: HTMLElement, callback: () => void): (() => void)
231
231
  }
232
232
  }
233
233
  },
234
- { rootMargin: "200px" },
234
+ { rootMargin: '200px' },
235
235
  )
236
236
 
237
237
  observer.observe(el)
package/src/handler.ts CHANGED
@@ -24,17 +24,17 @@
24
24
  * Bun.serve({ fetch: handler })
25
25
  */
26
26
 
27
- import type { ComponentFn } from "@pyreon/core"
28
- import { h } from "@pyreon/core"
29
- import { renderWithHead } from "@pyreon/head/ssr"
27
+ import type { ComponentFn } from '@pyreon/core'
28
+ import { h } from '@pyreon/core'
29
+ import { renderWithHead } from '@pyreon/head/ssr'
30
30
  import {
31
31
  createRouter,
32
32
  prefetchLoaderData,
33
33
  type RouteRecord,
34
34
  RouterProvider,
35
35
  serializeLoaderData,
36
- } from "@pyreon/router"
37
- import { renderToStream, runWithRequestContext } from "@pyreon/runtime-server"
36
+ } from '@pyreon/router'
37
+ import { renderToStream, runWithRequestContext } from '@pyreon/runtime-server'
38
38
  import {
39
39
  buildClientEntryTag,
40
40
  buildScriptsFast,
@@ -42,10 +42,10 @@ import {
42
42
  compileTemplate,
43
43
  DEFAULT_TEMPLATE,
44
44
  processCompiledTemplate,
45
- } from "./html"
46
- import type { Middleware, MiddlewareContext } from "./middleware"
45
+ } from './html'
46
+ import type { Middleware, MiddlewareContext } from './middleware'
47
47
 
48
- const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "production"
48
+ const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
49
49
 
50
50
  export interface HandlerOptions {
51
51
  /** Root application component */
@@ -70,7 +70,7 @@ export interface HandlerOptions {
70
70
  * "string" (default) — full renderToString, complete HTML in one response
71
71
  * "stream" — progressive streaming via renderToStream (Suspense out-of-order)
72
72
  */
73
- mode?: "string" | "stream"
73
+ mode?: 'string' | 'stream'
74
74
  }
75
75
 
76
76
  export function createHandler(options: HandlerOptions): (req: Request) => Promise<Response> {
@@ -78,9 +78,9 @@ export function createHandler(options: HandlerOptions): (req: Request) => Promis
78
78
  App,
79
79
  routes,
80
80
  template = DEFAULT_TEMPLATE,
81
- clientEntry = "/src/entry-client.ts",
81
+ clientEntry = '/src/entry-client.ts',
82
82
  middleware = [],
83
- mode = "string",
83
+ mode = 'string',
84
84
  } = options
85
85
 
86
86
  // Pre-compile once at handler creation — avoids 3x string scan per request
@@ -96,7 +96,7 @@ export function createHandler(options: HandlerOptions): (req: Request) => Promis
96
96
  req,
97
97
  url,
98
98
  path,
99
- headers: new Headers({ "Content-Type": "text/html; charset=utf-8" }),
99
+ headers: new Headers({ 'Content-Type': 'text/html; charset=utf-8' }),
100
100
  locals: {},
101
101
  }
102
102
 
@@ -106,7 +106,7 @@ export function createHandler(options: HandlerOptions): (req: Request) => Promis
106
106
  }
107
107
 
108
108
  // ── Per-request router ────────────────────────────────────────────────────
109
- const router = createRouter({ routes, mode: "history", url: path })
109
+ const router = createRouter({ routes, mode: 'history', url: path })
110
110
 
111
111
  return runWithRequestContext(async () => {
112
112
  try {
@@ -116,7 +116,7 @@ export function createHandler(options: HandlerOptions): (req: Request) => Promis
116
116
  // Build the VNode tree
117
117
  const app = h(RouterProvider, { router }, h(App, null))
118
118
 
119
- if (mode === "stream") {
119
+ if (mode === 'stream') {
120
120
  return renderStreamResponse(app, router, compiled, clientEntryTag, ctx.headers)
121
121
  }
122
122
 
@@ -129,11 +129,11 @@ export function createHandler(options: HandlerOptions): (req: Request) => Promis
129
129
  return new Response(fullHtml, { status: 200, headers: ctx.headers })
130
130
  } catch (err) {
131
131
  if (__DEV__) {
132
- console.error("[Pyreon Server] SSR render failed:", err)
132
+ console.error('[Pyreon Server] SSR render failed:', err)
133
133
  }
134
- return new Response("Internal Server Error", {
134
+ return new Response('Internal Server Error', {
135
135
  status: 500,
136
- headers: { "Content-Type": "text/plain" },
136
+ headers: { 'Content-Type': 'text/plain' },
137
137
  })
138
138
  }
139
139
  })
@@ -183,7 +183,7 @@ async function renderStreamResponse(
183
183
  push(shellTail)
184
184
  } catch (err) {
185
185
  if (__DEV__) {
186
- console.error("[Pyreon Server] Stream render failed:", err)
186
+ console.error('[Pyreon Server] Stream render failed:', err)
187
187
  }
188
188
  // Emit an inline error indicator — status code is already sent (200)
189
189
  push(`<script>console.error("[pyreon/server] Stream render failed")</script>`)