@creact-labs/creact 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,12 +3,8 @@ NOTE: !!! THIS IS A EXPERIMENT OF THOUGHT NOT A PRODUCTION READY PRODUCT !!!
3
3
 
4
4
  # CReact
5
5
 
6
- ![creact](https://i.postimg.cc/8P66GnT3/banner.jpg)
7
6
 
8
-
9
- Think of it as React's rendering model — but instead of a DOM, you render to the cloud.
10
-
11
- Write your cloud architecture in JSX. CReact figures out the dependencies, orchestrates the deployment, and keeps everything in sync.
7
+ Write your cloud architecture in JSX
12
8
 
13
9
  ```tsx
14
10
  function App() {
@@ -24,351 +20,8 @@ function App() {
24
20
  }
25
21
  ```
26
22
 
27
- That's it. No dependency graphs, no explicit ordering, no YAML. Just components that render to actual infrastructure.
28
-
29
- ## The Problem
30
-
31
- Terraform and Pulumi are great, but they're static. You write a plan, run it, and hope nothing breaks. If you need dynamic orchestration - like deploying a database, waiting for its endpoint, then using that endpoint in your API config - you're writing bash scripts or custom tooling.
32
-
33
- CReact treats infrastructure like React treats UI. Components re-render when their dependencies change. Outputs flow naturally through context. The Reconciler figures out what actually needs to deploy.
34
-
35
- ## What Makes It Different
36
-
37
- **It's reactive** - when a database finishes deploying and outputs its endpoint, components that depend on it automatically re-render and deploy. No manual dependency chains.
38
-
39
- **It's a compiler** - your JSX compiles to CloudDOM (basically an AST for infrastructure). You can diff it, version it, test it without cloud credentials. Only when you're ready does it materialize to real resources.
40
-
41
- **It's resilient** - every deployment is checkpointed and resumable. Crash halfway through? Resume from where you left off. The Reconciler only deploys what changed, like React's virtual DOM but for cloud resources.
42
-
43
- **It works with existing tools** - CReact doesn't replace Terraform or CDK. It orchestrates them. Wrap your Terraform modules as CReact components and compose them together.
44
-
45
23
  ## Watch It Work
46
24
 
47
- Here's a real deployment - 22 resources across 3 regions, 6 reactive cycles, fully automatic:
48
-
49
- ```bash
50
- $ creact dev --entry reactive-app.tsx --auto-approve
51
-
52
- Starting CReact development mode...
53
- Building initial state...
54
- Changes detected: 2 changes
55
-
56
- Planning changes for stack: optimized-reactive-app-stack
57
- ──────────────────────────────────────────────────
58
- + 2 to create
59
- + Analytics (Kinesis stream for API analytics)
60
- + VPC (Network foundation)
61
-
62
- Plan: 2 to add, 0 to change, 0 to destroy
63
- Auto-approving changes...
64
-
65
- Applying changes...
66
- [MockCloud] ✅ All resources materialized
67
- Apply complete! Resources: 2 added, 0 changed, 0 destroyed
68
- Duration: 0.3s
69
-
70
- Reactive changes detected
71
- + 6 to create
72
- + S3Bucket (Asset storage)
73
- + SecurityGroup (Network security)
74
- + Subnet × 4 (Multi-AZ subnets)
75
-
76
- Plan: 6 to add, 0 to change, 0 to destroy
77
- Reactive deployment cycle #2
78
- Apply complete! Resources: 6 added, 0 changed, 0 destroyed
79
- Duration: 0.1s
80
-
81
- Reactive changes detected
82
- + 4 to create
83
- + ElastiCache (Redis cluster)
84
- + CloudFront (CDN distribution)
85
- + LoadBalancer (Application load balancer)
86
- + RDSInstance (PostgreSQL database)
87
-
88
- Reactive deployment cycle #3
89
- Apply complete! Resources: 4 added, 0 changed, 0 destroyed
90
- Duration: 0.5s
91
-
92
- Reactive changes detected
93
- + 4 to create
94
- + Lambda × 3 (Regional API handlers: us-east-1, eu-west-1, ap-southeast-1)
95
- + Backup (Database backup vault)
96
-
97
- Reactive deployment cycle #4
98
- Apply complete! Resources: 4 added, 0 changed, 0 destroyed
99
- Duration: 0.5s
100
-
101
- Reactive changes detected
102
- + 3 to create
103
- + ApiGateway × 3 (Regional API endpoints)
104
-
105
- Reactive deployment cycle #5
106
- Apply complete! Resources: 3 added, 0 changed, 0 destroyed
107
- Duration: 0.4s
108
-
109
- Reactive changes detected
110
- + 3 to create
111
- + CloudWatch × 3 (Regional monitoring dashboards)
112
-
113
- Reactive deployment cycle #6
114
- Apply complete! Resources: 3 added, 0 changed, 0 destroyed
115
- Duration: 0.4s
116
-
117
- ✅ Deployment complete: 22 resources across 6 reactive cycles
118
- Watching for changes... (Press Ctrl+C to stop)
119
- ```
120
-
121
- **What happened:**
122
-
123
- VPC deployed first. When its outputs became available, subnets and security groups deployed in parallel. When those finished, database and cache deployed. When the database endpoint was ready, Lambdas deployed. When Lambdas got function ARNs, API Gateways deployed. When APIs were live, monitoring deployed.
124
-
125
- You didn't orchestrate anything manually. You just wrote JSX components that reference each other's outputs. CReact figured out the rest.
126
-
127
- ## How It Works
128
-
129
- ### JSX Compiles to CloudDOM
130
-
131
- ```tsx
132
- <VPC name="app-vpc" cidr="10.0.0.0/16" />
133
- ```
134
-
135
- becomes:
136
-
137
- ```json
138
- {
139
- "id": "app-vpc",
140
- "construct": "VPC",
141
- "props": { "cidr": "10.0.0.0/16" },
142
- "outputs": {}
143
- }
144
- ```
145
-
146
- CloudDOM is just a JSON tree. You can diff it like Git, version it, test it without touching AWS. It's the intermediate representation between your code and actual cloud resources.
147
-
148
- ### The Reconciler Figures Out What Changed
149
-
150
- Before deploying anything, CReact diffs the previous CloudDOM against the new one. Creates, updates, deletes, replacements - all computed ahead of time. You get a Terraform-style plan but it's just comparing two data structures.
151
-
152
- ### Deployment Is Checkpointed
153
-
154
- After each resource deploys, CReact saves a checkpoint. Crash halfway through? Run it again and it resumes from where it left off. No manual cleanup, no orphaned resources.
155
-
156
- ### Outputs Trigger Re-renders
157
-
158
- When a resource finishes deploying, its outputs (endpoint, ARN, ID) become available. Any component that depends on those outputs automatically re-renders and creates new resources. It's like useEffect but for infrastructure.
159
-
160
-
161
-
162
- ## Hooks
163
-
164
- ### useInstance - Create Resources
165
-
166
- Creates cloud resources and provides access to their outputs:
167
-
168
- ```tsx
169
- function DatabaseComponent() {
170
- // Create database resource
171
- const database = useInstance(Database, {
172
- name: 'my-db',
173
- engine: 'postgres'
174
- });
175
-
176
- // Access outputs after deployment
177
- console.log(database.outputs?.endpoint); // "db.example.com:5432"
178
-
179
- return <></>;
180
- }
181
- ```
182
-
183
- **Key Behavior**: If any prop value is `undefined`, creates a placeholder node that doesn't deploy. When dependencies become available, component re-renders and creates the real resource.
184
-
185
- ### useState - Persistent State
186
-
187
- Manages state that persists across deployments:
188
-
189
- ```tsx
190
- function App() {
191
- // State survives deployments (stored in backend)
192
- const [deployCount, setDeployCount] = useState(0);
193
- const [appVersion] = useState('1.0.0');
194
-
195
- // Update state (takes effect next deployment)
196
- setDeployCount(prev => (prev || 0) + 1);
197
-
198
- return <></>;
199
- }
200
- ```
201
-
202
- **Advanced**: Can bind to provider outputs for reactivity:
203
-
204
- ```tsx
205
- function DatabaseComponent() {
206
- const db = useInstance(Database, { name: 'my-db' });
207
-
208
- // This triggers re-renders when db.outputs.endpoint changes
209
- const [endpoint, setEndpoint] = useState(db.outputs?.endpoint);
210
-
211
- return <></>;
212
- }
213
- ```
214
-
215
- ### useContext - Share Dependencies
216
-
217
- Shares data between components with reactivity when containing outputs:
218
-
219
- ```tsx
220
- const DatabaseContext = createContext<{ endpoint?: string }>({});
221
-
222
- function DatabaseProvider({ children }) {
223
- const db = useInstance(Database, { name: 'my-db' });
224
-
225
- return (
226
- <DatabaseContext.Provider value={{
227
- endpoint: db.outputs?.endpoint // Makes context reactive
228
- }}>
229
- {children}
230
- </DatabaseContext.Provider>
231
- );
232
- }
233
-
234
- function ApiConsumer() {
235
- const { endpoint } = useContext(DatabaseContext); // Re-renders when endpoint available
236
-
237
- const api = useInstance(ApiGateway, {
238
- dbUrl: endpoint // Triggers re-render when endpoint changes
239
- });
240
-
241
- return <></>;
242
- }
243
- ```
244
-
245
- ## Reactivity Mechanisms
246
-
247
- CReact implements multiple interconnected reactivity systems:
248
-
249
- 1. **useState Binding**: `useState` can bind to provider outputs, triggering re-renders when outputs change
250
- 2. **Context Dependency Tracking**: Contexts containing outputs trigger re-renders in consuming components
251
- 3. **Output Change Detection**: Post-deployment effect system detects when provider outputs change
252
- 4. **Selective Re-rendering**: Only components affected by changes re-render
253
-
254
- ## Deployment Orchestration
255
-
256
- ### Reactive Deployment Cycles
257
-
258
- ```tsx
259
- function MultiLayerApp() {
260
- return (
261
- <NetworkStack> {/* Layer 1: Deploy VPC first */}
262
- <DatabaseStack> {/* Layer 2: Deploy DB/cache when VPC ready */}
263
- <StorageStack> {/* Layer 3: Deploy S3/CDN when network ready */}
264
- <ApiStack> {/* Layer 4: Deploy APIs when all deps ready */}
265
- <MonitoringStack /> {/* Layer 5: Deploy monitoring last */}
266
- </ApiStack>
267
- </StorageStack>
268
- </DatabaseStack>
269
- </NetworkStack>
270
- );
271
- }
272
- ```
273
-
274
- **Deployment Flow**:
275
- 1. **Initial Render**: All components create resource references
276
- 2. **Wave 1**: Network resources deploy (no dependencies)
277
- 3. **Wave 2**: Data resources deploy (depend on network outputs)
278
- 4. **Wave 3**: Storage resources deploy (depend on network outputs)
279
- 5. **Wave 4**: API resources deploy (depend on data + storage outputs)
280
- 6. **Wave 5**: Monitoring resources deploy (depend on API outputs)
281
-
282
- ## Provider Implementation
283
-
284
- ### Cloud Provider Example
285
-
286
- ```tsx
287
- class AWSProvider implements ICloudProvider {
288
- async materialize(resources: CloudDOMNode[]): Promise<void> {
289
- for (const resource of resources) {
290
- if (resource.construct?.name === 'Database') {
291
- const db = await this.createRDSInstance(resource.props);
292
- resource.outputs = {
293
- endpoint: db.endpoint,
294
- connectionUrl: db.connectionString
295
- };
296
- }
297
- // Handle other resource types...
298
- }
299
- }
300
- }
301
- ```
302
-
303
- ### Backend Provider Example
304
-
305
- ```tsx
306
- class S3BackendProvider implements IBackendProvider {
307
- async getState(stackName: string): Promise<any> {
308
- const state = await this.s3.getObject({
309
- Bucket: 'my-state-bucket',
310
- Key: `${stackName}.json`
311
- }).promise();
312
-
313
- return JSON.parse(state.Body.toString());
314
- }
315
-
316
- async saveState(stackName: string, state: any): Promise<void> {
317
- await this.s3.putObject({
318
- Bucket: 'my-state-bucket',
319
- Key: `${stackName}.json`,
320
- Body: JSON.stringify(state)
321
- }).promise();
322
- }
323
- }
324
- ```
325
-
326
- ## Real-World Example
327
-
328
- Here's how the 5-layer enterprise application from the examples works:
329
-
330
- ```tsx
331
- function EnterpriseApp() {
332
- return (
333
- <NetworkProvider> {/* VPC, subnets, security groups */}
334
- <DatabaseProvider> {/* Database + cache */}
335
- <StorageProvider> {/* S3 + CDN */}
336
- <ApiProvider> {/* APIs + load balancer */}
337
- <MonitoringProvider /> {/* Analytics + backups */}
338
- </ApiProvider>
339
- </StorageProvider>
340
- </DatabaseProvider>
341
- </NetworkProvider>
342
- );
343
- }
344
-
345
- // Each provider component:
346
- function NetworkProvider({ children }) {
347
- const vpc = useInstance(VPC, { cidr: '10.0.0.0/16' });
348
-
349
- return (
350
- <NetworkContext.Provider value={{ vpcId: vpc.outputs?.vpcId }}>
351
- {children}
352
- </NetworkContext.Provider>
353
- );
354
- }
355
-
356
- function DatabaseProvider({ children }) {
357
- const { vpcId } = useContext(NetworkContext);
358
-
359
- const db = useInstance(Database, { vpcId });
360
- const cache = useInstance(Redis, { vpcId });
361
-
362
- return (
363
- <DatabaseContext.Provider value={{
364
- dbUrl: db.outputs?.endpoint,
365
- cacheUrl: cache.outputs?.endpoint
366
- }}>
367
- {children}
368
- </DatabaseContext.Provider>
369
- );
370
- }
371
- ```
372
25
 
373
26
  ## Installation & Usage
374
27
 
@@ -129,7 +129,6 @@ class DevCommand extends BaseCommand_1.BaseCommand {
129
129
  try {
130
130
  const backendState = await result.instance.getBackendProvider().getState(result.stackName);
131
131
  previousCloudDOM = backendState?.cloudDOM || [];
132
- logger.debug(`DevCommand: Previous CloudDOM loaded with ${previousCloudDOM.length} resources`);
133
132
  }
134
133
  catch (error) {
135
134
  logger.debug(`DevCommand: Could not load previous state: ${error.message}`);
@@ -184,9 +183,19 @@ class DevCommand extends BaseCommand_1.BaseCommand {
184
183
  // Create new instance
185
184
  const result = await CLIContext_1.CLIContextManager.createCLIInstance(entryPath, this.verbose);
186
185
  logger.debug(`DevCommand: Hot reload CloudDOM built with ${result.cloudDOM.length} resources`);
186
+ // Load drift-corrected state for comparison
187
+ let previousCloudDOM = this.state.lastCloudDOM;
188
+ try {
189
+ const backendState = await result.instance.getBackendProvider().getState(result.stackName);
190
+ previousCloudDOM = backendState?.cloudDOM || this.state.lastCloudDOM;
191
+ logger.debug(`DevCommand: Comparing against drift-corrected state with ${previousCloudDOM?.length || 0} resources`);
192
+ }
193
+ catch (error) {
194
+ logger.debug(`DevCommand: Using cached state for comparison: ${error.message}`);
195
+ }
187
196
  // Compute diff using Reconciler
188
197
  const reconciler = new Reconciler_1.Reconciler();
189
- const changeSet = reconciler.reconcile(this.state.lastCloudDOM, result.cloudDOM);
198
+ const changeSet = reconciler.reconcile(previousCloudDOM, result.cloudDOM);
190
199
  const totalChanges = (0, Reconciler_1.getTotalChanges)(changeSet);
191
200
  logger.debug(`DevCommand: Hot reload total changes: ${totalChanges}`);
192
201
  // Always update state to preserve reactive changes
package/dist/cli/index.js CHANGED
File without changes
@@ -1096,9 +1096,28 @@ class CReact {
1096
1096
  CReact._lastInstance = instance;
1097
1097
  CReact._lastElement = element;
1098
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
+ }
1099
1106
  // CRITICAL: Load previous state from backend before rendering
1100
1107
  // This enables useState to hydrate from persisted state
1101
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
+ }
1102
1121
  // Build and return CloudDOM
1103
1122
  return instance.build(element, stackName);
1104
1123
  }
@@ -580,6 +580,13 @@ class Reconciler {
580
580
  });
581
581
  return 'replacement';
582
582
  }
