@fictjs/runtime 0.10.0 → 0.12.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.
Files changed (72) hide show
  1. package/dist/advanced.cjs +9 -9
  2. package/dist/advanced.d.cts +3 -3
  3. package/dist/advanced.d.ts +3 -3
  4. package/dist/advanced.js +4 -4
  5. package/dist/{binding-DqxS9ZQf.d.ts → binding-DcnhUSQK.d.ts} +1 -1
  6. package/dist/{binding-DUEukRxl.d.cts → binding-FRyTeLDn.d.cts} +1 -1
  7. package/dist/{chunk-DKA2I6ET.js → chunk-2UR2UWE2.js} +3 -3
  8. package/dist/{chunk-SZLJCQFZ.cjs → chunk-44EQF3AR.cjs} +63 -52
  9. package/dist/chunk-44EQF3AR.cjs.map +1 -0
  10. package/dist/{chunk-I4GKKAAY.cjs → chunk-4QGEN5SJ.cjs} +295 -262
  11. package/dist/chunk-4QGEN5SJ.cjs.map +1 -0
  12. package/dist/{chunk-V7BC64W2.cjs → chunk-C5IE4WUG.cjs} +8 -8
  13. package/dist/{chunk-V7BC64W2.cjs.map → chunk-C5IE4WUG.cjs.map} +1 -1
  14. package/dist/{chunk-F4RVNXOL.js → chunk-DIK33H5U.js} +8 -2
  15. package/dist/chunk-DIK33H5U.js.map +1 -0
  16. package/dist/{chunk-2JRPPCG7.js → chunk-FESAXMHT.js} +7 -6
  17. package/dist/{chunk-2JRPPCG7.js.map → chunk-FESAXMHT.js.map} +1 -1
  18. package/dist/chunk-FHQZCAAK.cjs +112 -0
  19. package/dist/chunk-FHQZCAAK.cjs.map +1 -0
  20. package/dist/{chunk-EQ5E4WOV.cjs → chunk-QNMYVXRL.cjs} +44 -38
  21. package/dist/chunk-QNMYVXRL.cjs.map +1 -0
  22. package/dist/{chunk-P4TZLFV6.js → chunk-S63VBIWN.js} +27 -16
  23. package/dist/chunk-S63VBIWN.js.map +1 -0
  24. package/dist/{chunk-R6FINS25.js → chunk-WIHNVN6L.js} +106 -73
  25. package/dist/chunk-WIHNVN6L.js.map +1 -0
  26. package/dist/{devtools-CMxlJUTx.d.cts → devtools-BtIkN77t.d.cts} +1 -1
  27. package/dist/{devtools-C4Hgfa-S.d.ts → devtools-D2z4llpA.d.ts} +1 -1
  28. package/dist/index.cjs +60 -58
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +4 -4
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.dev.js +72 -51
  33. package/dist/index.dev.js.map +1 -1
  34. package/dist/index.js +13 -11
  35. package/dist/index.js.map +1 -1
  36. package/dist/internal-list.cjs +4 -4
  37. package/dist/internal-list.d.cts +1 -1
  38. package/dist/internal-list.d.ts +1 -1
  39. package/dist/internal-list.js +3 -3
  40. package/dist/internal.cjs +5 -5
  41. package/dist/internal.d.cts +4 -4
  42. package/dist/internal.d.ts +4 -4
  43. package/dist/internal.js +4 -4
  44. package/dist/{list-BBzsJhrm.d.ts → list-BKM6YOPq.d.ts} +1 -1
  45. package/dist/{list-_NJCcjl1.d.cts → list-Bi8dDF8Q.d.cts} +1 -1
  46. package/dist/loader.cjs +28 -26
  47. package/dist/loader.cjs.map +1 -1
  48. package/dist/loader.js +11 -9
  49. package/dist/loader.js.map +1 -1
  50. package/dist/{props--zJ4ebbT.d.cts → props-9chMyBGb.d.cts} +1 -1
  51. package/dist/{props-BAGR7j-j.d.ts → props-D1nj2p_3.d.ts} +1 -1
  52. package/dist/{scope-CuImnvh1.d.ts → scope-BSkhJr0a.d.ts} +1 -1
  53. package/dist/{scope-Dq5hOu7c.d.cts → scope-Bn3sxem5.d.cts} +1 -1
  54. package/package.json +1 -1
  55. package/src/binding.ts +59 -29
  56. package/src/context.ts +4 -3
  57. package/src/dom.ts +65 -39
  58. package/src/error-boundary.ts +5 -5
  59. package/src/lifecycle.ts +8 -1
  60. package/src/list-helpers.ts +30 -13
  61. package/src/loader.ts +10 -8
  62. package/src/node-ops.ts +8 -5
  63. package/src/suspense.ts +5 -4
  64. package/dist/chunk-EQ5E4WOV.cjs.map +0 -1
  65. package/dist/chunk-F4RVNXOL.js.map +0 -1
  66. package/dist/chunk-I4GKKAAY.cjs.map +0 -1
  67. package/dist/chunk-K3DH5SD5.cjs +0 -111
  68. package/dist/chunk-K3DH5SD5.cjs.map +0 -1
  69. package/dist/chunk-P4TZLFV6.js.map +0 -1
  70. package/dist/chunk-R6FINS25.js.map +0 -1
  71. package/dist/chunk-SZLJCQFZ.cjs.map +0 -1
  72. /package/dist/{chunk-DKA2I6ET.js.map → chunk-2UR2UWE2.js.map} +0 -0
