@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.
- package/dist/core/CReact.js +19 -0
- package/dist/core/StateMachine.d.ts +17 -0
- package/dist/core/StateMachine.js +96 -0
- package/dist/hooks/useInstance.js +34 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +0 -1
- package/dist/jsx.d.ts +79 -0
- package/dist/providers/ICloudProvider.d.ts +84 -0
- package/package.json +1 -1
package/dist/core/CReact.js
CHANGED
|
@@ -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
|
|
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
|
}
|