@pyreon/runtime-server 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/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}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"2a32a0fd-1"}]}],"isRoot":true},"nodeParts":{"2a32a0fd-1":{"renderedLength":16920,"gzipLength":5559,"brotliLength":0,"metaUid":"2a32a0fd-0"}},"nodeMetas":{"2a32a0fd-0":{"id":"/src/index.ts","moduleParts":{"index.js":"2a32a0fd-1"},"imported":[{"uid":"2a32a0fd-2"},{"uid":"2a32a0fd-3"}],"importedBy":[],"isEntry":true},"2a32a0fd-2":{"id":"node:async_hooks","moduleParts":{},"imported":[],"importedBy":[{"uid":"2a32a0fd-0"}]},"2a32a0fd-3":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"2a32a0fd-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
@@ -1,5 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { ForSymbol, Fragment, Suspense, cx, normalizeStyleValue, runWithHooks, setContextStackProvider } from "@pyreon/core";
2
+ import { ForSymbol, Fragment, Suspense, captureContextStack, cx, makeReactiveProps, normalizeStyleValue, popContext, runWithHooks, setContextStackProvider } from "@pyreon/core";
3
3
 
4
4
  //#region src/index.ts
5
5
  /**
@@ -94,7 +94,8 @@ async function streamVNode(vnode, enqueue) {
94
94
  if (vnode.type === ForSymbol) {
95
95
  const { each, children, by } = vnode.props;
96
96
  enqueue("<!--pyreon-for-->");
97
- for (const item of each()) {
97
+ const items = typeof each === "function" ? each() : each;
98
+ for (const item of items) {
98
99
  const key = by(item);
99
100
  if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.for.keyMarker");
100
101
  enqueue(`<!--k:${safeKeyForMarker(key)}-->`);
@@ -115,6 +116,7 @@ async function streamComponentNode(vnode, enqueue) {
115
116
  return;
116
117
  }
117
118
  if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.component");
119
+ const stackLenBefore = captureContextStack().length;
118
120
  try {
119
121
  const { vnode: output } = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode));
120
122
  const resolved = output instanceof Promise ? await output : output;
@@ -127,6 +129,8 @@ async function streamComponentNode(vnode, enqueue) {
127
129
  const ctx = _streamCtxAls.getStore();
128
130
  if (ctx && ctx.suspenseDepth > 0) throw err;
129
131
  enqueue("<!--pyreon-error-->");
132
+ } finally {
133
+ trimContextStack(stackLenBefore);
130
134
  }
131
135
  }
132
136
  async function streamElementNode(vnode, enqueue) {
@@ -181,11 +185,18 @@ async function streamSuspenseBoundary(vnode, enqueue) {
181
185
  if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.suspense.boundary");
182
186
  const ctx = _streamCtxAls.getStore();
183
187
  const { fallback, children } = vnode.props;
188
+ /* c8 ignore start */
184
189
  if (!ctx) {
190
+ const stackLenBefore = captureContextStack().length;
185
191
  const { vnode: output } = runWithHooks(Suspense, vnode.props);
186
- if (output !== null) await streamNode(output, enqueue);
192
+ try {
193
+ if (output !== null) await streamNode(output, enqueue);
194
+ } finally {
195
+ trimContextStack(stackLenBefore);
196
+ }
187
197
  return;
188
198
  }
199
+ /* c8 ignore stop */
189
200
  const id = ctx.nextId();
190
201
  const { mainEnqueue } = ctx;
191
202
  if (id === 0) mainEnqueue(SUSPENSE_SWAP_FN);
@@ -229,7 +240,8 @@ async function renderNode(node) {
229
240
  if (vnode.type === ForSymbol) {
230
241
  const { each, children, by } = vnode.props;
231
242
  let forHtml = "<!--pyreon-for-->";
232
- for (const item of each()) {
243
+ const items = typeof each === "function" ? each() : each;
244
+ for (const item of items) {
233
245
  const key = by(item);
234
246
  if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.for.keyMarker");
235
247
  forHtml += `<!--k:${safeKeyForMarker(key)}-->`;
@@ -248,14 +260,39 @@ async function renderChildren(children) {
248
260
  }
249
261
  async function renderComponent(vnode) {
250
262
  if (__DEV__) _countSink.__pyreon_count__?.("runtime-server.component");
263
+ const stackLenBefore = captureContextStack().length;
251
264
  const { vnode: output } = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode));
252
- if (output instanceof Promise) {
253
- const resolved = await output;
254
- if (resolved === null) return "";
255
- return renderNode(resolved);
265
+ let html;
266
+ try {
267
+ if (output instanceof Promise) {
268
+ const resolved = await output;
269
+ html = resolved === null ? "" : await renderNode(resolved);
270
+ } else if (output === null) html = "";
271
+ else html = await renderNode(output);
272
+ } finally {
273
+ trimContextStack(stackLenBefore);
274
+ }
275
+ return html;
276
+ }
277
+ /**
278
+ * Pop context frames pushed during a component's render. Trims the global
279
+ * context stack back to the snapshot length captured before the component
280
+ * ran. Each iteration calls `popContext` once — symmetric with `provide()`'s
281
+ * `pushContext()` so frames pop in LIFO order even when a single component
282
+ * called `provide()` multiple times.
283
+ *
284
+ * Why not run user unmount hooks here? See `renderComponent` for the full
285
+ * architectural rationale — TL;DR: SSR has no unmount phase, `useHead` uses
286
+ * `onUnmount` to clear head entries that the post-render extraction still
287
+ * needs, etc. The structural fix is to clean up the ONE SSR-visible side
288
+ * effect of `provide()` (its context frame) without firing other hooks.
289
+ */
290
+ function trimContextStack(targetLen) {
291
+ let current = captureContextStack().length;
292
+ while (current > targetLen) {
293
+ popContext();
294
+ current--;
256
295
  }
257
- if (output === null) return "";
258
- return renderNode(output);
259
296
  }
260
297
  async function renderElement(vnode) {
261
298
  const tag = vnode.type;
@@ -399,16 +436,23 @@ function escapeHtml(str) {
399
436
  return str.replace(/[&<>"']/g, (c) => ESCAPE_MAP[c] ?? c);
400
437
  }
401
438
  /**
402
- * Merge vnode.children into props.children for component rendering.
403
- * Matches the behavior of mount.ts and hydrate.ts so components can
404
- * access children passed via h(Comp, props, child1, child2).
439
+ * Merge vnode.children into props.children for component rendering, AND
440
+ * convert compiler-emitted `_rp(() => expr)` reactive-prop wrappers into
441
+ * getter properties via `makeReactiveProps`.
442
+ *
443
+ * mount.ts (CSR) does the same dance — without it, components reading
444
+ * `props.x` get the raw `_rp` function instead of the resolved value.
445
+ * Pre-fix, SSR (and hydrate.ts — same bug, fixed alongside) skipped the
446
+ * `makeReactiveProps` step, so any `<Comp prop={signal()}>` shape rendered
447
+ * the function source as the attribute value (e.g. `<a href="() => …">`).
448
+ * Visible end-to-end through the fundamentals NavItem layout — see
449
+ * `e2e/fundamentals/playground.spec.ts`.
405
450
  */
406
451
  function mergeChildrenIntoProps(vnode) {
407
- if (vnode.children.length > 0 && vnode.props.children === void 0) return {
452
+ return makeReactiveProps(vnode.children.length > 0 && vnode.props.children === void 0 ? {
408
453
  ...vnode.props,
409
454
  children: vnode.children.length === 1 ? vnode.children[0] : vnode.children
410
- };
411
- return vnode.props;
455
+ } : vnode.props);
412
456
  }
413
457
 
414
458
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/runtime-server",
3
- "version": "0.14.0",
3
+ "version": "0.16.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": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "lib",
17
+ "!lib/**/*.map",
17
18
  "src",
18
19
  "README.md",
19
20
  "LICENSE"
@@ -42,7 +43,7 @@
42
43
  "prepublishOnly": "bun run build"
43
44
  },
44
45
  "dependencies": {
45
- "@pyreon/core": "^0.14.0",
46
- "@pyreon/reactivity": "^0.14.0"
46
+ "@pyreon/core": "^0.16.0",
47
+ "@pyreon/reactivity": "^0.16.0"
47
48
  }
48
49
  }
package/src/index.ts CHANGED
@@ -16,10 +16,13 @@
16
16
  import { AsyncLocalStorage } from 'node:async_hooks'
17
17
  import type { ClassValue, ComponentFn, ForProps, VNode, VNodeChild } from '@pyreon/core'
18
18
  import {
19
+ captureContextStack,
19
20
  cx,
20
21
  ForSymbol,
21
22
  Fragment,
23
+ makeReactiveProps,
22
24
  normalizeStyleValue,
25
+ popContext,
23
26
  runWithHooks,
24
27
  Suspense,
25
28
  setContextStackProvider,
@@ -157,7 +160,12 @@ async function streamVNode(vnode: VNode, enqueue: (s: string) => void): Promise<
157
160
  if (vnode.type === (ForSymbol as unknown as string)) {
158
161
  const { each, children, by } = vnode.props as unknown as ForProps<unknown>
159
162
  enqueue('<!--pyreon-for-->')
160
- for (const item of each()) {
163
+ // Defensive: `each` is normally `_rp(() => arr)` (a function). PR #410's
164
+ // `makeReactiveProps` in `mergeChildrenIntoProps` invokes `_rp` getters
165
+ // when the For COMPONENT runs, which flips `props.each` to the resolved
166
+ // array on the re-emitted ForSymbol vnode. Accept both forms.
167
+ const items = typeof each === 'function' ? each() : (each as Iterable<unknown>)
168
+ for (const item of items) {
161
169
  const key = by(item)
162
170
  if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.for.keyMarker')
163
171
  enqueue(`<!--k:${safeKeyForMarker(key)}-->`)
@@ -181,8 +189,19 @@ async function streamComponentNode(vnode: VNode, enqueue: (s: string) => void):
181
189
  return
182
190
  }
183
191
  if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.component')
192
+ // Snapshot the context stack BEFORE the component renders so we can pop
193
+ // any frames pushed via `provide()` after children stream. We do NOT run
194
+ // user-registered unmount hooks during SSR — that would clear state still
195
+ // needed by post-render extraction (e.g. `useHead` uses `onUnmount` to
196
+ // remove its registered tags from the head store; running it during SSR
197
+ // wipes the entries before `renderWithHead` reads them). See `renderComponent`
198
+ // for the full architectural rationale.
199
+ const stackLenBefore = captureContextStack().length
184
200
  try {
185
- const { vnode: output } = runWithHooks(vnode.type as ComponentFn, mergeChildrenIntoProps(vnode))
201
+ const { vnode: output } = runWithHooks(
202
+ vnode.type as ComponentFn,
203
+ mergeChildrenIntoProps(vnode),
204
+ )
186
205
  const resolved = output instanceof Promise ? await output : output
187
206
  if (resolved !== null) await streamNode(resolved, enqueue)
188
207
  } catch (err) {
@@ -196,6 +215,8 @@ async function streamComponentNode(vnode: VNode, enqueue: (s: string) => void):
196
215
  const ctx = _streamCtxAls.getStore()
197
216
  if (ctx && ctx.suspenseDepth > 0) throw err
198
217
  enqueue('<!--pyreon-error-->')
218
+ } finally {
219
+ trimContextStack(stackLenBefore)
199
220
  }
200
221
  }
201
222
 
@@ -276,12 +297,28 @@ async function streamSuspenseBoundary(vnode: VNode, enqueue: (s: string) => void
276
297
  const ctx = _streamCtxAls.getStore()
277
298
  const { fallback, children } = vnode.props as { fallback: VNodeChild; children?: VNodeChild }
278
299
 
279
- // No streaming context (e.g. called from renderToString) — render children inline
300
+ // Defensive: the streaming pipeline only enters this function via
301
+ // `_streamCtxAls.run(ctx, ...)` (set up in `renderToStream`), so `ctx`
302
+ // is always defined when `streamSuspenseBoundary` runs. Kept as a safety
303
+ // net in case a future entry point bypasses the streaming context — the
304
+ // block performs the same context-stack hygiene as `renderComponent` /
305
+ // `streamComponentNode` so a Suspense `provide()` wouldn't leak into
306
+ // siblings if it ever fires. Excluded from coverage because no public-API
307
+ // path reaches it; including a unit test would either require exporting
308
+ // `streamSuspenseBoundary` (leaks an internal) or stubbing the ALS (false
309
+ // signal).
310
+ /* c8 ignore start */
280
311
  if (!ctx) {
312
+ const stackLenBefore = captureContextStack().length
281
313
  const { vnode: output } = runWithHooks(Suspense as ComponentFn, vnode.props)
282
- if (output !== null) await streamNode(output, enqueue)
314
+ try {
315
+ if (output !== null) await streamNode(output, enqueue)
316
+ } finally {
317
+ trimContextStack(stackLenBefore)
318
+ }
283
319
  return
284
320
  }
321
+ /* c8 ignore stop */
285
322
 
286
323
  const id = ctx.nextId()
287
324
  const { mainEnqueue } = ctx
@@ -372,7 +409,12 @@ async function renderNode(node: VNodeChild | (() => VNodeChild)): Promise<string
372
409
  if (vnode.type === (ForSymbol as unknown as string)) {
373
410
  const { each, children, by } = vnode.props as unknown as ForProps<unknown>
374
411
  let forHtml = '<!--pyreon-for-->'
375
- for (const item of each()) {
412
+ // Defensive: `each` is normally `_rp(() => arr)` (a function). PR #410's
413
+ // `makeReactiveProps` in `mergeChildrenIntoProps` invokes `_rp` getters
414
+ // when the For COMPONENT runs, which flips `props.each` to the resolved
415
+ // array on the re-emitted ForSymbol vnode. Accept both forms.
416
+ const items = typeof each === 'function' ? each() : (each as Iterable<unknown>)
417
+ for (const item of items) {
376
418
  const key = by(item)
377
419
  if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.for.keyMarker')
378
420
  forHtml += `<!--k:${safeKeyForMarker(key)}-->`
@@ -397,17 +439,61 @@ async function renderChildren(children: VNodeChild[]): Promise<string> {
397
439
 
398
440
  async function renderComponent(vnode: VNode & { type: ComponentFn }): Promise<string> {
399
441
  if (__DEV__) _countSink.__pyreon_count__?.('runtime-server.component')
442
+ // Snapshot the context stack length BEFORE the component renders. After
443
+ // children render, trim back to this length — that pops every frame the
444
+ // component pushed via `provide(ctx, value)` (which calls `pushContext` +
445
+ // registers `onUnmount(popContext)`). Without this, every provider during
446
+ // SSR leaks its frame onto the global stack and subsequent siblings see
447
+ // the wrong context value (Bug 4 — bokisch.com `<PyreonUI inversed>`
448
+ // inside `<Intro>` flipped every later section to dark).
449
+ //
450
+ // We trim the stack DIRECTLY instead of running the component's unmount
451
+ // hooks because users register `onUnmount` for things still load-bearing
452
+ // at post-render time during SSR — `useHead({ title })` uses `onUnmount`
453
+ // to remove its head entries, and running it here wipes the head store
454
+ // before `renderWithHead` extracts it. SSR has no real "unmount" phase
455
+ // (the response ships, the process moves on); user-registered cleanup
456
+ // is for the CSR lifecycle. `provide()`'s frame cleanup is the only
457
+ // SSR-visible side effect and we handle it structurally below.
458
+ const stackLenBefore = captureContextStack().length
400
459
  const { vnode: output } = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode))
401
460
 
402
461
  // Async component function (async function Component()) — await the promise
403
- if (output instanceof Promise) {
404
- const resolved = await output
405
- if (resolved === null) return ''
406
- return renderNode(resolved)
462
+ let html: string
463
+ try {
464
+ if (output instanceof Promise) {
465
+ const resolved = await output
466
+ html = resolved === null ? '' : await renderNode(resolved)
467
+ } else if (output === null) {
468
+ html = ''
469
+ } else {
470
+ html = await renderNode(output)
471
+ }
472
+ } finally {
473
+ trimContextStack(stackLenBefore)
407
474
  }
475
+ return html
476
+ }
408
477
 
409
- if (output === null) return ''
410
- return renderNode(output)
478
+ /**
479
+ * Pop context frames pushed during a component's render. Trims the global
480
+ * context stack back to the snapshot length captured before the component
481
+ * ran. Each iteration calls `popContext` once — symmetric with `provide()`'s
482
+ * `pushContext()` so frames pop in LIFO order even when a single component
483
+ * called `provide()` multiple times.
484
+ *
485
+ * Why not run user unmount hooks here? See `renderComponent` for the full
486
+ * architectural rationale — TL;DR: SSR has no unmount phase, `useHead` uses
487
+ * `onUnmount` to clear head entries that the post-render extraction still
488
+ * needs, etc. The structural fix is to clean up the ONE SSR-visible side
489
+ * effect of `provide()` (its context frame) without firing other hooks.
490
+ */
491
+ function trimContextStack(targetLen: number): void {
492
+ let current = captureContextStack().length
493
+ while (current > targetLen) {
494
+ popContext()
495
+ current--
496
+ }
411
497
  }
412
498
 
413
499
  async function renderElement(vnode: VNode): Promise<string> {
@@ -621,19 +707,26 @@ function escapeHtml(str: string): string {
621
707
  }
622
708
 
623
709
  /**
624
- * Merge vnode.children into props.children for component rendering.
625
- * Matches the behavior of mount.ts and hydrate.ts so components can
626
- * access children passed via h(Comp, props, child1, child2).
710
+ * Merge vnode.children into props.children for component rendering, AND
711
+ * convert compiler-emitted `_rp(() => expr)` reactive-prop wrappers into
712
+ * getter properties via `makeReactiveProps`.
713
+ *
714
+ * mount.ts (CSR) does the same dance — without it, components reading
715
+ * `props.x` get the raw `_rp` function instead of the resolved value.
716
+ * Pre-fix, SSR (and hydrate.ts — same bug, fixed alongside) skipped the
717
+ * `makeReactiveProps` step, so any `<Comp prop={signal()}>` shape rendered
718
+ * the function source as the attribute value (e.g. `<a href="() => …">`).
719
+ * Visible end-to-end through the fundamentals NavItem layout — see
720
+ * `e2e/fundamentals/playground.spec.ts`.
627
721
  */
628
722
  function mergeChildrenIntoProps(vnode: VNode): Record<string, unknown> {
629
- if (
723
+ const raw =
630
724
  vnode.children.length > 0 &&
631
725
  (vnode.props as Record<string, unknown>).children === undefined
632
- ) {
633
- return {
634
- ...vnode.props,
635
- children: vnode.children.length === 1 ? vnode.children[0] : vnode.children,
636
- }
637
- }
638
- return vnode.props as Record<string, unknown>
726
+ ? {
727
+ ...vnode.props,
728
+ children: vnode.children.length === 1 ? vnode.children[0] : vnode.children,
729
+ }
730
+ : (vnode.props as Record<string, unknown>)
731
+ return makeReactiveProps(raw)
639
732
  }
@@ -0,0 +1,95 @@
1
+ import type { ComponentFn, Props } from '@pyreon/core'
2
+ import { _rp, For, h } from '@pyreon/core'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { renderToString } from '..'
5
+
6
+ // Regression for the SSR `_rp`/`makeReactiveProps` gap.
7
+ //
8
+ // The compiler wraps `<Comp prop={signalRead}>` as
9
+ // `h(Comp, { prop: _rp(() => signalRead) })`. mount.ts (CSR) calls
10
+ // `makeReactiveProps` on the raw vnode props before invoking the component, so
11
+ // inside the body `props.prop` invokes the getter and returns the resolved
12
+ // value. Pre-fix, `runtime-server` skipped that step — components received raw
13
+ // `_rp` functions, so `${props.prop}` stringified the function source and any
14
+ // downstream attribute / template-literal interpolation embedded code into the
15
+ // HTML (e.g. `<a href="() => props.path">` from the fundamentals layout).
16
+ //
17
+ // hydrate.ts had the same gap; lock-in lives in the runtime-dom suite.
18
+
19
+ describe('SSR — _rp-wrapped component props are resolved (makeReactiveProps wired into runtime-server)', () => {
20
+ it('resolves `_rp(() => string)` to the string when interpolated in component-emitted HTML', async () => {
21
+ const Link = (props: { to: string }) => h('a', { href: `#${props.to}` }, 'go')
22
+ const html = await renderToString(
23
+ h(Link, { to: _rp(() => '/about') as unknown as string }),
24
+ )
25
+ expect(html).toBe('<a href="#/about">go</a>')
26
+ expect(html).not.toContain('=>')
27
+ })
28
+
29
+ it('resolves `_rp` chain when a parent reads its own getter and forwards through another `_rp`', async () => {
30
+ // Layout-shape: outer passes `_rp(() => '/store')` as `path`; NavItem
31
+ // reads `props.path` (which is now a getter) and re-wraps as
32
+ // `_rp(() => props.path)` for the child. SSR must traverse BOTH layers.
33
+ const Inner = (props: { to: string }) => h('a', { href: props.to }, 'x')
34
+ const Outer = (props: { path: string }) =>
35
+ h(Inner, { to: _rp(() => props.path) as unknown as string })
36
+ const html = await renderToString(
37
+ h(Outer, { path: _rp(() => '/store') as unknown as string }),
38
+ )
39
+ expect(html).toBe('<a href="/store">x</a>')
40
+ })
41
+
42
+ it('`<For each={items()}>` from a component renders correctly under SSR', async () => {
43
+ // Regression: PR #410's `makeReactiveProps` in `mergeChildrenIntoProps`
44
+ // converts `_rp(() => arr)` props to getters that RESOLVE the array. The
45
+ // `<For>` function is a component (its body returns a ForSymbol vnode),
46
+ // so it goes through that path. Result: the re-emitted ForSymbol vnode
47
+ // has `props.each` as the resolved array, not the function. SSR's For
48
+ // handler used to call `each()` unconditionally → TypeError. Defensive
49
+ // normalization (typeof === 'function' ? each() : each) fixes both shapes.
50
+ type Item = { id: number; name: string }
51
+ const Page = () => {
52
+ const items = () => [
53
+ { id: 1, name: 'a' },
54
+ { id: 2, name: 'b' },
55
+ ] as Item[]
56
+ const forProps = {
57
+ each: _rp(items) as unknown as () => Item[],
58
+ by: (r: Item) => r.id,
59
+ children: (r: Item) => h('span', null, r.name),
60
+ }
61
+ return h(For as unknown as ComponentFn, forProps as unknown as Props)
62
+ }
63
+ const html = await renderToString(h(Page, null))
64
+ expect(html).toContain('a')
65
+ expect(html).toContain('b')
66
+ expect(html).not.toContain('SSR Error')
67
+ })
68
+
69
+ it('`<For each={arr}>` (already-array form) still renders correctly under SSR', async () => {
70
+ // When `each` is a plain array (not a function), the defensive shape must
71
+ // still iterate. `<For each={[1,2,3]}>` is the typical hand-coded form.
72
+ type Item = { id: number; name: string }
73
+ const forProps = {
74
+ each: [{ id: 1, name: 'plain' }] as unknown as () => Item[],
75
+ by: (r: Item) => r.id,
76
+ children: (r: Item) => h('span', null, r.name),
77
+ }
78
+ const html = await renderToString(
79
+ h(For as unknown as ComponentFn, forProps as unknown as Props),
80
+ )
81
+ expect(html).toContain('plain')
82
+ })
83
+
84
+ it('non-`_rp` function props (user-written accessors) still pass through to elements', async () => {
85
+ // `class={() => 'foo'}` is NOT `_rp`-wrapped (it's a user-written
86
+ // accessor, not a compiler emission). makeReactiveProps must leave it as
87
+ // a plain function on `props.class` so the runtime can call it. The SSR
88
+ // attribute renderer already invokes function-typed attribute values, so
89
+ // the result still hits the rendered HTML — but we lock the contract in.
90
+ const Wrapper = (props: { class: () => string }) =>
91
+ h('div', { class: props.class }, 'x')
92
+ const html = await renderToString(h(Wrapper, { class: () => 'foo' }))
93
+ expect(html).toBe('<div class="foo">x</div>')
94
+ })
95
+ })
@@ -1,5 +1,15 @@
1
1
  import type { ComponentFn, VNode } from '@pyreon/core'
2
- import { createContext, For, Fragment, h, pushContext, Suspense, useContext } from '@pyreon/core'
2
+ import {
3
+ createContext,
4
+ For,
5
+ Fragment,
6
+ h,
7
+ onUnmount,
8
+ provide,
9
+ pushContext,
10
+ Suspense,
11
+ useContext,
12
+ } from '@pyreon/core'
3
13
  import { signal } from '@pyreon/reactivity'
4
14
  import {
5
15
  configureStoreIsolation,
@@ -1106,6 +1116,140 @@ describe('renderToString — For key markers', () => {
1106
1116
 
1107
1117
  // ─── For SSR — key markers in stream ─────────────────────────────────────────
1108
1118
 
1119
+ // ─── Bug 4: SSR provide() context cleanup across siblings ───────────────────
1120
+ //
1121
+ // Regression: pre-fix, `renderComponent` invoked `runWithHooks(...)` to render
1122
+ // each component but DESTRUCTURED only the vnode — never invoked the
1123
+ // component's unmount hooks. `provide(context, value)` registers
1124
+ // `onUnmount(popContext)` to clean up its pushed context frame on unmount.
1125
+ // Without unmount-hook invocation during SSR, every `provide()` call left
1126
+ // its context frame on the global stack permanently. Subsequent siblings
1127
+ // saw the leaked context value instead of the outer provider's value.
1128
+ //
1129
+ // Real-world manifestation (bokisch.com): a `<PyreonUI inversed>` inside an
1130
+ // `<Intro>` section flipped mode to dark and pushed it as context. After
1131
+ // Intro rendered, every subsequent section (`<Quote>`, `<Companies>`, etc.)
1132
+ // saw the inverted dark mode → all sections rendered in dark even though
1133
+ // the page was in light mode → wrong colors everywhere.
1134
+
1135
+ describe('SSR — provide() context cleanup across siblings (Bug 4)', () => {
1136
+ test('sibling AFTER a provide() call sees the OUTER context value, not the leak', async () => {
1137
+ const Ctx = createContext('outer')
1138
+
1139
+ // Component that pushes a NEW context value via `provide()`.
1140
+ const Inner: ComponentFn = () => {
1141
+ provide(Ctx, 'inner')
1142
+ return h('span', { 'data-testid': 'inner' }, () => useContext(Ctx))
1143
+ }
1144
+
1145
+ // Sibling rendered AFTER Inner — should see 'outer', not 'inner'.
1146
+ const Sibling: ComponentFn = () => {
1147
+ return h('span', { 'data-testid': 'sibling' }, () => useContext(Ctx))
1148
+ }
1149
+
1150
+ // Outer wrapper: <div><Inner /><Sibling /></div>. Pre-fix, Sibling
1151
+ // sees 'inner' because Inner's provide() was never popped.
1152
+ const App: ComponentFn = () => {
1153
+ return h(Fragment, null, h(Inner, null), h(Sibling, null))
1154
+ }
1155
+
1156
+ const html = await renderToString(h(App, null))
1157
+
1158
+ expect(html).toContain('data-testid="inner">inner<')
1159
+ expect(html).toContain('data-testid="sibling">outer<') // not "inner"
1160
+ })
1161
+
1162
+ test('multiple sequential provide() calls each clean up their own frame', async () => {
1163
+ const Ctx = createContext('default')
1164
+
1165
+ const First: ComponentFn = () => {
1166
+ provide(Ctx, 'first')
1167
+ return h('span', { 'data-testid': 'first' }, () => useContext(Ctx))
1168
+ }
1169
+ const Second: ComponentFn = () => {
1170
+ provide(Ctx, 'second')
1171
+ return h('span', { 'data-testid': 'second' }, () => useContext(Ctx))
1172
+ }
1173
+ const Third: ComponentFn = () => {
1174
+ return h('span', { 'data-testid': 'third' }, () => useContext(Ctx))
1175
+ }
1176
+
1177
+ const html = await renderToString(
1178
+ h(Fragment, null, h(First, null), h(Second, null), h(Third, null)),
1179
+ )
1180
+
1181
+ expect(html).toContain('data-testid="first">first<')
1182
+ expect(html).toContain('data-testid="second">second<')
1183
+ // Third sees 'default' — no leakage from First or Second.
1184
+ expect(html).toContain('data-testid="third">default<')
1185
+ })
1186
+
1187
+ test('nested provide() — child sees parent provide, sibling outside sees outer', async () => {
1188
+ const Ctx = createContext('outer')
1189
+
1190
+ const InnerChild: ComponentFn = () =>
1191
+ h('span', { 'data-testid': 'inner-child' }, () => useContext(Ctx))
1192
+
1193
+ const Provider: ComponentFn = () => {
1194
+ provide(Ctx, 'provider-value')
1195
+ return h('div', null, h(InnerChild, null))
1196
+ }
1197
+
1198
+ const Sibling: ComponentFn = () =>
1199
+ h('span', { 'data-testid': 'sibling' }, () => useContext(Ctx))
1200
+
1201
+ const html = await renderToString(
1202
+ h(Fragment, null, h(Provider, null), h(Sibling, null)),
1203
+ )
1204
+
1205
+ // Inner child sees the provider's value (correct).
1206
+ expect(html).toContain('data-testid="inner-child">provider-value<')
1207
+ // Sibling outside the provider sees outer (must NOT see leaked
1208
+ // provider-value).
1209
+ expect(html).toContain('data-testid="sibling">outer<')
1210
+ })
1211
+
1212
+ // Bug 4 follow-up: the FIRST attempt at the fix (running runUnmountHooks
1213
+ // during SSR) overshot — it fired ALL user-registered onUnmount hooks,
1214
+ // not just `provide()`'s `popContext`. That broke @pyreon/head, where
1215
+ // `useHead({ title })` registers `onUnmount(() => removeFromHeadStore())`
1216
+ // to clean up on CSR unmount; running it during SSR cleared the head
1217
+ // store BEFORE `renderWithHead` extracted it → 38 head tests failed,
1218
+ // every SSR'd page lost its <title>/<meta>/<link> tags.
1219
+ //
1220
+ // Architectural rule: SSR has no real unmount phase (the response
1221
+ // ships, the process moves on). User-registered `onUnmount` hooks are
1222
+ // for the CSR lifecycle. The ONE SSR-visible side effect of `provide()`
1223
+ // is its context frame, and we clean that up structurally (snapshot +
1224
+ // trim the stack) without firing user hooks.
1225
+ test('user-registered onUnmount hooks DO NOT fire during SSR (head store contract)', async () => {
1226
+ // Simulate `useHead({ title })`-style registration: register an entry
1227
+ // in a per-render store, register `onUnmount` to clean it up. Real
1228
+ // `@pyreon/head` does this via a context'd HeadStore; we use a
1229
+ // module-local for the test.
1230
+ const store: { title?: string } = {}
1231
+
1232
+ const TitleRegister: ComponentFn = () => {
1233
+ store.title = 'Hello SSR'
1234
+ onUnmount(() => {
1235
+ delete store.title
1236
+ })
1237
+ return null
1238
+ }
1239
+
1240
+ const App: ComponentFn = () =>
1241
+ h('html', null, h('head', null, h(TitleRegister, null)), h('body', null))
1242
+
1243
+ await renderToString(h(App, null))
1244
+
1245
+ // After SSR completes, the head-style store entry MUST still be
1246
+ // present — `renderWithHead` and similar post-render extractors
1247
+ // need it. If `runUnmountHooks` were called here, `store.title`
1248
+ // would be undefined.
1249
+ expect(store.title).toBe('Hello SSR')
1250
+ })
1251
+ })
1252
+
1109
1253
  describe('renderToStream — For key markers', () => {
1110
1254
  test('emits key markers for each item in stream', async () => {
1111
1255
  const items = [
package/lib/index.js.map DELETED
@@ -1 +0,0 @@
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=\"&lt;svg&gt;…\">`) 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 '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\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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"}
@@ -1 +0,0 @@
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"}