@statelyai/sdk 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -25
- package/dist/embed.d.mts +43 -0
- package/dist/embed.mjs +175 -0
- package/dist/graph-C-7ZK_nK.d.mts +393 -0
- package/dist/graph.d.mts +2 -0
- package/dist/graph.mjs +344 -0
- package/dist/index.d.mts +34 -107
- package/dist/index.mjs +7 -200
- package/dist/inspect.d.mts +45 -0
- package/dist/inspect.mjs +122 -0
- package/dist/protocol-BC-_s3if.d.mts +172 -0
- package/dist/studio.d.mts +54 -0
- package/dist/studio.mjs +62 -0
- package/dist/sync-CzEOizjx.mjs +558 -0
- package/dist/sync.d.mts +43 -0
- package/dist/sync.mjs +5 -0
- package/dist/transport-D352iKKa.mjs +250 -0
- package/package.json +36 -8
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { createStatelyClient } from "./studio.mjs";
|
|
2
|
+
import { fromStudioMachine, toStudioMachine } from "./graph.mjs";
|
|
3
|
+
import { getDiff, isEmptyDiff } from "@statelyai/graph";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/serializeJS.ts
|
|
8
|
+
const VALID_IDENT = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
9
|
+
var RawCode = class {
|
|
10
|
+
constructor(code) {
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function raw(code) {
|
|
15
|
+
return new RawCode(code);
|
|
16
|
+
}
|
|
17
|
+
function serializeJS(value, indent = 0, step = 2) {
|
|
18
|
+
if (value instanceof RawCode) return value.code;
|
|
19
|
+
if (value === void 0) return "undefined";
|
|
20
|
+
if (value === null) return "null";
|
|
21
|
+
if (typeof value === "string") return `'${escapeString(value)}'`;
|
|
22
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
23
|
+
if (Array.isArray(value)) return serializeArray(value, indent, step);
|
|
24
|
+
if (typeof value === "object") return serializeObject(value, indent, step);
|
|
25
|
+
return String(value);
|
|
26
|
+
}
|
|
27
|
+
function escapeString(value) {
|
|
28
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
29
|
+
}
|
|
30
|
+
function serializeArray(array, indent, step) {
|
|
31
|
+
const values = array.filter((item) => item !== void 0);
|
|
32
|
+
if (values.length === 0) return "[]";
|
|
33
|
+
const innerIndent = indent + step;
|
|
34
|
+
const pad = " ".repeat(innerIndent);
|
|
35
|
+
const closePad = " ".repeat(indent);
|
|
36
|
+
return `[\n${values.map((item) => `${pad}${serializeJS(item, innerIndent, step)}`).join(",\n")},\n${closePad}]`;
|
|
37
|
+
}
|
|
38
|
+
function serializeObject(object, indent, step) {
|
|
39
|
+
const entries = Object.entries(object).filter(([, value]) => value !== void 0);
|
|
40
|
+
if (entries.length === 0) return "{}";
|
|
41
|
+
const innerIndent = indent + step;
|
|
42
|
+
const pad = " ".repeat(innerIndent);
|
|
43
|
+
const closePad = " ".repeat(indent);
|
|
44
|
+
return `{\n${entries.map(([key, value]) => {
|
|
45
|
+
const serializedKey = VALID_IDENT.test(key) ? key : `'${escapeString(key)}'`;
|
|
46
|
+
const serializedValue = serializeJS(value, innerIndent, step);
|
|
47
|
+
if (value instanceof RawCode && serializedValue.includes("\n")) return `${pad}${serializedKey}: ${indentRawCode(serializedValue, innerIndent)}`;
|
|
48
|
+
return `${pad}${serializedKey}: ${serializedValue}`;
|
|
49
|
+
}).join(",\n")},\n${closePad}}`;
|
|
50
|
+
}
|
|
51
|
+
function indentRawCode(code, indent) {
|
|
52
|
+
const lines = code.split("\n");
|
|
53
|
+
if (lines.length <= 1) return code;
|
|
54
|
+
const pad = " ".repeat(indent);
|
|
55
|
+
return [lines[0], ...lines.slice(1).map((line) => line.trim() === "" ? "" : `${pad}${line}`)].join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/graphToMachineConfig.ts
|
|
60
|
+
function singleOrArray(items) {
|
|
61
|
+
return items.length === 1 ? items[0] : items;
|
|
62
|
+
}
|
|
63
|
+
function singleOrArrayRecord(record) {
|
|
64
|
+
const next = {};
|
|
65
|
+
for (const [key, value] of Object.entries(record)) next[key] = singleOrArray(value);
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
function simplifyAttributes(value) {
|
|
69
|
+
for (const key of Object.keys(value)) {
|
|
70
|
+
const item = value[key];
|
|
71
|
+
if (item === void 0 || item === null) {
|
|
72
|
+
delete value[key];
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(item)) {
|
|
76
|
+
if (item.length === 0) delete value[key];
|
|
77
|
+
else if (item.length === 1) value[key] = item[0];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (typeof item === "object" && Object.keys(item).length === 0) {
|
|
81
|
+
delete value[key];
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (typeof item === "string" && item === "") delete value[key];
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function deepSimplify(config) {
|
|
89
|
+
for (const mapKey of ["on", "after"]) {
|
|
90
|
+
const value = config[mapKey];
|
|
91
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
92
|
+
for (const transition of Object.values(value)) if (transition && typeof transition === "object" && !Array.isArray(transition)) simplifyAttributes(transition);
|
|
93
|
+
else if (Array.isArray(transition)) {
|
|
94
|
+
for (const item of transition) if (item && typeof item === "object") simplifyAttributes(item);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (config.always && typeof config.always === "object") if (Array.isArray(config.always)) {
|
|
98
|
+
for (const item of config.always) if (item && typeof item === "object") simplifyAttributes(item);
|
|
99
|
+
} else simplifyAttributes(config.always);
|
|
100
|
+
simplifyAttributes(config);
|
|
101
|
+
if (config.states && typeof config.states === "object") for (const state of Object.values(config.states)) deepSimplify(state);
|
|
102
|
+
return config;
|
|
103
|
+
}
|
|
104
|
+
function serializeActionItem(action) {
|
|
105
|
+
const { type, params } = action;
|
|
106
|
+
switch (type) {
|
|
107
|
+
case "xstate.raise": return raw(`raise(${serializeJSValue(params?.event ?? { type: "unknown" })})`);
|
|
108
|
+
case "xstate.sendTo": return raw(`sendTo(${serializeJSValue(params?.to ?? "unknown")}, ${serializeJSValue(params?.event ?? { type: "unknown" })})`);
|
|
109
|
+
case "xstate.cancel": return raw(`cancel(${serializeJSValue(params?.sendId ?? "unknown")})`);
|
|
110
|
+
case "xstate.log": return raw(`log(${serializeJSValue(params?.label ?? "")})`);
|
|
111
|
+
default:
|
|
112
|
+
if (!params || Object.keys(params).length === 0) return { type };
|
|
113
|
+
return {
|
|
114
|
+
type,
|
|
115
|
+
params
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function serializeJSValue(value) {
|
|
120
|
+
if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
121
|
+
if (value && typeof value === "object") return rawObject(value);
|
|
122
|
+
return String(value);
|
|
123
|
+
}
|
|
124
|
+
function rawObject(value) {
|
|
125
|
+
return `{ ${Object.entries(value).map(([key, item]) => `${key}: ${serializeJSValue(item)}`).join(", ")} }`;
|
|
126
|
+
}
|
|
127
|
+
function graphToMachineConfig(graph, options = {}) {
|
|
128
|
+
const { showDescriptions = true } = options;
|
|
129
|
+
function getNodeConfig(nodeId) {
|
|
130
|
+
const node = graph.nodes.find((candidate) => candidate.id === nodeId);
|
|
131
|
+
if (!node) throw new Error(`Node not found: ${nodeId}`);
|
|
132
|
+
const config = {};
|
|
133
|
+
const initialNode = node.data.initialId ? graph.nodes.find((candidate) => candidate.id === node.data.initialId) : null;
|
|
134
|
+
Object.assign(config, {
|
|
135
|
+
id: node.parentId == null ? graph.id : node.id.endsWith(`.${node.data.key}`) ? void 0 : node.id,
|
|
136
|
+
...node.data.type ? { type: node.data.type } : {},
|
|
137
|
+
...initialNode ? { initial: initialNode.data.key } : {},
|
|
138
|
+
...node.data.entry?.length ? { entry: node.data.entry.map(serializeActionItem) } : {},
|
|
139
|
+
...node.data.exit?.length ? { exit: node.data.exit.map(serializeActionItem) } : {},
|
|
140
|
+
...node.data.invokes?.length ? { invoke: node.data.invokes.map((invoke) => ({
|
|
141
|
+
src: invoke.src,
|
|
142
|
+
id: invoke.id,
|
|
143
|
+
...invoke.input ? { input: invoke.input } : {}
|
|
144
|
+
})) } : {},
|
|
145
|
+
...node.data.tags?.length ? { tags: node.data.tags } : {},
|
|
146
|
+
...showDescriptions && node.data.description ? { description: node.data.description } : {},
|
|
147
|
+
...node.data.history ? { history: node.data.history } : {}
|
|
148
|
+
});
|
|
149
|
+
const childNodes = graph.nodes.filter((candidate) => candidate.parentId === node.id);
|
|
150
|
+
const edges = graph.edges.filter((edge) => edge.sourceId === node.id);
|
|
151
|
+
if (edges.length > 0) {
|
|
152
|
+
const on = {};
|
|
153
|
+
const after = {};
|
|
154
|
+
const always = [];
|
|
155
|
+
for (const edge of edges) {
|
|
156
|
+
const targetNode = graph.nodes.find((candidate) => candidate.id === edge.targetId);
|
|
157
|
+
const target = targetNode && targetNode.parentId === node.parentId ? targetNode.data.key : targetNode ? `#${targetNode.id}` : void 0;
|
|
158
|
+
const transition = {
|
|
159
|
+
...target ? { target } : {},
|
|
160
|
+
...edge.data.transitionType === "reenter" ? { reenter: true } : {},
|
|
161
|
+
...edge.data.guard ? { guard: edge.data.guard } : {},
|
|
162
|
+
...edge.data.actions?.length ? { actions: edge.data.actions.map(serializeActionItem) } : {},
|
|
163
|
+
...edge.data.description ? { description: edge.data.description } : {}
|
|
164
|
+
};
|
|
165
|
+
if (edge.data.eventType === "") {
|
|
166
|
+
always.push(transition);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (edge.data.eventType.startsWith("xstate.after.")) {
|
|
170
|
+
const delay = edge.data.eventType.slice(13).split(".")[0];
|
|
171
|
+
after[delay] ??= [];
|
|
172
|
+
after[delay].push(transition);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
on[edge.data.eventType] ??= [];
|
|
176
|
+
on[edge.data.eventType].push(transition);
|
|
177
|
+
}
|
|
178
|
+
if (Object.keys(on).length > 0) config.on = singleOrArrayRecord(on);
|
|
179
|
+
if (Object.keys(after).length > 0) config.after = singleOrArrayRecord(after);
|
|
180
|
+
if (always.length > 0) config.always = singleOrArray(always);
|
|
181
|
+
}
|
|
182
|
+
if (childNodes.length > 0) config.states = Object.fromEntries(childNodes.map((child) => [child.data.key, getNodeConfig(child.id)]));
|
|
183
|
+
return config;
|
|
184
|
+
}
|
|
185
|
+
const rootNode = graph.nodes.find((candidate) => candidate.parentId == null);
|
|
186
|
+
if (!rootNode) throw new Error("No root node found");
|
|
187
|
+
return deepSimplify(getNodeConfig(rootNode.id));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/jsonSchemaToTSType.ts
|
|
192
|
+
function jsonSchemaToTSType(schema) {
|
|
193
|
+
if (!schema) return "unknown";
|
|
194
|
+
if ("const" in schema) {
|
|
195
|
+
const value = schema.const;
|
|
196
|
+
return typeof value === "string" ? `'${value}'` : String(value);
|
|
197
|
+
}
|
|
198
|
+
if (schema.type === "array") return `Array<${jsonSchemaToTSType(schema.items)}>`;
|
|
199
|
+
if (schema.type === "object") {
|
|
200
|
+
const props = schema.properties;
|
|
201
|
+
if (!props || Object.keys(props).length === 0) return "Record<string, unknown>";
|
|
202
|
+
return `{ ${Object.entries(props).map(([key, value]) => `${key}: ${jsonSchemaToTSType(value)}`).join("; ")} }`;
|
|
203
|
+
}
|
|
204
|
+
switch (schema.type) {
|
|
205
|
+
case "string": return "string";
|
|
206
|
+
case "number": return "number";
|
|
207
|
+
case "boolean": return "boolean";
|
|
208
|
+
case "null": return "null";
|
|
209
|
+
default: return "unknown";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function contextSchemaToTSType(context) {
|
|
213
|
+
if (!context || Object.keys(context).length === 0) return null;
|
|
214
|
+
return `{ ${Object.entries(context).map(([key, schema]) => `${key}: ${jsonSchemaToTSType(schema)}`).join("; ")} }`;
|
|
215
|
+
}
|
|
216
|
+
function eventsSchemaToTSType(events) {
|
|
217
|
+
if (!events || Object.keys(events).length === 0) return null;
|
|
218
|
+
return Object.entries(events).map(([eventType, schema]) => {
|
|
219
|
+
const props = schema.properties;
|
|
220
|
+
const extraProps = Object.entries(props ?? {}).filter(([key]) => key !== "type").map(([key, value]) => `${key}?: ${jsonSchemaToTSType(value)}`);
|
|
221
|
+
if (extraProps.length === 0) return `{ type: '${eventType}' }`;
|
|
222
|
+
return `{ type: '${eventType}'; ${extraProps.join("; ")} }`;
|
|
223
|
+
}).join("\n | ");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/graphToXStateTS.ts
|
|
228
|
+
const BUILTIN_ACTION_IMPORTS = {
|
|
229
|
+
"xstate.raise": "raise",
|
|
230
|
+
"xstate.sendTo": "sendTo",
|
|
231
|
+
"xstate.cancel": "cancel",
|
|
232
|
+
"xstate.log": "log"
|
|
233
|
+
};
|
|
234
|
+
function graphToXStateTS(graph, options = {}) {
|
|
235
|
+
const { exportStyle = "named" } = options;
|
|
236
|
+
const schemas = graph.data.schemas;
|
|
237
|
+
const impls = graph.data.implementations;
|
|
238
|
+
const hasSchemas = Boolean(schemas && (schemas.context || schemas.events || schemas.input || schemas.output));
|
|
239
|
+
const hasActions = Boolean(impls?.actions.length);
|
|
240
|
+
const hasGuards = Boolean(impls?.guards.length);
|
|
241
|
+
const hasActors = Boolean(impls?.actors.length);
|
|
242
|
+
const hasDelays = Boolean(impls?.delays.length);
|
|
243
|
+
const hasSetup = hasSchemas || hasActions || hasGuards || hasActors || hasDelays;
|
|
244
|
+
const imports = new Set([hasSetup ? "setup" : "createMachine"]);
|
|
245
|
+
for (const node of graph.nodes) for (const action of [...node.data.entry ?? [], ...node.data.exit ?? []]) {
|
|
246
|
+
const builtInImport = BUILTIN_ACTION_IMPORTS[action.type];
|
|
247
|
+
if (builtInImport) imports.add(builtInImport);
|
|
248
|
+
}
|
|
249
|
+
for (const edge of graph.edges) for (const action of edge.data.actions ?? []) {
|
|
250
|
+
const builtInImport = BUILTIN_ACTION_IMPORTS[action.type];
|
|
251
|
+
if (builtInImport) imports.add(builtInImport);
|
|
252
|
+
}
|
|
253
|
+
const machineConfig = graphToMachineConfig(graph);
|
|
254
|
+
if (hasActors) imports.add("fromPromise");
|
|
255
|
+
const machineExpr = hasSetup ? `setup(${serializeJS(buildSetupObject(graph), 0)}).createMachine(${serializeJS(machineConfig, 0)})` : `createMachine(${serializeJS(machineConfig, 0)})`;
|
|
256
|
+
const lines = [
|
|
257
|
+
`import { ${Array.from(imports).join(", ")} } from 'xstate';`,
|
|
258
|
+
"",
|
|
259
|
+
`const machine = ${machineExpr};`
|
|
260
|
+
];
|
|
261
|
+
if (exportStyle === "named") lines.push("", "export { machine };");
|
|
262
|
+
else if (exportStyle === "default") lines.push("", "export default machine;");
|
|
263
|
+
return `${lines.join("\n")}\n`;
|
|
264
|
+
}
|
|
265
|
+
function buildSetupObject(graph) {
|
|
266
|
+
const setup = {};
|
|
267
|
+
const schemas = graph.data.schemas;
|
|
268
|
+
const impls = graph.data.implementations;
|
|
269
|
+
const types = buildTypesBlock(schemas);
|
|
270
|
+
if (types) setup.types = types;
|
|
271
|
+
if (impls?.actions.length) setup.actions = buildActionsBlock(impls.actions);
|
|
272
|
+
if (impls?.guards.length) setup.guards = buildGuardsBlock(impls.guards);
|
|
273
|
+
if (impls?.actors.length) setup.actors = buildActorsBlock(impls.actors);
|
|
274
|
+
if (impls?.delays.length) setup.delays = buildDelaysBlock(impls.delays);
|
|
275
|
+
return setup;
|
|
276
|
+
}
|
|
277
|
+
function buildTypesBlock(schemas) {
|
|
278
|
+
if (!schemas) return;
|
|
279
|
+
const types = {};
|
|
280
|
+
const contextType = contextSchemaToTSType(schemas.context);
|
|
281
|
+
if (contextType) types.context = raw(`{} as ${contextType}`);
|
|
282
|
+
const eventsType = eventsSchemaToTSType(schemas.events);
|
|
283
|
+
if (eventsType) types.events = raw(`{} as\n | ${eventsType}`);
|
|
284
|
+
if (schemas.input) types.input = raw(`{} as ${jsonSchemaToTSType(schemas.input)}`);
|
|
285
|
+
if (schemas.output) types.output = raw(`{} as ${jsonSchemaToTSType(schemas.output)}`);
|
|
286
|
+
return Object.keys(types).length > 0 ? types : void 0;
|
|
287
|
+
}
|
|
288
|
+
function buildActionsBlock(actions) {
|
|
289
|
+
return Object.fromEntries(actions.map((action) => [action.name, (() => {
|
|
290
|
+
const paramsType = action.paramsSchema ? `: ${jsonSchemaToTSType(action.paramsSchema)}` : "";
|
|
291
|
+
return raw(action.code?.body ? `function ({ context, event }, params${paramsType}) {\n ${action.code.body}\n}` : `function ({ context, event }) {\n // TODO: implement ${action.name}\n}`);
|
|
292
|
+
})()]));
|
|
293
|
+
}
|
|
294
|
+
function buildGuardsBlock(guards) {
|
|
295
|
+
return Object.fromEntries(guards.map((guard) => [guard.name, (() => {
|
|
296
|
+
const paramsType = guard.paramsSchema ? `: ${jsonSchemaToTSType(guard.paramsSchema)}` : "";
|
|
297
|
+
return raw(guard.code?.body ? `function ({ context, event }, params${paramsType}) {\n ${guard.code.body}\n}` : `function ({ context, event }) {\n // TODO: implement ${guard.name}\n return false;\n}`);
|
|
298
|
+
})()]));
|
|
299
|
+
}
|
|
300
|
+
function buildActorsBlock(actors) {
|
|
301
|
+
return Object.fromEntries(actors.map((actor) => [actor.name, (() => {
|
|
302
|
+
const inputType = actor.inputSchema ? `: { input: ${jsonSchemaToTSType(actor.inputSchema)} }` : "";
|
|
303
|
+
return raw(actor.code?.body ? `fromPromise(async ({ input }${inputType}) => {\n ${actor.code.body}\n})` : `fromPromise(async ({ input }) => {\n // TODO: implement ${actor.name}\n})`);
|
|
304
|
+
})()]));
|
|
305
|
+
}
|
|
306
|
+
function buildDelaysBlock(delays) {
|
|
307
|
+
return Object.fromEntries(delays.map((delay) => [delay.name, raw(delay.code?.body ? `function ({ context, event }) {\n ${delay.code.body}\n}` : `function () {\n // TODO: implement ${delay.name}\n return 1000;\n}`)]));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/sync.ts
|
|
312
|
+
function isUrl(value) {
|
|
313
|
+
try {
|
|
314
|
+
const url = new URL(value);
|
|
315
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function fileExists(filePath) {
|
|
321
|
+
try {
|
|
322
|
+
await fs.access(filePath);
|
|
323
|
+
return true;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function normalizeActions(value) {
|
|
329
|
+
if (value == null) return [];
|
|
330
|
+
return (Array.isArray(value) ? value : [value]).map((item) => {
|
|
331
|
+
if (typeof item === "string") return { type: item };
|
|
332
|
+
if (typeof item === "object" && item !== null && "type" in item) {
|
|
333
|
+
const type = item.type;
|
|
334
|
+
const params = "params" in item && typeof item.params === "object" && item.params ? item.params : void 0;
|
|
335
|
+
return {
|
|
336
|
+
type,
|
|
337
|
+
...params ? { params } : {}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return { type: String(item) };
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function normalizeTags(value) {
|
|
344
|
+
if (!value) return [];
|
|
345
|
+
if (Array.isArray(value)) return value.map((tag) => String(tag));
|
|
346
|
+
return [String(value)];
|
|
347
|
+
}
|
|
348
|
+
function normalizeInvoke(value) {
|
|
349
|
+
if (!value) return [];
|
|
350
|
+
return (Array.isArray(value) ? value : [value]).map((invoke, index) => ({
|
|
351
|
+
src: invoke.src,
|
|
352
|
+
id: invoke.id ?? `invoke[${index}]`,
|
|
353
|
+
...invoke.input ? { input: invoke.input } : {}
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
function resolveTargetId(sourceParentPath, target) {
|
|
357
|
+
if (!target) return;
|
|
358
|
+
if (target.startsWith("#")) {
|
|
359
|
+
const stripped = target.slice(1);
|
|
360
|
+
const machineIndex = stripped.indexOf(".");
|
|
361
|
+
if (machineIndex >= 0) return `(machine).${stripped.slice(machineIndex + 1)}`;
|
|
362
|
+
return `(machine).${stripped}`;
|
|
363
|
+
}
|
|
364
|
+
if (target.startsWith("(machine)")) return target;
|
|
365
|
+
if (target.includes(".")) return `(machine).${target}`;
|
|
366
|
+
return `${sourceParentPath}.${target}`;
|
|
367
|
+
}
|
|
368
|
+
function appendTransitionEdges(edges, sourceId, sourceParentPath, eventType, transition, edgeCounts) {
|
|
369
|
+
const transitions = Array.isArray(transition) ? transition : [transition];
|
|
370
|
+
for (const item of transitions) {
|
|
371
|
+
const normalized = typeof item === "string" ? { target: item } : item;
|
|
372
|
+
const targetId = resolveTargetId(sourceParentPath, Array.isArray(normalized.target) ? normalized.target[0] : normalized.target);
|
|
373
|
+
const groupKey = `${sourceId}:${eventType}`;
|
|
374
|
+
const index = edgeCounts.get(groupKey) ?? 0;
|
|
375
|
+
edgeCounts.set(groupKey, index + 1);
|
|
376
|
+
edges.push({
|
|
377
|
+
type: "edge",
|
|
378
|
+
id: `${sourceId}#${eventType}[${index}]`,
|
|
379
|
+
sourceId,
|
|
380
|
+
targetId: targetId ?? sourceId,
|
|
381
|
+
label: eventType,
|
|
382
|
+
data: {
|
|
383
|
+
eventType,
|
|
384
|
+
transitionType: normalized.reenter === true ? "reenter" : normalized.internal === true || !targetId ? "targetless" : "normal",
|
|
385
|
+
...normalized.guard ? { guard: typeof normalized.guard === "string" ? { type: normalized.guard } : {
|
|
386
|
+
type: normalized.guard.type,
|
|
387
|
+
...normalized.guard.params ? { params: normalized.guard.params } : {}
|
|
388
|
+
} } : {},
|
|
389
|
+
actions: normalizeActions(normalized.actions),
|
|
390
|
+
...normalized.description ? { description: normalized.description } : {}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function fromXStateConfig(config) {
|
|
396
|
+
const nodes = [];
|
|
397
|
+
const edges = [];
|
|
398
|
+
const edgeCounts = /* @__PURE__ */ new Map();
|
|
399
|
+
function visitNode(key, nodeConfig, parentId, parentPath) {
|
|
400
|
+
const nodeId = parentPath ? `${parentPath}.${key}` : "(machine)";
|
|
401
|
+
const currentPath = parentPath ? nodeId : "(machine)";
|
|
402
|
+
const initialId = nodeConfig.initial ? `${currentPath}.${nodeConfig.initial}` : void 0;
|
|
403
|
+
nodes.push({
|
|
404
|
+
type: "node",
|
|
405
|
+
id: nodeId,
|
|
406
|
+
parentId,
|
|
407
|
+
label: key,
|
|
408
|
+
...initialId ? { initialNodeId: initialId } : {},
|
|
409
|
+
data: {
|
|
410
|
+
key,
|
|
411
|
+
...nodeConfig.type ? { type: nodeConfig.type } : {},
|
|
412
|
+
...nodeConfig.history ? { history: nodeConfig.history } : {},
|
|
413
|
+
...initialId ? { initialId } : {},
|
|
414
|
+
entry: normalizeActions(nodeConfig.entry),
|
|
415
|
+
exit: normalizeActions(nodeConfig.exit),
|
|
416
|
+
invokes: normalizeInvoke(nodeConfig.invoke),
|
|
417
|
+
tags: normalizeTags(nodeConfig.tags),
|
|
418
|
+
...nodeConfig.description ? { description: nodeConfig.description } : {}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
for (const [eventType, transition] of Object.entries(nodeConfig.on ?? {})) appendTransitionEdges(edges, nodeId, parentPath ?? "(machine)", eventType, transition, edgeCounts);
|
|
422
|
+
if (nodeConfig.always) appendTransitionEdges(edges, nodeId, parentPath ?? "(machine)", "", nodeConfig.always, edgeCounts);
|
|
423
|
+
for (const [childKey, childConfig] of Object.entries(nodeConfig.states ?? {})) visitNode(childKey, childConfig, nodeId, currentPath);
|
|
424
|
+
}
|
|
425
|
+
visitNode("(machine)", config, null, null);
|
|
426
|
+
return {
|
|
427
|
+
id: config.id ?? "machine",
|
|
428
|
+
nodes,
|
|
429
|
+
edges,
|
|
430
|
+
data: {}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
async function resolveLocalFile(locator, options) {
|
|
434
|
+
const filePath = path.resolve(options.cwd ?? process.cwd(), locator);
|
|
435
|
+
const contents = await fs.readFile(filePath, "utf8");
|
|
436
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
437
|
+
if (extension === ".ts" || extension === ".tsx" || extension === ".js" || extension === ".jsx") {
|
|
438
|
+
const config = (await (options.client ?? createStatelyClient({
|
|
439
|
+
apiKey: options.apiKey,
|
|
440
|
+
baseUrl: options.baseUrl,
|
|
441
|
+
fetch: options.fetch
|
|
442
|
+
})).code.extractMachines(contents)).machines[0]?.config;
|
|
443
|
+
if (!config) throw new Error(`No machines extracted from ${filePath}`);
|
|
444
|
+
return {
|
|
445
|
+
kind: "local-file",
|
|
446
|
+
locator: filePath,
|
|
447
|
+
format: "xstate",
|
|
448
|
+
graph: fromXStateConfig(config)
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const parsed = JSON.parse(contents);
|
|
452
|
+
if ("rootNode" in parsed && "edges" in parsed) return {
|
|
453
|
+
kind: "local-file",
|
|
454
|
+
locator: filePath,
|
|
455
|
+
format: "digraph",
|
|
456
|
+
graph: fromStudioMachine(parsed)
|
|
457
|
+
};
|
|
458
|
+
if ("nodes" in parsed && "edges" in parsed) return {
|
|
459
|
+
kind: "local-file",
|
|
460
|
+
locator: filePath,
|
|
461
|
+
format: "graph",
|
|
462
|
+
graph: parsed
|
|
463
|
+
};
|
|
464
|
+
if ("states" in parsed) return {
|
|
465
|
+
kind: "local-file",
|
|
466
|
+
locator: filePath,
|
|
467
|
+
format: "xstate",
|
|
468
|
+
graph: fromXStateConfig(parsed)
|
|
469
|
+
};
|
|
470
|
+
throw new Error(`Unsupported local sync input: ${filePath}`);
|
|
471
|
+
}
|
|
472
|
+
function inferWritableTargetFormat(filePath) {
|
|
473
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx") || filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "xstate";
|
|
474
|
+
if (filePath.endsWith(".digraph.json")) return "digraph";
|
|
475
|
+
if (filePath.endsWith(".graph.json")) return "graph";
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
function serializeGraph(graph, format) {
|
|
479
|
+
switch (format) {
|
|
480
|
+
case "digraph": return `${JSON.stringify(toStudioMachine(graph), null, 2)}\n`;
|
|
481
|
+
case "graph": return `${JSON.stringify(graph, null, 2)}\n`;
|
|
482
|
+
case "xstate": return graphToXStateTS(graph);
|
|
483
|
+
default: {
|
|
484
|
+
const exhaustive = format;
|
|
485
|
+
throw new Error(`Unsupported sync output format: ${exhaustive}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function resolveRemoteMachine(machineId, baseUrl, kind, locator, options) {
|
|
490
|
+
return {
|
|
491
|
+
kind,
|
|
492
|
+
locator,
|
|
493
|
+
format: "digraph",
|
|
494
|
+
graph: fromStudioMachine(await (options.client ?? createStatelyClient({
|
|
495
|
+
apiKey: options.apiKey,
|
|
496
|
+
baseUrl,
|
|
497
|
+
fetch: options.fetch
|
|
498
|
+
})).machines.get(machineId))
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async function resolveSyncInput(locator, options) {
|
|
502
|
+
if (await fileExists(path.resolve(options.cwd ?? process.cwd(), locator))) return resolveLocalFile(locator, options);
|
|
503
|
+
if (isUrl(locator)) {
|
|
504
|
+
const url = new URL(locator);
|
|
505
|
+
const machineId = url.pathname.split("/").filter(Boolean).at(-1);
|
|
506
|
+
if (!machineId) throw new Error(`Could not resolve machine ID from URL: ${locator}`);
|
|
507
|
+
return resolveRemoteMachine(machineId, url.origin, "studio-url", locator, options);
|
|
508
|
+
}
|
|
509
|
+
return resolveRemoteMachine(locator, options.baseUrl, "studio-machine-id", locator, options);
|
|
510
|
+
}
|
|
511
|
+
function summarizeDiff(diff) {
|
|
512
|
+
const nodeChanges = diff.nodes.added.length + diff.nodes.removed.length + diff.nodes.updated.length;
|
|
513
|
+
const edgeChanges = diff.edges.added.length + diff.edges.removed.length + diff.edges.updated.length;
|
|
514
|
+
return {
|
|
515
|
+
hasChanges: !isEmptyDiff(diff),
|
|
516
|
+
nodeChanges,
|
|
517
|
+
edgeChanges
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async function planSync(options) {
|
|
521
|
+
const source = await resolveSyncInput(options.source, options);
|
|
522
|
+
const target = await resolveSyncInput(options.target, options);
|
|
523
|
+
const diff = getDiff(source.graph, target.graph);
|
|
524
|
+
return {
|
|
525
|
+
source,
|
|
526
|
+
target,
|
|
527
|
+
diff,
|
|
528
|
+
summary: summarizeDiff(diff),
|
|
529
|
+
warnings: []
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async function pullSync(options) {
|
|
533
|
+
const source = await resolveSyncInput(options.source, options);
|
|
534
|
+
const outputPath = path.resolve(options.cwd ?? process.cwd(), options.target);
|
|
535
|
+
const fallbackFormat = inferWritableTargetFormat(outputPath);
|
|
536
|
+
let targetFormat = fallbackFormat;
|
|
537
|
+
if (await fileExists(outputPath)) try {
|
|
538
|
+
targetFormat = (await resolveLocalFile(options.target, options)).format;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (!fallbackFormat) throw error;
|
|
541
|
+
}
|
|
542
|
+
if (!targetFormat) throw new Error(`Could not infer a writable target format from ${outputPath}. Use an existing digraph/graph file or a .digraph.json/.graph.json target.`);
|
|
543
|
+
const serialized = serializeGraph(source.graph, targetFormat);
|
|
544
|
+
await fs.writeFile(outputPath, serialized, "utf8");
|
|
545
|
+
return {
|
|
546
|
+
source,
|
|
547
|
+
target: {
|
|
548
|
+
kind: "local-file",
|
|
549
|
+
locator: outputPath,
|
|
550
|
+
format: targetFormat,
|
|
551
|
+
graph: source.graph
|
|
552
|
+
},
|
|
553
|
+
outputPath
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
//#endregion
|
|
558
|
+
export { pullSync as n, planSync as t };
|
package/dist/sync.d.mts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { i as StatelyGraph } from "./graph-C-7ZK_nK.mjs";
|
|
2
|
+
import { StudioClient } from "./studio.mjs";
|
|
3
|
+
import { GraphDiff } from "@statelyai/graph";
|
|
4
|
+
|
|
5
|
+
//#region src/sync.d.ts
|
|
6
|
+
interface PlanSyncOptions {
|
|
7
|
+
source: string;
|
|
8
|
+
target: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
client?: StudioClient;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}
|
|
15
|
+
type ResolvedSyncInputKind = 'local-file' | 'studio-machine-id' | 'studio-url';
|
|
16
|
+
type SyncInputFormat = 'digraph' | 'graph' | 'xstate';
|
|
17
|
+
interface ResolvedSyncInput {
|
|
18
|
+
kind: ResolvedSyncInputKind;
|
|
19
|
+
locator: string;
|
|
20
|
+
format: SyncInputFormat;
|
|
21
|
+
graph: StatelyGraph;
|
|
22
|
+
}
|
|
23
|
+
interface SyncPlanSummary {
|
|
24
|
+
hasChanges: boolean;
|
|
25
|
+
nodeChanges: number;
|
|
26
|
+
edgeChanges: number;
|
|
27
|
+
}
|
|
28
|
+
interface SyncPlan {
|
|
29
|
+
source: ResolvedSyncInput;
|
|
30
|
+
target: ResolvedSyncInput;
|
|
31
|
+
diff: GraphDiff;
|
|
32
|
+
summary: SyncPlanSummary;
|
|
33
|
+
warnings: string[];
|
|
34
|
+
}
|
|
35
|
+
interface PullSyncResult {
|
|
36
|
+
source: ResolvedSyncInput;
|
|
37
|
+
target: ResolvedSyncInput;
|
|
38
|
+
outputPath: string;
|
|
39
|
+
}
|
|
40
|
+
declare function planSync(options: PlanSyncOptions): Promise<SyncPlan>;
|
|
41
|
+
declare function pullSync(options: PlanSyncOptions): Promise<PullSyncResult>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { PlanSyncOptions, PullSyncResult, ResolvedSyncInput, SyncInputFormat, SyncPlan, SyncPlanSummary, planSync, pullSync };
|