583
+ // DRIFT DETECTION: If previous node has no outputs, it needs redeployment
584
+ // This happens when drift detection clears outputs for dead resources
585
+ // Treat as update to trigger redeployment
586
+ if (!previous.outputs || Object.keys(previous.outputs).length === 0) {
587
+ logger.debug(`Node ${current.id} has no outputs (drift detected) - needs redeployment`);
588
+ return 'update';
589
+ }
583
590
  // Hash-based diff acceleration: compute or reuse cached hashes
584
591
  const prevHash = ((_a = previous)._propHash ?? (_a._propHash = this.computeShallowHash(previous.props)));
585
592
  const currHash = ((_b = current)._propHash ?? (_b._propHash = this.computeShallowHash(current.props)));
@@ -421,4 +421,21 @@ export declare class StateMachine {
421
421
  changeSet?: ChangeSet;
422
422
  cloudDOM?: CloudDOMNode[];
423
423
  }>;
424
+ /**
425
+ * Detect and fix drift in deployed resources
426
+ *
427
+ * Checks if resources in the backend state still match reality.
428
+ * If drift is detected, refreshes the state to reflect actual cloud state.
429
+ *
430
+ * This is called automatically during state load to ensure state accuracy.
431
+ *
432
+ * @param stackName - Stack name to check for drift
433
+ * @param cloudProvider - Cloud provider with drift detection capabilities
434
+ * @returns Promise resolving to drift detection results
435
+ */
436
+ detectAndFixDrift(stackName: string, cloudProvider: import('../providers/ICloudProvider').ICloudProvider): Promise<{
437
+ driftDetected: boolean;
438
+ driftResults: import('../providers/ICloudProvider').DriftDetectionResult[];
439
+ resourcesFixed: number;
440
+ }>;
424
441
  }
