@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.
- package/dist/advanced.cjs +10 -8
- package/dist/advanced.cjs.map +1 -1
- package/dist/advanced.d.cts +4 -3
- package/dist/advanced.d.ts +4 -3
- package/dist/advanced.js +10 -8
- package/dist/advanced.js.map +1 -1
- package/dist/{chunk-TWELIZRY.js → chunk-5AA7HP4S.js} +5 -3
- package/dist/{chunk-TWELIZRY.js.map → chunk-5AA7HP4S.js.map} +1 -1
- package/dist/chunk-6SOPF5LZ.cjs +2363 -0
- package/dist/chunk-6SOPF5LZ.cjs.map +1 -0
- package/dist/{chunk-SO6X7G5S.js → chunk-BQG7VEBY.js} +501 -1880
- package/dist/chunk-BQG7VEBY.js.map +1 -0
- package/dist/chunk-FKDMDAUR.js +2363 -0
- package/dist/chunk-FKDMDAUR.js.map +1 -0
- package/dist/{chunk-L4DIV3RC.cjs → chunk-GHUV2FLD.cjs} +9 -7
- package/dist/chunk-GHUV2FLD.cjs.map +1 -0
- package/dist/{chunk-XLIZJMMJ.js → chunk-KKKYW54Z.js} +8 -6
- package/dist/{chunk-XLIZJMMJ.js.map → chunk-KKKYW54Z.js.map} +1 -1
- package/dist/{chunk-M2TSXZ4C.cjs → chunk-KYLNC4CD.cjs} +18 -16
- package/dist/chunk-KYLNC4CD.cjs.map +1 -0
- package/dist/chunk-TKWN42TA.cjs +2259 -0
- package/dist/chunk-TKWN42TA.cjs.map +1 -0
- package/dist/{context-B25xyQrJ.d.cts → context-CTBE00S_.d.cts} +1 -1
- package/dist/{context-CGdP7_Jb.d.ts → context-lkLhbkFJ.d.ts} +1 -1
- package/dist/{effect-D6kaLM2-.d.cts → effect-BpSNEJJz.d.cts} +7 -67
- package/dist/{effect-D6kaLM2-.d.ts → effect-BpSNEJJz.d.ts} +7 -67
- package/dist/index.cjs +40 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.dev.js +92 -4
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +19 -17
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +189 -202
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +13 -23
- package/dist/internal.d.ts +13 -23
- package/dist/internal.js +195 -208
- package/dist/internal.js.map +1 -1
- package/dist/loader.cjs +280 -0
- package/dist/loader.cjs.map +1 -0
- package/dist/loader.d.cts +57 -0
- package/dist/loader.d.ts +57 -0
- package/dist/loader.js +280 -0
- package/dist/loader.js.map +1 -0
- package/dist/{props-BIfromL0.d.cts → props-XTHYD19o.d.cts} +13 -2
- package/dist/{props-BEgIVMRx.d.ts → props-x-HbI-jX.d.ts} +13 -2
- package/dist/resume-BrAkmSTY.d.cts +79 -0
- package/dist/resume-Dx8_l72o.d.ts +79 -0
- package/dist/{scope-CzNkn587.d.ts → scope-CdbGmsFf.d.ts} +1 -1
- package/dist/{scope-Cx_3CjIZ.d.cts → scope-DfcP9I-A.d.cts} +1 -1
- package/dist/signal-C4ISF17w.d.cts +66 -0
- package/dist/signal-C4ISF17w.d.ts +66 -0
- package/package.json +8 -3
- package/src/binding.ts +254 -5
- package/src/dom.ts +103 -5
- package/src/hooks.ts +15 -2
- package/src/hydration.ts +75 -0
- package/src/internal.ts +34 -2
- package/src/list-helpers.ts +113 -12
- package/src/loader.ts +437 -0
- package/src/node-ops.ts +65 -0
- package/src/resume.ts +517 -0
- package/src/store.ts +8 -0
- package/dist/chunk-ID3WBWNO.cjs +0 -3638
- package/dist/chunk-ID3WBWNO.cjs.map +0 -1
- package/dist/chunk-L4DIV3RC.cjs.map +0 -1
- package/dist/chunk-M2TSXZ4C.cjs.map +0 -1
- package/dist/chunk-SO6X7G5S.js.map +0 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for creating a signal
|
|
3
|
+
*/
|
|
4
|
+
interface SignalOptions<T> {
|
|
5
|
+
/** Custom equality check */
|
|
6
|
+
equals?: false | ((prev: T, next: T) => boolean);
|
|
7
|
+
/** Debug name */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** Source location */
|
|
10
|
+
devToolsSource?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Options for creating a memo
|
|
14
|
+
*/
|
|
15
|
+
interface MemoOptions<T> {
|
|
16
|
+
/** Custom equality check */
|
|
17
|
+
equals?: false | ((prev: T, next: T) => boolean);
|
|
18
|
+
/** Debug name */
|
|
19
|
+
name?: string;
|
|
20
|
+
/** Source location */
|
|
21
|
+
devToolsSource?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Signal accessor - function to get/set signal value
|
|
25
|
+
*/
|
|
26
|
+
interface SignalAccessor<T> {
|
|
27
|
+
(): T;
|
|
28
|
+
(value: T): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Computed accessor - function to get computed value
|
|
32
|
+
*/
|
|
33
|
+
type ComputedAccessor<T> = () => T;
|
|
34
|
+
/**
|
|
35
|
+
* Effect scope disposer - function to dispose an effect scope
|
|
36
|
+
*/
|
|
37
|
+
type EffectScopeDisposer = () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Create a reactive signal
|
|
40
|
+
* @param initialValue - The initial value
|
|
41
|
+
* @returns A signal accessor function
|
|
42
|
+
*/
|
|
43
|
+
declare function signal<T>(initialValue: T, options?: SignalOptions<T>): SignalAccessor<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Create a reactive effect scope
|
|
46
|
+
* @param fn - The scope function
|
|
47
|
+
* @returns An effect scope disposer function
|
|
48
|
+
*/
|
|
49
|
+
declare function effectScope(fn: () => void): EffectScopeDisposer;
|
|
50
|
+
/**
|
|
51
|
+
* Reset all global reactive state for test isolation.
|
|
52
|
+
* ONLY use this in test setup/teardown - never in production code.
|
|
53
|
+
* This clears effect queues, resets batch depth, and clears pending flushes.
|
|
54
|
+
*/
|
|
55
|
+
declare function __resetReactiveState(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Create a selector signal that efficiently updates only when the selected key matches.
|
|
58
|
+
* Useful for large lists where only one item is selected.
|
|
59
|
+
*
|
|
60
|
+
* @param source - The source signal returning the current key
|
|
61
|
+
* @param equalityFn - Optional equality function
|
|
62
|
+
* @returns A selector function that takes a key and returns a boolean signal accessor
|
|
63
|
+
*/
|
|
64
|
+
declare function createSelector<T>(source: () => T, equalityFn?: (a: T, b: T) => boolean): (key: T) => boolean;
|
|
65
|
+
|
|
66
|
+
export { type ComputedAccessor as C, type MemoOptions as M, type SignalAccessor as S, __resetReactiveState as _, type SignalOptions as a, createSelector as c, effectScope as e, signal as s };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for creating a signal
|
|
3
|
+
*/
|
|
4
|
+
interface SignalOptions<T> {
|
|
5
|
+
/** Custom equality check */
|
|
6
|
+
equals?: false | ((prev: T, next: T) => boolean);
|
|
7
|
+
/** Debug name */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** Source location */
|
|
10
|
+
devToolsSource?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Options for creating a memo
|
|
14
|
+
*/
|
|
15
|
+
interface MemoOptions<T> {
|
|
16
|
+
/** Custom equality check */
|
|
17
|
+
equals?: false | ((prev: T, next: T) => boolean);
|
|
18
|
+
/** Debug name */
|
|
19
|
+
name?: string;
|
|
20
|
+
/** Source location */
|
|
21
|
+
devToolsSource?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Signal accessor - function to get/set signal value
|
|
25
|
+
*/
|
|
26
|
+
interface SignalAccessor<T> {
|
|
27
|
+
(): T;
|
|
28
|
+
(value: T): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Computed accessor - function to get computed value
|
|
32
|
+
*/
|
|
33
|
+
type ComputedAccessor<T> = () => T;
|
|
34
|
+
/**
|
|
35
|
+
* Effect scope disposer - function to dispose an effect scope
|
|
36
|
+
*/
|
|
37
|
+
type EffectScopeDisposer = () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Create a reactive signal
|
|
40
|
+
* @param initialValue - The initial value
|
|
41
|
+
* @returns A signal accessor function
|
|
42
|
+
*/
|
|
43
|
+
declare function signal<T>(initialValue: T, options?: SignalOptions<T>): SignalAccessor<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Create a reactive effect scope
|
|
46
|
+
* @param fn - The scope function
|
|
47
|
+
* @returns An effect scope disposer function
|
|
48
|
+
*/
|
|
49
|
+
declare function effectScope(fn: () => void): EffectScopeDisposer;
|
|
50
|
+
/**
|
|
51
|
+
* Reset all global reactive state for test isolation.
|
|
52
|
+
* ONLY use this in test setup/teardown - never in production code.
|
|
53
|
+
* This clears effect queues, resets batch depth, and clears pending flushes.
|
|
54
|
+
*/
|
|
55
|
+
declare function __resetReactiveState(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Create a selector signal that efficiently updates only when the selected key matches.
|
|
58
|
+
* Useful for large lists where only one item is selected.
|
|
59
|
+
*
|
|
60
|
+
* @param source - The source signal returning the current key
|
|
61
|
+
* @param equalityFn - Optional equality function
|
|
62
|
+
* @returns A selector function that takes a key and returns a boolean signal accessor
|
|
63
|
+
*/
|
|
64
|
+
declare function createSelector<T>(source: () => T, equalityFn?: (a: T, b: T) => boolean): (key: T) => boolean;
|
|
65
|
+
|
|
66
|
+
export { type ComputedAccessor as C, type MemoOptions as M, type SignalAccessor as S, __resetReactiveState as _, type SignalOptions as a, createSelector as c, effectScope as e, signal as s };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fictjs/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Fict reactive runtime",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
"signals",
|
|
13
13
|
"ui"
|
|
14
14
|
],
|
|
15
|
-
"author": "Michael Lin",
|
|
16
|
-
"license": "MIT",
|
|
17
15
|
"repository": {
|
|
18
16
|
"type": "git",
|
|
19
17
|
"url": "https://github.com/fictjs/fict.git",
|
|
@@ -40,6 +38,11 @@
|
|
|
40
38
|
"import": "./dist/advanced.js",
|
|
41
39
|
"require": "./dist/advanced.cjs"
|
|
42
40
|
},
|
|
41
|
+
"./loader": {
|
|
42
|
+
"types": "./dist/loader.d.ts",
|
|
43
|
+
"import": "./dist/loader.js",
|
|
44
|
+
"require": "./dist/loader.cjs"
|
|
45
|
+
},
|
|
43
46
|
"./jsx-runtime": {
|
|
44
47
|
"types": "./dist/jsx-runtime.d.ts",
|
|
45
48
|
"import": "./dist/jsx-runtime.js",
|
|
@@ -59,6 +62,8 @@
|
|
|
59
62
|
"jsdom": "^27.4.0",
|
|
60
63
|
"tsup": "^8.5.1"
|
|
61
64
|
},
|
|
65
|
+
"author": "unadlib",
|
|
66
|
+
"license": "MIT",
|
|
62
67
|
"scripts": {
|
|
63
68
|
"build": "tsup",
|
|
64
69
|
"dev": "tsup --watch",
|
package/src/binding.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
SVGNamespace,
|
|
22
22
|
} from './constants'
|
|
23
23
|
import { createRenderEffect } from './effect'
|
|
24
|
+
import { withHydrationRange, isHydratingActive } from './hydration'
|
|
24
25
|
import { Fragment } from './jsx'
|
|
25
26
|
import {
|
|
26
27
|
createRootContext,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
type RootContext,
|
|
36
37
|
} from './lifecycle'
|
|
37
38
|
import { toNodeArray, removeNodes, insertNodesBefore } from './node-ops'
|
|
39
|
+
import { __fictIsHydrating } from './resume'
|
|
38
40
|
import { batch } from './scheduler'
|
|
39
41
|
import { computed, untrack, isSignal, isComputed, isEffect, isEffectScope } from './signal'
|
|
40
42
|
import type { Cleanup, FictNode } from './types'
|
|
@@ -716,6 +718,180 @@ export function insert(
|
|
|
716
718
|
}
|
|
717
719
|
}
|
|
718
720
|
|
|
721
|
+
/**
|
|
722
|
+
* Insert reactive content between two marker comments.
|
|
723
|
+
* Supports hydration by claiming existing nodes between markers.
|
|
724
|
+
*/
|
|
725
|
+
export function insertBetween(
|
|
726
|
+
start: Comment,
|
|
727
|
+
end: Comment,
|
|
728
|
+
getValue: () => FictNode,
|
|
729
|
+
createElementFn?: CreateElementFn,
|
|
730
|
+
): Cleanup {
|
|
731
|
+
const hostRoot = getCurrentRoot()
|
|
732
|
+
let currentNodes: Node[] = []
|
|
733
|
+
let currentText: Text | null = null
|
|
734
|
+
let currentRoot: RootContext | null = null
|
|
735
|
+
let initialHydrating = __fictIsHydrating()
|
|
736
|
+
|
|
737
|
+
const collectBetween = (): Node[] => {
|
|
738
|
+
const nodes: Node[] = []
|
|
739
|
+
let cursor = start.nextSibling
|
|
740
|
+
while (cursor && cursor !== end) {
|
|
741
|
+
nodes.push(cursor)
|
|
742
|
+
cursor = cursor.nextSibling
|
|
743
|
+
}
|
|
744
|
+
return nodes
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const clearCurrentNodes = () => {
|
|
748
|
+
if (currentNodes.length > 0) {
|
|
749
|
+
removeNodes(currentNodes)
|
|
750
|
+
currentNodes = []
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const setTextNode = (textValue: string, shouldInsert: boolean) => {
|
|
755
|
+
if (!currentText) {
|
|
756
|
+
currentText = document.createTextNode(textValue)
|
|
757
|
+
} else if (currentText.data !== textValue) {
|
|
758
|
+
currentText.data = textValue
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (!shouldInsert) {
|
|
762
|
+
clearCurrentNodes()
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (currentNodes.length === 1 && currentNodes[0] === currentText) {
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
clearCurrentNodes()
|
|
771
|
+
const parentNode = start.parentNode as (ParentNode & Node) | null
|
|
772
|
+
if (parentNode) {
|
|
773
|
+
insertNodesBefore(parentNode, [currentText], end)
|
|
774
|
+
currentNodes = [currentText]
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const dispose = createRenderEffect(() => {
|
|
779
|
+
const value = getValue()
|
|
780
|
+
const parentNode = start.parentNode as (ParentNode & Node) | null
|
|
781
|
+
const isPrimitive =
|
|
782
|
+
value == null ||
|
|
783
|
+
value === false ||
|
|
784
|
+
typeof value === 'string' ||
|
|
785
|
+
typeof value === 'number' ||
|
|
786
|
+
typeof value === 'boolean'
|
|
787
|
+
|
|
788
|
+
if (isPrimitive) {
|
|
789
|
+
if (initialHydrating && isHydratingActive() && parentNode) {
|
|
790
|
+
const existing = collectBetween()
|
|
791
|
+
if (existing.length > 0) {
|
|
792
|
+
currentNodes = existing
|
|
793
|
+
const only = existing.length === 1 ? existing[0] : null
|
|
794
|
+
currentText = only && only.nodeType === 3 ? (only as Text) : null
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (currentRoot) {
|
|
798
|
+
destroyRoot(currentRoot)
|
|
799
|
+
currentRoot = null
|
|
800
|
+
}
|
|
801
|
+
if (!parentNode) {
|
|
802
|
+
clearCurrentNodes()
|
|
803
|
+
return
|
|
804
|
+
}
|
|
805
|
+
const textValue = value == null || value === false ? '' : String(value)
|
|
806
|
+
const shouldInsert = value != null && value !== false
|
|
807
|
+
setTextNode(textValue, shouldInsert)
|
|
808
|
+
initialHydrating = false
|
|
809
|
+
return
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (currentRoot) {
|
|
813
|
+
destroyRoot(currentRoot)
|
|
814
|
+
currentRoot = null
|
|
815
|
+
}
|
|
816
|
+
clearCurrentNodes()
|
|
817
|
+
|
|
818
|
+
const root = createRootContext(hostRoot)
|
|
819
|
+
const prev = pushRoot(root)
|
|
820
|
+
let nodes: Node[] = []
|
|
821
|
+
let handledError = false
|
|
822
|
+
try {
|
|
823
|
+
let newNode: Node | Node[] = undefined as unknown as Node | Node[]
|
|
824
|
+
const createValue = () => {
|
|
825
|
+
if (value instanceof Node) {
|
|
826
|
+
return value
|
|
827
|
+
}
|
|
828
|
+
if (Array.isArray(value)) {
|
|
829
|
+
if (value.every(v => v instanceof Node)) {
|
|
830
|
+
return value as Node[]
|
|
831
|
+
}
|
|
832
|
+
if (createElementFn) {
|
|
833
|
+
const mapped: Node[] = []
|
|
834
|
+
for (const item of value) {
|
|
835
|
+
mapped.push(...toNodeArray(createElementFn(item as any)))
|
|
836
|
+
}
|
|
837
|
+
return mapped
|
|
838
|
+
}
|
|
839
|
+
return document.createTextNode(String(value))
|
|
840
|
+
}
|
|
841
|
+
return createElementFn ? createElementFn(value) : document.createTextNode(String(value))
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (initialHydrating && isHydratingActive() && parentNode) {
|
|
845
|
+
withHydrationRange(start.nextSibling, end, parentNode.ownerDocument ?? document, () => {
|
|
846
|
+
newNode = createValue()
|
|
847
|
+
})
|
|
848
|
+
} else {
|
|
849
|
+
newNode = createValue()
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
nodes = toNodeArray(newNode)
|
|
853
|
+
if (root.suspended) {
|
|
854
|
+
handledError = true
|
|
855
|
+
destroyRoot(root)
|
|
856
|
+
return
|
|
857
|
+
}
|
|
858
|
+
if (parentNode && !initialHydrating) {
|
|
859
|
+
insertNodesBefore(parentNode, nodes, end)
|
|
860
|
+
}
|
|
861
|
+
} catch (err) {
|
|
862
|
+
if (handleSuspend(err as any, root)) {
|
|
863
|
+
handledError = true
|
|
864
|
+
destroyRoot(root)
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
if (handleError(err, { source: 'renderChild' }, root)) {
|
|
868
|
+
handledError = true
|
|
869
|
+
destroyRoot(root)
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
throw err
|
|
873
|
+
} finally {
|
|
874
|
+
popRoot(prev)
|
|
875
|
+
if (!handledError) {
|
|
876
|
+
flushOnMount(root)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
currentRoot = root
|
|
881
|
+
currentNodes = initialHydrating ? collectBetween() : nodes
|
|
882
|
+
initialHydrating = false
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
return () => {
|
|
886
|
+
dispose()
|
|
887
|
+
if (currentRoot) {
|
|
888
|
+
destroyRoot(currentRoot)
|
|
889
|
+
currentRoot = null
|
|
890
|
+
}
|
|
891
|
+
clearCurrentNodes()
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
719
895
|
/**
|
|
720
896
|
* Create a reactive child binding that updates when the child value changes.
|
|
721
897
|
* This is used for dynamic expressions like `{show && <Modal />}` or `{items.map(...)}`.
|
|
@@ -1299,7 +1475,11 @@ function assignProp(
|
|
|
1299
1475
|
// Event handling: on:eventname
|
|
1300
1476
|
if (prop.slice(0, 3) === 'on:') {
|
|
1301
1477
|
const eventName = prop.slice(3)
|
|
1302
|
-
if (
|
|
1478
|
+
if (typeof value === 'string') {
|
|
1479
|
+
node.setAttribute(prop, value)
|
|
1480
|
+
return value
|
|
1481
|
+
}
|
|
1482
|
+
if (prev && typeof prev !== 'string') node.removeEventListener(eventName, prev as EventListener)
|
|
1303
1483
|
if (value) node.addEventListener(eventName, value as EventListener)
|
|
1304
1484
|
return value
|
|
1305
1485
|
}
|
|
@@ -1433,17 +1613,33 @@ export function createConditional(
|
|
|
1433
1613
|
renderTrue: () => FictNode,
|
|
1434
1614
|
createElementFn: CreateElementFn,
|
|
1435
1615
|
renderFalse?: () => FictNode,
|
|
1616
|
+
startOverride?: Comment,
|
|
1617
|
+
endOverride?: Comment,
|
|
1436
1618
|
): BindingHandle {
|
|
1437
|
-
const
|
|
1438
|
-
const
|
|
1439
|
-
const
|
|
1440
|
-
fragment.
|
|
1619
|
+
const useProvided = !!(startOverride && endOverride)
|
|
1620
|
+
const startMarker = useProvided ? startOverride! : document.createComment('fict:cond:start')
|
|
1621
|
+
const endMarker = useProvided ? endOverride! : document.createComment('fict:cond:end')
|
|
1622
|
+
const fragment = useProvided ? startMarker : document.createDocumentFragment()
|
|
1623
|
+
if (!useProvided) {
|
|
1624
|
+
;(fragment as DocumentFragment).append(startMarker, endMarker)
|
|
1625
|
+
}
|
|
1441
1626
|
const hostRoot = getCurrentRoot()
|
|
1442
1627
|
|
|
1443
1628
|
let currentNodes: Node[] = []
|
|
1444
1629
|
let currentRoot: RootContext | null = null
|
|
1445
1630
|
let lastCondition: boolean | undefined = undefined
|
|
1446
1631
|
let pendingRender = false
|
|
1632
|
+
let initialHydrating = __fictIsHydrating()
|
|
1633
|
+
|
|
1634
|
+
const collectBetween = (): Node[] => {
|
|
1635
|
+
const nodes: Node[] = []
|
|
1636
|
+
let cursor = startMarker.nextSibling
|
|
1637
|
+
while (cursor && cursor !== endMarker) {
|
|
1638
|
+
nodes.push(cursor)
|
|
1639
|
+
cursor = cursor.nextSibling
|
|
1640
|
+
}
|
|
1641
|
+
return nodes
|
|
1642
|
+
}
|
|
1447
1643
|
|
|
1448
1644
|
// Use computed to memoize condition value - this prevents the effect from
|
|
1449
1645
|
// re-running when condition dependencies change but the boolean result stays same.
|
|
@@ -1460,6 +1656,59 @@ export function createConditional(
|
|
|
1460
1656
|
}
|
|
1461
1657
|
pendingRender = false
|
|
1462
1658
|
|
|
1659
|
+
if (initialHydrating && isHydratingActive()) {
|
|
1660
|
+
initialHydrating = false
|
|
1661
|
+
lastCondition = cond
|
|
1662
|
+
|
|
1663
|
+
const render = cond ? renderTrue : renderFalse
|
|
1664
|
+
if (!render) {
|
|
1665
|
+
currentNodes = collectBetween()
|
|
1666
|
+
return
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const root = createRootContext(hostRoot)
|
|
1670
|
+
const prev = pushRoot(root)
|
|
1671
|
+
let handledError = false
|
|
1672
|
+
try {
|
|
1673
|
+
// Call render() INSIDE withHydrationRange so that template() and insertBetween
|
|
1674
|
+
// see the correct hydration context for the conditional content
|
|
1675
|
+
withHydrationRange(
|
|
1676
|
+
startMarker.nextSibling,
|
|
1677
|
+
endMarker,
|
|
1678
|
+
parent.ownerDocument ?? document,
|
|
1679
|
+
() => {
|
|
1680
|
+
const output = untrack(render)
|
|
1681
|
+
if (output == null || output === false) {
|
|
1682
|
+
return
|
|
1683
|
+
}
|
|
1684
|
+
createElementFn(output)
|
|
1685
|
+
},
|
|
1686
|
+
)
|
|
1687
|
+
currentNodes = collectBetween()
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
if (handleSuspend(err as any, root)) {
|
|
1690
|
+
handledError = true
|
|
1691
|
+
destroyRoot(root)
|
|
1692
|
+
return
|
|
1693
|
+
}
|
|
1694
|
+
if (handleError(err, { source: 'renderChild' }, root)) {
|
|
1695
|
+
handledError = true
|
|
1696
|
+
destroyRoot(root)
|
|
1697
|
+
return
|
|
1698
|
+
}
|
|
1699
|
+
throw err
|
|
1700
|
+
} finally {
|
|
1701
|
+
popRoot(prev)
|
|
1702
|
+
if (!handledError) {
|
|
1703
|
+
flushOnMount(root)
|
|
1704
|
+
currentRoot = root
|
|
1705
|
+
} else {
|
|
1706
|
+
currentRoot = null
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1463
1712
|
if (lastCondition === cond && currentNodes.length > 0) {
|
|
1464
1713
|
return
|
|
1465
1714
|
}
|
package/src/dom.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { Properties, ChildProperties, getPropAlias, SVGElements, SVGNamespace } from './constants'
|
|
28
28
|
import { getDevtoolsHook } from './devtools'
|
|
29
29
|
import { __fictPushContext, __fictPopContext, __fictGetCurrentComponentId } from './hooks'
|
|
30
|
+
import { claimNodes, isHydratingActive, withHydration } from './hydration'
|
|
30
31
|
import { Fragment } from './jsx'
|
|
31
32
|
import {
|
|
32
33
|
createRootContext,
|
|
@@ -42,6 +43,13 @@ import {
|
|
|
42
43
|
onCleanup,
|
|
43
44
|
} from './lifecycle'
|
|
44
45
|
import { createPropsProxy, unwrapProps } from './props'
|
|
46
|
+
import {
|
|
47
|
+
__fictIsHydrating,
|
|
48
|
+
__fictIsResumable,
|
|
49
|
+
__fictRegisterScope,
|
|
50
|
+
__fictEnterHydration,
|
|
51
|
+
__fictExitHydration,
|
|
52
|
+
} from './resume'
|
|
45
53
|
import { untrack } from './scheduler'
|
|
46
54
|
import type { DOMElement, FictNode, FictVNode, RefObject } from './types'
|
|
47
55
|
|
|
@@ -76,17 +84,25 @@ let nextComponentId = 1
|
|
|
76
84
|
export function render(view: () => FictNode, container: HTMLElement): () => void {
|
|
77
85
|
const root = createRootContext()
|
|
78
86
|
const prev = pushRoot(root)
|
|
79
|
-
let dom: DOMElement
|
|
87
|
+
let dom: DOMElement = undefined as unknown as DOMElement
|
|
80
88
|
try {
|
|
81
89
|
const output = view()
|
|
82
90
|
// createElement must be called within the root context
|
|
83
91
|
// so that child components register their onMount callbacks correctly
|
|
84
|
-
|
|
92
|
+
if (__fictIsHydrating()) {
|
|
93
|
+
withHydration(container, () => {
|
|
94
|
+
dom = createElement(output)
|
|
95
|
+
})
|
|
96
|
+
} else {
|
|
97
|
+
dom = createElement(output)
|
|
98
|
+
}
|
|
85
99
|
} finally {
|
|
86
100
|
popRoot(prev)
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
|
|
103
|
+
if (!__fictIsHydrating()) {
|
|
104
|
+
container.replaceChildren(dom)
|
|
105
|
+
}
|
|
90
106
|
container.setAttribute('data-fict-fine-grained', '1')
|
|
91
107
|
|
|
92
108
|
flushOnMount(root)
|
|
@@ -99,6 +115,42 @@ export function render(view: () => FictNode, container: HTMLElement): () => void
|
|
|
99
115
|
return teardown
|
|
100
116
|
}
|
|
101
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Hydrate a component into an existing DOM container.
|
|
120
|
+
* Unlike render(), this runs the view function INSIDE the hydration context
|
|
121
|
+
* so that template() can claim existing DOM nodes.
|
|
122
|
+
*
|
|
123
|
+
* @param view - A function that returns the view to hydrate
|
|
124
|
+
* @param container - The DOM container with existing SSR content
|
|
125
|
+
* @returns A teardown function to unmount the view
|
|
126
|
+
*/
|
|
127
|
+
export function hydrateComponent(view: () => FictNode, container: HTMLElement): () => void {
|
|
128
|
+
const root = createRootContext()
|
|
129
|
+
const prev = pushRoot(root)
|
|
130
|
+
|
|
131
|
+
// Enable hydration flags for bindings that check __fictIsHydrating()
|
|
132
|
+
__fictEnterHydration()
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Run the view function INSIDE withHydration so template() can claim nodes
|
|
136
|
+
withHydration(container, () => {
|
|
137
|
+
view()
|
|
138
|
+
})
|
|
139
|
+
} finally {
|
|
140
|
+
__fictExitHydration()
|
|
141
|
+
popRoot(prev)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
container.setAttribute('data-fict-fine-grained', '1')
|
|
145
|
+
flushOnMount(root)
|
|
146
|
+
|
|
147
|
+
const teardown = () => {
|
|
148
|
+
destroyRoot(root)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return teardown
|
|
152
|
+
}
|
|
153
|
+
|
|
102
154
|
// ============================================================================
|
|
103
155
|
// Element Creation
|
|
104
156
|
// ============================================================================
|
|
@@ -140,6 +192,15 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
140
192
|
return document.createTextNode('')
|
|
141
193
|
}
|
|
142
194
|
|
|
195
|
+
// Reactive getter function - resolve to actual node
|
|
196
|
+
if (isReactive(node)) {
|
|
197
|
+
const resolved = (node as () => FictNode)()
|
|
198
|
+
if (resolved === node) {
|
|
199
|
+
return document.createTextNode('')
|
|
200
|
+
}
|
|
201
|
+
return createElementWithContext(resolved, namespace)
|
|
202
|
+
}
|
|
203
|
+
|
|
143
204
|
if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
|
|
144
205
|
// Handle BindingHandle (list/conditional bindings, etc)
|
|
145
206
|
if ('marker' in node) {
|
|
@@ -241,6 +302,32 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
241
302
|
})
|
|
242
303
|
onCleanup(() => hook.componentUnmount?.(componentId))
|
|
243
304
|
}
|
|
305
|
+
if (__fictIsResumable() && !__fictIsHydrating()) {
|
|
306
|
+
const content = createElementWithContext(rendered as FictNode, namespace)
|
|
307
|
+
const host =
|
|
308
|
+
namespace === 'svg'
|
|
309
|
+
? document.createElementNS(SVG_NS, 'fict-host')
|
|
310
|
+
: namespace === 'mathml'
|
|
311
|
+
? document.createElementNS(MATHML_NS, 'fict-host')
|
|
312
|
+
: document.createElement('fict-host')
|
|
313
|
+
host.setAttribute('data-fict-host', '')
|
|
314
|
+
if (namespace === null && (host as HTMLElement).style) {
|
|
315
|
+
;(host as HTMLElement).style.display = 'contents'
|
|
316
|
+
}
|
|
317
|
+
const meta = (vnode.type as unknown as { __fictMeta?: { id?: string; resume?: string } })
|
|
318
|
+
.__fictMeta
|
|
319
|
+
const typeKey = (meta?.id ?? vnode.type.name) || 'Anonymous'
|
|
320
|
+
__fictRegisterScope(ctx, host, typeKey, rawProps)
|
|
321
|
+
if (meta?.resume) {
|
|
322
|
+
host.setAttribute('data-fict-h', meta.resume)
|
|
323
|
+
}
|
|
324
|
+
if (content instanceof DocumentFragment) {
|
|
325
|
+
host.append(...Array.from(content.childNodes))
|
|
326
|
+
} else {
|
|
327
|
+
host.appendChild(content)
|
|
328
|
+
}
|
|
329
|
+
return host as DOMElement
|
|
330
|
+
}
|
|
244
331
|
|
|
245
332
|
return createElementWithContext(rendered as FictNode, namespace)
|
|
246
333
|
} catch (err) {
|
|
@@ -359,8 +446,19 @@ export function template(
|
|
|
359
446
|
|
|
360
447
|
// Create the cloning function
|
|
361
448
|
const fn = isImportNode
|
|
362
|
-
? () =>
|
|
363
|
-
|
|
449
|
+
? () =>
|
|
450
|
+
untrack(() => {
|
|
451
|
+
const base = node || (node = create())
|
|
452
|
+
return isHydratingActive()
|
|
453
|
+
? claimNodes(base, () => document.importNode(base, true))
|
|
454
|
+
: document.importNode(base, true)
|
|
455
|
+
})
|
|
456
|
+
: () => {
|
|
457
|
+
const base = node || (node = create())
|
|
458
|
+
return isHydratingActive()
|
|
459
|
+
? claimNodes(base, () => base.cloneNode(true))
|
|
460
|
+
: base.cloneNode(true)
|
|
461
|
+
}
|
|
364
462
|
|
|
365
463
|
// Add cloneNode property for compatibility
|
|
366
464
|
;(fn as { cloneNode?: typeof fn }).cloneNode = fn
|
package/src/hooks.ts
CHANGED
|
@@ -13,15 +13,19 @@ const isDev =
|
|
|
13
13
|
? __DEV__
|
|
14
14
|
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
15
15
|
|
|
16
|
-
interface HookContext {
|
|
16
|
+
export interface HookContext {
|
|
17
17
|
slots: unknown[]
|
|
18
18
|
cursor: number
|
|
19
19
|
rendering?: boolean
|
|
20
20
|
componentId?: number
|
|
21
21
|
parentId?: number
|
|
22
|
+
scopeId?: string
|
|
23
|
+
scopeType?: string
|
|
24
|
+
slotMap?: Record<string, number>
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
const ctxStack: HookContext[] = []
|
|
28
|
+
let preparedContext: HookContext | null = null
|
|
25
29
|
|
|
26
30
|
function assertRenderContext(ctx: HookContext, hookName: string): void {
|
|
27
31
|
if (!ctx.rendering) {
|
|
@@ -54,11 +58,16 @@ export function __fictUseContext(): HookContext {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export function __fictPushContext(): HookContext {
|
|
57
|
-
const ctx: HookContext = { slots: [], cursor: 0 }
|
|
61
|
+
const ctx: HookContext = preparedContext ?? { slots: [], cursor: 0 }
|
|
62
|
+
preparedContext = null
|
|
58
63
|
ctxStack.push(ctx)
|
|
59
64
|
return ctx
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
export function __fictPrepareContext(ctx: HookContext): void {
|
|
68
|
+
preparedContext = ctx
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
export function __fictGetCurrentComponentId(): number | undefined {
|
|
63
72
|
return ctxStack[ctxStack.length - 1]?.componentId
|
|
64
73
|
}
|
|
@@ -86,6 +95,10 @@ export function __fictUseSignal<T>(
|
|
|
86
95
|
if (!ctx.slots[index]) {
|
|
87
96
|
ctx.slots[index] = createSignal(initial, options)
|
|
88
97
|
}
|
|
98
|
+
if (options?.name) {
|
|
99
|
+
if (!ctx.slotMap) ctx.slotMap = {}
|
|
100
|
+
ctx.slotMap[options.name] = index
|
|
101
|
+
}
|
|
89
102
|
return ctx.slots[index] as SignalAccessor<T>
|
|
90
103
|
}
|
|
91
104
|
|