@statelyai/sdk 0.10.1 → 0.10.2
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 +97 -13
- package/dist/{graph-DmXh22Zu.d.mts → graph-GeuH-mFK.d.mts} +13 -2
- package/dist/graph.d.mts +1 -1
- package/dist/graphToXStateTS-DOaLOySy.mjs +5486 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.mjs +1 -1
- package/dist/protocol.d.mts +11 -0
- package/dist/sync.d.mts +1 -1
- package/dist/sync.mjs +3225 -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-DOaLOySy.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,3078 @@ 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
|
+
const result = {};
|
|
843
|
+
if (meta) {
|
|
844
|
+
for (const [k, v] of Object.entries(meta)) if (v !== void 0 && v !== null) result[k] = v;
|
|
845
|
+
}
|
|
846
|
+
if (color) result["@statelyai.color"] = color;
|
|
847
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
848
|
+
}
|
|
849
|
+
/** Format event param — supports { type: string } (new) and plain string (legacy) */
|
|
850
|
+
function formatEventParam(event) {
|
|
851
|
+
if (event && typeof event === "object" && "type" in event) return formatInlineObject(event);
|
|
852
|
+
if (typeof event === "string") return `{ type: '${event}' }`;
|
|
853
|
+
return "undefined";
|
|
854
|
+
}
|
|
855
|
+
function formatStringParam(value) {
|
|
856
|
+
return typeof value === "string" ? `'${value}'` : "undefined";
|
|
857
|
+
}
|
|
858
|
+
function formatInlineObject(value) {
|
|
859
|
+
return `{ ${Object.entries(value).filter(([, item]) => item !== void 0).map(([key, item]) => `${formatObjectKey(key)}: ${serializeJS(item)}`).join(", ")} }`;
|
|
860
|
+
}
|
|
861
|
+
function formatObjectKey(key) {
|
|
862
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
863
|
+
}
|
|
864
|
+
const eventParamSchema = union([string(), object({ type: string() }).catchall(unknown())]);
|
|
865
|
+
const delayParamSchema = union([number(), string()]);
|
|
866
|
+
const builtInActionParamSchemas = {
|
|
867
|
+
"xstate.raise": object({
|
|
868
|
+
event: eventParamSchema.optional(),
|
|
869
|
+
id: string().optional(),
|
|
870
|
+
delay: delayParamSchema.optional()
|
|
871
|
+
}),
|
|
872
|
+
"xstate.sendTo": object({
|
|
873
|
+
to: string().optional(),
|
|
874
|
+
event: eventParamSchema.optional(),
|
|
875
|
+
id: string().optional(),
|
|
876
|
+
delay: delayParamSchema.optional()
|
|
877
|
+
}),
|
|
878
|
+
"xstate.cancel": object({ sendId: string().optional() }),
|
|
879
|
+
"xstate.emit": object({ event: eventParamSchema.optional() }),
|
|
880
|
+
"xstate.spawnChild": object({
|
|
881
|
+
src: string().optional(),
|
|
882
|
+
id: string().optional(),
|
|
883
|
+
systemId: string().optional()
|
|
884
|
+
}),
|
|
885
|
+
"xstate.stopChild": object({ actorRef: string().optional() }),
|
|
886
|
+
"xstate.log": object({ label: string().optional() }),
|
|
887
|
+
"xstate.assign": object({ assignment: record(string(), unknown()).optional() }).catchall(unknown())
|
|
888
|
+
};
|
|
889
|
+
const XSTATE_BUILT_IN_ACTIONS = {
|
|
890
|
+
"xstate.raise": {
|
|
891
|
+
package: "xstate",
|
|
892
|
+
version: "^5",
|
|
893
|
+
import: "raise",
|
|
894
|
+
importKind: "named",
|
|
895
|
+
paramsSchema: builtInActionParamSchemas["xstate.raise"],
|
|
896
|
+
emit(params) {
|
|
897
|
+
const eventStr = formatEventParam(params?.event);
|
|
898
|
+
const opts = [];
|
|
899
|
+
if (params?.id) opts.push(`id: '${params.id}'`);
|
|
900
|
+
if (params?.delay != null) opts.push(`delay: ${JSON.stringify(params.delay)}`);
|
|
901
|
+
return `raise(${eventStr}${opts.length > 0 ? `, { ${opts.join(", ")} }` : ""})`;
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
"xstate.sendTo": {
|
|
905
|
+
package: "xstate",
|
|
906
|
+
version: "^5",
|
|
907
|
+
import: "sendTo",
|
|
908
|
+
importKind: "named",
|
|
909
|
+
paramsSchema: builtInActionParamSchemas["xstate.sendTo"],
|
|
910
|
+
emit(params) {
|
|
911
|
+
const to = formatStringParam(params?.to);
|
|
912
|
+
const eventStr = formatEventParam(params?.event);
|
|
913
|
+
const opts = [];
|
|
914
|
+
if (params?.id) opts.push(`id: '${params.id}'`);
|
|
915
|
+
if (params?.delay != null) opts.push(`delay: ${JSON.stringify(params.delay)}`);
|
|
916
|
+
return `sendTo(${to}, ${eventStr}${opts.length > 0 ? `, { ${opts.join(", ")} }` : ""})`;
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
"xstate.cancel": {
|
|
920
|
+
package: "xstate",
|
|
921
|
+
version: "^5",
|
|
922
|
+
import: "cancel",
|
|
923
|
+
importKind: "named",
|
|
924
|
+
paramsSchema: builtInActionParamSchemas["xstate.cancel"],
|
|
925
|
+
emit(params) {
|
|
926
|
+
return `cancel(${formatStringParam(params?.sendId)})`;
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
"xstate.emit": {
|
|
930
|
+
package: "xstate",
|
|
931
|
+
version: "^5",
|
|
932
|
+
import: "emit",
|
|
933
|
+
importKind: "named",
|
|
934
|
+
paramsSchema: builtInActionParamSchemas["xstate.emit"],
|
|
935
|
+
emit(params) {
|
|
936
|
+
return `emit(${formatEventParam(params?.event)})`;
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
"xstate.spawnChild": {
|
|
940
|
+
package: "xstate",
|
|
941
|
+
version: "^5",
|
|
942
|
+
import: "spawnChild",
|
|
943
|
+
importKind: "named",
|
|
944
|
+
paramsSchema: builtInActionParamSchemas["xstate.spawnChild"],
|
|
945
|
+
emit(params) {
|
|
946
|
+
const src = formatStringParam(params?.src);
|
|
947
|
+
const opts = [];
|
|
948
|
+
if (params?.id) opts.push(`id: '${params.id}'`);
|
|
949
|
+
if (params?.systemId) opts.push(`systemId: '${params.systemId}'`);
|
|
950
|
+
return `spawnChild(${src}${opts.length > 0 ? `, { ${opts.join(", ")} }` : ""})`;
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
"xstate.stopChild": {
|
|
954
|
+
package: "xstate",
|
|
955
|
+
version: "^5",
|
|
956
|
+
import: "stopChild",
|
|
957
|
+
importKind: "named",
|
|
958
|
+
paramsSchema: builtInActionParamSchemas["xstate.stopChild"],
|
|
959
|
+
emit(params) {
|
|
960
|
+
return `stopChild(${formatStringParam(params?.actorRef)})`;
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
"xstate.log": {
|
|
964
|
+
package: "xstate",
|
|
965
|
+
version: "^5",
|
|
966
|
+
import: "log",
|
|
967
|
+
importKind: "named",
|
|
968
|
+
paramsSchema: builtInActionParamSchemas["xstate.log"],
|
|
969
|
+
emit(params) {
|
|
970
|
+
return `log(${(params?.label ? `'${params.label}'` : void 0) ?? ""})`;
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
"xstate.assign": {
|
|
974
|
+
package: "xstate",
|
|
975
|
+
version: "^5",
|
|
976
|
+
import: "assign",
|
|
977
|
+
importKind: "named",
|
|
978
|
+
paramsSchema: builtInActionParamSchemas["xstate.assign"],
|
|
979
|
+
emit(params) {
|
|
980
|
+
const assignments = params?.assignment && typeof params.assignment === "object" && !Array.isArray(params.assignment) ? params.assignment : params;
|
|
981
|
+
const entries = Object.entries(assignments ?? {}).filter(([k, v]) => k && v !== void 0);
|
|
982
|
+
if (entries.length === 0) return "assign({})";
|
|
983
|
+
return `assign({ ${entries.map(([k, v]) => `${JSON.stringify(k)}: ${formatAssignValue(v)}`).join(", ")} })`;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
function isBuiltInActionType(type) {
|
|
988
|
+
return type in XSTATE_BUILT_IN_ACTIONS;
|
|
989
|
+
}
|
|
990
|
+
function emitBuiltInAction(type, params) {
|
|
991
|
+
return XSTATE_BUILT_IN_ACTIONS[type].emit(params);
|
|
992
|
+
}
|
|
993
|
+
function actionCodeExpression(expr, source) {
|
|
994
|
+
return {
|
|
995
|
+
"@type": "code",
|
|
996
|
+
lang: "ts",
|
|
997
|
+
expr,
|
|
998
|
+
imports: [{
|
|
999
|
+
package: source.package,
|
|
1000
|
+
version: source.version,
|
|
1001
|
+
import: source.import,
|
|
1002
|
+
importKind: source.importKind
|
|
1003
|
+
}]
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function inlineCodeExpression(expr) {
|
|
1007
|
+
return {
|
|
1008
|
+
"@type": "code",
|
|
1009
|
+
lang: "ts",
|
|
1010
|
+
expr: stripExportDefault(expr)
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Serialize an edge guard for a machine config. Codegen emits inline guards as
|
|
1015
|
+
* a code-expression directive so the source round-trips. The simulation passes
|
|
1016
|
+
* `asTypeRef` to get a plain `{ type }` reference instead (it stubs guards by
|
|
1017
|
+
* type and can't evaluate the source) — see MachineConfigOptions.
|
|
1018
|
+
*/
|
|
1019
|
+
function serializeGuard(guard, asTypeRef) {
|
|
1020
|
+
if (asTypeRef) return guard.params ? {
|
|
1021
|
+
type: guard.type,
|
|
1022
|
+
params: guard.params
|
|
1023
|
+
} : { type: guard.type };
|
|
1024
|
+
return guard.code ? inlineCodeExpression(guard.code) : guard;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Converts a built-in xstate ActionItem to a code expression directive,
|
|
1028
|
+
* or returns a plain { type, params } object for user-defined actions.
|
|
1029
|
+
*/
|
|
1030
|
+
function serializeActionItem(action) {
|
|
1031
|
+
const { type, params } = action;
|
|
1032
|
+
const exprCode = type === "xstate.expr" && typeof params?.code === "string" ? params.code : void 0;
|
|
1033
|
+
if (isBuiltInActionType(type)) {
|
|
1034
|
+
const builtInAction = XSTATE_BUILT_IN_ACTIONS[type];
|
|
1035
|
+
return actionCodeExpression(emitBuiltInAction(type, params), builtInAction);
|
|
1036
|
+
}
|
|
1037
|
+
if (exprCode) return inlineCodeExpression(exprCode);
|
|
1038
|
+
if (!params || Object.keys(params).length === 0) return { type };
|
|
1039
|
+
return {
|
|
1040
|
+
type,
|
|
1041
|
+
params
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function formatAssignValue(value) {
|
|
1045
|
+
if (typeof value !== "string") return JSON.stringify(value);
|
|
1046
|
+
const templateExpression = value.match(/^\{\{([\s\S]*)\}\}$/);
|
|
1047
|
+
if (templateExpression) return templateExpression[1].trim();
|
|
1048
|
+
return JSON.stringify(value);
|
|
1049
|
+
}
|
|
1050
|
+
/** Normalize tag to string — handles both `string` and `{ name: string }` */
|
|
1051
|
+
function tagToString(tag) {
|
|
1052
|
+
return typeof tag === "string" ? tag : tag.name;
|
|
1053
|
+
}
|
|
1054
|
+
function graphToMachineConfig(graph, options = {}) {
|
|
1055
|
+
const { showDescriptions = true, showMeta = true, inlineGuardsAsTypeRef = false } = options;
|
|
1056
|
+
const config = {};
|
|
1057
|
+
function getNodeConfig(node) {
|
|
1058
|
+
const nodeConfig = node.parentId ? {} : config;
|
|
1059
|
+
const resolvedNodeId = getEmittedNodeId(graph, node);
|
|
1060
|
+
const initialNode = node.data.initialId ? graph.nodes.find((n) => n.id === node.data.initialId) : null;
|
|
1061
|
+
const nodeType = [
|
|
1062
|
+
"final",
|
|
1063
|
+
"history",
|
|
1064
|
+
"parallel"
|
|
1065
|
+
].includes(node.data.type) ? node.data.type : void 0;
|
|
1066
|
+
const tags = node.data.tags;
|
|
1067
|
+
Object.assign(nodeConfig, {
|
|
1068
|
+
id: resolvedNodeId,
|
|
1069
|
+
type: nodeType === "normal" ? void 0 : nodeType ?? void 0,
|
|
1070
|
+
initial: initialNode?.data.key,
|
|
1071
|
+
...node.data.entry?.length ? { entry: node.data.entry.map(serializeActionItem) } : void 0,
|
|
1072
|
+
...node.data.exit?.length ? { exit: node.data.exit.map(serializeActionItem) } : void 0,
|
|
1073
|
+
...node.data.invokes?.length ? { invoke: node.data.invokes.map((inv) => ({
|
|
1074
|
+
src: inv.src,
|
|
1075
|
+
id: inv.invocationId ?? inv.id,
|
|
1076
|
+
...inv.input ? { input: inv.input } : void 0,
|
|
1077
|
+
...inv.output ? { output: inv.output } : void 0
|
|
1078
|
+
})) } : void 0,
|
|
1079
|
+
...tags?.length ? { tags: tags.map(tagToString) } : void 0,
|
|
1080
|
+
...showDescriptions && node.data.description ? { description: node.data.description } : void 0,
|
|
1081
|
+
...showMeta && node.data.meta && Object.keys(node.data.meta).length > 0 ? { meta: node.data.meta } : void 0,
|
|
1082
|
+
...node.data.history ? { history: node.data.history } : void 0
|
|
1083
|
+
});
|
|
1084
|
+
const childNodes = graph.nodes.filter((n) => n.parentId === node.id && !n.data.temp);
|
|
1085
|
+
const edges = graph.edges.filter((edge) => edge.sourceId === node.id && !edge.data.temp);
|
|
1086
|
+
if (edges.length > 0) {
|
|
1087
|
+
const on = {};
|
|
1088
|
+
const after = {};
|
|
1089
|
+
const alwaysArr = [];
|
|
1090
|
+
const onDone = {};
|
|
1091
|
+
const invokeMap = {};
|
|
1092
|
+
for (const edge of edges) {
|
|
1093
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.targetId);
|
|
1094
|
+
const resolvedTarget = targetNode ? resolveTransitionTarget(graph, node, targetNode) : "";
|
|
1095
|
+
const type = edge.data.eventType ?? "";
|
|
1096
|
+
const transitionMeta = showMeta ? buildMeta(edge.data.meta, edge.data.color) : void 0;
|
|
1097
|
+
const transitionObject = {
|
|
1098
|
+
target: edge.data.transitionType === "targetless" ? void 0 : `${resolvedTarget}`,
|
|
1099
|
+
...edge.data.transitionType === "reenter" ? { reenter: true } : void 0,
|
|
1100
|
+
guard: edge.data.guard ? serializeGuard(edge.data.guard, inlineGuardsAsTypeRef) : void 0,
|
|
1101
|
+
actions: edge.data.actions?.length ? edge.data.actions.map(serializeActionItem) : void 0,
|
|
1102
|
+
description: edge.data.description ?? void 0,
|
|
1103
|
+
...transitionMeta ? { meta: transitionMeta } : void 0
|
|
1104
|
+
};
|
|
1105
|
+
const pushOn = (key) => {
|
|
1106
|
+
if (!on[key]) on[key] = [];
|
|
1107
|
+
on[key].push(transitionObject);
|
|
1108
|
+
};
|
|
1109
|
+
const nestInvoke = (blockId, slot) => {
|
|
1110
|
+
invokeMap[blockId] = {
|
|
1111
|
+
...invokeMap[blockId],
|
|
1112
|
+
[slot]: (invokeMap[blockId]?.[slot] ?? []).concat(transitionObject)
|
|
1113
|
+
};
|
|
1114
|
+
};
|
|
1115
|
+
const ref = parseStatelyRef(type);
|
|
1116
|
+
if (ref) if (ref.kind === "invokeDone") nestInvoke(ref.blockId, "onDone");
|
|
1117
|
+
else if (ref.kind === "invokeError") nestInvoke(ref.blockId, "onError");
|
|
1118
|
+
else if (ref.kind === "invokeSnapshot") nestInvoke(ref.blockId, "onSnapshot");
|
|
1119
|
+
else if (ref.kind === "stateDone") {
|
|
1120
|
+
if (!onDone[type]) onDone[type] = [];
|
|
1121
|
+
onDone[type].push(transitionObject);
|
|
1122
|
+
} else pushOn(`xstate.${ref.kind === "actionDone" ? "done" : "error"}.actor.${resolveSpawnActorId$1(node, ref.blockId)}`);
|
|
1123
|
+
else if (type === "") alwaysArr.push(transitionObject);
|
|
1124
|
+
else if (type.startsWith("xstate.after.")) {
|
|
1125
|
+
const delayKey = type.slice(13).split(".")[0];
|
|
1126
|
+
if (!after[delayKey]) after[delayKey] = [];
|
|
1127
|
+
after[delayKey].push(transitionObject);
|
|
1128
|
+
} else if (type === "*") pushOn("*");
|
|
1129
|
+
else if (type.startsWith("xstate.done.state")) {
|
|
1130
|
+
if (!onDone[type]) onDone[type] = [];
|
|
1131
|
+
onDone[type].push(transitionObject);
|
|
1132
|
+
} else if (type.startsWith("xstate.done.actor.") || type.startsWith("xstate.error.actor.") || type.startsWith("xstate.snapshot.actor.")) {
|
|
1133
|
+
const verb = type.startsWith("xstate.done.actor.") ? "onDone" : type.startsWith("xstate.error.actor.") ? "onError" : "onSnapshot";
|
|
1134
|
+
const invoke = findInvokeByActorId(node, type.slice(type.lastIndexOf(".actor.") + 7));
|
|
1135
|
+
if (invoke) nestInvoke(invoke.id, verb);
|
|
1136
|
+
else pushOn(type);
|
|
1137
|
+
} else pushOn(type);
|
|
1138
|
+
}
|
|
1139
|
+
if (Object.keys(on).length > 0) nodeConfig.on = singleOrArrayRecord(on);
|
|
1140
|
+
if (Object.keys(after).length > 0) nodeConfig.after = singleOrArrayRecord(after);
|
|
1141
|
+
if (alwaysArr.length > 0) nodeConfig.always = singleOrArray(alwaysArr);
|
|
1142
|
+
if (Object.keys(onDone).length > 0) nodeConfig.onDone = singleOrArray(Object.values(onDone).flat());
|
|
1143
|
+
if (Object.keys(invokeMap).length > 0 && Array.isArray(nodeConfig.invoke)) nodeConfig.invoke = nodeConfig.invoke.map((inv, index) => {
|
|
1144
|
+
const mapped = invokeMap[node.data.invokes?.[index]?.id ?? inv.id];
|
|
1145
|
+
if (!mapped) return inv;
|
|
1146
|
+
return {
|
|
1147
|
+
...inv,
|
|
1148
|
+
...mapped.onDone ? { onDone: singleOrArray(mapped.onDone) } : void 0,
|
|
1149
|
+
...mapped.onError ? { onError: singleOrArray(mapped.onError) } : void 0,
|
|
1150
|
+
...mapped.onSnapshot ? { onSnapshot: singleOrArray(mapped.onSnapshot) } : void 0
|
|
1151
|
+
};
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
if (Array.isArray(nodeConfig.invoke)) nodeConfig.invoke = nodeConfig.invoke.map((inv) => {
|
|
1155
|
+
if (inv.id && !isAutoGeneratedId(inv.id)) return inv;
|
|
1156
|
+
const { id: _id, ...rest } = inv;
|
|
1157
|
+
return rest;
|
|
1158
|
+
});
|
|
1159
|
+
if (childNodes.length > 0) {
|
|
1160
|
+
nodeConfig.states = {};
|
|
1161
|
+
for (const childState of childNodes) nodeConfig.states[childState.data.key] = getNodeConfig(childState);
|
|
1162
|
+
}
|
|
1163
|
+
return nodeConfig;
|
|
1164
|
+
}
|
|
1165
|
+
const rootNode = graph.nodes.find((node) => node.data.parentId === null || node.parentId == null);
|
|
1166
|
+
if (!rootNode) throw new Error("No root node found");
|
|
1167
|
+
getNodeConfig(rootNode);
|
|
1168
|
+
if (graph.data.context !== void 0 && graph.data.context !== null) config.context = graph.data.context;
|
|
1169
|
+
return deepSimplify(config);
|
|
1170
|
+
}
|
|
1171
|
+
const REF_PREFIX = {
|
|
1172
|
+
invokeDone: "@statelyai.invoke.done",
|
|
1173
|
+
invokeError: "@statelyai.invoke.error",
|
|
1174
|
+
invokeSnapshot: "@statelyai.invoke.snapshot",
|
|
1175
|
+
actionDone: "@statelyai.action.done",
|
|
1176
|
+
actionError: "@statelyai.action.error",
|
|
1177
|
+
stateDone: "@statelyai.state.done"
|
|
1178
|
+
};
|
|
1179
|
+
function parseRef(eventType) {
|
|
1180
|
+
if (eventType.startsWith("@stately.")) eventType = `@statelyai${eventType.slice(8)}`;
|
|
1181
|
+
if (!eventType.startsWith("@statelyai.")) return null;
|
|
1182
|
+
for (const kind of Object.keys(REF_PREFIX)) {
|
|
1183
|
+
const prefix = REF_PREFIX[kind];
|
|
1184
|
+
if (eventType === prefix) return {
|
|
1185
|
+
kind,
|
|
1186
|
+
blockId: ""
|
|
1187
|
+
};
|
|
1188
|
+
if (eventType.startsWith(`${prefix}.`)) return {
|
|
1189
|
+
kind,
|
|
1190
|
+
blockId: eventType.slice(prefix.length + 1)
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
function getSourceNode(graph, edge) {
|
|
1196
|
+
return graph.nodes.find((node) => node.id === edge.sourceId);
|
|
1197
|
+
}
|
|
1198
|
+
function resolveSpawnActorId(sourceNode, actionId) {
|
|
1199
|
+
const actorId = [...sourceNode?.data.entry ?? [], ...sourceNode?.data.exit ?? []].find((action) => action.id === actionId)?.params?.["id"];
|
|
1200
|
+
return typeof actorId === "string" && actorId ? actorId : actionId;
|
|
1201
|
+
}
|
|
1202
|
+
function getTransitionRuntimeEventType(graph, edge) {
|
|
1203
|
+
const eventType = edge.data.eventType;
|
|
1204
|
+
const sourceNode = getSourceNode(graph, edge);
|
|
1205
|
+
if (eventType.startsWith("xstate.after.")) {
|
|
1206
|
+
const delayKey = eventType.slice(13).split(".")[0];
|
|
1207
|
+
return sourceNode ? `xstate.after.${delayKey}.${getResolvedNodeId(graph, sourceNode)}` : eventType;
|
|
1208
|
+
}
|
|
1209
|
+
const ref = parseRef(eventType);
|
|
1210
|
+
if (!ref) return eventType;
|
|
1211
|
+
if (ref.kind === "stateDone") {
|
|
1212
|
+
const node = graph.nodes.find((candidate) => candidate.id === ref.blockId) ?? sourceNode;
|
|
1213
|
+
return `xstate.done.state.${node ? getResolvedNodeId(graph, node) : ref.blockId}`;
|
|
1214
|
+
}
|
|
1215
|
+
if (ref.kind === "invokeDone" || ref.kind === "invokeError" || ref.kind === "invokeSnapshot") {
|
|
1216
|
+
const invokes = sourceNode?.data.invokes ?? [];
|
|
1217
|
+
const index = invokes.findIndex((invoke) => invoke.id === ref.blockId);
|
|
1218
|
+
const actorId = (index >= 0 ? invokes[index] : void 0)?.invocationId || (sourceNode && index >= 0 ? `${index}.${getResolvedNodeId(graph, sourceNode)}` : ref.blockId);
|
|
1219
|
+
return `xstate.${ref.kind === "invokeDone" ? "done" : ref.kind === "invokeError" ? "error" : "snapshot"}.actor.${actorId}`;
|
|
1220
|
+
}
|
|
1221
|
+
return `xstate.${ref.kind === "actionDone" ? "done" : "error"}.actor.${resolveSpawnActorId(sourceNode, ref.blockId)}`;
|
|
1222
|
+
}
|
|
1223
|
+
function transitionMatchesRuntimeEvent(graph, edge, eventType) {
|
|
1224
|
+
return getTransitionRuntimeEventType(graph, edge) === eventType;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Map an XState state node (identified by its array of keys from the root) to
|
|
1228
|
+
* the corresponding graph node id. The runtime snapshot's `_nodes[].id` are
|
|
1229
|
+
* XState resolved ids (delimited paths), which do not match the graph's opaque
|
|
1230
|
+
* node ids; consumers (highlighting, edge lookup) need graph node ids.
|
|
1231
|
+
*/
|
|
1232
|
+
function getGraphNodeIdForPath(graph, path) {
|
|
1233
|
+
const root = graph.nodes.find((n) => n.parentId === null);
|
|
1234
|
+
if (!root) return null;
|
|
1235
|
+
if (path.length === 0) return root.id;
|
|
1236
|
+
let parentId = root.id;
|
|
1237
|
+
let nodeId = null;
|
|
1238
|
+
for (const key of path) {
|
|
1239
|
+
const child = graph.nodes.find((n) => n.parentId === parentId && n.data.key === key);
|
|
1240
|
+
if (!child) return null;
|
|
1241
|
+
nodeId = child.id;
|
|
1242
|
+
parentId = child.id;
|
|
1243
|
+
}
|
|
1244
|
+
return nodeId;
|
|
1245
|
+
}
|
|
1246
|
+
function getStateIds(graph, snapshot) {
|
|
1247
|
+
const nodes = snapshot._nodes;
|
|
1248
|
+
if (!nodes) return [];
|
|
1249
|
+
return nodes.map((n) => getGraphNodeIdForPath(graph, n.path)).filter((id) => id !== null);
|
|
1250
|
+
}
|
|
1251
|
+
function getLeafStateIds(graph, snapshot) {
|
|
1252
|
+
const nodes = snapshot._nodes;
|
|
1253
|
+
if (!nodes) return [];
|
|
1254
|
+
const leaves = nodes.filter((n) => n.path.length > 0 && (n.type === "atomic" || n.type === "final"));
|
|
1255
|
+
return (leaves.length > 0 ? leaves : nodes).map((n) => getGraphNodeIdForPath(graph, n.path)).filter((id) => id !== null);
|
|
1256
|
+
}
|
|
1257
|
+
function getStatePath(snapshot) {
|
|
1258
|
+
const nodes = snapshot._nodes;
|
|
1259
|
+
if (!nodes?.length) return "(initial)";
|
|
1260
|
+
const withPath = nodes.filter((n) => n.path.length > 0);
|
|
1261
|
+
if (!withPath.length) return "(initial)";
|
|
1262
|
+
const leaves = withPath.filter((n) => n.type === "atomic" || n.type === "final");
|
|
1263
|
+
return (leaves.length > 0 ? leaves : withPath).map((n) => n.path.join(".")).join(", ");
|
|
1264
|
+
}
|
|
1265
|
+
function findEdgeId(graph, sourceStateIds, event) {
|
|
1266
|
+
const guardType = event["@xstate.guard"];
|
|
1267
|
+
for (const sourceId of sourceStateIds) {
|
|
1268
|
+
const edge = graph.edges.find((candidate) => candidate.sourceId === sourceId && transitionMatchesRuntimeEvent(graph, candidate, event.type) && (candidate.data.guard?.type ?? null) === (guardType ?? null));
|
|
1269
|
+
if (edge) return edge.id;
|
|
1270
|
+
}
|
|
1271
|
+
for (const sourceId of sourceStateIds) {
|
|
1272
|
+
const edge = graph.edges.find((candidate) => candidate.sourceId === sourceId && transitionMatchesRuntimeEvent(graph, candidate, event.type));
|
|
1273
|
+
if (edge) return edge.id;
|
|
1274
|
+
}
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
function formatEventLabel(event) {
|
|
1278
|
+
if (event.type === "") return "always";
|
|
1279
|
+
if (event.type.startsWith("xstate.after.")) return `after ${event.type.slice(13).split(".")[0]}`;
|
|
1280
|
+
const guardType = event["@xstate.guard"];
|
|
1281
|
+
if (guardType) return `${event.type} [${guardType}]`;
|
|
1282
|
+
return event.type;
|
|
1283
|
+
}
|
|
1284
|
+
function formatEdgeLabel(graph, edgeId) {
|
|
1285
|
+
const edge = graph.edges.find((candidate) => candidate.id === edgeId);
|
|
1286
|
+
if (!edge) return null;
|
|
1287
|
+
const baseLabel = formatEventLabel({ type: edge.data.eventType });
|
|
1288
|
+
const guardType = edge.data.guard?.type;
|
|
1289
|
+
if (guardType) return `${baseLabel} [${guardType}]`;
|
|
1290
|
+
return baseLabel;
|
|
1291
|
+
}
|
|
1292
|
+
function getFallbackTransitionLabels(graph, finalStateIds) {
|
|
1293
|
+
return getFallbackEventlessEdges(graph, finalStateIds).map((edge) => formatEdgeLabel(graph, edge.id)).filter((label) => !!label);
|
|
1294
|
+
}
|
|
1295
|
+
function getFallbackEventlessEdges(graph, finalStateIds) {
|
|
1296
|
+
const finalLeafIdSet = new Set(finalStateIds);
|
|
1297
|
+
return graph.edges.filter((edge) => finalLeafIdSet.has(edge.targetId) && edge.data.eventType === "");
|
|
1298
|
+
}
|
|
1299
|
+
function getPathName(finalStatePath, finalStateIds, steps, graph) {
|
|
1300
|
+
let transitionLabels = steps.map((step) => {
|
|
1301
|
+
if (step.edgeId) return formatEdgeLabel(graph, step.edgeId);
|
|
1302
|
+
if (step.eventType && step.eventType !== "xstate.init") return formatEventLabel({ type: step.eventType });
|
|
1303
|
+
return null;
|
|
1304
|
+
}).filter((label) => !!label);
|
|
1305
|
+
if (transitionLabels.length === 0) transitionLabels = getFallbackTransitionLabels(graph, finalStateIds);
|
|
1306
|
+
if (transitionLabels.length === 0) return {
|
|
1307
|
+
name: finalStatePath,
|
|
1308
|
+
viaLabel: null
|
|
1309
|
+
};
|
|
1310
|
+
return {
|
|
1311
|
+
name: finalStatePath,
|
|
1312
|
+
viaLabel: `via ${transitionLabels.join(" -> ")}`
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
function convertPath(path, graph, index) {
|
|
1316
|
+
const steps = [];
|
|
1317
|
+
const firstStep = path.steps[0];
|
|
1318
|
+
if (firstStep) {
|
|
1319
|
+
const allIds = getStateIds(graph, firstStep.state);
|
|
1320
|
+
steps.push({
|
|
1321
|
+
stateIds: getLeafStateIds(graph, firstStep.state),
|
|
1322
|
+
statePath: getStatePath(firstStep.state),
|
|
1323
|
+
eventType: firstStep.event.type,
|
|
1324
|
+
edgeId: findEdgeId(graph, allIds, firstStep.event)
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
for (let i = 1; i < path.steps.length; i++) {
|
|
1328
|
+
const prevStep = path.steps[i - 1];
|
|
1329
|
+
const step = path.steps[i];
|
|
1330
|
+
const prevStateIds = getStateIds(graph, prevStep.state);
|
|
1331
|
+
steps.push({
|
|
1332
|
+
stateIds: getLeafStateIds(graph, step.state),
|
|
1333
|
+
statePath: getStatePath(step.state),
|
|
1334
|
+
eventType: step.event.type,
|
|
1335
|
+
edgeId: findEdgeId(graph, prevStateIds, step.event)
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
const finalStatePath = getStatePath(path.state);
|
|
1339
|
+
const finalStateIds = getLeafStateIds(graph, path.state);
|
|
1340
|
+
const title = getPathName(finalStatePath, finalStateIds, steps, graph);
|
|
1341
|
+
return {
|
|
1342
|
+
id: `path-${index}`,
|
|
1343
|
+
name: title.name,
|
|
1344
|
+
viaLabel: title.viaLabel,
|
|
1345
|
+
steps,
|
|
1346
|
+
finalStatePath,
|
|
1347
|
+
finalStateIds
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
function serializeMachineTransition(_state, event, prevState) {
|
|
1351
|
+
if (!event) return "";
|
|
1352
|
+
const prevStateString = prevState ? ` from ${JSON.stringify(prevState.value)}` : "";
|
|
1353
|
+
return ` via ${JSON.stringify(event)}${prevStateString}`;
|
|
1354
|
+
}
|
|
1355
|
+
function getGraphPathSimImplementations(graph, guardState) {
|
|
1356
|
+
const guards = {};
|
|
1357
|
+
for (const edge of graph.edges) if (edge.data.guard) {
|
|
1358
|
+
const guardType = edge.data.guard.type;
|
|
1359
|
+
if (guardState) guards[guardType] = () => guardState[guardType] ?? false;
|
|
1360
|
+
else guards[guardType] = ({ event }) => {
|
|
1361
|
+
if (event["@xstate.guard"] === guardType) return true;
|
|
1362
|
+
return false;
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return { guards };
|
|
1366
|
+
}
|
|
1367
|
+
function getTraversalEvents(state, graph) {
|
|
1368
|
+
const events = [];
|
|
1369
|
+
for (const edge of graph.edges) {
|
|
1370
|
+
const eventType = getTransitionRuntimeEventType(graph, edge);
|
|
1371
|
+
const event = { type: eventType };
|
|
1372
|
+
if (edge.data.guard?.type) event["@xstate.guard"] = edge.data.guard.type;
|
|
1373
|
+
if (eventType && !state.can(event)) continue;
|
|
1374
|
+
events.push(event);
|
|
1375
|
+
}
|
|
1376
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1377
|
+
return events.filter((event) => {
|
|
1378
|
+
const key = JSON.stringify(event);
|
|
1379
|
+
if (seen.has(key)) return false;
|
|
1380
|
+
seen.add(key);
|
|
1381
|
+
return true;
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
function getStepKey(step) {
|
|
1385
|
+
return JSON.stringify({
|
|
1386
|
+
stateIds: step.stateIds,
|
|
1387
|
+
statePath: step.statePath,
|
|
1388
|
+
eventType: step.eventType,
|
|
1389
|
+
edgeId: step.edgeId
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
function getLeafGraphNodeIds(graph) {
|
|
1393
|
+
const activeNodes = graph.nodes.filter((node) => !node.data.temp);
|
|
1394
|
+
const parentIds = new Set(activeNodes.map((node) => node.parentId).filter(Boolean));
|
|
1395
|
+
return activeNodes.filter((node) => !parentIds.has(node.id)).map((node) => node.id);
|
|
1396
|
+
}
|
|
1397
|
+
function getCoverage(graph, paths) {
|
|
1398
|
+
const leafNodeIds = new Set(getLeafGraphNodeIds(graph));
|
|
1399
|
+
const coveredStateIds = /* @__PURE__ */ new Set();
|
|
1400
|
+
const coveredEdgeIds = /* @__PURE__ */ new Set();
|
|
1401
|
+
for (const path of paths) {
|
|
1402
|
+
let pathHasExplicitEdge = false;
|
|
1403
|
+
for (const step of path.steps) {
|
|
1404
|
+
for (const stateId of step.stateIds) if (leafNodeIds.has(stateId)) coveredStateIds.add(stateId);
|
|
1405
|
+
if (step.edgeId) {
|
|
1406
|
+
pathHasExplicitEdge = true;
|
|
1407
|
+
coveredEdgeIds.add(step.edgeId);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
for (const finalStateId of path.finalStateIds) if (leafNodeIds.has(finalStateId)) coveredStateIds.add(finalStateId);
|
|
1411
|
+
if (!pathHasExplicitEdge) for (const edge of getFallbackEventlessEdges(graph, path.finalStateIds)) coveredEdgeIds.add(edge.id);
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
states: {
|
|
1415
|
+
covered: coveredStateIds.size,
|
|
1416
|
+
total: leafNodeIds.size
|
|
1417
|
+
},
|
|
1418
|
+
transitions: {
|
|
1419
|
+
covered: coveredEdgeIds.size,
|
|
1420
|
+
total: graph.edges.filter((edge) => !edge.data.temp).length
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function deduplicateGeneratedPaths(paths) {
|
|
1425
|
+
const sorted = [...paths].sort((a, b) => b.steps.length - a.steps.length);
|
|
1426
|
+
const unique = [];
|
|
1427
|
+
for (const path of sorted) if (!unique.some((existing) => {
|
|
1428
|
+
if (path.steps.length >= existing.steps.length) return false;
|
|
1429
|
+
return path.steps.every((step, index) => getStepKey(step) === getStepKey(existing.steps[index]));
|
|
1430
|
+
})) unique.push(path);
|
|
1431
|
+
return unique;
|
|
1432
|
+
}
|
|
1433
|
+
function generateGraphPathsData(graph, options) {
|
|
1434
|
+
const machineConfig = graphToMachineConfig(graph);
|
|
1435
|
+
const implementations = getGraphPathSimImplementations(graph);
|
|
1436
|
+
const machine = createMachine(machineConfig).provide(implementations);
|
|
1437
|
+
let converted = (options.strategy === "shortest" ? getShortestPaths : getSimplePaths)(machine, {
|
|
1438
|
+
events: (state) => getTraversalEvents(state, graph),
|
|
1439
|
+
serializeState: (state, event, prevState) => {
|
|
1440
|
+
const serializedState = JSON.stringify(state.value);
|
|
1441
|
+
if (!options.preferTransitionCoverage) return serializedState;
|
|
1442
|
+
return `${serializedState}|${serializeMachineTransition(state, event, prevState)}`;
|
|
1443
|
+
}
|
|
1444
|
+
}).map((path, index) => convertPath(path, graph, index));
|
|
1445
|
+
if (options.reduceDuplicates) converted = deduplicateGeneratedPaths(converted);
|
|
1446
|
+
if (options.limit) converted = converted.slice(0, options.limit);
|
|
1447
|
+
return {
|
|
1448
|
+
paths: converted,
|
|
1449
|
+
coverage: getCoverage(graph, converted)
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
function toArray(value) {
|
|
1453
|
+
return Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
1454
|
+
}
|
|
1455
|
+
function actionItems(value) {
|
|
1456
|
+
return toArray(value).map((item, index) => {
|
|
1457
|
+
if (typeof item === "string") return {
|
|
1458
|
+
id: item,
|
|
1459
|
+
type: item
|
|
1460
|
+
};
|
|
1461
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
1462
|
+
const action = item;
|
|
1463
|
+
if (typeof action.type === "string") return {
|
|
1464
|
+
id: action.type || `action-${index}`,
|
|
1465
|
+
type: action.type,
|
|
1466
|
+
params: action.params
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
return null;
|
|
1470
|
+
}).filter((item) => item !== null);
|
|
1471
|
+
}
|
|
1472
|
+
function invokeItems(value) {
|
|
1473
|
+
return toArray(value).map((item, index) => {
|
|
1474
|
+
if (typeof item === "string") return {
|
|
1475
|
+
id: item || `invoke-${index}`,
|
|
1476
|
+
src: item
|
|
1477
|
+
};
|
|
1478
|
+
if (!item || typeof item !== "object") return null;
|
|
1479
|
+
const invoke = item;
|
|
1480
|
+
if (typeof invoke.src !== "string") return null;
|
|
1481
|
+
return {
|
|
1482
|
+
id: typeof invoke.id === "string" ? invoke.id : invoke.src,
|
|
1483
|
+
src: invoke.src,
|
|
1484
|
+
...invoke.input !== void 0 ? { input: invoke.input } : {},
|
|
1485
|
+
...invoke.output !== void 0 ? { output: invoke.output } : {}
|
|
1486
|
+
};
|
|
1487
|
+
}).filter((item) => item !== null);
|
|
1488
|
+
}
|
|
1489
|
+
function transitionTargets(target) {
|
|
1490
|
+
if (typeof target === "string") return [target];
|
|
1491
|
+
if (Array.isArray(target)) return target.filter((item) => typeof item === "string");
|
|
1492
|
+
return [];
|
|
1493
|
+
}
|
|
1494
|
+
function targetToNodeId(sourceId, target, rootKey) {
|
|
1495
|
+
if (!target) return sourceId;
|
|
1496
|
+
let clean = target.startsWith("#") ? target.slice(1) : target;
|
|
1497
|
+
if (target.startsWith("#")) {
|
|
1498
|
+
const rootPrefix = `${rootKey}.`;
|
|
1499
|
+
if (clean === rootKey) return "root";
|
|
1500
|
+
if (clean.startsWith(rootPrefix)) clean = clean.slice(rootPrefix.length);
|
|
1501
|
+
return clean ? `root.${clean}` : "root";
|
|
1502
|
+
}
|
|
1503
|
+
if (clean.includes(".")) return `root.${clean}`;
|
|
1504
|
+
return `${sourceId.split(".").slice(0, -1).join(".") || "root"}.${clean}`;
|
|
1505
|
+
}
|
|
1506
|
+
function transitionObjects(value) {
|
|
1507
|
+
return toArray(value).map((item) => {
|
|
1508
|
+
if (typeof item === "string") return { target: item };
|
|
1509
|
+
if (item && typeof item === "object") return item;
|
|
1510
|
+
return {};
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
function guardObject(value) {
|
|
1514
|
+
if (!value) return null;
|
|
1515
|
+
if (typeof value === "string") return { type: value };
|
|
1516
|
+
if (typeof value === "object" && "type" in value && typeof value.type === "string") {
|
|
1517
|
+
const guard = value;
|
|
1518
|
+
return {
|
|
1519
|
+
type: guard.type,
|
|
1520
|
+
params: guard.params
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
function machineConfigToGraph(config) {
|
|
1526
|
+
createMachine(config);
|
|
1527
|
+
const rootKey = String(config.id ?? "machine");
|
|
1528
|
+
const graph = {
|
|
1529
|
+
id: "root",
|
|
1530
|
+
nodes: [],
|
|
1531
|
+
edges: [],
|
|
1532
|
+
data: {
|
|
1533
|
+
implementations: {
|
|
1534
|
+
actions: [],
|
|
1535
|
+
guards: [],
|
|
1536
|
+
actors: [],
|
|
1537
|
+
delays: []
|
|
1538
|
+
},
|
|
1539
|
+
schemas: config.schemas ?? null
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
function visit(state, key, parentId) {
|
|
1543
|
+
const id = parentId ? `${parentId}.${key}` : "root";
|
|
1544
|
+
const initialId = state.initial ? `${id}.${state.initial}` : null;
|
|
1545
|
+
graph.nodes.push({
|
|
1546
|
+
id,
|
|
1547
|
+
parentId,
|
|
1548
|
+
data: {
|
|
1549
|
+
key,
|
|
1550
|
+
type: state.type === "parallel" || state.type === "final" || state.type === "history" ? state.type : null,
|
|
1551
|
+
initialId,
|
|
1552
|
+
history: state.history ?? false,
|
|
1553
|
+
entry: actionItems(state.entry),
|
|
1554
|
+
exit: actionItems(state.exit),
|
|
1555
|
+
invokes: invokeItems(state.invoke),
|
|
1556
|
+
tags: Array.isArray(state.tags) ? state.tags : typeof state.tags === "string" ? [state.tags] : [],
|
|
1557
|
+
description: state.description ?? null,
|
|
1558
|
+
meta: state.meta ?? null
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
for (const [eventType, value] of Object.entries(state.on ?? {})) for (const [index, transition] of transitionObjects(value).entries()) {
|
|
1562
|
+
const target = transitionTargets(transition.target)[0];
|
|
1563
|
+
graph.edges.push({
|
|
1564
|
+
id: `${id}-${eventType}-${index}`,
|
|
1565
|
+
sourceId: id,
|
|
1566
|
+
targetId: targetToNodeId(id, target, rootKey),
|
|
1567
|
+
data: {
|
|
1568
|
+
eventType,
|
|
1569
|
+
transitionType: target ? transition.reenter ? "reenter" : "normal" : "targetless",
|
|
1570
|
+
guard: guardObject(transition.guard),
|
|
1571
|
+
actions: actionItems(transition.actions),
|
|
1572
|
+
description: typeof transition.description === "string" ? transition.description : null,
|
|
1573
|
+
meta: transition.meta && typeof transition.meta === "object" ? transition.meta : null
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
for (const [delay, value] of Object.entries(state.after ?? {})) for (const [index, transition] of transitionObjects(value).entries()) {
|
|
1578
|
+
const target = transitionTargets(transition.target)[0];
|
|
1579
|
+
graph.edges.push({
|
|
1580
|
+
id: `${id}-after-${delay}-${index}`,
|
|
1581
|
+
sourceId: id,
|
|
1582
|
+
targetId: targetToNodeId(id, target, rootKey),
|
|
1583
|
+
data: {
|
|
1584
|
+
eventType: `xstate.after.${delay}.${id}`,
|
|
1585
|
+
transitionType: target ? "normal" : "targetless",
|
|
1586
|
+
guard: guardObject(transition.guard),
|
|
1587
|
+
actions: actionItems(transition.actions),
|
|
1588
|
+
description: typeof transition.description === "string" ? transition.description : null,
|
|
1589
|
+
meta: null
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
for (const [index, transition] of transitionObjects(state.always).entries()) {
|
|
1594
|
+
const target = transitionTargets(transition.target)[0];
|
|
1595
|
+
graph.edges.push({
|
|
1596
|
+
id: `${id}-always-${index}`,
|
|
1597
|
+
sourceId: id,
|
|
1598
|
+
targetId: targetToNodeId(id, target, rootKey),
|
|
1599
|
+
data: {
|
|
1600
|
+
eventType: "",
|
|
1601
|
+
transitionType: target ? "normal" : "targetless",
|
|
1602
|
+
guard: guardObject(transition.guard),
|
|
1603
|
+
actions: actionItems(transition.actions),
|
|
1604
|
+
description: typeof transition.description === "string" ? transition.description : null,
|
|
1605
|
+
meta: null
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
for (const [childKey, child] of Object.entries(state.states ?? {})) visit(child, childKey, id);
|
|
1610
|
+
}
|
|
1611
|
+
visit(config, rootKey, null);
|
|
1612
|
+
return graph;
|
|
1613
|
+
}
|
|
1614
|
+
const XSTATE_V6_FN_TRANSITION_META = "@statelyai.xstate.v6.fnTransition";
|
|
1615
|
+
const XSTATE_V6_FN_SOURCE_META = "@statelyai.xstate.v6.fnSource";
|
|
1616
|
+
function sourceFile(source) {
|
|
1617
|
+
return ts.createSourceFile("machine.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
1618
|
+
}
|
|
1619
|
+
function propName(name) {
|
|
1620
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
function unwrapExpression$2(node) {
|
|
1624
|
+
if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node)) return unwrapExpression$2(node.expression);
|
|
1625
|
+
return node;
|
|
1626
|
+
}
|
|
1627
|
+
function propertyContext(parent, key) {
|
|
1628
|
+
if (parent.transitionMap) return {
|
|
1629
|
+
v6: parent.v6,
|
|
1630
|
+
transitionValue: true
|
|
1631
|
+
};
|
|
1632
|
+
if (parent.transitionValue && key === "actions") return {
|
|
1633
|
+
v6: parent.v6,
|
|
1634
|
+
actionValue: true
|
|
1635
|
+
};
|
|
1636
|
+
if (parent.transitionValue && key === "to") return {
|
|
1637
|
+
v6: parent.v6,
|
|
1638
|
+
transitionValue: true
|
|
1639
|
+
};
|
|
1640
|
+
if (key === "entry" || key === "exit") return {
|
|
1641
|
+
v6: parent.v6,
|
|
1642
|
+
actionValue: true
|
|
1643
|
+
};
|
|
1644
|
+
if (key === "on" || key === "after") return {
|
|
1645
|
+
v6: parent.v6,
|
|
1646
|
+
transitionMap: true
|
|
1647
|
+
};
|
|
1648
|
+
if (key === "always" || key === "onDone" || key === "onError" || key === "onSnapshot" || key === "onTimeout") return {
|
|
1649
|
+
v6: parent.v6,
|
|
1650
|
+
transitionValue: true
|
|
1651
|
+
};
|
|
1652
|
+
return { v6: parent.v6 };
|
|
1653
|
+
}
|
|
1654
|
+
function fallbackFunctionTransition(fn) {
|
|
1655
|
+
return { meta: {
|
|
1656
|
+
[XSTATE_V6_FN_TRANSITION_META]: true,
|
|
1657
|
+
[XSTATE_V6_FN_SOURCE_META]: fn.getText()
|
|
1658
|
+
} };
|
|
1659
|
+
}
|
|
1660
|
+
function conditionText(cond) {
|
|
1661
|
+
return cond.negated ? `!(${cond.text})` : cond.text;
|
|
1662
|
+
}
|
|
1663
|
+
function simplifyConditions(conds) {
|
|
1664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1665
|
+
return conds.filter((cond) => {
|
|
1666
|
+
if (cond.negated) return false;
|
|
1667
|
+
const key = conditionText(cond);
|
|
1668
|
+
if (seen.has(key)) return false;
|
|
1669
|
+
seen.add(key);
|
|
1670
|
+
return true;
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
function guardFromConditions(conds) {
|
|
1674
|
+
const simplified = simplifyConditions(conds);
|
|
1675
|
+
if (simplified.length === 0) return void 0;
|
|
1676
|
+
return { type: simplified.map(conditionText).join(" && ") };
|
|
1677
|
+
}
|
|
1678
|
+
function mergeActions(left, right) {
|
|
1679
|
+
return [...left ?? [], ...right];
|
|
1680
|
+
}
|
|
1681
|
+
function transitionFromValue(value, actions) {
|
|
1682
|
+
if (typeof value === "string") {
|
|
1683
|
+
if (actions.length === 0) return value;
|
|
1684
|
+
return {
|
|
1685
|
+
target: value,
|
|
1686
|
+
actions
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return actions.length ? { actions } : null;
|
|
1690
|
+
const record = { ...value };
|
|
1691
|
+
const context = record.context;
|
|
1692
|
+
delete record.context;
|
|
1693
|
+
if (context !== void 0) record.actions = mergeActions(Array.isArray(record.actions) ? record.actions : record.actions ? [record.actions] : void 0, [{
|
|
1694
|
+
type: "xstate.assign",
|
|
1695
|
+
params: { assignment: context }
|
|
1696
|
+
}]);
|
|
1697
|
+
if (actions.length) record.actions = mergeActions(Array.isArray(record.actions) ? record.actions : record.actions ? [record.actions] : void 0, actions);
|
|
1698
|
+
return record;
|
|
1699
|
+
}
|
|
1700
|
+
function expressionBranches(expression, bindings, depth) {
|
|
1701
|
+
const node = unwrapExpression$2(expression);
|
|
1702
|
+
if (ts.isConditionalExpression(node)) {
|
|
1703
|
+
const cond = node.condition.getText();
|
|
1704
|
+
return [...expressionBranches(node.whenTrue, bindings, depth + 1).map((branch) => ({
|
|
1705
|
+
...branch,
|
|
1706
|
+
conds: [{ text: cond }, ...branch.conds]
|
|
1707
|
+
})), ...expressionBranches(node.whenFalse, bindings, depth + 1).map((branch) => ({
|
|
1708
|
+
...branch,
|
|
1709
|
+
conds: [{
|
|
1710
|
+
text: cond,
|
|
1711
|
+
negated: true
|
|
1712
|
+
}, ...branch.conds]
|
|
1713
|
+
}))];
|
|
1714
|
+
}
|
|
1715
|
+
if (ts.isBinaryExpression(node) && (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
|
|
1716
|
+
const cond = node.left.getText();
|
|
1717
|
+
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) return [...expressionBranches(node.right, bindings, depth + 1).map((branch) => ({
|
|
1718
|
+
...branch,
|
|
1719
|
+
conds: [{ text: cond }, ...branch.conds]
|
|
1720
|
+
})), {
|
|
1721
|
+
conds: [{
|
|
1722
|
+
text: cond,
|
|
1723
|
+
negated: true
|
|
1724
|
+
}],
|
|
1725
|
+
actions: []
|
|
1726
|
+
}];
|
|
1727
|
+
return [{
|
|
1728
|
+
conds: [{ text: cond }],
|
|
1729
|
+
actions: [],
|
|
1730
|
+
result: node.left.getText()
|
|
1731
|
+
}, ...expressionBranches(node.right, bindings, depth + 1).map((branch) => ({
|
|
1732
|
+
...branch,
|
|
1733
|
+
conds: [{
|
|
1734
|
+
text: cond,
|
|
1735
|
+
negated: true
|
|
1736
|
+
}, ...branch.conds]
|
|
1737
|
+
}))];
|
|
1738
|
+
}
|
|
1739
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1740
|
+
const targetProperty = node.properties.find((property) => ts.isPropertyAssignment(property) && propName(property.name) === "target");
|
|
1741
|
+
const target = targetProperty ? unwrapExpression$2(targetProperty.initializer) : null;
|
|
1742
|
+
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) => ({
|
|
1743
|
+
...branch,
|
|
1744
|
+
result: objectLiteralToValue(node, bindings, {}, depth + 1, new Map([["target", branch.result]]))
|
|
1745
|
+
}));
|
|
1746
|
+
}
|
|
1747
|
+
return [{
|
|
1748
|
+
conds: [],
|
|
1749
|
+
actions: [],
|
|
1750
|
+
result: expressionToValue(node, bindings, {}, depth + 1)
|
|
1751
|
+
}];
|
|
1752
|
+
}
|
|
1753
|
+
function mergeBranchPrefix(prefix, branch) {
|
|
1754
|
+
return {
|
|
1755
|
+
conds: [...prefix.conds, ...branch.conds],
|
|
1756
|
+
actions: [...prefix.actions, ...branch.actions],
|
|
1757
|
+
result: branch.result,
|
|
1758
|
+
unknown: prefix.unknown || branch.unknown
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function expressionStatementAction(statement, enqName, bindings) {
|
|
1762
|
+
const expression = unwrapExpression$2(statement.expression);
|
|
1763
|
+
if (!ts.isCallExpression(expression)) return null;
|
|
1764
|
+
const callee = expression.expression;
|
|
1765
|
+
if (ts.isIdentifier(callee) && callee.text === enqName) return {
|
|
1766
|
+
type: "xstate.expr",
|
|
1767
|
+
params: {
|
|
1768
|
+
code: expression.getText(),
|
|
1769
|
+
lang: "ts"
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
if (!ts.isPropertyAccessExpression(callee)) return null;
|
|
1773
|
+
if (!ts.isIdentifier(callee.expression) || callee.expression.text !== enqName) return null;
|
|
1774
|
+
const method = callee.name.text;
|
|
1775
|
+
const args = expression.arguments;
|
|
1776
|
+
const value = (index) => {
|
|
1777
|
+
const arg = args[index];
|
|
1778
|
+
return arg && ts.isExpression(arg) ? expressionToValue(arg, bindings, {}, 0) : void 0;
|
|
1779
|
+
};
|
|
1780
|
+
const spreadOpts = (index) => {
|
|
1781
|
+
const opts = value(index);
|
|
1782
|
+
return opts && typeof opts === "object" && !Array.isArray(opts) ? opts : {};
|
|
1783
|
+
};
|
|
1784
|
+
if (method === "emit") return {
|
|
1785
|
+
type: "xstate.emit",
|
|
1786
|
+
params: { event: value(0) }
|
|
1787
|
+
};
|
|
1788
|
+
if (method === "raise") return {
|
|
1789
|
+
type: "xstate.raise",
|
|
1790
|
+
params: {
|
|
1791
|
+
event: value(0),
|
|
1792
|
+
...spreadOpts(1)
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
if (method === "sendTo") return {
|
|
1796
|
+
type: "xstate.sendTo",
|
|
1797
|
+
params: {
|
|
1798
|
+
to: value(0),
|
|
1799
|
+
event: value(1),
|
|
1800
|
+
...spreadOpts(2)
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
if (method === "cancel") return {
|
|
1804
|
+
type: "xstate.cancel",
|
|
1805
|
+
params: { sendId: value(0) }
|
|
1806
|
+
};
|
|
1807
|
+
if (method === "log") return {
|
|
1808
|
+
type: "xstate.log",
|
|
1809
|
+
params: { label: value(0) }
|
|
1810
|
+
};
|
|
1811
|
+
if (method === "spawn") return {
|
|
1812
|
+
type: "xstate.spawnChild",
|
|
1813
|
+
params: {
|
|
1814
|
+
src: value(0),
|
|
1815
|
+
...spreadOpts(1)
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
if (method === "stop") return {
|
|
1819
|
+
type: "xstate.stopChild",
|
|
1820
|
+
params: { actorRef: value(0) }
|
|
1821
|
+
};
|
|
1822
|
+
return {
|
|
1823
|
+
type: "xstate.expr",
|
|
1824
|
+
params: {
|
|
1825
|
+
code: expression.getText(),
|
|
1826
|
+
lang: "ts"
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
function statementBranches(statements, bindings, enqName, prefixes = [{
|
|
1831
|
+
conds: [],
|
|
1832
|
+
actions: []
|
|
1833
|
+
}]) {
|
|
1834
|
+
let open = prefixes;
|
|
1835
|
+
const done = [];
|
|
1836
|
+
for (const statement of statements) {
|
|
1837
|
+
const nextOpen = [];
|
|
1838
|
+
for (const prefix of open) {
|
|
1839
|
+
if (ts.isReturnStatement(statement)) {
|
|
1840
|
+
if (!statement.expression) {
|
|
1841
|
+
done.push({
|
|
1842
|
+
...prefix,
|
|
1843
|
+
result: void 0
|
|
1844
|
+
});
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
for (const branch of expressionBranches(statement.expression, bindings, 0)) done.push(mergeBranchPrefix(prefix, branch));
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
if (ts.isExpressionStatement(statement)) {
|
|
1851
|
+
const action = expressionStatementAction(statement, enqName, bindings);
|
|
1852
|
+
if (action) {
|
|
1853
|
+
nextOpen.push({
|
|
1854
|
+
...prefix,
|
|
1855
|
+
actions: [...prefix.actions, action]
|
|
1856
|
+
});
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (ts.isIfStatement(statement)) {
|
|
1861
|
+
const cond = statement.expression.getText();
|
|
1862
|
+
const thenBranches = statementBranches(ts.isBlock(statement.thenStatement) ? statement.thenStatement.statements : [statement.thenStatement], bindings, enqName, [{
|
|
1863
|
+
...prefix,
|
|
1864
|
+
conds: [...prefix.conds, { text: cond }]
|
|
1865
|
+
}]);
|
|
1866
|
+
const elseStatements = statement.elseStatement ? ts.isBlock(statement.elseStatement) ? statement.elseStatement.statements : [statement.elseStatement] : [];
|
|
1867
|
+
const elseBranches = elseStatements.length > 0 ? statementBranches(elseStatements, bindings, enqName, [{
|
|
1868
|
+
...prefix,
|
|
1869
|
+
conds: [...prefix.conds, {
|
|
1870
|
+
text: cond,
|
|
1871
|
+
negated: true
|
|
1872
|
+
}]
|
|
1873
|
+
}]) : [{
|
|
1874
|
+
...prefix,
|
|
1875
|
+
conds: [...prefix.conds, {
|
|
1876
|
+
text: cond,
|
|
1877
|
+
negated: true
|
|
1878
|
+
}]
|
|
1879
|
+
}];
|
|
1880
|
+
for (const branch of [...thenBranches, ...elseBranches]) if ("result" in branch) done.push(branch);
|
|
1881
|
+
else nextOpen.push(branch);
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
if (ts.isSwitchStatement(statement)) {
|
|
1885
|
+
const expr = statement.expression.getText();
|
|
1886
|
+
const prior = [];
|
|
1887
|
+
for (const clause of statement.caseBlock.clauses) {
|
|
1888
|
+
const ownCond = ts.isCaseClause(clause) ? { text: `${expr} === ${clause.expression.getText()}` } : null;
|
|
1889
|
+
const conds = ownCond ? [...prior, ownCond] : prior.length ? [...prior] : [];
|
|
1890
|
+
const branches = statementBranches(clause.statements, bindings, enqName, [{
|
|
1891
|
+
...prefix,
|
|
1892
|
+
conds: [...prefix.conds, ...conds]
|
|
1893
|
+
}]);
|
|
1894
|
+
for (const branch of branches) if ("result" in branch) done.push(branch);
|
|
1895
|
+
else nextOpen.push(branch);
|
|
1896
|
+
if (ownCond) prior.push({
|
|
1897
|
+
...ownCond,
|
|
1898
|
+
negated: true
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
nextOpen.push({
|
|
1904
|
+
...prefix,
|
|
1905
|
+
unknown: true
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
open = nextOpen;
|
|
1909
|
+
}
|
|
1910
|
+
return [...done, ...open];
|
|
1911
|
+
}
|
|
1912
|
+
function enqueueParamName(fn) {
|
|
1913
|
+
const param = fn.parameters[1];
|
|
1914
|
+
return param && ts.isIdentifier(param.name) ? param.name.text : "enq";
|
|
1915
|
+
}
|
|
1916
|
+
function normalizeFunctionTransition(fn, bindings, depth) {
|
|
1917
|
+
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) => {
|
|
1918
|
+
const transition = transitionFromValue(branch.result, branch.actions);
|
|
1919
|
+
if (!transition) return null;
|
|
1920
|
+
if (typeof transition === "string") {
|
|
1921
|
+
const guard = guardFromConditions(branch.conds);
|
|
1922
|
+
return guard ? {
|
|
1923
|
+
target: transition,
|
|
1924
|
+
guard
|
|
1925
|
+
} : transition;
|
|
1926
|
+
}
|
|
1927
|
+
const guard = guardFromConditions(branch.conds);
|
|
1928
|
+
return guard ? {
|
|
1929
|
+
...transition,
|
|
1930
|
+
guard
|
|
1931
|
+
} : transition;
|
|
1932
|
+
}).filter((transition) => Boolean(transition));
|
|
1933
|
+
if (transitions.length === 1) return transitions[0];
|
|
1934
|
+
if (transitions.length > 1) return transitions;
|
|
1935
|
+
return fallbackFunctionTransition(fn);
|
|
1936
|
+
}
|
|
1937
|
+
function inlineAction(fn) {
|
|
1938
|
+
return {
|
|
1939
|
+
type: "xstate.expr",
|
|
1940
|
+
params: {
|
|
1941
|
+
code: fn.getText(),
|
|
1942
|
+
lang: "ts"
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function normalizeFunctionAction(fn, bindings) {
|
|
1947
|
+
if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) return inlineAction(fn);
|
|
1948
|
+
if (!fn.body || !ts.isBlock(fn.body)) return inlineAction(fn);
|
|
1949
|
+
const branches = statementBranches(fn.body.statements, bindings, enqueueParamName(fn));
|
|
1950
|
+
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;
|
|
1951
|
+
return inlineAction(fn);
|
|
1952
|
+
}
|
|
1953
|
+
function objectLiteralToValue(node, bindings, context, depth, overrides = /* @__PURE__ */ new Map()) {
|
|
1954
|
+
const result = {};
|
|
1955
|
+
for (const property of node.properties) {
|
|
1956
|
+
if (ts.isSpreadAssignment(property)) {
|
|
1957
|
+
const spread = expressionToValue(property.expression, bindings, context, depth + 1);
|
|
1958
|
+
if (spread && typeof spread === "object" && !Array.isArray(spread)) Object.assign(result, spread);
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) continue;
|
|
1962
|
+
const key = propName(property.name);
|
|
1963
|
+
if (!key) continue;
|
|
1964
|
+
if (overrides.has(key)) {
|
|
1965
|
+
result[key] = overrides.get(key);
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1968
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
1969
|
+
result[key] = expressionToValue(property.name, bindings, propertyContext(context, key), depth + 1);
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
result[key] = expressionToValue(property.initializer, bindings, propertyContext(context, key), depth + 1);
|
|
1973
|
+
}
|
|
1974
|
+
if (context.transitionValue && "to" in result) {
|
|
1975
|
+
const to = result.to;
|
|
1976
|
+
const { to: _to, ...rest } = result;
|
|
1977
|
+
if (typeof to === "string") return {
|
|
1978
|
+
...rest,
|
|
1979
|
+
target: to
|
|
1980
|
+
};
|
|
1981
|
+
if (Array.isArray(to)) return to.map((item) => typeof item === "string" ? {
|
|
1982
|
+
...rest,
|
|
1983
|
+
target: item
|
|
1984
|
+
} : item && typeof item === "object" ? {
|
|
1985
|
+
...rest,
|
|
1986
|
+
...item
|
|
1987
|
+
} : rest);
|
|
1988
|
+
if (to && typeof to === "object" && !Array.isArray(to)) return {
|
|
1989
|
+
...rest,
|
|
1990
|
+
...to
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
return result;
|
|
1994
|
+
}
|
|
1995
|
+
function expressionToValue(expression, bindings, context = {}, depth = 0) {
|
|
1996
|
+
if (depth > 30) return void 0;
|
|
1997
|
+
const node = unwrapExpression$2(expression);
|
|
1998
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
1999
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
2000
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
2001
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
2002
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
2003
|
+
if (ts.isIdentifier(node)) {
|
|
2004
|
+
if (node.text === "undefined") return void 0;
|
|
2005
|
+
const binding = bindings.get(node.text);
|
|
2006
|
+
return binding ? expressionToValue(binding, bindings, context, depth + 1) : node.text;
|
|
2007
|
+
}
|
|
2008
|
+
if (ts.isObjectLiteralExpression(node)) return objectLiteralToValue(node, bindings, context, depth);
|
|
2009
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
2010
|
+
const values = node.elements.map((element) => expressionToValue(element, bindings, context, depth + 1));
|
|
2011
|
+
return context.actionValue ? values.flat() : values;
|
|
2012
|
+
}
|
|
2013
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
2014
|
+
if (context.v6 && context.transitionValue) return normalizeFunctionTransition(node, bindings, depth);
|
|
2015
|
+
if (context.v6 && context.actionValue) return normalizeFunctionAction(node, bindings);
|
|
2016
|
+
return node.getText();
|
|
2017
|
+
}
|
|
2018
|
+
if (ts.isCallExpression(node)) {
|
|
2019
|
+
if (context.actionValue) return inlineAction(node);
|
|
2020
|
+
const callee = node.expression.getText();
|
|
2021
|
+
const type = callee.includes(".") ? callee.split(".").pop() : callee;
|
|
2022
|
+
return { type: type === "assign" ? "xstate.assign" : type };
|
|
2023
|
+
}
|
|
2024
|
+
return node.getText();
|
|
2025
|
+
}
|
|
2026
|
+
function collectBindings$1(file) {
|
|
2027
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
2028
|
+
for (const statement of file.statements) {
|
|
2029
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
2030
|
+
for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.initializer) bindings.set(declaration.name.text, declaration.initializer);
|
|
2031
|
+
}
|
|
2032
|
+
return bindings;
|
|
2033
|
+
}
|
|
2034
|
+
function isCreateMachineCall(node) {
|
|
2035
|
+
const expression = node.expression;
|
|
2036
|
+
if (ts.isIdentifier(expression)) return expression.text === "createMachine";
|
|
2037
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text === "createMachine";
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
function hasV6Syntax(source) {
|
|
2041
|
+
const file = sourceFile(source);
|
|
2042
|
+
let found = false;
|
|
2043
|
+
function visit(node) {
|
|
2044
|
+
if (found) return;
|
|
2045
|
+
if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))) {
|
|
2046
|
+
const key = node.name.text;
|
|
2047
|
+
if (key === "triggers" || key === "internalEvents" || key === "timeout" || key === "onTimeout" || key === "choice" || key === "route") {
|
|
2048
|
+
found = true;
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if ((key === "on" || key === "after" || key === "always" || key === "entry" || key === "exit" || key === "actions" && !ts.isObjectLiteralExpression(node.initializer)) && node.initializer.getText().includes("=>")) {
|
|
2052
|
+
found = true;
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
ts.forEachChild(node, visit);
|
|
2057
|
+
}
|
|
2058
|
+
visit(file);
|
|
2059
|
+
return found;
|
|
2060
|
+
}
|
|
2061
|
+
function detectXStateSourceProfile(source) {
|
|
2062
|
+
return hasV6Syntax(source) ? "xstate-v6-alpha" : "xstate-v5";
|
|
2063
|
+
}
|
|
2064
|
+
function xstateSourceToConfig(source, options = {}) {
|
|
2065
|
+
const file = sourceFile(source);
|
|
2066
|
+
const bindings = collectBindings$1(file);
|
|
2067
|
+
let found = null;
|
|
2068
|
+
const profile = options.profile === "auto" || !options.profile ? detectXStateSourceProfile(source) : options.profile;
|
|
2069
|
+
function visit(node) {
|
|
2070
|
+
if (found || !ts.isCallExpression(node) || !isCreateMachineCall(node)) {
|
|
2071
|
+
ts.forEachChild(node, visit);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const arg = node.arguments[0];
|
|
2075
|
+
if (!arg) return;
|
|
2076
|
+
const value = expressionToValue(arg, bindings, { v6: profile === "xstate-v6-alpha" });
|
|
2077
|
+
if (value && typeof value === "object" && !Array.isArray(value)) found = value;
|
|
2078
|
+
}
|
|
2079
|
+
visit(file);
|
|
2080
|
+
if (!found) throw new Error("No createMachine(...) or setup(...).createMachine(...) call found.");
|
|
2081
|
+
return found;
|
|
2082
|
+
}
|
|
2083
|
+
function parseJsonLike(input) {
|
|
2084
|
+
return JSON.parse(input);
|
|
2085
|
+
}
|
|
2086
|
+
function parseSimpleYaml(input) {
|
|
2087
|
+
const lines = input.split(/\r?\n/).filter((line) => line.trim() && !line.trim().startsWith("#"));
|
|
2088
|
+
const root = {};
|
|
2089
|
+
const stack = [{
|
|
2090
|
+
indent: -1,
|
|
2091
|
+
value: root
|
|
2092
|
+
}];
|
|
2093
|
+
for (const line of lines) {
|
|
2094
|
+
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
2095
|
+
const trimmed = line.trim();
|
|
2096
|
+
const match = /^([^:]+):(.*)$/.exec(trimmed);
|
|
2097
|
+
if (!match) continue;
|
|
2098
|
+
const key = match[1].trim();
|
|
2099
|
+
const raw = match[2].trim();
|
|
2100
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
|
|
2101
|
+
const parent = stack[stack.length - 1].value;
|
|
2102
|
+
if (!raw) {
|
|
2103
|
+
const child = {};
|
|
2104
|
+
parent[key] = child;
|
|
2105
|
+
stack.push({
|
|
2106
|
+
indent,
|
|
2107
|
+
value: child
|
|
2108
|
+
});
|
|
2109
|
+
} else if (raw === "true" || raw === "false") parent[key] = raw === "true";
|
|
2110
|
+
else if (/^-?\d+(?:\.\d+)?$/.test(raw)) parent[key] = Number(raw);
|
|
2111
|
+
else parent[key] = raw.replace(/^['"]|['"]$/g, "");
|
|
2112
|
+
}
|
|
2113
|
+
return root;
|
|
2114
|
+
}
|
|
2115
|
+
function parseMermaid(input) {
|
|
2116
|
+
const states = /* @__PURE__ */ new Set();
|
|
2117
|
+
const transitions = [];
|
|
2118
|
+
let initial;
|
|
2119
|
+
for (const line of input.split(/\r?\n/)) {
|
|
2120
|
+
const trimmed = line.trim();
|
|
2121
|
+
if (!trimmed || /^(stateDiagram|stateDiagram-v2|flowchart|graph)\b/.test(trimmed)) continue;
|
|
2122
|
+
const stateMatch = /^\[\*\]\s*-->\s*([A-Za-z0-9_.-]+)/.exec(trimmed);
|
|
2123
|
+
if (stateMatch?.[1]) {
|
|
2124
|
+
initial = stateMatch[1];
|
|
2125
|
+
states.add(initial);
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
const transitionMatch = /^([A-Za-z0-9_.-]+)\s*(?:-->|--)\s*(?:\|([^|]+)\|)?\s*([A-Za-z0-9_.-]+)(?:\s*:\s*(.+))?/.exec(trimmed);
|
|
2129
|
+
if (!transitionMatch) continue;
|
|
2130
|
+
const source = transitionMatch[1];
|
|
2131
|
+
const target = transitionMatch[3];
|
|
2132
|
+
const event = transitionMatch[2] ?? transitionMatch[4];
|
|
2133
|
+
states.add(source);
|
|
2134
|
+
states.add(target);
|
|
2135
|
+
transitions.push({
|
|
2136
|
+
source,
|
|
2137
|
+
target,
|
|
2138
|
+
event: event?.trim()
|
|
2139
|
+
});
|
|
2140
|
+
initial ??= source;
|
|
2141
|
+
}
|
|
2142
|
+
const stateConfigs = {};
|
|
2143
|
+
for (const state of states) stateConfigs[state] = {};
|
|
2144
|
+
for (const transition of transitions) {
|
|
2145
|
+
const source = stateConfigs[transition.source];
|
|
2146
|
+
const on = source.on ??= {};
|
|
2147
|
+
on[transition.event || "NEXT"] = transition.target;
|
|
2148
|
+
}
|
|
2149
|
+
return {
|
|
2150
|
+
id: "machine",
|
|
2151
|
+
initial: initial ?? [...states][0] ?? "idle",
|
|
2152
|
+
states: Object.keys(stateConfigs).length ? stateConfigs : { idle: {} }
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
function inferMachineFormat(machine, format) {
|
|
2156
|
+
if (format) return format;
|
|
2157
|
+
if (typeof machine !== "string") return "json";
|
|
2158
|
+
const trimmed = machine.trim();
|
|
2159
|
+
if (trimmed.startsWith("{")) return "json";
|
|
2160
|
+
if (/^(stateDiagram|flowchart|graph)\b/m.test(trimmed)) return "mermaid";
|
|
2161
|
+
if (/^<\?xml|^<scxml/m.test(trimmed)) return "scxml";
|
|
2162
|
+
if (/^\w+:/m.test(trimmed)) return "yaml";
|
|
2163
|
+
return "xstate";
|
|
2164
|
+
}
|
|
2165
|
+
function machineInputToConfig(input) {
|
|
2166
|
+
const format = inferMachineFormat(input.machine, input.format);
|
|
2167
|
+
if (typeof input.machine !== "string") {
|
|
2168
|
+
createMachine(input.machine);
|
|
2169
|
+
return input.machine;
|
|
2170
|
+
}
|
|
2171
|
+
if (format === "json") {
|
|
2172
|
+
const config = parseJsonLike(input.machine);
|
|
2173
|
+
createMachine(config);
|
|
2174
|
+
return config;
|
|
2175
|
+
}
|
|
2176
|
+
if (format === "yaml") {
|
|
2177
|
+
const config = parseSimpleYaml(input.machine);
|
|
2178
|
+
createMachine(config);
|
|
2179
|
+
return config;
|
|
2180
|
+
}
|
|
2181
|
+
if (format === "mermaid") {
|
|
2182
|
+
const config = parseMermaid(input.machine);
|
|
2183
|
+
createMachine(config);
|
|
2184
|
+
return config;
|
|
2185
|
+
}
|
|
2186
|
+
if (format === "xstate") {
|
|
2187
|
+
const config = xstateSourceToConfig(input.machine, { profile: input.profile ?? (input.xstateVersion === 6 ? "xstate-v6-alpha" : input.xstateVersion === 5 ? "xstate-v5" : "auto") });
|
|
2188
|
+
createMachine(config);
|
|
2189
|
+
return config;
|
|
2190
|
+
}
|
|
2191
|
+
throw new Error(`${format} source parsing is not supported by graph-tools v1 yet.`);
|
|
2192
|
+
}
|
|
2193
|
+
function machineInputToGraph(input) {
|
|
2194
|
+
return machineConfigToGraph(machineInputToConfig(input));
|
|
2195
|
+
}
|
|
2196
|
+
function graphToMermaid(graph) {
|
|
2197
|
+
const machine = graphToMachineConfig(graph);
|
|
2198
|
+
const lines = ["stateDiagram-v2"];
|
|
2199
|
+
if (machine.initial) lines.push(` [*] --> ${machine.initial}`);
|
|
2200
|
+
for (const [stateKey, state] of Object.entries(machine.states ?? {})) for (const [event, transition] of Object.entries(state.on ?? {})) {
|
|
2201
|
+
const target = typeof transition === "string" ? transition : Array.isArray(transition) ? void 0 : transition.target;
|
|
2202
|
+
if (target) lines.push(` ${stateKey} --> ${target} : ${event}`);
|
|
2203
|
+
}
|
|
2204
|
+
return lines.join("\n");
|
|
2205
|
+
}
|
|
2206
|
+
function toYaml(value, indent = 0) {
|
|
2207
|
+
const pad = " ".repeat(indent);
|
|
2208
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return `${JSON.stringify(value)}`;
|
|
2209
|
+
return Object.entries(value).map(([key, item]) => {
|
|
2210
|
+
if (item && typeof item === "object" && !Array.isArray(item)) return `${pad}${key}:\n${toYaml(item, indent + 2)}`;
|
|
2211
|
+
return `${pad}${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`;
|
|
2212
|
+
}).join("\n");
|
|
2213
|
+
}
|
|
2214
|
+
function graphToFormat(graph, format) {
|
|
2215
|
+
const config = graphToMachineConfig(graph);
|
|
2216
|
+
if (format === "xstate") return `import { createMachine } from 'xstate';\n\nexport const machine = createMachine(${serializeJS(config, 2)});\n`;
|
|
2217
|
+
if (format === "json") return config;
|
|
2218
|
+
if (format === "yaml") return toYaml(config);
|
|
2219
|
+
if (format === "mermaid") return graphToMermaid(graph);
|
|
2220
|
+
if (format === "scxml") {
|
|
2221
|
+
const machine = config;
|
|
2222
|
+
return `<scxml version="1.0" initial="${machine.initial ?? ""}" name="${machine.id ?? "machine"}"></scxml>`;
|
|
2223
|
+
}
|
|
2224
|
+
return config;
|
|
2225
|
+
}
|
|
2226
|
+
const colorSchema = _enum([
|
|
2227
|
+
"green",
|
|
2228
|
+
"red",
|
|
2229
|
+
"purple",
|
|
2230
|
+
"blue",
|
|
2231
|
+
"orange",
|
|
2232
|
+
"yellow",
|
|
2233
|
+
"pink",
|
|
2234
|
+
"teal"
|
|
2235
|
+
]);
|
|
2236
|
+
const stateTypeSchema = _enum([
|
|
2237
|
+
"normal",
|
|
2238
|
+
"parallel",
|
|
2239
|
+
"history",
|
|
2240
|
+
"final"
|
|
2241
|
+
]).nullable();
|
|
2242
|
+
const actionLocationSchema = union([object({
|
|
2243
|
+
nodeId: string(),
|
|
2244
|
+
group: _enum(["entry", "exit"])
|
|
2245
|
+
}), object({
|
|
2246
|
+
edgeId: string(),
|
|
2247
|
+
group: literal("transition")
|
|
2248
|
+
})]);
|
|
2249
|
+
const createStatePatchSchema = object({
|
|
2250
|
+
op: literal("createState"),
|
|
2251
|
+
description: string().optional(),
|
|
2252
|
+
id: string().optional(),
|
|
2253
|
+
parentId: string(),
|
|
2254
|
+
key: string(),
|
|
2255
|
+
type: stateTypeSchema.optional(),
|
|
2256
|
+
x: number().optional(),
|
|
2257
|
+
y: number().optional(),
|
|
2258
|
+
color: colorSchema.nullable().optional(),
|
|
2259
|
+
initial: boolean().optional()
|
|
2260
|
+
});
|
|
2261
|
+
const updateStatePatchSchema = object({
|
|
2262
|
+
op: literal("updateState"),
|
|
2263
|
+
description: string().optional(),
|
|
2264
|
+
stateId: string(),
|
|
2265
|
+
key: string().optional(),
|
|
2266
|
+
type: stateTypeSchema.optional(),
|
|
2267
|
+
stateDescription: string().optional(),
|
|
2268
|
+
initialId: string().nullable().optional(),
|
|
2269
|
+
color: colorSchema.nullable().optional(),
|
|
2270
|
+
meta: record(string(), unknown()).nullable().optional(),
|
|
2271
|
+
history: _enum(["shallow", "deep"]).nullable().optional()
|
|
2272
|
+
});
|
|
2273
|
+
const deleteStatePatchSchema = object({
|
|
2274
|
+
op: literal("deleteState"),
|
|
2275
|
+
description: string().optional(),
|
|
2276
|
+
stateId: string()
|
|
2277
|
+
});
|
|
2278
|
+
const guardSchema = object({
|
|
2279
|
+
type: string(),
|
|
2280
|
+
code: string().optional(),
|
|
2281
|
+
params: record(string(), unknown()).optional()
|
|
2282
|
+
});
|
|
2283
|
+
const createTransitionPatchSchema = object({
|
|
2284
|
+
op: literal("createTransition"),
|
|
2285
|
+
description: string().optional(),
|
|
2286
|
+
id: string().optional(),
|
|
2287
|
+
sourceId: string(),
|
|
2288
|
+
targetId: string(),
|
|
2289
|
+
eventType: string(),
|
|
2290
|
+
transitionType: _enum([
|
|
2291
|
+
"normal",
|
|
2292
|
+
"targetless",
|
|
2293
|
+
"reenter"
|
|
2294
|
+
]).optional(),
|
|
2295
|
+
guard: guardSchema.optional(),
|
|
2296
|
+
color: colorSchema.nullable().optional(),
|
|
2297
|
+
transitionDescription: string().nullable().optional()
|
|
2298
|
+
});
|
|
2299
|
+
const updateTransitionPatchSchema = object({
|
|
2300
|
+
op: literal("updateTransition"),
|
|
2301
|
+
description: string().optional(),
|
|
2302
|
+
transitionId: string(),
|
|
2303
|
+
sourceId: string().optional(),
|
|
2304
|
+
targetId: string().optional(),
|
|
2305
|
+
eventType: string().optional(),
|
|
2306
|
+
transitionType: _enum([
|
|
2307
|
+
"normal",
|
|
2308
|
+
"targetless",
|
|
2309
|
+
"reenter"
|
|
2310
|
+
]).optional(),
|
|
2311
|
+
transitionDescription: string().nullable().optional(),
|
|
2312
|
+
color: colorSchema.nullable().optional(),
|
|
2313
|
+
meta: record(string(), unknown()).nullable().optional()
|
|
2314
|
+
});
|
|
2315
|
+
const deleteTransitionPatchSchema = object({
|
|
2316
|
+
op: literal("deleteTransition"),
|
|
2317
|
+
description: string().optional(),
|
|
2318
|
+
transitionId: string()
|
|
2319
|
+
});
|
|
2320
|
+
const createActionPatchSchema = object({
|
|
2321
|
+
op: literal("createAction"),
|
|
2322
|
+
description: string().optional(),
|
|
2323
|
+
location: actionLocationSchema,
|
|
2324
|
+
action: object({
|
|
2325
|
+
id: string().optional(),
|
|
2326
|
+
type: string(),
|
|
2327
|
+
params: record(string(), unknown()).optional()
|
|
2328
|
+
}),
|
|
2329
|
+
index: number().optional()
|
|
2330
|
+
});
|
|
2331
|
+
const updateActionPatchSchema = object({
|
|
2332
|
+
op: literal("updateAction"),
|
|
2333
|
+
description: string().optional(),
|
|
2334
|
+
location: actionLocationSchema,
|
|
2335
|
+
actionId: string(),
|
|
2336
|
+
data: object({
|
|
2337
|
+
type: string().optional(),
|
|
2338
|
+
params: record(string(), unknown()).optional()
|
|
2339
|
+
})
|
|
2340
|
+
});
|
|
2341
|
+
const deleteActionPatchSchema = object({
|
|
2342
|
+
op: literal("deleteAction"),
|
|
2343
|
+
description: string().optional(),
|
|
2344
|
+
location: actionLocationSchema,
|
|
2345
|
+
actionId: string()
|
|
2346
|
+
});
|
|
2347
|
+
const setGuardPatchSchema = object({
|
|
2348
|
+
op: literal("setGuard"),
|
|
2349
|
+
description: string().optional(),
|
|
2350
|
+
edgeId: string(),
|
|
2351
|
+
guard: guardSchema
|
|
2352
|
+
});
|
|
2353
|
+
const deleteGuardPatchSchema = object({
|
|
2354
|
+
op: literal("deleteGuard"),
|
|
2355
|
+
description: string().optional(),
|
|
2356
|
+
edgeId: string()
|
|
2357
|
+
});
|
|
2358
|
+
const machinePatchSchema = discriminatedUnion("op", [
|
|
2359
|
+
createStatePatchSchema,
|
|
2360
|
+
updateStatePatchSchema,
|
|
2361
|
+
deleteStatePatchSchema,
|
|
2362
|
+
createTransitionPatchSchema,
|
|
2363
|
+
updateTransitionPatchSchema,
|
|
2364
|
+
deleteTransitionPatchSchema,
|
|
2365
|
+
createActionPatchSchema,
|
|
2366
|
+
updateActionPatchSchema,
|
|
2367
|
+
deleteActionPatchSchema,
|
|
2368
|
+
setGuardPatchSchema,
|
|
2369
|
+
deleteGuardPatchSchema
|
|
2370
|
+
]);
|
|
2371
|
+
const validationLevelSchema = _enum([
|
|
2372
|
+
"info",
|
|
2373
|
+
"warning",
|
|
2374
|
+
"error"
|
|
2375
|
+
]);
|
|
2376
|
+
const validationIssueKindSchema = _enum([
|
|
2377
|
+
"correctness",
|
|
2378
|
+
"reachability",
|
|
2379
|
+
"optimization",
|
|
2380
|
+
"maintainability",
|
|
2381
|
+
"security"
|
|
2382
|
+
]);
|
|
2383
|
+
const validationIssueCodeSchema = _enum([
|
|
2384
|
+
"parse_error",
|
|
2385
|
+
"unsupported_format",
|
|
2386
|
+
"empty_state_key",
|
|
2387
|
+
"invalid_state_key",
|
|
2388
|
+
"duplicate_state_key",
|
|
2389
|
+
"history_initial",
|
|
2390
|
+
"missing_initial",
|
|
2391
|
+
"invalid_initial",
|
|
2392
|
+
"unreachable_state",
|
|
2393
|
+
"final_state_invokes",
|
|
2394
|
+
"invalid_state_children",
|
|
2395
|
+
"empty_parallel",
|
|
2396
|
+
"single_child_compound",
|
|
2397
|
+
"noop_transient_state",
|
|
2398
|
+
"on_done_unreachable",
|
|
2399
|
+
"duplicate_transition",
|
|
2400
|
+
"invalid_delay",
|
|
2401
|
+
"repeated_guard",
|
|
2402
|
+
"transition_never_taken",
|
|
2403
|
+
"missing_guard",
|
|
2404
|
+
"redundant_transition",
|
|
2405
|
+
"missing_delay_implementation",
|
|
2406
|
+
"invalid_final_transition",
|
|
2407
|
+
"invalid_history_transition",
|
|
2408
|
+
"parallel_region_transition",
|
|
2409
|
+
"duplicate_source_name",
|
|
2410
|
+
"undefined_action",
|
|
2411
|
+
"undefined_guard",
|
|
2412
|
+
"undefined_actor"
|
|
2413
|
+
]);
|
|
2414
|
+
const validationFixKindSchema = _enum([
|
|
2415
|
+
"patch",
|
|
2416
|
+
"rewrite",
|
|
2417
|
+
"advice",
|
|
2418
|
+
"proposedPatch"
|
|
2419
|
+
]);
|
|
2420
|
+
const validationFixSourceSchema = _enum(["deterministic", "llm"]);
|
|
2421
|
+
const AUTO_NAME_PREFIXES = [
|
|
2422
|
+
":invocation:",
|
|
2423
|
+
"inline:",
|
|
2424
|
+
"$auto-"
|
|
2425
|
+
];
|
|
2426
|
+
const AUTO_NAME_SUBSTRINGS = [
|
|
2427
|
+
":invocation[",
|
|
2428
|
+
"#transition[",
|
|
2429
|
+
"#actor["
|
|
2430
|
+
];
|
|
2431
|
+
function isAutoGeneratedInlineName(name) {
|
|
2432
|
+
return AUTO_NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) || AUTO_NAME_SUBSTRINGS.some((part) => name.includes(part));
|
|
2433
|
+
}
|
|
2434
|
+
function getImplementations(graph) {
|
|
2435
|
+
return graph.data.implementations ?? {
|
|
2436
|
+
actions: [],
|
|
2437
|
+
guards: [],
|
|
2438
|
+
actors: [],
|
|
2439
|
+
delays: []
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function issue(input) {
|
|
2443
|
+
const nodeIds = input.nodeIds ?? [];
|
|
2444
|
+
const edgeIds = input.edgeIds ?? [];
|
|
2445
|
+
return {
|
|
2446
|
+
code: input.code,
|
|
2447
|
+
kind: input.kind,
|
|
2448
|
+
level: input.level,
|
|
2449
|
+
message: input.message,
|
|
2450
|
+
target: {
|
|
2451
|
+
nodeIds,
|
|
2452
|
+
edgeIds
|
|
2453
|
+
},
|
|
2454
|
+
metadata: input.metadata,
|
|
2455
|
+
fixes: input.fixes,
|
|
2456
|
+
source: "deterministic",
|
|
2457
|
+
nodeIds,
|
|
2458
|
+
edgeIds
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
function deleteTransitionFix(edge, title) {
|
|
2462
|
+
return {
|
|
2463
|
+
kind: "patch",
|
|
2464
|
+
source: "deterministic",
|
|
2465
|
+
title,
|
|
2466
|
+
patches: [{
|
|
2467
|
+
op: "deleteTransition",
|
|
2468
|
+
transitionId: edge.id
|
|
2469
|
+
}],
|
|
2470
|
+
confidence: .9
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
function setInitialFix(node, child, title) {
|
|
2474
|
+
return {
|
|
2475
|
+
kind: "patch",
|
|
2476
|
+
source: "deterministic",
|
|
2477
|
+
title,
|
|
2478
|
+
patches: [{
|
|
2479
|
+
op: "updateState",
|
|
2480
|
+
stateId: node.id,
|
|
2481
|
+
initialId: child.id
|
|
2482
|
+
}],
|
|
2483
|
+
confidence: .85
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
function findDuplicateNodes(graph, node, parent) {
|
|
2487
|
+
if (!parent) return [];
|
|
2488
|
+
return graph.nodes.filter((n) => n.parentId === parent.id && n.id !== node.id).filter((childNode) => childNode.data.key === node.data.key);
|
|
2489
|
+
}
|
|
2490
|
+
function findDuplicateEdges(graph, edge) {
|
|
2491
|
+
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);
|
|
2492
|
+
}
|
|
2493
|
+
function hasUnconditionalAlways(graph, sourceId) {
|
|
2494
|
+
return graph.edges.some((e) => e.sourceId === sourceId && e.data.eventType === "" && !e.data.guard && e.targetId !== e.sourceId);
|
|
2495
|
+
}
|
|
2496
|
+
function isTransitionPreempted(graph, edge) {
|
|
2497
|
+
if (edge.data.eventType === "") return false;
|
|
2498
|
+
return hasUnconditionalAlways(graph, edge.sourceId);
|
|
2499
|
+
}
|
|
2500
|
+
function computeReachableGraphNodes(graph) {
|
|
2501
|
+
const rootNode = graph.nodes.find((n) => n.parentId === null);
|
|
2502
|
+
if (!rootNode) return /* @__PURE__ */ new Set();
|
|
2503
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2504
|
+
function getNodeInitialStates(node) {
|
|
2505
|
+
const children = graph.nodes.filter((n) => n.parentId === node.id);
|
|
2506
|
+
if (!children.length) return [];
|
|
2507
|
+
if (node.data.type === "parallel") return children;
|
|
2508
|
+
const initial = children.find((n) => node.data.initialId === n.id);
|
|
2509
|
+
return initial ? [initial] : [];
|
|
2510
|
+
}
|
|
2511
|
+
function dfs(node) {
|
|
2512
|
+
if (visited.has(node.id)) return;
|
|
2513
|
+
visited.add(node.id);
|
|
2514
|
+
getNodeInitialStates(node).forEach((n) => dfs(n));
|
|
2515
|
+
if (node.data.type === "parallel") graph.nodes.filter((n) => n.parentId === node.id).forEach((region) => {
|
|
2516
|
+
getNodeInitialStates(region).forEach((n) => dfs(n));
|
|
2517
|
+
});
|
|
2518
|
+
if (node.data.type === "history") {
|
|
2519
|
+
const parent = graph.nodes.find((n) => n.id === node.parentId);
|
|
2520
|
+
if (parent) if (parent.data.type === "parallel") graph.nodes.filter((n) => n.parentId === parent.id).forEach((region) => {
|
|
2521
|
+
getNodeInitialStates(region).forEach((n) => dfs(n));
|
|
2522
|
+
});
|
|
2523
|
+
else getNodeInitialStates(parent).forEach((n) => dfs(n));
|
|
2524
|
+
}
|
|
2525
|
+
graph.edges.filter((edge) => edge.sourceId === node.id && !isTransitionPreempted(graph, edge)).forEach((edge) => {
|
|
2526
|
+
const target = graph.nodes.find((n) => n.id === edge.targetId);
|
|
2527
|
+
if (target) dfs(target);
|
|
2528
|
+
});
|
|
2529
|
+
let ancestor = graph.nodes.find((n) => n.id === node.parentId);
|
|
2530
|
+
while (ancestor) {
|
|
2531
|
+
graph.edges.filter((edge) => edge.sourceId === ancestor.id && !isTransitionPreempted(graph, edge)).forEach((edge) => {
|
|
2532
|
+
const target = graph.nodes.find((n) => n.id === edge.targetId);
|
|
2533
|
+
if (target) dfs(target);
|
|
2534
|
+
});
|
|
2535
|
+
ancestor = graph.nodes.find((n) => n.id === ancestor.parentId);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
dfs(rootNode);
|
|
2539
|
+
const reachableWithParents = new Set(visited);
|
|
2540
|
+
for (const nodeId of visited) {
|
|
2541
|
+
let node = graph.nodes.find((n) => n.id === nodeId);
|
|
2542
|
+
while (node?.parentId) {
|
|
2543
|
+
reachableWithParents.add(node.parentId);
|
|
2544
|
+
node = graph.nodes.find((n) => n.id === node.parentId);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return reachableWithParents;
|
|
2548
|
+
}
|
|
2549
|
+
function getNodeErrors(graph, node) {
|
|
2550
|
+
const errors = [];
|
|
2551
|
+
if (node.data.key.length === 0) errors.push({
|
|
2552
|
+
code: "empty_state_key",
|
|
2553
|
+
kind: "correctness",
|
|
2554
|
+
message: "State key cannot be empty"
|
|
2555
|
+
});
|
|
2556
|
+
const parent = graph.nodes.find((n) => n.id === node.parentId);
|
|
2557
|
+
const isInitial = parent?.data.initialId === node.id;
|
|
2558
|
+
const isHistoryNode = node.data.type === "history";
|
|
2559
|
+
if (isInitial && isHistoryNode) errors.push({
|
|
2560
|
+
code: "history_initial",
|
|
2561
|
+
kind: "correctness",
|
|
2562
|
+
message: "A history node cannot be the initial node. This will cause an infinite loop."
|
|
2563
|
+
});
|
|
2564
|
+
if (findDuplicateNodes(graph, node, parent).length > 0) errors.push({
|
|
2565
|
+
code: "duplicate_state_key",
|
|
2566
|
+
kind: "correctness",
|
|
2567
|
+
message: "A state with that name already exists"
|
|
2568
|
+
});
|
|
2569
|
+
if (node.data.key.includes("#")) errors.push({
|
|
2570
|
+
code: "invalid_state_key",
|
|
2571
|
+
kind: "correctness",
|
|
2572
|
+
message: "State key cannot contain \"#\"",
|
|
2573
|
+
metadata: { character: "#" }
|
|
2574
|
+
});
|
|
2575
|
+
const children = graph.nodes.filter((n) => n.parentId === node.id);
|
|
2576
|
+
if (children.length > 0 && node.data.type !== "parallel" && node.data.type !== "final" && node.data.type !== "history") {
|
|
2577
|
+
const isTargeted = graph.edges.some((edge) => edge.targetId === node.id);
|
|
2578
|
+
if (!node.data.initialId && isTargeted) errors.push({
|
|
2579
|
+
code: "missing_initial",
|
|
2580
|
+
kind: "correctness",
|
|
2581
|
+
message: "Compound state must have an initial state",
|
|
2582
|
+
metadata: {
|
|
2583
|
+
reason: "targeted_compound",
|
|
2584
|
+
childCount: children.length
|
|
2585
|
+
},
|
|
2586
|
+
fixes: children.length === 1 ? [setInitialFix(node, children[0], `Set ${children[0].data.key} as initial state`)] : void 0
|
|
2587
|
+
});
|
|
2588
|
+
else if (node.data.initialId) {
|
|
2589
|
+
if (!children.some((n) => n.id === node.data.initialId)) errors.push({
|
|
2590
|
+
code: "invalid_initial",
|
|
2591
|
+
kind: "correctness",
|
|
2592
|
+
message: "Initial state does not exist",
|
|
2593
|
+
metadata: {
|
|
2594
|
+
initialId: node.data.initialId,
|
|
2595
|
+
childCount: children.length
|
|
2596
|
+
},
|
|
2597
|
+
fixes: children.length === 1 ? [setInitialFix(node, children[0], `Set ${children[0].data.key} as initial state`)] : void 0
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return errors.length > 0 ? errors : null;
|
|
2602
|
+
}
|
|
2603
|
+
function getNodeWarningsWithReachability(graph, node, reachableNodes) {
|
|
2604
|
+
const warnings = [];
|
|
2605
|
+
if (!reachableNodes.has(node.id) && node.data.type !== "history") warnings.push({
|
|
2606
|
+
code: "unreachable_state",
|
|
2607
|
+
kind: "reachability",
|
|
2608
|
+
message: "Unreachable state"
|
|
2609
|
+
});
|
|
2610
|
+
if (node.data.type === "final" && (node.data.invokes?.length ?? 0) > 0) warnings.push({
|
|
2611
|
+
code: "final_state_invokes",
|
|
2612
|
+
kind: "correctness",
|
|
2613
|
+
message: "Final state cannot have invocations"
|
|
2614
|
+
});
|
|
2615
|
+
if (node.data.type && ["final", "history"].includes(node.data.type) && graph.nodes.filter((n) => n.parentId === node.id).length > 0) warnings.push({
|
|
2616
|
+
code: "invalid_state_children",
|
|
2617
|
+
kind: "correctness",
|
|
2618
|
+
message: `${node.data.type.charAt(0).toUpperCase() + node.data.type.slice(1)} state cannot have child states`,
|
|
2619
|
+
metadata: { stateType: node.data.type }
|
|
2620
|
+
});
|
|
2621
|
+
if (node.data.type === "parallel" && graph.nodes.filter((n) => n.parentId === node.id).length === 0) warnings.push({
|
|
2622
|
+
code: "empty_parallel",
|
|
2623
|
+
kind: "correctness",
|
|
2624
|
+
message: "Parallel state should have child regions"
|
|
2625
|
+
});
|
|
2626
|
+
const children = graph.nodes.filter((n) => n.parentId === node.id);
|
|
2627
|
+
if (children.length === 1 && node.data.type !== "parallel" && node.data.type !== "final" && node.data.type !== "history") warnings.push({
|
|
2628
|
+
code: "single_child_compound",
|
|
2629
|
+
kind: "optimization",
|
|
2630
|
+
message: "State with single child can be simplified"
|
|
2631
|
+
});
|
|
2632
|
+
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) {
|
|
2633
|
+
const alwaysEdges = graph.edges.filter((e) => e.sourceId === node.id && e.data.eventType === "");
|
|
2634
|
+
const hasUnconditionalNoop = alwaysEdges.some((e) => !e.data.guard && e.targetId !== e.sourceId && (e.data.actions?.length ?? 0) === 0);
|
|
2635
|
+
const hasMeaningfulAlways = alwaysEdges.some((e) => !!e.data.guard || (e.data.actions?.length ?? 0) > 0);
|
|
2636
|
+
if (hasUnconditionalNoop && !hasMeaningfulAlways) warnings.push({
|
|
2637
|
+
code: "noop_transient_state",
|
|
2638
|
+
kind: "optimization",
|
|
2639
|
+
message: "State has no effect: an unconditional transition exits it immediately, and it has no actions or invocations"
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
return warnings.length > 0 ? warnings : null;
|
|
2643
|
+
}
|
|
2644
|
+
function getOnDoneWarnings(graph, reachableNodes) {
|
|
2645
|
+
const results = [];
|
|
2646
|
+
for (const node of graph.nodes) {
|
|
2647
|
+
const doneEdges = graph.edges.filter((edge) => edge.sourceId === node.id && (edge.data.eventType.startsWith("@statelyai.state.done.") || edge.data.eventType.startsWith("xstate.done.state.")));
|
|
2648
|
+
if (doneEdges.length === 0) continue;
|
|
2649
|
+
const edgeIds = doneEdges.map((e) => e.id);
|
|
2650
|
+
if (node.data.type === "parallel") {
|
|
2651
|
+
const regions = graph.nodes.filter((n) => n.parentId === node.id);
|
|
2652
|
+
const regionsWithoutFinal = [];
|
|
2653
|
+
const regionsWithoutReachableFinal = [];
|
|
2654
|
+
for (const region of regions) {
|
|
2655
|
+
const finalChildren = graph.nodes.filter((n) => n.parentId === region.id).filter((child) => child.data.type === "final");
|
|
2656
|
+
if (finalChildren.length === 0) regionsWithoutFinal.push(region);
|
|
2657
|
+
else if (!finalChildren.some((child) => reachableNodes.has(child.id))) regionsWithoutReachableFinal.push(region);
|
|
2658
|
+
}
|
|
2659
|
+
if (regionsWithoutFinal.length > 0) results.push(issue({
|
|
2660
|
+
code: "on_done_unreachable",
|
|
2661
|
+
kind: "reachability",
|
|
2662
|
+
level: "warning",
|
|
2663
|
+
nodeIds: [node.id],
|
|
2664
|
+
edgeIds,
|
|
2665
|
+
message: "onDone transitions require each parallel region to have a final child state",
|
|
2666
|
+
metadata: { reason: "parallel_region_missing_final" }
|
|
2667
|
+
}));
|
|
2668
|
+
else if (regionsWithoutReachableFinal.length > 0) results.push(issue({
|
|
2669
|
+
code: "on_done_unreachable",
|
|
2670
|
+
kind: "reachability",
|
|
2671
|
+
level: "warning",
|
|
2672
|
+
nodeIds: [node.id],
|
|
2673
|
+
edgeIds,
|
|
2674
|
+
message: "onDone transitions will never trigger: a parallel region has no reachable final child state",
|
|
2675
|
+
metadata: { reason: "parallel_region_final_unreachable" }
|
|
2676
|
+
}));
|
|
2677
|
+
} else {
|
|
2678
|
+
const finalChildren = graph.nodes.filter((n) => n.parentId === node.id).filter((child) => child.data.type === "final");
|
|
2679
|
+
if (finalChildren.length === 0) results.push(issue({
|
|
2680
|
+
code: "on_done_unreachable",
|
|
2681
|
+
kind: "reachability",
|
|
2682
|
+
level: "warning",
|
|
2683
|
+
nodeIds: [node.id],
|
|
2684
|
+
edgeIds,
|
|
2685
|
+
message: "onDone transitions require a final child state to trigger",
|
|
2686
|
+
metadata: { reason: "missing_final_child" }
|
|
2687
|
+
}));
|
|
2688
|
+
else if (!finalChildren.some((child) => reachableNodes.has(child.id))) results.push(issue({
|
|
2689
|
+
code: "on_done_unreachable",
|
|
2690
|
+
kind: "reachability",
|
|
2691
|
+
level: "warning",
|
|
2692
|
+
nodeIds: [node.id],
|
|
2693
|
+
edgeIds,
|
|
2694
|
+
message: "onDone transitions will never trigger: no reachable final child state",
|
|
2695
|
+
metadata: { reason: "final_child_unreachable" }
|
|
2696
|
+
}));
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return results;
|
|
2700
|
+
}
|
|
2701
|
+
function getEdgeErrors(graph, edge) {
|
|
2702
|
+
const errors = [];
|
|
2703
|
+
const duplicates = findDuplicateEdges(graph, edge);
|
|
2704
|
+
if (duplicates.length > 0) errors.push({
|
|
2705
|
+
code: "duplicate_transition",
|
|
2706
|
+
kind: "correctness",
|
|
2707
|
+
message: "Two events with the same name and source state are not allowed",
|
|
2708
|
+
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
|
|
2709
|
+
});
|
|
2710
|
+
if (edge.data.eventType.startsWith("xstate.after.")) {
|
|
2711
|
+
const delayPart = edge.data.eventType.replace(/^xstate\.after\./, "").split(".")[0];
|
|
2712
|
+
if (delayPart !== void 0) {
|
|
2713
|
+
const numericValue = Number(delayPart);
|
|
2714
|
+
if (!Number.isNaN(numericValue) && numericValue < 0) errors.push({
|
|
2715
|
+
code: "invalid_delay",
|
|
2716
|
+
kind: "correctness",
|
|
2717
|
+
message: "Delay value cannot be negative",
|
|
2718
|
+
metadata: { delay: numericValue }
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return errors.length > 0 ? errors : null;
|
|
2723
|
+
}
|
|
2724
|
+
function getEdgeWarnings(graph, edge) {
|
|
2725
|
+
const warnings = [];
|
|
2726
|
+
const group = graph.edges.filter((e) => e.sourceId === edge.sourceId && e.data.eventType === edge.data.eventType);
|
|
2727
|
+
if (edge.data.guard && group.length > 1) {
|
|
2728
|
+
const guardType = edge.data.guard.type;
|
|
2729
|
+
const withRepeatedGuard = group.filter((e) => e.data.guard?.type === guardType);
|
|
2730
|
+
if (withRepeatedGuard.length > 1) warnings.push({
|
|
2731
|
+
code: "repeated_guard",
|
|
2732
|
+
kind: "maintainability",
|
|
2733
|
+
message: `Found ${withRepeatedGuard.length} guards with the name \`${guardType}\``,
|
|
2734
|
+
metadata: {
|
|
2735
|
+
guardType,
|
|
2736
|
+
count: withRepeatedGuard.length
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
const edgeIndex = group.findIndex((e) => e.id === edge.id);
|
|
2741
|
+
if (edgeIndex > 0 && group.slice(0, edgeIndex).some((e) => !e.data.guard) || isTransitionPreempted(graph, edge)) warnings.push({
|
|
2742
|
+
code: "transition_never_taken",
|
|
2743
|
+
kind: "reachability",
|
|
2744
|
+
message: "This transition will never be taken",
|
|
2745
|
+
fixes: edgeIndex > 0 && group.slice(0, edgeIndex).some((e) => !e.data.guard) ? [deleteTransitionFix(edge, "Delete unreachable transition")] : void 0
|
|
2746
|
+
});
|
|
2747
|
+
if (group.length > 1 && !edge.data.guard && edgeIndex < group.length - 1) warnings.push({
|
|
2748
|
+
code: "missing_guard",
|
|
2749
|
+
kind: "correctness",
|
|
2750
|
+
message: "This transition is missing a guard, so transitions after it will never be taken"
|
|
2751
|
+
});
|
|
2752
|
+
if (edgeIndex > 0) {
|
|
2753
|
+
const prevEdge = group[edgeIndex - 1];
|
|
2754
|
+
const sameTarget = prevEdge.targetId === edge.targetId;
|
|
2755
|
+
const sameActions = JSON.stringify(prevEdge.data.actions) === JSON.stringify(edge.data.actions);
|
|
2756
|
+
if (sameTarget && sameActions) warnings.push({
|
|
2757
|
+
code: "redundant_transition",
|
|
2758
|
+
kind: "optimization",
|
|
2759
|
+
message: "Redundant transition: same target and actions as previous transition",
|
|
2760
|
+
fixes: [deleteTransitionFix(edge, "Delete redundant transition")]
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
if (edge.data.eventType.startsWith("xstate.after.")) {
|
|
2764
|
+
const delayPart = edge.data.eventType.replace(/^xstate\.after\./, "").split(".")[0];
|
|
2765
|
+
if (delayPart !== void 0 && Number.isNaN(Number(delayPart))) {
|
|
2766
|
+
if (!getImplementations(graph).delays.find((d) => d.name === delayPart)?.code?.body?.trim()) warnings.push({
|
|
2767
|
+
code: "missing_delay_implementation",
|
|
2768
|
+
kind: "correctness",
|
|
2769
|
+
message: `Missing delay implementation for "${delayPart}"`,
|
|
2770
|
+
metadata: { delay: delayPart }
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
const sourceNode = graph.nodes.find((n) => n.id === edge.sourceId);
|
|
2775
|
+
if (sourceNode && sourceNode.data.type === "final") warnings.push({
|
|
2776
|
+
code: "invalid_final_transition",
|
|
2777
|
+
kind: "correctness",
|
|
2778
|
+
message: "Final state cannot transition to other states",
|
|
2779
|
+
fixes: [deleteTransitionFix(edge, "Delete transition from final state")]
|
|
2780
|
+
});
|
|
2781
|
+
if (sourceNode && sourceNode.data.type === "history") warnings.push({
|
|
2782
|
+
code: "invalid_history_transition",
|
|
2783
|
+
kind: "correctness",
|
|
2784
|
+
message: "History state cannot have transitions",
|
|
2785
|
+
fixes: [deleteTransitionFix(edge, "Delete transition from history state")]
|
|
2786
|
+
});
|
|
2787
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.targetId);
|
|
2788
|
+
if (sourceNode && targetNode && sourceNode.parentId === targetNode.parentId && sourceNode.id !== targetNode.id) {
|
|
2789
|
+
const parentNode = graph.nodes.find((n) => n.id === sourceNode.parentId);
|
|
2790
|
+
if (parentNode && parentNode.data.type === "parallel") warnings.push({
|
|
2791
|
+
code: "parallel_region_transition",
|
|
2792
|
+
kind: "correctness",
|
|
2793
|
+
message: "Parallel regions should communicate via events, not transitions"
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
return warnings.length > 0 ? warnings : null;
|
|
2797
|
+
}
|
|
2798
|
+
function getSourceWarnings(graph) {
|
|
2799
|
+
const results = [];
|
|
2800
|
+
const impls = getImplementations(graph);
|
|
2801
|
+
const groups = [
|
|
2802
|
+
{
|
|
2803
|
+
type: "action",
|
|
2804
|
+
items: impls.actions
|
|
2805
|
+
},
|
|
2806
|
+
{
|
|
2807
|
+
type: "guard",
|
|
2808
|
+
items: impls.guards
|
|
2809
|
+
},
|
|
2810
|
+
{
|
|
2811
|
+
type: "actor",
|
|
2812
|
+
items: impls.actors
|
|
2813
|
+
},
|
|
2814
|
+
{
|
|
2815
|
+
type: "delay",
|
|
2816
|
+
items: impls.delays
|
|
2817
|
+
}
|
|
2818
|
+
];
|
|
2819
|
+
for (const group of groups) {
|
|
2820
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2821
|
+
for (const item of group.items) {
|
|
2822
|
+
if (!item.name) continue;
|
|
2823
|
+
seen.set(item.name, (seen.get(item.name) ?? 0) + 1);
|
|
2824
|
+
}
|
|
2825
|
+
for (const [name, count] of seen) if (count > 1) results.push(issue({
|
|
2826
|
+
code: "duplicate_source_name",
|
|
2827
|
+
kind: "maintainability",
|
|
2828
|
+
level: "warning",
|
|
2829
|
+
message: `Duplicate ${group.type} source name "${name}" (${count} occurrences)`,
|
|
2830
|
+
metadata: {
|
|
2831
|
+
sourceType: group.type,
|
|
2832
|
+
name,
|
|
2833
|
+
count
|
|
2834
|
+
}
|
|
2835
|
+
}));
|
|
2836
|
+
}
|
|
2837
|
+
return results;
|
|
2838
|
+
}
|
|
2839
|
+
function getSourceRestrictionErrors(graph, sourceAllowed) {
|
|
2840
|
+
if (!sourceAllowed) return [];
|
|
2841
|
+
const results = [];
|
|
2842
|
+
const impls = getImplementations(graph);
|
|
2843
|
+
const actionIds = new Set(impls.actions.map((a) => a.id));
|
|
2844
|
+
const guardIds = new Set(impls.guards.map((g) => g.id));
|
|
2845
|
+
const actorIds = new Set(impls.actors.map((a) => a.id));
|
|
2846
|
+
const isInvalidActionType = (type) => {
|
|
2847
|
+
if (!type) return true;
|
|
2848
|
+
if (actionIds.has(type)) return false;
|
|
2849
|
+
if (type.startsWith("xstate.")) return false;
|
|
2850
|
+
if (isAutoGeneratedInlineName(type)) return false;
|
|
2851
|
+
return true;
|
|
2852
|
+
};
|
|
2853
|
+
const formatActionMsg = (type) => type ? `Action "${type}" is not a defined action implementation` : "Action is not selected";
|
|
2854
|
+
if (sourceAllowed.actions === "predefined") {
|
|
2855
|
+
for (const node of graph.nodes) {
|
|
2856
|
+
for (const action of node.data.entry ?? []) if (isInvalidActionType(action.type)) results.push(issue({
|
|
2857
|
+
code: "undefined_action",
|
|
2858
|
+
kind: "correctness",
|
|
2859
|
+
level: "error",
|
|
2860
|
+
nodeIds: [node.id],
|
|
2861
|
+
message: formatActionMsg(action.type)
|
|
2862
|
+
}));
|
|
2863
|
+
for (const action of node.data.exit ?? []) if (isInvalidActionType(action.type)) results.push(issue({
|
|
2864
|
+
code: "undefined_action",
|
|
2865
|
+
kind: "correctness",
|
|
2866
|
+
level: "error",
|
|
2867
|
+
nodeIds: [node.id],
|
|
2868
|
+
message: formatActionMsg(action.type)
|
|
2869
|
+
}));
|
|
2870
|
+
}
|
|
2871
|
+
for (const edge of graph.edges) for (const action of edge.data.actions ?? []) if (isInvalidActionType(action.type)) results.push(issue({
|
|
2872
|
+
code: "undefined_action",
|
|
2873
|
+
kind: "correctness",
|
|
2874
|
+
level: "error",
|
|
2875
|
+
edgeIds: [edge.id],
|
|
2876
|
+
message: formatActionMsg(action.type)
|
|
2877
|
+
}));
|
|
2878
|
+
}
|
|
2879
|
+
if (sourceAllowed.guards === "predefined") for (const edge of graph.edges) {
|
|
2880
|
+
const guard = edge.data.guard;
|
|
2881
|
+
if (!guard) continue;
|
|
2882
|
+
const type = guard.type;
|
|
2883
|
+
if (!type || !guardIds.has(type) && !type.startsWith("inline:") && !isAutoGeneratedInlineName(type)) results.push(issue({
|
|
2884
|
+
code: "undefined_guard",
|
|
2885
|
+
kind: "correctness",
|
|
2886
|
+
level: "error",
|
|
2887
|
+
edgeIds: [edge.id],
|
|
2888
|
+
message: type ? `Guard "${type}" is not a defined guard implementation` : "Guard is not selected"
|
|
2889
|
+
}));
|
|
2890
|
+
}
|
|
2891
|
+
if (sourceAllowed.actors === "predefined") for (const node of graph.nodes) for (const invoke of node.data.invokes ?? []) {
|
|
2892
|
+
const src = invoke.src;
|
|
2893
|
+
if (!src || !actorIds.has(src) && !isAutoGeneratedInlineName(src)) results.push(issue({
|
|
2894
|
+
code: "undefined_actor",
|
|
2895
|
+
kind: "correctness",
|
|
2896
|
+
level: "error",
|
|
2897
|
+
nodeIds: [node.id],
|
|
2898
|
+
message: src ? `Actor "${src}" is not a defined actor implementation` : "Actor is not selected"
|
|
2899
|
+
}));
|
|
2900
|
+
}
|
|
2901
|
+
return results;
|
|
2902
|
+
}
|
|
2903
|
+
function pushMessages(results, messages, level, nodeIds, edgeIds) {
|
|
2904
|
+
messages?.forEach((message) => {
|
|
2905
|
+
results.push(issue({
|
|
2906
|
+
...message,
|
|
2907
|
+
level,
|
|
2908
|
+
nodeIds,
|
|
2909
|
+
edgeIds
|
|
2910
|
+
}));
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
function validateGraph(graph, sourceAllowed) {
|
|
2914
|
+
const results = [];
|
|
2915
|
+
const nonTempNodes = graph.nodes.filter((n) => !n.data?.temp);
|
|
2916
|
+
const nonTempEdges = graph.edges.filter((e) => !e.data?.temp);
|
|
2917
|
+
const nonTempGraph = {
|
|
2918
|
+
...graph,
|
|
2919
|
+
nodes: nonTempNodes,
|
|
2920
|
+
edges: nonTempEdges
|
|
2921
|
+
};
|
|
2922
|
+
const reachableNodes = computeReachableGraphNodes(nonTempGraph);
|
|
2923
|
+
for (const node of nonTempNodes) {
|
|
2924
|
+
pushMessages(results, getNodeErrors(nonTempGraph, node), "error", [node.id], []);
|
|
2925
|
+
pushMessages(results, getNodeWarningsWithReachability(nonTempGraph, node, reachableNodes), "warning", [node.id], []);
|
|
2926
|
+
}
|
|
2927
|
+
for (const edge of nonTempEdges) {
|
|
2928
|
+
pushMessages(results, getEdgeErrors(nonTempGraph, edge), "error", [], [edge.id]);
|
|
2929
|
+
pushMessages(results, getEdgeWarnings(nonTempGraph, edge), "warning", [], [edge.id]);
|
|
2930
|
+
}
|
|
2931
|
+
results.push(...getOnDoneWarnings(nonTempGraph, reachableNodes));
|
|
2932
|
+
results.push(...getSourceWarnings(nonTempGraph));
|
|
2933
|
+
results.push(...getSourceRestrictionErrors(nonTempGraph, sourceAllowed));
|
|
2934
|
+
return results;
|
|
2935
|
+
}
|
|
2936
|
+
function validationOk(issues) {
|
|
2937
|
+
return !issues.some((item) => item.level === "error");
|
|
2938
|
+
}
|
|
2939
|
+
const validationIssueTargetSchema = object({
|
|
2940
|
+
nodeIds: array(string()).optional(),
|
|
2941
|
+
edgeIds: array(string()).optional(),
|
|
2942
|
+
source: object({
|
|
2943
|
+
path: string().optional(),
|
|
2944
|
+
start: number().optional(),
|
|
2945
|
+
end: number().optional()
|
|
2946
|
+
}).optional()
|
|
2947
|
+
});
|
|
2948
|
+
const validationFixSchema = object({
|
|
2949
|
+
kind: validationFixKindSchema,
|
|
2950
|
+
source: validationFixSourceSchema,
|
|
2951
|
+
title: string(),
|
|
2952
|
+
patches: array(unknown()).optional(),
|
|
2953
|
+
output: unknown().optional(),
|
|
2954
|
+
diff: unknown().optional(),
|
|
2955
|
+
confidence: number().optional(),
|
|
2956
|
+
rationale: string().optional(),
|
|
2957
|
+
rank: number().optional(),
|
|
2958
|
+
postValidation: object({
|
|
2959
|
+
ok: boolean(),
|
|
2960
|
+
remainingIssueCodes: array(validationIssueCodeSchema)
|
|
2961
|
+
}).optional()
|
|
2962
|
+
});
|
|
2963
|
+
const validationIssueSchema = object({
|
|
2964
|
+
code: validationIssueCodeSchema,
|
|
2965
|
+
kind: validationIssueKindSchema,
|
|
2966
|
+
level: validationLevelSchema,
|
|
2967
|
+
message: string(),
|
|
2968
|
+
target: validationIssueTargetSchema,
|
|
2969
|
+
metadata: record(string(), unknown()).optional(),
|
|
2970
|
+
fixes: array(validationFixSchema).optional(),
|
|
2971
|
+
source: _enum(["deterministic", "llm"]).optional(),
|
|
2972
|
+
nodeIds: array(string()),
|
|
2973
|
+
edgeIds: array(string())
|
|
2974
|
+
});
|
|
2975
|
+
const machineFormatSchema = _enum([
|
|
2976
|
+
"xstate",
|
|
2977
|
+
"json",
|
|
2978
|
+
"yaml",
|
|
2979
|
+
"scxml",
|
|
2980
|
+
"mermaid"
|
|
2981
|
+
]);
|
|
2982
|
+
const xstateSourceProfileSchema = _enum([
|
|
2983
|
+
"auto",
|
|
2984
|
+
"xstate-v5",
|
|
2985
|
+
"xstate-v6-alpha"
|
|
2986
|
+
]);
|
|
2987
|
+
const machineInputSchema = object({
|
|
2988
|
+
machine: union([record(string(), unknown()), string()]),
|
|
2989
|
+
format: machineFormatSchema.optional(),
|
|
2990
|
+
profile: xstateSourceProfileSchema.optional(),
|
|
2991
|
+
xstateVersion: union([literal(5), literal(6)]).optional()
|
|
2992
|
+
});
|
|
2993
|
+
const graphSchema = record(string(), unknown());
|
|
2994
|
+
function asError(error) {
|
|
2995
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
2996
|
+
}
|
|
2997
|
+
function simImplementations(graph, guardState) {
|
|
2998
|
+
const guards = {};
|
|
2999
|
+
for (const edge of graph.edges) if (edge.data.guard) {
|
|
3000
|
+
const guardType = edge.data.guard.type;
|
|
3001
|
+
guards[guardType] = ({ event }) => {
|
|
3002
|
+
if (event["@xstate.guard"] === guardType) return true;
|
|
3003
|
+
return guardState?.[guardType] ?? false;
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
return { guards };
|
|
3007
|
+
}
|
|
3008
|
+
const operations = {
|
|
3009
|
+
validate_machine: {
|
|
3010
|
+
description: "Validate a machine without writing files.",
|
|
3011
|
+
input: machineInputSchema,
|
|
3012
|
+
output: object({
|
|
3013
|
+
ok: boolean(),
|
|
3014
|
+
issues: array(validationIssueSchema)
|
|
3015
|
+
}),
|
|
3016
|
+
async handler(input) {
|
|
3017
|
+
try {
|
|
3018
|
+
const issues = validateGraph(machineInputToGraph(input));
|
|
3019
|
+
return {
|
|
3020
|
+
ok: validationOk(issues),
|
|
3021
|
+
issues
|
|
3022
|
+
};
|
|
3023
|
+
} catch (error) {
|
|
3024
|
+
return {
|
|
3025
|
+
ok: false,
|
|
3026
|
+
issues: [{
|
|
3027
|
+
code: "parse_error",
|
|
3028
|
+
kind: "correctness",
|
|
3029
|
+
level: "error",
|
|
3030
|
+
message: asError(error).message,
|
|
3031
|
+
target: {
|
|
3032
|
+
nodeIds: [],
|
|
3033
|
+
edgeIds: []
|
|
3034
|
+
},
|
|
3035
|
+
source: "deterministic",
|
|
3036
|
+
nodeIds: [],
|
|
3037
|
+
edgeIds: []
|
|
3038
|
+
}]
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
},
|
|
3043
|
+
convert_to_graph: {
|
|
3044
|
+
description: "Convert a machine into an internal graph for patching.",
|
|
3045
|
+
input: machineInputSchema,
|
|
3046
|
+
output: object({ graph: graphSchema }),
|
|
3047
|
+
async handler(input) {
|
|
3048
|
+
return { graph: machineInputToGraph(input) };
|
|
3049
|
+
}
|
|
3050
|
+
},
|
|
3051
|
+
convert_format: {
|
|
3052
|
+
description: "Convert a machine or graph to a machine format.",
|
|
3053
|
+
input: object({
|
|
3054
|
+
machine: union([record(string(), unknown()), string()]).optional(),
|
|
3055
|
+
graph: graphSchema.optional(),
|
|
3056
|
+
format: machineFormatSchema.optional(),
|
|
3057
|
+
to: machineFormatSchema
|
|
3058
|
+
}).refine((value) => Boolean(value.machine) !== Boolean(value.graph), { message: "Provide exactly one of machine or graph." }),
|
|
3059
|
+
output: object({
|
|
3060
|
+
format: machineFormatSchema,
|
|
3061
|
+
output: union([record(string(), unknown()), string()])
|
|
3062
|
+
}),
|
|
3063
|
+
async handler(input) {
|
|
3064
|
+
const graph = input.graph ?? machineInputToGraph({
|
|
3065
|
+
machine: input.machine,
|
|
3066
|
+
format: input.format
|
|
3067
|
+
});
|
|
3068
|
+
return {
|
|
3069
|
+
format: input.to,
|
|
3070
|
+
output: graphToFormat(graph, input.to)
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
},
|
|
3074
|
+
apply_patch: {
|
|
3075
|
+
description: "Apply semantic machine patches to a graph without writing files.",
|
|
3076
|
+
input: object({
|
|
3077
|
+
graph: graphSchema,
|
|
3078
|
+
patches: array(machinePatchSchema),
|
|
3079
|
+
outputFormat: machineFormatSchema.default("json")
|
|
3080
|
+
}),
|
|
3081
|
+
output: union([object({
|
|
3082
|
+
ok: literal(true),
|
|
3083
|
+
graph: graphSchema,
|
|
3084
|
+
machine: union([record(string(), unknown()), string()]),
|
|
3085
|
+
format: machineFormatSchema,
|
|
3086
|
+
diff: unknown(),
|
|
3087
|
+
patches: array(machinePatchSchema)
|
|
3088
|
+
}), object({
|
|
3089
|
+
ok: literal(false),
|
|
3090
|
+
reason: string(),
|
|
3091
|
+
errors: array(object({ message: string() }))
|
|
3092
|
+
})]),
|
|
3093
|
+
async handler(input) {
|
|
3094
|
+
try {
|
|
3095
|
+
const graph = applyGraphPatches(input.graph, input.patches);
|
|
3096
|
+
createMachine(graphToMachineConfig(graph));
|
|
3097
|
+
const diff = diffGraphsSemantically(input.graph, graph).diff;
|
|
3098
|
+
return {
|
|
3099
|
+
ok: true,
|
|
3100
|
+
graph,
|
|
3101
|
+
machine: graphToFormat(graph, input.outputFormat),
|
|
3102
|
+
format: input.outputFormat,
|
|
3103
|
+
diff,
|
|
3104
|
+
patches: input.patches
|
|
3105
|
+
};
|
|
3106
|
+
} catch (error) {
|
|
3107
|
+
return {
|
|
3108
|
+
ok: false,
|
|
3109
|
+
reason: "validation",
|
|
3110
|
+
errors: [asError(error)]
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
},
|
|
3115
|
+
diff_machines: {
|
|
3116
|
+
description: "Compute semantic diff between two machines.",
|
|
3117
|
+
input: object({
|
|
3118
|
+
left: machineInputSchema,
|
|
3119
|
+
right: machineInputSchema,
|
|
3120
|
+
includePatches: boolean().optional()
|
|
3121
|
+
}),
|
|
3122
|
+
output: object({
|
|
3123
|
+
diff: unknown(),
|
|
3124
|
+
patches: array(machinePatchSchema).optional()
|
|
3125
|
+
}),
|
|
3126
|
+
async handler(input) {
|
|
3127
|
+
return { diff: diffGraphsSemantically(machineInputToGraph(input.left), machineInputToGraph(input.right)).diff };
|
|
3128
|
+
}
|
|
3129
|
+
},
|
|
3130
|
+
generate_paths: {
|
|
3131
|
+
description: "Generate paths through a machine.",
|
|
3132
|
+
input: machineInputSchema.extend({
|
|
3133
|
+
targetPath: array(string()).optional(),
|
|
3134
|
+
kind: _enum(["shortest", "simple"]).default("shortest"),
|
|
3135
|
+
max: number().int().positive().optional()
|
|
3136
|
+
}),
|
|
3137
|
+
output: object({ paths: array(unknown()) }),
|
|
3138
|
+
async handler(input) {
|
|
3139
|
+
const result = generateGraphPathsData(machineInputToGraph(input), {
|
|
3140
|
+
strategy: input.kind,
|
|
3141
|
+
reduceDuplicates: false,
|
|
3142
|
+
preferTransitionCoverage: false,
|
|
3143
|
+
limit: input.max
|
|
3144
|
+
});
|
|
3145
|
+
return { paths: input.targetPath ? result.paths.filter((path) => path.finalStatePath === input.targetPath.join(".")) : result.paths };
|
|
3146
|
+
}
|
|
3147
|
+
},
|
|
3148
|
+
simulate: {
|
|
3149
|
+
description: "Simulate one event from a machine state.",
|
|
3150
|
+
input: machineInputSchema.extend({
|
|
3151
|
+
stateValue: unknown(),
|
|
3152
|
+
event: object({ type: string() }).passthrough(),
|
|
3153
|
+
guardState: record(string(), boolean()).optional()
|
|
3154
|
+
}),
|
|
3155
|
+
output: object({
|
|
3156
|
+
nextStateValue: unknown(),
|
|
3157
|
+
actions: array(unknown()),
|
|
3158
|
+
changed: boolean(),
|
|
3159
|
+
microsteps: array(unknown()).optional()
|
|
3160
|
+
}),
|
|
3161
|
+
async handler(input) {
|
|
3162
|
+
const graph = machineInputToGraph(input);
|
|
3163
|
+
const machine = createMachine(graphToMachineConfig(graph)).provide(simImplementations(graph, input.guardState));
|
|
3164
|
+
const snapshot = machine.resolveState({
|
|
3165
|
+
value: input.stateValue,
|
|
3166
|
+
context: {},
|
|
3167
|
+
status: "active"
|
|
3168
|
+
});
|
|
3169
|
+
const [nextSnapshot, actions] = transition(machine, snapshot, input.event);
|
|
3170
|
+
return {
|
|
3171
|
+
nextStateValue: nextSnapshot.value,
|
|
3172
|
+
actions,
|
|
3173
|
+
changed: JSON.stringify(nextSnapshot.value) !== JSON.stringify(snapshot.value)
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
|
|
110
3179
|
//#endregion
|
|
111
3180
|
//#region ../editor-sync/src/internal/sourceLocations.ts
|
|
112
3181
|
const sourceFileUris = /* @__PURE__ */ new WeakMap();
|
|
113
3182
|
function collectXStateSourceLocations(sourceText, document, options = {}) {
|
|
114
|
-
const sourceFile = ts$1.createSourceFile(document.fileName, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind$
|
|
3183
|
+
const sourceFile = ts$1.createSourceFile(document.fileName, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind$2(document.fileName));
|
|
115
3184
|
const uri = document.uri?.toString() ?? document.fileName;
|
|
116
3185
|
sourceFileUris.set(sourceFile, uri);
|
|
117
3186
|
const bindings = collectBindings(sourceFile, uri, document.fileName, options);
|
|
@@ -119,13 +3188,18 @@ function collectXStateSourceLocations(sourceText, document, options = {}) {
|
|
|
119
3188
|
const root = findMachineConfigObjectLiteral(sourceFile, bindings);
|
|
120
3189
|
if (!root) return;
|
|
121
3190
|
const states = [];
|
|
122
|
-
|
|
3191
|
+
const statesObjectLocations = [];
|
|
3192
|
+
collectStateLocations(root, [], states, statesObjectLocations, bindings, staticValues, root);
|
|
3193
|
+
const rootProfile = detectXStateSourceProfile(sourceText);
|
|
3194
|
+
const referencedStateSymbols = new Set([...states, ...statesObjectLocations].map((state) => state.symbol).filter((symbol) => typeof symbol === "string"));
|
|
123
3195
|
return {
|
|
124
3196
|
version: 1,
|
|
3197
|
+
profile: rootProfile === "xstate-v6-alpha" || [...referencedStateSymbols].some((symbol) => detectXStateSourceProfile(bindings.get(symbol)?.sourceFile.text ?? "") === "xstate-v6-alpha") ? "xstate-v6-alpha" : rootProfile,
|
|
125
3198
|
root: {
|
|
126
3199
|
uri,
|
|
127
|
-
range: toSourceRange(sourceFile, root)
|
|
3200
|
+
range: toSourceRange$1(sourceFile, root)
|
|
128
3201
|
},
|
|
3202
|
+
statesObject: statesObjectLocations[0],
|
|
129
3203
|
states,
|
|
130
3204
|
staticValues: collectStaticValueReferences(sourceFile, uri, root, staticValues)
|
|
131
3205
|
};
|
|
@@ -143,7 +3217,7 @@ function collectStaticValueBindings(sourceFile) {
|
|
|
143
3217
|
kind: "const",
|
|
144
3218
|
symbol: declaration.name.text,
|
|
145
3219
|
value,
|
|
146
|
-
valueRange: toSourceRange(sourceFile, declaration.initializer)
|
|
3220
|
+
valueRange: toSourceRange$1(sourceFile, declaration.initializer)
|
|
147
3221
|
});
|
|
148
3222
|
}
|
|
149
3223
|
continue;
|
|
@@ -158,15 +3232,18 @@ function collectStaticValueBindings(sourceFile) {
|
|
|
158
3232
|
kind: "enumMember",
|
|
159
3233
|
symbol,
|
|
160
3234
|
value,
|
|
161
|
-
valueRange: toSourceRange(sourceFile, member.initializer)
|
|
3235
|
+
valueRange: toSourceRange$1(sourceFile, member.initializer)
|
|
162
3236
|
});
|
|
163
3237
|
}
|
|
164
3238
|
}
|
|
165
3239
|
return bindings;
|
|
166
3240
|
}
|
|
167
|
-
function collectBindings(sourceFile, uri, fileName, options) {
|
|
3241
|
+
function collectBindings(sourceFile, uri, fileName, options, cache = /* @__PURE__ */ new Map()) {
|
|
3242
|
+
const cached = cache.get(fileName);
|
|
3243
|
+
if (cached) return cached;
|
|
168
3244
|
const bindings = collectTopLevelBindings(sourceFile, uri);
|
|
169
|
-
|
|
3245
|
+
cache.set(fileName, bindings);
|
|
3246
|
+
addImportedBindings(bindings, sourceFile, uri, fileName, options, cache);
|
|
170
3247
|
return bindings;
|
|
171
3248
|
}
|
|
172
3249
|
function collectTopLevelBindings(sourceFile, uri) {
|
|
@@ -198,7 +3275,7 @@ function collectTopLevelBindings(sourceFile, uri) {
|
|
|
198
3275
|
}
|
|
199
3276
|
return bindings;
|
|
200
3277
|
}
|
|
201
|
-
function addImportedBindings(bindings, sourceFile, uri, fileName, options) {
|
|
3278
|
+
function addImportedBindings(bindings, sourceFile, uri, fileName, options, cache) {
|
|
202
3279
|
const resolver = options.resolver;
|
|
203
3280
|
if (!resolver) return;
|
|
204
3281
|
for (const statement of sourceFile.statements) {
|
|
@@ -210,9 +3287,9 @@ function addImportedBindings(bindings, sourceFile, uri, fileName, options) {
|
|
|
210
3287
|
uri
|
|
211
3288
|
}, statement.moduleSpecifier.text);
|
|
212
3289
|
if (!importedDocument) continue;
|
|
213
|
-
const importedSourceFile = ts$1.createSourceFile(importedDocument.fileName, importedDocument.text, ts$1.ScriptTarget.Latest, true, getScriptKind$
|
|
3290
|
+
const importedSourceFile = ts$1.createSourceFile(importedDocument.fileName, importedDocument.text, ts$1.ScriptTarget.Latest, true, getScriptKind$2(importedDocument.fileName));
|
|
214
3291
|
sourceFileUris.set(importedSourceFile, importedDocument.uri);
|
|
215
|
-
const importedBindings = collectBindings(importedSourceFile, importedDocument.uri, importedDocument.fileName, options);
|
|
3292
|
+
const importedBindings = collectBindings(importedSourceFile, importedDocument.uri, importedDocument.fileName, options, cache);
|
|
216
3293
|
for (const element of namedBindings.elements) {
|
|
217
3294
|
const importedName = element.propertyName?.text ?? element.name.text;
|
|
218
3295
|
const binding = importedBindings.get(importedName);
|
|
@@ -235,11 +3312,20 @@ function findMachineConfigObjectLiteral(sourceFile, bindings) {
|
|
|
235
3312
|
visit(sourceFile);
|
|
236
3313
|
return found;
|
|
237
3314
|
}
|
|
238
|
-
function collectStateLocations(stateObject, parentPath, locations, bindings, staticValues, root) {
|
|
239
|
-
const statesObject =
|
|
3315
|
+
function collectStateLocations(stateObject, parentPath, locations, statesObjectLocations, bindings, staticValues, root) {
|
|
3316
|
+
const statesObject = resolveObjectLiteralProperty(stateObject, "states", bindings);
|
|
240
3317
|
if (!statesObject) return;
|
|
241
|
-
|
|
242
|
-
|
|
3318
|
+
statesObjectLocations.push({
|
|
3319
|
+
uri: statesObject.uri,
|
|
3320
|
+
path: parentPath,
|
|
3321
|
+
kind: statesObject.kind,
|
|
3322
|
+
symbol: statesObject.symbol,
|
|
3323
|
+
keyRange: statesObject.keyRange,
|
|
3324
|
+
referenceRange: statesObject.referenceRange,
|
|
3325
|
+
range: toSourceRange$1(statesObject.sourceFile, statesObject.object)
|
|
3326
|
+
});
|
|
3327
|
+
for (const property of statesObject.object.properties) {
|
|
3328
|
+
const resolved = resolveStateProperty(property, statesObject.bindings, staticValues, root);
|
|
243
3329
|
if (!resolved) continue;
|
|
244
3330
|
const path = [...parentPath, resolved.key];
|
|
245
3331
|
locations.push({
|
|
@@ -251,9 +3337,9 @@ function collectStateLocations(stateObject, parentPath, locations, bindings, sta
|
|
|
251
3337
|
referenceRange: resolved.referenceRange,
|
|
252
3338
|
keySource: resolved.keySource,
|
|
253
3339
|
keyReplacementText: resolved.keyReplacementText,
|
|
254
|
-
range: toSourceRange(resolved.sourceFile, resolved.stateObject)
|
|
3340
|
+
range: toSourceRange$1(resolved.sourceFile, resolved.stateObject)
|
|
255
3341
|
});
|
|
256
|
-
collectStateLocations(resolved.stateObject, path, locations, resolved.bindings, staticValues, root);
|
|
3342
|
+
collectStateLocations(resolved.stateObject, path, locations, statesObjectLocations, resolved.bindings, staticValues, root);
|
|
257
3343
|
}
|
|
258
3344
|
}
|
|
259
3345
|
function resolveStateProperty(property, bindings, staticValues, root) {
|
|
@@ -264,8 +3350,8 @@ function resolveStateProperty(property, bindings, staticValues, root) {
|
|
|
264
3350
|
key: property.name.text,
|
|
265
3351
|
kind: binding.kind,
|
|
266
3352
|
symbol: property.name.text,
|
|
267
|
-
keyRange: toSourceRange(property.getSourceFile(), property.name),
|
|
268
|
-
referenceRange: toSourceRange(property.getSourceFile(), property.name),
|
|
3353
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
3354
|
+
referenceRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
269
3355
|
stateObject: binding.node,
|
|
270
3356
|
sourceFile: binding.sourceFile,
|
|
271
3357
|
uri: binding.uri,
|
|
@@ -279,7 +3365,7 @@ function resolveStateProperty(property, bindings, staticValues, root) {
|
|
|
279
3365
|
if (inlineObject) return {
|
|
280
3366
|
key: resolvedKey.key,
|
|
281
3367
|
kind: "inline",
|
|
282
|
-
keyRange: toSourceRange(property.getSourceFile(), property.name),
|
|
3368
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
283
3369
|
keySource: resolvedKey.keySource,
|
|
284
3370
|
keyReplacementText: resolvedKey.keyReplacementText,
|
|
285
3371
|
stateObject: inlineObject,
|
|
@@ -294,8 +3380,8 @@ function resolveStateProperty(property, bindings, staticValues, root) {
|
|
|
294
3380
|
key: resolvedKey.key,
|
|
295
3381
|
kind: binding.kind,
|
|
296
3382
|
symbol: property.initializer.text,
|
|
297
|
-
keyRange: toSourceRange(property.getSourceFile(), property.name),
|
|
298
|
-
referenceRange: toSourceRange(property.getSourceFile(), property.initializer),
|
|
3383
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
3384
|
+
referenceRange: toSourceRange$1(property.getSourceFile(), property.initializer),
|
|
299
3385
|
keySource: resolvedKey.keySource,
|
|
300
3386
|
keyReplacementText: resolvedKey.keyReplacementText,
|
|
301
3387
|
stateObject: binding.node,
|
|
@@ -327,11 +3413,34 @@ function resolveIdentifierObject(expression, bindings) {
|
|
|
327
3413
|
const binding = bindings.get(unwrapped.text);
|
|
328
3414
|
return binding?.kind === "objectReference" ? binding.node : null;
|
|
329
3415
|
}
|
|
330
|
-
function
|
|
3416
|
+
function resolveObjectLiteralProperty(objectLiteral, propertyName, bindings) {
|
|
331
3417
|
for (const property of objectLiteral.properties) {
|
|
332
3418
|
if (!ts$1.isPropertyAssignment(property)) continue;
|
|
333
3419
|
if (getPropertyNameText$1(property.name) !== propertyName) continue;
|
|
334
|
-
|
|
3420
|
+
const inlineObject = unwrapObjectLiteralExpression(property.initializer);
|
|
3421
|
+
if (inlineObject) return {
|
|
3422
|
+
object: inlineObject,
|
|
3423
|
+
kind: "inline",
|
|
3424
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
3425
|
+
sourceFile: property.getSourceFile(),
|
|
3426
|
+
uri: getSourceFileUri(property.getSourceFile()),
|
|
3427
|
+
bindings
|
|
3428
|
+
};
|
|
3429
|
+
if (ts$1.isIdentifier(property.initializer)) {
|
|
3430
|
+
const binding = bindings.get(property.initializer.text);
|
|
3431
|
+
if (binding?.kind !== "objectReference") return null;
|
|
3432
|
+
return {
|
|
3433
|
+
object: binding.node,
|
|
3434
|
+
kind: binding.kind,
|
|
3435
|
+
symbol: property.initializer.text,
|
|
3436
|
+
keyRange: toSourceRange$1(property.getSourceFile(), property.name),
|
|
3437
|
+
referenceRange: toSourceRange$1(property.getSourceFile(), property.initializer),
|
|
3438
|
+
sourceFile: binding.sourceFile,
|
|
3439
|
+
uri: binding.uri,
|
|
3440
|
+
bindings: binding.bindings
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
return null;
|
|
335
3444
|
}
|
|
336
3445
|
return null;
|
|
337
3446
|
}
|
|
@@ -383,7 +3492,7 @@ function collectStaticValueReferences(sourceFile, uri, root, staticValues) {
|
|
|
383
3492
|
const binding = getStaticValueBindingForExpression(node.expression, staticValues);
|
|
384
3493
|
if (binding && !isStateKeyComputedPropertyName(node)) references.push({
|
|
385
3494
|
uri,
|
|
386
|
-
range: toSourceRange(sourceFile, node),
|
|
3495
|
+
range: toSourceRange$1(sourceFile, node),
|
|
387
3496
|
value: binding.value
|
|
388
3497
|
});
|
|
389
3498
|
return;
|
|
@@ -396,7 +3505,7 @@ function collectStaticValueReferences(sourceFile, uri, root, staticValues) {
|
|
|
396
3505
|
if (binding) {
|
|
397
3506
|
references.push({
|
|
398
3507
|
uri,
|
|
399
|
-
range: toSourceRange(sourceFile, node),
|
|
3508
|
+
range: toSourceRange$1(sourceFile, node),
|
|
400
3509
|
value: binding.value
|
|
401
3510
|
});
|
|
402
3511
|
return;
|
|
@@ -478,7 +3587,7 @@ function unwrapExpression$1(expression) {
|
|
|
478
3587
|
return current;
|
|
479
3588
|
}
|
|
480
3589
|
}
|
|
481
|
-
function toSourceRange(sourceFile, node) {
|
|
3590
|
+
function toSourceRange$1(sourceFile, node) {
|
|
482
3591
|
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
483
3592
|
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
484
3593
|
return {
|
|
@@ -488,7 +3597,7 @@ function toSourceRange(sourceFile, node) {
|
|
|
488
3597
|
endChar: end.character
|
|
489
3598
|
};
|
|
490
3599
|
}
|
|
491
|
-
function getScriptKind$
|
|
3600
|
+
function getScriptKind$2(fileName) {
|
|
492
3601
|
if (fileName.endsWith(".tsx")) return ts$1.ScriptKind.TSX;
|
|
493
3602
|
if (fileName.endsWith(".jsx")) return ts$1.ScriptKind.JSX;
|
|
494
3603
|
if (fileName.endsWith(".js")) return ts$1.ScriptKind.JS;
|
|
@@ -512,7 +3621,7 @@ function parseSourceToMachine(text, document, options = {}) {
|
|
|
512
3621
|
range: location.range,
|
|
513
3622
|
newText: JSON.stringify(location.value)
|
|
514
3623
|
}));
|
|
515
|
-
const stateReplacements = sourceLocations.states.filter((location) => location.path.length === 1 && location.keyRange).flatMap((location) => {
|
|
3624
|
+
const stateReplacements = sourceLocations.states.filter((location) => location.path.length === 1 && location.keyRange && !sourceLocations.statesObject?.referenceRange).flatMap((location) => {
|
|
516
3625
|
const keyRange = location.keyRange;
|
|
517
3626
|
const keyReplacement = location.keyReplacementText ? [{
|
|
518
3627
|
range: keyRange,
|
|
@@ -530,7 +3639,93 @@ function parseSourceToMachine(text, document, options = {}) {
|
|
|
530
3639
|
newText: `${location.path[0]}: ${stateText}`
|
|
531
3640
|
}];
|
|
532
3641
|
});
|
|
533
|
-
|
|
3642
|
+
const statesObjectReplacements = sourceLocations.statesObject?.referenceRange && sourceLocations.statesObject.kind !== "inline" ? (() => {
|
|
3643
|
+
const statesText = readHydratedStatesObject(sourceLocations.statesObject.uri, sourceLocations.statesObject.range, sourceLocations.states, options.resolver);
|
|
3644
|
+
return statesText ? [{
|
|
3645
|
+
range: sourceLocations.statesObject.referenceRange,
|
|
3646
|
+
newText: statesText
|
|
3647
|
+
}] : [];
|
|
3648
|
+
})() : [];
|
|
3649
|
+
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] : []]));
|
|
3650
|
+
const hydratedText = applySourceRangeReplacements(text, [
|
|
3651
|
+
...staticValueReplacements,
|
|
3652
|
+
...stateReplacements,
|
|
3653
|
+
...statesObjectReplacements,
|
|
3654
|
+
...importReplacements
|
|
3655
|
+
].filter(removeOverlappingRanges).sort(compareSourceRangesDescending));
|
|
3656
|
+
if (sourceLocations.profile === "xstate-v6-alpha") return `import { createMachine } from "xstate";
|
|
3657
|
+
|
|
3658
|
+
export const machine = createMachine(${serializeJS(xstateSourceToConfig(hydratedText, { profile: "xstate-v6-alpha" }), 0)});
|
|
3659
|
+
`;
|
|
3660
|
+
return hydratedText;
|
|
3661
|
+
}
|
|
3662
|
+
function readHydratedStatesObject(uri, range, states, resolver) {
|
|
3663
|
+
if (!resolver) return null;
|
|
3664
|
+
const document = resolver.read(uri);
|
|
3665
|
+
if (!document) return null;
|
|
3666
|
+
const replacements = states.filter((state) => state.path.length === 1 && state.referenceRange).flatMap((state) => {
|
|
3667
|
+
const stateText = readSourceRange(state.uri, state.range, resolver);
|
|
3668
|
+
if (!stateText || !state.referenceRange) return [];
|
|
3669
|
+
const isShorthand = state.keyRange && areSourceRangesEqual(state.referenceRange, state.keyRange);
|
|
3670
|
+
return [{
|
|
3671
|
+
range: state.referenceRange,
|
|
3672
|
+
newText: isShorthand ? `${state.path[0]}: ${stateText}` : stateText
|
|
3673
|
+
}];
|
|
3674
|
+
});
|
|
3675
|
+
return applySourceRangeReplacementsToSlice(document.text, range, replacements);
|
|
3676
|
+
}
|
|
3677
|
+
function applySourceRangeReplacementsToSlice(text, range, replacements) {
|
|
3678
|
+
const sliceStart = offsetAtRangePosition(text, range.startLine, range.startChar);
|
|
3679
|
+
const sliceEnd = offsetAtRangePosition(text, range.endLine, range.endChar);
|
|
3680
|
+
let updated = text.slice(sliceStart, sliceEnd);
|
|
3681
|
+
for (const replacement of replacements.sort(compareSourceRangesDescending)) {
|
|
3682
|
+
const start = offsetAtRangePosition(text, replacement.range.startLine, replacement.range.startChar);
|
|
3683
|
+
const end = offsetAtRangePosition(text, replacement.range.endLine, replacement.range.endChar);
|
|
3684
|
+
if (start < sliceStart || end > sliceEnd) continue;
|
|
3685
|
+
const localStart = start - sliceStart;
|
|
3686
|
+
const localEnd = end - sliceStart;
|
|
3687
|
+
updated = `${updated.slice(0, localStart)}${replacement.newText}${updated.slice(localEnd)}`;
|
|
3688
|
+
}
|
|
3689
|
+
return updated;
|
|
3690
|
+
}
|
|
3691
|
+
function collectHydratedImportReplacements(text, document, hydratedSymbols) {
|
|
3692
|
+
if (hydratedSymbols.size === 0) return [];
|
|
3693
|
+
const sourceFile = ts$1.createSourceFile(document.fileName, text, ts$1.ScriptTarget.Latest, true, getScriptKind$1(document.fileName));
|
|
3694
|
+
const replacements = [];
|
|
3695
|
+
for (const statement of sourceFile.statements) {
|
|
3696
|
+
if (!ts$1.isImportDeclaration(statement) || !ts$1.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
3697
|
+
if (!statement.moduleSpecifier.text.startsWith(".")) continue;
|
|
3698
|
+
const namedBindings = statement.importClause?.namedBindings;
|
|
3699
|
+
if (!namedBindings || !ts$1.isNamedImports(namedBindings)) continue;
|
|
3700
|
+
const importedNames = namedBindings.elements.map((element) => element.name.text);
|
|
3701
|
+
if (importedNames.length === 0 || !importedNames.every((name) => hydratedSymbols.has(name))) continue;
|
|
3702
|
+
replacements.push({
|
|
3703
|
+
range: toSourceRange(sourceFile, statement, { includeTrailingNewline: true }),
|
|
3704
|
+
newText: ""
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
return replacements;
|
|
3708
|
+
}
|
|
3709
|
+
function toSourceRange(sourceFile, node, options = {}) {
|
|
3710
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
3711
|
+
let endOffset = node.getEnd();
|
|
3712
|
+
if (options.includeTrailingNewline) {
|
|
3713
|
+
const match = sourceFile.text.slice(endOffset).match(/^\r?\n/);
|
|
3714
|
+
if (match) endOffset += match[0].length;
|
|
3715
|
+
}
|
|
3716
|
+
const end = sourceFile.getLineAndCharacterOfPosition(endOffset);
|
|
3717
|
+
return {
|
|
3718
|
+
startLine: start.line,
|
|
3719
|
+
startChar: start.character,
|
|
3720
|
+
endLine: end.line,
|
|
3721
|
+
endChar: end.character
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
function getScriptKind$1(fileName) {
|
|
3725
|
+
if (fileName.endsWith(".tsx")) return ts$1.ScriptKind.TSX;
|
|
3726
|
+
if (fileName.endsWith(".jsx")) return ts$1.ScriptKind.JSX;
|
|
3727
|
+
if (fileName.endsWith(".js")) return ts$1.ScriptKind.JS;
|
|
3728
|
+
return ts$1.ScriptKind.TS;
|
|
534
3729
|
}
|
|
535
3730
|
function removeOverlappingRanges(replacement, index, replacements) {
|
|
536
3731
|
return !replacements.some((candidate, candidateIndex) => {
|