@doeixd/machine 0.0.4
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/LICENSE +7 -0
- package/README.md +1070 -0
- package/dist/cjs/development/index.js +198 -0
- package/dist/cjs/development/index.js.map +7 -0
- package/dist/cjs/production/index.js +1 -0
- package/dist/esm/development/index.js +175 -0
- package/dist/esm/development/index.js.map +7 -0
- package/dist/esm/production/index.js +1 -0
- package/dist/types/generators.d.ts +314 -0
- package/dist/types/generators.d.ts.map +1 -0
- package/dist/types/index.d.ts +339 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +110 -0
- package/src/devtools.ts +74 -0
- package/src/extract.ts +190 -0
- package/src/generators.ts +421 -0
- package/src/index.ts +528 -0
- package/src/primitives.ts +191 -0
- package/src/react.ts +44 -0
- package/src/solid.ts +502 -0
- package/src/test.ts +207 -0
- package/src/utils.ts +167 -0
package/package.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@doeixd/machine",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist",
|
|
6
|
+
"src"
|
|
7
|
+
],
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=16"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"state-machine",
|
|
14
|
+
"finite-state-machine",
|
|
15
|
+
"fsm",
|
|
16
|
+
"statechart",
|
|
17
|
+
"type-state",
|
|
18
|
+
"typescript",
|
|
19
|
+
"type-safe",
|
|
20
|
+
"immutable",
|
|
21
|
+
"functional",
|
|
22
|
+
"reactive",
|
|
23
|
+
"state-management",
|
|
24
|
+
"react",
|
|
25
|
+
"solid-js",
|
|
26
|
+
"async",
|
|
27
|
+
"transitions",
|
|
28
|
+
"minimal",
|
|
29
|
+
"lightweight",
|
|
30
|
+
"xstate-alternative",
|
|
31
|
+
"state-pattern",
|
|
32
|
+
"type-driven",
|
|
33
|
+
"compile-time-safety",
|
|
34
|
+
"zero-runtime",
|
|
35
|
+
"discriminated-unions"
|
|
36
|
+
],
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
39
|
+
"solid-js": "^1.8.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"react": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"solid-js": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.10.2",
|
|
51
|
+
"@types/react": "^19.2.4",
|
|
52
|
+
"ajv": "^8.17.1",
|
|
53
|
+
"chalk": "^5.6.2",
|
|
54
|
+
"chokidar": "^4.0.3",
|
|
55
|
+
"commander": "^14.0.2",
|
|
56
|
+
"pridepack": "2.6.4",
|
|
57
|
+
"react": "^19.2.0",
|
|
58
|
+
"solid-js": "^1.8.22",
|
|
59
|
+
"standard-version": "^9.5.0",
|
|
60
|
+
"tslib": "^2.8.1",
|
|
61
|
+
"tsx": "^4.20.6",
|
|
62
|
+
"typescript": "^5.7.2",
|
|
63
|
+
"vitest": "^2.1.8"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"release": "standard-version && git push --follow-tags origin main",
|
|
67
|
+
"prepublishOnly": "pridepack clean && pridepack build",
|
|
68
|
+
"build": "pridepack build",
|
|
69
|
+
"type-check": "pridepack check",
|
|
70
|
+
"clean": "pridepack clean",
|
|
71
|
+
"watch": "pridepack watch",
|
|
72
|
+
"start": "pridepack start",
|
|
73
|
+
"dev": "pridepack dev",
|
|
74
|
+
"test": "vitest"
|
|
75
|
+
},
|
|
76
|
+
"private": false,
|
|
77
|
+
"description": "A minimal, type-safe state machine library for TypeScript centered on Type-State Programming. Uses the TypeScript compiler to catch invalid state transitions at compile-time, making illegal states unrepresentable. Features immutable updates, discriminated unions, async support, and integrations for React and Solid.js. Includes static analysis tools for generating formal statecharts compatible with Stately Viz and XState tooling.",
|
|
78
|
+
"repository": {
|
|
79
|
+
"url": "https://github.com/doeixd/machine.git",
|
|
80
|
+
"type": "git"
|
|
81
|
+
},
|
|
82
|
+
"homepage": "https://github.com/doeixd/machine#readme",
|
|
83
|
+
"bugs": {
|
|
84
|
+
"url": "https://github.com/doeixd/machine/issues"
|
|
85
|
+
},
|
|
86
|
+
"author": "Patrick Glenn",
|
|
87
|
+
"publishConfig": {
|
|
88
|
+
"access": "public"
|
|
89
|
+
},
|
|
90
|
+
"dependencies": {
|
|
91
|
+
"ts-morph": "^27.0.2"
|
|
92
|
+
},
|
|
93
|
+
"types": "./dist/types/index.d.ts",
|
|
94
|
+
"main": "./dist/cjs/production/index.js",
|
|
95
|
+
"module": "./dist/esm/production/index.js",
|
|
96
|
+
"exports": {
|
|
97
|
+
".": {
|
|
98
|
+
"types": "./dist/types/index.d.ts",
|
|
99
|
+
"development": {
|
|
100
|
+
"require": "./dist/cjs/development/index.js",
|
|
101
|
+
"import": "./dist/esm/development/index.js"
|
|
102
|
+
},
|
|
103
|
+
"require": "./dist/cjs/production/index.js",
|
|
104
|
+
"import": "./dist/esm/production/index.js"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"typesVersions": {
|
|
108
|
+
"*": {}
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Browser DevTools integration for @doeixd/machine
|
|
3
|
+
* @description Connects state machines to browser extension for visualization and debugging
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { runMachine, Event, AsyncMachine } from './index';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* DevTools interface for browser extension communication
|
|
10
|
+
*/
|
|
11
|
+
interface MachineDevTools {
|
|
12
|
+
init(context: any): void;
|
|
13
|
+
send(message: { type: string; payload: any }): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Augment Window interface to include DevTools extension
|
|
18
|
+
*/
|
|
19
|
+
declare global {
|
|
20
|
+
interface Window {
|
|
21
|
+
__MACHINE_DEVTOOLS__?: MachineDevTools;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extended runner type with lastEvent tracking
|
|
27
|
+
*/
|
|
28
|
+
interface DevToolsRunner<M extends AsyncMachine<any>> extends ReturnType<typeof runMachine<any>> {
|
|
29
|
+
lastEvent?: Event<M>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Connects a state machine to the browser DevTools extension
|
|
34
|
+
* @template M - The async machine type
|
|
35
|
+
* @param initialMachine - The initial machine instance
|
|
36
|
+
* @returns A runner with DevTools integration
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const runner = connectToDevTools(createAuthMachine());
|
|
40
|
+
* runner.dispatch({ type: 'login', args: ['user'] });
|
|
41
|
+
*/
|
|
42
|
+
export function connectToDevTools<M extends AsyncMachine<any>>(
|
|
43
|
+
initialMachine: M
|
|
44
|
+
): DevToolsRunner<M> {
|
|
45
|
+
// Connect to the browser extension via window object or other means
|
|
46
|
+
const devTools = typeof window !== 'undefined' ? window.__MACHINE_DEVTOOLS__ : undefined;
|
|
47
|
+
if (!devTools) return runMachine(initialMachine) as DevToolsRunner<M>; // No DevTools, run normally
|
|
48
|
+
|
|
49
|
+
// The key is the onChange handler
|
|
50
|
+
const runner = runMachine(initialMachine, (nextState) => {
|
|
51
|
+
// This is where we send data to the extension
|
|
52
|
+
devTools.send({
|
|
53
|
+
type: 'STATE_CHANGED',
|
|
54
|
+
payload: {
|
|
55
|
+
// We need the event that *caused* this change
|
|
56
|
+
event: (runner as DevToolsRunner<M>).lastEvent,
|
|
57
|
+
// We serialize the context, not the whole class instance
|
|
58
|
+
context: nextState.context,
|
|
59
|
+
// The name of the new state's class is our state identifier
|
|
60
|
+
currentState: nextState.constructor.name,
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}) as DevToolsRunner<M>;
|
|
64
|
+
|
|
65
|
+
// We wrap the dispatch function to capture the event
|
|
66
|
+
const originalDispatch = runner.dispatch.bind(runner);
|
|
67
|
+
runner.dispatch = ((event: any) => {
|
|
68
|
+
(runner as DevToolsRunner<M>).lastEvent = event; // Capture the event
|
|
69
|
+
return originalDispatch(event);
|
|
70
|
+
}) as typeof runner.dispatch;
|
|
71
|
+
|
|
72
|
+
devTools.init(initialMachine.context); // Send initial state
|
|
73
|
+
return runner;
|
|
74
|
+
}
|
package/src/extract.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Static Statechart Extractor for @doeixd/machine
|
|
3
|
+
* @description
|
|
4
|
+
* This build-time script uses the TypeScript Compiler API via `ts-morph` to analyze
|
|
5
|
+
* your machine source code. It reads the "type-level metadata" encoded by the
|
|
6
|
+
* primitives (`transitionTo`, `guarded`, etc.) and generates a formal, JSON-serializable
|
|
7
|
+
* statechart definition compatible with tools like Stately Viz.
|
|
8
|
+
*
|
|
9
|
+
* This script does NOT execute your code. It performs a purely static analysis of the types.
|
|
10
|
+
*
|
|
11
|
+
* @usage
|
|
12
|
+
* 1. Ensure you have `ts-node` and `ts-morph` installed: `npm install -D ts-node ts-morph`
|
|
13
|
+
* 2. Configure the settings in the `generateChart` function below.
|
|
14
|
+
* 3. Run the script from your project root: `npx ts-node ./scripts/extract.ts > chart.json`
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Project, ts, Type, Symbol as TSSymbol, Node } from 'ts-morph';
|
|
18
|
+
import { META_KEY } from './primitives';
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// SECTION: CORE ANALYSIS LOGIC
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recursively traverses a `ts-morph` Type object and serializes it into a
|
|
26
|
+
* plain JSON-compatible value. It's smart enough to resolve class constructor
|
|
27
|
+
* types into their string names.
|
|
28
|
+
*
|
|
29
|
+
* @param type - The `ts-morph` Type object to serialize.
|
|
30
|
+
* @returns A JSON-compatible value (string, number, object, array).
|
|
31
|
+
*/
|
|
32
|
+
function typeToJson(type: Type): any {
|
|
33
|
+
// --- Terminal Types ---
|
|
34
|
+
const symbol = type.getSymbol();
|
|
35
|
+
if (symbol && symbol.getDeclarations().some(Node.isClassDeclaration)) {
|
|
36
|
+
return symbol.getName(); // Resolve class types to their string name
|
|
37
|
+
}
|
|
38
|
+
if (type.isStringLiteral()) return type.getLiteralValue();
|
|
39
|
+
if (type.isNumberLiteral()) return type.getLiteralValue();
|
|
40
|
+
if (type.isBooleanLiteral()) return type.getLiteralValue();
|
|
41
|
+
|
|
42
|
+
// --- Recursive Types ---
|
|
43
|
+
if (type.isArray()) {
|
|
44
|
+
const elementType = type.getArrayElementTypeOrThrow();
|
|
45
|
+
return [typeToJson(elementType)]; // Note: Assumes homogenous array for simplicity
|
|
46
|
+
}
|
|
47
|
+
if (type.isObject()) {
|
|
48
|
+
const obj: { [key: string]: any } = {};
|
|
49
|
+
for (const prop of type.getProperties()) {
|
|
50
|
+
const declaration = prop.getValueDeclaration();
|
|
51
|
+
if (!declaration) continue;
|
|
52
|
+
obj[prop.getName()] = typeToJson(declaration.getType());
|
|
53
|
+
}
|
|
54
|
+
return obj;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 'unknown'; // Fallback for unhandled types
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Given a type, this function looks for our special `META_KEY` brand and,
|
|
62
|
+
* if found, extracts and serializes the metadata type into a plain object.
|
|
63
|
+
*
|
|
64
|
+
* @param type - The type of a class member (e.g., a transition method).
|
|
65
|
+
* @returns The extracted metadata object, or `null` if no metadata is found.
|
|
66
|
+
*/
|
|
67
|
+
function extractMetaFromType(type: Type): any | null {
|
|
68
|
+
// The META_KEY is escaped because it's a unique symbol, not a plain string property.
|
|
69
|
+
const escapedKey = String(ts.escapeLeadingUnderscores(META_KEY.description!));
|
|
70
|
+
const metaProperty = type.getProperty(escapedKey);
|
|
71
|
+
if (!metaProperty) return null;
|
|
72
|
+
|
|
73
|
+
const declaration = metaProperty.getValueDeclaration();
|
|
74
|
+
if (!declaration) return null;
|
|
75
|
+
|
|
76
|
+
return typeToJson(declaration.getType());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Analyzes a single class symbol to find all annotated transitions and effects,
|
|
81
|
+
* building a state node definition for the final statechart.
|
|
82
|
+
*
|
|
83
|
+
* @param classSymbol - The `ts-morph` Symbol for the class to analyze.
|
|
84
|
+
* @returns A state node object (e.g., `{ on: {...}, invoke: [...] }`).
|
|
85
|
+
*/
|
|
86
|
+
function analyzeStateNode(classSymbol: TSSymbol): object {
|
|
87
|
+
const chartNode: any = { on: {} };
|
|
88
|
+
const classDeclaration = classSymbol.getDeclarations()[0];
|
|
89
|
+
if (!classDeclaration || !Node.isClassDeclaration(classDeclaration)) {
|
|
90
|
+
return chartNode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const member of classDeclaration.getInstanceMembers()) {
|
|
94
|
+
const meta = extractMetaFromType(member.getType());
|
|
95
|
+
if (!meta) continue;
|
|
96
|
+
|
|
97
|
+
// Separate `invoke` metadata from standard `on` transitions, as it's a
|
|
98
|
+
// special property of a state node in XState/Stately syntax.
|
|
99
|
+
const { invoke, ...onEntry } = meta;
|
|
100
|
+
|
|
101
|
+
if (invoke) {
|
|
102
|
+
if (!chartNode.invoke) chartNode.invoke = [];
|
|
103
|
+
chartNode.invoke.push({
|
|
104
|
+
src: invoke.src,
|
|
105
|
+
onDone: { target: invoke.onDone },
|
|
106
|
+
onError: { target: invoke.onError },
|
|
107
|
+
description: invoke.description,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If there's a target, it's a standard event transition.
|
|
112
|
+
if (onEntry.target) {
|
|
113
|
+
if (onEntry.guards) {
|
|
114
|
+
// Stately/XState syntax for guards is the `cond` property.
|
|
115
|
+
onEntry.cond = onEntry.guards.map((g: any) => g.name).join(' && ');
|
|
116
|
+
}
|
|
117
|
+
chartNode.on[member.getName()] = onEntry;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return chartNode;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// SECTION: MAIN ORCHESTRATOR
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The main analysis function.
|
|
130
|
+
* Configures the project, specifies which files and classes to analyze,
|
|
131
|
+
* and orchestrates the generation of the final JSON chart to standard output.
|
|
132
|
+
*/
|
|
133
|
+
export function generateChart() {
|
|
134
|
+
// --- 🎨 CONFIGURATION 🎨 ---
|
|
135
|
+
// Adjust these settings to match your project structure.
|
|
136
|
+
|
|
137
|
+
/** The relative path to the file containing your machine class definitions. */
|
|
138
|
+
const sourceFilePath = "src/authMachine.ts";
|
|
139
|
+
|
|
140
|
+
/** An array of the string names of all classes that represent a state. */
|
|
141
|
+
const classesToAnalyze = [
|
|
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");
|
|
158
|
+
|
|
159
|
+
const sourceFile = project.getSourceFile(sourceFilePath);
|
|
160
|
+
if (!sourceFile) {
|
|
161
|
+
console.error(`❌ Error: Source file not found at '${sourceFilePath}'.`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const fullChart: any = {
|
|
166
|
+
id: chartId,
|
|
167
|
+
initial: initialState,
|
|
168
|
+
states: {},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const className of classesToAnalyze) {
|
|
172
|
+
const classDeclaration = sourceFile.getClass(className);
|
|
173
|
+
if (!classDeclaration) {
|
|
174
|
+
console.warn(`⚠️ Warning: Class '${className}' not found in '${sourceFilePath}'. Skipping.`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const classSymbol = classDeclaration.getSymbolOrThrow();
|
|
178
|
+
const stateNode = analyzeStateNode(classSymbol);
|
|
179
|
+
fullChart.states[className] = stateNode;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.error("✅ Analysis complete. Generating JSON chart...");
|
|
183
|
+
// Print the final JSON to stdout so it can be piped to a file.
|
|
184
|
+
console.log(JSON.stringify(fullChart, null, 2));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// This allows the script to be executed directly from the command line.
|
|
188
|
+
if (require.main === module) {
|
|
189
|
+
generateChart();
|
|
190
|
+
}
|