@fictjs/runtime 0.4.0 → 0.5.1

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 +10 -8
  2. package/dist/advanced.cjs.map +1 -1
  3. package/dist/advanced.d.cts +4 -3
  4. package/dist/advanced.d.ts +4 -3
  5. package/dist/advanced.js +10 -8
  6. package/dist/advanced.js.map +1 -1
  7. package/dist/{chunk-L4DIV3RC.cjs → chunk-4ZPZM5IG.cjs} +9 -7
  8. package/dist/chunk-4ZPZM5IG.cjs.map +1 -0
  9. package/dist/{chunk-XLIZJMMJ.js → chunk-5OYBRKE4.js} +8 -6
  10. package/dist/{chunk-XLIZJMMJ.js.map → chunk-5OYBRKE4.js.map} +1 -1
  11. package/dist/chunk-6RCEIWZL.cjs +2380 -0
  12. package/dist/chunk-6RCEIWZL.cjs.map +1 -0
  13. package/dist/chunk-7BO6P2KP.js +2380 -0
  14. package/dist/chunk-7BO6P2KP.js.map +1 -0
  15. package/dist/{chunk-TWELIZRY.js → chunk-AR6NSCZM.js} +5 -3
  16. package/dist/{chunk-TWELIZRY.js.map → chunk-AR6NSCZM.js.map} +1 -1
  17. package/dist/{chunk-M2TSXZ4C.cjs → chunk-LFMXNQZC.cjs} +18 -16
  18. package/dist/chunk-LFMXNQZC.cjs.map +1 -0
  19. package/dist/{chunk-SO6X7G5S.js → chunk-RY5CY4CI.js} +501 -1880
  20. package/dist/chunk-RY5CY4CI.js.map +1 -0
  21. package/dist/chunk-WJHXPF7M.cjs +2259 -0
  22. package/dist/chunk-WJHXPF7M.cjs.map +1 -0
  23. package/dist/{context-B25xyQrJ.d.cts → context-CTBE00S_.d.cts} +1 -1
  24. package/dist/{context-CGdP7_Jb.d.ts → context-lkLhbkFJ.d.ts} +1 -1
  25. package/dist/{effect-D6kaLM2-.d.cts → effect-BpSNEJJz.d.cts} +7 -67
  26. package/dist/{effect-D6kaLM2-.d.ts → effect-BpSNEJJz.d.ts} +7 -67
  27. package/dist/index.cjs +40 -38
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +5 -4
  30. package/dist/index.d.ts +5 -4
  31. package/dist/index.dev.js +125 -22
  32. package/dist/index.dev.js.map +1 -1
  33. package/dist/index.js +19 -17
  34. package/dist/index.js.map +1 -1
  35. package/dist/internal.cjs +202 -203
  36. package/dist/internal.cjs.map +1 -1
  37. package/dist/internal.d.cts +13 -23
  38. package/dist/internal.d.ts +13 -23
  39. package/dist/internal.js +207 -208
  40. package/dist/internal.js.map +1 -1
  41. package/dist/loader.cjs +280 -0
  42. package/dist/loader.cjs.map +1 -0
  43. package/dist/loader.d.cts +57 -0
  44. package/dist/loader.d.ts +57 -0
  45. package/dist/loader.js +280 -0
  46. package/dist/loader.js.map +1 -0
  47. package/dist/{props-BIfromL0.d.cts → props-XTHYD19o.d.cts} +13 -2
  48. package/dist/{props-BEgIVMRx.d.ts → props-x-HbI-jX.d.ts} +13 -2
  49. package/dist/resume-BrAkmSTY.d.cts +79 -0
  50. package/dist/resume-Dx8_l72o.d.ts +79 -0
  51. package/dist/{scope-CzNkn587.d.ts → scope-CdbGmsFf.d.ts} +1 -1
  52. package/dist/{scope-Cx_3CjIZ.d.cts → scope-DfcP9I-A.d.cts} +1 -1
  53. package/dist/signal-C4ISF17w.d.cts +66 -0
  54. package/dist/signal-C4ISF17w.d.ts +66 -0
  55. package/package.json +6 -1
  56. package/src/binding.ts +254 -5
  57. package/src/cycle-guard.ts +1 -1
  58. package/src/dom.ts +103 -5
  59. package/src/hooks.ts +15 -2
  60. package/src/hydration.ts +75 -0
  61. package/src/internal.ts +34 -2
  62. package/src/list-helpers.ts +127 -11
  63. package/src/loader.ts +437 -0
  64. package/src/node-ops.ts +65 -0
  65. package/src/resume.ts +517 -0
  66. package/src/signal.ts +47 -22
  67. package/src/store.ts +8 -0
  68. package/dist/chunk-ID3WBWNO.cjs +0 -3638
  69. package/dist/chunk-ID3WBWNO.cjs.map +0 -1
  70. package/dist/chunk-L4DIV3RC.cjs.map +0 -1
  71. package/dist/chunk-M2TSXZ4C.cjs.map +0 -1
  72. package/dist/chunk-SO6X7G5S.js.map +0 -1
