@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/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
+ }
@@ -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
+ }