@contember/bindx-react 0.1.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 (194) hide show
  1. package/dist/batching/QueryBatcher.d.ts +31 -0
  2. package/dist/batching/QueryBatcher.d.ts.map +1 -0
  3. package/dist/batching/index.d.ts +2 -0
  4. package/dist/batching/index.d.ts.map +1 -0
  5. package/dist/components/EntityScope.d.ts +28 -0
  6. package/dist/components/EntityScope.d.ts.map +1 -0
  7. package/dist/dataview/DataGrid.d.ts +45 -0
  8. package/dist/dataview/DataGrid.d.ts.map +1 -0
  9. package/dist/dataview/DataGridPagination.d.ts +24 -0
  10. package/dist/dataview/DataGridPagination.d.ts.map +1 -0
  11. package/dist/dataview/DataGridToolbar.d.ts +16 -0
  12. package/dist/dataview/DataGridToolbar.d.ts.map +1 -0
  13. package/dist/dataview/DataViewContext.d.ts +32 -0
  14. package/dist/dataview/DataViewContext.d.ts.map +1 -0
  15. package/dist/dataview/columns.d.ts +115 -0
  16. package/dist/dataview/columns.d.ts.map +1 -0
  17. package/dist/dataview/contextHooks.d.ts +48 -0
  18. package/dist/dataview/contextHooks.d.ts.map +1 -0
  19. package/dist/dataview/dataAttribute.d.ts +6 -0
  20. package/dist/dataview/dataAttribute.d.ts.map +1 -0
  21. package/dist/dataview/export.d.ts +46 -0
  22. package/dist/dataview/export.d.ts.map +1 -0
  23. package/dist/dataview/filterComponents.d.ts +127 -0
  24. package/dist/dataview/filterComponents.d.ts.map +1 -0
  25. package/dist/dataview/filterContext.d.ts +10 -0
  26. package/dist/dataview/filterContext.d.ts.map +1 -0
  27. package/dist/dataview/filterHooks.d.ts +74 -0
  28. package/dist/dataview/filterHooks.d.ts.map +1 -0
  29. package/dist/dataview/filterScope.d.ts +54 -0
  30. package/dist/dataview/filterScope.d.ts.map +1 -0
  31. package/dist/dataview/filterWrappers.d.ts +65 -0
  32. package/dist/dataview/filterWrappers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +21 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/infiniteLoad.d.ts +32 -0
  36. package/dist/dataview/infiniteLoad.d.ts.map +1 -0
  37. package/dist/dataview/loaderComponents.d.ts +80 -0
  38. package/dist/dataview/loaderComponents.d.ts.map +1 -0
  39. package/dist/dataview/pagingComponents.d.ts +50 -0
  40. package/dist/dataview/pagingComponents.d.ts.map +1 -0
  41. package/dist/dataview/renderCellValue.d.ts +9 -0
  42. package/dist/dataview/renderCellValue.d.ts.map +1 -0
  43. package/dist/dataview/selectionComponents.d.ts +62 -0
  44. package/dist/dataview/selectionComponents.d.ts.map +1 -0
  45. package/dist/dataview/sortingComponents.d.ts +39 -0
  46. package/dist/dataview/sortingComponents.d.ts.map +1 -0
  47. package/dist/dataview/stateStorage.d.ts +19 -0
  48. package/dist/dataview/stateStorage.d.ts.map +1 -0
  49. package/dist/dataview/useDataView.d.ts +58 -0
  50. package/dist/dataview/useDataView.d.ts.map +1 -0
  51. package/dist/dataview/useDataViewState.d.ts +89 -0
  52. package/dist/dataview/useDataViewState.d.ts.map +1 -0
  53. package/dist/hooks/BackendAdapterContext.d.ts +110 -0
  54. package/dist/hooks/BackendAdapterContext.d.ts.map +1 -0
  55. package/dist/hooks/ContemberBindxProvider.d.ts +67 -0
  56. package/dist/hooks/ContemberBindxProvider.d.ts.map +1 -0
  57. package/dist/hooks/createBindx.d.ts +39 -0
  58. package/dist/hooks/createBindx.d.ts.map +1 -0
  59. package/dist/hooks/index.d.ts +10 -0
  60. package/dist/hooks/index.d.ts.map +1 -0
  61. package/dist/hooks/useBindxEvents.d.ts +83 -0
  62. package/dist/hooks/useBindxEvents.d.ts.map +1 -0
  63. package/dist/hooks/useEntity.d.ts +123 -0
  64. package/dist/hooks/useEntity.d.ts.map +1 -0
  65. package/dist/hooks/useEntityBeforePersist.d.ts +22 -0
  66. package/dist/hooks/useEntityBeforePersist.d.ts.map +1 -0
  67. package/dist/hooks/useEntityCore.d.ts +52 -0
  68. package/dist/hooks/useEntityCore.d.ts.map +1 -0
  69. package/dist/hooks/useEntityImpl.d.ts +105 -0
  70. package/dist/hooks/useEntityImpl.d.ts.map +1 -0
  71. package/dist/hooks/useEntityList.d.ts +102 -0
  72. package/dist/hooks/useEntityList.d.ts.map +1 -0
  73. package/dist/hooks/useEntityListCore.d.ts +49 -0
  74. package/dist/hooks/useEntityListCore.d.ts.map +1 -0
  75. package/dist/hooks/useEntityListImpl.d.ts +70 -0
  76. package/dist/hooks/useEntityListImpl.d.ts.map +1 -0
  77. package/dist/hooks/useErrors.d.ts +72 -0
  78. package/dist/hooks/useErrors.d.ts.map +1 -0
  79. package/dist/hooks/usePersist.d.ts +115 -0
  80. package/dist/hooks/usePersist.d.ts.map +1 -0
  81. package/dist/hooks/useSelectionCollection.d.ts +41 -0
  82. package/dist/hooks/useSelectionCollection.d.ts.map +1 -0
  83. package/dist/hooks/useSelectionCollectionForList.d.ts +40 -0
  84. package/dist/hooks/useSelectionCollectionForList.d.ts.map +1 -0
  85. package/dist/hooks/useStableSelectionMeta.d.ts +11 -0
  86. package/dist/hooks/useStableSelectionMeta.d.ts.map +1 -0
  87. package/dist/hooks/useStoreSubscription.d.ts +19 -0
  88. package/dist/hooks/useStoreSubscription.d.ts.map +1 -0
  89. package/dist/hooks/useUndo.d.ts +45 -0
  90. package/dist/hooks/useUndo.d.ts.map +1 -0
  91. package/dist/index.d.ts +17 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/jsx/SelectionMeta.d.ts +2 -0
  94. package/dist/jsx/SelectionMeta.d.ts.map +1 -0
  95. package/dist/jsx/analyzer.d.ts +22 -0
  96. package/dist/jsx/analyzer.d.ts.map +1 -0
  97. package/dist/jsx/collectorProxy.d.ts +14 -0
  98. package/dist/jsx/collectorProxy.d.ts.map +1 -0
  99. package/dist/jsx/componentBuilder.d.ts +51 -0
  100. package/dist/jsx/componentBuilder.d.ts.map +1 -0
  101. package/dist/jsx/componentBuilder.types.d.ts +268 -0
  102. package/dist/jsx/componentBuilder.types.d.ts.map +1 -0
  103. package/dist/jsx/componentBuilderCompat.d.ts +17 -0
  104. package/dist/jsx/componentBuilderCompat.d.ts.map +1 -0
  105. package/dist/jsx/componentFactory.d.ts +39 -0
  106. package/dist/jsx/componentFactory.d.ts.map +1 -0
  107. package/dist/jsx/components/Entity.d.ts +82 -0
  108. package/dist/jsx/components/Entity.d.ts.map +1 -0
  109. package/dist/jsx/components/EntityList.d.ts +48 -0
  110. package/dist/jsx/components/EntityList.d.ts.map +1 -0
  111. package/dist/jsx/components/Field.d.ts +27 -0
  112. package/dist/jsx/components/Field.d.ts.map +1 -0
  113. package/dist/jsx/components/HasMany.d.ts +34 -0
  114. package/dist/jsx/components/HasMany.d.ts.map +1 -0
  115. package/dist/jsx/components/HasOne.d.ts +25 -0
  116. package/dist/jsx/components/HasOne.d.ts.map +1 -0
  117. package/dist/jsx/components/HasRole.d.ts +51 -0
  118. package/dist/jsx/components/HasRole.d.ts.map +1 -0
  119. package/dist/jsx/components/If.d.ts +51 -0
  120. package/dist/jsx/components/If.d.ts.map +1 -0
  121. package/dist/jsx/components/Show.d.ts +33 -0
  122. package/dist/jsx/components/Show.d.ts.map +1 -0
  123. package/dist/jsx/components/index.d.ts +8 -0
  124. package/dist/jsx/components/index.d.ts.map +1 -0
  125. package/dist/jsx/conditions.d.ts +310 -0
  126. package/dist/jsx/conditions.d.ts.map +1 -0
  127. package/dist/jsx/createComponent.d.ts +44 -0
  128. package/dist/jsx/createComponent.d.ts.map +1 -0
  129. package/dist/jsx/index.d.ts +19 -0
  130. package/dist/jsx/index.d.ts.map +1 -0
  131. package/dist/jsx/inlineProxy.d.ts +14 -0
  132. package/dist/jsx/inlineProxy.d.ts.map +1 -0
  133. package/dist/jsx/legacyTypes.d.ts +26 -0
  134. package/dist/jsx/legacyTypes.d.ts.map +1 -0
  135. package/dist/jsx/proxy.d.ts +2 -0
  136. package/dist/jsx/proxy.d.ts.map +1 -0
  137. package/dist/jsx/proxyShared.d.ts +14 -0
  138. package/dist/jsx/proxyShared.d.ts.map +1 -0
  139. package/dist/jsx/runtimeProxy.d.ts +11 -0
  140. package/dist/jsx/runtimeProxy.d.ts.map +1 -0
  141. package/dist/jsx/standaloneCreateComponent.d.ts +30 -0
  142. package/dist/jsx/standaloneCreateComponent.d.ts.map +1 -0
  143. package/dist/jsx/types.d.ts +109 -0
  144. package/dist/jsx/types.d.ts.map +1 -0
  145. package/dist/jsx/withCollector.d.ts +39 -0
  146. package/dist/jsx/withCollector.d.ts.map +1 -0
  147. package/dist/roles/RoleContext.d.ts +31 -0
  148. package/dist/roles/RoleContext.d.ts.map +1 -0
  149. package/dist/roles/index.d.ts +2 -0
  150. package/dist/roles/index.d.ts.map +1 -0
  151. package/package.json +34 -0
  152. package/src/batching/QueryBatcher.ts +114 -0
  153. package/src/batching/index.ts +1 -0
  154. package/src/components/EntityScope.tsx +43 -0
  155. package/src/hooks/BackendAdapterContext.tsx +214 -0
  156. package/src/hooks/ContemberBindxProvider.tsx +158 -0
  157. package/src/hooks/index.ts +61 -0
  158. package/src/hooks/useBindxEvents.ts +229 -0
  159. package/src/hooks/useEntity.ts +489 -0
  160. package/src/hooks/useEntityBeforePersist.ts +32 -0
  161. package/src/hooks/useEntityList.ts +444 -0
  162. package/src/hooks/useErrors.ts +150 -0
  163. package/src/hooks/usePersist.ts +377 -0
  164. package/src/hooks/useSelectionCollection.ts +122 -0
  165. package/src/hooks/useStoreSubscription.ts +51 -0
  166. package/src/hooks/useUndo.ts +83 -0
  167. package/src/index.ts +357 -0
  168. package/src/jsx/SelectionMeta.ts +2 -0
  169. package/src/jsx/analyzer.ts +261 -0
  170. package/src/jsx/collectorProxy.ts +282 -0
  171. package/src/jsx/componentBuilder.ts +180 -0
  172. package/src/jsx/componentBuilder.types.ts +463 -0
  173. package/src/jsx/componentBuilderCompat.ts +39 -0
  174. package/src/jsx/componentFactory.ts +400 -0
  175. package/src/jsx/components/Entity.tsx +383 -0
  176. package/src/jsx/components/EntityList.tsx +147 -0
  177. package/src/jsx/components/Field.tsx +79 -0
  178. package/src/jsx/components/HasMany.tsx +109 -0
  179. package/src/jsx/components/HasOne.tsx +78 -0
  180. package/src/jsx/components/HasRole.tsx +95 -0
  181. package/src/jsx/components/If.tsx +127 -0
  182. package/src/jsx/components/Show.tsx +56 -0
  183. package/src/jsx/components/index.ts +20 -0
  184. package/src/jsx/conditions.ts +472 -0
  185. package/src/jsx/createComponent.ts +175 -0
  186. package/src/jsx/index.ts +135 -0
  187. package/src/jsx/legacyTypes.ts +30 -0
  188. package/src/jsx/proxy.ts +1 -0
  189. package/src/jsx/proxyShared.ts +42 -0
  190. package/src/jsx/standaloneCreateComponent.ts +47 -0
  191. package/src/jsx/types.ts +192 -0
  192. package/src/jsx/withCollector.ts +45 -0
  193. package/src/roles/RoleContext.tsx +57 -0
  194. package/src/roles/index.ts +1 -0
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Component factory - builds React components with bindx selection metadata.
3
+ *
4
+ * This module handles the actual component creation, including:
5
+ * - Building React components with selection metadata
6
+ * - Implicit selection collection from JSX
7
+ * - Fragment creation
8
+ * - Selection provider interface
9
+ */
10
+
11
+ import type { ReactNode } from 'react'
12
+ import { memo } from 'react'
13
+ import type {
14
+ FluentFragment,
15
+ SelectionMeta,
16
+ SelectionBuilder,
17
+ AnyBrand,
18
+ SchemaDefinition,
19
+ } from '@contember/bindx'
20
+ import {
21
+ SchemaRegistry,
22
+ SELECTION_META,
23
+ ComponentBrand,
24
+ createSelectionBuilder,
25
+ SelectionScope,
26
+ } from '@contember/bindx'
27
+ import type {
28
+ SelectionPropMeta,
29
+ } from './componentBuilder.types.js'
30
+ import type { SelectionProvider, SelectionFieldMeta } from './types.js'
31
+ import { FIELD_REF_META, BINDX_COMPONENT } from './types.js'
32
+ import { createCollectorProxy } from './proxy.js'
33
+ import { collectSelection } from './analyzer.js'
34
+ import { type Condition, evaluateCondition } from './conditions.js'
35
+
36
+ // ============================================================================
37
+ // Symbols
38
+ // ============================================================================
39
+
40
+ /**
41
+ * Marker symbol for identifying components created with createComponent
42
+ */
43
+ export const COMPONENT_MARKER = Symbol('BINDX_COMPONENT')
44
+
45
+ /**
46
+ * Symbol for storing component brand on the component
47
+ */
48
+ export const COMPONENT_BRAND = Symbol('COMPONENT_BRAND')
49
+
50
+ /**
51
+ * Symbol for stored selection metadata
52
+ */
53
+ export const COMPONENT_SELECTIONS = Symbol('COMPONENT_SELECTIONS')
54
+
55
+ // ============================================================================
56
+ // Entity Config (Runtime)
57
+ // ============================================================================
58
+
59
+ export interface EntityConfig {
60
+ readonly entityName: string | null // null for interface-based props
61
+ readonly selector?: (builder: SelectionBuilder<object>) => SelectionBuilder<object, object, object>
62
+ readonly isInterface?: boolean
63
+ readonly schema?: SchemaDefinition<Record<string, object>>
64
+ }
65
+
66
+ // ============================================================================
67
+ // Component Building
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Builds a bindx component from entity configs and render function.
72
+ *
73
+ * Note: Implicit selection collection is deferred (lazy) to avoid TDZ errors
74
+ * when components reference other components defined later in the same file.
75
+ */
76
+ export function buildComponent<TProps extends object>(
77
+ entityConfigs: Map<string, EntityConfig>,
78
+ roles: readonly string[],
79
+ renderFn: (props: TProps) => ReactNode,
80
+ hasInterfacesMode: boolean,
81
+ schemaRegistry: SchemaRegistry<Record<string, object>> | null,
82
+ conditionFn: ((props: TProps) => Condition) | null,
83
+ ): unknown {
84
+ const selectionsMap = new Map<string, SelectionPropMeta>()
85
+
86
+ // Generate unique brand for this component
87
+ const componentBrand = new ComponentBrand(`component_${Math.random().toString(36).slice(2)}`)
88
+
89
+ // 1. Process explicit entities (those with selectors) - these are safe to do eagerly
90
+ for (const [propName, config] of entityConfigs) {
91
+ if (config.selector) {
92
+ const builder = createSelectionBuilder<object>()
93
+ const resultBuilder = config.selector(builder)
94
+ const selection = resultBuilder[SELECTION_META]
95
+
96
+ selectionsMap.set(propName, {
97
+ selection,
98
+ fragment: createFragment(selection, componentBrand, roles),
99
+ })
100
+ }
101
+ }
102
+
103
+ // 2. Implicit entities - collect lazily to avoid TDZ errors
104
+ const implicitConfigs = [...entityConfigs.entries()].filter(([_, c]) => !c.selector)
105
+ let implicitCollected = false
106
+
107
+ function ensureImplicitCollected(): void {
108
+ // In interfaces mode, we also need to collect even if no explicit implicit configs exist
109
+ // (because interface props may be discovered dynamically)
110
+ // Also collect if there's a conditionFn (it may access entity fields)
111
+ if (implicitCollected || (implicitConfigs.length === 0 && !hasInterfacesMode && !conditionFn)) {
112
+ return
113
+ }
114
+ implicitCollected = true
115
+ collectImplicitSelections(implicitConfigs, renderFn, selectionsMap, componentBrand, roles, hasInterfacesMode, schemaRegistry, conditionFn)
116
+ }
117
+
118
+ // 3. Create React component
119
+ function ComponentImpl(props: TProps): ReactNode {
120
+ ensureImplicitCollected()
121
+ // Evaluate condition at runtime
122
+ if (conditionFn) {
123
+ const condition = conditionFn(props)
124
+ if (!evaluateCondition(condition)) {
125
+ return null
126
+ }
127
+ }
128
+ return renderFn(props)
129
+ }
130
+
131
+ const MemoizedComponent = memo(ComponentImpl)
132
+
133
+ // 4. Attach metadata
134
+ const comp = MemoizedComponent as unknown as Record<symbol | string, unknown>
135
+ comp[BINDX_COMPONENT] = true
136
+ comp[COMPONENT_MARKER] = true
137
+ comp[COMPONENT_SELECTIONS] = selectionsMap
138
+ comp[COMPONENT_BRAND] = componentBrand
139
+
140
+ if (roles.length > 0) {
141
+ comp['__componentRoles'] = roles
142
+ }
143
+
144
+ // 5. Add SelectionProvider interface with lazy collection
145
+ ;(MemoizedComponent as unknown as SelectionProvider).getSelection = (
146
+ props: Record<string, unknown>,
147
+ collectNested,
148
+ ): SelectionFieldMeta | SelectionFieldMeta[] | null => {
149
+ ensureImplicitCollected()
150
+ return createGetSelection(selectionsMap)(props, collectNested)
151
+ }
152
+
153
+ // 6. Attach fragment properties ($propName) for explicit entities
154
+ for (const [propName, meta] of selectionsMap) {
155
+ comp[`$${propName}`] = meta.fragment
156
+ }
157
+
158
+ // 7. Define lazy getters for implicit entity props
159
+ // This allows accessing $propName without first rendering the component
160
+ for (const [propName] of implicitConfigs) {
161
+ Object.defineProperty(comp, `$${propName}`, {
162
+ get(): FluentFragment<unknown, object, AnyBrand> | undefined {
163
+ ensureImplicitCollected()
164
+ return selectionsMap.get(propName)?.fragment
165
+ },
166
+ enumerable: true,
167
+ configurable: true,
168
+ })
169
+ }
170
+
171
+ // 8. In interfaces mode, wrap the component in a Proxy to handle dynamic $propName access
172
+ // This is needed because interface prop names are only known at type-level, not runtime
173
+ if (hasInterfacesMode) {
174
+ return new Proxy(MemoizedComponent, {
175
+ get(target, prop, receiver): unknown {
176
+ // Handle $propName access for interface props discovered during collection
177
+ if (typeof prop === 'string' && prop.startsWith('$')) {
178
+ const propName = prop.slice(1)
179
+ // Check if it's already defined on the target
180
+ if (prop in target) {
181
+ return Reflect.get(target, prop, receiver)
182
+ }
183
+ // Trigger collection and return the fragment
184
+ ensureImplicitCollected()
185
+ return selectionsMap.get(propName)?.fragment
186
+ }
187
+ return Reflect.get(target, prop, receiver)
188
+ },
189
+ has(target, prop): boolean {
190
+ if (typeof prop === 'string' && prop.startsWith('$')) {
191
+ ensureImplicitCollected()
192
+ const propName = prop.slice(1)
193
+ return selectionsMap.has(propName)
194
+ }
195
+ return Reflect.has(target, prop)
196
+ },
197
+ })
198
+ }
199
+
200
+ return MemoizedComponent
201
+ }
202
+
203
+ // ============================================================================
204
+ // Implicit Selection Collection
205
+ // ============================================================================
206
+
207
+ /**
208
+ * Creates a mock object for explicit entity props during collection phase.
209
+ * This prevents crashes when accessing .data or .fields on explicit props.
210
+ */
211
+ function createExplicitPropMock(): unknown {
212
+ const fieldsProxy = new Proxy({}, {
213
+ get(): unknown {
214
+ return { value: null }
215
+ },
216
+ })
217
+
218
+ return new Proxy({}, {
219
+ get(_target, prop): unknown {
220
+ switch (prop) {
221
+ case 'data':
222
+ case 'id':
223
+ return null
224
+ case 'fields':
225
+ return fieldsProxy
226
+ default:
227
+ return undefined
228
+ }
229
+ },
230
+ })
231
+ }
232
+
233
+ /**
234
+ * Collects selections from JSX for implicit entity props.
235
+ *
236
+ * In interfaces mode (hasInterfacesMode=true), any prop that is accessed
237
+ * and used as an entity-like object will be captured as a potential interface prop.
238
+ */
239
+ function collectImplicitSelections<TProps extends object>(
240
+ implicitConfigs: [string, EntityConfig][],
241
+ renderFn: (props: TProps) => ReactNode,
242
+ selectionsMap: Map<string, SelectionPropMeta>,
243
+ componentBrand: ComponentBrand,
244
+ roles: readonly string[],
245
+ hasInterfacesMode: boolean,
246
+ schemaRegistry: SchemaRegistry<Record<string, object>> | null,
247
+ conditionFn: ((props: TProps) => Condition) | null,
248
+ ): void {
249
+ const propScopes = new Map<string, SelectionScope>()
250
+ const implicitConfigsMap = new Map(implicitConfigs)
251
+ const implicitPropNames = new Set(implicitConfigs.map(([name]) => name))
252
+ const explicitPropNames = new Set(
253
+ [...selectionsMap.keys()].filter(name => !implicitPropNames.has(name)),
254
+ )
255
+
256
+ // Create proxy for props that captures field accesses using SelectionScope
257
+ const propsProxy = new Proxy({} as TProps, {
258
+ get(_target, propName: string | symbol): unknown {
259
+ if (typeof propName === 'symbol') {
260
+ return undefined
261
+ }
262
+
263
+ // For explicit entity props, return a mock object that won't crash
264
+ // when accessing .data or .fields during the collection phase
265
+ if (explicitPropNames.has(propName)) {
266
+ return createExplicitPropMock()
267
+ }
268
+
269
+ // For implicit entity props, create or reuse scopes
270
+ if (implicitPropNames.has(propName)) {
271
+ // Reuse existing scope or create new one
272
+ // This ensures both conditionFn and renderFn selections are captured in the same scope
273
+ let scope = propScopes.get(propName)
274
+ if (!scope) {
275
+ scope = new SelectionScope()
276
+ propScopes.set(propName, scope)
277
+ }
278
+ const config = implicitConfigsMap.get(propName)
279
+ const entityName = config?.entityName ?? null
280
+ // Prefer schema from entity def, fall back to provided schemaRegistry
281
+ const resolvedRegistry = config?.schema
282
+ ? new SchemaRegistry(config.schema)
283
+ : schemaRegistry
284
+ return createCollectorProxy(scope, entityName, resolvedRegistry)
285
+ }
286
+
287
+ // In interfaces mode, any unknown prop could be an interface entity prop
288
+ // Create or reuse a scope for it and return a collector proxy
289
+ if (hasInterfacesMode) {
290
+ let scope = propScopes.get(propName)
291
+ if (!scope) {
292
+ scope = new SelectionScope()
293
+ propScopes.set(propName, scope)
294
+ }
295
+ return createCollectorProxy(scope, null, schemaRegistry)
296
+ }
297
+
298
+ // Scalar prop - return undefined
299
+ return undefined
300
+ },
301
+ })
302
+
303
+ // Execute conditionFn to capture field accesses from the condition
304
+ if (conditionFn) {
305
+ conditionFn(propsProxy)
306
+ }
307
+
308
+ // Execute render to capture field accesses
309
+ const jsx = renderFn(propsProxy)
310
+
311
+ // Analyze JSX tree for component-level selections (handles nested createComponent)
312
+ try {
313
+ collectSelection(jsx)
314
+ } catch {
315
+ // staticRender of nested components may crash during collection phase
316
+ // when collector proxies don't fully implement all runtime interfaces.
317
+ // Field accesses are still captured in the scope tree via proxy.
318
+ }
319
+
320
+ // Create fragments for captured entities
321
+ for (const [propName, scope] of propScopes) {
322
+ if (scope.hasFields()) {
323
+ const selection = scope.toSelectionMeta()
324
+
325
+ selectionsMap.set(propName, {
326
+ selection,
327
+ fragment: createFragment(selection, componentBrand, roles),
328
+ })
329
+ }
330
+ }
331
+ }
332
+
333
+ // ============================================================================
334
+ // Fragment Creation
335
+ // ============================================================================
336
+
337
+ /**
338
+ * Creates a FluentFragment from selection metadata.
339
+ */
340
+ function createFragment(
341
+ selection: SelectionMeta,
342
+ componentBrand: ComponentBrand,
343
+ roles: readonly string[],
344
+ ): FluentFragment<unknown, object, AnyBrand> {
345
+ return {
346
+ __meta: selection,
347
+ __resultType: {} as object,
348
+ __modelType: undefined as unknown,
349
+ __isFragment: true,
350
+ __brand: componentBrand,
351
+ __brands: new Set([componentBrand.brandSymbol]),
352
+ __roles: roles.length > 0 ? roles : undefined,
353
+ }
354
+ }
355
+
356
+ // ============================================================================
357
+ // Selection Provider
358
+ // ============================================================================
359
+
360
+ /**
361
+ * Creates the getSelection function for SelectionProvider interface.
362
+ */
363
+ function createGetSelection(
364
+ selectionsMap: Map<string, SelectionPropMeta>,
365
+ ): SelectionProvider['getSelection'] {
366
+ return (
367
+ props: Record<string, unknown>,
368
+ _collectNested,
369
+ ): SelectionFieldMeta[] | null => {
370
+ const fields: SelectionFieldMeta[] = []
371
+
372
+ for (const [propName, meta] of selectionsMap) {
373
+ const propValue = props[propName]
374
+
375
+ // Case 1: Prop is a field reference (from relation)
376
+ if (propValue && typeof propValue === 'object' && FIELD_REF_META in propValue) {
377
+ const refMeta = (propValue as { [FIELD_REF_META]: { path: string[]; fieldName: string } })[FIELD_REF_META]
378
+
379
+ for (const [_key, field] of meta.selection.fields) {
380
+ if (field.path.length === 1) {
381
+ fields.push({
382
+ ...field,
383
+ path: [...refMeta.path, ...field.path],
384
+ })
385
+ }
386
+ }
387
+ }
388
+ // Case 2: Prop is an EntityRef from root level
389
+ else if (propValue && typeof propValue === 'object' && 'id' in propValue && 'fields' in propValue) {
390
+ for (const [_key, field] of meta.selection.fields) {
391
+ if (field.path.length === 1) {
392
+ fields.push({ ...field })
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ return fields.length > 0 ? fields : null
399
+ }
400
+ }