@pyreon/server 0.11.4 → 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.
@@ -2,100 +2,100 @@
2
2
  * @vitest-environment happy-dom
3
3
  */
4
4
 
5
- import type { ComponentFn } from "@pyreon/core"
6
- import { h } from "@pyreon/core"
7
- import { hydrateIslands, startClient } from "../client"
5
+ import type { ComponentFn } from '@pyreon/core'
6
+ import { h } from '@pyreon/core'
7
+ import { hydrateIslands, startClient } from '../client'
8
8
 
9
9
  // ─── startClient ────────────────────────────────────────────────────────────
10
10
 
11
- describe("startClient", () => {
11
+ describe('startClient', () => {
12
12
  beforeEach(() => {
13
- document.body.innerHTML = ""
13
+ document.body.innerHTML = ''
14
14
  delete (window as unknown as Record<string, unknown>).__PYREON_LOADER_DATA__
15
15
  })
16
16
 
17
- test("throws when container is not found", () => {
18
- const App: ComponentFn = () => h("div", null, "app")
17
+ test('throws when container is not found', () => {
18
+ const App: ComponentFn = () => h('div', null, 'app')
19
19
  expect(() => startClient({ App, routes: [] })).toThrow('Container "#app" not found')
20
20
  })
21
21
 
22
- test("throws with custom container selector not found", () => {
23
- const App: ComponentFn = () => h("div", null, "app")
24
- expect(() => startClient({ App, routes: [], container: "#missing" })).toThrow(
22
+ test('throws with custom container selector not found', () => {
23
+ const App: ComponentFn = () => h('div', null, 'app')
24
+ expect(() => startClient({ App, routes: [], container: '#missing' })).toThrow(
25
25
  'Container "#missing" not found',
26
26
  )
27
27
  })
28
28
 
29
- test("mounts app into empty container", () => {
29
+ test('mounts app into empty container', () => {
30
30
  document.body.innerHTML = '<div id="app"></div>'
31
- const App: ComponentFn = () => h("div", null, "Hello")
32
- const cleanup = startClient({ App, routes: [{ path: "/", component: App }] })
33
- expect(typeof cleanup).toBe("function")
31
+ const App: ComponentFn = () => h('div', null, 'Hello')
32
+ const cleanup = startClient({ App, routes: [{ path: '/', component: App }] })
33
+ expect(typeof cleanup).toBe('function')
34
34
  cleanup()
35
35
  })
36
36
 
37
- test("hydrates app when container has SSR content", () => {
37
+ test('hydrates app when container has SSR content', () => {
38
38
  document.body.innerHTML = '<div id="app"><div>SSR Content</div></div>'
39
- const App: ComponentFn = () => h("div", null, "SSR Content")
40
- const cleanup = startClient({ App, routes: [{ path: "/", component: App }] })
41
- expect(typeof cleanup).toBe("function")
39
+ const App: ComponentFn = () => h('div', null, 'SSR Content')
40
+ const cleanup = startClient({ App, routes: [{ path: '/', component: App }] })
41
+ expect(typeof cleanup).toBe('function')
42
42
  cleanup()
43
43
  })
44
44
 
45
- test("hydrates loader data from window global", () => {
45
+ test('hydrates loader data from window global', () => {
46
46
  document.body.innerHTML = '<div id="app"></div>'
47
47
  ;(window as unknown as Record<string, unknown>).__PYREON_LOADER_DATA__ = {
48
- "/": { items: [1, 2] },
48
+ '/': { items: [1, 2] },
49
49
  }
50
- const App: ComponentFn = () => h("div", null, "app")
51
- const cleanup = startClient({ App, routes: [{ path: "/", component: App }] })
52
- expect(typeof cleanup).toBe("function")
50
+ const App: ComponentFn = () => h('div', null, 'app')
51
+ const cleanup = startClient({ App, routes: [{ path: '/', component: App }] })
52
+ expect(typeof cleanup).toBe('function')
53
53
  cleanup()
54
54
  })
55
55
 
56
- test("ignores non-object loader data", () => {
56
+ test('ignores non-object loader data', () => {
57
57
  document.body.innerHTML = '<div id="app"></div>'
58
- ;(window as unknown as Record<string, unknown>).__PYREON_LOADER_DATA__ = "invalid"
59
- const App: ComponentFn = () => h("div", null, "app")
60
- const cleanup = startClient({ App, routes: [{ path: "/", component: App }] })
61
- expect(typeof cleanup).toBe("function")
58
+ ;(window as unknown as Record<string, unknown>).__PYREON_LOADER_DATA__ = 'invalid'
59
+ const App: ComponentFn = () => h('div', null, 'app')
60
+ const cleanup = startClient({ App, routes: [{ path: '/', component: App }] })
61
+ expect(typeof cleanup).toBe('function')
62
62
  cleanup()
63
63
  })
64
64
 
65
- test("accepts Element directly as container", () => {
65
+ test('accepts Element directly as container', () => {
66
66
  document.body.innerHTML = '<div id="custom"></div>'
67
- const el = document.getElementById("custom")!
68
- const App: ComponentFn = () => h("div", null, "app")
69
- const cleanup = startClient({ App, routes: [{ path: "/", component: App }], container: el })
70
- expect(typeof cleanup).toBe("function")
67
+ const el = document.getElementById('custom')!
68
+ const App: ComponentFn = () => h('div', null, 'app')
69
+ const cleanup = startClient({ App, routes: [{ path: '/', component: App }], container: el })
70
+ expect(typeof cleanup).toBe('function')
71
71
  cleanup()
72
72
  })
73
73
  })
74
74
 
75
75
  // ─── hydrateIslands ─────────────────────────────────────────────────────────
76
76
 
77
- describe("hydrateIslands", () => {
77
+ describe('hydrateIslands', () => {
78
78
  beforeEach(() => {
79
- document.body.innerHTML = ""
79
+ document.body.innerHTML = ''
80
80
  })
81
81
 
82
- test("returns cleanup function with no islands on page", () => {
82
+ test('returns cleanup function with no islands on page', () => {
83
83
  const cleanup = hydrateIslands({})
84
- expect(typeof cleanup).toBe("function")
84
+ expect(typeof cleanup).toBe('function')
85
85
  cleanup()
86
86
  })
87
87
 
88
- test("skips islands without data-component attribute", () => {
89
- document.body.innerHTML = "<pyreon-island></pyreon-island>"
88
+ test('skips islands without data-component attribute', () => {
89
+ document.body.innerHTML = '<pyreon-island></pyreon-island>'
90
90
  const cleanup = hydrateIslands({})
91
- expect(typeof cleanup).toBe("function")
91
+ expect(typeof cleanup).toBe('function')
92
92
  cleanup()
93
93
  })
94
94
 
95
- test("warns and skips islands with no matching loader", () => {
95
+ test('warns and skips islands with no matching loader', () => {
96
96
  document.body.innerHTML =
97
97
  '<pyreon-island data-component="Missing" data-props="{}"></pyreon-island>'
98
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
98
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
99
99
  const cleanup = hydrateIslands({})
100
100
  expect(warnSpy).toHaveBeenCalledWith(
101
101
  expect.stringContaining('No loader registered for island "Missing"'),
@@ -109,22 +109,22 @@ describe("hydrateIslands", () => {
109
109
  '<pyreon-island data-component="Counter" data-props=\'{"count":5}\'></pyreon-island>'
110
110
 
111
111
  const Counter: ComponentFn = (props) =>
112
- h("button", null, `Count: ${(props as Record<string, unknown>).count}`)
112
+ h('button', null, `Count: ${(props as Record<string, unknown>).count}`)
113
113
 
114
114
  const cleanup = hydrateIslands({
115
115
  Counter: () => Promise.resolve({ default: Counter }),
116
116
  })
117
117
  // Give async hydration time to resolve
118
118
  await new Promise((r) => setTimeout(r, 50))
119
- expect(typeof cleanup).toBe("function")
119
+ expect(typeof cleanup).toBe('function')
120
120
  cleanup()
121
121
  })
122
122
 
123
- test("hydrates island with direct function module", async () => {
123
+ test('hydrates island with direct function module', async () => {
124
124
  document.body.innerHTML =
125
125
  '<pyreon-island data-component="Widget" data-props="{}"></pyreon-island>'
126
126
 
127
- const Widget: ComponentFn = () => h("div", null, "widget")
127
+ const Widget: ComponentFn = () => h('div', null, 'widget')
128
128
 
129
129
  const cleanup = hydrateIslands({
130
130
  Widget: () => Promise.resolve({ default: Widget }),
@@ -137,14 +137,14 @@ describe("hydrateIslands", () => {
137
137
  document.body.innerHTML =
138
138
  '<pyreon-island data-component="Lazy" data-hydrate="idle" data-props="{}"></pyreon-island>'
139
139
 
140
- const Lazy: ComponentFn = () => h("div", null, "lazy")
140
+ const Lazy: ComponentFn = () => h('div', null, 'lazy')
141
141
 
142
142
  const cleanup = hydrateIslands({
143
143
  Lazy: () => Promise.resolve({ default: Lazy }),
144
144
  })
145
145
  // Wait for idle callback or timeout fallback
146
146
  await new Promise((r) => setTimeout(r, 300))
147
- expect(typeof cleanup).toBe("function")
147
+ expect(typeof cleanup).toBe('function')
148
148
  cleanup()
149
149
  })
150
150
 
@@ -155,7 +155,7 @@ describe("hydrateIslands", () => {
155
155
  let called = false
156
156
  const Static: ComponentFn = () => {
157
157
  called = true
158
- return h("div", null, "static")
158
+ return h('div', null, 'static')
159
159
  }
160
160
 
161
161
  const cleanup = hydrateIslands({
@@ -170,12 +170,12 @@ describe("hydrateIslands", () => {
170
170
  document.body.innerHTML =
171
171
  '<pyreon-island data-component="Visible" data-hydrate="visible" data-props="{}"></pyreon-island>'
172
172
 
173
- const Visible: ComponentFn = () => h("div", null, "visible")
173
+ const Visible: ComponentFn = () => h('div', null, 'visible')
174
174
 
175
175
  const cleanup = hydrateIslands({
176
176
  Visible: () => Promise.resolve({ default: Visible }),
177
177
  })
178
- expect(typeof cleanup).toBe("function")
178
+ expect(typeof cleanup).toBe('function')
179
179
  cleanup()
180
180
  })
181
181
 
@@ -183,7 +183,7 @@ describe("hydrateIslands", () => {
183
183
  document.body.innerHTML =
184
184
  '<pyreon-island data-component="Responsive" data-hydrate="media(all)" data-props="{}"></pyreon-island>'
185
185
 
186
- const Responsive: ComponentFn = () => h("div", null, "responsive")
186
+ const Responsive: ComponentFn = () => h('div', null, 'responsive')
187
187
 
188
188
  const cleanup = hydrateIslands({
189
189
  Responsive: () => Promise.resolve({ default: Responsive }),
@@ -201,7 +201,7 @@ describe("hydrateIslands", () => {
201
201
  let hydrated = false
202
202
  const Responsive: ComponentFn = () => {
203
203
  hydrated = true
204
- return h("div", null, "responsive")
204
+ return h('div', null, 'responsive')
205
205
  }
206
206
 
207
207
  const cleanup = hydrateIslands({
@@ -213,71 +213,71 @@ describe("hydrateIslands", () => {
213
213
  cleanup()
214
214
  })
215
215
 
216
- test("handles invalid island props JSON gracefully", async () => {
216
+ test('handles invalid island props JSON gracefully', async () => {
217
217
  document.body.innerHTML =
218
218
  '<pyreon-island data-component="Bad" data-props="not valid json"></pyreon-island>'
219
219
 
220
- const Bad: ComponentFn = () => h("div", null, "bad")
221
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
220
+ const Bad: ComponentFn = () => h('div', null, 'bad')
221
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
222
222
 
223
223
  const cleanup = hydrateIslands({
224
224
  Bad: () => Promise.resolve({ default: Bad }),
225
225
  })
226
226
  await new Promise((r) => setTimeout(r, 50))
227
227
  expect(errorSpy).toHaveBeenCalledWith(
228
- expect.stringContaining("Invalid island props JSON"),
228
+ expect.stringContaining('Invalid island props JSON'),
229
229
  expect.anything(),
230
230
  )
231
231
  errorSpy.mockRestore()
232
232
  cleanup()
233
233
  })
234
234
 
235
- test("handles island props that parse to non-object", async () => {
235
+ test('handles island props that parse to non-object', async () => {
236
236
  document.body.innerHTML =
237
237
  '<pyreon-island data-component="Arr" data-props="[1,2,3]"></pyreon-island>'
238
238
 
239
- const Arr: ComponentFn = () => h("div", null)
240
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
239
+ const Arr: ComponentFn = () => h('div', null)
240
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
241
241
 
242
242
  const cleanup = hydrateIslands({
243
243
  Arr: () => Promise.resolve({ default: Arr }),
244
244
  })
245
245
  await new Promise((r) => setTimeout(r, 50))
246
246
  expect(errorSpy).toHaveBeenCalledWith(
247
- expect.stringContaining("Invalid island props JSON"),
247
+ expect.stringContaining('Invalid island props JSON'),
248
248
  expect.anything(),
249
249
  )
250
250
  errorSpy.mockRestore()
251
251
  cleanup()
252
252
  })
253
253
 
254
- test("handles island props that parse to null", async () => {
254
+ test('handles island props that parse to null', async () => {
255
255
  document.body.innerHTML =
256
256
  '<pyreon-island data-component="Null" data-props="null"></pyreon-island>'
257
257
 
258
- const Null: ComponentFn = () => h("div", null)
259
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
258
+ const Null: ComponentFn = () => h('div', null)
259
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
260
260
 
261
261
  const cleanup = hydrateIslands({
262
262
  Null: () => Promise.resolve({ default: Null }),
263
263
  })
264
264
  await new Promise((r) => setTimeout(r, 50))
265
265
  expect(errorSpy).toHaveBeenCalledWith(
266
- expect.stringContaining("Invalid island props JSON"),
266
+ expect.stringContaining('Invalid island props JSON'),
267
267
  expect.anything(),
268
268
  )
269
269
  errorSpy.mockRestore()
270
270
  cleanup()
271
271
  })
272
272
 
273
- test("handles loader failure gracefully", async () => {
273
+ test('handles loader failure gracefully', async () => {
274
274
  document.body.innerHTML =
275
275
  '<pyreon-island data-component="Fail" data-props="{}"></pyreon-island>'
276
276
 
277
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
277
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
278
278
 
279
279
  const cleanup = hydrateIslands({
280
- Fail: () => Promise.reject(new Error("import failed")),
280
+ Fail: () => Promise.reject(new Error('import failed')),
281
281
  })
282
282
  await new Promise((r) => setTimeout(r, 50))
283
283
  expect(errorSpy).toHaveBeenCalledWith(
@@ -288,10 +288,10 @@ describe("hydrateIslands", () => {
288
288
  cleanup()
289
289
  })
290
290
 
291
- test("uses default empty props when data-props is missing", async () => {
291
+ test('uses default empty props when data-props is missing', async () => {
292
292
  document.body.innerHTML = '<pyreon-island data-component="NoProps"></pyreon-island>'
293
293
 
294
- const NoProps: ComponentFn = () => h("div", null, "no-props")
294
+ const NoProps: ComponentFn = () => h('div', null, 'no-props')
295
295
 
296
296
  const cleanup = hydrateIslands({
297
297
  NoProps: () => Promise.resolve({ default: NoProps }),
@@ -304,7 +304,7 @@ describe("hydrateIslands", () => {
304
304
  document.body.innerHTML =
305
305
  '<pyreon-island data-component="Default" data-props="{}"></pyreon-island>'
306
306
 
307
- const Default: ComponentFn = () => h("div", null, "default")
307
+ const Default: ComponentFn = () => h('div', null, 'default')
308
308
 
309
309
  const cleanup = hydrateIslands({
310
310
  Default: () => Promise.resolve({ default: Default }),
@@ -313,14 +313,14 @@ describe("hydrateIslands", () => {
313
313
  cleanup()
314
314
  })
315
315
 
316
- test("cleanup cancels idle callbacks", async () => {
316
+ test('cleanup cancels idle callbacks', async () => {
317
317
  document.body.innerHTML =
318
318
  '<pyreon-island data-component="IdleCancel" data-hydrate="idle" data-props="{}"></pyreon-island>'
319
319
 
320
320
  let hydrated = false
321
321
  const IdleCancel: ComponentFn = () => {
322
322
  hydrated = true
323
- return h("div", null)
323
+ return h('div', null)
324
324
  }
325
325
 
326
326
  const cleanup = hydrateIslands({
@@ -332,11 +332,11 @@ describe("hydrateIslands", () => {
332
332
  expect(hydrated).toBe(false)
333
333
  })
334
334
 
335
- test("handles unknown strategy as fallback (immediate hydration)", async () => {
335
+ test('handles unknown strategy as fallback (immediate hydration)', async () => {
336
336
  document.body.innerHTML =
337
337
  '<pyreon-island data-component="Unknown" data-hydrate="custom-unknown" data-props="{}"></pyreon-island>'
338
338
 
339
- const Unknown: ComponentFn = () => h("div", null, "unknown")
339
+ const Unknown: ComponentFn = () => h('div', null, 'unknown')
340
340
 
341
341
  const cleanup = hydrateIslands({
342
342
  Unknown: () => Promise.resolve({ default: Unknown }),
@@ -345,14 +345,14 @@ describe("hydrateIslands", () => {
345
345
  cleanup()
346
346
  })
347
347
 
348
- test("multiple islands hydrate independently", async () => {
348
+ test('multiple islands hydrate independently', async () => {
349
349
  document.body.innerHTML = `
350
350
  <pyreon-island data-component="A" data-props='{"id":1}'></pyreon-island>
351
351
  <pyreon-island data-component="B" data-props='{"id":2}'></pyreon-island>
352
352
  `
353
353
 
354
- const A: ComponentFn = () => h("div", null, "a")
355
- const B: ComponentFn = () => h("div", null, "b")
354
+ const A: ComponentFn = () => h('div', null, 'a')
355
+ const B: ComponentFn = () => h('div', null, 'b')
356
356
 
357
357
  const cleanup = hydrateIslands({
358
358
  A: () => Promise.resolve({ default: A }),
@@ -362,14 +362,14 @@ describe("hydrateIslands", () => {
362
362
  cleanup()
363
363
  })
364
364
 
365
- test("observeVisibility falls back to immediate callback when IntersectionObserver is missing", async () => {
365
+ test('observeVisibility falls back to immediate callback when IntersectionObserver is missing', async () => {
366
366
  document.body.innerHTML =
367
367
  '<pyreon-island data-component="Fallback" data-hydrate="visible" data-props="{}"></pyreon-island>'
368
368
 
369
369
  let hydrated = false
370
370
  const Fallback: ComponentFn = () => {
371
371
  hydrated = true
372
- return h("div", null, "fallback")
372
+ return h('div', null, 'fallback')
373
373
  }
374
374
 
375
375
  // Remove IntersectionObserver to trigger fallback
@@ -386,14 +386,14 @@ describe("hydrateIslands", () => {
386
386
  ;(window as unknown as Record<string, unknown>).IntersectionObserver = origIO
387
387
  })
388
388
 
389
- test("visible strategy: IntersectionObserver fires when element becomes visible", async () => {
389
+ test('visible strategy: IntersectionObserver fires when element becomes visible', async () => {
390
390
  document.body.innerHTML =
391
391
  '<pyreon-island data-component="VisObs" data-hydrate="visible" data-props="{}"></pyreon-island>'
392
392
 
393
393
  let hydrated = false
394
394
  const VisObs: ComponentFn = () => {
395
395
  hydrated = true
396
- return h("div", null, "vis")
396
+ return h('div', null, 'vis')
397
397
  }
398
398
 
399
399
  // Mock IntersectionObserver to call callback with isIntersecting: true
@@ -421,7 +421,7 @@ describe("hydrateIslands", () => {
421
421
  return null
422
422
  }
423
423
  get rootMargin() {
424
- return ""
424
+ return ''
425
425
  }
426
426
  get thresholds() {
427
427
  return []
@@ -438,14 +438,14 @@ describe("hydrateIslands", () => {
438
438
  globalThis.IntersectionObserver = origIO
439
439
  })
440
440
 
441
- test("visible strategy: IntersectionObserver entry not intersecting does not hydrate", async () => {
441
+ test('visible strategy: IntersectionObserver entry not intersecting does not hydrate', async () => {
442
442
  document.body.innerHTML =
443
443
  '<pyreon-island data-component="VisNoInt" data-hydrate="visible" data-props="{}"></pyreon-island>'
444
444
 
445
445
  let hydrated = false
446
446
  const VisNoInt: ComponentFn = () => {
447
447
  hydrated = true
448
- return h("div", null, "vis")
448
+ return h('div', null, 'vis')
449
449
  }
450
450
 
451
451
  const origIO = globalThis.IntersectionObserver
@@ -467,7 +467,7 @@ describe("hydrateIslands", () => {
467
467
  return null
468
468
  }
469
469
  get rootMargin() {
470
- return ""
470
+ return ''
471
471
  }
472
472
  get thresholds() {
473
473
  return []
@@ -484,14 +484,14 @@ describe("hydrateIslands", () => {
484
484
  globalThis.IntersectionObserver = origIO
485
485
  })
486
486
 
487
- test("media strategy: deferred match fires onChange handler", async () => {
487
+ test('media strategy: deferred match fires onChange handler', async () => {
488
488
  document.body.innerHTML =
489
489
  '<pyreon-island data-component="MediaDeferred" data-hydrate="media((min-width: 99999px))" data-props="{}"></pyreon-island>'
490
490
 
491
491
  let hydrated = false
492
492
  const MediaDeferred: ComponentFn = () => {
493
493
  hydrated = true
494
- return h("div", null, "media")
494
+ return h('div', null, 'media')
495
495
  }
496
496
 
497
497
  // Mock matchMedia to initially not match, then trigger change
@@ -531,14 +531,14 @@ describe("hydrateIslands", () => {
531
531
  window.matchMedia = origMatchMedia
532
532
  })
533
533
 
534
- test("media strategy: onChange handler ignores non-matching events", async () => {
534
+ test('media strategy: onChange handler ignores non-matching events', async () => {
535
535
  document.body.innerHTML =
536
536
  '<pyreon-island data-component="MediaNoMatch" data-hydrate="media((min-width: 99999px))" data-props="{}"></pyreon-island>'
537
537
 
538
538
  let hydrated = false
539
539
  const MediaNoMatch: ComponentFn = () => {
540
540
  hydrated = true
541
- return h("div", null, "media")
541
+ return h('div', null, 'media')
542
542
  }
543
543
 
544
544
  const origMatchMedia = window.matchMedia