@pyreon/runtime-server 0.13.1 → 0.14.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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +15 -2
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +16 -0
|
@@ -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/index.ts","uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"e6124ca4-1"}]}],"isRoot":true},"nodeParts":{"e6124ca4-1":{"renderedLength":15033,"gzipLength":4827,"brotliLength":0,"metaUid":"e6124ca4-0"}},"nodeMetas":{"e6124ca4-0":{"id":"/src/index.ts","moduleParts":{"index.js":"e6124ca4-1"},"imported":[{"uid":"e6124ca4-2"},{"uid":"e6124ca4-3"}],"importedBy":[],"isEntry":true},"e6124ca4-2":{"id":"node:async_hooks","moduleParts":{},"imported":[],"importedBy":[{"uid":"e6124ca4-0"}]},"e6124ca4-3":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"e6124ca4-0"}]}},"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
|
@@ -17,6 +17,7 @@ import { ForSymbol, Fragment, Suspense, cx, normalizeStyleValue, runWithHooks, s
|
|
|
17
17
|
* renderToStream(vnode) → ReadableStream<string>
|
|
18
18
|
*/
|
|
19
19
|
const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
|
|
20
|
+
const _countSink = globalThis;
|
|
20
21
|
const _streamCtxAls = new AsyncLocalStorage();
|
|
21
22
|
const _contextAls = new AsyncLocalStorage();
|
|
22
23
|
const _fallbackStack = [];
|
|
@@ -43,6 +44,7 @@ function withStoreContext(fn) {
|
|
|
43
44
|
/** Render a VNode tree to an HTML string. Supports async component functions. */
|
|
44
45
|
async function renderToString(root) {
|
|
45
46
|
if (root === null) return "";
|
|
47
|
+
if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.render");
|
|
46
48
|
return withStoreContext(() => _contextAls.run([], () => renderNode(root)));
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
@@ -67,6 +69,7 @@ function runWithRequestContext(fn) {
|
|
|
67
69
|
* Each renderToStream call gets its own isolated ALS context stack.
|
|
68
70
|
*/
|
|
69
71
|
function renderToStream(root) {
|
|
72
|
+
if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.stream");
|
|
70
73
|
return new ReadableStream({ start(controller) {
|
|
71
74
|
const enqueue = (chunk) => controller.enqueue(chunk);
|
|
72
75
|
let bid = 0;
|
|
@@ -92,7 +95,9 @@ async function streamVNode(vnode, enqueue) {
|
|
|
92
95
|
const { each, children, by } = vnode.props;
|
|
93
96
|
enqueue("<!--pyreon-for-->");
|
|
94
97
|
for (const item of each()) {
|
|
95
|
-
|
|
98
|
+
const key = by(item);
|
|
99
|
+
if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.for.keyMarker");
|
|
100
|
+
enqueue(`<!--k:${safeKeyForMarker(key)}-->`);
|
|
96
101
|
await streamNode(children(item), enqueue);
|
|
97
102
|
}
|
|
98
103
|
enqueue("<!--/pyreon-for-->");
|
|
@@ -109,6 +114,7 @@ async function streamComponentNode(vnode, enqueue) {
|
|
|
109
114
|
await streamSuspenseBoundary(vnode, enqueue);
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
117
|
+
if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.component");
|
|
112
118
|
try {
|
|
113
119
|
const { vnode: output } = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode));
|
|
114
120
|
const resolved = output instanceof Promise ? await output : output;
|
|
@@ -172,6 +178,7 @@ const SUSPENSE_SWAP_FN = "<script>function __NS(s,t){var e=document.getElementBy
|
|
|
172
178
|
* main stream enqueue so it always arrives after the fallback placeholder.
|
|
173
179
|
*/
|
|
174
180
|
async function streamSuspenseBoundary(vnode, enqueue) {
|
|
181
|
+
if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.suspense.boundary");
|
|
175
182
|
const ctx = _streamCtxAls.getStore();
|
|
176
183
|
const { fallback, children } = vnode.props;
|
|
177
184
|
if (!ctx) {
|
|
@@ -192,7 +199,10 @@ async function streamSuspenseBoundary(vnode, enqueue) {
|
|
|
192
199
|
ctx.suspenseDepth++;
|
|
193
200
|
const buf = [];
|
|
194
201
|
if (await Promise.race([streamNode(children ?? null, (s) => buf.push(s)).then(() => "resolved"), new Promise((resolve) => setTimeout(() => resolve("timeout"), SUSPENSE_TIMEOUT_MS))]) === "timeout") {
|
|
195
|
-
if (__DEV__)
|
|
202
|
+
if (__DEV__) {
|
|
203
|
+
_countSink.__pyreon_count__?.("runtime-server.suspense.fallback");
|
|
204
|
+
console.warn(`[Pyreon SSR] Suspense boundary timed out after ${SUSPENSE_TIMEOUT_MS}ms — fallback will remain.`);
|
|
205
|
+
}
|
|
196
206
|
return;
|
|
197
207
|
}
|
|
198
208
|
mainEnqueue(`<template id="pyreon-t-${id}">${buf.join("").replace(/<\/template/gi, "<\\/template")}</template>`);
|
|
@@ -221,6 +231,7 @@ async function renderNode(node) {
|
|
|
221
231
|
let forHtml = "<!--pyreon-for-->";
|
|
222
232
|
for (const item of each()) {
|
|
223
233
|
const key = by(item);
|
|
234
|
+
if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.for.keyMarker");
|
|
224
235
|
forHtml += `<!--k:${safeKeyForMarker(key)}-->`;
|
|
225
236
|
forHtml += await renderNode(children(item));
|
|
226
237
|
}
|
|
@@ -236,6 +247,7 @@ async function renderChildren(children) {
|
|
|
236
247
|
return html;
|
|
237
248
|
}
|
|
238
249
|
async function renderComponent(vnode) {
|
|
250
|
+
if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.component");
|
|
239
251
|
const { vnode: output } = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode));
|
|
240
252
|
if (output instanceof Promise) {
|
|
241
253
|
const resolved = await output;
|
|
@@ -383,6 +395,7 @@ function warnIfUnsafeTag(tag) {
|
|
|
383
395
|
const NEEDS_ESCAPE_RE = /[&<>"']/;
|
|
384
396
|
function escapeHtml(str) {
|
|
385
397
|
if (!NEEDS_ESCAPE_RE.test(str)) return str;
|
|
398
|
+
if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.escape");
|
|
386
399
|
return str.replace(/[&<>"']/g, (c) => ESCAPE_MAP[c] ?? c);
|
|
387
400
|
}
|
|
388
401
|
/**
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @pyreon/runtime-server — SSR/SSG renderer for Pyreon.\n *\n * Walks a VNode tree and produces HTML strings.\n * Signal accessors (reactive getters `() => value`) are called synchronously\n * to snapshot their current value — no effects are set up on the server.\n *\n * Async components (`async function Component()`) are fully supported:\n * renderToString will await them before continuing the tree walk.\n *\n * API:\n * renderToString(vnode) → Promise<string>\n * renderToStream(vnode) → ReadableStream<string>\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport type { ClassValue, ComponentFn, ForProps, VNode, VNodeChild } from '@pyreon/core'\nimport {\n cx,\n ForSymbol,\n Fragment,\n normalizeStyleValue,\n runWithHooks,\n Suspense,\n setContextStackProvider,\n} from '@pyreon/core'\n\nconst __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\n\n// ─── Streaming Suspense context ───────────────────────────────────────────────\n// Tracks in-flight async Suspense boundary resolutions within a single stream.\n\ninterface StreamCtx {\n pending: Promise<void>[]\n nextId: () => number\n mainEnqueue: (s: string) => void\n /** Depth counter — non-zero when rendering inside a Suspense child resolution. */\n suspenseDepth: number\n}\n\nconst _streamCtxAls = new AsyncLocalStorage<StreamCtx>()\n\n// ─── Concurrent SSR context isolation ────────────────────────────────────────\n// Each renderToString call runs in its own ALS store (a fresh empty stack[]).\n// Concurrent requests never share context frames.\n\nconst _contextAls = new AsyncLocalStorage<Map<symbol, unknown>[]>()\nconst _fallbackStack: Map<symbol, unknown>[] = []\n\nsetContextStackProvider(() => _contextAls.getStore() ?? _fallbackStack)\n\n// ─── Store isolation (optional) ───────────────────────────────────────────────\n// A second ALS isolates store registries between concurrent requests.\n// Activated only when the user calls configureStoreIsolation().\n\nconst _storeAls = new AsyncLocalStorage<Map<string, unknown>>()\nlet _storeIsolationActive = false\n\n/**\n * Wire up per-request store isolation.\n * Call once at server startup, passing a `setStoreRegistryProvider` function.\n *\n * @example\n * import { configureStoreIsolation } from \"@pyreon/runtime-server\"\n * configureStoreIsolation(setStoreRegistryProvider)\n */\nexport function configureStoreIsolation(\n setStoreRegistryProvider: (fn: () => Map<string, unknown>) => void,\n): void {\n setStoreRegistryProvider(() => _storeAls.getStore() ?? new Map())\n _storeIsolationActive = true\n}\n\n/** Wrap a function call in a fresh store registry (no-op if isolation not configured). */\nfunction withStoreContext<T>(fn: () => T): T {\n if (!_storeIsolationActive) return fn()\n return _storeAls.run(new Map(), fn)\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/** Render a VNode tree to an HTML string. Supports async component functions. */\nexport async function renderToString(root: VNode | null): Promise<string> {\n if (root === null) return ''\n // Each call gets a fresh isolated context stack and (optionally) store registry\n return withStoreContext(() => _contextAls.run([], () => renderNode(root)))\n}\n\n/**\n * Run an async function with a fresh, isolated context stack and store registry.\n * Useful when you need to call Pyreon APIs (e.g. useHead, prefetchLoaderData)\n * outside of renderToString but still want per-request isolation.\n */\nexport function runWithRequestContext<T>(fn: () => Promise<T>): Promise<T> {\n return withStoreContext(() => _contextAls.run([], fn))\n}\n\n/**\n * Render a VNode tree to a Web-standard ReadableStream of HTML chunks.\n *\n * True progressive streaming: HTML is flushed to the client as soon as each\n * node is ready. Synchronous subtrees are enqueued immediately; async component\n * boundaries are awaited in-order and their output is enqueued as it resolves.\n *\n * Suspense boundaries are streamed out-of-order: the fallback is emitted\n * immediately, and the resolved children are sent as a `<template>` + inline\n * swap script once ready — without blocking the rest of the page.\n *\n * Each renderToStream call gets its own isolated ALS context stack.\n */\nexport function renderToStream(root: VNode | null): ReadableStream<string> {\n return new ReadableStream<string>({\n start(controller) {\n const enqueue = (chunk: string) => controller.enqueue(chunk)\n let bid = 0\n const ctx: StreamCtx = {\n pending: [],\n nextId: () => bid++,\n mainEnqueue: enqueue,\n suspenseDepth: 0,\n }\n return withStoreContext(() =>\n _contextAls.run([], () =>\n _streamCtxAls\n .run(ctx, async () => {\n await streamNode(root, enqueue)\n // Drain all pending Suspense resolutions (may spawn nested ones)\n while (ctx.pending.length > 0) {\n await Promise.all(ctx.pending.splice(0))\n }\n controller.close()\n })\n .catch((err) => controller.error(err)),\n ),\n )\n },\n })\n}\n\n// ─── Streaming renderer ───────────────────────────────────────────────────────\n\nasync function streamVNode(vnode: VNode, enqueue: (s: string) => void): Promise<void> {\n if (vnode.type === Fragment) {\n for (const child of vnode.children) await streamNode(child, enqueue)\n return\n }\n\n if (vnode.type === (ForSymbol as unknown as string)) {\n const { each, children, by } = vnode.props as unknown as ForProps<unknown>\n enqueue('<!--pyreon-for-->')\n for (const item of each()) {\n const key = by(item)\n enqueue(`<!--k:${safeKeyForMarker(key)}-->`)\n await streamNode(children(item) as VNodeChild, enqueue)\n }\n enqueue('<!--/pyreon-for-->')\n return\n }\n\n if (typeof vnode.type === 'function') {\n await streamComponentNode(vnode, enqueue)\n return\n }\n\n await streamElementNode(vnode, enqueue)\n}\n\nasync function streamComponentNode(vnode: VNode, enqueue: (s: string) => void): Promise<void> {\n if (vnode.type === Suspense) {\n await streamSuspenseBoundary(vnode, enqueue)\n return\n }\n try {\n const { vnode: output } = runWithHooks(vnode.type as ComponentFn, mergeChildrenIntoProps(vnode))\n const resolved = output instanceof Promise ? await output : output\n if (resolved !== null) await streamNode(resolved, enqueue)\n } catch (err) {\n if (__DEV__) {\n const name = (vnode.type as ComponentFn).name || 'Anonymous'\n console.error(`[Pyreon SSR] Error rendering <${name}>:`, err)\n }\n // Inside a Suspense child resolution, re-throw so the boundary can catch and\n // suppress the swap (fallback stays visible). Outside Suspense, swallow the\n // error and emit a marker so the stream can continue.\n const ctx = _streamCtxAls.getStore()\n if (ctx && ctx.suspenseDepth > 0) throw err\n enqueue('<!--pyreon-error-->')\n }\n}\n\nasync function streamElementNode(vnode: VNode, enqueue: (s: string) => void): Promise<void> {\n const tag = vnode.type as string\n warnIfUnsafeTag(tag)\n let open = `<${tag}`\n const props = vnode.props as Record<string, unknown>\n for (const key in props) {\n const attr = renderProp(key, props[key])\n if (attr) open += ` ${attr}`\n }\n if (isVoidElement(tag)) {\n enqueue(`${open} />`)\n return\n }\n enqueue(`${open}>`)\n // `dangerouslySetInnerHTML` and `innerHTML` both become inner content\n // of the element (NOT attributes). Skipped in `renderPropSkipped` to\n // keep them out of the open-tag attribute list. Function values —\n // emitted by the compiler for signal-derived prop expressions — are\n // called once at render time (SSR is one-shot; any reactivity happens\n // post-hydration on the client).\n const dangerous = props.dangerouslySetInnerHTML as { __html: string } | (() => { __html: string }) | undefined\n const innerHtml = props.innerHTML as string | (() => string) | undefined\n const dangerousHtml =\n typeof dangerous === 'function' ? (dangerous as () => { __html: string })()?.__html : dangerous?.__html\n const plainInnerHtml =\n typeof innerHtml === 'function' ? (innerHtml as () => string)() : innerHtml\n if (dangerousHtml) {\n enqueue(dangerousHtml)\n } else if (plainInnerHtml != null && plainInnerHtml !== '') {\n enqueue(String(plainInnerHtml))\n } else {\n for (const child of vnode.children) await streamNode(child, enqueue)\n }\n enqueue(`</${tag}>`)\n}\n\nasync function streamNode(\n node: VNodeChild | null | (() => VNodeChild),\n enqueue: (s: string) => void,\n): Promise<void> {\n if (typeof node === 'function') {\n return streamNode((node as () => VNodeChild)(), enqueue)\n }\n if (node == null || node === false) return\n if (typeof node === 'string') {\n enqueue(escapeHtml(node))\n return\n }\n if (typeof node === 'number' || typeof node === 'boolean') {\n enqueue(String(node))\n return\n }\n if (Array.isArray(node)) {\n for (const child of node) await streamNode(child, enqueue)\n return\n }\n\n await streamVNode(node as VNode, enqueue)\n}\n\n// Inline swap helper emitted once per stream, before the first <template>\nconst SUSPENSE_SWAP_FN =\n '<script>function __NS(s,t){var e=document.getElementById(s),l=document.getElementById(t);' +\n 'if(e&&l){e.replaceWith(l.content.cloneNode(!0));l.remove()}}</script>'\n\n/**\n * Stream a Suspense boundary: emit fallback immediately, then resolve children\n * asynchronously and emit them as a `<template>` + client-side swap.\n *\n * The actual children HTML is buffered until fully resolved, then emitted to the\n * main stream enqueue so it always arrives after the fallback placeholder.\n */\nasync function streamSuspenseBoundary(vnode: VNode, enqueue: (s: string) => void): Promise<void> {\n const ctx = _streamCtxAls.getStore()\n const { fallback, children } = vnode.props as { fallback: VNodeChild; children?: VNodeChild }\n\n // No streaming context (e.g. called from renderToString) — render children inline\n if (!ctx) {\n const { vnode: output } = runWithHooks(Suspense as ComponentFn, vnode.props)\n if (output !== null) await streamNode(output, enqueue)\n return\n }\n\n const id = ctx.nextId()\n const { mainEnqueue } = ctx\n\n // Emit the swap helper function once (before first use)\n if (id === 0) mainEnqueue(SUSPENSE_SWAP_FN)\n\n // Stream the fallback synchronously (no await on children)\n mainEnqueue(`<div id=\"pyreon-s-${id}\">`)\n await streamNode(fallback ?? null, enqueue)\n mainEnqueue('</div>')\n\n // Capture the context store for the async resolution so it inherits context\n const ctxStore = _contextAls.getStore() ?? []\n\n // Queue async resolution — runs in parallel, emits to main stream when done.\n // Errors are caught per-boundary so one failing Suspense doesn't abort the stream.\n // Timeout prevents hung async children from keeping the stream open forever.\n const SUSPENSE_TIMEOUT_MS = 30_000\n\n ctx.pending.push(\n _contextAls.run(ctxStore, async () => {\n try {\n ctx.suspenseDepth++\n const buf: string[] = []\n\n // Race the async children against a timeout\n const result = await Promise.race([\n streamNode(children ?? null, (s) => buf.push(s)).then(() => 'resolved' as const),\n new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), SUSPENSE_TIMEOUT_MS)),\n ])\n\n if (result === 'timeout') {\n if (__DEV__) {\n console.warn(\n `[Pyreon SSR] Suspense boundary timed out after ${SUSPENSE_TIMEOUT_MS}ms — fallback will remain.`,\n )\n }\n // Fallback stays visible — no swap\n return\n }\n\n // Escape </template> in buffered content to prevent early close + XSS\n const content = buf.join('').replace(/<\\/template/gi, '<\\\\/template')\n mainEnqueue(`<template id=\"pyreon-t-${id}\">${content}</template>`)\n mainEnqueue(`<script>__NS(\"pyreon-s-${id}\",\"pyreon-t-${id}\")</script>`)\n } catch (err) {\n if (__DEV__) {\n console.error(\n `[Pyreon SSR] Suspense boundary caught an error — fallback will remain:`,\n err,\n )\n }\n // Fallback stays visible — no swap script emitted\n } finally {\n ctx.suspenseDepth--\n }\n }),\n )\n}\n\n// ─── Core renderer ───────────────────────────────────────────────────────────\n\nasync function renderNode(node: VNodeChild | (() => VNodeChild)): Promise<string> {\n // Reactive accessor — call it synchronously (snapshot)\n if (typeof node === 'function') {\n return renderNode((node as () => VNodeChild)())\n }\n\n if (node == null || node === false) return ''\n\n if (typeof node === 'string') return escapeHtml(node)\n if (typeof node === 'number' || typeof node === 'boolean') return String(node)\n\n if (Array.isArray(node)) {\n let html = ''\n for (const child of node) html += await renderNode(child)\n return html\n }\n\n const vnode = node as VNode\n\n if (vnode.type === Fragment) {\n return renderChildren(vnode.children)\n }\n\n if (vnode.type === (ForSymbol as unknown as string)) {\n const { each, children, by } = vnode.props as unknown as ForProps<unknown>\n let forHtml = '<!--pyreon-for-->'\n for (const item of each()) {\n const key = by(item)\n forHtml += `<!--k:${safeKeyForMarker(key)}-->`\n forHtml += await renderNode(children(item) as VNodeChild)\n }\n forHtml += '<!--/pyreon-for-->'\n return forHtml\n }\n\n if (typeof vnode.type === 'function') {\n return renderComponent(vnode as VNode & { type: ComponentFn })\n }\n\n return renderElement(vnode)\n}\n\nasync function renderChildren(children: VNodeChild[]): Promise<string> {\n let html = ''\n for (const child of children) html += await renderNode(child)\n return html\n}\n\nasync function renderComponent(vnode: VNode & { type: ComponentFn }): Promise<string> {\n const { vnode: output } = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode))\n\n // Async component function (async function Component()) — await the promise\n if (output instanceof Promise) {\n const resolved = await output\n if (resolved === null) return ''\n return renderNode(resolved)\n }\n\n if (output === null) return ''\n return renderNode(output)\n}\n\nasync function renderElement(vnode: VNode): Promise<string> {\n const tag = vnode.type as string\n warnIfUnsafeTag(tag)\n let html = `<${tag}`\n\n const props = vnode.props as Record<string, unknown>\n for (const key in props) {\n const attr = renderProp(key, props[key])\n if (attr) html += ` ${attr}`\n }\n\n if (isVoidElement(tag)) {\n html += ' />'\n return html\n }\n\n html += '>'\n\n // `dangerouslySetInnerHTML` and `innerHTML` become inner content of the\n // element (NOT attributes — skipped in `renderPropSkipped`). Function\n // values — emitted by the compiler for signal-derived prop expressions —\n // are called once at render time (SSR is one-shot; reactivity happens\n // post-hydration on the client). Kept in sync with `streamElementNode`.\n const dangerous = props.dangerouslySetInnerHTML as\n | { __html: string }\n | (() => { __html: string })\n | undefined\n const innerHtml = props.innerHTML as string | (() => string) | undefined\n const dangerousHtml =\n typeof dangerous === 'function'\n ? (dangerous as () => { __html: string })()?.__html\n : dangerous?.__html\n const plainInnerHtml =\n typeof innerHtml === 'function' ? (innerHtml as () => string)() : innerHtml\n if (dangerousHtml) {\n html += dangerousHtml\n } else if (plainInnerHtml != null && plainInnerHtml !== '') {\n html += String(plainInnerHtml)\n } else {\n for (const child of vnode.children) {\n html += await renderNode(child)\n }\n }\n\n html += `</${tag}>`\n return html\n}\n\nconst SSR_URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'poster', 'cite', 'data'])\nconst SSR_UNSAFE_URL_RE = /^\\s*(?:javascript|data):/i\n\nfunction renderPropSkipped(key: string): boolean {\n // `innerHTML` and `dangerouslySetInnerHTML` are NOT attributes — they\n // get written as inner content in `streamElementNode`. Without this\n // skip, `innerHTML` would be emitted as a literal HTML attribute\n // (`<span innerHTML=\"<svg>…\">`) and the client hydration would\n // fix it up — wasted bytes, hydration mismatch, and (with the recent\n // client-side `innerHTML` bug) literal closure text visible before\n // hydration completed.\n if (key === 'key' || key === 'ref') return true\n if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') return true\n if (/^on[A-Z]/.test(key)) return true\n return false\n}\n\nfunction renderPropValue(key: string, value: unknown): string | null {\n if (value === null || value === undefined || value === false) return null\n if (value === true) return escapeHtml(toAttrName(key))\n\n if (key === 'class') {\n const cls = cx(value as ClassValue)\n return cls ? `class=\"${escapeHtml(cls)}\"` : null\n }\n\n if (key === 'style') {\n const style = normalizeStyle(value)\n return style ? `style=\"${escapeHtml(style)}\"` : null\n }\n\n return `${escapeHtml(toAttrName(key))}=\"${escapeHtml(String(value))}\"`\n}\n\nfunction renderProp(key: string, value: unknown): string | null {\n if (renderPropSkipped(key)) return null\n\n if (typeof value === 'function') {\n return renderProp(key, (value as () => unknown)())\n }\n\n if (SSR_URL_ATTRS.has(key) && typeof value === 'string' && SSR_UNSAFE_URL_RE.test(value)) {\n return null\n }\n\n return renderPropValue(key, value)\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nconst VOID_ELEMENTS = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\nfunction isVoidElement(tag: string): boolean {\n return VOID_ELEMENTS.has(tag.toLowerCase())\n}\n\n/** camelCase prop → kebab-case HTML attribute (e.g. className → class, htmlFor → for) */\nfunction toAttrName(key: string): string {\n if (key === 'className') return 'class'\n if (key === 'htmlFor') return 'for'\n return key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)\n}\n\nfunction isStyleObject(value: unknown): value is Record<string, unknown> {\n if (!value) return false\n return typeof value === 'object'\n}\n\nfunction normalizeStyle(value: unknown): string {\n if (typeof value === 'string') return value\n if (isStyleObject(value)) {\n const proto = Object.getPrototypeOf(value)\n if (proto === Object.prototype || proto === null) {\n return Object.entries(value)\n .map(([k, v]) => `${toKebab(k)}: ${normalizeStyleValue(k, v)}`)\n .join('; ')\n }\n }\n return ''\n}\n\nfunction toKebab(str: string): string {\n return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)\n}\n\nconst ESCAPE_MAP: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n}\n\n/**\n * Encode a For-list key so it's safe to inline inside an HTML comment\n * marker `<!--k:KEY-->`. If a user-supplied key contains `-->` the naive\n * form breaks out of the comment and may inject markup. Per-byte URL\n * encoding with an extra `-` substitution makes `-->` impossible in the\n * output: `%2D%2D>` no longer terminates the comment. Client-side\n * hydration does not read the marker body today, so any reversible-or-\n * irreversible encoding works; this one is predictable enough for a\n * future consumer to decode if needed.\n */\nfunction safeKeyForMarker(key: unknown): string {\n return encodeURIComponent(String(key)).replace(/-/g, '%2D')\n}\n\n/**\n * Inverse of `safeKeyForMarker` — decode a marker-safe key back to the\n * original string. Not used by runtime today (hydration does not read\n * per-item `<!--k:KEY-->` markers) but shipped alongside the encoder so\n * future hydration or devtools consumers decode symmetrically without\n * having to re-derive the encoding from source.\n */\nexport function decodeKeyFromMarker(encoded: string): string {\n return decodeURIComponent(encoded.replace(/%2D/gi, '-'))\n}\n\n// Detect tag names that would break out of the `<TAG>` or `</TAG>` form\n// and inject HTML. If user data ever feeds `h(userTag, ...)` the attack\n// `userTag = 'div><script>alert(1)</script><div'` yields executable\n// markup. Framework doesn't HTML-escape tag names (React/Vue/Solid\n// match) — responsibility is on the caller — but a dev-mode warning\n// catches the mistake before it reaches prod. Safe tag pattern covers\n// HTML element names and custom elements (letter start, then\n// alphanumerics + hyphens).\nconst SAFE_TAG_RE = /^[a-zA-Z][a-zA-Z0-9-]*$/\nfunction warnIfUnsafeTag(tag: string): void {\n if (!__DEV__) return\n if (SAFE_TAG_RE.test(tag)) return\n // oxlint-disable-next-line no-console\n console.warn(\n `[Pyreon SSR] Tag name \"${tag}\" contains characters that could break HTML structure. ` +\n `Tag names must match /^[a-zA-Z][a-zA-Z0-9-]*$/. ` +\n `If user-supplied data drives a tag name, validate it against an allowlist before passing to h().`,\n )\n}\n\n// Fast test — most strings in SSR have no special chars (tag names, class names, etc.)\nconst NEEDS_ESCAPE_RE = /[&<>\"']/\n\nfunction escapeHtml(str: string): string {\n if (!NEEDS_ESCAPE_RE.test(str)) return str\n return str.replace(/[&<>\"']/g, (c) => ESCAPE_MAP[c] ?? c)\n}\n\n/**\n * Merge vnode.children into props.children for component rendering.\n * Matches the behavior of mount.ts and hydrate.ts so components can\n * access children passed via h(Comp, props, child1, child2).\n */\nfunction mergeChildrenIntoProps(vnode: VNode): Record<string, unknown> {\n if (\n vnode.children.length > 0 &&\n (vnode.props as Record<string, unknown>).children === undefined\n ) {\n return {\n ...vnode.props,\n children: vnode.children.length === 1 ? vnode.children[0] : vnode.children,\n }\n }\n return vnode.props as Record<string, unknown>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @pyreon/runtime-server — SSR/SSG renderer for Pyreon.\n *\n * Walks a VNode tree and produces HTML strings.\n * Signal accessors (reactive getters `() => value`) are called synchronously\n * to snapshot their current value — no effects are set up on the server.\n *\n * Async components (`async function Component()`) are fully supported:\n * renderToString will await them before continuing the tree walk.\n *\n * API:\n * renderToString(vnode) → Promise<string>\n * renderToStream(vnode) → ReadableStream<string>\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport type { ClassValue, ComponentFn, ForProps, VNode, VNodeChild } from '@pyreon/core'\nimport {\n cx,\n ForSymbol,\n Fragment,\n normalizeStyleValue,\n runWithHooks,\n Suspense,\n setContextStackProvider,\n} from '@pyreon/core'\n\nconst __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\n\n// Dev-mode perf counter sink. Zero coupling to @pyreon/perf-harness — we just\n// call the global if it's installed. Guarded on __DEV__ so NODE_ENV=production\n// short-circuits at runtime; @pyreon/runtime-server is a server package, so the\n// `typeof process` gate is correct here (not `import.meta.env.DEV`, which is a\n// browser-bundler concern). See .claude/rules/test-environment-parity.md.\nconst _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }\n\n// ─── Streaming Suspense context ───────────────────────────────────────────────\n// Tracks in-flight async Suspense boundary resolutions within a single stream.\n\ninterface StreamCtx {\n pending: Promise<void>[]\n nextId: () => number\n mainEnqueue: (s: string) => void\n /** Depth counter — non-zero when rendering inside a Suspense child resolution. */\n suspenseDepth: number\n}\n\nconst _streamCtxAls = new AsyncLocalStorage<StreamCtx>()\n\n// ─── Concurrent SSR context isolation ────────────────────────────────────────\n// Each renderToString call runs in its own ALS store (a fresh empty stack[]).\n// Concurrent requests never share context frames.\n\nconst _contextAls = new AsyncLocalStorage<Map<symbol, unknown>[]>()\nconst _fallbackStack: Map<symbol, unknown>[] = []\n\nsetContextStackProvider(() => _contextAls.getStore() ?? _fallbackStack)\n\n// ─── Store isolation (optional) ───────────────────────────────────────────────\n// A second ALS isolates store registries between concurrent requests.\n// Activated only when the user calls configureStoreIsolation().\n\nconst _storeAls = new AsyncLocalStorage<Map<string, unknown>>()\nlet _storeIsolationActive = false\n\n/**\n * Wire up per-request store isolation.\n * Call once at server startup, passing a `setStoreRegistryProvider` function.\n *\n * @example\n * import { configureStoreIsolation } from \"@pyreon/runtime-server\"\n * configureStoreIsolation(setStoreRegistryProvider)\n */\nexport function configureStoreIsolation(\n setStoreRegistryProvider: (fn: () => Map<string, unknown>) => void,\n): void {\n setStoreRegistryProvider(() => _storeAls.getStore() ?? new Map())\n _storeIsolationActive = true\n}\n\n/** Wrap a function call in a fresh store registry (no-op if isolation not configured). */\nfunction withStoreContext<T>(fn: () => T): T {\n if (!_storeIsolationActive) return fn()\n return _storeAls.run(new Map(), fn)\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/** Render a VNode tree to an HTML string. Supports async component functions. */\nexport async function renderToString(root: VNode | null): Promise<string> {\n if (root === null) return ''\n if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.render')\n // Each call gets a fresh isolated context stack and (optionally) store registry\n return withStoreContext(() => _contextAls.run([], () => renderNode(root)))\n}\n\n/**\n * Run an async function with a fresh, isolated context stack and store registry.\n * Useful when you need to call Pyreon APIs (e.g. useHead, prefetchLoaderData)\n * outside of renderToString but still want per-request isolation.\n */\nexport function runWithRequestContext<T>(fn: () => Promise<T>): Promise<T> {\n return withStoreContext(() => _contextAls.run([], fn))\n}\n\n/**\n * Render a VNode tree to a Web-standard ReadableStream of HTML chunks.\n *\n * True progressive streaming: HTML is flushed to the client as soon as each\n * node is ready. Synchronous subtrees are enqueued immediately; async component\n * boundaries are awaited in-order and their output is enqueued as it resolves.\n *\n * Suspense boundaries are streamed out-of-order: the fallback is emitted\n * immediately, and the resolved children are sent as a `<template>` + inline\n * swap script once ready — without blocking the rest of the page.\n *\n * Each renderToStream call gets its own isolated ALS context stack.\n */\nexport function renderToStream(root: VNode | null): ReadableStream<string> {\n if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.stream')\n return new ReadableStream<string>({\n start(controller) {\n const enqueue = (chunk: string) => controller.enqueue(chunk)\n let bid = 0\n const ctx: StreamCtx = {\n pending: [],\n nextId: () => bid++,\n mainEnqueue: enqueue,\n suspenseDepth: 0,\n }\n return withStoreContext(() =>\n _contextAls.run([], () =>\n _streamCtxAls\n .run(ctx, async () => {\n await streamNode(root, enqueue)\n // Drain all pending Suspense resolutions (may spawn nested ones)\n while (ctx.pending.length > 0) {\n await Promise.all(ctx.pending.splice(0))\n }\n controller.close()\n })\n .catch((err) => controller.error(err)),\n ),\n )\n },\n })\n}\n\n// ─── Streaming renderer ───────────────────────────────────────────────────────\n\nasync function streamVNode(vnode: VNode, enqueue: (s: string) => void): Promise<void> {\n if (vnode.type === Fragment) {\n for (const child of vnode.children) await streamNode(child, enqueue)\n return\n }\n\n if (vnode.type === (ForSymbol as unknown as string)) {\n const { each, children, by } = vnode.props as unknown as ForProps<unknown>\n enqueue('<!--pyreon-for-->')\n for (const item of each()) {\n const key = by(item)\n if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.for.keyMarker')\n enqueue(`<!--k:${safeKeyForMarker(key)}-->`)\n await streamNode(children(item) as VNodeChild, enqueue)\n }\n enqueue('<!--/pyreon-for-->')\n return\n }\n\n if (typeof vnode.type === 'function') {\n await streamComponentNode(vnode, enqueue)\n return\n }\n\n await streamElementNode(vnode, enqueue)\n}\n\nasync function streamComponentNode(vnode: VNode, enqueue: (s: string) => void): Promise<void> {\n if (vnode.type === Suspense) {\n await streamSuspenseBoundary(vnode, enqueue)\n return\n }\n if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.component')\n try {\n const { vnode: output } = runWithHooks(vnode.type as ComponentFn, mergeChildrenIntoProps(vnode))\n const resolved = output instanceof Promise ? await output : output\n if (resolved !== null) await streamNode(resolved, enqueue)\n } catch (err) {\n if (__DEV__) {\n const name = (vnode.type as ComponentFn).name || 'Anonymous'\n console.error(`[Pyreon SSR] Error rendering <${name}>:`, err)\n }\n // Inside a Suspense child resolution, re-throw so the boundary can catch and\n // suppress the swap (fallback stays visible). Outside Suspense, swallow the\n // error and emit a marker so the stream can continue.\n const ctx = _streamCtxAls.getStore()\n if (ctx && ctx.suspenseDepth > 0) throw err\n enqueue('<!--pyreon-error-->')\n }\n}\n\nasync function streamElementNode(vnode: VNode, enqueue: (s: string) => void): Promise<void> {\n const tag = vnode.type as string\n warnIfUnsafeTag(tag)\n let open = `<${tag}`\n const props = vnode.props as Record<string, unknown>\n for (const key in props) {\n const attr = renderProp(key, props[key])\n if (attr) open += ` ${attr}`\n }\n if (isVoidElement(tag)) {\n enqueue(`${open} />`)\n return\n }\n enqueue(`${open}>`)\n // `dangerouslySetInnerHTML` and `innerHTML` both become inner content\n // of the element (NOT attributes). Skipped in `renderPropSkipped` to\n // keep them out of the open-tag attribute list. Function values —\n // emitted by the compiler for signal-derived prop expressions — are\n // called once at render time (SSR is one-shot; any reactivity happens\n // post-hydration on the client).\n const dangerous = props.dangerouslySetInnerHTML as { __html: string } | (() => { __html: string }) | undefined\n const innerHtml = props.innerHTML as string | (() => string) | undefined\n const dangerousHtml =\n typeof dangerous === 'function' ? (dangerous as () => { __html: string })()?.__html : dangerous?.__html\n const plainInnerHtml =\n typeof innerHtml === 'function' ? (innerHtml as () => string)() : innerHtml\n if (dangerousHtml) {\n enqueue(dangerousHtml)\n } else if (plainInnerHtml != null && plainInnerHtml !== '') {\n enqueue(String(plainInnerHtml))\n } else {\n for (const child of vnode.children) await streamNode(child, enqueue)\n }\n enqueue(`</${tag}>`)\n}\n\nasync function streamNode(\n node: VNodeChild | null | (() => VNodeChild),\n enqueue: (s: string) => void,\n): Promise<void> {\n if (typeof node === 'function') {\n return streamNode((node as () => VNodeChild)(), enqueue)\n }\n if (node == null || node === false) return\n if (typeof node === 'string') {\n enqueue(escapeHtml(node))\n return\n }\n if (typeof node === 'number' || typeof node === 'boolean') {\n enqueue(String(node))\n return\n }\n if (Array.isArray(node)) {\n for (const child of node) await streamNode(child, enqueue)\n return\n }\n\n await streamVNode(node as VNode, enqueue)\n}\n\n// Inline swap helper emitted once per stream, before the first <template>\nconst SUSPENSE_SWAP_FN =\n '<script>function __NS(s,t){var e=document.getElementById(s),l=document.getElementById(t);' +\n 'if(e&&l){e.replaceWith(l.content.cloneNode(!0));l.remove()}}</script>'\n\n/**\n * Stream a Suspense boundary: emit fallback immediately, then resolve children\n * asynchronously and emit them as a `<template>` + client-side swap.\n *\n * The actual children HTML is buffered until fully resolved, then emitted to the\n * main stream enqueue so it always arrives after the fallback placeholder.\n */\nasync function streamSuspenseBoundary(vnode: VNode, enqueue: (s: string) => void): Promise<void> {\n if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.suspense.boundary')\n const ctx = _streamCtxAls.getStore()\n const { fallback, children } = vnode.props as { fallback: VNodeChild; children?: VNodeChild }\n\n // No streaming context (e.g. called from renderToString) — render children inline\n if (!ctx) {\n const { vnode: output } = runWithHooks(Suspense as ComponentFn, vnode.props)\n if (output !== null) await streamNode(output, enqueue)\n return\n }\n\n const id = ctx.nextId()\n const { mainEnqueue } = ctx\n\n // Emit the swap helper function once (before first use)\n if (id === 0) mainEnqueue(SUSPENSE_SWAP_FN)\n\n // Stream the fallback synchronously (no await on children)\n mainEnqueue(`<div id=\"pyreon-s-${id}\">`)\n await streamNode(fallback ?? null, enqueue)\n mainEnqueue('</div>')\n\n // Capture the context store for the async resolution so it inherits context\n const ctxStore = _contextAls.getStore() ?? []\n\n // Queue async resolution — runs in parallel, emits to main stream when done.\n // Errors are caught per-boundary so one failing Suspense doesn't abort the stream.\n // Timeout prevents hung async children from keeping the stream open forever.\n const SUSPENSE_TIMEOUT_MS = 30_000\n\n ctx.pending.push(\n _contextAls.run(ctxStore, async () => {\n try {\n ctx.suspenseDepth++\n const buf: string[] = []\n\n // Race the async children against a timeout\n const result = await Promise.race([\n streamNode(children ?? null, (s) => buf.push(s)).then(() => 'resolved' as const),\n new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), SUSPENSE_TIMEOUT_MS)),\n ])\n\n if (result === 'timeout') {\n if (__DEV__) {\n _countSink.__pyreon_count__?.('runtime-server.suspense.fallback')\n console.warn(\n `[Pyreon SSR] Suspense boundary timed out after ${SUSPENSE_TIMEOUT_MS}ms — fallback will remain.`,\n )\n }\n // Fallback stays visible — no swap\n return\n }\n\n // Escape </template> in buffered content to prevent early close + XSS\n const content = buf.join('').replace(/<\\/template/gi, '<\\\\/template')\n mainEnqueue(`<template id=\"pyreon-t-${id}\">${content}</template>`)\n mainEnqueue(`<script>__NS(\"pyreon-s-${id}\",\"pyreon-t-${id}\")</script>`)\n } catch (err) {\n if (__DEV__) {\n console.error(\n `[Pyreon SSR] Suspense boundary caught an error — fallback will remain:`,\n err,\n )\n }\n // Fallback stays visible — no swap script emitted\n } finally {\n ctx.suspenseDepth--\n }\n }),\n )\n}\n\n// ─── Core renderer ───────────────────────────────────────────────────────────\n\nasync function renderNode(node: VNodeChild | (() => VNodeChild)): Promise<string> {\n // Reactive accessor — call it synchronously (snapshot)\n if (typeof node === 'function') {\n return renderNode((node as () => VNodeChild)())\n }\n\n if (node == null || node === false) return ''\n\n if (typeof node === 'string') return escapeHtml(node)\n if (typeof node === 'number' || typeof node === 'boolean') return String(node)\n\n if (Array.isArray(node)) {\n let html = ''\n for (const child of node) html += await renderNode(child)\n return html\n }\n\n const vnode = node as VNode\n\n if (vnode.type === Fragment) {\n return renderChildren(vnode.children)\n }\n\n if (vnode.type === (ForSymbol as unknown as string)) {\n const { each, children, by } = vnode.props as unknown as ForProps<unknown>\n let forHtml = '<!--pyreon-for-->'\n for (const item of each()) {\n const key = by(item)\n if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.for.keyMarker')\n forHtml += `<!--k:${safeKeyForMarker(key)}-->`\n forHtml += await renderNode(children(item) as VNodeChild)\n }\n forHtml += '<!--/pyreon-for-->'\n return forHtml\n }\n\n if (typeof vnode.type === 'function') {\n return renderComponent(vnode as VNode & { type: ComponentFn })\n }\n\n return renderElement(vnode)\n}\n\nasync function renderChildren(children: VNodeChild[]): Promise<string> {\n let html = ''\n for (const child of children) html += await renderNode(child)\n return html\n}\n\nasync function renderComponent(vnode: VNode & { type: ComponentFn }): Promise<string> {\n if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.component')\n const { vnode: output } = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode))\n\n // Async component function (async function Component()) — await the promise\n if (output instanceof Promise) {\n const resolved = await output\n if (resolved === null) return ''\n return renderNode(resolved)\n }\n\n if (output === null) return ''\n return renderNode(output)\n}\n\nasync function renderElement(vnode: VNode): Promise<string> {\n const tag = vnode.type as string\n warnIfUnsafeTag(tag)\n let html = `<${tag}`\n\n const props = vnode.props as Record<string, unknown>\n for (const key in props) {\n const attr = renderProp(key, props[key])\n if (attr) html += ` ${attr}`\n }\n\n if (isVoidElement(tag)) {\n html += ' />'\n return html\n }\n\n html += '>'\n\n // `dangerouslySetInnerHTML` and `innerHTML` become inner content of the\n // element (NOT attributes — skipped in `renderPropSkipped`). Function\n // values — emitted by the compiler for signal-derived prop expressions —\n // are called once at render time (SSR is one-shot; reactivity happens\n // post-hydration on the client). Kept in sync with `streamElementNode`.\n const dangerous = props.dangerouslySetInnerHTML as\n | { __html: string }\n | (() => { __html: string })\n | undefined\n const innerHtml = props.innerHTML as string | (() => string) | undefined\n const dangerousHtml =\n typeof dangerous === 'function'\n ? (dangerous as () => { __html: string })()?.__html\n : dangerous?.__html\n const plainInnerHtml =\n typeof innerHtml === 'function' ? (innerHtml as () => string)() : innerHtml\n if (dangerousHtml) {\n html += dangerousHtml\n } else if (plainInnerHtml != null && plainInnerHtml !== '') {\n html += String(plainInnerHtml)\n } else {\n for (const child of vnode.children) {\n html += await renderNode(child)\n }\n }\n\n html += `</${tag}>`\n return html\n}\n\nconst SSR_URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'poster', 'cite', 'data'])\nconst SSR_UNSAFE_URL_RE = /^\\s*(?:javascript|data):/i\n\nfunction renderPropSkipped(key: string): boolean {\n // `innerHTML` and `dangerouslySetInnerHTML` are NOT attributes — they\n // get written as inner content in `streamElementNode`. Without this\n // skip, `innerHTML` would be emitted as a literal HTML attribute\n // (`<span innerHTML=\"<svg>…\">`) and the client hydration would\n // fix it up — wasted bytes, hydration mismatch, and (with the recent\n // client-side `innerHTML` bug) literal closure text visible before\n // hydration completed.\n if (key === 'key' || key === 'ref') return true\n if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') return true\n if (/^on[A-Z]/.test(key)) return true\n return false\n}\n\nfunction renderPropValue(key: string, value: unknown): string | null {\n if (value === null || value === undefined || value === false) return null\n if (value === true) return escapeHtml(toAttrName(key))\n\n if (key === 'class') {\n const cls = cx(value as ClassValue)\n return cls ? `class=\"${escapeHtml(cls)}\"` : null\n }\n\n if (key === 'style') {\n const style = normalizeStyle(value)\n return style ? `style=\"${escapeHtml(style)}\"` : null\n }\n\n return `${escapeHtml(toAttrName(key))}=\"${escapeHtml(String(value))}\"`\n}\n\nfunction renderProp(key: string, value: unknown): string | null {\n if (renderPropSkipped(key)) return null\n\n if (typeof value === 'function') {\n return renderProp(key, (value as () => unknown)())\n }\n\n if (SSR_URL_ATTRS.has(key) && typeof value === 'string' && SSR_UNSAFE_URL_RE.test(value)) {\n return null\n }\n\n return renderPropValue(key, value)\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nconst VOID_ELEMENTS = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\nfunction isVoidElement(tag: string): boolean {\n return VOID_ELEMENTS.has(tag.toLowerCase())\n}\n\n/** camelCase prop → kebab-case HTML attribute (e.g. className → class, htmlFor → for) */\nfunction toAttrName(key: string): string {\n if (key === 'className') return 'class'\n if (key === 'htmlFor') return 'for'\n return key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)\n}\n\nfunction isStyleObject(value: unknown): value is Record<string, unknown> {\n if (!value) return false\n return typeof value === 'object'\n}\n\nfunction normalizeStyle(value: unknown): string {\n if (typeof value === 'string') return value\n if (isStyleObject(value)) {\n const proto = Object.getPrototypeOf(value)\n if (proto === Object.prototype || proto === null) {\n return Object.entries(value)\n .map(([k, v]) => `${toKebab(k)}: ${normalizeStyleValue(k, v)}`)\n .join('; ')\n }\n }\n return ''\n}\n\nfunction toKebab(str: string): string {\n return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)\n}\n\nconst ESCAPE_MAP: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n}\n\n/**\n * Encode a For-list key so it's safe to inline inside an HTML comment\n * marker `<!--k:KEY-->`. If a user-supplied key contains `-->` the naive\n * form breaks out of the comment and may inject markup. Per-byte URL\n * encoding with an extra `-` substitution makes `-->` impossible in the\n * output: `%2D%2D>` no longer terminates the comment. Client-side\n * hydration does not read the marker body today, so any reversible-or-\n * irreversible encoding works; this one is predictable enough for a\n * future consumer to decode if needed.\n */\nfunction safeKeyForMarker(key: unknown): string {\n return encodeURIComponent(String(key)).replace(/-/g, '%2D')\n}\n\n/**\n * Inverse of `safeKeyForMarker` — decode a marker-safe key back to the\n * original string. Not used by runtime today (hydration does not read\n * per-item `<!--k:KEY-->` markers) but shipped alongside the encoder so\n * future hydration or devtools consumers decode symmetrically without\n * having to re-derive the encoding from source.\n */\nexport function decodeKeyFromMarker(encoded: string): string {\n return decodeURIComponent(encoded.replace(/%2D/gi, '-'))\n}\n\n// Detect tag names that would break out of the `<TAG>` or `</TAG>` form\n// and inject HTML. If user data ever feeds `h(userTag, ...)` the attack\n// `userTag = 'div><script>alert(1)</script><div'` yields executable\n// markup. Framework doesn't HTML-escape tag names (React/Vue/Solid\n// match) — responsibility is on the caller — but a dev-mode warning\n// catches the mistake before it reaches prod. Safe tag pattern covers\n// HTML element names and custom elements (letter start, then\n// alphanumerics + hyphens).\nconst SAFE_TAG_RE = /^[a-zA-Z][a-zA-Z0-9-]*$/\nfunction warnIfUnsafeTag(tag: string): void {\n if (!__DEV__) return\n if (SAFE_TAG_RE.test(tag)) return\n // oxlint-disable-next-line no-console\n console.warn(\n `[Pyreon SSR] Tag name \"${tag}\" contains characters that could break HTML structure. ` +\n `Tag names must match /^[a-zA-Z][a-zA-Z0-9-]*$/. ` +\n `If user-supplied data drives a tag name, validate it against an allowlist before passing to h().`,\n )\n}\n\n// Fast test — most strings in SSR have no special chars (tag names, class names, etc.)\nconst NEEDS_ESCAPE_RE = /[&<>\"']/\n\nfunction escapeHtml(str: string): string {\n if (!NEEDS_ESCAPE_RE.test(str)) return str\n if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.escape')\n return str.replace(/[&<>\"']/g, (c) => ESCAPE_MAP[c] ?? c)\n}\n\n/**\n * Merge vnode.children into props.children for component rendering.\n * Matches the behavior of mount.ts and hydrate.ts so components can\n * access children passed via h(Comp, props, child1, child2).\n */\nfunction mergeChildrenIntoProps(vnode: VNode): Record<string, unknown> {\n if (\n vnode.children.length > 0 &&\n (vnode.props as Record<string, unknown>).children === undefined\n ) {\n return {\n ...vnode.props,\n children: vnode.children.length === 1 ? vnode.children[0] : vnode.children,\n }\n }\n return vnode.props as Record<string, unknown>\n}\n"],"mappings":"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"}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;;;;;;;AAqGA;;iBA5BgB,uBAAA,CACd,wBAAA,GAA2B,EAAA,QAAU,GAAA;;iBAejB,cAAA,CAAe,IAAA,EAAM,KAAA,UAAe,OAAA;;;;;;iBAY1C,qBAAA,GAAA,CAAyB,EAAA,QAAU,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;;;;;;;AAiBxE;;;;;;;iBAAgB,cAAA,CAAe,IAAA,EAAM,KAAA,UAAe,cAAA;;AAudpD;;;;;;iBAAgB,mBAAA,CAAoB,OAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/runtime-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "SSR/SSG renderer for Pyreon — streaming HTML + static generation",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/runtime-server#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"prepublishOnly": "bun run build"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@pyreon/core": "^0.
|
|
46
|
-
"@pyreon/reactivity": "^0.
|
|
45
|
+
"@pyreon/core": "^0.14.0",
|
|
46
|
+
"@pyreon/reactivity": "^0.14.0"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,13 @@ import {
|
|
|
27
27
|
|
|
28
28
|
const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
|
|
29
29
|
|
|
30
|
+
// Dev-mode perf counter sink. Zero coupling to @pyreon/perf-harness — we just
|
|
31
|
+
// call the global if it's installed. Guarded on __DEV__ so NODE_ENV=production
|
|
32
|
+
// short-circuits at runtime; @pyreon/runtime-server is a server package, so the
|
|
33
|
+
// `typeof process` gate is correct here (not `import.meta.env.DEV`, which is a
|
|
34
|
+
// browser-bundler concern). See .claude/rules/test-environment-parity.md.
|
|
35
|
+
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
36
|
+
|
|
30
37
|
// ─── Streaming Suspense context ───────────────────────────────────────────────
|
|
31
38
|
// Tracks in-flight async Suspense boundary resolutions within a single stream.
|
|
32
39
|
|
|
@@ -82,6 +89,7 @@ function withStoreContext<T>(fn: () => T): T {
|
|
|
82
89
|
/** Render a VNode tree to an HTML string. Supports async component functions. */
|
|
83
90
|
export async function renderToString(root: VNode | null): Promise<string> {
|
|
84
91
|
if (root === null) return ''
|
|
92
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.render')
|
|
85
93
|
// Each call gets a fresh isolated context stack and (optionally) store registry
|
|
86
94
|
return withStoreContext(() => _contextAls.run([], () => renderNode(root)))
|
|
87
95
|
}
|
|
@@ -109,6 +117,7 @@ export function runWithRequestContext<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
109
117
|
* Each renderToStream call gets its own isolated ALS context stack.
|
|
110
118
|
*/
|
|
111
119
|
export function renderToStream(root: VNode | null): ReadableStream<string> {
|
|
120
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.stream')
|
|
112
121
|
return new ReadableStream<string>({
|
|
113
122
|
start(controller) {
|
|
114
123
|
const enqueue = (chunk: string) => controller.enqueue(chunk)
|
|
@@ -150,6 +159,7 @@ async function streamVNode(vnode: VNode, enqueue: (s: string) => void): Promise<
|
|
|
150
159
|
enqueue('<!--pyreon-for-->')
|
|
151
160
|
for (const item of each()) {
|
|
152
161
|
const key = by(item)
|
|
162
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.for.keyMarker')
|
|
153
163
|
enqueue(`<!--k:${safeKeyForMarker(key)}-->`)
|
|
154
164
|
await streamNode(children(item) as VNodeChild, enqueue)
|
|
155
165
|
}
|
|
@@ -170,6 +180,7 @@ async function streamComponentNode(vnode: VNode, enqueue: (s: string) => void):
|
|
|
170
180
|
await streamSuspenseBoundary(vnode, enqueue)
|
|
171
181
|
return
|
|
172
182
|
}
|
|
183
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.component')
|
|
173
184
|
try {
|
|
174
185
|
const { vnode: output } = runWithHooks(vnode.type as ComponentFn, mergeChildrenIntoProps(vnode))
|
|
175
186
|
const resolved = output instanceof Promise ? await output : output
|
|
@@ -261,6 +272,7 @@ const SUSPENSE_SWAP_FN =
|
|
|
261
272
|
* main stream enqueue so it always arrives after the fallback placeholder.
|
|
262
273
|
*/
|
|
263
274
|
async function streamSuspenseBoundary(vnode: VNode, enqueue: (s: string) => void): Promise<void> {
|
|
275
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.suspense.boundary')
|
|
264
276
|
const ctx = _streamCtxAls.getStore()
|
|
265
277
|
const { fallback, children } = vnode.props as { fallback: VNodeChild; children?: VNodeChild }
|
|
266
278
|
|
|
@@ -304,6 +316,7 @@ async function streamSuspenseBoundary(vnode: VNode, enqueue: (s: string) => void
|
|
|
304
316
|
|
|
305
317
|
if (result === 'timeout') {
|
|
306
318
|
if (__DEV__) {
|
|
319
|
+
_countSink.__pyreon_count__?.('runtime-server.suspense.fallback')
|
|
307
320
|
console.warn(
|
|
308
321
|
`[Pyreon SSR] Suspense boundary timed out after ${SUSPENSE_TIMEOUT_MS}ms — fallback will remain.`,
|
|
309
322
|
)
|
|
@@ -361,6 +374,7 @@ async function renderNode(node: VNodeChild | (() => VNodeChild)): Promise<string
|
|
|
361
374
|
let forHtml = '<!--pyreon-for-->'
|
|
362
375
|
for (const item of each()) {
|
|
363
376
|
const key = by(item)
|
|
377
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.for.keyMarker')
|
|
364
378
|
forHtml += `<!--k:${safeKeyForMarker(key)}-->`
|
|
365
379
|
forHtml += await renderNode(children(item) as VNodeChild)
|
|
366
380
|
}
|
|
@@ -382,6 +396,7 @@ async function renderChildren(children: VNodeChild[]): Promise<string> {
|
|
|
382
396
|
}
|
|
383
397
|
|
|
384
398
|
async function renderComponent(vnode: VNode & { type: ComponentFn }): Promise<string> {
|
|
399
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.component')
|
|
385
400
|
const { vnode: output } = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode))
|
|
386
401
|
|
|
387
402
|
// Async component function (async function Component()) — await the promise
|
|
@@ -601,6 +616,7 @@ const NEEDS_ESCAPE_RE = /[&<>"']/
|
|
|
601
616
|
|
|
602
617
|
function escapeHtml(str: string): string {
|
|
603
618
|
if (!NEEDS_ESCAPE_RE.test(str)) return str
|
|
619
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.escape')
|
|
604
620
|
return str.replace(/[&<>"']/g, (c) => ESCAPE_MAP[c] ?? c)
|
|
605
621
|
}
|
|
606
622
|
|