@@ -0,0 +1,75 @@
1
+ interface HydrationContext {
2
+ cursor: Node | null
3
+ boundary: Node | null
4
+ owner: Document
5
+ }
6
+
7
+ const hydrationStack: HydrationContext[] = []
8
+
9
+ export function withHydration(root: ParentNode & Node, fn: () => void): void {
10
+ const owner = root.ownerDocument ?? document
11
+ hydrationStack.push({
12
+ cursor: root.firstChild,
13
+ boundary: null,
14
+ owner,
15
+ })
16
+ try {
17
+ fn()
18
+ } finally {
19
+ hydrationStack.pop()
20
+ }
21
+ }
22
+
23
+ export function withHydrationRange(
24
+ start: Node | null,
25
+ end: Node | null,
26
+ owner: Document,
27
+ fn: () => void,
28
+ ): void {
29
+ hydrationStack.push({
30
+ cursor: start,
31
+ boundary: end,
32
+ owner,
33
+ })
34
+ try {
35
+ fn()
36
+ } finally {
37
+ hydrationStack.pop()
38
+ }
39
+ }
40
+
41
+ export function claimNodes(templateRoot: Node, fallback: () => Node): Node {
42
+ const ctx = hydrationStack[hydrationStack.length - 1]
43
+ if (!ctx || !ctx.cursor) {
44
+ return fallback()
45
+ }
46
+
47
+ const count = templateRoot.nodeType === 11 ? templateRoot.childNodes.length : 1
48
+ if (count === 0) return fallback()
49
+
50
+ const claimed: Node[] = []
51
+ let cursor: Node | null = ctx.cursor
52
+ for (let i = 0; i < count; i++) {
53
+ if (!cursor || cursor === ctx.boundary) {
54
+ return fallback()
55
+ }
56
+ claimed.push(cursor)
57
+ cursor = cursor.nextSibling
58
+ }
59
+
60
+ ctx.cursor = cursor
61
+
62
+ if (claimed.length === 1) {
63
+ return claimed[0]!
64
+ }
65
+
66
+ const frag = ctx.owner.createDocumentFragment()
67
+ for (const node of claimed) {
68
+ frag.appendChild(node)
69
+ }
70
+ return frag
71
+ }
72
+
73
+ export function isHydratingActive(): boolean {
74
+ return hydrationStack.length > 0
75
+ }
package/src/internal.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  // ============================================================================
14
14
 
15
15
  export { createSignal, createSelector, __resetReactiveState } from './signal'
16
- export { createStore, type Store } from './store'
16
+ export { createStore, type Store, isStoreProxy, unwrapStore } from './store'
17
17
  export { createMemo } from './memo'
18
18
  export { createEffect } from './effect'
19
19
  export { Fragment } from './jsx'
@@ -31,8 +31,38 @@ export {
31
31
  __fictUseEffect,
32
32
  __fictRender,
33
33
  __fictResetContext,
34
+ __fictPrepareContext,
34
35
  } from './hooks'
35
36
 
