@kaskad/component-tree 0.0.1 → 0.0.2

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 (136) hide show
  1. package/fesm2022/kaskad-component-tree.mjs +2380 -0
  2. package/fesm2022/kaskad-component-tree.mjs.map +1 -0
  3. package/package.json +10 -10
  4. package/types/kaskad-component-tree.d.ts +582 -0
  5. package/esm2022/index.js +0 -12
  6. package/esm2022/index.js.map +0 -1
  7. package/esm2022/kaskad-component-tree.js +0 -5
  8. package/esm2022/kaskad-component-tree.js.map +0 -1
  9. package/esm2022/lib/component-lookup/index.js +0 -68
  10. package/esm2022/lib/component-lookup/index.js.map +0 -1
  11. package/esm2022/lib/component-lookup/traverses/array-traverser.js +0 -18
  12. package/esm2022/lib/component-lookup/traverses/array-traverser.js.map +0 -1
  13. package/esm2022/lib/component-lookup/traverses/component-traverser.js +0 -120
  14. package/esm2022/lib/component-lookup/traverses/component-traverser.js.map +0 -1
  15. package/esm2022/lib/component-lookup/traverses/index.js +0 -29
  16. package/esm2022/lib/component-lookup/traverses/index.js.map +0 -1
  17. package/esm2022/lib/component-lookup/traverses/object-traverser.js +0 -22
  18. package/esm2022/lib/component-lookup/traverses/object-traverser.js.map +0 -1
  19. package/esm2022/lib/component-lookup/types.js +0 -1
  20. package/esm2022/lib/component-lookup/types.js.map +0 -1
  21. package/esm2022/lib/component-tree-api.js +0 -121
  22. package/esm2022/lib/component-tree-api.js.map +0 -1
  23. package/esm2022/lib/computation/computation-frame.js +0 -27
  24. package/esm2022/lib/computation/computation-frame.js.map +0 -1
  25. package/esm2022/lib/computation/computation-stack.js +0 -83
  26. package/esm2022/lib/computation/computation-stack.js.map +0 -1
  27. package/esm2022/lib/computation/index.js +0 -3
  28. package/esm2022/lib/computation/index.js.map +0 -1
  29. package/esm2022/lib/config.js +0 -6
  30. package/esm2022/lib/config.js.map +0 -1
  31. package/esm2022/lib/mobx/component.js +0 -110
  32. package/esm2022/lib/mobx/component.js.map +0 -1
  33. package/esm2022/lib/mobx/create-root-node.js +0 -21
  34. package/esm2022/lib/mobx/create-root-node.js.map +0 -1
  35. package/esm2022/lib/mobx/ref-space.js +0 -25
  36. package/esm2022/lib/mobx/ref-space.js.map +0 -1
  37. package/esm2022/lib/mobx/store.js +0 -827
  38. package/esm2022/lib/mobx/store.js.map +0 -1
  39. package/esm2022/lib/node/create-node-options.js +0 -16
  40. package/esm2022/lib/node/create-node-options.js.map +0 -1
  41. package/esm2022/lib/node/creators/array-creator.js +0 -9
  42. package/esm2022/lib/node/creators/array-creator.js.map +0 -1
  43. package/esm2022/lib/node/creators/command-creator.js +0 -16
  44. package/esm2022/lib/node/creators/command-creator.js.map +0 -1
  45. package/esm2022/lib/node/creators/component-creator.js +0 -58
  46. package/esm2022/lib/node/creators/component-creator.js.map +0 -1
  47. package/esm2022/lib/node/creators/leaf-creator.js +0 -5
  48. package/esm2022/lib/node/creators/leaf-creator.js.map +0 -1
  49. package/esm2022/lib/node/creators/map-creator.js +0 -14
  50. package/esm2022/lib/node/creators/map-creator.js.map +0 -1
  51. package/esm2022/lib/node/creators/object-creator.js +0 -17
  52. package/esm2022/lib/node/creators/object-creator.js.map +0 -1
  53. package/esm2022/lib/node/creators/set-creator.js +0 -13
  54. package/esm2022/lib/node/creators/set-creator.js.map +0 -1
  55. package/esm2022/lib/node/creators/shape-creator.js +0 -15
  56. package/esm2022/lib/node/creators/shape-creator.js.map +0 -1
  57. package/esm2022/lib/node/creators/variant-shape-creator.js +0 -23
  58. package/esm2022/lib/node/creators/variant-shape-creator.js.map +0 -1
  59. package/esm2022/lib/node/guards.js +0 -5
  60. package/esm2022/lib/node/guards.js.map +0 -1
  61. package/esm2022/lib/node/node-creation.js +0 -47
  62. package/esm2022/lib/node/node-creation.js.map +0 -1
  63. package/esm2022/lib/node/node-type.js +0 -1
  64. package/esm2022/lib/node/node-type.js.map +0 -1
  65. package/esm2022/lib/node/node.js +0 -305
  66. package/esm2022/lib/node/node.js.map +0 -1
  67. package/esm2022/lib/parsers/index.js +0 -3
  68. package/esm2022/lib/parsers/index.js.map +0 -1
  69. package/esm2022/lib/parsers/node-selector.types.js +0 -1
  70. package/esm2022/lib/parsers/node-selector.types.js.map +0 -1
  71. package/esm2022/lib/parsers/parse-component-selector.js +0 -43
  72. package/esm2022/lib/parsers/parse-component-selector.js.map +0 -1
  73. package/esm2022/lib/parsers/parse-node-selector.js +0 -263
  74. package/esm2022/lib/parsers/parse-node-selector.js.map +0 -1
  75. package/esm2022/lib/types/command.js +0 -1
  76. package/esm2022/lib/types/command.js.map +0 -1
  77. package/esm2022/lib/types/index.js +0 -5
  78. package/esm2022/lib/types/index.js.map +0 -1
  79. package/esm2022/lib/types/node.guards.js +0 -7
  80. package/esm2022/lib/types/node.guards.js.map +0 -1
  81. package/esm2022/lib/types/schema.js +0 -1
  82. package/esm2022/lib/types/schema.js.map +0 -1
  83. package/esm2022/lib/util/extract-node-value.js +0 -45
  84. package/esm2022/lib/util/extract-node-value.js.map +0 -1
  85. package/esm2022/lib/util/format-source.js +0 -7
  86. package/esm2022/lib/util/format-source.js.map +0 -1
  87. package/esm2022/lib/util/get-component.js +0 -8
  88. package/esm2022/lib/util/get-component.js.map +0 -1
  89. package/esm2022/lib/util/id-generator.js +0 -10
  90. package/esm2022/lib/util/id-generator.js.map +0 -1
  91. package/esm2022/lib/util/traverse-node.js +0 -50
  92. package/esm2022/lib/util/traverse-node.js.map +0 -1
  93. package/index.d.ts +0 -11
  94. package/kaskad-component-tree.d.ts +0 -5
  95. package/lib/component-lookup/index.d.ts +0 -8
  96. package/lib/component-lookup/traverses/array-traverser.d.ts +0 -3
  97. package/lib/component-lookup/traverses/component-traverser.d.ts +0 -3
  98. package/lib/component-lookup/traverses/index.d.ts +0 -9
  99. package/lib/component-lookup/traverses/object-traverser.d.ts +0 -3
  100. package/lib/component-lookup/types.d.ts +0 -13
  101. package/lib/component-tree-api.d.ts +0 -21
  102. package/lib/computation/computation-frame.d.ts +0 -14
  103. package/lib/computation/computation-stack.d.ts +0 -48
  104. package/lib/computation/index.d.ts +0 -2
  105. package/lib/config.d.ts +0 -4
  106. package/lib/mobx/component.d.ts +0 -45
  107. package/lib/mobx/create-root-node.d.ts +0 -3
  108. package/lib/mobx/ref-space.d.ts +0 -10
  109. package/lib/mobx/store.d.ts +0 -238
  110. package/lib/node/create-node-options.d.ts +0 -12
  111. package/lib/node/creators/array-creator.d.ts +0 -4
  112. package/lib/node/creators/command-creator.d.ts +0 -4
  113. package/lib/node/creators/component-creator.d.ts +0 -4
  114. package/lib/node/creators/leaf-creator.d.ts +0 -4
  115. package/lib/node/creators/map-creator.d.ts +0 -4
  116. package/lib/node/creators/object-creator.d.ts +0 -4
  117. package/lib/node/creators/set-creator.d.ts +0 -4
  118. package/lib/node/creators/shape-creator.d.ts +0 -4
  119. package/lib/node/creators/variant-shape-creator.d.ts +0 -4
  120. package/lib/node/guards.d.ts +0 -3
  121. package/lib/node/node-creation.d.ts +0 -4
  122. package/lib/node/node-type.d.ts +0 -49
  123. package/lib/node/node.d.ts +0 -107
  124. package/lib/parsers/index.d.ts +0 -5
  125. package/lib/parsers/node-selector.types.d.ts +0 -25
  126. package/lib/parsers/parse-component-selector.d.ts +0 -8
  127. package/lib/parsers/parse-node-selector.d.ts +0 -87
  128. package/lib/types/command.d.ts +0 -3
  129. package/lib/types/index.d.ts +0 -4
  130. package/lib/types/node.guards.d.ts +0 -4
  131. package/lib/types/schema.d.ts +0 -13
  132. package/lib/util/extract-node-value.d.ts +0 -3
  133. package/lib/util/format-source.d.ts +0 -1
  134. package/lib/util/get-component.d.ts +0 -4
  135. package/lib/util/id-generator.d.ts +0 -5
  136. package/lib/util/traverse-node.d.ts +0 -26
