@fictjs/runtime 0.9.0 → 0.11.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 +9 -9
- package/dist/advanced.d.cts +4 -4
- package/dist/advanced.d.ts +4 -4
- package/dist/advanced.js +4 -4
- package/dist/{binding-BWchH3Kp.d.cts → binding-DcnhUSQK.d.ts} +5 -3
- package/dist/{binding-BWchH3Kp.d.ts → binding-FRyTeLDn.d.cts} +5 -3
- package/dist/{chunk-FVX77557.js → chunk-2UR2UWE2.js} +3 -3
- package/dist/{chunk-LBE6DC3V.cjs → chunk-44EQF3AR.cjs} +63 -52
- package/dist/chunk-44EQF3AR.cjs.map +1 -0
- package/dist/{chunk-OAM7HABA.cjs → chunk-4QGEN5SJ.cjs} +340 -263
- package/dist/chunk-4QGEN5SJ.cjs.map +1 -0
- package/dist/{chunk-PD6IQY2Y.cjs → chunk-C5IE4WUG.cjs} +8 -8
- package/dist/{chunk-PD6IQY2Y.cjs.map → chunk-C5IE4WUG.cjs.map} +1 -1
- package/dist/{chunk-DXG3TARY.js → chunk-DIK33H5U.js} +202 -30
- package/dist/chunk-DIK33H5U.js.map +1 -0
- package/dist/{chunk-JVYH76ZX.js → chunk-FESAXMHT.js} +7 -6
- package/dist/{chunk-JVYH76ZX.js.map → chunk-FESAXMHT.js.map} +1 -1
- package/dist/chunk-FHQZCAAK.cjs +112 -0
- package/dist/chunk-FHQZCAAK.cjs.map +1 -0
- package/dist/{chunk-UBFDB6OL.cjs → chunk-QNMYVXRL.cjs} +222 -50
- package/dist/chunk-QNMYVXRL.cjs.map +1 -0
- package/dist/{chunk-N6ODUM2Y.js → chunk-S63VBIWN.js} +27 -16
- package/dist/chunk-S63VBIWN.js.map +1 -0
- package/dist/{chunk-T2LNV5Q5.js → chunk-WIHNVN6L.js} +153 -76
- package/dist/chunk-WIHNVN6L.js.map +1 -0
- package/dist/{devtools-BDp76luf.d.ts → devtools-BtIkN77t.d.cts} +14 -2
- package/dist/{devtools-5AipK9CX.d.cts → devtools-D2z4llpA.d.ts} +14 -2
- package/dist/index.cjs +60 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.dev.js +300 -74
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/internal-list.cjs +4 -4
- package/dist/internal-list.d.cts +2 -2
- package/dist/internal-list.d.ts +2 -2
- package/dist/internal-list.js +3 -3
- package/dist/internal.cjs +5 -5
- package/dist/internal.d.cts +6 -6
- package/dist/internal.d.ts +6 -6
- package/dist/internal.js +4 -4
- 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-DL5DOFcO.d.ts → list-BKM6YOPq.d.ts} +2 -2
- package/dist/{list-hP7hQ9Vk.d.cts → list-Bi8dDF8Q.d.cts} +2 -2
- package/dist/loader.cjs +34 -28
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.d.cts +2 -2
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +17 -11
- package/dist/loader.js.map +1 -1
- package/dist/{props-BpZz0AOq.d.cts → props-9chMyBGb.d.cts} +2 -2
- package/dist/{props-CjLH0JE-.d.ts → props-D1nj2p_3.d.ts} +2 -2
- package/dist/{resume-BJ4oHLi_.d.cts → resume-C5IKAIdh.d.ts} +2 -2
- package/dist/{resume-CuyJWXP_.d.ts → resume-DPZxmA95.d.cts} +2 -2
- package/dist/{scope-jPt5DHRT.d.ts → scope-BSkhJr0a.d.ts} +1 -1
- package/dist/{scope-BJCtq8hJ.d.cts → scope-Bn3sxem5.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 +2 -2
- package/src/binding.ts +59 -29
- package/src/context.ts +4 -3
- package/src/devtools.ts +19 -2
- package/src/dom.ts +122 -42
- package/src/effect.ts +5 -5
- package/src/error-boundary.ts +5 -5
- package/src/hooks.ts +13 -5
- package/src/lifecycle.ts +48 -3
- package/src/list-helpers.ts +30 -13
- package/src/loader.ts +20 -12
- package/src/node-ops.ts +8 -5
- package/src/signal.ts +191 -18
- package/src/suspense.ts +5 -4
- package/src/transition.ts +9 -3
- package/dist/chunk-DXG3TARY.js.map +0 -1
- package/dist/chunk-LBE6DC3V.cjs.map +0 -1
- package/dist/chunk-N6ODUM2Y.js.map +0 -1
- package/dist/chunk-OAM7HABA.cjs.map +0 -1
- package/dist/chunk-PG4QX2I2.cjs +0 -111
- package/dist/chunk-PG4QX2I2.cjs.map +0 -1
- package/dist/chunk-T2LNV5Q5.js.map +0 -1
- package/dist/chunk-UBFDB6OL.cjs.map +0 -1
- /package/dist/{chunk-FVX77557.js.map → chunk-2UR2UWE2.js.map} +0 -0
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
|
// ============================================================================
|
|
@@ -83,6 +123,7 @@ let nextComponentId = 1
|
|
|
83
123
|
*/
|
|
84
124
|
export function render(view: () => FictNode, container: HTMLElement): () => void {
|
|
85
125
|
const root = createRootContext()
|
|
126
|
+
root.ownerDocument = container.ownerDocument ?? document
|
|
86
127
|
const prev = pushRoot(root)
|
|
87
128
|
let dom: DOMElement = undefined as unknown as DOMElement
|
|
88
129
|
try {
|
|
@@ -126,6 +167,7 @@ export function render(view: () => FictNode, container: HTMLElement): () => void
|
|
|
126
167
|
*/
|
|
127
168
|
export function hydrateComponent(view: () => FictNode, container: HTMLElement): () => void {
|
|
128
169
|
const root = createRootContext()
|
|
170
|
+
root.ownerDocument = container.ownerDocument ?? document
|
|
129
171
|
const prev = pushRoot(root)
|
|
130
172
|
|
|
131
173
|
// Enable hydration flags for bindings that check __fictIsHydrating()
|
|
@@ -169,7 +211,7 @@ export function hydrateComponent(view: () => FictNode, container: HTMLElement):
|
|
|
169
211
|
* - Reactive values (functions returning any of the above)
|
|
170
212
|
*/
|
|
171
213
|
export function createElement(node: FictNode): DOMElement {
|
|
172
|
-
return createElementWithContext(node, null)
|
|
214
|
+
return createElementWithContext(node, null, resolveOwnerDocument())
|
|
173
215
|
}
|
|
174
216
|
|
|
175
217
|
function resolveNamespace(tagName: string, namespace: NamespaceContext): NamespaceContext {
|
|
@@ -181,7 +223,15 @@ function resolveNamespace(tagName: string, namespace: NamespaceContext): Namespa
|
|
|
181
223
|
return null
|
|
182
224
|
}
|
|
183
225
|
|
|
184
|
-
function
|
|
226
|
+
function resolveOwnerDocument(ownerDocument?: Document): Document {
|
|
227
|
+
return ownerDocument ?? getCurrentRoot()?.ownerDocument ?? document
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function createElementWithContext(
|
|
231
|
+
node: FictNode,
|
|
232
|
+
namespace: NamespaceContext,
|
|
233
|
+
ownerDocument: Document,
|
|
234
|
+
): DOMElement {
|
|
185
235
|
// Already a DOM node - pass through
|
|
186
236
|
if (node instanceof Node) {
|
|
187
237
|
return node
|
|
@@ -189,22 +239,22 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
189
239
|
|
|
190
240
|
// Null/undefined/false - empty placeholder
|
|
191
241
|
if (node === null || node === undefined || node === false) {
|
|
192
|
-
return
|
|
242
|
+
return ownerDocument.createTextNode('')
|
|
193
243
|
}
|
|
194
244
|
|
|
195
245
|
// Reactive getter function - resolve to actual node
|
|
196
246
|
if (isReactive(node)) {
|
|
197
247
|
const resolved = (node as () => FictNode)()
|
|
198
248
|
if (resolved === node) {
|
|
199
|
-
return
|
|
249
|
+
return ownerDocument.createTextNode('')
|
|
200
250
|
}
|
|
201
|
-
return createElementWithContext(resolved, namespace)
|
|
251
|
+
return createElementWithContext(resolved, namespace, ownerDocument)
|
|
202
252
|
}
|
|
203
253
|
|
|
204
254
|
// Non-reactive function values are not valid DOM nodes.
|
|
205
255
|
// Keep callback values inert instead of stringifying function source.
|
|
206
256
|
if (typeof node === 'function') {
|
|
207
|
-
return
|
|
257
|
+
return ownerDocument.createTextNode('')
|
|
208
258
|
}
|
|
209
259
|
|
|
210
260
|
if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
|
|
@@ -225,26 +275,26 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
225
275
|
.catch(() => undefined)
|
|
226
276
|
}
|
|
227
277
|
}
|
|
228
|
-
return
|
|
278
|
+
return createElementWithContext(handle.marker as FictNode, namespace, ownerDocument)
|
|
229
279
|
}
|
|
230
280
|
}
|
|
231
281
|
|
|
232
282
|
// Array - create fragment
|
|
233
283
|
if (Array.isArray(node)) {
|
|
234
|
-
const frag =
|
|
284
|
+
const frag = ownerDocument.createDocumentFragment()
|
|
235
285
|
for (const child of node) {
|
|
236
|
-
appendChildNode(frag, child, namespace)
|
|
286
|
+
appendChildNode(frag, child, namespace, ownerDocument)
|
|
237
287
|
}
|
|
238
288
|
return frag
|
|
239
289
|
}
|
|
240
290
|
|
|
241
291
|
// Primitive values - text node
|
|
242
292
|
if (typeof node === 'string' || typeof node === 'number') {
|
|
243
|
-
return
|
|
293
|
+
return ownerDocument.createTextNode(String(node))
|
|
244
294
|
}
|
|
245
295
|
|
|
246
296
|
if (typeof node === 'boolean') {
|
|
247
|
-
return
|
|
297
|
+
return ownerDocument.createTextNode('')
|
|
248
298
|
}
|
|
249
299
|
|
|
250
300
|
// VNode
|
|
@@ -282,12 +332,13 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
282
332
|
// Create a fresh hook context for this component instance.
|
|
283
333
|
// This preserves slot state across re-renders driven by __fictRender.
|
|
284
334
|
const hook = isDev ? getDevtoolsHook() : undefined
|
|
335
|
+
const componentName = vnode.type.name || 'Anonymous'
|
|
285
336
|
const parentId = hook ? __fictGetCurrentComponentId() : undefined
|
|
286
337
|
const componentId = hook ? nextComponentId++ : undefined
|
|
287
338
|
|
|
288
339
|
// Register component
|
|
289
340
|
if (hook?.registerComponent && componentId !== undefined) {
|
|
290
|
-
hook.registerComponent(componentId,
|
|
341
|
+
hook.registerComponent(componentId, componentName, parentId)
|
|
291
342
|
}
|
|
292
343
|
|
|
293
344
|
const ctx = __fictPushContext()
|
|
@@ -300,22 +351,27 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
300
351
|
|
|
301
352
|
try {
|
|
302
353
|
const rendered = vnode.type(props)
|
|
354
|
+
let mountElements: HTMLElement[] | undefined
|
|
355
|
+
|
|
356
|
+
if (hook && componentId !== undefined) {
|
|
357
|
+
hook.componentRender?.(componentId)
|
|
358
|
+
}
|
|
303
359
|
|
|
304
360
|
// Register lifecycle hooks
|
|
305
361
|
if (hook && componentId !== undefined) {
|
|
306
362
|
onMount(() => {
|
|
307
|
-
hook.componentMount?.(componentId)
|
|
363
|
+
hook.componentMount?.(componentId, mountElements)
|
|
308
364
|
})
|
|
309
365
|
onCleanup(() => hook.componentUnmount?.(componentId))
|
|
310
366
|
}
|
|
311
367
|
if (__fictIsResumable() && !__fictIsHydrating()) {
|
|
312
|
-
const content = createElementWithContext(rendered as FictNode, namespace)
|
|
368
|
+
const content = createElementWithContext(rendered as FictNode, namespace, ownerDocument)
|
|
313
369
|
const host =
|
|
314
370
|
namespace === 'svg'
|
|
315
|
-
?
|
|
371
|
+
? ownerDocument.createElementNS(SVG_NS, 'fict-host')
|
|
316
372
|
: namespace === 'mathml'
|
|
317
|
-
?
|
|
318
|
-
:
|
|
373
|
+
? ownerDocument.createElementNS(MATHML_NS, 'fict-host')
|
|
374
|
+
: ownerDocument.createElement('fict-host')
|
|
319
375
|
host.setAttribute('data-fict-host', '')
|
|
320
376
|
if (namespace === null && (host as HTMLElement).style) {
|
|
321
377
|
;(host as HTMLElement).style.display = 'contents'
|
|
@@ -332,13 +388,21 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
332
388
|
} else {
|
|
333
389
|
host.appendChild(content)
|
|
334
390
|
}
|
|
391
|
+
if (hook && componentId !== undefined) {
|
|
392
|
+
mountElements = collectComponentMountElements(host)
|
|
393
|
+
annotateComponentElements(mountElements, componentId, componentName)
|
|
394
|
+
}
|
|
335
395
|
return host as DOMElement
|
|
336
396
|
}
|
|
337
|
-
|
|
338
|
-
|
|
397
|
+
const componentRoot = createElementWithContext(rendered as FictNode, namespace, ownerDocument)
|
|
398
|
+
if (hook && componentId !== undefined) {
|
|
399
|
+
mountElements = collectComponentMountElements(componentRoot)
|
|
400
|
+
annotateComponentElements(mountElements, componentId, componentName)
|
|
401
|
+
}
|
|
402
|
+
return componentRoot
|
|
339
403
|
} catch (err) {
|
|
340
404
|
if (handleSuspend(err as any)) {
|
|
341
|
-
return
|
|
405
|
+
return ownerDocument.createComment('fict:suspend')
|
|
342
406
|
}
|
|
343
407
|
handleError(err, { source: 'render', componentName: vnode.type.name })
|
|
344
408
|
throw err
|
|
@@ -349,9 +413,9 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
349
413
|
|
|
350
414
|
// Fragment
|
|
351
415
|
if (vnode.type === Fragment) {
|
|
352
|
-
const frag =
|
|
416
|
+
const frag = ownerDocument.createDocumentFragment()
|
|
353
417
|
const children = vnode.props?.children as FictNode | FictNode[] | undefined
|
|
354
|
-
appendChildren(frag, children, namespace)
|
|
418
|
+
appendChildren(frag, children, namespace, ownerDocument)
|
|
355
419
|
return frag
|
|
356
420
|
}
|
|
357
421
|
|
|
@@ -360,15 +424,16 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
360
424
|
const resolvedNamespace = resolveNamespace(tagName, namespace)
|
|
361
425
|
const el =
|
|
362
426
|
resolvedNamespace === 'svg'
|
|
363
|
-
?
|
|
427
|
+
? ownerDocument.createElementNS(SVG_NS, tagName)
|
|
364
428
|
: resolvedNamespace === 'mathml'
|
|
365
|
-
?
|
|
366
|
-
:
|
|
429
|
+
? ownerDocument.createElementNS(MATHML_NS, tagName)
|
|
430
|
+
: ownerDocument.createElement(tagName)
|
|
367
431
|
applyProps(el, vnode.props ?? {}, resolvedNamespace === 'svg')
|
|
368
432
|
appendChildren(
|
|
369
433
|
el as unknown as ParentNode & Node,
|
|
370
434
|
vnode.props?.children as FictNode | FictNode[] | undefined,
|
|
371
435
|
tagName === 'foreignObject' ? null : resolvedNamespace,
|
|
436
|
+
ownerDocument,
|
|
372
437
|
)
|
|
373
438
|
return el as DOMElement
|
|
374
439
|
}
|
|
@@ -388,10 +453,10 @@ export function template(
|
|
|
388
453
|
isSVG?: boolean,
|
|
389
454
|
isMathML?: boolean,
|
|
390
455
|
): () => Node {
|
|
391
|
-
|
|
456
|
+
const nodeByDocument = new WeakMap<Document, Node>()
|
|
392
457
|
|
|
393
|
-
const create = (): Node => {
|
|
394
|
-
const t =
|
|
458
|
+
const create = (ownerDocument: Document): Node => {
|
|
459
|
+
const t = ownerDocument.createElement('template')
|
|
395
460
|
|
|
396
461
|
if (isSVG) {
|
|
397
462
|
// fix: Wrap HTML in <svg> to parse content in SVG namespace
|
|
@@ -409,7 +474,7 @@ export function template(
|
|
|
409
474
|
return wrapper.firstChild!
|
|
410
475
|
}
|
|
411
476
|
// Preserve all root nodes by returning a fragment
|
|
412
|
-
const fragment =
|
|
477
|
+
const fragment = ownerDocument.createDocumentFragment()
|
|
413
478
|
fragment.append(...Array.from(wrapper.childNodes))
|
|
414
479
|
return fragment
|
|
415
480
|
}
|
|
@@ -429,7 +494,7 @@ export function template(
|
|
|
429
494
|
return wrapper.firstChild!
|
|
430
495
|
}
|
|
431
496
|
// Preserve all root nodes by returning a fragment
|
|
432
|
-
const fragment =
|
|
497
|
+
const fragment = ownerDocument.createDocumentFragment()
|
|
433
498
|
fragment.append(...Array.from(wrapper.childNodes))
|
|
434
499
|
return fragment
|
|
435
500
|
}
|
|
@@ -450,17 +515,26 @@ export function template(
|
|
|
450
515
|
return content
|
|
451
516
|
}
|
|
452
517
|
|
|
518
|
+
const getBase = (ownerDocument: Document): Node => {
|
|
519
|
+
const cached = nodeByDocument.get(ownerDocument)
|
|
520
|
+
if (cached) return cached
|
|
521
|
+
const created = create(ownerDocument)
|
|
522
|
+
nodeByDocument.set(ownerDocument, created)
|
|
523
|
+
return created
|
|
524
|
+
}
|
|
525
|
+
|
|
453
526
|
// Create the cloning function
|
|
454
527
|
const fn = isImportNode
|
|
455
528
|
? () =>
|
|
456
529
|
untrack(() => {
|
|
457
|
-
const
|
|
530
|
+
const ownerDocument = resolveOwnerDocument()
|
|
531
|
+
const base = getBase(ownerDocument)
|
|
458
532
|
return isHydratingActive()
|
|
459
|
-
? claimNodes(base, () =>
|
|
460
|
-
:
|
|
533
|
+
? claimNodes(base, () => ownerDocument.importNode(base, true))
|
|
534
|
+
: ownerDocument.importNode(base, true)
|
|
461
535
|
})
|
|
462
536
|
: () => {
|
|
463
|
-
const base =
|
|
537
|
+
const base = getBase(resolveOwnerDocument())
|
|
464
538
|
return isHydratingActive()
|
|
465
539
|
? claimNodes(base, () => base.cloneNode(true))
|
|
466
540
|
: base.cloneNode(true)
|
|
@@ -499,7 +573,10 @@ function appendChildNode(
|
|
|
499
573
|
parent: ParentNode & Node,
|
|
500
574
|
child: FictNode,
|
|
501
575
|
namespace: NamespaceContext,
|
|
576
|
+
ownerDocument: Document,
|
|
502
577
|
): void {
|
|
578
|
+
const parentOwnerDocument = parent.ownerDocument ?? ownerDocument
|
|
579
|
+
|
|
503
580
|
// Skip nullish values
|
|
504
581
|
if (child === null || child === undefined || child === false) {
|
|
505
582
|
return
|
|
@@ -507,7 +584,7 @@ function appendChildNode(
|
|
|
507
584
|
|
|
508
585
|
// Handle BindingHandle (recursive)
|
|
509
586
|
if (isBindingHandle(child)) {
|
|
510
|
-
appendChildNode(parent, child.marker, namespace)
|
|
587
|
+
appendChildNode(parent, child.marker, namespace, parentOwnerDocument)
|
|
511
588
|
// Flush pending nodes now that markers are in the DOM
|
|
512
589
|
child.flush?.()
|
|
513
590
|
return
|
|
@@ -519,7 +596,9 @@ function appendChildNode(
|
|
|
519
596
|
if (typeof child === 'function') {
|
|
520
597
|
const childGetter = child as () => FictNode
|
|
521
598
|
if (isReactive(childGetter)) {
|
|
522
|
-
createChildBinding(parent, childGetter, node =>
|
|
599
|
+
createChildBinding(parent, childGetter, node =>
|
|
600
|
+
createElementWithContext(node, namespace, parentOwnerDocument),
|
|
601
|
+
)
|
|
523
602
|
return
|
|
524
603
|
}
|
|
525
604
|
return
|
|
@@ -528,7 +607,7 @@ function appendChildNode(
|
|
|
528
607
|
// Static child - create element and append
|
|
529
608
|
if (Array.isArray(child)) {
|
|
530
609
|
for (const item of child) {
|
|
531
|
-
appendChildNode(parent, item, namespace)
|
|
610
|
+
appendChildNode(parent, item, namespace, parentOwnerDocument)
|
|
532
611
|
}
|
|
533
612
|
return
|
|
534
613
|
}
|
|
@@ -536,16 +615,16 @@ function appendChildNode(
|
|
|
536
615
|
// Cast to Node for remaining logic
|
|
537
616
|
let domNode: Node
|
|
538
617
|
if (typeof child !== 'object' || child === null) {
|
|
539
|
-
domNode =
|
|
618
|
+
domNode = parentOwnerDocument.createTextNode(String(child ?? ''))
|
|
540
619
|
} else {
|
|
541
|
-
domNode = createElementWithContext(child as any, namespace) as Node
|
|
620
|
+
domNode = createElementWithContext(child as any, namespace, parentOwnerDocument) as Node
|
|
542
621
|
}
|
|
543
622
|
|
|
544
623
|
// Handle DocumentFragment manually to avoid JSDOM issues
|
|
545
624
|
if (domNode.nodeType === 11) {
|
|
546
625
|
const children = Array.from(domNode.childNodes)
|
|
547
626
|
for (const node of children) {
|
|
548
|
-
appendChildNode(parent, node as FictNode, namespace)
|
|
627
|
+
appendChildNode(parent, node as FictNode, namespace, parentOwnerDocument)
|
|
549
628
|
}
|
|
550
629
|
return
|
|
551
630
|
}
|
|
@@ -573,17 +652,18 @@ function appendChildren(
|
|
|
573
652
|
parent: ParentNode & Node,
|
|
574
653
|
children: FictNode | FictNode[] | undefined,
|
|
575
654
|
namespace: NamespaceContext,
|
|
655
|
+
ownerDocument: Document,
|
|
576
656
|
): void {
|
|
577
657
|
if (children === undefined) return
|
|
578
658
|
|
|
579
659
|
if (Array.isArray(children)) {
|
|
580
660
|
for (const child of children) {
|
|
581
|
-
appendChildren(parent, child, namespace)
|
|
661
|
+
appendChildren(parent, child, namespace, ownerDocument)
|
|
582
662
|
}
|
|
583
663
|
return
|
|
584
664
|
}
|
|
585
665
|
|
|
586
|
-
appendChildNode(parent, children, namespace)
|
|
666
|
+
appendChildNode(parent, children, namespace, ownerDocument)
|
|
587
667
|
}
|
|
588
668
|
|
|
589
669
|
// ============================================================================
|
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/error-boundary.ts
CHANGED
|
@@ -19,11 +19,11 @@ interface ErrorBoundaryProps extends BaseProps {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
|
|
22
|
-
const fragment = document.createDocumentFragment()
|
|
23
|
-
const marker = document.createComment('fict:error-boundary')
|
|
24
|
-
fragment.appendChild(marker)
|
|
25
|
-
|
|
26
22
|
const hostRoot = getCurrentRoot()
|
|
23
|
+
const markerOwnerDocument = hostRoot?.ownerDocument ?? document
|
|
24
|
+
const fragment = markerOwnerDocument.createDocumentFragment()
|
|
25
|
+
const marker = markerOwnerDocument.createComment('fict:error-boundary')
|
|
26
|
+
fragment.appendChild(marker)
|
|
27
27
|
|
|
28
28
|
let cleanup: (() => void) | undefined
|
|
29
29
|
let activeNodes: Node[] = []
|
|
@@ -58,7 +58,7 @@ export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
|
|
|
58
58
|
let nodes: Node[] = []
|
|
59
59
|
try {
|
|
60
60
|
const output = createElement(value)
|
|
61
|
-
nodes = toNodeArray(output)
|
|
61
|
+
nodes = toNodeArray(output, markerOwnerDocument)
|
|
62
62
|
const parentNode = marker.parentNode as (ParentNode & Node) | null
|
|
63
63
|
if (parentNode) {
|
|
64
64
|
insertNodesBefore(parentNode, nodes, marker)
|
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
|
|
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 =
|
|
@@ -10,6 +11,7 @@ type LifecycleFn = () => void | Cleanup
|
|
|
10
11
|
|
|
11
12
|
export interface RootContext {
|
|
12
13
|
parent?: RootContext | undefined
|
|
14
|
+
ownerDocument?: Document | undefined
|
|
13
15
|
onMountCallbacks?: LifecycleFn[]
|
|
14
16
|
cleanups: Cleanup[]
|
|
15
17
|
destroyCallbacks: Cleanup[]
|
|
@@ -29,9 +31,45 @@ let currentRoot: RootContext | undefined
|
|
|
29
31
|
let currentEffectCleanups: Cleanup[] | undefined
|
|
30
32
|
const globalErrorHandlers = new WeakMap<RootContext, ErrorHandler[]>()
|
|
31
33
|
const globalSuspenseHandlers = new WeakMap<RootContext, SuspenseHandler[]>()
|
|
34
|
+
const rootDevtoolsIds = new WeakMap<RootContext, number>()
|
|
35
|
+
let nextRootDevtoolsId = 0
|
|
36
|
+
|
|
37
|
+
function registerRootDevtools(root: RootContext): void {
|
|
38
|
+
if (!isDev) return
|
|
39
|
+
const hook = getDevtoolsHook()
|
|
40
|
+
if (!hook?.registerRoot) return
|
|
41
|
+
const id = ++nextRootDevtoolsId
|
|
42
|
+
rootDevtoolsIds.set(root, id)
|
|
43
|
+
hook.registerRoot(id)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function disposeRootDevtools(root: RootContext): void {
|
|
47
|
+
if (!isDev) return
|
|
48
|
+
const id = rootDevtoolsIds.get(root)
|
|
49
|
+
if (id === undefined) return
|
|
50
|
+
const hook = getDevtoolsHook()
|
|
51
|
+
hook?.disposeRoot?.(id)
|
|
52
|
+
rootDevtoolsIds.delete(root)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function setRootSuspendDevtools(root: RootContext, suspended: boolean): void {
|
|
56
|
+
if (!isDev) return
|
|
57
|
+
const id = rootDevtoolsIds.get(root)
|
|
58
|
+
if (id === undefined) return
|
|
59
|
+
const hook = getDevtoolsHook()
|
|
60
|
+
hook?.rootSuspend?.(id, suspended)
|
|
61
|
+
}
|
|
32
62
|
|
|
33
63
|
export function createRootContext(parent?: RootContext): RootContext {
|
|
34
|
-
|
|
64
|
+
const root = {
|
|
65
|
+
parent,
|
|
66
|
+
ownerDocument: parent?.ownerDocument,
|
|
67
|
+
cleanups: [],
|
|
68
|
+
destroyCallbacks: [],
|
|
69
|
+
suspended: false,
|
|
70
|
+
}
|
|
71
|
+
registerRootDevtools(root)
|
|
72
|
+
return root
|
|
35
73
|
}
|
|
36
74
|
|
|
37
75
|
export function pushRoot(root: RootContext): RootContext | undefined {
|
|
@@ -122,6 +160,7 @@ export function destroyRoot(root: RootContext): void {
|
|
|
122
160
|
if (globalSuspenseHandlers.has(root)) {
|
|
123
161
|
globalSuspenseHandlers.delete(root)
|
|
124
162
|
}
|
|
163
|
+
disposeRootDevtools(root)
|
|
125
164
|
}
|
|
126
165
|
|
|
127
166
|
export function createRoot<T>(
|
|
@@ -285,7 +324,10 @@ export function handleSuspend(
|
|
|
285
324
|
const handled = handler(token)
|
|
286
325
|
if (handled !== false) {
|
|
287
326
|
// Only set suspended = true when a handler actually handles the token
|
|
288
|
-
if (originRoot)
|
|
327
|
+
if (originRoot) {
|
|
328
|
+
originRoot.suspended = true
|
|
329
|
+
setRootSuspendDevtools(originRoot, true)
|
|
330
|
+
}
|
|
289
331
|
return true
|
|
290
332
|
}
|
|
291
333
|
}
|
|
@@ -304,7 +346,10 @@ export function handleSuspend(
|
|
|
304
346
|
const handled = handler(token)
|
|
305
347
|
if (handled !== false) {
|
|
306
348
|
// Only set suspended = true when a handler actually handles the token
|
|
307
|
-
if (originRoot)
|
|
349
|
+
if (originRoot) {
|
|
350
|
+
originRoot.suspended = true
|
|
351
|
+
setRootSuspendDevtools(originRoot, true)
|
|
352
|
+
}
|
|
308
353
|
return true
|
|
309
354
|
}
|
|
310
355
|
}
|