@creact-labs/creact 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -1,4 +1,10 @@
1
- # CReact — React for Infrastructure
1
+
2
+ NOTE: !!! THIS IS A EXPERIMENT OF THOUGHT NOT A PRODUCTION READY PRODUCT !!!
3
+
4
+ # CReact
5
+
6
+ ![creact](https://i.postimg.cc/8P66GnT3/banner.jpg)
7
+
2
8
 
3
9
  Think of it as React's rendering model — but instead of a DOM, you render to the cloud.
4
10
 
@@ -106,7 +106,7 @@ class CReact {
106
106
  this.renderer.setRenderScheduler(this.renderScheduler);
107
107
  this.renderer.setContextDependencyTracker(this.contextDependencyTracker);
108
108
  this.renderer.setStructuralChangeDetector(this.structuralChangeDetector);
109
- this.cloudDOMBuilder.setReactiveComponents(this.stateBindingManager, this.providerOutputTracker);
109
+ this.cloudDOMBuilder.setReactiveComponents(this.stateBindingManager, this.providerOutputTracker, this.contextDependencyTracker);
110
110
  this.contextDependencyTracker.setStateBindingManager(this.stateBindingManager);
111
111
  // Set the context dependency tracker in useContext hook
112
112
  (0, useContext_1.setContextDependencyTracker)(this.contextDependencyTracker);
@@ -513,16 +513,20 @@ class CReact {
513
513
  this.log('Executing post-deployment effects...');
514
514
  // Integrate with post-deployment effects and output sync
515
515
  const affectedFibers = await this.cloudDOMBuilder.integrateWithPostDeploymentEffects(this.lastFiberTree, cloudDOM, previousCloudDOM);
516
- // REQ-7.1, 7.2, 7.3: Check if outputs actually changed (including undefined → value)
517
- const hasActualChanges = this.hasActualOutputChanges(previousCloudDOM, cloudDOM);
518
- // CRITICAL: If state outputs changed but no provider output bindings triggered,
519
- // we still need to re-render to display updated state in the component
520
- const needsReRender = hasActualChanges || affectedFibers.length > 0;
516
+ // CRITICAL: Only re-render if there are actually affected fibers
517
+ // If outputs changed but no fibers are bound to those outputs, skip re-render
518
+ console.log(`[BUG DEBUG] affectedFibers.length = ${affectedFibers.length}`);
519
+ if (affectedFibers.length > 0) {
520
+ console.log(`[BUG DEBUG] affectedFibers paths:`, affectedFibers.map(f => f.path?.join('.')));
521
+ }
522
+ else {
523
+ console.log('[BUG DEBUG] No affected fibers - should NOT re-render!');
524
+ }
525
+ const needsReRender = affectedFibers.length > 0;
521
526
  if (needsReRender) {
522
- // If no fibers were affected by provider outputs, but state changed,
523
- // re-render the root component to display updated state
524
- const fibersToReRender = affectedFibers.length > 0 ? affectedFibers : [this.lastFiberTree];
525
- this.log(`Triggering re-renders for ${fibersToReRender.length} affected components (${hasActualChanges ? 'output changes detected' : 'state changes detected'})`);
527
+ // Re-render only the affected components
528
+ const fibersToReRender = affectedFibers;
529
+ logger.info(`Triggering re-renders for ${fibersToReRender.length} affected components`);
526
530
  // REQ-7.4, 7.5: Schedule re-renders with proper batching and deduplication
527
531
  fibersToReRender.forEach((fiber) => {
528
532
  this.scheduleReRender(fiber, 'output-update');
@@ -536,7 +540,8 @@ class CReact {
536
540
  // Execute the scheduled re-renders
537
541
  const updatedFiber = this.renderer.reRenderComponents(fibersToReRender, 'output-update');
538
542
  // Build updated CloudDOM from re-rendered components
539
- const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber);
543
+ // CRITICAL FIX: Pass previousCloudDOM to preserve non-re-rendered nodes
544
+ const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber, cloudDOM);
540
545
  // Check if the re-render produced new resources to deploy
541
546
  const reactiveChangeSet = this.reconciler.reconcile(cloudDOM, updatedCloudDOM);
542
547
  if ((0, Reconciler_1.hasChanges)(reactiveChangeSet)) {
@@ -31,6 +31,7 @@ import { FiberNode, CloudDOMNode, OutputChange } from './types';
31
31
  import { ICloudProvider } from '../providers/ICloudProvider';
32
32
  import { StateBindingManager } from './StateBindingManager';
33
33
  import { ProviderOutputTracker } from './ProviderOutputTracker';
34
+ import { ContextDependencyTracker } from './ContextDependencyTracker';
34
35
  /**
35
36
  * CloudDOMBuilder transforms a Fiber tree into a CloudDOM tree
36
37
  *
@@ -59,6 +60,10 @@ export declare class CloudDOMBuilder {
59
60
  * Provider output tracker for instance-output binding
60
61
  */
61
62
  private providerOutputTracker?;
63
+ /**
64
+ * Context dependency tracker for context reactivity
65
+ */
66
+ private contextDependencyTracker?;
62
67
  /**
63
68
  * Track existing nodes to handle re-render scenarios
64
69
  * Maps node ID to fiber path for duplicate detection during re-renders
@@ -98,8 +103,9 @@ export declare class CloudDOMBuilder {
98
103
  *
99
104
  * @param stateBindingManager - State binding manager instance
100
105
  * @param providerOutputTracker - Provider output tracker instance
106
+ * @param contextDependencyTracker - Context dependency tracker instance (optional)
101
107
  */
102
- setReactiveComponents(stateBindingManager: StateBindingManager, providerOutputTracker: ProviderOutputTracker): void;
108
+ setReactiveComponents(stateBindingManager: StateBindingManager, providerOutputTracker: ProviderOutputTracker, contextDependencyTracker?: ContextDependencyTracker): void;
103
109
  /**
104
110
  * Build CloudDOM tree from Fiber tree
105
111
  *
@@ -109,12 +115,23 @@ export declare class CloudDOMBuilder {
109
115
  *
110
116
  * Supports async lifecycle hooks for validation and provider preparation.
111
117
  *
118
+ * CRITICAL FIX: Preserves nodes from previousCloudDOM that weren't re-rendered.
119
+ * This prevents node loss during selective re-renders (e.g., multi-environment apps).
120
+ *
112
121
  * REQ-01: Transform Fiber → CloudDOM
113
122
  *
114
123
  * @param fiber - Root Fiber node
124
+ * @param previousCloudDOM - Optional previous CloudDOM for incremental builds
115
125
  * @returns Promise resolving to array of CloudDOM nodes (top-level resources)
116
126
  */
117
- build(fiber: FiberNode): Promise<CloudDOMNode[]>;
127
+ build(fiber: FiberNode, previousCloudDOM?: CloudDOMNode[]): Promise<CloudDOMNode[]>;
128
+ /**
129
+ * Flatten CloudDOM tree into a flat array of all nodes
130
+ *
131
+ * @param nodes - Root CloudDOM nodes
132
+ * @returns Flattened array of all nodes
133
+ */
134
+ private flattenCloudDOM;
118
135
  /**
119
136
  * Build CloudDOM tree with error handling for CLI/CI environments
120
137
  *
@@ -122,9 +139,10 @@ export declare class CloudDOMBuilder {
122
139
  * crashing the entire process. Useful for CI/CD pipelines.
123
140
  *
124
141
  * @param fiber - Root Fiber node
142
+ * @param previousCloudDOM - Optional previous CloudDOM for incremental builds
125
143
  * @returns Promise resolving to array of CloudDOM nodes, or empty array on error
126
144
  */
127
- buildSafe(fiber: FiberNode): Promise<CloudDOMNode[]>;
145
+ buildSafe(fiber: FiberNode, previousCloudDOM?: CloudDOMNode[]): Promise<CloudDOMNode[]>;
128
146
  /**
129
147
  * Recursively collect CloudDOM nodes from Fiber tree
130
148
  *
@@ -123,10 +123,12 @@ class CloudDOMBuilder {
123
123
  *
124
124
  * @param stateBindingManager - State binding manager instance
125
125
  * @param providerOutputTracker - Provider output tracker instance
126
+ * @param contextDependencyTracker - Context dependency tracker instance (optional)
126
127
  */
127
- setReactiveComponents(stateBindingManager, providerOutputTracker) {
128
+ setReactiveComponents(stateBindingManager, providerOutputTracker, contextDependencyTracker) {
128
129
  this.stateBindingManager = stateBindingManager;
129
130
  this.providerOutputTracker = providerOutputTracker;
131
+ this.contextDependencyTracker = contextDependencyTracker;
130
132
  }
131
133
  /**
132
134
  * Build CloudDOM tree from Fiber tree
@@ -137,12 +139,16 @@ class CloudDOMBuilder {
137
139
  *
138
140
  * Supports async lifecycle hooks for validation and provider preparation.
139
141
  *
142
+ * CRITICAL FIX: Preserves nodes from previousCloudDOM that weren't re-rendered.
143
+ * This prevents node loss during selective re-renders (e.g., multi-environment apps).
144
+ *
140
145
  * REQ-01: Transform Fiber → CloudDOM
141
146
  *
142
147
  * @param fiber - Root Fiber node
148
+ * @param previousCloudDOM - Optional previous CloudDOM for incremental builds
143
149
  * @returns Promise resolving to array of CloudDOM nodes (top-level resources)
144
150
  */
145
- async build(fiber) {
151
+ async build(fiber, previousCloudDOM) {
146
152
  if (!fiber) {
147
153
  throw new Error('[CloudDOMBuilder] Cannot build CloudDOM from null Fiber tree');
148
154
  }
@@ -163,6 +169,17 @@ class CloudDOMBuilder {
163
169
  // Collect all CloudDOM nodes from the Fiber tree
164
170
  const cloudDOMNodes = [];
165
171
  this.collectCloudDOMNodes(fiber, cloudDOMNodes);
172
+ // CRITICAL FIX: If we have previous CloudDOM and this is an incremental build,
173
+ // preserve nodes that aren't in the current fiber tree
174
+ if (previousCloudDOM && previousCloudDOM.length > 0) {
175
+ const currentNodeIds = new Set(cloudDOMNodes.map(n => n.id));
176
+ // Find nodes from previous CloudDOM that aren't in current build
177
+ const preservedNodes = this.flattenCloudDOM(previousCloudDOM).filter(node => !currentNodeIds.has(node.id));
178
+ if (preservedNodes.length > 0) {
179
+ logger.info(`[CloudDOMBuilder] Preserving ${preservedNodes.length} nodes from previous CloudDOM that weren't re-rendered`);
180
+ cloudDOMNodes.push(...preservedNodes);
181
+ }
182
+ }
166
183
  // Validate collected nodes
167
184
  this.validateCloudDOMNodes(cloudDOMNodes);
168
185
  // Build parent-child relationships
@@ -188,6 +205,25 @@ class CloudDOMBuilder {
188
205
  }
189
206
  return rootNodes;
190
207
  }
208
+ /**
209
+ * Flatten CloudDOM tree into a flat array of all nodes
210
+ *
211
+ * @param nodes - Root CloudDOM nodes
212
+ * @returns Flattened array of all nodes
213
+ */
214
+ flattenCloudDOM(nodes) {
215
+ const flattened = [];
216
+ const walk = (nodeList) => {
217
+ for (const node of nodeList) {
218
+ flattened.push(node);
219
+ if (node.children && node.children.length > 0) {
220
+ walk(node.children);
221
+ }
222
+ }
223
+ };
224
+ walk(nodes);
225
+ return flattened;
226
+ }
191
227
  /**
192
228
  * Build CloudDOM tree with error handling for CLI/CI environments
193
229
  *
@@ -195,11 +231,12 @@ class CloudDOMBuilder {
195
231
  * crashing the entire process. Useful for CI/CD pipelines.
196
232
  *
197
233
  * @param fiber - Root Fiber node
234
+ * @param previousCloudDOM - Optional previous CloudDOM for incremental builds
198
235
  * @returns Promise resolving to array of CloudDOM nodes, or empty array on error
199
236
  */
200
- async buildSafe(fiber) {
237
+ async buildSafe(fiber, previousCloudDOM) {
201
238
  try {
202
- return await this.build(fiber);
239
+ return await this.build(fiber, previousCloudDOM);
203
240
  }
204
241
  catch (error) {
205
242
  logger.error('Build failed:', error);
@@ -232,6 +269,7 @@ class CloudDOMBuilder {
232
269
  cloudNode.state = { ...stateValues };
233
270
  }
234
271
  collected.push(cloudNode);
272
+ logger.debug(`Collected CloudDOM node: ${cloudNode.id} from fiber: ${fiber.path?.join('.')}`);
235
273
  }
236
274
  else {
237
275
  const nodeId = cloudNode?.id ?? 'unknown';
@@ -239,6 +277,14 @@ class CloudDOMBuilder {
239
277
  }
240
278
  }
241
279
  }
280
+ else {
281
+ // CRITICAL FIX: If this fiber has no cloudDOMNodes but has children,
282
+ // it might be a component that wasn't re-executed during selective re-rendering.
283
+ // Log this for debugging purposes.
284
+ if (fiber.children && fiber.children.length > 0) {
285
+ logger.debug(`Fiber ${fiber.path?.join('.')} has no cloudDOMNodes but has ${fiber.children.length} children`);
286
+ }
287
+ }
242
288
  // Legacy cloudDOMNode removed - only cloudDOMNodes array is supported
243
289
  // Recursively collect from children
244
290
  if (fiber.children && fiber.children.length > 0) {
@@ -399,6 +445,10 @@ class CloudDOMBuilder {
399
445
  * @returns True if node is a valid CloudDOM node
400
446
  */
401
447
  isValidCloudNode(node) {
448
+ // Filter out placeholder nodes - they shouldn't be deployed
449
+ if (node?.id?.includes?.('__placeholder__') || node?.constructType === 'Placeholder') {
450
+ return false;
451
+ }
402
452
  return (typeof node?.id === 'string' &&
403
453
  Array.isArray(node?.path) &&
404
454
  node.path.length > 0 &&
@@ -957,24 +1007,10 @@ class CloudDOMBuilder {
957
1007
  * 2. Have children that are Context.Provider components
958
1008
  */
959
1009
  findContextValueCreators(fiber, affectedFibers) {
960
- // Check if this fiber uses outputs (has cloudDOMNodes)
961
- const usesOutputs = fiber.cloudDOMNodes && fiber.cloudDOMNodes.length > 0;
962
- // Check if this fiber has Context.Provider children
963
- const hasProviderChild = fiber.children &&
964
- fiber.children.some((child) => typeof child.type === 'function' && child.type._isContextProvider);
965
- logger.info(`[Context Reactivity] Checking fiber: ${fiber.path?.join('.')}`);
966
- logger.info(` Uses outputs: ${usesOutputs}`);
967
- logger.info(` Has provider child: ${hasProviderChild}`);
968
- if (usesOutputs && hasProviderChild) {
969
- // This component creates context values from outputs - needs re-render
970
- logger.info(`[Context Reactivity] ✓ Found context value creator: ${fiber.path?.join('.')}`);
971
- logger.info(` Has ${fiber.cloudDOMNodes?.length || 0} output dependencies`);
972
- // Add to affected fibers for re-rendering
973
- if (!affectedFibers.includes(fiber)) {
974
- affectedFibers.push(fiber);
975
- }
976
- }
977
- // Recursively process children
1010
+ // IMPORTANT: Don't automatically mark context value creators as affected
1011
+ // Only mark them if their specific outputs changed (handled by ProviderOutputTracker)
1012
+ // This prevents unnecessary re-renders that lose context values from parent providers
1013
+ // Recursively process children only (don't mark anything as affected)
978
1014
  if (fiber.children && fiber.children.length > 0) {
979
1015
  for (const child of fiber.children) {
980
1016
  this.findContextValueCreators(child, affectedFibers);
@@ -232,6 +232,15 @@ export declare class Renderer {
232
232
  * @param currentPath - Current path in the tree
233
233
  */
234
234
  private selectiveReRenderRecursive;
235
+ /**
236
+ * Check if component should re-render by matching paths
237
+ * This works with cloned fibers where object identity doesn't match
238
+ *
239
+ * @param fiber - Current fiber to check
240
+ * @param componentsToReRender - Set of original fibers to re-render
241
+ * @returns True if this fiber's path matches any in the set
242
+ */
243
+ private shouldReRenderComponentByPath;
235
244
  /**
236
245
  * Determine if a component should be re-rendered
237
246
  *
@@ -247,13 +256,64 @@ export declare class Renderer {
247
256
  * @param currentPath - Current path in the tree
248
257
  */
249
258
  private reExecuteComponent;
259
+ /**
260
+ * Restore context stack for a fiber by traversing parent providers
261
+ * This ensures useContext() gets the correct values during re-renders
262
+ *
263
+ * @param fiber - Fiber node to restore context for
264
+ */
265
+ private restoreContextStackForFiber;
266
+ /**
267
+ * Find all ancestor context providers for a fiber
268
+ * Returns providers in order from root to the fiber's parent
269
+ *
270
+ * @param fiber - Fiber node to find ancestors for
271
+ * @returns Array of provider fibers (root to leaf order)
272
+ */
273
+ private findAncestorProviders;
274
+ /**
275
+ * Recursively search for ancestor providers
276
+ *
277
+ * @param currentFiber - Current fiber being examined
278
+ * @param targetFiber - Target fiber we're looking for ancestors of
279
+ * @param providers - Array to collect providers
280
+ * @param path - Current path of provider fibers
281
+ * @returns True if targetFiber was found in this subtree
282
+ */
283
+ private findAncestorProvidersRecursive;
250
284
  /**
251
285
  * Find the root component from a set of components
252
286
  *
287
+ * CRITICAL FIX: Find common ancestor instead of just shortest path
288
+ * When multiple components have the same path length (e.g., multi-environment apps),
289
+ * we need to find their common ancestor, not just pick the first one.
290
+ *
253
291
  * @param components - Array of components
254
292
  * @returns Root component or null
255
293
  */
256
294
  private findRootComponent;
295
+ /**
296
+ * Find common path prefix among multiple paths
297
+ *
298
+ * @param paths - Array of path arrays
299
+ * @returns Common prefix path
300
+ */
301
+ private findCommonPathPrefix;
302
+ /**
303
+ * Find fiber by path in the fiber tree
304
+ *
305
+ * @param fiber - Root fiber to search from
306
+ * @param targetPath - Path to find
307
+ * @returns Fiber at path or null
308
+ */
309
+ private findFiberByPath;
310
+ /**
311
+ * Find actual root by traversing up the fiber tree
312
+ *
313
+ * @param fiber - Starting fiber
314
+ * @returns Root fiber
315
+ */
316
+ private findActualRoot;
257
317
  /**
258
318
  * Recursively find dependent components
259
319
  *
@@ -472,28 +472,22 @@ class Renderer {
472
472
  throw new Error('[Renderer] No components provided for re-rendering');
473
473
  }
474
474
  return (0, context_1.runWithHookContext)(() => {
475
- try {
476
- // Find the root component to determine the full tree structure
477
- const rootComponent = this.findRootComponent(components);
478
- if (!rootComponent) {
479
- throw new Error('[Renderer] Could not determine root component for re-rendering');
480
- }
481
- // Track dependencies during re-render
482
- this.trackDependenciesDuringRender(components);
483
- // Selectively re-render the components
484
- const updatedRoot = this.selectiveReRender(rootComponent, new Set(components), reason);
485
- // Detect structural changes if this is a structural re-render
486
- if (reason === 'structural-change' && this.structuralChangeDetector) {
487
- this.handleStructuralChangesComparison(rootComponent, updatedRoot);
488
- }
489
- // Update current fiber reference
490
- this.currentFiber = updatedRoot;
491
- return updatedRoot;
475
+ // Find the root component to determine the full tree structure
476
+ const rootComponent = this.findRootComponent(components);
477
+ if (!rootComponent) {
478
+ throw new Error('[Renderer] Could not determine root component for re-rendering');
492
479
  }
493
- finally {
494
- // Clear context stacks to prevent memory leaks
495
- (0, context_1.clearContextStacks)();
480
+ // Track dependencies during re-render
481
+ this.trackDependenciesDuringRender(components);
482
+ // Selectively re-render the components
483
+ const updatedRoot = this.selectiveReRender(rootComponent, new Set(components), reason);
484
+ // Detect structural changes if this is a structural re-render
485
+ if (reason === 'structural-change' && this.structuralChangeDetector) {
486
+ this.handleStructuralChangesComparison(rootComponent, updatedRoot);
496
487
  }
488
+ // Update current fiber reference
489
+ this.currentFiber = updatedRoot;
490
+ return updatedRoot;
497
491
  });
498
492
  }
499
493
  /**
@@ -646,7 +640,8 @@ class Renderer {
646
640
  */
647
641
  selectiveReRenderRecursive(fiber, componentsToReRender, reason, currentPath) {
648
642
  // Check if this component needs re-rendering
649
- const needsReRender = this.shouldReRenderComponent(fiber, componentsToReRender);
643
+ // Match by path instead of object identity (fibers are cloned)
644
+ const needsReRender = this.shouldReRenderComponentByPath(fiber, componentsToReRender);
650
645
  if (needsReRender) {
651
646
  // Update reactive state
652
647
  if (!fiber.reactiveState) {
@@ -672,6 +667,25 @@ class Renderer {
672
667
  });
673
668
  }
674
669
  }
670
+ /**
671
+ * Check if component should re-render by matching paths
672
+ * This works with cloned fibers where object identity doesn't match
673
+ *
674
+ * @param fiber - Current fiber to check
675
+ * @param componentsToReRender - Set of original fibers to re-render
676
+ * @returns True if this fiber's path matches any in the set
677
+ */
678
+ shouldReRenderComponentByPath(fiber, componentsToReRender) {
679
+ // Match by path (works with cloned fibers)
680
+ const fiberPath = fiber.path?.join('.') || '';
681
+ for (const componentToReRender of componentsToReRender) {
682
+ const targetPath = componentToReRender.path?.join('.') || '';
683
+ if (fiberPath === targetPath) {
684
+ return true;
685
+ }
686
+ }
687
+ return false;
688
+ }
675
689
  /**
676
690
  * Determine if a component should be re-rendered
677
691
  *
@@ -706,6 +720,8 @@ class Renderer {
706
720
  * @param currentPath - Current path in the tree
707
721
  */
708
722
  reExecuteComponent(fiber, currentPath) {
723
+ // CRITICAL: Restore context stack from parent providers before re-executing
724
+ this.restoreContextStackForFiber(fiber);
709
725
  // Set up rendering context
710
726
  (0, context_1.setRenderContext)(fiber, fiber.path);
711
727
  (0, useInstance_1.resetConstructCounts)(fiber);
@@ -724,23 +740,158 @@ class Renderer {
724
740
  (0, context_1.clearRenderContext)();
725
741
  }
726
742
  }
743
+ /**
744
+ * Restore context stack for a fiber by traversing parent providers
745
+ * This ensures useContext() gets the correct values during re-renders
746
+ *
747
+ * @param fiber - Fiber node to restore context for
748
+ */
749
+ restoreContextStackForFiber(fiber) {
750
+ // Find all ancestor providers up to the root
751
+ const ancestorProviders = this.findAncestorProviders(fiber);
752
+ // Push context values onto the stack in order (root to leaf)
753
+ for (const providerFiber of ancestorProviders) {
754
+ const contextId = providerFiber.type._contextId;
755
+ const contextValue = providerFiber.props.value;
756
+ (0, context_1.pushContextValue)(contextId, contextValue);
757
+ }
758
+ }
759
+ /**
760
+ * Find all ancestor context providers for a fiber
761
+ * Returns providers in order from root to the fiber's parent
762
+ *
763
+ * @param fiber - Fiber node to find ancestors for
764
+ * @returns Array of provider fibers (root to leaf order)
765
+ */
766
+ findAncestorProviders(fiber) {
767
+ if (!this.currentFiber) {
768
+ return [];
769
+ }
770
+ const providers = [];
771
+ this.findAncestorProvidersRecursive(this.currentFiber, fiber, providers, []);
772
+ return providers;
773
+ }
774
+ /**
775
+ * Recursively search for ancestor providers
776
+ *
777
+ * @param currentFiber - Current fiber being examined
778
+ * @param targetFiber - Target fiber we're looking for ancestors of
779
+ * @param providers - Array to collect providers
780
+ * @param path - Current path of provider fibers
781
+ * @returns True if targetFiber was found in this subtree
782
+ */
783
+ findAncestorProvidersRecursive(currentFiber, targetFiber, providers, path) {
784
+ // Check if we found the target by comparing paths (works with cloned fibers)
785
+ const currentPath = currentFiber.path?.join('.') || '';
786
+ const targetPath = targetFiber.path?.join('.') || '';
787
+ if (currentPath === targetPath) {
788
+ // Add all providers in the path
789
+ providers.push(...path);
790
+ return true;
791
+ }
792
+ // Check if this is a provider
793
+ const isProvider = typeof currentFiber.type === 'function' && currentFiber.type._isContextProvider;
794
+ // Search children
795
+ if (currentFiber.children) {
796
+ for (const child of currentFiber.children) {
797
+ // Add this fiber to path if it's a provider
798
+ const newPath = isProvider ? [...path, currentFiber] : path;
799
+ if (this.findAncestorProvidersRecursive(child, targetFiber, providers, newPath)) {
800
+ return true;
801
+ }
802
+ }
803
+ }
804
+ return false;
805
+ }
727
806
  /**
728
807
  * Find the root component from a set of components
729
808
  *
809
+ * CRITICAL FIX: Find common ancestor instead of just shortest path
810
+ * When multiple components have the same path length (e.g., multi-environment apps),
811
+ * we need to find their common ancestor, not just pick the first one.
812
+ *
730
813
  * @param components - Array of components
731
814
  * @returns Root component or null
732
815
  */
733
816
  findRootComponent(components) {
734
- // Find the component with the shortest path (closest to root)
735
- let rootComponent = null;
736
- let shortestPathLength = Infinity;
737
- for (const component of components) {
738
- if (component.path.length < shortestPathLength) {
739
- shortestPathLength = component.path.length;
740
- rootComponent = component;
817
+ if (components.length === 0)
818
+ return null;
819
+ if (components.length === 1) {
820
+ // Single component - traverse up to find actual root
821
+ return this.findActualRoot(components[0]);
822
+ }
823
+ // Find common ancestor by comparing paths
824
+ const paths = components.map(c => c.path || []);
825
+ const commonPath = this.findCommonPathPrefix(paths);
826
+ // Find the fiber at the common path
827
+ return this.findFiberByPath(this.currentFiber, commonPath);
828
+ }
829
+ /**
830
+ * Find common path prefix among multiple paths
831
+ *
832
+ * @param paths - Array of path arrays
833
+ * @returns Common prefix path
834
+ */
835
+ findCommonPathPrefix(paths) {
836
+ if (paths.length === 0)
837
+ return [];
838
+ const shortest = paths.reduce((a, b) => a.length <= b.length ? a : b);
839
+ const commonPath = [];
840
+ for (let i = 0; i < shortest.length; i++) {
841
+ const segment = shortest[i];
842
+ if (paths.every(path => path[i] === segment)) {
843
+ commonPath.push(segment);
844
+ }
845
+ else {
846
+ break;
847
+ }
848
+ }
849
+ return commonPath;
850
+ }
851
+ /**
852
+ * Find fiber by path in the fiber tree
853
+ *
854
+ * @param fiber - Root fiber to search from
855
+ * @param targetPath - Path to find
856
+ * @returns Fiber at path or null
857
+ */
858
+ findFiberByPath(fiber, targetPath) {
859
+ if (!fiber)
860
+ return null;
861
+ const fiberPath = fiber.path?.join('.') || '';
862
+ const target = targetPath.join('.');
863
+ if (fiberPath === target)
864
+ return fiber;
865
+ if (fiber.children) {
866
+ for (const child of fiber.children) {
867
+ const result = this.findFiberByPath(child, targetPath);
868
+ if (result)
869
+ return result;
870
+ }
871
+ }
872
+ return null;
873
+ }
874
+ /**
875
+ * Find actual root by traversing up the fiber tree
876
+ *
877
+ * @param fiber - Starting fiber
878
+ * @returns Root fiber
879
+ */
880
+ findActualRoot(fiber) {
881
+ // Traverse up to find the actual root
882
+ let current = fiber;
883
+ while (current.path && current.path.length > 1) {
884
+ // Try to find parent in currentFiber tree
885
+ const parentPath = current.path.slice(0, -1);
886
+ const parent = this.findFiberByPath(this.currentFiber, parentPath);
887
+ if (parent) {
888
+ current = parent;
889
+ }
890
+ else {
891
+ break;
741
892
  }
742
893
  }
743
- return rootComponent;
894
+ return current;
744
895
  }
745
896
  /**
746
897
  * Recursively find dependent components
@@ -785,6 +936,8 @@ class Renderer {
785
936
  reactiveState: fiber.reactiveState ? { ...fiber.reactiveState } : undefined,
786
937
  dependencies: fiber.dependencies ? new Set(fiber.dependencies) : undefined,
787
938
  dependents: fiber.dependents ? new Set(fiber.dependents) : undefined,
939
+ cloudDOMNodes: fiber.cloudDOMNodes ? [...fiber.cloudDOMNodes] : undefined,
940
+ effectBindings: fiber.effectBindings ? new Map(fiber.effectBindings) : undefined,
788
941
  };
789
942
  }
790
943
  }
@@ -72,6 +72,8 @@ export interface FiberNode {
72
72
  boundOutputs: string[];
73
73
  registeredAt: number;
74
74
  }>;
75
+ /** Context stack snapshot at the time this component was rendered */
76
+ contextSnapshot?: Map<symbol, any[]>;
75
77
  }
76
78
  /**
77
79
  * CloudDOM Event Callbacks - Lifecycle event handlers for resources
@@ -76,16 +76,21 @@ function getProviderOutputTrackerInstance() {
76
76
  * This node won't be included in CloudDOM but allows the component to continue rendering
77
77
  * All output accesses return undefined
78
78
  *
79
+ * Uses the same ID generation logic as real nodes for proper reconciliation tracking
80
+ *
79
81
  * @internal
80
82
  */
81
- function createPlaceholderNode() {
83
+ function createPlaceholderNode(construct, currentPath, key, currentFiber, props) {
84
+ // Generate ID using same logic as real nodes (use existing naming utilities)
85
+ const id = (0, naming_1.getNodeName)(construct, props, key);
86
+ const fullPath = [...currentPath, id];
87
+ const resourceId = (0, naming_1.generateResourceId)(fullPath);
82
88
  const placeholderNode = {
83
- id: '__placeholder__',
84
- path: ['__placeholder__'],
85
- construct: class Placeholder {
86
- },
87
- constructType: 'Placeholder',
88
- props: {},
89
+ id: resourceId,
90
+ path: fullPath,
91
+ construct,
92
+ constructType: construct.name || 'UnknownConstruct',
93
+ props: props,
89
94
  children: [],
90
95
  outputs: {},
91
96
  };
@@ -258,7 +263,8 @@ function useInstance(construct, props) {
258
263
  // Don't attach anything to fiber - this resource will be skipped entirely
259
264
  // Return a placeholder node that won't be included in CloudDOM
260
265
  // The proxy will return undefined for all output accesses
261
- return createPlaceholderNode();
266
+ // Use the same ID generation logic as real nodes for proper reconciliation tracking
267
+ return createPlaceholderNode(construct, currentPath, key, currentFiber, restProps);
262
268
  }
263
269
  // Remove undefined values from props to match JSON serialization behavior
264
270
  // When CloudDOM is saved to backend, JSON.stringify strips undefined values
@@ -284,35 +290,9 @@ function useInstance(construct, props) {
284
290
  if (typeof componentProps.onDestroy === 'function') {
285
291
  eventCallbacks.onDestroy = componentProps.onDestroy;
286
292
  }
287
- // Generate ID from key or construct type
288
- // Priority: explicit key from useInstance props > component fiber key > auto-generated
289
- let id;
290
- if (key !== undefined) {
291
- // Use explicit key if provided in useInstance props
292
- id = String(key);
293
- }
294
- else {
295
- // Auto-generate ID from construct type name (kebab-case)
296
- const constructName = (0, naming_1.toKebabCase)(construct.name);
297
- // Track call count for this construct type in this component
298
- if (!constructCallCounts.has(currentFiber)) {
299
- constructCallCounts.set(currentFiber, new Map());
300
- }
301
- const counts = constructCallCounts.get(currentFiber);
302
- const count = counts.get(construct) || 0;
303
- counts.set(construct, count + 1);
304
- // Append index if multiple calls with same type
305
- if (count > 0) {
306
- id = `${constructName}-${count}`;
307
- }
308
- else {
309
- id = constructName;
310
- }
311
- // If component has a key (from JSX), prepend it to make resources unique per component instance
312
- if (currentFiber.key) {
313
- id = `${currentFiber.key}-${id}`;
314
- }
315
- }
293
+ // Generate ID using getNodeName for consistency with placeholder nodes
294
+ // This ensures placeholder and real nodes have matching IDs for proper reconciliation
295
+ const id = (0, naming_1.getNodeName)(construct, restProps, key);
316
296
  // Generate full resource ID from current path
317
297
  // Example: ['registry', 'service'] + 'bucket' → 'registry.service.bucket'
318
298
  const fullPath = [...currentPath, id];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@creact-labs/creact",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CReact - React for Infrastructure. Render JSX to CloudDOM.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -84,4 +84,4 @@
84
84
  "ora": "^5.4.1",
85
85
  "tsx": "^4.20.6"
86
86
  }
87
- }
87
+ }