process-graph 1.3.0 → 2.0.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 +21 -19
- package/dist/bin.js +2 -2
- package/dist/cli.d.ts +56 -76
- package/dist/cli.js +4 -3
- package/dist/{data-file-BVMxkTug.js → data-file-B8__c10l.js} +1 -1
- package/dist/dist-B0mKgR5B.js +4 -0
- package/dist/{dist-BI01yxFA.js → dist-BnymJpfk.js} +258 -473
- package/dist/graphql-CGWZ6USe.d.ts +17 -0
- package/dist/index-7nV7hCmv.d.ts +118 -0
- package/dist/{index-BC4tmAuQ.d.ts → index-B2p3mEJ3.d.ts} +2 -2
- package/dist/{index-9Au8pAmP.d.ts → index-C-ZNF81j.d.ts} +5 -24
- package/dist/{index-hri2a13-.d.ts → index-CiaCmQDP.d.ts} +2 -1
- package/dist/{graphql-BE7xm5dB.d.ts → index-CotF9Lhh.d.ts} +4 -45
- package/dist/index-DAevZqW-.d.ts +71 -0
- package/dist/{index-Dvz_I_uQ.d.ts → index-DYl6lZLe.d.ts} +29 -1
- package/dist/{index-D06bcdfw.d.ts → index-DluJZTbT.d.ts} +23 -2
- package/dist/{index-CR8xUK3S.d.ts → index-KhygugOP.d.ts} +6 -44
- package/dist/{index-D4CnOhNf.d.ts → index-wQnLXJmD.d.ts} +1 -1
- package/dist/index.d.ts +25 -4
- package/dist/index.js +4 -3
- package/dist/layout.d.ts +1 -1
- package/dist/project-CP_ye6KI.js +27 -0
- package/dist/react-flow.d.ts +5 -5
- package/dist/react.d.ts +2 -1
- package/dist/react.js +1 -24
- package/dist/{router-Dk91O_9p.js → router-DzVcQ28U.js} +2 -2
- package/dist/{schema-BPT8fmfa.js → schema-CL95Lyni.js} +24 -13
- package/dist/schema-DMXa8gAZ.js +117 -0
- package/dist/source/github/node.d.ts +1 -1
- package/dist/source/github.d.ts +4 -3
- package/dist/source/github.js +39 -2
- package/dist/{source-D51N7K3A.js → source-DM4fLS5P.js} +2 -161
- package/dist/store/node.d.ts +1 -1
- package/dist/store/node.js +1 -1
- package/dist/store/wasm/worker.js +2 -2
- package/dist/store/wasm.d.ts +1 -1
- package/dist/{store-DKzvLGgl.js → store-CgweIG8J.js} +3 -12
- package/dist/store.d.ts +2 -2
- package/dist/store.js +3 -3
- package/dist/sync-CkaJO3OQ.js +213 -0
- package/dist/sync.d.ts +26 -0
- package/dist/sync.js +16 -0
- package/dist/ui.d.ts +4 -4
- package/dist/{version-Dagrkvwt.js → version-_UOnL566.js} +1 -1
- package/dist/view-DcktInd7.js +392 -0
- package/package.json +21 -15
- package/dist/dist-81kTKnMq.js +0 -3
- package/dist/parse-C1Q1zsGt.js +0 -41
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { n as modelSchema, s as toPointer } from "./schema-CL95Lyni.js";
|
|
2
|
+
import { t as validate } from "./validate-DF3mQZww.js";
|
|
3
|
+
//#region ../../core/graph/schema/dist/parse.js
|
|
4
|
+
/** Thrown by `parseModel` when the input is not a valid model. */
|
|
5
|
+
var ModelParseError = class extends Error {
|
|
6
|
+
/** Every problem found, in the order the schema and then `validate` reported them. Never empty. */
|
|
7
|
+
issues;
|
|
8
|
+
/**
|
|
9
|
+
* @param issues - The problems found. Must not be empty.
|
|
10
|
+
*/
|
|
11
|
+
constructor(issues) {
|
|
12
|
+
super(`Invalid model: ${issues.map((issue) => issue.source === "schema" ? `${issue.pointer === "" ? "/" : issue.pointer}: ${issue.message}` : issue.message).join("; ")}`);
|
|
13
|
+
this.name = "ModelParseError";
|
|
14
|
+
this.issues = issues;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Loads a model from untrusted input, such as parsed JSON.
|
|
19
|
+
*
|
|
20
|
+
* The input is checked against the open schema first, so every shape problem is reported with the JSON pointer of the offending value. Only a well-shaped model is then passed to `validate`, whose issues are surfaced the same way.
|
|
21
|
+
* @param input - The value to load.
|
|
22
|
+
* @returns The validated model, with identifiers as plain strings.
|
|
23
|
+
* @throws ModelParseError when the input is not a well-shaped model or fails `validate`.
|
|
24
|
+
*/
|
|
25
|
+
const parseModel = (input) => {
|
|
26
|
+
const parsed = modelSchema.safeParse(input);
|
|
27
|
+
if (!parsed.success) throw new ModelParseError(parsed.error.issues.map((issue) => ({
|
|
28
|
+
source: "schema",
|
|
29
|
+
pointer: toPointer(issue.path),
|
|
30
|
+
message: issue.message
|
|
31
|
+
})));
|
|
32
|
+
const issues = validate(parsed.data);
|
|
33
|
+
if (issues.length > 0) throw new ModelParseError(issues.map((issue) => ({
|
|
34
|
+
source: "validate",
|
|
35
|
+
code: issue.code,
|
|
36
|
+
message: issue.message
|
|
37
|
+
})));
|
|
38
|
+
return parsed.data;
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region ../../core/graph/export/dist/terminal-text.js
|
|
42
|
+
/**
|
|
43
|
+
* The characters that must never reach a terminal as they are, by Unicode general category: controls (`Cc`, where ESC starts the sequences that clear the screen, move the cursor or, through OSC 52, set the clipboard, and the C1 controls some terminals read as the same sequences), format characters (`Cf`, which holds every bidirectional control, the invisible operators, the soft hyphen, the interlinear annotation characters and the tag characters, all of which reorder, hide or change what is shown), the line and paragraph separators (`Zl`, `Zp`), and the unassigned and private-use code points (`Cn`, `Co`), which no font shows reliably. Text from a source (an issue title, a label, an actor's name) can carry any of them. A category, not a list of ranges, so a character Unicode adds to one of them is covered without a change here.
|
|
44
|
+
*
|
|
45
|
+
* Three are allowed: line feed and tab, which every text output uses, and the zero-width non-joiner and joiner (U+200C, U+200D), formats that emoji sequences (a family, a profession) and scripts such as Persian, Hindi and Malayalam need to be written correctly, and that cannot reorder or hide text on their own.
|
|
46
|
+
*/
|
|
47
|
+
const unsafeCharacters = /[[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cn}\p{Co}]--[\n\t\u200C\u200D]]/gv;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a character is one of the unsafe ones (see `unsafeCharacters`).
|
|
50
|
+
* @param character - One character: one code point, which may be two UTF-16 code units.
|
|
51
|
+
* @returns Whether it must be escaped.
|
|
52
|
+
*/
|
|
53
|
+
const isUnsafeCharacter = (character) => character.replaceAll(unsafeCharacters, "") !== character;
|
|
54
|
+
/** Code points are written in hexadecimal. */
|
|
55
|
+
const hexadecimal = 16;
|
|
56
|
+
/** A JSON `\u` escape always has four hexadecimal digits. */
|
|
57
|
+
const jsonEscapeDigits = 4;
|
|
58
|
+
/**
|
|
59
|
+
* A character's code point.
|
|
60
|
+
* @param character - One character, as the unsafe-character pattern matches it.
|
|
61
|
+
* @returns Its code point.
|
|
62
|
+
* @throws Error when given an empty string, which the pattern never matches.
|
|
63
|
+
*/
|
|
64
|
+
const codePointOf = (character) => {
|
|
65
|
+
const point = character.codePointAt(0);
|
|
66
|
+
if (point === void 0) throw new Error("an unsafe character match was empty");
|
|
67
|
+
return point;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Text made safe to write to a terminal. Each unsafe character (see `unsafeCharacters`) is written as the visible escape `\u{<hex>}` of its code point, such as `\u{1b}` for ESC, `\u{202e}` for a right-to-left override or `\u{e0041}` for a tag character, so nothing is lost and nothing acts on the terminal. Line feed, tab, the zero-width joiners and every other character, accented letters, CJK, Arabic and emoji included, pass through. A literal `\u{...}` already in the text is left as it is, so the escaped form is for reading, not for reversing.
|
|
71
|
+
* @param text - The text as written.
|
|
72
|
+
* @returns The text with every unsafe character escaped.
|
|
73
|
+
*/
|
|
74
|
+
const terminalText = (text) => text.replaceAll(unsafeCharacters, (character) => `\\u{${codePointOf(character).toString(hexadecimal)}}`);
|
|
75
|
+
/**
|
|
76
|
+
* A value as JSON that is also safe to write to a terminal. `JSON.stringify` escapes the C0 controls but writes every other unsafe character (see `unsafeCharacters`) as it is; this writes those as `\u` escapes too, a surrogate pair for a code point beyond the Basic Multilingual Plane, which leaves the JSON, and every value parsed from it, exactly the same.
|
|
77
|
+
* @param value - The value to write.
|
|
78
|
+
* @returns The JSON text, indented by two spaces and ending in a line break.
|
|
79
|
+
*/
|
|
80
|
+
const terminalJson = (value) => `${JSON.stringify(value, null, 2).replaceAll(unsafeCharacters, (character) => character.split("").map((unit) => `\\u${unit.charCodeAt(0).toString(hexadecimal).padStart(jsonEscapeDigits, "0")}`).join(""))}\n`;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region ../../core/graph/export/dist/text.js
|
|
83
|
+
/**
|
|
84
|
+
* The Mermaid entity code for a character that would end or change the meaning of a label: quotes, statement and edge-label delimiters, the entity marker itself and HTML.
|
|
85
|
+
*/
|
|
86
|
+
const mermaidEntity = (character) => {
|
|
87
|
+
switch (character) {
|
|
88
|
+
case "#": return "#35;";
|
|
89
|
+
case "\"": return "#quot;";
|
|
90
|
+
case "<": return "#lt;";
|
|
91
|
+
case ">": return "#gt;";
|
|
92
|
+
case "&": return "#amp;";
|
|
93
|
+
case ";": return "#59;";
|
|
94
|
+
case "|": return "#124;";
|
|
95
|
+
case ":": return "#58;";
|
|
96
|
+
default: return character;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Text made safe to place inside any Mermaid label: a line break becomes a space, every control and invisible format character is written as its visible `\u{<hex>}` escape (see `terminalText`), and every character that Mermaid reads as syntax is written as its entity code, so a label from a source (an issue title, a repository name) can never break the diagram or act on the terminal it is printed to.
|
|
101
|
+
* @param text - The label as written.
|
|
102
|
+
* @returns The label as Mermaid source.
|
|
103
|
+
*/
|
|
104
|
+
const mermaidText = (text) => terminalText(text.replaceAll(/[\r\n]/gu, " ")).replaceAll(/[#"<>&;|:]/gu, mermaidEntity);
|
|
105
|
+
/**
|
|
106
|
+
* A Graphviz DOT double-quoted string: line breaks written as DOT's own centred line break, every control and invisible format character as its visible `\u{<hex>}` escape (see `terminalText`), then backslashes, the escapes' included, and quotes escaped. Any identifier or label is valid DOT once quoted this way, and reads back as the escaped text.
|
|
107
|
+
* @param text - The text as written.
|
|
108
|
+
* @returns The quoted DOT string, quotes included.
|
|
109
|
+
*/
|
|
110
|
+
const dotString = (text) => `"${text.split(/\r?\n/u).map((line) => terminalText(line).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")).join("\\n")}"`;
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region ../../core/graph/export/dist/mermaid.js
|
|
113
|
+
/**
|
|
114
|
+
* Model identifiers are free text (`github:org/repo#12`), which Mermaid does not accept as a node identifier, so every emitter names things by position in the view instead: a prefix and the index. The view's order is the model's order, so the same model always gives the same names.
|
|
115
|
+
*/
|
|
116
|
+
const positionalIds = (prefix, ids) => new Map(ids.map((id, index) => [id, `${prefix}${String(index)}`]));
|
|
117
|
+
/** Reads a name the emitter itself assigned, which exists for every identifier the view lists. */
|
|
118
|
+
const nameIn = (names, id) => {
|
|
119
|
+
const name = names.get(id);
|
|
120
|
+
if (name === void 0) throw new Error(`"${id}" is not part of the view being emitted`);
|
|
121
|
+
return name;
|
|
122
|
+
};
|
|
123
|
+
const indent$1 = " ";
|
|
124
|
+
/** Each flowchart shape in Mermaid's syntax around a node name and quoted label: a stadium for a terminator, a rectangle for a process, a rhombus for a decision and a fork bar for a fork or join. */
|
|
125
|
+
const mermaidShapes = {
|
|
126
|
+
terminator: (name, label) => `${name}(["${label}"])`,
|
|
127
|
+
process: (name, label) => `${name}["${label}"]`,
|
|
128
|
+
decision: (name, label) => `${name}{"${label}"}`,
|
|
129
|
+
bar: (name, label) => `${name}@{ shape: fork, label: "${label}" }`
|
|
130
|
+
};
|
|
131
|
+
const mermaidNode = (name, node) => mermaidShapes[node.shape](name, mermaidText(node.label));
|
|
132
|
+
/** The edges of a flowchart, a hand-off (an edge whose ends are in different lanes) drawn thick. */
|
|
133
|
+
const mermaidEdges = (view, names, isHandoff) => view.edges.map((edge) => {
|
|
134
|
+
const arrow = isHandoff(edge.from, edge.to) ? "==>" : "-->";
|
|
135
|
+
const label = edge.label === void 0 ? "" : `|"${mermaidText(edge.label)}"|`;
|
|
136
|
+
return `${indent$1}${nameIn(names, edge.from)} ${arrow}${label} ${nameIn(names, edge.to)}`;
|
|
137
|
+
});
|
|
138
|
+
/**
|
|
139
|
+
* A flowchart as a Mermaid `flowchart`, top to bottom. Nodes are declared in the view's order, then edges, each with its guard as the edge label.
|
|
140
|
+
* @param view - The projected flowchart.
|
|
141
|
+
* @returns Mermaid source, ending with a line break.
|
|
142
|
+
*/
|
|
143
|
+
const flowchartToMermaid = (view) => {
|
|
144
|
+
const names = positionalIds("n", view.nodes.map((node) => node.id));
|
|
145
|
+
return `${[
|
|
146
|
+
"flowchart TD",
|
|
147
|
+
...view.nodes.map((node) => `${indent$1}${mermaidNode(nameIn(names, node.id), node)}`),
|
|
148
|
+
...mermaidEdges(view, names, () => false)
|
|
149
|
+
].join("\n")}\n`;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* A swimlane as a Mermaid `flowchart`, left to right, with one subgraph per lane in the view's lane order (an empty lane is an empty subgraph, so it stays visible) and nodes with no actor outside every lane. Hand-offs, the edges that change lane, are drawn thick.
|
|
153
|
+
* @param view - The projected swimlane.
|
|
154
|
+
* @returns Mermaid source, ending with a line break.
|
|
155
|
+
*/
|
|
156
|
+
const swimlaneToMermaid = (view) => {
|
|
157
|
+
const names = positionalIds("n", view.nodes.map((node) => node.id));
|
|
158
|
+
const nodes = new Map(view.nodes.map((node) => [node.id, node]));
|
|
159
|
+
const laneOf = /* @__PURE__ */ new Map();
|
|
160
|
+
view.lanes.forEach((lane, index) => {
|
|
161
|
+
for (const id of lane.nodes) laneOf.set(id, index);
|
|
162
|
+
});
|
|
163
|
+
const declare = (id, depth) => {
|
|
164
|
+
const node = nodes.get(id);
|
|
165
|
+
if (node === void 0) throw new Error(`lane member "${id}" is not a node of the view`);
|
|
166
|
+
return `${depth}${mermaidNode(nameIn(names, id), node)}`;
|
|
167
|
+
};
|
|
168
|
+
return `${[
|
|
169
|
+
"flowchart LR",
|
|
170
|
+
...view.lanes.flatMap((lane, index) => [
|
|
171
|
+
`${indent$1}subgraph lane${String(index)}["${mermaidText(lane.label)}"]`,
|
|
172
|
+
...lane.nodes.map((id) => declare(id, indent$1.repeat(2))),
|
|
173
|
+
`${indent$1}end`
|
|
174
|
+
]),
|
|
175
|
+
...view.unassigned.map((id) => declare(id, indent$1)),
|
|
176
|
+
...mermaidEdges(view, names, (from, to) => {
|
|
177
|
+
const fromLane = laneOf.get(from);
|
|
178
|
+
const toLane = laneOf.get(to);
|
|
179
|
+
return fromLane !== void 0 && toLane !== void 0 && fromLane !== toLane;
|
|
180
|
+
})
|
|
181
|
+
].join("\n")}\n`;
|
|
182
|
+
};
|
|
183
|
+
/** A state machine transition's label: its event, its guard in brackets and its effect after a slash, each only when present. */
|
|
184
|
+
const transitionLabel = (transition) => [
|
|
185
|
+
transition.event,
|
|
186
|
+
transition.guard === void 0 ? void 0 : `[${transition.guard}]`,
|
|
187
|
+
transition.effect === void 0 ? void 0 : `/ ${transition.effect}`
|
|
188
|
+
].filter((part) => part !== void 0).join(" ");
|
|
189
|
+
/**
|
|
190
|
+
* A state machine as a Mermaid `stateDiagram-v2`. Initial and final pseudo-states are Mermaid's `[*]`; every other state is declared with its label. Each transition carries its event, guard and effect.
|
|
191
|
+
* @param view - The projected state machine.
|
|
192
|
+
* @returns Mermaid source, ending with a line break.
|
|
193
|
+
*/
|
|
194
|
+
const stateMachineToMermaid = (view) => {
|
|
195
|
+
const drawn = view.states.filter((state) => state.role === "state");
|
|
196
|
+
const names = new Map([...positionalIds("s", drawn.map((state) => state.id)), ...view.states.filter((state) => state.role !== "state").map((state) => [state.id, "[*]"])]);
|
|
197
|
+
return `${[
|
|
198
|
+
"stateDiagram-v2",
|
|
199
|
+
...drawn.map((state) => `${indent$1}state "${mermaidText(state.label)}" as ${nameIn(names, state.id)}`),
|
|
200
|
+
...view.transitions.map((transition) => {
|
|
201
|
+
const label = transitionLabel(transition);
|
|
202
|
+
return `${indent$1}${nameIn(names, transition.source)} --> ${nameIn(names, transition.target)}${label === "" ? "" : ` : ${mermaidText(label)}`}`;
|
|
203
|
+
})
|
|
204
|
+
].join("\n")}\n`;
|
|
205
|
+
};
|
|
206
|
+
/** The Mermaid keyword that separates one operand of a fragment from the next, for the operators with more than one operand. */
|
|
207
|
+
const operandSeparator = (operator) => operator === "par" ? "and" : "else";
|
|
208
|
+
/**
|
|
209
|
+
* A sequence diagram as a Mermaid `sequenceDiagram`. Lifelines are declared in order of first appearance, labelled through `participantLabel`; messages follow in scenario order, wrapped in the fragments the projection drew (`alt` operands separated by `else`, `par` operands by `and`), each operand headed by its guard.
|
|
210
|
+
* @param view - The projected sequence diagram.
|
|
211
|
+
* @param participantLabel - The label of a lifeline's participant.
|
|
212
|
+
* @returns Mermaid source, ending with a line break.
|
|
213
|
+
*/
|
|
214
|
+
const sequenceToMermaid = (view, participantLabel) => {
|
|
215
|
+
const names = positionalIds("p", view.lifelines);
|
|
216
|
+
const items = (list, depth) => list.flatMap((item) => {
|
|
217
|
+
const prefix = indent$1.repeat(depth);
|
|
218
|
+
if (item.kind === "message") {
|
|
219
|
+
const { message } = item;
|
|
220
|
+
return [`${prefix}${nameIn(names, message.from)}->>${nameIn(names, message.to)}: ${mermaidText(message.label)}`];
|
|
221
|
+
}
|
|
222
|
+
return [...item.operands.flatMap((operand, index) => {
|
|
223
|
+
const keyword = index === 0 ? item.operator : operandSeparator(item.operator);
|
|
224
|
+
const guard = operand.guard === void 0 ? "" : ` ${mermaidText(operand.guard)}`;
|
|
225
|
+
return [`${prefix}${keyword}${guard}`, ...items(operand.items, depth + 1)];
|
|
226
|
+
}), `${prefix}end`];
|
|
227
|
+
});
|
|
228
|
+
return `${[
|
|
229
|
+
"sequenceDiagram",
|
|
230
|
+
...view.lifelines.map((lifeline) => `${indent$1}participant ${nameIn(names, lifeline)} as ${mermaidText(participantLabel(lifeline))}`),
|
|
231
|
+
...items(view.items, 1)
|
|
232
|
+
].join("\n")}\n`;
|
|
233
|
+
};
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region ../../core/graph/export/dist/dot.js
|
|
236
|
+
const indent = " ";
|
|
237
|
+
/** A DOT attribute list from name and value pairs, each value quoted; empty when there are none. */
|
|
238
|
+
const attributes = (pairs) => pairs.length === 0 ? "" : ` [${pairs.map(([name, value]) => `${name}=${dotString(value)}`).join(", ")}]`;
|
|
239
|
+
/** A flowchart node's DOT shape: an oval terminator, a box process, a diamond decision, and a filled bar for a fork or join. */
|
|
240
|
+
const nodeShapes = {
|
|
241
|
+
terminator: [["shape", "oval"]],
|
|
242
|
+
process: [["shape", "box"]],
|
|
243
|
+
decision: [["shape", "diamond"]],
|
|
244
|
+
bar: [
|
|
245
|
+
["shape", "box"],
|
|
246
|
+
["style", "filled"],
|
|
247
|
+
["fillcolor", "black"],
|
|
248
|
+
["fontcolor", "white"]
|
|
249
|
+
]
|
|
250
|
+
};
|
|
251
|
+
const dotNode = (node, depth) => `${depth}${dotString(node.id)}${attributes([["label", node.label], ...nodeShapes[node.shape]])};`;
|
|
252
|
+
/** The edges of a flowchart, a hand-off (an edge whose ends are in different lanes) drawn bold. */
|
|
253
|
+
const dotEdges = (view, isHandoff) => view.edges.map((edge) => `${indent}${dotString(edge.from)} -> ${dotString(edge.to)}${attributes([...edge.label === void 0 ? [] : [["label", edge.label]], ...isHandoff(edge.from, edge.to) ? [["style", "bold"]] : []])};`);
|
|
254
|
+
/**
|
|
255
|
+
* A flowchart as a Graphviz `digraph`, top to bottom. Nodes are named by their model identifiers, so the DOT source reads back to the model directly.
|
|
256
|
+
* @param view - The projected flowchart.
|
|
257
|
+
* @returns DOT source, ending with a line break.
|
|
258
|
+
*/
|
|
259
|
+
const flowchartToDot = (view) => `${[
|
|
260
|
+
`digraph ${dotString("flowchart")} {`,
|
|
261
|
+
`${indent}rankdir="TB";`,
|
|
262
|
+
...view.nodes.map((node) => dotNode(node, indent)),
|
|
263
|
+
...dotEdges(view, () => false),
|
|
264
|
+
"}"
|
|
265
|
+
].join("\n")}\n`;
|
|
266
|
+
/**
|
|
267
|
+
* A swimlane as a Graphviz `digraph`, left to right, with one `cluster` subgraph per lane in the view's lane order and nodes with no actor outside every lane. An empty lane holds one invisible point so Graphviz still draws it. Hand-offs, the edges that change lane, are drawn bold.
|
|
268
|
+
* @param view - The projected swimlane.
|
|
269
|
+
* @returns DOT source, ending with a line break.
|
|
270
|
+
*/
|
|
271
|
+
const swimlaneToDot = (view) => {
|
|
272
|
+
const nodes = new Map(view.nodes.map((node) => [node.id, node]));
|
|
273
|
+
const laneOf = /* @__PURE__ */ new Map();
|
|
274
|
+
view.lanes.forEach((lane, index) => {
|
|
275
|
+
for (const id of lane.nodes) laneOf.set(id, index);
|
|
276
|
+
});
|
|
277
|
+
const declare = (id, depth) => {
|
|
278
|
+
const node = nodes.get(id);
|
|
279
|
+
if (node === void 0) throw new Error(`lane member "${id}" is not a node of the view`);
|
|
280
|
+
return dotNode(node, depth);
|
|
281
|
+
};
|
|
282
|
+
return `${[
|
|
283
|
+
`digraph ${dotString("swimlane")} {`,
|
|
284
|
+
`${indent}rankdir="LR";`,
|
|
285
|
+
...view.lanes.flatMap((lane, index) => {
|
|
286
|
+
const cluster = `cluster_lane${String(index)}`;
|
|
287
|
+
return [
|
|
288
|
+
`${indent}subgraph ${dotString(cluster)} {`,
|
|
289
|
+
`${indent.repeat(2)}label=${dotString(lane.label)};`,
|
|
290
|
+
...lane.nodes.length === 0 ? [`${indent.repeat(2)}${dotString(`${cluster}:empty`)}${attributes([["shape", "point"], ["style", "invis"]])};`] : lane.nodes.map((id) => declare(id, indent.repeat(2))),
|
|
291
|
+
`${indent}}`
|
|
292
|
+
];
|
|
293
|
+
}),
|
|
294
|
+
...view.unassigned.map((id) => declare(id, indent)),
|
|
295
|
+
...dotEdges(view, (from, to) => {
|
|
296
|
+
const fromLane = laneOf.get(from);
|
|
297
|
+
const toLane = laneOf.get(to);
|
|
298
|
+
return fromLane !== void 0 && toLane !== void 0 && fromLane !== toLane;
|
|
299
|
+
}),
|
|
300
|
+
"}"
|
|
301
|
+
].join("\n")}\n`;
|
|
302
|
+
};
|
|
303
|
+
/** A state's DOT shape: a filled point for an initial pseudo-state, a double circle for a final one, a rounded box for a state. */
|
|
304
|
+
const stateShape = (state) => {
|
|
305
|
+
if (state.role === "initial") return [["label", ""], ["shape", "point"]];
|
|
306
|
+
if (state.role === "final") return [["label", ""], ["shape", "doublecircle"]];
|
|
307
|
+
return [
|
|
308
|
+
["label", state.label],
|
|
309
|
+
["shape", "box"],
|
|
310
|
+
["style", "rounded"]
|
|
311
|
+
];
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* A state machine as a Graphviz `digraph`, left to right. Each transition is labelled with its event, guard and effect.
|
|
315
|
+
* @param view - The projected state machine.
|
|
316
|
+
* @returns DOT source, ending with a line break.
|
|
317
|
+
*/
|
|
318
|
+
const stateMachineToDot = (view) => `${[
|
|
319
|
+
`digraph ${dotString("state machine")} {`,
|
|
320
|
+
`${indent}rankdir="LR";`,
|
|
321
|
+
...view.states.map((state) => `${indent}${dotString(state.id)}${attributes(stateShape(state))};`),
|
|
322
|
+
...view.transitions.map((transition) => {
|
|
323
|
+
const label = transitionLabel(transition);
|
|
324
|
+
return `${indent}${dotString(transition.source)} -> ${dotString(transition.target)}${attributes(label === "" ? [] : [["label", label]])};`;
|
|
325
|
+
}),
|
|
326
|
+
"}"
|
|
327
|
+
].join("\n")}\n`;
|
|
328
|
+
const placeMessages = (items, fragments) => items.flatMap((item) => item.kind === "message" ? [{
|
|
329
|
+
message: item.message,
|
|
330
|
+
fragments
|
|
331
|
+
}] : item.operands.flatMap((operand) => placeMessages(operand.items, [...fragments, operand.guard === void 0 ? item.operator : `${item.operator} [${operand.guard}]`])));
|
|
332
|
+
/**
|
|
333
|
+
* A sequence diagram as a Graphviz `digraph`. DOT has no sequence notation, so the diagram is drawn as a grid: a column per lifeline, headed by its participant's label, with one row per message in scenario order. Each message is an arrow across its row, numbered by its step and prefixed with the fragments around it (operator and guard, outermost first), since DOT cannot draw a box around arrows.
|
|
334
|
+
* @param view - The projected sequence diagram.
|
|
335
|
+
* @param participantLabel - The label of a lifeline's participant.
|
|
336
|
+
* @returns DOT source, ending with a line break.
|
|
337
|
+
*/
|
|
338
|
+
const sequenceToDot = (view, participantLabel) => {
|
|
339
|
+
const columns = new Map(view.lifelines.map((lifeline, index) => [lifeline, index]));
|
|
340
|
+
const column = (lifeline) => {
|
|
341
|
+
const index = columns.get(lifeline);
|
|
342
|
+
if (index === void 0) throw new Error(`"${lifeline}" is not a lifeline of the view`);
|
|
343
|
+
return index;
|
|
344
|
+
};
|
|
345
|
+
const head = (index) => dotString(`p${String(index)}`);
|
|
346
|
+
const cell = (index, row) => dotString(`p${String(index)}.${String(row)}`);
|
|
347
|
+
const placed = placeMessages(view.items, []);
|
|
348
|
+
const rows = placed.map((_, row) => row + 1);
|
|
349
|
+
const pointAttributes = attributes([["shape", "point"], ["width", "0"]]);
|
|
350
|
+
return `${[
|
|
351
|
+
`digraph ${dotString("sequence")} {`,
|
|
352
|
+
`${indent}rankdir="TB";`,
|
|
353
|
+
...view.lifelines.map((lifeline, index) => `${indent}${head(index)}${attributes([["label", participantLabel(lifeline)], ["shape", "box"]])};`),
|
|
354
|
+
`${indent}{ rank="same"; ${view.lifelines.map((_, index) => `${head(index)};`).join(" ")} }`,
|
|
355
|
+
...view.lifelines.slice(1).map((_, index) => `${indent}${head(index)} -> ${head(index + 1)}${attributes([["style", "invis"]])};`),
|
|
356
|
+
...rows.flatMap((row) => [...view.lifelines.map((_, index) => `${indent}${cell(index, row)}${pointAttributes};`), `${indent}{ rank="same"; ${view.lifelines.map((_, index) => `${cell(index, row)};`).join(" ")} }`]),
|
|
357
|
+
...rows.length === 0 ? [] : view.lifelines.map((_, index) => `${indent}${[head(index), ...rows.map((row) => cell(index, row))].join(" -> ")}${attributes([["style", "dashed"], ["arrowhead", "none"]])};`),
|
|
358
|
+
...placed.map(({ message, fragments }, index) => {
|
|
359
|
+
const row = index + 1;
|
|
360
|
+
const text = fragments.length === 0 ? message.label : `${fragments.join(" / ")}: ${message.label}`;
|
|
361
|
+
return `${indent}${cell(column(message.from), row)} -> ${cell(column(message.to), row)}${attributes([["label", `${String(row)}. ${text}`], ["constraint", "false"]])};`;
|
|
362
|
+
}),
|
|
363
|
+
"}"
|
|
364
|
+
].join("\n")}\n`;
|
|
365
|
+
};
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region ../../core/graph/export/dist/view.js
|
|
368
|
+
/** The text formats a view is written in: Mermaid, Graphviz DOT, and the projection itself as JSON. */
|
|
369
|
+
const exportFormats = [
|
|
370
|
+
"mermaid",
|
|
371
|
+
"dot",
|
|
372
|
+
"json"
|
|
373
|
+
];
|
|
374
|
+
/**
|
|
375
|
+
* A projected view as text: Mermaid or Graphviz DOT source, or the projection as JSON indented by two spaces and ending in a line break, with every control, format, separator, unassigned and private-use character in it written as a JSON `\u` escape (see `terminalJson`), so the text shows nothing hidden or reordered wherever it is saved or printed and parses back to exactly the projection. This is the one writer of a view's text, so the command line's `show` and a download from the web app give the same text for the same view.
|
|
376
|
+
* @param model - The model the view was projected from, for the labels of a sequence diagram's participants.
|
|
377
|
+
* @param projected - The view, as graph-projection's `project` gives it.
|
|
378
|
+
* @param format - The format to write.
|
|
379
|
+
* @returns The text.
|
|
380
|
+
*/
|
|
381
|
+
const viewText = (model, projected, format) => {
|
|
382
|
+
if (format === "json") return terminalJson(projected.view);
|
|
383
|
+
const mermaid = format === "mermaid";
|
|
384
|
+
if (projected.notation === "flowchart") return mermaid ? flowchartToMermaid(projected.view) : flowchartToDot(projected.view);
|
|
385
|
+
if (projected.notation === "swimlane") return mermaid ? swimlaneToMermaid(projected.view) : swimlaneToDot(projected.view);
|
|
386
|
+
if (projected.notation === "stateMachine") return mermaid ? stateMachineToMermaid(projected.view) : stateMachineToDot(projected.view);
|
|
387
|
+
const labels = new Map(model.participants.map((participant) => [participant.id, participant.label]));
|
|
388
|
+
const participantLabel = (id) => labels.get(id) ?? id;
|
|
389
|
+
return mermaid ? sequenceToMermaid(projected.view, participantLabel) : sequenceToDot(projected.view, participantLabel);
|
|
390
|
+
};
|
|
391
|
+
//#endregion
|
|
392
|
+
export { stateMachineToDot as a, sequenceToMermaid as c, isUnsafeCharacter as d, terminalJson as f, parseModel as h, sequenceToDot as i, stateMachineToMermaid as l, ModelParseError as m, viewText as n, swimlaneToDot as o, terminalText as p, flowchartToDot as r, flowchartToMermaid as s, exportFormats as t, swimlaneToMermaid as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "process-graph",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A process and dependency graph model with projections into flowcharts, swimlanes, state machines and sequence diagrams, reasoning over a run, layout, the GitHub issues source, the SQLite store for Node and the browser, and the process-graph command line, each behind its own subpath.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
"types": "./dist/store/wasm/worker.d.ts",
|
|
49
49
|
"import": "./dist/store/wasm/worker.js"
|
|
50
50
|
},
|
|
51
|
+
"./sync": {
|
|
52
|
+
"types": "./dist/sync.d.ts",
|
|
53
|
+
"import": "./dist/sync.js"
|
|
54
|
+
},
|
|
51
55
|
"./react": {
|
|
52
56
|
"types": "./dist/react.d.ts",
|
|
53
57
|
"import": "./dist/react.js"
|
|
@@ -117,21 +121,23 @@
|
|
|
117
121
|
],
|
|
118
122
|
"devDependencies": {
|
|
119
123
|
"@arethetypeswrong/cli": "0.18.5",
|
|
120
|
-
"@exadev/process-graph-cli": "1.
|
|
124
|
+
"@exadev/process-graph-cli": "1.3.1",
|
|
121
125
|
"@exadev/process-graph-dag": "1.1.1",
|
|
122
|
-
"@exadev/process-graph-domain": "1.2.
|
|
123
|
-
"@exadev/process-graph-
|
|
124
|
-
"@exadev/process-graph-
|
|
125
|
-
"@exadev/process-graph-
|
|
126
|
-
"@exadev/process-graph-react
|
|
127
|
-
"@exadev/process-graph-
|
|
128
|
-
"@exadev/process-graph-
|
|
129
|
-
"@exadev/process-graph-source-
|
|
130
|
-
"@exadev/process-graph-
|
|
131
|
-
"@exadev/process-graph-sqlite-
|
|
132
|
-
"@exadev/process-graph-
|
|
133
|
-
"@exadev/process-graph-store-
|
|
134
|
-
"@exadev/process-graph-
|
|
126
|
+
"@exadev/process-graph-domain": "1.2.2",
|
|
127
|
+
"@exadev/process-graph-export": "1.0.1",
|
|
128
|
+
"@exadev/process-graph-layout": "1.0.7",
|
|
129
|
+
"@exadev/process-graph-projection": "1.1.4",
|
|
130
|
+
"@exadev/process-graph-react": "1.1.4",
|
|
131
|
+
"@exadev/process-graph-react-flow": "1.1.4",
|
|
132
|
+
"@exadev/process-graph-schema": "2.0.0",
|
|
133
|
+
"@exadev/process-graph-source-contract": "2.0.0",
|
|
134
|
+
"@exadev/process-graph-source-github-issues": "2.0.0",
|
|
135
|
+
"@exadev/process-graph-sqlite-node": "1.1.1",
|
|
136
|
+
"@exadev/process-graph-sqlite-wasm": "1.0.7",
|
|
137
|
+
"@exadev/process-graph-store-contract": "2.0.0",
|
|
138
|
+
"@exadev/process-graph-store-sqlite": "1.2.2",
|
|
139
|
+
"@exadev/process-graph-sync": "1.0.1",
|
|
140
|
+
"@exadev/process-graph-ui": "1.1.2",
|
|
135
141
|
"@orpc/client": "2.0.0-beta.41",
|
|
136
142
|
"@orpc/contract": "2.0.0-beta.41",
|
|
137
143
|
"@orpc/server": "2.0.0-beta.41",
|
package/dist/dist-81kTKnMq.js
DELETED
package/dist/parse-C1Q1zsGt.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { o as toPointer, t as modelSchema } from "./schema-BPT8fmfa.js";
|
|
2
|
-
import { t as validate } from "./validate-DF3mQZww.js";
|
|
3
|
-
//#region ../../core/graph/schema/dist/parse.js
|
|
4
|
-
/** Thrown by `parseModel` when the input is not a valid model. */
|
|
5
|
-
var ModelParseError = class extends Error {
|
|
6
|
-
/** Every problem found, in the order the schema and then `validate` reported them. Never empty. */
|
|
7
|
-
issues;
|
|
8
|
-
/**
|
|
9
|
-
* @param issues - The problems found. Must not be empty.
|
|
10
|
-
*/
|
|
11
|
-
constructor(issues) {
|
|
12
|
-
super(`Invalid model: ${issues.map((issue) => issue.source === "schema" ? `${issue.pointer === "" ? "/" : issue.pointer}: ${issue.message}` : issue.message).join("; ")}`);
|
|
13
|
-
this.name = "ModelParseError";
|
|
14
|
-
this.issues = issues;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Loads a model from untrusted input, such as parsed JSON.
|
|
19
|
-
*
|
|
20
|
-
* The input is checked against the open schema first, so every shape problem is reported with the JSON pointer of the offending value. Only a well-shaped model is then passed to `validate`, whose issues are surfaced the same way.
|
|
21
|
-
* @param input - The value to load.
|
|
22
|
-
* @returns The validated model, with identifiers as plain strings.
|
|
23
|
-
* @throws ModelParseError when the input is not a well-shaped model or fails `validate`.
|
|
24
|
-
*/
|
|
25
|
-
const parseModel = (input) => {
|
|
26
|
-
const parsed = modelSchema.safeParse(input);
|
|
27
|
-
if (!parsed.success) throw new ModelParseError(parsed.error.issues.map((issue) => ({
|
|
28
|
-
source: "schema",
|
|
29
|
-
pointer: toPointer(issue.path),
|
|
30
|
-
message: issue.message
|
|
31
|
-
})));
|
|
32
|
-
const issues = validate(parsed.data);
|
|
33
|
-
if (issues.length > 0) throw new ModelParseError(issues.map((issue) => ({
|
|
34
|
-
source: "validate",
|
|
35
|
-
code: issue.code,
|
|
36
|
-
message: issue.message
|
|
37
|
-
})));
|
|
38
|
-
return parsed.data;
|
|
39
|
-
};
|
|
40
|
-
//#endregion
|
|
41
|
-
export { parseModel as n, ModelParseError as t };
|