@@ -771,6 +771,102 @@ class StateMachine {
771
771
  return { action: 'rolled_back' };
772
772
  }
773
773
  }
774
+ /**
775
+ * Detect and fix drift in deployed resources
776
+ *
777
+ * Checks if resources in the backend state still match reality.
778
+ * If drift is detected, refreshes the state to reflect actual cloud state.
779
+ *
780
+ * This is called automatically during state load to ensure state accuracy.
781
+ *
782
+ * @param stackName - Stack name to check for drift
783
+ * @param cloudProvider - Cloud provider with drift detection capabilities
784
+ * @returns Promise resolving to drift detection results
785
+ */
786
+ async detectAndFixDrift(stackName, cloudProvider) {
787
+ const state = await this.getState(stackName);
788
+ if (!state?.cloudDOM) {
789
+ return { driftDetected: false, driftResults: [], resourcesFixed: 0 };
790
+ }
791
+ logger.debug(`Detecting drift for stack: ${stackName}`);
792
+ const driftResults = [];
793
+ let resourcesFixed = 0;
794
+ for (const node of state.cloudDOM) {
795
+ // Skip nodes without outputs (not yet deployed)
796
+ if (!node.outputs) {
797
+ continue;
798
+ }
799
+ // Detect drift (required method)
800
+ const result = await cloudProvider.detectDrift(node);
801
+ driftResults.push(result);
802
+ if (result.hasDrifted) {
803
+ logger.info(`Drift detected: ${node.id} - ${result.driftDescription || 'State mismatch'}`);
804
+ // Refresh state to fix drift (required method)
805
+ logger.debug(`Refreshing state for: ${node.id}`);
806
+ await cloudProvider.refreshState(node);
807
+ resourcesFixed++;
808
+ }
809
+ }
810
+ // If any drift was detected, clear outputs for drifted resources and their children
811
+ // With the "one useInstance per component" constraint, dependencies = nesting
812
+ // So clearing a drifted node + its children ensures complete redeployment
813
+ if (resourcesFixed > 0) {
814
+ const driftedNodeIds = new Set(driftResults.filter(r => r.hasDrifted).map(r => r.nodeId));
815
+ logger.info(`Clearing outputs for ${driftedNodeIds.size} drifted resources and their children`);
816
+ // Clear outputs for drifted nodes and all their descendants
817
+ const clearDriftedOutputs = (nodes) => {
818
+ for (const node of nodes) {
819
+ const isDrifted = driftedNodeIds.has(node.id);
820
+ if (isDrifted && node.outputs) {
821
+ logger.debug(`Clearing outputs for drifted resource: ${node.id}`);
822
+ node.outputs = undefined;
823
+ }
824
+ // If this node is drifted, clear all its children too
825
+ if (node.children) {
826
+ if (isDrifted) {
827
+ // Clear all children of drifted nodes
828
+ const clearAllChildren = (childNodes) => {
829
+ for (const child of childNodes) {
830
+ if (child.outputs) {
831
+ logger.debug(`Clearing outputs for child of drifted resource: ${child.id}`);
832
+ child.outputs = undefined;
833
+ }
834
+ if (child.children) {
835
+ clearAllChildren(child.children);
836
+ }
837
+ }
838
+ };
839
+ clearAllChildren(node.children);
840
+ }
841
+ else {
842
+ // Continue searching for drifted nodes in children
843
+ clearDriftedOutputs(node.children);
844
+ }
845
+ }
846
+ }
847
+ };
848
+ clearDriftedOutputs(state.cloudDOM);
849
+ }
850
+ // If drift was detected and state was refreshed, save updated state
851
+ const driftDetected = driftResults.some(r => r.hasDrifted);
852
+ if (driftDetected && resourcesFixed > 0) {
853
+ logger.info(`Saving refreshed state after fixing ${resourcesFixed} drifted resources`);
854
+ await this.withRetry(() => this.backendProvider.saveState(stackName, {
855
+ ...state,
856
+ cloudDOM: state.cloudDOM,
857
+ timestamp: Date.now(),
858
+ }));
859
+ // Log drift detection to audit trail
860
+ await this.logAction(stackName, 'checkpoint', state);
861
+ }
862
+ if (driftDetected) {
863
+ logger.info(`Drift detection complete: ${driftResults.filter(r => r.hasDrifted).length} resources drifted, ${resourcesFixed} fixed`);
864
+ }
865
+ else {
866
+ logger.debug('No drift detected');
867
+ }
868
+ return { driftDetected, driftResults, resourcesFixed };
869
+ }
774
870
  }
