@tanishqkancharla/diffmap 0.3.0 → 0.6.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 +171 -1
- package/index.html +6 -0
- package/package.json +3 -2
- package/src/annotations.ts +17 -1
- package/src/changes.ts +15 -0
- package/src/codeNotes.ts +123 -0
- package/src/errors.ts +5 -0
- package/src/extractDocument.ts +10 -2
- package/src/git.test.ts +13 -1
- package/src/git.ts +15 -2
- package/src/host/HostHome.tsx +22 -6
- package/src/host/home.md +154 -109
- package/src/host/home.test.ts +6 -8
- package/src/parseFence.ts +132 -23
- package/src/parseViewer.test.ts +359 -2
- package/src/parseViewer.ts +126 -25
- package/src/serve.test.ts +22 -1
- package/src/services.ts +396 -0
- package/src/share.ts +1 -1
- package/src/viewer/CallStackDiff.tsx +119 -55
- package/src/viewer/ChangesCard.tsx +133 -45
- package/src/viewer/CodeBlock.tsx +204 -0
- package/src/viewer/ComarkView.tsx +34 -0
- package/src/viewer/Fence.tsx +26 -2
- package/src/viewer/HtmlBlock.tsx +2 -1
- package/src/viewer/MermaidBlock.tsx +55 -14
- package/src/viewer/ServiceDiagramBlock.tsx +358 -0
- package/src/viewer/ServiceStage.tsx +1129 -0
- package/src/viewer/SourceDiffPanel.tsx +147 -48
- package/src/viewer/TableOfContents.tsx +47 -18
- package/src/viewer/TocButton.tsx +28 -0
- package/src/viewer/ViewerApp.tsx +28 -3
- package/src/viewer/diagramDiff.ts +69 -0
- package/src/viewer/diagramLinks.ts +54 -6
- package/src/viewer/notes.tsx +126 -0
- package/src/viewer/pierre.ts +20 -2
- package/src/viewer/serviceLayout.ts +200 -0
- package/vite.config.ts +5 -0
- package/vite.gist.config.ts +1 -0
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { colors } from "maui";
|
|
1
2
|
import type { DiagramAnnotation } from "../diagramAnnotations.js";
|
|
2
3
|
import { DiffmapAnnotationError } from "../errors.js";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Arrowhead colors for edges that are not plain gray: changed in a diff,
|
|
7
|
+
* hovered, or selected. Later tones win when several apply.
|
|
8
|
+
*/
|
|
9
|
+
const markerTones = [
|
|
10
|
+
["added", "[data-change='+']", colors.green[9]],
|
|
11
|
+
["removed", "[data-change='-']", colors.red[9]],
|
|
12
|
+
["hover", ".source-edge:hover", colors.blue[8]],
|
|
13
|
+
["selected", ".source-edge[aria-pressed='true']", colors.blue[9]],
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
4
16
|
export function linkDiagram(svg: string, links: DiagramAnnotation[]) {
|
|
5
17
|
const document = new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
6
|
-
|
|
7
|
-
if (markerPart.getAttribute("fill") === "var(--_arrow)")
|
|
8
|
-
markerPart.setAttribute("fill", "context-stroke");
|
|
9
|
-
if (markerPart.getAttribute("stroke") === "var(--_arrow)")
|
|
10
|
-
markerPart.setAttribute("stroke", "context-stroke");
|
|
11
|
-
}
|
|
18
|
+
colorMarkers(document);
|
|
12
19
|
const edges = [
|
|
13
20
|
...document.querySelectorAll<SVGElement>(
|
|
14
21
|
".edge, .message, .class-relationship, .er-relationship",
|
|
@@ -72,3 +79,44 @@ export function linkDiagram(svg: string, links: DiagramAnnotation[]) {
|
|
|
72
79
|
}
|
|
73
80
|
return document.documentElement.outerHTML;
|
|
74
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A marker draws in its own colors, not its edge's, and `context-stroke`
|
|
85
|
+
* falls back to black where browsers lack it. So each marker gets one copy
|
|
86
|
+
* per tone, and a style rule swaps an edge to the copy its state needs.
|
|
87
|
+
*/
|
|
88
|
+
function colorMarkers(document: Document) {
|
|
89
|
+
const rules: string[] = [];
|
|
90
|
+
for (const marker of document.querySelectorAll("marker[id]")) {
|
|
91
|
+
const id = marker.id;
|
|
92
|
+
for (const [tone, , color] of markerTones) {
|
|
93
|
+
// SAFETY: cloning an element preserves its element type.
|
|
94
|
+
const copy = marker.cloneNode(true) as Element;
|
|
95
|
+
copy.id = `${id}--${tone}`;
|
|
96
|
+
paintArrow(copy, color);
|
|
97
|
+
marker.after(copy);
|
|
98
|
+
}
|
|
99
|
+
// Mermaid draws arrowheads in the accent color; match the gray line.
|
|
100
|
+
paintArrow(marker, "var(--_line)");
|
|
101
|
+
for (const end of ["start", "end"]) {
|
|
102
|
+
const uses = `[marker-${end}="url(#${id})"]`;
|
|
103
|
+
if (document.querySelector(uses) === null) continue;
|
|
104
|
+
for (const [tone, state] of markerTones)
|
|
105
|
+
rules.push(
|
|
106
|
+
`:is(${state}, ${state} *)${uses} { marker-${end}: url(#${id}--${tone}); }`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (rules.length === 0) return;
|
|
111
|
+
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
112
|
+
style.textContent = rules.join("\n");
|
|
113
|
+
document.documentElement.prepend(style);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Repaints a marker's mermaid arrow color. */
|
|
117
|
+
function paintArrow(marker: Element, color: string) {
|
|
118
|
+
for (const part of [marker, ...marker.querySelectorAll("*")])
|
|
119
|
+
for (const paint of ["fill", "stroke"])
|
|
120
|
+
if (part.getAttribute(paint) === "var(--_arrow)")
|
|
121
|
+
part.setAttribute(paint, color);
|
|
122
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Code, colors, focusRing, Tooltip } from "maui";
|
|
3
|
+
import { style, useStyles } from "purse-styles";
|
|
4
|
+
import type { SymbolMatch } from "../codeNotes.js";
|
|
5
|
+
|
|
6
|
+
export type Token = {
|
|
7
|
+
content: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
fontStyle?: number;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Splits tokens at symbol note boundaries and wraps each match in a tooltip. */
|
|
14
|
+
export function NoteTokens(props: {
|
|
15
|
+
tokens: Token[];
|
|
16
|
+
matches: SymbolMatch[];
|
|
17
|
+
/** False inside a button, which cannot contain focusable elements. */
|
|
18
|
+
focusable?: boolean;
|
|
19
|
+
}) {
|
|
20
|
+
const { tokens, matches } = props;
|
|
21
|
+
const symbolClassName = useStyles(symbolNoteClass);
|
|
22
|
+
const cuts = new Set(matches.flatMap((match) => [match.start, match.end]));
|
|
23
|
+
const pieces: { start: number; token: Token }[] = [];
|
|
24
|
+
let column = 0;
|
|
25
|
+
for (const token of tokens) {
|
|
26
|
+
let start = column;
|
|
27
|
+
for (let at = column + 1; at < column + token.content.length; at++) {
|
|
28
|
+
if (!cuts.has(at)) continue;
|
|
29
|
+
pieces.push({
|
|
30
|
+
start,
|
|
31
|
+
token: {
|
|
32
|
+
...token,
|
|
33
|
+
content: token.content.slice(start - column, at - column),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
start = at;
|
|
37
|
+
}
|
|
38
|
+
pieces.push({
|
|
39
|
+
start,
|
|
40
|
+
token: { ...token, content: token.content.slice(start - column) },
|
|
41
|
+
});
|
|
42
|
+
column += token.content.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const out: ReactNode[] = [];
|
|
46
|
+
let group: ReactNode[] = [];
|
|
47
|
+
for (const piece of pieces) {
|
|
48
|
+
const span = <TokenSpan key={piece.start} token={piece.token} />;
|
|
49
|
+
const match = matches.find(
|
|
50
|
+
(m) => piece.start >= m.start && piece.start < m.end,
|
|
51
|
+
);
|
|
52
|
+
if (match === undefined) {
|
|
53
|
+
out.push(span);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
group.push(span);
|
|
57
|
+
if (piece.start + piece.token.content.length < match.end) continue;
|
|
58
|
+
out.push(
|
|
59
|
+
<Tooltip
|
|
60
|
+
key={`note-${match.start}`}
|
|
61
|
+
content={<NoteText text={match.note.text} />}
|
|
62
|
+
>
|
|
63
|
+
<span
|
|
64
|
+
className={symbolClassName}
|
|
65
|
+
tabIndex={props.focusable === false ? undefined : 0}
|
|
66
|
+
>
|
|
67
|
+
{group}
|
|
68
|
+
</span>
|
|
69
|
+
</Tooltip>,
|
|
70
|
+
);
|
|
71
|
+
group = [];
|
|
72
|
+
}
|
|
73
|
+
return <>{out}</>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function TokenSpan(props: { token: Token }) {
|
|
77
|
+
const fontStyle = props.token.fontStyle ?? 0;
|
|
78
|
+
return (
|
|
79
|
+
<span
|
|
80
|
+
className={props.token.className}
|
|
81
|
+
style={{
|
|
82
|
+
color: props.token.color,
|
|
83
|
+
fontStyle: fontStyle & 1 ? "italic" : undefined,
|
|
84
|
+
fontWeight: fontStyle & 2 ? 500 : undefined,
|
|
85
|
+
textDecoration: fontStyle & 4 ? "underline" : undefined,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{props.token.content}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Note text with `backtick` spans as inline code. */
|
|
94
|
+
export function NoteText(props: { text: string }) {
|
|
95
|
+
return props.text
|
|
96
|
+
.split(/(`[^`]+`)/)
|
|
97
|
+
.map((part, index) =>
|
|
98
|
+
part.startsWith("`") && part.endsWith("`") && part.length > 1 ? (
|
|
99
|
+
<Code key={index}>{part.slice(1, -1)}</Code>
|
|
100
|
+
) : (
|
|
101
|
+
part
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const symbolNoteClass = style(focusRing(), {
|
|
107
|
+
borderRadius: "3px",
|
|
108
|
+
cursor: "help",
|
|
109
|
+
textDecorationLine: "underline",
|
|
110
|
+
textDecorationStyle: "dotted",
|
|
111
|
+
textDecorationColor: colors.gray[9],
|
|
112
|
+
textUnderlineOffset: "3px",
|
|
113
|
+
"&:hover": { backgroundColor: colors.blueAlpha[5] },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** A dot in the left gutter of the first row a line note covers. */
|
|
117
|
+
export const lineNoteDot = {
|
|
118
|
+
content: '""',
|
|
119
|
+
position: "absolute",
|
|
120
|
+
left: "4px",
|
|
121
|
+
top: "calc(0.5lh - 2.5px)",
|
|
122
|
+
width: "5px",
|
|
123
|
+
height: "5px",
|
|
124
|
+
borderRadius: "50%",
|
|
125
|
+
backgroundColor: colors.blue[9],
|
|
126
|
+
} as const;
|
package/src/viewer/pierre.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
backgroundColor,
|
|
3
|
+
border,
|
|
4
|
+
colors,
|
|
5
|
+
radius,
|
|
6
|
+
shadow,
|
|
7
|
+
spacing,
|
|
8
|
+
text,
|
|
9
|
+
} from "maui";
|
|
2
10
|
import { style } from "purse-styles";
|
|
3
11
|
import { codeFontFamily } from "../codeFont.js";
|
|
4
12
|
import { diffsTheme } from "./diffsTheme.ts";
|
|
5
13
|
|
|
6
|
-
|
|
14
|
+
// Pierre paints its own white; widgets use the element surface instead.
|
|
15
|
+
const pierreUnsafeCss = `:host { --diffs-font-family: ${codeFontFamily}; --diffs-light-bg: ${backgroundColor.app}; --diffs-dark-bg: ${backgroundColor.app}; }`;
|
|
7
16
|
|
|
8
17
|
export const sourceSelectionCss = `
|
|
9
18
|
[data-selected-line] {
|
|
@@ -18,11 +27,20 @@ export const sourceSelectionCss = `
|
|
|
18
27
|
}
|
|
19
28
|
`;
|
|
20
29
|
|
|
30
|
+
/** The card every widget sits in: a subtle ring on the page background. */
|
|
21
31
|
export const pierreShell = style(radius.md, shadow.subtle, {
|
|
22
32
|
overflow: "hidden",
|
|
23
33
|
minWidth: 0,
|
|
24
34
|
});
|
|
25
35
|
|
|
36
|
+
/** The header row atop a card: a fence's `[Title]` or a diagram's caption. */
|
|
37
|
+
export const fenceTitle = style(
|
|
38
|
+
text({ size: "xs", fontWeight: 500, color: "lowContrast" }),
|
|
39
|
+
spacing.padding({ x: 6, y: 4 }),
|
|
40
|
+
border(["bottom"], "border"),
|
|
41
|
+
{ margin: 0, overflowWrap: "anywhere", backgroundColor: colors.gray[2] },
|
|
42
|
+
);
|
|
43
|
+
|
|
26
44
|
export function pierreDiffOptions(input: {
|
|
27
45
|
themeType: "light" | "dark";
|
|
28
46
|
disableFileHeader: boolean;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import ELK, {
|
|
2
|
+
type ElkExtendedEdge,
|
|
3
|
+
type ElkNode,
|
|
4
|
+
} from "elkjs/lib/elk.bundled.js";
|
|
5
|
+
import {
|
|
6
|
+
flattenServices,
|
|
7
|
+
type Service,
|
|
8
|
+
type ServiceDiagram,
|
|
9
|
+
type ServiceEvent,
|
|
10
|
+
type Sign,
|
|
11
|
+
} from "../services.js";
|
|
12
|
+
|
|
13
|
+
export type Point = { x: number; y: number };
|
|
14
|
+
export type Rect = { x: number; y: number; width: number; height: number };
|
|
15
|
+
export type Size = { width: number; height: number };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One connection per pair of services that ever talk, pointing from the one
|
|
19
|
+
* declared first. Requests and replies share it, so the graph has no cycles
|
|
20
|
+
* and ELK never routes a reply around the whole diagram.
|
|
21
|
+
*/
|
|
22
|
+
export type ServiceLink = {
|
|
23
|
+
id: string;
|
|
24
|
+
a: string;
|
|
25
|
+
b: string;
|
|
26
|
+
/** Some event goes a → b. */
|
|
27
|
+
forward: boolean;
|
|
28
|
+
/** Some event goes b → a. */
|
|
29
|
+
backward: boolean;
|
|
30
|
+
/** The longest label on the link. */
|
|
31
|
+
label: string | undefined;
|
|
32
|
+
/** In a diff, `+` or `-` when every event on the link has that sign. */
|
|
33
|
+
sign: Sign;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** An event's label as its pill shows it, or "" when it has none. */
|
|
37
|
+
export function pillText(event: ServiceEvent) {
|
|
38
|
+
if (event.label === "") return "";
|
|
39
|
+
const sign = { "+": "+ ", "-": "− ", " ": "" }[event.sign];
|
|
40
|
+
const link = event.annotation === undefined ? "" : " ↗";
|
|
41
|
+
return `${sign}${event.label}${link}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type ServiceLayout = {
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
/** Each service's own header and state, measured before layout. */
|
|
48
|
+
sizes: Map<string, Size>;
|
|
49
|
+
boxes: Map<string, Rect>;
|
|
50
|
+
routes: Map<string, Point[]>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function serviceLinks(diagram: ServiceDiagram): ServiceLink[] {
|
|
54
|
+
const order = new Map(
|
|
55
|
+
flattenServices(diagram.services).map(({ service }, index) => [
|
|
56
|
+
service.name,
|
|
57
|
+
index,
|
|
58
|
+
]),
|
|
59
|
+
);
|
|
60
|
+
const links = new Map<string, ServiceLink>();
|
|
61
|
+
for (const step of diagram.steps)
|
|
62
|
+
for (const event of step.events) {
|
|
63
|
+
const forward = order.get(event.from)! <= order.get(event.to)!;
|
|
64
|
+
const [a, b] = forward ? [event.from, event.to] : [event.to, event.from];
|
|
65
|
+
const id = linkId(a, b);
|
|
66
|
+
const link = links.get(id) ?? {
|
|
67
|
+
id,
|
|
68
|
+
a,
|
|
69
|
+
b,
|
|
70
|
+
forward: false,
|
|
71
|
+
backward: false,
|
|
72
|
+
label: undefined,
|
|
73
|
+
sign: event.sign,
|
|
74
|
+
};
|
|
75
|
+
if (forward) link.forward = true;
|
|
76
|
+
else link.backward = true;
|
|
77
|
+
if (link.sign !== event.sign) link.sign = " ";
|
|
78
|
+
// Measure the pill as drawn.
|
|
79
|
+
const shown = pillText(event);
|
|
80
|
+
if (shown.length > (link.label?.length ?? 0)) link.label = shown;
|
|
81
|
+
links.set(id, link);
|
|
82
|
+
}
|
|
83
|
+
return [...links.values()];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function linkId(from: string, to: string) {
|
|
87
|
+
return from < to ? `${from}|${to}` : `${to}|${from}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const elk = new ELK();
|
|
91
|
+
|
|
92
|
+
function contains(service: Service, name: string): boolean {
|
|
93
|
+
return service.children.some(
|
|
94
|
+
(child) => child.name === name || contains(child, name),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `sizes` are each service's own header and state; a parent grows around its
|
|
100
|
+
* children. `label` is the largest event label, which must fit between layers.
|
|
101
|
+
*/
|
|
102
|
+
export async function layoutServices(input: {
|
|
103
|
+
diagram: ServiceDiagram;
|
|
104
|
+
links: ServiceLink[];
|
|
105
|
+
sizes: Map<string, Size>;
|
|
106
|
+
label: Size;
|
|
107
|
+
direction: "RIGHT" | "DOWN";
|
|
108
|
+
}): Promise<ServiceLayout> {
|
|
109
|
+
// Room for a label pill on the edge between two layers. Each parent lays out
|
|
110
|
+
// its own children, so parents need it too.
|
|
111
|
+
const betweenLayers = String(
|
|
112
|
+
input.direction === "RIGHT"
|
|
113
|
+
? Math.max(40, input.label.width + 24)
|
|
114
|
+
: Math.max(40, input.label.height + 28),
|
|
115
|
+
);
|
|
116
|
+
const node = (service: Service): ElkNode => {
|
|
117
|
+
const own = input.sizes.get(service.name)!;
|
|
118
|
+
if (service.children.length === 0)
|
|
119
|
+
return { id: service.name, width: own.width, height: own.height };
|
|
120
|
+
// A parent that talks to its own children needs room for that route and
|
|
121
|
+
// its label between its header and the children.
|
|
122
|
+
const inner = input.links.some(
|
|
123
|
+
(link) => link.a === service.name && contains(service, link.b),
|
|
124
|
+
);
|
|
125
|
+
const room = (size: number) => (inner ? size + 24 : 0);
|
|
126
|
+
const top =
|
|
127
|
+
own.height +
|
|
128
|
+
4 +
|
|
129
|
+
(input.direction === "DOWN" ? room(input.label.height) : 0);
|
|
130
|
+
const left =
|
|
131
|
+
16 + (input.direction === "RIGHT" ? room(input.label.width) : 0);
|
|
132
|
+
return {
|
|
133
|
+
id: service.name,
|
|
134
|
+
layoutOptions: {
|
|
135
|
+
"elk.padding": `[top=${top},left=${left},bottom=16,right=16]`,
|
|
136
|
+
"elk.nodeSize.constraints": "MINIMUM_SIZE",
|
|
137
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": betweenLayers,
|
|
138
|
+
// Wide enough for its own header. ELK applies this before rotating
|
|
139
|
+
// a top-to-bottom layout, so the axes swap.
|
|
140
|
+
"elk.nodeSize.minimum":
|
|
141
|
+
input.direction === "DOWN" ? `(0,${own.width})` : `(${own.width},0)`,
|
|
142
|
+
},
|
|
143
|
+
children: service.children.map(node),
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const graph: ElkNode = {
|
|
147
|
+
id: "root",
|
|
148
|
+
layoutOptions: {
|
|
149
|
+
"elk.algorithm": "layered",
|
|
150
|
+
"elk.direction": input.direction,
|
|
151
|
+
"elk.edgeRouting": "ORTHOGONAL",
|
|
152
|
+
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
|
|
153
|
+
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
154
|
+
"elk.json.edgeCoords": "ROOT",
|
|
155
|
+
"elk.json.shapeCoords": "ROOT",
|
|
156
|
+
"elk.padding": "[top=8,left=8,bottom=8,right=8]",
|
|
157
|
+
"elk.spacing.nodeNode": "32",
|
|
158
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": betweenLayers,
|
|
159
|
+
"elk.spacing.edgeNode": "16",
|
|
160
|
+
"elk.spacing.edgeEdge": "12",
|
|
161
|
+
},
|
|
162
|
+
children: input.diagram.services.map(node),
|
|
163
|
+
edges: input.links.map((link): ElkExtendedEdge => ({
|
|
164
|
+
id: link.id,
|
|
165
|
+
sources: [link.a],
|
|
166
|
+
targets: [link.b],
|
|
167
|
+
})),
|
|
168
|
+
};
|
|
169
|
+
const result = await elk.layout(graph);
|
|
170
|
+
|
|
171
|
+
const boxes = new Map<string, Rect>();
|
|
172
|
+
const visit = (nodes: ElkNode[] | undefined) => {
|
|
173
|
+
for (const child of nodes ?? []) {
|
|
174
|
+
boxes.set(child.id, {
|
|
175
|
+
x: child.x!,
|
|
176
|
+
y: child.y!,
|
|
177
|
+
width: child.width!,
|
|
178
|
+
height: child.height!,
|
|
179
|
+
});
|
|
180
|
+
visit(child.children);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
visit(result.children);
|
|
184
|
+
const routes = new Map(
|
|
185
|
+
(result.edges ?? []).map((edge) => {
|
|
186
|
+
const section = edge.sections![0]!;
|
|
187
|
+
return [
|
|
188
|
+
edge.id,
|
|
189
|
+
[section.startPoint, ...(section.bendPoints ?? []), section.endPoint],
|
|
190
|
+
];
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
return {
|
|
194
|
+
width: result.width!,
|
|
195
|
+
height: result.height!,
|
|
196
|
+
sizes: input.sizes,
|
|
197
|
+
boxes,
|
|
198
|
+
routes,
|
|
199
|
+
};
|
|
200
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -26,6 +26,7 @@ export default defineConfig(({ command }) => {
|
|
|
26
26
|
"purse-styles",
|
|
27
27
|
"@pierre/diffs/react",
|
|
28
28
|
"beautiful-mermaid",
|
|
29
|
+
"elkjs/lib/elk.bundled.js",
|
|
29
30
|
"errore",
|
|
30
31
|
],
|
|
31
32
|
},
|
|
@@ -37,6 +38,10 @@ export default defineConfig(({ command }) => {
|
|
|
37
38
|
server: {
|
|
38
39
|
port: 0,
|
|
39
40
|
strictPort: false,
|
|
41
|
+
// Vite turns this on when it detects a coding agent, and its plugin
|
|
42
|
+
// changes the prebundle hash. Pin it so agent and terminal runs share
|
|
43
|
+
// one prebundle instead of rebuilding each other's.
|
|
44
|
+
forwardConsole: false,
|
|
40
45
|
},
|
|
41
46
|
};
|
|
42
47
|
});
|