@fictjs/runtime 0.0.12 → 0.0.14

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 (65) hide show
  1. package/dist/advanced.cjs +79 -0
  2. package/dist/advanced.cjs.map +1 -0
  3. package/dist/advanced.d.cts +50 -0
  4. package/dist/advanced.d.ts +50 -0
  5. package/dist/advanced.js +79 -0
  6. package/dist/advanced.js.map +1 -0
  7. package/dist/chunk-624QY53A.cjs +45 -0
  8. package/dist/chunk-624QY53A.cjs.map +1 -0
  9. package/dist/chunk-F3AIYQB7.js +45 -0
  10. package/dist/chunk-F3AIYQB7.js.map +1 -0
  11. package/dist/chunk-GJTYOFMO.cjs +109 -0
  12. package/dist/chunk-GJTYOFMO.cjs.map +1 -0
  13. package/dist/chunk-IUZXKAAY.js +109 -0
  14. package/dist/chunk-IUZXKAAY.js.map +1 -0
  15. package/dist/{slim.cjs → chunk-PMF6MWEV.cjs} +2557 -3110
  16. package/dist/chunk-PMF6MWEV.cjs.map +1 -0
  17. package/dist/{slim.js → chunk-RY4WDS6R.js} +2596 -3097
  18. package/dist/chunk-RY4WDS6R.js.map +1 -0
  19. package/dist/context-B7UYnfzM.d.ts +153 -0
  20. package/dist/context-UXySaqI_.d.cts +153 -0
  21. package/dist/effect-Auji1rz9.d.cts +350 -0
  22. package/dist/effect-Auji1rz9.d.ts +350 -0
  23. package/dist/index.cjs +108 -4441
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +5 -1492
  26. package/dist/index.d.ts +5 -1492
  27. package/dist/index.dev.js +1020 -2788
  28. package/dist/index.dev.js.map +1 -1
  29. package/dist/index.js +63 -4301
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal.cjs +901 -0
  32. package/dist/internal.cjs.map +1 -0
  33. package/dist/internal.d.cts +158 -0
  34. package/dist/internal.d.ts +158 -0
  35. package/dist/internal.js +901 -0
  36. package/dist/internal.js.map +1 -0
  37. package/dist/{jsx-dev-runtime.d.ts → props-CrOMYbLv.d.cts} +107 -18
  38. package/dist/{jsx-dev-runtime.d.cts → props-ES0Ag_Wd.d.ts} +107 -18
  39. package/dist/scope-DKYzWfTn.d.cts +55 -0
  40. package/dist/scope-S6eAzBJZ.d.ts +55 -0
  41. package/package.json +10 -5
  42. package/src/advanced.ts +101 -0
  43. package/src/binding.ts +25 -422
  44. package/src/constants.ts +345 -344
  45. package/src/context.ts +300 -0
  46. package/src/cycle-guard.ts +124 -97
  47. package/src/delegated-events.ts +24 -0
  48. package/src/dom.ts +19 -25
  49. package/src/effect.ts +4 -0
  50. package/src/hooks.ts +9 -1
  51. package/src/index.ts +41 -130
  52. package/src/internal.ts +130 -0
  53. package/src/lifecycle.ts +13 -2
  54. package/src/list-helpers.ts +6 -65
  55. package/src/props.ts +48 -46
  56. package/src/signal.ts +59 -39
  57. package/src/store.ts +47 -7
  58. package/src/versioned-signal.ts +3 -3
  59. package/dist/jsx-runtime.d.cts +0 -671
  60. package/dist/jsx-runtime.d.ts +0 -671
  61. package/dist/slim.cjs.map +0 -1
  62. package/dist/slim.d.cts +0 -504
  63. package/dist/slim.d.ts +0 -504
  64. package/dist/slim.js.map +0 -1
  65. package/src/slim.ts +0 -69
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
- throw new Error(`${hookName} can only be used during render execution`)
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
@@ -1,38 +1,37 @@
1
+ /**
2
+ * @fileoverview Fict Runtime - Public API
3
+ *
4
+ * This module exports the public API for the Fict reactive UI framework.
5
+ *
6
+ * ## Recommended Import Pattern (v1.0+)
7
+ *
8
+ * ```typescript
9
+ * // Core public API (most users need only this)
10
+ * import { createEffect, render } from '@fictjs/runtime'
11
+ *
12
+ * // Advanced APIs (power users, escape hatches)
13
+ * import { createSignal, createContext, createScope } from '@fictjs/runtime/advanced'
14
+ *
15
+ * // Internal APIs (compiler/library authors only)
16
+ * import { bindText, __fictUseSignal } from '@fictjs/runtime/internal'
17
+ * ```
18
+ *
19
+ * @public
20
+ * @packageDocumentation
21
+ */
22
+
1
23
  // ============================================================================
