@doeixd/machine 0.0.4 → 0.0.5
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 +952 -13
- package/dist/cjs/development/index.js +691 -0
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -1
- package/dist/esm/development/index.js +698 -0
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -1
- package/dist/types/extract.d.ts +71 -0
- package/dist/types/extract.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/multi.d.ts +838 -0
- package/dist/types/multi.d.ts.map +1 -0
- package/dist/types/primitives.d.ts +202 -0
- package/dist/types/primitives.d.ts.map +1 -0
- package/dist/types/runtime-extract.d.ts +53 -0
- package/dist/types/runtime-extract.d.ts.map +1 -0
- package/package.json +6 -2
- package/src/extract.ts +452 -67
- package/src/index.ts +49 -0
- package/src/multi.ts +1145 -0
- package/src/primitives.ts +134 -0
- package/src/react.ts +349 -28
- package/src/runtime-extract.ts +141 -0
package/src/extract.ts
CHANGED
|
@@ -10,12 +10,49 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @usage
|
|
12
12
|
* 1. Ensure you have `ts-node` and `ts-morph` installed: `npm install -D ts-node ts-morph`
|
|
13
|
-
* 2.
|
|
14
|
-
* 3. Run the script from your project root: `npx ts-node ./scripts/extract.ts
|
|
13
|
+
* 2. Create a configuration object or use .statechart.config.ts
|
|
14
|
+
* 3. Run the script from your project root: `npx ts-node ./scripts/extract-statechart.ts`
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { Project,
|
|
18
|
-
|
|
17
|
+
import { Project, Type, Node } from 'ts-morph';
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// SECTION: CONFIGURATION TYPES
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for a single machine to extract
|
|
25
|
+
*/
|
|
26
|
+
export interface MachineConfig {
|
|
27
|
+
/** Path to the source file containing the machine */
|
|
28
|
+
input: string;
|
|
29
|
+
/** Array of class names that represent states */
|
|
30
|
+
classes: string[];
|
|
31
|
+
/** Output file path (optional, defaults to stdout) */
|
|
32
|
+
output?: string;
|
|
33
|
+
/** Top-level ID for the statechart */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Name of the class that represents the initial state */
|
|
36
|
+
initialState: string;
|
|
37
|
+
/** Optional description of the machine */
|
|
38
|
+
description?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Global extraction configuration
|
|
43
|
+
*/
|
|
44
|
+
export interface ExtractionConfig {
|
|
45
|
+
/** Array of machines to extract */
|
|
46
|
+
machines: MachineConfig[];
|
|
47
|
+
/** Validate output against XState JSON schema (optional) */
|
|
48
|
+
validate?: boolean;
|
|
49
|
+
/** Output format (json, mermaid, or both) */
|
|
50
|
+
format?: 'json' | 'mermaid' | 'both';
|
|
51
|
+
/** Watch mode - auto-regenerate on file changes */
|
|
52
|
+
watch?: boolean;
|
|
53
|
+
/** Verbose logging */
|
|
54
|
+
verbose?: boolean;
|
|
55
|
+
}
|
|
19
56
|
|
|
20
57
|
// =============================================================================
|
|
21
58
|
// SECTION: CORE ANALYSIS LOGIC
|
|
@@ -27,9 +64,10 @@ import { META_KEY } from './primitives';
|
|
|
27
64
|
* types into their string names.
|
|
28
65
|
*
|
|
29
66
|
* @param type - The `ts-morph` Type object to serialize.
|
|
67
|
+
* @param verbose - Enable debug logging
|
|
30
68
|
* @returns A JSON-compatible value (string, number, object, array).
|
|
31
69
|
*/
|
|
32
|
-
function typeToJson(type: Type): any {
|
|
70
|
+
function typeToJson(type: Type, verbose = false): any {
|
|
33
71
|
// --- Terminal Types ---
|
|
34
72
|
const symbol = type.getSymbol();
|
|
35
73
|
if (symbol && symbol.getDeclarations().some(Node.isClassDeclaration)) {
|
|
@@ -38,42 +76,273 @@ function typeToJson(type: Type): any {
|
|
|
38
76
|
if (type.isStringLiteral()) return type.getLiteralValue();
|
|
39
77
|
if (type.isNumberLiteral()) return type.getLiteralValue();
|
|
40
78
|
if (type.isBooleanLiteral()) return type.getLiteralValue();
|
|
79
|
+
if (type.isString()) return 'string';
|
|
80
|
+
if (type.isNumber()) return 'number';
|
|
81
|
+
if (type.isBoolean()) return 'boolean';
|
|
41
82
|
|
|
42
83
|
// --- Recursive Types ---
|
|
43
84
|
if (type.isArray()) {
|
|
44
85
|
const elementType = type.getArrayElementTypeOrThrow();
|
|
45
|
-
return [typeToJson(elementType)];
|
|
86
|
+
return [typeToJson(elementType, verbose)];
|
|
46
87
|
}
|
|
47
|
-
|
|
88
|
+
|
|
89
|
+
// --- Object Types ---
|
|
90
|
+
if (type.isObject() || type.isIntersection()) {
|
|
48
91
|
const obj: { [key: string]: any } = {};
|
|
49
|
-
|
|
92
|
+
const properties = type.getProperties();
|
|
93
|
+
|
|
94
|
+
// Filter out symbol properties and internal properties
|
|
95
|
+
for (const prop of properties) {
|
|
96
|
+
const propName = prop.getName();
|
|
97
|
+
|
|
98
|
+
// Skip symbol properties (those starting with "__@")
|
|
99
|
+
if (propName.startsWith('__@')) continue;
|
|
100
|
+
|
|
50
101
|
const declaration = prop.getValueDeclaration();
|
|
51
102
|
if (!declaration) continue;
|
|
52
|
-
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
obj[propName] = typeToJson(declaration.getType(), verbose);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if (verbose) console.error(` Warning: Failed to serialize property ${propName}:`, e);
|
|
108
|
+
obj[propName] = 'unknown';
|
|
109
|
+
}
|
|
53
110
|
}
|
|
54
|
-
|
|
111
|
+
|
|
112
|
+
// If we got an empty object, return null (no metadata)
|
|
113
|
+
return Object.keys(obj).length > 0 ? obj : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (verbose) {
|
|
117
|
+
console.error(` Unhandled type: ${type.getText()}`);
|
|
55
118
|
}
|
|
56
119
|
|
|
57
120
|
return 'unknown'; // Fallback for unhandled types
|
|
58
121
|
}
|
|
59
122
|
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// SECTION: AST-BASED METADATA EXTRACTION
|
|
125
|
+
// =============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolves a class name from an AST node (handles identifiers and typeof expressions)
|
|
129
|
+
*/
|
|
130
|
+
function resolveClassName(node: Node): string {
|
|
131
|
+
// Handle: LoggingInMachine
|
|
132
|
+
if (Node.isIdentifier(node)) {
|
|
133
|
+
return node.getText();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle: typeof LoggingInMachine
|
|
137
|
+
if (Node.isTypeOfExpression(node)) {
|
|
138
|
+
return node.getExpression().getText();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return 'unknown';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parses an object literal expression into a plain JavaScript object
|
|
146
|
+
*/
|
|
147
|
+
function parseObjectLiteral(obj: Node): any {
|
|
148
|
+
if (!Node.isObjectLiteralExpression(obj)) {
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const result: any = {};
|
|
153
|
+
|
|
154
|
+
for (const prop of obj.getProperties()) {
|
|
155
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
156
|
+
const name = prop.getName();
|
|
157
|
+
const init = prop.getInitializer();
|
|
158
|
+
|
|
159
|
+
if (init) {
|
|
160
|
+
if (Node.isStringLiteral(init)) {
|
|
161
|
+
result[name] = init.getLiteralValue();
|
|
162
|
+
} else if (Node.isNumericLiteral(init)) {
|
|
163
|
+
result[name] = init.getLiteralValue();
|
|
164
|
+
} else if (init.getText() === 'true' || init.getText() === 'false') {
|
|
165
|
+
result[name] = init.getText() === 'true';
|
|
166
|
+
} else if (Node.isIdentifier(init)) {
|
|
167
|
+
result[name] = init.getText();
|
|
168
|
+
} else if (Node.isObjectLiteralExpression(init)) {
|
|
169
|
+
result[name] = parseObjectLiteral(init);
|
|
170
|
+
} else if (Node.isArrayLiteralExpression(init)) {
|
|
171
|
+
result[name] = init.getElements().map(el => {
|
|
172
|
+
if (Node.isObjectLiteralExpression(el)) {
|
|
173
|
+
return parseObjectLiteral(el);
|
|
174
|
+
}
|
|
175
|
+
return el.getText();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Parses an invoke service configuration, resolving class names for onDone/onError
|
|
187
|
+
*/
|
|
188
|
+
function parseInvokeService(obj: Node): any {
|
|
189
|
+
if (!Node.isObjectLiteralExpression(obj)) {
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const service: any = {};
|
|
194
|
+
|
|
195
|
+
for (const prop of obj.getProperties()) {
|
|
196
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
197
|
+
const name = prop.getName();
|
|
198
|
+
const init = prop.getInitializer();
|
|
199
|
+
|
|
200
|
+
if (!init) continue;
|
|
201
|
+
|
|
202
|
+
if (name === 'onDone' || name === 'onError') {
|
|
203
|
+
// Resolve class names for state targets
|
|
204
|
+
service[name] = resolveClassName(init);
|
|
205
|
+
} else if (Node.isStringLiteral(init)) {
|
|
206
|
+
service[name] = init.getLiteralValue();
|
|
207
|
+
} else if (Node.isIdentifier(init)) {
|
|
208
|
+
service[name] = init.getText();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return service;
|
|
214
|
+
}
|
|
215
|
+
|
|
60
216
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
217
|
+
* Recursively extracts metadata from a call expression chain
|
|
218
|
+
* Handles nested DSL primitive calls like: describe(text, guarded(guard, transitionTo(...)))
|
|
219
|
+
*/
|
|
220
|
+
function extractFromCallExpression(call: Node, verbose = false): any | null {
|
|
221
|
+
if (!Node.isCallExpression(call)) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const expression = call.getExpression();
|
|
226
|
+
const fnName = Node.isIdentifier(expression) ? expression.getText() : null;
|
|
227
|
+
|
|
228
|
+
if (!fnName) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const metadata: any = {};
|
|
233
|
+
const args = call.getArguments();
|
|
234
|
+
|
|
235
|
+
switch (fnName) {
|
|
236
|
+
case 'transitionTo':
|
|
237
|
+
// Args: (target, implementation)
|
|
238
|
+
if (args[0]) {
|
|
239
|
+
metadata.target = resolveClassName(args[0]);
|
|
240
|
+
}
|
|
241
|
+
// The second argument might be another call expression, but we don't recurse there
|
|
242
|
+
// because transitionTo is the innermost wrapper
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'describe':
|
|
246
|
+
// Args: (description, transition)
|
|
247
|
+
if (args[0] && Node.isStringLiteral(args[0])) {
|
|
248
|
+
metadata.description = args[0].getLiteralValue();
|
|
249
|
+
}
|
|
250
|
+
// Recurse into wrapped transition
|
|
251
|
+
if (args[1] && Node.isCallExpression(args[1])) {
|
|
252
|
+
const nested = extractFromCallExpression(args[1], verbose);
|
|
253
|
+
if (nested) {
|
|
254
|
+
Object.assign(metadata, nested);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case 'guarded':
|
|
260
|
+
// Args: (guard, transition)
|
|
261
|
+
if (args[0]) {
|
|
262
|
+
const guard = parseObjectLiteral(args[0]);
|
|
263
|
+
if (Object.keys(guard).length > 0) {
|
|
264
|
+
metadata.guards = [guard];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Recurse into wrapped transition
|
|
268
|
+
if (args[1] && Node.isCallExpression(args[1])) {
|
|
269
|
+
const nested = extractFromCallExpression(args[1], verbose);
|
|
270
|
+
if (nested) {
|
|
271
|
+
Object.assign(metadata, nested);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'invoke':
|
|
277
|
+
// Args: (service, implementation)
|
|
278
|
+
if (args[0]) {
|
|
279
|
+
const service = parseInvokeService(args[0]);
|
|
280
|
+
if (Object.keys(service).length > 0) {
|
|
281
|
+
metadata.invoke = service;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
|
|
286
|
+
case 'action':
|
|
287
|
+
// Args: (action, transition)
|
|
288
|
+
if (args[0]) {
|
|
289
|
+
const actionMeta = parseObjectLiteral(args[0]);
|
|
290
|
+
if (Object.keys(actionMeta).length > 0) {
|
|
291
|
+
metadata.actions = [actionMeta];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Recurse into wrapped transition
|
|
295
|
+
if (args[1] && Node.isCallExpression(args[1])) {
|
|
296
|
+
const nested = extractFromCallExpression(args[1], verbose);
|
|
297
|
+
if (nested) {
|
|
298
|
+
Object.assign(metadata, nested);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
|
|
303
|
+
default:
|
|
304
|
+
// Not a DSL primitive we recognize
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return Object.keys(metadata).length > 0 ? metadata : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Extracts metadata by parsing the AST of DSL primitive calls.
|
|
313
|
+
* This is the new approach that solves the generic type parameter resolution problem.
|
|
63
314
|
*
|
|
64
|
-
* @param
|
|
315
|
+
* @param member - The class member (property declaration) to analyze
|
|
316
|
+
* @param verbose - Enable debug logging
|
|
65
317
|
* @returns The extracted metadata object, or `null` if no metadata is found.
|
|
66
318
|
*/
|
|
67
|
-
function
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
319
|
+
function extractMetaFromMember(member: Node, verbose = false): any | null {
|
|
320
|
+
// Only process property declarations (methods with initializers)
|
|
321
|
+
if (!Node.isPropertyDeclaration(member)) {
|
|
322
|
+
if (verbose) console.error(` ⚠️ Not a property declaration`);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const initializer = member.getInitializer();
|
|
327
|
+
if (!initializer) {
|
|
328
|
+
if (verbose) console.error(` ⚠️ No initializer`);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check if it's a call expression (DSL primitive call)
|
|
333
|
+
if (!Node.isCallExpression(initializer)) {
|
|
334
|
+
if (verbose) console.error(` ⚠️ Initializer is not a call expression`);
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Extract metadata by parsing the call chain
|
|
339
|
+
const metadata = extractFromCallExpression(initializer, verbose);
|
|
72
340
|
|
|
73
|
-
|
|
74
|
-
|
|
341
|
+
if (metadata && verbose) {
|
|
342
|
+
console.error(` ✅ Extracted metadata:`, JSON.stringify(metadata, null, 2));
|
|
343
|
+
}
|
|
75
344
|
|
|
76
|
-
return
|
|
345
|
+
return metadata;
|
|
77
346
|
}
|
|
78
347
|
|
|
79
348
|
/**
|
|
@@ -81,22 +350,41 @@ function extractMetaFromType(type: Type): any | null {
|
|
|
81
350
|
* building a state node definition for the final statechart.
|
|
82
351
|
*
|
|
83
352
|
* @param classSymbol - The `ts-morph` Symbol for the class to analyze.
|
|
353
|
+
* @param verbose - Enable verbose logging
|
|
84
354
|
* @returns A state node object (e.g., `{ on: {...}, invoke: [...] }`).
|
|
85
355
|
*/
|
|
86
|
-
function analyzeStateNode(classSymbol:
|
|
356
|
+
function analyzeStateNode(classSymbol: any, verbose = false): object {
|
|
87
357
|
const chartNode: any = { on: {} };
|
|
88
358
|
const classDeclaration = classSymbol.getDeclarations()[0];
|
|
89
359
|
if (!classDeclaration || !Node.isClassDeclaration(classDeclaration)) {
|
|
360
|
+
if (verbose) {
|
|
361
|
+
console.error(`⚠️ Warning: Could not get class declaration for ${classSymbol.getName()}`);
|
|
362
|
+
}
|
|
90
363
|
return chartNode;
|
|
91
364
|
}
|
|
92
365
|
|
|
366
|
+
const className = classSymbol.getName();
|
|
367
|
+
if (verbose) {
|
|
368
|
+
console.error(` Analyzing state: ${className}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
93
371
|
for (const member of classDeclaration.getInstanceMembers()) {
|
|
94
|
-
const
|
|
372
|
+
const memberName = member.getName();
|
|
373
|
+
if (verbose) {
|
|
374
|
+
console.error(` Checking member: ${memberName}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// NEW: Use AST-based extraction instead of type-based
|
|
378
|
+
const meta = extractMetaFromMember(member, verbose);
|
|
95
379
|
if (!meta) continue;
|
|
96
380
|
|
|
381
|
+
if (verbose) {
|
|
382
|
+
console.error(` Found transition: ${memberName}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
97
385
|
// Separate `invoke` metadata from standard `on` transitions, as it's a
|
|
98
386
|
// special property of a state node in XState/Stately syntax.
|
|
99
|
-
const { invoke, ...onEntry } = meta;
|
|
387
|
+
const { invoke, actions, guards, ...onEntry } = meta;
|
|
100
388
|
|
|
101
389
|
if (invoke) {
|
|
102
390
|
if (!chartNode.invoke) chartNode.invoke = [];
|
|
@@ -106,15 +394,40 @@ function analyzeStateNode(classSymbol: TSSymbol): object {
|
|
|
106
394
|
onError: { target: invoke.onError },
|
|
107
395
|
description: invoke.description,
|
|
108
396
|
});
|
|
397
|
+
if (verbose) {
|
|
398
|
+
console.error(` → Invoke: ${invoke.src}`);
|
|
399
|
+
}
|
|
109
400
|
}
|
|
110
401
|
|
|
111
402
|
// If there's a target, it's a standard event transition.
|
|
112
403
|
if (onEntry.target) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
404
|
+
const transition: any = { target: onEntry.target };
|
|
405
|
+
|
|
406
|
+
// Add description if present
|
|
407
|
+
if (onEntry.description) {
|
|
408
|
+
transition.description = onEntry.description;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Add guards as 'cond' property
|
|
412
|
+
if (guards) {
|
|
413
|
+
transition.cond = guards.map((g: any) => g.name).join(' && ');
|
|
414
|
+
if (verbose) {
|
|
415
|
+
console.error(` → Guard: ${transition.cond}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Add actions array
|
|
420
|
+
if (actions && actions.length > 0) {
|
|
421
|
+
transition.actions = actions.map((a: any) => a.name);
|
|
422
|
+
if (verbose) {
|
|
423
|
+
console.error(` → Actions: ${transition.actions.join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
chartNode.on[memberName] = transition;
|
|
428
|
+
if (verbose) {
|
|
429
|
+
console.error(` → Target: ${onEntry.target}`);
|
|
116
430
|
}
|
|
117
|
-
chartNode.on[member.getName()] = onEntry;
|
|
118
431
|
}
|
|
119
432
|
}
|
|
120
433
|
|
|
@@ -126,62 +439,134 @@ function analyzeStateNode(classSymbol: TSSymbol): object {
|
|
|
126
439
|
// =============================================================================
|
|
127
440
|
|
|
128
441
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
442
|
+
* Extracts a single machine configuration to a statechart
|
|
443
|
+
*
|
|
444
|
+
* @param config - Machine configuration
|
|
445
|
+
* @param project - ts-morph Project instance
|
|
446
|
+
* @param verbose - Enable verbose logging
|
|
447
|
+
* @returns The generated statechart object
|
|
132
448
|
*/
|
|
133
|
-
export function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"LoggedOutMachine",
|
|
143
|
-
"LoggedInMachine",
|
|
144
|
-
];
|
|
145
|
-
|
|
146
|
-
/** The top-level ID for your statechart. */
|
|
147
|
-
const chartId = "auth";
|
|
148
|
-
|
|
149
|
-
/** The string name of the class that represents the initial state. */
|
|
150
|
-
const initialState = "LoggedOutMachine";
|
|
151
|
-
|
|
152
|
-
// --- End Configuration ---
|
|
153
|
-
|
|
154
|
-
console.error("🔍 Analyzing state machine from:", sourceFilePath);
|
|
155
|
-
|
|
156
|
-
const project = new Project();
|
|
157
|
-
project.addSourceFilesAtPaths("src/**/*.ts");
|
|
449
|
+
export function extractMachine(
|
|
450
|
+
config: MachineConfig,
|
|
451
|
+
project: Project,
|
|
452
|
+
verbose = false
|
|
453
|
+
): any {
|
|
454
|
+
if (verbose) {
|
|
455
|
+
console.error(`\n🔍 Analyzing machine: ${config.id}`);
|
|
456
|
+
console.error(` Source: ${config.input}`);
|
|
457
|
+
}
|
|
158
458
|
|
|
159
|
-
const sourceFile = project.getSourceFile(
|
|
459
|
+
const sourceFile = project.getSourceFile(config.input);
|
|
160
460
|
if (!sourceFile) {
|
|
161
|
-
|
|
162
|
-
process.exit(1);
|
|
461
|
+
throw new Error(`Source file not found: ${config.input}`);
|
|
163
462
|
}
|
|
164
463
|
|
|
165
464
|
const fullChart: any = {
|
|
166
|
-
id:
|
|
167
|
-
initial: initialState,
|
|
465
|
+
id: config.id,
|
|
466
|
+
initial: config.initialState,
|
|
168
467
|
states: {},
|
|
169
468
|
};
|
|
170
469
|
|
|
171
|
-
|
|
470
|
+
if (config.description) {
|
|
471
|
+
fullChart.description = config.description;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const className of config.classes) {
|
|
172
475
|
const classDeclaration = sourceFile.getClass(className);
|
|
173
476
|
if (!classDeclaration) {
|
|
174
|
-
console.warn(`⚠️ Warning: Class '${className}' not found in '${
|
|
477
|
+
console.warn(`⚠️ Warning: Class '${className}' not found in '${config.input}'. Skipping.`);
|
|
175
478
|
continue;
|
|
176
479
|
}
|
|
177
480
|
const classSymbol = classDeclaration.getSymbolOrThrow();
|
|
178
|
-
const stateNode = analyzeStateNode(classSymbol);
|
|
481
|
+
const stateNode = analyzeStateNode(classSymbol, verbose);
|
|
179
482
|
fullChart.states[className] = stateNode;
|
|
180
483
|
}
|
|
181
484
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
485
|
+
if (verbose) {
|
|
486
|
+
console.error(` ✅ Extracted ${config.classes.length} states`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return fullChart;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Extracts multiple machines based on configuration
|
|
494
|
+
*
|
|
495
|
+
* @param config - Full extraction configuration
|
|
496
|
+
* @returns Array of generated statecharts
|
|
497
|
+
*/
|
|
498
|
+
export function extractMachines(config: ExtractionConfig): any[] {
|
|
499
|
+
const verbose = config.verbose ?? false;
|
|
500
|
+
|
|
501
|
+
if (verbose) {
|
|
502
|
+
console.error(`\n📊 Starting statechart extraction`);
|
|
503
|
+
console.error(` Machines to extract: ${config.machines.length}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const project = new Project();
|
|
507
|
+
project.addSourceFilesAtPaths("src/**/*.ts");
|
|
508
|
+
project.addSourceFilesAtPaths("examples/**/*.ts");
|
|
509
|
+
|
|
510
|
+
const results: any[] = [];
|
|
511
|
+
|
|
512
|
+
for (const machineConfig of config.machines) {
|
|
513
|
+
try {
|
|
514
|
+
const chart = extractMachine(machineConfig, project, verbose);
|
|
515
|
+
results.push(chart);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.error(`❌ Error extracting machine '${machineConfig.id}':`, error);
|
|
518
|
+
if (!verbose) {
|
|
519
|
+
console.error(` Run with --verbose for more details`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (verbose) {
|
|
525
|
+
console.error(`\n✅ Extraction complete: ${results.length}/${config.machines.length} machines extracted`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return results;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Legacy function for backwards compatibility
|
|
533
|
+
* Extracts a single hardcoded machine configuration
|
|
534
|
+
* @deprecated Use extractMachine or extractMachines instead
|
|
535
|
+
*/
|
|
536
|
+
export function generateChart() {
|
|
537
|
+
// --- 🎨 CONFIGURATION 🎨 ---
|
|
538
|
+
// Adjust these settings to match your project structure.
|
|
539
|
+
|
|
540
|
+
const config: MachineConfig = {
|
|
541
|
+
input: "examples/authMachine.ts",
|
|
542
|
+
classes: [
|
|
543
|
+
"LoggedOutMachine",
|
|
544
|
+
"LoggingInMachine",
|
|
545
|
+
"LoggedInMachine",
|
|
546
|
+
"SessionExpiredMachine",
|
|
547
|
+
"ErrorMachine"
|
|
548
|
+
],
|
|
549
|
+
id: "auth",
|
|
550
|
+
initialState: "LoggedOutMachine",
|
|
551
|
+
description: "Authentication state machine"
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// --- End Configuration ---
|
|
555
|
+
|
|
556
|
+
console.error("🔍 Using legacy generateChart function");
|
|
557
|
+
console.error("⚠️ Consider using extractMachines() with a config file instead\n");
|
|
558
|
+
|
|
559
|
+
const project = new Project();
|
|
560
|
+
project.addSourceFilesAtPaths("src/**/*.ts");
|
|
561
|
+
project.addSourceFilesAtPaths("examples/**/*.ts");
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const chart = extractMachine(config, project, true);
|
|
565
|
+
console.log(JSON.stringify(chart, null, 2));
|
|
566
|
+
} catch (error) {
|
|
567
|
+
console.error(`❌ Error:`, error);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
185
570
|
}
|
|
186
571
|
|
|
187
572
|
// This allows the script to be executed directly from the command line.
|
package/src/index.ts
CHANGED
|
@@ -526,3 +526,52 @@ export {
|
|
|
526
526
|
runAsync,
|
|
527
527
|
stepAsync
|
|
528
528
|
} from './generators';
|
|
529
|
+
|
|
530
|
+
// =============================================================================
|
|
531
|
+
// SECTION: TYPE-LEVEL METADATA PRIMITIVES
|
|
532
|
+
// =============================================================================
|
|
533
|
+
|
|
534
|
+
export {
|
|
535
|
+
transitionTo,
|
|
536
|
+
describe,
|
|
537
|
+
guarded,
|
|
538
|
+
invoke,
|
|
539
|
+
action,
|
|
540
|
+
metadata,
|
|
541
|
+
META_KEY,
|
|
542
|
+
type TransitionMeta,
|
|
543
|
+
type GuardMeta,
|
|
544
|
+
type InvokeMeta,
|
|
545
|
+
type ActionMeta,
|
|
546
|
+
type ClassConstructor,
|
|
547
|
+
type WithMeta
|
|
548
|
+
} from './primitives';
|
|
549
|
+
|
|
550
|
+
// =============================================================================
|
|
551
|
+
// SECTION: STATECHART EXTRACTION
|
|
552
|
+
// =============================================================================
|
|
553
|
+
|
|
554
|
+
export {
|
|
555
|
+
extractMachine,
|
|
556
|
+
extractMachines,
|
|
557
|
+
generateChart,
|
|
558
|
+
type MachineConfig,
|
|
559
|
+
type ExtractionConfig
|
|
560
|
+
} from './extract';
|
|
561
|
+
|
|
562
|
+
// =============================================================================
|
|
563
|
+
// SECTION: RUNTIME EXTRACTION
|
|
564
|
+
// =============================================================================
|
|
565
|
+
|
|
566
|
+
export {
|
|
567
|
+
extractFunctionMetadata,
|
|
568
|
+
extractStateNode,
|
|
569
|
+
generateStatechart,
|
|
570
|
+
extractFromInstance
|
|
571
|
+
} from './runtime-extract';
|
|
572
|
+
|
|
573
|
+
// Export runtime metadata symbol and type (for advanced use)
|
|
574
|
+
export { RUNTIME_META, type RuntimeTransitionMeta } from './primitives';
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
export * from './multi'
|