@pyreon/runtime-server 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
- import type { ComponentFn, VNode } from "@pyreon/core"
2
- import { createContext, For, Fragment, h, pushContext, Suspense, useContext } from "@pyreon/core"
3
- import { signal } from "@pyreon/reactivity"
1
+ import type { ComponentFn, VNode } from '@pyreon/core'
2
+ import { createContext, For, Fragment, h, pushContext, Suspense, useContext } from '@pyreon/core'
3
+ import { signal } from '@pyreon/reactivity'
4
4
  import {
5
5
  configureStoreIsolation,
6
6
  renderToStream,
7
7
  renderToString,
8
8
  runWithRequestContext,
9
- } from "../index"
9
+ } from '../index'
10
10
 
11
11
  async function collectStream(stream: ReadableStream<string>): Promise<string> {
12
12
  const reader = stream.getReader()
@@ -16,113 +16,113 @@ async function collectStream(stream: ReadableStream<string>): Promise<string> {
16
16
  if (done) break
17
17
  chunks.push(value)
18
18
  }
19
- return chunks.join("")
19
+ return chunks.join('')
20
20
  }
21
21
 
22
22
  // ─── renderToString ───────────────────────────────────────────────────────────
23
23
 
24
- describe("renderToString — elements", () => {
25
- test("renders a simple element", async () => {
26
- expect(await renderToString(h("div", null))).toBe("<div></div>")
24
+ describe('renderToString — elements', () => {
25
+ test('renders a simple element', async () => {
26
+ expect(await renderToString(h('div', null))).toBe('<div></div>')
27
27
  })
28
28
 
29
- test("renders void element self-closing", async () => {
30
- expect(await renderToString(h("br", null))).toBe("<br />")
29
+ test('renders void element self-closing', async () => {
30
+ expect(await renderToString(h('br', null))).toBe('<br />')
31
31
  })
32
32
 
33
- test("renders static text child", async () => {
34
- expect(await renderToString(h("p", null, "hello"))).toBe("<p>hello</p>")
33
+ test('renders static text child', async () => {
34
+ expect(await renderToString(h('p', null, 'hello'))).toBe('<p>hello</p>')
35
35
  })
36
36
 
37
- test("escapes text content", async () => {
38
- expect(await renderToString(h("p", null, "<script>"))).toBe("<p>&lt;script&gt;</p>")
37
+ test('escapes text content', async () => {
38
+ expect(await renderToString(h('p', null, '<script>'))).toBe('<p>&lt;script&gt;</p>')
39
39
  })
40
40
 
41
- test("renders nested elements", async () => {
42
- const vnode = h("ul", null, h("li", null, "a"), h("li", null, "b"))
43
- expect(await renderToString(vnode)).toBe("<ul><li>a</li><li>b</li></ul>")
41
+ test('renders nested elements', async () => {
42
+ const vnode = h('ul', null, h('li', null, 'a'), h('li', null, 'b'))
43
+ expect(await renderToString(vnode)).toBe('<ul><li>a</li><li>b</li></ul>')
44
44
  })
45
45
 
46
- test("renders null as empty string", async () => {
47
- expect(await renderToString(null)).toBe("")
46
+ test('renders null as empty string', async () => {
47
+ expect(await renderToString(null)).toBe('')
48
48
  })
49
49
  })
50
50
 
51
- describe("renderToString — props", () => {
52
- test("renders static string prop", async () => {
53
- expect(await renderToString(h("div", { class: "box" }))).toBe(`<div class="box"></div>`)
51
+ describe('renderToString — props', () => {
52
+ test('renders static string prop', async () => {
53
+ expect(await renderToString(h('div', { class: 'box' }))).toBe(`<div class="box"></div>`)
54
54
  })
55
55
 
56
- test("renders boolean prop (true → attribute name only)", async () => {
57
- const html = await renderToString(h("input", { disabled: true }))
58
- expect(html).toContain("disabled")
56
+ test('renders boolean prop (true → attribute name only)', async () => {
57
+ const html = await renderToString(h('input', { disabled: true }))
58
+ expect(html).toContain('disabled')
59
59
  })
60
60
 
61
- test("omits false prop", async () => {
62
- const html = await renderToString(h("input", { disabled: false }))
63
- expect(html).not.toContain("disabled")
61
+ test('omits false prop', async () => {
62
+ const html = await renderToString(h('input', { disabled: false }))
63
+ expect(html).not.toContain('disabled')
64
64
  })
65
65
 
66
- test("omits event handlers", async () => {
67
- const html = await renderToString(h("button", { onClick: () => {} }))
68
- expect(html).not.toContain("onClick")
69
- expect(html).not.toContain("on-click")
66
+ test('omits event handlers', async () => {
67
+ const html = await renderToString(h('button', { onClick: () => {} }))
68
+ expect(html).not.toContain('onClick')
69
+ expect(html).not.toContain('on-click')
70
70
  })
71
71
 
72
- test("omits key and ref", async () => {
73
- const html = await renderToString(h("div", { key: "k", ref: null }))
74
- expect(html).not.toContain("key")
75
- expect(html).not.toContain("ref")
72
+ test('omits key and ref', async () => {
73
+ const html = await renderToString(h('div', { key: 'k', ref: null }))
74
+ expect(html).not.toContain('key')
75
+ expect(html).not.toContain('ref')
76
76
  })
77
77
 
78
- test("renders style object", async () => {
79
- const html = await renderToString(h("div", { style: { color: "red", fontSize: "16px" } }))
80
- expect(html).toContain("color: red")
81
- expect(html).toContain("font-size: 16px")
78
+ test('renders style object', async () => {
79
+ const html = await renderToString(h('div', { style: { color: 'red', fontSize: '16px' } }))
80
+ expect(html).toContain('color: red')
81
+ expect(html).toContain('font-size: 16px')
82
82
  })
83
83
  })
84
84
 
85
- describe("renderToString — reactive props (signal snapshots)", () => {
86
- test("snapshots a reactive prop getter", async () => {
85
+ describe('renderToString — reactive props (signal snapshots)', () => {
86
+ test('snapshots a reactive prop getter', async () => {
87
87
  const count = signal(42)
88
- const html = await renderToString(h("span", { "data-count": () => count() }))
88
+ const html = await renderToString(h('span', { 'data-count': () => count() }))
89
89
  expect(html).toBe(`<span data-count="42"></span>`)
90
90
  })
91
91
 
92
- test("snapshots a reactive child getter", async () => {
93
- const name = signal("world")
94
- const html = await renderToString(h("p", null, () => name()))
95
- expect(html).toBe("<p>world</p>")
92
+ test('snapshots a reactive child getter', async () => {
93
+ const name = signal('world')
94
+ const html = await renderToString(h('p', null, () => name()))
95
+ expect(html).toBe('<p>world</p>')
96
96
  })
97
97
  })
98
98
 
99
- describe("renderToString — Fragment", () => {
100
- test("renders Fragment children without wrapper", async () => {
101
- const vnode = h(Fragment, null, h("span", null, "a"), h("span", null, "b"))
102
- expect(await renderToString(vnode)).toBe("<span>a</span><span>b</span>")
99
+ describe('renderToString — Fragment', () => {
100
+ test('renders Fragment children without wrapper', async () => {
101
+ const vnode = h(Fragment, null, h('span', null, 'a'), h('span', null, 'b'))
102
+ expect(await renderToString(vnode)).toBe('<span>a</span><span>b</span>')
103
103
  })
104
104
  })
105
105
 
106
- describe("renderToString — components", () => {
107
- test("renders a component", async () => {
108
- const Greeting = (props: { name: string }) => h("p", null, `Hello, ${props.name}!`)
109
- const html = await renderToString(h(Greeting, { name: "Pyreon" }))
110
- expect(html).toBe("<p>Hello, Pyreon!</p>")
106
+ describe('renderToString — components', () => {
107
+ test('renders a component', async () => {
108
+ const Greeting = (props: { name: string }) => h('p', null, `Hello, ${props.name}!`)
109
+ const html = await renderToString(h(Greeting, { name: 'Pyreon' }))
110
+ expect(html).toBe('<p>Hello, Pyreon!</p>')
111
111
  })
112
112
 
113
- test("renders a component returning null", async () => {
113
+ test('renders a component returning null', async () => {
114
114
  const Empty = () => null
115
115
  const html = await renderToString(h(Empty, null))
116
- expect(html).toBe("")
116
+ expect(html).toBe('')
117
117
  })
118
118
  })
119
119
 
120
120
  // ─── renderToStream ───────────────────────────────────────────────────────────
121
121
 
122
- describe("renderToStream", () => {
122
+ describe('renderToStream', () => {
123
123
  async function collect(stream: ReadableStream<string>): Promise<string> {
124
124
  const reader = stream.getReader()
125
- let result = ""
125
+ let result = ''
126
126
  while (true) {
127
127
  const { done, value } = await reader.read()
128
128
  if (done) break
@@ -131,24 +131,24 @@ describe("renderToStream", () => {
131
131
  return result
132
132
  }
133
133
 
134
- test("streams a simple element", async () => {
135
- const html = await collect(renderToStream(h("div", null, "hi")))
136
- expect(html).toBe("<div>hi</div>")
134
+ test('streams a simple element', async () => {
135
+ const html = await collect(renderToStream(h('div', null, 'hi')))
136
+ expect(html).toBe('<div>hi</div>')
137
137
  })
138
138
 
139
- test("streams null as empty", async () => {
140
- expect(await collect(renderToStream(null))).toBe("")
139
+ test('streams null as empty', async () => {
140
+ expect(await collect(renderToStream(null))).toBe('')
141
141
  })
142
142
 
143
- test("streams chunks progressively — opening tag before children", async () => {
143
+ test('streams chunks progressively — opening tag before children', async () => {
144
144
  // An async child that resolves after a delay.
145
145
  // The parent opening tag must be enqueued BEFORE the child resolves.
146
146
  const chunks: string[] = []
147
147
  async function SlowChild() {
148
148
  await new Promise<void>((r) => setTimeout(r, 5))
149
- return h("span", null, "done")
149
+ return h('span', null, 'done')
150
150
  }
151
- const stream = renderToStream(h("div", null, h(SlowChild as unknown as ComponentFn, null)))
151
+ const stream = renderToStream(h('div', null, h(SlowChild as unknown as ComponentFn, null)))
152
152
  const reader = stream.getReader()
153
153
  while (true) {
154
154
  const { done, value } = await reader.read()
@@ -156,78 +156,78 @@ describe("renderToStream", () => {
156
156
  chunks.push(value)
157
157
  }
158
158
  // First chunk must be the opening tag, not the full string
159
- expect(chunks[0]).toBe("<div>")
160
- expect(chunks.join("")).toBe("<div><span>done</span></div>")
159
+ expect(chunks[0]).toBe('<div>')
160
+ expect(chunks.join('')).toBe('<div><span>done</span></div>')
161
161
  })
162
162
 
163
- test("streams async component output", async () => {
163
+ test('streams async component output', async () => {
164
164
  async function Async() {
165
165
  await new Promise<void>((r) => setTimeout(r, 1))
166
- return h("p", null, "async")
166
+ return h('p', null, 'async')
167
167
  }
168
168
  const html = await collect(renderToStream(h(Async as unknown as ComponentFn, null)))
169
- expect(html).toBe("<p>async</p>")
169
+ expect(html).toBe('<p>async</p>')
170
170
  })
171
171
  })
172
172
 
173
173
  // ─── Concurrent SSR isolation ────────────────────────────────────────────────
174
174
 
175
- describe("concurrent SSR — context isolation", () => {
176
- test("two concurrent renderToString calls do not share context", async () => {
177
- const Ctx = createContext("default")
175
+ describe('concurrent SSR — context isolation', () => {
176
+ test('two concurrent renderToString calls do not share context', async () => {
177
+ const Ctx = createContext('default')
178
178
 
179
179
  // Each HeadInjector runs inside its own ALS scope.
180
180
  // If context were shared, the second render would see the first's value.
181
181
  function makeInjector(value: string): ComponentFn {
182
182
  return function Injector() {
183
183
  pushContext(new Map([[Ctx.id, value]]))
184
- return h("span", null, () => useContext(Ctx))
184
+ return h('span', null, () => useContext(Ctx))
185
185
  }
186
186
  }
187
187
 
188
188
  // Stagger start slightly so they interleave
189
189
  const [html1, html2] = await Promise.all([
190
- renderToString(h(makeInjector("request-A"), null)),
191
- renderToString(h(makeInjector("request-B"), null)),
190
+ renderToString(h(makeInjector('request-A'), null)),
191
+ renderToString(h(makeInjector('request-B'), null)),
192
192
  ])
193
193
 
194
- expect(html1).toBe("<span>request-A</span>")
195
- expect(html2).toBe("<span>request-B</span>")
194
+ expect(html1).toBe('<span>request-A</span>')
195
+ expect(html2).toBe('<span>request-B</span>')
196
196
  })
197
197
 
198
- test("concurrent renders with async components stay isolated", async () => {
199
- const Ctx = createContext("none")
198
+ test('concurrent renders with async components stay isolated', async () => {
199
+ const Ctx = createContext('none')
200
200
 
201
201
  function makeApp(value: string): ComponentFn {
202
202
  return async function App() {
203
203
  // Inject context, then yield (simulates async data loading)
204
204
  pushContext(new Map([[Ctx.id, value]]))
205
205
  await new Promise<void>((r) => setTimeout(r, 5))
206
- return h("div", null, () => useContext(Ctx))
206
+ return h('div', null, () => useContext(Ctx))
207
207
  } as unknown as ComponentFn
208
208
  }
209
209
 
210
210
  const [html1, html2] = await Promise.all([
211
- renderToString(h(makeApp("R1"), null)),
212
- renderToString(h(makeApp("R2"), null)),
211
+ renderToString(h(makeApp('R1'), null)),
212
+ renderToString(h(makeApp('R2'), null)),
213
213
  ])
214
214
 
215
- expect(html1).toBe("<div>R1</div>")
216
- expect(html2).toBe("<div>R2</div>")
215
+ expect(html1).toBe('<div>R1</div>')
216
+ expect(html2).toBe('<div>R2</div>')
217
217
  })
218
218
  })
219
219
 
220
220
  // ─── Streaming Suspense SSR ───────────────────────────────────────────────────
221
221
 
222
- describe("renderToStream — Suspense boundaries", () => {
223
- test("streams fallback immediately, then resolved content with swap", async () => {
222
+ describe('renderToStream — Suspense boundaries', () => {
223
+ test('streams fallback immediately, then resolved content with swap', async () => {
224
224
  async function Slow(): Promise<ReturnType<typeof h>> {
225
225
  await new Promise<void>((r) => setTimeout(r, 10))
226
- return h("p", { id: "resolved" }, "loaded")
226
+ return h('p', { id: 'resolved' }, 'loaded')
227
227
  }
228
228
 
229
229
  const vnode = h(Suspense, {
230
- fallback: h("p", { id: "fallback" }, "loading..."),
230
+ fallback: h('p', { id: 'fallback' }, 'loading...'),
231
231
  children: h(Slow as unknown as unknown as ComponentFn, null),
232
232
  })
233
233
 
@@ -235,29 +235,29 @@ describe("renderToStream — Suspense boundaries", () => {
235
235
 
236
236
  // Fallback placeholder was emitted
237
237
  expect(html).toContain('id="pyreon-s-0"')
238
- expect(html).toContain("loading...")
238
+ expect(html).toContain('loading...')
239
239
  // Resolved content emitted in template + swap
240
240
  expect(html).toContain('id="pyreon-t-0"')
241
- expect(html).toContain("loaded")
242
- expect(html).toContain("__NS")
241
+ expect(html).toContain('loaded')
242
+ expect(html).toContain('__NS')
243
243
  })
244
244
 
245
- test("renderToStream emits chunks progressively (placeholder before resolution)", async () => {
245
+ test('renderToStream emits chunks progressively (placeholder before resolution)', async () => {
246
246
  const chunkOrder: string[] = []
247
247
 
248
248
  async function SlowContent(): Promise<ReturnType<typeof h>> {
249
249
  await new Promise<void>((r) => setTimeout(r, 10))
250
- return h("span", null, "done")
250
+ return h('span', null, 'done')
251
251
  }
252
252
 
253
253
  const vnode = h(
254
- "div",
254
+ 'div',
255
255
  null,
256
256
  h(Suspense, {
257
- fallback: h("span", { id: "fb" }, "wait"),
257
+ fallback: h('span', { id: 'fb' }, 'wait'),
258
258
  children: h(SlowContent as unknown as unknown as ComponentFn, null),
259
259
  }),
260
- h("p", null, "after"),
260
+ h('p', null, 'after'),
261
261
  )
262
262
 
263
263
  const reader = renderToStream(vnode).getReader()
@@ -267,45 +267,45 @@ describe("renderToStream — Suspense boundaries", () => {
267
267
  chunkOrder.push(value)
268
268
  }
269
269
 
270
- const full = chunkOrder.join("")
270
+ const full = chunkOrder.join('')
271
271
  // Placeholder chunk must appear before the resolved template chunk
272
- const placeholderIdx = full.indexOf("pyreon-s-0")
273
- const templateIdx = full.indexOf("pyreon-t-0")
272
+ const placeholderIdx = full.indexOf('pyreon-s-0')
273
+ const templateIdx = full.indexOf('pyreon-t-0')
274
274
  expect(placeholderIdx).toBeGreaterThanOrEqual(0)
275
275
  expect(templateIdx).toBeGreaterThan(placeholderIdx)
276
276
  // "after" sibling is in the HTML (not blocked by Suspense)
277
- expect(full).toContain("<p>after</p>")
277
+ expect(full).toContain('<p>after</p>')
278
278
  })
279
279
 
280
- test("renderToString renders Suspense children synchronously (no streaming)", async () => {
280
+ test('renderToString renders Suspense children synchronously (no streaming)', async () => {
281
281
  async function Data(): Promise<ReturnType<typeof h>> {
282
- return h("span", null, "ssr-data")
282
+ return h('span', null, 'ssr-data')
283
283
  }
284
284
 
285
285
  const vnode = h(Suspense, {
286
- fallback: h("span", null, "fb"),
286
+ fallback: h('span', null, 'fb'),
287
287
  children: h(Data as unknown as unknown as ComponentFn, null),
288
288
  })
289
289
 
290
290
  const html = await renderToString(vnode)
291
291
  // renderToString should render fallback via Suspense's sync path
292
292
  // (Suspense on server with non-lazy child just shows the fallback or children)
293
- expect(typeof html).toBe("string")
293
+ expect(typeof html).toBe('string')
294
294
  expect(html.length).toBeGreaterThan(0)
295
295
  })
296
296
  })
297
297
 
298
298
  // ─── Concurrent SSR — context isolation ───────────────────────────────────────
299
299
 
300
- describe("concurrent SSR — context isolation", () => {
300
+ describe('concurrent SSR — context isolation', () => {
301
301
  test("50 concurrent requests with async components don't bleed context", async () => {
302
- const ReqIdCtx = createContext("none")
302
+ const ReqIdCtx = createContext('none')
303
303
 
304
304
  // Async component that reads context after a variable delay — simulates DB/fetch latency
305
305
  async function AsyncReader(props: { delay: number }): Promise<VNode> {
306
306
  await new Promise<void>((r) => setTimeout(r, props.delay))
307
307
  const id = useContext(ReqIdCtx)
308
- return h("span", null, id)
308
+ return h('span', null, id)
309
309
  }
310
310
 
311
311
  // Wrapper component that injects a per-request context value then renders AsyncReader
@@ -329,12 +329,12 @@ describe("concurrent SSR — context isolation", () => {
329
329
  })
330
330
  })
331
331
 
332
- test("context is isolated even when all requests resolve in reverse order", async () => {
333
- const ReqIdCtx = createContext("none")
332
+ test('context is isolated even when all requests resolve in reverse order', async () => {
333
+ const ReqIdCtx = createContext('none')
334
334
 
335
335
  async function SlowReader(props: { delay: number }): Promise<VNode> {
336
336
  await new Promise<void>((r) => setTimeout(r, props.delay))
337
- return h("span", null, useContext(ReqIdCtx))
337
+ return h('span', null, useContext(ReqIdCtx))
338
338
  }
339
339
 
340
340
  const N = 10
@@ -362,8 +362,8 @@ describe("concurrent SSR — context isolation", () => {
362
362
 
363
363
  // ─── configureStoreIsolation ────────────────────────────────────────────────
364
364
 
365
- describe("configureStoreIsolation", () => {
366
- test("activates store isolation — withStoreContext wraps in ALS", async () => {
365
+ describe('configureStoreIsolation', () => {
366
+ test('activates store isolation — withStoreContext wraps in ALS', async () => {
367
367
  let providerCalled = false
368
368
  const mockSetProvider = (fn: () => Map<string, unknown>) => {
369
369
  providerCalled = true
@@ -375,204 +375,204 @@ describe("configureStoreIsolation", () => {
375
375
  expect(providerCalled).toBe(true)
376
376
 
377
377
  // After activation, renderToString should work (it uses withStoreContext internally)
378
- const html = await renderToString(h("div", null, "store-iso"))
379
- expect(html).toBe("<div>store-iso</div>")
378
+ const html = await renderToString(h('div', null, 'store-iso'))
379
+ expect(html).toBe('<div>store-iso</div>')
380
380
  })
381
381
  })
382
382
 
383
383
  // ─── runWithRequestContext ───────────────────────────────────────────────────
384
384
 
385
- describe("runWithRequestContext", () => {
386
- test("provides isolated context for async operations", async () => {
387
- const Ctx = createContext("default")
385
+ describe('runWithRequestContext', () => {
386
+ test('provides isolated context for async operations', async () => {
387
+ const Ctx = createContext('default')
388
388
  const result = await runWithRequestContext(async () => {
389
- pushContext(new Map([[Ctx.id, "request-val"]]))
389
+ pushContext(new Map([[Ctx.id, 'request-val']]))
390
390
  return useContext(Ctx)
391
391
  })
392
- expect(result).toBe("request-val")
392
+ expect(result).toBe('request-val')
393
393
  })
394
394
 
395
- test("two concurrent runWithRequestContext calls are isolated", async () => {
396
- const Ctx = createContext("none")
395
+ test('two concurrent runWithRequestContext calls are isolated', async () => {
396
+ const Ctx = createContext('none')
397
397
  const [r1, r2] = await Promise.all([
398
398
  runWithRequestContext(async () => {
399
- pushContext(new Map([[Ctx.id, "ctx-A"]]))
399
+ pushContext(new Map([[Ctx.id, 'ctx-A']]))
400
400
  await new Promise<void>((r) => setTimeout(r, 5))
401
401
  return useContext(Ctx)
402
402
  }),
403
403
  runWithRequestContext(async () => {
404
- pushContext(new Map([[Ctx.id, "ctx-B"]]))
404
+ pushContext(new Map([[Ctx.id, 'ctx-B']]))
405
405
  await new Promise<void>((r) => setTimeout(r, 5))
406
406
  return useContext(Ctx)
407
407
  }),
408
408
  ])
409
- expect(r1).toBe("ctx-A")
410
- expect(r2).toBe("ctx-B")
409
+ expect(r1).toBe('ctx-A')
410
+ expect(r2).toBe('ctx-B')
411
411
  })
412
412
  })
413
413
 
414
414
  // ─── renderToString — uncovered branches ─────────────────────────────────────
415
415
 
416
- describe("renderToString — For component", () => {
417
- test("renders For with hydration markers", async () => {
418
- const items = signal(["a", "b", "c"])
416
+ describe('renderToString — For component', () => {
417
+ test('renders For with hydration markers', async () => {
418
+ const items = signal(['a', 'b', 'c'])
419
419
  const vnode = For({
420
420
  each: items,
421
421
  by: (item: unknown) => item as string,
422
- children: (item: unknown) => h("li", null, item as string),
422
+ children: (item: unknown) => h('li', null, item as string),
423
423
  })
424
424
  const html = await renderToString(vnode)
425
- expect(html).toContain("<!--pyreon-for-->")
426
- expect(html).toContain("<!--/pyreon-for-->")
427
- expect(html).toContain("<li>a</li>")
428
- expect(html).toContain("<li>b</li>")
429
- expect(html).toContain("<li>c</li>")
425
+ expect(html).toContain('<!--pyreon-for-->')
426
+ expect(html).toContain('<!--/pyreon-for-->')
427
+ expect(html).toContain('<li>a</li>')
428
+ expect(html).toContain('<li>b</li>')
429
+ expect(html).toContain('<li>c</li>')
430
430
  })
431
431
  })
432
432
 
433
- describe("renderToString — array children", () => {
434
- test("renders array of VNodes", async () => {
435
- const arr = [h("span", null, "x"), h("span", null, "y")]
433
+ describe('renderToString — array children', () => {
434
+ test('renders array of VNodes', async () => {
435
+ const arr = [h('span', null, 'x'), h('span', null, 'y')]
436
436
  // Components can return arrays
437
437
  const Comp: ComponentFn = () => arr as unknown as VNode
438
438
  const html = await renderToString(h(Comp, null))
439
- expect(html).toContain("<span>x</span>")
440
- expect(html).toContain("<span>y</span>")
439
+ expect(html).toContain('<span>x</span>')
440
+ expect(html).toContain('<span>y</span>')
441
441
  })
442
442
  })
443
443
 
444
- describe("renderToString — class and style edge cases", () => {
445
- test("renders class as array", async () => {
446
- const html = await renderToString(h("div", { class: ["foo", null, "bar", false, "baz"] }))
444
+ describe('renderToString — class and style edge cases', () => {
445
+ test('renders class as array', async () => {
446
+ const html = await renderToString(h('div', { class: ['foo', null, 'bar', false, 'baz'] }))
447
447
  expect(html).toContain('class="foo bar baz"')
448
448
  })
449
449
 
450
- test("renders class as object (truthy/falsy)", async () => {
451
- const html = await renderToString(h("div", { class: { active: true, hidden: false, bold: 1 } }))
452
- expect(html).toContain("active")
453
- expect(html).toContain("bold")
454
- expect(html).not.toContain("hidden")
450
+ test('renders class as object (truthy/falsy)', async () => {
451
+ const html = await renderToString(h('div', { class: { active: true, hidden: false, bold: 1 } }))
452
+ expect(html).toContain('active')
453
+ expect(html).toContain('bold')
454
+ expect(html).not.toContain('hidden')
455
455
  })
456
456
 
457
- test("renders empty class as no attribute", async () => {
458
- const html = await renderToString(h("div", { class: "" }))
459
- expect(html).toBe("<div></div>")
457
+ test('renders empty class as no attribute', async () => {
458
+ const html = await renderToString(h('div', { class: '' }))
459
+ expect(html).toBe('<div></div>')
460
460
  })
461
461
 
462
- test("renders numeric class value as string", async () => {
462
+ test('renders numeric class value as string', async () => {
463
463
  // cx() converts numbers to strings — class="42" is valid HTML
464
- const html = await renderToString(h("div", { class: 42 }))
464
+ const html = await renderToString(h('div', { class: 42 }))
465
465
  expect(html).toBe('<div class="42"></div>')
466
466
  })
467
467
 
468
- test("renders class from array", async () => {
469
- const html = await renderToString(h("div", { class: ["foo", false, "bar"] }))
468
+ test('renders class from array', async () => {
469
+ const html = await renderToString(h('div', { class: ['foo', false, 'bar'] }))
470
470
  expect(html).toBe('<div class="foo bar"></div>')
471
471
  })
472
472
 
473
- test("renders class from object", async () => {
473
+ test('renders class from object', async () => {
474
474
  const html = await renderToString(
475
- h("div", { class: { active: true, hidden: false, bold: true } }),
475
+ h('div', { class: { active: true, hidden: false, bold: true } }),
476
476
  )
477
477
  expect(html).toBe('<div class="active bold"></div>')
478
478
  })
479
479
 
480
- test("renders class from nested array/object", async () => {
481
- const html = await renderToString(h("div", { class: ["base", { active: true }, ["nested"]] }))
480
+ test('renders class from nested array/object', async () => {
481
+ const html = await renderToString(h('div', { class: ['base', { active: true }, ['nested']] }))
482
482
  expect(html).toBe('<div class="base active nested"></div>')
483
483
  })
484
484
 
485
- test("renders style as string", async () => {
486
- const html = await renderToString(h("div", { style: "color: red" }))
485
+ test('renders style as string', async () => {
486
+ const html = await renderToString(h('div', { style: 'color: red' }))
487
487
  expect(html).toContain('style="color: red"')
488
488
  })
489
489
 
490
- test("renders empty style object as no attribute", async () => {
490
+ test('renders empty style object as no attribute', async () => {
491
491
  // normalizeStyle with non-object/non-string falls through to return ""
492
- const html = await renderToString(h("div", { style: 42 }))
493
- expect(html).toBe("<div></div>")
492
+ const html = await renderToString(h('div', { style: 42 }))
493
+ expect(html).toBe('<div></div>')
494
494
  })
495
495
 
496
- test("renders className → class and htmlFor → for", async () => {
497
- const html = await renderToString(h("label", { className: "lbl", htmlFor: "inp" }))
496
+ test('renders className → class and htmlFor → for', async () => {
497
+ const html = await renderToString(h('label', { className: 'lbl', htmlFor: 'inp' }))
498
498
  expect(html).toContain('class="lbl"')
499
499
  expect(html).toContain('for="inp"')
500
500
  })
501
501
 
502
- test("renders camelCase props as kebab-case attributes", async () => {
503
- const html = await renderToString(h("div", { dataTestId: "val" }))
502
+ test('renders camelCase props as kebab-case attributes', async () => {
503
+ const html = await renderToString(h('div', { dataTestId: 'val' }))
504
504
  expect(html).toContain('data-test-id="val"')
505
505
  })
506
506
  })
507
507
 
508
- describe("renderToString — URL injection blocking", () => {
509
- test("blocks javascript: in href", async () => {
510
- const html = await renderToString(h("a", { href: "javascript:alert(1)" }))
511
- expect(html).not.toContain("javascript")
512
- expect(html).toBe("<a></a>")
508
+ describe('renderToString — URL injection blocking', () => {
509
+ test('blocks javascript: in href', async () => {
510
+ const html = await renderToString(h('a', { href: 'javascript:alert(1)' }))
511
+ expect(html).not.toContain('javascript')
512
+ expect(html).toBe('<a></a>')
513
513
  })
514
514
 
515
- test("blocks data: in src", async () => {
516
- const html = await renderToString(h("img", { src: "data:text/html,<h1>hi</h1>" }))
517
- expect(html).not.toContain("data:")
515
+ test('blocks data: in src', async () => {
516
+ const html = await renderToString(h('img', { src: 'data:text/html,<h1>hi</h1>' }))
517
+ expect(html).not.toContain('data:')
518
518
  })
519
519
  })
520
520
 
521
- describe("renderToString — null/undefined/boolean prop values", () => {
522
- test("omits null and undefined props", async () => {
523
- const html = await renderToString(h("div", { "data-a": null, "data-b": undefined }))
524
- expect(html).toBe("<div></div>")
521
+ describe('renderToString — null/undefined/boolean prop values', () => {
522
+ test('omits null and undefined props', async () => {
523
+ const html = await renderToString(h('div', { 'data-a': null, 'data-b': undefined }))
524
+ expect(html).toBe('<div></div>')
525
525
  })
526
526
 
527
- test("renders number children", async () => {
528
- const html = await renderToString(h("span", null, 42))
529
- expect(html).toBe("<span>42</span>")
527
+ test('renders number children', async () => {
528
+ const html = await renderToString(h('span', null, 42))
529
+ expect(html).toBe('<span>42</span>')
530
530
  })
531
531
 
532
- test("renders boolean true as text in children", async () => {
533
- const html = await renderToString(h("span", null, true))
534
- expect(html).toBe("<span>true</span>")
532
+ test('renders boolean true as text in children', async () => {
533
+ const html = await renderToString(h('span', null, true))
534
+ expect(html).toBe('<span>true</span>')
535
535
  })
536
536
 
537
- test("omits false children", async () => {
538
- const html = await renderToString(h("span", null, false))
539
- expect(html).toBe("<span></span>")
537
+ test('omits false children', async () => {
538
+ const html = await renderToString(h('span', null, false))
539
+ expect(html).toBe('<span></span>')
540
540
  })
541
541
  })
542
542
 
543
- describe("renderToString — component with children via h()", () => {
544
- test("mergeChildrenIntoProps passes children to component", async () => {
543
+ describe('renderToString — component with children via h()', () => {
544
+ test('mergeChildrenIntoProps passes children to component', async () => {
545
545
  const Wrapper: ComponentFn = (props: Record<string, unknown>) => {
546
- return h("div", null, props.children as VNode)
546
+ return h('div', null, props.children as VNode)
547
547
  }
548
- const html = await renderToString(h(Wrapper, null, h("span", null, "child")))
549
- expect(html).toBe("<div><span>child</span></div>")
548
+ const html = await renderToString(h(Wrapper, null, h('span', null, 'child')))
549
+ expect(html).toBe('<div><span>child</span></div>')
550
550
  })
551
551
 
552
- test("mergeChildrenIntoProps passes multiple children as array", async () => {
552
+ test('mergeChildrenIntoProps passes multiple children as array', async () => {
553
553
  const Wrapper: ComponentFn = (props: Record<string, unknown>) => {
554
554
  const kids = props.children as VNode[]
555
- return h("div", null, ...kids)
555
+ return h('div', null, ...kids)
556
556
  }
557
- const html = await renderToString(h(Wrapper, null, h("a", null, "1"), h("b", null, "2")))
558
- expect(html).toBe("<div><a>1</a><b>2</b></div>")
557
+ const html = await renderToString(h(Wrapper, null, h('a', null, '1'), h('b', null, '2')))
558
+ expect(html).toBe('<div><a>1</a><b>2</b></div>')
559
559
  })
560
560
 
561
- test("does not override explicit children prop", async () => {
561
+ test('does not override explicit children prop', async () => {
562
562
  const Wrapper: ComponentFn = (props: Record<string, unknown>) => {
563
- return h("div", null, props.children as VNode)
563
+ return h('div', null, props.children as VNode)
564
564
  }
565
- const html = await renderToString(h(Wrapper, { children: h("em", null, "explicit") }))
566
- expect(html).toBe("<div><em>explicit</em></div>")
565
+ const html = await renderToString(h(Wrapper, { children: h('em', null, 'explicit') }))
566
+ expect(html).toBe('<div><em>explicit</em></div>')
567
567
  })
568
568
  })
569
569
 
570
570
  // ─── renderToStream — uncovered branches ─────────────────────────────────────
571
571
 
572
- describe("renderToStream — additional coverage", () => {
572
+ describe('renderToStream — additional coverage', () => {
573
573
  async function collect(stream: ReadableStream<string>): Promise<string> {
574
574
  const reader = stream.getReader()
575
- let result = ""
575
+ let result = ''
576
576
  while (true) {
577
577
  const { done, value } = await reader.read()
578
578
  if (done) break
@@ -581,93 +581,93 @@ describe("renderToStream — additional coverage", () => {
581
581
  return result
582
582
  }
583
583
 
584
- test("streams Fragment children", async () => {
584
+ test('streams Fragment children', async () => {
585
585
  const html = await collect(
586
- renderToStream(h(Fragment, null, h("a", null, "1"), h("b", null, "2"))),
586
+ renderToStream(h(Fragment, null, h('a', null, '1'), h('b', null, '2'))),
587
587
  )
588
- expect(html).toBe("<a>1</a><b>2</b>")
588
+ expect(html).toBe('<a>1</a><b>2</b>')
589
589
  })
590
590
 
591
- test("streams For component with markers", async () => {
592
- const items = signal(["x", "y"])
591
+ test('streams For component with markers', async () => {
592
+ const items = signal(['x', 'y'])
593
593
  const vnode = For({
594
594
  each: items,
595
595
  by: (item: unknown) => item as string,
596
- children: (item: unknown) => h("li", null, item as string),
596
+ children: (item: unknown) => h('li', null, item as string),
597
597
  })
598
598
  const html = await collect(renderToStream(vnode))
599
- expect(html).toContain("<!--pyreon-for-->")
600
- expect(html).toContain("<li>x</li>")
601
- expect(html).toContain("<li>y</li>")
602
- expect(html).toContain("<!--/pyreon-for-->")
599
+ expect(html).toContain('<!--pyreon-for-->')
600
+ expect(html).toContain('<li>x</li>')
601
+ expect(html).toContain('<li>y</li>')
602
+ expect(html).toContain('<!--/pyreon-for-->')
603
603
  })
604
604
 
605
- test("streams reactive getter children", async () => {
606
- const name = signal("streamed")
607
- const html = await collect(renderToStream(h("p", null, () => name())))
608
- expect(html).toContain("streamed")
605
+ test('streams reactive getter children', async () => {
606
+ const name = signal('streamed')
607
+ const html = await collect(renderToStream(h('p', null, () => name())))
608
+ expect(html).toContain('streamed')
609
609
  })
610
610
 
611
- test("streams number and boolean children", async () => {
612
- const html = await collect(renderToStream(h("span", null, 99)))
613
- expect(html).toContain("99")
611
+ test('streams number and boolean children', async () => {
612
+ const html = await collect(renderToStream(h('span', null, 99)))
613
+ expect(html).toContain('99')
614
614
  })
615
615
 
616
- test("streams array children", async () => {
617
- const Comp: ComponentFn = () => [h("a", null, "1"), h("b", null, "2")] as unknown as VNode
616
+ test('streams array children', async () => {
617
+ const Comp: ComponentFn = () => [h('a', null, '1'), h('b', null, '2')] as unknown as VNode
618
618
  const html = await collect(renderToStream(h(Comp, null)))
619
- expect(html).toContain("<a>1</a>")
620
- expect(html).toContain("<b>2</b>")
619
+ expect(html).toContain('<a>1</a>')
620
+ expect(html).toContain('<b>2</b>')
621
621
  })
622
622
 
623
- test("streams void elements", async () => {
624
- const html = await collect(renderToStream(h("img", { src: "/pic.png" })))
625
- expect(html).toContain("<img")
626
- expect(html).toContain("/>")
623
+ test('streams void elements', async () => {
624
+ const html = await collect(renderToStream(h('img', { src: '/pic.png' })))
625
+ expect(html).toContain('<img')
626
+ expect(html).toContain('/>')
627
627
  })
628
628
 
629
- test("streams component returning null", async () => {
629
+ test('streams component returning null', async () => {
630
630
  const Empty: ComponentFn = () => null
631
631
  const html = await collect(renderToStream(h(Empty, null)))
632
- expect(html).toBe("")
632
+ expect(html).toBe('')
633
633
  })
634
634
 
635
- test("streams false/null children as empty", async () => {
636
- const html = await collect(renderToStream(h("div", null, false, null)))
637
- expect(html).toBe("<div></div>")
635
+ test('streams false/null children as empty', async () => {
636
+ const html = await collect(renderToStream(h('div', null, false, null)))
637
+ expect(html).toBe('<div></div>')
638
638
  })
639
639
 
640
- test("streams string children directly", async () => {
641
- const html = await collect(renderToStream(h("p", null, "text & <tag>")))
642
- expect(html).toContain("text &amp; &lt;tag&gt;")
640
+ test('streams string children directly', async () => {
641
+ const html = await collect(renderToStream(h('p', null, 'text & <tag>')))
642
+ expect(html).toContain('text &amp; &lt;tag&gt;')
643
643
  })
644
644
 
645
- test("multiple Suspense boundaries get incrementing IDs", async () => {
645
+ test('multiple Suspense boundaries get incrementing IDs', async () => {
646
646
  async function Slow1(): Promise<VNode> {
647
647
  await new Promise<void>((r) => setTimeout(r, 5))
648
- return h("span", null, "s1")
648
+ return h('span', null, 's1')
649
649
  }
650
650
  async function Slow2(): Promise<VNode> {
651
651
  await new Promise<void>((r) => setTimeout(r, 5))
652
- return h("span", null, "s2")
652
+ return h('span', null, 's2')
653
653
  }
654
654
  const vnode = h(
655
- "div",
655
+ 'div',
656
656
  null,
657
657
  h(Suspense, {
658
- fallback: h("p", null, "fb1"),
658
+ fallback: h('p', null, 'fb1'),
659
659
  children: h(Slow1 as unknown as unknown as ComponentFn, null),
660
660
  }),
661
661
  h(Suspense, {
662
- fallback: h("p", null, "fb2"),
662
+ fallback: h('p', null, 'fb2'),
663
663
  children: h(Slow2 as unknown as unknown as ComponentFn, null),
664
664
  }),
665
665
  )
666
666
  const html = await collect(renderToStream(vnode))
667
- expect(html).toContain("pyreon-s-0")
668
- expect(html).toContain("pyreon-s-1")
669
- expect(html).toContain("pyreon-t-0")
670
- expect(html).toContain("pyreon-t-1")
667
+ expect(html).toContain('pyreon-s-0')
668
+ expect(html).toContain('pyreon-s-1')
669
+ expect(html).toContain('pyreon-t-0')
670
+ expect(html).toContain('pyreon-t-1')
671
671
  // The swap script should only be emitted once
672
672
  const scriptMatches = html.match(/function __NS/g)
673
673
  expect(scriptMatches).toHaveLength(1)
@@ -676,10 +676,10 @@ describe("renderToStream — additional coverage", () => {
676
676
 
677
677
  // ─── Concurrent SSR isolation ─────────────────────────────────────────────────
678
678
 
679
- describe("concurrent SSR isolation", () => {
680
- test("50 concurrent renders produce correct isolated output", async () => {
679
+ describe('concurrent SSR isolation', () => {
680
+ test('50 concurrent renders produce correct isolated output', async () => {
681
681
  function Page(props: { id: number }) {
682
- return h("div", { "data-id": props.id }, `page-${props.id}`)
682
+ return h('div', { 'data-id': props.id }, `page-${props.id}`)
683
683
  }
684
684
 
685
685
  const renders = Array.from({ length: 50 }, (_, i) =>
@@ -696,9 +696,9 @@ describe("concurrent SSR isolation", () => {
696
696
  }
697
697
  })
698
698
 
699
- test("concurrent renders with different props do not leak state", async () => {
699
+ test('concurrent renders with different props do not leak state', async () => {
700
700
  function UserPage(props: { name: string }) {
701
- return h("div", null, `user:${props.name}`)
701
+ return h('div', null, `user:${props.name}`)
702
702
  }
703
703
 
704
704
  // Launch 40 concurrent renders with alternating data
@@ -716,10 +716,10 @@ describe("concurrent SSR isolation", () => {
716
716
  }
717
717
  })
718
718
 
719
- test("concurrent renders with async components stay isolated", async () => {
719
+ test('concurrent renders with async components stay isolated', async () => {
720
720
  async function SlowPage(props: { label: string }): Promise<VNode> {
721
721
  await new Promise<void>((r) => setTimeout(r, Math.random() * 10))
722
- return h("span", null, props.label)
722
+ return h('span', null, props.label)
723
723
  }
724
724
 
725
725
  const renders = Array.from({ length: 30 }, (_, i) =>
@@ -737,27 +737,27 @@ describe("concurrent SSR isolation", () => {
737
737
 
738
738
  // ─── Additional coverage — edge cases ─────────────────────────────────────────
739
739
 
740
- describe("renderToString — escapeHtml edge cases", () => {
741
- test("escapes single quotes in attribute values", async () => {
742
- const html = await renderToString(h("div", { title: "it's here" }))
743
- expect(html).toContain("it&#39;s here")
740
+ describe('renderToString — escapeHtml edge cases', () => {
741
+ test('escapes single quotes in attribute values', async () => {
742
+ const html = await renderToString(h('div', { title: "it's here" }))
743
+ expect(html).toContain('it&#39;s here')
744
744
  })
745
745
 
746
- test("escapes ampersand in text content", async () => {
747
- const html = await renderToString(h("p", null, "A & B"))
748
- expect(html).toBe("<p>A &amp; B</p>")
746
+ test('escapes ampersand in text content', async () => {
747
+ const html = await renderToString(h('p', null, 'A & B'))
748
+ expect(html).toBe('<p>A &amp; B</p>')
749
749
  })
750
750
 
751
- test("escapes double quotes in attribute values", async () => {
752
- const html = await renderToString(h("div", { title: 'say "hello"' }))
753
- expect(html).toContain("say &quot;hello&quot;")
751
+ test('escapes double quotes in attribute values', async () => {
752
+ const html = await renderToString(h('div', { title: 'say "hello"' }))
753
+ expect(html).toContain('say &quot;hello&quot;')
754
754
  })
755
755
  })
756
756
 
757
- describe("renderToStream — boolean and edge-case children", () => {
757
+ describe('renderToStream — boolean and edge-case children', () => {
758
758
  async function collect(stream: ReadableStream<string>): Promise<string> {
759
759
  const reader = stream.getReader()
760
- let result = ""
760
+ let result = ''
761
761
  while (true) {
762
762
  const { done, value } = await reader.read()
763
763
  if (done) break
@@ -767,35 +767,35 @@ describe("renderToStream — boolean and edge-case children", () => {
767
767
  }
768
768
 
769
769
  test("streams boolean true child as 'true'", async () => {
770
- const html = await collect(renderToStream(h("span", null, true)))
771
- expect(html).toBe("<span>true</span>")
770
+ const html = await collect(renderToStream(h('span', null, true)))
771
+ expect(html).toBe('<span>true</span>')
772
772
  })
773
773
 
774
- test("streams boolean false child as empty", async () => {
775
- const html = await collect(renderToStream(h("span", null, false)))
776
- expect(html).toBe("<span></span>")
774
+ test('streams boolean false child as empty', async () => {
775
+ const html = await collect(renderToStream(h('span', null, false)))
776
+ expect(html).toBe('<span></span>')
777
777
  })
778
778
 
779
- test("streams props with reactive getter", async () => {
780
- const cls = signal("active")
781
- const html = await collect(renderToStream(h("div", { class: () => cls() })))
779
+ test('streams props with reactive getter', async () => {
780
+ const cls = signal('active')
781
+ const html = await collect(renderToStream(h('div', { class: () => cls() })))
782
782
  expect(html).toContain('class="active"')
783
783
  })
784
784
 
785
- test("streams element with multiple props", async () => {
785
+ test('streams element with multiple props', async () => {
786
786
  const html = await collect(
787
- renderToStream(h("input", { type: "text", placeholder: "enter", disabled: true })),
787
+ renderToStream(h('input', { type: 'text', placeholder: 'enter', disabled: true })),
788
788
  )
789
789
  expect(html).toContain('type="text"')
790
790
  expect(html).toContain('placeholder="enter"')
791
- expect(html).toContain("disabled")
791
+ expect(html).toContain('disabled')
792
792
  })
793
793
  })
794
794
 
795
- describe("renderToStream — Suspense edge cases", () => {
795
+ describe('renderToStream — Suspense edge cases', () => {
796
796
  async function collect(stream: ReadableStream<string>): Promise<string> {
797
797
  const reader = stream.getReader()
798
- let result = ""
798
+ let result = ''
799
799
  while (true) {
800
800
  const { done, value } = await reader.read()
801
801
  if (done) break
@@ -804,82 +804,82 @@ describe("renderToStream — Suspense edge cases", () => {
804
804
  return result
805
805
  }
806
806
 
807
- test("Suspense boundary with no fallback prop", async () => {
807
+ test('Suspense boundary with no fallback prop', async () => {
808
808
  async function Content(): Promise<VNode> {
809
809
  await new Promise<void>((r) => setTimeout(r, 5))
810
- return h("span", null, "content")
810
+ return h('span', null, 'content')
811
811
  }
812
812
 
813
813
  const vnode = h(Suspense, {
814
- fallback: h("span", null, ""),
814
+ fallback: h('span', null, ''),
815
815
  children: h(Content as unknown as ComponentFn, null),
816
816
  })
817
817
  const html = await collect(renderToStream(vnode))
818
- expect(html).toContain("content")
818
+ expect(html).toContain('content')
819
819
  })
820
820
 
821
- test("Suspense boundary with no children prop", async () => {
821
+ test('Suspense boundary with no children prop', async () => {
822
822
  const vnode = h(Suspense, {
823
- fallback: h("span", null, "loading"),
823
+ fallback: h('span', null, 'loading'),
824
824
  })
825
825
  const html = await collect(renderToStream(vnode))
826
- expect(html).toContain("loading")
826
+ expect(html).toContain('loading')
827
827
  })
828
828
  })
829
829
 
830
- describe("renderToString — prop rendering edge cases", () => {
831
- test("renders true boolean prop as attribute name only (escaped)", async () => {
832
- const html = await renderToString(h("input", { disabled: true }))
833
- expect(html).toContain("disabled")
830
+ describe('renderToString — prop rendering edge cases', () => {
831
+ test('renders true boolean prop as attribute name only (escaped)', async () => {
832
+ const html = await renderToString(h('input', { disabled: true }))
833
+ expect(html).toContain('disabled')
834
834
  // Should not contain ="true"
835
835
  expect(html).not.toContain('disabled="true"')
836
836
  })
837
837
 
838
- test("omits props with null value", async () => {
839
- const html = await renderToString(h("div", { "data-x": null }))
840
- expect(html).toBe("<div></div>")
838
+ test('omits props with null value', async () => {
839
+ const html = await renderToString(h('div', { 'data-x': null }))
840
+ expect(html).toBe('<div></div>')
841
841
  })
842
842
 
843
- test("omits props with undefined value", async () => {
844
- const html = await renderToString(h("div", { "data-x": undefined }))
845
- expect(html).toBe("<div></div>")
843
+ test('omits props with undefined value', async () => {
844
+ const html = await renderToString(h('div', { 'data-x': undefined }))
845
+ expect(html).toBe('<div></div>')
846
846
  })
847
847
 
848
- test("blocks javascript: URI in action attribute", async () => {
849
- const html = await renderToString(h("form", { action: "javascript:void(0)" }))
850
- expect(html).not.toContain("javascript")
848
+ test('blocks javascript: URI in action attribute', async () => {
849
+ const html = await renderToString(h('form', { action: 'javascript:void(0)' }))
850
+ expect(html).not.toContain('javascript')
851
851
  })
852
852
 
853
- test("blocks data: URI in poster attribute", async () => {
854
- const html = await renderToString(h("video", { poster: "data:image/png;base64,abc" }))
855
- expect(html).not.toContain("data:")
853
+ test('blocks data: URI in poster attribute', async () => {
854
+ const html = await renderToString(h('video', { poster: 'data:image/png;base64,abc' }))
855
+ expect(html).not.toContain('data:')
856
856
  })
857
857
 
858
- test("allows safe URLs in href", async () => {
859
- const html = await renderToString(h("a", { href: "https://example.com" }))
858
+ test('allows safe URLs in href', async () => {
859
+ const html = await renderToString(h('a', { href: 'https://example.com' }))
860
860
  expect(html).toContain('href="https://example.com"')
861
861
  })
862
862
 
863
- test("renders style object with camelCase keys as kebab-case", async () => {
864
- const html = await renderToString(h("div", { style: { backgroundColor: "red" } }))
865
- expect(html).toContain("background-color: red")
863
+ test('renders style object with camelCase keys as kebab-case', async () => {
864
+ const html = await renderToString(h('div', { style: { backgroundColor: 'red' } }))
865
+ expect(html).toContain('background-color: red')
866
866
  })
867
867
 
868
- test("renders style object with auto-px for numeric values", async () => {
868
+ test('renders style object with auto-px for numeric values', async () => {
869
869
  const html = await renderToString(
870
- h("div", { style: { height: 100, marginTop: 20, opacity: 0.5, zIndex: 10 } }),
870
+ h('div', { style: { height: 100, marginTop: 20, opacity: 0.5, zIndex: 10 } }),
871
871
  )
872
- expect(html).toContain("height: 100px")
873
- expect(html).toContain("margin-top: 20px")
874
- expect(html).toContain("opacity: 0.5")
875
- expect(html).toContain("z-index: 10")
872
+ expect(html).toContain('height: 100px')
873
+ expect(html).toContain('margin-top: 20px')
874
+ expect(html).toContain('opacity: 0.5')
875
+ expect(html).toContain('z-index: 10')
876
876
  })
877
877
  })
878
878
 
879
- describe("renderToStream — error handling", () => {
879
+ describe('renderToStream — error handling', () => {
880
880
  async function collectStream(stream: ReadableStream<string>): Promise<string> {
881
881
  const reader = stream.getReader()
882
- let result = ""
882
+ let result = ''
883
883
  while (true) {
884
884
  const { done, value } = await reader.read()
885
885
  if (done) break
@@ -888,103 +888,103 @@ describe("renderToStream — error handling", () => {
888
888
  return result
889
889
  }
890
890
 
891
- test("stream emits error comment when component throws outside Suspense", async () => {
891
+ test('stream emits error comment when component throws outside Suspense', async () => {
892
892
  function Boom(): VNode {
893
- throw new Error("render error")
893
+ throw new Error('render error')
894
894
  }
895
895
 
896
896
  const html = await collectStream(renderToStream(h(Boom as ComponentFn, null)))
897
- expect(html).toContain("<!--pyreon-error-->")
897
+ expect(html).toContain('<!--pyreon-error-->')
898
898
  })
899
899
 
900
- test("stream renders element with skipped prop (event handler)", async () => {
900
+ test('stream renders element with skipped prop (event handler)', async () => {
901
901
  // Event handlers return null from renderProp — exercises `if (attr)` false branch in streamNode
902
902
  const html = await collectStream(
903
- renderToStream(h("button", { onClick: () => {}, id: "btn" }, "click")),
903
+ renderToStream(h('button', { onClick: () => {}, id: 'btn' }, 'click')),
904
904
  )
905
905
  expect(html).toContain('<button id="btn">')
906
- expect(html).not.toContain("onClick")
906
+ expect(html).not.toContain('onClick')
907
907
  })
908
908
  })
909
909
 
910
910
  // ─── Edge-case branches ──────────────────────────────────────────────────────
911
911
 
912
- describe("edge-case branches", () => {
913
- test("async component returning null via renderToString", async () => {
912
+ describe('edge-case branches', () => {
913
+ test('async component returning null via renderToString', async () => {
914
914
  async function NullComp(): Promise<null> {
915
915
  return null
916
916
  }
917
917
  const html = await renderToString(h(NullComp as unknown as ComponentFn, null))
918
- expect(html).toBe("")
918
+ expect(html).toBe('')
919
919
  })
920
920
 
921
- test("Suspense in stream without streaming context (renderToString path)", async () => {
921
+ test('Suspense in stream without streaming context (renderToString path)', async () => {
922
922
  // This tests the !ctx branch in streamSuspenseBoundary
923
923
  // renderToString handles Suspense via renderNode, not streamNode, so we test it there
924
924
  function Child(): VNode {
925
- return h("span", null, "resolved")
925
+ return h('span', null, 'resolved')
926
926
  }
927
927
  const vnode = h(Suspense, {
928
- fallback: h("span", null, "loading"),
928
+ fallback: h('span', null, 'loading'),
929
929
  children: h(Child as ComponentFn, null),
930
930
  })
931
931
  const html = await renderToString(vnode)
932
- expect(typeof html).toBe("string")
932
+ expect(typeof html).toBe('string')
933
933
  })
934
934
  })
935
935
 
936
936
  // ─── renderToString — Suspense with async components ─────────────────────────
937
937
 
938
- describe("renderToString — Suspense async paths", () => {
939
- test("renderToString with Suspense waits for async component and renders inline", async () => {
938
+ describe('renderToString — Suspense async paths', () => {
939
+ test('renderToString with Suspense waits for async component and renders inline', async () => {
940
940
  async function AsyncData(): Promise<ReturnType<typeof h>> {
941
941
  await new Promise<void>((r) => setTimeout(r, 5))
942
- return h("p", { id: "content" }, "loaded data")
942
+ return h('p', { id: 'content' }, 'loaded data')
943
943
  }
944
944
 
945
945
  const vnode = h(Suspense, {
946
- fallback: h("span", null, "loading..."),
946
+ fallback: h('span', null, 'loading...'),
947
947
  children: h(AsyncData as unknown as ComponentFn, null),
948
948
  })
949
949
 
950
950
  const html = await renderToString(vnode)
951
951
  // The async content should be present (renderToString awaits async components)
952
- expect(html).toContain("loaded data")
952
+ expect(html).toContain('loaded data')
953
953
  })
954
954
 
955
- test("renderToString with Suspense where async component throws propagates error", async () => {
955
+ test('renderToString with Suspense where async component throws propagates error', async () => {
956
956
  async function FailingComponent(): Promise<ReturnType<typeof h>> {
957
957
  await new Promise<void>((r) => setTimeout(r, 1))
958
- throw new Error("SSR component failure")
958
+ throw new Error('SSR component failure')
959
959
  }
960
960
 
961
961
  const vnode = h(Suspense, {
962
- fallback: h("span", null, "fallback"),
962
+ fallback: h('span', null, 'fallback'),
963
963
  children: h(FailingComponent as unknown as ComponentFn, null),
964
964
  })
965
965
 
966
966
  // renderToString does not catch per-boundary — the error propagates
967
- await expect(renderToString(vnode)).rejects.toThrow("SSR component failure")
967
+ await expect(renderToString(vnode)).rejects.toThrow('SSR component failure')
968
968
  })
969
969
  })
970
970
 
971
- describe("renderToStream — Suspense error fallback", () => {
972
- test("renderToStream keeps fallback visible when async component throws", async () => {
971
+ describe('renderToStream — Suspense error fallback', () => {
972
+ test('renderToStream keeps fallback visible when async component throws', async () => {
973
973
  async function ThrowingChild(): Promise<ReturnType<typeof h>> {
974
974
  await new Promise<void>((r) => setTimeout(r, 5))
975
- throw new Error("stream component error")
975
+ throw new Error('stream component error')
976
976
  }
977
977
 
978
978
  const vnode = h(Suspense, {
979
- fallback: h("span", { id: "fb" }, "fallback content"),
979
+ fallback: h('span', { id: 'fb' }, 'fallback content'),
980
980
  children: h(ThrowingChild as unknown as ComponentFn, null),
981
981
  })
982
982
 
983
983
  const html = await collectStream(renderToStream(vnode))
984
984
  // Fallback should be present
985
- expect(html).toContain("fallback content")
985
+ expect(html).toContain('fallback content')
986
986
  // No swap script invocation should be emitted (error was caught, no template + swap)
987
- expect(html).not.toContain("pyreon-t-")
987
+ expect(html).not.toContain('pyreon-t-')
988
988
  expect(html).not.toContain('__NS("pyreon-s-')
989
989
  })
990
990
  })