package/src/dom.ts CHANGED
@@ -123,6 +123,7 @@ function annotateComponentElements(
123
123
  */
124
124
  export function render(view: () => FictNode, container: HTMLElement): () => void {
125
125
  const root = createRootContext()
126
+ root.ownerDocument = container.ownerDocument ?? document
126
127
  const prev = pushRoot(root)
127
128
  let dom: DOMElement = undefined as unknown as DOMElement
128
129
  try {
@@ -166,6 +167,7 @@ export function render(view: () => FictNode, container: HTMLElement): () => void
166
167
  */
167
168
  export function hydrateComponent(view: () => FictNode, container: HTMLElement): () => void {
168
169
  const root = createRootContext()
170
+ root.ownerDocument = container.ownerDocument ?? document
169
171
  const prev = pushRoot(root)
170
172
 
171
173
  // Enable hydration flags for bindings that check __fictIsHydrating()
@@ -209,7 +211,7 @@ export function hydrateComponent(view: () => FictNode, container: HTMLElement):
209
211
  * - Reactive values (functions returning any of the above)
210
212
  */
211
213
  export function createElement(node: FictNode): DOMElement {
212
- return createElementWithContext(node, null)
214
+ return createElementWithContext(node, null, resolveOwnerDocument())
213
215
  }
214
216
 
215
217
  function resolveNamespace(tagName: string, namespace: NamespaceContext): NamespaceContext {
@@ -221,7 +223,15 @@ function resolveNamespace(tagName: string, namespace: NamespaceContext): Namespa
221
223
  return null
222
224
  }
223
225
 
224
- function createElementWithContext(node: FictNode, namespace: NamespaceContext): DOMElement {
226
+ function resolveOwnerDocument(ownerDocument?: Document): Document {
227
+ return ownerDocument ?? getCurrentRoot()?.ownerDocument ?? document
228
+ }
229
+
230
+ function createElementWithContext(
231
+ node: FictNode,
232
+ namespace: NamespaceContext,
233
+ ownerDocument: Document,
234
+ ): DOMElement {
225
235
  // Already a DOM node - pass through
226
236
  if (node instanceof Node) {
227
237
  return node
@@ -229,22 +239,22 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
229
239
 
230
240
  // Null/undefined/false - empty placeholder
231
241
  if (node === null || node === undefined || node === false) {
232
- return document.createTextNode('')
242
+ return ownerDocument.createTextNode('')
233
243
  }
234
244
 
235
245
  // Reactive getter function - resolve to actual node
236
246
  if (isReactive(node)) {
237
247
  const resolved = (node as () => FictNode)()
238
248
  if (resolved === node) {
239
- return document.createTextNode('')
249
+ return ownerDocument.createTextNode('')
240
250
  }
241
- return createElementWithContext(resolved, namespace)
251
+ return createElementWithContext(resolved, namespace, ownerDocument)
242
252
  }
243
253
 
244
254
  // Non-reactive function values are not valid DOM nodes.
245
255
  // Keep callback values inert instead of stringifying function source.
246
256
  if (typeof node === 'function') {
247
- return document.createTextNode('')
257
+ return ownerDocument.createTextNode('')
248
258
  }
249
259
 
250
260
  if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
@@ -265,26 +275,26 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
265
275
  .catch(() => undefined)
266
276
  }
267
277
  }
268
- return createElement(handle.marker as FictNode)
278
+ return createElementWithContext(handle.marker as FictNode, namespace, ownerDocument)
269
279
  }
270
280
  }
271
281
 
272
282
  // Array - create fragment
273
283
  if (Array.isArray(node)) {
274
- const frag = document.createDocumentFragment()
284
+ const frag = ownerDocument.createDocumentFragment()
275
285
  for (const child of node) {
276
- appendChildNode(frag, child, namespace)
286
+ appendChildNode(frag, child, namespace, ownerDocument)
277
287
  }
278
288
  return frag
279
289
  }
280
290
 
281
291
  // Primitive values - text node
282
292
  if (typeof node === 'string' || typeof node === 'number') {
283
- return document.createTextNode(String(node))
293
+ return ownerDocument.createTextNode(String(node))
284
294
  }
285
295
 
286
296
  if (typeof node === 'boolean') {
287
- return document.createTextNode('')
297
+ return ownerDocument.createTextNode('')
288
298
  }
289
299
 
290
300
  // VNode
@@ -355,13 +365,13 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
355
365
  onCleanup(() => hook.componentUnmount?.(componentId))
356
366
  }
357
367
  if (__fictIsResumable() && !__fictIsHydrating()) {
358
- const content = createElementWithContext(rendered as FictNode, namespace)
368
+ const content = createElementWithContext(rendered as FictNode, namespace, ownerDocument)
359
369
  const host =
360
370
  namespace === 'svg'
361
- ? document.createElementNS(SVG_NS, 'fict-host')
371
+ ? ownerDocument.createElementNS(SVG_NS, 'fict-host')
362
372
  : namespace === 'mathml'
363
- ? document.createElementNS(MATHML_NS, 'fict-host')
364
- : document.createElement('fict-host')
373
+ ? ownerDocument.createElementNS(MATHML_NS, 'fict-host')
374
+ : ownerDocument.createElement('fict-host')
365
375
  host.setAttribute('data-fict-host', '')
366
376
  if (namespace === null && (host as HTMLElement).style) {
367
377
  ;(host as HTMLElement).style.display = 'contents'
@@ -384,7 +394,7 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
384
394
  }
385
395
  return host as DOMElement
386
396
  }
387
- const componentRoot = createElementWithContext(rendered as FictNode, namespace)
397
+ const componentRoot = createElementWithContext(rendered as FictNode, namespace, ownerDocument)
388
398
  if (hook && componentId !== undefined) {
389
399
  mountElements = collectComponentMountElements(componentRoot)
390
400
  annotateComponentElements(mountElements, componentId, componentName)
@@ -392,7 +402,7 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
392
402
  return componentRoot
393
403
  } catch (err) {
394
404
  if (handleSuspend(err as any)) {
395
- return document.createComment('fict:suspend')
405
+ return ownerDocument.createComment('fict:suspend')
396
406
  }
397
407
  handleError(err, { source: 'render', componentName: vnode.type.name })
398
408
  throw err
@@ -403,9 +413,9 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
403
413
 
404
414
  // Fragment
405
415
  if (vnode.type === Fragment) {
406
- const frag = document.createDocumentFragment()
416
+ const frag = ownerDocument.createDocumentFragment()
407
417
  const children = vnode.props?.children as FictNode | FictNode[] | undefined
408
- appendChildren(frag, children, namespace)
418
+ appendChildren(frag, children, namespace, ownerDocument)
409
419
  return frag
410
420
  }
411
421
 
@@ -414,15 +424,16 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
414
424
  const resolvedNamespace = resolveNamespace(tagName, namespace)
415
425
  const el =
416
426
  resolvedNamespace === 'svg'
417
- ? document.createElementNS(SVG_NS, tagName)
427
+ ? ownerDocument.createElementNS(SVG_NS, tagName)
418
428
  : resolvedNamespace === 'mathml'
419
- ? document.createElementNS(MATHML_NS, tagName)
420
- : document.createElement(tagName)
429
+ ? ownerDocument.createElementNS(MATHML_NS, tagName)
430
+ : ownerDocument.createElement(tagName)
421
431
  applyProps(el, vnode.props ?? {}, resolvedNamespace === 'svg')
422
432
  appendChildren(
423
433
  el as unknown as ParentNode & Node,
424
434
  vnode.props?.children as FictNode | FictNode[] | undefined,
425
435
  tagName === 'foreignObject' ? null : resolvedNamespace,
436
+ ownerDocument,
426
437
  )
427
438
  return el as DOMElement
428
439
  }
@@ -442,10 +453,10 @@ export function template(
442
453
  isSVG?: boolean,
443
454
  isMathML?: boolean,
444
455
  ): () => Node {
445
- let node: Node | null = null
456
+ const nodeByDocument = new WeakMap<Document, Node>()
446
457
 
447
- const create = (): Node => {
448
- const t = document.createElement('template')
458
+ const create = (ownerDocument: Document): Node => {
459
+ const t = ownerDocument.createElement('template')
449
460
 
450
461
  if (isSVG) {
451
462
  // fix: Wrap HTML in <svg> to parse content in SVG namespace
@@ -463,7 +474,7 @@ export function template(
463
474
  return wrapper.firstChild!
464
475
  }
465
476
  // Preserve all root nodes by returning a fragment
466
- const fragment = document.createDocumentFragment()
477
+ const fragment = ownerDocument.createDocumentFragment()
467
478
  fragment.append(...Array.from(wrapper.childNodes))
468
479
  return fragment
469
480
  }
@@ -483,7 +494,7 @@ export function template(
483
494
  return wrapper.firstChild!
484
495
  }
485
496
  // Preserve all root nodes by returning a fragment
486
- const fragment = document.createDocumentFragment()
497
+ const fragment = ownerDocument.createDocumentFragment()
487
498
  fragment.append(...Array.from(wrapper.childNodes))
488
499
  return fragment
489
500
  }
@@ -504,17 +515,26 @@ export function template(
504
515
  return content
505
516
  }
506
517
 
518
+ const getBase = (ownerDocument: Document): Node => {
519
+ const cached = nodeByDocument.get(ownerDocument)
520
+ if (cached) return cached
521
+ const created = create(ownerDocument)
522
+ nodeByDocument.set(ownerDocument, created)
523
+ return created
524
+ }
525
+
507
526
  // Create the cloning function
508
527
  const fn = isImportNode
509
528
  ? () =>
510
529
  untrack(() => {
511
- const base = node || (node = create())
530
+ const ownerDocument = resolveOwnerDocument()
531
+ const base = getBase(ownerDocument)
512
532
  return isHydratingActive()
513
- ? claimNodes(base, () => document.importNode(base, true))
514
- : document.importNode(base, true)
533
+ ? claimNodes(base, () => ownerDocument.importNode(base, true))
534
+ : ownerDocument.importNode(base, true)
515
535
  })
516
536
  : () => {
517
- const base = node || (node = create())
537
+ const base = getBase(resolveOwnerDocument())
518
538
  return isHydratingActive()
519
539
  ? claimNodes(base, () => base.cloneNode(true))
520
540
  : base.cloneNode(true)
@@ -553,7 +573,10 @@ function appendChildNode(
553
573
  parent: ParentNode & Node,
554
574
  child: FictNode,
555
575
  namespace: NamespaceContext,
576
+ ownerDocument: Document,
556
577
  ): void {
578
+ const parentOwnerDocument = parent.ownerDocument ?? ownerDocument
579
+
557
580
  // Skip nullish values
558
581
  if (child === null || child === undefined || child === false) {
559
582
  return
@@ -561,7 +584,7 @@ function appendChildNode(
561
584
 
562
585
  // Handle BindingHandle (recursive)
563
586
  if (isBindingHandle(child)) {
564
- appendChildNode(parent, child.marker, namespace)
587
+ appendChildNode(parent, child.marker, namespace, parentOwnerDocument)
565
588
  // Flush pending nodes now that markers are in the DOM
566
589
  child.flush?.()
567
590
  return
@@ -573,7 +596,9 @@ function appendChildNode(
573
596
  if (typeof child === 'function') {
574
597
  const childGetter = child as () => FictNode
575
598
  if (isReactive(childGetter)) {
576
- createChildBinding(parent, childGetter, node => createElementWithContext(node, namespace))
599
+ createChildBinding(parent, childGetter, node =>
600
+ createElementWithContext(node, namespace, parentOwnerDocument),
601
+ )
577
602
  return
578
603
  }
579
604
  return
@@ -582,7 +607,7 @@ function appendChildNode(
582
607
  // Static child - create element and append
583
608
  if (Array.isArray(child)) {
584
609
  for (const item of child) {
585
- appendChildNode(parent, item, namespace)
610
+ appendChildNode(parent, item, namespace, parentOwnerDocument)
586
611
  }
587
612
  return
588
613
  }
@@ -590,16 +615,16 @@ function appendChildNode(
590
615
  // Cast to Node for remaining logic
591
616
  let domNode: Node
592
617
  if (typeof child !== 'object' || child === null) {
593
- domNode = document.createTextNode(String(child ?? ''))
618
+ domNode = parentOwnerDocument.createTextNode(String(child ?? ''))
594
619
  } else {
595
- domNode = createElementWithContext(child as any, namespace) as Node
620
+ domNode = createElementWithContext(child as any, namespace, parentOwnerDocument) as Node
596
621
  }
597
622
 
598
623
  // Handle DocumentFragment manually to avoid JSDOM issues
599
624
  if (domNode.nodeType === 11) {
600
625
  const children = Array.from(domNode.childNodes)
601
626
  for (const node of children) {
602
- appendChildNode(parent, node as FictNode, namespace)
627
+ appendChildNode(parent, node as FictNode, namespace, parentOwnerDocument)
603
628
  }
604
629
  return
605
630
  }
@@ -627,17 +652,18 @@ function appendChildren(
627
652
  parent: ParentNode & Node,
628
653
  children: FictNode | FictNode[] | undefined,
629
654
  namespace: NamespaceContext,
655
+ ownerDocument: Document,
630
656
  ): void {
631
657
  if (children === undefined) return
632
658
 
633
659
  if (Array.isArray(children)) {
634
660
  for (const child of children) {
635
- appendChildren(parent, child, namespace)
661
+ appendChildren(parent, child, namespace, ownerDocument)
636
662
  }
637
663
  return
638
664
  }
639
665
 
640
- appendChildNode(parent, children, namespace)
666
+ appendChildNode(parent, children, namespace, ownerDocument)
641
667
  }
642
668
 
643
669
  // ============================================================================
@@ -19,11 +19,11 @@ interface ErrorBoundaryProps extends BaseProps {
19
19
  }
20
20
 
21
21
  export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
22
- const fragment = document.createDocumentFragment()
23
- const marker = document.createComment('fict:error-boundary')
24
- fragment.appendChild(marker)
25
-
26
22
  const hostRoot = getCurrentRoot()
23
+ const markerOwnerDocument = hostRoot?.ownerDocument ?? document
24
+ const fragment = markerOwnerDocument.createDocumentFragment()
25
+ const marker = markerOwnerDocument.createComment('fict:error-boundary')
26
+ fragment.appendChild(marker)
27
27
 
28
28
  let cleanup: (() => void) | undefined
29
29
  let activeNodes: Node[] = []
@@ -58,7 +58,7 @@ export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
58
58
  let nodes: Node[] = []
59
59
  try {
60
60
  const output = createElement(value)
61
- nodes = toNodeArray(output)
61
+ nodes = toNodeArray(output, markerOwnerDocument)
62
62
  const parentNode = marker.parentNode as (ParentNode & Node) | null
63
63
  if (parentNode) {
64
64
  insertNodesBefore(parentNode, nodes, marker)
package/src/lifecycle.ts CHANGED
@@ -11,6 +11,7 @@ type LifecycleFn = () => void | Cleanup
11
11
 
12
12
  export interface RootContext {
13
13
  parent?: RootContext | undefined
14
+ ownerDocument?: Document | undefined
14
15
  onMountCallbacks?: LifecycleFn[]
15
16
  cleanups: Cleanup[]
16
17
  destroyCallbacks: Cleanup[]
@@ -60,7 +61,13 @@ function setRootSuspendDevtools(root: RootContext, suspended: boolean): void {
60
61
  }
61
62
 
62
63
  export function createRootContext(parent?: RootContext): RootContext {
63
- const root = { parent, cleanups: [], destroyCallbacks: [], suspended: false }
64
+ const root = {
65
+ parent,
66
+ ownerDocument: parent?.ownerDocument,
67
+ cleanups: [],
68
+ destroyCallbacks: [],
69
+ suspended: false,
70
+ }
64
71
  registerRootDevtools(root)
65
72
  return root
66
73
  }
@@ -203,9 +203,12 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
203
203
  function createKeyedListContainer<T = unknown>(
204
204
  startOverride?: Comment,
205
205
  endOverride?: Comment,
206
+ defaultOwnerDocument?: Document,
206
207
  ): KeyedListContainer<T> {
207
- const startMarker = startOverride ?? document.createComment('fict:list:start')
208
- const endMarker = endOverride ?? document.createComment('fict:list:end')
208
+ const markerOwnerDocument =
209
+ startOverride?.ownerDocument ?? endOverride?.ownerDocument ?? defaultOwnerDocument ?? document
210
+ const startMarker = startOverride ?? markerOwnerDocument.createComment('fict:list:start')
211
+ const endMarker = endOverride ?? markerOwnerDocument.createComment('fict:list:end')
209
212
 
210
213
  const dispose = () => {
211
214
  // Clean up all blocks
@@ -228,7 +231,9 @@ function createKeyedListContainer<T = unknown>(
228
231
  container.orderedIndexByKey.clear()
229
232
  return
230
233
  }
231
- const range = document.createRange()
234
+ const rangeOwnerDocument =
235
+ startMarker.ownerDocument ?? endMarker.ownerDocument ?? markerOwnerDocument
236
+ const range = rangeOwnerDocument.createRange()
232
237
  range.setStartBefore(startMarker)
233
238
  range.setEndAfter(endMarker)
234
239
  range.deleteContents()
@@ -284,12 +289,13 @@ function createKeyedBlock<T>(
284
289
 
285
290
  const indexSig = needsIndex
286
291
  ? createSignal<number>(index)
287
- : (((next?: number) => {
292
+ : (function indexSignal(next?: number) {
288
293
  if (arguments.length === 0) return index
289
294
  index = next as number
290
295
  return index
291
- }) as Signal<number>)
296
+ } as Signal<number>)
292
297
  const root = createRootContext(hostRoot)
298
+ const nodeOwnerDocument = root.ownerDocument ?? hostRoot?.ownerDocument ?? document
293
299
  const prevRoot = pushRoot(root)
294
300
  // maintaining proper cleanup chain. The scope will be disposed when
295
301
  // the root is destroyed, ensuring nested effects are properly cleaned up.
@@ -310,10 +316,10 @@ function createKeyedBlock<T>(
310
316
  rendered instanceof Node ||
311
317
  (Array.isArray(rendered) && rendered.every(n => n instanceof Node))
312
318
  ) {
313
- nodes = toNodeArray(rendered)
319
+ nodes = toNodeArray(rendered, nodeOwnerDocument)
314
320
  } else {
315
321
  const element = createElement(rendered as unknown as FictNode)
316
- nodes = toNodeArray(element)
322
+ nodes = toNodeArray(element, nodeOwnerDocument)
317
323
  }
318
324
  })
319
325
 
@@ -500,10 +506,18 @@ function createFineGrainedKeyedList<T>(
500
506
  startOverride?: Comment,
501
507
  endOverride?: Comment,
502
508
  ): KeyedListBinding {
503
- const container = createKeyedListContainer<T>(startOverride, endOverride)
504
509
  const hostRoot = getCurrentRoot()
510
+ const container = createKeyedListContainer<T>(
511
+ startOverride,
512
+ endOverride,
513
+ hostRoot?.ownerDocument ?? document,
514
+ )
515
+ const markerOwnerDocument =
516
+ container.startMarker.ownerDocument ?? hostRoot?.ownerDocument ?? document
505
517
  const useProvided = !!(startOverride && endOverride)
506
- const fragment = useProvided ? container.startMarker : document.createDocumentFragment()
518
+ const fragment = useProvided
519
+ ? container.startMarker
520
+ : markerOwnerDocument.createDocumentFragment()
507
521
  if (!useProvided) {
508
522
  ;(fragment as DocumentFragment).append(container.startMarker, container.endMarker)
509
523
  }
@@ -580,7 +594,7 @@ function createFineGrainedKeyedList<T>(
580
594
  withHydrationRange(
581
595
  container.startMarker.nextSibling,
582
596
  container.endMarker,
583
- parent.ownerDocument ?? document,
597
+ parent.ownerDocument ?? markerOwnerDocument,
584
598
  () => {
585
599
  for (let index = 0; index < newItems.length; index++) {
586
600
  const item = newItems[index]!
@@ -632,7 +646,7 @@ function createFineGrainedKeyedList<T>(
632
646
  destroyRoot(block.root)
633
647
  }
634
648
  // Use Range.deleteContents for efficient bulk DOM removal
635
- const range = document.createRange()
649
+ const range = (parent.ownerDocument ?? markerOwnerDocument).createRange()
636
650
  range.setStartAfter(container.startMarker)
637
651
  range.setEndBefore(container.endMarker)
638
652
  range.deleteContents()
@@ -934,7 +948,7 @@ function createFineGrainedKeyedList<T>(
934
948
 
935
949
  const waitForConnection = () => {
936
950
  if (connectObserver || typeof MutationObserver === 'undefined') return
937
- const root = container.startMarker.getRootNode?.() ?? document
951
+ const root = container.startMarker.getRootNode?.() ?? markerOwnerDocument
938
952
  const shadowRoot =
939
953
  root && root.nodeType === 11 && isShadowRoot(root as Node) ? (root as ShadowRoot) : null
940
954
  connectObserver = new MutationObserver(() => {
@@ -946,7 +960,10 @@ function createFineGrainedKeyedList<T>(
946
960
  }
947
961
  }
948
962
  })
949
- connectObserver.observe(document, { childList: true, subtree: true })
963
+ connectObserver.observe(markerOwnerDocument, { childList: true, subtree: true })
964
+ if (root && root.nodeType === 11) {
965
+ connectObserver.observe(root as Node, { childList: true, subtree: true })
966
+ }
950
967
  if (shadowRoot) {
951
968
  connectObserver.observe(shadowRoot, { childList: true, subtree: true })
952
969
  }
package/src/loader.ts CHANGED
@@ -447,19 +447,20 @@ function setupHoverPrefetch(doc: Document, delay: number): () => void {
447
447
  }
448
448
 
449
449
  function prefetchElementQrls(el: Element): void {
450
+ const ownerDocument = el.ownerDocument ?? (typeof document !== 'undefined' ? document : undefined)
450
451
  // Prefetch event handler QRLs
451
452
  const eventAttrs = ['on:click', 'on:input', 'on:change', 'on:submit', 'on:keydown', 'on:keyup']
452
453
  for (const attr of eventAttrs) {
453
454
  const qrl = el.getAttribute(attr)
454
455
  if (qrl) {
455
- prefetchQrl(qrl)
456
+ prefetchQrl(qrl, ownerDocument)
456
457
  }
457
458
  }
458
459
 
459
460
  // Prefetch resume handler QRL
460
461
  const resumeQrl = el.getAttribute('data-fict-h')
461
462
  if (resumeQrl) {
462
- prefetchQrl(resumeQrl)
463
+ prefetchQrl(resumeQrl, ownerDocument)
463
464
  }
464
465
 
465
466
  // Also check children for nested QRLs
@@ -470,17 +471,17 @@ function prefetchElementQrls(el: Element): void {
470
471
  for (const attr of eventAttrs) {
471
472
  const qrl = child.getAttribute(attr)
472
473
  if (qrl) {
473
- prefetchQrl(qrl)
474
+ prefetchQrl(qrl, ownerDocument)
474
475
  }
475
476
  }
476
477
  const childResumeQrl = child.getAttribute('data-fict-h')
477
478
  if (childResumeQrl) {
478
- prefetchQrl(childResumeQrl)
479
+ prefetchQrl(childResumeQrl, ownerDocument)
479
480
  }
480
481
  })
481
482
  }
482
483
 
483
- function prefetchQrl(qrl: string): void {
484
+ function prefetchQrl(qrl: string, ownerDocument?: Document): void {
484
485
  const { url } = parseQrl(qrl)
485
486
  if (!url || prefetchedUrls.has(url)) return
486
487
 
@@ -490,12 +491,13 @@ function prefetchQrl(qrl: string): void {
490
491
  const resolvedUrl = resolveModuleUrl(url)
491
492
 
492
493
  // Use modulepreload link for best browser support
493
- if (typeof document !== 'undefined') {
494
- const link = document.createElement('link')
494
+ const doc = ownerDocument ?? (typeof document !== 'undefined' ? document : undefined)
495
+ if (doc) {
496
+ const link = doc.createElement('link')
495
497
  link.rel = 'modulepreload'
496
498
  link.href = resolvedUrl
497
499
  link.crossOrigin = 'anonymous'
498
- document.head.appendChild(link)
500
+ doc.head?.appendChild(link)
499
501
  }
500
502
  }
501
503
 
package/src/node-ops.ts CHANGED
@@ -7,7 +7,10 @@
7
7
  * Convert a value to a flat array of DOM nodes.
8
8
  * Defensively handles proxies and non-DOM values.
9
9
  */
10
- export function toNodeArray(node: Node | Node[] | unknown): Node[] {
10
+ export function toNodeArray(
11
+ node: Node | Node[] | unknown,
12
+ ownerDocument: Document = document,
13
+ ): Node[] {
11
14
  try {
12
15
  if (Array.isArray(node)) {
13
16
  // Preserve original array reference when it's already a flat Node array
@@ -29,7 +32,7 @@ export function toNodeArray(node: Node | Node[] | unknown): Node[] {
29
32
  }
30
33
  const result: Node[] = []
31
34
  for (const item of node) {
32
- result.push(...toNodeArray(item))
35
+ result.push(...toNodeArray(item, ownerDocument))
33
36
  }
34
37
  return result
35
38
  }
@@ -62,7 +65,7 @@ export function toNodeArray(node: Node | Node[] | unknown): Node[] {
62
65
  try {
63
66
  // Duck-type BindingHandle-like values
64
67
  if (typeof node === 'object' && node !== null && 'marker' in node) {
65
- return toNodeArray((node as { marker: unknown }).marker)
68
+ return toNodeArray((node as { marker: unknown }).marker, ownerDocument)
66
69
  }
67
70
  } catch {
68
71
  // Ignore property check error
@@ -70,9 +73,9 @@ export function toNodeArray(node: Node | Node[] | unknown): Node[] {
70
73
 
71
74
  // Primitive fallback
72
75
  try {
73
- return [document.createTextNode(String(node))]
76
+ return [ownerDocument.createTextNode(String(node))]
74
77
  } catch {
75
- return [document.createTextNode('')]
78
+ return [ownerDocument.createTextNode('')]
76
79
  }
77
80
  }
78
81
 
package/src/suspense.ts CHANGED
@@ -55,6 +55,7 @@ export function Suspense(props: SuspenseProps): FictNode {
55
55
  let resolvedOnce = false
56
56
  let epoch = 0
57
57
  const hostRoot = getCurrentRoot()
58
+ const markerOwnerDocument = hostRoot?.ownerDocument ?? document
58
59
 
59
60
  const toFallback = (err?: unknown) =>
60
61
  typeof props.fallback === 'function'
@@ -85,7 +86,7 @@ export function Suspense(props: SuspenseProps): FictNode {
85
86
  boundaryPushed = true
86
87
  }
87
88
  const output = createElement(view)
88
- nodes = toNodeArray(output)
89
+ nodes = toNodeArray(output, markerOwnerDocument)
89
90
  // Suspended view: child threw a suspense token and was handled upstream.
90
91
  // Avoid replacing existing fallback content; tear down this attempt.
91
92
  const suspendedAttempt =
@@ -123,9 +124,9 @@ export function Suspense(props: SuspenseProps): FictNode {
123
124
  activeNodes = nodes
124
125
  }
125
126
 
126
- const fragment = document.createDocumentFragment()
127
- const startMarker = document.createComment('fict:suspense-start')
128
- const endMarker = document.createComment('fict:suspense-end')
127
+ const fragment = markerOwnerDocument.createDocumentFragment()
128
+ const startMarker = markerOwnerDocument.createComment('fict:suspense-start')
129
+ const endMarker = markerOwnerDocument.createComment('fict:suspense-end')
129
130
  fragment.appendChild(startMarker)
130
131
  fragment.appendChild(endMarker)
131
132
  let cleanup: (() => void) | undefined