@creact-labs/creact 0.1.4 → 0.1.6

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.
@@ -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
  }
@@ -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
@@ -28,7 +28,7 @@
28
28
 
29
29
  */
30
30
  export { CReact, JSXElement } from './jsx';
31
- export type { FC, PropsWithChildren } from './jsx.d';
31
+ export type { FC, PropsWithChildren, ComponentProps } from './jsx';
32
32
  import { CReact as CReactClass } from './core/CReact';
33
33
  export { CReact as CReactCore, CReactConfig } from './core/CReact';
34
34
  export declare const renderCloudDOM: typeof CReactClass.renderCloudDOM;
@@ -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';
package/dist/index.js CHANGED
@@ -30,7 +30,6 @@
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
32
  exports.parseResourceId = exports.formatPath = exports.normalizePath = exports.normalizePathSegment = exports.validateIdUniqueness = exports.getNodeName = exports.toKebabCase = exports.generateResourceId = exports.createContext = exports.useEffect = exports.useContext = exports.useState = exports.useInstance = exports.CloudDOMBuilder = exports.Validator = exports.Renderer = exports.renderCloudDOM = exports.CReactCore = exports.CReact = void 0;
33
- /// <reference path="./jsx.d.ts" />
34
33
  // CReact - Infrastructure as Code with JSX
35
34
  // Main entry point for the library
36
35
  // JSX support
package/dist/jsx.d.ts CHANGED
@@ -62,3 +62,82 @@ export declare namespace CReact {
62
62
  children?: any;
63
63
  }) => JSXElement;
64
64
  }
65
+ /**
66
+ * Global JSX namespace declarations
67
+ *
68
+ * These declarations tell TypeScript how to validate JSX syntax and component props.
69
+ * By including them in this file (not a separate .d.ts), they are automatically
70
+ * included in the compiled output and available to package consumers.
71
+ */
72
+ declare global {
73
+ namespace JSX {
74
+ /**
75
+ * The type returned by JSX expressions
76
+ */
77
+ interface Element extends JSXElement {
78
+ }
79
+ /**
80
+ * Intrinsic elements (HTML-like elements)
81
+ * Empty for CReact - we only support component elements
82
+ */
83
+ interface IntrinsicElements {
84
+ }
85
+ /**
86
+ * Defines which prop contains children
87
+ * This allows TypeScript to understand the children prop
88
+ */
89
+ interface ElementChildrenAttribute {
90
+ children: {};
91
+ }
92
+ /**
93
+ * Base attributes available on all elements (including key)
94
+ * This applies to all JSX elements, both intrinsic and component-based
95
+ *
96
+ * IMPORTANT: This makes 'key' available on ALL JSX elements,
97
+ * including function components, without needing to add it to props
98
+ */
99
+ interface IntrinsicAttributes {
100
+ key?: string | number;
101
+ }
102
+ /**
103
+ * Attributes available on class components
104
+ */
105
+ interface IntrinsicClassAttributes<T> {
106
+ key?: string | number;
107
+ }
108
+ /**
109
+ * Tell TypeScript how to extract props from a component type
110
+ * This is critical for making LibraryManagedAttributes work
111
+ */
112
+ interface ElementAttributesProperty {
113
+ props: {};
114
+ }
115
+ /**
116
+ * Augment LibraryManagedAttributes to properly handle key prop
117
+ *
118
+ * This tells TypeScript that when checking JSX element props:
119
+ * 1. Take the component's declared props (P)
120
+ * 2. Make key optional (it's in IntrinsicAttributes but shouldn't be required)
121
+ * 3. Allow key to be passed even if not in component props
122
+ *
123
+ * We use Omit to remove 'key' from P if it exists, then add it back as optional
124
+ */
125
+ type LibraryManagedAttributes<C, P> = Omit<P, 'key'> & IntrinsicAttributes;
126
+ }
127
+ }
128
+ /**
129
+ * Type helper for component props with children
130
+ */
131
+ export interface PropsWithChildren {
132
+ children?: JSX.Element | JSX.Element[];
133
+ }
134
+ /**
135
+ * Type helper for functional components
136
+ * Note: key is handled by JSX.IntrinsicAttributes and doesn't need to be in props
137
+ */
138
+ export type FC<P = Record<string, unknown>> = (props: P & PropsWithChildren) => JSX.Element | null;
139
+ /**
140
+ * Type helper for component props that includes JSX attributes (like key)
141
+ * Use this when defining component prop types to allow key prop
142
+ */
143
+ export type ComponentProps<P = Record<string, unknown>> = P & JSX.IntrinsicAttributes;
@@ -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.4",
3
+ "version": "0.1.6",
4
4
  "description": "CReact - React for Infrastructure. Render JSX to CloudDOM.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",