@fictjs/runtime 0.9.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.
Files changed (75) hide show
  1. package/dist/advanced.cjs +9 -9
  2. package/dist/advanced.d.cts +4 -4
  3. package/dist/advanced.d.ts +4 -4
  4. package/dist/advanced.js +4 -4
  5. package/dist/{binding-BWchH3Kp.d.ts → binding-DUEukRxl.d.cts} +4 -2
  6. package/dist/{binding-BWchH3Kp.d.cts → binding-DqxS9ZQf.d.ts} +4 -2
  7. package/dist/{chunk-JVYH76ZX.js → chunk-2JRPPCG7.js} +3 -3
  8. package/dist/{chunk-FVX77557.js → chunk-DKA2I6ET.js} +3 -3
  9. package/dist/{chunk-UBFDB6OL.cjs → chunk-EQ5E4WOV.cjs} +216 -50
  10. package/dist/chunk-EQ5E4WOV.cjs.map +1 -0
  11. package/dist/{chunk-DXG3TARY.js → chunk-F4RVNXOL.js} +196 -30
  12. package/dist/chunk-F4RVNXOL.js.map +1 -0
  13. package/dist/{chunk-OAM7HABA.cjs → chunk-I4GKKAAY.cjs} +226 -182
  14. package/dist/chunk-I4GKKAAY.cjs.map +1 -0
  15. package/dist/{chunk-PG4QX2I2.cjs → chunk-K3DH5SD5.cjs} +17 -17
  16. package/dist/{chunk-PG4QX2I2.cjs.map → chunk-K3DH5SD5.cjs.map} +1 -1
  17. package/dist/{chunk-N6ODUM2Y.js → chunk-P4TZLFV6.js} +3 -3
  18. package/dist/{chunk-T2LNV5Q5.js → chunk-R6FINS25.js} +50 -6
  19. package/dist/chunk-R6FINS25.js.map +1 -0
  20. package/dist/{chunk-LBE6DC3V.cjs → chunk-SZLJCQFZ.cjs} +40 -40
  21. package/dist/{chunk-LBE6DC3V.cjs.map → chunk-SZLJCQFZ.cjs.map} +1 -1
  22. package/dist/{chunk-PD6IQY2Y.cjs → chunk-V7BC64W2.cjs} +8 -8
  23. package/dist/{chunk-PD6IQY2Y.cjs.map → chunk-V7BC64W2.cjs.map} +1 -1
  24. package/dist/{devtools-5AipK9CX.d.cts → devtools-C4Hgfa-S.d.ts} +14 -2
  25. package/dist/{devtools-BDp76luf.d.ts → devtools-CMxlJUTx.d.cts} +14 -2
  26. package/dist/index.cjs +42 -42
  27. package/dist/index.d.cts +5 -5
  28. package/dist/index.d.ts +5 -5
  29. package/dist/index.dev.js +230 -25
  30. package/dist/index.dev.js.map +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/internal-list.cjs +4 -4
  33. package/dist/internal-list.d.cts +2 -2
  34. package/dist/internal-list.d.ts +2 -2
  35. package/dist/internal-list.js +3 -3
  36. package/dist/internal.cjs +5 -5
  37. package/dist/internal.d.cts +6 -6
  38. package/dist/internal.d.ts +6 -6
  39. package/dist/internal.js +4 -4
  40. package/dist/jsx-dev-runtime.d.cts +671 -0
  41. package/dist/jsx-dev-runtime.d.ts +671 -0
  42. package/dist/jsx-runtime.d.cts +671 -0
  43. package/dist/jsx-runtime.d.ts +671 -0
  44. package/dist/{list-DL5DOFcO.d.ts → list-BBzsJhrm.d.ts} +1 -1
  45. package/dist/{list-hP7hQ9Vk.d.cts → list-_NJCcjl1.d.cts} +1 -1
  46. package/dist/loader.cjs +24 -20
  47. package/dist/loader.cjs.map +1 -1
  48. package/dist/loader.d.cts +2 -2
  49. package/dist/loader.d.ts +2 -2
  50. package/dist/loader.js +7 -3
  51. package/dist/loader.js.map +1 -1
  52. package/dist/{props-BpZz0AOq.d.cts → props--zJ4ebbT.d.cts} +2 -2
  53. package/dist/{props-CjLH0JE-.d.ts → props-BAGR7j-j.d.ts} +2 -2
  54. package/dist/{resume-BJ4oHLi_.d.cts → resume-C5IKAIdh.d.ts} +2 -2
  55. package/dist/{resume-CuyJWXP_.d.ts → resume-DPZxmA95.d.cts} +2 -2
  56. package/dist/{scope-jPt5DHRT.d.ts → scope-CuImnvh1.d.ts} +1 -1
  57. package/dist/{scope-BJCtq8hJ.d.cts → scope-Dq5hOu7c.d.cts} +1 -1
  58. package/dist/{signal-C4ISF17w.d.cts → signal-Z4KkDk9h.d.cts} +12 -1
  59. package/dist/{signal-C4ISF17w.d.ts → signal-Z4KkDk9h.d.ts} +12 -1
  60. package/package.json +2 -2
  61. package/src/devtools.ts +19 -2
  62. package/src/dom.ts +58 -4
  63. package/src/effect.ts +5 -5
  64. package/src/hooks.ts +13 -5
  65. package/src/lifecycle.ts +41 -3
  66. package/src/loader.ts +10 -4
  67. package/src/signal.ts +191 -18
  68. package/src/transition.ts +9 -3
  69. package/dist/chunk-DXG3TARY.js.map +0 -1
  70. package/dist/chunk-OAM7HABA.cjs.map +0 -1
  71. package/dist/chunk-T2LNV5Q5.js.map +0 -1
  72. package/dist/chunk-UBFDB6OL.cjs.map +0 -1
  73. /package/dist/{chunk-JVYH76ZX.js.map → chunk-2JRPPCG7.js.map} +0 -0
  74. /package/dist/{chunk-FVX77557.js.map → chunk-DKA2I6ET.js.map} +0 -0
  75. /package/dist/{chunk-N6ODUM2Y.js.map → chunk-P4TZLFV6.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
  // ============================================================================
@@ -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, vnode.type.name || 'Anonymous', parentId)
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
- return createElementWithContext(rendered as FictNode, namespace)
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(ctx: HookContext, fn: () => void, slot?: number): void {
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 (slot !== undefined) {
126
- if (ctx.slots[slot]) {
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[slot] = createEffect(fn)
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 =
@@ -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
- return { parent, cleanups: [], destroyCallbacks: [], suspended: false }
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) originRoot.suspended = true
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) originRoot.suspended = true
342
+ if (originRoot) {
343
+ originRoot.suspended = true
344
+ setRootSuspendDevtools(originRoot, true)
345
+ }
308
346
  return true
309
347
  }
310
348
  }
package/src/loader.ts CHANGED
@@ -507,9 +507,15 @@ function prefetchQrl(qrl: string): void {
507
507
  function handleResumableEvent(event: Event): void {
508
508
  const promise = handleResumableEventAsync(event)
509
509
  pendingHandlers.add(promise)
510
- promise.finally(() => {
511
- pendingHandlers.delete(promise)
512
- })
510
+ void promise
511
+ .catch(error => {
512
+ if (typeof console !== 'undefined' && typeof console.error === 'function') {
513
+ console.error('[fict/loader] Failed to handle resumable event.', error)
514
+ }
515
+ })
516
+ .finally(() => {
517
+ pendingHandlers.delete(promise)
518
+ })
513
519
  }
514
520
 
515
521
  async function handleResumableEventAsync(event: Event): Promise<void> {
@@ -535,7 +541,7 @@ async function handleResumableEventAsync(event: Event): Promise<void> {
535
541
  expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
536
542
  scopeId,
537
543
  })
538
- return
544
+ continue
539
545
  }
540
546
  __fictEnsureScope(scopeId, host, snapshot)
541
547