@fictjs/runtime 0.0.11 → 0.0.13
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/index.cjs +2373 -3048
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -141
- package/dist/index.d.ts +11 -141
- package/dist/index.dev.js +2558 -2653
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +2374 -3042
- package/dist/index.js.map +1 -1
- package/package.json +1 -6
- package/src/binding.ts +28 -423
- package/src/constants.ts +368 -344
- package/src/cycle-guard.ts +124 -97
- package/src/dev.d.ts +5 -0
- package/src/dom.ts +39 -27
- package/src/effect.ts +4 -0
- package/src/error-boundary.ts +11 -2
- package/src/hooks.ts +9 -1
- package/src/index.ts +1 -19
- package/src/lifecycle.ts +17 -3
- package/src/list-helpers.ts +248 -86
- package/src/props.ts +2 -4
- package/src/reconcile.ts +4 -0
- package/src/signal.ts +128 -63
- package/src/suspense.ts +24 -7
- package/src/transition.ts +4 -1
- package/dist/slim.cjs +0 -3668
- package/dist/slim.cjs.map +0 -1
- package/dist/slim.d.cts +0 -504
- package/dist/slim.d.ts +0 -504
- package/dist/slim.js +0 -3616
- package/dist/slim.js.map +0 -1
- package/src/slim.ts +0 -69
package/src/cycle-guard.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { getDevtoolsHook } from './devtools'
|
|
2
2
|
|
|
3
|
+
const isDev =
|
|
4
|
+
typeof __DEV__ !== 'undefined'
|
|
5
|
+
? __DEV__
|
|
6
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
7
|
+
|
|
3
8
|
export interface CycleProtectionOptions {
|
|
4
9
|
maxFlushCyclesPerMicrotask?: number
|
|
5
10
|
maxEffectRunsPerFlush?: number
|
|
@@ -15,120 +20,142 @@ interface CycleWindowEntry {
|
|
|
15
20
|
budget: number
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
devMode: false,
|
|
26
|
-
}
|
|
23
|
+
let setCycleProtectionOptions: (opts: CycleProtectionOptions) => void = () => {}
|
|
24
|
+
let resetCycleProtectionStateForTests: () => void = () => {}
|
|
25
|
+
let beginFlushGuard: () => void = () => {}
|
|
26
|
+
let beforeEffectRunGuard: () => boolean = () => true
|
|
27
|
+
let endFlushGuard: () => void = () => {}
|
|
28
|
+
let enterRootGuard: (root: object) => boolean = () => true
|
|
29
|
+
let exitRootGuard: (root: object) => void = () => {}
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
if (isDev) {
|
|
32
|
+
const defaultOptions = {
|
|
33
|
+
maxFlushCyclesPerMicrotask: 10_000,
|
|
34
|
+
maxEffectRunsPerFlush: 20_000,
|
|
35
|
+
windowSize: 5,
|
|
36
|
+
highUsageRatio: 0.8,
|
|
37
|
+
maxRootReentrantDepth: 10,
|
|
38
|
+
enableWindowWarning: true,
|
|
39
|
+
devMode: false,
|
|
40
|
+
}
|
|
31
41
|
|
|
32
|
-
let
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let flushWarned = false
|
|
36
|
-
let rootWarned = false
|
|
37
|
-
let windowWarned = false
|
|
42
|
+
let options: Required<CycleProtectionOptions> = {
|
|
43
|
+
...defaultOptions,
|
|
44
|
+
} as Required<CycleProtectionOptions>
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
let effectRunsThisFlush = 0
|
|
47
|
+
let windowUsage: CycleWindowEntry[] = []
|
|
48
|
+
let rootDepth = new WeakMap<object, number>()
|
|
49
|
+
let flushWarned = false
|
|
50
|
+
let rootWarned = false
|
|
51
|
+
let windowWarned = false
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
windowUsage = []
|
|
47
|
-
rootDepth = new WeakMap<object, number>()
|
|
48
|
-
flushWarned = false
|
|
49
|
-
rootWarned = false
|
|
50
|
-
windowWarned = false
|
|
51
|
-
}
|
|
53
|
+
setCycleProtectionOptions = opts => {
|
|
54
|
+
options = { ...options, ...opts }
|
|
55
|
+
}
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
resetCycleProtectionStateForTests = () => {
|
|
58
|
+
options = { ...defaultOptions } as Required<CycleProtectionOptions>
|
|
59
|
+
effectRunsThisFlush = 0
|
|
60
|
+
windowUsage = []
|
|
61
|
+
rootDepth = new WeakMap<object, number>()
|
|
62
|
+
flushWarned = false
|
|
63
|
+
rootWarned = false
|
|
64
|
+
windowWarned = false
|
|
65
|
+
}
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
beginFlushGuard = () => {
|
|
68
|
+
effectRunsThisFlush = 0
|
|
69
|
+
flushWarned = false
|
|
70
|
+
windowWarned = false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
beforeEffectRunGuard = () => {
|
|
74
|
+
const next = ++effectRunsThisFlush
|
|
75
|
+
if (next > options.maxFlushCyclesPerMicrotask || next > options.maxEffectRunsPerFlush) {
|
|
76
|
+
const message = `[fict] cycle protection triggered: flush-budget-exceeded`
|
|
77
|
+
if (options.devMode) {
|
|
78
|
+
throw new Error(message)
|
|
79
|
+
}
|
|
80
|
+
if (!flushWarned) {
|
|
81
|
+
flushWarned = true
|
|
82
|
+
console.warn(message, { effectRuns: next })
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
69
85
|
}
|
|
70
|
-
return
|
|
86
|
+
return true
|
|
71
87
|
}
|
|
72
|
-
return true
|
|
73
|
-
}
|
|
74
88
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
89
|
+
endFlushGuard = () => {
|
|
90
|
+
recordWindowUsage(effectRunsThisFlush, options.maxFlushCyclesPerMicrotask)
|
|
91
|
+
effectRunsThisFlush = 0
|
|
92
|
+
}
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
enterRootGuard = root => {
|
|
95
|
+
const depth = (rootDepth.get(root) ?? 0) + 1
|
|
96
|
+
if (depth > options.maxRootReentrantDepth) {
|
|
97
|
+
const message = `[fict] cycle protection triggered: root-reentry`
|
|
98
|
+
if (options.devMode) {
|
|
99
|
+
throw new Error(message)
|
|
100
|
+
}
|
|
101
|
+
if (!rootWarned) {
|
|
102
|
+
rootWarned = true
|
|
103
|
+
console.warn(message, { depth })
|
|
104
|
+
}
|
|
105
|
+
return false
|
|
86
106
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.warn(message, { depth })
|
|
90
|
-
}
|
|
91
|
-
return false
|
|
107
|
+
rootDepth.set(root, depth)
|
|
108
|
+
return true
|
|
92
109
|
}
|
|
93
|
-
rootDepth.set(root, depth)
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
110
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
exitRootGuard = root => {
|
|
112
|
+
const depth = rootDepth.get(root)
|
|
113
|
+
if (depth === undefined) return
|
|
114
|
+
if (depth <= 1) {
|
|
115
|
+
rootDepth.delete(root)
|
|
116
|
+
} else {
|
|
117
|
+
rootDepth.set(root, depth - 1)
|
|
118
|
+
}
|
|
104
119
|
}
|
|
105
|
-
}
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
const recordWindowUsage = (used: number, budget: number): void => {
|
|
122
|
+
if (!options.enableWindowWarning) return
|
|
123
|
+
const entry = { used, budget }
|
|
124
|
+
windowUsage.push(entry)
|
|
125
|
+
if (windowUsage.length > options.windowSize) {
|
|
126
|
+
windowUsage.shift()
|
|
127
|
+
}
|
|
128
|
+
if (windowWarned) return
|
|
129
|
+
if (
|
|
130
|
+
windowUsage.length >= options.windowSize &&
|
|
131
|
+
windowUsage.every(
|
|
132
|
+
item => item.budget > 0 && item.used / item.budget >= options.highUsageRatio,
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
windowWarned = true
|
|
136
|
+
reportCycle('high-usage-window', {
|
|
137
|
+
windowSize: options.windowSize,
|
|
138
|
+
ratio: options.highUsageRatio,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
113
141
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
ratio: options.highUsageRatio,
|
|
123
|
-
})
|
|
142
|
+
|
|
143
|
+
const reportCycle = (
|
|
144
|
+
reason: string,
|
|
145
|
+
detail: Record<string, unknown> | undefined = undefined,
|
|
146
|
+
): void => {
|
|
147
|
+
const hook = getDevtoolsHook()
|
|
148
|
+
hook?.cycleDetected?.(detail ? { reason, detail } : { reason })
|
|
149
|
+
console.warn(`[fict] cycle protection triggered: ${reason}`, detail ?? '')
|
|
124
150
|
}
|
|
125
151
|
}
|
|
126
152
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
export {
|
|
154
|
+
setCycleProtectionOptions,
|
|
155
|
+
resetCycleProtectionStateForTests,
|
|
156
|
+
beginFlushGuard,
|
|
157
|
+
beforeEffectRunGuard,
|
|
158
|
+
endFlushGuard,
|
|
159
|
+
enterRootGuard,
|
|
160
|
+
exitRootGuard,
|
|
134
161
|
}
|
package/src/dev.d.ts
ADDED
package/src/dom.ts
CHANGED
|
@@ -20,19 +20,11 @@ import {
|
|
|
20
20
|
createChildBinding,
|
|
21
21
|
bindEvent,
|
|
22
22
|
isReactive,
|
|
23
|
-
PRIMITIVE_PROXY,
|
|
24
23
|
type MaybeReactive,
|
|
25
24
|
type AttributeSetter,
|
|
26
25
|
type BindingHandle,
|
|
27
26
|
} from './binding'
|
|
28
|
-
import {
|
|
29
|
-
Properties,
|
|
30
|
-
ChildProperties,
|
|
31
|
-
Aliases,
|
|
32
|
-
getPropAlias,
|
|
33
|
-
SVGElements,
|
|
34
|
-
SVGNamespace,
|
|
35
|
-
} from './constants'
|
|
27
|
+
import { Properties, ChildProperties, getPropAlias, SVGElements, SVGNamespace } from './constants'
|
|
36
28
|
import { __fictPushContext, __fictPopContext } from './hooks'
|
|
37
29
|
import { Fragment } from './jsx'
|
|
38
30
|
import {
|
|
@@ -54,6 +46,10 @@ type NamespaceContext = 'svg' | 'mathml' | null
|
|
|
54
46
|
|
|
55
47
|
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
56
48
|
const MATHML_NS = 'http://www.w3.org/1998/Math/MathML'
|
|
49
|
+
const isDev =
|
|
50
|
+
typeof __DEV__ !== 'undefined'
|
|
51
|
+
? __DEV__
|
|
52
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
57
53
|
|
|
58
54
|
// ============================================================================
|
|
59
55
|
// Main Render Function
|
|
@@ -124,7 +120,7 @@ function resolveNamespace(tagName: string, namespace: NamespaceContext): Namespa
|
|
|
124
120
|
if (tagName === 'math') return 'mathml'
|
|
125
121
|
if (namespace === 'mathml') return 'mathml'
|
|
126
122
|
if (namespace === 'svg') return 'svg'
|
|
127
|
-
if (SVGElements.has(tagName)) return 'svg'
|
|
123
|
+
if (isDev && SVGElements.has(tagName)) return 'svg'
|
|
128
124
|
return null
|
|
129
125
|
}
|
|
130
126
|
|
|
@@ -139,9 +135,8 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
139
135
|
return document.createTextNode('')
|
|
140
136
|
}
|
|
141
137
|
|
|
142
|
-
// Primitive proxy produced by keyed list binding
|
|
143
138
|
if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
|
|
144
|
-
// Handle BindingHandle (
|
|
139
|
+
// Handle BindingHandle (list/conditional bindings, etc)
|
|
145
140
|
if ('marker' in node) {
|
|
146
141
|
const handle = node as { marker: unknown; dispose?: () => void; flush?: () => void }
|
|
147
142
|
// Register dispose cleanup if available
|
|
@@ -160,14 +155,6 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
160
155
|
}
|
|
161
156
|
return createElement(handle.marker as FictNode)
|
|
162
157
|
}
|
|
163
|
-
|
|
164
|
-
const nodeRecord = node as unknown as Record<PropertyKey, unknown>
|
|
165
|
-
if (nodeRecord[PRIMITIVE_PROXY]) {
|
|
166
|
-
const primitiveGetter = nodeRecord[Symbol.toPrimitive]
|
|
167
|
-
const value =
|
|
168
|
-
typeof primitiveGetter === 'function' ? primitiveGetter.call(node, 'default') : node
|
|
169
|
-
return document.createTextNode(value == null || value === false ? '' : String(value))
|
|
170
|
-
}
|
|
171
158
|
}
|
|
172
159
|
|
|
173
160
|
// Array - create fragment
|
|
@@ -431,10 +418,17 @@ function applyRef(el: Element, value: unknown): void {
|
|
|
431
418
|
refFn(el)
|
|
432
419
|
|
|
433
420
|
// Match React behavior: call ref(null) on unmount
|
|
434
|
-
|
|
421
|
+
const root = getCurrentRoot()
|
|
422
|
+
if (root) {
|
|
435
423
|
registerRootCleanup(() => {
|
|
436
424
|
refFn(null)
|
|
437
425
|
})
|
|
426
|
+
} else if (isDev) {
|
|
427
|
+
console.warn(
|
|
428
|
+
'[fict] Ref applied outside of a root context. ' +
|
|
429
|
+
'The ref cleanup (setting to null) will not run automatically. ' +
|
|
430
|
+
'Consider using createRoot() or ensure the element is created within a component.',
|
|
431
|
+
)
|
|
438
432
|
}
|
|
439
433
|
} else if (value && typeof value === 'object' && 'current' in value) {
|
|
440
434
|
// Object ref
|
|
@@ -442,10 +436,17 @@ function applyRef(el: Element, value: unknown): void {
|
|
|
442
436
|
refObj.current = el
|
|
443
437
|
|
|
444
438
|
// Auto-cleanup on unmount
|
|
445
|
-
|
|
439
|
+
const root = getCurrentRoot()
|
|
440
|
+
if (root) {
|
|
446
441
|
registerRootCleanup(() => {
|
|
447
442
|
refObj.current = null
|
|
448
443
|
})
|
|
444
|
+
} else if (isDev) {
|
|
445
|
+
console.warn(
|
|
446
|
+
'[fict] Ref applied outside of a root context. ' +
|
|
447
|
+
'The ref cleanup (setting to null) will not run automatically. ' +
|
|
448
|
+
'Consider using createRoot() or ensure the element is created within a component.',
|
|
449
|
+
)
|
|
449
450
|
}
|
|
450
451
|
}
|
|
451
452
|
}
|
|
@@ -541,7 +542,13 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
|
|
|
541
542
|
}
|
|
542
543
|
|
|
543
544
|
// Child properties (innerHTML, textContent, etc.)
|
|
544
|
-
if (
|
|
545
|
+
if (
|
|
546
|
+
(isDev && ChildProperties.has(key)) ||
|
|
547
|
+
key === 'innerHTML' ||
|
|
548
|
+
key === 'textContent' ||
|
|
549
|
+
key === 'innerText' ||
|
|
550
|
+
key === 'children'
|
|
551
|
+
) {
|
|
545
552
|
createAttributeBinding(el, key, value as MaybeReactive<unknown>, setProperty)
|
|
546
553
|
continue
|
|
547
554
|
}
|
|
@@ -565,13 +572,18 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
|
|
|
565
572
|
}
|
|
566
573
|
|
|
567
574
|
// Check for property alias (element-specific mappings)
|
|
568
|
-
const propAlias = !isSVG ? getPropAlias(key, tagName) : undefined
|
|
575
|
+
const propAlias = !isSVG && isDev ? getPropAlias(key, tagName) : undefined
|
|
576
|
+
const isProperty = !isSVG
|
|
577
|
+
? isDev
|
|
578
|
+
? Properties.has(key)
|
|
579
|
+
: key in (el as unknown as Record<string, unknown>)
|
|
580
|
+
: false
|
|
569
581
|
|
|
570
582
|
// Handle properties and element-specific attributes
|
|
571
|
-
if (propAlias ||
|
|
583
|
+
if (propAlias || isProperty || (isCE && !isSVG)) {
|
|
572
584
|
const propName = propAlias || key
|
|
573
585
|
// Custom elements use toPropertyName conversion
|
|
574
|
-
if (isCE && !
|
|
586
|
+
if (isCE && !isProperty && !propAlias) {
|
|
575
587
|
createAttributeBinding(
|
|
576
588
|
el,
|
|
577
589
|
toPropertyName(propName),
|
|
@@ -598,7 +610,7 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
|
|
|
598
610
|
|
|
599
611
|
// Regular attributes (potentially reactive)
|
|
600
612
|
// Apply alias mapping (className -> class, htmlFor -> for)
|
|
601
|
-
const attrName =
|
|
613
|
+
const attrName = key === 'htmlFor' ? 'for' : key
|
|
602
614
|
createAttributeBinding(el, attrName, value as MaybeReactive<unknown>, setAttribute)
|
|
603
615
|
}
|
|
604
616
|
}
|
package/src/effect.ts
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
import { effectWithCleanup } from './signal'
|
|
9
9
|
import type { Cleanup } from './types'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Effect callback run synchronously; async callbacks are not tracked after the first await.
|
|
13
|
+
* TypeScript will reject `async () => {}` here—split async work or read signals before awaiting.
|
|
14
|
+
*/
|
|
11
15
|
export type Effect = () => void | Cleanup
|
|
12
16
|
|
|
13
17
|
export function createEffect(fn: Effect): () => void {
|
package/src/error-boundary.ts
CHANGED
|
@@ -72,13 +72,22 @@ export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
|
|
|
72
72
|
if (renderingFallback) {
|
|
73
73
|
throw err
|
|
74
74
|
}
|
|
75
|
+
// nested errors. If fallback rendering also throws, we should NOT reset
|
|
76
|
+
// the flag until we're sure no more recursion is happening.
|
|
75
77
|
renderingFallback = true
|
|
76
78
|
try {
|
|
77
79
|
renderValue(toView(err))
|
|
78
|
-
|
|
80
|
+
// Only reset if successful - if renderValue threw, we want to keep
|
|
81
|
+
// renderingFallback = true to prevent infinite recursion
|
|
79
82
|
renderingFallback = false
|
|
83
|
+
props.onError?.(err)
|
|
84
|
+
} catch (fallbackErr) {
|
|
85
|
+
// Fallback rendering failed - keep renderingFallback = true
|
|
86
|
+
// to prevent further attempts, then rethrow
|
|
87
|
+
// If fallback fails, report both errors
|
|
88
|
+
props.onError?.(err)
|
|
89
|
+
throw fallbackErr
|
|
80
90
|
}
|
|
81
|
-
props.onError?.(err)
|
|
82
91
|
return
|
|
83
92
|
}
|
|
84
93
|
popRoot(prev)
|
package/src/hooks.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { createEffect } from './effect'
|
|
|
2
2
|
import { createMemo } from './memo'
|
|
3
3
|
import { createSignal, type SignalAccessor, type ComputedAccessor } from './signal'
|
|
4
4
|
|
|
5
|
+
const isDev =
|
|
6
|
+
typeof __DEV__ !== 'undefined'
|
|
7
|
+
? __DEV__
|
|
8
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
9
|
+
|
|
5
10
|
interface HookContext {
|
|
6
11
|
slots: unknown[]
|
|
7
12
|
cursor: number
|
|
@@ -12,7 +17,10 @@ const ctxStack: HookContext[] = []
|
|
|
12
17
|
|
|
13
18
|
function assertRenderContext(ctx: HookContext, hookName: string): void {
|
|
14
19
|
if (!ctx.rendering) {
|
|
15
|
-
|
|
20
|
+
const message = isDev
|
|
21
|
+
? `${hookName} can only be used during render execution`
|
|
22
|
+
: 'FICT:E_HOOK_RENDER'
|
|
23
|
+
throw new Error(message)
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
|
package/src/index.ts
CHANGED
|
@@ -101,12 +101,10 @@ export {
|
|
|
101
101
|
isReactive,
|
|
102
102
|
// Advanced bindings
|
|
103
103
|
createConditional,
|
|
104
|
-
createList,
|
|
105
104
|
createPortal,
|
|
106
105
|
createShow,
|
|
107
106
|
// Utility functions
|
|
108
107
|
unwrap,
|
|
109
|
-
unwrapPrimitive,
|
|
110
108
|
} from './binding'
|
|
111
109
|
|
|
112
110
|
// Constants for DOM handling
|
|
@@ -129,13 +127,7 @@ export { default as reconcileArrays } from './reconcile'
|
|
|
129
127
|
// Types
|
|
130
128
|
// ============================================================================
|
|
131
129
|
|
|
132
|
-
export type {
|
|
133
|
-
MaybeReactive,
|
|
134
|
-
BindingHandle,
|
|
135
|
-
KeyFn,
|
|
136
|
-
CreateElementFn,
|
|
137
|
-
AttributeSetter,
|
|
138
|
-
} from './binding'
|
|
130
|
+
export type { MaybeReactive, BindingHandle, CreateElementFn, AttributeSetter } from './binding'
|
|
139
131
|
|
|
140
132
|
export type {
|
|
141
133
|
FictNode,
|
|
@@ -167,21 +159,11 @@ export {
|
|
|
167
159
|
moveNodesBefore,
|
|
168
160
|
removeNodes,
|
|
169
161
|
insertNodesBefore,
|
|
170
|
-
moveMarkerBlock,
|
|
171
|
-
destroyMarkerBlock,
|
|
172
|
-
// Keyed list container
|
|
173
|
-
createKeyedListContainer,
|
|
174
|
-
// Block creation
|
|
175
|
-
createKeyedBlock,
|
|
176
162
|
// High-level list binding (for compiler-generated code)
|
|
177
163
|
createKeyedList,
|
|
178
164
|
// Utilities
|
|
179
165
|
toNodeArray,
|
|
180
|
-
getFirstNodeAfter,
|
|
181
166
|
isNodeBetweenMarkers,
|
|
182
167
|
// Types
|
|
183
|
-
type KeyedBlock,
|
|
184
|
-
type KeyedListContainer,
|
|
185
168
|
type KeyedListBinding,
|
|
186
|
-
type MarkerBlock,
|
|
187
169
|
} from './list-helpers'
|
package/src/lifecycle.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { enterRootGuard, exitRootGuard } from './cycle-guard'
|
|
2
2
|
import type { Cleanup, ErrorInfo, SuspenseToken } from './types'
|
|
3
3
|
|
|
4
|
+
const isDev =
|
|
5
|
+
typeof __DEV__ !== 'undefined'
|
|
6
|
+
? __DEV__
|
|
7
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
8
|
+
|
|
4
9
|
type LifecycleFn = () => void | Cleanup
|
|
5
10
|
|
|
6
11
|
export interface RootContext {
|
|
@@ -169,7 +174,10 @@ function runLifecycle(fn: LifecycleFn): void {
|
|
|
169
174
|
|
|
170
175
|
export function registerErrorHandler(fn: ErrorHandler): void {
|
|
171
176
|
if (!currentRoot) {
|
|
172
|
-
|
|
177
|
+
const message = isDev
|
|
178
|
+
? 'registerErrorHandler must be called within a root'
|
|
179
|
+
: 'FICT:E_ROOT_HANDLER'
|
|
180
|
+
throw new Error(message)
|
|
173
181
|
}
|
|
174
182
|
if (!currentRoot.errorHandlers) {
|
|
175
183
|
currentRoot.errorHandlers = []
|
|
@@ -185,7 +193,10 @@ export function registerErrorHandler(fn: ErrorHandler): void {
|
|
|
185
193
|
|
|
186
194
|
export function registerSuspenseHandler(fn: SuspenseHandler): void {
|
|
187
195
|
if (!currentRoot) {
|
|
188
|
-
|
|
196
|
+
const message = isDev
|
|
197
|
+
? 'registerSuspenseHandler must be called within a root'
|
|
198
|
+
: 'FICT:E_ROOT_SUSPENSE'
|
|
199
|
+
throw new Error(message)
|
|
189
200
|
}
|
|
190
201
|
if (!currentRoot.suspenseHandlers) {
|
|
191
202
|
currentRoot.suspenseHandlers = []
|
|
@@ -237,7 +248,10 @@ export function handleError(err: unknown, info?: ErrorInfo, startRoot?: RootCont
|
|
|
237
248
|
}
|
|
238
249
|
}
|
|
239
250
|
}
|
|
240
|
-
|
|
251
|
+
// The caller (e.g., runCleanupList) can decide whether to rethrow.
|
|
252
|
+
// This makes the API consistent: handleError always returns a boolean
|
|
253
|
+
// indicating whether the error was handled.
|
|
254
|
+
return false
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
export function handleSuspend(
|