@the-package-labs/idocs-core 0.3.1 → 0.5.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/dist/browser.js +2 -2
- package/dist/chunk-P2X2XSBM.js +127 -0
- package/dist/{chunk-5BMICZRC.js → chunk-Q2NHZ6TV.js} +214 -40
- package/dist/{chunk-FRVCGXIG.js → chunk-ZYFFV7Y7.js} +35 -3
- package/dist/compiler.d.ts +11 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +338 -2
- package/dist/nested-layout.d.ts +69 -0
- package/dist/nesting.d.ts +24 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +15 -3
- package/dist/outline.d.ts +63 -0
- package/dist/phases.d.ts +16 -0
- package/dist/schema.d.ts +69 -0
- package/dist/scrubber.d.ts +34 -0
- package/dist/start.d.ts +22 -0
- package/dist/suggest.d.ts +1 -0
- package/dist/types.d.ts +50 -0
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
didYouMean
|
|
3
|
+
} from "./chunk-ZYFFV7Y7.js";
|
|
4
|
+
|
|
5
|
+
// src/nesting.ts
|
|
6
|
+
function nestedDiagrams(all, name) {
|
|
7
|
+
const out = {};
|
|
8
|
+
const queue = [name];
|
|
9
|
+
for (let cur = queue.shift(); cur !== void 0; cur = queue.shift()) {
|
|
10
|
+
for (const node of Object.values(all[cur]?.nodes ?? {})) {
|
|
11
|
+
const next = node.diagram?.name;
|
|
12
|
+
if (next === void 0 || next === name || out[next] || !all[next]) continue;
|
|
13
|
+
out[next] = all[next];
|
|
14
|
+
queue.push(next);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
function attachNested(all) {
|
|
20
|
+
const flat = {};
|
|
21
|
+
for (const [name, diagram] of Object.entries(all)) {
|
|
22
|
+
const { nested: _, ...rest } = diagram;
|
|
23
|
+
flat[name] = rest;
|
|
24
|
+
}
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [name, diagram] of Object.entries(flat)) {
|
|
27
|
+
const nested = nestedDiagrams(flat, name);
|
|
28
|
+
out[name] = Object.keys(nested).length ? { ...diagram, nested } : diagram;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function linkDiagrams(entries) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const names = Object.keys(entries).sort();
|
|
35
|
+
const known = names.filter((n) => entries[n].diagram);
|
|
36
|
+
const report = (from, path, message, hint) => {
|
|
37
|
+
const at = entries[from].where?.(path);
|
|
38
|
+
out.push({
|
|
39
|
+
severity: "error",
|
|
40
|
+
message,
|
|
41
|
+
file: at?.file ?? `diagrams/${from}/diagram.yaml`,
|
|
42
|
+
line: at?.line,
|
|
43
|
+
col: at?.col,
|
|
44
|
+
path: path.join("."),
|
|
45
|
+
hint
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const edges = /* @__PURE__ */ new Map();
|
|
49
|
+
for (const from of names) {
|
|
50
|
+
const diagram = entries[from].diagram;
|
|
51
|
+
if (!diagram) continue;
|
|
52
|
+
for (const node of Object.values(diagram.nodes)) {
|
|
53
|
+
const ref = node.diagram;
|
|
54
|
+
if (!ref) continue;
|
|
55
|
+
const at = ["nodes", node.id, "diagram"];
|
|
56
|
+
const sub = (key) => [...at, key];
|
|
57
|
+
const target = entries[ref.name];
|
|
58
|
+
if (!target) {
|
|
59
|
+
report(
|
|
60
|
+
from,
|
|
61
|
+
at,
|
|
62
|
+
`Node "${node.id}" opens diagram "${ref.name}", which does not exist`,
|
|
63
|
+
didYouMean(ref.name, names) ?? (names.length > 1 ? `Diagrams: ${names.filter((n) => n !== from).join(", ")}` : void 0)
|
|
64
|
+
);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!target.diagram) continue;
|
|
68
|
+
edges.set(from, (edges.get(from) ?? /* @__PURE__ */ new Set()).add(ref.name));
|
|
69
|
+
const views = target.diagram.views.map((v) => v.id);
|
|
70
|
+
const scenario = ref.scenario ? target.diagram.scenarios.find((s) => s.id === ref.scenario) : void 0;
|
|
71
|
+
if (ref.view && !views.includes(ref.view)) {
|
|
72
|
+
report(
|
|
73
|
+
from,
|
|
74
|
+
sub("view"),
|
|
75
|
+
`Diagram "${ref.name}" has no view "${ref.view}"`,
|
|
76
|
+
didYouMean(ref.view, views) ?? `Views: ${views.join(", ")}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (ref.scenario && !scenario) {
|
|
80
|
+
const ids = target.diagram.scenarios.map((s) => s.id);
|
|
81
|
+
report(
|
|
82
|
+
from,
|
|
83
|
+
sub("scenario"),
|
|
84
|
+
`Diagram "${ref.name}" has no scenario "${ref.scenario}"`,
|
|
85
|
+
didYouMean(ref.scenario, ids) ?? (ids.length ? `Scenarios: ${ids.join(", ")}` : "It has no scenarios.")
|
|
86
|
+
);
|
|
87
|
+
} else if (scenario && ref.view && scenario.view !== ref.view) {
|
|
88
|
+
report(
|
|
89
|
+
from,
|
|
90
|
+
sub("scenario"),
|
|
91
|
+
`Scenario "${scenario.id}" plays on view "${scenario.view}", not "${ref.view}"`,
|
|
92
|
+
"Drop `view`, and the diagram opens on the scenario's own view."
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const state = /* @__PURE__ */ new Map();
|
|
98
|
+
const stack = [];
|
|
99
|
+
const visit = (name) => {
|
|
100
|
+
state.set(name, "open");
|
|
101
|
+
stack.push(name);
|
|
102
|
+
for (const next of edges.get(name) ?? /* @__PURE__ */ new Set()) {
|
|
103
|
+
if (state.get(next) === "open") {
|
|
104
|
+
const circle = [...stack.slice(stack.indexOf(next)), next];
|
|
105
|
+
const node = Object.values(entries[name].diagram.nodes).find(
|
|
106
|
+
(n) => n.diagram?.name === next
|
|
107
|
+
);
|
|
108
|
+
report(
|
|
109
|
+
name,
|
|
110
|
+
["nodes", node.id, "diagram"],
|
|
111
|
+
next === name ? `Node "${node.id}" opens its own diagram "${name}"` : `Node "${node.id}" opens "${next}", which leads back to "${name}"`,
|
|
112
|
+
`A diagram cannot be inside itself: ${circle.join(" \u2192 ")}`
|
|
113
|
+
);
|
|
114
|
+
} else if (!state.has(next)) visit(next);
|
|
115
|
+
}
|
|
116
|
+
stack.pop();
|
|
117
|
+
state.set(name, "done");
|
|
118
|
+
};
|
|
119
|
+
for (const name of known) if (!state.has(name)) visit(name);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
nestedDiagrams,
|
|
125
|
+
attachNested,
|
|
126
|
+
linkDiagrams
|
|
127
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BUILTIN_EDGE_KINDS,
|
|
3
3
|
BUILTIN_KINDS,
|
|
4
|
+
DEFAULT_ENTITY_WIDTH,
|
|
5
|
+
didYouMean,
|
|
4
6
|
letterKey
|
|
5
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZYFFV7Y7.js";
|
|
6
8
|
|
|
7
9
|
// src/icon-resolver.ts
|
|
8
10
|
import { getIconData, iconToSVG } from "@iconify/utils";
|
|
@@ -214,10 +216,28 @@ var ChipSchema = z.union([
|
|
|
214
216
|
z.string(),
|
|
215
217
|
z.strictObject({ label: z.string(), sub: z.string().optional() })
|
|
216
218
|
]);
|
|
219
|
+
var FieldProps = z.strictObject({
|
|
220
|
+
type: z.string().optional().describe("Its type: `keyword`, `uuid`, `string[]`."),
|
|
221
|
+
key: z.boolean().optional().describe("Part of the primary key. Shown with a `#` in front."),
|
|
222
|
+
ref: z.string().optional().describe(
|
|
223
|
+
"The `node` or `node.field` this one points at, like a foreign key. Draws a relation between the two cards."
|
|
224
|
+
),
|
|
225
|
+
note: z.string().optional().describe("Shown on hover and in the drawer.")
|
|
226
|
+
});
|
|
227
|
+
var FieldSchema = z.union([
|
|
228
|
+
z.string().min(1),
|
|
229
|
+
z.record(z.string().min(1), z.union([z.string(), z.null(), FieldProps])).refine((o) => Object.keys(o).length === 1, {
|
|
230
|
+
message: "One field per list item: `- name: type`"
|
|
231
|
+
})
|
|
232
|
+
]);
|
|
217
233
|
var NodeSchema = z.strictObject({
|
|
218
234
|
title: z.string().optional().describe("Display name. Defaults to the node id."),
|
|
219
235
|
sub: z.string().optional().describe("One line under the title, rendered in mono (ports, stack, version)."),
|
|
236
|
+
pill: z.string().optional().describe("Short label in the card header, like `TABLE` or `ES INDEX`."),
|
|
220
237
|
lines: z.array(z.string()).optional().describe("Short body lines inside the card."),
|
|
238
|
+
fields: z.array(FieldSchema).optional().describe(
|
|
239
|
+
"Makes the card an entity: a table of fields, each `name: type`. `ref` points a field at another entity."
|
|
240
|
+
),
|
|
221
241
|
icon: z.string().optional().describe("`postgresql`, `logos:redis`, `lucide:server` or ./path/to.svg."),
|
|
222
242
|
kind: z.string().optional().describe("Gives a default icon and colour family. See `kinds`."),
|
|
223
243
|
family: FamilySchema.optional().describe("Accent colour family; overrides the kind default."),
|
|
@@ -231,7 +251,23 @@ var NodeSchema = z.strictObject({
|
|
|
231
251
|
doc: z.string().optional().describe("Markdown file for the drawer. Defaults to nodes/<id>.md when present."),
|
|
232
252
|
links: z.array(z.strictObject({ label: z.string(), url: z.string() })).optional(),
|
|
233
253
|
refs: z.array(z.string()).optional().describe("Source files or URLs this node is based on."),
|
|
234
|
-
tags: z.array(z.string()).optional()
|
|
254
|
+
tags: z.array(z.string()).optional(),
|
|
255
|
+
diagram: z.union([
|
|
256
|
+
z.string().min(1),
|
|
257
|
+
z.strictObject({
|
|
258
|
+
name: z.string().min(1).describe("Folder name of the diagram, under diagrams/."),
|
|
259
|
+
view: Id.optional().describe(
|
|
260
|
+
"View to open first. Defaults to the scenario view, then the first."
|
|
261
|
+
),
|
|
262
|
+
scenario: Id.optional().describe("Scenario to select when the diagram opens."),
|
|
263
|
+
mode: z.enum(["both", "expand", "open"]).optional().describe(
|
|
264
|
+
"both (default): the reader can expand it in place or open it. expand: only in place. open: only move into it."
|
|
265
|
+
),
|
|
266
|
+
expanded: z.boolean().optional().describe("Start expanded in place, so the whole inner diagram shows as a group.")
|
|
267
|
+
})
|
|
268
|
+
]).optional().describe(
|
|
269
|
+
"Another diagram (its folder name) that shows the inside of this node. The card gets buttons to expand it in place, as a group, or to open it one level deeper, with a way back."
|
|
270
|
+
)
|
|
235
271
|
});
|
|
236
272
|
var GroupSchema = z.strictObject({
|
|
237
273
|
label: z.string(),
|
|
@@ -337,10 +373,31 @@ var ScenarioSchema = ScenarioBase.refine(
|
|
|
337
373
|
(s) => (s.phases?.length ?? 0) + (s.steps?.length ?? 0) > 0,
|
|
338
374
|
{ message: "A scenario needs `steps` or `phases`" }
|
|
339
375
|
);
|
|
376
|
+
var DefaultsSchema = z.strictObject({
|
|
377
|
+
view: Id.optional().describe(
|
|
378
|
+
"View shown first. Defaults to the view of `scenario`, or else the first view."
|
|
379
|
+
),
|
|
380
|
+
scenario: Id.optional().describe(
|
|
381
|
+
"Scenario selected first. Without it the diagram opens on its overview. It does not play until a reader presses play, unless `autoplay` is on."
|
|
382
|
+
),
|
|
383
|
+
mode: z.enum(["flow", "sequence", "story"]).optional().describe(
|
|
384
|
+
"How a scenario is drawn when it has no `mode` of its own: packets over the diagram, a sequence diagram, or a swimlane story."
|
|
385
|
+
),
|
|
386
|
+
sidebar: z.boolean().optional().describe(
|
|
387
|
+
"Whether the panel over the picture starts open. By default it does when there is a scenario or another view to choose, and the frame is wide enough."
|
|
388
|
+
),
|
|
389
|
+
subtitles: z.boolean().optional().describe("Words over the picture saying what the scenario is doing. On by default."),
|
|
390
|
+
autoplay: z.boolean().optional().describe(
|
|
391
|
+
"Play `scenario` as soon as the diagram is on screen. Needs `scenario`, and is ignored when the system asks for reduced motion."
|
|
392
|
+
)
|
|
393
|
+
});
|
|
340
394
|
var RootSchema = z.strictObject({
|
|
341
395
|
$schema: z.string().optional(),
|
|
342
396
|
title: z.string(),
|
|
343
397
|
description: z.string().optional().describe("Markdown shown above the first view."),
|
|
398
|
+
defaults: DefaultsSchema.optional().describe(
|
|
399
|
+
"How the diagram opens: its view, scenario and mode, and whether the panel and subtitles show. A page can still choose otherwise with the props of `<Diagram>`."
|
|
400
|
+
),
|
|
344
401
|
kinds: z.record(Id, z.strictObject({ icon: z.string().optional(), family: FamilySchema.optional() })).optional(),
|
|
345
402
|
edgeKinds: z.record(
|
|
346
403
|
Id,
|
|
@@ -363,7 +420,8 @@ var KNOWN_KEYS = {
|
|
|
363
420
|
group: Object.keys(GroupSchema.shape),
|
|
364
421
|
edge: Object.keys(EdgeSchema.shape),
|
|
365
422
|
view: Object.keys(ViewSchema.shape),
|
|
366
|
-
scenario: Object.keys(ScenarioBase.shape)
|
|
423
|
+
scenario: Object.keys(ScenarioBase.shape),
|
|
424
|
+
defaults: Object.keys(DefaultsSchema.shape)
|
|
367
425
|
};
|
|
368
426
|
function authoringJsonSchema() {
|
|
369
427
|
const schema = z.toJSONSchema(RootSchema, { io: "input", unrepresentable: "any" });
|
|
@@ -449,15 +507,17 @@ function compileDiagram(src, assets) {
|
|
|
449
507
|
scenarios[f.id] = body;
|
|
450
508
|
scenarioSources.set(f.id, s);
|
|
451
509
|
}
|
|
452
|
-
const
|
|
510
|
+
const where = (path) => {
|
|
453
511
|
let source = main;
|
|
454
512
|
let p = path;
|
|
455
513
|
if (path[0] === "scenarios" && typeof path[1] === "string" && scenarioSources.has(path[1])) {
|
|
456
514
|
source = scenarioSources.get(path[1]);
|
|
457
515
|
p = path.slice(2);
|
|
458
516
|
}
|
|
459
|
-
|
|
460
|
-
|
|
517
|
+
return { file: source.file, ...locate(source, p) };
|
|
518
|
+
};
|
|
519
|
+
const report = (severity, message, path, hint) => {
|
|
520
|
+
diagnostics.push({ severity, message, ...where(path), path: path.join("."), hint });
|
|
461
521
|
};
|
|
462
522
|
const error = (message, path, hint) => report("error", message, path, hint);
|
|
463
523
|
const warn = (message, path, hint) => report("warning", message, path, hint);
|
|
@@ -473,7 +533,7 @@ function compileDiagram(src, assets) {
|
|
|
473
533
|
}
|
|
474
534
|
const diagram = build(parsed.data, src, error, warn, assets);
|
|
475
535
|
if (diagnostics.some((d) => d.severity === "error")) return { diagnostics };
|
|
476
|
-
return { diagram, diagnostics };
|
|
536
|
+
return Object.defineProperty({ diagram, diagnostics }, "where", { value: where });
|
|
477
537
|
}
|
|
478
538
|
function build(data, src, error, warn, { resolveIcon }) {
|
|
479
539
|
const kinds = { ...BUILTIN_KINDS };
|
|
@@ -570,6 +630,7 @@ function build(data, src, error, warn, { resolveIcon }) {
|
|
|
570
630
|
const fixParents = Object.values(groups);
|
|
571
631
|
for (const g of fixParents) g.style = g.parent ? "frame" : "zone";
|
|
572
632
|
const nodes = {};
|
|
633
|
+
const refTexts = [];
|
|
573
634
|
for (const [id, n] of Object.entries(data.nodes)) {
|
|
574
635
|
if (n.kind && !kinds[n.kind]) {
|
|
575
636
|
warn(
|
|
@@ -586,16 +647,38 @@ function build(data, src, error, warn, { resolveIcon }) {
|
|
|
586
647
|
didYouMean(n.in, Object.keys(groups))
|
|
587
648
|
);
|
|
588
649
|
}
|
|
650
|
+
if (typeof n.diagram === "object" && n.diagram.expanded && n.diagram.mode === "open") {
|
|
651
|
+
warn(
|
|
652
|
+
"`expanded` has no effect with `mode: open`",
|
|
653
|
+
["nodes", id, "diagram", "expanded"],
|
|
654
|
+
"Use `mode: expand` or `mode: both`, or drop `expanded`."
|
|
655
|
+
);
|
|
656
|
+
}
|
|
589
657
|
const docPath = n.doc ?? `nodes/${id}.md`;
|
|
590
658
|
const docText = src.readFile?.(docPath);
|
|
591
659
|
if (n.doc && docText === void 0)
|
|
592
660
|
warn(`Doc file "${n.doc}" was not found`, ["nodes", id, "doc"]);
|
|
593
661
|
const docHtml = [n.description, docText].filter((t) => !!t).map(renderMarkdown).join("");
|
|
662
|
+
const fields = (n.fields ?? []).map(toField);
|
|
663
|
+
const seenFields = /* @__PURE__ */ new Set();
|
|
664
|
+
fields.forEach((f, i) => {
|
|
665
|
+
if (seenFields.has(f.name)) {
|
|
666
|
+
error(`Field "${f.name}" appears twice in "${id}"`, ["nodes", id, "fields", i]);
|
|
667
|
+
}
|
|
668
|
+
seenFields.add(f.name);
|
|
669
|
+
});
|
|
670
|
+
n.fields?.forEach((item, i) => {
|
|
671
|
+
const ref = typeof item === "object" && item ? Object.values(item)[0] : void 0;
|
|
672
|
+
if (ref && typeof ref === "object" && ref.ref !== void 0)
|
|
673
|
+
refTexts.push({ node: id, index: i, text: ref.ref });
|
|
674
|
+
});
|
|
594
675
|
nodes[id] = {
|
|
595
676
|
id,
|
|
596
677
|
title: n.title ?? id,
|
|
597
678
|
sub: n.sub,
|
|
679
|
+
pill: n.pill,
|
|
598
680
|
lines: n.lines ?? [],
|
|
681
|
+
fields,
|
|
599
682
|
icon: iconFor(n.icon ?? kind?.icon, ["nodes", id, n.icon ? "icon" : "kind"]),
|
|
600
683
|
kind: n.kind,
|
|
601
684
|
family: n.family ?? kind?.family,
|
|
@@ -604,25 +687,47 @@ function build(data, src, error, warn, { resolveIcon }) {
|
|
|
604
687
|
badge: n.badge === void 0 ? void 0 : String(n.badge),
|
|
605
688
|
uses: n.uses ?? [],
|
|
606
689
|
chips: (n.chips ?? []).map((c) => typeof c === "string" ? { label: c } : c),
|
|
607
|
-
w: n.w,
|
|
690
|
+
w: n.w ?? (fields.length ? DEFAULT_ENTITY_WIDTH : void 0),
|
|
608
691
|
docHtml: docHtml || void 0,
|
|
609
692
|
links: n.links ?? [],
|
|
610
693
|
refs: n.refs ?? [],
|
|
611
|
-
tags: n.tags ?? []
|
|
694
|
+
tags: n.tags ?? [],
|
|
695
|
+
diagram: typeof n.diagram === "string" ? { name: n.diagram } : n.diagram
|
|
612
696
|
};
|
|
613
697
|
}
|
|
698
|
+
for (const { node: id, index, text } of refTexts) {
|
|
699
|
+
const found = resolveEnd(text, nodes);
|
|
700
|
+
if ("end" in found) {
|
|
701
|
+
nodes[id].fields[index].ref = found.end;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const name = nodes[id].fields[index].name;
|
|
705
|
+
error(
|
|
706
|
+
found.message ?? `Field "${name}" points at "${text}", which is not a node`,
|
|
707
|
+
["nodes", id, "fields", index, name, "ref"],
|
|
708
|
+
found.hint ?? didYouMean(text, Object.keys(nodes))
|
|
709
|
+
);
|
|
710
|
+
}
|
|
614
711
|
const edges = {};
|
|
615
712
|
const nodeIds = Object.keys(nodes);
|
|
616
713
|
(data.edges ?? []).forEach((e, i) => {
|
|
617
714
|
const path = ["edges", i];
|
|
618
715
|
let ok = true;
|
|
716
|
+
const ends = {};
|
|
619
717
|
for (const end of ["from", "to"]) {
|
|
620
|
-
|
|
621
|
-
|
|
718
|
+
const found = resolveEnd(e[end], nodes);
|
|
719
|
+
if ("end" in found) ends[end] = found.end;
|
|
720
|
+
else {
|
|
721
|
+
error(
|
|
722
|
+
found.message ?? `Edge ${end} "${e[end]}" is not a node`,
|
|
723
|
+
[...path, end],
|
|
724
|
+
found.hint ?? didYouMean(e[end], nodeIds)
|
|
725
|
+
);
|
|
622
726
|
ok = false;
|
|
623
727
|
}
|
|
624
728
|
}
|
|
625
|
-
const
|
|
729
|
+
const between = ends.from && ends.to && [ends.from, ends.to].every((x) => nodes[x.node].fields.length);
|
|
730
|
+
const kind = e.kind ?? (between ? "relation" : "http");
|
|
626
731
|
if (!edgeKinds[kind]) {
|
|
627
732
|
error(
|
|
628
733
|
`Unknown edge kind "${kind}"`,
|
|
@@ -631,7 +736,7 @@ function build(data, src, error, warn, { resolveIcon }) {
|
|
|
631
736
|
);
|
|
632
737
|
ok = false;
|
|
633
738
|
}
|
|
634
|
-
if (
|
|
739
|
+
if (ok && ends.from.node === ends.to.node) {
|
|
635
740
|
warn("Edge connects a node to itself and is not drawn", path);
|
|
636
741
|
ok = false;
|
|
637
742
|
}
|
|
@@ -643,8 +748,10 @@ function build(data, src, error, warn, { resolveIcon }) {
|
|
|
643
748
|
}
|
|
644
749
|
edges[id] = {
|
|
645
750
|
id,
|
|
646
|
-
from:
|
|
647
|
-
to:
|
|
751
|
+
from: ends.from.node,
|
|
752
|
+
to: ends.to.node,
|
|
753
|
+
fromField: ends.from.field,
|
|
754
|
+
toField: ends.to.field,
|
|
648
755
|
label: e.label,
|
|
649
756
|
kind,
|
|
650
757
|
both: e.both ?? false,
|
|
@@ -657,6 +764,32 @@ function build(data, src, error, warn, { resolveIcon }) {
|
|
|
657
764
|
noteHtml: e.note ? renderMarkdown(e.note) : void 0
|
|
658
765
|
};
|
|
659
766
|
});
|
|
767
|
+
const written = new Set(
|
|
768
|
+
Object.values(edges).flatMap((e) => {
|
|
769
|
+
const a = `${e.from}.${e.fromField ?? ""}`;
|
|
770
|
+
const b = `${e.to}.${e.toField ?? ""}`;
|
|
771
|
+
return [`${a}>${b}`, `${b}>${a}`];
|
|
772
|
+
})
|
|
773
|
+
);
|
|
774
|
+
for (const node of Object.values(nodes)) {
|
|
775
|
+
for (const f of node.fields) {
|
|
776
|
+
if (!f.ref || f.ref.node === node.id) continue;
|
|
777
|
+
const key = `${node.id}.${f.name}>${f.ref.node}.${f.ref.field ?? ""}`;
|
|
778
|
+
if (written.has(key)) continue;
|
|
779
|
+
const id = `ref:${node.id}.${f.name}`;
|
|
780
|
+
edges[id] = {
|
|
781
|
+
id,
|
|
782
|
+
from: node.id,
|
|
783
|
+
to: f.ref.node,
|
|
784
|
+
fromField: f.name,
|
|
785
|
+
toField: f.ref.field,
|
|
786
|
+
kind: "relation",
|
|
787
|
+
both: false,
|
|
788
|
+
hidden: false,
|
|
789
|
+
status: "built"
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
660
793
|
const viewDefs = Object.keys(data.views ?? {}).length ? data.views : { main: { title: data.title } };
|
|
661
794
|
const views = Object.entries(viewDefs).map(
|
|
662
795
|
([id, v]) => buildView(
|
|
@@ -674,10 +807,12 @@ function build(data, src, error, warn, { resolveIcon }) {
|
|
|
674
807
|
const sc = buildScenario(id, s, views, nodes, edges, error, warn);
|
|
675
808
|
if (sc) scenarios.push(sc);
|
|
676
809
|
}
|
|
810
|
+
const defaults = buildDefaults(data.defaults, views, scenarios, error);
|
|
677
811
|
return {
|
|
678
812
|
name: src.name,
|
|
679
813
|
title: data.title,
|
|
680
814
|
descriptionHtml: data.description ? renderMarkdown(data.description) : void 0,
|
|
815
|
+
defaults,
|
|
681
816
|
nodes,
|
|
682
817
|
groups,
|
|
683
818
|
edges,
|
|
@@ -686,6 +821,37 @@ function build(data, src, error, warn, { resolveIcon }) {
|
|
|
686
821
|
scenarios
|
|
687
822
|
};
|
|
688
823
|
}
|
|
824
|
+
function toField(item) {
|
|
825
|
+
if (typeof item === "string") return { name: item };
|
|
826
|
+
const [name, value] = Object.entries(item)[0];
|
|
827
|
+
if (value == null) return { name };
|
|
828
|
+
if (typeof value === "string") return { name, type: value };
|
|
829
|
+
const { ref: _ref, ...rest } = value;
|
|
830
|
+
return { name, ...rest };
|
|
831
|
+
}
|
|
832
|
+
function resolveEnd(text, nodes) {
|
|
833
|
+
if (nodes[text]) return { end: { node: text } };
|
|
834
|
+
const matches = [];
|
|
835
|
+
let known;
|
|
836
|
+
for (let i = text.indexOf("."); i > 0; i = text.indexOf(".", i + 1)) {
|
|
837
|
+
const node = nodes[text.slice(0, i)];
|
|
838
|
+
if (!node) continue;
|
|
839
|
+
known ??= node;
|
|
840
|
+
const field2 = text.slice(i + 1);
|
|
841
|
+
if (node.fields.some((f) => f.name === field2)) matches.push({ node: node.id, field: field2 });
|
|
842
|
+
}
|
|
843
|
+
if (matches.length === 1) return { end: matches[0] };
|
|
844
|
+
if (matches.length > 1) {
|
|
845
|
+
return { message: `"${text}" could be more than one field`, hint: "Rename a node or a field." };
|
|
846
|
+
}
|
|
847
|
+
if (!known) return {};
|
|
848
|
+
const field = text.slice(known.id.length + 1);
|
|
849
|
+
const names = known.fields.map((f) => f.name);
|
|
850
|
+
return {
|
|
851
|
+
message: `Node "${known.id}" has no field "${field}"`,
|
|
852
|
+
hint: didYouMean(field, names) ?? (names.length ? `Fields: ${names.join(", ")}` : void 0)
|
|
853
|
+
};
|
|
854
|
+
}
|
|
689
855
|
function buildView(id, v, diagramTitle, nodes, groups, edges, error) {
|
|
690
856
|
const known = [...Object.keys(nodes), ...Object.keys(groups)];
|
|
691
857
|
const members = (gid, out) => {
|
|
@@ -738,6 +904,38 @@ function buildView(id, v, diagramTitle, nodes, groups, edges, error) {
|
|
|
738
904
|
interfaces: v.interfaces ?? false
|
|
739
905
|
};
|
|
740
906
|
}
|
|
907
|
+
function buildDefaults(d, views, scenarios, error) {
|
|
908
|
+
if (!d) return void 0;
|
|
909
|
+
const ids = (list) => list.map((x) => x.id);
|
|
910
|
+
const view = views.find((v) => v.id === d.view);
|
|
911
|
+
if (d.view !== void 0 && !view) {
|
|
912
|
+
error(
|
|
913
|
+
`defaults.view "${d.view}" is not a view`,
|
|
914
|
+
["defaults", "view"],
|
|
915
|
+
didYouMean(d.view, ids(views))
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
const scenario = scenarios.find((s) => s.id === d.scenario);
|
|
919
|
+
if (d.scenario !== void 0 && !scenario) {
|
|
920
|
+
error(
|
|
921
|
+
`defaults.scenario "${d.scenario}" is not a scenario`,
|
|
922
|
+
["defaults", "scenario"],
|
|
923
|
+
didYouMean(d.scenario, ids(scenarios))
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
if (view && scenario && scenario.view !== view.id) {
|
|
927
|
+
error(
|
|
928
|
+
`defaults.scenario "${scenario.id}" plays on view "${scenario.view}", not "${view.id}"`,
|
|
929
|
+
["defaults", "scenario"],
|
|
930
|
+
`Drop \`view\` and the diagram opens on "${scenario.view}", or pick a scenario of "${view.id}".`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
if (d.autoplay && d.scenario === void 0) {
|
|
934
|
+
error("defaults.autoplay needs a defaults.scenario to play", ["defaults", "autoplay"]);
|
|
935
|
+
}
|
|
936
|
+
const out = Object.fromEntries(Object.entries(d).filter(([, v]) => v !== void 0));
|
|
937
|
+
return Object.keys(out).length ? out : void 0;
|
|
938
|
+
}
|
|
741
939
|
function buildScenario(id, s, views, nodes, edges, error, warn) {
|
|
742
940
|
const viewId = s.view ?? views[0].id;
|
|
743
941
|
const view = views.find((v) => v.id === viewId);
|
|
@@ -1015,6 +1213,7 @@ function describeIssue(issue) {
|
|
|
1015
1213
|
function allowedKeysFor(path) {
|
|
1016
1214
|
const [section] = path;
|
|
1017
1215
|
if (path.length === 0) return KNOWN_KEYS.root;
|
|
1216
|
+
if (path.length === 1) return section === "defaults" ? KNOWN_KEYS.defaults : void 0;
|
|
1018
1217
|
if (path.length !== 2) return void 0;
|
|
1019
1218
|
switch (section) {
|
|
1020
1219
|
case "nodes":
|
|
@@ -1031,31 +1230,6 @@ function allowedKeysFor(path) {
|
|
|
1031
1230
|
return void 0;
|
|
1032
1231
|
}
|
|
1033
1232
|
}
|
|
1034
|
-
function didYouMean(word, options) {
|
|
1035
|
-
let best;
|
|
1036
|
-
let bestDist = Math.max(2, Math.floor(word.length / 3)) + 1;
|
|
1037
|
-
for (const o of options) {
|
|
1038
|
-
const d = levenshtein(word.toLowerCase(), o.toLowerCase());
|
|
1039
|
-
if (d < bestDist) {
|
|
1040
|
-
best = o;
|
|
1041
|
-
bestDist = d;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
return best ? `Did you mean "${best}"?` : void 0;
|
|
1045
|
-
}
|
|
1046
|
-
function levenshtein(a, b) {
|
|
1047
|
-
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1048
|
-
for (let i = 1; i <= a.length; i++) {
|
|
1049
|
-
let prev = row[0];
|
|
1050
|
-
row[0] = i;
|
|
1051
|
-
for (let j = 1; j <= b.length; j++) {
|
|
1052
|
-
const tmp = row[j];
|
|
1053
|
-
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1054
|
-
prev = tmp;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
return row[b.length];
|
|
1058
|
-
}
|
|
1059
1233
|
|
|
1060
1234
|
export {
|
|
1061
1235
|
createIconResolver,
|
|
@@ -17,7 +17,8 @@ var BUILTIN_KINDS = {
|
|
|
17
17
|
network: { icon: "lucide:network", family: "violet" },
|
|
18
18
|
vm: { icon: "lucide:box", family: "violet" },
|
|
19
19
|
auth: { icon: "lucide:key-round", family: "teal" },
|
|
20
|
-
monitor: { icon: "lucide:activity", family: "teal" }
|
|
20
|
+
monitor: { icon: "lucide:activity", family: "teal" },
|
|
21
|
+
entity: { icon: "lucide:table-2", family: "blue" }
|
|
21
22
|
};
|
|
22
23
|
var BUILTIN_EDGE_KINDS = {
|
|
23
24
|
http: { label: "HTTP request / response", color: "ink", width: 1.5 },
|
|
@@ -25,7 +26,8 @@ var BUILTIN_EDGE_KINDS = {
|
|
|
25
26
|
tunnel: { label: "Overlay tunnel", color: "tunnel", width: 5 },
|
|
26
27
|
queue: { label: "Queue / broker", color: "ok", width: 2, dash: "2 4" },
|
|
27
28
|
ws: { label: "WebSocket", color: "ink", width: 1.6, dash: "7 4" },
|
|
28
|
-
data: { label: "Data access", color: "muted", width: 1.3 }
|
|
29
|
+
data: { label: "Data access", color: "muted", width: 1.3 },
|
|
30
|
+
relation: { label: "Relation between entities", color: "muted", width: 1.4 }
|
|
29
31
|
};
|
|
30
32
|
var STEP_KIND_COLORS = {
|
|
31
33
|
request: "accent",
|
|
@@ -35,6 +37,7 @@ var STEP_KIND_COLORS = {
|
|
|
35
37
|
event: "warn"
|
|
36
38
|
};
|
|
37
39
|
var DEFAULT_NODE_WIDTH = 178;
|
|
40
|
+
var DEFAULT_ENTITY_WIDTH = 280;
|
|
38
41
|
|
|
39
42
|
// src/geometry.ts
|
|
40
43
|
function polylineLength(points) {
|
|
@@ -104,14 +107,43 @@ function letterKey(index) {
|
|
|
104
107
|
return out;
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
// src/suggest.ts
|
|
111
|
+
function didYouMean(word, options) {
|
|
112
|
+
let best;
|
|
113
|
+
let bestDist = Math.max(2, Math.floor(word.length / 3)) + 1;
|
|
114
|
+
for (const o of options) {
|
|
115
|
+
const d = levenshtein(word.toLowerCase(), o.toLowerCase());
|
|
116
|
+
if (d < bestDist) {
|
|
117
|
+
best = o;
|
|
118
|
+
bestDist = d;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return best ? `Did you mean "${best}"?` : void 0;
|
|
122
|
+
}
|
|
123
|
+
function levenshtein(a, b) {
|
|
124
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
125
|
+
for (let i = 1; i <= a.length; i++) {
|
|
126
|
+
let prev = row[0];
|
|
127
|
+
row[0] = i;
|
|
128
|
+
for (let j = 1; j <= b.length; j++) {
|
|
129
|
+
const tmp = row[j];
|
|
130
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
131
|
+
prev = tmp;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return row[b.length];
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
export {
|
|
108
138
|
BUILTIN_KINDS,
|
|
109
139
|
BUILTIN_EDGE_KINDS,
|
|
110
140
|
STEP_KIND_COLORS,
|
|
111
141
|
DEFAULT_NODE_WIDTH,
|
|
142
|
+
DEFAULT_ENTITY_WIDTH,
|
|
112
143
|
polylineLength,
|
|
113
144
|
dist,
|
|
114
145
|
pointAt,
|
|
115
146
|
roundedPath,
|
|
116
|
-
letterKey
|
|
147
|
+
letterKey,
|
|
148
|
+
didYouMean
|
|
117
149
|
};
|
package/dist/compiler.d.ts
CHANGED
|
@@ -23,9 +23,20 @@ export interface DiagramSource {
|
|
|
23
23
|
/** Read a file relative to the diagram directory (docs, local icons). Undefined when missing. */
|
|
24
24
|
readFile?: (relPath: string) => string | undefined;
|
|
25
25
|
}
|
|
26
|
+
/** Where a dotted path of the diagram lives in the files it was compiled from. */
|
|
27
|
+
export type Where = (path: (string | number)[]) => {
|
|
28
|
+
file: string;
|
|
29
|
+
line?: number;
|
|
30
|
+
col?: number;
|
|
31
|
+
};
|
|
26
32
|
export interface CompileResult {
|
|
27
33
|
diagram?: CompiledDiagram;
|
|
28
34
|
diagnostics: Diagnostic[];
|
|
35
|
+
/**
|
|
36
|
+
* Lets a later pass (linking diagrams to each other) report at a line. A closure over the parsed
|
|
37
|
+
* files, so it is not enumerable: spreading, cloning or serialising a result leaves it behind.
|
|
38
|
+
*/
|
|
39
|
+
readonly where?: Where;
|
|
29
40
|
}
|
|
30
41
|
export interface CompilerAssets {
|
|
31
42
|
resolveIcon: (spec: string) => IconResult;
|
package/dist/defaults.d.ts
CHANGED
|
@@ -8,3 +8,5 @@ export declare const BUILTIN_KINDS: Record<string, KindDef>;
|
|
|
8
8
|
export declare const BUILTIN_EDGE_KINDS: Record<string, EdgeKindDef>;
|
|
9
9
|
export declare const STEP_KIND_COLORS: Record<string, string>;
|
|
10
10
|
export declare const DEFAULT_NODE_WIDTH = 178;
|
|
11
|
+
/** Cards with fields need room for a name and a type side by side. */
|
|
12
|
+
export declare const DEFAULT_ENTITY_WIDTH = 280;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export * from './defaults';
|
|
2
2
|
export * from './geometry';
|
|
3
3
|
export * from './layout';
|
|
4
|
+
export * from './nested-layout';
|
|
5
|
+
export * from './nesting';
|
|
6
|
+
export * from './outline';
|
|
7
|
+
export * from './phases';
|
|
4
8
|
export * from './route';
|
|
9
|
+
export * from './scrubber';
|
|
10
|
+
export * from './start';
|
|
5
11
|
export * from './story';
|
|
6
12
|
export * from './svg';
|
|
7
13
|
export * from './timeline';
|