@creact-labs/creact 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +85 -22
  2. package/dist/cli.d.ts +11 -0
  3. package/dist/cli.js +88 -0
  4. package/dist/index.d.ts +19 -44
  5. package/dist/index.js +20 -68
  6. package/dist/jsx/index.d.ts +2 -0
  7. package/dist/jsx/index.js +1 -0
  8. package/dist/jsx/jsx-dev-runtime.d.ts +4 -0
  9. package/dist/jsx/jsx-dev-runtime.js +4 -0
  10. package/dist/jsx/jsx-runtime.d.ts +38 -0
  11. package/dist/jsx/jsx-runtime.js +38 -0
  12. package/dist/jsx/types.d.ts +12 -0
  13. package/dist/jsx/types.js +4 -0
  14. package/dist/primitives/context.d.ts +34 -0
  15. package/dist/primitives/context.js +63 -0
  16. package/dist/primitives/index.d.ts +3 -0
  17. package/dist/primitives/index.js +3 -0
  18. package/dist/primitives/instance.d.ts +72 -0
  19. package/dist/primitives/instance.js +235 -0
  20. package/dist/primitives/store.d.ts +22 -0
  21. package/dist/primitives/store.js +97 -0
  22. package/dist/provider/backend.d.ts +110 -0
  23. package/dist/provider/backend.js +37 -0
  24. package/dist/provider/interface.d.ts +48 -0
  25. package/dist/provider/interface.js +39 -0
  26. package/dist/reactive/effect.d.ts +11 -0
  27. package/dist/reactive/effect.js +42 -0
  28. package/dist/reactive/index.d.ts +3 -0
  29. package/dist/reactive/index.js +3 -0
  30. package/dist/reactive/signal.d.ts +32 -0
  31. package/dist/reactive/signal.js +60 -0
  32. package/dist/reactive/tracking.d.ts +41 -0
  33. package/dist/reactive/tracking.js +161 -0
  34. package/dist/runtime/fiber.d.ts +21 -0
  35. package/dist/runtime/fiber.js +16 -0
  36. package/dist/runtime/index.d.ts +4 -0
  37. package/dist/runtime/index.js +4 -0
  38. package/dist/runtime/reconcile.d.ts +66 -0
  39. package/dist/runtime/reconcile.js +210 -0
  40. package/dist/runtime/render.d.ts +42 -0
  41. package/dist/runtime/render.js +231 -0
  42. package/dist/runtime/run.d.ts +119 -0
  43. package/dist/runtime/run.js +334 -0
  44. package/dist/runtime/state-machine.d.ts +95 -0
  45. package/dist/runtime/state-machine.js +209 -0
  46. package/dist/types.d.ts +13 -0
  47. package/dist/types.js +4 -0
  48. package/package.json +29 -24
  49. package/dist/cli/commands/BuildCommand.d.ts +0 -40
  50. package/dist/cli/commands/BuildCommand.js +0 -151
  51. package/dist/cli/commands/DeployCommand.d.ts +0 -38
  52. package/dist/cli/commands/DeployCommand.js +0 -194
  53. package/dist/cli/commands/DevCommand.d.ts +0 -52
  54. package/dist/cli/commands/DevCommand.js +0 -394
  55. package/dist/cli/commands/PlanCommand.d.ts +0 -39
  56. package/dist/cli/commands/PlanCommand.js +0 -164
  57. package/dist/cli/commands/index.d.ts +0 -36
  58. package/dist/cli/commands/index.js +0 -43
  59. package/dist/cli/core/ArgumentParser.d.ts +0 -46
  60. package/dist/cli/core/ArgumentParser.js +0 -127
  61. package/dist/cli/core/BaseCommand.d.ts +0 -75
  62. package/dist/cli/core/BaseCommand.js +0 -95
  63. package/dist/cli/core/CLIContext.d.ts +0 -68
  64. package/dist/cli/core/CLIContext.js +0 -183
  65. package/dist/cli/core/CommandRegistry.d.ts +0 -64
  66. package/dist/cli/core/CommandRegistry.js +0 -89
  67. package/dist/cli/core/index.d.ts +0 -36
  68. package/dist/cli/core/index.js +0 -43
  69. package/dist/cli/index.d.ts +0 -35
  70. package/dist/cli/index.js +0 -100
  71. package/dist/cli/output.d.ts +0 -204
  72. package/dist/cli/output.js +0 -437
  73. package/dist/cli/utils.d.ts +0 -59
  74. package/dist/cli/utils.js +0 -76
  75. package/dist/context/createContext.d.ts +0 -90
  76. package/dist/context/createContext.js +0 -113
  77. package/dist/context/index.d.ts +0 -30
  78. package/dist/context/index.js +0 -35
  79. package/dist/core/CReact.d.ts +0 -409
  80. package/dist/core/CReact.js +0 -1151
  81. package/dist/core/CloudDOMBuilder.d.ts +0 -447
  82. package/dist/core/CloudDOMBuilder.js +0 -1234
  83. package/dist/core/ContextDependencyTracker.d.ts +0 -165
  84. package/dist/core/ContextDependencyTracker.js +0 -448
  85. package/dist/core/ErrorRecoveryManager.d.ts +0 -145
  86. package/dist/core/ErrorRecoveryManager.js +0 -443
  87. package/dist/core/EventBus.d.ts +0 -91
  88. package/dist/core/EventBus.js +0 -185
  89. package/dist/core/ProviderOutputTracker.d.ts +0 -211
  90. package/dist/core/ProviderOutputTracker.js +0 -476
  91. package/dist/core/ReactiveUpdateQueue.d.ts +0 -76
  92. package/dist/core/ReactiveUpdateQueue.js +0 -121
  93. package/dist/core/Reconciler.d.ts +0 -415
  94. package/dist/core/Reconciler.js +0 -1044
  95. package/dist/core/RenderScheduler.d.ts +0 -153
  96. package/dist/core/RenderScheduler.js +0 -519
  97. package/dist/core/Renderer.d.ts +0 -336
  98. package/dist/core/Renderer.js +0 -944
  99. package/dist/core/Runtime.d.ts +0 -246
  100. package/dist/core/Runtime.js +0 -640
  101. package/dist/core/StateBindingManager.d.ts +0 -121
  102. package/dist/core/StateBindingManager.js +0 -309
  103. package/dist/core/StateMachine.d.ts +0 -441
  104. package/dist/core/StateMachine.js +0 -883
  105. package/dist/core/StructuralChangeDetector.d.ts +0 -140
  106. package/dist/core/StructuralChangeDetector.js +0 -363
  107. package/dist/core/Validator.d.ts +0 -127
  108. package/dist/core/Validator.js +0 -279
  109. package/dist/core/errors.d.ts +0 -153
  110. package/dist/core/errors.js +0 -202
  111. package/dist/core/index.d.ts +0 -38
  112. package/dist/core/index.js +0 -64
  113. package/dist/core/types.d.ts +0 -265
  114. package/dist/core/types.js +0 -48
  115. package/dist/hooks/context.d.ts +0 -147
  116. package/dist/hooks/context.js +0 -334
  117. package/dist/hooks/useContext.d.ts +0 -113
  118. package/dist/hooks/useContext.js +0 -169
  119. package/dist/hooks/useEffect.d.ts +0 -105
  120. package/dist/hooks/useEffect.js +0 -540
  121. package/dist/hooks/useInstance.d.ts +0 -139
  122. package/dist/hooks/useInstance.js +0 -455
  123. package/dist/hooks/useState.d.ts +0 -120
  124. package/dist/hooks/useState.js +0 -298
  125. package/dist/jsx.d.ts +0 -143
  126. package/dist/jsx.js +0 -76
  127. package/dist/providers/DummyBackendProvider.d.ts +0 -193
  128. package/dist/providers/DummyBackendProvider.js +0 -189
  129. package/dist/providers/DummyCloudProvider.d.ts +0 -128
  130. package/dist/providers/DummyCloudProvider.js +0 -157
  131. package/dist/providers/IBackendProvider.d.ts +0 -177
  132. package/dist/providers/IBackendProvider.js +0 -31
  133. package/dist/providers/ICloudProvider.d.ts +0 -230
  134. package/dist/providers/ICloudProvider.js +0 -31
  135. package/dist/providers/index.d.ts +0 -31
  136. package/dist/providers/index.js +0 -31
  137. package/dist/test-event-callbacks.d.ts +0 -0
  138. package/dist/test-event-callbacks.js +0 -1
  139. package/dist/utils/Logger.d.ts +0 -144
  140. package/dist/utils/Logger.js +0 -220
  141. package/dist/utils/Output.d.ts +0 -161
  142. package/dist/utils/Output.js +0 -401
  143. package/dist/utils/deepEqual.d.ts +0 -71
  144. package/dist/utils/deepEqual.js +0 -276
  145. package/dist/utils/naming.d.ts +0 -241
  146. package/dist/utils/naming.js +0 -376
