@fictjs/runtime 0.3.0 → 0.5.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 (70) 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-TWELIZRY.js → chunk-5AA7HP4S.js} +5 -3
  8. package/dist/{chunk-TWELIZRY.js.map → chunk-5AA7HP4S.js.map} +1 -1
  9. package/dist/chunk-6SOPF5LZ.cjs +2363 -0
  10. package/dist/chunk-6SOPF5LZ.cjs.map +1 -0
  11. package/dist/{chunk-SO6X7G5S.js → chunk-BQG7VEBY.js} +501 -1880
  12. package/dist/chunk-BQG7VEBY.js.map +1 -0
  13. package/dist/chunk-FKDMDAUR.js +2363 -0
  14. package/dist/chunk-FKDMDAUR.js.map +1 -0
  15. package/dist/{chunk-L4DIV3RC.cjs → chunk-GHUV2FLD.cjs} +9 -7
  16. package/dist/chunk-GHUV2FLD.cjs.map +1 -0
  17. package/dist/{chunk-XLIZJMMJ.js → chunk-KKKYW54Z.js} +8 -6
  18. package/dist/{chunk-XLIZJMMJ.js.map → chunk-KKKYW54Z.js.map} +1 -1
  19. package/dist/{chunk-M2TSXZ4C.cjs → chunk-KYLNC4CD.cjs} +18 -16
  20. package/dist/chunk-KYLNC4CD.cjs.map +1 -0
  21. package/dist/chunk-TKWN42TA.cjs +2259 -0
  22. package/dist/chunk-TKWN42TA.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 +92 -4
  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 +189 -202
  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 +195 -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 +8 -3
  56. package/src/binding.ts +254 -5
  57. package/src/dom.ts +103 -5
  58. package/src/hooks.ts +15 -2
  59. package/src/hydration.ts +75 -0
  60. package/src/internal.ts +34 -2
  61. package/src/list-helpers.ts +113 -12
  62. package/src/loader.ts +437 -0
  63. package/src/node-ops.ts +65 -0
  64. package/src/resume.ts +517 -0
  65. package/src/store.ts +8 -0
  66. package/dist/chunk-ID3WBWNO.cjs +0 -3638
  67. package/dist/chunk-ID3WBWNO.cjs.map +0 -1
  68. package/dist/chunk-L4DIV3RC.cjs.map +0 -1
  69. package/dist/chunk-M2TSXZ4C.cjs.map +0 -1
  70. 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'
