@creact-labs/creact 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +212 -0
- package/README.md +379 -0
- package/dist/cli/commands/BuildCommand.d.ts +40 -0
- package/dist/cli/commands/BuildCommand.js +151 -0
- package/dist/cli/commands/DeployCommand.d.ts +38 -0
- package/dist/cli/commands/DeployCommand.js +194 -0
- package/dist/cli/commands/DevCommand.d.ts +52 -0
- package/dist/cli/commands/DevCommand.js +385 -0
- package/dist/cli/commands/PlanCommand.d.ts +39 -0
- package/dist/cli/commands/PlanCommand.js +164 -0
- package/dist/cli/commands/index.d.ts +36 -0
- package/dist/cli/commands/index.js +43 -0
- package/dist/cli/core/ArgumentParser.d.ts +46 -0
- package/dist/cli/core/ArgumentParser.js +127 -0
- package/dist/cli/core/BaseCommand.d.ts +75 -0
- package/dist/cli/core/BaseCommand.js +95 -0
- package/dist/cli/core/CLIContext.d.ts +68 -0
- package/dist/cli/core/CLIContext.js +183 -0
- package/dist/cli/core/CommandRegistry.d.ts +64 -0
- package/dist/cli/core/CommandRegistry.js +89 -0
- package/dist/cli/core/index.d.ts +36 -0
- package/dist/cli/core/index.js +43 -0
- package/dist/cli/index.d.ts +35 -0
- package/dist/cli/index.js +100 -0
- package/dist/cli/output.d.ts +204 -0
- package/dist/cli/output.js +437 -0
- package/dist/cli/utils.d.ts +59 -0
- package/dist/cli/utils.js +76 -0
- package/dist/context/createContext.d.ts +90 -0
- package/dist/context/createContext.js +113 -0
- package/dist/context/index.d.ts +30 -0
- package/dist/context/index.js +35 -0
- package/dist/core/CReact.d.ts +409 -0
- package/dist/core/CReact.js +1127 -0
- package/dist/core/CloudDOMBuilder.d.ts +429 -0
- package/dist/core/CloudDOMBuilder.js +1198 -0
- package/dist/core/ContextDependencyTracker.d.ts +165 -0
- package/dist/core/ContextDependencyTracker.js +448 -0
- package/dist/core/ErrorRecoveryManager.d.ts +145 -0
- package/dist/core/ErrorRecoveryManager.js +443 -0
- package/dist/core/EventBus.d.ts +91 -0
- package/dist/core/EventBus.js +185 -0
- package/dist/core/ProviderOutputTracker.d.ts +211 -0
- package/dist/core/ProviderOutputTracker.js +476 -0
- package/dist/core/ReactiveUpdateQueue.d.ts +76 -0
- package/dist/core/ReactiveUpdateQueue.js +121 -0
- package/dist/core/Reconciler.d.ts +415 -0
- package/dist/core/Reconciler.js +1037 -0
- package/dist/core/RenderScheduler.d.ts +153 -0
- package/dist/core/RenderScheduler.js +519 -0
- package/dist/core/Renderer.d.ts +276 -0
- package/dist/core/Renderer.js +791 -0
- package/dist/core/Runtime.d.ts +246 -0
- package/dist/core/Runtime.js +640 -0
- package/dist/core/StateBindingManager.d.ts +121 -0
- package/dist/core/StateBindingManager.js +309 -0
- package/dist/core/StateMachine.d.ts +424 -0
- package/dist/core/StateMachine.js +787 -0
- package/dist/core/StructuralChangeDetector.d.ts +140 -0
- package/dist/core/StructuralChangeDetector.js +363 -0
- package/dist/core/Validator.d.ts +127 -0
- package/dist/core/Validator.js +279 -0
- package/dist/core/errors.d.ts +153 -0
- package/dist/core/errors.js +202 -0
- package/dist/core/index.d.ts +38 -0
- package/dist/core/index.js +64 -0
- package/dist/core/types.d.ts +263 -0
- package/dist/core/types.js +48 -0
- package/dist/hooks/context.d.ts +147 -0
- package/dist/hooks/context.js +334 -0
- package/dist/hooks/useContext.d.ts +113 -0
- package/dist/hooks/useContext.js +169 -0
- package/dist/hooks/useEffect.d.ts +105 -0
- package/dist/hooks/useEffect.js +540 -0
- package/dist/hooks/useInstance.d.ts +139 -0
- package/dist/hooks/useInstance.js +441 -0
- package/dist/hooks/useState.d.ts +120 -0
- package/dist/hooks/useState.js +298 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +70 -0
- package/dist/jsx.d.ts +64 -0
- package/dist/jsx.js +76 -0
- package/dist/providers/DummyBackendProvider.d.ts +193 -0
- package/dist/providers/DummyBackendProvider.js +189 -0
- package/dist/providers/DummyCloudProvider.d.ts +128 -0
- package/dist/providers/DummyCloudProvider.js +157 -0
- package/dist/providers/IBackendProvider.d.ts +177 -0
- package/dist/providers/IBackendProvider.js +31 -0
- package/dist/providers/ICloudProvider.d.ts +146 -0
- package/dist/providers/ICloudProvider.js +31 -0
- package/dist/providers/index.d.ts +31 -0
- package/dist/providers/index.js +31 -0
- package/dist/test-event-callbacks.d.ts +0 -0
- package/dist/test-event-callbacks.js +1 -0
- package/dist/utils/Logger.d.ts +144 -0
- package/dist/utils/Logger.js +220 -0
- package/dist/utils/Output.d.ts +161 -0
- package/dist/utils/Output.js +401 -0
- package/dist/utils/deepEqual.d.ts +71 -0
- package/dist/utils/deepEqual.js +276 -0
- package/dist/utils/naming.d.ts +241 -0
- package/dist/utils/naming.js +376 -0
- package/package.json +87 -0
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
|
|
10
|
+
*
|
|
11
|
+
|
|
12
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
|
|
14
|
+
*
|
|
15
|
+
|
|
16
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
|
|
18
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
+
|
|
20
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
|
|
22
|
+
* See the License for the specific language governing permissions and
|
|
23
|
+
|
|
24
|
+
* limitations under the License.
|
|
25
|
+
|
|
26
|
+
*
|
|
27
|
+
|
|
28
|
+
* Copyright 2025 Daniel Coutinho Ribeiro
|
|
29
|
+
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.getCReactInstance = exports.CReact = void 0;
|
|
33
|
+
// REQ-01, REQ-04, REQ-05, REQ-07, REQ-09: CReact orchestrator - main class
|
|
34
|
+
// Orchestrates the entire pipeline: render → validate → build → deploy
|
|
35
|
+
const Renderer_1 = require("./Renderer");
|
|
36
|
+
const Validator_1 = require("./Validator");
|
|
37
|
+
const CloudDOMBuilder_1 = require("./CloudDOMBuilder");
|
|
38
|
+
const StateMachine_1 = require("./StateMachine");
|
|
39
|
+
const Reconciler_1 = require("./Reconciler");
|
|
40
|
+
const types_1 = require("./types");
|
|
41
|
+
const EventBus_1 = require("./EventBus");
|
|
42
|
+
const useInstance_1 = require("../hooks/useInstance");
|
|
43
|
+
const useState_1 = require("../hooks/useState");
|
|
44
|
+
const useContext_1 = require("../hooks/useContext");
|
|
45
|
+
const errors_1 = require("./errors");
|
|
46
|
+
const jsx_1 = require("../jsx");
|
|
47
|
+
const RenderScheduler_1 = require("./RenderScheduler");
|
|
48
|
+
const StateBindingManager_1 = require("./StateBindingManager");
|
|
49
|
+
const ProviderOutputTracker_1 = require("./ProviderOutputTracker");
|
|
50
|
+
const ContextDependencyTracker_1 = require("./ContextDependencyTracker");
|
|
51
|
+
const ErrorRecoveryManager_1 = require("./ErrorRecoveryManager");
|
|
52
|
+
const StructuralChangeDetector_1 = require("./StructuralChangeDetector");
|
|
53
|
+
const Logger_1 = require("../utils/Logger");
|
|
54
|
+
const logger = Logger_1.LoggerFactory.getLogger('runtime');
|
|
55
|
+
/**
|
|
56
|
+
* CReact orchestrator - main class
|
|
57
|
+
*
|
|
58
|
+
* Orchestrates the entire infrastructure-as-code pipeline:
|
|
59
|
+
* 1. Render JSX → Fiber tree
|
|
60
|
+
* 2. Validate Fiber tree
|
|
61
|
+
* 3. Build CloudDOM from Fiber
|
|
62
|
+
* 4. Compare CloudDOM trees (diff via Reconciler)
|
|
63
|
+
* 5. Deploy CloudDOM via StateMachine
|
|
64
|
+
*
|
|
65
|
+
* REQ-01: JSX → CloudDOM rendering
|
|
66
|
+
* REQ-04: Dependency injection pattern
|
|
67
|
+
* REQ-05: Deployment orchestration via StateMachine
|
|
68
|
+
* REQ-07: Validation before commit/deploy
|
|
69
|
+
* REQ-09: Provider lifecycle hooks
|
|
70
|
+
* REQ-O01: StateMachine handles all state management
|
|
71
|
+
*/
|
|
72
|
+
class CReact {
|
|
73
|
+
/**
|
|
74
|
+
* Constructor receives all dependencies via config (dependency injection)
|
|
75
|
+
*
|
|
76
|
+
* REQ-04: Providers are injected, not inherited
|
|
77
|
+
*
|
|
78
|
+
* @param config - Configuration with injected providers
|
|
79
|
+
*/
|
|
80
|
+
constructor(config) {
|
|
81
|
+
this.config = config;
|
|
82
|
+
this.lastFiberTree = null; // Store the last rendered Fiber tree for effects
|
|
83
|
+
// Reactive deployment tracking
|
|
84
|
+
this.hasReactiveChanges = false;
|
|
85
|
+
this.reactiveCloudDOM = null;
|
|
86
|
+
this.preReactiveCloudDOM = null; // CloudDOM before re-render
|
|
87
|
+
// Set global instance for hooks to access
|
|
88
|
+
CReact.globalInstance = this;
|
|
89
|
+
// Instantiate core components
|
|
90
|
+
this.renderer = new Renderer_1.Renderer();
|
|
91
|
+
this.validator = new Validator_1.Validator();
|
|
92
|
+
// Inject cloud provider into CloudDOMBuilder (REQ-04)
|
|
93
|
+
this.cloudDOMBuilder = new CloudDOMBuilder_1.CloudDOMBuilder(config.cloudProvider);
|
|
94
|
+
// Instantiate Reconciler for diff computation
|
|
95
|
+
this.reconciler = new Reconciler_1.Reconciler();
|
|
96
|
+
// Instantiate StateMachine for deployment orchestration (REQ-O01)
|
|
97
|
+
this.stateMachine = new StateMachine_1.StateMachine(config.backendProvider);
|
|
98
|
+
// Initialize reactive system components
|
|
99
|
+
this.renderScheduler = new RenderScheduler_1.RenderScheduler(config.eventHooks);
|
|
100
|
+
this.stateBindingManager = new StateBindingManager_1.StateBindingManager();
|
|
101
|
+
this.providerOutputTracker = new ProviderOutputTracker_1.ProviderOutputTracker(config.eventHooks);
|
|
102
|
+
this.contextDependencyTracker = new ContextDependencyTracker_1.ContextDependencyTracker(config.eventHooks);
|
|
103
|
+
this.errorRecoveryManager = new ErrorRecoveryManager_1.ErrorRecoveryManager(config.eventHooks);
|
|
104
|
+
this.structuralChangeDetector = new StructuralChangeDetector_1.StructuralChangeDetector(config.eventHooks);
|
|
105
|
+
// Wire up reactive components
|
|
106
|
+
this.renderer.setRenderScheduler(this.renderScheduler);
|
|
107
|
+
this.renderer.setContextDependencyTracker(this.contextDependencyTracker);
|
|
108
|
+
this.renderer.setStructuralChangeDetector(this.structuralChangeDetector);
|
|
109
|
+
this.cloudDOMBuilder.setReactiveComponents(this.stateBindingManager, this.providerOutputTracker);
|
|
110
|
+
this.contextDependencyTracker.setStateBindingManager(this.stateBindingManager);
|
|
111
|
+
// Set the context dependency tracker in useContext hook
|
|
112
|
+
(0, useContext_1.setContextDependencyTracker)(this.contextDependencyTracker);
|
|
113
|
+
// Set the state binding manager in useState hook
|
|
114
|
+
(0, useState_1.setStateBindingManager)(this.stateBindingManager);
|
|
115
|
+
// Set the provider output tracker in useInstance hook
|
|
116
|
+
(0, useInstance_1.setProviderOutputTracker)(this.providerOutputTracker);
|
|
117
|
+
// Subscribe to provider output change events (event-driven reactivity)
|
|
118
|
+
if (this.config.cloudProvider.on) {
|
|
119
|
+
this.config.cloudProvider.on('outputsChanged', (change) => {
|
|
120
|
+
this.handleProviderOutputChange(change).catch((error) => {
|
|
121
|
+
logger.error('Error handling provider output change:', error);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
logger.debug('Subscribed to provider output change events');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Debug logging helper
|
|
129
|
+
* Logs messages when CREACT_DEBUG environment variable is set
|
|
130
|
+
*
|
|
131
|
+
* @param message - Message to log
|
|
132
|
+
*/
|
|
133
|
+
log(message) {
|
|
134
|
+
logger.debug(message);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get the global CReact instance for hooks to access
|
|
138
|
+
* @internal
|
|
139
|
+
*/
|
|
140
|
+
static getCReactInstance() {
|
|
141
|
+
return CReact.globalInstance;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Schedule a component for re-rendering
|
|
145
|
+
* This is called by hooks when reactive state changes
|
|
146
|
+
*
|
|
147
|
+
* @param fiber - Fiber node to re-render
|
|
148
|
+
* @param reason - Reason for the re-render
|
|
149
|
+
*/
|
|
150
|
+
scheduleReRender(fiber, reason) {
|
|
151
|
+
this.log(`Scheduling re-render for ${fiber.path.join('.')} (reason: ${reason})`);
|
|
152
|
+
this.renderScheduler.schedule(fiber, reason);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Handle output change events from provider (event-driven reactivity)
|
|
156
|
+
* Called when provider emits 'outputsChanged' event
|
|
157
|
+
*
|
|
158
|
+
* This enables real-time reactivity without polling:
|
|
159
|
+
* 1. Update ProviderOutputTracker with new outputs
|
|
160
|
+
* 2. Execute useEffect callbacks bound to these outputs
|
|
161
|
+
* 3. Update bound state and enqueue affected fibers for re-render
|
|
162
|
+
*
|
|
163
|
+
* @param change - Output change event from provider
|
|
164
|
+
*/
|
|
165
|
+
async handleProviderOutputChange(change) {
|
|
166
|
+
if (!this.lastFiberTree) {
|
|
167
|
+
return; // No fiber tree to update
|
|
168
|
+
}
|
|
169
|
+
logger.debug(`Provider output changed: ${change.nodeId}`, change.outputs);
|
|
170
|
+
// Step 1: Update ProviderOutputTracker
|
|
171
|
+
const outputChanges = this.providerOutputTracker.updateInstanceOutputs(change.nodeId, change.outputs);
|
|
172
|
+
if (outputChanges.length === 0) {
|
|
173
|
+
return; // No actual changes detected
|
|
174
|
+
}
|
|
175
|
+
// Step 2: Execute useEffect callbacks bound to these outputs
|
|
176
|
+
// TODO: Implement executeEffectsOnOutputChange when useEffect is ready
|
|
177
|
+
// await executeEffectsOnOutputChange(this.lastFiberTree, outputChanges);
|
|
178
|
+
// Step 3: Update bound state and get affected fibers
|
|
179
|
+
const affectedFibers = this.stateBindingManager.processOutputChanges(outputChanges);
|
|
180
|
+
if (affectedFibers.length > 0) {
|
|
181
|
+
logger.debug(`Output change affected ${affectedFibers.length} fibers`);
|
|
182
|
+
// Schedule re-renders for affected components
|
|
183
|
+
affectedFibers.forEach((fiber) => {
|
|
184
|
+
this.scheduleReRender(fiber, 'output-update');
|
|
185
|
+
});
|
|
186
|
+
// Note: Actual re-render execution happens in the next deployment cycle
|
|
187
|
+
// or can be triggered immediately via renderScheduler.flushBatch()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Handle context value changes and trigger selective re-renders
|
|
192
|
+
* This is called when a context provider value changes
|
|
193
|
+
*
|
|
194
|
+
* @param contextId - Context identifier
|
|
195
|
+
* @param newValue - New context value
|
|
196
|
+
* @returns Promise resolving to affected fibers that were re-rendered
|
|
197
|
+
*/
|
|
198
|
+
async handleContextChange(contextId, newValue) {
|
|
199
|
+
this.log(`Context change detected for context: ${String(contextId)}`);
|
|
200
|
+
try {
|
|
201
|
+
// Update context value and get affected fibers
|
|
202
|
+
const affectedFibers = this.contextDependencyTracker.updateContextValue(contextId, newValue);
|
|
203
|
+
if (affectedFibers.length === 0) {
|
|
204
|
+
this.log('No components affected by context change');
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
this.log(`Context change affects ${affectedFibers.length} components`);
|
|
208
|
+
// Schedule re-renders for affected components
|
|
209
|
+
affectedFibers.forEach((fiber) => {
|
|
210
|
+
this.scheduleReRender(fiber, 'context-change');
|
|
211
|
+
});
|
|
212
|
+
// Execute the scheduled re-renders
|
|
213
|
+
const updatedFiber = this.renderer.reRenderComponents(affectedFibers, 'context-change');
|
|
214
|
+
// Update the last fiber tree
|
|
215
|
+
this.lastFiberTree = updatedFiber;
|
|
216
|
+
return affectedFibers;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
this.log(`Context change handling failed: ${error.message}`);
|
|
220
|
+
// Attempt rollback
|
|
221
|
+
const rollbackSuccess = this.contextDependencyTracker.rollbackContextValue(contextId);
|
|
222
|
+
if (rollbackSuccess) {
|
|
223
|
+
this.log('Context value rolled back successfully');
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Manual re-render trigger for CLI/testing
|
|
230
|
+
* Re-renders the entire stack or specific components
|
|
231
|
+
*
|
|
232
|
+
* @param stackName - Stack name to re-render (default: 'default')
|
|
233
|
+
* @param targetComponents - Optional specific components to re-render
|
|
234
|
+
* @returns Promise resolving to updated CloudDOM
|
|
235
|
+
*/
|
|
236
|
+
async rerender(stackName = 'default', targetComponents) {
|
|
237
|
+
this.log(`Manual re-render triggered for stack: ${stackName}`);
|
|
238
|
+
try {
|
|
239
|
+
// Get current state
|
|
240
|
+
const currentState = await this.stateMachine.getState(stackName);
|
|
241
|
+
if (!currentState?.cloudDOM) {
|
|
242
|
+
throw new Error(`No existing state found for stack: ${stackName}`);
|
|
243
|
+
}
|
|
244
|
+
// If no target components specified, re-render from last fiber tree
|
|
245
|
+
if (!targetComponents && this.lastFiberTree) {
|
|
246
|
+
this.log('Re-rendering entire stack from last fiber tree');
|
|
247
|
+
// Re-render the entire fiber tree
|
|
248
|
+
const updatedFiber = this.renderer.reRenderComponents([this.lastFiberTree], 'manual');
|
|
249
|
+
this.lastFiberTree = updatedFiber;
|
|
250
|
+
// Build updated CloudDOM
|
|
251
|
+
const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber);
|
|
252
|
+
// Sync outputs and trigger any additional re-renders
|
|
253
|
+
await this.cloudDOMBuilder.syncOutputsAndReRender(updatedFiber, updatedCloudDOM, currentState.cloudDOM);
|
|
254
|
+
return updatedCloudDOM;
|
|
255
|
+
}
|
|
256
|
+
// Re-render specific components
|
|
257
|
+
if (targetComponents && targetComponents.length > 0) {
|
|
258
|
+
this.log(`Re-rendering ${targetComponents.length} specific components`);
|
|
259
|
+
const updatedFiber = this.renderer.reRenderComponents(targetComponents, 'manual');
|
|
260
|
+
const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber);
|
|
261
|
+
return updatedCloudDOM;
|
|
262
|
+
}
|
|
263
|
+
throw new Error('No components available for re-rendering');
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
this.log(`Re-render failed: ${error.message}`);
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Build CloudDOM from JSX
|
|
272
|
+
*
|
|
273
|
+
* Pipeline: render → validate → build → restore outputs
|
|
274
|
+
*
|
|
275
|
+
* REQ-01: JSX → CloudDOM rendering
|
|
276
|
+
* REQ-07: Validate before commit
|
|
277
|
+
*
|
|
278
|
+
* @param jsx - JSX element to render
|
|
279
|
+
* @param stackName - Stack name for state restoration (default: 'default')
|
|
280
|
+
* @returns Promise resolving to CloudDOM tree
|
|
281
|
+
*/
|
|
282
|
+
async build(jsx, stackName = 'default') {
|
|
283
|
+
this.log('Starting build pipeline');
|
|
284
|
+
// Step 1: Load previous state to get outputs (via StateMachine)
|
|
285
|
+
this.log('Loading previous state');
|
|
286
|
+
const previousState = await this.stateMachine.getState(stackName);
|
|
287
|
+
// Step 2: If we have previous state, prepare hydration for useState AND inject outputs for useInstance
|
|
288
|
+
if (previousState?.cloudDOM) {
|
|
289
|
+
// CRITICAL: Prepare hydration BEFORE rendering so useState can restore values
|
|
290
|
+
this.log('Preparing hydration for useState');
|
|
291
|
+
this.prepareHydration(previousState.cloudDOM);
|
|
292
|
+
this.log('Injecting previous outputs into useInstance hook');
|
|
293
|
+
(0, useInstance_1.setPreviousOutputs)(this.buildOutputsMap(previousState.cloudDOM));
|
|
294
|
+
}
|
|
295
|
+
// Step 3: Render JSX → Fiber (with hydration and outputs available)
|
|
296
|
+
this.log('Rendering JSX to Fiber tree');
|
|
297
|
+
const fiber = this.renderer.render(jsx);
|
|
298
|
+
// Step 4: Store the Fiber tree for post-deployment effects
|
|
299
|
+
this.lastFiberTree = fiber;
|
|
300
|
+
// Step 5: Clear hydration and previous outputs after render
|
|
301
|
+
this.log('Clearing hydration data');
|
|
302
|
+
this.clearHydration();
|
|
303
|
+
(0, useInstance_1.setPreviousOutputs)(null);
|
|
304
|
+
// Step 6: Validate Fiber (REQ-07)
|
|
305
|
+
this.log('Validating Fiber tree');
|
|
306
|
+
this.validator.validate(fiber);
|
|
307
|
+
// Step 7: Build CloudDOM from Fiber (commit phase)
|
|
308
|
+
this.log('Building CloudDOM from Fiber');
|
|
309
|
+
const cloudDOM = await this.cloudDOMBuilder.build(fiber);
|
|
310
|
+
// Step 8: Detect structural changes if we have previous state
|
|
311
|
+
if (previousState?.cloudDOM) {
|
|
312
|
+
this.log('Detecting structural changes');
|
|
313
|
+
const structuralChanges = this.structuralChangeDetector.detectStructuralChanges(previousState.cloudDOM, cloudDOM, fiber);
|
|
314
|
+
if (structuralChanges.length > 0) {
|
|
315
|
+
this.log(`Detected ${structuralChanges.length} structural changes`);
|
|
316
|
+
// Trigger re-renders for affected components
|
|
317
|
+
this.structuralChangeDetector.triggerStructuralReRenders(structuralChanges, this.renderScheduler);
|
|
318
|
+
// Check if deployment plan needs updating
|
|
319
|
+
if (this.structuralChangeDetector.requiresDeploymentPlanUpdate(structuralChanges)) {
|
|
320
|
+
this.log('Structural changes require deployment plan update');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.log('Build pipeline complete');
|
|
325
|
+
return cloudDOM;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Build a map of node ID → outputs from previous CloudDOM
|
|
329
|
+
*
|
|
330
|
+
* @param previousCloudDOM - Previous CloudDOM state
|
|
331
|
+
* @returns Map of node ID → outputs
|
|
332
|
+
*/
|
|
333
|
+
buildOutputsMap(previousCloudDOM) {
|
|
334
|
+
const outputsMap = new Map();
|
|
335
|
+
const walk = (nodes) => {
|
|
336
|
+
for (const node of nodes) {
|
|
337
|
+
if (node.outputs && Object.keys(node.outputs).length > 0) {
|
|
338
|
+
outputsMap.set(node.id, node.outputs);
|
|
339
|
+
}
|
|
340
|
+
if (node.children && node.children.length > 0) {
|
|
341
|
+
walk(node.children);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
walk(previousCloudDOM);
|
|
346
|
+
return outputsMap;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Build CloudDOM with error handling for CLI/CI environments
|
|
350
|
+
*
|
|
351
|
+
* Provides a safer entrypoint that handles errors gracefully without
|
|
352
|
+
* crashing the entire process. Useful for CI/CD pipelines.
|
|
353
|
+
*
|
|
354
|
+
* @param jsx - JSX element to render
|
|
355
|
+
* @returns Promise resolving to CloudDOM tree, or empty array on error
|
|
356
|
+
*/
|
|
357
|
+
async buildSafe(jsx) {
|
|
358
|
+
try {
|
|
359
|
+
return await this.build(jsx);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
logger.error('Build failed:', error);
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Compare two CloudDOM trees and return diff
|
|
368
|
+
*
|
|
369
|
+
* REQ-05: Reconciliation and diff
|
|
370
|
+
* REQ-07.6: Validate before comparing
|
|
371
|
+
*
|
|
372
|
+
* @param previous - Previous CloudDOM tree
|
|
373
|
+
* @param current - Current CloudDOM tree
|
|
374
|
+
* @returns ChangeSet with creates, updates, deletes, and deployment order
|
|
375
|
+
*/
|
|
376
|
+
async compare(previous, current) {
|
|
377
|
+
// REQ-07.6: Validate before comparing
|
|
378
|
+
const currentFiber = this.renderer.getCurrentFiber();
|
|
379
|
+
if (currentFiber) {
|
|
380
|
+
this.validator.validate(currentFiber);
|
|
381
|
+
}
|
|
382
|
+
// Use Reconciler to compute diff
|
|
383
|
+
this.log('Computing diff between previous and current CloudDOM');
|
|
384
|
+
return this.reconciler.reconcile(previous, current);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get reactive deployment information if changes are pending
|
|
388
|
+
* Returns null if no reactive changes detected
|
|
389
|
+
*
|
|
390
|
+
* This encapsulates the logic for checking reactive changes and computing diffs,
|
|
391
|
+
* keeping separation of concerns between core and CLI layers.
|
|
392
|
+
*
|
|
393
|
+
* @param stackName - Stack name to compute diff against
|
|
394
|
+
* @returns Reactive deployment info with CloudDOM and ChangeSet, or null
|
|
395
|
+
*/
|
|
396
|
+
async getReactiveDeploymentInfo(stackName) {
|
|
397
|
+
if (!this.hasReactiveChanges || !this.reactiveCloudDOM || !this.preReactiveCloudDOM) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
// Compute diff between pre-reactive and post-reactive CloudDOM
|
|
401
|
+
// This shows what NEW resources were created by the re-render
|
|
402
|
+
const changeSet = this.reconciler.reconcile(this.preReactiveCloudDOM, this.reactiveCloudDOM);
|
|
403
|
+
return {
|
|
404
|
+
cloudDOM: this.reactiveCloudDOM,
|
|
405
|
+
changeSet,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Clear reactive changes flag (called after deployment)
|
|
410
|
+
*/
|
|
411
|
+
clearReactiveChanges() {
|
|
412
|
+
this.hasReactiveChanges = false;
|
|
413
|
+
this.reactiveCloudDOM = null;
|
|
414
|
+
this.preReactiveCloudDOM = null;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Deploy CloudDOM to cloud provider using StateMachine
|
|
418
|
+
*
|
|
419
|
+
* Pipeline: validate → compute diff → start deployment → materialize → checkpoint → complete
|
|
420
|
+
*
|
|
421
|
+
* REQ-05: Deployment orchestration via StateMachine
|
|
422
|
+
* REQ-05.4: Idempotent deployment (via Reconciler diff)
|
|
423
|
+
* REQ-07.6: Validate before deploying
|
|
424
|
+
* REQ-09: Provider lifecycle hooks
|
|
425
|
+
* REQ-O01: StateMachine handles all state management
|
|
426
|
+
*
|
|
427
|
+
* @param cloudDOM - CloudDOM tree to deploy
|
|
428
|
+
* @param stackName - Stack name for state management (default: 'default')
|
|
429
|
+
* @param user - User initiating deployment (default: 'system')
|
|
430
|
+
*/
|
|
431
|
+
async deploy(cloudDOM, stackName = 'default', user = 'system') {
|
|
432
|
+
// Clear reactive changes flag at start of deployment
|
|
433
|
+
this.clearReactiveChanges();
|
|
434
|
+
// REQ-07.6: Validate before deploying
|
|
435
|
+
const currentFiber = this.renderer.getCurrentFiber();
|
|
436
|
+
if (currentFiber) {
|
|
437
|
+
this.validator.validate(currentFiber);
|
|
438
|
+
}
|
|
439
|
+
// REQ-05.4: Compute diff for idempotent deployment
|
|
440
|
+
this.log('Computing diff for idempotent deployment');
|
|
441
|
+
const previousState = await this.stateMachine.getState(stackName);
|
|
442
|
+
const previousCloudDOM = previousState?.cloudDOM || [];
|
|
443
|
+
const changeSet = this.reconciler.reconcile(previousCloudDOM, cloudDOM);
|
|
444
|
+
// Use single source of truth for checking changes
|
|
445
|
+
if (!(0, Reconciler_1.hasChanges)(changeSet)) {
|
|
446
|
+
this.log('No changes detected. Deployment skipped.');
|
|
447
|
+
this.log('No resources to deploy');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
this.log(`Changes detected: ${changeSet.creates.length} creates, ${changeSet.updates.length} updates, ${changeSet.deletes.length} deletes`);
|
|
451
|
+
this.log(`Deployment order: ${changeSet.deploymentOrder.length} resources`);
|
|
452
|
+
try {
|
|
453
|
+
// Start deployment via StateMachine
|
|
454
|
+
this.log('Starting deployment via StateMachine');
|
|
455
|
+
await this.stateMachine.startDeployment(stackName, changeSet, cloudDOM, user);
|
|
456
|
+
// REQ-09.1: Lifecycle hook - preDeploy
|
|
457
|
+
if (this.config.cloudProvider.preDeploy) {
|
|
458
|
+
this.log('Calling preDeploy lifecycle hook');
|
|
459
|
+
await this.config.cloudProvider.preDeploy(cloudDOM);
|
|
460
|
+
}
|
|
461
|
+
// Process deletes first (before creates/updates to avoid conflicts)
|
|
462
|
+
if (changeSet.deletes.length > 0) {
|
|
463
|
+
this.log(`Processing ${changeSet.deletes.length} deletes`);
|
|
464
|
+
for (const deleteNode of changeSet.deletes) {
|
|
465
|
+
this.log(`Deleting resource: ${deleteNode.id}`);
|
|
466
|
+
// Trigger onDestroy event callback for this resource
|
|
467
|
+
await EventBus_1.CloudDOMEventBus.triggerEventCallbacks(deleteNode, 'destroy');
|
|
468
|
+
// Note: Actual deletion would be handled by the cloud provider
|
|
469
|
+
// For now, we just trigger the callback
|
|
470
|
+
// TODO: Add actual deletion logic when provider supports it
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Deploy resources in order with checkpoints
|
|
474
|
+
// Only deploy resources that have changes (creates, updates, replacements)
|
|
475
|
+
const filteredDeploymentOrder = (0, types_1.getResourcesToDeploy)(changeSet);
|
|
476
|
+
this.log('Deploying resources with checkpoints');
|
|
477
|
+
this.log(`Total resources: ${changeSet.deploymentOrder.length}, Resources to deploy: ${filteredDeploymentOrder.length}`);
|
|
478
|
+
for (let i = 0; i < filteredDeploymentOrder.length; i++) {
|
|
479
|
+
const resourceId = filteredDeploymentOrder[i];
|
|
480
|
+
this.log(`Deploying resource ${i + 1}/${filteredDeploymentOrder.length}: ${resourceId}`);
|
|
481
|
+
// Find the resource node
|
|
482
|
+
const resourceNode = this.findNodeById(cloudDOM, resourceId);
|
|
483
|
+
if (!resourceNode) {
|
|
484
|
+
throw new errors_1.DeploymentError(`Resource not found: ${resourceId}`, {
|
|
485
|
+
message: `Resource not found: ${resourceId}`,
|
|
486
|
+
code: 'RESOURCE_NOT_FOUND',
|
|
487
|
+
details: { resourceId, stackName },
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
// Materialize single resource
|
|
491
|
+
await this.config.cloudProvider.materialize([resourceNode], null);
|
|
492
|
+
// Trigger onDeploy event callback for this resource
|
|
493
|
+
await EventBus_1.CloudDOMEventBus.triggerEventCallbacks(resourceNode, 'deploy');
|
|
494
|
+
// Update checkpoint after successful deployment
|
|
495
|
+
await this.stateMachine.updateCheckpoint(stackName, i);
|
|
496
|
+
}
|
|
497
|
+
// Collect outputs from materialization (REQ-02, REQ-06)
|
|
498
|
+
this.log('Extracting outputs from CloudDOM');
|
|
499
|
+
const outputs = this.extractOutputs(cloudDOM);
|
|
500
|
+
// REQ-09.2: Lifecycle hook - postDeploy
|
|
501
|
+
if (this.config.cloudProvider.postDeploy) {
|
|
502
|
+
this.log('Calling postDeploy lifecycle hook');
|
|
503
|
+
await this.config.cloudProvider.postDeploy(cloudDOM, outputs);
|
|
504
|
+
}
|
|
505
|
+
// Complete deployment via StateMachine
|
|
506
|
+
this.log('Completing deployment via StateMachine');
|
|
507
|
+
await this.stateMachine.completeDeployment(stackName);
|
|
508
|
+
// Note: Previously marked initial build as complete, but now reactive system
|
|
509
|
+
// works throughout the entire lifecycle thanks to improved CloudDOMBuilder
|
|
510
|
+
// Execute post-deployment effects with reactive output synchronization
|
|
511
|
+
this.log('Executing post-deployment effects with reactive sync');
|
|
512
|
+
if (this.lastFiberTree) {
|
|
513
|
+
this.log('Executing post-deployment effects...');
|
|
514
|
+
// Integrate with post-deployment effects and output sync
|
|
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;
|
|
521
|
+
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'})`);
|
|
526
|
+
// REQ-7.4, 7.5: Schedule re-renders with proper batching and deduplication
|
|
527
|
+
fibersToReRender.forEach((fiber) => {
|
|
528
|
+
this.scheduleReRender(fiber, 'output-update');
|
|
529
|
+
});
|
|
530
|
+
// CRITICAL: Update previousOutputsMap with current CloudDOM outputs
|
|
531
|
+
// This allows useInstance to access outputs during re-render
|
|
532
|
+
const outputsMap = this.buildOutputsMap(cloudDOM);
|
|
533
|
+
logger.debug('Updating previousOutputsMap for re-render with latest outputs');
|
|
534
|
+
logger.debug(`OutputsMap has ${outputsMap.size} entries:`, Array.from(outputsMap.keys()));
|
|
535
|
+
(0, useInstance_1.setPreviousOutputs)(outputsMap);
|
|
536
|
+
// Execute the scheduled re-renders
|
|
537
|
+
const updatedFiber = this.renderer.reRenderComponents(fibersToReRender, 'output-update');
|
|
538
|
+
// Build updated CloudDOM from re-rendered components
|
|
539
|
+
const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber);
|
|
540
|
+
// Check if the re-render produced new resources to deploy
|
|
541
|
+
const reactiveChangeSet = this.reconciler.reconcile(cloudDOM, updatedCloudDOM);
|
|
542
|
+
if ((0, Reconciler_1.hasChanges)(reactiveChangeSet)) {
|
|
543
|
+
this.log(`Re-render produced new changes: ${reactiveChangeSet.creates.length} creates, ${reactiveChangeSet.updates.length} updates`);
|
|
544
|
+
this.log('Reactive changes detected - will need another deployment cycle');
|
|
545
|
+
// Store the CloudDOM BEFORE re-render for diff comparison
|
|
546
|
+
this.preReactiveCloudDOM = JSON.parse(JSON.stringify(cloudDOM));
|
|
547
|
+
// Update the CloudDOM with reactive changes
|
|
548
|
+
cloudDOM.splice(0, cloudDOM.length, ...updatedCloudDOM);
|
|
549
|
+
// Store flag indicating reactive changes need deployment
|
|
550
|
+
this.hasReactiveChanges = true;
|
|
551
|
+
this.reactiveCloudDOM = updatedCloudDOM;
|
|
552
|
+
this.log('Post-deployment effects and reactive sync completed');
|
|
553
|
+
this.log('Deployment complete (reactive changes pending)');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
this.log('Re-render produced no new changes');
|
|
558
|
+
// Update the stored CloudDOM with reactive changes
|
|
559
|
+
cloudDOM.splice(0, cloudDOM.length, ...updatedCloudDOM);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
logger.debug(`No output or state changes detected, skipping re-render`);
|
|
564
|
+
}
|
|
565
|
+
// Save the updated CloudDOM state with new outputs
|
|
566
|
+
logger.debug('Saving updated state with new outputs...');
|
|
567
|
+
await this.stateMachine.updateCloudDOM(stackName, cloudDOM);
|
|
568
|
+
this.log('Post-deployment effects and reactive sync completed');
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
logger.warn('No fiber tree found for effects execution');
|
|
572
|
+
}
|
|
573
|
+
this.log('Deployment complete');
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
logger.error('Deployment failed:', error);
|
|
577
|
+
// NEW (Gap 3): Attempt error recovery before failing
|
|
578
|
+
if (this.lastFiberTree) {
|
|
579
|
+
try {
|
|
580
|
+
logger.info('Attempting error recovery...');
|
|
581
|
+
// Create snapshot for potential rollback
|
|
582
|
+
this.errorRecoveryManager.createComponentSnapshot(this.lastFiberTree);
|
|
583
|
+
// Attempt recovery using re-render error handler
|
|
584
|
+
const recoveryResult = await this.errorRecoveryManager.handleReRenderError(error, [this.lastFiberTree], 'manual');
|
|
585
|
+
if (recoveryResult.success) {
|
|
586
|
+
logger.info(`Error recovery succeeded: ${recoveryResult.message}`);
|
|
587
|
+
// Recovery succeeded - return partial result instead of throwing
|
|
588
|
+
// This allows deployment to complete with recovered state
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
logger.warn(`Error recovery failed: ${recoveryResult.message}`);
|
|
592
|
+
}
|
|
593
|
+
catch (recoveryError) {
|
|
594
|
+
logger.error('Error during recovery attempt:', recoveryError);
|
|
595
|
+
// Continue with normal error handling
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Trigger onError event callbacks for all resources
|
|
599
|
+
await EventBus_1.CloudDOMEventBus.triggerEventCallbacksRecursive(cloudDOM, 'error', error);
|
|
600
|
+
// REQ-09.3: Lifecycle hook - onError
|
|
601
|
+
if (this.config.cloudProvider.onError) {
|
|
602
|
+
this.log('Calling onError lifecycle hook');
|
|
603
|
+
await this.config.cloudProvider.onError(error, cloudDOM);
|
|
604
|
+
}
|
|
605
|
+
// Mark deployment as failed via StateMachine
|
|
606
|
+
this.log('Marking deployment as failed via StateMachine');
|
|
607
|
+
const deploymentError = error instanceof errors_1.DeploymentError
|
|
608
|
+
? error
|
|
609
|
+
: new errors_1.DeploymentError(error.message, {
|
|
610
|
+
message: error.message,
|
|
611
|
+
code: 'DEPLOYMENT_FAILED',
|
|
612
|
+
stack: error.stack,
|
|
613
|
+
});
|
|
614
|
+
await this.stateMachine.failDeployment(stackName, deploymentError);
|
|
615
|
+
// Re-throw error to halt deployment (REQ-09.4)
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Hydration map is now STATIC (see declaration above with globalInstance)
|
|
620
|
+
/**
|
|
621
|
+
* Load previous state from backend and prepare for hydration
|
|
622
|
+
* This should be called before rendering to restore persisted state
|
|
623
|
+
*
|
|
624
|
+
* @param stackName - Stack name to load state for
|
|
625
|
+
*/
|
|
626
|
+
async loadStateForHydration(stackName) {
|
|
627
|
+
try {
|
|
628
|
+
const previousState = await this.stateMachine.getState(stackName);
|
|
629
|
+
if (previousState && previousState.cloudDOM) {
|
|
630
|
+
this.prepareHydration(previousState.cloudDOM);
|
|
631
|
+
this.log(`Loaded state for hydration from backend: ${stackName}`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
this.log(`No previous state found for stack: ${stackName}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
logger.warn(`Failed to load state for hydration: ${error.message}`);
|
|
639
|
+
// Continue without hydration - this is not a fatal error
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Prepare hydration data from previous CloudDOM
|
|
644
|
+
* This must be called BEFORE rendering to make state available to useState
|
|
645
|
+
*
|
|
646
|
+
* CRITICAL: State outputs belong to the COMPONENT that called useState,
|
|
647
|
+
* not the CloudDOM node. We need to key by component path (parent of node).
|
|
648
|
+
*
|
|
649
|
+
* Example:
|
|
650
|
+
* - Component path: ['web-app-stack']
|
|
651
|
+
* - CloudDOM node paths: ['web-app-stack', 'vpc'], ['web-app-stack', 'database']
|
|
652
|
+
* - All nodes from same component share the same state outputs
|
|
653
|
+
* - Hydration should be keyed by 'web-app-stack', not 'web-app-stack.vpc'
|
|
654
|
+
*
|
|
655
|
+
* @param previousCloudDOM - Previous CloudDOM with persisted state outputs
|
|
656
|
+
* @param clearExisting - Whether to clear existing hydration data (default: false)
|
|
657
|
+
*/
|
|
658
|
+
prepareHydration(previousCloudDOM, clearExisting = false) {
|
|
659
|
+
if (clearExisting) {
|
|
660
|
+
CReact.hydrationMap.clear();
|
|
661
|
+
}
|
|
662
|
+
if (!previousCloudDOM || previousCloudDOM.length === 0) {
|
|
663
|
+
this.log('No previous state to prepare for hydration');
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// Extract state outputs from previous CloudDOM and build hydration map
|
|
667
|
+
const extractStateOutputs = (nodes) => {
|
|
668
|
+
for (const node of nodes) {
|
|
669
|
+
// CRITICAL FIX: Extract component path (parent of CloudDOM node)
|
|
670
|
+
// node.path = ['web-app-stack', 'vpc']
|
|
671
|
+
// componentPath = 'web-app-stack' (where useState was called)
|
|
672
|
+
const componentPath = node.path.length > 1
|
|
673
|
+
? node.path.slice(0, -1).join('.') // Normal case: parent component
|
|
674
|
+
: node.path.join('.'); // Edge case: root node
|
|
675
|
+
// Validate path
|
|
676
|
+
if (!componentPath) {
|
|
677
|
+
logger.warn(`Invalid component path for hydration:`, node.path);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
// Read from state field instead of filtering state.* from outputs
|
|
681
|
+
if (node.state && Object.keys(node.state).length > 0) {
|
|
682
|
+
// Convert state object to array: { state1: "a", state2: "b" } → ["a", "b"]
|
|
683
|
+
// Ensure keys are sorted (state1, state2, state3...) before converting to array
|
|
684
|
+
const stateValues = Object.keys(node.state)
|
|
685
|
+
.sort() // Sort to ensure state1, state2, state3... order
|
|
686
|
+
.map((key) => node.state[key]);
|
|
687
|
+
if (stateValues.length > 0) {
|
|
688
|
+
// Only set if not already set (first node from component wins)
|
|
689
|
+
if (!CReact.hydrationMap.has(componentPath)) {
|
|
690
|
+
CReact.hydrationMap.set(componentPath, stateValues);
|
|
691
|
+
logger.debug(`Prepared hydration for component "${componentPath}" (from node "${node.path.join('.')}"): ${JSON.stringify(stateValues)}`);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
logger.debug(`Skipping duplicate hydration for component "${componentPath}" (already set from another node)`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (node.children && node.children.length > 0) {
|
|
699
|
+
extractStateOutputs(node.children);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
extractStateOutputs(previousCloudDOM);
|
|
704
|
+
logger.debug(`Hydration prepared: ${CReact.hydrationMap.size} components with state`);
|
|
705
|
+
logger.debug(`Hydration map keys:`, Array.from(CReact.hydrationMap.keys()));
|
|
706
|
+
logger.debug(`This instance is global: ${this === CReact.globalInstance}`);
|
|
707
|
+
logger.debug(`Full hydration map:`, Object.fromEntries(CReact.hydrationMap));
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Get hydrated value for a specific fiber and hook index
|
|
711
|
+
* Called by useState during render to check for persisted state
|
|
712
|
+
*
|
|
713
|
+
* @param fiberPath - Path of the fiber
|
|
714
|
+
* @param hookIndex - Index of the hook
|
|
715
|
+
* @returns Hydrated value or undefined if not found
|
|
716
|
+
*/
|
|
717
|
+
getHydratedValue(fiberPath, hookIndex) {
|
|
718
|
+
const values = CReact.hydrationMap.get(fiberPath);
|
|
719
|
+
if (values && hookIndex < values.length) {
|
|
720
|
+
return values[hookIndex];
|
|
721
|
+
}
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get hydrated value for a component by searching for any child node
|
|
726
|
+
* This handles the case where useState is in a parent component but state
|
|
727
|
+
* outputs are stored on child CloudDOM nodes
|
|
728
|
+
*
|
|
729
|
+
* With the path mapping fix, this should now find exact matches.
|
|
730
|
+
* The child node search is kept as a fallback for edge cases.
|
|
731
|
+
*
|
|
732
|
+
* @param componentPath - Path of the component (e.g., 'web-app-stack')
|
|
733
|
+
* @param hookIndex - Index of the hook
|
|
734
|
+
* @returns Hydrated value or undefined if not found
|
|
735
|
+
*/
|
|
736
|
+
getHydratedValueForComponent(componentPath, hookIndex) {
|
|
737
|
+
logger.debug(`getHydratedValueForComponent: path="${componentPath}", hookIndex=${hookIndex}`);
|
|
738
|
+
logger.debug(`Available hydration keys:`, Array.from(CReact.hydrationMap.keys()));
|
|
739
|
+
// Try exact match first (should work now with path mapping fix)
|
|
740
|
+
const exactMatch = this.getHydratedValue(componentPath, hookIndex);
|
|
741
|
+
if (exactMatch !== undefined) {
|
|
742
|
+
logger.debug(`✅ Found exact match for "${componentPath}"[${hookIndex}]:`, exactMatch);
|
|
743
|
+
return exactMatch;
|
|
744
|
+
}
|
|
745
|
+
// Fallback: Search for any child node that starts with this component path
|
|
746
|
+
// This handles edge cases where path mapping might not work as expected
|
|
747
|
+
for (const [nodePath, values] of CReact.hydrationMap.entries()) {
|
|
748
|
+
if (nodePath.startsWith(componentPath + '.') && hookIndex < values.length) {
|
|
749
|
+
logger.debug(`✅ Found hydration in child node: ${nodePath} for component: ${componentPath}`);
|
|
750
|
+
return values[hookIndex];
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
// No match found
|
|
754
|
+
logger.debug(`❌ No hydration found for "${componentPath}"[${hookIndex}]`);
|
|
755
|
+
return undefined;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Check if hydration data is available
|
|
759
|
+
*
|
|
760
|
+
* @returns True if hydration map has data
|
|
761
|
+
*/
|
|
762
|
+
hasHydrationData() {
|
|
763
|
+
const hasData = CReact.hydrationMap.size > 0;
|
|
764
|
+
logger.debug(`hasHydrationData: size=${CReact.hydrationMap.size}, hasData=${hasData}, instance=${this === CReact.globalInstance ? 'GLOBAL' : 'OTHER'}`);
|
|
765
|
+
return hasData;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Get hydration map keys for debugging
|
|
769
|
+
*
|
|
770
|
+
* @returns Array of component paths with hydration data
|
|
771
|
+
*/
|
|
772
|
+
getHydrationMapKeys() {
|
|
773
|
+
return Array.from(CReact.hydrationMap.keys());
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Clear hydration data after rendering is complete
|
|
777
|
+
*/
|
|
778
|
+
clearHydration() {
|
|
779
|
+
logger.debug(`clearHydration: Clearing ${CReact.hydrationMap.size} entries`);
|
|
780
|
+
logger.debug('clearHydration: Stack trace:', new Error().stack);
|
|
781
|
+
CReact.hydrationMap.clear();
|
|
782
|
+
this.log('Hydration data cleared');
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Serialize internal reactive state for hot reload preservation
|
|
786
|
+
* This captures the state of reactive systems that need to survive recompilation
|
|
787
|
+
*
|
|
788
|
+
* @returns Serialized state object
|
|
789
|
+
*/
|
|
790
|
+
serializeReactiveState() {
|
|
791
|
+
try {
|
|
792
|
+
const state = {
|
|
793
|
+
timestamp: Date.now(),
|
|
794
|
+
};
|
|
795
|
+
// Note: Most reactive state is already persisted in CloudDOM outputs
|
|
796
|
+
// This is mainly for tracking metadata and failure statistics
|
|
797
|
+
if (this.renderScheduler) {
|
|
798
|
+
// Preserve failure statistics but not pending renders
|
|
799
|
+
state.renderSchedulerStats = {
|
|
800
|
+
// Add any failure stats if the scheduler tracks them
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
return state;
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
logger.warn('Failed to serialize reactive state:', error);
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Restore internal reactive state after hot reload recompilation
|
|
812
|
+
* This restores the state of reactive systems from serialized data
|
|
813
|
+
*
|
|
814
|
+
* @param serializedState - Previously serialized state
|
|
815
|
+
*/
|
|
816
|
+
restoreReactiveState(serializedState) {
|
|
817
|
+
if (!serializedState) {
|
|
818
|
+
this.log('No serialized state to restore');
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
// Clear any pending renders from the previous session
|
|
823
|
+
if (this.renderScheduler && this.renderScheduler.clearPending) {
|
|
824
|
+
this.renderScheduler.clearPending();
|
|
825
|
+
}
|
|
826
|
+
this.log('Reactive state restored from hot reload');
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
logger.warn('Failed to restore reactive state:', error);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Hydrate the current fiber tree with state values from previous CloudDOM
|
|
834
|
+
* This is used during hot reload to restore persisted state values
|
|
835
|
+
*
|
|
836
|
+
* @param previousCloudDOM - Previous CloudDOM with persisted state outputs
|
|
837
|
+
*/
|
|
838
|
+
hydrateStateFromPreviousCloudDOM(previousCloudDOM) {
|
|
839
|
+
if (!this.lastFiberTree || !previousCloudDOM || previousCloudDOM.length === 0) {
|
|
840
|
+
this.log('No previous state to hydrate');
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
// Extract state outputs from previous CloudDOM
|
|
844
|
+
const stateOutputs = new Map();
|
|
845
|
+
const extractStateOutputs = (nodes) => {
|
|
846
|
+
for (const node of nodes) {
|
|
847
|
+
// Read from state field instead of filtering state.* from outputs
|
|
848
|
+
if (node.state && Object.keys(node.state).length > 0) {
|
|
849
|
+
// Convert state object to array: { state1: "a", state2: "b" } → ["a", "b"]
|
|
850
|
+
// Ensure keys are sorted (state1, state2, state3...) before converting to array
|
|
851
|
+
const stateValues = Object.keys(node.state)
|
|
852
|
+
.sort() // Sort to ensure state1, state2, state3... order
|
|
853
|
+
.map((key) => node.state[key]);
|
|
854
|
+
if (stateValues.length > 0) {
|
|
855
|
+
stateOutputs.set(node.id, stateValues);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (node.children && node.children.length > 0) {
|
|
859
|
+
extractStateOutputs(node.children);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
extractStateOutputs(previousCloudDOM);
|
|
864
|
+
if (stateOutputs.size === 0) {
|
|
865
|
+
this.log('No state outputs found in previous CloudDOM');
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// Hydrate fiber tree with state values
|
|
869
|
+
const hydrateFiber = (fiber) => {
|
|
870
|
+
// Match fiber to CloudDOM node by path
|
|
871
|
+
if (fiber.cloudDOMNodes && Array.isArray(fiber.cloudDOMNodes)) {
|
|
872
|
+
for (const node of fiber.cloudDOMNodes) {
|
|
873
|
+
const savedState = stateOutputs.get(node.id);
|
|
874
|
+
if (savedState && savedState.length > 0) {
|
|
875
|
+
// Initialize hooks array if not present
|
|
876
|
+
if (!fiber.hooks) {
|
|
877
|
+
fiber.hooks = [];
|
|
878
|
+
}
|
|
879
|
+
// Restore state values
|
|
880
|
+
for (let i = 0; i < savedState.length; i++) {
|
|
881
|
+
fiber.hooks[i] = savedState[i];
|
|
882
|
+
}
|
|
883
|
+
this.log(`Hydrated ${savedState.length} state values for ${node.id}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// Recursively hydrate children
|
|
888
|
+
if (fiber.children && fiber.children.length > 0) {
|
|
889
|
+
for (const child of fiber.children) {
|
|
890
|
+
hydrateFiber(child);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
hydrateFiber(this.lastFiberTree);
|
|
895
|
+
this.log(`State hydration complete: ${stateOutputs.size} nodes hydrated`);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Check if outputs actually changed between previous and current CloudDOM
|
|
899
|
+
* REQ-7.1, 7.2, 7.3: Proper output change detection including undefined → value transitions
|
|
900
|
+
*
|
|
901
|
+
* @param previous - Previous CloudDOM state
|
|
902
|
+
* @param current - Current CloudDOM state
|
|
903
|
+
* @returns True if any outputs actually changed
|
|
904
|
+
*/
|
|
905
|
+
hasActualOutputChanges(previous, current) {
|
|
906
|
+
// Build maps for efficient comparison
|
|
907
|
+
const previousMap = new Map();
|
|
908
|
+
const currentMap = new Map();
|
|
909
|
+
const buildMap = (nodes, map) => {
|
|
910
|
+
for (const node of nodes) {
|
|
911
|
+
if (node.outputs) {
|
|
912
|
+
map.set(node.id, node.outputs);
|
|
913
|
+
}
|
|
914
|
+
if (node.children && node.children.length > 0) {
|
|
915
|
+
buildMap(node.children, map);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
buildMap(previous, previousMap);
|
|
920
|
+
buildMap(current, currentMap);
|
|
921
|
+
// Check for changed outputs in existing nodes
|
|
922
|
+
for (const [nodeId, currentOutputs] of currentMap) {
|
|
923
|
+
const previousOutputs = previousMap.get(nodeId) || {};
|
|
924
|
+
// Check each output key
|
|
925
|
+
for (const [outputKey, currentValue] of Object.entries(currentOutputs)) {
|
|
926
|
+
const previousValue = previousOutputs[outputKey];
|
|
927
|
+
// REQ-7.1, 7.3: undefined → value IS a change (initial deployment scenario)
|
|
928
|
+
if (previousValue !== currentValue) {
|
|
929
|
+
logger.debug(`Output changed: ${nodeId}.${outputKey} (${previousValue} → ${currentValue})`);
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Check for removed outputs
|
|
934
|
+
for (const outputKey of Object.keys(previousOutputs)) {
|
|
935
|
+
if (!(outputKey in currentOutputs)) {
|
|
936
|
+
logger.debug(`Output removed: ${nodeId}.${outputKey}`);
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
// Check for new nodes with outputs
|
|
942
|
+
for (const [nodeId, currentOutputs] of currentMap) {
|
|
943
|
+
if (!previousMap.has(nodeId) && Object.keys(currentOutputs).length > 0) {
|
|
944
|
+
logger.debug(`New node with outputs: ${nodeId}`);
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Find a CloudDOM node by ID
|
|
952
|
+
*
|
|
953
|
+
* @param nodes - CloudDOM tree to search
|
|
954
|
+
* @param id - Node ID to find
|
|
955
|
+
* @returns CloudDOM node or undefined if not found
|
|
956
|
+
*/
|
|
957
|
+
findNodeById(nodes, id) {
|
|
958
|
+
for (const node of nodes) {
|
|
959
|
+
if (node.id === id) {
|
|
960
|
+
return node;
|
|
961
|
+
}
|
|
962
|
+
if (node.children && node.children.length > 0) {
|
|
963
|
+
const found = this.findNodeById(node.children, id);
|
|
964
|
+
if (found) {
|
|
965
|
+
return found;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Extract outputs from CloudDOM nodes
|
|
973
|
+
*
|
|
974
|
+
* Walks the CloudDOM tree and collects all outputs from nodes.
|
|
975
|
+
* Outputs are formatted as nodeId.outputKey (e.g., 'registry.state-0').
|
|
976
|
+
*
|
|
977
|
+
* REQ-02: Extract outputs from useState calls
|
|
978
|
+
* REQ-06: Universal output access
|
|
979
|
+
*
|
|
980
|
+
* @param cloudDOM - CloudDOM tree
|
|
981
|
+
* @returns Outputs object with keys in format nodeId.outputKey
|
|
982
|
+
*/
|
|
983
|
+
extractOutputs(cloudDOM) {
|
|
984
|
+
const outputs = {};
|
|
985
|
+
// Safety check
|
|
986
|
+
if (!Array.isArray(cloudDOM)) {
|
|
987
|
+
this.log('Warning: cloudDOM is not an array, returning empty outputs');
|
|
988
|
+
return outputs;
|
|
989
|
+
}
|
|
990
|
+
const walk = (nodes) => {
|
|
991
|
+
if (!nodes) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (!Array.isArray(nodes)) {
|
|
995
|
+
this.log(`Warning: nodes is not an array: ${typeof nodes}, value: ${JSON.stringify(nodes)}`);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
for (const node of nodes) {
|
|
999
|
+
if (!node) {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
// Extract outputs from node
|
|
1003
|
+
if (node.outputs && typeof node.outputs === 'object') {
|
|
1004
|
+
for (const [key, value] of Object.entries(node.outputs)) {
|
|
1005
|
+
// Output name format: nodeId.outputKey (REQ-06)
|
|
1006
|
+
const outputName = `${node.id}.${key}`;
|
|
1007
|
+
outputs[outputName] = value;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
// Recursively walk children
|
|
1011
|
+
if (node.children) {
|
|
1012
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
1013
|
+
walk(node.children);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
walk(cloudDOM);
|
|
1019
|
+
return outputs;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Get the cloud provider (for testing/debugging)
|
|
1023
|
+
*
|
|
1024
|
+
* @returns The injected cloud provider
|
|
1025
|
+
*/
|
|
1026
|
+
getCloudProvider() {
|
|
1027
|
+
return this.config.cloudProvider;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Get the backend provider (for testing/debugging)
|
|
1031
|
+
*
|
|
1032
|
+
* @returns The injected backend provider
|
|
1033
|
+
*/
|
|
1034
|
+
getBackendProvider() {
|
|
1035
|
+
return this.config.backendProvider;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get the state machine (for testing/debugging)
|
|
1039
|
+
*
|
|
1040
|
+
* @returns The state machine instance
|
|
1041
|
+
*/
|
|
1042
|
+
getStateMachine() {
|
|
1043
|
+
return this.stateMachine;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Get backend state for a stack (for CLI/comparison)
|
|
1047
|
+
*
|
|
1048
|
+
* @param stackName - Stack name to get state for
|
|
1049
|
+
* @returns Promise resolving to backend state or null
|
|
1050
|
+
*/
|
|
1051
|
+
async getBackendState(stackName) {
|
|
1052
|
+
return this.stateMachine.getState(stackName);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* React-style render method (singleton API)
|
|
1056
|
+
*
|
|
1057
|
+
* Renders JSX to CloudDOM using the singleton configuration.
|
|
1058
|
+
* This is the recommended API for entry files.
|
|
1059
|
+
*
|
|
1060
|
+
* Example:
|
|
1061
|
+
* ```typescript
|
|
1062
|
+
* // Configure providers
|
|
1063
|
+
* CReact.cloudProvider = new AwsCloudProvider();
|
|
1064
|
+
* CReact.backendProvider = new S3BackendProvider();
|
|
1065
|
+
*
|
|
1066
|
+
* // Render app
|
|
1067
|
+
* export default CReact.renderCloudDOM(<MyApp />, 'my-stack');
|
|
1068
|
+
* ```
|
|
1069
|
+
*
|
|
1070
|
+
* @param element - JSX element to render
|
|
1071
|
+
* @param stackName - Stack name for state management
|
|
1072
|
+
* @returns Promise resolving to CloudDOM tree
|
|
1073
|
+
* @throws Error if providers are not configured
|
|
1074
|
+
*/
|
|
1075
|
+
static async renderCloudDOM(element, stackName) {
|
|
1076
|
+
// Validate that providers are configured
|
|
1077
|
+
if (!CReact.cloudProvider) {
|
|
1078
|
+
throw new Error('CReact.cloudProvider must be set before calling renderCloudDOM.\n\n');
|
|
1079
|
+
}
|
|
1080
|
+
if (!CReact.backendProvider) {
|
|
1081
|
+
throw new Error('CReact.backendProvider must be set before calling renderCloudDOM.\n\n');
|
|
1082
|
+
}
|
|
1083
|
+
// Create instance with singleton configuration
|
|
1084
|
+
const instance = new CReact({
|
|
1085
|
+
cloudProvider: CReact.cloudProvider,
|
|
1086
|
+
backendProvider: CReact.backendProvider,
|
|
1087
|
+
migrationMap: CReact.migrationMap,
|
|
1088
|
+
asyncTimeout: CReact.asyncTimeout,
|
|
1089
|
+
});
|
|
1090
|
+
// Store the instance globally so CLI can access it
|
|
1091
|
+
CReact._lastInstance = instance;
|
|
1092
|
+
CReact._lastElement = element;
|
|
1093
|
+
CReact._lastStackName = stackName;
|
|
1094
|
+
// CRITICAL: Load previous state from backend before rendering
|
|
1095
|
+
// This enables useState to hydrate from persisted state
|
|
1096
|
+
await instance.loadStateForHydration(stackName);
|
|
1097
|
+
// Build and return CloudDOM
|
|
1098
|
+
return instance.build(element, stackName);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Get the last instance created by renderCloudDOM (for CLI use)
|
|
1102
|
+
* @internal
|
|
1103
|
+
*/
|
|
1104
|
+
static getLastInstance() {
|
|
1105
|
+
if (CReact._lastInstance) {
|
|
1106
|
+
return {
|
|
1107
|
+
instance: CReact._lastInstance,
|
|
1108
|
+
element: CReact._lastElement,
|
|
1109
|
+
stackName: CReact._lastStackName,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
exports.CReact = CReact;
|
|
1116
|
+
// Re-export JSX functions for convenience (so users only need one import)
|
|
1117
|
+
CReact.createElement = jsx_1.CReact.createElement;
|
|
1118
|
+
CReact.Fragment = jsx_1.CReact.Fragment;
|
|
1119
|
+
// Global instance for hooks to access
|
|
1120
|
+
CReact.globalInstance = null;
|
|
1121
|
+
// CRITICAL: Hydration map must be STATIC (shared across all instances)
|
|
1122
|
+
// During hot reload, multiple CReact instances are created, but they all need
|
|
1123
|
+
// to share the same hydration data. The hydration is prepared on one instance
|
|
1124
|
+
// but useState (via getCReactInstance) accesses another instance.
|
|
1125
|
+
CReact.hydrationMap = new Map();
|
|
1126
|
+
// Export the getCReactInstance function for hooks to use
|
|
1127
|
+
exports.getCReactInstance = CReact.getCReactInstance;
|