2
24
  // Core Reactive Primitives
3
25
  // ============================================================================
4
26
 
5
- export { createSignal, createSelector, type Signal, $state } from './signal'
6
- export { effectScope } from './signal'
7
- export { createStore, type Store } from './store'
8
- export { createMemo, type Memo, $memo } from './memo'
9
- export { createEffect, createRenderEffect, type Effect, $effect } from './effect'
10
- export { createScope, runInScope, type ReactiveScope } from './scope'
11
- export {
12
- __fictUseContext,
13
- __fictPushContext,
14
- __fictPopContext,
15
- __fictUseSignal,
16
- __fictUseMemo,
17
- __fictUseEffect,
18
- __fictRender,
19
- __fictResetContext,
20
- } from './hooks'
21
- export {
22
- createVersionedSignal,
23
- type VersionedSignal,
24
- type VersionedSignalOptions,
25
- } from './versioned-signal'
26
-
27
- // Props helpers
28
- export {
29
- __fictProp,
30
- __fictProp as prop,
31
- __fictPropsRest,
32
- mergeProps,
33
- useProp,
34
- createPropsProxy,
35
- } from './props'
27
+ /**
28
+ * Note: createSignal is exported from ./advanced as an escape hatch.
29
+ * For most use cases, prefer:
30
+ * - $state: For component-local state (compiler-transformed, safe scoping)
31
+ * - $store: For cross-component shared state with deep reactivity
32
+ */
33
+ export { createMemo, type Memo } from './memo'
34
+ export { createEffect, type Effect } from './effect'
36
35
 
37
36
  // ============================================================================
38
37
  // Lifecycle
