@fictjs/runtime 0.0.15 → 0.2.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 +8 -8
- package/dist/advanced.d.cts +3 -3
- package/dist/advanced.d.ts +3 -3
- package/dist/advanced.js +3 -3
- package/dist/{chunk-GJTYOFMO.cjs → chunk-527QSKFM.cjs} +16 -16
- package/dist/{chunk-GJTYOFMO.cjs.map → chunk-527QSKFM.cjs.map} +1 -1
- package/dist/{chunk-RY4WDS6R.js → chunk-5KXEEQUO.js} +115 -20
- package/dist/chunk-5KXEEQUO.js.map +1 -0
- package/dist/{chunk-624QY53A.cjs → chunk-BSUMPMKX.cjs} +7 -7
- package/dist/chunk-BSUMPMKX.cjs.map +1 -0
- package/dist/{chunk-IUZXKAAY.js → chunk-FG3M7EBL.js} +2 -2
- package/dist/{chunk-PMF6MWEV.cjs → chunk-J74L7UYP.cjs} +128 -33
- package/dist/chunk-J74L7UYP.cjs.map +1 -0
- package/dist/{chunk-F3AIYQB7.js → chunk-QV5GOCR5.js} +3 -3
- package/dist/chunk-QV5GOCR5.js.map +1 -0
- package/dist/{context-B7UYnfzM.d.ts → context-4woHo7-L.d.ts} +1 -1
- package/dist/{context-UXySaqI_.d.cts → context-9gFXOdJl.d.cts} +1 -1
- package/dist/{effect-Auji1rz9.d.cts → effect-ClARNUCc.d.cts} +23 -2
- package/dist/{effect-Auji1rz9.d.ts → effect-ClARNUCc.d.ts} +23 -2
- package/dist/index.cjs +51 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.dev.js +96 -28
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +10 -13
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +44 -35
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +9 -4
- package/dist/internal.d.ts +9 -4
- package/dist/internal.js +12 -3
- package/dist/internal.js.map +1 -1
- package/dist/jsx-runtime.d.cts +671 -0
- package/dist/jsx-runtime.d.ts +671 -0
- package/dist/{props-ES0Ag_Wd.d.ts → props-CBwuh35e.d.cts} +13 -6
- package/dist/{props-CrOMYbLv.d.cts → props-DAyeRPwH.d.ts} +13 -6
- package/dist/{scope-S6eAzBJZ.d.ts → scope-DvgMquEy.d.ts} +1 -1
- package/dist/{scope-DKYzWfTn.d.cts → scope-xmdo6lVU.d.cts} +1 -1
- package/package.json +1 -1
- package/src/binding.ts +62 -8
- package/src/constants.ts +2 -3
- package/src/dev-entry.ts +22 -0
- package/src/effect.ts +9 -2
- package/src/lifecycle.ts +24 -6
- package/src/list-helpers.ts +14 -3
- package/src/props.ts +29 -3
- package/src/scope.ts +1 -1
- package/src/signal.ts +43 -4
- package/src/suspense.ts +17 -13
- package/dist/chunk-624QY53A.cjs.map +0 -1
- package/dist/chunk-F3AIYQB7.js.map +0 -1
- package/dist/chunk-PMF6MWEV.cjs.map +0 -1
- package/dist/chunk-RY4WDS6R.js.map +0 -1
- /package/dist/{chunk-IUZXKAAY.js.map → chunk-FG3M7EBL.js.map} +0 -0
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { C as Cleanup, F as FictNode, D as DOMElement } from './effect-
|
|
2
|
-
|
|
3
|
-
type Memo<T> = () => T;
|
|
4
|
-
declare function createMemo<T>(fn: () => T): Memo<T>;
|
|
1
|
+
import { C as Cleanup, F as FictNode, D as DOMElement } from './effect-ClARNUCc.cjs';
|
|
5
2
|
|
|
6
3
|
type LifecycleFn = () => void | Cleanup;
|
|
4
|
+
interface CreateRootOptions {
|
|
5
|
+
inherit?: boolean;
|
|
6
|
+
}
|
|
7
7
|
declare function onMount(fn: LifecycleFn): void;
|
|
8
8
|
declare function onDestroy(fn: LifecycleFn): void;
|
|
9
9
|
declare function onCleanup(fn: Cleanup): void;
|
|
10
|
-
declare function createRoot<T>(fn: () => T): {
|
|
10
|
+
declare function createRoot<T>(fn: () => T, options?: CreateRootOptions): {
|
|
11
11
|
dispose: () => void;
|
|
12
12
|
value: T;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
type Memo<T> = () => T;
|
|
16
|
+
declare function createMemo<T>(fn: () => T): Memo<T>;
|
|
17
|
+
|
|
15
18
|
declare const Fragment: unique symbol;
|
|
16
19
|
declare namespace JSX {
|
|
17
20
|
type Element = FictNode;
|
|
@@ -741,9 +744,13 @@ declare function mergeProps<T extends Record<string, unknown>>(...sources: (Merg
|
|
|
741
744
|
type PropGetter<T> = (() => T) & {
|
|
742
745
|
__fictProp: true;
|
|
743
746
|
};
|
|
747
|
+
interface PropOptions {
|
|
748
|
+
unwrap?: boolean;
|
|
749
|
+
}
|
|
744
750
|
/**
|
|
745
751
|
* Memoize a prop getter to cache expensive computations.
|
|
746
752
|
* Use when prop expressions involve heavy calculations or you need lazy, reactive props.
|
|
753
|
+
* Set { unwrap: false } to keep nested prop getters as values.
|
|
747
754
|
*
|
|
748
755
|
* @example
|
|
749
756
|
* ```tsx
|
|
@@ -755,6 +762,6 @@ type PropGetter<T> = (() => T) & {
|
|
|
755
762
|
* <Child data={memoizedData} />
|
|
756
763
|
* ```
|
|
757
764
|
*/
|
|
758
|
-
declare function prop<T>(getter: () => T): PropGetter<T>;
|
|
765
|
+
declare function prop<T>(getter: () => T, options?: PropOptions): PropGetter<T>;
|
|
759
766
|
|
|
760
767
|
export { Fragment as F, JSX as J, type Memo as M, __fictProp as _, onDestroy as a, onCleanup as b, createMemo as c, createRoot as d, createElement as e, __fictPropsRest as f, createPropsProxy as g, mergeProps as m, onMount as o, prop as p, render as r, template as t };
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { C as Cleanup, F as FictNode, D as DOMElement } from './effect-
|
|
2
|
-
|
|
3
|
-
type Memo<T> = () => T;
|
|
4
|
-
declare function createMemo<T>(fn: () => T): Memo<T>;
|
|
1
|
+
import { C as Cleanup, F as FictNode, D as DOMElement } from './effect-ClARNUCc.js';
|
|
5
2
|
|
|
6
3
|
type LifecycleFn = () => void | Cleanup;
|
|
4
|
+
interface CreateRootOptions {
|
|
5
|
+
inherit?: boolean;
|
|
6
|
+
}
|
|
7
7
|
declare function onMount(fn: LifecycleFn): void;
|
|
8
8
|
declare function onDestroy(fn: LifecycleFn): void;
|
|
9
9
|
declare function onCleanup(fn: Cleanup): void;
|
|
10
|
-
declare function createRoot<T>(fn: () => T): {
|
|
10
|
+
declare function createRoot<T>(fn: () => T, options?: CreateRootOptions): {
|
|
11
11
|
dispose: () => void;
|
|
12
12
|
value: T;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
type Memo<T> = () => T;
|
|
16
|
+
declare function createMemo<T>(fn: () => T): Memo<T>;
|
|
17
|
+
|
|
15
18
|
declare const Fragment: unique symbol;
|
|
16
19
|
declare namespace JSX {
|
|
17
20
|
type Element = FictNode;
|
|
@@ -741,9 +744,13 @@ declare function mergeProps<T extends Record<string, unknown>>(...sources: (Merg
|
|
|
741
744
|
type PropGetter<T> = (() => T) & {
|
|
742
745
|
__fictProp: true;
|
|
743
746
|
};
|
|
747
|
+
interface PropOptions {
|
|
748
|
+
unwrap?: boolean;
|
|
749
|
+
}
|
|
744
750
|
/**
|
|
745
751
|
* Memoize a prop getter to cache expensive computations.
|
|
746
752
|
* Use when prop expressions involve heavy calculations or you need lazy, reactive props.
|
|
753
|
+
* Set { unwrap: false } to keep nested prop getters as values.
|
|
747
754
|
*
|
|
748
755
|
* @example
|
|
749
756
|
* ```tsx
|
|
@@ -755,6 +762,6 @@ type PropGetter<T> = (() => T) & {
|
|
|
755
762
|
* <Child data={memoizedData} />
|
|
756
763
|
* ```
|
|
757
764
|
*/
|
|
758
|
-
declare function prop<T>(getter: () => T): PropGetter<T>;
|
|
765
|
+
declare function prop<T>(getter: () => T, options?: PropOptions): PropGetter<T>;
|
|
759
766
|
|
|
760
767
|
export { Fragment as F, JSX as J, type Memo as M, __fictProp as _, onDestroy as a, onCleanup as b, createMemo as c, createRoot as d, createElement as e, __fictPropsRest as f, createPropsProxy as g, mergeProps as m, onMount as o, prop as p, render as r, template as t };
|
package/package.json
CHANGED
package/src/binding.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from './lifecycle'
|
|
37
37
|
import { toNodeArray, removeNodes, insertNodesBefore } from './node-ops'
|
|
38
38
|
import { batch } from './scheduler'
|
|
39
|
-
import { computed, untrack } from './signal'
|
|
39
|
+
import { computed, untrack, isSignal, isComputed, isEffect, isEffectScope } from './signal'
|
|
40
40
|
import type { Cleanup, FictNode } from './types'
|
|
41
41
|
|
|
42
42
|
const isDev =
|
|
@@ -70,11 +70,43 @@ export interface BindingHandle {
|
|
|
70
70
|
// ============================================================================
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Check if a value is reactive (a getter function)
|
|
74
|
-
*
|
|
73
|
+
* Check if a value is reactive (a getter function that returns a value).
|
|
74
|
+
*
|
|
75
|
+
* A value is considered reactive if:
|
|
76
|
+
* 1. It's a signal or computed value created by the runtime (marked with Symbol)
|
|
77
|
+
* 2. It's a zero-argument function (getter pattern used by the compiler)
|
|
78
|
+
*
|
|
79
|
+
* NOT considered reactive:
|
|
80
|
+
* - Event handlers (functions that take arguments)
|
|
81
|
+
* - Effect disposers (zero-arg but for cleanup, not value access)
|
|
82
|
+
* - Effect scopes (zero-arg but for scope management)
|
|
83
|
+
*
|
|
84
|
+
* @param value - The value to check
|
|
85
|
+
* @returns true if the value is a reactive getter
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const [count, setCount] = createSignal(0)
|
|
90
|
+
* isReactive(count) // true - signal accessor
|
|
91
|
+
* isReactive(() => 42) // true - getter pattern
|
|
92
|
+
* isReactive((x) => x) // false - takes argument
|
|
93
|
+
* isReactive('hello') // false - not a function
|
|
94
|
+
* isReactive(effectDisposer) // false - effect cleanup function
|
|
95
|
+
* ```
|
|
75
96
|
*/
|
|
76
97
|
export function isReactive(value: unknown): value is () => unknown {
|
|
77
|
-
|
|
98
|
+
if (typeof value !== 'function') return false
|
|
99
|
+
|
|
100
|
+
// Check for runtime-created signals/computed (most reliable)
|
|
101
|
+
if (isSignal(value) || isComputed(value)) return true
|
|
102
|
+
|
|
103
|
+
// Exclude effect disposers and effect scopes - they are zero-arg
|
|
104
|
+
// functions but not reactive getters
|
|
105
|
+
if (isEffect(value) || isEffectScope(value)) return false
|
|
106
|
+
|
|
107
|
+
// Fall back to length check for compiler-generated getters
|
|
108
|
+
// Zero-argument functions are treated as reactive getters
|
|
109
|
+
return value.length === 0
|
|
78
110
|
}
|
|
79
111
|
|
|
80
112
|
/**
|
|
@@ -590,6 +622,7 @@ export function insert(
|
|
|
590
622
|
const root = createRootContext(hostRoot)
|
|
591
623
|
const prev = pushRoot(root)
|
|
592
624
|
let nodes: Node[] = []
|
|
625
|
+
let handledError = false
|
|
593
626
|
try {
|
|
594
627
|
let newNode: Node | Node[]
|
|
595
628
|
|
|
@@ -614,14 +647,34 @@ export function insert(
|
|
|
614
647
|
}
|
|
615
648
|
|
|
616
649
|
nodes = toNodeArray(newNode)
|
|
650
|
+
if (root.suspended) {
|
|
651
|
+
handledError = true
|
|
652
|
+
destroyRoot(root)
|
|
653
|
+
return
|
|
654
|
+
}
|
|
617
655
|
if (parentNode) {
|
|
618
656
|
insertNodesBefore(parentNode, nodes, marker)
|
|
619
657
|
}
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (handleSuspend(err as any, root)) {
|
|
660
|
+
handledError = true
|
|
661
|
+
destroyRoot(root)
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
if (handleError(err, { source: 'renderChild' }, root)) {
|
|
665
|
+
handledError = true
|
|
666
|
+
destroyRoot(root)
|
|
667
|
+
return
|
|
668
|
+
}
|
|
669
|
+
throw err
|
|
620
670
|
} finally {
|
|
621
671
|
popRoot(prev)
|
|
622
|
-
|
|
672
|
+
if (!handledError) {
|
|
673
|
+
flushOnMount(root)
|
|
674
|
+
}
|
|
623
675
|
}
|
|
624
676
|
|
|
677
|
+
// If we reach here, no error was handled (handledError blocks return early)
|
|
625
678
|
currentRoot = root
|
|
626
679
|
currentNodes = nodes
|
|
627
680
|
})
|
|
@@ -952,9 +1005,10 @@ export function bindEvent(
|
|
|
952
1005
|
const rootRef = getCurrentRoot()
|
|
953
1006
|
|
|
954
1007
|
// Optimization: Global Event Delegation
|
|
955
|
-
// If the event is delegatable and no
|
|
1008
|
+
// If the event is delegatable and no options were provided,
|
|
956
1009
|
// we attach the handler to the element property and rely on the global listener.
|
|
957
|
-
|
|
1010
|
+
const shouldDelegate = options == null && DelegatedEvents.has(eventName)
|
|
1011
|
+
if (shouldDelegate) {
|
|
958
1012
|
const key = `$$${eventName}`
|
|
959
1013
|
|
|
960
1014
|
// Ensure global delegation is active for this event
|
|
@@ -1235,7 +1289,7 @@ function assignProp(
|
|
|
1235
1289
|
// Standard event handling: onClick, onInput, etc.
|
|
1236
1290
|
if (prop.slice(0, 2) === 'on') {
|
|
1237
1291
|
const eventName = prop.slice(2).toLowerCase()
|
|
1238
|
-
const shouldDelegate =
|
|
1292
|
+
const shouldDelegate = DelegatedEvents.has(eventName)
|
|
1239
1293
|
if (!shouldDelegate && prev) {
|
|
1240
1294
|
const handler = Array.isArray(prev) ? prev[0] : prev
|
|
1241
1295
|
node.removeEventListener(eventName, handler as EventListener)
|
package/src/constants.ts
CHANGED
|
@@ -263,10 +263,9 @@ export const $$EVENTS = '_$FICT_DELEGATE'
|
|
|
263
263
|
/**
|
|
264
264
|
* Events that should use event delegation for performance
|
|
265
265
|
* These events bubble and are commonly used across many elements
|
|
266
|
+
* Note: This must match the compiler's DelegatedEvents set
|
|
266
267
|
*/
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
export const DelegatedEvents = new Set<string>(delegatedEvents)
|
|
268
|
+
export const DelegatedEvents = new Set<string>(DelegatedEventNames)
|
|
270
269
|
|
|
271
270
|
// ============================================================================
|
|
272
271
|
// SVG Support
|
package/src/dev-entry.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Unified runtime entry for dev/test aliasing.
|
|
2
|
+
// Re-export internal compiler APIs plus public runtime exports so all code
|
|
3
|
+
// shares a single reactive instance.
|
|
4
|
+
export * from './internal'
|
|
5
|
+
export {
|
|
6
|
+
batch,
|
|
7
|
+
createContext,
|
|
8
|
+
createRef,
|
|
9
|
+
createRoot,
|
|
10
|
+
ErrorBoundary,
|
|
11
|
+
hasContext,
|
|
12
|
+
onCleanup,
|
|
13
|
+
onMount,
|
|
14
|
+
render,
|
|
15
|
+
startTransition,
|
|
16
|
+
Suspense,
|
|
17
|
+
createSuspenseToken,
|
|
18
|
+
untrack,
|
|
19
|
+
useContext,
|
|
20
|
+
useDeferredValue,
|
|
21
|
+
useTransition,
|
|
22
|
+
} from './index'
|
package/src/effect.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCurrentRoot,
|
|
3
3
|
handleError,
|
|
4
|
+
handleSuspend,
|
|
4
5
|
registerRootCleanup,
|
|
5
6
|
runCleanupList,
|
|
6
7
|
withEffectCleanups,
|
|
@@ -34,6 +35,9 @@ export function createEffect(fn: Effect): () => void {
|
|
|
34
35
|
bucket.push(maybeCleanup)
|
|
35
36
|
}
|
|
36
37
|
} catch (err) {
|
|
38
|
+
if (handleSuspend(err as any, rootForError)) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
37
41
|
if (handleError(err, { source: 'effect' }, rootForError)) {
|
|
38
42
|
return
|
|
39
43
|
}
|
|
@@ -43,7 +47,7 @@ export function createEffect(fn: Effect): () => void {
|
|
|
43
47
|
cleanups = bucket
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
const disposeEffect = effectWithCleanup(run, doCleanup)
|
|
50
|
+
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError)
|
|
47
51
|
const teardown = () => {
|
|
48
52
|
runCleanupList(cleanups)
|
|
49
53
|
disposeEffect()
|
|
@@ -76,6 +80,9 @@ export function createRenderEffect(fn: Effect): () => void {
|
|
|
76
80
|
cleanup = maybeCleanup
|
|
77
81
|
}
|
|
78
82
|
} catch (err) {
|
|
83
|
+
if (handleSuspend(err as any, rootForError)) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
79
86
|
const handled = handleError(err, { source: 'effect' }, rootForError)
|
|
80
87
|
if (handled) {
|
|
81
88
|
return
|
|
@@ -84,7 +91,7 @@ export function createRenderEffect(fn: Effect): () => void {
|
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
const disposeEffect = effectWithCleanup(run, doCleanup)
|
|
94
|
+
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError)
|
|
88
95
|
const teardown = () => {
|
|
89
96
|
if (cleanup) {
|
|
90
97
|
cleanup()
|
package/src/lifecycle.ts
CHANGED
|
@@ -15,6 +15,11 @@ export interface RootContext {
|
|
|
15
15
|
destroyCallbacks: Cleanup[]
|
|
16
16
|
errorHandlers?: ErrorHandler[]
|
|
17
17
|
suspenseHandlers?: SuspenseHandler[]
|
|
18
|
+
suspended?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CreateRootOptions {
|
|
22
|
+
inherit?: boolean
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
type ErrorHandler = (err: unknown, info?: ErrorInfo) => boolean | void
|
|
@@ -25,8 +30,8 @@ let currentEffectCleanups: Cleanup[] | undefined
|
|
|
25
30
|
const globalErrorHandlers = new WeakMap<RootContext, ErrorHandler[]>()
|
|
26
31
|
const globalSuspenseHandlers = new WeakMap<RootContext, SuspenseHandler[]>()
|
|
27
32
|
|
|
28
|
-
export function createRootContext(parent
|
|
29
|
-
return { parent, cleanups: [], destroyCallbacks: [] }
|
|
33
|
+
export function createRootContext(parent?: RootContext): RootContext {
|
|
34
|
+
return { parent, cleanups: [], destroyCallbacks: [], suspended: false }
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export function pushRoot(root: RootContext): RootContext | undefined {
|
|
@@ -111,8 +116,12 @@ export function destroyRoot(root: RootContext): void {
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
export function createRoot<T>(
|
|
115
|
-
|
|
119
|
+
export function createRoot<T>(
|
|
120
|
+
fn: () => T,
|
|
121
|
+
options?: CreateRootOptions,
|
|
122
|
+
): { dispose: () => void; value: T } {
|
|
123
|
+
const parent = options?.inherit ? currentRoot : undefined
|
|
124
|
+
const root = createRootContext(parent)
|
|
116
125
|
const prev = pushRoot(root)
|
|
117
126
|
let value: T
|
|
118
127
|
try {
|
|
@@ -259,13 +268,18 @@ export function handleSuspend(
|
|
|
259
268
|
startRoot?: RootContext,
|
|
260
269
|
): boolean {
|
|
261
270
|
let root: RootContext | undefined = startRoot ?? currentRoot
|
|
271
|
+
const originRoot = root // Preserve reference to set suspended flag on success
|
|
262
272
|
while (root) {
|
|
263
273
|
const handlers = root.suspenseHandlers
|
|
264
274
|
if (handlers && handlers.length) {
|
|
265
275
|
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
266
276
|
const handler = handlers[i]!
|
|
267
277
|
const handled = handler(token)
|
|
268
|
-
if (handled !== false)
|
|
278
|
+
if (handled !== false) {
|
|
279
|
+
// Only set suspended = true when a handler actually handles the token
|
|
280
|
+
if (originRoot) originRoot.suspended = true
|
|
281
|
+
return true
|
|
282
|
+
}
|
|
269
283
|
}
|
|
270
284
|
}
|
|
271
285
|
root = root.parent
|
|
@@ -280,7 +294,11 @@ export function handleSuspend(
|
|
|
280
294
|
for (let i = globalForRoot.length - 1; i >= 0; i--) {
|
|
281
295
|
const handler = globalForRoot[i]!
|
|
282
296
|
const handled = handler(token)
|
|
283
|
-
if (handled !== false)
|
|
297
|
+
if (handled !== false) {
|
|
298
|
+
// Only set suspended = true when a handler actually handles the token
|
|
299
|
+
if (originRoot) originRoot.suspended = true
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
284
302
|
}
|
|
285
303
|
}
|
|
286
304
|
return false
|
package/src/list-helpers.ts
CHANGED
|
@@ -135,8 +135,17 @@ export function moveNodesBefore(parent: Node, nodes: Node[], anchor: Node | null
|
|
|
135
135
|
try {
|
|
136
136
|
const clone = parent.ownerDocument.importNode(node, true)
|
|
137
137
|
parent.insertBefore(clone, anchor)
|
|
138
|
-
//
|
|
139
|
-
// This
|
|
138
|
+
// Update the nodes array with the clone to maintain correct references.
|
|
139
|
+
// This ensures future operations (like removal or reordering) work correctly.
|
|
140
|
+
nodes[i] = clone
|
|
141
|
+
if (isDev) {
|
|
142
|
+
console.warn(
|
|
143
|
+
`[fict] Node cloning fallback triggered during list reordering. ` +
|
|
144
|
+
`This may indicate cross-document node insertion. ` +
|
|
145
|
+
`The node reference has been updated to the clone.`,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
anchor = clone
|
|
140
149
|
continue
|
|
141
150
|
} catch {
|
|
142
151
|
// Clone fallback failed
|
|
@@ -491,7 +500,9 @@ function createFineGrainedKeyedList<T>(
|
|
|
491
500
|
endParent === startParent &&
|
|
492
501
|
(endParent as Node).nodeType !== 11
|
|
493
502
|
) {
|
|
494
|
-
|
|
503
|
+
const parentNode = endParent as ParentNode & Node
|
|
504
|
+
if ('isConnected' in parentNode && !parentNode.isConnected) return null
|
|
505
|
+
return parentNode
|
|
495
506
|
}
|
|
496
507
|
return null
|
|
497
508
|
}
|
package/src/props.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createMemo } from './memo'
|
|
2
2
|
|
|
3
|
+
const PROP_GETTER_MARKER = Symbol.for('fict:prop-getter')
|
|
3
4
|
const propGetters = new WeakSet<(...args: unknown[]) => unknown>()
|
|
4
5
|
const rawToProxy = new WeakMap<object, object>()
|
|
5
6
|
const proxyToRaw = new WeakMap<object, object>()
|
|
@@ -12,12 +13,21 @@ const proxyToRaw = new WeakMap<object, object>()
|
|
|
12
13
|
export function __fictProp<T>(getter: () => T): () => T {
|
|
13
14
|
if (typeof getter === 'function' && getter.length === 0) {
|
|
14
15
|
propGetters.add(getter)
|
|
16
|
+
if (Object.isExtensible(getter)) {
|
|
17
|
+
try {
|
|
18
|
+
;(getter as (() => T) & { [PROP_GETTER_MARKER]?: boolean })[PROP_GETTER_MARKER] = true
|
|
19
|
+
} catch {
|
|
20
|
+
// Ignore marker failures on non-standard function objects.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
24
|
return getter
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
function isPropGetter(value: unknown): value is () => unknown {
|
|
20
|
-
|
|
28
|
+
if (typeof value !== 'function') return false
|
|
29
|
+
const fn = value as (() => unknown) & { [PROP_GETTER_MARKER]?: boolean }
|
|
30
|
+
return propGetters.has(fn as (...args: unknown[]) => unknown) || fn[PROP_GETTER_MARKER] === true
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export function createPropsProxy<T extends Record<string, unknown>>(props: T): T {
|
|
@@ -189,9 +199,14 @@ export function mergeProps<T extends Record<string, unknown>>(
|
|
|
189
199
|
}
|
|
190
200
|
|
|
191
201
|
export type PropGetter<T> = (() => T) & { __fictProp: true }
|
|
202
|
+
|
|
203
|
+
export interface PropOptions {
|
|
204
|
+
unwrap?: boolean
|
|
205
|
+
}
|
|
192
206
|
/**
|
|
193
207
|
* Memoize a prop getter to cache expensive computations.
|
|
194
208
|
* Use when prop expressions involve heavy calculations or you need lazy, reactive props.
|
|
209
|
+
* Set { unwrap: false } to keep nested prop getters as values.
|
|
195
210
|
*
|
|
196
211
|
* @example
|
|
197
212
|
* ```tsx
|
|
@@ -203,10 +218,21 @@ export type PropGetter<T> = (() => T) & { __fictProp: true }
|
|
|
203
218
|
* <Child data={memoizedData} />
|
|
204
219
|
* ```
|
|
205
220
|
*/
|
|
206
|
-
export function prop<T>(getter: () => T): PropGetter<T> {
|
|
221
|
+
export function prop<T>(getter: () => T, options?: PropOptions): PropGetter<T> {
|
|
207
222
|
if (isPropGetter(getter)) {
|
|
208
223
|
return getter as PropGetter<T>
|
|
209
224
|
}
|
|
225
|
+
// Capture getter to avoid type narrowing from isPropGetter guard
|
|
226
|
+
const fn: () => T = getter
|
|
227
|
+
const unwrap = options?.unwrap !== false
|
|
210
228
|
// Wrap in prop so component props proxy auto-unwraps when passed down.
|
|
211
|
-
return __fictProp(
|
|
229
|
+
return __fictProp(
|
|
230
|
+
createMemo(() => {
|
|
231
|
+
const value = fn()
|
|
232
|
+
if (unwrap && isPropGetter(value)) {
|
|
233
|
+
return (value as () => T)()
|
|
234
|
+
}
|
|
235
|
+
return value
|
|
236
|
+
}),
|
|
237
|
+
) as PropGetter<T>
|
|
212
238
|
}
|
package/src/scope.ts
CHANGED
|
@@ -25,7 +25,7 @@ export function createScope(): ReactiveScope {
|
|
|
25
25
|
|
|
26
26
|
const run = <T>(fn: () => T): T => {
|
|
27
27
|
stop()
|
|
28
|
-
const { dispose: rootDispose, value } = createRoot(fn)
|
|
28
|
+
const { dispose: rootDispose, value } = createRoot(fn, { inherit: true })
|
|
29
29
|
dispose = rootDispose
|
|
30
30
|
return value
|
|
31
31
|
}
|
package/src/signal.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { beginFlushGuard, beforeEffectRunGuard, endFlushGuard } from './cycle-guard'
|
|
2
2
|
import { getDevtoolsHook } from './devtools'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getCurrentRoot,
|
|
5
|
+
handleError,
|
|
6
|
+
handleSuspend,
|
|
7
|
+
registerRootCleanup,
|
|
8
|
+
type RootContext,
|
|
9
|
+
} from './lifecycle'
|
|
4
10
|
|
|
5
11
|
const isDev =
|
|
6
12
|
typeof __DEV__ !== 'undefined'
|
|
@@ -103,6 +109,8 @@ export interface EffectNode extends BaseNode {
|
|
|
103
109
|
depsTail: Link | undefined
|
|
104
110
|
/** Optional cleanup runner to be called before checkDirty */
|
|
105
111
|
runCleanup?: () => void
|
|
112
|
+
/** Root context for error/suspense handling */
|
|
113
|
+
root?: RootContext
|
|
106
114
|
/** Devtools ID */
|
|
107
115
|
__id?: number | undefined
|
|
108
116
|
}
|
|
@@ -808,7 +816,25 @@ function runEffect(e: EffectNode): void {
|
|
|
808
816
|
inCleanup = false
|
|
809
817
|
}
|
|
810
818
|
}
|
|
811
|
-
|
|
819
|
+
let isDirty = false
|
|
820
|
+
try {
|
|
821
|
+
isDirty = checkDirty(e.deps, e)
|
|
822
|
+
} catch (err) {
|
|
823
|
+
if (handleSuspend(err as any, e.root)) {
|
|
824
|
+
if (e.flags !== 0) {
|
|
825
|
+
e.flags = Watching
|
|
826
|
+
}
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
if (handleError(err, { source: 'effect' }, e.root)) {
|
|
830
|
+
if (e.flags !== 0) {
|
|
831
|
+
e.flags = Watching
|
|
832
|
+
}
|
|
833
|
+
return
|
|
834
|
+
}
|
|
835
|
+
throw err
|
|
836
|
+
}
|
|
837
|
+
if (isDirty) {
|
|
812
838
|
++cycle
|
|
813
839
|
effectRunDevtools(e)
|
|
814
840
|
e.depsTail = undefined
|
|
@@ -1031,7 +1057,7 @@ function computedOper<T>(this: ComputedNode<T>): T {
|
|
|
1031
1057
|
* @returns An effect disposer function
|
|
1032
1058
|
*/
|
|
1033
1059
|
export function effect(fn: () => void): EffectDisposer {
|
|
1034
|
-
const e = {
|
|
1060
|
+
const e: EffectNode = {
|
|
1035
1061
|
fn,
|
|
1036
1062
|
subs: undefined,
|
|
1037
1063
|
subsTail: undefined,
|
|
@@ -1040,6 +1066,10 @@ export function effect(fn: () => void): EffectDisposer {
|
|
|
1040
1066
|
flags: WatchingRunning,
|
|
1041
1067
|
__id: undefined as number | undefined,
|
|
1042
1068
|
}
|
|
1069
|
+
const root = getCurrentRoot()
|
|
1070
|
+
if (root) {
|
|
1071
|
+
e.root = root
|
|
1072
|
+
}
|
|
1043
1073
|
|
|
1044
1074
|
registerEffectDevtools(e)
|
|
1045
1075
|
|
|
@@ -1066,9 +1096,14 @@ export function effect(fn: () => void): EffectDisposer {
|
|
|
1066
1096
|
* cleanup functions to access the previous values of signals.
|
|
1067
1097
|
* @param fn - The effect function
|
|
1068
1098
|
* @param cleanupRunner - Function to run cleanups before signal value commit
|
|
1099
|
+
* @param root - Root context for error/suspense handling (defaults to current root)
|
|
1069
1100
|
* @returns An effect disposer function
|
|
1070
1101
|
*/
|
|
1071
|
-
export function effectWithCleanup(
|
|
1102
|
+
export function effectWithCleanup(
|
|
1103
|
+
fn: () => void,
|
|
1104
|
+
cleanupRunner: () => void,
|
|
1105
|
+
root?: RootContext,
|
|
1106
|
+
): EffectDisposer {
|
|
1072
1107
|
const e: EffectNode = {
|
|
1073
1108
|
fn,
|
|
1074
1109
|
subs: undefined,
|
|
@@ -1079,6 +1114,10 @@ export function effectWithCleanup(fn: () => void, cleanupRunner: () => void): Ef
|
|
|
1079
1114
|
runCleanup: cleanupRunner,
|
|
1080
1115
|
__id: undefined as number | undefined,
|
|
1081
1116
|
}
|
|
1117
|
+
const resolvedRoot = root ?? getCurrentRoot()
|
|
1118
|
+
if (resolvedRoot) {
|
|
1119
|
+
e.root = resolvedRoot
|
|
1120
|
+
}
|
|
1082
1121
|
|
|
1083
1122
|
registerEffectDevtools(e)
|
|
1084
1123
|
|
package/src/suspense.ts
CHANGED
|
@@ -60,11 +60,6 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
60
60
|
? (props.fallback as (e?: unknown) => FictNode)(err)
|
|
61
61
|
: props.fallback
|
|
62
62
|
|
|
63
|
-
const switchView = (view: FictNode | null) => {
|
|
64
|
-
currentView(view)
|
|
65
|
-
renderView(view)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
63
|
const renderView = (view: FictNode | null) => {
|
|
69
64
|
if (cleanup) {
|
|
70
65
|
cleanup()
|
|
@@ -88,8 +83,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
88
83
|
// Suspended view: child threw a suspense token and was handled upstream.
|
|
89
84
|
// Avoid replacing existing fallback content; tear down this attempt.
|
|
90
85
|
const suspendedAttempt =
|
|
91
|
-
|
|
92
|
-
nodes.
|
|
86
|
+
root.suspended ||
|
|
87
|
+
(nodes.length > 0 &&
|
|
88
|
+
nodes.every(node => node instanceof Comment && (node as Comment).data === 'fict:suspend'))
|
|
93
89
|
if (suspendedAttempt) {
|
|
94
90
|
popRoot(prev)
|
|
95
91
|
destroyRoot(root)
|
|
@@ -134,7 +130,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
134
130
|
registerSuspenseHandler(token => {
|
|
135
131
|
const tokenEpoch = epoch
|
|
136
132
|
pending(pending() + 1)
|
|
137
|
-
switchView
|
|
133
|
+
// Directly render fallback instead of using switchView to avoid
|
|
134
|
+
// triggering the effect which would cause duplicate renders
|
|
135
|
+
currentView(toFallback())
|
|
136
|
+
renderView(toFallback())
|
|
138
137
|
|
|
139
138
|
const thenable = (token as SuspenseToken).then
|
|
140
139
|
? (token as SuspenseToken)
|
|
@@ -157,7 +156,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
157
156
|
const newPending = Math.max(0, pending() - 1)
|
|
158
157
|
pending(newPending)
|
|
159
158
|
if (newPending === 0) {
|
|
160
|
-
|
|
159
|
+
// Directly render children instead of using switchView
|
|
160
|
+
currentView(props.children ?? null)
|
|
161
|
+
renderView(props.children ?? null)
|
|
161
162
|
onResolveMaybe()
|
|
162
163
|
}
|
|
163
164
|
},
|
|
@@ -180,9 +181,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
180
181
|
return false
|
|
181
182
|
})
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
// Initial render - render children directly
|
|
185
|
+
// Note: This will be called synchronously during component creation.
|
|
186
|
+
// If children suspend, the handler above will be called and switch to fallback.
|
|
187
|
+
renderView(props.children ?? null)
|
|
186
188
|
|
|
187
189
|
if (props.resetKeys !== undefined) {
|
|
188
190
|
const isGetter =
|
|
@@ -195,7 +197,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
195
197
|
prev = next
|
|
196
198
|
epoch++
|
|
197
199
|
pending(0)
|
|
198
|
-
|
|
200
|
+
// Directly render children instead of using switchView
|
|
201
|
+
currentView(props.children ?? null)
|
|
202
|
+
renderView(props.children ?? null)
|
|
199
203
|
}
|
|
200
204
|
})
|
|
201
205
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/fict/fict/packages/runtime/dist/chunk-624QY53A.cjs","../src/scope.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACA;ACOO,SAAS,WAAA,CAAA,EAA6B;AAC3C,EAAA,IAAI,QAAA,EAA+B,IAAA;AAEnC,EAAA,MAAM,KAAA,EAAO,CAAA,EAAA,GAAM;AACjB,IAAA,GAAA,CAAI,OAAA,EAAS;AACX,MAAA,OAAA,CAAQ,CAAA;AACR,MAAA,QAAA,EAAU,IAAA;AAAA,IACZ;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,IAAA,EAAM,CAAI,EAAA,EAAA,GAAmB;AACjC,IAAA,IAAA,CAAK,CAAA;AACL,IAAA,MAAM,EAAE,OAAA,EAAS,WAAA,EAAa,MAAM,EAAA,EAAI,0CAAA,EAAa,CAAA;AACrD,IAAA,QAAA,EAAU,WAAA;AACV,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AAEA,EAAA,mDAAA,IAAwB,CAAA;AACxB,EAAA,OAAO,EAAE,GAAA,EAAK,KAAK,CAAA;AACrB;AAMO,SAAS,UAAA,CAAW,IAAA,EAA8B,EAAA,EAAsB;AAC7E,EAAA,MAAM,MAAA,EAAQ,WAAA,CAAY,CAAA;AAC1B,EAAA,MAAM,SAAA,EAAW,CAAA,EAAA,GAAO,0CAAA,IAAe,EAAA,EAAK,IAAA,CAAuB,EAAA,EAAI,CAAC,CAAC,IAAA;AAEzE,EAAA,4CAAA,CAAa,EAAA,GAAM;AACjB,IAAA,MAAM,QAAA,EAAU,QAAA,CAAS,CAAA;AACzB,IAAA,GAAA,CAAI,OAAA,EAAS;AACX,MAAA,KAAA,CAAM,GAAA,CAAI,EAAE,CAAA;AAAA,IACd,EAAA,KAAO;AACL,MAAA,KAAA,CAAM,IAAA,CAAK,CAAA;AAAA,IACb;AAAA,EACF,CAAC,CAAA;AAED,EAAA,yCAAA,KAAU,CAAM,IAAI,CAAA;AACtB;ADfA;AACA;AACE;AACA;AACF,mEAAC","file":"/home/runner/work/fict/fict/packages/runtime/dist/chunk-624QY53A.cjs","sourcesContent":[null,"import { isReactive, type MaybeReactive } from './binding'\nimport { createEffect } from './effect'\nimport { createRoot, onCleanup, registerRootCleanup } from './lifecycle'\n\nexport { effectScope } from './signal'\n\nexport interface ReactiveScope {\n run<T>(fn: () => T): T\n stop(): void\n}\n\n/**\n * Create an explicit reactive scope that can contain effects/memos and be stopped manually.\n * The scope registers with the current root for cleanup.\n */\nexport function createScope(): ReactiveScope {\n let dispose: (() => void) | null = null\n\n const stop = () => {\n if (dispose) {\n dispose()\n dispose = null\n }\n }\n\n const run = <T>(fn: () => T): T => {\n stop()\n const { dispose: rootDispose, value } = createRoot(fn)\n dispose = rootDispose\n return value\n }\n\n registerRootCleanup(stop)\n return { run, stop }\n}\n\n/**\n * Run a block of reactive code inside a managed scope that follows a boolean flag.\n * When the flag turns false, the scope is disposed and all contained effects/memos are cleaned up.\n */\nexport function runInScope(flag: MaybeReactive<boolean>, fn: () => void): void {\n const scope = createScope()\n const evaluate = () => (isReactive(flag) ? (flag as () => boolean)() : !!flag)\n\n createEffect(() => {\n const enabled = evaluate()\n if (enabled) {\n scope.run(fn)\n } else {\n scope.stop()\n }\n })\n\n onCleanup(scope.stop)\n}\n"]}
|