@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
@@ -0,0 +1,2380 @@
1
+ import { makeObservable, observable, runInAction, computed, action, configure, untracked } from 'mobx';
2
+ import { isObject, unfoldNodeSchema } from '@kaskad/schema';
3
+ import { Delimiters, log } from '@kaskad/config';
4
+ import { DefinitionStore } from '@kaskad/definition';
5
+
6
+ function isArrayNode(node) {
7
+ return node.valueType.type === 'array';
8
+ }
9
+ function isCommandNode(node) {
10
+ return node.valueType.type === 'command';
11
+ }
12
+
13
+ class ComputationFrame {
14
+ schema;
15
+ disposers;
16
+ activated = false;
17
+ constructor(schema, disposers = [], activated = false) {
18
+ this.schema = schema;
19
+ this.disposers = disposers;
20
+ this.activated = activated;
21
+ makeObservable(this, {
22
+ activated: observable.ref,
23
+ });
24
+ }
25
+ /**
26
+ * Mark this computation frame as activated.
27
+ * This is an action that ensures MobX compliance.
28
+ */
29
+ markActivated() {
30
+ runInAction(() => {
31
+ this.activated = true;
32
+ });
33
+ }
34
+ dispose() {
35
+ this.disposers.forEach((d) => d());
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Manages a stack of computation frames for nested computations.
41
+ * For example: formula → formula → template chain.
42
+ */
43
+ class ComputationStack {
44
+ /**
45
+ * Maximum allowed stack depth to prevent infinite recursion.
46
+ */
47
+ static MAX_DEPTH = 10;
48
+ _frames;
49
+ constructor() {
50
+ this._frames = observable.array([], { deep: false, name: 'computation-stack' });
51
+ }
52
+ /**
53
+ * Get the topmost computation frame from the stack.
54
+ * Returns null if stack is empty.
55
+ */
56
+ get top() {
57
+ return this._frames[this._frames.length - 1] ?? null;
58
+ }
59
+ /**
60
+ * Returns true if all frames in the stack are activated.
61
+ * Empty stack returns true (vacuously).
62
+ */
63
+ get allActivated() {
64
+ return this._frames.length === 0 || this._frames.every((f) => f.activated);
65
+ }
66
+ /**
67
+ * Get the current stack depth.
68
+ */
69
+ get depth() {
70
+ return this._frames.length;
71
+ }
72
+ /**
73
+ * Check if the stack is empty.
74
+ */
75
+ get isEmpty() {
76
+ return this._frames.length === 0;
77
+ }
78
+ /**
79
+ * Push a new computation frame onto the stack.
80
+ * Throws if depth limit is exceeded.
81
+ */
82
+ push(schema) {
83
+ if (this._frames.length >= ComputationStack.MAX_DEPTH) {
84
+ throw new Error(`Computation stack overflow: depth ${this._frames.length + 1} exceeds maximum ${ComputationStack.MAX_DEPTH}. ` +
85
+ `This may indicate infinite recursion in computation chain.`);
86
+ }
87
+ return runInAction(() => {
88
+ const frame = new ComputationFrame(schema, [], false);
89
+ this._frames.push(frame);
90
+ return frame;
91
+ });
92
+ }
93
+ /**
94
+ * Pop all frames from the given depth onward.
95
+ * For example, if depth=2, this removes frames at indices 2, 3, 4, etc.
96
+ * Useful when a computation at a specific depth re-evaluates and needs
97
+ * to clear its results without affecting parent computations.
98
+ */
99
+ popFrom(depth) {
100
+ runInAction(() => {
101
+ while (this._frames.length > depth) {
102
+ const frame = this._frames.pop();
103
+ frame?.dispose();
104
+ }
105
+ });
106
+ }
107
+ /**
108
+ * Dispose all frames in the stack.
109
+ */
110
+ dispose() {
111
+ runInAction(() => {
112
+ while (this._frames.length > 0) {
113
+ const frame = this._frames.pop();
114
+ frame?.dispose();
115
+ }
116
+ });
117
+ }
118
+ }
119
+
120
+ const componentTreeConfig = {
121
+ runCommand: (_node, ..._args) => {
122
+ throw new Error('runCommand is not implemented. Please provide an implementation.');
123
+ },
124
+ };
125
+
126
+ function isNode(value) {
127
+ return isObject(value) && 'nodeType' in value;
128
+ }
129
+
130
+ class AbstractNode {
131
+ valueType;
132
+ position;
133
+ nodeType;
134
+ computationStack = new ComputationStack();
135
+ bindingSchema = null;
136
+ extraBinding = null;
137
+ disposers = [];
138
+ selfEvaluating = false;
139
+ selfFailed = false;
140
+ selfActivated = false;
141
+ constructor(valueType, structure, position) {
142
+ this.valueType = valueType;
143
+ this.position = position;
144
+ if (structure === undefined) {
145
+ throw new Error(`Node structure cannot be undefined`);
146
+ }
147
+ this._structure = structure;
148
+ this.nodeType = valueType.type;
149
+ makeObservable(this, {
150
+ _structure: observable.ref,
151
+ selfEvaluating: observable.ref,
152
+ selfFailed: observable.ref,
153
+ selfActivated: observable.ref,
154
+ evaluating: computed,
155
+ failed: computed,
156
+ fullyActivated: computed,
157
+ computationActivated: computed,
158
+ isSelfActivated: computed,
159
+ extractedValue: computed,
160
+ viewModel: computed,
161
+ }, { name: `Node-${this.valueType.type}` });
162
+ }
163
+ _structure;
164
+ get structure() {
165
+ return this._structure;
166
+ }
167
+ set structure(value) {
168
+ if (value === undefined) {
169
+ throw new Error(`Node structure cannot be undefined`);
170
+ }
171
+ runInAction(() => (this._structure = value));
172
+ }
173
+ get evaluating() {
174
+ return this.selfEvaluating || this.childrenEvaluating;
175
+ }
176
+ set evaluating(evaluating) {
177
+ this.selfEvaluating = evaluating;
178
+ }
179
+ get failed() {
180
+ return this.selfFailed || this.childrenFailed;
181
+ }
182
+ set failed(failed) {
183
+ this.selfFailed = failed;
184
+ }
185
+ get fullyActivated() {
186
+ return this.selfActivated && this.computationActivated && this.childrenActivated;
187
+ }
188
+ get computationActivated() {
189
+ return this.computationStack.allActivated;
190
+ }
191
+ get isSelfActivated() {
192
+ return this.selfActivated;
193
+ }
194
+ get viewModel() {
195
+ return this.extractedValue;
196
+ }
197
+ markActivated() {
198
+ runInAction(() => (this.selfActivated = true));
199
+ }
200
+ dispose() {
201
+ runInAction(() => {
202
+ // Dispose all computation frames in the stack
203
+ this.computationStack.dispose();
204
+ // run self-level disposers
205
+ this.disposers.forEach((disposer) => disposer());
206
+ this.disposers.length = 0; // reset array
207
+ });
208
+ // delegate to subclass for the child walk
209
+ this.disposeChildren();
210
+ }
211
+ }
212
+ class LeafNode extends AbstractNode {
213
+ constructor(valueType, structure, position) {
214
+ super(valueType, structure, position);
215
+ }
216
+ get extractedValue() {
217
+ return this.structure;
218
+ }
219
+ get childrenEvaluating() {
220
+ return false;
221
+ }
222
+ get childrenFailed() {
223
+ return false;
224
+ }
225
+ get childrenActivated() {
226
+ return true;
227
+ }
228
+ getNodeByPath(path) {
229
+ // Leaf nodes cannot be traversed further
230
+ return path.length === 0 ? this : null;
231
+ }
232
+ disposeChildren() {
233
+ return;
234
+ }
235
+ }
236
+ class CommandNode extends LeafNode {
237
+ inFlight = 0;
238
+ constructor(valueType, structure, position) {
239
+ super(valueType, structure, position);
240
+ makeObservable(this, {
241
+ inFlight: observable,
242
+ executing: computed,
243
+ });
244
+ }
245
+ get executing() {
246
+ return this.inFlight > 0;
247
+ }
248
+ get viewModel() {
249
+ return {
250
+ execute: this.#execute,
251
+ executing: this.executing,
252
+ failed: this.failed,
253
+ };
254
+ }
255
+ #execute = async (...args) => {
256
+ runInAction(() => this.inFlight++);
257
+ this.failed = false; // Reset failed state before execution
258
+ try {
259
+ return await componentTreeConfig.runCommand(this, ...args);
260
+ }
261
+ catch (error) {
262
+ this.failed = true; // Set failed state on error
263
+ throw error;
264
+ }
265
+ finally {
266
+ runInAction(() => this.inFlight--);
267
+ }
268
+ };
269
+ }
270
+ class ArrayNode extends AbstractNode {
271
+ constructor(valueType, structure, position) {
272
+ super(valueType, structure, position);
273
+ }
274
+ get extractedValue() {
275
+ const value = this.structure;
276
+ if (!value)
277
+ return null;
278
+ return value.map((item) => item.extractedValue);
279
+ }
280
+ get childrenEvaluating() {
281
+ return this.structure ? this.structure.some((n) => n.evaluating) : false;
282
+ }
283
+ get childrenFailed() {
284
+ return this.structure ? this.structure.some((n) => n.failed) : false;
285
+ }
286
+ get childrenActivated() {
287
+ return this.structure ? this.structure.every((n) => n.fullyActivated) : true;
288
+ }
289
+ getNodeByPath(path) {
290
+ if (path.length === 0) {
291
+ return this;
292
+ }
293
+ const [head, ...tail] = path;
294
+ if (!this.structure)
295
+ return null;
296
+ const found = this.structure[head];
297
+ if (!found)
298
+ return null;
299
+ return found.getNodeByPath(tail);
300
+ }
301
+ disposeChildren() {
302
+ this.structure?.forEach((child) => isNode(child) && child.dispose());
303
+ }
304
+ }
305
+ class AbstractObjectLikeNode extends AbstractNode {
306
+ constructor(valueType, structure, position) {
307
+ super(valueType, structure, position);
308
+ }
309
+ get childrenEvaluating() {
310
+ const obj = this.object();
311
+ return !!obj && Object.values(obj).some((n) => n.evaluating);
312
+ }
313
+ get childrenFailed() {
314
+ const obj = this.object();
315
+ return !!obj && Object.values(obj).some((n) => n.failed);
316
+ }
317
+ get childrenActivated() {
318
+ const obj = this.object();
319
+ return obj ? Object.values(obj).every((n) => n.fullyActivated) : true;
320
+ }
321
+ getNodeByPath(path) {
322
+ if (path.length === 0) {
323
+ return this;
324
+ }
325
+ const [head, ...tail] = path;
326
+ const obj = this.object();
327
+ if (!obj)
328
+ return null;
329
+ const found = obj[head];
330
+ if (!found)
331
+ return null;
332
+ return found.getNodeByPath(tail);
333
+ }
334
+ disposeChildren() {
335
+ const obj = this.object();
336
+ if (!obj)
337
+ return;
338
+ Object.values(obj).forEach((n) => n.dispose());
339
+ }
340
+ mapObjectExtracted() {
341
+ const obj = this.object();
342
+ if (!obj)
343
+ return null;
344
+ return Object.fromEntries(Object.entries(obj).map(([k, n]) => [k, n.extractedValue]));
345
+ }
346
+ }
347
+ class MapNode extends AbstractObjectLikeNode {
348
+ constructor(valueType, structure, position) {
349
+ super(valueType, structure, position);
350
+ }
351
+ get extractedValue() {
352
+ return this.mapObjectExtracted();
353
+ }
354
+ object() {
355
+ return this.structure;
356
+ }
357
+ }
358
+ class SetNode extends AbstractNode {
359
+ constructor(valueType, structure, position) {
360
+ super(valueType, structure, position);
361
+ }
362
+ get extractedValue() {
363
+ const value = this.structure;
364
+ if (!value)
365
+ return null;
366
+ const extractedValues = new Set();
367
+ value.forEach((item) => {
368
+ extractedValues.add(item.extractedValue);
369
+ });
370
+ return extractedValues;
371
+ }
372
+ get childrenEvaluating() {
373
+ return this.structure ? Array.from(this.structure).some((n) => n.evaluating) : false;
374
+ }
375
+ get childrenFailed() {
376
+ return this.structure ? Array.from(this.structure).some((n) => n.failed) : false;
377
+ }
378
+ get childrenActivated() {
379
+ return this.structure ? Array.from(this.structure).every((n) => n.fullyActivated) : true;
380
+ }
381
+ getNodeByPath(path) {
382
+ if (path.length === 0) {
383
+ return this;
384
+ }
385
+ // Sets don't support path traversal since they don't have indexed access
386
+ return null;
387
+ }
388
+ disposeChildren() {
389
+ this.structure?.forEach((child) => isNode(child) && child.dispose());
390
+ }
391
+ }
392
+ class ObjectNode extends AbstractObjectLikeNode {
393
+ constructor(valueType, structure, position) {
394
+ super(valueType, structure, position);
395
+ }
396
+ get extractedValue() {
397
+ return this.mapObjectExtracted();
398
+ }
399
+ object() {
400
+ return this.structure;
401
+ }
402
+ }
403
+ class ShapeNode extends AbstractObjectLikeNode {
404
+ constructor(valueType, structure, position) {
405
+ super(valueType, structure, position);
406
+ }
407
+ get extractedValue() {
408
+ return this.mapObjectExtracted();
409
+ }
410
+ object() {
411
+ return this.structure;
412
+ }
413
+ }
414
+ class VariantShapeNode extends AbstractObjectLikeNode {
415
+ constructor(valueType, structure, position) {
416
+ super(valueType, structure, position);
417
+ }
418
+ get extractedValue() {
419
+ const base = this.mapObjectExtracted();
420
+ if (!base || !this.structure)
421
+ return null;
422
+ const kindField = `${this.valueType.shapeType}Type`;
423
+ return { ...base, [kindField]: this.structure.kind };
424
+ }
425
+ object() {
426
+ return this.structure?.fields ?? null;
427
+ }
428
+ }
429
+ // TNode is removed - use AbstractNode<ValueType> directly
430
+
431
+ const modifiers = [':', '^', '&'];
432
+ function parseComponentSelector(selector) {
433
+ if (!selector.trim()) {
434
+ return [];
435
+ }
436
+ const result = [];
437
+ const parts = selector.split(/\s+/);
438
+ for (let i = 0; i < parts.length; i++) {
439
+ let part = parts[i];
440
+ if (part.includes('[')) {
441
+ while (!part.endsWith(']') && i < parts.length - 1) {
442
+ i++;
443
+ part += ' ' + parts[i];
444
+ }
445
+ }
446
+ result.push(parseSelectorSegment(part));
447
+ }
448
+ return result;
449
+ }
450
+ function parseSelectorSegment(rawSegment) {
451
+ const segment = {
452
+ qualifier: '',
453
+ modifier: null,
454
+ conditions: [],
455
+ };
456
+ // Extract :nth(N) pseudo-selector
457
+ const nthMatch = rawSegment.match(/:nth\((-?\d+)\)/);
458
+ if (nthMatch) {
459
+ segment.index = parseInt(nthMatch[1], 10);
460
+ rawSegment = rawSegment.replace(/:nth\(-?\d+\)/, '');
461
+ }
462
+ const conditionsMatch = rawSegment.match(/\[(.*?)]/g);
463
+ if (conditionsMatch) {
464
+ segment.conditions = conditionsMatch.map((item) => item.slice(1, -1).split('='));
465
+ const conditionStart = rawSegment.indexOf('[');
466
+ rawSegment = rawSegment.slice(0, conditionStart);
467
+ }
468
+ const firstChar = rawSegment[0];
469
+ segment.modifier = modifiers.includes(firstChar) ? firstChar : null;
470
+ segment.qualifier = rawSegment.replace(/^[:^&]/, '');
471
+ return segment;
472
+ }
473
+
474
+ /**
475
+ * Parse a path string into structured segments.
476
+ *
477
+ * ## Path Notation Rules
478
+ *
479
+ * **Dot notation (.)** - Access object properties
480
+ * - `user.profile.name` - Access nested object properties
481
+ * - `config.0` - Access object property named "0" (string key)
482
+ *
483
+ * **Bracket notation ([])** - Access array elements by numeric index
484
+ * - `items[0].name` - Access first array element, then its 'name' property
485
+ * - `matrix[0][1]` - Access multi-dimensional arrays
486
+ *
487
+ * **Optional chaining (?.)** - Safely access potentially missing properties
488
+ * - `user?.profile?.email` - Returns null if any segment is missing
489
+ *
490
+ * **Variable prefix ($)** - Access variables instead of properties
491
+ * - `$config.theme` - Access variable 'config', then its 'theme' property
492
+ * - `$items[0]` - Access variable 'items' array, then first element
493
+ *
494
+ * **Component selector (->)** - Access properties on other components
495
+ * - `&ref->prop.field` - Access 'prop.field' on component with ref
496
+ * - `:Button[disabled=true]->label` - Access 'label' on Button with condition
497
+ * - `^parent->config` - Access 'config' on parent component
498
+ *
499
+ * **Optional component selector (?->)** - Safely access components that might not exist
500
+ * - `&ref?->value` - Returns null if component with ref doesn't exist
501
+ * - `:OptionalType?->property` - Returns null if component type not found
502
+ * - `^Parent?->config` - Returns null if parent doesn't match
503
+ *
504
+ * ## Component Selector Syntax
505
+ *
506
+ * Component selectors support modifiers and conditions:
507
+ * - `&ref` - Select by ref
508
+ * - `:ComponentType` - Select by component type
509
+ * - `^parent` - Select parent component
510
+ * - `[prop=value]` - Filter by property value
511
+ * - Multiple conditions: `:Button[disabled=true][type=submit]`
512
+ *
513
+ * ## Notation Disambiguation
514
+ *
515
+ * The choice between dot and bracket notation determines the access type:
516
+ * ```typescript
517
+ * 'data.0' // Object property named "0" (string key)
518
+ * 'data[0]' // Array element at index 0 (numeric index)
519
+ * 'columns.1' // Object property named "1"
520
+ * 'columns[1]' // Array element at index 1
521
+ * ```
522
+ *
523
+ * @example
524
+ * parseNodeSelector('users[0].name')
525
+ * // => { kind: 'property', head: 'users', tail: [{ key: 0, optional: false }, { key: 'name', optional: false }] }
526
+ *
527
+ * @example
528
+ * parseNodeSelector('config.0')
529
+ * // => { kind: 'property', head: 'config', tail: [{ key: '0', optional: false }] }
530
+ *
531
+ * @example
532
+ * parseNodeSelector('prop?.nested?.field')
533
+ * // => { kind: 'property', head: 'prop', tail: [{ key: 'nested', optional: true }, { key: 'field', optional: true }] }
534
+ *
535
+ * @example
536
+ * parseNodeSelector('$varName.field')
537
+ * // => { kind: 'variable', head: 'varName', tail: [{ key: 'field', optional: false }] }
538
+ *
539
+ * @example
540
+ * parseNodeSelector('&ref->prop.field')
541
+ * // => {
542
+ * // componentSelector: [{ qualifier: 'ref', modifier: '&', conditions: [] }],
543
+ * // kind: 'property',
544
+ * // head: 'prop',
545
+ * // tail: [{ key: 'field', optional: false }]
546
+ * // }
547
+ *
548
+ * @example
549
+ * parseNodeSelector(':Button[disabled=true]->label.text')
550
+ * // => {
551
+ * // componentSelector: [{ qualifier: 'Button', modifier: ':', conditions: [['disabled', 'true']] }],
552
+ * // kind: 'property',
553
+ * // head: 'label',
554
+ * // tail: [{ key: 'text', optional: false }]
555
+ * // }
556
+ */
557
+ function parseNodeSelector(path) {
558
+ if (!path) {
559
+ throw new Error('Empty path provided');
560
+ }
561
+ // Handle variable prefix
562
+ if (path.startsWith(Delimiters.Variable)) {
563
+ const withoutPrefix = path.slice(1);
564
+ // Check for both . and ?. separators
565
+ const dotIndex = withoutPrefix.indexOf(Delimiters.NodePath);
566
+ const optionalIndex = withoutPrefix.indexOf('?.');
567
+ // If no separator found, it's just the variable name
568
+ if (dotIndex === -1 && optionalIndex === -1) {
569
+ return {
570
+ kind: 'variable',
571
+ head: withoutPrefix,
572
+ tail: [],
573
+ };
574
+ }
575
+ // Find the first separator (. or ?.)
576
+ let firstSeparatorIndex;
577
+ let isOptional;
578
+ if (dotIndex === -1) {
579
+ firstSeparatorIndex = optionalIndex;
580
+ isOptional = true;
581
+ }
582
+ else if (optionalIndex === -1) {
583
+ firstSeparatorIndex = dotIndex;
584
+ isOptional = false;
585
+ }
586
+ else {
587
+ firstSeparatorIndex = Math.min(dotIndex, optionalIndex);
588
+ isOptional = firstSeparatorIndex === optionalIndex;
589
+ }
590
+ const varName = withoutPrefix.substring(0, firstSeparatorIndex);
591
+ const restPath = withoutPrefix.substring(firstSeparatorIndex + (isOptional ? 2 : 1));
592
+ return {
593
+ kind: 'variable',
594
+ head: varName,
595
+ tail: parseSegments(restPath, isOptional), // Pass the optional flag
596
+ };
597
+ }
598
+ // Handle component selector arrow (both ?-> and ->)
599
+ const optionalArrowIndex = path.indexOf('?->');
600
+ const strictArrowIndex = path.indexOf('->');
601
+ // Determine which arrow to use (prefer ?-> if both are found)
602
+ let arrowIndex = -1;
603
+ let arrowLength = 2;
604
+ let isOptionalComponent = false;
605
+ if (optionalArrowIndex !== -1 && (strictArrowIndex === -1 || optionalArrowIndex < strictArrowIndex)) {
606
+ arrowIndex = optionalArrowIndex;
607
+ arrowLength = 3;
608
+ isOptionalComponent = true;
609
+ }
610
+ else if (strictArrowIndex !== -1) {
611
+ arrowIndex = strictArrowIndex;
612
+ arrowLength = 2;
613
+ isOptionalComponent = false;
614
+ }
615
+ if (arrowIndex !== -1) {
616
+ const componentSelectorStr = path.substring(0, arrowIndex);
617
+ const nodePath = path.substring(arrowIndex + arrowLength);
618
+ const parsed = parseNodeSelector(nodePath);
619
+ return {
620
+ component: {
621
+ selector: parseComponentSelector(componentSelectorStr),
622
+ optional: isOptionalComponent,
623
+ },
624
+ kind: parsed.kind,
625
+ head: parsed.head,
626
+ tail: parsed.tail,
627
+ };
628
+ }
629
+ // Regular property path
630
+ return parsePropertyPath(path);
631
+ }
632
+ /**
633
+ * Parse a property path (no variable prefix or component selector)
634
+ */
635
+ function parsePropertyPath(path) {
636
+ if (!path) {
637
+ throw new Error('Empty property path provided');
638
+ }
639
+ const allSegments = parseSegments(path);
640
+ if (allSegments.length === 0) {
641
+ throw new Error('Empty property path provided');
642
+ }
643
+ const [first, ...rest] = allSegments;
644
+ return {
645
+ kind: 'property',
646
+ head: first.key,
647
+ tail: rest,
648
+ };
649
+ }
650
+ /**
651
+ * Parse path segments, handling optional chaining and array notation
652
+ * @param path - The path to parse
653
+ * @param startOptional - If true, the first segment should be marked as optional
654
+ */
655
+ function parseSegments(path, startOptional = false) {
656
+ if (!path)
657
+ return [];
658
+ const segments = [];
659
+ let current = '';
660
+ let nextOptional = startOptional; // Start with the passed optional flag
661
+ let i = 0;
662
+ while (i < path.length) {
663
+ const char = path[i];
664
+ const nextChar = path[i + 1];
665
+ // Handle optional chaining ?.
666
+ if (char === '?' && nextChar === '.') {
667
+ if (current) {
668
+ segments.push(makeSegment(current, nextOptional, false));
669
+ current = '';
670
+ }
671
+ nextOptional = true;
672
+ i += 2;
673
+ continue;
674
+ }
675
+ // Handle array bracket notation [0]
676
+ if (char === '[') {
677
+ if (current) {
678
+ segments.push(makeSegment(current, nextOptional, false));
679
+ current = '';
680
+ nextOptional = false;
681
+ }
682
+ let index = '';
683
+ i++;
684
+ while (i < path.length && path[i] !== ']') {
685
+ index += path[i];
686
+ i++;
687
+ }
688
+ if (path[i] === ']') {
689
+ segments.push(makeSegment(index, nextOptional, true)); // fromBracket = true
690
+ i++; // Skip ]
691
+ // Skip following . if present
692
+ if (path[i] === '.')
693
+ i++;
694
+ }
695
+ continue;
696
+ }
697
+ // Handle regular . separator
698
+ if (char === '.') {
699
+ if (current) {
700
+ segments.push(makeSegment(current, nextOptional, false));
701
+ current = '';
702
+ nextOptional = false;
703
+ }
704
+ i++;
705
+ continue;
706
+ }
707
+ current += char;
708
+ i++;
709
+ }
710
+ if (current) {
711
+ segments.push(makeSegment(current, nextOptional, false));
712
+ }
713
+ return segments;
714
+ }
715
+ /**
716
+ * Create a path segment, parsing numbers only for bracket notation.
717
+ * Dot notation keeps keys as strings to allow object properties named "0", "1", etc.
718
+ *
719
+ * @param key - The segment key
720
+ * @param optional - Whether this segment uses optional chaining
721
+ * @param fromBracket - True if this came from bracket notation [0], false if from dot notation
722
+ */
723
+ function makeSegment(key, optional, fromBracket = false) {
724
+ // Only convert to number if it came from bracket notation
725
+ if (fromBracket) {
726
+ const asNumber = Number(key);
727
+ if (!Number.isNaN(asNumber)) {
728
+ return { key: asNumber, optional };
729
+ }
730
+ }
731
+ // Keep as string for dot notation or non-numeric bracket content
732
+ return { key, optional };
733
+ }
734
+
735
+ function formatSource(sourceUrl, path) {
736
+ if (!sourceUrl)
737
+ return '';
738
+ const pathStr = path?.length ? ` @ ${path.join('.')}` : '';
739
+ return `[${sourceUrl}${pathStr}]`;
740
+ }
741
+
742
+ /**
743
+ * Traverse a node using parsed path segments, respecting optional chaining.
744
+ *
745
+ * @param node - The starting node
746
+ * @param segments - Array of path segments to traverse
747
+ * @returns The found node, or null if not found and optional chaining was used
748
+ * @throws Error if a required segment is not found
749
+ *
750
+ * @example
751
+ * const userNode = component.getNode('user');
752
+ * const nameNode = traverseNode(userNode, [
753
+ * { key: 'profile', optional: false },
754
+ * { key: 'name', optional: false }
755
+ * ]);
756
+ *
757
+ * @example
758
+ * // With optional chaining - returns null instead of throwing
759
+ * const emailNode = traverseNode(userNode, [
760
+ * { key: 'contact', optional: true },
761
+ * { key: 'email', optional: false }
762
+ * ]);
763
+ */
764
+ function traverseNode(node, segments) {
765
+ if (segments.length === 0) {
766
+ return node;
767
+ }
768
+ const [head, ...tail] = segments;
769
+ const found = node.getNodeByPath([head.key]);
770
+ if (!found) {
771
+ if (head.optional) {
772
+ return null; // Optional chaining returns null
773
+ }
774
+ // Check if this is a shape/object-like node with null value
775
+ // In this case, accessing properties should return null instead of throwing
776
+ // This is common in reactive systems where shape values might not be initialized yet
777
+ if (node.nodeType === 'shape' ||
778
+ node.nodeType === 'variantShape' ||
779
+ node.nodeType === 'object' ||
780
+ node.nodeType === 'map') {
781
+ const extracted = node.extractedValue;
782
+ if (extracted === null || extracted === undefined) {
783
+ return null;
784
+ }
785
+ }
786
+ throw new Error(`Cannot access ${typeof head.key === 'number' ? 'index' : 'property'} ` +
787
+ `'${head.key}' on ${node.nodeType} node at path ${node.position.path.join('.')}`);
788
+ }
789
+ return traverseNode(found, tail);
790
+ }
791
+
792
+ class TComponent {
793
+ id;
794
+ position;
795
+ refSpaceId;
796
+ sourceUrl;
797
+ componentType;
798
+ props;
799
+ variables;
800
+ onNodeChange;
801
+ handlerDisposers = [];
802
+ constructor(config) {
803
+ this.id = config.id;
804
+ this.position = config.position;
805
+ this.refSpaceId = config.refSpaceId;
806
+ this.sourceUrl = config.sourceUrl ?? null;
807
+ this.componentType = config.componentType;
808
+ this.props = config.props;
809
+ this.variables = config.variables;
810
+ this.onNodeChange = config.onNodeChange;
811
+ makeObservable(this, {
812
+ props: observable.shallow,
813
+ variables: observable.shallow,
814
+ setVariable: action,
815
+ });
816
+ }
817
+ setVariable(variableName, node) {
818
+ this.variables.set(variableName, node);
819
+ }
820
+ /**
821
+ * Find a node by selector. Returns null if the node is not found.
822
+ * This is an alias for getNode for consistency with ComponentStore's API.
823
+ */
824
+ findNode(nodeSelector) {
825
+ const parsed = parseNodeSelector(nodeSelector);
826
+ return this.findNodeFromParsed(parsed);
827
+ }
828
+ /**
829
+ * Get a node by selector. Throws an error if the node is not found.
830
+ * Use findNode() for optional lookups that return null instead of throwing.
831
+ *
832
+ * Note: Even if the selector contains optional chaining (?.), this method will throw
833
+ * if the node is not found. Use findNode() for optional chaining support.
834
+ */
835
+ getNode(nodeSelector) {
836
+ const parsed = parseNodeSelector(nodeSelector);
837
+ return this.getNodeFromParsed(parsed);
838
+ }
839
+ findNodeFromParsed(parsed) {
840
+ // Handle variable access
841
+ if (parsed.kind === 'variable') {
842
+ const varNode = this.variables.get(parsed.head);
843
+ if (!varNode) {
844
+ return null;
845
+ }
846
+ return traverseNode(varNode, parsed.tail);
847
+ }
848
+ // Handle property access
849
+ const propNode = this.props.get(parsed.head);
850
+ if (!propNode) {
851
+ return null;
852
+ }
853
+ return traverseNode(propNode, parsed.tail);
854
+ }
855
+ getNodeFromParsed(parsed) {
856
+ const src = formatSource(this.sourceUrl);
857
+ // Handle variable access
858
+ if (parsed.kind === 'variable') {
859
+ const varNode = this.variables.get(parsed.head);
860
+ if (!varNode) {
861
+ throw new Error(`${src} Variable '${parsed.head}' does not exist in component '${this.id}' (type: ${this.componentType})`);
862
+ }
863
+ const result = traverseNode(varNode, parsed.tail);
864
+ if (!result) {
865
+ throw new Error(`${src} Node not found for variable path '${parsed.head}${parsed.tail.map((s) => `.${s.key}`).join('')}' in component '${this.id}'`);
866
+ }
867
+ return result;
868
+ }
869
+ // Handle property access - strict mode, always throw if not found
870
+ const propNode = this.props.get(parsed.head);
871
+ if (!propNode) {
872
+ throw new Error(`${src} Property '${parsed.head}' does not exist on component '${this.id}' (type: ${this.componentType}). ` +
873
+ `Available properties: [${Array.from(this.props.keys()).join(', ')}]`);
874
+ }
875
+ const result = traverseNode(propNode, parsed.tail);
876
+ if (!result) {
877
+ throw new Error(`${src} Node not found for property path '${parsed.head}${parsed.tail.map((s) => `.${s.key}`).join('')}' in component '${this.id}'`);
878
+ }
879
+ return result;
880
+ }
881
+ getNodeValue(nodeSelector) {
882
+ // Use findNode to support optional chaining and return null if not found
883
+ const node = this.findNode(nodeSelector);
884
+ return node ? node.extractedValue : null;
885
+ }
886
+ dispose() {
887
+ this.handlerDisposers.forEach((disposer) => disposer());
888
+ this.handlerDisposers = [];
889
+ for (const [, prop] of this.props.entries()) {
890
+ prop.dispose();
891
+ }
892
+ for (const [, variable] of this.variables.entries()) {
893
+ variable.dispose();
894
+ }
895
+ }
896
+ }
897
+
898
+ class RefSpace {
899
+ id;
900
+ parentRefSpaceId;
901
+ byRef;
902
+ constructor(id, parentRefSpaceId = null) {
903
+ this.id = id;
904
+ this.parentRefSpaceId = parentRefSpaceId;
905
+ this.byRef = observable.map({}, { name: 'by-ref-map', deep: false });
906
+ }
907
+ get refs() {
908
+ return this.byRef;
909
+ }
910
+ addRefs(refs) {
911
+ runInAction(() => {
912
+ for (const ref in refs) {
913
+ this.byRef.set(ref, refs[ref]);
914
+ }
915
+ });
916
+ }
917
+ getComponentIdByRef(ref) {
918
+ return this.byRef.get(ref) || null;
919
+ }
920
+ }
921
+
922
+ function traversArray(ctx, nodeQueue) {
923
+ if (!ctx.selectorSegments.length) {
924
+ return;
925
+ }
926
+ const arrayValue = ctx.currentNode.value || [];
927
+ const description = ctx.currentNode.valueType;
928
+ for (const item of arrayValue) {
929
+ const childCtx = {
930
+ ...ctx,
931
+ currentNode: {
932
+ valueType: description.item,
933
+ value: item,
934
+ },
935
+ };
936
+ nodeQueue.push(childCtx);
937
+ }
938
+ }
939
+
940
+ function traversComponent(ctx, nodeQueue, foundComponents) {
941
+ const componentId = ctx.currentNode.value;
942
+ if (componentId === null)
943
+ return;
944
+ const segmentQueue = [...ctx.selectorSegments];
945
+ const store = ComponentStore.getInstance();
946
+ const currentComponent = store.getComponentOrThrow(componentId);
947
+ while (segmentQueue.length > 0) {
948
+ const selectorSegment = segmentQueue.shift();
949
+ if (!selectorSegment)
950
+ break;
951
+ const isLastSelectorSegment = segmentQueue.length === 0;
952
+ if (selectorSegment.modifier === '&') {
953
+ const refSpace = store.getRefSpaceOrThrow(currentComponent.refSpaceId);
954
+ const foundComponentId = refSpace.getComponentIdByRef(selectorSegment.qualifier);
955
+ if (!foundComponentId) {
956
+ if (!refSpace.parentRefSpaceId) {
957
+ return;
958
+ }
959
+ const parentRefSpace = store.getRefSpaceOrThrow(refSpace.parentRefSpaceId);
960
+ const foundInParent = parentRefSpace.getComponentIdByRef(selectorSegment.qualifier);
961
+ if (!foundInParent) {
962
+ return;
963
+ }
964
+ if (isLastSelectorSegment) {
965
+ foundComponents.push(foundInParent);
966
+ return;
967
+ }
968
+ addToQueue(currentComponent.id, {
969
+ valueType: { type: 'component' },
970
+ value: foundInParent,
971
+ }, segmentQueue, nodeQueue);
972
+ return;
973
+ }
974
+ if (isLastSelectorSegment) {
975
+ foundComponents.push(foundComponentId);
976
+ return;
977
+ }
978
+ continue;
979
+ }
980
+ if (selectorSegment.modifier === '^') {
981
+ const matched = testComponentType(selectorSegment.qualifier, currentComponent.componentType) &&
982
+ testConditions(selectorSegment.conditions, currentComponent);
983
+ if (!matched) {
984
+ addToQueue(currentComponent.id, {
985
+ valueType: { type: 'component' },
986
+ value: currentComponent.position.componentId,
987
+ }, [selectorSegment, ...segmentQueue], nodeQueue);
988
+ return;
989
+ }
990
+ if (isLastSelectorSegment) {
991
+ foundComponents.push(componentId);
992
+ }
993
+ continue;
994
+ }
995
+ if (selectorSegment.modifier === ':') {
996
+ if (!DefinitionStore.getInstance().hasComponentWithType(selectorSegment.qualifier)) {
997
+ log.trace(`Component with type "${selectorSegment.qualifier}" does not exist.`);
998
+ return;
999
+ }
1000
+ if (currentComponent.id === ctx.relativeTo) {
1001
+ addComponentNodesToQueue(currentComponent, [selectorSegment, ...segmentQueue], nodeQueue);
1002
+ return;
1003
+ }
1004
+ const matched = testComponentType(selectorSegment.qualifier, currentComponent.componentType) &&
1005
+ testConditions(selectorSegment.conditions, currentComponent);
1006
+ if (!matched) {
1007
+ addComponentNodesToQueue(currentComponent, [selectorSegment, ...segmentQueue], nodeQueue);
1008
+ return;
1009
+ }
1010
+ if (isLastSelectorSegment) {
1011
+ foundComponents.push(componentId);
1012
+ return;
1013
+ }
1014
+ addToQueue(currentComponent.id, {
1015
+ valueType: { type: 'component' },
1016
+ value: currentComponent.id,
1017
+ }, segmentQueue, nodeQueue);
1018
+ return;
1019
+ }
1020
+ log.trace('Potentially invalid selector', ctx.selectorSegments);
1021
+ }
1022
+ }
1023
+ function testComponentType(qualifier, componentType) {
1024
+ return DefinitionStore.getInstance().getComponentTraits(componentType).includes(qualifier);
1025
+ }
1026
+ function testConditions(conditions, component) {
1027
+ return conditions.every((condition) => {
1028
+ const [key, expectedValue] = condition;
1029
+ if (!component.props.has(key))
1030
+ return false;
1031
+ const actualValue = String(extractNodeValue(component.props.get(key)));
1032
+ return actualValue === expectedValue;
1033
+ });
1034
+ }
1035
+ function addComponentNodesToQueue(component, selectorSegments, nodeQueue) {
1036
+ for (const node of component.props.values()) {
1037
+ addToQueue(component.id, {
1038
+ valueType: node.valueType,
1039
+ value: extractNodeValue(node),
1040
+ }, selectorSegments, nodeQueue);
1041
+ }
1042
+ }
1043
+ function addToQueue(componentId, node, selectorSegments, nodeQueue) {
1044
+ const childCtx = {
1045
+ selectorSegments,
1046
+ currentNode: node,
1047
+ relativeTo: componentId,
1048
+ };
1049
+ nodeQueue.push(childCtx);
1050
+ }
1051
+ function extractNodeValue(node) {
1052
+ if (!node)
1053
+ return null;
1054
+ return node.extractedValue;
1055
+ }
1056
+
1057
+ function traversObject(ctx, nodeQueue) {
1058
+ if (!ctx.selectorSegments.length) {
1059
+ return;
1060
+ }
1061
+ const description = ctx.currentNode.valueType;
1062
+ const objectNodeValue = ctx.currentNode.value;
1063
+ if (!objectNodeValue) {
1064
+ return;
1065
+ }
1066
+ Object.entries(description.fields).forEach(([fieldKey, fieldDescription]) => {
1067
+ const childCtx = {
1068
+ ...ctx,
1069
+ currentNode: {
1070
+ valueType: fieldDescription,
1071
+ value: objectNodeValue[fieldKey],
1072
+ },
1073
+ };
1074
+ nodeQueue.push(childCtx);
1075
+ });
1076
+ return;
1077
+ }
1078
+
1079
+ const traversesRegistry = {
1080
+ _registry: {
1081
+ array: traversArray,
1082
+ object: traversObject,
1083
+ component: traversComponent,
1084
+ map: () => [], // TODO: implement
1085
+ // the following node types cannot contain components
1086
+ set: () => [],
1087
+ unknown: () => [],
1088
+ boolean: () => [],
1089
+ string: () => [],
1090
+ number: () => [],
1091
+ componentSchema: () => [],
1092
+ validator: () => [],
1093
+ command: () => [],
1094
+ shape: () => [],
1095
+ variantShape: () => [],
1096
+ },
1097
+ get(type) {
1098
+ if (!this._registry[type]) {
1099
+ throw new Error(`Travers for type "${type}" not found`);
1100
+ }
1101
+ return this._registry[type];
1102
+ },
1103
+ };
1104
+
1105
+ function findComponent(relativeTo, selector) {
1106
+ const ids = findComponentsIds(relativeTo, selector);
1107
+ if (!ids.length) {
1108
+ return null;
1109
+ }
1110
+ if (ids.length > 1) {
1111
+ throw new Error(`Selector matched ${ids.length} components (expected 1): ${ids.join(', ')}`);
1112
+ }
1113
+ return ComponentStore.getInstance().getComponentOrThrow(ids[0]);
1114
+ }
1115
+ function findComponents(relativeTo, selector) {
1116
+ const ids = findComponentsIds(relativeTo, selector);
1117
+ return ids.map((componentId) => ComponentStore.getInstance().getComponentOrThrow(componentId));
1118
+ }
1119
+ function findComponentsIds(relativeTo, selector) {
1120
+ const selectors = [selector];
1121
+ const foundComponents = [];
1122
+ for (const parsedSelector of selectors) {
1123
+ if (parsedSelector.length === 0) {
1124
+ foundComponents.push(relativeTo);
1125
+ continue;
1126
+ }
1127
+ const startFrom = {
1128
+ currentNode: {
1129
+ valueType: { type: 'component' },
1130
+ value: relativeTo,
1131
+ },
1132
+ selectorSegments: parsedSelector,
1133
+ relativeTo,
1134
+ };
1135
+ const nodeQueue = [startFrom];
1136
+ while (nodeQueue.length > 0) {
1137
+ const ctx = nodeQueue.shift();
1138
+ if (!ctx)
1139
+ break;
1140
+ const traverser = traversesRegistry.get(ctx.currentNode.valueType.type);
1141
+ traverser(ctx, nodeQueue, foundComponents);
1142
+ }
1143
+ }
1144
+ const uniqueComponents = Array.from(new Set(foundComponents));
1145
+ // Apply :nth(N) filtering if the last segment has an index
1146
+ const lastSegment = selector[selector.length - 1];
1147
+ if (lastSegment && lastSegment.index !== undefined) {
1148
+ const index = lastSegment.index;
1149
+ const actualIndex = index < 0 ? uniqueComponents.length + index : index;
1150
+ if (actualIndex < 0 || actualIndex >= uniqueComponents.length) {
1151
+ const selectorStr = selector.map(formatSelectorSegment).join(' ');
1152
+ throw new Error(`Index ${index} out of bounds for selector "${selectorStr}" (found ${uniqueComponents.length} elements)`);
1153
+ }
1154
+ return [uniqueComponents[actualIndex]];
1155
+ }
1156
+ return uniqueComponents;
1157
+ }
1158
+ function formatSelectorSegment(segment) {
1159
+ let result = segment.modifier ? segment.modifier + segment.qualifier : segment.qualifier;
1160
+ if (segment.conditions.length > 0) {
1161
+ result += segment.conditions.map(([key, value]) => `[${key}=${value}]`).join('');
1162
+ }
1163
+ if (segment.index !== undefined) {
1164
+ result += `:nth(${segment.index})`;
1165
+ }
1166
+ return result;
1167
+ }
1168
+
1169
+ function withPath(opts, newPath) {
1170
+ return {
1171
+ ...opts,
1172
+ position: { componentId: opts.position.componentId, path: newPath },
1173
+ };
1174
+ }
1175
+ function withPosition(opts, componentId, path) {
1176
+ return {
1177
+ ...opts,
1178
+ position: { componentId, path },
1179
+ };
1180
+ }
1181
+ function appendPath(opts, ...segments) {
1182
+ return withPath(opts, [...opts.position.path, ...segments]);
1183
+ }
1184
+
1185
+ function createArrayNode(schema, opts) {
1186
+ // TODO: set empty array on unfolding ?
1187
+ const value = (schema.value || []).map((item, index) => createNode(item, appendPath(opts, index)));
1188
+ return new ArrayNode(schema.valueType, value, opts.position);
1189
+ }
1190
+
1191
+ function createCommandNode(schema, opts) {
1192
+ if (!opts.position.componentId) {
1193
+ throw new Error(`componentId is required for command.`);
1194
+ }
1195
+ if (!isObject(schema.value)) {
1196
+ return new CommandNode(schema.valueType, null, opts.position);
1197
+ }
1198
+ const structure = { ...schema.value }; // Clone the value to avoid mutation
1199
+ if (!structure.owner) {
1200
+ structure.owner = opts.position.componentId;
1201
+ }
1202
+ return new CommandNode(schema.valueType, structure, opts.position);
1203
+ }
1204
+
1205
+ const componentIdGenerator = {
1206
+ increment: 0,
1207
+ generateId() {
1208
+ return ['csid', ++this.increment].filter((item) => !!item).join('-');
1209
+ },
1210
+ reset() {
1211
+ this.increment = 0;
1212
+ },
1213
+ };
1214
+
1215
+ function createComponentNode(schema, opts) {
1216
+ const componentSchema = schema.value;
1217
+ if (!componentSchema || typeof componentSchema === 'string') {
1218
+ return new LeafNode(schema.valueType, componentSchema, opts.position);
1219
+ }
1220
+ if (componentSchema.ref && opts.refs[opts.refSpaceId]?.[componentSchema.ref]) {
1221
+ throw new Error(`Duplicated ref "${componentSchema.ref}".`);
1222
+ }
1223
+ const id = componentIdGenerator.generateId();
1224
+ const isFacade = componentSchema.componentType === 'sys.Facade';
1225
+ const isRefSpace = componentSchema.componentType === 'sys.RefSpace';
1226
+ const needsContentSpace = isFacade || isRefSpace;
1227
+ const contentSpaceId = isFacade ? `${id}-template` : isRefSpace ? `${id}-instance` : opts.refSpaceId;
1228
+ const props = new Map();
1229
+ for (const [key, propSchema] of componentSchema.props.entries()) {
1230
+ // 'content' prop uses isolated RefSpace
1231
+ const propOpts = needsContentSpace && key === 'content' ? { ...opts, refSpaceId: contentSpaceId } : opts;
1232
+ props.set(key, createNode(propSchema, withPosition(propOpts, id, [key])));
1233
+ }
1234
+ const variables = new Map();
1235
+ for (const [key, variableSchema] of componentSchema.variables.entries()) {
1236
+ variables.set(key, createNode(variableSchema, withPosition(opts, id, [key])));
1237
+ }
1238
+ const onNodeChange = componentSchema.onNodeChange.map((handler) => ({
1239
+ ...handler,
1240
+ command: createNode(handler.command, withPosition(opts, id, [handler.selector])),
1241
+ }));
1242
+ opts.components[id] = new TComponent({
1243
+ id,
1244
+ componentType: componentSchema.componentType,
1245
+ props,
1246
+ variables,
1247
+ onNodeChange,
1248
+ position: opts.position,
1249
+ refSpaceId: isFacade ? opts.refSpaceId : contentSpaceId,
1250
+ sourceUrl: opts.sourceUrl,
1251
+ });
1252
+ if (componentSchema.ref) {
1253
+ opts.refs[opts.refSpaceId] ??= {};
1254
+ opts.refs[opts.refSpaceId][componentSchema.ref] = id;
1255
+ }
1256
+ if (needsContentSpace) {
1257
+ const refSpace = new RefSpace(contentSpaceId, opts.refSpaceId);
1258
+ ComponentStore.getInstance().addRefSpace(refSpace);
1259
+ if (opts.refs[contentSpaceId]) {
1260
+ refSpace.addRefs(opts.refs[contentSpaceId]);
1261
+ }
1262
+ }
1263
+ return new LeafNode({ type: 'component' }, id, opts.position);
1264
+ }
1265
+
1266
+ function createLeafNode(schema, opts) {
1267
+ return new LeafNode(schema.valueType, schema.value, opts.position);
1268
+ }
1269
+
1270
+ function createMapNode(schema, opts) {
1271
+ if (!schema.value) {
1272
+ return new MapNode(schema.valueType, null, opts.position);
1273
+ }
1274
+ const children = {};
1275
+ for (const key in schema.value) {
1276
+ children[key] = createNode(schema.value[key], appendPath(opts, key));
1277
+ }
1278
+ return new MapNode(schema.valueType, children, opts.position);
1279
+ }
1280
+
1281
+ function createObjectNode(schema, opts) {
1282
+ if (!schema.value) {
1283
+ return new ObjectNode(schema.valueType, null, opts.position);
1284
+ }
1285
+ if (!Object.keys(schema.valueType).length) {
1286
+ return new ObjectNode(schema.valueType, null, opts.position);
1287
+ }
1288
+ const fields = {};
1289
+ for (const key in schema.value) {
1290
+ fields[key] = createNode(schema.value[key], appendPath(opts, key));
1291
+ }
1292
+ return new ObjectNode(schema.valueType, fields, opts.position);
1293
+ }
1294
+
1295
+ function createSetNode(schema, opts) {
1296
+ if (!schema.value) {
1297
+ return new SetNode(schema.valueType, null, opts.position);
1298
+ }
1299
+ // Convert array to Set of nodes
1300
+ const items = schema.value.map((item, index) => createNode(item, appendPath(opts, index)));
1301
+ const value = new Set(items);
1302
+ return new SetNode(schema.valueType, value, opts.position);
1303
+ }
1304
+
1305
+ function createShapeNode(schema, opts) {
1306
+ if (!isObject(schema.value)) {
1307
+ return new ShapeNode(schema.valueType, null, opts.position);
1308
+ }
1309
+ const fields = {};
1310
+ for (const key in schema.value) {
1311
+ fields[key] = createNode(schema.value[key], appendPath(opts, key));
1312
+ }
1313
+ return new ShapeNode(schema.valueType, fields, opts.position);
1314
+ }
1315
+
1316
+ function createVariantShapeNode(schema, opts) {
1317
+ if (!schema.value) {
1318
+ return new VariantShapeNode(schema.valueType, null, opts.position);
1319
+ }
1320
+ const value = splitValue(schema.value, schema.valueType);
1321
+ const fields = {};
1322
+ for (const key in value.fields) {
1323
+ fields[key] = createNode(value.fields[key], appendPath(opts, key));
1324
+ }
1325
+ return new VariantShapeNode(schema.valueType, { kind: value.kind, fields }, opts.position);
1326
+ }
1327
+ function splitValue(value, valueType) {
1328
+ const kindKey = `${valueType.shapeType}Type`;
1329
+ const { [kindKey]: kind, ...fields } = value;
1330
+ if (!kind) {
1331
+ throw new Error(`Missing kind field ${kindKey}`);
1332
+ }
1333
+ return { kind, fields };
1334
+ }
1335
+
1336
+ function createNode(unfolded, opts) {
1337
+ if (unfolded instanceof AbstractNode) {
1338
+ return unfolded;
1339
+ }
1340
+ const node = _create(unfolded, opts);
1341
+ if (unfolded.computation) {
1342
+ node.computationStack.push(unfolded.computation);
1343
+ }
1344
+ node.bindingSchema = unfolded.binding;
1345
+ node.extraBinding = unfolded.extraBinding;
1346
+ return node;
1347
+ }
1348
+ const nodeCreators = {
1349
+ component: (node, opts) => createComponentNode(node, opts),
1350
+ array: (node, opts) => createArrayNode(node, opts),
1351
+ map: (node, opts) => createMapNode(node, opts),
1352
+ set: (node, opts) => createSetNode(node, opts),
1353
+ object: (node, opts) => createObjectNode(node, opts),
1354
+ command: (node, opts) => createCommandNode(node, opts),
1355
+ shape: (node, opts) => createShapeNode(node, opts),
1356
+ variantShape: (node, opts) => createVariantShapeNode(node, opts),
1357
+ // Leaf types
1358
+ string: (node, opts) => createLeafNode(node, opts),
1359
+ number: (node, opts) => createLeafNode(node, opts),
1360
+ boolean: (node, opts) => createLeafNode(node, opts),
1361
+ unknown: (node, opts) => createLeafNode(node, opts),
1362
+ componentSchema: (node, opts) => createLeafNode(node, opts),
1363
+ };
1364
+ function _create(node, opts) {
1365
+ const creator = nodeCreators[node.valueType.type];
1366
+ if (creator) {
1367
+ return creator(node, opts);
1368
+ }
1369
+ // Fallback for any unknown types
1370
+ return createLeafNode(node, opts);
1371
+ }
1372
+
1373
+ const componentIdExtractors = {
1374
+ array: (value, ids) => {
1375
+ if (!Array.isArray(value))
1376
+ return; // the check is needed for booleanInputGroup value proxies (TODO: verify)
1377
+ for (const node of value) {
1378
+ extractComponentsIds(node, ids);
1379
+ }
1380
+ },
1381
+ object: (value, ids) => {
1382
+ if (!value)
1383
+ return;
1384
+ for (const node of Object.values(value)) {
1385
+ extractComponentsIds(node, ids);
1386
+ }
1387
+ },
1388
+ map: (value, ids) => {
1389
+ if (!value)
1390
+ return;
1391
+ for (const node of Object.values(value)) {
1392
+ extractComponentsIds(node, ids);
1393
+ }
1394
+ },
1395
+ component: (value, ids) => {
1396
+ if (!value)
1397
+ return;
1398
+ const componentId = value;
1399
+ ids.push(componentId);
1400
+ const component = ComponentStore.getInstance().getComponent(componentId);
1401
+ if (component) {
1402
+ for (const [, propNode] of component.props) {
1403
+ extractComponentsIds(propNode, ids);
1404
+ }
1405
+ }
1406
+ },
1407
+ };
1408
+ function extractComponentsIds(node, ids = []) {
1409
+ const value = node.structure;
1410
+ const extractor = componentIdExtractors[node.valueType.type];
1411
+ if (extractor) {
1412
+ extractor(value, ids);
1413
+ }
1414
+ return ids;
1415
+ }
1416
+
1417
+ function getComponent(node) {
1418
+ if (!node.position.componentId) {
1419
+ throw new Error('Node does not have a component context');
1420
+ }
1421
+ return ComponentStore.getInstance().getComponentOrThrow(node.position.componentId);
1422
+ }
1423
+
1424
+ configure({
1425
+ enforceActions: 'always',
1426
+ });
1427
+ class ComponentStore {
1428
+ static instance;
1429
+ components = new Map();
1430
+ refSpaces = observable.map({}, { deep: false, name: 'ref-spaces-map' });
1431
+ componentsToPreload = new Set();
1432
+ // Index for O(m) RefSpace cleanup during component removal
1433
+ componentsByRefSpace = new Map();
1434
+ static getInstance() {
1435
+ if (!ComponentStore.instance) {
1436
+ ComponentStore.instance = new ComponentStore();
1437
+ }
1438
+ return ComponentStore.instance;
1439
+ }
1440
+ // tests purposes only
1441
+ static reset() {
1442
+ const store = ComponentStore.getInstance();
1443
+ runInAction(() => {
1444
+ // Remove all root components (components without parents)
1445
+ // This will cascade and remove all child components and RefSpaces
1446
+ const rootComponents = Array.from(store.components.values()).filter((c) => !c.position.componentId);
1447
+ for (const component of rootComponents) {
1448
+ store.removeComponent(component.id);
1449
+ }
1450
+ // Clear any remaining orphaned data (shouldn't be any, but just to be safe)
1451
+ store.components.clear();
1452
+ store.refSpaces.clear();
1453
+ store.componentsToPreload.clear();
1454
+ store.componentsByRefSpace.clear();
1455
+ componentIdGenerator.reset();
1456
+ });
1457
+ return store;
1458
+ }
1459
+ addRefSpace(refSpace) {
1460
+ runInAction(() => {
1461
+ this.refSpaces.set(refSpace.id, refSpace);
1462
+ });
1463
+ }
1464
+ addComponents(components) {
1465
+ runInAction(() => {
1466
+ for (const id in components) {
1467
+ const component = components[id];
1468
+ this.components.set(id, component);
1469
+ // Maintain componentsByRefSpace index
1470
+ if (!this.componentsByRefSpace.has(component.refSpaceId)) {
1471
+ this.componentsByRefSpace.set(component.refSpaceId, new Set());
1472
+ }
1473
+ this.componentsByRefSpace.get(component.refSpaceId)?.add(id);
1474
+ }
1475
+ });
1476
+ }
1477
+ getRefSpace(refSpaceId) {
1478
+ return this.refSpaces.get(refSpaceId) ?? null;
1479
+ }
1480
+ getRefSpaceOrThrow(refSpaceId) {
1481
+ const refSpace = this.getRefSpace(refSpaceId);
1482
+ if (!refSpace) {
1483
+ throw new Error(`RefSpace with id "${refSpaceId}" is not found.`);
1484
+ }
1485
+ return refSpace;
1486
+ }
1487
+ getComponent(componentId) {
1488
+ return this.components.get(componentId) || null;
1489
+ }
1490
+ getComponentOrThrow(componentId) {
1491
+ const component = this.getComponent(componentId);
1492
+ if (!component) {
1493
+ throw new Error(`Component with id "${componentId}" is not found.`);
1494
+ }
1495
+ return component;
1496
+ }
1497
+ findVariable(componentId, variableName) {
1498
+ const component = this.getComponentOrThrow(componentId);
1499
+ const node = component.variables.get(variableName);
1500
+ if (node) {
1501
+ return node;
1502
+ }
1503
+ const parentComponentId = component.position.componentId;
1504
+ if (!parentComponentId) {
1505
+ return null;
1506
+ }
1507
+ return this.findVariable(parentComponentId, variableName);
1508
+ }
1509
+ /**
1510
+ * Find a single component using a selector relative to another component.
1511
+ * Throws if multiple components match the selector.
1512
+ *
1513
+ * @param relativeTo - The component ID to search relative to
1514
+ * @param selector - Component selector string (e.g., 'Button', '&myRef', '^parent', ':button', 'Button[disabled=true]')
1515
+ * @returns The matching component or null if not found
1516
+ * @throws If the selector matches multiple components
1517
+ *
1518
+ * @example
1519
+ * ```typescript
1520
+ * const button = store.findComponent(componentId, 'Button');
1521
+ * const aliased = store.findComponent(componentId, '&myRef');
1522
+ * const parent = store.findComponent(componentId, '^parent');
1523
+ * const byType = store.findComponent(componentId, ':button');
1524
+ * const filtered = store.findComponent(componentId, 'Button[disabled=true]');
1525
+ * ```
1526
+ */
1527
+ findComponent(relativeTo, selector) {
1528
+ const parsed = parseComponentSelector(selector);
1529
+ return findComponent(relativeTo, parsed);
1530
+ }
1531
+ /**
1532
+ * Find all components matching a selector relative to another component.
1533
+ *
1534
+ * @param relativeTo - The component ID to search relative to
1535
+ * @param selector - Component selector string (e.g., 'Button', '&myRef', ':button', 'Button[disabled=true]')
1536
+ * @returns Array of matching components (empty if none found)
1537
+ *
1538
+ * @example
1539
+ * ```typescript
1540
+ * const buttons = store.findComponents(componentId, 'Button');
1541
+ * const allOfType = store.findComponents(componentId, ':button');
1542
+ * const filtered = store.findComponents(componentId, 'Button[disabled=true]');
1543
+ * ```
1544
+ */
1545
+ findComponents(relativeTo, selector) {
1546
+ const parsed = parseComponentSelector(selector);
1547
+ return findComponents(relativeTo, parsed);
1548
+ }
1549
+ /**
1550
+ * Get a node by selector relative to a component.
1551
+ * Throws an error if the component or node is not found.
1552
+ * Use findNode() for optional lookups that return null instead of throwing.
1553
+ *
1554
+ * Note: Even if the selector contains optional chaining (?.), this method will throw
1555
+ * if the node is not found. Use findNode() for optional chaining support.
1556
+ *
1557
+ * Component selector behavior:
1558
+ * - `&ref->value` - Throws if component with ref doesn't exist
1559
+ * - `&ref?->value` - Throws if node not found, but allows missing component (returns null for the whole expression)
1560
+ *
1561
+ * @param relativeTo - The component ID to search relative to
1562
+ * @param selector - Node selector string (e.g., 'prop', '$variable', '^Parent->prop', '&ref?->value')
1563
+ * @returns The node
1564
+ * @throws If the component or node is not found (unless using ?-> for component)
1565
+ *
1566
+ * @example
1567
+ * ```typescript
1568
+ * const node = store.getNode(componentId, 'requiredProp'); // throws if not found
1569
+ * const optionalNode = store.findNode(componentId, 'user?.profile'); // returns null if not found
1570
+ * const optionalComponent = store.getNode(componentId, '&optional?->value'); // throws only if value missing, not if component missing
1571
+ * ```
1572
+ */
1573
+ getNode(relativeTo, selector) {
1574
+ const parsed = parseNodeSelector(selector);
1575
+ // Look up the component
1576
+ let component;
1577
+ if (parsed.component) {
1578
+ component = findComponent(relativeTo, parsed.component.selector);
1579
+ // If component not found and not using optional selector, throw error
1580
+ if (!component && !parsed.component.optional) {
1581
+ const src = formatSource(this.getComponent(relativeTo)?.sourceUrl);
1582
+ throw new Error(`${src} Component not found for selector '${selector}' relative to '${relativeTo}'`);
1583
+ }
1584
+ }
1585
+ else {
1586
+ component = this.getComponentOrThrow(relativeTo);
1587
+ }
1588
+ // If component is null (only possible with component.optional), throw
1589
+ if (!component) {
1590
+ const src = formatSource(this.getComponent(relativeTo)?.sourceUrl);
1591
+ throw new Error(`${src} Component not found for selector '${selector}' relative to '${relativeTo}'`);
1592
+ }
1593
+ const src = formatSource(component.sourceUrl);
1594
+ // If it's a variable path, use RefSpace chain lookup
1595
+ if (parsed.kind === 'variable') {
1596
+ const node = this.findVariable(component.id, parsed.head);
1597
+ if (!node) {
1598
+ throw new Error(`${src} Variable '${parsed.head}' does not exist in component '${component.id}' or its parent RefSpaces`);
1599
+ }
1600
+ const result = traverseNode(node, parsed.tail);
1601
+ if (!result) {
1602
+ throw new Error(`${src} Node not found for selector '${selector}' in component '${component.id}'`);
1603
+ }
1604
+ return result;
1605
+ }
1606
+ // For property paths, use strict lookup that throws on missing nodes
1607
+ const node = component.getNodeFromParsed(parsed);
1608
+ if (!node) {
1609
+ throw new Error(`${src} Node not found for selector '${selector}' in component '${component.id}'`);
1610
+ }
1611
+ return node;
1612
+ }
1613
+ /**
1614
+ * Find a node by selector relative to a component.
1615
+ * Returns null if the node is not found.
1616
+ *
1617
+ * Component selector behavior:
1618
+ * - `&ref->value` - Throws if component doesn't exist (strict by default)
1619
+ * - `&ref?->value` - Returns null if component doesn't exist (lenient when explicit)
1620
+ *
1621
+ * Use `?->` to explicitly indicate that a missing component is acceptable.
1622
+ * This makes the code self-documenting and prevents accidental null returns.
1623
+ *
1624
+ * @param relativeTo - The component ID to search relative to
1625
+ * @param selector - Node selector string (e.g., 'prop', '$variable', '^Parent->prop', '&ref?->value')
1626
+ * @returns The node or null if not found
1627
+ *
1628
+ * @example
1629
+ * ```typescript
1630
+ * // Strict - throws if component missing
1631
+ * const node = store.findNode(componentId, '&ref->value');
1632
+ *
1633
+ * // Lenient - returns null if component missing
1634
+ * const optionalNode = store.findNode(componentId, '&ref?->value');
1635
+ * if (optionalNode) {
1636
+ * // Use the node
1637
+ * }
1638
+ * ```
1639
+ */
1640
+ findNode(relativeTo, selector) {
1641
+ const parsed = parseNodeSelector(selector);
1642
+ // If a component selector is specified, look up the component
1643
+ let component = parsed.component
1644
+ ? findComponent(relativeTo, parsed.component.selector)
1645
+ : this.getComponent(relativeTo);
1646
+ // If component not found and not using optional selector, throw error
1647
+ if (!component && parsed.component && !parsed.component.optional) {
1648
+ const src = formatSource(this.getComponent(relativeTo)?.sourceUrl);
1649
+ throw new Error(`${src} Component not found for selector '${selector}' relative to '${relativeTo}'`);
1650
+ }
1651
+ if (!component) {
1652
+ return null;
1653
+ }
1654
+ // If looking up a variable in a sys.Facade component (without explicit component selector),
1655
+ // start search from the facade's parent. This ensures variables are resolved from the facade's
1656
+ // context, not from within the facade itself. This only applies when using the implicit
1657
+ // relativeTo component, not when explicitly selecting a Facade (e.g., ':sys.Facade->$var')
1658
+ if (parsed.kind === 'variable' && !parsed.component && component.componentType === 'sys.Facade') {
1659
+ const parentComponentId = component.position.componentId;
1660
+ if (parentComponentId) {
1661
+ const parentComponent = this.getComponent(parentComponentId);
1662
+ // If parent exists, use it for variable lookup; otherwise fall back to the Facade itself
1663
+ if (parentComponent) {
1664
+ component = parentComponent;
1665
+ }
1666
+ }
1667
+ }
1668
+ const src = formatSource(component.sourceUrl);
1669
+ // If it's a variable path, use RefSpace chain lookup
1670
+ if (parsed.kind === 'variable') {
1671
+ const node = this.findVariable(component.id, parsed.head);
1672
+ if (!node) {
1673
+ // Variable itself doesn't exist - this is likely a typo, so throw
1674
+ throw new Error(`${src} Variable "${parsed.head}" not found in component "${component.id}"`);
1675
+ }
1676
+ // Variable exists, traverse the path (may return null if path is missing)
1677
+ return traverseNode(node, parsed.tail);
1678
+ }
1679
+ // For property paths, throw if property doesn't exist but allow null paths
1680
+ const node = component.props.get(parsed.head);
1681
+ if (!node) {
1682
+ throw new Error(`${src} Property "${parsed.head}" not found in component "${component.id}" (type: ${component.componentType}). ` +
1683
+ `Available properties: [${Array.from(component.props.keys()).join(', ')}]`);
1684
+ }
1685
+ // Property exists, traverse the path (may return null if path is missing)
1686
+ return traverseNode(node, parsed.tail);
1687
+ }
1688
+ /**
1689
+ * Collect node values from multiple components into an array.
1690
+ * Maps over all components matching the selector and extracts a node value from each.
1691
+ *
1692
+ * @param relativeTo - The component ID to search relative to
1693
+ * @param componentSelector - Selector for finding components (e.g., ':forms.Input', 'Button')
1694
+ * @param nodeSelector - The node path to extract from each component (e.g., 'value', 'inputKey')
1695
+ * @returns Array of node values from matched components
1696
+ *
1697
+ * @example
1698
+ * ```typescript
1699
+ * // Get all input values
1700
+ * const values = store.collectValuesArray(componentId, ':forms.Input', 'value');
1701
+ * // Returns: ['value1', 'value2', 'value3']
1702
+ * ```
1703
+ */
1704
+ collectValuesArray(relativeTo, componentSelector, nodeSelector) {
1705
+ const components = this.findComponents(relativeTo, componentSelector);
1706
+ return components.map((component) => component.getNodeValue(nodeSelector));
1707
+ }
1708
+ /**
1709
+ * Collect node values from multiple components into a map/object.
1710
+ * Reduces over all components matching the selector, using one node as the key and another as the value.
1711
+ *
1712
+ * @param relativeTo - The component ID to search relative to
1713
+ * @param componentSelector - Selector for finding components (e.g., ':forms.Input', 'Button')
1714
+ * @param keyNodeSelector - The node path to use as the object key (e.g., 'inputKey', 'id')
1715
+ * @param valueNodeSelector - The node path to use as the object value (e.g., 'value', 'label')
1716
+ * @returns Object/map with keys and values from matched components
1717
+ *
1718
+ * @example
1719
+ * ```typescript
1720
+ * // Create a map from inputKey to value
1721
+ * const map = store.collectValuesMap(componentId, ':forms.Input', 'inputKey', 'value');
1722
+ * // Returns: { key1: 'value1', key2: 'value2' }
1723
+ * ```
1724
+ */
1725
+ collectValuesMap(relativeTo, componentSelector, keyNodeSelector, valueNodeSelector) {
1726
+ const components = this.findComponents(relativeTo, componentSelector);
1727
+ return components.reduce((acc, component) => {
1728
+ const key = component.getNodeValue(keyNodeSelector);
1729
+ const value = component.getNodeValue(valueNodeSelector);
1730
+ // skip if the key is not defined
1731
+ if (key) {
1732
+ acc[key] = value;
1733
+ }
1734
+ return acc;
1735
+ }, {});
1736
+ }
1737
+ /**
1738
+ * Set a node from a raw schema value.
1739
+ * The raw schema will be unfolded before being applied.
1740
+ * Handles structure, computation stack, and nested components.
1741
+ *
1742
+ * @param node - The node to update
1743
+ * @param rawSchema - The raw schema value (will be unfolded into a NodeSchema)
1744
+ */
1745
+ setNodeRawSchema(node, rawSchema) {
1746
+ try {
1747
+ const unfolded = unfoldNodeSchema(rawSchema, node.valueType);
1748
+ this.setNodeSchema(node, unfolded);
1749
+ }
1750
+ catch (e) {
1751
+ const component = getComponent(node);
1752
+ const src = formatSource(component.sourceUrl, node.position.path);
1753
+ throw new Error(`${src} ${e.message}`, { cause: e });
1754
+ }
1755
+ }
1756
+ /**
1757
+ * Set a node from an already-unfolded schema.
1758
+ * Use this when you already have a NodeSchema to avoid double-unfolding.
1759
+ * Handles structure, nested components, and computation stack.
1760
+ *
1761
+ * @param node - The node to update
1762
+ * @param schema - The unfolded NodeSchema to set
1763
+ */
1764
+ setNodeSchema(node, schema) {
1765
+ const component = getComponent(node);
1766
+ const refSpace = this.getRefSpaceOrThrow(component.refSpaceId);
1767
+ const opts = {
1768
+ refSpaceId: refSpace.id,
1769
+ position: { componentId: component.id, path: node.position.path },
1770
+ components: {},
1771
+ refs: {},
1772
+ sourceUrl: component.sourceUrl ?? undefined,
1773
+ };
1774
+ const tmpNode = createNode(schema, opts);
1775
+ runInAction(() => {
1776
+ // Handle component removal for component-type nodes
1777
+ // Only remove components that are in the old structure but NOT in the new structure
1778
+ // This is important for component[] types where existing component IDs may be reused
1779
+ const oldComponents = extractComponentsIds(node);
1780
+ const newComponents = new Set(extractComponentsIds(tmpNode));
1781
+ for (const id of oldComponents) {
1782
+ if (!newComponents.has(id)) {
1783
+ this.removeComponent(id);
1784
+ }
1785
+ }
1786
+ // Set new structure and register new components/refs
1787
+ node.structure = tmpNode.structure;
1788
+ // Add refs for all RefSpaces (parent RefSpace and any nested RefSpaces created)
1789
+ for (const refSpaceId in opts.refs) {
1790
+ const targetRefSpace = this.getRefSpace(refSpaceId);
1791
+ if (targetRefSpace) {
1792
+ const refs = opts.refs[refSpaceId] ?? {};
1793
+ targetRefSpace.addRefs(refs);
1794
+ }
1795
+ }
1796
+ this.addComponents(opts.components);
1797
+ });
1798
+ // Push computation frame onto stack (after runAction, before activation)
1799
+ if (schema.computation) {
1800
+ const resultFrame = tmpNode.computationStack.top;
1801
+ if (resultFrame) {
1802
+ node.computationStack.push(resultFrame.schema);
1803
+ }
1804
+ }
1805
+ }
1806
+ /**
1807
+ * Remove an item from an array node.
1808
+ * Handles cleanup of nested components in the removed item.
1809
+ *
1810
+ * @param arrayNode - The array node to remove from
1811
+ * @param index - The index of the item to remove
1812
+ */
1813
+ removeArrayItem(arrayNode, index) {
1814
+ runInAction(() => {
1815
+ const value = arrayNode.structure || [];
1816
+ const removedItem = value[index];
1817
+ // Clean up components in the removed item
1818
+ if (removedItem) {
1819
+ const oldComponents = extractComponentsIds(removedItem);
1820
+ for (const id of oldComponents) {
1821
+ this.removeComponent(id);
1822
+ }
1823
+ }
1824
+ arrayNode.structure = [...value.slice(0, index), ...value.slice(index + 1)];
1825
+ });
1826
+ }
1827
+ /**
1828
+ * Insert an item into an array node at a specific index.
1829
+ * Creates a node structure from the raw value and inserts it at the specified position.
1830
+ * Shifts existing items to the right.
1831
+ *
1832
+ * @param arrayNode - The array node to insert into
1833
+ * @param index - The index at which to insert the item
1834
+ * @param value - The raw value to insert (will be transformed into a node structure)
1835
+ */
1836
+ insertArrayItem(arrayNode, index, value) {
1837
+ const { itemNode, opts, refSpace } = this.createArrayItemNode(arrayNode, index, value);
1838
+ runInAction(() => {
1839
+ const currentValue = arrayNode.structure || [];
1840
+ arrayNode.structure = [...currentValue.slice(0, index), itemNode, ...currentValue.slice(index)];
1841
+ const refs = opts.refs[refSpace.id] ?? {};
1842
+ refSpace.addRefs(refs);
1843
+ this.addComponents(opts.components);
1844
+ });
1845
+ }
1846
+ /**
1847
+ * Replace an item in an array node at a specific index.
1848
+ * Creates a node structure from the raw value and replaces the item at the specified position.
1849
+ * Properly cleans up nested components in the replaced item.
1850
+ *
1851
+ * @param arrayNode - The array node containing the item to replace
1852
+ * @param index - The index of the item to replace
1853
+ * @param value - The raw value to set (will be transformed into a node structure)
1854
+ */
1855
+ replaceArrayItem(arrayNode, index, value) {
1856
+ const { itemNode, opts, refSpace } = this.createArrayItemNode(arrayNode, index, value);
1857
+ runInAction(() => {
1858
+ const currentValue = arrayNode.structure || [];
1859
+ const replacedItem = currentValue[index];
1860
+ // Clean up components in the replaced item
1861
+ if (replacedItem) {
1862
+ const oldComponents = extractComponentsIds(replacedItem);
1863
+ for (const id of oldComponents) {
1864
+ this.removeComponent(id);
1865
+ }
1866
+ }
1867
+ arrayNode.structure = [...currentValue.slice(0, index), itemNode, ...currentValue.slice(index + 1)];
1868
+ const refs = opts.refs[refSpace.id] ?? {};
1869
+ refSpace.addRefs(refs);
1870
+ this.addComponents(opts.components);
1871
+ });
1872
+ }
1873
+ /**
1874
+ * Append an item to the end of an array node.
1875
+ * Creates a node structure from the raw value and appends it to the array.
1876
+ *
1877
+ * @param arrayNode - The array node to append to
1878
+ * @param value - The raw value to append (will be transformed into a node structure)
1879
+ */
1880
+ appendArrayItem(arrayNode, value) {
1881
+ // Calculate the index for the new item
1882
+ const currentLength = arrayNode.structure?.length ?? 0;
1883
+ const { itemNode, opts, refSpace } = this.createArrayItemNode(arrayNode, currentLength, value);
1884
+ runInAction(() => {
1885
+ arrayNode.structure = [...(arrayNode.structure ?? []), itemNode];
1886
+ const refs = opts.refs[refSpace.id] ?? {};
1887
+ refSpace.addRefs(refs);
1888
+ this.addComponents(opts.components);
1889
+ });
1890
+ }
1891
+ /**
1892
+ * Reconcile for-loop children using key-based diffing.
1893
+ * Instead of destroying all children and recreating them,
1894
+ * this method compares old vs new keys and only creates/destroys what's necessary.
1895
+ *
1896
+ * @param arrayNode - The array node containing for-loop children
1897
+ * @param newSchemas - The new schemas to reconcile
1898
+ * @returns Indices of newly added items for activation
1899
+ */
1900
+ reconcileForLoopChildren(arrayNode, newSchemas) {
1901
+ const oldChildren = arrayNode.structure || [];
1902
+ // Handle null/undefined newSchemas
1903
+ if (!newSchemas) {
1904
+ this.setNodeRawSchema(arrayNode, []);
1905
+ return [];
1906
+ }
1907
+ // If there are no old children, just set the new schemas
1908
+ if (oldChildren.length === 0) {
1909
+ this.setNodeRawSchema(arrayNode, newSchemas);
1910
+ return newSchemas.map((_, index) => index);
1911
+ }
1912
+ // If there are no new schemas, just set empty array (will remove all)
1913
+ if (newSchemas.length === 0) {
1914
+ this.setNodeRawSchema(arrayNode, []);
1915
+ return [];
1916
+ }
1917
+ // Check if explicit keys are provided (trackBy syntax)
1918
+ const hasExplicitKeys = this.checkForExplicitKeys(newSchemas);
1919
+ if (!hasExplicitKeys) {
1920
+ // No explicit keys - fall back to full replacement
1921
+ this.setNodeRawSchema(arrayNode, newSchemas);
1922
+ return newSchemas.map((_, index) => index);
1923
+ }
1924
+ // Extract keys from old children and new schemas
1925
+ const oldKeyMap = this.extractOldKeys(oldChildren);
1926
+ const newKeys = this.extractNewKeys(newSchemas);
1927
+ // Check for duplicate keys in new schemas
1928
+ const duplicateKeys = this.findDuplicateKeys(newKeys);
1929
+ if (duplicateKeys.size > 0) {
1930
+ log.warn(`Duplicate keys detected in for-loop: ${Array.from(duplicateKeys).join(', ')}. ` +
1931
+ 'This may cause unexpected behavior. Falling back to full replacement.');
1932
+ this.setNodeRawSchema(arrayNode, newSchemas);
1933
+ return newSchemas.map((_, index) => index);
1934
+ }
1935
+ // Build sets for diffing
1936
+ const oldKeySet = new Set(oldKeyMap.keys());
1937
+ const newKeySet = new Set(newKeys);
1938
+ // Determine what to add, remove, and keep
1939
+ const toRemove = [];
1940
+ const toKeepSet = new Set();
1941
+ for (const key of oldKeySet) {
1942
+ if (newKeySet.has(key)) {
1943
+ toKeepSet.add(key);
1944
+ }
1945
+ else {
1946
+ toRemove.push(key);
1947
+ }
1948
+ }
1949
+ const toAdd = [];
1950
+ newKeys.forEach((key, index) => {
1951
+ if (!oldKeySet.has(key)) {
1952
+ toAdd.push(index);
1953
+ }
1954
+ });
1955
+ // Perform the actual reconciliation
1956
+ this.applyForLoopReconciliation(arrayNode, {
1957
+ oldKeyMap,
1958
+ newKeys,
1959
+ newSchemas,
1960
+ toRemove,
1961
+ toKeep: Array.from(toKeepSet),
1962
+ });
1963
+ // Update loop variables (index, first, last, and item variables) for repositioned components
1964
+ this.updateLoopVariables(arrayNode, newKeys, newSchemas, oldKeyMap);
1965
+ return toAdd;
1966
+ }
1967
+ /**
1968
+ * Apply for-loop reconciliation by building new structure and cleaning up removed components.
1969
+ */
1970
+ applyForLoopReconciliation(arrayNode, reconcileInfo) {
1971
+ const { oldKeyMap, newKeys, newSchemas, toRemove, toKeep } = reconcileInfo;
1972
+ const toKeepSet = new Set(toKeep);
1973
+ const component = getComponent(arrayNode);
1974
+ const refSpace = this.getRefSpaceOrThrow(component.refSpaceId);
1975
+ // Build the new structure array, reusing kept nodes and creating new ones
1976
+ const newStructure = [];
1977
+ const newComponents = {};
1978
+ const newRefs = {};
1979
+ // Build new structure in order of new keys
1980
+ // Use untracked to avoid MobX reactivity issues when reading structure
1981
+ const oldStructure = untracked(() => arrayNode.structure);
1982
+ for (let i = 0; i < newKeys.length; i++) {
1983
+ const key = newKeys[i];
1984
+ const oldEntry = oldKeyMap.get(key);
1985
+ if (oldEntry && toKeepSet.has(key) && oldStructure && oldStructure[oldEntry.index]) {
1986
+ // Reuse existing node
1987
+ newStructure.push(oldStructure[oldEntry.index]);
1988
+ }
1989
+ else {
1990
+ // Create new node
1991
+ const schema = newSchemas[i];
1992
+ const itemPath = [...arrayNode.position.path, i];
1993
+ const opts = {
1994
+ refSpaceId: refSpace.id,
1995
+ position: { componentId: component.id, path: itemPath },
1996
+ components: {},
1997
+ refs: {},
1998
+ sourceUrl: component.sourceUrl ?? undefined,
1999
+ };
2000
+ const itemNode = createNode(unfoldNodeSchema(schema, arrayNode.valueType.item), opts);
2001
+ newStructure.push(itemNode);
2002
+ // Collect new components and refs
2003
+ Object.assign(newComponents, opts.components);
2004
+ const itemRefs = opts.refs[refSpace.id] ?? {};
2005
+ Object.assign(newRefs, itemRefs);
2006
+ }
2007
+ }
2008
+ runInAction(() => {
2009
+ // Remove components that are no longer needed
2010
+ for (const key of toRemove) {
2011
+ const entry = oldKeyMap.get(key);
2012
+ if (entry) {
2013
+ this.removeComponent(entry.componentId);
2014
+ }
2015
+ }
2016
+ // Update the structure
2017
+ arrayNode.structure = newStructure;
2018
+ // Register new refs and components
2019
+ refSpace.addRefs(newRefs);
2020
+ this.addComponents(newComponents);
2021
+ });
2022
+ }
2023
+ /**
2024
+ * Update loop variables (including item variable) on components that have been repositioned or whose data changed.
2025
+ */
2026
+ updateLoopVariables(node, newKeys, newSchemas, oldKeyMap) {
2027
+ const structure = node.structure || [];
2028
+ const totalItems = newKeys.length;
2029
+ runInAction(() => {
2030
+ newKeys.forEach((key, newIndex) => {
2031
+ const oldEntry = oldKeyMap.get(key);
2032
+ if (!oldEntry)
2033
+ return; // Skip newly added items
2034
+ const child = structure[newIndex];
2035
+ if (!child)
2036
+ return;
2037
+ const componentId = child.structure;
2038
+ if (!componentId)
2039
+ return;
2040
+ const component = this.getComponent(componentId);
2041
+ if (!component)
2042
+ return;
2043
+ // Update $index variable if it exists and the index changed
2044
+ // Variables are stored without the $ prefix (e.g., 'index' not '$index')
2045
+ const indexNode = component.variables.get('index');
2046
+ if (indexNode && oldEntry.index !== newIndex) {
2047
+ indexNode.structure = newIndex;
2048
+ }
2049
+ // Update $first variable if it exists
2050
+ const firstNode = component.variables.get('first');
2051
+ if (firstNode) {
2052
+ const wasFirst = oldEntry.index === 0;
2053
+ const isFirst = newIndex === 0;
2054
+ if (wasFirst !== isFirst) {
2055
+ firstNode.structure = isFirst;
2056
+ }
2057
+ }
2058
+ // Update $last variable if it exists
2059
+ const lastNode = component.variables.get('last');
2060
+ if (lastNode) {
2061
+ lastNode.structure = newIndex === totalItems - 1;
2062
+ }
2063
+ // Update item variables from new schema
2064
+ const newSchema = newSchemas[newIndex];
2065
+ for (const [schemaKey, schemaValue] of Object.entries(newSchema)) {
2066
+ if (!schemaKey.startsWith('$'))
2067
+ continue;
2068
+ const varName = schemaKey.slice(1); // Remove '$' prefix
2069
+ // Skip loop control variables (already handled above)
2070
+ if (varName === 'index' || varName === 'first' || varName === 'last' || varName === 'trackBy') {
2071
+ continue;
2072
+ }
2073
+ const varNode = component.variables.get(varName);
2074
+ if (!varNode)
2075
+ continue;
2076
+ const varDef = schemaValue;
2077
+ if (!varDef || !('_value' in varDef))
2078
+ continue;
2079
+ // Update item variable using setNodeRawSchema for proper object handling
2080
+ this.setNodeRawSchema(varNode, varDef._value);
2081
+ }
2082
+ });
2083
+ });
2084
+ }
2085
+ /**
2086
+ * Check if schemas have explicit trackBy keys that can be used for diffing.
2087
+ */
2088
+ checkForExplicitKeys(schemas) {
2089
+ if (schemas.length === 0)
2090
+ return false;
2091
+ const firstSchema = schemas[0];
2092
+ if (!firstSchema)
2093
+ return false;
2094
+ const forKeySchema = firstSchema['$trackBy'];
2095
+ if (forKeySchema && '_value' in forKeySchema) {
2096
+ const value = forKeySchema._value;
2097
+ if (typeof value === 'number') {
2098
+ return false;
2099
+ }
2100
+ if (typeof value === 'string') {
2101
+ const indexPattern = /^\d+(?::\d+)*$/;
2102
+ return !indexPattern.test(value);
2103
+ }
2104
+ return true;
2105
+ }
2106
+ return false;
2107
+ }
2108
+ /**
2109
+ * Extract keys from existing children (RefSpace components with $trackBy variable)
2110
+ */
2111
+ extractOldKeys(children) {
2112
+ const keyMap = new Map();
2113
+ children.forEach((child, index) => {
2114
+ const componentId = child.structure;
2115
+ if (!componentId)
2116
+ return;
2117
+ const component = this.getComponent(componentId);
2118
+ if (!component || component.componentType !== 'sys.RefSpace')
2119
+ return;
2120
+ const keyNode = component.variables.get('trackBy');
2121
+ const key = keyNode ? keyNode.extractedValue : component.id;
2122
+ keyMap.set(String(key), { index, componentId });
2123
+ });
2124
+ return keyMap;
2125
+ }
2126
+ /**
2127
+ * Extract keys from new schemas (generated by repeat())
2128
+ */
2129
+ extractNewKeys(schemas) {
2130
+ return schemas.map((schema, index) => {
2131
+ const forKeyValue = schema['$trackBy'];
2132
+ if (forKeyValue && '_value' in forKeyValue) {
2133
+ return String(forKeyValue._value);
2134
+ }
2135
+ return String(index);
2136
+ });
2137
+ }
2138
+ /**
2139
+ * Find duplicate keys in an array
2140
+ */
2141
+ findDuplicateKeys(keys) {
2142
+ const seen = new Set();
2143
+ const duplicates = new Set();
2144
+ for (const key of keys) {
2145
+ if (seen.has(key)) {
2146
+ duplicates.add(key);
2147
+ }
2148
+ seen.add(key);
2149
+ }
2150
+ return duplicates;
2151
+ }
2152
+ removeComponent(componentId) {
2153
+ const component = this.components.get(componentId);
2154
+ if (!component)
2155
+ return;
2156
+ runInAction(() => {
2157
+ // Collect all nested child component IDs (depth-first traversal)
2158
+ const allChildIds = [];
2159
+ for (const [, propNode] of component.props) {
2160
+ extractComponentsIds(propNode, allChildIds);
2161
+ }
2162
+ for (const [, varNode] of component.variables) {
2163
+ extractComponentsIds(varNode, allChildIds);
2164
+ }
2165
+ // Deduplicate while preserving order (later items are deeper in tree)
2166
+ const childIds = [...new Set(allChildIds)];
2167
+ const allIds = new Set([componentId, ...childIds]);
2168
+ const spacesToRemove = this.collectRefSpacesToRemove(allIds);
2169
+ // Dispose components in leaf-first order (reverse of depth-first)
2170
+ for (let i = childIds.length - 1; i >= 0; i--) {
2171
+ this.components.get(childIds[i])?.dispose();
2172
+ }
2173
+ component.dispose();
2174
+ // Finally, delete all components and RefSpaces from registries
2175
+ for (const id of allIds) {
2176
+ // Remove from componentsByRefSpace index
2177
+ const comp = this.components.get(id);
2178
+ if (comp) {
2179
+ this.componentsByRefSpace.get(comp.refSpaceId)?.delete(id);
2180
+ }
2181
+ this.components.delete(id);
2182
+ }
2183
+ for (const refSpaceId of spacesToRemove) {
2184
+ this.refSpaces.delete(refSpaceId);
2185
+ // Clean up empty index entries
2186
+ this.componentsByRefSpace.delete(refSpaceId);
2187
+ }
2188
+ });
2189
+ }
2190
+ /**
2191
+ * Creates a new node for an array item from a raw value.
2192
+ * Returns the created node and the options used (for registering components/refs).
2193
+ * @throws If the array node has no parent component
2194
+ */
2195
+ createArrayItemNode(arrayNode, index, value) {
2196
+ const component = getComponent(arrayNode);
2197
+ const refSpace = this.getRefSpaceOrThrow(component.refSpaceId);
2198
+ const itemPath = [...arrayNode.position.path, index];
2199
+ const opts = {
2200
+ refSpaceId: refSpace.id,
2201
+ position: { componentId: component.position.componentId, path: itemPath },
2202
+ components: {},
2203
+ refs: {},
2204
+ sourceUrl: component.sourceUrl ?? undefined,
2205
+ };
2206
+ const itemNode = createNode(unfoldNodeSchema(value, arrayNode.valueType.item), opts);
2207
+ return { itemNode, opts, refSpace };
2208
+ }
2209
+ collectRefSpacesToRemove(componentIdsToDelete) {
2210
+ // Collect unique RefSpace IDs that components belong to (candidates for removal)
2211
+ const candidateSpaceIds = new Set();
2212
+ for (const componentId of componentIdsToDelete) {
2213
+ const refSpaceId = this.components.get(componentId)?.refSpaceId;
2214
+ if (refSpaceId) {
2215
+ candidateSpaceIds.add(refSpaceId);
2216
+ }
2217
+ }
2218
+ // Filter to only RefSpaces that have no remaining components after deletion
2219
+ const refSpaceIdsToDelete = new Set();
2220
+ for (const refSpaceId of candidateSpaceIds) {
2221
+ const componentsInSpace = this.componentsByRefSpace.get(refSpaceId);
2222
+ if (!componentsInSpace)
2223
+ continue;
2224
+ // Check if ALL components in this RefSpace are being deleted
2225
+ let allDeleted = true;
2226
+ for (const id of componentsInSpace) {
2227
+ if (!componentIdsToDelete.has(id)) {
2228
+ allDeleted = false;
2229
+ break;
2230
+ }
2231
+ }
2232
+ if (allDeleted) {
2233
+ refSpaceIdsToDelete.add(refSpaceId);
2234
+ }
2235
+ }
2236
+ return refSpaceIdsToDelete;
2237
+ }
2238
+ }
2239
+
2240
+ function createRootNode(refSpaceId, schema, ownerComponentId, parentRefSpaceId, sourceUrl) {
2241
+ const opts = {
2242
+ refSpaceId: refSpaceId,
2243
+ position: { componentId: ownerComponentId, path: [] },
2244
+ components: {},
2245
+ refs: {},
2246
+ sourceUrl,
2247
+ };
2248
+ const root = createNode(schema, opts);
2249
+ const refSpace = new RefSpace(refSpaceId, parentRefSpaceId);
2250
+ const store = ComponentStore.getInstance();
2251
+ store.addRefSpace(refSpace);
2252
+ store.addComponents(opts.components);
2253
+ const refs = opts.refs[refSpaceId] ?? {};
2254
+ refSpace.addRefs(refs);
2255
+ return root;
2256
+ }
2257
+
2258
+ class ComponentTreeApi {
2259
+ static store = ComponentStore.getInstance();
2260
+ static findComponent(relativeTo, componentSelector) {
2261
+ return this.store.findComponent(relativeTo, componentSelector);
2262
+ }
2263
+ static findComponentsIds(relativeTo, componentSelector) {
2264
+ return findComponentsIds(relativeTo, parseComponentSelector(componentSelector));
2265
+ }
2266
+ static findComponents(relativeTo, componentSelector) {
2267
+ const ids = this.findComponentsIds(relativeTo, componentSelector);
2268
+ return ids.map((id) => this.store.getComponentOrThrow(id));
2269
+ }
2270
+ static getComponent(relativeTo, componentSelector) {
2271
+ const component = this.store.findComponent(relativeTo, componentSelector);
2272
+ if (!component) {
2273
+ throw new Error(`Component ${componentSelector} not found`);
2274
+ }
2275
+ return component;
2276
+ }
2277
+ static findNode(relativeTo, componentSelector) {
2278
+ return this.store.findNode(relativeTo, componentSelector);
2279
+ }
2280
+ static getNode(relativeTo, componentSelector) {
2281
+ return this.store.getNode(relativeTo, componentSelector);
2282
+ }
2283
+ static collectValuesArray(componentId, componentSelector, nodeSelector) {
2284
+ try {
2285
+ return this.store.collectValuesArray(componentId, componentSelector, nodeSelector);
2286
+ }
2287
+ catch (e) {
2288
+ log.error(`Error collecting values as array by component selector ${componentSelector}`, e);
2289
+ return [];
2290
+ }
2291
+ }
2292
+ static collectValuesMap(componentId, componentSelector, keyNodeSelector, valueNodeSelector) {
2293
+ try {
2294
+ return this.store.collectValuesMap(componentId, componentSelector, keyNodeSelector, valueNodeSelector);
2295
+ }
2296
+ catch (e) {
2297
+ log.error(`Error collecting values as map by component selector ${componentSelector}`, e);
2298
+ return {};
2299
+ }
2300
+ }
2301
+ static isCommandRunning(componentId, selector) {
2302
+ try {
2303
+ const node = this.getNode(componentId, selector);
2304
+ if (!isCommandNode(node)) {
2305
+ throw new Error(`Error`);
2306
+ }
2307
+ return node.viewModel.executing;
2308
+ }
2309
+ catch (e) {
2310
+ log.error(`Error checking command running state by selector ${selector}`, e);
2311
+ return false;
2312
+ }
2313
+ }
2314
+ static runCommandNode(componentId, selector, ...args) {
2315
+ try {
2316
+ const node = this.getNode(componentId, selector);
2317
+ if (!isCommandNode(node)) {
2318
+ throw new Error(`Node at "${selector}" is not a command`);
2319
+ }
2320
+ log.trace(`Running command "${selector}" on ${componentId}`);
2321
+ return node.viewModel.execute(...args);
2322
+ }
2323
+ catch (e) {
2324
+ log.error(`Error running command by selector "${selector}" with args:`, JSON.stringify(args), e);
2325
+ return null;
2326
+ }
2327
+ }
2328
+ static setNodeRawSchema(componentId, selector, value) {
2329
+ try {
2330
+ const node = this.getNode(componentId, selector);
2331
+ this.store.setNodeRawSchema(node, value);
2332
+ }
2333
+ catch (e) {
2334
+ log.error(`Error setting node by selector "${selector}" with value:`, value, e);
2335
+ }
2336
+ }
2337
+ static appendArrayItem(componentId, selector, item) {
2338
+ try {
2339
+ const arrayNode = this.store.getNode(componentId, selector);
2340
+ this.store.appendArrayItem(arrayNode, item);
2341
+ }
2342
+ catch (e) {
2343
+ log.error(`Error appending array item to selector "${selector}" with value:`, item, e);
2344
+ }
2345
+ }
2346
+ static insertArrayItem(componentId, selector, index, item) {
2347
+ try {
2348
+ const arrayNode = this.store.getNode(componentId, selector);
2349
+ this.store.insertArrayItem(arrayNode, index, item);
2350
+ }
2351
+ catch (e) {
2352
+ log.error(`Error inserting array item to selector "${selector}" with value:`, item, e);
2353
+ }
2354
+ }
2355
+ static removeArrayItem(componentId, selector, index) {
2356
+ try {
2357
+ const arrayNode = this.store.getNode(componentId, selector);
2358
+ this.store.removeArrayItem(arrayNode, index);
2359
+ }
2360
+ catch (e) {
2361
+ log.error(`Error removing array item from selector "${selector}" at index ${index}`, e);
2362
+ }
2363
+ }
2364
+ static replaceArrayItem(componentId, selector, index, item) {
2365
+ try {
2366
+ const arrayNode = this.store.getNode(componentId, selector);
2367
+ this.store.replaceArrayItem(arrayNode, index, item);
2368
+ }
2369
+ catch (e) {
2370
+ log.error(`Error replacing array item at selector "${selector}" at index ${index} with value:`, item, e);
2371
+ }
2372
+ }
2373
+ }
2374
+
2375
+ /**
2376
+ * Generated bundle index. Do not edit.
2377
+ */
2378
+
2379
+ export { AbstractNode, AbstractObjectLikeNode, ArrayNode, CommandNode, ComponentStore, ComponentTreeApi, ComputationFrame, ComputationStack, LeafNode, MapNode, ObjectNode, RefSpace, SetNode, ShapeNode, TComponent, VariantShapeNode, componentTreeConfig, createRootNode, formatSource, getComponent, isArrayNode, isCommandNode };
2380
+ //# sourceMappingURL=kaskad-component-tree.mjs.map