@@ -85,7 +87,7 @@ interface KeyedListContainer<T = unknown> {
85
87
  */
86
88
  export interface KeyedListBinding {
87
89
  /** Document fragment placeholder inserted by the compiler/runtime */
88
- marker: DocumentFragment
90
+ marker: Comment | DocumentFragment
89
91
  /** Start marker comment node */
90
92
  startMarker: Comment
91
93
  /** End marker comment node */
@@ -195,9 +197,12 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
195
197
  *
196
198
  * @returns Container object with markers, blocks map, and dispose function
197
199
  */
198
- function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
199
- const startMarker = document.createComment('fict:list:start')
200
- const endMarker = document.createComment('fict:list:end')
200
+ function createKeyedListContainer<T = unknown>(
201
+ startOverride?: Comment,
202
+ endOverride?: Comment,
203
+ ): KeyedListContainer<T> {
204
+ const startMarker = startOverride ?? document.createComment('fict:list:start')
205
+ const endMarker = endOverride ?? document.createComment('fict:list:end')
201
206
 
202
207
  const dispose = () => {
203
208
  // Clean up all blocks
@@ -464,32 +469,57 @@ function reorderByLIS<T>(
464
469
  * @param renderItem - Function that creates DOM nodes for each item
465
470
  * @returns Binding handle with markers and dispose function
466
471
  */
467
- export function createKeyedList<T>(
472
+ export function createKeyedList<T extends object>(
468
473
  getItems: () => T[],
469
474
  keyFn: (item: T, index: number) => string | number,
470
475
  renderItem: FineGrainedRenderItem<T>,
471
476
  needsIndex?: boolean,
477
+ startMarker?: Comment,
478
+ endMarker?: Comment,
472
479
  ): KeyedListBinding {
473
480
  const resolvedNeedsIndex =
474
481
  arguments.length >= 4 ? !!needsIndex : renderItem.length > 1 /* has index param */
475
- return createFineGrainedKeyedList(getItems, keyFn, renderItem, resolvedNeedsIndex)
482
+ return createFineGrainedKeyedList(
483
+ getItems,
484
+ keyFn,
485
+ renderItem,
486
+ resolvedNeedsIndex,
487
+ startMarker,
488
+ endMarker,
489
+ )
476
490
  }
477
491
 
478
- function createFineGrainedKeyedList<T>(
492
+ function createFineGrainedKeyedList<T extends object>(
479
493
  getItems: () => T[],
480
494
  keyFn: (item: T, index: number) => string | number,
481
495
  renderItem: FineGrainedRenderItem<T>,
482
496
  needsIndex: boolean,
497
+ startOverride?: Comment,
498
+ endOverride?: Comment,
483
499
  ): KeyedListBinding {
484
- const container = createKeyedListContainer<T>()
500
+ const container = createKeyedListContainer<T>(startOverride, endOverride)
485
501
  const hostRoot = getCurrentRoot()
486
- const fragment = document.createDocumentFragment()
487
- fragment.append(container.startMarker, container.endMarker)
502
+ const useProvided = !!(startOverride && endOverride)
503
+ const fragment = useProvided ? container.startMarker : document.createDocumentFragment()
504
+ if (!useProvided) {
505
+ ;(fragment as DocumentFragment).append(container.startMarker, container.endMarker)
506
+ }
488
507
  let disposed = false
489
508
  let effectDispose: (() => void) | undefined
490
509
  let connectObserver: MutationObserver | null = null
491
510
  let effectStarted = false
492
511
  let startScheduled = false
512
+ let initialHydrating = __fictIsHydrating()
513
+
514
+ const collectBetween = (): Node[] => {
515
+ const nodes: Node[] = []
516
+ let cursor = container.startMarker.nextSibling
517
+ while (cursor && cursor !== container.endMarker) {
518
+ nodes.push(cursor)
519
+ cursor = cursor.nextSibling
520
+ }
521
+ return nodes
522
+ }
493
523
 
494
524
  const getConnectedParent = (): (ParentNode & Node) | null => {
495
525
  const endParent = container.endMarker.parentNode
@@ -509,7 +539,11 @@ function createFineGrainedKeyedList<T>(
509
539
 
510
540
  const performDiff = () => {
511
541
  if (disposed) return
512
- const parent = getConnectedParent()
542
+ // During SSR, render synchronously without waiting for DOM connection
543
+ const isSSR = __fictIsSSR()
544
+ const parent = isSSR
545
+ ? (container.startMarker.parentNode as (ParentNode & Node) | null)
546
+ : getConnectedParent()
513
547
  if (!parent) return
514
548
  batch(() => {
515
549
  const oldBlocks = container.blocks
@@ -519,6 +553,69 @@ function createFineGrainedKeyedList<T>(
519
553
  const orderedIndexByKey = container.orderedIndexByKey
520
554
  const newItems = getItems()
521
555
 
556
+ if (initialHydrating && isHydratingActive()) {
557
+ initialHydrating = false
558
+ newBlocks.clear()
559
+ nextOrderedBlocks.length = 0
560
+ orderedIndexByKey.clear()
561
+
562
+ if (newItems.length === 0) {
563
+ oldBlocks.clear()
564
+ prevOrderedBlocks.length = 0
565
+ container.currentNodes = [container.startMarker, container.endMarker]
566
+ container.nextNodes.length = 0
567
+ return
568
+ }
569
+
570
+ const createdBlocks: KeyedBlock<T>[] = []
571
+ withHydrationRange(
572
+ container.startMarker.nextSibling,
573
+ container.endMarker,
574
+ parent.ownerDocument ?? document,
575
+ () => {
576
+ for (let index = 0; index < newItems.length; index++) {
577
+ const item = newItems[index]!
578
+ const key = keyFn(item, index)
579
+ if (newBlocks.has(key)) {
580
+ if (isDev) {
581
+ console.warn(
582
+ `[fict] Duplicate key "${String(key)}" detected in list hydration. ` +
583
+ `Each item should have a unique key.`,
584
+ )
585
+ }
586
+ const existing = newBlocks.get(key)
587
+ if (existing) {
588
+ destroyRoot(existing.root)
589
+ removeNodes(existing.nodes)
590
+ }
591
+ }
592
+ const block = createKeyedBlock(key, item, index, renderItem, needsIndex, hostRoot)
593
+ createdBlocks.push(block)
594
+ newBlocks.set(key, block)
595
+ orderedIndexByKey.set(key, nextOrderedBlocks.length)
596
+ nextOrderedBlocks.push(block)
597
+ }
598
+ },
599
+ )
600
+
601
+ container.blocks = newBlocks
602
+ container.nextBlocks = oldBlocks
603
+ container.orderedBlocks = nextOrderedBlocks
604
+ container.nextOrderedBlocks = prevOrderedBlocks
605
+ oldBlocks.clear()
606
+ prevOrderedBlocks.length = 0
607
+ container.currentNodes = [container.startMarker, ...collectBetween(), container.endMarker]
608
+ container.nextNodes.length = 0
609
+
610
+ for (const block of createdBlocks) {
611
+ if (newBlocks.get(block.key) === block) {
612
+ flushOnMount(block.root)
613
+ }
614
+ }
615
+
616
+ return
617
+ }
618
+
522
619
  if (newItems.length === 0) {
523
620
  if (oldBlocks.size > 0) {
524
621
  // Destroy all block roots first
@@ -803,7 +900,11 @@ function createFineGrainedKeyedList<T>(
803
900
 
804
901
  const ensureEffectStarted = (): boolean => {
805
902
  if (disposed || effectStarted) return effectStarted
806
- const parent = getConnectedParent()
903
+ // During SSR, render synchronously without waiting for DOM connection
904
+ const isSSR = __fictIsSSR()
905
+ const parent = isSSR
906
+ ? (container.startMarker.parentNode as (ParentNode & Node) | null)
907
+ : getConnectedParent()
807
908
  if (!parent) return false
808
909
  const start = () => {
809
910
  effectDispose = createRenderEffect(performDiff)