37
+ // ============================================================================
38
+ // SSR / Resumability (Internal)
39
+ // ============================================================================
40
+
41
+ export {
42
+ __fictEnableSSR,
43
+ __fictDisableSSR,
44
+ __fictIsSSR,
45
+ __fictEnableResumable,
46
+ __fictDisableResumable,
47
+ __fictIsResumable,
48
+ __fictEnterHydration,
49
+ __fictExitHydration,
50
+ __fictIsHydrating,
51
+ __fictRegisterScope,
52
+ __fictGetScopeRegistry,
53
+ __fictSerializeSSRState,
54
+ __fictSetSSRState,
55
+ __fictGetSSRScope,
56
+ __fictEnsureScope,
57
+ __fictUseLexicalScope,
58
+ __fictGetScopeProps,
59
+ __fictQrl,
60
+ __fictRegisterResume,
61
+ __fictGetResume,
62
+ serializeValue,
63
+ deserializeValue,
64
+ } from './resume'
65
+
36
66
  // ============================================================================
37
67
  // Props Helpers (Compiler-generated code)
38
68
  // ============================================================================
@@ -53,6 +83,7 @@ export {
53
83
  bindProperty,
54
84
  bindRef,
55
85
  insert,
86
+ insertBetween,
56
87
  createConditional,
57
88
  createPortal,
58
89
  spread,
@@ -61,6 +92,7 @@ export {
61
92
  isReactive,
62
93
  unwrap,
63
94
  } from './binding'
95
+ export { resolvePath, getSlotEnd } from './node-ops'
64
96
 
65
97
  // ============================================================================
66
98
  // Event Delegation (Compiler-generated code)
@@ -86,7 +118,7 @@ export {
86
118
  // DOM Creation (Compiler-generated code)
87
119
  // ============================================================================
88
120
 
89
- export { createElement, template } from './dom'
121
+ export { createElement, template, render, hydrateComponent } from './dom'
90
122
  export { createRenderEffect } from './effect'
91
123
 
92
124
  // ============================================================================
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { createElement } from './dom'
9
9
  import { createRenderEffect } from './effect'
10
+ import { isHydratingActive, withHydrationRange } from './hydration'
10
11
  import {
11
12
  createRootContext,
12
13
  destroyRoot,
@@ -18,6 +19,7 @@ import {
18
19
  } from './lifecycle'
19
20
  import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
20
21
  import reconcileArrays from './reconcile'
22
+ import { __fictIsHydrating, __fictIsSSR } from './resume'
21
23
  import { batch } from './scheduler'
22
24
  import { createSignal, effectScope, flush, setActiveSub, type Signal } from './signal'
23
25
  import type { FictNode } from './types'
@@ -30,6 +32,9 @@ const isDev =
30
32
  ? __DEV__
31
33
  : typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
32
34
 
35
+ const isShadowRoot = (node: Node): node is ShadowRoot =>
36
+ typeof ShadowRoot !== 'undefined' && node instanceof ShadowRoot
37
+
33
38
  // ============================================================================
34
39
  // Types
35
40
  // ============================================================================
@@ -85,7 +90,7 @@ interface KeyedListContainer<T = unknown> {
85
90
  */
86
91
  export interface KeyedListBinding {
87
92
  /** Document fragment placeholder inserted by the compiler/runtime */
88
- marker: DocumentFragment
93
+ marker: Comment | DocumentFragment
89
94
  /** Start marker comment node */
90
95
  startMarker: Comment
91
96
  /** End marker comment node */
@@ -195,9 +200,12 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
195
200
  *
196
201
  * @returns Container object with markers, blocks map, and dispose function
197
202
  */
198
- function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
199
- const startMarker = document.createComment('fict:list:start')
200
- const endMarker = document.createComment('fict:list:end')
203
+ function createKeyedListContainer<T = unknown>(
204
+ startOverride?: Comment,
205
+ endOverride?: Comment,
206
+ ): KeyedListContainer<T> {
207
+ const startMarker = startOverride ?? document.createComment('fict:list:start')
208
+ const endMarker = endOverride ?? document.createComment('fict:list:end')
201
209
 
202
210
  const dispose = () => {
203
211
  // Clean up all blocks
@@ -469,10 +477,19 @@ export function createKeyedList<T>(
469
477
  keyFn: (item: T, index: number) => string | number,
470
478
  renderItem: FineGrainedRenderItem<T>,
471
479
  needsIndex?: boolean,
480
+ startMarker?: Comment,
481
+ endMarker?: Comment,
472
482
  ): KeyedListBinding {
473
483
  const resolvedNeedsIndex =
474
484
  arguments.length >= 4 ? !!needsIndex : renderItem.length > 1 /* has index param */
475
- return createFineGrainedKeyedList(getItems, keyFn, renderItem, resolvedNeedsIndex)
485
+ return createFineGrainedKeyedList(
486
+ getItems,
487
+ keyFn,
488
+ renderItem,
489
+ resolvedNeedsIndex,
490
+ startMarker,
491
+ endMarker,
492
+ )
476
493
  }
477
494
 
478
495
  function createFineGrainedKeyedList<T>(
@@ -480,16 +497,32 @@ function createFineGrainedKeyedList<T>(
480
497
  keyFn: (item: T, index: number) => string | number,
481
498
  renderItem: FineGrainedRenderItem<T>,
482
499
  needsIndex: boolean,
500
+ startOverride?: Comment,
501
+ endOverride?: Comment,
483
502
  ): KeyedListBinding {
484
- const container = createKeyedListContainer<T>()
503
+ const container = createKeyedListContainer<T>(startOverride, endOverride)
485
504
  const hostRoot = getCurrentRoot()
486
- const fragment = document.createDocumentFragment()
487
- fragment.append(container.startMarker, container.endMarker)
505
+ const useProvided = !!(startOverride && endOverride)
506
+ const fragment = useProvided ? container.startMarker : document.createDocumentFragment()
507
+ if (!useProvided) {
508
+ ;(fragment as DocumentFragment).append(container.startMarker, container.endMarker)
509
+ }
488
510
  let disposed = false
489
511
  let effectDispose: (() => void) | undefined
490
512
  let connectObserver: MutationObserver | null = null
491
513
  let effectStarted = false
492
514
  let startScheduled = false
515
+ let initialHydrating = __fictIsHydrating()
516
+
517
+ const collectBetween = (): Node[] => {
518
+ const nodes: Node[] = []
519
+ let cursor = container.startMarker.nextSibling
520
+ while (cursor && cursor !== container.endMarker) {
521
+ nodes.push(cursor)
522
+ cursor = cursor.nextSibling
523
+ }
524
+ return nodes
525
+ }
493
526
 
494
527
  const getConnectedParent = (): (ParentNode & Node) | null => {
495
528
  const endParent = container.endMarker.parentNode
@@ -504,12 +537,22 @@ function createFineGrainedKeyedList<T>(
504
537
  if ('isConnected' in parentNode && !parentNode.isConnected) return null
505
538
  return parentNode
506
539
  }
540
+ if (endParent && startParent && endParent === startParent && isShadowRoot(endParent as Node)) {
541
+ const shadowRoot = endParent as ShadowRoot
542
+ const host = shadowRoot.host
543
+ if ('isConnected' in host && !host.isConnected) return null
544
+ return shadowRoot as unknown as ParentNode & Node
545
+ }
507
546
  return null
508
547
  }
509
548
 
510
549
  const performDiff = () => {
511
550
  if (disposed) return
512
- const parent = getConnectedParent()
551
+ // During SSR, render synchronously without waiting for DOM connection
552
+ const isSSR = __fictIsSSR()
553
+ const parent = isSSR
554
+ ? (container.startMarker.parentNode as (ParentNode & Node) | null)
555
+ : getConnectedParent()
513
556
  if (!parent) return
514
557
  batch(() => {
515
558
  const oldBlocks = container.blocks
@@ -519,6 +562,69 @@ function createFineGrainedKeyedList<T>(
519
562
  const orderedIndexByKey = container.orderedIndexByKey
520
563
  const newItems = getItems()
521
564
 
565
+ if (initialHydrating && isHydratingActive()) {
566
+ initialHydrating = false
567
+ newBlocks.clear()
568
+ nextOrderedBlocks.length = 0
569
+ orderedIndexByKey.clear()
570
+
571
+ if (newItems.length === 0) {
572
+ oldBlocks.clear()
573
+ prevOrderedBlocks.length = 0
574
+ container.currentNodes = [container.startMarker, container.endMarker]
575
+ container.nextNodes.length = 0
576
+ return
577
+ }
578
+
579
+ const createdBlocks: KeyedBlock<T>[] = []
580
+ withHydrationRange(
581
+ container.startMarker.nextSibling,
582
+ container.endMarker,
583
+ parent.ownerDocument ?? document,
584
+ () => {
585
+ for (let index = 0; index < newItems.length; index++) {
586
+ const item = newItems[index]!
587
+ const key = keyFn(item, index)
588
+ if (newBlocks.has(key)) {
589
+ if (isDev) {
590
+ console.warn(
591
+ `[fict] Duplicate key "${String(key)}" detected in list hydration. ` +
592
+ `Each item should have a unique key.`,
593
+ )
594
+ }
595
+ const existing = newBlocks.get(key)
596
+ if (existing) {
597
+ destroyRoot(existing.root)
598
+ removeNodes(existing.nodes)
599
+ }
600
+ }
601
+ const block = createKeyedBlock<T>(key, item, index, renderItem, needsIndex, hostRoot)
602
+ createdBlocks.push(block)
603
+ newBlocks.set(key, block)
604
+ orderedIndexByKey.set(key, nextOrderedBlocks.length)
605
+ nextOrderedBlocks.push(block)
606
+ }
607
+ },
608
+ )
609
+
610
+ container.blocks = newBlocks
611
+ container.nextBlocks = oldBlocks
612
+ container.orderedBlocks = nextOrderedBlocks
613
+ container.nextOrderedBlocks = prevOrderedBlocks
614
+ oldBlocks.clear()
615
+ prevOrderedBlocks.length = 0
616
+ container.currentNodes = [container.startMarker, ...collectBetween(), container.endMarker]
617
+ container.nextNodes.length = 0
618
+
619
+ for (const block of createdBlocks) {
620
+ if (newBlocks.get(block.key) === block) {
621
+ flushOnMount(block.root)
622
+ }
623
+ }
624
+
625
+ return
626
+ }
627
+
522
628
  if (newItems.length === 0) {
523
629
  if (oldBlocks.size > 0) {
524
630
  // Destroy all block roots first
@@ -619,7 +725,7 @@ function createFineGrainedKeyedList<T>(
619
725
  removeNodes(existingBlock.nodes)
620
726
  }
621
727
  // Create new block
622
- block = createKeyedBlock(key, item, index, renderItem, needsIndex, hostRoot)
728
+ block = createKeyedBlock<T>(key, item, index, renderItem, needsIndex, hostRoot)
623
729
  createdBlocks.push(block)
624
730
  }
625
731
 
@@ -803,7 +909,11 @@ function createFineGrainedKeyedList<T>(
803
909
 
804
910
  const ensureEffectStarted = (): boolean => {
805
911
  if (disposed || effectStarted) return effectStarted
806
- const parent = getConnectedParent()
912
+ // During SSR, render synchronously without waiting for DOM connection
913
+ const isSSR = __fictIsSSR()
914
+ const parent = isSSR
915
+ ? (container.startMarker.parentNode as (ParentNode & Node) | null)
916
+ : getConnectedParent()
807
917
  if (!parent) return false
808
918
  const start = () => {
809
919
  effectDispose = createRenderEffect(performDiff)
@@ -824,6 +934,9 @@ function createFineGrainedKeyedList<T>(
824
934
 
825
935
  const waitForConnection = () => {
826
936
  if (connectObserver || typeof MutationObserver === 'undefined') return
937
+ const root = container.startMarker.getRootNode?.() ?? document
938
+ const shadowRoot =
939
+ root && root.nodeType === 11 && isShadowRoot(root as Node) ? (root as ShadowRoot) : null
827
940
  connectObserver = new MutationObserver(() => {
828
941
  if (disposed) return
829
942
  if (getConnectedParent()) {
@@ -834,6 +947,9 @@ function createFineGrainedKeyedList<T>(
834
947
  }
835
948
  })
836
949
  connectObserver.observe(document, { childList: true, subtree: true })
950
+ if (shadowRoot) {
951
+ connectObserver.observe(shadowRoot, { childList: true, subtree: true })
952
+ }
837
953
  }
838
954
 
839
955
  const scheduleStart = () => {