@@ -1,1151 +0,0 @@
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, this.contextDependencyTracker);
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
- // 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;
526
- if (needsReRender) {
527
- // Re-render only the affected components
528
- const fibersToReRender = affectedFibers;
529
- logger.info(`Triggering re-renders for ${fibersToReRender.length} affected components`);
530
- // REQ-7.4, 7.5: Schedule re-renders with proper batching and deduplication
531
- fibersToReRender.forEach((fiber) => {
532
- this.scheduleReRender(fiber, 'output-update');
533
- });
534
- // CRITICAL: Update previousOutputsMap with current CloudDOM outputs
535
- // This allows useInstance to access outputs during re-render
536
- const outputsMap = this.buildOutputsMap(cloudDOM);
537
- logger.debug('Updating previousOutputsMap for re-render with latest outputs');
538
- logger.debug(`OutputsMap has ${outputsMap.size} entries:`, Array.from(outputsMap.keys()));
539
- (0, useInstance_1.setPreviousOutputs)(outputsMap);
540
- // Execute the scheduled re-renders
541
- const updatedFiber = this.renderer.reRenderComponents(fibersToReRender, 'output-update');
542
- // Build updated CloudDOM from re-rendered components
543
- // CRITICAL FIX: Pass previousCloudDOM to preserve non-re-rendered nodes
544
- const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber, cloudDOM);
545
- // Check if the re-render produced new resources to deploy
546
- const reactiveChangeSet = this.reconciler.reconcile(cloudDOM, updatedCloudDOM);
547
- if ((0, Reconciler_1.hasChanges)(reactiveChangeSet)) {
548
- this.log(`Re-render produced new changes: ${reactiveChangeSet.creates.length} creates, ${reactiveChangeSet.updates.length} updates`);
549
- this.log('Reactive changes detected - will need another deployment cycle');
550
- // Store the CloudDOM BEFORE re-render for diff comparison
551
- this.preReactiveCloudDOM = JSON.parse(JSON.stringify(cloudDOM));
552
- // Update the CloudDOM with reactive changes
553
- cloudDOM.splice(0, cloudDOM.length, ...updatedCloudDOM);
554
- // Store flag indicating reactive changes need deployment
555
- this.hasReactiveChanges = true;
556
- this.reactiveCloudDOM = updatedCloudDOM;
557
- this.log('Post-deployment effects and reactive sync completed');
558
- this.log('Deployment complete (reactive changes pending)');
559
- return;
560
- }
561
- else {
562
- this.log('Re-render produced no new changes');
563
- // Update the stored CloudDOM with reactive changes
564
- cloudDOM.splice(0, cloudDOM.length, ...updatedCloudDOM);
565
- }
566
- }
567
- else {
568
- logger.debug(`No output or state changes detected, skipping re-render`);
569
- }
570
- // Save the updated CloudDOM state with new outputs
571
- logger.debug('Saving updated state with new outputs...');
572
- await this.stateMachine.updateCloudDOM(stackName, cloudDOM);
573
- this.log('Post-deployment effects and reactive sync completed');
574
- }
575
- else {
576
- logger.warn('No fiber tree found for effects execution');
577
- }
578
- this.log('Deployment complete');
579
- }
580
- catch (error) {
581
- logger.error('Deployment failed:', error);
582
- // NEW (Gap 3): Attempt error recovery before failing
583
- if (this.lastFiberTree) {
584
- try {
585
- logger.info('Attempting error recovery...');
586
- // Create snapshot for potential rollback
587
- this.errorRecoveryManager.createComponentSnapshot(this.lastFiberTree);
588
- // Attempt recovery using re-render error handler
589
- const recoveryResult = await this.errorRecoveryManager.handleReRenderError(error, [this.lastFiberTree], 'manual');
590
- if (recoveryResult.success) {
591
- logger.info(`Error recovery succeeded: ${recoveryResult.message}`);
592
- // Recovery succeeded - return partial result instead of throwing
593
- // This allows deployment to complete with recovered state
594
- return;
595
- }
596
- logger.warn(`Error recovery failed: ${recoveryResult.message}`);
597
- }
598
- catch (recoveryError) {
599
- logger.error('Error during recovery attempt:', recoveryError);
600
- // Continue with normal error handling
601
- }
602
- }
603
- // Trigger onError event callbacks for all resources
604
- await EventBus_1.CloudDOMEventBus.triggerEventCallbacksRecursive(cloudDOM, 'error', error);
605
- // REQ-09.3: Lifecycle hook - onError
606
- if (this.config.cloudProvider.onError) {
607
- this.log('Calling onError lifecycle hook');
608
- await this.config.cloudProvider.onError(error, cloudDOM);
609
- }
610
- // Mark deployment as failed via StateMachine
611
- this.log('Marking deployment as failed via StateMachine');
612
- const deploymentError = error instanceof errors_1.DeploymentError
613
- ? error
614
- : new errors_1.DeploymentError(error.message, {
615
- message: error.message,
616
- code: 'DEPLOYMENT_FAILED',
617
- stack: error.stack,
618
- });
619
- await this.stateMachine.failDeployment(stackName, deploymentError);
620
- // Re-throw error to halt deployment (REQ-09.4)
621
- throw error;
622
- }
623
- }
624
- // Hydration map is now STATIC (see declaration above with globalInstance)
625
- /**
626
- * Load previous state from backend and prepare for hydration
627
- * This should be called before rendering to restore persisted state
628
- *
629
- * @param stackName - Stack name to load state for
630
- */
631
- async loadStateForHydration(stackName) {
632
- try {
633
- const previousState = await this.stateMachine.getState(stackName);
634
- if (previousState && previousState.cloudDOM) {
635
- this.prepareHydration(previousState.cloudDOM);
636
- this.log(`Loaded state for hydration from backend: ${stackName}`);
637
- }
638
- else {
639
- this.log(`No previous state found for stack: ${stackName}`);
640
- }
641
- }
642
- catch (error) {
643
- logger.warn(`Failed to load state for hydration: ${error.message}`);
644
- // Continue without hydration - this is not a fatal error
645
- }
646
- }
647
- /**
648
- * Prepare hydration data from previous CloudDOM
649
- * This must be called BEFORE rendering to make state available to useState
650
- *
651
- * CRITICAL: State outputs belong to the COMPONENT that called useState,
652
- * not the CloudDOM node. We need to key by component path (parent of node).
653
- *
654
- * Example:
655
- * - Component path: ['web-app-stack']
656
- * - CloudDOM node paths: ['web-app-stack', 'vpc'], ['web-app-stack', 'database']
657
- * - All nodes from same component share the same state outputs
658
- * - Hydration should be keyed by 'web-app-stack', not 'web-app-stack.vpc'
659
- *
660
- * @param previousCloudDOM - Previous CloudDOM with persisted state outputs
661
- * @param clearExisting - Whether to clear existing hydration data (default: false)
662
- */
663
- prepareHydration(previousCloudDOM, clearExisting = false) {
664
- if (clearExisting) {
665
- CReact.hydrationMap.clear();
666
- }
667
- if (!previousCloudDOM || previousCloudDOM.length === 0) {
668
- this.log('No previous state to prepare for hydration');
669
- return;
670
- }
671
- // Extract state outputs from previous CloudDOM and build hydration map
672
- const extractStateOutputs = (nodes) => {
673
- for (const node of nodes) {
674
- // CRITICAL FIX: Extract component path (parent of CloudDOM node)
675
- // node.path = ['web-app-stack', 'vpc']
676
- // componentPath = 'web-app-stack' (where useState was called)
677
- const componentPath = node.path.length > 1
678
- ? node.path.slice(0, -1).join('.') // Normal case: parent component
679
- : node.path.join('.'); // Edge case: root node
680
- // Validate path
681
- if (!componentPath) {
682
- logger.warn(`Invalid component path for hydration:`, node.path);
683
- continue;
684
- }
685
- // Read from state field instead of filtering state.* from outputs
686
- if (node.state && Object.keys(node.state).length > 0) {
687
- // Convert state object to array: { state1: "a", state2: "b" } → ["a", "b"]
688
- // Ensure keys are sorted (state1, state2, state3...) before converting to array
689
- const stateValues = Object.keys(node.state)
690
- .sort() // Sort to ensure state1, state2, state3... order
691
- .map((key) => node.state[key]);
692
- if (stateValues.length > 0) {
693
- // Only set if not already set (first node from component wins)
694
- if (!CReact.hydrationMap.has(componentPath)) {
695
- CReact.hydrationMap.set(componentPath, stateValues);
696
- logger.debug(`Prepared hydration for component "${componentPath}" (from node "${node.path.join('.')}"): ${JSON.stringify(stateValues)}`);
697
- }
698
- else {
699
- logger.debug(`Skipping duplicate hydration for component "${componentPath}" (already set from another node)`);
700
- }
701
- }
702
- }
703
- if (node.children && node.children.length > 0) {
704
- extractStateOutputs(node.children);
705
- }
706
- }
707
- };
708
- extractStateOutputs(previousCloudDOM);
709
- logger.debug(`Hydration prepared: ${CReact.hydrationMap.size} components with state`);
710
- logger.debug(`Hydration map keys:`, Array.from(CReact.hydrationMap.keys()));
711
- logger.debug(`This instance is global: ${this === CReact.globalInstance}`);
712
- logger.debug(`Full hydration map:`, Object.fromEntries(CReact.hydrationMap));
713
- }
714
- /**
715
- * Get hydrated value for a specific fiber and hook index
716
- * Called by useState during render to check for persisted state
717
- *
718
- * @param fiberPath - Path of the fiber
719
- * @param hookIndex - Index of the hook
720
- * @returns Hydrated value or undefined if not found
721
- */
722
- getHydratedValue(fiberPath, hookIndex) {
723
- const values = CReact.hydrationMap.get(fiberPath);
724
- if (values && hookIndex < values.length) {
725
- return values[hookIndex];
726
- }
727
- return undefined;
728
- }
729
- /**
730
- * Get hydrated value for a component by searching for any child node
731
- * This handles the case where useState is in a parent component but state
732
- * outputs are stored on child CloudDOM nodes
733
- *
734
- * With the path mapping fix, this should now find exact matches.
735
- * The child node search is kept as a fallback for edge cases.
736
- *
737
- * @param componentPath - Path of the component (e.g., 'web-app-stack')
738
- * @param hookIndex - Index of the hook
739
- * @returns Hydrated value or undefined if not found
740
- */
741
- getHydratedValueForComponent(componentPath, hookIndex) {
742
- logger.debug(`getHydratedValueForComponent: path="${componentPath}", hookIndex=${hookIndex}`);
743
- logger.debug(`Available hydration keys:`, Array.from(CReact.hydrationMap.keys()));
744
- // Try exact match first (should work now with path mapping fix)
745
- const exactMatch = this.getHydratedValue(componentPath, hookIndex);
746
- if (exactMatch !== undefined) {
747
- logger.debug(`✅ Found exact match for "${componentPath}"[${hookIndex}]:`, exactMatch);
748
- return exactMatch;
749
- }
750
- // Fallback: Search for any child node that starts with this component path
751
- // This handles edge cases where path mapping might not work as expected
752
- for (const [nodePath, values] of CReact.hydrationMap.entries()) {
753
- if (nodePath.startsWith(componentPath + '.') && hookIndex < values.length) {
754
- logger.debug(`✅ Found hydration in child node: ${nodePath} for component: ${componentPath}`);
755
- return values[hookIndex];
756
- }
757
- }
758
- // No match found
759
- logger.debug(`❌ No hydration found for "${componentPath}"[${hookIndex}]`);
760
- return undefined;
761
- }
762
- /**
763
- * Check if hydration data is available
764
- *
765
- * @returns True if hydration map has data
766
- */
767
- hasHydrationData() {
768
- const hasData = CReact.hydrationMap.size > 0;
769
- logger.debug(`hasHydrationData: size=${CReact.hydrationMap.size}, hasData=${hasData}, instance=${this === CReact.globalInstance ? 'GLOBAL' : 'OTHER'}`);
770
- return hasData;
771
- }
772
- /**
773
- * Get hydration map keys for debugging
774
- *
775
- * @returns Array of component paths with hydration data
776
- */
777
- getHydrationMapKeys() {
778
- return Array.from(CReact.hydrationMap.keys());
779
- }
780
- /**
781
- * Clear hydration data after rendering is complete
782
- */
783
- clearHydration() {
784
- logger.debug(`clearHydration: Clearing ${CReact.hydrationMap.size} entries`);
785
- logger.debug('clearHydration: Stack trace:', new Error().stack);
786
- CReact.hydrationMap.clear();
787
- this.log('Hydration data cleared');
788
- }
789
- /**
790
- * Serialize internal reactive state for hot reload preservation
791
- * This captures the state of reactive systems that need to survive recompilation
792
- *
793
- * @returns Serialized state object
794
- */
795
- serializeReactiveState() {
796
- try {
797
- const state = {
798
- timestamp: Date.now(),
799
- };
800
- // Note: Most reactive state is already persisted in CloudDOM outputs
801
- // This is mainly for tracking metadata and failure statistics
802
- if (this.renderScheduler) {
803
- // Preserve failure statistics but not pending renders
804
- state.renderSchedulerStats = {
805
- // Add any failure stats if the scheduler tracks them
806
- };
807
- }
808
- return state;
809
- }
810
- catch (error) {
811
- logger.warn('Failed to serialize reactive state:', error);
812
- return null;
813
- }
814
- }
815
- /**
816
- * Restore internal reactive state after hot reload recompilation
817
- * This restores the state of reactive systems from serialized data
818
- *
819
- * @param serializedState - Previously serialized state
820
- */
821
- restoreReactiveState(serializedState) {
822
- if (!serializedState) {
823
- this.log('No serialized state to restore');
824
- return;
825
- }
826
- try {
827
- // Clear any pending renders from the previous session
828
- if (this.renderScheduler && this.renderScheduler.clearPending) {
829
- this.renderScheduler.clearPending();
830
- }
831
- this.log('Reactive state restored from hot reload');
832
- }
833
- catch (error) {
834
- logger.warn('Failed to restore reactive state:', error);
835
- }
836
- }
837
- /**
838
- * Hydrate the current fiber tree with state values from previous CloudDOM
839
- * This is used during hot reload to restore persisted state values
840
- *
841
- * @param previousCloudDOM - Previous CloudDOM with persisted state outputs
842
- */
843
- hydrateStateFromPreviousCloudDOM(previousCloudDOM) {
844
- if (!this.lastFiberTree || !previousCloudDOM || previousCloudDOM.length === 0) {
845
- this.log('No previous state to hydrate');
846
- return;
847
- }
848
- // Extract state outputs from previous CloudDOM
849
- const stateOutputs = new Map();
850
- const extractStateOutputs = (nodes) => {
851
- for (const node of nodes) {
852
- // Read from state field instead of filtering state.* from outputs
853
- if (node.state && Object.keys(node.state).length > 0) {
854
- // Convert state object to array: { state1: "a", state2: "b" } → ["a", "b"]
855
- // Ensure keys are sorted (state1, state2, state3...) before converting to array
856
- const stateValues = Object.keys(node.state)
857
- .sort() // Sort to ensure state1, state2, state3... order
858
- .map((key) => node.state[key]);
859
- if (stateValues.length > 0) {
860
- stateOutputs.set(node.id, stateValues);
861
- }
862
- }
863
- if (node.children && node.children.length > 0) {
864
- extractStateOutputs(node.children);
865
- }
866
- }
867
- };
868
- extractStateOutputs(previousCloudDOM);
869
- if (stateOutputs.size === 0) {
870
- this.log('No state outputs found in previous CloudDOM');
871
- return;
872
- }
873
- // Hydrate fiber tree with state values
874
- const hydrateFiber = (fiber) => {
875
- // Match fiber to CloudDOM node by path
876
- if (fiber.cloudDOMNodes && Array.isArray(fiber.cloudDOMNodes)) {
877
- for (const node of fiber.cloudDOMNodes) {
878
- const savedState = stateOutputs.get(node.id);
879
- if (savedState && savedState.length > 0) {
880
- // Initialize hooks array if not present
881
- if (!fiber.hooks) {
882
- fiber.hooks = [];
883
- }
884
- // Restore state values
885
- for (let i = 0; i < savedState.length; i++) {
886
- fiber.hooks[i] = savedState[i];
887
- }
888
- this.log(`Hydrated ${savedState.length} state values for ${node.id}`);
889
- }
890
- }
891
- }
892
- // Recursively hydrate children
893
- if (fiber.children && fiber.children.length > 0) {
894
- for (const child of fiber.children) {
895
- hydrateFiber(child);
896
- }
897
- }
898
- };
899
- hydrateFiber(this.lastFiberTree);
900
- this.log(`State hydration complete: ${stateOutputs.size} nodes hydrated`);
901
- }
902
- /**
903
- * Check if outputs actually changed between previous and current CloudDOM
904
- * REQ-7.1, 7.2, 7.3: Proper output change detection including undefined → value transitions
905
- *
906
- * @param previous - Previous CloudDOM state
907
- * @param current - Current CloudDOM state
908
- * @returns True if any outputs actually changed
909
- */
910
- hasActualOutputChanges(previous, current) {
911
- // Build maps for efficient comparison
912
- const previousMap = new Map();
913
- const currentMap = new Map();
914
- const buildMap = (nodes, map) => {
915
- for (const node of nodes) {
916
- if (node.outputs) {
917
- map.set(node.id, node.outputs);
918
- }
919
- if (node.children && node.children.length > 0) {
920
- buildMap(node.children, map);
921
- }
922
- }
923
- };
924
- buildMap(previous, previousMap);
925
- buildMap(current, currentMap);
926
- // Check for changed outputs in existing nodes
927
- for (const [nodeId, currentOutputs] of currentMap) {
928
- const previousOutputs = previousMap.get(nodeId) || {};
929
- // Check each output key
930
- for (const [outputKey, currentValue] of Object.entries(currentOutputs)) {
931
- const previousValue = previousOutputs[outputKey];
932
- // REQ-7.1, 7.3: undefined → value IS a change (initial deployment scenario)
933
- if (previousValue !== currentValue) {
934
- logger.debug(`Output changed: ${nodeId}.${outputKey} (${previousValue} → ${currentValue})`);
935
- return true;
936
- }
937
- }
938
- // Check for removed outputs
939
- for (const outputKey of Object.keys(previousOutputs)) {
940
- if (!(outputKey in currentOutputs)) {
941
- logger.debug(`Output removed: ${nodeId}.${outputKey}`);
942
- return true;
943
- }
944
- }
945
- }
946
- // Check for new nodes with outputs
947
- for (const [nodeId, currentOutputs] of currentMap) {
948
- if (!previousMap.has(nodeId) && Object.keys(currentOutputs).length > 0) {
949
- logger.debug(`New node with outputs: ${nodeId}`);
950
- return true;
951
- }
952
- }
953
- return false;
954
- }
955
- /**
956
- * Find a CloudDOM node by ID
957
- *
958
- * @param nodes - CloudDOM tree to search
959
- * @param id - Node ID to find
960
- * @returns CloudDOM node or undefined if not found
961
- */
962
- findNodeById(nodes, id) {
963
- for (const node of nodes) {
964
- if (node.id === id) {
965
- return node;
966
- }
967
- if (node.children && node.children.length > 0) {
968
- const found = this.findNodeById(node.children, id);
969
- if (found) {
970
- return found;
971
- }
972
- }
973
- }
974
- return undefined;
975
- }
976
- /**
977
- * Extract outputs from CloudDOM nodes
978
- *
979
- * Walks the CloudDOM tree and collects all outputs from nodes.
980
- * Outputs are formatted as nodeId.outputKey (e.g., 'registry.state-0').
981
- *
982
- * REQ-02: Extract outputs from useState calls
983
- * REQ-06: Universal output access
984
- *
985
- * @param cloudDOM - CloudDOM tree
986
- * @returns Outputs object with keys in format nodeId.outputKey
987
- */
988
- extractOutputs(cloudDOM) {
989
- const outputs = {};
990
- // Safety check
991
- if (!Array.isArray(cloudDOM)) {
992
- this.log('Warning: cloudDOM is not an array, returning empty outputs');
993
- return outputs;
994
- }
995
- const walk = (nodes) => {
996
- if (!nodes) {
997
- return;
998
- }
999
- if (!Array.isArray(nodes)) {
1000
- this.log(`Warning: nodes is not an array: ${typeof nodes}, value: ${JSON.stringify(nodes)}`);
1001
- return;
1002
- }
1003
- for (const node of nodes) {
1004
- if (!node) {
1005
- continue;
1006
- }
1007
- // Extract outputs from node
1008
- if (node.outputs && typeof node.outputs === 'object') {
1009
- for (const [key, value] of Object.entries(node.outputs)) {
1010
- // Output name format: nodeId.outputKey (REQ-06)
1011
- const outputName = `${node.id}.${key}`;
1012
- outputs[outputName] = value;
1013
- }
1014
- }
1015
- // Recursively walk children
1016
- if (node.children) {
1017
- if (Array.isArray(node.children) && node.children.length > 0) {
1018
- walk(node.children);
1019
- }
1020
- }
1021
- }
1022
- };
1023
- walk(cloudDOM);
1024
- return outputs;
1025
- }
1026
- /**
1027
- * Get the cloud provider (for testing/debugging)
1028
- *
1029
- * @returns The injected cloud provider
1030
- */
1031
- getCloudProvider() {
1032
- return this.config.cloudProvider;
1033
- }
1034
- /**
1035
- * Get the backend provider (for testing/debugging)
1036
- *
1037
- * @returns The injected backend provider
1038
- */
1039
- getBackendProvider() {
1040
- return this.config.backendProvider;
1041
- }
1042
- /**
1043
- * Get the state machine (for testing/debugging)
1044
- *
1045
- * @returns The state machine instance
1046
- */
1047
- getStateMachine() {
1048
- return this.stateMachine;
1049
- }
1050
- /**
1051
- * Get backend state for a stack (for CLI/comparison)
1052
- *
1053
- * @param stackName - Stack name to get state for
1054
- * @returns Promise resolving to backend state or null
1055
- */
1056
- async getBackendState(stackName) {
1057
- return this.stateMachine.getState(stackName);
1058
- }
1059
- /**
1060
- * React-style render method (singleton API)
1061
- *
1062
- * Renders JSX to CloudDOM using the singleton configuration.
1063
- * This is the recommended API for entry files.
1064
- *
1065
- * Example:
1066
- * ```typescript
1067
- * // Configure providers
1068
- * CReact.cloudProvider = new AwsCloudProvider();
1069
- * CReact.backendProvider = new S3BackendProvider();
1070
- *
1071
- * // Render app
1072
- * export default CReact.renderCloudDOM(<MyApp />, 'my-stack');
1073
- * ```
1074
- *
1075
- * @param element - JSX element to render
1076
- * @param stackName - Stack name for state management
1077
- * @returns Promise resolving to CloudDOM tree
1078
- * @throws Error if providers are not configured
1079
- */
1080
- static async renderCloudDOM(element, stackName) {
1081
- // Validate that providers are configured
1082
- if (!CReact.cloudProvider) {
1083
- throw new Error('CReact.cloudProvider must be set before calling renderCloudDOM.\n\n');
1084
- }
1085
- if (!CReact.backendProvider) {
1086
- throw new Error('CReact.backendProvider must be set before calling renderCloudDOM.\n\n');
1087
- }
1088
- // Create instance with singleton configuration
1089
- const instance = new CReact({
1090
- cloudProvider: CReact.cloudProvider,
1091
- backendProvider: CReact.backendProvider,
1092
- migrationMap: CReact.migrationMap,
1093
- asyncTimeout: CReact.asyncTimeout,
1094
- });
1095
- // Store the instance globally so CLI can access it
1096
- CReact._lastInstance = instance;
1097
- CReact._lastElement = element;
1098
- CReact._lastStackName = stackName;
1099
- // Initialize providers if they have an initialize method
1100
- if (CReact.cloudProvider.initialize) {
1101
- await CReact.cloudProvider.initialize();
1102
- }
1103
- if (CReact.backendProvider.initialize) {
1104
- await CReact.backendProvider.initialize();
1105
- }
1106
- // CRITICAL: Load previous state from backend before rendering
1107
- // This enables useState to hydrate from persisted state
1108
- await instance.loadStateForHydration(stackName);
1109
- // CRITICAL: Detect and fix drift before rendering
1110
- // This ensures state matches reality and prevents stale deployments
1111
- const driftResult = await instance.stateMachine.detectAndFixDrift(stackName, CReact.cloudProvider);
1112
- // If drift was detected and fixed, reload state to get updated outputs
1113
- if (driftResult.resourcesFixed > 0) {
1114
- logger.debug(`Reloading state after fixing ${driftResult.resourcesFixed} drifted resources`);
1115
- // Clear existing hydration data and reload with fresh state
1116
- const freshState = await instance.stateMachine.getState(stackName);
1117
- if (freshState?.cloudDOM) {
1118
- instance.prepareHydration(freshState.cloudDOM, true); // clearExisting = true
1119
- }
1120
- }
1121
- // Build and return CloudDOM
1122
- return instance.build(element, stackName);
1123
- }
1124
- /**
1125
- * Get the last instance created by renderCloudDOM (for CLI use)
1126
- * @internal
1127
- */
1128
- static getLastInstance() {
1129
- if (CReact._lastInstance) {
1130
- return {
1131
- instance: CReact._lastInstance,
1132
- element: CReact._lastElement,
1133
- stackName: CReact._lastStackName,
1134
- };
1135
- }
1136
- return null;
1137
- }
1138
- }
1139
- exports.CReact = CReact;
1140
- // Re-export JSX functions for convenience (so users only need one import)
1141
- CReact.createElement = jsx_1.CReact.createElement;
1142
- CReact.Fragment = jsx_1.CReact.Fragment;
1143
- // Global instance for hooks to access
1144
- CReact.globalInstance = null;
1145
- // CRITICAL: Hydration map must be STATIC (shared across all instances)
1146
- // During hot reload, multiple CReact instances are created, but they all need
1147
- // to share the same hydration data. The hydration is prepared on one instance
1148
- // but useState (via getCReactInstance) accesses another instance.
1149
- CReact.hydrationMap = new Map();
1150
- // Export the getCReactInstance function for hooks to use
1151
- exports.getCReactInstance = CReact.getCReactInstance;