775
871
  exports.StateMachine = StateMachine;
776
872
  /**
@@ -248,6 +248,40 @@ function useInstance(construct, props) {
248
248
  const { currentPath, previousOutputsMap } = context;
249
249
  // Get hook index for this useInstance call (instance-specific)
250
250
  const hookIndex = (0, context_1.incrementHookIndex)('instance');
251
+ // CONSTRAINT: Only one useInstance per component
252
+ // This simplifies dependency tracking and drift recovery
253
+ if (hookIndex > 0) {
254
+ const componentName = currentFiber.type?.name || 'Anonymous';
255
+ throw new Error(`[CReact Constraint] Only one useInstance call is allowed per component.\n\n` +
256
+ `Component: ${componentName}\n` +
257
+ `Path: ${currentPath.join('.')}\n\n` +
258
+ `This constraint ensures:\n` +
259
+ ` 1. Clear resource dependencies (parent-child nesting)\n` +
260
+ ` 2. Simpler drift recovery (clear drifted node + children)\n` +
261
+ ` 3. Better component composition\n\n` +
262
+ `Solution: Split your component into multiple components, one per resource.\n\n` +
263
+ `Example:\n` +
264
+ ` ❌ function Stack() {\n` +
265
+ ` const db = useInstance(Database, {...});\n` +
266
+ ` const api = useInstance(API, {...}); // Error!\n` +
267
+ ` }\n\n` +
268
+ ` ✅ function Stack() {\n` +
269
+ ` return (\n` +
270
+ ` <>\n` +
271
+ ` <PrimaryDatabase />\n` +
272
+ ` <ApiServer />\n` +
273
+ ` </>\n` +
274
+ ` );\n` +
275
+ ` }\n` +
276
+ ` function PrimaryDatabase() {\n` +
277
+ ` const db = useInstance(Database, {...});\n` +
278
+ ` return <></>;\n` +
279
+ ` }\n` +
280
+ ` function ApiServer() {\n` +
281
+ ` const api = useInstance(API, {...});\n` +
282
+ ` return <></>;\n` +
283
+ ` }\n`);
284
+ }
251
285
  // Extract key from props (React-like)
252
286
  const { key, ...restProps } = props;
253
287
  // Check for undefined dependencies - enforce deployment order
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export { Renderer } from './core/Renderer';
36
36
  export { Validator } from './core/Validator';
37
37
  export { CloudDOMBuilder } from './core/CloudDOMBuilder';
38
38
  export { FiberNode, CloudDOMNode } from './core/types';
39
- export { ICloudProvider } from './providers/ICloudProvider';
39
+ export { ICloudProvider, DriftDetectionResult, OutputChangeEvent } from './providers/ICloudProvider';
40
40
  export { IBackendProvider } from './providers/IBackendProvider';
41
41
  export { useInstance } from './hooks/useInstance';
42
42
  export { useState } from './hooks/useState';
@@ -40,6 +40,23 @@ export interface OutputChangeEvent {
40
40
  /** Timestamp of the change */
