@xstate-devtools/adapter 0.1.3 → 0.1.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 +70 -0
- package/dist/chunk-QLRTDBT5.js +300 -0
- package/dist/chunk-QLRTDBT5.js.map +1 -0
- package/dist/chunk-SUDAY5H3.js +34 -0
- package/dist/chunk-SUDAY5H3.js.map +1 -0
- package/dist/index.cjs +352 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +425 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +38 -0
- package/dist/react.d.ts +38 -0
- package/dist/react.js +91 -0
- package/dist/react.js.map +1 -0
- package/dist/server.cjs +493 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +32 -0
- package/dist/server.d.ts +32 -0
- package/dist/server.js +164 -0
- package/dist/server.js.map +1 -0
- package/package.json +55 -6
- package/src/core.test.ts +0 -287
- package/src/core.ts +0 -667
- package/src/index.ts +0 -73
- package/src/logging.test.ts +0 -39
- package/src/logging.ts +0 -40
- package/src/react.tsx +0 -50
- package/src/sanitize.test.ts +0 -68
- package/src/sanitize.ts +0 -70
- package/src/serialize.test.ts +0 -170
- package/src/serialize.ts +0 -148
- package/src/server.ts +0 -310
- package/tsconfig.json +0 -14
package/dist/server.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createInspector
|
|
3
|
+
} from "./chunk-QLRTDBT5.js";
|
|
4
|
+
|
|
5
|
+
// src/server.ts
|
|
6
|
+
var OPEN_STATE = 1;
|
|
7
|
+
function trackLive(server, message) {
|
|
8
|
+
switch (message.type) {
|
|
9
|
+
case "XSTATE_ACTOR_REGISTERED":
|
|
10
|
+
server.liveActors.set(message.sessionId, { reg: message, snapshot: message.snapshot });
|
|
11
|
+
break;
|
|
12
|
+
case "XSTATE_SNAPSHOT": {
|
|
13
|
+
const live = server.liveActors.get(message.sessionId);
|
|
14
|
+
if (live) {
|
|
15
|
+
live.snapshot = message.snapshot;
|
|
16
|
+
}
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case "XSTATE_EVENT": {
|
|
20
|
+
const live = server.liveActors.get(message.sessionId);
|
|
21
|
+
if (live) {
|
|
22
|
+
live.snapshot = message.snapshotAfter;
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
case "XSTATE_ACTOR_STOPPED":
|
|
27
|
+
server.liveActors.delete(message.sessionId);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function createServerAdapter(options = {}) {
|
|
32
|
+
const port = options.port ?? (Number(process.env.XSTATE_DEVTOOLS_PORT) || 9301);
|
|
33
|
+
const host = options.host ?? "127.0.0.1";
|
|
34
|
+
const bufferSize = options.bufferSize ?? 200;
|
|
35
|
+
const key = `__xstate_devtools_server_${port}__`;
|
|
36
|
+
const cache = globalThis[key];
|
|
37
|
+
let server;
|
|
38
|
+
if (cache) {
|
|
39
|
+
server = cache;
|
|
40
|
+
if (bufferSize > server.bufferSize) server.bufferSize = bufferSize;
|
|
41
|
+
} else {
|
|
42
|
+
const clients = /* @__PURE__ */ new Set();
|
|
43
|
+
const dispatchHandlers = /* @__PURE__ */ new Set();
|
|
44
|
+
const liveActors = /* @__PURE__ */ new Map();
|
|
45
|
+
const recentEvents = [];
|
|
46
|
+
let wss = null;
|
|
47
|
+
let closed = false;
|
|
48
|
+
server = {
|
|
49
|
+
clients,
|
|
50
|
+
dispatchHandlers,
|
|
51
|
+
liveActors,
|
|
52
|
+
recentEvents,
|
|
53
|
+
bufferSize,
|
|
54
|
+
activated: false,
|
|
55
|
+
close: () => {
|
|
56
|
+
closed = true;
|
|
57
|
+
try {
|
|
58
|
+
wss?.close();
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
clients.clear();
|
|
62
|
+
dispatchHandlers.clear();
|
|
63
|
+
liveActors.clear();
|
|
64
|
+
recentEvents.length = 0;
|
|
65
|
+
delete globalThis[key];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
void (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const mod = await import("ws");
|
|
71
|
+
const WSServer = mod.WebSocketServer ?? mod.Server;
|
|
72
|
+
if (closed) return;
|
|
73
|
+
wss = new WSServer({ port, host });
|
|
74
|
+
wss.on("connection", (ws) => {
|
|
75
|
+
for (const { reg, snapshot } of server.liveActors.values()) {
|
|
76
|
+
try {
|
|
77
|
+
ws.send(JSON.stringify({ ...reg, __xstateDevtools: true }));
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
if (snapshot !== reg.snapshot) {
|
|
81
|
+
try {
|
|
82
|
+
ws.send(JSON.stringify({
|
|
83
|
+
type: "XSTATE_SNAPSHOT",
|
|
84
|
+
sessionId: reg.sessionId,
|
|
85
|
+
snapshot,
|
|
86
|
+
timestamp: reg.timestamp,
|
|
87
|
+
globalSeq: reg.globalSeq,
|
|
88
|
+
__xstateDevtools: true
|
|
89
|
+
}));
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
ws.send(JSON.stringify({
|
|
96
|
+
type: "XSTATE_REPLAY_DONE",
|
|
97
|
+
sessionIds: [...server.liveActors.keys()],
|
|
98
|
+
__xstateDevtools: true
|
|
99
|
+
}));
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
if (!server.activated) {
|
|
103
|
+
server.activated = true;
|
|
104
|
+
for (const payload of server.recentEvents) {
|
|
105
|
+
try {
|
|
106
|
+
ws.send(payload);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
server.recentEvents.length = 0;
|
|
111
|
+
}
|
|
112
|
+
server.clients.add(ws);
|
|
113
|
+
ws.on("message", (raw) => {
|
|
114
|
+
try {
|
|
115
|
+
const text = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
116
|
+
const msg = JSON.parse(text);
|
|
117
|
+
for (const cb of server.dispatchHandlers) cb(msg);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
ws.on("close", () => server.clients.delete(ws));
|
|
122
|
+
ws.on("error", () => server.clients.delete(ws));
|
|
123
|
+
});
|
|
124
|
+
wss.on("error", (err) => {
|
|
125
|
+
console.warn("[xstate-devtools] WS server error:", err.message);
|
|
126
|
+
});
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.warn(
|
|
129
|
+
"[xstate-devtools] could not start server adapter \u2014 install `ws` to enable.",
|
|
130
|
+
e.message
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
globalThis[key] = server;
|
|
135
|
+
}
|
|
136
|
+
const transport = {
|
|
137
|
+
send(message) {
|
|
138
|
+
trackLive(server, message);
|
|
139
|
+
const payload = JSON.stringify({ ...message, __xstateDevtools: true });
|
|
140
|
+
if (!server.activated && (message.type === "XSTATE_EVENT" || message.type === "XSTATE_SNAPSHOT")) {
|
|
141
|
+
server.recentEvents.push(payload);
|
|
142
|
+
if (server.recentEvents.length > server.bufferSize) server.recentEvents.shift();
|
|
143
|
+
}
|
|
144
|
+
for (const ws of server.clients) {
|
|
145
|
+
if (ws.readyState === OPEN_STATE) {
|
|
146
|
+
try {
|
|
147
|
+
ws.send(payload);
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
subscribe(handler) {
|
|
154
|
+
server.dispatchHandlers.add(handler);
|
|
155
|
+
return () => server.dispatchHandlers.delete(handler);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const inspector = createInspector(transport, "srv");
|
|
159
|
+
return { ...inspector, close: server.close };
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
createServerAdapter
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["// Server entrypoint — exposes a WebSocket bridge so the DevTools panel\n// can connect to actors running in Node.\nimport type { ExtensionToPageMessage, PageToExtensionMessage } from '@xstate-devtools/protocol'\nimport { createInspector, type Transport } from './core.js'\n\nexport interface ServerAdapterOptions {\n /** Port to listen on. Defaults to env XSTATE_DEVTOOLS_PORT or 9301. */\n port?: number\n /** Host to bind. Defaults to '127.0.0.1'. */\n host?: string\n /** Max events to buffer before the first panel connects. Default 200. */\n bufferSize?: number\n}\n\ninterface ClientLike {\n send(data: string): void\n on(event: string, listener: (...args: unknown[]) => void): void\n readyState: number\n}\n\nconst OPEN_STATE = 1\n\ntype ActorRegistered = Extract<PageToExtensionMessage, { type: 'XSTATE_ACTOR_REGISTERED' }>\n\ninterface LiveActor {\n /** The registration message, kept immutable so its snapshot stays the\n * registration-time one (the panel's time-travel floor). */\n reg: ActorRegistered\n /** Latest snapshot seen for this actor (the registration snapshot until updated). */\n snapshot: ActorRegistered['snapshot']\n}\n\ninterface CachedServer {\n clients: Set<ClientLike>\n dispatchHandlers: Set<(msg: ExtensionToPageMessage) => void>\n /** Currently-live actors (immutable registration + latest snapshot). */\n liveActors: Map<string, LiveActor>\n /** Pre-first-connection event/snapshot backlog, flushed once to the first panel. */\n recentEvents: string[]\n bufferSize: number\n /** Set once the first panel connects and drains the backlog. */\n activated: boolean\n close: () => void\n}\n\n/** Track live-actor state so it can be replayed to every connecting panel. */\nfunction trackLive(server: CachedServer, message: PageToExtensionMessage): void {\n switch (message.type) {\n case 'XSTATE_ACTOR_REGISTERED':\n server.liveActors.set(message.sessionId, { reg: message, snapshot: message.snapshot })\n break\n case 'XSTATE_SNAPSHOT': {\n const live = server.liveActors.get(message.sessionId)\n if (live) { live.snapshot = message.snapshot }\n break\n }\n case 'XSTATE_EVENT': {\n const live = server.liveActors.get(message.sessionId)\n if (live) { live.snapshot = message.snapshotAfter }\n break\n }\n case 'XSTATE_ACTOR_STOPPED':\n server.liveActors.delete(message.sessionId)\n break\n }\n}\n\n/**\n * Start a local WebSocket server that the DevTools panel can connect to.\n * Returns the inspector callback. Multiple panels can connect simultaneously.\n *\n * The WS server, connected clients, dispatch handlers, and the live-actor\n * registry are all stashed on globalThis keyed by port. This makes the function\n * idempotent across HMR re-evaluation: subsequent calls reuse the existing\n * server and only register new inspector hooks.\n *\n * Every connecting panel — including a reconnect after the editor/host restarts\n * — is replayed the current set of live actors (with their latest snapshots),\n * so actors registered at boot stay visible across reconnects (not just for\n * the first panel). The pre-connection event backlog, by contrast, is flushed\n * only to the very first panel; replaying it on every reconnect would re-flood\n * the log with stale events.\n */\nexport function createServerAdapter(options: ServerAdapterOptions = {}) {\n const port = options.port\n ?? (Number(process.env.XSTATE_DEVTOOLS_PORT) || 9301)\n const host = options.host ?? '127.0.0.1'\n const bufferSize = options.bufferSize ?? 200\n\n const key = `__xstate_devtools_server_${port}__`\n const cache = (globalThis as Record<string, unknown>)[key] as CachedServer | undefined\n\n let server: CachedServer\n if (cache) {\n server = cache\n // honour the most recent caller's buffer size if larger\n if (bufferSize > server.bufferSize) server.bufferSize = bufferSize\n } else {\n const clients = new Set<ClientLike>()\n const dispatchHandlers = new Set<(msg: ExtensionToPageMessage) => void>()\n const liveActors = new Map<string, LiveActor>()\n const recentEvents: string[] = []\n let wss: any = null\n let closed = false\n\n server = {\n clients, dispatchHandlers, liveActors, recentEvents, bufferSize,\n activated: false,\n close: () => {\n closed = true\n try { wss?.close() } catch { /* noop */ }\n clients.clear()\n dispatchHandlers.clear()\n liveActors.clear()\n recentEvents.length = 0\n delete (globalThis as Record<string, unknown>)[key]\n },\n }\n\n // Lazily import ws so this module is import-safe in environments that\n // never use the server entrypoint (or where ws isn't installed).\n void (async () => {\n try {\n const mod = await import('ws')\n const WSServer = (mod as any).WebSocketServer ?? (mod as any).Server\n if (closed) return\n wss = new WSServer({ port, host })\n wss.on('connection', (ws: ClientLike) => {\n // Replay current live actors to every connecting panel, so reconnects\n // see the current set. Send the immutable registration (carrying the\n // registration-time snapshot → correct time-travel floor), then a\n // snapshot update if the actor has advanced since.\n for (const { reg, snapshot } of server.liveActors.values()) {\n try { ws.send(JSON.stringify({ ...reg, __xstateDevtools: true })) } catch { /* ignore */ }\n if (snapshot !== reg.snapshot) {\n try {\n ws.send(JSON.stringify({\n type: 'XSTATE_SNAPSHOT', sessionId: reg.sessionId, snapshot,\n timestamp: reg.timestamp, globalSeq: reg.globalSeq, __xstateDevtools: true,\n }))\n } catch { /* ignore */ }\n }\n }\n // Tell the panel the authoritative live set so it can prune actors\n // from a previous session (reconnect/app-restart) without wiping the\n // ones we just replayed.\n try {\n ws.send(JSON.stringify({\n type: 'XSTATE_REPLAY_DONE',\n sessionIds: [...server.liveActors.keys()],\n __xstateDevtools: true,\n }))\n } catch { /* ignore */ }\n // Flush the pre-connection event backlog once, to the first panel only.\n if (!server.activated) {\n server.activated = true\n for (const payload of server.recentEvents) {\n try { ws.send(payload) } catch { /* ignore */ }\n }\n server.recentEvents.length = 0\n }\n server.clients.add(ws)\n ws.on('message', (raw: unknown) => {\n try {\n const text = typeof raw === 'string' ? raw : (raw as Buffer).toString('utf8')\n const msg = JSON.parse(text) as ExtensionToPageMessage\n for (const cb of server.dispatchHandlers) cb(msg)\n } catch {\n // ignore malformed messages\n }\n })\n ws.on('close', () => server.clients.delete(ws))\n ws.on('error', () => server.clients.delete(ws))\n })\n wss.on('error', (err: Error) => {\n console.warn('[xstate-devtools] WS server error:', err.message)\n })\n } catch (e) {\n console.warn(\n '[xstate-devtools] could not start server adapter — install `ws` to enable.',\n (e as Error).message,\n )\n }\n })()\n\n ;(globalThis as Record<string, unknown>)[key] = server\n }\n\n const transport: Transport = {\n send(message: PageToExtensionMessage) {\n // Maintain the live-actor registry so any panel that connects (or\n // reconnects) later can be replayed the current state.\n trackLive(server, message)\n const payload = JSON.stringify({ ...message, __xstateDevtools: true })\n // Buffer events only until the first panel connects; afterwards the log\n // streams live and the backlog is no longer replayed (avoids reconnect\n // re-flooding the log with stale events).\n if (!server.activated && (message.type === 'XSTATE_EVENT' || message.type === 'XSTATE_SNAPSHOT')) {\n server.recentEvents.push(payload)\n if (server.recentEvents.length > server.bufferSize) server.recentEvents.shift()\n }\n for (const ws of server.clients) {\n if (ws.readyState === OPEN_STATE) {\n try { ws.send(payload) } catch { /* ignore */ }\n }\n }\n },\n subscribe(handler) {\n server.dispatchHandlers.add(handler)\n return () => server.dispatchHandlers.delete(handler)\n },\n }\n\n const inspector = createInspector(transport, 'srv')\n return { ...inspector, close: server.close }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,21 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xstate-devtools/adapter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Inspect XState v5 actors from a running app (browser, Node/SSR, or React) and stream them to the XState DevTools / VS Code live debugger.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/mjbeswick/xstate-devtools.git",
|
|
9
|
+
"directory": "packages/adapter"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"xstate",
|
|
13
|
+
"statechart",
|
|
14
|
+
"devtools",
|
|
15
|
+
"inspector",
|
|
16
|
+
"debugger"
|
|
17
|
+
],
|
|
4
18
|
"type": "module",
|
|
5
|
-
"main": "./
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
6
22
|
"exports": {
|
|
7
|
-
".":
|
|
8
|
-
|
|
9
|
-
|
|
23
|
+
".": {
|
|
24
|
+
"development": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
28
|
+
},
|
|
29
|
+
"./server": {
|
|
30
|
+
"development": "./src/server.ts",
|
|
31
|
+
"types": "./dist/server.d.ts",
|
|
32
|
+
"import": "./dist/server.js",
|
|
33
|
+
"require": "./dist/server.cjs"
|
|
34
|
+
},
|
|
35
|
+
"./react": {
|
|
36
|
+
"development": "./src/react.tsx",
|
|
37
|
+
"types": "./dist/react.d.ts",
|
|
38
|
+
"import": "./dist/react.js",
|
|
39
|
+
"require": "./dist/react.cjs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
10
47
|
},
|
|
11
48
|
"scripts": {
|
|
12
|
-
"
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"prepublishOnly": "npm run build"
|
|
13
52
|
},
|
|
14
53
|
"peerDependencies": {
|
|
54
|
+
"@xstate/react": "^4.0.0",
|
|
55
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
15
56
|
"ws": "^8.0.0",
|
|
16
57
|
"xstate": "^5.0.0"
|
|
17
58
|
},
|
|
18
59
|
"peerDependenciesMeta": {
|
|
60
|
+
"@xstate/react": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"react": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
19
66
|
"ws": {
|
|
20
67
|
"optional": true
|
|
21
68
|
}
|
|
@@ -23,8 +70,10 @@
|
|
|
23
70
|
"devDependencies": {
|
|
24
71
|
"@types/react": "^18.3.0",
|
|
25
72
|
"@types/ws": "^8.18.1",
|
|
73
|
+
"@xstate-devtools/protocol": "*",
|
|
26
74
|
"@xstate/react": "^4.1.0",
|
|
27
75
|
"react": "^18.3.0",
|
|
76
|
+
"tsup": "^8.0.0",
|
|
28
77
|
"typescript": "^5.5.0",
|
|
29
78
|
"vitest": "^2.0.0",
|
|
30
79
|
"ws": "^8.20.0",
|
package/src/core.test.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { createActor, createMachine } from 'xstate'
|
|
3
|
-
import { createInspector, getSourceLocationFromStack, type Transport } from './core.js'
|
|
4
|
-
|
|
5
|
-
describe('createInspector', () => {
|
|
6
|
-
it('sanitizes outbound inspection events before sending them to the transport', () => {
|
|
7
|
-
const sent: unknown[] = []
|
|
8
|
-
const transport: Transport = {
|
|
9
|
-
send(message) {
|
|
10
|
-
sent.push(message)
|
|
11
|
-
},
|
|
12
|
-
subscribe() {
|
|
13
|
-
return () => {}
|
|
14
|
-
},
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const inspector = createInspector(transport, 'srv')
|
|
18
|
-
const snapshot = { value: 'idle', context: {}, status: 'active' }
|
|
19
|
-
const actorRef = {
|
|
20
|
-
sessionId: 'actor-1',
|
|
21
|
-
getSnapshot: vi.fn(() => snapshot),
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const event: Record<string, unknown> = { type: 'route.changed' }
|
|
25
|
-
event.self = event
|
|
26
|
-
event.handler = function routeHandler() {}
|
|
27
|
-
|
|
28
|
-
inspector.inspect({
|
|
29
|
-
type: '@xstate.event',
|
|
30
|
-
actorRef,
|
|
31
|
-
event,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const message = sent.find(
|
|
35
|
-
(candidate): candidate is { type: string; event: Record<string, unknown> } =>
|
|
36
|
-
typeof candidate === 'object' &&
|
|
37
|
-
candidate !== null &&
|
|
38
|
-
'type' in candidate &&
|
|
39
|
-
(candidate as { type?: string }).type === 'XSTATE_EVENT',
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
expect(message).toBeDefined()
|
|
43
|
-
expect(message?.event.type).toBe('route.changed')
|
|
44
|
-
expect(message?.event.handler).toBe('[Function: routeHandler]')
|
|
45
|
-
expect(message?.event.self).not.toBe(event)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('sets a selected state node as active for machine actors', () => {
|
|
49
|
-
const sent: unknown[] = []
|
|
50
|
-
let handler: ((message: Parameters<Transport['subscribe']>[0]) => void) | undefined
|
|
51
|
-
const transport: Transport = {
|
|
52
|
-
send(message) {
|
|
53
|
-
sent.push(message)
|
|
54
|
-
},
|
|
55
|
-
subscribe(callback) {
|
|
56
|
-
handler = callback
|
|
57
|
-
return () => {}
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const inspector = createInspector(transport, 'srv')
|
|
62
|
-
const actor = createActor(
|
|
63
|
-
createMachine({
|
|
64
|
-
id: 'traffic',
|
|
65
|
-
initial: 'green',
|
|
66
|
-
states: {
|
|
67
|
-
green: {},
|
|
68
|
-
yellow: {},
|
|
69
|
-
red: {},
|
|
70
|
-
},
|
|
71
|
-
}),
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
actor.start()
|
|
75
|
-
inspector.inspect({ type: '@xstate.actor', actorRef: actor })
|
|
76
|
-
|
|
77
|
-
handler?.({
|
|
78
|
-
type: 'XSTATE_SET_ACTIVE_STATE',
|
|
79
|
-
sessionId: `srv:${actor.sessionId}`,
|
|
80
|
-
stateNodeId: 'traffic.red',
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
expect(actor.getSnapshot().value).toBe('red')
|
|
84
|
-
|
|
85
|
-
const snapshotMessage = sent.find(
|
|
86
|
-
(candidate): candidate is { type: string; snapshot: { value: unknown }; sessionId: string } =>
|
|
87
|
-
typeof candidate === 'object' &&
|
|
88
|
-
candidate !== null &&
|
|
89
|
-
'type' in candidate &&
|
|
90
|
-
(candidate as { type?: string }).type === 'XSTATE_SNAPSHOT' &&
|
|
91
|
-
'snapshot' in candidate,
|
|
92
|
-
)
|
|
93
|
-
expect(snapshotMessage).toBeDefined()
|
|
94
|
-
expect(snapshotMessage?.sessionId).toBe(`srv:${actor.sessionId}`)
|
|
95
|
-
expect(snapshotMessage?.snapshot.value).toBe('red')
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
describe('getSourceLocationFromStack', () => {
|
|
100
|
-
it('skips anonymous and node internal frames and returns filesystem frame', () => {
|
|
101
|
-
const stack = [
|
|
102
|
-
'Error',
|
|
103
|
-
' at getSourceLocation (packages/adapter/src/core.ts:10:1)',
|
|
104
|
-
' at inspect (packages/adapter/src/core.ts:20:1)',
|
|
105
|
-
' at Map.forEach (<anonymous>)',
|
|
106
|
-
' at WebSocket.emit (node:events:508:28)',
|
|
107
|
-
' at createMachine (/Users/me/project/app/machine.ts:12:3)',
|
|
108
|
-
].join('\n')
|
|
109
|
-
|
|
110
|
-
expect(getSourceLocationFromStack(stack)).toBe(
|
|
111
|
-
'createMachine (/Users/me/project/app/machine.ts:12:3)',
|
|
112
|
-
)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('accepts vite /@fs/ urls and ignores plain browser urls', () => {
|
|
116
|
-
const stack = [
|
|
117
|
-
'Error',
|
|
118
|
-
' at getSourceLocation (packages/adapter/src/core.ts:10:1)',
|
|
119
|
-
' at inspect (packages/adapter/src/core.ts:20:1)',
|
|
120
|
-
' at createMachine (http://localhost:5173/app/machines/auth.machine.ts:12:3)',
|
|
121
|
-
' at createMachine (http://localhost:5173/@fs/Users/me/project/app/machine.ts:12:3)',
|
|
122
|
-
].join('\n')
|
|
123
|
-
|
|
124
|
-
expect(getSourceLocationFromStack(stack)).toBe(
|
|
125
|
-
'createMachine (http://localhost:5173/@fs/Users/me/project/app/machine.ts:12:3)',
|
|
126
|
-
)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('maps plain browser app urls when a web source root is configured', () => {
|
|
130
|
-
const stack = [
|
|
131
|
-
'Error',
|
|
132
|
-
' at getSourceLocation (packages/adapter/src/core.ts:10:1)',
|
|
133
|
-
' at inspect (packages/adapter/src/core.ts:20:1)',
|
|
134
|
-
' at createMachine (http://localhost:5273/app/machines/auth.machine.ts:12:3)',
|
|
135
|
-
].join('\n')
|
|
136
|
-
|
|
137
|
-
expect(
|
|
138
|
-
getSourceLocationFromStack(stack, 'web', {
|
|
139
|
-
webSourceRoot: '/Users/me/project/packages/example-remix',
|
|
140
|
-
}),
|
|
141
|
-
).toBe(
|
|
142
|
-
'createMachine (/Users/me/project/packages/example-remix/app/machines/auth.machine.ts:12:3)',
|
|
143
|
-
)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('returns undefined when no filesystem-backed frame exists', () => {
|
|
147
|
-
const stack = [
|
|
148
|
-
'Error',
|
|
149
|
-
' at getSourceLocation (packages/adapter/src/core.ts:10:1)',
|
|
150
|
-
' at inspect (packages/adapter/src/core.ts:20:1)',
|
|
151
|
-
' at Map.forEach (<anonymous>)',
|
|
152
|
-
' at WebSocket.emit (node:events:508:28)',
|
|
153
|
-
].join('\n')
|
|
154
|
-
|
|
155
|
-
expect(getSourceLocationFromStack(stack)).toBeUndefined()
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
// --- Investigation tests for the "source link not appearing" bug ---
|
|
159
|
-
// These tests simulate the realistic Vite/React/XState browser stack to
|
|
160
|
-
// pinpoint why sourceLocation is undefined when inspecting machines in the
|
|
161
|
-
// example app.
|
|
162
|
-
|
|
163
|
-
it('skips vite pre-bundled xstate deps and finds user component frame', () => {
|
|
164
|
-
// In Vite dev mode, XState and @xstate/react are pre-bundled and served at
|
|
165
|
-
// /node_modules/.vite/deps/*.js, NOT at /node_modules/xstate/ or
|
|
166
|
-
// /node_modules/@xstate/. isLibraryStackFrame misses these, but they must
|
|
167
|
-
// still be skipped (via hasFilesystemBackedPath returning false).
|
|
168
|
-
const stack = [
|
|
169
|
-
'Error',
|
|
170
|
-
' at getSourceLocation (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:225:21)',
|
|
171
|
-
' at inspect (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:576:47)',
|
|
172
|
-
' at Actor._sendInspectionEvent (http://localhost:5273/node_modules/.vite/deps/xstate.js:123:45)',
|
|
173
|
-
' at new Actor (http://localhost:5273/node_modules/.vite/deps/xstate.js:234:12)',
|
|
174
|
-
' at createActor (http://localhost:5273/node_modules/.vite/deps/xstate.js:345:10)',
|
|
175
|
-
' at useIdleActorRef (http://localhost:5273/node_modules/.vite/deps/@xstate_react.js:67:23)',
|
|
176
|
-
' at useMachine (http://localhost:5273/node_modules/.vite/deps/@xstate_react.js:207:10)',
|
|
177
|
-
' at MediaPlayer (http://localhost:5273/app/components/MediaPlayer.tsx:6:43)',
|
|
178
|
-
' at renderWithHooks (http://localhost:5273/node_modules/.vite/deps/react-dom_client.js:456:22)',
|
|
179
|
-
].join('\n')
|
|
180
|
-
|
|
181
|
-
expect(
|
|
182
|
-
getSourceLocationFromStack(stack, 'web', {
|
|
183
|
-
webSourceRoot: '/Users/me/xstate-devtools/packages/example-remix',
|
|
184
|
-
}),
|
|
185
|
-
).toBe(
|
|
186
|
-
'MediaPlayer (/Users/me/xstate-devtools/packages/example-remix/app/components/MediaPlayer.tsx:6:43)',
|
|
187
|
-
)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('returns undefined for vite pre-bundled stack without webSourceRoot', () => {
|
|
191
|
-
// Without webSourceRoot, /app/ URLs cannot be remapped to filesystem paths,
|
|
192
|
-
// so sourceLocation should be undefined and the source link hidden.
|
|
193
|
-
const stack = [
|
|
194
|
-
'Error',
|
|
195
|
-
' at getSourceLocation (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:225:21)',
|
|
196
|
-
' at inspect (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:576:47)',
|
|
197
|
-
' at new Actor (http://localhost:5273/node_modules/.vite/deps/xstate.js:234:12)',
|
|
198
|
-
' at useMachine (http://localhost:5273/node_modules/.vite/deps/@xstate_react.js:207:10)',
|
|
199
|
-
' at MediaPlayer (http://localhost:5273/app/components/MediaPlayer.tsx:6:43)',
|
|
200
|
-
].join('\n')
|
|
201
|
-
|
|
202
|
-
// No webSourceRoot: plain /app/ browser URLs have no filesystem mapping.
|
|
203
|
-
expect(getSourceLocationFromStack(stack, 'web')).toBeUndefined()
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('captures real Node.js stack from within a createActor inspect callback', () => {
|
|
207
|
-
// This test runs in Node.js. It verifies:
|
|
208
|
-
// 1. @xstate.actor fires synchronously during createActor (user code IS on stack)
|
|
209
|
-
// 2. getSourceLocationFromStack finds a frame — but in this test environment,
|
|
210
|
-
// the test file itself is inside packages/adapter/ so it's filtered out by
|
|
211
|
-
// isLibraryStackFrame. The Vitest runner frame (node_modules/@vitest) is
|
|
212
|
-
// returned instead, which is the first "non-library" filesystem-backed frame.
|
|
213
|
-
// This is expected here; in production the first non-library frame is user
|
|
214
|
-
// component code (e.g. app/components/MediaPlayer.tsx).
|
|
215
|
-
const machine = createMachine({ id: 'src-test', initial: 'idle', states: { idle: {} } })
|
|
216
|
-
|
|
217
|
-
let capturedStack: string | undefined
|
|
218
|
-
|
|
219
|
-
// createActor synchronously fires @xstate.actor in the Actor constructor.
|
|
220
|
-
createActor(machine, {
|
|
221
|
-
inspect(event) {
|
|
222
|
-
if (event.type === '@xstate.actor') {
|
|
223
|
-
capturedStack = new Error().stack
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
expect(capturedStack).toBeDefined()
|
|
229
|
-
|
|
230
|
-
// Confirm @xstate.actor fires synchronously: capturedStack is set immediately.
|
|
231
|
-
expect(capturedStack).toMatch(/@xstate\.actor|Actor|createActor/)
|
|
232
|
-
|
|
233
|
-
const location = getSourceLocationFromStack(capturedStack, 'srv')
|
|
234
|
-
|
|
235
|
-
// In this test env, the test file is filtered (it's in /packages/adapter/).
|
|
236
|
-
// The function returns the Vitest runner frame as the first "non-library" frame.
|
|
237
|
-
// This exposes a real limitation: the /packages/adapter/ filter also catches
|
|
238
|
-
// test files. In a real browser, user component files at /app/ are not filtered.
|
|
239
|
-
expect(location).toBeDefined()
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('shows that the test file itself is filtered by isLibraryStackFrame', () => {
|
|
243
|
-
// This is an explicit documentation of the limitation: any frame inside
|
|
244
|
-
// packages/adapter/ is treated as a library frame and skipped. This is
|
|
245
|
-
// correct in production but means test-side assertions about "user code"
|
|
246
|
-
// in these tests will see Vitest runner frames instead.
|
|
247
|
-
const testFilePath = '/Users/me/xstate-devtools/packages/adapter/src/core.test.ts'
|
|
248
|
-
const fakeStack = [
|
|
249
|
-
'Error',
|
|
250
|
-
' at getSourceLocation (/Users/me/xstate-devtools/packages/adapter/src/core.ts:225:21)',
|
|
251
|
-
' at inspect (/Users/me/xstate-devtools/packages/adapter/src/core.ts:576:47)',
|
|
252
|
-
` at it (/Users/me/xstate-devtools/packages/adapter/src/core.test.ts:200:5)`,
|
|
253
|
-
' at runTest (file:///Users/me/xstate-devtools/node_modules/@vitest/runner/dist/index.js:146:14)',
|
|
254
|
-
].join('\n')
|
|
255
|
-
|
|
256
|
-
// The test file frame is filtered because it contains '/packages/adapter/'
|
|
257
|
-
// — so the Vitest runner frame is what gets returned.
|
|
258
|
-
const location = getSourceLocationFromStack(fakeStack, 'srv')
|
|
259
|
-
expect(location).toMatch(/vitest/)
|
|
260
|
-
expect(location).not.toContain(testFilePath)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('does not show source link when @xstate.actor fires inside useEffect (deferred start)', () => {
|
|
264
|
-
// @xstate/react calls actorRef.start() inside React.useEffect, not during
|
|
265
|
-
// createActor. If inspection fires at start() time instead of createActor
|
|
266
|
-
// time, the React scheduler is on the stack and user code is NOT present.
|
|
267
|
-
// This simulates what would happen if start() (not createActor) triggered
|
|
268
|
-
// the @xstate.actor event.
|
|
269
|
-
const stack = [
|
|
270
|
-
'Error',
|
|
271
|
-
' at getSourceLocation (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:225:21)',
|
|
272
|
-
' at inspect (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:576:47)',
|
|
273
|
-
' at Actor.start (http://localhost:5273/node_modules/.vite/deps/xstate.js:300:8)',
|
|
274
|
-
' at useEffect (http://localhost:5273/node_modules/.vite/deps/@xstate_react.js:99:14)',
|
|
275
|
-
// React scheduler — no user component frame at all
|
|
276
|
-
' at commitHookEffectListMount (http://localhost:5273/node_modules/.vite/deps/react-dom_client.js:22728:26)',
|
|
277
|
-
' at commitPassiveMountOnFiber (http://localhost:5273/node_modules/.vite/deps/react-dom_client.js:24502:13)',
|
|
278
|
-
].join('\n')
|
|
279
|
-
|
|
280
|
-
// Without any /app/ user frame, sourceLocation should be undefined.
|
|
281
|
-
expect(
|
|
282
|
-
getSourceLocationFromStack(stack, 'web', {
|
|
283
|
-
webSourceRoot: '/Users/me/xstate-devtools/packages/example-remix',
|
|
284
|
-
}),
|
|
285
|
-
).toBeUndefined()
|
|
286
|
-
})
|
|
287
|
-
})
|