@fictjs/runtime 0.8.0 → 0.10.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/README.md +46 -0
- package/dist/advanced.cjs +9 -9
- package/dist/advanced.d.cts +5 -5
- package/dist/advanced.d.ts +5 -5
- package/dist/advanced.js +4 -4
- package/dist/{effect-DAzpH7Mm.d.ts → binding-DUEukRxl.d.cts} +35 -24
- package/dist/{effect-DAzpH7Mm.d.cts → binding-DqxS9ZQf.d.ts} +35 -24
- package/dist/{chunk-WRU3IZOA.js → chunk-2JRPPCG7.js} +3 -3
- package/dist/{chunk-TLDT76RV.js → chunk-DKA2I6ET.js} +3 -3
- package/dist/{chunk-FSCBL7RI.cjs → chunk-EQ5E4WOV.cjs} +702 -534
- package/dist/chunk-EQ5E4WOV.cjs.map +1 -0
- package/dist/{chunk-7YQK3XKY.js → chunk-F4RVNXOL.js} +687 -519
- package/dist/chunk-F4RVNXOL.js.map +1 -0
- package/dist/{chunk-PRF4QG73.cjs → chunk-I4GKKAAY.cjs} +469 -248
- package/dist/chunk-I4GKKAAY.cjs.map +1 -0
- package/dist/{chunk-HHDHQGJY.cjs → chunk-K3DH5SD5.cjs} +17 -17
- package/dist/{chunk-HHDHQGJY.cjs.map → chunk-K3DH5SD5.cjs.map} +1 -1
- package/dist/chunk-P4TZLFV6.js +768 -0
- package/dist/chunk-P4TZLFV6.js.map +1 -0
- package/dist/{chunk-4LCHQ7U4.js → chunk-R6FINS25.js} +318 -97
- package/dist/chunk-R6FINS25.js.map +1 -0
- package/dist/chunk-SZLJCQFZ.cjs +768 -0
- package/dist/chunk-SZLJCQFZ.cjs.map +1 -0
- package/dist/{chunk-CEV6TO5U.cjs → chunk-V7BC64W2.cjs} +8 -8
- package/dist/{chunk-CEV6TO5U.cjs.map → chunk-V7BC64W2.cjs.map} +1 -1
- package/dist/{context-BFbHf9nC.d.cts → devtools-C4Hgfa-S.d.ts} +47 -35
- package/dist/{context-C4vBQbb4.d.ts → devtools-CMxlJUTx.d.cts} +47 -35
- package/dist/index.cjs +42 -42
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.dev.js +233 -28
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/internal-list.cjs +12 -0
- package/dist/internal-list.cjs.map +1 -0
- package/dist/internal-list.d.cts +2 -0
- package/dist/internal-list.d.ts +2 -0
- package/dist/internal-list.js +12 -0
- package/dist/internal-list.js.map +1 -0
- package/dist/internal.cjs +6 -746
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +7 -75
- package/dist/internal.d.ts +7 -75
- package/dist/internal.js +12 -752
- package/dist/internal.js.map +1 -1
- package/dist/jsx-dev-runtime.d.cts +671 -0
- package/dist/jsx-dev-runtime.d.ts +671 -0
- package/dist/jsx-runtime.d.cts +671 -0
- package/dist/jsx-runtime.d.ts +671 -0
- package/dist/list-BBzsJhrm.d.ts +71 -0
- package/dist/list-_NJCcjl1.d.cts +71 -0
- package/dist/loader.cjs +99 -16
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.d.cts +17 -3
- package/dist/loader.d.ts +17 -3
- package/dist/loader.js +92 -9
- package/dist/loader.js.map +1 -1
- package/dist/{props-84UJeWO8.d.cts → props--zJ4ebbT.d.cts} +3 -3
- package/dist/{props-BRhFK50f.d.ts → props-BAGR7j-j.d.ts} +3 -3
- package/dist/{resume-i-A3EFox.d.cts → resume-C5IKAIdh.d.ts} +5 -3
- package/dist/{resume-CqeQ3v_q.d.ts → resume-DPZxmA95.d.cts} +5 -3
- package/dist/{scope-D3DpsfoG.d.ts → scope-CuImnvh1.d.ts} +1 -1
- package/dist/{scope-DlCBL1Ft.d.cts → scope-Dq5hOu7c.d.cts} +1 -1
- package/dist/{signal-C4ISF17w.d.cts → signal-Z4KkDk9h.d.cts} +12 -1
- package/dist/{signal-C4ISF17w.d.ts → signal-Z4KkDk9h.d.ts} +12 -1
- package/package.json +9 -2
- package/src/binding.ts +113 -36
- package/src/cycle-guard.ts +3 -3
- package/src/devtools.ts +19 -2
- package/src/dom.ts +58 -4
- package/src/effect.ts +5 -5
- package/src/hooks.ts +13 -5
- package/src/internal/list.ts +7 -0
- package/src/internal.ts +1 -0
- package/src/lifecycle.ts +41 -3
- package/src/list-helpers.ts +1 -1
- package/src/loader.ts +128 -12
- package/src/resume.ts +6 -3
- package/src/signal.ts +200 -20
- package/src/transition.ts +9 -3
- package/dist/chunk-4LCHQ7U4.js.map +0 -1
- package/dist/chunk-7YQK3XKY.js.map +0 -1
- package/dist/chunk-FSCBL7RI.cjs.map +0 -1
- package/dist/chunk-PRF4QG73.cjs.map +0 -1
- /package/dist/{chunk-WRU3IZOA.js.map → chunk-2JRPPCG7.js.map} +0 -0
- /package/dist/{chunk-TLDT76RV.js.map → chunk-DKA2I6ET.js.map} +0 -0
package/src/binding.ts
CHANGED
|
@@ -112,6 +112,16 @@ export interface BindingHandle {
|
|
|
112
112
|
dispose: Cleanup
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export interface ConditionalBindingOptions {
|
|
116
|
+
/**
|
|
117
|
+
* When true, track signal reads inside active branch render callbacks and
|
|
118
|
+
* re-run the branch callback on updates even if the top-level condition stays
|
|
119
|
+
* the same. This preserves reactivity for control-flow callbacks that cannot
|
|
120
|
+
* be lowered into fine-grained bindings.
|
|
121
|
+
*/
|
|
122
|
+
trackBranchReads?: boolean
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
/** Managed child node with its dispose function */
|
|
116
126
|
// ============================================================================
|
|
117
127
|
// Utility Functions
|
|
@@ -1743,7 +1753,9 @@ export function createConditional(
|
|
|
1743
1753
|
renderFalse?: () => FictNode,
|
|
1744
1754
|
startOverride?: Comment,
|
|
1745
1755
|
endOverride?: Comment,
|
|
1756
|
+
options?: ConditionalBindingOptions,
|
|
1746
1757
|
): BindingHandle {
|
|
1758
|
+
const trackBranchReads = options?.trackBranchReads === true
|
|
1747
1759
|
const useProvided = !!(startOverride && endOverride)
|
|
1748
1760
|
const startMarker = useProvided ? startOverride! : document.createComment('fict:cond:start')
|
|
1749
1761
|
const endMarker = useProvided ? endOverride! : document.createComment('fict:cond:end')
|
|
@@ -1805,7 +1817,7 @@ export function createConditional(
|
|
|
1805
1817
|
endMarker,
|
|
1806
1818
|
parent.ownerDocument ?? document,
|
|
1807
1819
|
() => {
|
|
1808
|
-
const output = untrack(render)
|
|
1820
|
+
const output = trackBranchReads ? render() : untrack(render)
|
|
1809
1821
|
if (output == null || output === false) {
|
|
1810
1822
|
return
|
|
1811
1823
|
}
|
|
@@ -1837,10 +1849,102 @@ export function createConditional(
|
|
|
1837
1849
|
return
|
|
1838
1850
|
}
|
|
1839
1851
|
|
|
1840
|
-
if (
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1852
|
+
if (!trackBranchReads) {
|
|
1853
|
+
if (lastCondition === cond && currentNodes.length > 0) {
|
|
1854
|
+
return
|
|
1855
|
+
}
|
|
1856
|
+
if (lastCondition === cond && lastCondition === false && renderFalse === undefined) {
|
|
1857
|
+
return
|
|
1858
|
+
}
|
|
1859
|
+
} else if (lastCondition === cond) {
|
|
1860
|
+
const render = cond ? renderTrue : renderFalse
|
|
1861
|
+
if (!render) {
|
|
1862
|
+
return
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
let patched = false
|
|
1866
|
+
const scratchRoot = createRootContext(hostRoot)
|
|
1867
|
+
const prevScratch = pushRoot(scratchRoot)
|
|
1868
|
+
let handledPatchError = false
|
|
1869
|
+
let scratchOutput: FictNode = null
|
|
1870
|
+
try {
|
|
1871
|
+
const output = render()
|
|
1872
|
+
scratchOutput = output
|
|
1873
|
+
if (output != null && output !== false) {
|
|
1874
|
+
if (currentNodes.length === 1) {
|
|
1875
|
+
patched = patchNode(currentNodes[0] ?? null, output)
|
|
1876
|
+
}
|
|
1877
|
+
if (!patched && Array.isArray(output)) {
|
|
1878
|
+
patched = _patchFragmentChildren(currentNodes, output)
|
|
1879
|
+
}
|
|
1880
|
+
if (!patched && _isFragmentVNode(output)) {
|
|
1881
|
+
patched = _patchFragmentChildren(currentNodes, output.props?.children)
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
if (handleSuspend(err as any, scratchRoot)) {
|
|
1886
|
+
handledPatchError = true
|
|
1887
|
+
return
|
|
1888
|
+
}
|
|
1889
|
+
if (handleError(err, { source: 'renderChild' }, scratchRoot)) {
|
|
1890
|
+
handledPatchError = true
|
|
1891
|
+
return
|
|
1892
|
+
}
|
|
1893
|
+
throw err
|
|
1894
|
+
} finally {
|
|
1895
|
+
popRoot(prevScratch)
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (handledPatchError) {
|
|
1899
|
+
destroyRoot(scratchRoot)
|
|
1900
|
+
return
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (patched) {
|
|
1904
|
+
destroyRoot(scratchRoot)
|
|
1905
|
+
return
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
lastCondition = cond
|
|
1909
|
+
|
|
1910
|
+
if (currentRoot) {
|
|
1911
|
+
destroyRoot(currentRoot)
|
|
1912
|
+
currentRoot = null
|
|
1913
|
+
}
|
|
1914
|
+
removeNodes(currentNodes)
|
|
1915
|
+
currentNodes = []
|
|
1916
|
+
|
|
1917
|
+
const prev = pushRoot(scratchRoot)
|
|
1918
|
+
let handledError = false
|
|
1919
|
+
try {
|
|
1920
|
+
if (scratchOutput == null || scratchOutput === false) {
|
|
1921
|
+
return
|
|
1922
|
+
}
|
|
1923
|
+
const el = createElementFn(scratchOutput)
|
|
1924
|
+
const nodes = toNodeArray(el)
|
|
1925
|
+
insertNodesBefore(parent, nodes, endMarker)
|
|
1926
|
+
currentNodes = nodes
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
if (handleSuspend(err as any, scratchRoot)) {
|
|
1929
|
+
handledError = true
|
|
1930
|
+
destroyRoot(scratchRoot)
|
|
1931
|
+
return
|
|
1932
|
+
}
|
|
1933
|
+
if (handleError(err, { source: 'renderChild' }, scratchRoot)) {
|
|
1934
|
+
handledError = true
|
|
1935
|
+
destroyRoot(scratchRoot)
|
|
1936
|
+
return
|
|
1937
|
+
}
|
|
1938
|
+
throw err
|
|
1939
|
+
} finally {
|
|
1940
|
+
popRoot(prev)
|
|
1941
|
+
if (!handledError) {
|
|
1942
|
+
flushOnMount(scratchRoot)
|
|
1943
|
+
currentRoot = scratchRoot
|
|
1944
|
+
} else {
|
|
1945
|
+
currentRoot = null
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1844
1948
|
return
|
|
1845
1949
|
}
|
|
1846
1950
|
lastCondition = cond
|
|
@@ -1865,7 +1969,7 @@ export function createConditional(
|
|
|
1865
1969
|
// tracked by createConditional's effect. This ensures that signals used
|
|
1866
1970
|
// inside the render function (e.g., nested if conditions) don't cause
|
|
1867
1971
|
// createConditional to re-run, which would purge child effect deps.
|
|
1868
|
-
const output = untrack(render)
|
|
1972
|
+
const output = trackBranchReads ? render() : untrack(render)
|
|
1869
1973
|
if (output == null || output === false) {
|
|
1870
1974
|
return
|
|
1871
1975
|
}
|
|
@@ -2050,23 +2154,6 @@ export function createPortal(
|
|
|
2050
2154
|
}
|
|
2051
2155
|
|
|
2052
2156
|
function patchElement(el: Element, output: FictNode): boolean {
|
|
2053
|
-
if (
|
|
2054
|
-
output === null ||
|
|
2055
|
-
output === undefined ||
|
|
2056
|
-
output === false ||
|
|
2057
|
-
typeof output === 'string' ||
|
|
2058
|
-
typeof output === 'number'
|
|
2059
|
-
) {
|
|
2060
|
-
el.textContent =
|
|
2061
|
-
output === null || output === undefined || output === false ? '' : String(output)
|
|
2062
|
-
return true
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
if (output instanceof Text) {
|
|
2066
|
-
el.textContent = output.data
|
|
2067
|
-
return true
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
2157
|
if (output && typeof output === 'object' && !(output instanceof Node)) {
|
|
2071
2158
|
const vnode = output as { type?: unknown; props?: Record<string, unknown> }
|
|
2072
2159
|
if (typeof vnode.type === 'string' && vnode.type.toLowerCase() === el.tagName.toLowerCase()) {
|
|
@@ -2131,19 +2218,6 @@ function patchElement(el: Element, output: FictNode): boolean {
|
|
|
2131
2218
|
}
|
|
2132
2219
|
}
|
|
2133
2220
|
|
|
2134
|
-
if (output instanceof Node) {
|
|
2135
|
-
if (output.nodeType === Node.ELEMENT_NODE) {
|
|
2136
|
-
const nextEl = output as Element
|
|
2137
|
-
if (nextEl.tagName.toLowerCase() === el.tagName.toLowerCase()) {
|
|
2138
|
-
el.textContent = nextEl.textContent
|
|
2139
|
-
return true
|
|
2140
|
-
}
|
|
2141
|
-
} else if (output.nodeType === Node.TEXT_NODE) {
|
|
2142
|
-
el.textContent = (output as Text).data
|
|
2143
|
-
return true
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
2221
|
return false
|
|
2148
2222
|
}
|
|
2149
2223
|
|
|
@@ -2207,6 +2281,9 @@ function normalizeChildren(
|
|
|
2207
2281
|
if (children === null || children === false) {
|
|
2208
2282
|
return result
|
|
2209
2283
|
}
|
|
2284
|
+
if (_isFragmentVNode(children)) {
|
|
2285
|
+
return normalizeChildren(children.props?.children, result)
|
|
2286
|
+
}
|
|
2210
2287
|
result.push(children)
|
|
2211
2288
|
return result
|
|
2212
2289
|
}
|
package/src/cycle-guard.ts
CHANGED
|
@@ -6,7 +6,7 @@ const isDev =
|
|
|
6
6
|
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
7
7
|
|
|
8
8
|
export interface CycleProtectionOptions {
|
|
9
|
-
/** Enable cycle protection guards (enabled by default in
|
|
9
|
+
/** Enable cycle protection guards (enabled by default in dev mode) */
|
|
10
10
|
enabled?: boolean
|
|
11
11
|
maxFlushCyclesPerMicrotask?: number
|
|
12
12
|
maxEffectRunsPerFlush?: number
|
|
@@ -35,8 +35,8 @@ let enterRootGuard: (root: object) => boolean = () => true
|
|
|
35
35
|
let exitRootGuard: (root: object) => void = () => {}
|
|
36
36
|
|
|
37
37
|
const defaultOptions = {
|
|
38
|
-
//
|
|
39
|
-
enabled:
|
|
38
|
+
// DX-first in development, performance-first in production.
|
|
39
|
+
enabled: isDev,
|
|
40
40
|
maxFlushCyclesPerMicrotask: 10_000,
|
|
41
41
|
maxEffectRunsPerFlush: 20_000,
|
|
42
42
|
windowSize: 5,
|
package/src/devtools.ts
CHANGED
|
@@ -5,18 +5,35 @@ export interface FictDevtoolsHook {
|
|
|
5
5
|
options?: { name?: string; source?: string; ownerId?: number },
|
|
6
6
|
) => void
|
|
7
7
|
updateSignal: (id: number, value: unknown) => void
|
|
8
|
+
disposeSignal?: (id: number) => void
|
|
8
9
|
registerComputed: (
|
|
9
10
|
id: number,
|
|
10
11
|
value: unknown,
|
|
11
|
-
options?: {
|
|
12
|
+
options?: {
|
|
13
|
+
name?: string
|
|
14
|
+
source?: string
|
|
15
|
+
ownerId?: number
|
|
16
|
+
hasValue?: boolean
|
|
17
|
+
internal?: boolean
|
|
18
|
+
},
|
|
12
19
|
) => void
|
|
13
20
|
updateComputed: (id: number, value: unknown) => void
|
|
21
|
+
disposeComputed?: (id: number) => void
|
|
14
22
|
registerEffect: (id: number, options?: { ownerId?: number; source?: string }) => void
|
|
15
|
-
effectRun: (id: number) => void
|
|
23
|
+
effectRun: (id: number, duration?: number) => void
|
|
24
|
+
effectCleanup?: (id: number) => void
|
|
25
|
+
disposeEffect?: (id: number) => void
|
|
16
26
|
/** Track a dependency relationship between subscriber and dependency */
|
|
17
27
|
trackDependency?: (subscriberId: number, dependencyId: number) => void
|
|
18
28
|
/** Remove a dependency relationship when unlinked */
|
|
19
29
|
untrackDependency?: (subscriberId: number, dependencyId: number) => void
|
|
30
|
+
registerRoot?: (id: number, name?: string) => void
|
|
31
|
+
disposeRoot?: (id: number) => void
|
|
32
|
+
rootSuspend?: (id: number, suspended: boolean) => void
|
|
33
|
+
batchStart?: () => void
|
|
34
|
+
batchEnd?: () => void
|
|
35
|
+
flushStart?: () => void
|
|
36
|
+
flushEnd?: () => void
|
|
20
37
|
cycleDetected?: (payload: { reason: string; detail?: Record<string, unknown> }) => void
|
|
21
38
|
|
|
22
39
|
// Component lifecycle
|
package/src/dom.ts
CHANGED
|
@@ -64,6 +64,46 @@ const isDev =
|
|
|
64
64
|
|
|
65
65
|
let nextComponentId = 1
|
|
66
66
|
|
|
67
|
+
type DevtoolsAnnotatedElement = HTMLElement & {
|
|
68
|
+
__fict_component_id__?: number
|
|
69
|
+
__fict_component_name__?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function collectComponentMountElements(node: Node): HTMLElement[] {
|
|
73
|
+
if (node instanceof DocumentFragment) {
|
|
74
|
+
return Array.from(node.childNodes).filter(
|
|
75
|
+
(child): child is HTMLElement => child instanceof HTMLElement,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (node instanceof HTMLElement) {
|
|
80
|
+
// Resumable hosts use display: contents; surface concrete child elements for inspection.
|
|
81
|
+
if (node.hasAttribute('data-fict-host')) {
|
|
82
|
+
const children = Array.from(node.children).filter(
|
|
83
|
+
(child): child is HTMLElement => child instanceof HTMLElement,
|
|
84
|
+
)
|
|
85
|
+
if (children.length > 0) return children
|
|
86
|
+
}
|
|
87
|
+
return [node]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return []
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function annotateComponentElements(
|
|
94
|
+
elements: HTMLElement[],
|
|
95
|
+
componentId: number,
|
|
96
|
+
componentName: string,
|
|
97
|
+
): void {
|
|
98
|
+
for (const element of elements) {
|
|
99
|
+
element.setAttribute('data-fict-component', componentName)
|
|
100
|
+
element.setAttribute('data-fict-component-id', String(componentId))
|
|
101
|
+
const annotated = element as DevtoolsAnnotatedElement
|
|
102
|
+
annotated.__fict_component_id__ = componentId
|
|
103
|
+
annotated.__fict_component_name__ = componentName
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
// ============================================================================
|
|
68
108
|
// Main Render Function
|
|
69
109
|
// ============================================================================
|
|
@@ -282,12 +322,13 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
282
322
|
// Create a fresh hook context for this component instance.
|
|
283
323
|
// This preserves slot state across re-renders driven by __fictRender.
|
|
284
324
|
const hook = isDev ? getDevtoolsHook() : undefined
|
|
325
|
+
const componentName = vnode.type.name || 'Anonymous'
|
|
285
326
|
const parentId = hook ? __fictGetCurrentComponentId() : undefined
|
|
286
327
|
const componentId = hook ? nextComponentId++ : undefined
|
|
287
328
|
|
|
288
329
|
// Register component
|
|
289
330
|
if (hook?.registerComponent && componentId !== undefined) {
|
|
290
|
-
hook.registerComponent(componentId,
|
|
331
|
+
hook.registerComponent(componentId, componentName, parentId)
|
|
291
332
|
}
|
|
292
333
|
|
|
293
334
|
const ctx = __fictPushContext()
|
|
@@ -300,11 +341,16 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
300
341
|
|
|
301
342
|
try {
|
|
302
343
|
const rendered = vnode.type(props)
|
|
344
|
+
let mountElements: HTMLElement[] | undefined
|
|
345
|
+
|
|
346
|
+
if (hook && componentId !== undefined) {
|
|
347
|
+
hook.componentRender?.(componentId)
|
|
348
|
+
}
|
|
303
349
|
|
|
304
350
|
// Register lifecycle hooks
|
|
305
351
|
if (hook && componentId !== undefined) {
|
|
306
352
|
onMount(() => {
|
|
307
|
-
hook.componentMount?.(componentId)
|
|
353
|
+
hook.componentMount?.(componentId, mountElements)
|
|
308
354
|
})
|
|
309
355
|
onCleanup(() => hook.componentUnmount?.(componentId))
|
|
310
356
|
}
|
|
@@ -332,10 +378,18 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
332
378
|
} else {
|
|
333
379
|
host.appendChild(content)
|
|
334
380
|
}
|
|
381
|
+
if (hook && componentId !== undefined) {
|
|
382
|
+
mountElements = collectComponentMountElements(host)
|
|
383
|
+
annotateComponentElements(mountElements, componentId, componentName)
|
|
384
|
+
}
|
|
335
385
|
return host as DOMElement
|
|
336
386
|
}
|
|
337
|
-
|
|
338
|
-
|
|
387
|
+
const componentRoot = createElementWithContext(rendered as FictNode, namespace)
|
|
388
|
+
if (hook && componentId !== undefined) {
|
|
389
|
+
mountElements = collectComponentMountElements(componentRoot)
|
|
390
|
+
annotateComponentElements(mountElements, componentId, componentName)
|
|
391
|
+
}
|
|
392
|
+
return componentRoot
|
|
339
393
|
} catch (err) {
|
|
340
394
|
if (handleSuspend(err as any)) {
|
|
341
395
|
return document.createComment('fict:suspend')
|
package/src/effect.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
runCleanupList,
|
|
7
7
|
withEffectCleanups,
|
|
8
8
|
} from './lifecycle'
|
|
9
|
-
import { effectWithCleanup } from './signal'
|
|
9
|
+
import { effectWithCleanup, type EffectOptions } from './signal'
|
|
10
10
|
import type { Cleanup } from './types'
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -15,7 +15,7 @@ import type { Cleanup } from './types'
|
|
|
15
15
|
*/
|
|
16
16
|
export type Effect = () => void | Cleanup
|
|
17
17
|
|
|
18
|
-
export function createEffect(fn: Effect): () => void {
|
|
18
|
+
export function createEffect(fn: Effect, options?: EffectOptions): () => void {
|
|
19
19
|
let cleanups: Cleanup[] = []
|
|
20
20
|
const rootForError = getCurrentRoot()
|
|
21
21
|
|
|
@@ -47,7 +47,7 @@ export function createEffect(fn: Effect): () => void {
|
|
|
47
47
|
cleanups = bucket
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError)
|
|
50
|
+
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError, options)
|
|
51
51
|
const teardown = () => {
|
|
52
52
|
runCleanupList(cleanups)
|
|
53
53
|
disposeEffect()
|
|
@@ -60,7 +60,7 @@ export function createEffect(fn: Effect): () => void {
|
|
|
60
60
|
|
|
61
61
|
export const $effect = createEffect
|
|
62
62
|
|
|
63
|
-
export function createRenderEffect(fn: Effect): () => void {
|
|
63
|
+
export function createRenderEffect(fn: Effect, options?: EffectOptions): () => void {
|
|
64
64
|
let cleanup: Cleanup | undefined
|
|
65
65
|
const rootForError = getCurrentRoot()
|
|
66
66
|
|
|
@@ -91,7 +91,7 @@ export function createRenderEffect(fn: Effect): () => void {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError)
|
|
94
|
+
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError, options)
|
|
95
95
|
const teardown = () => {
|
|
96
96
|
if (cleanup) {
|
|
97
97
|
cleanup()
|
package/src/hooks.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createSignal,
|
|
5
5
|
type SignalAccessor,
|
|
6
6
|
type ComputedAccessor,
|
|
7
|
+
type EffectOptions,
|
|
7
8
|
type MemoOptions,
|
|
8
9
|
type SignalOptions,
|
|
9
10
|
} from './signal'
|
|
@@ -118,17 +119,24 @@ export function __fictUseMemo<T>(
|
|
|
118
119
|
return ctx.slots[index] as ComputedAccessor<T>
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
export function __fictUseEffect(
|
|
122
|
+
export function __fictUseEffect(
|
|
123
|
+
ctx: HookContext,
|
|
124
|
+
fn: () => void,
|
|
125
|
+
optionsOrSlot?: number | EffectOptions,
|
|
126
|
+
slot?: number,
|
|
127
|
+
): void {
|
|
128
|
+
const options = typeof optionsOrSlot === 'number' ? undefined : optionsOrSlot
|
|
129
|
+
const resolvedSlot = typeof optionsOrSlot === 'number' ? optionsOrSlot : slot
|
|
122
130
|
// fix: When a slot number is provided, we trust the compiler has allocated this slot.
|
|
123
131
|
// This allows effects inside conditional callbacks to work even outside render context.
|
|
124
132
|
// The slot number proves this is a known, statically-allocated effect location.
|
|
125
|
-
if (
|
|
126
|
-
if (ctx.slots[
|
|
133
|
+
if (resolvedSlot !== undefined) {
|
|
134
|
+
if (ctx.slots[resolvedSlot]) {
|
|
127
135
|
// Effect already exists, nothing to do
|
|
128
136
|
return
|
|
129
137
|
}
|
|
130
138
|
// Create the effect even outside render context - the slot number proves validity
|
|
131
|
-
ctx.slots[
|
|
139
|
+
ctx.slots[resolvedSlot] = createEffect(fn, options)
|
|
132
140
|
return
|
|
133
141
|
}
|
|
134
142
|
|
|
@@ -136,7 +144,7 @@ export function __fictUseEffect(ctx: HookContext, fn: () => void, slot?: number)
|
|
|
136
144
|
assertRenderContext(ctx, '__fictUseEffect')
|
|
137
145
|
const index = ctx.cursor++
|
|
138
146
|
if (!ctx.slots[index]) {
|
|
139
|
-
ctx.slots[index] = createEffect(fn)
|
|
147
|
+
ctx.slots[index] = createEffect(fn, options)
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight internal list helpers for compiler-generated keyed list paths.
|
|
3
|
+
*
|
|
4
|
+
* This subpath avoids pulling the broad `@fictjs/runtime/internal` barrel when
|
|
5
|
+
* code only needs list primitives.
|
|
6
|
+
*/
|
|
7
|
+
export { createKeyedList, toNodeArray, type KeyedListBinding } from '../list-helpers'
|
package/src/internal.ts
CHANGED
package/src/lifecycle.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { enterRootGuard, exitRootGuard } from './cycle-guard'
|
|
2
|
+
import { getDevtoolsHook } from './devtools'
|
|
2
3
|
import type { Cleanup, ErrorInfo, SuspenseToken } from './types'
|
|
3
4
|
|
|
4
5
|
const isDev =
|
|
@@ -29,9 +30,39 @@ let currentRoot: RootContext | undefined
|
|
|
29
30
|
let currentEffectCleanups: Cleanup[] | undefined
|
|
30
31
|
const globalErrorHandlers = new WeakMap<RootContext, ErrorHandler[]>()
|
|
31
32
|
const globalSuspenseHandlers = new WeakMap<RootContext, SuspenseHandler[]>()
|
|
33
|
+
const rootDevtoolsIds = new WeakMap<RootContext, number>()
|
|
34
|
+
let nextRootDevtoolsId = 0
|
|
35
|
+
|
|
36
|
+
function registerRootDevtools(root: RootContext): void {
|
|
37
|
+
if (!isDev) return
|
|
38
|
+
const hook = getDevtoolsHook()
|
|
39
|
+
if (!hook?.registerRoot) return
|
|
40
|
+
const id = ++nextRootDevtoolsId
|
|
41
|
+
rootDevtoolsIds.set(root, id)
|
|
42
|
+
hook.registerRoot(id)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function disposeRootDevtools(root: RootContext): void {
|
|
46
|
+
if (!isDev) return
|
|
47
|
+
const id = rootDevtoolsIds.get(root)
|
|
48
|
+
if (id === undefined) return
|
|
49
|
+
const hook = getDevtoolsHook()
|
|
50
|
+
hook?.disposeRoot?.(id)
|
|
51
|
+
rootDevtoolsIds.delete(root)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setRootSuspendDevtools(root: RootContext, suspended: boolean): void {
|
|
55
|
+
if (!isDev) return
|
|
56
|
+
const id = rootDevtoolsIds.get(root)
|
|
57
|
+
if (id === undefined) return
|
|
58
|
+
const hook = getDevtoolsHook()
|
|
59
|
+
hook?.rootSuspend?.(id, suspended)
|
|
60
|
+
}
|
|
32
61
|
|
|
33
62
|
export function createRootContext(parent?: RootContext): RootContext {
|
|
34
|
-
|
|
63
|
+
const root = { parent, cleanups: [], destroyCallbacks: [], suspended: false }
|
|
64
|
+
registerRootDevtools(root)
|
|
65
|
+
return root
|
|
35
66
|
}
|
|
36
67
|
|
|
37
68
|
export function pushRoot(root: RootContext): RootContext | undefined {
|
|
@@ -122,6 +153,7 @@ export function destroyRoot(root: RootContext): void {
|
|
|
122
153
|
if (globalSuspenseHandlers.has(root)) {
|
|
123
154
|
globalSuspenseHandlers.delete(root)
|
|
124
155
|
}
|
|
156
|
+
disposeRootDevtools(root)
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
export function createRoot<T>(
|
|
@@ -285,7 +317,10 @@ export function handleSuspend(
|
|
|
285
317
|
const handled = handler(token)
|
|
286
318
|
if (handled !== false) {
|
|
287
319
|
// Only set suspended = true when a handler actually handles the token
|
|
288
|
-
if (originRoot)
|
|
320
|
+
if (originRoot) {
|
|
321
|
+
originRoot.suspended = true
|
|
322
|
+
setRootSuspendDevtools(originRoot, true)
|
|
323
|
+
}
|
|
289
324
|
return true
|
|
290
325
|
}
|
|
291
326
|
}
|
|
@@ -304,7 +339,10 @@ export function handleSuspend(
|
|
|
304
339
|
const handled = handler(token)
|
|
305
340
|
if (handled !== false) {
|
|
306
341
|
// Only set suspended = true when a handler actually handles the token
|
|
307
|
-
if (originRoot)
|
|
342
|
+
if (originRoot) {
|
|
343
|
+
originRoot.suspended = true
|
|
344
|
+
setRootSuspendDevtools(originRoot, true)
|
|
345
|
+
}
|
|
308
346
|
return true
|
|
309
347
|
}
|
|
310
348
|
}
|
package/src/list-helpers.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { insertNodesBefore, removeNodes, toNodeArray }
|
|
|
30
30
|
const isDev =
|
|
31
31
|
typeof __DEV__ !== 'undefined'
|
|
32
32
|
? __DEV__
|
|
33
|
-
: typeof process
|
|
33
|
+
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
34
34
|
|
|
35
35
|
const isShadowRoot = (node: Node): node is ShadowRoot =>
|
|
36
36
|
typeof ShadowRoot !== 'undefined' && node instanceof ShadowRoot
|