@@ -1,827 +0,0 @@
1
- import { log } from '@kaskad/config';
2
- import { unfoldNodeSchema } from '@kaskad/schema';
3
- import { configure, observable, runInAction, untracked } from 'mobx';
4
- import { findComponent, findComponents, parseComponentSelector } from '../component-lookup';
5
- import { createNode } from '../node/node-creation';
6
- import { parseNodeSelector } from '../parsers';
7
- import { extractComponentsIds } from '../util/extract-node-value';
8
- import { formatSource } from '../util/format-source';
9
- import { getComponent } from '../util/get-component';
10
- import { componentIdGenerator } from '../util/id-generator';
11
- import { traverseNode } from '../util/traverse-node';
12
- configure({
13
- enforceActions: 'always',
14
- });
15
- export class ComponentStore {
16
- static instance;
17
- components = new Map();
18
- refSpaces = observable.map({}, { deep: false, name: 'ref-spaces-map' });
19
- componentsToPreload = new Set();
20
- // Index for O(m) RefSpace cleanup during component removal
21
- componentsByRefSpace = new Map();
22
- static getInstance() {
23
- if (!ComponentStore.instance) {
24
- ComponentStore.instance = new ComponentStore();
25
- }
26
- return ComponentStore.instance;
27
- }
28
- // tests purposes only
29
- static reset() {
30
- const store = ComponentStore.getInstance();
31
- runInAction(() => {
32
- // Remove all root components (components without parents)
33
- // This will cascade and remove all child components and RefSpaces
34
- const rootComponents = Array.from(store.components.values()).filter((c) => !c.position.componentId);
35
- for (const component of rootComponents) {
36
- store.removeComponent(component.id);
37
- }
38
- // Clear any remaining orphaned data (shouldn't be any, but just to be safe)
39
- store.components.clear();
40
- store.refSpaces.clear();
41
- store.componentsToPreload.clear();
42
- store.componentsByRefSpace.clear();
43
- componentIdGenerator.reset();
44
- });
45
- return store;
46
- }
47
- addRefSpace(refSpace) {
48
- runInAction(() => {
49
- this.refSpaces.set(refSpace.id, refSpace);
50
- });
51
- }
52
- addComponents(components) {
53
- runInAction(() => {
54
- for (const id in components) {
55
- const component = components[id];
56
- this.components.set(id, component);
57
- // Maintain componentsByRefSpace index
58
- if (!this.componentsByRefSpace.has(component.refSpaceId)) {
59
- this.componentsByRefSpace.set(component.refSpaceId, new Set());
60
- }
61
- this.componentsByRefSpace.get(component.refSpaceId)?.add(id);
62
- }
63
- });
64
- }
65
- getRefSpace(refSpaceId) {
66
- return this.refSpaces.get(refSpaceId) ?? null;
67
- }
68
- getRefSpaceOrThrow(refSpaceId) {
69
- const refSpace = this.getRefSpace(refSpaceId);
70
- if (!refSpace) {
71
- throw new Error(`RefSpace with id "${refSpaceId}" is not found.`);
72
- }
73
- return refSpace;
74
- }
75
- getComponent(componentId) {
76
- return this.components.get(componentId) || null;
77
- }
78
- getComponentOrThrow(componentId) {
79
- const component = this.getComponent(componentId);
80
- if (!component) {
81
- throw new Error(`Component with id "${componentId}" is not found.`);
82
- }
83
- return component;
84
- }
85
- findVariable(componentId, variableName) {
86
- const component = this.getComponentOrThrow(componentId);
87
- const node = component.variables.get(variableName);
88
- if (node) {
89
- return node;
90
- }
91
- const parentComponentId = component.position.componentId;
92
- if (!parentComponentId) {
93
- return null;
94
- }
95
- return this.findVariable(parentComponentId, variableName);
96
- }
97
- /**
98
- * Find a single component using a selector relative to another component.
99
- * Throws if multiple components match the selector.
100
- *
101
- * @param relativeTo - The component ID to search relative to
102
- * @param selector - Component selector string (e.g., 'Button', '&myRef', '^parent', ':button', 'Button[disabled=true]')
103
- * @returns The matching component or null if not found
104
- * @throws If the selector matches multiple components
105
- *
106
- * @example
107
- * ```typescript
108
- * const button = store.findComponent(componentId, 'Button');
109
- * const aliased = store.findComponent(componentId, '&myRef');
110
- * const parent = store.findComponent(componentId, '^parent');
111
- * const byType = store.findComponent(componentId, ':button');
112
- * const filtered = store.findComponent(componentId, 'Button[disabled=true]');
113
- * ```
114
- */
115
- findComponent(relativeTo, selector) {
116
- const parsed = parseComponentSelector(selector);
117
- return findComponent(relativeTo, parsed);
118
- }
119
- /**
120
- * Find all components matching a selector relative to another component.
121
- *
122
- * @param relativeTo - The component ID to search relative to
123
- * @param selector - Component selector string (e.g., 'Button', '&myRef', ':button', 'Button[disabled=true]')
124
- * @returns Array of matching components (empty if none found)
125
- *
126
- * @example
127
- * ```typescript
128
- * const buttons = store.findComponents(componentId, 'Button');
129
- * const allOfType = store.findComponents(componentId, ':button');
130
- * const filtered = store.findComponents(componentId, 'Button[disabled=true]');
131
- * ```
132
- */
133
- findComponents(relativeTo, selector) {
134
- const parsed = parseComponentSelector(selector);
135
- return findComponents(relativeTo, parsed);
136
- }
137
- /**
138
- * Get a node by selector relative to a component.
139
- * Throws an error if the component or node is not found.
140
- * Use findNode() for optional lookups that return null instead of throwing.
141
- *
142
- * Note: Even if the selector contains optional chaining (?.), this method will throw
143
- * if the node is not found. Use findNode() for optional chaining support.
144
- *
145
- * Component selector behavior:
146
- * - `&ref->value` - Throws if component with ref doesn't exist
147
- * - `&ref?->value` - Throws if node not found, but allows missing component (returns null for the whole expression)
148
- *
149
- * @param relativeTo - The component ID to search relative to
150
- * @param selector - Node selector string (e.g., 'prop', '$variable', '^Parent->prop', '&ref?->value')
151
- * @returns The node
152
- * @throws If the component or node is not found (unless using ?-> for component)
153
- *
154
- * @example
155
- * ```typescript
156
- * const node = store.getNode(componentId, 'requiredProp'); // throws if not found
157
- * const optionalNode = store.findNode(componentId, 'user?.profile'); // returns null if not found
158
- * const optionalComponent = store.getNode(componentId, '&optional?->value'); // throws only if value missing, not if component missing
159
- * ```
160
- */
161
- getNode(relativeTo, selector) {
162
- const parsed = parseNodeSelector(selector);
163
- // Look up the component
164
- let component;
165
- if (parsed.component) {
166
- component = findComponent(relativeTo, parsed.component.selector);
167
- // If component not found and not using optional selector, throw error
168
- if (!component && !parsed.component.optional) {
169
- const src = formatSource(this.getComponent(relativeTo)?.sourceUrl);
170
- throw new Error(`${src} Component not found for selector '${selector}' relative to '${relativeTo}'`);
171
- }
172
- }
173
- else {
174
- component = this.getComponentOrThrow(relativeTo);
175
- }
176
- // If component is null (only possible with component.optional), throw
177
- if (!component) {
178
- const src = formatSource(this.getComponent(relativeTo)?.sourceUrl);
179
- throw new Error(`${src} Component not found for selector '${selector}' relative to '${relativeTo}'`);
180
- }
181
- const src = formatSource(component.sourceUrl);
182
- // If it's a variable path, use RefSpace chain lookup
183
- if (parsed.kind === 'variable') {
184
- const node = this.findVariable(component.id, parsed.head);
185
- if (!node) {
186
- throw new Error(`${src} Variable '${parsed.head}' does not exist in component '${component.id}' or its parent RefSpaces`);
187
- }
188
- const result = traverseNode(node, parsed.tail);
189
- if (!result) {
190
- throw new Error(`${src} Node not found for selector '${selector}' in component '${component.id}'`);
191
- }
192
- return result;
193
- }
194
- // For property paths, use strict lookup that throws on missing nodes
195
- const node = component.getNodeFromParsed(parsed);
196
- if (!node) {
197
- throw new Error(`${src} Node not found for selector '${selector}' in component '${component.id}'`);
198
- }
199
- return node;
200
- }
201
- /**
202
- * Find a node by selector relative to a component.
203
- * Returns null if the node is not found.
204
- *
205
- * Component selector behavior:
206
- * - `&ref->value` - Throws if component doesn't exist (strict by default)
207
- * - `&ref?->value` - Returns null if component doesn't exist (lenient when explicit)
208
- *
209
- * Use `?->` to explicitly indicate that a missing component is acceptable.
210
- * This makes the code self-documenting and prevents accidental null returns.
211
- *
212
- * @param relativeTo - The component ID to search relative to
213
- * @param selector - Node selector string (e.g., 'prop', '$variable', '^Parent->prop', '&ref?->value')
214
- * @returns The node or null if not found
215
- *
216
- * @example
217
- * ```typescript
218
- * // Strict - throws if component missing
219
- * const node = store.findNode(componentId, '&ref->value');
220
- *
221
- * // Lenient - returns null if component missing
222
- * const optionalNode = store.findNode(componentId, '&ref?->value');
223
- * if (optionalNode) {
224
- * // Use the node
225
- * }
226
- * ```
227
- */
228
- findNode(relativeTo, selector) {
229
- const parsed = parseNodeSelector(selector);
230
- // If a component selector is specified, look up the component
231
- let component = parsed.component
232
- ? findComponent(relativeTo, parsed.component.selector)
233
- : this.getComponent(relativeTo);
234
- // If component not found and not using optional selector, throw error
235
- if (!component && parsed.component && !parsed.component.optional) {
236
- const src = formatSource(this.getComponent(relativeTo)?.sourceUrl);
237
- throw new Error(`${src} Component not found for selector '${selector}' relative to '${relativeTo}'`);
238
- }
239
- if (!component) {
240
- return null;
241
- }
242
- // If looking up a variable in a sys.Facade component (without explicit component selector),
243
- // start search from the facade's parent. This ensures variables are resolved from the facade's
244
- // context, not from within the facade itself. This only applies when using the implicit
245
- // relativeTo component, not when explicitly selecting a Facade (e.g., ':sys.Facade->$var')
246
- if (parsed.kind === 'variable' && !parsed.component && component.componentType === 'sys.Facade') {
247
- const parentComponentId = component.position.componentId;
248
- if (parentComponentId) {
249
- const parentComponent = this.getComponent(parentComponentId);
250
- // If parent exists, use it for variable lookup; otherwise fall back to the Facade itself
251
- if (parentComponent) {
252
- component = parentComponent;
253
- }
254
- }
255
- }
256
- const src = formatSource(component.sourceUrl);
257
- // If it's a variable path, use RefSpace chain lookup
258
- if (parsed.kind === 'variable') {
259
- const node = this.findVariable(component.id, parsed.head);
260
- if (!node) {
261
- // Variable itself doesn't exist - this is likely a typo, so throw
262
- throw new Error(`${src} Variable "${parsed.head}" not found in component "${component.id}"`);
263
- }
264
- // Variable exists, traverse the path (may return null if path is missing)
265
- return traverseNode(node, parsed.tail);
266
- }
267
- // For property paths, throw if property doesn't exist but allow null paths
268
- const node = component.props.get(parsed.head);
269
- if (!node) {
270
- throw new Error(`${src} Property "${parsed.head}" not found in component "${component.id}" (type: ${component.componentType}). ` +
271
- `Available properties: [${Array.from(component.props.keys()).join(', ')}]`);
272
- }
273
- // Property exists, traverse the path (may return null if path is missing)
274
- return traverseNode(node, parsed.tail);
275
- }
276
- /**
277
- * Collect node values from multiple components into an array.
278
- * Maps over all components matching the selector and extracts a node value from each.
279
- *
280
- * @param relativeTo - The component ID to search relative to
281
- * @param componentSelector - Selector for finding components (e.g., ':forms.Input', 'Button')
282
- * @param nodeSelector - The node path to extract from each component (e.g., 'value', 'inputKey')
283
- * @returns Array of node values from matched components
284
- *
285
- * @example
286
- * ```typescript
287
- * // Get all input values
288
- * const values = store.collectValuesArray(componentId, ':forms.Input', 'value');
289
- * // Returns: ['value1', 'value2', 'value3']
290
- * ```
291
- */
292
- collectValuesArray(relativeTo, componentSelector, nodeSelector) {
293
- const components = this.findComponents(relativeTo, componentSelector);
294
- return components.map((component) => component.getNodeValue(nodeSelector));
295
- }
296
- /**
297
- * Collect node values from multiple components into a map/object.
298
- * Reduces over all components matching the selector, using one node as the key and another as the value.
299
- *
300
- * @param relativeTo - The component ID to search relative to
301
- * @param componentSelector - Selector for finding components (e.g., ':forms.Input', 'Button')
302
- * @param keyNodeSelector - The node path to use as the object key (e.g., 'inputKey', 'id')
303
- * @param valueNodeSelector - The node path to use as the object value (e.g., 'value', 'label')
304
- * @returns Object/map with keys and values from matched components
305
- *
306
- * @example
307
- * ```typescript
308
- * // Create a map from inputKey to value
309
- * const map = store.collectValuesMap(componentId, ':forms.Input', 'inputKey', 'value');
310
- * // Returns: { key1: 'value1', key2: 'value2' }
311
- * ```
312
- */
313
- collectValuesMap(relativeTo, componentSelector, keyNodeSelector, valueNodeSelector) {
314
- const components = this.findComponents(relativeTo, componentSelector);
315
- return components.reduce((acc, component) => {
316
- const key = component.getNodeValue(keyNodeSelector);
317
- const value = component.getNodeValue(valueNodeSelector);
318
- // skip if the key is not defined
319
- if (key) {
320
- acc[key] = value;
321
- }
322
- return acc;
323
- }, {});
324
- }
325
- /**
326
- * Set a node from a raw schema value.
327
- * The raw schema will be unfolded before being applied.
328
- * Handles structure, computation stack, and nested components.
329
- *
330
- * @param node - The node to update
331
- * @param rawSchema - The raw schema value (will be unfolded into a NodeSchema)
332
- */
333
- setNodeRawSchema(node, rawSchema) {
334
- try {
335
- const unfolded = unfoldNodeSchema(rawSchema, node.valueType);
336
- this.setNodeSchema(node, unfolded);
337
- }
338
- catch (e) {
339
- const component = getComponent(node);
340
- const src = formatSource(component.sourceUrl, node.position.path);
341
- throw new Error(`${src} ${e.message}`, { cause: e });
342
- }
343
- }
344
- /**
345
- * Set a node from an already-unfolded schema.
346
- * Use this when you already have a NodeSchema to avoid double-unfolding.
347
- * Handles structure, nested components, and computation stack.
348
- *
349
- * @param node - The node to update
350
- * @param schema - The unfolded NodeSchema to set
351
- */
352
- setNodeSchema(node, schema) {
353
- const component = getComponent(node);
354
- const refSpace = this.getRefSpaceOrThrow(component.refSpaceId);
355
- const opts = {
356
- refSpaceId: refSpace.id,
357
- position: { componentId: component.id, path: node.position.path },
358
- components: {},
359
- refs: {},
360
- sourceUrl: component.sourceUrl ?? undefined,
361
- };
362
- const tmpNode = createNode(schema, opts);
363
- runInAction(() => {
364
- // Handle component removal for component-type nodes
365
- // Only remove components that are in the old structure but NOT in the new structure
366
- // This is important for component[] types where existing component IDs may be reused
367
- const oldComponents = extractComponentsIds(node);
368
- const newComponents = new Set(extractComponentsIds(tmpNode));
369
- for (const id of oldComponents) {
370
- if (!newComponents.has(id)) {
371
- this.removeComponent(id);
372
- }
373
- }
374
- // Set new structure and register new components/refs
375
- node.structure = tmpNode.structure;
376
- // Add refs for all RefSpaces (parent RefSpace and any nested RefSpaces created)
377
- for (const refSpaceId in opts.refs) {
378
- const targetRefSpace = this.getRefSpace(refSpaceId);
379
- if (targetRefSpace) {
380
- const refs = opts.refs[refSpaceId] ?? {};
381
- targetRefSpace.addRefs(refs);
382
- }
383
- }
384
- this.addComponents(opts.components);
385
- });
386
- // Push computation frame onto stack (after runAction, before activation)
387
- if (schema.computation) {
388
- const resultFrame = tmpNode.computationStack.top;
389
- if (resultFrame) {
390
- node.computationStack.push(resultFrame.schema);
391
- }
392
- }
393
- }
394
- /**
395
- * Remove an item from an array node.
396
- * Handles cleanup of nested components in the removed item.
397
- *
398
- * @param arrayNode - The array node to remove from
399
- * @param index - The index of the item to remove
400
- */
401
- removeArrayItem(arrayNode, index) {
402
- runInAction(() => {
403
- const value = arrayNode.structure || [];
404
- const removedItem = value[index];
405
- // Clean up components in the removed item
406
- if (removedItem) {
407
- const oldComponents = extractComponentsIds(removedItem);
408
- for (const id of oldComponents) {
409
- this.removeComponent(id);
410
- }
411
- }
412
- arrayNode.structure = [...value.slice(0, index), ...value.slice(index + 1)];
413
- });
414
- }
415
- /**
416
- * Insert an item into an array node at a specific index.
417
- * Creates a node structure from the raw value and inserts it at the specified position.
418
- * Shifts existing items to the right.
419
- *
420
- * @param arrayNode - The array node to insert into
421
- * @param index - The index at which to insert the item
422
- * @param value - The raw value to insert (will be transformed into a node structure)
423
- */
424
- insertArrayItem(arrayNode, index, value) {
425
- const { itemNode, opts, refSpace } = this.createArrayItemNode(arrayNode, index, value);
426
- runInAction(() => {
427
- const currentValue = arrayNode.structure || [];
428
- arrayNode.structure = [...currentValue.slice(0, index), itemNode, ...currentValue.slice(index)];
429
- const refs = opts.refs[refSpace.id] ?? {};
430
- refSpace.addRefs(refs);
431
- this.addComponents(opts.components);
432
- });
433
- }
434
- /**
435
- * Replace an item in an array node at a specific index.
436
- * Creates a node structure from the raw value and replaces the item at the specified position.
437
- * Properly cleans up nested components in the replaced item.
438
- *
439
- * @param arrayNode - The array node containing the item to replace
440
- * @param index - The index of the item to replace
441
- * @param value - The raw value to set (will be transformed into a node structure)
442
- */
443
- replaceArrayItem(arrayNode, index, value) {
444
- const { itemNode, opts, refSpace } = this.createArrayItemNode(arrayNode, index, value);
445
- runInAction(() => {
446
- const currentValue = arrayNode.structure || [];
447
- const replacedItem = currentValue[index];
448
- // Clean up components in the replaced item
449
- if (replacedItem) {
450
- const oldComponents = extractComponentsIds(replacedItem);
451
- for (const id of oldComponents) {
452
- this.removeComponent(id);
453
- }
454
- }
455
- arrayNode.structure = [...currentValue.slice(0, index), itemNode, ...currentValue.slice(index + 1)];
456
- const refs = opts.refs[refSpace.id] ?? {};
457
- refSpace.addRefs(refs);
458
- this.addComponents(opts.components);
459
- });
460
- }
461
- /**
462
- * Append an item to the end of an array node.
463
- * Creates a node structure from the raw value and appends it to the array.
464
- *
465
- * @param arrayNode - The array node to append to
466
- * @param value - The raw value to append (will be transformed into a node structure)
467
- */
468
- appendArrayItem(arrayNode, value) {
469
- // Calculate the index for the new item
470
- const currentLength = arrayNode.structure?.length ?? 0;
471
- const { itemNode, opts, refSpace } = this.createArrayItemNode(arrayNode, currentLength, value);
472
- runInAction(() => {
473
- arrayNode.structure = [...(arrayNode.structure ?? []), itemNode];
474
- const refs = opts.refs[refSpace.id] ?? {};
475
- refSpace.addRefs(refs);
476
- this.addComponents(opts.components);
477
- });
478
- }
479
- /**
480
- * Reconcile for-loop children using key-based diffing.
481
- * Instead of destroying all children and recreating them,
482
- * this method compares old vs new keys and only creates/destroys what's necessary.
483
- *
484
- * @param arrayNode - The array node containing for-loop children
485
- * @param newSchemas - The new schemas to reconcile
486
- * @returns Indices of newly added items for activation
487
- */
488
- reconcileForLoopChildren(arrayNode, newSchemas) {
489
- const oldChildren = arrayNode.structure || [];
490
- // Handle null/undefined newSchemas
491
- if (!newSchemas) {
492
- this.setNodeRawSchema(arrayNode, []);
493
- return [];
494
- }
495
- // If there are no old children, just set the new schemas
496
- if (oldChildren.length === 0) {
497
- this.setNodeRawSchema(arrayNode, newSchemas);
498
- return newSchemas.map((_, index) => index);
499
- }
500
- // If there are no new schemas, just set empty array (will remove all)
501
- if (newSchemas.length === 0) {
502
- this.setNodeRawSchema(arrayNode, []);
503
- return [];
504
- }
505
- // Check if explicit keys are provided (trackBy syntax)
506
- const hasExplicitKeys = this.checkForExplicitKeys(newSchemas);
507
- if (!hasExplicitKeys) {
508
- // No explicit keys - fall back to full replacement
509
- this.setNodeRawSchema(arrayNode, newSchemas);
510
- return newSchemas.map((_, index) => index);
511
- }
512
- // Extract keys from old children and new schemas
513
- const oldKeyMap = this.extractOldKeys(oldChildren);
514
- const newKeys = this.extractNewKeys(newSchemas);
515
- // Check for duplicate keys in new schemas
516
- const duplicateKeys = this.findDuplicateKeys(newKeys);
517
- if (duplicateKeys.size > 0) {
518
- log.warn(`Duplicate keys detected in for-loop: ${Array.from(duplicateKeys).join(', ')}. ` +
519
- 'This may cause unexpected behavior. Falling back to full replacement.');
520
- this.setNodeRawSchema(arrayNode, newSchemas);
521
- return newSchemas.map((_, index) => index);
522
- }
523
- // Build sets for diffing
524
- const oldKeySet = new Set(oldKeyMap.keys());
525
- const newKeySet = new Set(newKeys);
526
- // Determine what to add, remove, and keep
527
- const toRemove = [];
528
- const toKeepSet = new Set();
529
- for (const key of oldKeySet) {
530
- if (newKeySet.has(key)) {
531
- toKeepSet.add(key);
532
- }
533
- else {
534
- toRemove.push(key);
535
- }
536
- }
537
- const toAdd = [];
538
- newKeys.forEach((key, index) => {
539
- if (!oldKeySet.has(key)) {
540
- toAdd.push(index);
541
- }
542
- });
543
- // Perform the actual reconciliation
544
- this.applyForLoopReconciliation(arrayNode, {
545
- oldKeyMap,
546
- newKeys,
547
- newSchemas,
548
- toRemove,
549
- toKeep: Array.from(toKeepSet),
550
- });
551
- // Update loop variables (index, first, last, and item variables) for repositioned components
552
- this.updateLoopVariables(arrayNode, newKeys, newSchemas, oldKeyMap);
553
- return toAdd;
554
- }
555
- /**
556
- * Apply for-loop reconciliation by building new structure and cleaning up removed components.
557
- */
558
- applyForLoopReconciliation(arrayNode, reconcileInfo) {
559
- const { oldKeyMap, newKeys, newSchemas, toRemove, toKeep } = reconcileInfo;
560
- const toKeepSet = new Set(toKeep);
561
- const component = getComponent(arrayNode);
562
- const refSpace = this.getRefSpaceOrThrow(component.refSpaceId);
563
- // Build the new structure array, reusing kept nodes and creating new ones
564
- const newStructure = [];
565
- const newComponents = {};
566
- const newRefs = {};
567
- // Build new structure in order of new keys
568
- // Use untracked to avoid MobX reactivity issues when reading structure
569
- const oldStructure = untracked(() => arrayNode.structure);
570
- for (let i = 0; i < newKeys.length; i++) {
571
- const key = newKeys[i];
572
- const oldEntry = oldKeyMap.get(key);
573
- if (oldEntry && toKeepSet.has(key) && oldStructure && oldStructure[oldEntry.index]) {
574
- // Reuse existing node
575
- newStructure.push(oldStructure[oldEntry.index]);
576
- }
577
- else {
578
- // Create new node
579
- const schema = newSchemas[i];
580
- const itemPath = [...arrayNode.position.path, i];
581
- const opts = {
582
- refSpaceId: refSpace.id,
583
- position: { componentId: component.id, path: itemPath },
584
- components: {},
585
- refs: {},
586
- sourceUrl: component.sourceUrl ?? undefined,
587
- };
588
- const itemNode = createNode(unfoldNodeSchema(schema, arrayNode.valueType.item), opts);
589
- newStructure.push(itemNode);
590
- // Collect new components and refs
591
- Object.assign(newComponents, opts.components);
592
- const itemRefs = opts.refs[refSpace.id] ?? {};
593
- Object.assign(newRefs, itemRefs);
594
- }
595
- }
596
- runInAction(() => {
597
- // Remove components that are no longer needed
598
- for (const key of toRemove) {
599
- const entry = oldKeyMap.get(key);
600
- if (entry) {
601
- this.removeComponent(entry.componentId);
602
- }
603
- }
604
- // Update the structure
605
- arrayNode.structure = newStructure;
606
- // Register new refs and components
607
- refSpace.addRefs(newRefs);
608
- this.addComponents(newComponents);
609
- });
610
- }
611
- /**
612
- * Update loop variables (including item variable) on components that have been repositioned or whose data changed.
613
- */
614
- updateLoopVariables(node, newKeys, newSchemas, oldKeyMap) {
615
- const structure = node.structure || [];
616
- const totalItems = newKeys.length;
617
- runInAction(() => {
618
- newKeys.forEach((key, newIndex) => {
619
- const oldEntry = oldKeyMap.get(key);
620
- if (!oldEntry)
621
- return; // Skip newly added items
622
- const child = structure[newIndex];
623
- if (!child)
624
- return;
625
- const componentId = child.structure;
626
- if (!componentId)
627
- return;
628
- const component = this.getComponent(componentId);
629
- if (!component)
630
- return;
631
- // Update $index variable if it exists and the index changed
632
- // Variables are stored without the $ prefix (e.g., 'index' not '$index')
633
- const indexNode = component.variables.get('index');
634
- if (indexNode && oldEntry.index !== newIndex) {
635
- indexNode.structure = newIndex;
636
- }
637
- // Update $first variable if it exists
638
- const firstNode = component.variables.get('first');
639
- if (firstNode) {
640
- const wasFirst = oldEntry.index === 0;
641
- const isFirst = newIndex === 0;
642
- if (wasFirst !== isFirst) {
643
- firstNode.structure = isFirst;
644
- }
645
- }
646
- // Update $last variable if it exists
647
- const lastNode = component.variables.get('last');
648
- if (lastNode) {
649
- lastNode.structure = newIndex === totalItems - 1;
650
- }
651
- // Update item variables from new schema
652
- const newSchema = newSchemas[newIndex];
653
- for (const [schemaKey, schemaValue] of Object.entries(newSchema)) {
654
- if (!schemaKey.startsWith('$'))
655
- continue;
656
- const varName = schemaKey.slice(1); // Remove '$' prefix
657
- // Skip loop control variables (already handled above)
658
- if (varName === 'index' || varName === 'first' || varName === 'last' || varName === 'trackBy') {
659
- continue;
660
- }
661
- const varNode = component.variables.get(varName);
662
- if (!varNode)
663
- continue;
664
- const varDef = schemaValue;
665
- if (!varDef || !('_value' in varDef))
666
- continue;
667
- // Update item variable using setNodeRawSchema for proper object handling
668
- this.setNodeRawSchema(varNode, varDef._value);
669
- }
670
- });
671
- });
672
- }
673
- /**
674
- * Check if schemas have explicit trackBy keys that can be used for diffing.
675
- */
676
- checkForExplicitKeys(schemas) {
677
- if (schemas.length === 0)
678
- return false;
679
- const firstSchema = schemas[0];
680
- if (!firstSchema)
681
- return false;
682
- const forKeySchema = firstSchema['$trackBy'];
683
- if (forKeySchema && '_value' in forKeySchema) {
684
- const value = forKeySchema._value;
685
- if (typeof value === 'number') {
686
- return false;
687
- }
688
- if (typeof value === 'string') {
689
- const indexPattern = /^\d+(?::\d+)*$/;
690
- return !indexPattern.test(value);
691
- }
692
- return true;
693
- }
694
- return false;
695
- }
696
- /**
697
- * Extract keys from existing children (RefSpace components with $trackBy variable)
698
- */
699
- extractOldKeys(children) {
700
- const keyMap = new Map();
701
- children.forEach((child, index) => {
702
- const componentId = child.structure;
703
- if (!componentId)
704
- return;
705
- const component = this.getComponent(componentId);
706
- if (!component || component.componentType !== 'sys.RefSpace')
707
- return;
708
- const keyNode = component.variables.get('trackBy');
709
- const key = keyNode ? keyNode.extractedValue : component.id;
710
- keyMap.set(String(key), { index, componentId });
711
- });
712
- return keyMap;
713
- }
714
- /**
715
- * Extract keys from new schemas (generated by repeat())
716
- */
717
- extractNewKeys(schemas) {
718
- return schemas.map((schema, index) => {
719
- const forKeyValue = schema['$trackBy'];
720
- if (forKeyValue && '_value' in forKeyValue) {
721
- return String(forKeyValue._value);
722
- }
723
- return String(index);
724
- });
725
- }
726
- /**
727
- * Find duplicate keys in an array
728
- */
729
- findDuplicateKeys(keys) {
730
- const seen = new Set();
731
- const duplicates = new Set();
732
- for (const key of keys) {
733
- if (seen.has(key)) {
734
- duplicates.add(key);
735
- }
736
- seen.add(key);
737
- }
738
- return duplicates;
739
- }
740
- removeComponent(componentId) {
741
- const component = this.components.get(componentId);
742
- if (!component)
743
- return;
744
- runInAction(() => {
745
- // Collect all nested child component IDs (depth-first traversal)
746
- const allChildIds = [];
747
- for (const [, propNode] of component.props) {
748
- extractComponentsIds(propNode, allChildIds);
749
- }
750
- for (const [, varNode] of component.variables) {
751
- extractComponentsIds(varNode, allChildIds);
752
- }
753
- // Deduplicate while preserving order (later items are deeper in tree)
754
- const childIds = [...new Set(allChildIds)];
755
- const allIds = new Set([componentId, ...childIds]);
756
- const spacesToRemove = this.collectRefSpacesToRemove(allIds);
757
- // Dispose components in leaf-first order (reverse of depth-first)
758
- for (let i = childIds.length - 1; i >= 0; i--) {
759
- this.components.get(childIds[i])?.dispose();
760
- }
761
- component.dispose();
762
- // Finally, delete all components and RefSpaces from registries
763
- for (const id of allIds) {
764
- // Remove from componentsByRefSpace index
765
- const comp = this.components.get(id);
766
- if (comp) {
767
- this.componentsByRefSpace.get(comp.refSpaceId)?.delete(id);
768
- }
769
- this.components.delete(id);
770
- }
771
- for (const refSpaceId of spacesToRemove) {
772
- this.refSpaces.delete(refSpaceId);
773
- // Clean up empty index entries
774
- this.componentsByRefSpace.delete(refSpaceId);
775
- }
776
- });
777
- }
778
- /**
779
- * Creates a new node for an array item from a raw value.
780
- * Returns the created node and the options used (for registering components/refs).
781
- * @throws If the array node has no parent component
782
- */
783
- createArrayItemNode(arrayNode, index, value) {
784
- const component = getComponent(arrayNode);
785
- const refSpace = this.getRefSpaceOrThrow(component.refSpaceId);
786
- const itemPath = [...arrayNode.position.path, index];
787
- const opts = {
788
- refSpaceId: refSpace.id,
789
- position: { componentId: component.position.componentId, path: itemPath },
790
- components: {},
791
- refs: {},
792
- sourceUrl: component.sourceUrl ?? undefined,
793
- };
794
- const itemNode = createNode(unfoldNodeSchema(value, arrayNode.valueType.item), opts);
795
- return { itemNode, opts, refSpace };
796
- }
797
- collectRefSpacesToRemove(componentIdsToDelete) {
798
- // Collect unique RefSpace IDs that components belong to (candidates for removal)
799
- const candidateSpaceIds = new Set();
800
- for (const componentId of componentIdsToDelete) {
801
- const refSpaceId = this.components.get(componentId)?.refSpaceId;
802
- if (refSpaceId) {
803
- candidateSpaceIds.add(refSpaceId);
804
- }
805
- }
806
- // Filter to only RefSpaces that have no remaining components after deletion
807
- const refSpaceIdsToDelete = new Set();
808
- for (const refSpaceId of candidateSpaceIds) {
809
- const componentsInSpace = this.componentsByRefSpace.get(refSpaceId);
810
- if (!componentsInSpace)
811
- continue;
812
- // Check if ALL components in this RefSpace are being deleted
813
- let allDeleted = true;
814
- for (const id of componentsInSpace) {
815
- if (!componentIdsToDelete.has(id)) {
816
- allDeleted = false;
817
- break;
818
- }
819
- }
820
- if (allDeleted) {
821
- refSpaceIdsToDelete.add(refSpaceId);
822
- }
823
- }
824
- return refSpaceIdsToDelete;
825
- }
826
- }
827
- //# sourceMappingURL=store.js.map