@fictjs/runtime 0.17.0 → 0.17.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.
- package/dist/advanced.cjs +9 -9
- package/dist/advanced.d.cts +3 -3
- package/dist/advanced.d.ts +3 -3
- package/dist/advanced.js +4 -4
- package/dist/{binding-CQUGLLBI.d.ts → binding-BfzY9rae.d.ts} +2 -2
- package/dist/{binding-BlABuUiG.d.cts → binding-CDR2ERoq.d.cts} +2 -2
- package/dist/{chunk-5FVWBK4M.cjs → chunk-2J4INHDT.cjs} +40 -40
- package/dist/{chunk-5FVWBK4M.cjs.map → chunk-2J4INHDT.cjs.map} +1 -1
- package/dist/{chunk-6DNYVH5U.cjs → chunk-CKKZDUHM.cjs} +21 -18
- package/dist/chunk-CKKZDUHM.cjs.map +1 -0
- package/dist/{chunk-UQTWIV3S.js → chunk-DHRRJJ6W.js} +8 -5
- package/dist/chunk-DHRRJJ6W.js.map +1 -0
- package/dist/{chunk-IIWHTV23.js → chunk-LFLFSJFU.js} +3 -3
- package/dist/{chunk-ECKYFH5Q.cjs → chunk-NBDEMBBX.cjs} +43 -81
- package/dist/chunk-NBDEMBBX.cjs.map +1 -0
- package/dist/{chunk-CFAWL76V.js → chunk-OKPQWORE.js} +43 -81
- package/dist/chunk-OKPQWORE.js.map +1 -0
- package/dist/{chunk-M42N54LG.js → chunk-OLHZBAIF.js} +3 -3
- package/dist/{chunk-F5SDRX4J.js → chunk-R2HYEOP7.js} +470 -172
- package/dist/chunk-R2HYEOP7.js.map +1 -0
- package/dist/{chunk-INYTG4NG.cjs → chunk-UG2IFQOY.cjs} +650 -352
- package/dist/chunk-UG2IFQOY.cjs.map +1 -0
- package/dist/{chunk-WY4LI5PB.cjs → chunk-VP2WC7X3.cjs} +8 -8
- package/dist/{chunk-WY4LI5PB.cjs.map → chunk-VP2WC7X3.cjs.map} +1 -1
- package/dist/{devtools-DWIZRe7L.d.cts → devtools-BwkkQ6DN.d.cts} +1 -1
- package/dist/{devtools-DNnnDGu1.d.ts → devtools-CK3SVU_w.d.ts} +1 -1
- package/dist/index.cjs +55 -42
- 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 +260 -156
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/internal-list.cjs +4 -4
- package/dist/internal-list.js +3 -3
- package/dist/internal.cjs +5 -5
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +4 -4
- package/dist/loader.cjs +18 -18
- package/dist/loader.js +1 -1
- package/dist/{props-C04ScJgm.d.ts → props-CFoQ471Y.d.ts} +1 -1
- package/dist/{props-CdmuXCiu.d.cts → props-D4tK8Gn0.d.cts} +1 -1
- package/dist/{scope-gpOMWTlf.d.ts → scope-BFzD_7hx.d.ts} +1 -1
- package/dist/{scope-GwC4DJ50.d.cts → scope-Ck3mTQVS.d.cts} +1 -1
- package/package.json +1 -1
- package/src/binding.ts +561 -166
- package/src/context.ts +8 -1
- package/src/dom.ts +26 -44
- package/src/effect.ts +9 -12
- package/src/error-boundary.ts +8 -0
- package/src/hydration.ts +25 -6
- package/src/lifecycle.ts +31 -79
- package/src/signal.ts +4 -1
- package/src/suspense.ts +8 -0
- package/dist/chunk-6DNYVH5U.cjs.map +0 -1
- package/dist/chunk-CFAWL76V.js.map +0 -1
- package/dist/chunk-ECKYFH5Q.cjs.map +0 -1
- package/dist/chunk-F5SDRX4J.js.map +0 -1
- package/dist/chunk-INYTG4NG.cjs.map +0 -1
- package/dist/chunk-UQTWIV3S.js.map +0 -1
- /package/dist/{chunk-IIWHTV23.js.map → chunk-LFLFSJFU.js.map} +0 -0
- /package/dist/{chunk-M42N54LG.js.map → chunk-OLHZBAIF.js.map} +0 -0
package/src/context.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
type RootContext,
|
|
57
57
|
} from './lifecycle'
|
|
58
58
|
import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
|
|
59
|
+
import { untrack } from './signal'
|
|
59
60
|
import type { BaseProps, FictNode } from './types'
|
|
60
61
|
|
|
61
62
|
// ============================================================================
|
|
@@ -222,7 +223,13 @@ export function createContext<T>(defaultValue: T): Context<T> {
|
|
|
222
223
|
createRenderEffect(() => {
|
|
223
224
|
// Update context value on re-render (if value prop changes reactively)
|
|
224
225
|
contextMap.set(id, props.value)
|
|
225
|
-
|
|
226
|
+
|
|
227
|
+
// Provider value updates should not subscribe this effect to arbitrary
|
|
228
|
+
// signal reads that happen while rendering descendants. Child trees own
|
|
229
|
+
// their own reactivity; the provider only needs to react to its props.
|
|
230
|
+
untrack(() => {
|
|
231
|
+
renderChildren(props.children)
|
|
232
|
+
})
|
|
226
233
|
})
|
|
227
234
|
|
|
228
235
|
return fragment
|
package/src/dom.ts
CHANGED
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
createClassBinding,
|
|
20
20
|
createChildBinding,
|
|
21
21
|
bindEvent,
|
|
22
|
+
bindRef,
|
|
22
23
|
isReactive,
|
|
24
|
+
registerCreateElement,
|
|
23
25
|
type MaybeReactive,
|
|
24
26
|
type AttributeSetter,
|
|
25
27
|
type BindingHandle,
|
|
@@ -27,7 +29,7 @@ import {
|
|
|
27
29
|
import { Properties, ChildProperties, getPropAlias, SVGElements, SVGNamespace } from './constants'
|
|
28
30
|
import { getDevtoolsHook } from './devtools'
|
|
29
31
|
import { __fictPushContext, __fictPopContext, __fictGetCurrentComponentId } from './hooks'
|
|
30
|
-
import { claimNodes, isHydratingActive, withHydration } from './hydration'
|
|
32
|
+
import { claimNodes, claimText, isHydratingActive, withHydration } from './hydration'
|
|
31
33
|
import { Fragment } from './jsx'
|
|
32
34
|
import {
|
|
33
35
|
createRootContext,
|
|
@@ -51,7 +53,7 @@ import {
|
|
|
51
53
|
__fictExitHydration,
|
|
52
54
|
} from './resume'
|
|
53
55
|
import { untrack } from './scheduler'
|
|
54
|
-
import type { DOMElement, FictNode, FictVNode
|
|
56
|
+
import type { DOMElement, FictNode, FictVNode } from './types'
|
|
55
57
|
|
|
56
58
|
type NamespaceContext = 'svg' | 'mathml' | null
|
|
57
59
|
|
|
@@ -214,6 +216,8 @@ export function createElement(node: FictNode): DOMElement {
|
|
|
214
216
|
return createElementWithContext(node, null, resolveOwnerDocument())
|
|
215
217
|
}
|
|
216
218
|
|
|
219
|
+
registerCreateElement(createElement)
|
|
220
|
+
|
|
217
221
|
function resolveNamespace(tagName: string, namespace: NamespaceContext): NamespaceContext {
|
|
218
222
|
if (tagName === 'svg') return 'svg'
|
|
219
223
|
if (tagName === 'math') return 'mathml'
|
|
@@ -227,6 +231,13 @@ function resolveOwnerDocument(ownerDocument?: Document): Document {
|
|
|
227
231
|
return ownerDocument ?? getCurrentRoot()?.ownerDocument ?? document
|
|
228
232
|
}
|
|
229
233
|
|
|
234
|
+
function createTextNodeWithHydration(value: string, ownerDocument: Document): Text {
|
|
235
|
+
if (!isHydratingActive()) {
|
|
236
|
+
return ownerDocument.createTextNode(value)
|
|
237
|
+
}
|
|
238
|
+
return claimText(value, () => ownerDocument.createTextNode(value))
|
|
239
|
+
}
|
|
240
|
+
|
|
230
241
|
function createElementWithContext(
|
|
231
242
|
node: FictNode,
|
|
232
243
|
namespace: NamespaceContext,
|
|
@@ -239,14 +250,14 @@ function createElementWithContext(
|
|
|
239
250
|
|
|
240
251
|
// Null/undefined/false - empty placeholder
|
|
241
252
|
if (node === null || node === undefined || node === false) {
|
|
242
|
-
return
|
|
253
|
+
return createTextNodeWithHydration('', ownerDocument)
|
|
243
254
|
}
|
|
244
255
|
|
|
245
256
|
// Reactive getter function - resolve to actual node
|
|
246
257
|
if (isReactive(node)) {
|
|
247
258
|
const resolved = (node as () => FictNode)()
|
|
248
259
|
if (resolved === node) {
|
|
249
|
-
return
|
|
260
|
+
return createTextNodeWithHydration('', ownerDocument)
|
|
250
261
|
}
|
|
251
262
|
return createElementWithContext(resolved, namespace, ownerDocument)
|
|
252
263
|
}
|
|
@@ -254,7 +265,7 @@ function createElementWithContext(
|
|
|
254
265
|
// Non-reactive function values are not valid DOM nodes.
|
|
255
266
|
// Keep callback values inert instead of stringifying function source.
|
|
256
267
|
if (typeof node === 'function') {
|
|
257
|
-
return
|
|
268
|
+
return createTextNodeWithHydration('', ownerDocument)
|
|
258
269
|
}
|
|
259
270
|
|
|
260
271
|
if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
|
|
@@ -290,11 +301,11 @@ function createElementWithContext(
|
|
|
290
301
|
|
|
291
302
|
// Primitive values - text node
|
|
292
303
|
if (typeof node === 'string' || typeof node === 'number') {
|
|
293
|
-
return
|
|
304
|
+
return createTextNodeWithHydration(String(node), ownerDocument)
|
|
294
305
|
}
|
|
295
306
|
|
|
296
307
|
if (typeof node === 'boolean') {
|
|
297
|
-
return
|
|
308
|
+
return createTextNodeWithHydration('', ownerDocument)
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
// VNode
|
|
@@ -615,7 +626,7 @@ function appendChildNode(
|
|
|
615
626
|
// Cast to Node for remaining logic
|
|
616
627
|
let domNode: Node
|
|
617
628
|
if (typeof child !== 'object' || child === null) {
|
|
618
|
-
domNode =
|
|
629
|
+
domNode = createTextNodeWithHydration(String(child ?? ''), parentOwnerDocument)
|
|
619
630
|
} else {
|
|
620
631
|
domNode = createElementWithContext(child as any, namespace, parentOwnerDocument) as Node
|
|
621
632
|
}
|
|
@@ -675,43 +686,14 @@ function appendChildren(
|
|
|
675
686
|
* Both types are automatically cleaned up on unmount.
|
|
676
687
|
*/
|
|
677
688
|
function applyRef(el: Element, value: unknown): void {
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const root = getCurrentRoot()
|
|
685
|
-
if (root) {
|
|
686
|
-
registerRootCleanup(() => {
|
|
687
|
-
refFn(null)
|
|
688
|
-
})
|
|
689
|
-
} else if (isDev) {
|
|
690
|
-
console.warn(
|
|
691
|
-
'[fict] Ref applied outside of a root context. ' +
|
|
692
|
-
'The ref cleanup (setting to null) will not run automatically. ' +
|
|
693
|
-
'Consider using createRoot() or ensure the element is created within a component.',
|
|
694
|
-
)
|
|
695
|
-
}
|
|
696
|
-
} else if (value && typeof value === 'object' && 'current' in value) {
|
|
697
|
-
// Object ref
|
|
698
|
-
const refObj = value as RefObject<Element>
|
|
699
|
-
refObj.current = el
|
|
700
|
-
|
|
701
|
-
// Auto-cleanup on unmount
|
|
702
|
-
const root = getCurrentRoot()
|
|
703
|
-
if (root) {
|
|
704
|
-
registerRootCleanup(() => {
|
|
705
|
-
refObj.current = null
|
|
706
|
-
})
|
|
707
|
-
} else if (isDev) {
|
|
708
|
-
console.warn(
|
|
709
|
-
'[fict] Ref applied outside of a root context. ' +
|
|
710
|
-
'The ref cleanup (setting to null) will not run automatically. ' +
|
|
711
|
-
'Consider using createRoot() or ensure the element is created within a component.',
|
|
712
|
-
)
|
|
713
|
-
}
|
|
689
|
+
if (!getCurrentRoot() && isDev) {
|
|
690
|
+
console.warn(
|
|
691
|
+
'[fict] Ref applied outside of a root context. ' +
|
|
692
|
+
'The ref cleanup (setting to null) will not run automatically. ' +
|
|
693
|
+
'Consider using createRoot() or ensure the element is created within a component.',
|
|
694
|
+
)
|
|
714
695
|
}
|
|
696
|
+
bindRef(el, value)
|
|
715
697
|
}
|
|
716
698
|
|
|
717
699
|
// ============================================================================
|
package/src/effect.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function createEffect(fn: Effect, options?: EffectOptions): () => void {
|
|
|
21
21
|
|
|
22
22
|
// Cleanup runner - called by runEffect BEFORE signal values are committed
|
|
23
23
|
const doCleanup = () => {
|
|
24
|
-
runCleanupList(cleanups)
|
|
24
|
+
runCleanupList(cleanups, rootForError)
|
|
25
25
|
cleanups = []
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -49,7 +49,7 @@ export function createEffect(fn: Effect, options?: EffectOptions): () => void {
|
|
|
49
49
|
|
|
50
50
|
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError, options)
|
|
51
51
|
const teardown = () => {
|
|
52
|
-
runCleanupList(cleanups)
|
|
52
|
+
runCleanupList(cleanups, rootForError)
|
|
53
53
|
disposeEffect()
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -61,15 +61,13 @@ export function createEffect(fn: Effect, options?: EffectOptions): () => void {
|
|
|
61
61
|
export const $effect = createEffect
|
|
62
62
|
|
|
63
63
|
export function createRenderEffect(fn: Effect, options?: EffectOptions): () => void {
|
|
64
|
-
let
|
|
64
|
+
let cleanups: Cleanup[] = []
|
|
65
65
|
const rootForError = getCurrentRoot()
|
|
66
66
|
|
|
67
67
|
// Cleanup runner - called by runEffect BEFORE signal values are committed
|
|
68
68
|
const doCleanup = () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
cleanup = undefined
|
|
72
|
-
}
|
|
69
|
+
runCleanupList(cleanups, rootForError)
|
|
70
|
+
cleanups = []
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
const run = () => {
|
|
@@ -77,7 +75,9 @@ export function createRenderEffect(fn: Effect, options?: EffectOptions): () => v
|
|
|
77
75
|
try {
|
|
78
76
|
const maybeCleanup = fn()
|
|
79
77
|
if (typeof maybeCleanup === 'function') {
|
|
80
|
-
|
|
78
|
+
cleanups = [maybeCleanup]
|
|
79
|
+
} else {
|
|
80
|
+
cleanups = []
|
|
81
81
|
}
|
|
82
82
|
} catch (err) {
|
|
83
83
|
if (handleSuspend(err as any, rootForError)) {
|
|
@@ -93,10 +93,7 @@ export function createRenderEffect(fn: Effect, options?: EffectOptions): () => v
|
|
|
93
93
|
|
|
94
94
|
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError, options)
|
|
95
95
|
const teardown = () => {
|
|
96
|
-
|
|
97
|
-
cleanup()
|
|
98
|
-
cleanup = undefined
|
|
99
|
-
}
|
|
96
|
+
runCleanupList(cleanups, rootForError)
|
|
100
97
|
disposeEffect()
|
|
101
98
|
}
|
|
102
99
|
|
package/src/error-boundary.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
pushRoot,
|
|
9
9
|
popRoot,
|
|
10
10
|
registerErrorHandler,
|
|
11
|
+
registerRootCleanup,
|
|
11
12
|
} from './lifecycle'
|
|
12
13
|
import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
|
|
13
14
|
import type { BaseProps, FictNode } from './types'
|
|
@@ -105,6 +106,13 @@ export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
|
|
|
105
106
|
|
|
106
107
|
renderValue(props.children ?? null)
|
|
107
108
|
|
|
109
|
+
registerRootCleanup(() => {
|
|
110
|
+
if (cleanup) {
|
|
111
|
+
cleanup()
|
|
112
|
+
cleanup = undefined
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
108
116
|
registerErrorHandler(err => {
|
|
109
117
|
renderValue(toView(err))
|
|
110
118
|
props.onError?.(err)
|
package/src/hydration.ts
CHANGED
|
@@ -6,7 +6,7 @@ interface HydrationContext {
|
|
|
6
6
|
|
|
7
7
|
const hydrationStack: HydrationContext[] = []
|
|
8
8
|
|
|
9
|
-
export function withHydration(root: ParentNode & Node, fn: () =>
|
|
9
|
+
export function withHydration<T>(root: ParentNode & Node, fn: () => T): T {
|
|
10
10
|
const owner = root.ownerDocument ?? document
|
|
11
11
|
hydrationStack.push({
|
|
12
12
|
cursor: root.firstChild,
|
|
@@ -14,25 +14,25 @@ export function withHydration(root: ParentNode & Node, fn: () => void): void {
|
|
|
14
14
|
owner,
|
|
15
15
|
})
|
|
16
16
|
try {
|
|
17
|
-
fn()
|
|
17
|
+
return fn()
|
|
18
18
|
} finally {
|
|
19
19
|
hydrationStack.pop()
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function withHydrationRange(
|
|
23
|
+
export function withHydrationRange<T>(
|
|
24
24
|
start: Node | null,
|
|
25
25
|
end: Node | null,
|
|
26
26
|
owner: Document,
|
|
27
|
-
fn: () =>
|
|
28
|
-
):
|
|
27
|
+
fn: () => T,
|
|
28
|
+
): T {
|
|
29
29
|
hydrationStack.push({
|
|
30
30
|
cursor: start,
|
|
31
31
|
boundary: end,
|
|
32
32
|
owner,
|
|
33
33
|
})
|
|
34
34
|
try {
|
|
35
|
-
fn()
|
|
35
|
+
return fn()
|
|
36
36
|
} finally {
|
|
37
37
|
hydrationStack.pop()
|
|
38
38
|
}
|
|
@@ -70,6 +70,25 @@ export function claimNodes(templateRoot: Node, fallback: () => Node): Node {
|
|
|
70
70
|
return frag
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export function claimText(value: string, fallback: () => Text): Text {
|
|
74
|
+
const ctx = hydrationStack[hydrationStack.length - 1]
|
|
75
|
+
if (
|
|
76
|
+
!ctx ||
|
|
77
|
+
!ctx.cursor ||
|
|
78
|
+
ctx.cursor === ctx.boundary ||
|
|
79
|
+
ctx.cursor.nodeType !== Node.TEXT_NODE
|
|
80
|
+
) {
|
|
81
|
+
return fallback()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const text = ctx.cursor as Text
|
|
85
|
+
ctx.cursor = text.nextSibling
|
|
86
|
+
if (text.data !== value) {
|
|
87
|
+
text.data = value
|
|
88
|
+
}
|
|
89
|
+
return text
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
export function isHydratingActive(): boolean {
|
|
74
93
|
return hydrationStack.length > 0
|
|
75
94
|
}
|
package/src/lifecycle.ts
CHANGED
|
@@ -29,8 +29,6 @@ type SuspenseHandler = (token: SuspenseToken | PromiseLike<unknown>) => boolean
|
|
|
29
29
|
|
|
30
30
|
let currentRoot: RootContext | undefined
|
|
31
31
|
let currentEffectCleanups: Cleanup[] | undefined
|
|
32
|
-
const globalErrorHandlers = new WeakMap<RootContext, ErrorHandler[]>()
|
|
33
|
-
const globalSuspenseHandlers = new WeakMap<RootContext, SuspenseHandler[]>()
|
|
34
32
|
const rootDevtoolsIds = new WeakMap<RootContext, number>()
|
|
35
33
|
let nextRootDevtoolsId = 0
|
|
36
34
|
|
|
@@ -115,20 +113,28 @@ export function onCleanup(fn: Cleanup): void {
|
|
|
115
113
|
export function flushOnMount(root: RootContext): void {
|
|
116
114
|
const cbs = root.onMountCallbacks
|
|
117
115
|
if (!cbs || cbs.length === 0) return
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
try {
|
|
117
|
+
withRootContext(root, () => {
|
|
118
|
+
for (let i = 0; i < cbs.length; i++) {
|
|
119
|
+
const cleanup = cbs[i]!()
|
|
120
|
+
if (typeof cleanup === 'function') {
|
|
121
|
+
root.cleanups.push(cleanup)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
} finally {
|
|
126
|
+
cbs.length = 0
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function withRootContext<T>(root: RootContext | undefined, fn: () => T): T {
|
|
131
|
+
if (!root) return fn()
|
|
120
132
|
const prevRoot = currentRoot
|
|
121
133
|
currentRoot = root
|
|
122
134
|
try {
|
|
123
|
-
|
|
124
|
-
const cleanup = cbs[i]!()
|
|
125
|
-
if (typeof cleanup === 'function') {
|
|
126
|
-
root.cleanups.push(cleanup)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
135
|
+
return fn()
|
|
129
136
|
} finally {
|
|
130
137
|
currentRoot = prevRoot
|
|
131
|
-
cbs.length = 0
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
|
|
@@ -139,7 +145,7 @@ export function registerRootCleanup(fn: Cleanup): void {
|
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
export function clearRoot(root: RootContext): void {
|
|
142
|
-
runCleanupList(root.cleanups)
|
|
148
|
+
runCleanupList(root.cleanups, root)
|
|
143
149
|
if (root.onMountCallbacks) {
|
|
144
150
|
root.onMountCallbacks.length = 0
|
|
145
151
|
}
|
|
@@ -147,19 +153,13 @@ export function clearRoot(root: RootContext): void {
|
|
|
147
153
|
|
|
148
154
|
export function destroyRoot(root: RootContext): void {
|
|
149
155
|
clearRoot(root)
|
|
150
|
-
runCleanupList(root.destroyCallbacks)
|
|
156
|
+
runCleanupList(root.destroyCallbacks, root)
|
|
151
157
|
if (root.errorHandlers) {
|
|
152
158
|
root.errorHandlers.length = 0
|
|
153
159
|
}
|
|
154
|
-
if (globalErrorHandlers.has(root)) {
|
|
155
|
-
globalErrorHandlers.delete(root)
|
|
156
|
-
}
|
|
157
160
|
if (root.suspenseHandlers) {
|
|
158
161
|
root.suspenseHandlers.length = 0
|
|
159
162
|
}
|
|
160
|
-
if (globalSuspenseHandlers.has(root)) {
|
|
161
|
-
globalSuspenseHandlers.delete(root)
|
|
162
|
-
}
|
|
163
163
|
disposeRootDevtools(root)
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -201,21 +201,23 @@ export function registerEffectCleanup(fn: Cleanup): void {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
export function runCleanupList(list: Cleanup[]): void {
|
|
204
|
+
export function runCleanupList(list: Cleanup[], root?: RootContext): void {
|
|
205
205
|
let error: unknown
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
error
|
|
206
|
+
withRootContext(root, () => {
|
|
207
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
208
|
+
try {
|
|
209
|
+
const cleanup = list[i]
|
|
210
|
+
if (cleanup) cleanup()
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (error === undefined) {
|
|
213
|
+
error = err
|
|
214
|
+
}
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
|
-
}
|
|
217
|
+
})
|
|
216
218
|
list.length = 0
|
|
217
219
|
if (error !== undefined) {
|
|
218
|
-
if (!handleError(error, { source: 'cleanup' })) {
|
|
220
|
+
if (!handleError(error, { source: 'cleanup' }, root)) {
|
|
219
221
|
throw error
|
|
220
222
|
}
|
|
221
223
|
}
|
|
@@ -239,12 +241,6 @@ export function registerErrorHandler(fn: ErrorHandler): void {
|
|
|
239
241
|
currentRoot.errorHandlers = []
|
|
240
242
|
}
|
|
241
243
|
currentRoot.errorHandlers.push(fn)
|
|
242
|
-
const existing = globalErrorHandlers.get(currentRoot)
|
|
243
|
-
if (existing) {
|
|
244
|
-
existing.push(fn)
|
|
245
|
-
} else {
|
|
246
|
-
globalErrorHandlers.set(currentRoot, [fn])
|
|
247
|
-
}
|
|
248
244
|
}
|
|
249
245
|
|
|
250
246
|
export function registerSuspenseHandler(fn: SuspenseHandler): void {
|
|
@@ -258,12 +254,6 @@ export function registerSuspenseHandler(fn: SuspenseHandler): void {
|
|
|
258
254
|
currentRoot.suspenseHandlers = []
|
|
259
255
|
}
|
|
260
256
|
currentRoot.suspenseHandlers.push(fn)
|
|
261
|
-
const existing = globalSuspenseHandlers.get(currentRoot)
|
|
262
|
-
if (existing) {
|
|
263
|
-
existing.push(fn)
|
|
264
|
-
} else {
|
|
265
|
-
globalSuspenseHandlers.set(currentRoot, [fn])
|
|
266
|
-
}
|
|
267
257
|
}
|
|
268
258
|
|
|
269
259
|
export function handleError(err: unknown, info?: ErrorInfo, startRoot?: RootContext): boolean {
|
|
@@ -286,24 +276,6 @@ export function handleError(err: unknown, info?: ErrorInfo, startRoot?: RootCont
|
|
|
286
276
|
}
|
|
287
277
|
root = root.parent
|
|
288
278
|
}
|
|
289
|
-
const globalForRoot = startRoot
|
|
290
|
-
? globalErrorHandlers.get(startRoot)
|
|
291
|
-
: currentRoot
|
|
292
|
-
? globalErrorHandlers.get(currentRoot)
|
|
293
|
-
: undefined
|
|
294
|
-
if (globalForRoot && globalForRoot.length) {
|
|
295
|
-
for (let i = globalForRoot.length - 1; i >= 0; i--) {
|
|
296
|
-
const handler = globalForRoot[i]!
|
|
297
|
-
try {
|
|
298
|
-
const handled = handler(error, info)
|
|
299
|
-
if (handled !== false) {
|
|
300
|
-
return true
|
|
301
|
-
}
|
|
302
|
-
} catch (nextErr) {
|
|
303
|
-
error = nextErr
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
279
|
// The caller (e.g., runCleanupList) can decide whether to rethrow.
|
|
308
280
|
// This makes the API consistent: handleError always returns a boolean
|
|
309
281
|
// indicating whether the error was handled.
|
|
@@ -334,25 +306,5 @@ export function handleSuspend(
|
|
|
334
306
|
}
|
|
335
307
|
root = root.parent
|
|
336
308
|
}
|
|
337
|
-
const globalForRoot =
|
|
338
|
-
startRoot && globalSuspenseHandlers.get(startRoot)
|
|
339
|
-
? globalSuspenseHandlers.get(startRoot)
|
|
340
|
-
: currentRoot
|
|
341
|
-
? globalSuspenseHandlers.get(currentRoot)
|
|
342
|
-
: undefined
|
|
343
|
-
if (globalForRoot && globalForRoot.length) {
|
|
344
|
-
for (let i = globalForRoot.length - 1; i >= 0; i--) {
|
|
345
|
-
const handler = globalForRoot[i]!
|
|
346
|
-
const handled = handler(token)
|
|
347
|
-
if (handled !== false) {
|
|
348
|
-
// Only set suspended = true when a handler actually handles the token
|
|
349
|
-
if (originRoot) {
|
|
350
|
-
originRoot.suspended = true
|
|
351
|
-
setRootSuspendDevtools(originRoot, true)
|
|
352
|
-
}
|
|
353
|
-
return true
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
309
|
return false
|
|
358
310
|
}
|
package/src/signal.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
handleError,
|
|
7
7
|
handleSuspend,
|
|
8
8
|
registerRootCleanup,
|
|
9
|
+
withRootContext,
|
|
9
10
|
type RootContext,
|
|
10
11
|
} from './lifecycle'
|
|
11
12
|
import type { SuspenseToken } from './types'
|
|
@@ -890,7 +891,9 @@ function runEffect(e: EffectNode): void {
|
|
|
890
891
|
inCleanup = true
|
|
891
892
|
activeCleanupFlushId = currentFlushId
|
|
892
893
|
try {
|
|
893
|
-
e.
|
|
894
|
+
withRootContext(e.root, () => {
|
|
895
|
+
e.runCleanup!()
|
|
896
|
+
})
|
|
894
897
|
} finally {
|
|
895
898
|
activeCleanupFlushId = 0
|
|
896
899
|
inCleanup = false
|
package/src/suspense.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
handleError,
|
|
9
9
|
pushRoot,
|
|
10
10
|
popRoot,
|
|
11
|
+
registerRootCleanup,
|
|
11
12
|
registerSuspenseHandler,
|
|
12
13
|
} from './lifecycle'
|
|
13
14
|
import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
|
|
@@ -234,6 +235,13 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
234
235
|
// If children suspend, the handler above will be called and switch to fallback.
|
|
235
236
|
renderView(props.children ?? null)
|
|
236
237
|
|
|
238
|
+
registerRootCleanup(() => {
|
|
239
|
+
if (cleanup) {
|
|
240
|
+
cleanup()
|
|
241
|
+
cleanup = undefined
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
237
245
|
if (props.resetKeys !== undefined) {
|
|
238
246
|
const isGetter =
|
|
239
247
|
typeof props.resetKeys === 'function' && (props.resetKeys as () => unknown).length === 0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/fict/fict/packages/runtime/dist/chunk-6DNYVH5U.cjs","../src/context.ts"],"names":[],"mappings":"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","file":"/home/runner/work/fict/fict/packages/runtime/dist/chunk-6DNYVH5U.cjs","sourcesContent":[null,"/**\n * @fileoverview Context API for Fict\n *\n * Provides a way to pass data through the component tree without having to pass\n * props down manually at every level. Context is designed for:\n *\n * - SSR isolation (different request = different context values)\n * - Multi-instance support (multiple app roots with different values)\n * - Subtree scoping (override values in specific parts of the tree)\n *\n * ## Design Principles\n *\n * 1. **Reuses existing RootContext hierarchy** - Uses parent chain for value lookup,\n * consistent with handleError/handleSuspend mechanisms.\n *\n * 2. **Zero extra root creation overhead** - Provider doesn't create new root,\n * only mounts value on current root.\n *\n * 3. **Auto-aligned with insert/suspense boundaries** - Because they create child\n * roots that inherit parent, context values propagate correctly.\n *\n * ## Usage\n *\n * ```tsx\n * // Create context with default value\n * const ThemeContext = createContext<'light' | 'dark'>('light')\n *\n * // Provide value to subtree\n * function App() {\n * return (\n * <ThemeContext.Provider value=\"dark\">\n * <ThemedComponent />\n * </ThemeContext.Provider>\n * )\n * }\n *\n * // Consume value\n * function ThemedComponent() {\n * const theme = useContext(ThemeContext)\n * return <div class={theme}>...</div>\n * }\n * ```\n *\n * @module\n */\n\nimport { createElement } from './dom'\nimport { createRenderEffect } from './effect'\nimport {\n createRootContext,\n destroyRoot,\n flushOnMount,\n getCurrentRoot,\n popRoot,\n pushRoot,\n type RootContext,\n} from './lifecycle'\nimport { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'\nimport type { BaseProps, FictNode } from './types'\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Context object created by createContext.\n * Contains the Provider component and serves as a key for context lookup.\n */\nexport interface Context<T> {\n /** Unique identifier for this context */\n readonly id: symbol\n /** Default value when no provider is found */\n readonly defaultValue: T\n /** Provider component for supplying context values */\n Provider: ContextProvider<T>\n /** Display name for debugging */\n displayName?: string\n}\n\n/**\n * Props for the Context Provider component\n */\nexport interface ProviderProps<T> extends BaseProps {\n /** The value to provide to the subtree */\n value: T\n}\n\n/**\n * Provider component type\n */\nexport type ContextProvider<T> = (props: ProviderProps<T>) => FictNode\n\n// ============================================================================\n// Internal Context Storage\n// ============================================================================\n\n/**\n * WeakMap to store context values per RootContext.\n * Using WeakMap ensures proper garbage collection when roots are destroyed.\n */\nconst contextStorage = new WeakMap<RootContext, Map<symbol, unknown>>()\n\n/**\n * Get the context map for a root, creating it if needed\n */\nfunction getContextMap(root: RootContext): Map<symbol, unknown> {\n let map = contextStorage.get(root)\n if (!map) {\n map = new Map()\n contextStorage.set(root, map)\n }\n return map\n}\n\n// ============================================================================\n// Context API\n// ============================================================================\n\n/**\n * Creates a new context with the given default value.\n *\n * Context provides a way to pass values through the component tree without\n * explicit props drilling. It's especially useful for:\n *\n * - Theme data\n * - Locale/i18n settings\n * - Authentication state\n * - Feature flags\n * - Any data that many components at different nesting levels need\n *\n * @param defaultValue - The value to use when no Provider is found above in the tree\n * @returns A context object with a Provider component\n *\n * @example\n * ```tsx\n * // Create a theme context\n * const ThemeContext = createContext<'light' | 'dark'>('light')\n *\n * // Use the provider\n * function App() {\n * return (\n * <ThemeContext.Provider value=\"dark\">\n * <Content />\n * </ThemeContext.Provider>\n * )\n * }\n *\n * // Consume the context\n * function Content() {\n * const theme = useContext(ThemeContext)\n * return <div class={`theme-${theme}`}>Hello</div>\n * }\n * ```\n */\nexport function createContext<T>(defaultValue: T): Context<T> {\n const id = Symbol('fict.context')\n\n const context: Context<T> = {\n id,\n defaultValue,\n Provider: null as unknown as ContextProvider<T>,\n }\n\n // Create the Provider component\n context.Provider = function Provider(props: ProviderProps<T>): FictNode {\n const hostRoot = getCurrentRoot()\n\n // Create a child root for the provider's subtree\n // This establishes the provider boundary - children will look up from here\n const providerRoot = createRootContext(hostRoot)\n\n // Store the context value on this root\n const contextMap = getContextMap(providerRoot)\n contextMap.set(id, props.value)\n\n // Create DOM structure\n const markerOwnerDocument = providerRoot.ownerDocument ?? hostRoot?.ownerDocument ?? document\n const fragment = markerOwnerDocument.createDocumentFragment()\n const marker = markerOwnerDocument.createComment('fict:ctx')\n fragment.appendChild(marker)\n\n let cleanup: (() => void) | undefined\n let activeNodes: Node[] = []\n\n const renderChildren = (children: FictNode) => {\n // Cleanup previous render\n if (cleanup) {\n cleanup()\n cleanup = undefined\n }\n if (activeNodes.length) {\n removeNodes(activeNodes)\n activeNodes = []\n }\n\n if (children == null || children === false) {\n return\n }\n\n const prev = pushRoot(providerRoot)\n let nodes: Node[] = []\n try {\n const output = createElement(children)\n nodes = toNodeArray(output, markerOwnerDocument)\n const parentNode = marker.parentNode as (ParentNode & Node) | null\n if (parentNode) {\n insertNodesBefore(parentNode, nodes, marker)\n }\n } finally {\n popRoot(prev)\n flushOnMount(providerRoot)\n }\n\n cleanup = () => {\n destroyRoot(providerRoot)\n removeNodes(nodes)\n }\n activeNodes = nodes\n }\n\n // Initial render\n createRenderEffect(() => {\n // Update context value on re-render (if value prop changes reactively)\n contextMap.set(id, props.value)\n renderChildren(props.children)\n })\n\n return fragment\n }\n\n return context\n}\n\n/**\n * Reads the current value of a context.\n *\n * useContext looks up through the RootContext parent chain to find the\n * nearest Provider for this context. If no Provider is found, returns\n * the context's default value.\n *\n * @param context - The context object created by createContext\n * @returns The current context value\n *\n * @example\n * ```tsx\n * const ThemeContext = createContext('light')\n *\n * function ThemedButton() {\n * const theme = useContext(ThemeContext)\n * return <button class={theme === 'dark' ? 'btn-dark' : 'btn-light'}>Click</button>\n * }\n * ```\n */\nexport function useContext<T>(context: Context<T>): T {\n let root = getCurrentRoot()\n\n // Walk up the parent chain looking for the context value\n while (root) {\n const contextMap = contextStorage.get(root)\n if (contextMap && contextMap.has(context.id)) {\n return contextMap.get(context.id) as T\n }\n root = root.parent\n }\n\n // No provider found, return default value\n return context.defaultValue\n}\n\n/**\n * Checks if a context value is currently provided in the tree.\n *\n * Useful for conditional behavior when a provider may or may not exist.\n *\n * @param context - The context object to check\n * @returns true if a Provider exists above in the tree\n *\n * @example\n * ```tsx\n * function OptionalTheme() {\n * if (hasContext(ThemeContext)) {\n * const theme = useContext(ThemeContext)\n * return <div class={theme}>Themed content</div>\n * }\n * return <div>Default content</div>\n * }\n * ```\n */\nexport function hasContext<T>(context: Context<T>): boolean {\n let root = getCurrentRoot()\n\n while (root) {\n const contextMap = contextStorage.get(root)\n if (contextMap && contextMap.has(context.id)) {\n return true\n }\n root = root.parent\n }\n\n return false\n}\n"]}
|