@@ -40,7 +39,10 @@ export {
40
39
 
41
40
  export { onMount, onDestroy, onCleanup, createRoot } from './lifecycle'
42
41
 
43
- // Ref utilities
42
+ // ============================================================================
43
+ // Ref
44
+ // ============================================================================
45
+
44
46
  export { createRef } from './ref'
45
47
 
46
48
  // ============================================================================
@@ -48,9 +50,6 @@ export { createRef } from './ref'
48
50
  // ============================================================================
49
51
 
50
52
  export { batch, untrack } from './scheduler'
51
- export { setCycleProtectionOptions } from './cycle-guard'
52
-
53
- // Transition API for priority scheduling
54
53
  export { startTransition, useTransition, useDeferredValue } from './scheduler'
55
54
 
56
55
  // ============================================================================
@@ -64,79 +63,22 @@ export { Fragment } from './jsx'
64
63
  // DOM Rendering
65
64
  // ============================================================================
66
65
 
67
- export { createElement, render, template } from './dom'
66
+ export { createElement, render } from './dom'
67
+ export { createPortal } from './binding'
68
68
  export { ErrorBoundary } from './error-boundary'
69
69
  export { Suspense, createSuspenseToken } from './suspense'
70
+ export { createContext, useContext, hasContext, type Context, type ProviderProps } from './context'
70
71
 
71
72
  // ============================================================================
72
- // Reactive DOM Bindings
73
- // ============================================================================
74
-
75
- export {
76
- // Core binding utilities
77
- createTextBinding,
78
- createChildBinding,
79
- createAttributeBinding,
80
- createStyleBinding,
81
- createClassBinding,
82
- // Low-level binding helpers (for direct DOM manipulation)
83
- bindText,
84
- bindAttribute,
85
- bindStyle,
86
- bindClass,
87
- bindEvent,
88
- callEventHandler,
89
- bindProperty,
90
- bindRef,
91
- insert,
92
- // Event delegation
93
- delegateEvents,
94
- clearDelegatedEvents,
95
- addEventListener,
96
- // Spread props
97
- spread,
98
- assign,
99
- classList,
100
- // Reactive detection
101
- isReactive,
102
- // Advanced bindings
103
- createConditional,
104
- createList,
105
- createPortal,
106
- createShow,
107
- // Utility functions
108
- unwrap,
109
- unwrapPrimitive,
110
- } from './binding'
111
-
112
- // Constants for DOM handling
113
- export {
114
- Properties,
115
- ChildProperties,
116
- Aliases,
117
- getPropAlias,
118
- BooleanAttributes,
119
- SVGElements,
120
- SVGNamespace,
121
- DelegatedEvents,
122
- UnitlessStyles,
123
- } from './constants'
124
-
125
- // Reconcile algorithm
126
- export { default as reconcileArrays } from './reconcile'
73
+ // Props Utilities (Public)
74
+ // ============================================================================
75
+
76
+ export { prop, mergeProps } from './props'
127
77
 
128
78
  // ============================================================================
129
79
  // Types
130
80
  // ============================================================================
131
81
 
132
- export type {
133
- MaybeReactive,
134
- BindingHandle,
135
- KeyFn,
136
- CreateElementFn,
137
- AttributeSetter,
138
- } from './binding'
139
-
140
82
  export type {
141
83
  FictNode,
142
84
  FictVNode,
@@ -154,34 +96,3 @@ export type {
154
96
  ErrorInfo,
155
97
  SuspenseToken,
156
98
  } from './types'
157
-
158
- // Devtools hook (optional)
159
- export { getDevtoolsHook, type FictDevtoolsHook } from './devtools'
160
-
161
- // ============================================================================
162
- // List Helpers (for compiler-generated code)
163
- // ============================================================================
164
-
165
- export {
166
- // DOM manipulation primitives
167
- moveNodesBefore,
168
- removeNodes,
169
- insertNodesBefore,
170
- moveMarkerBlock,
171
- destroyMarkerBlock,
172
- // Keyed list container
173
- createKeyedListContainer,
174
- // Block creation
175
- createKeyedBlock,
176
- // High-level list binding (for compiler-generated code)
177
- createKeyedList,
178
- // Utilities
179
- toNodeArray,
180
- getFirstNodeAfter,
181
- isNodeBetweenMarkers,
182
- // Types
183
- type KeyedBlock,
184
- type KeyedListContainer,
185
- type KeyedListBinding,
186
- type MarkerBlock,
187
- } from './list-helpers'
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @fileoverview Internal APIs for Fict Compiler
3
+ *
4
+ * This module exports internal APIs used by compiler-generated code.
5
+ * These APIs are NOT part of the public API and should NOT be used directly.
6
+ *
7
+ * @internal
8
+ * @packageDocumentation
9
+ */
10
+
11
+ // ============================================================================
12
+ // Core Primitives (also exported from main, but needed by compiler)
13
+ // ============================================================================
14
+
15
+ export { createSignal, createSelector } from './signal'
16
+ export { createStore, type Store } from './store'
17
+ export { createMemo } from './memo'
18
+ export { createEffect } from './effect'
19
+ export { Fragment } from './jsx'
20
+
21
+ // ============================================================================
22
+ // Hook Context Management (Compiler-generated code)
23
+ // ============================================================================
24
+
25
+ export {
26
+ __fictUseContext,
27
+ __fictPushContext,
28
+ __fictPopContext,
29
+ __fictUseSignal,
30
+ __fictUseMemo,
31
+ __fictUseEffect,
32
+ __fictRender,
33
+ __fictResetContext,
34
+ } from './hooks'
35
+
36
+ // ============================================================================
37
+ // Props Helpers (Compiler-generated code)
38
+ // ============================================================================
39
+
40
+ export { __fictProp, __fictPropsRest, createPropsProxy, mergeProps, prop } from './props'
41
+
42
+ // ============================================================================
43
+ // DOM Bindings (Compiler-generated code)
44
+ // ============================================================================
45
+
46
+ export {
47
+ bindText,
48
+ bindAttribute,
49
+ bindStyle,
50
+ bindClass,
51
+ bindEvent,
52
+ callEventHandler,
53
+ bindProperty,
54
+ bindRef,
55
+ insert,
56
+ createConditional,
57
+ createPortal,
58
+ spread,
59
+ assign,
60
+ classList,
61
+ isReactive,
62
+ unwrap,
63
+ } from './binding'
64
+
65
+ // ============================================================================
66
+ // Event Delegation (Compiler-generated code)
67
+ // ============================================================================
68
+
69
+ export { delegateEvents, clearDelegatedEvents, addEventListener } from './binding'
70
+
71
+ // ============================================================================
72
+ // List Helpers (Compiler-generated code)
73
+ // ============================================================================
74
+
75
+ export {
76
+ moveNodesBefore,
77
+ removeNodes,
78
+ insertNodesBefore,
79
+ createKeyedList,
80
+ toNodeArray,
81
+ isNodeBetweenMarkers,
82
+ type KeyedListBinding,
83
+ } from './list-helpers'
84
+
85
+ // ============================================================================
86
+ // DOM Creation (Compiler-generated code)
87
+ // ============================================================================
88
+
89
+ export { createElement, template } from './dom'
90
+ export { createRenderEffect } from './effect'
91
+
92
+ // ============================================================================
93
+ // Lifecycle (Compiler-generated code)
94
+ // ============================================================================
95
+
96
+ export { onDestroy } from './lifecycle'
97
+
98
+ // ============================================================================
99
+ // Scope (Compiler-generated code)
100
+ // ============================================================================
101
+
102
+ export { runInScope } from './scope'
103
+
104
+ // ============================================================================
105
+ // Constants (Compiler/Runtime shared)
106
+ // ============================================================================
107
+
108
+ export {
109
+ Properties,
110
+ ChildProperties,
111
+ Aliases,
112
+ getPropAlias,
113
+ BooleanAttributes,
114
+ SVGElements,
115
+ SVGNamespace,
116
+ DelegatedEvents,
117
+ UnitlessStyles,
118
+ } from './constants'
119
+
120
+ // ============================================================================
121
+ // Reconciliation (Internal)
122
+ // ============================================================================
123
+
124
+ export { default as reconcileArrays } from './reconcile'
125
+
126
+ // ============================================================================
127
+ // Types (Internal)
128
+ // ============================================================================
129
+
130
+ export type { MaybeReactive, BindingHandle, CreateElementFn, AttributeSetter } from './binding'
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
- throw new Error('registerErrorHandler must be called within a root')
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
- throw new Error('registerSuspenseHandler must be called within a root')
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 = []
@@ -37,7 +37,7 @@ const isDev =
37
37
  /**
38
38
  * A keyed block represents a single item in a list with its associated DOM nodes and state
39
39
  */
40
- export interface KeyedBlock<T = unknown> {
40
+ interface KeyedBlock<T = unknown> {
41
41
  /** Unique key for this block */
42
42
  key: string | number
43
43
  /** DOM nodes belonging to this block */
@@ -57,7 +57,7 @@ export interface KeyedBlock<T = unknown> {
57
57
  /**
58
58
  * Container for managing keyed list blocks
59
59
  */
60
- export interface KeyedListContainer<T = unknown> {
60
+ interface KeyedListContainer<T = unknown> {
61
61
  /** Start marker comment node */
62
62
  startMarker: Comment
63
63
  /** End marker comment node */
@@ -102,15 +102,6 @@ type FineGrainedRenderItem<T> = (
102
102
  key: string | number,
103
103
  ) => Node[]
104
104
 
105
- /**
106
- * A block identified by start/end comment markers.
107
- */
108
- export interface MarkerBlock {
109
- start: Comment
110
- end: Comment
111
- root?: RootContext
112
- }
113
-
114
105
  // ============================================================================
115
106
  // DOM Manipulation Primitives
116
107
  // ============================================================================
@@ -129,7 +120,8 @@ export function moveNodesBefore(parent: Node, nodes: Node[], anchor: Node | null
129
120
  for (let i = nodes.length - 1; i >= 0; i--) {
130
121
  const node = nodes[i]!
131
122
  if (!node || !(node instanceof Node)) {
132
- throw new Error('Invalid node in moveNodesBefore')
123
+ const message = isDev ? 'Invalid node in moveNodesBefore' : 'FICT:E_NODE'
124
+ throw new Error(message)
133
125
  }
134
126
  // Only move if not already in correct position
135
127
  if (node.nextSibling !== anchor) {
@@ -162,50 +154,6 @@ export function moveNodesBefore(parent: Node, nodes: Node[], anchor: Node | null
162
154
  *
163
155
  * @param nodes - Array of nodes to remove
164
156
  */
165
- /**
166
- * Move an entire marker-delimited block (including markers) before the anchor.
167
- */
168
- export function moveMarkerBlock(parent: Node, block: MarkerBlock, anchor: Node | null): void {
169
- const nodes = collectBlockNodes(block)
170
- if (nodes.length === 0) return
171
- moveNodesBefore(parent, nodes, anchor)
172
- }
173
-
174
- /**
175
- * Destroy a marker-delimited block, removing nodes and destroying the associated root.
176
- */
177
- export function destroyMarkerBlock(block: MarkerBlock): void {
178
- if (block.root) {
179
- destroyRoot(block.root)
180
- }
181
- removeBlockRange(block)
182
- }
183
-
184
- function collectBlockNodes(block: MarkerBlock): Node[] {
185
- const nodes: Node[] = []
186
- let cursor: Node | null = block.start
187
- while (cursor) {
188
- nodes.push(cursor)
189
- if (cursor === block.end) {
190
- break
191
- }
192
- cursor = cursor.nextSibling
193
- }
194
- return nodes
195
- }
196
-
197
- function removeBlockRange(block: MarkerBlock): void {
198
- let cursor: Node | null = block.start
199
- while (cursor) {
200
- const next: Node | null = cursor.nextSibling
201
- cursor.parentNode?.removeChild(cursor)
202
- if (cursor === block.end) {
203
- break
204
- }
205
- cursor = next
206
- }
207
- }
208
-
209
157
  // Number.MAX_SAFE_INTEGER is 2^53 - 1, but we reset earlier to avoid any precision issues
210
158
  const MAX_SAFE_VERSION = 0x1fffffffffffff // 2^53 - 1
211
159
 
@@ -238,7 +186,7 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
238
186
  *
239
187
  * @returns Container object with markers, blocks map, and dispose function
240
188
  */
241
- export function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
189
+ function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
242
190
  const startMarker = document.createComment('fict:list:start')
243
191
  const endMarker = document.createComment('fict:list:end')
244
192
 
@@ -306,7 +254,7 @@ export function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
306
254
  * @param render - Function that creates the DOM nodes and sets up bindings
307
255
  * @returns New KeyedBlock
308
256
  */
309
- export function createKeyedBlock<T>(
257
+ function createKeyedBlock<T>(
310
258
  key: string | number,
311
259
  item: T,
312
260
  index: number,
@@ -377,13 +325,6 @@ export function createKeyedBlock<T>(
377
325
  // Utilities
378
326
  // ============================================================================
379
327
 
380
- /**
381
- * Find the first node after the start marker (for getting current anchor)
382
- */
383
- export function getFirstNodeAfter(marker: Comment): Node | null {
384
- return marker.nextSibling
385
- }
386
-
387
328
  /**
388
329
  * Check if a node is between two markers
389
330
  */
package/src/props.ts CHANGED
@@ -122,38 +122,46 @@ export function mergeProps<T extends Record<string, unknown>>(
122
122
  return unwrapProps(value as T)
123
123
  }
124
124
 
125
+ const hasProp = (prop: string | symbol) => {
126
+ for (const src of validSources) {
127
+ const raw = resolveSource(src)
128
+ if (raw && prop in raw) {
129
+ return true
130
+ }
131
+ }
132
+ return false
133
+ }
134
+
135
+ const readProp = (prop: string | symbol) => {
136
+ // Only return undefined if no source has this Symbol property
137
+ // Search sources in reverse order (last wins)
138
+ for (let i = validSources.length - 1; i >= 0; i--) {
139
+ const src = validSources[i]!
140
+ const raw = resolveSource(src)
141
+ if (!raw || !(prop in raw)) continue
142
+
143
+ const value = (raw as Record<string | symbol, unknown>)[prop]
144
+ // Preserve prop getters - let child component's createPropsProxy unwrap lazily
145
+ // Note: For Symbol properties, we still wrap in getter if source is dynamic
146
+ if (typeof src === 'function' && !isPropGetter(value)) {
147
+ return __fictProp(() => {
148
+ const latest = resolveSource(src)
149
+ if (!latest || !(prop in latest)) return undefined
150
+ return (latest as Record<string | symbol, unknown>)[prop]
151
+ })
152
+ }
153
+ return value
154
+ }
155
+ return undefined
156
+ }
157
+
125
158
  return new Proxy({} as Record<string, unknown>, {
126
159
  get(_, prop) {
127
- // Only return undefined if no source has this Symbol property
128
- // Search sources in reverse order (last wins)
129
- for (let i = validSources.length - 1; i >= 0; i--) {
130
- const src = validSources[i]!
131
- const raw = resolveSource(src)
132
- if (!raw || !(prop in raw)) continue
133
-
134
- const value = (raw as Record<string | symbol, unknown>)[prop]
135
- // Preserve prop getters - let child component's createPropsProxy unwrap lazily
136
- // Note: For Symbol properties, we still wrap in getter if source is dynamic
137
- if (typeof src === 'function' && !isPropGetter(value)) {
138
- return __fictProp(() => {
139
- const latest = resolveSource(src)
140
- if (!latest || !(prop in latest)) return undefined
141
- return (latest as Record<string | symbol, unknown>)[prop]
142
- })
143
- }
144
- return value
145
- }
146
- return undefined
160
+ return readProp(prop)
147
161
  },
148
162
 
149
163
  has(_, prop) {
150
- for (const src of validSources) {
151
- const raw = resolveSource(src)
152
- if (raw && prop in raw) {
153
- return true
154
- }
155
- }
156
- return false
164
+ return hasProp(prop)
157
165
  },
158
166
 
159
167
  ownKeys() {
@@ -170,21 +178,12 @@ export function mergeProps<T extends Record<string, unknown>>(
170
178
  },
171
179
 
172
180
  getOwnPropertyDescriptor(_, prop) {
173
- for (let i = validSources.length - 1; i >= 0; i--) {
174
- const raw = resolveSource(validSources[i]!)
175
- if (raw && prop in raw) {
176
- return {
177
- enumerable: true,
178
- configurable: true,
179
- get: () => {
180
- const value = (raw as Record<string | symbol, unknown>)[prop]
181
- // Preserve prop getters - let child component's createPropsProxy unwrap lazily
182
- return value
183
- },
184
- }
185
- }
181
+ if (!hasProp(prop)) return undefined
182
+ return {
183
+ enumerable: true,
184
+ configurable: true,
185
+ get: () => readProp(prop),
186
186
  }
187
- return undefined
188
187
  },
189
188
  })
190
189
  }
@@ -192,19 +191,22 @@ export function mergeProps<T extends Record<string, unknown>>(
192
191
  export type PropGetter<T> = (() => T) & { __fictProp: true }
193
192
  /**
194
193
  * Memoize a prop getter to cache expensive computations.
195
- * Use when prop expressions involve heavy calculations.
194
+ * Use when prop expressions involve heavy calculations or you need lazy, reactive props.
196
195
  *
197
196
  * @example
198
197
  * ```tsx
199
- * // Without useProp - recomputes on every access
198
+ * // Without prop - recomputes on every access
200
199
  * <Child data={expensiveComputation(list, filter)} />
201
200
  *
202
- * // With useProp - cached until dependencies change, auto-unwrapped by props proxy
203
- * const memoizedData = useProp(() => expensiveComputation(list, filter))
201
+ * // With prop - cached until dependencies change, auto-unwrapped by props proxy
202
+ * const memoizedData = prop(() => expensiveComputation(list, filter))
204
203
  * <Child data={memoizedData} />
205
204
  * ```
206
205
  */
207
- export function useProp<T>(getter: () => T): PropGetter<T> {
206
+ export function prop<T>(getter: () => T): PropGetter<T> {
207
+ if (isPropGetter(getter)) {
208
+ return getter as PropGetter<T>
209
+ }
208
210
  // Wrap in prop so component props proxy auto-unwraps when passed down.
209
211
  return __fictProp(createMemo(getter)) as PropGetter<T>
210
212
  }