41
41
  timestamp: number;
42
42
  }
43
+ /**
44
+ * DriftDetectionResult represents the result of checking if a resource has drifted
45
+ */
46
+ export interface DriftDetectionResult {
47
+ /** Resource ID that was checked */
48
+ nodeId: string;
49
+ /** Whether the resource has drifted from expected state */
50
+ hasDrifted: boolean;
51
+ /** Expected state from CloudDOM */
52
+ expectedState?: Record<string, any>;
53
+ /** Actual state from cloud provider */
54
+ actualState?: Record<string, any>;
55
+ /** Human-readable description of the drift */
56
+ driftDescription?: string;
57
+ /** Timestamp of the check */
58
+ timestamp: number;
59
+ }
43
60
  /**
44
61
  * ICloudProvider defines the interface for cloud infrastructure providers.
45
62
  * Implementations materialize CloudDOM trees into actual cloud resources.
@@ -143,4 +160,71 @@ export interface ICloudProvider {
143
160
  * @param change - Output change details
144
161
  */
145
162
  emit?(event: 'outputsChanged', change: OutputChangeEvent): void;
163
+ /**
164
+ * Detect drift for a specific resource (REQUIRED)
165
+ *
166
+ * Compares the expected state (from CloudDOM) with the actual state
167
+ * (from the cloud provider) to detect if the resource has drifted.
168
+ *
169
+ * This is called by CReact automatically during:
170
+ * - Every state load (to detect stale state)
171
+ * - Plan command (to show drift before deployment)
172
+ * - Deploy command (to ensure state accuracy)
173
+ *
174
+ * Providers MUST implement this to ensure state accuracy.
175
+ *
176
+ * @param node - CloudDOM node representing expected state
177
+ * @returns Promise resolving to drift detection result
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * async detectDrift(node: CloudDOMNode): Promise<DriftDetectionResult> {
182
+ * // For resources without outputs, no drift possible
183
+ * if (!node.outputs) {
184
+ * return { nodeId: node.id, hasDrifted: false, timestamp: Date.now() };
185
+ * }
186
+ *
187
+ * const actualState = await this.getActualResourceState(node.id);
188
+ * const hasDrifted = !this.statesMatch(node.outputs, actualState);
189
+ *
190
+ * return {
191
+ * nodeId: node.id,
192
+ * hasDrifted,
193
+ * expectedState: node.outputs,
194
+ * actualState,
195
+ * driftDescription: hasDrifted ? 'Resource no longer exists' : undefined,
196
+ * timestamp: Date.now(),
197
+ * };
198
+ * }
199
+ * ```
200
+ */
201
+ detectDrift(node: CloudDOMNode): Promise<DriftDetectionResult>;
202
+ /**
203
+ * Refresh resource state from actual cloud provider (REQUIRED)
204
+ *
205
+ * Queries the actual state of a resource and updates the node's outputs
206
+ * to reflect reality. This is the mechanism for fixing drift.
207
+ *
208
+ * Called automatically by CReact when drift is detected.
209
+ *
210
+ * Providers MUST implement this to enable automatic drift recovery.
211
+ *
212
+ * @param node - CloudDOM node to refresh
213
+ * @returns Promise resolving when refresh is complete (node.outputs updated)
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * async refreshState(node: CloudDOMNode): Promise<void> {
218
+ * const actualState = await this.getActualResourceState(node.id);
219
+ * if (actualState) {
220
+ * // Resource exists - update outputs to match reality
221
+ * node.outputs = actualState;
222
+ * } else {
223
+ * // Resource doesn't exist - clear outputs to force redeployment
224
+ * node.outputs = undefined;
225
+ * }
226
+ * }
227
+ * ```
228
+ */
229
+ refreshState(node: CloudDOMNode): Promise<void>;
146
230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@creact-labs/creact",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CReact - React for Infrastructure. Render JSX to CloudDOM.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",