@statelyai/sdk 0.10.1 → 0.11.0
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 +98 -13
- package/dist/{graph-DmXh22Zu.d.mts → graph-GeuH-mFK.d.mts} +13 -2
- package/dist/graph.d.mts +1 -1
- package/dist/graphToXStateTS-DV62vO_d.mjs +5490 -0
- package/dist/index.d.mts +11 -2
- package/dist/index.mjs +1 -1
- package/dist/{inspect-BLlM3qKf.d.mts → inspect-CZ8iLJR7.d.mts} +8 -10
- package/dist/inspect.d.mts +1 -1
- package/dist/inspect.mjs +50 -34
- package/dist/protocol.d.mts +11 -0
- package/dist/sync.d.mts +1 -1
- package/dist/sync.mjs +3242 -30
- package/package.json +2 -2
- package/dist/graphToXStateTS-moihsH_U.mjs +0 -710
package/dist/sync.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createStatelyClient } from "./studio.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { S as upsertStatelyPragma, _ as string, b as findStatelyPragmaAttachments, d as boolean, f as discriminatedUnion, g as record, h as object, l as _enum, m as number, p as literal, t as graphToXStateTS, u as array, v as union, x as getStatelyPragma, y as unknown } from "./graphToXStateTS-DV62vO_d.mjs";
|
|
3
3
|
import { fromStudioMachine, toStudioMachine } from "./graph.mjs";
|
|
4
4
|
import * as ts$1 from "typescript";
|
|
5
5
|
import ts from "typescript";
|
|
@@ -11,6 +11,8 @@ import { promisify } from "node:util";
|
|
|
11
11
|
import * as fs$1 from "fs";
|
|
12
12
|
import * as path$1 from "path";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
14
|
+
import { createMachine, transition } from "xstate";
|
|
15
|
+
import { getShortestPaths, getSimplePaths } from "xstate/graph";
|
|
14
16
|
|
|
15
17
|
//#region ../editor-sync/src/fs.ts
|
|
16
18
|
function createTextDocument(fileName, text, uri = path$1.isAbsolute(fileName) ? pathToFileURL(fileName).toString() : fileName) {
|
|
@@ -107,11 +109,3095 @@ function getLineStarts(text) {
|
|
|
107
109
|
return lineStarts;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region ../graph-tools/dist/index.mjs
|
|
114
|
+
function summarizeGraphDiff(diff) {
|
|
115
|
+
return {
|
|
116
|
+
hasChanges: !isEmptyDiff(diff),
|
|
117
|
+
nodeChanges: diff.nodes.added.length + diff.nodes.removed.length + diff.nodes.updated.length,
|
|
118
|
+
edgeChanges: diff.edges.added.length + diff.edges.removed.length + diff.edges.updated.length
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function nodeDepth(graph, node) {
|
|
122
|
+
let depth = 0;
|
|
123
|
+
let parentId = node.parentId;
|
|
124
|
+
while (parentId) {
|
|
125
|
+
depth++;
|
|
126
|
+
parentId = graph.nodes.find((candidate) => candidate.id === parentId)?.parentId;
|
|
127
|
+
}
|
|
128
|
+
return depth;
|
|
129
|
+
}
|
|
130
|
+
function getStableNodeId(node, parentStableId, siblingIndex) {
|
|
131
|
+
if (node.parentId === null) return "root";
|
|
132
|
+
const key = node.data.key || node.id;
|
|
133
|
+
const suffix = siblingIndex === 0 ? key : `${key}_${siblingIndex + 1}`;
|
|
134
|
+
return parentStableId ? `${parentStableId}.${suffix}` : suffix;
|
|
135
|
+
}
|
|
136
|
+
function getStableEdgeId(edge, siblingIndex) {
|
|
137
|
+
const suffix = siblingIndex === 0 ? "" : `.${siblingIndex + 1}`;
|
|
138
|
+
return [
|
|
139
|
+
edge.sourceId,
|
|
140
|
+
edge.data.eventType,
|
|
141
|
+
edge.targetId,
|
|
142
|
+
edge.data.guard?.type ?? ""
|
|
143
|
+
].join("->").concat(suffix);
|
|
144
|
+
}
|
|
145
|
+
function normalizeGraphForSemanticDiff(graph) {
|
|
146
|
+
const normalized = structuredClone(graph);
|
|
147
|
+
const nodeIdMap = /* @__PURE__ */ new Map();
|
|
148
|
+
const siblingKeys = /* @__PURE__ */ new Map();
|
|
149
|
+
const nodesByDepth = [...normalized.nodes].sort((a, b) => nodeDepth(normalized, a) - nodeDepth(normalized, b));
|
|
150
|
+
for (const node of nodesByDepth) {
|
|
151
|
+
const parentStableId = node.parentId ? nodeIdMap.get(node.parentId) ?? node.parentId : null;
|
|
152
|
+
const key = `${parentStableId ?? ""}:${node.data.key || node.id}`;
|
|
153
|
+
const siblingIndex = siblingKeys.get(key) ?? 0;
|
|
154
|
+
siblingKeys.set(key, siblingIndex + 1);
|
|
155
|
+
nodeIdMap.set(node.id, getStableNodeId(node, parentStableId, siblingIndex));
|
|
156
|
+
}
|
|
157
|
+
for (const node of normalized.nodes) {
|
|
158
|
+
node.id = nodeIdMap.get(node.id) ?? node.id;
|
|
159
|
+
if (node.parentId) node.parentId = nodeIdMap.get(node.parentId) ?? node.parentId;
|
|
160
|
+
if (node.initialNodeId) node.initialNodeId = nodeIdMap.get(node.initialNodeId) ?? node.initialNodeId;
|
|
161
|
+
if (node.data.initialId) node.data.initialId = nodeIdMap.get(node.data.initialId) ?? node.data.initialId;
|
|
162
|
+
}
|
|
163
|
+
const edgeSiblingKeys = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const edge of normalized.edges) {
|
|
165
|
+
edge.sourceId = nodeIdMap.get(edge.sourceId) ?? edge.sourceId;
|
|
166
|
+
edge.targetId = nodeIdMap.get(edge.targetId) ?? edge.targetId;
|
|
167
|
+
const key = [
|
|
168
|
+
edge.sourceId,
|
|
169
|
+
edge.data.eventType,
|
|
170
|
+
edge.targetId,
|
|
171
|
+
edge.data.guard?.type ?? ""
|
|
172
|
+
].join("->");
|
|
173
|
+
const siblingIndex = edgeSiblingKeys.get(key) ?? 0;
|
|
174
|
+
edgeSiblingKeys.set(key, siblingIndex + 1);
|
|
175
|
+
edge.id = getStableEdgeId(edge, siblingIndex);
|
|
176
|
+
}
|
|
177
|
+
if (normalized.initialNodeId) normalized.initialNodeId = nodeIdMap.get(normalized.initialNodeId) ?? normalized.initialNodeId;
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
|
180
|
+
function diffGraphsSemantically(baseGraph, compareGraph) {
|
|
181
|
+
const base = normalizeGraphForSemanticDiff(baseGraph);
|
|
182
|
+
const compare = normalizeGraphForSemanticDiff(compareGraph);
|
|
183
|
+
const diff = getDiff(base, compare);
|
|
184
|
+
return {
|
|
185
|
+
base,
|
|
186
|
+
compare,
|
|
187
|
+
diff,
|
|
188
|
+
summary: summarizeGraphDiff(diff)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function hasEntityId(graph, id) {
|
|
192
|
+
return graph.nodes.some((node) => node.id === id) || graph.edges.some((edge) => edge.id === id);
|
|
193
|
+
}
|
|
194
|
+
function uniqueId(graph, prefix, preferred) {
|
|
195
|
+
if (preferred && !hasEntityId(graph, preferred)) return preferred;
|
|
196
|
+
let index = 1;
|
|
197
|
+
let id = `${prefix}${index}`;
|
|
198
|
+
while (hasEntityId(graph, id)) {
|
|
199
|
+
index++;
|
|
200
|
+
id = `${prefix}${index}`;
|
|
201
|
+
}
|
|
202
|
+
return id;
|
|
203
|
+
}
|
|
204
|
+
function resolveLocation(location, resolve) {
|
|
205
|
+
return "nodeId" in location ? {
|
|
206
|
+
...location,
|
|
207
|
+
nodeId: resolve(location.nodeId)
|
|
208
|
+
} : {
|
|
209
|
+
...location,
|
|
210
|
+
edgeId: resolve(location.edgeId)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function getActionArray(graph, location) {
|
|
214
|
+
if ("nodeId" in location) {
|
|
215
|
+
const node = graph.nodes.find((candidate) => candidate.id === location.nodeId);
|
|
216
|
+
return node ? node.data[location.group] : null;
|
|
217
|
+
}
|
|
218
|
+
return graph.edges.find((candidate) => candidate.id === location.edgeId)?.data.actions ?? null;
|
|
219
|
+
}
|
|
220
|
+
function hasActionId(graph, id) {
|
|
221
|
+
for (const node of graph.nodes) {
|
|
222
|
+
if (node.data.entry.some((action) => action.id === id)) return true;
|
|
223
|
+
if (node.data.exit.some((action) => action.id === id)) return true;
|
|
224
|
+
}
|
|
225
|
+
for (const edge of graph.edges) if (edge.data.actions.some((action) => action.id === id)) return true;
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
function hasInvokeId(graph, id) {
|
|
229
|
+
return graph.nodes.some((node) => node.data.invokes.some((invoke) => invoke.id === id));
|
|
230
|
+
}
|
|
231
|
+
function createNodeInGraph(graph, opts) {
|
|
232
|
+
if (hasEntityId(graph, opts.id)) return;
|
|
233
|
+
const parentNode = graph.nodes.find((node) => node.id === opts.parentId);
|
|
234
|
+
if (!parentNode) return;
|
|
235
|
+
graph.nodes.push({
|
|
236
|
+
type: "node",
|
|
237
|
+
id: opts.id,
|
|
238
|
+
parentId: opts.parentId,
|
|
239
|
+
position: {
|
|
240
|
+
x: opts.x ?? 0,
|
|
241
|
+
y: opts.y ?? 0
|
|
242
|
+
},
|
|
243
|
+
x: opts.x ?? 0,
|
|
244
|
+
y: opts.y ?? 0,
|
|
245
|
+
width: 0,
|
|
246
|
+
height: 0,
|
|
247
|
+
dx: 0,
|
|
248
|
+
dy: 0,
|
|
249
|
+
label: opts.key,
|
|
250
|
+
data: {
|
|
251
|
+
key: opts.key,
|
|
252
|
+
type: opts.type ?? null,
|
|
253
|
+
history: false,
|
|
254
|
+
entry: [],
|
|
255
|
+
exit: [],
|
|
256
|
+
meta: null,
|
|
257
|
+
tags: [],
|
|
258
|
+
description: void 0,
|
|
259
|
+
initialId: null,
|
|
260
|
+
invokes: []
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
if (!parentNode.data.initialId) {
|
|
264
|
+
parentNode.data.initialId = opts.id;
|
|
265
|
+
parentNode.initialNodeId = opts.id;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function updateNodeInGraph(graph, nodeId, data) {
|
|
269
|
+
const node = graph.nodes.find((candidate) => candidate.id === nodeId);
|
|
270
|
+
if (!node) return;
|
|
271
|
+
if (data.key !== void 0) {
|
|
272
|
+
data.key = data.key.trim();
|
|
273
|
+
node.label = data.key;
|
|
274
|
+
}
|
|
275
|
+
if (data.initialId !== void 0) node.initialNodeId = data.initialId;
|
|
276
|
+
Object.assign(node.data, data);
|
|
277
|
+
}
|
|
278
|
+
function createEdgeInGraph(graph, opts) {
|
|
279
|
+
if (hasEntityId(graph, opts.id)) return;
|
|
280
|
+
const sourceNode = graph.nodes.find((node) => node.id === opts.sourceId);
|
|
281
|
+
if (!sourceNode) return;
|
|
282
|
+
graph.edges.push({
|
|
283
|
+
type: "edge",
|
|
284
|
+
id: opts.id,
|
|
285
|
+
sourceId: opts.sourceId,
|
|
286
|
+
targetId: opts.targetId,
|
|
287
|
+
label: opts.eventType,
|
|
288
|
+
position: {
|
|
289
|
+
x: sourceNode.position?.x ?? sourceNode.x ?? 0,
|
|
290
|
+
y: (sourceNode.position?.y ?? sourceNode.y ?? 0) + (sourceNode.height ?? 0) + 60
|
|
291
|
+
},
|
|
292
|
+
x: sourceNode.position?.x ?? sourceNode.x ?? 0,
|
|
293
|
+
y: (sourceNode.position?.y ?? sourceNode.y ?? 0) + (sourceNode.height ?? 0) + 60,
|
|
294
|
+
width: 0,
|
|
295
|
+
height: 0,
|
|
296
|
+
dx: 0,
|
|
297
|
+
dy: 0,
|
|
298
|
+
data: {
|
|
299
|
+
eventType: opts.eventType,
|
|
300
|
+
transitionType: opts.transitionType ?? "normal",
|
|
301
|
+
guard: null,
|
|
302
|
+
description: null,
|
|
303
|
+
actions: [],
|
|
304
|
+
meta: null
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function updateEdgeInGraph(graph, edgeId, data) {
|
|
309
|
+
const edge = graph.edges.find((candidate) => candidate.id === edgeId);
|
|
310
|
+
if (!edge) return;
|
|
311
|
+
if (data.sourceId !== void 0) edge.sourceId = data.sourceId;
|
|
312
|
+
if (data.targetId !== void 0) edge.targetId = data.targetId;
|
|
313
|
+
if (data.eventType !== void 0) {
|
|
314
|
+
data.eventType = data.eventType.trim();
|
|
315
|
+
edge.label = data.eventType;
|
|
316
|
+
}
|
|
317
|
+
const { sourceId: _sourceId, targetId: _targetId, ...edgeData } = data;
|
|
318
|
+
Object.assign(edge.data, edgeData);
|
|
319
|
+
}
|
|
320
|
+
function deleteEdgeFromGraph(graph, edgeId) {
|
|
321
|
+
const index = graph.edges.findIndex((edge) => edge.id === edgeId);
|
|
322
|
+
if (index >= 0) graph.edges.splice(index, 1);
|
|
323
|
+
}
|
|
324
|
+
function deleteEntitiesFromGraph(graph, entityIds) {
|
|
325
|
+
const deletedNodeIds = /* @__PURE__ */ new Set();
|
|
326
|
+
const nodesToDelete = /* @__PURE__ */ new Set();
|
|
327
|
+
for (const id of entityIds) {
|
|
328
|
+
const node = graph.nodes.find((candidate) => candidate.id === id);
|
|
329
|
+
if (node && node.parentId !== null) {
|
|
330
|
+
nodesToDelete.add(id);
|
|
331
|
+
const collectDescendants = (parentId) => {
|
|
332
|
+
for (const child of graph.nodes) if (child.parentId === parentId && !nodesToDelete.has(child.id)) {
|
|
333
|
+
nodesToDelete.add(child.id);
|
|
334
|
+
collectDescendants(child.id);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
collectDescendants(id);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const id of nodesToDelete) {
|
|
341
|
+
const index = graph.nodes.findIndex((node) => node.id === id);
|
|
342
|
+
if (index === -1) continue;
|
|
343
|
+
const nodeToDelete = graph.nodes[index];
|
|
344
|
+
const parentNode = graph.nodes.find((node) => node.id === nodeToDelete.parentId);
|
|
345
|
+
graph.nodes.splice(index, 1);
|
|
346
|
+
deletedNodeIds.add(id);
|
|
347
|
+
if (parentNode && parentNode.data.initialId === id) {
|
|
348
|
+
const childNodes = graph.nodes.filter((node) => node.parentId === parentNode.id);
|
|
349
|
+
parentNode.data.initialId = childNodes[0]?.id ?? null;
|
|
350
|
+
parentNode.initialNodeId = parentNode.data.initialId;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
graph.edges = graph.edges.filter((edge) => !entityIds.has(edge.id) && !deletedNodeIds.has(edge.sourceId) && !deletedNodeIds.has(edge.targetId));
|
|
354
|
+
graph.annotations = graph.annotations.filter((annotation) => !entityIds.has(annotation.id));
|
|
355
|
+
}
|
|
356
|
+
function ensureImplementations(graph) {
|
|
357
|
+
graph.data.implementations ??= {
|
|
358
|
+
actions: [],
|
|
359
|
+
guards: [],
|
|
360
|
+
actors: [],
|
|
361
|
+
delays: [],
|
|
362
|
+
tags: []
|
|
363
|
+
};
|
|
364
|
+
return graph.data.implementations;
|
|
365
|
+
}
|
|
366
|
+
function implementationList(graph, sourceType) {
|
|
367
|
+
const implementations = ensureImplementations(graph);
|
|
368
|
+
if (sourceType === "action") return implementations.actions;
|
|
369
|
+
if (sourceType === "guard") return implementations.guards;
|
|
370
|
+
if (sourceType === "actor") return implementations.actors;
|
|
371
|
+
return implementations.delays;
|
|
372
|
+
}
|
|
373
|
+
function applyGraphPatches(graph, patches) {
|
|
374
|
+
const nextGraph = structuredClone(graph);
|
|
375
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
376
|
+
const resolve = (id) => idMap.get(id) ?? id;
|
|
377
|
+
for (const patch of patches) switch (patch.op) {
|
|
378
|
+
case "createState": {
|
|
379
|
+
const realId = uniqueId(nextGraph, "node-", patch.id);
|
|
380
|
+
if (patch.id) idMap.set(patch.id, realId);
|
|
381
|
+
createNodeInGraph(nextGraph, {
|
|
382
|
+
id: realId,
|
|
383
|
+
parentId: resolve(patch.parentId),
|
|
384
|
+
key: patch.key,
|
|
385
|
+
x: patch.x,
|
|
386
|
+
y: patch.y,
|
|
387
|
+
type: patch.type
|
|
388
|
+
});
|
|
389
|
+
const node = nextGraph.nodes.find((candidate) => candidate.id === realId);
|
|
390
|
+
if (node) {
|
|
391
|
+
if (patch.color !== void 0) node.data.color = patch.color ?? void 0;
|
|
392
|
+
if (patch.initial) {
|
|
393
|
+
const parentNode = nextGraph.nodes.find((candidate) => candidate.id === node.parentId);
|
|
394
|
+
if (parentNode) {
|
|
395
|
+
parentNode.data.initialId = realId;
|
|
396
|
+
parentNode.initialNodeId = realId;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case "updateState": {
|
|
403
|
+
const nodeData = {};
|
|
404
|
+
if (patch.key !== void 0) nodeData.key = patch.key;
|
|
405
|
+
if (patch.type !== void 0) nodeData.type = patch.type;
|
|
406
|
+
if (patch.stateDescription !== void 0) nodeData.description = patch.stateDescription;
|
|
407
|
+
if (patch.initialId !== void 0) nodeData.initialId = patch.initialId ? resolve(patch.initialId) : patch.initialId;
|
|
408
|
+
if (patch.color !== void 0) nodeData.color = patch.color ?? void 0;
|
|
409
|
+
if (patch.meta !== void 0) nodeData.meta = patch.meta;
|
|
410
|
+
if (patch.history !== void 0) nodeData.history = patch.history === null ? false : patch.history;
|
|
411
|
+
updateNodeInGraph(nextGraph, resolve(patch.stateId), nodeData);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case "deleteState":
|
|
415
|
+
deleteEntitiesFromGraph(nextGraph, new Set([resolve(patch.stateId)]));
|
|
416
|
+
break;
|
|
417
|
+
case "createTransition": {
|
|
418
|
+
const realId = uniqueId(nextGraph, "edge-", patch.id);
|
|
419
|
+
if (patch.id) idMap.set(patch.id, realId);
|
|
420
|
+
createEdgeInGraph(nextGraph, {
|
|
421
|
+
id: realId,
|
|
422
|
+
sourceId: resolve(patch.sourceId),
|
|
423
|
+
targetId: resolve(patch.targetId),
|
|
424
|
+
eventType: patch.eventType,
|
|
425
|
+
transitionType: patch.transitionType
|
|
426
|
+
});
|
|
427
|
+
const edge = nextGraph.edges.find((candidate) => candidate.id === realId);
|
|
428
|
+
if (edge) {
|
|
429
|
+
if (patch.guard) edge.data.guard = patch.guard;
|
|
430
|
+
if (patch.color !== void 0) edge.data.color = patch.color ?? void 0;
|
|
431
|
+
if (patch.transitionDescription !== void 0) edge.data.description = patch.transitionDescription;
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
case "updateTransition": {
|
|
436
|
+
const edgeData = {};
|
|
437
|
+
if (patch.sourceId !== void 0) edgeData.sourceId = resolve(patch.sourceId);
|
|
438
|
+
if (patch.targetId !== void 0) edgeData.targetId = resolve(patch.targetId);
|
|
439
|
+
if (patch.eventType !== void 0) edgeData.eventType = patch.eventType;
|
|
440
|
+
if (patch.transitionType !== void 0) edgeData.transitionType = patch.transitionType;
|
|
441
|
+
if (patch.transitionDescription !== void 0) edgeData.description = patch.transitionDescription;
|
|
442
|
+
if (patch.color !== void 0) edgeData.color = patch.color ?? void 0;
|
|
443
|
+
if (patch.meta !== void 0) edgeData.meta = patch.meta;
|
|
444
|
+
updateEdgeInGraph(nextGraph, resolve(patch.transitionId), edgeData);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
case "deleteTransition":
|
|
448
|
+
deleteEdgeFromGraph(nextGraph, resolve(patch.transitionId));
|
|
449
|
+
break;
|
|
450
|
+
case "createAction": {
|
|
451
|
+
const realId = patch.action.id ? uniqueId(nextGraph, "action-", patch.action.id) : `action-${Math.max(1, patches.indexOf(patch) + 1)}`;
|
|
452
|
+
if (patch.action.id) idMap.set(patch.action.id, realId);
|
|
453
|
+
if (!patch.action.type.trim()) break;
|
|
454
|
+
if (hasActionId(nextGraph, realId)) break;
|
|
455
|
+
const actions = getActionArray(nextGraph, resolveLocation(patch.location, resolve));
|
|
456
|
+
if (!actions) break;
|
|
457
|
+
const action = {
|
|
458
|
+
...patch.action,
|
|
459
|
+
id: realId,
|
|
460
|
+
params: patch.action.params ?? {}
|
|
461
|
+
};
|
|
462
|
+
if (patch.index !== void 0) actions.splice(patch.index, 0, action);
|
|
463
|
+
else actions.push(action);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
case "updateAction": {
|
|
467
|
+
const action = getActionArray(nextGraph, resolveLocation(patch.location, resolve))?.find((candidate) => candidate.id === resolve(patch.actionId));
|
|
468
|
+
if (action) Object.assign(action, patch.data);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "deleteAction": {
|
|
472
|
+
const actions = getActionArray(nextGraph, resolveLocation(patch.location, resolve));
|
|
473
|
+
if (!actions) break;
|
|
474
|
+
const index = actions.findIndex((candidate) => candidate.id === resolve(patch.actionId));
|
|
475
|
+
if (index >= 0) actions.splice(index, 1);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "createInvoke": {
|
|
479
|
+
const realId = patch.invoke.id ? uniqueId(nextGraph, "invoke-", patch.invoke.id) : `invoke-${Math.max(1, patches.indexOf(patch) + 1)}`;
|
|
480
|
+
if (patch.invoke.id) idMap.set(patch.invoke.id, realId);
|
|
481
|
+
if (!patch.invoke.src.trim()) break;
|
|
482
|
+
if (hasInvokeId(nextGraph, realId)) break;
|
|
483
|
+
nextGraph.nodes.find((candidate) => candidate.id === resolve(patch.nodeId))?.data.invokes.push({
|
|
484
|
+
...patch.invoke,
|
|
485
|
+
id: realId
|
|
486
|
+
});
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case "updateInvoke": {
|
|
490
|
+
const invoke = nextGraph.nodes.find((candidate) => candidate.id === resolve(patch.nodeId))?.data.invokes.find((candidate) => candidate.id === resolve(patch.invokeId));
|
|
491
|
+
if (invoke) Object.assign(invoke, patch.data);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case "deleteInvoke": {
|
|
495
|
+
const node = nextGraph.nodes.find((candidate) => candidate.id === resolve(patch.nodeId));
|
|
496
|
+
if (!node) break;
|
|
497
|
+
const index = node.data.invokes.findIndex((candidate) => candidate.id === resolve(patch.invokeId));
|
|
498
|
+
if (index >= 0) node.data.invokes.splice(index, 1);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
case "createImplementation": {
|
|
502
|
+
const code = patch.implementation.code;
|
|
503
|
+
const base = {
|
|
504
|
+
id: patch.implementation.id,
|
|
505
|
+
name: patch.implementation.name ?? patch.implementation.id,
|
|
506
|
+
description: null,
|
|
507
|
+
code: code ? {
|
|
508
|
+
body: code,
|
|
509
|
+
lang: "ts",
|
|
510
|
+
deps: {}
|
|
511
|
+
} : null
|
|
512
|
+
};
|
|
513
|
+
implementationList(nextGraph, patch.sourceType).push(patch.sourceType === "actor" ? {
|
|
514
|
+
...base,
|
|
515
|
+
inputSchema: null,
|
|
516
|
+
outputSchema: null
|
|
517
|
+
} : {
|
|
518
|
+
...base,
|
|
519
|
+
paramsSchema: null
|
|
520
|
+
});
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case "updateImplementation": {
|
|
524
|
+
const item = implementationList(nextGraph, patch.sourceType).find((implementation) => implementation.id === resolve(patch.implementationId));
|
|
525
|
+
if (!item) break;
|
|
526
|
+
if (patch.data.name !== void 0) {
|
|
527
|
+
item.name = patch.data.name;
|
|
528
|
+
item.id = patch.data.name;
|
|
529
|
+
}
|
|
530
|
+
if (patch.data.code !== void 0) item.code = {
|
|
531
|
+
body: patch.data.code,
|
|
532
|
+
lang: "ts",
|
|
533
|
+
deps: {}
|
|
534
|
+
};
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case "deleteImplementation": {
|
|
538
|
+
const list = implementationList(nextGraph, patch.sourceType);
|
|
539
|
+
const index = list.findIndex((implementation) => implementation.id === resolve(patch.implementationId));
|
|
540
|
+
if (index >= 0) list.splice(index, 1);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case "setGuard": {
|
|
544
|
+
if (!patch.guard.type.trim()) break;
|
|
545
|
+
const edge = nextGraph.edges.find((candidate) => candidate.id === resolve(patch.edgeId));
|
|
546
|
+
if (edge) edge.data.guard = patch.guard;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
case "deleteGuard": {
|
|
550
|
+
const edge = nextGraph.edges.find((candidate) => candidate.id === resolve(patch.edgeId));
|
|
551
|
+
if (edge) edge.data.guard = null;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case "createTag": {
|
|
555
|
+
const node = nextGraph.nodes.find((candidate) => candidate.id === resolve(patch.nodeId));
|
|
556
|
+
if (node && !node.data.tags.some((tag) => tag.name === patch.tag.name)) node.data.tags.push(patch.tag);
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case "deleteTag": {
|
|
560
|
+
const node = nextGraph.nodes.find((candidate) => candidate.id === resolve(patch.nodeId));
|
|
561
|
+
if (!node) break;
|
|
562
|
+
const index = node.data.tags.findIndex((tag) => tag.name === patch.tagName);
|
|
563
|
+
if (index >= 0) node.data.tags.splice(index, 1);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case "createAnnotation": {
|
|
567
|
+
const realId = uniqueId(nextGraph, "annotation-", patch.id);
|
|
568
|
+
if (patch.id) idMap.set(patch.id, realId);
|
|
569
|
+
nextGraph.annotations.push({
|
|
570
|
+
type: "node",
|
|
571
|
+
id: realId,
|
|
572
|
+
parentId: patch.parentId,
|
|
573
|
+
position: {
|
|
574
|
+
x: patch.x ?? 0,
|
|
575
|
+
y: patch.y ?? 0
|
|
576
|
+
},
|
|
577
|
+
x: patch.x ?? 0,
|
|
578
|
+
y: patch.y ?? 0,
|
|
579
|
+
dx: 0,
|
|
580
|
+
dy: 0,
|
|
581
|
+
width: 0,
|
|
582
|
+
height: 0,
|
|
583
|
+
content: patch.content,
|
|
584
|
+
data: { referenceIds: [] }
|
|
585
|
+
});
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
case "updateAnnotation": {
|
|
589
|
+
const annotation = nextGraph.annotations.find((candidate) => candidate.id === resolve(patch.annotationId));
|
|
590
|
+
if (annotation && patch.data.content !== void 0) annotation.content = patch.data.content;
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case "deleteAnnotation":
|
|
594
|
+
deleteEntitiesFromGraph(nextGraph, new Set([resolve(patch.annotationId)]));
|
|
595
|
+
break;
|
|
596
|
+
case "setContext":
|
|
597
|
+
nextGraph.data.context = patch.context;
|
|
598
|
+
break;
|
|
599
|
+
case "updateSchemas":
|
|
600
|
+
nextGraph.data.schemas ??= {
|
|
601
|
+
context: null,
|
|
602
|
+
events: null,
|
|
603
|
+
input: null,
|
|
604
|
+
output: null
|
|
605
|
+
};
|
|
606
|
+
if (patch.schemas.context !== void 0) nextGraph.data.schemas.context = patch.schemas.context;
|
|
607
|
+
if (patch.schemas.events !== void 0) nextGraph.data.schemas.events = patch.schemas.events;
|
|
608
|
+
if (patch.schemas.input !== void 0) nextGraph.data.schemas.input = patch.schemas.input;
|
|
609
|
+
if (patch.schemas.output !== void 0) nextGraph.data.schemas.output = patch.schemas.output;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
return nextGraph;
|
|
613
|
+
}
|
|
614
|
+
function isAutoGeneratedId(id) {
|
|
615
|
+
return !!id && id.startsWith("$auto-");
|
|
616
|
+
}
|
|
617
|
+
function getAuthoredNodeId(node) {
|
|
618
|
+
const authored = node.data.nodeId ?? void 0;
|
|
619
|
+
return authored && !isAutoGeneratedId(authored) ? authored : void 0;
|
|
620
|
+
}
|
|
621
|
+
function getRootEmittedId(graph, node) {
|
|
622
|
+
if (graph.id === "root" && node.data.key && node.data.key !== "root") return getAuthoredNodeId(node) ?? node.data.key;
|
|
623
|
+
return getAuthoredNodeId(node) ?? (isAutoGeneratedId(graph.id) ? void 0 : graph.id);
|
|
624
|
+
}
|
|
625
|
+
function getDefaultPathId(graph, node) {
|
|
626
|
+
if (!node.parentId) return getRootEmittedId(graph, node) ?? "(machine)";
|
|
627
|
+
const parentNode = graph.nodes.find((candidate) => candidate.id === node.parentId);
|
|
628
|
+
if (!parentNode) return node.data.key;
|
|
629
|
+
return `${getResolvedNodeId(graph, parentNode)}.${node.data.key}`;
|
|
630
|
+
}
|
|
631
|
+
function getEmittedNodeId(graph, node) {
|
|
632
|
+
const authored = getAuthoredNodeId(node);
|
|
633
|
+
if (!node.parentId) return getRootEmittedId(graph, node);
|
|
634
|
+
if (!authored) return void 0;
|
|
635
|
+
return authored === getDefaultPathId(graph, node) ? void 0 : authored;
|
|
636
|
+
}
|
|
637
|
+
function getResolvedNodeId(graph, node) {
|
|
638
|
+
return getEmittedNodeId(graph, node) ?? getDefaultPathId(graph, node);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Serializes JavaScript values as JS source code (not JSON).
|
|
642
|
+
* - Unquoted keys for valid identifiers
|
|
643
|
+
* - Single-quoted strings
|
|
644
|
+
* - Omits undefined values
|
|
645
|
+
* - Supports RawCode for verbatim expressions
|
|
646
|
+
* - Supports inline expression directives for verbatim expressions
|
|
647
|
+
*/
|
|
648
|
+
const VALID_IDENT = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
649
|
+
var RawCode = class {
|
|
650
|
+
constructor(code) {
|
|
651
|
+
this.code = code;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
function serializeJS(value, indent = 0, step = 2) {
|
|
655
|
+
if (value instanceof RawCode) return value.code;
|
|
656
|
+
if (isInlineExpressionDirective(value)) return value.expr;
|
|
657
|
+
if (value === void 0) return "undefined";
|
|
658
|
+
if (value === null) return "null";
|
|
659
|
+
if (typeof value === "string") return `'${escapeString(value)}'`;
|
|
660
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
661
|
+
if (Array.isArray(value)) return serializeArray(value, indent, step);
|
|
662
|
+
if (typeof value === "object") return serializeObject(value, indent, step);
|
|
663
|
+
return String(value);
|
|
664
|
+
}
|
|
665
|
+
function escapeString(s) {
|
|
666
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
667
|
+
}
|
|
668
|
+
function serializeArray(arr, indent, step) {
|
|
669
|
+
const filtered = arr.filter((v) => v !== void 0);
|
|
670
|
+
if (filtered.length === 0) return "[]";
|
|
671
|
+
const inner = indent + step;
|
|
672
|
+
const pad = " ".repeat(inner);
|
|
673
|
+
const closePad = " ".repeat(indent);
|
|
674
|
+
return `[\n${filtered.map((v) => `${pad}${serializeJS(v, inner, step)}`).join(",\n")},\n${closePad}]`;
|
|
675
|
+
}
|
|
676
|
+
function serializeObject(obj, indent, step) {
|
|
677
|
+
const entries = Object.entries(obj).filter(([, v]) => v !== void 0);
|
|
678
|
+
if (entries.length === 0) return "{}";
|
|
679
|
+
const inner = indent + step;
|
|
680
|
+
const pad = " ".repeat(inner);
|
|
681
|
+
const closePad = " ".repeat(indent);
|
|
682
|
+
return `{\n${entries.map(([key, val]) => {
|
|
683
|
+
const k = VALID_IDENT.test(key) ? key : `'${escapeString(key)}'`;
|
|
684
|
+
const serialized = serializeJS(val, inner, step);
|
|
685
|
+
if (isRawExpression(val) && serialized.includes("\n")) return `${pad}${k}: ${indentRawCode(serialized, inner)}`;
|
|
686
|
+
return `${pad}${k}: ${serialized}`;
|
|
687
|
+
}).join(",\n")},\n${closePad}}`;
|
|
688
|
+
}
|
|
689
|
+
function isRawExpression(value) {
|
|
690
|
+
return value instanceof RawCode || isInlineExpressionDirective(value);
|
|
691
|
+
}
|
|
692
|
+
function isInlineExpressionDirective(value) {
|
|
693
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
694
|
+
const directive = value;
|
|
695
|
+
return directive["@type"] === "code" && typeof directive.expr === "string";
|
|
696
|
+
}
|
|
697
|
+
function indentRawCode(code, indent) {
|
|
698
|
+
const lines = code.split("\n");
|
|
699
|
+
if (lines.length <= 1) return code;
|
|
700
|
+
const pad = " ".repeat(indent);
|
|
701
|
+
return [lines[0], ...lines.slice(1).map((line) => line.trim() === "" ? "" : `${pad}${line}`)].join("\n");
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Strips `export default` wrapper from an expression string.
|
|
705
|
+
* E.g. `"export default assign({ ... })"` → `"assign({ ... })"`
|
|
706
|
+
*/
|
|
707
|
+
function stripExportDefault(code) {
|
|
708
|
+
const trimmed = code.trim();
|
|
709
|
+
if (trimmed.startsWith("export default ")) return trimmed.slice(15).replace(/;$/, "");
|
|
710
|
+
return trimmed;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Graph-native transition references. Transitions point at invoke/action/state
|
|
714
|
+
* blocks by graph id (`@statelyai.*`); the literal `xstate.*` event is produced
|
|
715
|
+
* here at codegen time. Literal `xstate.*` strings (pre-resolved, e.g. baked by
|
|
716
|
+
* an import) are passed through unchanged.
|
|
717
|
+
*/
|
|
718
|
+
const STATELY_PREFIX = "@statelyai";
|
|
719
|
+
/** Legacy prefix accepted on read for graphs persisted before the rename. */
|
|
720
|
+
const LEGACY_STATELY_PREFIX = "@stately";
|
|
721
|
+
const STATELY_REF_PREFIX = {
|
|
722
|
+
invokeDone: "@statelyai.invoke.done",
|
|
723
|
+
invokeError: "@statelyai.invoke.error",
|
|
724
|
+
invokeSnapshot: "@statelyai.invoke.snapshot",
|
|
725
|
+
actionDone: "@statelyai.action.done",
|
|
726
|
+
actionError: "@statelyai.action.error",
|
|
727
|
+
stateDone: "@statelyai.state.done"
|
|
728
|
+
};
|
|
729
|
+
function parseStatelyRef(eventType) {
|
|
730
|
+
if (eventType.startsWith(`${LEGACY_STATELY_PREFIX}.`)) eventType = `${STATELY_PREFIX}${eventType.slice(8)}`;
|
|
731
|
+
if (!eventType.startsWith(`${STATELY_PREFIX}.`)) return null;
|
|
732
|
+
for (const kind of Object.keys(STATELY_REF_PREFIX)) {
|
|
733
|
+
const prefix = STATELY_REF_PREFIX[kind];
|
|
734
|
+
if (eventType === prefix) return {
|
|
735
|
+
kind,
|
|
736
|
+
blockId: ""
|
|
737
|
+
};
|
|
738
|
+
if (eventType.startsWith(`${prefix}.`)) return {
|
|
739
|
+
kind,
|
|
740
|
+
blockId: eventType.slice(prefix.length + 1)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
/** Find an invoke block by its graph id, or by its emitted id (legacy literals). */
|
|
746
|
+
function findInvokeByActorId(node, actorId) {
|
|
747
|
+
const invokes = node.data.invokes ?? [];
|
|
748
|
+
return invokes.find((inv) => inv.id === actorId) ?? invokes.find((inv) => (inv.invocationId ?? inv.id) === actorId);
|
|
749
|
+
}
|
|
750
|
+
/** Resolve a spawn action block id to its spawned actor id (`params.id`). */
|
|
751
|
+
function resolveSpawnActorId$1(node, actionId) {
|
|
752
|
+
const spawnId = [...node.data.entry ?? [], ...node.data.exit ?? []].find((action) => action.id === actionId)?.params?.["id"];
|
|
753
|
+
return typeof spawnId === "string" && spawnId ? spawnId : actionId;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Resolve an edge's target into the string XState expects on the source state.
|
|
757
|
+
* Prefer a relative key path (resolved against the source's parent, or the root
|
|
758
|
+
* itself for root transitions) so output reads like a hand-written machine —
|
|
759
|
+
* e.g. a sibling's child becomes `Unlocked.Manual`. Fall back to an absolute
|
|
760
|
+
* `#id` reference only when the target lives outside the source's subtree.
|
|
761
|
+
*/
|
|
762
|
+
function resolveTransitionTarget(graph, sourceNode, targetNode) {
|
|
763
|
+
const isRootSource = sourceNode.parentId == null;
|
|
764
|
+
const baseId = sourceNode.parentId ?? sourceNode.id;
|
|
765
|
+
const keys = [];
|
|
766
|
+
let current = targetNode;
|
|
767
|
+
while (current) {
|
|
768
|
+
keys.unshift(current.data.key);
|
|
769
|
+
if (current.parentId === baseId) {
|
|
770
|
+
if (isRootSource) return `.${keys.join(".")}`;
|
|
771
|
+
return keys.join(".");
|
|
772
|
+
}
|
|
773
|
+
const parentId = current.parentId;
|
|
774
|
+
current = graph.nodes.find((n) => n.id === parentId);
|
|
775
|
+
}
|
|
776
|
+
return `#${getAbsoluteTargetId(graph, targetNode)}`;
|
|
777
|
+
}
|
|
778
|
+
function getAbsoluteTargetId(graph, targetNode) {
|
|
779
|
+
const resolved = getResolvedNodeId(graph, targetNode);
|
|
780
|
+
if (!resolved.startsWith("root.")) return resolved;
|
|
781
|
+
const rootNode = graph.nodes.find((node) => node.parentId === null);
|
|
782
|
+
const rootKey = rootNode?.data.nodeId && !isAutoGeneratedId(rootNode.data.nodeId) ? rootNode.data.nodeId : rootNode?.data.key;
|
|
783
|
+
return rootKey && rootKey !== "root" ? `${rootKey}.${resolved.slice(5)}` : resolved;
|
|
784
|
+
}
|
|
785
|
+
/** Unwrap single-element arrays to a single value (XState single-or-array convention) */
|
|
786
|
+
function singleOrArray(arr) {
|
|
787
|
+
return arr.length === 1 ? arr[0] : arr;
|
|
788
|
+
}
|
|
789
|
+
/** Unwrap each value in a record of arrays */
|
|
790
|
+
function singleOrArrayRecord(record) {
|
|
791
|
+
const result = {};
|
|
792
|
+
for (const [key, arr] of Object.entries(record)) result[key] = singleOrArray(arr);
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Remove empty arrays/objects/strings, unwrap single-element arrays.
|
|
797
|
+
* Matches studio's simplifyAttributes behavior.
|
|
798
|
+
*/
|
|
799
|
+
function simplifyAttributes(obj) {
|
|
800
|
+
for (const key of Object.keys(obj)) {
|
|
801
|
+
const value = obj[key];
|
|
802
|
+
if (value === void 0 || value === null) delete obj[key];
|
|
803
|
+
else if (Array.isArray(value)) {
|
|
804
|
+
if (value.length === 0) delete obj[key];
|
|
805
|
+
else if (value.length === 1) obj[key] = value[0];
|
|
806
|
+
} else if (typeof value === "object" && Object.keys(value).length === 0) delete obj[key];
|
|
807
|
+
else if (typeof value === "string" && value === "") delete obj[key];
|
|
808
|
+
}
|
|
809
|
+
return obj;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Recursively apply simplifyAttributes to a config tree.
|
|
813
|
+
* Cleans up the config and all nested state nodes, transitions, and invokes.
|
|
814
|
+
*/
|
|
815
|
+
function deepSimplify(config) {
|
|
816
|
+
for (const mapKey of ["on", "after"]) {
|
|
817
|
+
const map = config[mapKey];
|
|
818
|
+
if (map && typeof map === "object" && !Array.isArray(map)) {
|
|
819
|
+
for (const [, val] of Object.entries(map)) if (val && typeof val === "object" && !Array.isArray(val)) simplifyAttributes(val);
|
|
820
|
+
else if (Array.isArray(val)) {
|
|
821
|
+
for (const item of val) if (item && typeof item === "object") simplifyAttributes(item);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
for (const key of ["always", "onDone"]) {
|
|
826
|
+
const val = config[key];
|
|
827
|
+
if (val && typeof val === "object" && !Array.isArray(val)) simplifyAttributes(val);
|
|
828
|
+
else if (Array.isArray(val)) {
|
|
829
|
+
for (const item of val) if (item && typeof item === "object") simplifyAttributes(item);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const invoke = config.invoke;
|
|
833
|
+
if (invoke && typeof invoke === "object" && !Array.isArray(invoke)) simplifyAttributes(invoke);
|
|
834
|
+
else if (Array.isArray(invoke)) {
|
|
835
|
+
for (const item of invoke) if (item && typeof item === "object") simplifyAttributes(item);
|
|
836
|
+
}
|
|
837
|
+
simplifyAttributes(config);
|
|
838
|
+
if (config.states && typeof config.states === "object") for (const stateConfig of Object.values(config.states)) deepSimplify(stateConfig);
|
|
839
|
+
return config;
|
|
840
|
+
}
|
|
841
|
+
function buildMeta(meta, color) {
|
|
842
|
+
if (isCodeExpression(meta)) return meta;
|
|
843
|
+
const result = {};
|
|
844
|
+
if (meta) {
|
|
845
|
+
for (const [k, v] of Object.entries(meta)) if (v !== void 0 && v !== null) result[k] = v;
|
|
846
|
+
}
|
|
847
|
+
if (color) result["@statelyai.color"] = color;
|
|
848
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
849
|
+
}
|
|
850
|
+
function isCodeExpression(value) {
|
|
851
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && value["@type"] === "code" && typeof value.expr === "string");
|
|
852
|
+
}
|
|
853
|
+
/** Format event param — supports { type: string } (new) and plain string (legacy) */
|
|
854
|
+
function formatEventParam(event) {
|
|
855
|
+
if (event && typeof event === "object" && "type" in event) return formatInlineObject(event);
|
|
856
|
+
if (typeof event === "string") return `{ type: '${event}' }`;
|
|
857
|
+
return "undefined";
|
|
858
|
+
}
|
|
859
|
+
function formatStringParam(value) {
|
|
860
|
+
return typeof value === "string" ? `'${value}'` : "undefined";
|
|
861
|
+
}
|
|
862
|
+
function formatInlineObject(value) {
|
|
863
|
+
return `{ ${Object.entries(value).filter(([, item]) => item !== void 0).map(([key, item]) => `${formatObjectKey(key)}: ${serializeJS(item)}`).join(", ")} }`;
|
|
864
|
+
}
|
|
865
|
+
function formatObjectKey(key) {
|
|
866
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
867
|
+
}
|
|
868
|
+
const eventParamSchema = union([string(), object({ type: string() }).catchall(unknown())]);
|
|
869
|
+
const delayParamSchema = union([number(), string()]);
|
|
870
|
+
const builtInActionParamSchemas = {
|
|
871
|
+
"xstate.raise": object({
|
|
872
|
+
event: eventParamSchema.optional(),
|
|
873
|
+
id: string().optional(),
|
|
874
|
+
delay: delayParamSchema.optional()
|
|
875
|
+
}),
|
|
876
|
+
"xstate.sendTo": object({
|
|
877
|
+
to: string().optional(),
|
|
878
|
+
event: eventParamSchema.optional(),
|
|
879
|
+
id: string().optional(),
|
|
880
|
+
delay: delayParamSchema.optional()
|
|
881
|
+
}),
|
|
882
|
+
"xstate.cancel": object({ sendId: string().optional() }),
|
|
883
|
+
"xstate.emit": object({ event: eventParamSchema.optional() }),
|
|
884
|
+
"xstate.spawnChild": object({
|
|
885
|
+
src: string().optional(),
|
|
886
|
+
id: string().optional(),
|
|
887
|
+
systemId: string().optional()
|
|
888
|
+
}),
|
|
889
|
+
"xstate.stopChild": object({ actorRef: string().optional() }),
|
|
890
|
+
"xstate.log": object({ label: string().optional() }),
|
|
891
|
+
"xstate.assign": object({ assignment: record(string(), unknown()).optional() }).catchall(unknown())
|
|
892
|
+
};
|
|
893
|
+
const XSTATE_BUILT_IN_ACTIONS = {
|
|
894
|
+
"xstate.raise": {
|
|
895
|
+
package: "xstate",
|
|
896
|
+
version: "^5",
|
|
897
|
+
import: "raise",
|
|
898
|
+
importKind: "named",
|
|
899
|
+
paramsSchema: builtInActionParamSchemas["xstate.raise"],
|
|
900
|
+
emit(params) {
|
|
901
|
+
const eventStr = formatEventParam(params?.event);
|
|
902
|
+
const opts = [];
|
|
903
|
+
if (params?.id) opts.push(`id: '${params.id}'`);
|
|
904
|
+
if (params?.delay != null) opts.push(`delay: ${JSON.stringify(params.delay)}`);
|
|
905
|
+
return `raise(${eventStr}${opts.length > 0 ? `, { ${opts.join(", ")} }` : ""})`;
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
"xstate.sendTo": {
|
|
909
|
+
package: "xstate",
|
|
910
|
+
version: "^5",
|
|
911
|
+
import: "sendTo",
|
|
912
|
+
importKind: "named",
|
|
913
|
+
paramsSchema: builtInActionParamSchemas["xstate.sendTo"],
|
|
914
|
+
emit(params) {
|
|
915
|
+
const to = formatStringParam(params?.to);
|
|
916
|
+
const eventStr = formatEventParam(params?.event);
|
|
917
|
+
const opts = [];
|
|
918
|
+
if (params?.id) opts.push(`id: '${params.id}'`);
|
|
919
|
+
if (params?.delay != null) opts.push(`delay: ${JSON.stringify(params.delay)}`);
|
|
920
|
+
return `sendTo(${to}, ${eventStr}${opts.length > 0 ? `, { ${opts.join(", ")} }` : ""})`;
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
"xstate.cancel": {
|
|
924
|
+
package: "xstate",
|
|
925
|
+
version: "^5",
|
|
926
|
+
import: "cancel",
|
|
927
|
+
importKind: "named",
|
|
928
|
+
paramsSchema: builtInActionParamSchemas["xstate.cancel"],
|
|
929
|
+
emit(params) {
|
|
930
|
+
return `cancel(${formatStringParam(params?.sendId)})`;
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
"xstate.emit": {
|
|
934
|
+
package: "xstate",
|
|
935
|
+
version: "^5",
|
|
936
|
+
import: "emit",
|
|
937
|
+
importKind: "named",
|
|
938
|
+
paramsSchema: builtInActionParamSchemas["xstate.emit"],
|
|
939
|
+
emit(params) {
|
|
940
|
+
return `emit(${formatEventParam(params?.event)})`;
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
"xstate.spawnChild": {
|
|
944
|
+
package: "xstate",
|
|
945
|
+
version: "^5",
|
|
946
|
+
import: "spawnChild",
|
|
947
|
+
importKind: "named",
|
|
948
|
+
paramsSchema: builtInActionParamSchemas["xstate.spawnChild"],
|
|
949
|
+
emit(params) {
|
|
950
|
+
const src = formatStringParam(params?.src);
|
|
951
|
+
const opts = [];
|
|
952
|
+
if (params?.id) opts.push(`id: '${params.id}'`);
|
|
953
|
+
if (params?.systemId) opts.push(`systemId: '${params.systemId}'`);
|
|
954
|
+
return `spawnChild(${src}${opts.length > 0 ? `, { ${opts.join(", ")} }` : ""})`;
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
"xstate.stopChild": {
|
|
958
|
+
package: "xstate",
|
|
959
|
+
version: "^5",
|
|
960
|
+
import: "stopChild",
|
|
961
|
+
importKind: "named",
|
|
962
|
+
paramsSchema: builtInActionParamSchemas["xstate.stopChild"],
|
|
963
|
+
emit(params) {
|
|
964
|
+
return `stopChild(${formatStringParam(params?.actorRef)})`;
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
"xstate.log": {
|
|
968
|
+
package: "xstate",
|
|
969
|
+
version: "^5",
|
|
970
|
+
import: "log",
|
|
971
|
+
importKind: "named",
|
|
972
|
+
paramsSchema: builtInActionParamSchemas["xstate.log"],
|
|
973
|
+
emit(params) {
|
|
974
|
+
return `log(${(params?.label ? `'${params.label}'` : void 0) ?? ""})`;
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
"xstate.assign": {
|
|
978
|
+
package: "xstate",
|
|
979
|
+
version: "^5",
|
|
980
|
+
import: "assign",
|
|
981
|
+
importKind: "named",
|
|
982
|
+
paramsSchema: builtInActionParamSchemas["xstate.assign"],
|
|
983
|
+
emit(params) {
|
|
984
|
+
const assignments = params?.assignment && typeof params.assignment === "object" && !Array.isArray(params.assignment) ? params.assignment : params;
|
|
985
|
+
const entries = Object.entries(assignments ?? {}).filter(([k, v]) => k && v !== void 0);
|
|
986
|
+
if (entries.length === 0) return "assign({})";
|
|
987
|
+
return `assign({ ${entries.map(([k, v]) => `${JSON.stringify(k)}: ${formatAssignValue(v)}`).join(", ")} })`;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
function isBuiltInActionType(type) {
|
|
992
|
+
return type in XSTATE_BUILT_IN_ACTIONS;
|
|
993
|
+
}
|
|
994
|
+
function emitBuiltInAction(type, params) {
|
|
995
|
+
return XSTATE_BUILT_IN_ACTIONS[type].emit(params);
|
|
996
|
+
}
|
|
997
|
+
function actionCodeExpression(expr, source) {
|
|
998
|
+
return {
|
|
999
|
+
"@type": "code",
|
|
1000
|
+
lang: "ts",
|
|
1001
|
+
expr,
|
|
1002
|
+
imports: [{
|
|
1003
|
+
package: source.package,
|
|
1004
|
+
version: source.version,
|
|
1005
|
+
import: source.import,
|
|
1006
|
+
importKind: source.importKind
|
|
1007
|
+
}]
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function inlineCodeExpression$1(expr) {
|
|
1011
|
+
return {
|
|
1012
|
+
"@type": "code",
|
|
1013
|
+
lang: "ts",
|
|
1014
|
+
expr: stripExportDefault(expr)
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Serialize an edge guard for a machine config. Codegen emits inline guards as
|
|
1019
|
+
* a code-expression directive so the source round-trips. The simulation passes
|
|
1020
|
+
* `asTypeRef` to get a plain `{ type }` reference instead (it stubs guards by
|
|
1021
|
+
* type and can't evaluate the source) — see MachineConfigOptions.
|
|
1022
|
+
*/
|
|
1023
|
+
function serializeGuard(guard, asTypeRef) {
|
|
1024
|
+
if (asTypeRef) return guard.params ? {
|
|
1025
|
+
type: guard.type,
|
|
1026
|
+
params: guard.params
|
|
1027
|
+
} : { type: guard.type };
|
|
1028
|
+
return guard.code ? inlineCodeExpression$1(guard.code) : guard;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Converts a built-in xstate ActionItem to a code expression directive,
|
|
1032
|
+
* or returns a plain { type, params } object for user-defined actions.
|
|
1033
|
+
*/
|
|
1034
|
+
function serializeActionItem(action) {
|
|
1035
|
+
const { type, params } = action;
|
|
1036
|
+
const exprCode = type === "xstate.expr" && typeof params?.code === "string" ? params.code : void 0;
|
|
1037
|
+
if (isBuiltInActionType(type)) {
|
|
1038
|
+
const builtInAction = XSTATE_BUILT_IN_ACTIONS[type];
|
|
1039
|
+
return actionCodeExpression(emitBuiltInAction(type, params), builtInAction);
|
|
1040
|
+
}
|
|
1041
|
+
if (exprCode) return inlineCodeExpression$1(exprCode);
|
|
1042
|
+
if (!params || Object.keys(params).length === 0) return { type };
|
|
1043
|
+
return {
|
|
1044
|
+
type,
|
|
1045
|
+
params
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function formatAssignValue(value) {
|
|
1049
|
+
if (typeof value !== "string") return JSON.stringify(value);
|
|
1050
|
+
const templateExpression = value.match(/^\{\{([\s\S]*)\}\}$/);
|
|
1051
|
+
if (templateExpression) return templateExpression[1].trim();
|
|
1052
|
+
return JSON.stringify(value);
|
|
1053
|
+
}
|
|
1054
|
+
/** Normalize tag to string — handles both `string` and `{ name: string }` */
|
|
1055
|
+
function tagToString(tag) {
|
|
1056
|
+
return typeof tag === "string" ? tag : tag.name;
|
|
1057
|
+
}
|
|
1058
|
+
function graphToMachineConfig(graph, options = {}) {
|
|
1059
|
+
const { showDescriptions = true, showMeta = true, inlineGuardsAsTypeRef = false } = options;
|
|
1060
|
+
const config = {};
|
|
1061
|
+
function getNodeConfig(node) {
|
|
1062
|
+
const nodeConfig = node.parentId ? {} : config;
|
|
1063
|
+
const resolvedNodeId = getEmittedNodeId(graph, node);
|
|
1064
|
+
const initialNode = node.data.initialId ? graph.nodes.find((n) => n.id === node.data.initialId) : null;
|
|
1065
|
+
const nodeType = [
|
|
1066
|
+
"final",
|
|
1067
|
+
"history",
|
|
1068
|
+
"parallel"
|
|
1069
|
+
].includes(node.data.type) ? node.data.type : void 0;
|
|
1070
|
+
const tags = node.data.tags;
|
|
1071
|
+
Object.assign(nodeConfig, {
|
|
1072
|
+
id: resolvedNodeId,
|
|
1073
|
+
type: nodeType === "normal" ? void 0 : nodeType ?? void 0,
|
|
1074
|
+
initial: initialNode?.data.key,
|
|
1075
|
+
...node.data.entry?.length ? { entry: node.data.entry.map(serializeActionItem) } : void 0,
|
|
1076
|
+
...node.data.exit?.length ? { exit: node.data.exit.map(serializeActionItem) } : void 0,
|
|
1077
|
+
...node.data.invokes?.length ? { invoke: node.data.invokes.map((inv) => ({
|
|
1078
|
+
src: inv.src,
|
|
1079
|
+
id: inv.invocationId ?? inv.id,
|
|
1080
|
+
...inv.input ? { input: inv.input } : void 0,
|
|
1081
|
+
...inv.output ? { output: inv.output } : void 0
|
|
1082
|
+
})) } : void 0,
|
|
1083
|
+
...tags?.length ? { tags: tags.map(tagToString) } : void 0,
|
|
1084
|
+
...showDescriptions && node.data.description ? { description: node.data.description } : void 0,
|
|
1085
|
+
...showMeta && node.data.meta && Object.keys(node.data.meta).length > 0 ? { meta: node.data.meta } : void 0,
|
|
1086
|
+
...node.data.history ? { history: node.data.history } : void 0
|
|
1087
|
+
});
|
|
1088
|
+
const childNodes = graph.nodes.filter((n) => n.parentId === node.id && !n.data.temp);
|
|
1089
|
+
const edges = graph.edges.filter((edge) => edge.sourceId === node.id && !edge.data.temp);
|
|
1090
|
+
if (edges.length > 0) {
|
|
1091
|
+
const on = {};
|
|
1092
|
+
const after = {};
|
|
1093
|
+
const alwaysArr = [];
|
|
1094
|
+
const onDone = {};
|
|
1095
|
+
const invokeMap = {};
|
|
1096
|
+
for (const edge of edges) {
|
|
1097
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.targetId);
|
|
1098
|
+
const resolvedTarget = targetNode ? resolveTransitionTarget(graph, node, targetNode) : "";
|
|
1099
|
+
const type = edge.data.eventType ?? "";
|
|
1100
|
+
const transitionMeta = showMeta ? buildMeta(edge.data.meta, edge.data.color) : void 0;
|
|
1101
|
+
const transitionObject = {
|
|
1102
|
+
target: edge.data.transitionType === "targetless" ? void 0 : `${resolvedTarget}`,
|
|
1103
|
+
...edge.data.transitionType === "reenter" ? { reenter: true } : void 0,
|
|
1104
|
+
guard: edge.data.guard ? serializeGuard(edge.data.guard, inlineGuardsAsTypeRef) : void 0,
|
|
1105
|
+
actions: edge.data.actions?.length ? edge.data.actions.map(serializeActionItem) : void 0,
|
|
1106
|
+
description: edge.data.description ?? void 0,
|
|
1107
|
+
...transitionMeta ? { meta: transitionMeta } : void 0
|
|
1108
|
+
};
|
|
1109
|
+
const pushOn = (key) => {
|
|
1110
|
+
if (!on[key]) on[key] = [];
|
|
1111
|
+
on[key].push(transitionObject);
|
|
1112
|
+
};
|
|
1113
|
+
const nestInvoke = (blockId, slot) => {
|
|
1114
|
+
invokeMap[blockId] = {
|
|
1115
|
+
...invokeMap[blockId],
|
|
1116
|
+
[slot]: (invokeMap[blockId]?.[slot] ?? []).concat(transitionObject)
|
|
1117
|
+
};
|
|
1118
|
+
};
|
|
1119
|
+
const ref = parseStatelyRef(type);
|
|
1120
|
+
if (ref) if (ref.kind === "invokeDone") nestInvoke(ref.blockId, "onDone");
|
|
1121
|
+
else if (ref.kind === "invokeError") nestInvoke(ref.blockId, "onError");
|
|
1122
|
+
else if (ref.kind === "invokeSnapshot") nestInvoke(ref.blockId, "onSnapshot");
|
|
1123
|
+
else if (ref.kind === "stateDone") {
|
|
1124
|
+
if (!onDone[type]) onDone[type] = [];
|
|
1125
|
+
onDone[type].push(transitionObject);
|
|
1126
|
+
} else pushOn(`xstate.${ref.kind === "actionDone" ? "done" : "error"}.actor.${resolveSpawnActorId$1(node, ref.blockId)}`);
|
|
1127
|
+
else if (type === "") alwaysArr.push(transitionObject);
|
|
1128
|
+
else if (type.startsWith("xstate.after.")) {
|
|
1129
|
+
const delayKey = type.slice(13).split(".")[0];
|
|
1130
|
+
if (!after[delayKey]) after[delayKey] = [];
|
|
1131
|
+
after[delayKey].push(transitionObject);
|
|
1132
|
+
} else if (type === "*") pushOn("*");
|
|
1133
|
+
else if (type.startsWith("xstate.done.state")) {
|
|
1134
|
+
if (!onDone[type]) onDone[type] = [];
|
|
1135
|
+
onDone[type].push(transitionObject);
|
|
1136
|
+
} else if (type.startsWith("xstate.done.actor.") || type.startsWith("xstate.error.actor.") || type.startsWith("xstate.snapshot.actor.")) {
|
|
1137
|
+
const verb = type.startsWith("xstate.done.actor.") ? "onDone" : type.startsWith("xstate.error.actor.") ? "onError" : "onSnapshot";
|
|
1138
|
+
const invoke = findInvokeByActorId(node, type.slice(type.lastIndexOf(".actor.") + 7));
|
|
1139
|
+
if (invoke) nestInvoke(invoke.id, verb);
|
|
1140
|
+
else pushOn(type);
|
|
1141
|
+
} else pushOn(type);
|
|
1142
|
+
}
|
|
1143
|
+
if (Object.keys(on).length > 0) nodeConfig.on = singleOrArrayRecord(on);
|
|
1144
|
+
if (Object.keys(after).length > 0) nodeConfig.after = singleOrArrayRecord(after);
|
|
1145
|
+
if (alwaysArr.length > 0) nodeConfig.always = singleOrArray(alwaysArr);
|
|
1146
|
+
if (Object.keys(onDone).length > 0) nodeConfig.onDone = singleOrArray(Object.values(onDone).flat());
|
|
1147
|
+
if (Object.keys(invokeMap).length > 0 && Array.isArray(nodeConfig.invoke)) nodeConfig.invoke = nodeConfig.invoke.map((inv, index) => {
|
|
1148
|
+
const mapped = invokeMap[node.data.invokes?.[index]?.id ?? inv.id];
|
|
1149
|
+
if (!mapped) return inv;
|
|
1150
|
+
return {
|
|
1151
|
+
...inv,
|
|
1152
|
+
...mapped.onDone ? { onDone: singleOrArray(mapped.onDone) } : void 0,
|
|
1153
|
+
...mapped.onError ? { onError: singleOrArray(mapped.onError) } : void 0,
|
|
1154
|
+
...mapped.onSnapshot ? { onSnapshot: singleOrArray(mapped.onSnapshot) } : void 0
|
|
1155
|
+
};
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
if (Array.isArray(nodeConfig.invoke)) nodeConfig.invoke = nodeConfig.invoke.map((inv) => {
|
|
1159
|
+
if (inv.id && !isAutoGeneratedId(inv.id)) return inv;
|
|
1160
|
+
const { id: _id, ...rest } = inv;
|
|
1161
|
+
return rest;
|
|
1162
|
+
});
|
|
1163
|
+
if (childNodes.length > 0) {
|
|
1164
|
+
nodeConfig.states = {};
|
|
1165
|
+
for (const childState of childNodes) nodeConfig.states[childState.data.key] = getNodeConfig(childState);
|
|
1166
|
+
}
|
|
1167
|
+
return nodeConfig;
|
|
1168
|
+
}
|
|
1169
|
+
const rootNode = graph.nodes.find((node) => node.data.parentId === null || node.parentId == null);
|
|
1170
|
+
if (!rootNode) throw new Error("No root node found");
|
|
1171
|
+
getNodeConfig(rootNode);
|
|
1172
|
+
if (graph.data.context !== void 0 && graph.data.context !== null) config.context = graph.data.context;
|
|
1173
|
+
return deepSimplify(config);
|
|
1174
|
+
}
|
|
1175
|
+
const REF_PREFIX = {
|
|
1176
|
+
invokeDone: "@statelyai.invoke.done",
|
|
1177
|
+
invokeError: "@statelyai.invoke.error",
|
|
1178
|
+
invokeSnapshot: "@statelyai.invoke.snapshot",
|
|
1179
|
+
actionDone: "@statelyai.action.done",
|
|
1180
|
+
actionError: "@statelyai.action.error",
|
|
1181
|
+
stateDone: "@statelyai.state.done"
|
|
1182
|
+
};
|
|
1183
|
+
function parseRef(eventType) {
|
|
1184
|
+
if (eventType.startsWith("@stately.")) eventType = `@statelyai${eventType.slice(8)}`;
|
|
1185
|
+
if (!eventType.startsWith("@statelyai.")) return null;
|
|
1186
|
+
for (const kind of Object.keys(REF_PREFIX)) {
|
|
1187
|
+
const prefix = REF_PREFIX[kind];
|
|
1188
|
+
if (eventType === prefix) return {
|
|
1189
|
+
kind,
|
|
1190
|
+
blockId: ""
|
|
1191
|
+
};
|
|
1192
|
+
if (eventType.startsWith(`${prefix}.`)) return {
|
|
1193
|
+
kind,
|
|
1194
|
+
blockId: eventType.slice(prefix.length + 1)
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
function getSourceNode(graph, edge) {
|
|
1200
|
+
return graph.nodes.find((node) => node.id === edge.sourceId);
|
|
1201
|
+
}
|
|
1202
|
+
function resolveSpawnActorId(sourceNode, actionId) {
|
|
1203
|
+
const actorId = [...sourceNode?.data.entry ?? [], ...sourceNode?.data.exit ?? []].find((action) => action.id === actionId)?.params?.["id"];
|
|
1204
|
+
return typeof actorId === "string" && actorId ? actorId : actionId;
|
|
1205
|
+
}
|
|
1206
|
+
function getTransitionRuntimeEventType(graph, edge) {
|
|
1207
|
+
const eventType = edge.data.eventType;
|
|
1208
|
+
const sourceNode = getSourceNode(graph, edge);
|
|
1209
|
+
if (eventType.startsWith("xstate.after.")) {
|
|
1210
|
+
const delayKey = eventType.slice(13).split(".")[0];
|
|
1211
|
+
return sourceNode ? `xstate.after.${delayKey}.${getResolvedNodeId(graph, sourceNode)}` : eventType;
|
|
1212
|
+
}
|
|
1213
|
+
const ref = parseRef(eventType);
|
|
1214
|
+
if (!ref) return eventType;
|
|
1215
|
+
if (ref.kind === "stateDone") {
|
|
1216
|
+
const node = graph.nodes.find((candidate) => candidate.id === ref.blockId) ?? sourceNode;
|
|
1217
|
+
return `xstate.done.state.${node ? getResolvedNodeId(graph, node) : ref.blockId}`;
|
|
1218
|
+
}
|
|
1219
|
+
if (ref.kind === "invokeDone" || ref.kind === "invokeError" || ref.kind === "invokeSnapshot") {
|
|
1220
|
+
const invokes = sourceNode?.data.invokes ?? [];
|
|
1221
|
+
const index = invokes.findIndex((invoke) => invoke.id === ref.blockId);
|
|
1222
|
+
const actorId = (index >= 0 ? invokes[index] : void 0)?.invocationId || (sourceNode && index >= 0 ? `${index}.${getResolvedNodeId(graph, sourceNode)}` : ref.blockId);
|
|
1223
|
+
return `xstate.${ref.kind === "invokeDone" ? "done" : ref.kind === "invokeError" ? "error" : "snapshot"}.actor.${actorId}`;
|
|
1224
|
+
}
|
|
1225
|
+
return `xstate.${ref.kind === "actionDone" ? "done" : "error"}.actor.${resolveSpawnActorId(sourceNode, ref.blockId)}`;
|
|
1226
|
+
}
|
|
1227
|
+
function transitionMatchesRuntimeEvent(graph, edge, eventType) {
|
|
1228
|
+
return getTransitionRuntimeEventType(graph, edge) === eventType;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Map an XState state node (identified by its array of keys from the root) to
|
|
1232
|
+
* the corresponding graph node id. The runtime snapshot's `_nodes[].id` are
|
|
1233
|
+
* XState resolved ids (delimited paths), which do not match the graph's opaque
|
|
1234
|
+
* node ids; consumers (highlighting, edge lookup) need graph node ids.
|
|
1235
|
+
*/
|
|
1236
|
+
function getGraphNodeIdForPath(graph, path) {
|
|
1237
|
+
const root = graph.nodes.find((n) => n.parentId === null);
|
|
1238
|
+
if (!root) return null;
|
|
1239
|
+
if (path.length === 0) return root.id;
|
|
1240
|
+
let parentId = root.id;
|
|
1241
|
+
let nodeId = null;
|
|
1242
|
+
for (const key of path) {
|
|
1243
|
+
const child = graph.nodes.find((n) => n.parentId === parentId && n.data.key === key);
|
|
1244
|
+
if (!child) return null;
|
|
1245
|
+
nodeId = child.id;
|
|
1246
|
+
parentId = child.id;
|
|
1247
|
+
}
|
|
1248
|
+
return nodeId;
|
|
1249
|
+
}
|
|
1250
|
+
function getStateIds(graph, snapshot) {
|
|
1251
|
+
const nodes = snapshot._nodes;
|
|
1252
|
+
if (!nodes) return [];
|
|
1253
|
+
return nodes.map((n) => getGraphNodeIdForPath(graph, n.path)).filter((id) => id !== null);
|
|
1254
|
+
}
|
|
1255
|
+
function getLeafStateIds(graph, snapshot) {
|
|
1256
|
+
const nodes = snapshot._nodes;
|
|
1257
|
+
if (!nodes) return [];
|
|
1258
|
+
const leaves = nodes.filter((n) => n.path.length > 0 && (n.type === "atomic" || n.type === "final"));
|
|
1259
|
+
return (leaves.length > 0 ? leaves : nodes).map((n) => getGraphNodeIdForPath(graph, n.path)).filter((id) => id !== null);
|
|
1260
|
+
}
|
|
1261
|
+
function getStatePath(snapshot) {
|
|
1262
|
+
const nodes = snapshot._nodes;
|
|
1263
|
+
if (!nodes?.length) return "(initial)";
|
|
1264
|
+
const withPath = nodes.filter((n) => n.path.length > 0);
|
|
1265
|
+
if (!withPath.length) return "(initial)";
|
|
1266
|
+
const leaves = withPath.filter((n) => n.type === "atomic" || n.type === "final");
|
|
1267
|
+
return (leaves.length > 0 ? leaves : withPath).map((n) => n.path.join(".")).join(", ");
|
|
1268
|
+
}
|
|
1269
|
+
function findEdgeId(graph, sourceStateIds, event) {
|
|
1270
|
+
const guardType = event["@xstate.guard"];
|
|
1271
|
+
for (const sourceId of sourceStateIds) {
|
|
1272
|
+
const edge = graph.edges.find((candidate) => candidate.sourceId === sourceId && transitionMatchesRuntimeEvent(graph, candidate, event.type) && (candidate.data.guard?.type ?? null) === (guardType ?? null));
|
|
1273
|
+
if (edge) return edge.id;
|
|
1274
|
+
}
|
|
1275
|
+
for (const sourceId of sourceStateIds) {
|
|
1276
|
+
const edge = graph.edges.find((candidate) => candidate.sourceId === sourceId && transitionMatchesRuntimeEvent(graph, candidate, event.type));
|
|
1277
|
+
if (edge) return edge.id;
|
|
1278
|
+
}
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
function formatEventLabel(event) {
|
|
1282
|
+
if (event.type === "") return "always";
|
|
1283
|
+
if (event.type.startsWith("xstate.after.")) return `after ${event.type.slice(13).split(".")[0]}`;
|
|
1284
|
+
const guardType = event["@xstate.guard"];
|
|
1285
|
+
if (guardType) return `${event.type} [${guardType}]`;
|
|
1286
|
+
return event.type;
|
|
1287
|
+
}
|
|
1288
|
+
function formatEdgeLabel(graph, edgeId) {
|
|
1289
|
+
const edge = graph.edges.find((candidate) => candidate.id === edgeId);
|
|
1290
|
+
if (!edge) return null;
|
|
1291
|
+
const baseLabel = formatEventLabel({ type: edge.data.eventType });
|
|
1292
|
+
const guardType = edge.data.guard?.type;
|
|
1293
|
+
if (guardType) return `${baseLabel} [${guardType}]`;
|
|
1294
|
+
return baseLabel;
|
|
1295
|
+
}
|
|
1296
|
+
function getFallbackTransitionLabels(graph, finalStateIds) {
|
|
1297
|
+
return getFallbackEventlessEdges(graph, finalStateIds).map((edge) => formatEdgeLabel(graph, edge.id)).filter((label) => !!label);
|
|
1298
|
+
}
|
|
1299
|
+
function getFallbackEventlessEdges(graph, finalStateIds) {
|
|
1300
|
+
const finalLeafIdSet = new Set(finalStateIds);
|
|
1301
|
+
return graph.edges.filter((edge) => finalLeafIdSet.has(edge.targetId) && edge.data.eventType === "");
|
|
1302
|
+
}
|
|
1303
|
+
function getPathName(finalStatePath, finalStateIds, steps, graph) {
|
|
1304
|
+
let transitionLabels = steps.map((step) => {
|
|
1305
|
+
if (step.edgeId) return formatEdgeLabel(graph, step.edgeId);
|
|
1306
|
+
if (step.eventType && step.eventType !== "xstate.init") return formatEventLabel({ type: step.eventType });
|
|
1307
|
+
return null;
|
|
1308
|
+
}).filter((label) => !!label);
|
|
1309
|
+
if (transitionLabels.length === 0) transitionLabels = getFallbackTransitionLabels(graph, finalStateIds);
|
|
1310
|
+
if (transitionLabels.length === 0) return {
|
|
1311
|
+
name: finalStatePath,
|
|
1312
|
+
viaLabel: null
|
|
1313
|
+
};
|
|
1314
|
+
return {
|
|
1315
|
+
name: finalStatePath,
|
|
1316
|
+
viaLabel: `via ${transitionLabels.join(" -> ")}`
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function convertPath(path, graph, index) {
|
|
1320
|
+
const steps = [];
|
|
1321
|
+
const firstStep = path.steps[0];
|
|
1322
|
+
if (firstStep) {
|
|
1323
|
+
const allIds = getStateIds(graph, firstStep.state);
|
|
1324
|
+
steps.push({
|
|
1325
|
+
stateIds: getLeafStateIds(graph, firstStep.state),
|
|
1326
|
+
statePath: getStatePath(firstStep.state),
|
|
1327
|
+
eventType: firstStep.event.type,
|
|
1328
|
+
edgeId: findEdgeId(graph, allIds, firstStep.event)
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
for (let i = 1; i < path.steps.length; i++) {
|
|
1332
|
+
const prevStep = path.steps[i - 1];
|
|
1333
|
+
const step = path.steps[i];
|
|
1334
|
+
const prevStateIds = getStateIds(graph, prevStep.state);
|
|
1335
|
+
steps.push({
|
|
1336
|
+
stateIds: getLeafStateIds(graph, step.state),
|
|
1337
|
+
statePath: getStatePath(step.state),
|
|
1338
|
+
eventType: step.event.type,
|
|
1339
|
+
edgeId: findEdgeId(graph, prevStateIds, step.event)
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
const finalStatePath = getStatePath(path.state);
|
|
1343
|
+
const finalStateIds = getLeafStateIds(graph, path.state);
|
|
1344
|
+
const title = getPathName(finalStatePath, finalStateIds, steps, graph);
|
|
1345
|
+
return {
|
|
1346
|
+
id: `path-${index}`,
|
|
1347
|
+
name: title.name,
|
|
1348
|
+
viaLabel: title.viaLabel,
|
|
1349
|
+
steps,
|
|
1350
|
+
finalStatePath,
|
|
1351
|
+
finalStateIds
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function serializeMachineTransition(_state, event, prevState) {
|
|
1355
|
+
if (!event) return "";
|
|
1356
|
+
const prevStateString = prevState ? ` from ${JSON.stringify(prevState.value)}` : "";
|
|
1357
|
+
return ` via ${JSON.stringify(event)}${prevStateString}`;
|
|
1358
|
+
}
|
|
1359
|
+
function getGraphPathSimImplementations(graph, guardState) {
|
|
1360
|
+
const guards = {};
|
|
1361
|
+
for (const edge of graph.edges) if (edge.data.guard) {
|
|
1362
|
+
const guardType = edge.data.guard.type;
|
|
1363
|
+
if (guardState) guards[guardType] = () => guardState[guardType] ?? false;
|
|
1364
|
+
else guards[guardType] = ({ event }) => {
|
|
1365
|
+
if (event["@xstate.guard"] === guardType) return true;
|
|
1366
|
+
return false;
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
return { guards };
|
|
1370
|
+
}
|
|
1371
|
+
function getTraversalEvents(state, graph) {
|
|
1372
|
+
const events = [];
|
|
1373
|
+
for (const edge of graph.edges) {
|
|
1374
|
+
const eventType = getTransitionRuntimeEventType(graph, edge);
|
|
1375
|
+
const event = { type: eventType };
|
|
1376
|
+
if (edge.data.guard?.type) event["@xstate.guard"] = edge.data.guard.type;
|
|
1377
|
+
if (eventType && !state.can(event)) continue;
|
|
1378
|
+
events.push(event);
|
|
1379
|
+
}
|
|
1380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1381
|
+
return events.filter((event) => {
|
|
1382
|
+
const key = JSON.stringify(event);
|
|
1383
|
+
if (seen.has(key)) return false;
|
|
1384
|
+
seen.add(key);
|
|
1385
|
+
return true;
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
function getStepKey(step) {
|
|
1389
|
+
return JSON.stringify({
|
|
1390
|
+
stateIds: step.stateIds,
|
|
1391
|
+
statePath: step.statePath,
|
|
1392
|
+
eventType: step.eventType,
|
|
1393
|
+
edgeId: step.edgeId
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
function getLeafGraphNodeIds(graph) {
|
|
1397
|
+
const activeNodes = graph.nodes.filter((node) => !node.data.temp);
|
|
1398
|
+
const parentIds = new Set(activeNodes.map((node) => node.parentId).filter(Boolean));
|
|
1399
|
+
return activeNodes.filter((node) => !parentIds.has(node.id)).map((node) => node.id);
|
|
1400
|
+
}
|
|
1401
|
+
function getCoverage(graph, paths) {
|
|
1402
|
+
const leafNodeIds = new Set(getLeafGraphNodeIds(graph));
|
|
1403
|
+
const coveredStateIds = /* @__PURE__ */ new Set();
|
|
1404
|
+
const coveredEdgeIds = /* @__PURE__ */ new Set();
|
|
1405
|
+
for (const path of paths) {
|
|
1406
|
+
let pathHasExplicitEdge = false;
|
|
1407
|
+
for (const step of path.steps) {
|
|
1408
|
+
for (const stateId of step.stateIds) if (leafNodeIds.has(stateId)) coveredStateIds.add(stateId);
|
|
1409
|
+
if (step.edgeId) {
|
|
1410
|
+
pathHasExplicitEdge = true;
|
|
1411
|
+
coveredEdgeIds.add(step.edgeId);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
for (const finalStateId of path.finalStateIds) if (leafNodeIds.has(finalStateId)) coveredStateIds.add(finalStateId);
|
|
1415
|
+
if (!pathHasExplicitEdge) for (const edge of getFallbackEventlessEdges(graph, path.finalStateIds)) coveredEdgeIds.add(edge.id);
|
|
1416
|
+
}
|
|
1417
|
+
return {
|
|
1418
|
+
states: {
|
|
1419
|
+
covered: coveredStateIds.size,
|
|
1420
|
+
total: leafNodeIds.size
|
|
1421
|
+
},
|
|
1422
|
+
transitions: {
|
|
1423
|
+
covered: coveredEdgeIds.size,
|
|
1424
|
+
total: graph.edges.filter((edge) => !edge.data.temp).length
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function deduplicateGeneratedPaths(paths) {
|
|
1429
|
+
const sorted = [...paths].sort((a, b) => b.steps.length - a.steps.length);
|
|
1430
|
+
const unique = [];
|
|
1431
|
+
for (const path of sorted) if (!unique.some((existing) => {
|
|
1432
|
+
if (path.steps.length >= existing.steps.length) return false;
|
|
1433
|
+
return path.steps.every((step, index) => getStepKey(step) === getStepKey(existing.steps[index]));
|
|
1434
|
+
})) unique.push(path);
|
|
1435
|
+
return unique;
|
|
1436
|
+
}
|
|
1437
|
+
function generateGraphPathsData(graph, options) {
|
|
1438
|
+
const machineConfig = graphToMachineConfig(graph);
|
|
1439
|
+
const implementations = getGraphPathSimImplementations(graph);
|
|
1440
|
+
const machine = createMachine(machineConfig).provide(implementations);
|
|
1441
|
+
let converted = (options.strategy === "shortest" ? getShortestPaths : getSimplePaths)(machine, {
|
|
1442
|
+
events: (state) => getTraversalEvents(state, graph),
|
|
1443
|
+
serializeState: (state, event, prevState) => {
|
|
1444
|
+
const serializedState = JSON.stringify(state.value);
|
|
1445
|
+
if (!options.preferTransitionCoverage) return serializedState;
|
|
1446
|
+
return `${serializedState}|${serializeMachineTransition(state, event, prevState)}`;
|
|
1447
|
+
}
|
|
1448
|
+
}).map((path, index) => convertPath(path, graph, index));
|
|
1449
|
+
if (options.reduceDuplicates) converted = deduplicateGeneratedPaths(converted);
|
|
1450
|
+
if (options.limit) converted = converted.slice(0, options.limit);
|
|
1451
|
+
return {
|
|
1452
|
+
paths: converted,
|
|
1453
|
+
coverage: getCoverage(graph, converted)
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function toArray(value) {
|
|
1457
|
+
return Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
1458
|
+
}
|
|
1459
|
+
function actionItems(value) {
|
|
1460
|
+
return toArray(value).map((item, index) => {
|
|
1461
|
+
if (typeof item === "string") return {
|
|
1462
|
+
id: item,
|
|
1463
|
+
type: item
|
|
1464
|
+
};
|
|
1465
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
1466
|
+
const action = item;
|
|
1467
|
+
if (typeof action.type === "string") return {
|
|
1468
|
+
id: action.type || `action-${index}`,
|
|
1469
|
+
type: action.type,
|
|
1470
|
+
params: action.params
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
return null;
|
|
1474
|
+
}).filter((item) => item !== null);
|
|
1475
|
+
}
|
|
1476
|
+
function invokeItems(value) {
|
|
1477
|
+
return toArray(value).map((item, index) => {
|
|
1478
|
+
if (typeof item === "string") return {
|
|
1479
|
+
id: item || `invoke-${index}`,
|
|
1480
|
+
src: item
|
|
1481
|
+
};
|
|
1482
|
+
if (!item || typeof item !== "object") return null;
|
|
1483
|
+
const invoke = item;
|
|
1484
|
+
if (typeof invoke.src !== "string") return null;
|
|
1485
|
+
return {
|
|
1486
|
+
id: typeof invoke.id === "string" ? invoke.id : invoke.src,
|
|
1487
|
+
src: invoke.src,
|
|
1488
|
+
...invoke.input !== void 0 ? { input: invoke.input } : {},
|
|
1489
|
+
...invoke.output !== void 0 ? { output: invoke.output } : {}
|
|
1490
|
+
};
|
|
1491
|
+
}).filter((item) => item !== null);
|
|
1492
|
+
}
|
|
1493
|
+
function transitionTargets(target) {
|
|
1494
|
+
if (typeof target === "string") return [target];
|
|
1495
|
+
if (Array.isArray(target)) return target.filter((item) => typeof item === "string");
|
|
1496
|
+
return [];
|
|
1497
|
+
}
|
|
1498
|
+
function targetToNodeId(sourceId, target, rootKey) {
|
|
1499
|
+
if (!target) return sourceId;
|
|
1500
|
+
let clean = target.startsWith("#") ? target.slice(1) : target;
|
|
1501
|
+
if (target.startsWith("#")) {
|
|
1502
|
+
const rootPrefix = `${rootKey}.`;
|
|
1503
|
+
if (clean === rootKey) return "root";
|
|
1504
|
+
if (clean.startsWith(rootPrefix)) clean = clean.slice(rootPrefix.length);
|
|
1505
|
+
return clean ? `root.${clean}` : "root";
|
|
1506
|
+
}
|
|
1507
|
+
if (clean.includes(".")) return `root.${clean}`;
|
|
1508
|
+
return `${sourceId.split(".").slice(0, -1).join(".") || "root"}.${clean}`;
|
|
1509
|
+
}
|
|
1510
|
+
function transitionObjects(value) {
|
|
1511
|
+
return toArray(value).map((item) => {
|
|
1512
|
+
if (typeof item === "string") return { target: item };
|
|
1513
|
+
if (item && typeof item === "object") return item;
|
|
1514
|
+
return {};
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
function guardObject(value) {
|
|
1518
|
+
if (!value) return null;
|
|
1519
|
+
if (typeof value === "string") return { type: value };
|
|
1520
|
+
if (typeof value === "object" && "type" in value && typeof value.type === "string") {
|
|
1521
|
+
const guard = value;
|
|
1522
|
+
return {
|
|
1523
|
+
type: guard.type,
|
|
1524
|
+
params: guard.params
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
function machineConfigToGraph(config) {
|
|
1530
|
+
createMachine(config);
|
|
1531
|
+
const rootKey = String(config.id ?? "machine");
|
|
1532
|
+
const graph = {
|
|
1533
|
+
id: "root",
|
|
1534
|
+
nodes: [],
|
|
1535
|
+
edges: [],
|
|
1536
|
+
data: {
|
|
1537
|
+
implementations: {
|
|
1538
|
+
actions: [],
|
|
1539
|
+
guards: [],
|
|
1540
|
+
actors: [],
|
|
1541
|
+
delays: []
|
|
1542
|
+
},
|
|
1543
|
+
schemas: config.schemas ?? null
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
function visit(state, key, parentId) {
|
|
1547
|
+
const id = parentId ? `${parentId}.${key}` : "root";
|
|
1548
|
+
const initialId = state.initial ? `${id}.${state.initial}` : null;
|
|
1549
|
+
graph.nodes.push({
|
|
1550
|
+
id,
|
|
1551
|
+
parentId,
|
|
1552
|
+
data: {
|
|
1553
|
+
key,
|
|
1554
|
+
type: state.type === "parallel" || state.type === "final" || state.type === "history" ? state.type : null,
|
|
1555
|
+
initialId,
|
|
1556
|
+
history: state.history ?? false,
|
|
1557
|
+
entry: actionItems(state.entry),
|
|
1558
|
+
exit: actionItems(state.exit),
|
|
1559
|
+
invokes: invokeItems(state.invoke),
|
|
1560
|
+
tags: Array.isArray(state.tags) ? state.tags : typeof state.tags === "string" ? [state.tags] : [],
|
|
1561
|
+
description: state.description ?? null,
|
|
1562
|
+
meta: state.meta ?? null
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
for (const [eventType, value] of Object.entries(state.on ?? {})) for (const [index, transition] of transitionObjects(value).entries()) {
|
|
1566
|
+
const target = transitionTargets(transition.target)[0];
|
|
1567
|
+
graph.edges.push({
|
|
1568
|
+
id: `${id}-${eventType}-${index}`,
|
|
1569
|
+
sourceId: id,
|
|
1570
|
+
targetId: targetToNodeId(id, target, rootKey),
|
|
1571
|
+
data: {
|
|
1572
|
+
eventType,
|
|
1573
|
+
transitionType: target ? transition.reenter ? "reenter" : "normal" : "targetless",
|
|
1574
|
+
guard: guardObject(transition.guard),
|
|
1575
|
+
actions: actionItems(transition.actions),
|
|
1576
|
+
description: typeof transition.description === "string" ? transition.description : null,
|
|
1577
|
+
meta: transition.meta && typeof transition.meta === "object" ? transition.meta : null
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
for (const [delay, value] of Object.entries(state.after ?? {})) for (const [index, transition] of transitionObjects(value).entries()) {
|
|
1582
|
+
const target = transitionTargets(transition.target)[0];
|
|
1583
|
+
graph.edges.push({
|
|
1584
|
+
id: `${id}-after-${delay}-${index}`,
|
|
1585
|
+
sourceId: id,
|
|
1586
|
+
targetId: targetToNodeId(id, target, rootKey),
|
|
1587
|
+
data: {
|
|
1588
|
+
eventType: `xstate.after.${delay}.${id}`,
|
|
1589
|
+
transitionType: target ? "normal" : "targetless",
|
|
1590
|
+
guard: guardObject(transition.guard),
|
|
1591
|
+
actions: actionItems(transition.actions),
|
|
1592
|
+
description: typeof transition.description === "string" ? transition.description : null,
|
|
1593
|
+
meta: null
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
for (const [index, transition] of transitionObjects(state.always).entries()) {
|
|
1598
|
+
const target = transitionTargets(transition.target)[0];
|
|
1599
|
+
graph.edges.push({
|
|
1600
|
+
id: `${id}-always-${index}`,
|
|
1601
|
+
sourceId: id,
|
|
1602
|
+
targetId: targetToNodeId(id, target, rootKey),
|
|
1603
|
+
data: {
|
|
1604
|
+
eventType: "",
|
|
1605
|
+
transitionType: target ? "normal" : "targetless",
|
|
1606
|
+
guard: guardObject(transition.guard),
|
|
1607
|
+
actions: actionItems(transition.actions),
|
|
1608
|
+
description: typeof transition.description === "string" ? transition.description : null,
|
|
1609
|
+
meta: null
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
for (const [childKey, child] of Object.entries(state.states ?? {})) visit(child, childKey, id);
|
|
1614
|
+
}
|
|
1615
|
+
visit(config, rootKey, null);
|
|
1616
|
+
return graph;
|
|
1617
|
+
}
|
|
1618
|
+
const XSTATE_V6_FN_TRANSITION_META = "@statelyai.xstate.v6.fnTransition";
|
|
1619
|
+
const XSTATE_V6_FN_SOURCE_META = "@statelyai.xstate.v6.fnSource";
|
|
1620
|
+
function sourceFile(source) {
|
|
1621
|
+
return ts.createSourceFile("machine.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
1622
|
+
}
|
|
1623
|
+
function propName(name) {
|
|
1624
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
function unwrapExpression$2(node) {
|
|
1628
|
+
if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node)) return unwrapExpression$2(node.expression);
|
|
1629
|
+
return node;
|
|
1630
|
+
}
|
|
1631
|
+
function propertyContext(parent, key) {
|
|
1632
|
+
if (parent.transitionMap) return {
|
|
1633
|
+
v6: parent.v6,
|
|
1634
|
+
transitionValue: true
|
|
1635
|
+
};
|
|
1636
|
+
if (parent.transitionValue && key === "actions") return {
|
|
1637
|
+
v6: parent.v6,
|
|
1638
|
+
actionValue: true
|
|
1639
|
+
};
|
|
1640
|
+
if (key === "meta") return {
|
|
1641
|
+
v6: parent.v6,
|
|
1642
|
+
metaValue: true
|
|
1643
|
+
};
|
|
1644
|
+
if (parent.transitionValue && key === "to") return {
|
|
1645
|
+
v6: parent.v6,
|
|
1646
|
+
transitionValue: true
|
|
1647
|
+
};
|
|
1648
|
+
if (key === "entry" || key === "exit") return {
|
|
1649
|
+
v6: parent.v6,
|
|
1650
|
+
actionValue: true
|
|
1651
|
+
};
|
|
1652
|
+
if (key === "on" || key === "after") return {
|
|
1653
|
+
v6: parent.v6,
|
|
1654
|
+
transitionMap: true
|
|
1655
|
+
};
|
|
1656
|
+
if (key === "always" || key === "onDone" || key === "onError" || key === "onSnapshot" || key === "onTimeout") return {
|
|
1657
|
+
v6: parent.v6,
|
|
1658
|
+
transitionValue: true
|
|
1659
|
+
};
|
|
1660
|
+
return { v6: parent.v6 };
|
|
1661
|
+
}
|
|
1662
|
+
function fallbackFunctionTransition(fn) {
|
|
1663
|
+
return { meta: {
|
|
1664
|
+
[XSTATE_V6_FN_TRANSITION_META]: true,
|
|
1665
|
+
[XSTATE_V6_FN_SOURCE_META]: fn.getText()
|
|
1666
|
+
} };
|
|
1667
|
+
}
|
|
1668
|
+
function conditionText(cond) {
|
|
1669
|
+
return cond.negated ? `!(${cond.text})` : cond.text;
|
|
1670
|
+
}
|
|
1671
|
+
function simplifyConditions(conds) {
|
|
1672
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1673
|
+
return conds.filter((cond) => {
|
|
1674
|
+
if (cond.negated) return false;
|
|
1675
|
+
const key = conditionText(cond);
|
|
1676
|
+
if (seen.has(key)) return false;
|
|
1677
|
+
seen.add(key);
|
|
1678
|
+
return true;
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
function guardFromConditions(conds) {
|
|
1682
|
+
const simplified = simplifyConditions(conds);
|
|
1683
|
+
if (simplified.length === 0) return void 0;
|
|
1684
|
+
return { type: simplified.map(conditionText).join(" && ") };
|
|
1685
|
+
}
|
|
1686
|
+
function mergeActions(left, right) {
|
|
1687
|
+
return [...left ?? [], ...right];
|
|
1688
|
+
}
|
|
1689
|
+
function transitionFromValue(value, actions) {
|
|
1690
|
+
if (typeof value === "string") {
|
|
1691
|
+
if (actions.length === 0) return value;
|
|
1692
|
+
return {
|
|
1693
|
+
target: value,
|
|
1694
|
+
actions
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return actions.length ? { actions } : null;
|
|
1698
|
+
const record = { ...value };
|
|
1699
|
+
const context = record.context;
|
|
1700
|
+
delete record.context;
|
|
1701
|
+
if (context !== void 0) record.actions = mergeActions(Array.isArray(record.actions) ? record.actions : record.actions ? [record.actions] : void 0, [{
|
|
1702
|
+
type: "xstate.assign",
|
|
1703
|
+
params: { assignment: context }
|
|
1704
|
+
}]);
|
|
1705
|
+
if (actions.length) record.actions = mergeActions(Array.isArray(record.actions) ? record.actions : record.actions ? [record.actions] : void 0, actions);
|
|
1706
|
+
return record;
|
|
1707
|
+
}
|
|
1708
|
+
function expressionBranches(expression, bindings, depth) {
|
|
1709
|
+
const node = unwrapExpression$2(expression);
|
|
1710
|
+
if (ts.isConditionalExpression(node)) {
|
|
1711
|
+
const cond = node.condition.getText();
|
|
1712
|
+
return [...expressionBranches(node.whenTrue, bindings, depth + 1).map((branch) => ({
|
|
1713
|
+
...branch,
|
|
1714
|
+
conds: [{ text: cond }, ...branch.conds]
|
|
1715
|
+
})), ...expressionBranches(node.whenFalse, bindings, depth + 1).map((branch) => ({
|
|
1716
|
+
...branch,
|
|
1717
|
+
conds: [{
|
|
1718
|
+
text: cond,
|
|
1719
|
+
negated: true
|
|
1720
|
+
}, ...branch.conds]
|
|
1721
|
+
}))];
|
|
1722
|
+
}
|
|
1723
|
+
if (ts.isBinaryExpression(node) && (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
|
|
1724
|
+
const cond = node.left.getText();
|
|
1725
|
+
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) return [...expressionBranches(node.right, bindings, depth + 1).map((branch) => ({
|
|
1726
|
+
...branch,
|
|
1727
|
+
conds: [{ text: cond }, ...branch.conds]
|
|
1728
|
+
})), {
|
|
1729
|
+
conds: [{
|
|
1730
|
+
text: cond,
|
|
1731
|
+
negated: true
|
|
1732
|
+
}],
|
|
1733
|
+
actions: []
|
|
1734
|
+
}];
|
|
1735
|
+
return [{
|
|
1736
|
+
conds: [{ text: cond }],
|
|
1737
|
+
actions: [],
|
|
1738
|
+
result: node.left.getText()
|
|
1739
|
+
}, ...expressionBranches(node.right, bindings, depth + 1).map((branch) => ({
|
|
1740
|
+
...branch,
|
|
1741
|
+
conds: [{
|
|
1742
|
+
text: cond,
|
|
1743
|
+
negated: true
|
|
1744
|
+
}, ...branch.conds]
|
|
1745
|
+
}))];
|
|
1746
|
+
}
|
|
1747
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1748
|
+
const targetProperty = node.properties.find((property) => ts.isPropertyAssignment(property) && propName(property.name) === "target");
|
|
1749
|
+
const target = targetProperty ? unwrapExpression$2(targetProperty.initializer) : null;
|
|
1750
|
+
if (target && (ts.isConditionalExpression(target) || ts.isBinaryExpression(target) && (target.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || target.operatorToken.kind === ts.SyntaxKind.BarBarToken))) return expressionBranches(target, bindings, depth + 1).map((branch) => ({
|
|
1751
|
+
...branch,
|
|
1752
|
+
result: objectLiteralToValue(node, bindings, {}, depth + 1, new Map([["target", branch.result]]))
|
|
1753
|
+
}));
|
|
1754
|
+
}
|
|
1755
|
+
return [{
|
|
1756
|
+
conds: [],
|
|
1757
|
+
actions: [],
|
|
1758
|
+
result: expressionToValue(node, bindings, {}, depth + 1)
|
|
1759
|
+
}];
|
|
1760
|
+
}
|
|
1761
|
+
function mergeBranchPrefix(prefix, branch) {
|
|
1762
|
+
return {
|
|
1763
|
+
conds: [...prefix.conds, ...branch.conds],
|
|
1764
|
+
actions: [...prefix.actions, ...branch.actions],
|
|
1765
|
+
result: branch.result,
|
|
1766
|
+
unknown: prefix.unknown || branch.unknown
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
function expressionStatementAction(statement, enqName, bindings) {
|
|
1770
|
+
const expression = unwrapExpression$2(statement.expression);
|
|
1771
|
+
if (!ts.isCallExpression(expression)) return null;
|
|
1772
|
+
const callee = expression.expression;
|
|
1773
|
+
if (ts.isIdentifier(callee) && callee.text === enqName) return {
|
|
1774
|
+
type: "xstate.expr",
|
|
1775
|
+
params: {
|
|
1776
|
+
code: expression.getText(),
|
|
1777
|
+
lang: "ts"
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
if (!ts.isPropertyAccessExpression(callee)) return null;
|
|
1781
|
+
if (!ts.isIdentifier(callee.expression) || callee.expression.text !== enqName) return null;
|
|
1782
|
+
const method = callee.name.text;
|
|
1783
|
+
const args = expression.arguments;
|
|
1784
|
+
const value = (index) => {
|
|
1785
|
+
const arg = args[index];
|
|
1786
|
+
return arg && ts.isExpression(arg) ? expressionToValue(arg, bindings, {}, 0) : void 0;
|
|
1787
|
+
};
|
|
1788
|
+
const spreadOpts = (index) => {
|
|
1789
|
+
const opts = value(index);
|
|
1790
|
+
return opts && typeof opts === "object" && !Array.isArray(opts) ? opts : {};
|
|
1791
|
+
};
|
|
1792
|
+
if (method === "emit") return {
|
|
1793
|
+
type: "xstate.emit",
|
|
1794
|
+
params: { event: value(0) }
|
|
1795
|
+
};
|
|
1796
|
+
if (method === "raise") return {
|
|
1797
|
+
type: "xstate.raise",
|
|
1798
|
+
params: {
|
|
1799
|
+
event: value(0),
|
|
1800
|
+
...spreadOpts(1)
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
if (method === "sendTo") return {
|
|
1804
|
+
type: "xstate.sendTo",
|
|
1805
|
+
params: {
|
|
1806
|
+
to: value(0),
|
|
1807
|
+
event: value(1),
|
|
1808
|
+
...spreadOpts(2)
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
if (method === "cancel") return {
|
|
1812
|
+
type: "xstate.cancel",
|
|
1813
|
+
params: { sendId: value(0) }
|
|
1814
|
+
};
|
|
1815
|
+
if (method === "log") return {
|
|
1816
|
+
type: "xstate.log",
|
|
1817
|
+
params: { label: value(0) }
|
|
1818
|
+
};
|
|
1819
|
+
if (method === "spawn") return {
|
|
1820
|
+
type: "xstate.spawnChild",
|
|
1821
|
+
params: {
|
|
1822
|
+
src: value(0),
|
|
1823
|
+
...spreadOpts(1)
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
if (method === "stop") return {
|
|
1827
|
+
type: "xstate.stopChild",
|
|
1828
|
+
params: { actorRef: value(0) }
|
|
1829
|
+
};
|
|
1830
|
+
return {
|
|
1831
|
+
type: "xstate.expr",
|
|
1832
|
+
params: {
|
|
1833
|
+
code: expression.getText(),
|
|
1834
|
+
lang: "ts"
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function statementBranches(statements, bindings, enqName, prefixes = [{
|
|
1839
|
+
conds: [],
|
|
1840
|
+
actions: []
|
|
1841
|
+
}]) {
|
|
1842
|
+
let open = prefixes;
|
|
1843
|
+
const done = [];
|
|
1844
|
+
for (const statement of statements) {
|
|
1845
|
+
const nextOpen = [];
|
|
1846
|
+
for (const prefix of open) {
|
|
1847
|
+
if (ts.isReturnStatement(statement)) {
|
|
1848
|
+
if (!statement.expression) {
|
|
1849
|
+
done.push({
|
|
1850
|
+
...prefix,
|
|
1851
|
+
result: void 0
|
|
1852
|
+
});
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
for (const branch of expressionBranches(statement.expression, bindings, 0)) done.push(mergeBranchPrefix(prefix, branch));
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
if (ts.isExpressionStatement(statement)) {
|
|
1859
|
+
const action = expressionStatementAction(statement, enqName, bindings);
|
|
1860
|
+
if (action) {
|
|
1861
|
+
nextOpen.push({
|
|
1862
|
+
...prefix,
|
|
1863
|
+
actions: [...prefix.actions, action]
|
|
1864
|
+
});
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if (ts.isIfStatement(statement)) {
|
|
1869
|
+
const cond = statement.expression.getText();
|
|
1870
|
+
const thenBranches = statementBranches(ts.isBlock(statement.thenStatement) ? statement.thenStatement.statements : [statement.thenStatement], bindings, enqName, [{
|
|
1871
|
+
...prefix,
|
|
1872
|
+
conds: [...prefix.conds, { text: cond }]
|
|
1873
|
+
}]);
|
|
1874
|
+
const elseStatements = statement.elseStatement ? ts.isBlock(statement.elseStatement) ? statement.elseStatement.statements : [statement.elseStatement] : [];
|
|
1875
|
+
const elseBranches = elseStatements.length > 0 ? statementBranches(elseStatements, bindings, enqName, [{
|
|
1876
|
+
...prefix,
|
|
1877
|
+
conds: [...prefix.conds, {
|
|
1878
|
+
text: cond,
|
|
1879
|
+
negated: true
|
|
1880
|
+
}]
|
|
1881
|
+
}]) : [{
|
|
1882
|
+
...prefix,
|
|
1883
|
+
conds: [...prefix.conds, {
|
|
1884
|
+
text: cond,
|
|
1885
|
+
negated: true
|
|
1886
|
+
}]
|
|
1887
|
+
}];
|
|
1888
|
+
for (const branch of [...thenBranches, ...elseBranches]) if ("result" in branch) done.push(branch);
|
|
1889
|
+
else nextOpen.push(branch);
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (ts.isSwitchStatement(statement)) {
|
|
1893
|
+
const expr = statement.expression.getText();
|
|
1894
|
+
const prior = [];
|
|
1895
|
+
for (const clause of statement.caseBlock.clauses) {
|
|
1896
|
+
const ownCond = ts.isCaseClause(clause) ? { text: `${expr} === ${clause.expression.getText()}` } : null;
|
|
1897
|
+
const conds = ownCond ? [...prior, ownCond] : prior.length ? [...prior] : [];
|
|
1898
|
+
const branches = statementBranches(clause.statements, bindings, enqName, [{
|
|
1899
|
+
...prefix,
|
|
1900
|
+
conds: [...prefix.conds, ...conds]
|
|
1901
|
+
}]);
|
|
1902
|
+
for (const branch of branches) if ("result" in branch) done.push(branch);
|
|
1903
|
+
else nextOpen.push(branch);
|
|
1904
|
+
if (ownCond) prior.push({
|
|
1905
|
+
...ownCond,
|
|
1906
|
+
negated: true
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
nextOpen.push({
|
|
1912
|
+
...prefix,
|
|
1913
|
+
unknown: true
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
open = nextOpen;
|
|
1917
|
+
}
|
|
1918
|
+
return [...done, ...open];
|
|
1919
|
+
}
|
|
1920
|
+
function enqueueParamName(fn) {
|
|
1921
|
+
const param = fn.parameters[1];
|
|
1922
|
+
return param && ts.isIdentifier(param.name) ? param.name.text : "enq";
|
|
1923
|
+
}
|
|
1924
|
+
function normalizeFunctionTransition(fn, bindings, depth) {
|
|
1925
|
+
const transitions = (ts.isArrowFunction(fn) && !ts.isBlock(fn.body) ? expressionBranches(fn.body, bindings, depth + 1) : fn.body && ts.isBlock(fn.body) ? statementBranches(fn.body.statements, bindings, enqueueParamName(fn)) : []).filter((branch) => !branch.unknown).map((branch) => {
|
|
1926
|
+
const transition = transitionFromValue(branch.result, branch.actions);
|
|
1927
|
+
if (!transition) return null;
|
|
1928
|
+
if (typeof transition === "string") {
|
|
1929
|
+
const guard = guardFromConditions(branch.conds);
|
|
1930
|
+
return guard ? {
|
|
1931
|
+
target: transition,
|
|
1932
|
+
guard
|
|
1933
|
+
} : transition;
|
|
1934
|
+
}
|
|
1935
|
+
const guard = guardFromConditions(branch.conds);
|
|
1936
|
+
return guard ? {
|
|
1937
|
+
...transition,
|
|
1938
|
+
guard
|
|
1939
|
+
} : transition;
|
|
1940
|
+
}).filter((transition) => Boolean(transition));
|
|
1941
|
+
if (transitions.length === 1) return transitions[0];
|
|
1942
|
+
if (transitions.length > 1) return transitions;
|
|
1943
|
+
return fallbackFunctionTransition(fn);
|
|
1944
|
+
}
|
|
1945
|
+
function inlineAction(fn) {
|
|
1946
|
+
return {
|
|
1947
|
+
type: "xstate.expr",
|
|
1948
|
+
params: {
|
|
1949
|
+
code: fn.getText(),
|
|
1950
|
+
lang: "ts"
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function inlineCodeExpression(expr) {
|
|
1955
|
+
return {
|
|
1956
|
+
"@type": "code",
|
|
1957
|
+
lang: "ts",
|
|
1958
|
+
expr
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function normalizeFunctionAction(fn, bindings) {
|
|
1962
|
+
if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) return inlineAction(fn);
|
|
1963
|
+
if (!fn.body || !ts.isBlock(fn.body)) return inlineAction(fn);
|
|
1964
|
+
const branches = statementBranches(fn.body.statements, bindings, enqueueParamName(fn));
|
|
1965
|
+
if (branches.length === 1 && !branches[0].unknown && branches[0].conds.length === 0 && branches[0].actions.length > 0 && !("result" in branches[0])) return branches[0].actions;
|
|
1966
|
+
return inlineAction(fn);
|
|
1967
|
+
}
|
|
1968
|
+
function objectLiteralToValue(node, bindings, context, depth, overrides = /* @__PURE__ */ new Map()) {
|
|
1969
|
+
const result = {};
|
|
1970
|
+
for (const property of node.properties) {
|
|
1971
|
+
if (ts.isSpreadAssignment(property)) {
|
|
1972
|
+
const spread = expressionToValue(property.expression, bindings, context, depth + 1);
|
|
1973
|
+
if (spread && typeof spread === "object" && !Array.isArray(spread)) Object.assign(result, spread);
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) continue;
|
|
1977
|
+
const key = propName(property.name);
|
|
1978
|
+
if (!key) continue;
|
|
1979
|
+
if (overrides.has(key)) {
|
|
1980
|
+
result[key] = overrides.get(key);
|
|
1981
|
+
continue;
|
|
1982
|
+
}
|
|
1983
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
1984
|
+
result[key] = expressionToValue(property.name, bindings, propertyContext(context, key), depth + 1);
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
result[key] = expressionToValue(property.initializer, bindings, propertyContext(context, key), depth + 1);
|
|
1988
|
+
}
|
|
1989
|
+
if (context.transitionValue && "to" in result) {
|
|
1990
|
+
const to = result.to;
|
|
1991
|
+
const { to: _to, ...rest } = result;
|
|
1992
|
+
if (typeof to === "string") return {
|
|
1993
|
+
...rest,
|
|
1994
|
+
target: to
|
|
1995
|
+
};
|
|
1996
|
+
if (Array.isArray(to)) return to.map((item) => typeof item === "string" ? {
|
|
1997
|
+
...rest,
|
|
1998
|
+
target: item
|
|
1999
|
+
} : item && typeof item === "object" ? {
|
|
2000
|
+
...rest,
|
|
2001
|
+
...item
|
|
2002
|
+
} : rest);
|
|
2003
|
+
if (to && typeof to === "object" && !Array.isArray(to)) return {
|
|
2004
|
+
...rest,
|
|
2005
|
+
...to
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
return result;
|
|
2009
|
+
}
|
|
2010
|
+
function expressionToValue(expression, bindings, context = {}, depth = 0) {
|
|
2011
|
+
if (depth > 30) return void 0;
|
|
2012
|
+
const node = unwrapExpression$2(expression);
|
|
2013
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
2014
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
2015
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
2016
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
2017
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
2018
|
+
if (ts.isIdentifier(node)) {
|
|
2019
|
+
if (node.text === "undefined") return void 0;
|
|
2020
|
+
const binding = bindings.get(node.text);
|
|
2021
|
+
return binding ? expressionToValue(binding, bindings, context, depth + 1) : context.metaValue ? inlineCodeExpression(node.getText()) : node.text;
|
|
2022
|
+
}
|
|
2023
|
+
if (ts.isObjectLiteralExpression(node)) return objectLiteralToValue(node, bindings, context, depth);
|
|
2024
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
2025
|
+
const values = node.elements.map((element) => expressionToValue(element, bindings, context, depth + 1));
|
|
2026
|
+
return context.actionValue ? values.flat() : values;
|
|
2027
|
+
}
|
|
2028
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
2029
|
+
if (context.metaValue) return inlineCodeExpression(node.getText());
|
|
2030
|
+
if (context.v6 && context.transitionValue) return normalizeFunctionTransition(node, bindings, depth);
|
|
2031
|
+
if (context.v6 && context.actionValue) return normalizeFunctionAction(node, bindings);
|
|
2032
|
+
return node.getText();
|
|
2033
|
+
}
|
|
2034
|
+
if (ts.isCallExpression(node)) {
|
|
2035
|
+
if (context.metaValue) return inlineCodeExpression(node.getText());
|
|
2036
|
+
if (context.actionValue) return inlineAction(node);
|
|
2037
|
+
const callee = node.expression.getText();
|
|
2038
|
+
const type = callee.includes(".") ? callee.split(".").pop() : callee;
|
|
2039
|
+
return { type: type === "assign" ? "xstate.assign" : type };
|
|
2040
|
+
}
|
|
2041
|
+
return context.metaValue ? inlineCodeExpression(node.getText()) : node.getText();
|
|
2042
|
+
}
|
|
2043
|
+
function collectBindings$1(file) {
|
|
2044
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
2045
|
+
for (const statement of file.statements) {
|
|
2046
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
2047
|
+
for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.initializer) bindings.set(declaration.name.text, declaration.initializer);
|
|
2048
|
+
}
|
|
2049
|
+
return bindings;
|
|
2050
|
+
}
|
|
2051
|
+
function isCreateMachineCall(node) {
|
|
2052
|
+
const expression = node.expression;
|
|
2053
|
+
if (ts.isIdentifier(expression)) return expression.text === "createMachine";
|
|
2054
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text === "createMachine";
|
|
2055
|
+
return false;
|
|
2056
|
+
}
|
|
2057
|
+
function hasV6Syntax(source) {
|
|
2058
|
+
const file = sourceFile(source);
|
|
2059
|
+
let found = false;
|
|
2060
|
+
function visit(node) {
|
|
2061
|
+
if (found) return;
|
|
2062
|
+
if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))) {
|
|
2063
|
+
const key = node.name.text;
|
|
2064
|
+
if (key === "triggers" || key === "internalEvents" || key === "timeout" || key === "onTimeout" || key === "choice" || key === "route") {
|
|
2065
|
+
found = true;
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
if ((key === "on" || key === "after" || key === "always" || key === "entry" || key === "exit" || key === "actions" && !ts.isObjectLiteralExpression(node.initializer)) && node.initializer.getText().includes("=>")) {
|
|
2069
|
+
found = true;
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
ts.forEachChild(node, visit);
|
|
2074
|
+
}
|
|
2075
|
+
visit(file);
|
|
2076
|
+
return found;
|
|
2077
|
+
}
|
|
2078
|
+
function detectXStateSourceProfile(source) {
|
|
2079
|
+
return hasV6Syntax(source) ? "xstate-v6-alpha" : "xstate-v5";
|
|
2080
|
+
}
|
|
2081
|
+
function xstateSourceToConfig(source, options = {}) {
|
|
2082
|
+
const file = sourceFile(source);
|
|
2083
|
+
const bindings = collectBindings$1(file);
|
|
2084
|
+
let found = null;
|
|
2085
|
+
const profile = options.profile === "auto" || !options.profile ? detectXStateSourceProfile(source) : options.profile;
|
|
2086
|
+
function visit(node) {
|
|
2087
|
+
if (found || !ts.isCallExpression(node) || !isCreateMachineCall(node)) {
|
|
2088
|
+
ts.forEachChild(node, visit);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const arg = node.arguments[0];
|
|
2092
|
+
if (!arg) return;
|
|
2093
|
+
const value = expressionToValue(arg, bindings, { v6: profile === "xstate-v6-alpha" });
|
|
2094
|
+
if (value && typeof value === "object" && !Array.isArray(value)) found = value;
|
|
2095
|
+
}
|
|
2096
|
+
visit(file);
|
|
2097
|
+
if (!found) throw new Error("No createMachine(...) or setup(...).createMachine(...) call found.");
|
|
2098
|
+
return found;
|
|
2099
|
+
}
|
|
2100
|
+
function parseJsonLike(input) {
|
|
2101
|
+
return JSON.parse(input);
|
|
2102
|
+
}
|
|
2103
|
+
function parseSimpleYaml(input) {
|
|
2104
|
+
const lines = input.split(/\r?\n/).filter((line) => line.trim() && !line.trim().startsWith("#"));
|
|
2105
|
+
const root = {};
|
|
2106
|
+
const stack = [{
|
|
2107
|
+
indent: -1,
|
|
2108
|
+
value: root
|
|
2109
|
+
}];
|
|
2110
|
+
for (const line of lines) {
|
|
2111
|
+
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
2112
|
+
const trimmed = line.trim();
|
|
2113
|
+
const match = /^([^:]+):(.*)$/.exec(trimmed);
|
|
2114
|
+
if (!match) continue;
|
|
2115
|
+
const key = match[1].trim();
|
|
2116
|
+
const raw = match[2].trim();
|
|
2117
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
|
|
2118
|
+
const parent = stack[stack.length - 1].value;
|
|
2119
|
+
if (!raw) {
|
|
2120
|
+
const child = {};
|
|
2121
|
+
parent[key] = child;
|
|
2122
|
+
stack.push({
|
|
2123
|
+
indent,
|
|
2124
|
+
value: child
|
|
2125
|
+
});
|
|
2126
|
+
} else if (raw === "true" || raw === "false") parent[key] = raw === "true";
|
|
2127
|
+
else if (/^-?\d+(?:\.\d+)?$/.test(raw)) parent[key] = Number(raw);
|
|
2128
|
+
else parent[key] = raw.replace(/^['"]|['"]$/g, "");
|
|
2129
|
+
}
|
|
2130
|
+
return root;
|
|
2131
|
+
}
|
|
2132
|
+
function parseMermaid(input) {
|
|
2133
|
+
const states = /* @__PURE__ */ new Set();
|
|
2134
|
+
const transitions = [];
|
|
2135
|
+
let initial;
|
|
2136
|
+
for (const line of input.split(/\r?\n/)) {
|
|
2137
|
+
const trimmed = line.trim();
|
|
2138
|
+
if (!trimmed || /^(stateDiagram|stateDiagram-v2|flowchart|graph)\b/.test(trimmed)) continue;
|
|
2139
|
+
const stateMatch = /^\[\*\]\s*-->\s*([A-Za-z0-9_.-]+)/.exec(trimmed);
|
|
2140
|
+
if (stateMatch?.[1]) {
|
|
2141
|
+
initial = stateMatch[1];
|
|
2142
|
+
states.add(initial);
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
const transitionMatch = /^([A-Za-z0-9_.-]+)\s*(?:-->|--)\s*(?:\|([^|]+)\|)?\s*([A-Za-z0-9_.-]+)(?:\s*:\s*(.+))?/.exec(trimmed);
|
|
2146
|
+
if (!transitionMatch) continue;
|
|
2147
|
+
const source = transitionMatch[1];
|
|
2148
|
+
const target = transitionMatch[3];
|
|
2149
|
+
const event = transitionMatch[2] ?? transitionMatch[4];
|
|
2150
|
+
states.add(source);
|
|
2151
|
+
states.add(target);
|
|
2152
|
+
transitions.push({
|
|
2153
|
+
source,
|
|
2154
|
+
target,
|
|
2155
|
+
event: event?.trim()
|
|
2156
|
+
});
|
|
2157
|
+
initial ??= source;
|
|
2158
|
+
}
|
|
2159
|
+
const stateConfigs = {};
|
|
2160
|
+
for (const state of states) stateConfigs[state] = {};
|
|
2161
|
+
for (const transition of transitions) {
|
|
2162
|
+
const source = stateConfigs[transition.source];
|
|
2163
|
+
const on = source.on ??= {};
|
|
2164
|
+
on[transition.event || "NEXT"] = transition.target;
|
|
2165
|
+
}
|
|
2166
|
+
return {
|
|
2167
|
+
id: "machine",
|
|
2168
|
+
initial: initial ?? [...states][0] ?? "idle",
|
|
2169
|
+
states: Object.keys(stateConfigs).length ? stateConfigs : { idle: {} }
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function inferMachineFormat(machine, format) {
|
|
2173
|
+
if (format) return format;
|
|
2174
|
+
if (typeof machine !== "string") return "json";
|
|
2175
|
+
const trimmed = machine.trim();
|
|
2176
|
+
if (trimmed.startsWith("{")) return "json";
|
|
2177
|
+
if (/^(stateDiagram|flowchart|graph)\b/m.test(trimmed)) return "mermaid";
|
|
2178
|
+
if (/^<\?xml|^<scxml/m.test(trimmed)) return "scxml";
|
|
2179
|
+
if (/^\w+:/m.test(trimmed)) return "yaml";
|
|
2180
|
+
return "xstate";
|
|
2181
|
+
}
|
|
2182
|
+
function machineInputToConfig(input) {
|
|
2183
|
+
const format = inferMachineFormat(input.machine, input.format);
|
|
2184
|
+
if (typeof input.machine !== "string") {
|
|
2185
|
+
createMachine(input.machine);
|
|
2186
|
+
return input.machine;
|
|
2187
|
+
}
|
|
2188
|
+
if (format === "json") {
|
|
2189
|
+
const config = parseJsonLike(input.machine);
|
|
2190
|
+
createMachine(config);
|
|
2191
|
+
return config;
|
|
2192
|
+
}
|
|
2193
|
+
if (format === "yaml") {
|
|
2194
|
+
const config = parseSimpleYaml(input.machine);
|
|
2195
|
+
createMachine(config);
|
|
2196
|
+
return config;
|
|
2197
|
+
}
|
|
2198
|
+
if (format === "mermaid") {
|
|
2199
|
+
const config = parseMermaid(input.machine);
|
|
2200
|
+
createMachine(config);
|
|
2201
|
+
return config;
|
|
2202
|
+
}
|
|
2203
|
+
if (format === "xstate") {
|
|
2204
|
+
const config = xstateSourceToConfig(input.machine, { profile: input.profile ?? (input.xstateVersion === 6 ? "xstate-v6-alpha" : input.xstateVersion === 5 ? "xstate-v5" : "auto") });
|
|
2205
|
+
createMachine(config);
|
|
2206
|
+
return config;
|
|
2207
|
+
}
|
|
2208
|
+
throw new Error(`${format} source parsing is not supported by graph-tools v1 yet.`);
|
|
2209
|
+
}
|
|
2210
|
+
function machineInputToGraph(input) {
|
|
2211
|
+
return machineConfigToGraph(machineInputToConfig(input));
|
|
2212
|
+
}
|
|
2213
|
+
function graphToMermaid(graph) {
|
|
2214
|
+
const machine = graphToMachineConfig(graph);
|
|
2215
|
+
const lines = ["stateDiagram-v2"];
|
|
2216
|
+
if (machine.initial) lines.push(` [*] --> ${machine.initial}`);
|
|
2217
|
+
for (const [stateKey, state] of Object.entries(machine.states ?? {})) for (const [event, transition] of Object.entries(state.on ?? {})) {
|
|
2218
|
+
const target = typeof transition === "string" ? transition : Array.isArray(transition) ? void 0 : transition.target;
|
|
2219
|
+
if (target) lines.push(` ${stateKey} --> ${target} : ${event}`);
|
|
2220
|
+
}
|
|
2221
|
+
return lines.join("\n");
|
|
2222
|
+
}
|
|
2223
|
+
function toYaml(value, indent = 0) {
|
|
2224
|
+
const pad = " ".repeat(indent);
|
|
2225
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return `${JSON.stringify(value)}`;
|
|
2226
|
+
return Object.entries(value).map(([key, item]) => {
|
|
2227
|
+
if (item && typeof item === "object" && !Array.isArray(item)) return `${pad}${key}:\n${toYaml(item, indent + 2)}`;
|
|
2228
|
+
return `${pad}${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`;
|
|
2229
|
+
}).join("\n");
|
|
2230
|
+
}
|
|
2231
|
+
function graphToFormat(graph, format) {
|
|
2232
|
+
const config = graphToMachineConfig(graph);
|
|
2233
|
+
if (format === "xstate") return `import { createMachine } from 'xstate';\n\nexport const machine = createMachine(${serializeJS(config, 2)});\n`;
|
|
2234
|
+
if (format === "json") return config;
|
|
2235
|
+
if (format === "yaml") return toYaml(config);
|
|
2236
|
+
if (format === "mermaid") return graphToMermaid(graph);
|
|
2237
|
+
if (format === "scxml") {
|
|
2238
|
+
const machine = config;
|
|
2239
|
+
return `<scxml version="1.0" initial="${machine.initial ?? ""}" name="${machine.id ?? "machine"}"></scxml>`;
|
|
2240
|
+
}
|
|
2241
|
+
return config;
|
|
2242
|
+
}
|
|
2243
|
+
const colorSchema = _enum([
|
|
2244
|
+
"green",
|
|
2245
|
+
"red",
|
|
2246
|
+
"purple",
|
|
2247
|
+
"blue",
|
|
2248
|
+
"orange",
|
|
2249
|
+
"yellow",
|
|
2250
|
+
"pink",
|
|
2251
|
+
"teal"
|
|
2252
|
+
]);
|
|
2253
|
+
const stateTypeSchema = _enum([
|
|
2254
|
+
"normal",
|
|
2255
|
+
"parallel",
|
|
2256
|
+
"history",
|
|
2257
|
+
"final"
|
|
2258
|
+
]).nullable();
|
|
2259
|
+
const actionLocationSchema = union([object({
|
|
2260
|
+
nodeId: string(),
|
|
2261
|
+
group: _enum(["entry", "exit"])
|
|
2262
|
+
}), object({
|
|
2263
|
+
edgeId: string(),
|
|
2264
|
+
group: literal("transition")
|
|
2265
|
+
})]);
|
|
2266
|
+
const createStatePatchSchema = object({
|
|
2267
|
+
op: literal("createState"),
|
|
2268
|
+
description: string().optional(),
|
|
2269
|
+
id: string().optional(),
|
|
2270
|
+
parentId: string(),
|
|
2271
|
+
key: string(),
|
|
2272
|
+
type: stateTypeSchema.optional(),
|
|
2273
|
+
x: number().optional(),
|
|
2274
|
+
y: number().optional(),
|
|
2275
|
+
color: colorSchema.nullable().optional(),
|
|
2276
|
+
initial: boolean().optional()
|
|
2277
|
+
});
|
|
2278
|
+
const updateStatePatchSchema = object({
|
|
2279
|
+
op: literal("updateState"),
|
|
2280
|
+
description: string().optional(),
|
|
2281
|
+
stateId: string(),
|
|
2282
|
+
key: string().optional(),
|
|
2283
|
+
type: stateTypeSchema.optional(),
|
|
2284
|
+
stateDescription: string().optional(),
|
|
2285
|
+
initialId: string().nullable().optional(),
|
|
2286
|
+
color: colorSchema.nullable().optional(),
|
|
2287
|
+
meta: record(string(), unknown()).nullable().optional(),
|
|
2288
|
+
history: _enum(["shallow", "deep"]).nullable().optional()
|
|
2289
|
+
});
|
|
2290
|
+
const deleteStatePatchSchema = object({
|
|
2291
|
+
op: literal("deleteState"),
|
|
2292
|
+
description: string().optional(),
|
|
2293
|
+
stateId: string()
|
|
2294
|
+
});
|
|
2295
|
+
const guardSchema = object({
|
|
2296
|
+
type: string(),
|
|
2297
|
+
code: string().optional(),
|
|
2298
|
+
params: record(string(), unknown()).optional()
|
|
2299
|
+
});
|
|
2300
|
+
const createTransitionPatchSchema = object({
|
|
2301
|
+
op: literal("createTransition"),
|
|
2302
|
+
description: string().optional(),
|
|
2303
|
+
id: string().optional(),
|
|
2304
|
+
sourceId: string(),
|
|
2305
|
+
targetId: string(),
|
|
2306
|
+
eventType: string(),
|
|
2307
|
+
transitionType: _enum([
|
|
2308
|
+
"normal",
|
|
2309
|
+
"targetless",
|
|
2310
|
+
"reenter"
|
|
2311
|
+
]).optional(),
|
|
2312
|
+
guard: guardSchema.optional(),
|
|
2313
|
+
color: colorSchema.nullable().optional(),
|
|
2314
|
+
transitionDescription: string().nullable().optional()
|
|
2315
|
+
});
|
|
2316
|
+
const updateTransitionPatchSchema = object({
|
|
2317
|
+
op: literal("updateTransition"),
|
|
2318
|
+
description: string().optional(),
|
|
2319
|
+
transitionId: string(),
|
|
2320
|
+
sourceId: string().optional(),
|
|
2321
|
+
targetId: string().optional(),
|
|
2322
|
+
eventType: string().optional(),
|
|
2323
|
+
transitionType: _enum([
|
|
2324
|
+
"normal",
|
|
2325
|
+
"targetless",
|
|
2326
|
+
"reenter"
|
|
2327
|
+
]).optional(),
|
|
2328
|
+
transitionDescription: string().nullable().optional(),
|
|
2329
|
+
color: colorSchema.nullable().optional(),
|
|
2330
|
+
meta: record(string(), unknown()).nullable().optional()
|
|
2331
|
+
});
|
|
2332
|
+
const deleteTransitionPatchSchema = object({
|
|
2333
|
+
op: literal("deleteTransition"),
|
|
2334
|
+
description: string().optional(),
|
|
2335
|
+
transitionId: string()
|
|
2336
|
+
});
|
|
2337
|
+
const createActionPatchSchema = object({
|
|
2338
|
+
op: literal("createAction"),
|
|
2339
|
+
description: string().optional(),
|
|
2340
|
+
location: actionLocationSchema,
|
|
2341
|
+
action: object({
|
|
2342
|
+
id: string().optional(),
|
|
2343
|
+
type: string(),
|
|
2344
|
+
params: record(string(), unknown()).optional()
|
|
2345
|
+
}),
|
|
2346
|
+
index: number().optional()
|
|
2347
|
+
});
|
|
2348
|
+
const updateActionPatchSchema = object({
|
|
2349
|
+
op: literal("updateAction"),
|
|
2350
|
+
description: string().optional(),
|
|
2351
|
+
location: actionLocationSchema,
|
|
2352
|
+
actionId: string(),
|
|
2353
|
+
data: object({
|
|
2354
|
+
type: string().optional(),
|
|
2355
|
+
params: record(string(), unknown()).optional()
|
|
2356
|
+
})
|
|
2357
|
+
});
|
|
2358
|
+
const deleteActionPatchSchema = object({
|
|
2359
|
+
op: literal("deleteAction"),
|
|
2360
|
+
description: string().optional(),
|
|
2361
|
+
location: actionLocationSchema,
|
|
2362
|
+
actionId: string()
|
|
2363
|
+
});
|
|
2364
|
+
const setGuardPatchSchema = object({
|
|
2365
|
+
op: literal("setGuard"),
|
|
2366
|
+
description: string().optional(),
|
|
2367
|
+
edgeId: string(),
|
|
2368
|
+
guard: guardSchema
|
|
2369
|
+
});
|
|
2370
|
+
const deleteGuardPatchSchema = object({
|
|
2371
|
+
op: literal("deleteGuard"),
|
|
2372
|
+
description: string().optional(),
|
|
2373
|
+
edgeId: string()
|
|
2374
|
+
});
|
|
2375
|
+
const machinePatchSchema = discriminatedUnion("op", [
|
|
2376
|
+
createStatePatchSchema,
|
|
2377
|
+
updateStatePatchSchema,
|
|
2378
|
+
deleteStatePatchSchema,
|
|
2379
|
+
createTransitionPatchSchema,
|
|
2380
|
+
updateTransitionPatchSchema,
|
|
2381
|
+
deleteTransitionPatchSchema,
|
|
2382
|
+
createActionPatchSchema,
|
|
2383
|
+
updateActionPatchSchema,
|
|
2384
|
+
deleteActionPatchSchema,
|
|
2385
|
+
setGuardPatchSchema,
|
|
2386
|
+
deleteGuardPatchSchema
|
|
2387
|
+
]);
|
|
2388
|
+
const validationLevelSchema = _enum([
|
|
2389
|
+
"info",
|
|
2390
|
+
"warning",
|
|
2391
|
+
"error"
|
|
2392
|
+
]);
|
|
2393
|
+
const validationIssueKindSchema = _enum([
|
|
2394
|
+
"correctness",
|
|
2395
|
+
"reachability",
|
|
2396
|
+
"optimization",
|
|
2397
|
+
"maintainability",
|
|
2398
|
+
"security"
|
|
2399
|
+
]);
|
|
2400
|
+
const validationIssueCodeSchema = _enum([
|
|
2401
|
+
"parse_error",
|
|
2402
|
+
"unsupported_format",
|
|
2403
|
+
"empty_state_key",
|
|
2404
|
+
"invalid_state_key",
|
|
2405
|
+
"duplicate_state_key",
|
|
2406
|
+
"history_initial",
|
|
2407
|
+
"missing_initial",
|
|
2408
|
+
"invalid_initial",
|
|
2409
|
+
"unreachable_state",
|
|
2410
|
+
"final_state_invokes",
|
|
2411
|
+
"invalid_state_children",
|
|
2412
|
+
"empty_parallel",
|
|
2413
|
+
"single_child_compound",
|
|
2414
|
+
"noop_transient_state",
|
|
2415
|
+
"on_done_unreachable",
|
|
2416
|
+
"duplicate_transition",
|
|
2417
|
+
"invalid_delay",
|
|
2418
|
+
"repeated_guard",
|
|
2419
|
+
"transition_never_taken",
|
|
2420
|
+
"missing_guard",
|
|
2421
|
+
"redundant_transition",
|
|
2422
|
+
"missing_delay_implementation",
|
|
2423
|
+
"invalid_final_transition",
|
|
2424
|
+
"invalid_history_transition",
|
|
2425
|
+
"parallel_region_transition",
|
|
2426
|
+
"duplicate_source_name",
|
|
2427
|
+
"undefined_action",
|
|
2428
|
+
"undefined_guard",
|
|
2429
|
+
"undefined_actor"
|
|
2430
|
+
]);
|
|
2431
|
+
const validationFixKindSchema = _enum([
|
|
2432
|
+
"patch",
|
|
2433
|
+
"rewrite",
|
|
2434
|
+
"advice",
|
|
2435
|
+
"proposedPatch"
|
|
2436
|
+
]);
|
|
2437
|
+
const validationFixSourceSchema = _enum(["deterministic", "llm"]);
|
|
2438
|
+
const AUTO_NAME_PREFIXES = [
|
|
2439
|
+
":invocation:",
|
|
2440
|
+
"inline:",
|
|
2441
|
+
"$auto-"
|
|
2442
|
+
];
|
|
2443
|
+
const AUTO_NAME_SUBSTRINGS = [
|
|
2444
|
+
":invocation[",
|
|
2445
|
+
"#transition[",
|
|
2446
|
+
"#actor["
|
|
2447
|
+
];
|
|
2448
|
+
function isAutoGeneratedInlineName(name) {
|
|
2449
|
+
return AUTO_NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) || AUTO_NAME_SUBSTRINGS.some((part) => name.includes(part));
|
|
2450
|
+
}
|
|
2451
|
+
function getImplementations(graph) {
|
|
2452
|
+
return graph.data.implementations ?? {
|
|
2453
|
+
actions: [],
|
|
2454
|
+
guards: [],
|
|
2455
|
+
actors: [],
|
|
2456
|
+
delays: []
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function issue(input) {
|
|
2460
|
+
const nodeIds = input.nodeIds ?? [];
|
|
2461
|
+
const edgeIds = input.edgeIds ?? [];
|
|
2462
|
+
return {
|
|
2463
|
+
code: input.code,
|
|
2464
|
+
kind: input.kind,
|
|
2465
|
+
level: input.level,
|
|
2466
|
+
message: input.message,
|
|
2467
|
+
target: {
|
|
2468
|
+
nodeIds,
|
|
2469
|
+
edgeIds
|
|
2470
|
+
},
|
|
2471
|
+
metadata: input.metadata,
|
|
2472
|
+
fixes: input.fixes,
|
|
2473
|
+
source: "deterministic",
|
|
2474
|
+
nodeIds,
|
|
2475
|
+
edgeIds
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
function deleteTransitionFix(edge, title) {
|
|
2479
|
+
return {
|
|
2480
|
+
kind: "patch",
|
|
2481
|
+
source: "deterministic",
|
|
2482
|
+
title,
|
|
2483
|
+
patches: [{
|
|
2484
|
+
op: "deleteTransition",
|
|
2485
|
+
transitionId: edge.id
|
|
2486
|
+
}],
|
|
2487
|
+
confidence: .9
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
function setInitialFix(node, child, title) {
|
|
2491
|
+
return {
|
|
2492
|
+
kind: "patch",
|
|
2493
|
+
source: "deterministic",
|
|
2494
|
+
title,
|
|
2495
|
+
patches: [{
|
|
2496
|
+
op: "updateState",
|
|
2497
|
+
stateId: node.id,
|
|
2498
|
+
initialId: child.id
|
|
2499
|
+
}],
|
|
2500
|
+
confidence: .85
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
function findDuplicateNodes(graph, node, parent) {
|
|
2504
|
+
if (!parent) return [];
|
|
2505
|
+
return graph.nodes.filter((n) => n.parentId === parent.id && n.id !== node.id).filter((childNode) => childNode.data.key === node.data.key);
|
|
2506
|
+
}
|
|
2507
|
+
function findDuplicateEdges(graph, edge) {
|
|
2508
|
+
return graph.edges.filter((graphEdge) => graphEdge.id !== edge.id && !graphEdge.data.guard && !edge.data.guard && graphEdge.sourceId === edge.sourceId && graphEdge.data.eventType === edge.data.eventType);
|
|
2509
|
+
}
|
|
2510
|
+
function hasUnconditionalAlways(graph, sourceId) {
|
|
2511
|
+
return graph.edges.some((e) => e.sourceId === sourceId && e.data.eventType === "" && !e.data.guard && e.targetId !== e.sourceId);
|
|
2512
|
+
}
|
|
2513
|
+
function isTransitionPreempted(graph, edge) {
|
|
2514
|
+
if (edge.data.eventType === "") return false;
|
|
2515
|
+
return hasUnconditionalAlways(graph, edge.sourceId);
|
|
2516
|
+
}
|
|
2517
|
+
function computeReachableGraphNodes(graph) {
|
|
2518
|
+
const rootNode = graph.nodes.find((n) => n.parentId === null);
|
|
2519
|
+
if (!rootNode) return /* @__PURE__ */ new Set();
|
|
2520
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2521
|
+
function getNodeInitialStates(node) {
|
|
2522
|
+
const children = graph.nodes.filter((n) => n.parentId === node.id);
|
|
2523
|
+
if (!children.length) return [];
|
|
2524
|
+
if (node.data.type === "parallel") return children;
|
|
2525
|
+
const initial = children.find((n) => node.data.initialId === n.id);
|
|
2526
|
+
return initial ? [initial] : [];
|
|
2527
|
+
}
|
|
2528
|
+
function dfs(node) {
|
|
2529
|
+
if (visited.has(node.id)) return;
|
|
2530
|
+
visited.add(node.id);
|
|
2531
|
+
getNodeInitialStates(node).forEach((n) => dfs(n));
|
|
2532
|
+
if (node.data.type === "parallel") graph.nodes.filter((n) => n.parentId === node.id).forEach((region) => {
|
|
2533
|
+
getNodeInitialStates(region).forEach((n) => dfs(n));
|
|
2534
|
+
});
|
|
2535
|
+
if (node.data.type === "history") {
|
|
2536
|
+
const parent = graph.nodes.find((n) => n.id === node.parentId);
|
|
2537
|
+
if (parent) if (parent.data.type === "parallel") graph.nodes.filter((n) => n.parentId === parent.id).forEach((region) => {
|
|
2538
|
+
getNodeInitialStates(region).forEach((n) => dfs(n));
|
|
2539
|
+
});
|
|
2540
|
+
else getNodeInitialStates(parent).forEach((n) => dfs(n));
|
|
2541
|
+
}
|
|
2542
|
+
graph.edges.filter((edge) => edge.sourceId === node.id && !isTransitionPreempted(graph, edge)).forEach((edge) => {
|
|
2543
|
+
const target = graph.nodes.find((n) => n.id === edge.targetId);
|
|
2544
|
+
if (target) dfs(target);
|
|
2545
|
+
});
|
|
2546
|
+
let ancestor = graph.nodes.find((n) => n.id === node.parentId);
|
|
2547
|
+
while (ancestor) {
|
|
2548
|
+
graph.edges.filter((edge) => edge.sourceId === ancestor.id && !isTransitionPreempted(graph, edge)).forEach((edge) => {
|
|
2549
|
+
const target = graph.nodes.find((n) => n.id === edge.targetId);
|
|
2550
|
+
if (target) dfs(target);
|
|
2551
|
+
});
|
|
2552
|
+
ancestor = graph.nodes.find((n) => n.id === ancestor.parentId);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
dfs(rootNode);
|
|
2556
|
+
const reachableWithParents = new Set(visited);
|
|
2557
|
+
for (const nodeId of visited) {
|
|
2558
|
+
let node = graph.nodes.find((n) => n.id === nodeId);
|
|
2559
|
+
while (node?.parentId) {
|
|
2560
|
+
reachableWithParents.add(node.parentId);
|
|
2561
|
+
node = graph.nodes.find((n) => n.id === node.parentId);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
return reachableWithParents;
|
|
2565
|
+
}
|
|
2566
|
+
function getNodeErrors(graph, node) {
|
|
2567
|
+
const errors = [];
|
|
2568
|
+
if (node.data.key.length === 0) errors.push({
|
|
2569
|
+
code: "empty_state_key",
|
|
2570
|
+
kind: "correctness",
|
|
2571
|
+
message: "State key cannot be empty"
|
|
2572
|
+
});
|
|
2573
|
+
const parent = graph.nodes.find((n) => n.id === node.parentId);
|
|
2574
|
+
const isInitial = parent?.data.initialId === node.id;
|
|
2575
|
+
const isHistoryNode = node.data.type === "history";
|
|
2576
|
+
if (isInitial && isHistoryNode) errors.push({
|
|
2577
|
+
code: "history_initial",
|
|
2578
|
+
kind: "correctness",
|
|
2579
|
+
message: "A history node cannot be the initial node. This will cause an infinite loop."
|
|
2580
|
+
});
|
|
2581
|
+
if (findDuplicateNodes(graph, node, parent).length > 0) errors.push({
|
|
2582
|
+
code: "duplicate_state_key",
|
|
2583
|
+
kind: "correctness",
|
|
2584
|
+
message: "A state with that name already exists"
|
|
2585
|
+
});
|
|
2586
|
+
if (node.data.key.includes("#")) errors.push({
|
|
2587
|
+
code: "invalid_state_key",
|
|
2588
|
+
kind: "correctness",
|
|
2589
|
+
message: "State key cannot contain \"#\"",
|
|
2590
|
+
metadata: { character: "#" }
|
|
2591
|
+
});
|
|
2592
|
+
const children = graph.nodes.filter((n) => n.parentId === node.id);
|
|
2593
|
+
if (children.length > 0 && node.data.type !== "parallel" && node.data.type !== "final" && node.data.type !== "history") {
|
|
2594
|
+
const isTargeted = graph.edges.some((edge) => edge.targetId === node.id);
|
|
2595
|
+
if (!node.data.initialId && isTargeted) errors.push({
|
|
2596
|
+
code: "missing_initial",
|
|
2597
|
+
kind: "correctness",
|
|
2598
|
+
message: "Compound state must have an initial state",
|
|
2599
|
+
metadata: {
|
|
2600
|
+
reason: "targeted_compound",
|
|
2601
|
+
childCount: children.length
|
|
2602
|
+
},
|
|
2603
|
+
fixes: children.length === 1 ? [setInitialFix(node, children[0], `Set ${children[0].data.key} as initial state`)] : void 0
|
|
2604
|
+
});
|
|
2605
|
+
else if (node.data.initialId) {
|
|
2606
|
+
if (!children.some((n) => n.id === node.data.initialId)) errors.push({
|
|
2607
|
+
code: "invalid_initial",
|
|
2608
|
+
kind: "correctness",
|
|
2609
|
+
message: "Initial state does not exist",
|
|
2610
|
+
metadata: {
|
|
2611
|
+
initialId: node.data.initialId,
|
|
2612
|
+
childCount: children.length
|
|
2613
|
+
},
|
|
2614
|
+
fixes: children.length === 1 ? [setInitialFix(node, children[0], `Set ${children[0].data.key} as initial state`)] : void 0
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
return errors.length > 0 ? errors : null;
|
|
2619
|
+
}
|
|
2620
|
+
function getNodeWarningsWithReachability(graph, node, reachableNodes) {
|
|
2621
|
+
const warnings = [];
|
|
2622
|
+
if (!reachableNodes.has(node.id) && node.data.type !== "history") warnings.push({
|
|
2623
|
+
code: "unreachable_state",
|
|
2624
|
+
kind: "reachability",
|
|
2625
|
+
message: "Unreachable state"
|
|
2626
|
+
});
|
|
2627
|
+
if (node.data.type === "final" && (node.data.invokes?.length ?? 0) > 0) warnings.push({
|
|
2628
|
+
code: "final_state_invokes",
|
|
2629
|
+
kind: "correctness",
|
|
2630
|
+
message: "Final state cannot have invocations"
|
|
2631
|
+
});
|
|
2632
|
+
if (node.data.type && ["final", "history"].includes(node.data.type) && graph.nodes.filter((n) => n.parentId === node.id).length > 0) warnings.push({
|
|
2633
|
+
code: "invalid_state_children",
|
|
2634
|
+
kind: "correctness",
|
|
2635
|
+
message: `${node.data.type.charAt(0).toUpperCase() + node.data.type.slice(1)} state cannot have child states`,
|
|
2636
|
+
metadata: { stateType: node.data.type }
|
|
2637
|
+
});
|
|
2638
|
+
if (node.data.type === "parallel" && graph.nodes.filter((n) => n.parentId === node.id).length === 0) warnings.push({
|
|
2639
|
+
code: "empty_parallel",
|
|
2640
|
+
kind: "correctness",
|
|
2641
|
+
message: "Parallel state should have child regions"
|
|
2642
|
+
});
|
|
2643
|
+
const children = graph.nodes.filter((n) => n.parentId === node.id);
|
|
2644
|
+
if (children.length === 1 && node.data.type !== "parallel" && node.data.type !== "final" && node.data.type !== "history") warnings.push({
|
|
2645
|
+
code: "single_child_compound",
|
|
2646
|
+
kind: "optimization",
|
|
2647
|
+
message: "State with single child can be simplified"
|
|
2648
|
+
});
|
|
2649
|
+
if (node.data.type !== "parallel" && node.data.type !== "final" && node.data.type !== "history" && children.length === 0 && (node.data.entry?.length ?? 0) === 0 && (node.data.exit?.length ?? 0) === 0 && (node.data.invokes?.length ?? 0) === 0) {
|
|
2650
|
+
const alwaysEdges = graph.edges.filter((e) => e.sourceId === node.id && e.data.eventType === "");
|
|
2651
|
+
const hasUnconditionalNoop = alwaysEdges.some((e) => !e.data.guard && e.targetId !== e.sourceId && (e.data.actions?.length ?? 0) === 0);
|
|
2652
|
+
const hasMeaningfulAlways = alwaysEdges.some((e) => !!e.data.guard || (e.data.actions?.length ?? 0) > 0);
|
|
2653
|
+
if (hasUnconditionalNoop && !hasMeaningfulAlways) warnings.push({
|
|
2654
|
+
code: "noop_transient_state",
|
|
2655
|
+
kind: "optimization",
|
|
2656
|
+
message: "State has no effect: an unconditional transition exits it immediately, and it has no actions or invocations"
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
return warnings.length > 0 ? warnings : null;
|
|
2660
|
+
}
|
|
2661
|
+
function getOnDoneWarnings(graph, reachableNodes) {
|
|
2662
|
+
const results = [];
|
|
2663
|
+
for (const node of graph.nodes) {
|
|
2664
|
+
const doneEdges = graph.edges.filter((edge) => edge.sourceId === node.id && (edge.data.eventType.startsWith("@statelyai.state.done.") || edge.data.eventType.startsWith("xstate.done.state.")));
|
|
2665
|
+
if (doneEdges.length === 0) continue;
|
|
2666
|
+
const edgeIds = doneEdges.map((e) => e.id);
|
|
2667
|
+
if (node.data.type === "parallel") {
|
|
2668
|
+
const regions = graph.nodes.filter((n) => n.parentId === node.id);
|
|
2669
|
+
const regionsWithoutFinal = [];
|
|
2670
|
+
const regionsWithoutReachableFinal = [];
|
|
2671
|
+
for (const region of regions) {
|
|
2672
|
+
const finalChildren = graph.nodes.filter((n) => n.parentId === region.id).filter((child) => child.data.type === "final");
|
|
2673
|
+
if (finalChildren.length === 0) regionsWithoutFinal.push(region);
|
|
2674
|
+
else if (!finalChildren.some((child) => reachableNodes.has(child.id))) regionsWithoutReachableFinal.push(region);
|
|
2675
|
+
}
|
|
2676
|
+
if (regionsWithoutFinal.length > 0) results.push(issue({
|
|
2677
|
+
code: "on_done_unreachable",
|
|
2678
|
+
kind: "reachability",
|
|
2679
|
+
level: "warning",
|
|
2680
|
+
nodeIds: [node.id],
|
|
2681
|
+
edgeIds,
|
|
2682
|
+
message: "onDone transitions require each parallel region to have a final child state",
|
|
2683
|
+
metadata: { reason: "parallel_region_missing_final" }
|
|
2684
|
+
}));
|
|
2685
|
+
else if (regionsWithoutReachableFinal.length > 0) results.push(issue({
|
|
2686
|
+
code: "on_done_unreachable",
|
|
2687
|
+
kind: "reachability",
|
|
2688
|
+
level: "warning",
|
|
2689
|
+
nodeIds: [node.id],
|
|
2690
|
+
edgeIds,
|
|
2691
|
+
message: "onDone transitions will never trigger: a parallel region has no reachable final child state",
|
|
2692
|
+
metadata: { reason: "parallel_region_final_unreachable" }
|
|
2693
|
+
}));
|
|
2694
|
+
} else {
|
|
2695
|
+
const finalChildren = graph.nodes.filter((n) => n.parentId === node.id).filter((child) => child.data.type === "final");
|
|
2696
|
+
if (finalChildren.length === 0) results.push(issue({
|
|
2697
|
+
code: "on_done_unreachable",
|
|
2698
|
+
kind: "reachability",
|
|
2699
|
+
level: "warning",
|
|
2700
|
+
nodeIds: [node.id],
|
|
2701
|
+
edgeIds,
|
|
2702
|
+
message: "onDone transitions require a final child state to trigger",
|
|
2703
|
+
metadata: { reason: "missing_final_child" }
|
|
2704
|
+
}));
|
|
2705
|
+
else if (!finalChildren.some((child) => reachableNodes.has(child.id))) results.push(issue({
|
|
2706
|
+
code: "on_done_unreachable",
|
|
2707
|
+
kind: "reachability",
|
|
2708
|
+
level: "warning",
|
|
2709
|
+
nodeIds: [node.id],
|
|
2710
|
+
edgeIds,
|
|
2711
|
+
message: "onDone transitions will never trigger: no reachable final child state",
|
|
2712
|
+
metadata: { reason: "final_child_unreachable" }
|
|
2713
|
+
}));
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
return results;
|
|
2717
|
+
}
|
|
2718
|
+
function getEdgeErrors(graph, edge) {
|
|
2719
|
+
const errors = [];
|
|
2720
|
+
const duplicates = findDuplicateEdges(graph, edge);
|
|
2721
|
+
if (duplicates.length > 0) errors.push({
|
|
2722
|
+
code: "duplicate_transition",
|
|
2723
|
+
kind: "correctness",
|
|
2724
|
+
message: "Two events with the same name and source state are not allowed",
|
|
2725
|
+
fixes: graph.edges.findIndex((e) => e.id === edge.id) > graph.edges.findIndex((e) => e.id === duplicates[0].id) ? [deleteTransitionFix(edge, "Delete duplicate transition")] : void 0
|
|
2726
|
+
});
|
|
2727
|
+
if (edge.data.eventType.startsWith("xstate.after.")) {
|
|
2728
|
+
const delayPart = edge.data.eventType.replace(/^xstate\.after\./, "").split(".")[0];
|
|
2729
|
+
if (delayPart !== void 0) {
|
|
2730
|
+
const numericValue = Number(delayPart);
|
|
2731
|
+
if (!Number.isNaN(numericValue) && numericValue < 0) errors.push({
|
|
2732
|
+
code: "invalid_delay",
|
|
2733
|
+
kind: "correctness",
|
|
2734
|
+
message: "Delay value cannot be negative",
|
|
2735
|
+
metadata: { delay: numericValue }
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return errors.length > 0 ? errors : null;
|
|
2740
|
+
}
|
|
2741
|
+
function getEdgeWarnings(graph, edge) {
|
|
2742
|
+
const warnings = [];
|
|
2743
|
+
const group = graph.edges.filter((e) => e.sourceId === edge.sourceId && e.data.eventType === edge.data.eventType);
|
|
2744
|
+
if (edge.data.guard && group.length > 1) {
|
|
2745
|
+
const guardType = edge.data.guard.type;
|
|
2746
|
+
const withRepeatedGuard = group.filter((e) => e.data.guard?.type === guardType);
|
|
2747
|
+
if (withRepeatedGuard.length > 1) warnings.push({
|
|
2748
|
+
code: "repeated_guard",
|
|
2749
|
+
kind: "maintainability",
|
|
2750
|
+
message: `Found ${withRepeatedGuard.length} guards with the name \`${guardType}\``,
|
|
2751
|
+
metadata: {
|
|
2752
|
+
guardType,
|
|
2753
|
+
count: withRepeatedGuard.length
|
|
2754
|
+
}
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
const edgeIndex = group.findIndex((e) => e.id === edge.id);
|
|
2758
|
+
if (edgeIndex > 0 && group.slice(0, edgeIndex).some((e) => !e.data.guard) || isTransitionPreempted(graph, edge)) warnings.push({
|
|
2759
|
+
code: "transition_never_taken",
|
|
2760
|
+
kind: "reachability",
|
|
2761
|
+
message: "This transition will never be taken",
|
|
2762
|
+
fixes: edgeIndex > 0 && group.slice(0, edgeIndex).some((e) => !e.data.guard) ? [deleteTransitionFix(edge, "Delete unreachable transition")] : void 0
|
|
2763
|
+
});
|
|
2764
|
+
if (group.length > 1 && !edge.data.guard && edgeIndex < group.length - 1) warnings.push({
|
|
2765
|
+
code: "missing_guard",
|
|
2766
|
+
kind: "correctness",
|
|
2767
|
+
message: "This transition is missing a guard, so transitions after it will never be taken"
|
|
2768
|
+
});
|
|
2769
|
+
if (edgeIndex > 0) {
|
|
2770
|
+
const prevEdge = group[edgeIndex - 1];
|
|
2771
|
+
const sameTarget = prevEdge.targetId === edge.targetId;
|
|
2772
|
+
const sameActions = JSON.stringify(prevEdge.data.actions) === JSON.stringify(edge.data.actions);
|
|
2773
|
+
if (sameTarget && sameActions) warnings.push({
|
|
2774
|
+
code: "redundant_transition",
|
|
2775
|
+
kind: "optimization",
|
|
2776
|
+
message: "Redundant transition: same target and actions as previous transition",
|
|
2777
|
+
fixes: [deleteTransitionFix(edge, "Delete redundant transition")]
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
if (edge.data.eventType.startsWith("xstate.after.")) {
|
|
2781
|
+
const delayPart = edge.data.eventType.replace(/^xstate\.after\./, "").split(".")[0];
|
|
2782
|
+
if (delayPart !== void 0 && Number.isNaN(Number(delayPart))) {
|
|
2783
|
+
if (!getImplementations(graph).delays.find((d) => d.name === delayPart)?.code?.body?.trim()) warnings.push({
|
|
2784
|
+
code: "missing_delay_implementation",
|
|
2785
|
+
kind: "correctness",
|
|
2786
|
+
message: `Missing delay implementation for "${delayPart}"`,
|
|
2787
|
+
metadata: { delay: delayPart }
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const sourceNode = graph.nodes.find((n) => n.id === edge.sourceId);
|
|
2792
|
+
if (sourceNode && sourceNode.data.type === "final") warnings.push({
|
|
2793
|
+
code: "invalid_final_transition",
|
|
2794
|
+
kind: "correctness",
|
|
2795
|
+
message: "Final state cannot transition to other states",
|
|
2796
|
+
fixes: [deleteTransitionFix(edge, "Delete transition from final state")]
|
|
2797
|
+
});
|
|
2798
|
+
if (sourceNode && sourceNode.data.type === "history") warnings.push({
|
|
2799
|
+
code: "invalid_history_transition",
|
|
2800
|
+
kind: "correctness",
|
|
2801
|
+
message: "History state cannot have transitions",
|
|
2802
|
+
fixes: [deleteTransitionFix(edge, "Delete transition from history state")]
|
|
2803
|
+
});
|
|
2804
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.targetId);
|
|
2805
|
+
if (sourceNode && targetNode && sourceNode.parentId === targetNode.parentId && sourceNode.id !== targetNode.id) {
|
|
2806
|
+
const parentNode = graph.nodes.find((n) => n.id === sourceNode.parentId);
|
|
2807
|
+
if (parentNode && parentNode.data.type === "parallel") warnings.push({
|
|
2808
|
+
code: "parallel_region_transition",
|
|
2809
|
+
kind: "correctness",
|
|
2810
|
+
message: "Parallel regions should communicate via events, not transitions"
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
return warnings.length > 0 ? warnings : null;
|
|
2814
|
+
}
|
|
2815
|
+
function getSourceWarnings(graph) {
|
|
2816
|
+
const results = [];
|
|
2817
|
+
const impls = getImplementations(graph);
|
|
2818
|
+
const groups = [
|
|
2819
|
+
{
|
|
2820
|
+
type: "action",
|
|
2821
|
+
items: impls.actions
|
|
2822
|
+
},
|
|
2823
|
+
{
|
|
2824
|
+
type: "guard",
|
|
2825
|
+
items: impls.guards
|
|
2826
|
+
},
|
|
2827
|
+
{
|
|
2828
|
+
type: "actor",
|
|
2829
|
+
items: impls.actors
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
type: "delay",
|
|
2833
|
+
items: impls.delays
|
|
2834
|
+
}
|
|
2835
|
+
];
|
|
2836
|
+
for (const group of groups) {
|
|
2837
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2838
|
+
for (const item of group.items) {
|
|
2839
|
+
if (!item.name) continue;
|
|
2840
|
+
seen.set(item.name, (seen.get(item.name) ?? 0) + 1);
|
|
2841
|
+
}
|
|
2842
|
+
for (const [name, count] of seen) if (count > 1) results.push(issue({
|
|
2843
|
+
code: "duplicate_source_name",
|
|
2844
|
+
kind: "maintainability",
|
|
2845
|
+
level: "warning",
|
|
2846
|
+
message: `Duplicate ${group.type} source name "${name}" (${count} occurrences)`,
|
|
2847
|
+
metadata: {
|
|
2848
|
+
sourceType: group.type,
|
|
2849
|
+
name,
|
|
2850
|
+
count
|
|
2851
|
+
}
|
|
2852
|
+
}));
|
|
2853
|
+
}
|
|
2854
|
+
return results;
|
|
2855
|
+
}
|
|
2856
|
+
function getSourceRestrictionErrors(graph, sourceAllowed) {
|
|
2857
|
+
if (!sourceAllowed) return [];
|
|
2858
|
+
const results = [];
|
|
2859
|
+
const impls = getImplementations(graph);
|
|
2860
|
+
const actionIds = new Set(impls.actions.map((a) => a.id));
|
|
2861
|
+
const guardIds = new Set(impls.guards.map((g) => g.id));
|
|
2862
|
+
const actorIds = new Set(impls.actors.map((a) => a.id));
|
|
2863
|
+
const isInvalidActionType = (type) => {
|
|
2864
|
+
if (!type) return true;
|
|
2865
|
+
if (actionIds.has(type)) return false;
|
|
2866
|
+
if (type.startsWith("xstate.")) return false;
|
|
2867
|
+
if (isAutoGeneratedInlineName(type)) return false;
|
|
2868
|
+
return true;
|
|
2869
|
+
};
|
|
2870
|
+
const formatActionMsg = (type) => type ? `Action "${type}" is not a defined action implementation` : "Action is not selected";
|
|
2871
|
+
if (sourceAllowed.actions === "predefined") {
|
|
2872
|
+
for (const node of graph.nodes) {
|
|
2873
|
+
for (const action of node.data.entry ?? []) if (isInvalidActionType(action.type)) results.push(issue({
|
|
2874
|
+
code: "undefined_action",
|
|
2875
|
+
kind: "correctness",
|
|
2876
|
+
level: "error",
|
|
2877
|
+
nodeIds: [node.id],
|
|
2878
|
+
message: formatActionMsg(action.type)
|
|
2879
|
+
}));
|
|
2880
|
+
for (const action of node.data.exit ?? []) if (isInvalidActionType(action.type)) results.push(issue({
|
|
2881
|
+
code: "undefined_action",
|
|
2882
|
+
kind: "correctness",
|
|
2883
|
+
level: "error",
|
|
2884
|
+
nodeIds: [node.id],
|
|
2885
|
+
message: formatActionMsg(action.type)
|
|
2886
|
+
}));
|
|
2887
|
+
}
|
|
2888
|
+
for (const edge of graph.edges) for (const action of edge.data.actions ?? []) if (isInvalidActionType(action.type)) results.push(issue({
|
|
2889
|
+
code: "undefined_action",
|
|
2890
|
+
kind: "correctness",
|
|
2891
|
+
level: "error",
|
|
2892
|
+
edgeIds: [edge.id],
|
|
2893
|
+
message: formatActionMsg(action.type)
|
|
2894
|
+
}));
|
|
2895
|
+
}
|
|
2896
|
+
if (sourceAllowed.guards === "predefined") for (const edge of graph.edges) {
|
|
2897
|
+
const guard = edge.data.guard;
|
|
2898
|
+
if (!guard) continue;
|
|
2899
|
+
const type = guard.type;
|
|
2900
|
+
if (!type || !guardIds.has(type) && !type.startsWith("inline:") && !isAutoGeneratedInlineName(type)) results.push(issue({
|
|
2901
|
+
code: "undefined_guard",
|
|
2902
|
+
kind: "correctness",
|
|
2903
|
+
level: "error",
|
|
2904
|
+
edgeIds: [edge.id],
|
|
2905
|
+
message: type ? `Guard "${type}" is not a defined guard implementation` : "Guard is not selected"
|
|
2906
|
+
}));
|
|
2907
|
+
}
|
|
2908
|
+
if (sourceAllowed.actors === "predefined") for (const node of graph.nodes) for (const invoke of node.data.invokes ?? []) {
|
|
2909
|
+
const src = invoke.src;
|
|
2910
|
+
if (!src || !actorIds.has(src) && !isAutoGeneratedInlineName(src)) results.push(issue({
|
|
2911
|
+
code: "undefined_actor",
|
|
2912
|
+
kind: "correctness",
|
|
2913
|
+
level: "error",
|
|
2914
|
+
nodeIds: [node.id],
|
|
2915
|
+
message: src ? `Actor "${src}" is not a defined actor implementation` : "Actor is not selected"
|
|
2916
|
+
}));
|
|
2917
|
+
}
|
|
2918
|
+
return results;
|
|
2919
|
+
}
|
|
2920
|
+
function pushMessages(results, messages, level, nodeIds, edgeIds) {
|
|
2921
|
+
messages?.forEach((message) => {
|
|
2922
|
+
results.push(issue({
|
|
2923
|
+
...message,
|
|
2924
|
+
level,
|
|
2925
|
+
nodeIds,
|
|
2926
|
+
edgeIds
|
|
2927
|
+
}));
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
function validateGraph(graph, sourceAllowed) {
|
|
2931
|
+
const results = [];
|
|
2932
|
+
const nonTempNodes = graph.nodes.filter((n) => !n.data?.temp);
|
|
2933
|
+
const nonTempEdges = graph.edges.filter((e) => !e.data?.temp);
|
|
2934
|
+
const nonTempGraph = {
|
|
2935
|
+
...graph,
|
|
2936
|
+
nodes: nonTempNodes,
|
|
2937
|
+
edges: nonTempEdges
|
|
2938
|
+
};
|
|
2939
|
+
const reachableNodes = computeReachableGraphNodes(nonTempGraph);
|
|
2940
|
+
for (const node of nonTempNodes) {
|
|
2941
|
+
pushMessages(results, getNodeErrors(nonTempGraph, node), "error", [node.id], []);
|
|
2942
|
+
pushMessages(results, getNodeWarningsWithReachability(nonTempGraph, node, reachableNodes), "warning", [node.id], []);
|
|
2943
|
+
}
|
|
2944
|
+
for (const edge of nonTempEdges) {
|
|
2945
|
+
pushMessages(results, getEdgeErrors(nonTempGraph, edge), "error", [], [edge.id]);
|
|
2946
|
+
pushMessages(results, getEdgeWarnings(nonTempGraph, edge), "warning", [], [edge.id]);
|
|
2947
|
+
}
|
|
2948
|
+
results.push(...getOnDoneWarnings(nonTempGraph, reachableNodes));
|
|
2949
|
+
results.push(...getSourceWarnings(nonTempGraph));
|
|
2950
|
+
results.push(...getSourceRestrictionErrors(nonTempGraph, sourceAllowed));
|
|
2951
|
+
return results;
|
|
2952
|
+
}
|
|
2953
|
+
function validationOk(issues) {
|
|
2954
|
+
return !issues.some((item) => item.level === "error");
|
|
2955
|
+
}
|
|
2956
|
+
const validationIssueTargetSchema = object({
|
|
2957
|
+
nodeIds: array(string()).optional(),
|
|
2958
|
+
edgeIds: array(string()).optional(),
|
|
2959
|
+
source: object({
|
|
2960
|
+
path: string().optional(),
|
|
2961
|
+
start: number().optional(),
|
|
2962
|
+
end: number().optional()
|
|
2963
|
+
}).optional()
|
|
2964
|
+
});
|
|
2965
|
+
const validationFixSchema = object({
|
|
2966
|
+
kind: validationFixKindSchema,
|
|
2967
|
+
source: validationFixSourceSchema,
|
|
2968
|
+
title: string(),
|
|
2969
|
+
patches: array(unknown()).optional(),
|
|
2970
|
+
output: unknown().optional(),
|
|
2971
|
+
diff: unknown().optional(),
|
|
2972
|
+
confidence: number().optional(),
|
|
2973
|
+
rationale: string().optional(),
|
|
2974
|
+
rank: number().optional(),
|
|
2975
|
+
postValidation: object({
|
|
2976
|
+
ok: boolean(),
|
|
2977
|
+
remainingIssueCodes: array(validationIssueCodeSchema)
|
|
2978
|
+
}).optional()
|
|
2979
|
+
});
|
|
2980
|
+
const validationIssueSchema = object({
|
|
2981
|
+
code: validationIssueCodeSchema,
|
|
2982
|
+
kind: validationIssueKindSchema,
|
|
2983
|
+
level: validationLevelSchema,
|
|
2984
|
+
message: string(),
|
|
2985
|
+
target: validationIssueTargetSchema,
|
|
2986
|
+
metadata: record(string(), unknown()).optional(),
|
|
2987
|
+
fixes: array(validationFixSchema).optional(),
|
|
2988
|
+
source: _enum(["deterministic", "llm"]).optional(),
|
|
2989
|
+
nodeIds: array(string()),
|
|
2990
|
+
edgeIds: array(string())
|
|
2991
|
+
});
|
|
2992
|
+
const machineFormatSchema = _enum([
|
|
2993
|
+
"xstate",
|
|
2994
|
+
"json",
|
|
2995
|
+
"yaml",
|
|
2996
|
+
"scxml",
|
|
2997
|
+
"mermaid"
|
|
2998
|
+
]);
|
|
2999
|
+
const xstateSourceProfileSchema = _enum([
|
|
3000
|
+
"auto",
|
|
3001
|
+
"xstate-v5",
|
|
3002
|
+
"xstate-v6-alpha"
|
|
3003
|
+
]);
|
|
3004
|
+
const machineInputSchema = object({
|
|
3005
|
+
machine: union([record(string(), unknown()), string()]),
|
|
3006
|
+
format: machineFormatSchema.optional(),
|
|
3007
|
+
profile: xstateSourceProfileSchema.optional(),
|
|
3008
|
+
xstateVersion: union([literal(5), literal(6)]).optional()
|
|
3009
|
+
});
|
|
3010
|
+
const graphSchema = record(string(), unknown());
|
|
3011
|
+
function asError(error) {
|
|
3012
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
3013
|
+
}
|
|
3014
|
+
function simImplementations(graph, guardState) {
|
|
3015
|
+
const guards = {};
|
|
3016
|
+
for (const edge of graph.edges) if (edge.data.guard) {
|
|
3017
|
+
const guardType = edge.data.guard.type;
|
|
3018
|
+
guards[guardType] = ({ event }) => {
|
|
3019
|
+
if (event["@xstate.guard"] === guardType) return true;
|
|
3020
|
+
return guardState?.[guardType] ?? false;
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
return { guards };
|
|
3024
|
+
}
|
|
3025
|
+
const operations = {
|
|
3026
|
+
validate_machine: {
|
|
3027
|
+
description: "Validate a machine without writing files.",
|
|
3028
|
+
input: machineInputSchema,
|
|
3029
|
+
output: object({
|
|
3030
|
+
ok: boolean(),
|
|
3031
|
+
issues: array(validationIssueSchema)
|
|
3032
|
+
}),
|
|
3033
|
+
async handler(input) {
|
|
3034
|
+
try {
|
|
3035
|
+
const issues = validateGraph(machineInputToGraph(input));
|
|
3036
|
+
return {
|
|
3037
|
+
ok: validationOk(issues),
|
|
3038
|
+
issues
|
|
3039
|
+
};
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
return {
|
|
3042
|
+
ok: false,
|
|
3043
|
+
issues: [{
|
|
3044
|
+
code: "parse_error",
|
|
3045
|
+
kind: "correctness",
|
|
3046
|
+
level: "error",
|
|
3047
|
+
message: asError(error).message,
|
|
3048
|
+
target: {
|
|
3049
|
+
nodeIds: [],
|
|
3050
|
+
edgeIds: []
|
|
3051
|
+
},
|
|
3052
|
+
source: "deterministic",
|
|
3053
|
+
nodeIds: [],
|
|
3054
|
+
edgeIds: []
|
|
3055
|
+
}]
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
},
|
|
3060
|
+
convert_to_graph: {
|
|
3061
|
+
description: "Convert a machine into an internal graph for patching.",
|
|
3062
|
+
input: machineInputSchema,
|
|
3063
|
+
output: object({ graph: graphSchema }),
|
|
3064
|
+
async handler(input) {
|
|
3065
|
+
return { graph: machineInputToGraph(input) };
|
|
3066
|
+
}
|
|
3067
|
+
},
|
|
3068
|
+
convert_format: {
|
|
3069
|
+
description: "Convert a machine or graph to a machine format.",
|
|
3070
|
+
input: object({
|
|
3071
|
+
machine: union([record(string(), unknown()), string()]).optional(),
|
|
3072
|
+
graph: graphSchema.optional(),
|
|
3073
|
+
format: machineFormatSchema.optional(),
|
|
3074
|
+
to: machineFormatSchema
|
|
3075
|
+
}).refine((value) => Boolean(value.machine) !== Boolean(value.graph), { message: "Provide exactly one of machine or graph." }),
|
|
3076
|
+
output: object({
|
|
3077
|
+
format: machineFormatSchema,
|
|
3078
|
+
output: union([record(string(), unknown()), string()])
|
|
3079
|
+
}),
|
|
3080
|
+
async handler(input) {
|
|
3081
|
+
const graph = input.graph ?? machineInputToGraph({
|
|
3082
|
+
machine: input.machine,
|
|
3083
|
+
format: input.format
|
|
3084
|
+
});
|
|
3085
|
+
return {
|
|
3086
|
+
format: input.to,
|
|
3087
|
+
output: graphToFormat(graph, input.to)
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
},
|
|
3091
|
+
apply_patch: {
|
|
3092
|
+
description: "Apply semantic machine patches to a graph without writing files.",
|
|
3093
|
+
input: object({
|
|
3094
|
+
graph: graphSchema,
|
|
3095
|
+
patches: array(machinePatchSchema),
|
|
3096
|
+
outputFormat: machineFormatSchema.default("json")
|
|
3097
|
+
}),
|
|
3098
|
+
output: union([object({
|
|
3099
|
+
ok: literal(true),
|
|
3100
|
+
graph: graphSchema,
|
|
3101
|
+
machine: union([record(string(), unknown()), string()]),
|
|
3102
|
+
format: machineFormatSchema,
|
|
3103
|
+
diff: unknown(),
|
|
3104
|
+
patches: array(machinePatchSchema)
|
|
3105
|
+
}), object({
|
|
3106
|
+
ok: literal(false),
|
|
3107
|
+
reason: string(),
|
|
3108
|
+
errors: array(object({ message: string() }))
|
|
3109
|
+
})]),
|
|
3110
|
+
async handler(input) {
|
|
3111
|
+
try {
|
|
3112
|
+
const graph = applyGraphPatches(input.graph, input.patches);
|
|
3113
|
+
createMachine(graphToMachineConfig(graph));
|
|
3114
|
+
const diff = diffGraphsSemantically(input.graph, graph).diff;
|
|
3115
|
+
return {
|
|
3116
|
+
ok: true,
|
|
3117
|
+
graph,
|
|
3118
|
+
machine: graphToFormat(graph, input.outputFormat),
|
|
3119
|
+
format: input.outputFormat,
|
|
3120
|
+
diff,
|
|
3121
|
+
patches: input.patches
|
|
3122
|
+
};
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
return {
|
|
3125
|
+
ok: false,
|
|
3126
|
+
reason: "validation",
|
|
3127
|
+
errors: [asError(error)]
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
},
|
|
3132
|
+
diff_machines: {
|
|
3133
|
+
description: "Compute semantic diff between two machines.",
|
|
3134
|
+
input: object({
|
|
3135
|
+
left: machineInputSchema,
|
|
3136
|
+
right: machineInputSchema,
|
|
3137
|
+
includePatches: boolean().optional()
|
|
3138
|
+
}),
|
|
3139
|
+
output: object({
|
|
3140
|
+
diff: unknown(),
|
|
3141
|
+
patches: array(machinePatchSchema).optional()
|
|
3142
|
+
}),
|
|
3143
|
+
async handler(input) {
|
|
3144
|
+
return { diff: diffGraphsSemantically(machineInputToGraph(input.left), machineInputToGraph(input.right)).diff };
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
generate_paths: {
|
|
3148
|
+
description: "Generate paths through a machine.",
|
|
3149
|
+
input: machineInputSchema.extend({
|
|
3150
|
+
targetPath: array(string()).optional(),
|
|
3151
|
+
kind: _enum(["shortest", "simple"]).default("shortest"),
|
|
3152
|
+
max: number().int().positive().optional()
|
|
3153
|
+
}),
|
|
3154
|
+
output: object({ paths: array(unknown()) }),
|
|
3155
|
+
async handler(input) {
|
|
3156
|
+
const result = generateGraphPathsData(machineInputToGraph(input), {
|
|
3157
|
+
strategy: input.kind,
|
|
3158
|
+
reduceDuplicates: false,
|
|
3159
|
+
preferTransitionCoverage: false,
|
|
3160
|
+
limit: input.max
|
|
3161
|
+
});
|
|
3162
|
+
return { paths: input.targetPath ? result.paths.filter((path) => path.finalStatePath === input.targetPath.join(".")) : result.paths };
|
|
3163
|
+
}
|
|
3164
|
+
},
|
|
3165
|
+
simulate: {
|
|
3166
|
+
description: "Simulate one event from a machine state.",
|
|
3167
|
+
input: machineInputSchema.extend({
|
|
3168
|
+
stateValue: unknown(),
|
|
3169
|
+
event: object({ type: string() }).passthrough(),
|
|
3170
|
+
guardState: record(string(), boolean()).optional()
|
|
3171
|
+
}),
|
|
3172
|
+
output: object({
|
|
3173
|
+
nextStateValue: unknown(),
|
|
3174
|
+
actions: array(unknown()),
|
|
3175
|
+
changed: boolean(),
|
|
3176
|
+
microsteps: array(unknown()).optional()
|
|
3177
|
+
}),
|
|
3178
|
+
async handler(input) {
|
|
3179
|
+
const graph = machineInputToGraph(input);
|
|
3180
|
+
const machine = createMachine(graphToMachineConfig(graph)).provide(simImplementations(graph, input.guardState));
|
|
3181
|
+
const snapshot = machine.resolveState({
|
|
3182
|
+
value: input.stateValue,
|
|
3183
|
+
context: {},
|
|
3184
|
+
status: "active"
|
|
3185
|
+
});
|
|
3186
|
+
const [nextSnapshot, actions] = transition(machine, snapshot, input.event);
|
|
3187
|
+
return {
|
|
3188
|
+
nextStateValue: nextSnapshot.value,
|
|
3189
|
+
actions,
|
|
3190
|
+
changed: JSON.stringify(nextSnapshot.value) !== JSON.stringify(snapshot.value)
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
};
|
|
3195
|
+
|
|
110
3196
|
//#endregion
|
|
111
3197
|
//#region ../editor-sync/src/internal/sourceLocations.ts
|
|
112
3198
|
const sourceFileUris = /* @__PURE__ */ new WeakMap();
|
|
113
3199
|
function collectXStateSourceLocations(sourceText, document, options = {}) {
|
|
114
|
-
const sourceFile = ts$1.createSourceFile(document.fileName, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind$
|
|
3200
|
+
const sourceFile = ts$1.createSourceFile(document.fileName, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind$2(document.fileName));
|
|
115
3201
|
const uri = document.uri?.toString() ?? document.fileName;
|
|
116
3202
|
sourceFileUris.set(sourceFile, uri);
|
|
117
3203
|
const bindings = collectBindings(sourceFile, uri, document.fileName, options);
|
|
@@ -119,13 +3205,18 @@ function collectXStateSourceLocations(sourceText, document, options = {}) {
|
|
|
119
3205
|
const root = findMachineConfigObjectLiteral(sourceFile, bindings);
|
|
120
3206
|
if (!root) return;
|
|
121
3207
|
const states = [];
|
|
122
|
-
|
|
3208
|
+
const statesObjectLocations = [];
|
|
3209
|
+
collectStateLocations(root, [], states, statesObjectLocations, bindings, staticValues, root);
|
|
3210
|
+
const rootProfile = detectXStateSourceProfile(sourceText);
|
|
3211
|
+
const referencedStateSymbols = new Set([...states, ...statesObjectLocations].map((state) => state.symbol).filter((symbol) => typeof symbol === "string"));
|
|
123
3212
|
return {
|
|
124
3213
|
version: 1,
|
|
3214
|
+
profile: rootProfile === "xstate-v6-alpha" || [...referencedStateSymbols].some((symbol) => detectXStateSourceProfile(bindings.get(symbol)?.sourceFile.text ?? "") === "xstate-v6-alpha") ? "xstate-v6-alpha" : rootProfile,
|
|
125
3215
|
root: {
|
|
126
3216
|
uri,
|
|
127
|
-
range: toSourceRange(sourceFile, root)
|
|
3217
|
+
range: toSourceRange$1(sourceFile, root)
|
|
128
3218
|
},
|
|
3219
|
+
statesObject: statesObjectLocations[0],
|
|
129
3220
|
states,
|
|
130
3221
|
staticValues: collectStaticValueReferences(sourceFile, uri, root, staticValues)
|
|
131
3222
|
};
|
|
@@ -143,7 +3234,7 @@ function collectStaticValueBindings(sourceFile) {
|
|
|
143
3234
|
kind: "const",
|
|
144
3235
|
symbol: declaration.name.text,
|
|
145
3236
|
value,
|
|
146
|
-
valueRange: toSourceRange(sourceFile, declaration.initializer)
|
|
3237
|
+
valueRange: toSourceRange$1(sourceFile, declaration.initializer)
|
|
147
3238
|
});
|
|
148
3239
|
}
|
|
149
3240
|
continue;
|
|
@@ -158,15 +3249,18 @@ function collectStaticValueBindings(sourceFile) {
|
|
|
158
3249
|
kind: "enumMember",
|
|
159
3250
|
symbol,
|
|
160
3251
|
value,
|
|
161
|
-
valueRange: toSourceRange(sourceFile, member.initializer)
|
|
3252
|
+
valueRange: toSourceRange$1(sourceFile, member.initializer)
|
|
162
3253
|
});
|
|
163
3254
|
}
|
|
164
3255
|
}
|
|
165
3256
|
return bindings;
|
|
166
3257
|
}
|
|
167
|
-
function collectBindings(sourceFile, uri, fileName, options) {
|
|
3258
|
+
function collectBindings(sourceFile, uri, fileName, options, cache = /* @__PURE__ */ new Map()) {
|
|
3259
|
+
const cached = cache.get(fileName);
|
|
3260
|
+
if (cached) return cached;
|
|
168
3261
|
const bindings = collectTopLevelBindings(sourceFile, uri);
|
|
169
|
-
|
|
3262
|
+
cache.set(fileName, bindings);
|
|
3263
|
+
addImportedBindings(bindings, sourceFile, uri, fileName, options, cache);
|
|
170
3264
|
return bindings;
|
|
171
3265
|
}
|
|
172
3266
|
function collectTopLevelBindings(sourceFile, uri) {
|
|
@@ -198,7 +3292,7 @@ function collectTopLevelBindings(sourceFile, uri) {
|
|
|
198
3292
|
}
|
|
199
3293
|
return bindings;
|
|
200
3294
|
}
|
|
201
|
-
function addImportedBindings(bindings, sourceFile, uri, fileName, options) {
|
|
3295
|
+
function addImportedBindings(bindings, sourceFile, uri, fileName, options, cache) {
|
|
202
3296
|
const resolver = options.resolver;
|
|
203
3297
|
if (!resolver) return;
|
|
204
3298
|
for (const statement of sourceFile.statements) {
|
|
@@ -210,9 +3304,9 @@ function addImportedBindings(bindings, sourceFile, uri, fileName, options) {
|
|
|
210
3304
|
uri
|
|
211
3305
|
}, statement.moduleSpecifier.text);
|
|
212
3306
|
if (!importedDocument) continue;
|
|
213
|
-
const importedSourceFile = ts$1.createSourceFile(importedDocument.fileName, importedDocument.text, ts$1.ScriptTarget.Latest, true, getScriptKind$
|
|
3307
|
+
const importedSourceFile = ts$1.createSourceFile(importedDocument.fileName, importedDocument.text, ts$1.ScriptTarget.Latest, true, getScriptKind$2(importedDocument.fileName));
|
|
214
3308
|
sourceFileUris.set(importedSourceFile, importedDocument.uri);
|
|
215
|
-
const importedBindings = collectBindings(importedSourceFile, importedDocument.uri, importedDocument.fileName, options);
|
|
3309
|
+
const importedBindings = collectBindings(importedSourceFile, importedDocument.uri, importedDocument.fileName, options, cache);
|
|
216
3310
|
for (const element of namedBindings.elements) {
|
|
217
3311
|
const importedName = element.propertyName?.text ?? element.name.text;
|
|
218
3312
|
const binding = importedBindings.get(importedName);
|
|
@@ -235,11 +3329,20 @@ function findMachineConfigObjectLiteral(sourceFile, bindings) {
|
|
|
235
3329
|
visit(sourceFile);
|
|
236
3330
|
return found;
|
|
237
3331
|
}
|
|
238
|
-
function collectStateLocations(stateObject, parentPath, locations, bindings, staticValues, root) {
|
|
239
|
-
const statesObject =
|
|
3332
|
+
function collectStateLocations(stateObject, parentPath, locations, statesObjectLocations, bindings, staticValues, root) {
|
|
3333
|
+
const statesObject = resolveObjectLiteralProperty(stateObject, "states", bindings);
|
|
240
3334
|
if (!statesObject) return;
|
|
241
|
-
|
|
242
|
-
|
|
3335
|
+
statesObjectLocations.push({
|
|
3336
|
+
uri: statesObject.uri,
|
|
3337
|
+
path: parentPath,
|
|
3338
|
+
kind: statesObject.kind,
|
|
3339
|
+
symbol: statesObject.symbol,
|
|
3340
|
+
keyRange: statesObject.keyRange,
|
|
3341
|
+
referenceRange: statesObject.referenceRange,
|
|
3342
|
+
range: toSourceRange$1(statesObject.sourceFile, statesObject.object)
|
|
3343
|
+
});
|
|
3344
|
+
for (const property of statesObject.object.properties) {
|
|
3345
|
+
const resolved = resolveStateProperty(property, statesObject.bindings, staticValues, root);
|
|
243
3346
|
if (!resolved) continue;
|
|
244
3347
|
const path = [...parentPath, resolved.key];
|
|
245
3348
|
locations.push({
|
|
@@ -251,9 +3354,9 @@ function collectStateLocations(stateObject, parentPath, locations, bindings, sta
|
|
|
251
3354
|
referenceRange: resolved.referenceRange,
|
|
252
3355
|
keySource: resolved.keySource,
|
|
253
3356
|
keyReplacementText: resolved.keyReplacementText,
|
|
254
|
-
range: toSourceRange(resolved.sourceFile, resolved.stateObject)
|
|
3357
|
+
range: toSourceRange$1(resolved.sourceFile, resolved.stateObject)
|
|
255
3358
|
});
|
|
256
|
-
collectStateLocations(resolved.stateObject, path, locations, resolved.bindings, staticValues, root);
|
|
3359
|
+
collectStateLocations(resolved.stateObject, path, locations, statesObjectLocations, resolved.bindings, staticValues, root);
|
|
257
3360
|
}
|
|
258
3361
|
}
|
|
259
3362
|
function resolveStateProperty(property, bindings, staticValues, root) {
|
|
@@ -264,8 +3367,8 @@ function resolveStateProperty(property, bindings, staticValues, root) {
|
|
|
264
3367
|
key: property.name.text,
|
|
265
3368
|
kind: binding.kind,
|
|
266
3369
|
symbol: property.name.text,
|
|
267
|
-
keyRange: toSourceRange(property.getSourceFile(), property.name),
|
|
268
|
-
referenceRange: toSourceRange(property.getSourceFile(), property.name),
|
|
3370
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
3371
|
+
referenceRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
269
3372
|
stateObject: binding.node,
|
|
270
3373
|
sourceFile: binding.sourceFile,
|
|
271
3374
|
uri: binding.uri,
|
|
@@ -279,7 +3382,7 @@ function resolveStateProperty(property, bindings, staticValues, root) {
|
|
|
279
3382
|
if (inlineObject) return {
|
|
280
3383
|
key: resolvedKey.key,
|
|
281
3384
|
kind: "inline",
|
|
282
|
-
keyRange: toSourceRange(property.getSourceFile(), property.name),
|
|
3385
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
283
3386
|
keySource: resolvedKey.keySource,
|
|
284
3387
|
keyReplacementText: resolvedKey.keyReplacementText,
|
|
285
3388
|
stateObject: inlineObject,
|
|
@@ -294,8 +3397,8 @@ function resolveStateProperty(property, bindings, staticValues, root) {
|
|
|
294
3397
|
key: resolvedKey.key,
|
|
295
3398
|
kind: binding.kind,
|
|
296
3399
|
symbol: property.initializer.text,
|
|
297
|
-
keyRange: toSourceRange(property.getSourceFile(), property.name),
|
|
298
|
-
referenceRange: toSourceRange(property.getSourceFile(), property.initializer),
|
|
3400
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
3401
|
+
referenceRange: toSourceRange$1(property.getSourceFile(), property.initializer),
|
|
299
3402
|
keySource: resolvedKey.keySource,
|
|
300
3403
|
keyReplacementText: resolvedKey.keyReplacementText,
|
|
301
3404
|
stateObject: binding.node,
|
|
@@ -327,11 +3430,34 @@ function resolveIdentifierObject(expression, bindings) {
|
|
|
327
3430
|
const binding = bindings.get(unwrapped.text);
|
|
328
3431
|
return binding?.kind === "objectReference" ? binding.node : null;
|
|
329
3432
|
}
|
|
330
|
-
function
|
|
3433
|
+
function resolveObjectLiteralProperty(objectLiteral, propertyName, bindings) {
|
|
331
3434
|
for (const property of objectLiteral.properties) {
|
|
332
3435
|
if (!ts$1.isPropertyAssignment(property)) continue;
|
|
333
3436
|
if (getPropertyNameText$1(property.name) !== propertyName) continue;
|
|
334
|
-
|
|
3437
|
+
const inlineObject = unwrapObjectLiteralExpression(property.initializer);
|
|
3438
|
+
if (inlineObject) return {
|
|
3439
|
+
object: inlineObject,
|
|
3440
|
+
kind: "inline",
|
|
3441
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
3442
|
+
sourceFile: property.getSourceFile(),
|
|
3443
|
+
uri: getSourceFileUri(property.getSourceFile()),
|
|
3444
|
+
bindings
|
|
3445
|
+
};
|
|
3446
|
+
if (ts$1.isIdentifier(property.initializer)) {
|
|
3447
|
+
const binding = bindings.get(property.initializer.text);
|
|
3448
|
+
if (binding?.kind !== "objectReference") return null;
|
|
3449
|
+
return {
|
|
3450
|
+
object: binding.node,
|
|
3451
|
+
kind: binding.kind,
|
|
3452
|
+
symbol: property.initializer.text,
|
|
3453
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
3454
|
+
referenceRange: toSourceRange$1(property.getSourceFile(), property.initializer),
|
|
3455
|
+
sourceFile: binding.sourceFile,
|
|
3456
|
+
uri: binding.uri,
|
|
3457
|
+
bindings: binding.bindings
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
return null;
|
|
335
3461
|
}
|
|
336
3462
|
return null;
|
|
337
3463
|
}
|
|
@@ -383,7 +3509,7 @@ function collectStaticValueReferences(sourceFile, uri, root, staticValues) {
|
|
|
383
3509
|
const binding = getStaticValueBindingForExpression(node.expression, staticValues);
|
|
384
3510
|
if (binding && !isStateKeyComputedPropertyName(node)) references.push({
|
|
385
3511
|
uri,
|
|
386
|
-
range: toSourceRange(sourceFile, node),
|
|
3512
|
+
range: toSourceRange$1(sourceFile, node),
|
|
387
3513
|
value: binding.value
|
|
388
3514
|
});
|
|
389
3515
|
return;
|
|
@@ -396,7 +3522,7 @@ function collectStaticValueReferences(sourceFile, uri, root, staticValues) {
|
|
|
396
3522
|
if (binding) {
|
|
397
3523
|
references.push({
|
|
398
3524
|
uri,
|
|
399
|
-
range: toSourceRange(sourceFile, node),
|
|
3525
|
+
range: toSourceRange$1(sourceFile, node),
|
|
400
3526
|
value: binding.value
|
|
401
3527
|
});
|
|
402
3528
|
return;
|
|
@@ -478,7 +3604,7 @@ function unwrapExpression$1(expression) {
|
|
|
478
3604
|
return current;
|
|
479
3605
|
}
|
|
480
3606
|
}
|
|
481
|
-
function toSourceRange(sourceFile, node) {
|
|
3607
|
+
function toSourceRange$1(sourceFile, node) {
|
|
482
3608
|
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
483
3609
|
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
484
3610
|
return {
|
|
@@ -488,7 +3614,7 @@ function toSourceRange(sourceFile, node) {
|
|
|
488
3614
|
endChar: end.character
|
|
489
3615
|
};
|
|
490
3616
|
}
|
|
491
|
-
function getScriptKind$
|
|
3617
|
+
function getScriptKind$2(fileName) {
|
|
492
3618
|
if (fileName.endsWith(".tsx")) return ts$1.ScriptKind.TSX;
|
|
493
3619
|
if (fileName.endsWith(".jsx")) return ts$1.ScriptKind.JSX;
|
|
494
3620
|
if (fileName.endsWith(".js")) return ts$1.ScriptKind.JS;
|
|
@@ -512,7 +3638,7 @@ function parseSourceToMachine(text, document, options = {}) {
|
|
|
512
3638
|
range: location.range,
|
|
513
3639
|
newText: JSON.stringify(location.value)
|
|
514
3640
|
}));
|
|
515
|
-
const stateReplacements = sourceLocations.states.filter((location) => location.path.length === 1 && location.keyRange).flatMap((location) => {
|
|
3641
|
+
const stateReplacements = sourceLocations.states.filter((location) => location.path.length === 1 && location.keyRange && !sourceLocations.statesObject?.referenceRange).flatMap((location) => {
|
|
516
3642
|
const keyRange = location.keyRange;
|
|
517
3643
|
const keyReplacement = location.keyReplacementText ? [{
|
|
518
3644
|
range: keyRange,
|
|
@@ -530,7 +3656,93 @@ function parseSourceToMachine(text, document, options = {}) {
|
|
|
530
3656
|
newText: `${location.path[0]}: ${stateText}`
|
|
531
3657
|
}];
|
|
532
3658
|
});
|
|
533
|
-
|
|
3659
|
+
const statesObjectReplacements = sourceLocations.statesObject?.referenceRange && sourceLocations.statesObject.kind !== "inline" ? (() => {
|
|
3660
|
+
const statesText = readHydratedStatesObject(sourceLocations.statesObject.uri, sourceLocations.statesObject.range, sourceLocations.states, options.resolver);
|
|
3661
|
+
return statesText ? [{
|
|
3662
|
+
range: sourceLocations.statesObject.referenceRange,
|
|
3663
|
+
newText: statesText
|
|
3664
|
+
}] : [];
|
|
3665
|
+
})() : [];
|
|
3666
|
+
const importReplacements = collectHydratedImportReplacements(text, document, new Set([...sourceLocations.states.filter((location) => location.path.length === 1 && location.referenceRange).map((location) => location.symbol).filter((symbol) => typeof symbol === "string"), ...sourceLocations.statesObject?.symbol ? [sourceLocations.statesObject.symbol] : []]));
|
|
3667
|
+
const hydratedText = applySourceRangeReplacements(text, [
|
|
3668
|
+
...staticValueReplacements,
|
|
3669
|
+
...stateReplacements,
|
|
3670
|
+
...statesObjectReplacements,
|
|
3671
|
+
...importReplacements
|
|
3672
|
+
].filter(removeOverlappingRanges).sort(compareSourceRangesDescending));
|
|
3673
|
+
if (sourceLocations.profile === "xstate-v6-alpha") return `import { createMachine } from "xstate";
|
|
3674
|
+
|
|
3675
|
+
export const machine = createMachine(${serializeJS(xstateSourceToConfig(hydratedText, { profile: "xstate-v6-alpha" }), 0)});
|
|
3676
|
+
`;
|
|
3677
|
+
return hydratedText;
|
|
3678
|
+
}
|
|
3679
|
+
function readHydratedStatesObject(uri, range, states, resolver) {
|
|
3680
|
+
if (!resolver) return null;
|
|
3681
|
+
const document = resolver.read(uri);
|
|
3682
|
+
if (!document) return null;
|
|
3683
|
+
const replacements = states.filter((state) => state.path.length === 1 && state.referenceRange).flatMap((state) => {
|
|
3684
|
+
const stateText = readSourceRange(state.uri, state.range, resolver);
|
|
3685
|
+
if (!stateText || !state.referenceRange) return [];
|
|
3686
|
+
const isShorthand = state.keyRange && areSourceRangesEqual(state.referenceRange, state.keyRange);
|
|
3687
|
+
return [{
|
|
3688
|
+
range: state.referenceRange,
|
|
3689
|
+
newText: isShorthand ? `${state.path[0]}: ${stateText}` : stateText
|
|
3690
|
+
}];
|
|
3691
|
+
});
|
|
3692
|
+
return applySourceRangeReplacementsToSlice(document.text, range, replacements);
|
|
3693
|
+
}
|
|
3694
|
+
function applySourceRangeReplacementsToSlice(text, range, replacements) {
|
|
3695
|
+
const sliceStart = offsetAtRangePosition(text, range.startLine, range.startChar);
|
|
3696
|
+
const sliceEnd = offsetAtRangePosition(text, range.endLine, range.endChar);
|
|
3697
|
+
let updated = text.slice(sliceStart, sliceEnd);
|
|
3698
|
+
for (const replacement of replacements.sort(compareSourceRangesDescending)) {
|
|
3699
|
+
const start = offsetAtRangePosition(text, replacement.range.startLine, replacement.range.startChar);
|
|
3700
|
+
const end = offsetAtRangePosition(text, replacement.range.endLine, replacement.range.endChar);
|
|
3701
|
+
if (start < sliceStart || end > sliceEnd) continue;
|
|
3702
|
+
const localStart = start - sliceStart;
|
|
3703
|
+
const localEnd = end - sliceStart;
|
|
3704
|
+
updated = `${updated.slice(0, localStart)}${replacement.newText}${updated.slice(localEnd)}`;
|
|
3705
|
+
}
|
|
3706
|
+
return updated;
|
|
3707
|
+
}
|
|
3708
|
+
function collectHydratedImportReplacements(text, document, hydratedSymbols) {
|
|
3709
|
+
if (hydratedSymbols.size === 0) return [];
|
|
3710
|
+
const sourceFile = ts$1.createSourceFile(document.fileName, text, ts$1.ScriptTarget.Latest, true, getScriptKind$1(document.fileName));
|
|
3711
|
+
const replacements = [];
|
|
3712
|
+
for (const statement of sourceFile.statements) {
|
|
3713
|
+
if (!ts$1.isImportDeclaration(statement) || !ts$1.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
3714
|
+
if (!statement.moduleSpecifier.text.startsWith(".")) continue;
|
|
3715
|
+
const namedBindings = statement.importClause?.namedBindings;
|
|
3716
|
+
if (!namedBindings || !ts$1.isNamedImports(namedBindings)) continue;
|
|
3717
|
+
const importedNames = namedBindings.elements.map((element) => element.name.text);
|
|
3718
|
+
if (importedNames.length === 0 || !importedNames.every((name) => hydratedSymbols.has(name))) continue;
|
|
3719
|
+
replacements.push({
|
|
3720
|
+
range: toSourceRange(sourceFile, statement, { includeTrailingNewline: true }),
|
|
3721
|
+
newText: ""
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
return replacements;
|
|
3725
|
+
}
|
|
3726
|
+
function toSourceRange(sourceFile, node, options = {}) {
|
|
3727
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
3728
|
+
let endOffset = node.getEnd();
|
|
3729
|
+
if (options.includeTrailingNewline) {
|
|
3730
|
+
const match = sourceFile.text.slice(endOffset).match(/^\r?\n/);
|
|
3731
|
+
if (match) endOffset += match[0].length;
|
|
3732
|
+
}
|
|
3733
|
+
const end = sourceFile.getLineAndCharacterOfPosition(endOffset);
|
|
3734
|
+
return {
|
|
3735
|
+
startLine: start.line,
|
|
3736
|
+
startChar: start.character,
|
|
3737
|
+
endLine: end.line,
|
|
3738
|
+
endChar: end.character
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
function getScriptKind$1(fileName) {
|
|
3742
|
+
if (fileName.endsWith(".tsx")) return ts$1.ScriptKind.TSX;
|
|
3743
|
+
if (fileName.endsWith(".jsx")) return ts$1.ScriptKind.JSX;
|
|
3744
|
+
if (fileName.endsWith(".js")) return ts$1.ScriptKind.JS;
|
|
3745
|
+
return ts$1.ScriptKind.TS;
|
|
534
3746
|
}
|
|
535
3747
|
function removeOverlappingRanges(replacement, index, replacements) {
|
|
536
3748
|
return !replacements.some((candidate, candidateIndex) => {
|