@creact-labs/creact 0.1.0 → 0.1.3
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 +7 -1
- package/dist/core/CReact.js +16 -11
- package/dist/core/CloudDOMBuilder.d.ts +21 -3
- package/dist/core/CloudDOMBuilder.js +58 -22
- package/dist/core/Renderer.d.ts +60 -0
- package/dist/core/Renderer.js +182 -29
- package/dist/core/types.d.ts +2 -0
- package/dist/hooks/useInstance.js +17 -37
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
NOTE: !!! THIS IS A EXPERIMENT OF THOUGHT NOT A PRODUCTION READY PRODUCT !!!
|
|
3
|
+
|
|
4
|
+
# CReact
|
|
5
|
+
|
|
6
|
+

|
|
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
|
|
package/dist/core/CReact.js
CHANGED
|
@@ -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
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
961
|
-
|
|
962
|
-
//
|
|
963
|
-
|
|
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);
|
package/dist/core/Renderer.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/core/Renderer.js
CHANGED
|
@@ -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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
|
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
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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:
|
|
84
|
-
path:
|
|
85
|
-
construct
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
288
|
-
//
|
|
289
|
-
|
|
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/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
export { CReact, JSXElement } from './jsx';
|
|
31
31
|
export type { FC, PropsWithChildren } from './jsx.d';
|
|
32
|
+
import './jsx.d';
|
|
32
33
|
import { CReact as CReactClass } from './core/CReact';
|
|
33
34
|
export { CReact as CReactCore, CReactConfig } from './core/CReact';
|
|
34
35
|
export declare const renderCloudDOM: typeof CReactClass.renderCloudDOM;
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,8 @@ exports.parseResourceId = exports.formatPath = exports.normalizePath = exports.n
|
|
|
35
35
|
// JSX support
|
|
36
36
|
var jsx_1 = require("./jsx");
|
|
37
37
|
Object.defineProperty(exports, "CReact", { enumerable: true, get: function () { return jsx_1.CReact; } });
|
|
38
|
+
// Re-export JSX type definitions for consumers
|
|
39
|
+
require("./jsx.d");
|
|
38
40
|
// Core classes
|
|
39
41
|
const CReact_1 = require("./core/CReact");
|
|
40
42
|
var CReact_2 = require("./core/CReact");
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@creact-labs/creact",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CReact - React for Infrastructure. Render JSX to CloudDOM.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"typesVersions": {
|
|
8
|
+
"*": {
|
|
9
|
+
"jsx": [
|
|
10
|
+
"dist/jsx.d.ts"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
7
14
|
"bin": {
|
|
8
15
|
"creact": "./dist/cli/index.js"
|
|
9
16
|
},
|
|
@@ -84,4 +91,4 @@
|
|
|
84
91
|
"ora": "^5.4.1",
|
|
85
92
|
"tsx": "^4.20.6"
|
|
86
93
|
}
|
|
87
|
-
}
|
|
94
|
+
}
|