@tanishqkancharla/diffmap 0.3.0 → 0.4.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 +97 -1
- package/index.html +6 -0
- package/package.json +3 -2
- package/src/changes.ts +15 -0
- package/src/codeNotes.ts +123 -0
- package/src/errors.ts +5 -0
- 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 +148 -102
- package/src/host/home.test.ts +6 -7
- package/src/parseFence.ts +57 -17
- package/src/parseViewer.test.ts +216 -2
- package/src/parseViewer.ts +85 -19
- package/src/services.ts +289 -0
- package/src/share.ts +1 -1
- package/src/viewer/CallStackDiff.tsx +90 -24
- package/src/viewer/ChangesCard.tsx +133 -43
- package/src/viewer/CodeBlock.tsx +131 -0
- package/src/viewer/Fence.tsx +22 -2
- package/src/viewer/ServiceDiagramBlock.tsx +323 -0
- package/src/viewer/ServiceStage.tsx +988 -0
- package/src/viewer/SourceDiffPanel.tsx +11 -8
- package/src/viewer/TableOfContents.tsx +3 -0
- package/src/viewer/ViewerApp.tsx +18 -3
- package/src/viewer/notes.tsx +126 -0
- package/src/viewer/serviceLayout.ts +189 -0
- package/vite.config.ts +1 -0
- package/vite.gist.config.ts +1 -0
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type SourceAnnotation,
|
|
20
20
|
type SourceReference,
|
|
21
21
|
} from "../annotations.js";
|
|
22
|
-
import { changedFileItem, type Rev } from "../changes.js";
|
|
22
|
+
import { changedFileItem, orderForReview, type Rev } from "../changes.js";
|
|
23
23
|
import type { SourceDefinition } from "../definitions.js";
|
|
24
24
|
import { useGistPin } from "../gist/pin.ts";
|
|
25
25
|
import { useGitHubPin } from "../github/pin.ts";
|
|
@@ -46,6 +46,8 @@ type DefinitionNavigation = {
|
|
|
46
46
|
export function SourceDiffPanel(props: {
|
|
47
47
|
/** The changes to show; references with their own rev open that rev instead. */
|
|
48
48
|
rev: Rev;
|
|
49
|
+
/** Paths to show first, in order, as the rev's changes card lists them. */
|
|
50
|
+
review: string[];
|
|
49
51
|
/** Local viewers pick other changes with the range picker. */
|
|
50
52
|
onRevChange?: (rev: Rev) => void;
|
|
51
53
|
selection: SourceSelection | undefined;
|
|
@@ -85,13 +87,14 @@ export function SourceDiffPanel(props: {
|
|
|
85
87
|
const selection = props.selection;
|
|
86
88
|
const reference = selection?.reference;
|
|
87
89
|
const changes = useChanges(props.rev);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const { review } = props;
|
|
91
|
+
const items = useMemo(() => {
|
|
92
|
+
if (changes === undefined || changes instanceof Error) return [];
|
|
93
|
+
const { reviewed, other } = orderForReview(changes.files, review);
|
|
94
|
+
return [...reviewed, ...other].flatMap(
|
|
95
|
+
(changed) => changedFileItem(changed) ?? [],
|
|
96
|
+
);
|
|
97
|
+
}, [changes, review]);
|
|
95
98
|
// A rev-less reference resolves against current files; a commit reference names its lines directly.
|
|
96
99
|
const source = useSourceReference(
|
|
97
100
|
reference?.kind === "file" && reference.rev === undefined
|
|
@@ -390,6 +390,9 @@ const styles = {
|
|
|
390
390
|
justifyContent: "center",
|
|
391
391
|
alignItems: "flex-start",
|
|
392
392
|
pointerEvents: "none",
|
|
393
|
+
"@media (max-width: 600px)": {
|
|
394
|
+
display: "none",
|
|
395
|
+
},
|
|
393
396
|
}),
|
|
394
397
|
cluster: style(spacing.padding({ top: 4, bottom: 4, left: 2, right: 6 }), {
|
|
395
398
|
position: "relative",
|
package/src/viewer/ViewerApp.tsx
CHANGED
|
@@ -45,7 +45,7 @@ export function ViewerApp(props: {
|
|
|
45
45
|
const local = props.mode === "local";
|
|
46
46
|
const documentRev =
|
|
47
47
|
props.defaultRev ??
|
|
48
|
-
viewerDocument.
|
|
48
|
+
viewerDocument.changeCards[0]?.rev ??
|
|
49
49
|
viewerDocument.references.find((ref) => ref.rev !== undefined)?.rev ??
|
|
50
50
|
(local ? ALL : WORKING);
|
|
51
51
|
const [panelRev, setPanelRev] = useState(documentRev);
|
|
@@ -58,9 +58,14 @@ export function ViewerApp(props: {
|
|
|
58
58
|
const hasSourceDiffs =
|
|
59
59
|
local ||
|
|
60
60
|
viewerDocument.references.length > 0 ||
|
|
61
|
-
viewerDocument.
|
|
61
|
+
viewerDocument.changeCards.length > 0 ||
|
|
62
62
|
props.defaultRev !== undefined;
|
|
63
63
|
const diffPanelOpen = hasSourceDiffs && showDiffPanel;
|
|
64
|
+
// The panel lists files in the order the rev's changes card asks them to be reviewed.
|
|
65
|
+
const panelReview =
|
|
66
|
+
viewerDocument.changeCards.find(
|
|
67
|
+
(card) => card.rev === panelRev && card.review.length > 0,
|
|
68
|
+
)?.review ?? NO_REVIEW;
|
|
64
69
|
const body = useStyles(styles.body);
|
|
65
70
|
const [shutDown, setShutDown] = useState(false);
|
|
66
71
|
const parsedTitle =
|
|
@@ -157,6 +162,7 @@ export function ViewerApp(props: {
|
|
|
157
162
|
{diffPanelOpen && (
|
|
158
163
|
<SourceDiffPanel
|
|
159
164
|
rev={panelRev}
|
|
165
|
+
review={panelReview}
|
|
160
166
|
onRevChange={
|
|
161
167
|
local
|
|
162
168
|
? (rev) => {
|
|
@@ -177,6 +183,8 @@ export function ViewerApp(props: {
|
|
|
177
183
|
);
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
const NO_REVIEW: string[] = [];
|
|
187
|
+
|
|
180
188
|
function useViewerMeta(enabled: boolean) {
|
|
181
189
|
const [meta, setMeta] = useState<ViewerMeta>();
|
|
182
190
|
useEffect(() => {
|
|
@@ -265,6 +273,10 @@ const styles = {
|
|
|
265
273
|
backgroundColor: backgroundColor.app,
|
|
266
274
|
// Scrollport size for the sticky gutter rail (`100cqh`).
|
|
267
275
|
containerType: "size",
|
|
276
|
+
"@media (max-width: 600px)": {
|
|
277
|
+
paddingInline: spacing.value(6),
|
|
278
|
+
paddingBlock: spacing.value(8),
|
|
279
|
+
},
|
|
268
280
|
}),
|
|
269
281
|
prose: style({
|
|
270
282
|
display: "grid",
|
|
@@ -273,7 +285,10 @@ const styles = {
|
|
|
273
285
|
maxWidth: "none",
|
|
274
286
|
minWidth: 0,
|
|
275
287
|
"&[data-has-toc='true']": {
|
|
276
|
-
gridTemplateColumns: `minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr) minmax(0, ${proseMaxWidth}) minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr)`,
|
|
288
|
+
gridTemplateColumns: `var(--diffmap-toc-columns, minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr) minmax(0, ${proseMaxWidth}) minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr))`,
|
|
289
|
+
},
|
|
290
|
+
"@media (max-width: 600px)": {
|
|
291
|
+
"--diffmap-toc-columns": "0 minmax(0, 1fr) 0",
|
|
277
292
|
},
|
|
278
293
|
}),
|
|
279
294
|
content: style({
|
|
@@ -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;
|
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
} from "../services.js";
|
|
10
|
+
|
|
11
|
+
export type Point = { x: number; y: number };
|
|
12
|
+
export type Rect = { x: number; y: number; width: number; height: number };
|
|
13
|
+
export type Size = { width: number; height: number };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One connection per pair of services that ever talk, pointing from the one
|
|
17
|
+
* declared first. Requests and replies share it, so the graph has no cycles
|
|
18
|
+
* and ELK never routes a reply around the whole diagram.
|
|
19
|
+
*/
|
|
20
|
+
export type ServiceLink = {
|
|
21
|
+
id: string;
|
|
22
|
+
a: string;
|
|
23
|
+
b: string;
|
|
24
|
+
/** Some event goes a → b. */
|
|
25
|
+
forward: boolean;
|
|
26
|
+
/** Some event goes b → a. */
|
|
27
|
+
backward: boolean;
|
|
28
|
+
/** The longest label on the link. */
|
|
29
|
+
label: string | undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ServiceLayout = {
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
/** Each service's own header and state, measured before layout. */
|
|
36
|
+
sizes: Map<string, Size>;
|
|
37
|
+
boxes: Map<string, Rect>;
|
|
38
|
+
routes: Map<string, Point[]>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function serviceLinks(diagram: ServiceDiagram): ServiceLink[] {
|
|
42
|
+
const order = new Map(
|
|
43
|
+
flattenServices(diagram.services).map(({ service }, index) => [
|
|
44
|
+
service.name,
|
|
45
|
+
index,
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
const links = new Map<string, ServiceLink>();
|
|
49
|
+
for (const step of diagram.steps)
|
|
50
|
+
for (const event of step.events) {
|
|
51
|
+
const forward = order.get(event.from)! <= order.get(event.to)!;
|
|
52
|
+
const [a, b] = forward ? [event.from, event.to] : [event.to, event.from];
|
|
53
|
+
const id = linkId(a, b);
|
|
54
|
+
const link = links.get(id) ?? {
|
|
55
|
+
id,
|
|
56
|
+
a,
|
|
57
|
+
b,
|
|
58
|
+
forward: false,
|
|
59
|
+
backward: false,
|
|
60
|
+
label: undefined,
|
|
61
|
+
};
|
|
62
|
+
if (forward) link.forward = true;
|
|
63
|
+
else link.backward = true;
|
|
64
|
+
// Measure the pill as drawn: a linked event shows ↗.
|
|
65
|
+
const shown =
|
|
66
|
+
event.label !== "" && event.annotation !== undefined
|
|
67
|
+
? `${event.label} ↗`
|
|
68
|
+
: event.label;
|
|
69
|
+
if (shown.length > (link.label?.length ?? 0)) link.label = shown;
|
|
70
|
+
links.set(id, link);
|
|
71
|
+
}
|
|
72
|
+
return [...links.values()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function linkId(from: string, to: string) {
|
|
76
|
+
return from < to ? `${from}|${to}` : `${to}|${from}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const elk = new ELK();
|
|
80
|
+
|
|
81
|
+
function contains(service: Service, name: string): boolean {
|
|
82
|
+
return service.children.some(
|
|
83
|
+
(child) => child.name === name || contains(child, name),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `sizes` are each service's own header and state; a parent grows around its
|
|
89
|
+
* children. `label` is the largest event label, which must fit between layers.
|
|
90
|
+
*/
|
|
91
|
+
export async function layoutServices(input: {
|
|
92
|
+
diagram: ServiceDiagram;
|
|
93
|
+
links: ServiceLink[];
|
|
94
|
+
sizes: Map<string, Size>;
|
|
95
|
+
label: Size;
|
|
96
|
+
direction: "RIGHT" | "DOWN";
|
|
97
|
+
}): Promise<ServiceLayout> {
|
|
98
|
+
// Room for a label pill on the edge between two layers. Each parent lays out
|
|
99
|
+
// its own children, so parents need it too.
|
|
100
|
+
const betweenLayers = String(
|
|
101
|
+
input.direction === "RIGHT"
|
|
102
|
+
? Math.max(40, input.label.width + 24)
|
|
103
|
+
: Math.max(40, input.label.height + 28),
|
|
104
|
+
);
|
|
105
|
+
const node = (service: Service): ElkNode => {
|
|
106
|
+
const own = input.sizes.get(service.name)!;
|
|
107
|
+
if (service.children.length === 0)
|
|
108
|
+
return { id: service.name, width: own.width, height: own.height };
|
|
109
|
+
// A parent that talks to its own children needs room for that route and
|
|
110
|
+
// its label between its header and the children.
|
|
111
|
+
const inner = input.links.some(
|
|
112
|
+
(link) => link.a === service.name && contains(service, link.b),
|
|
113
|
+
);
|
|
114
|
+
const room = (size: number) => (inner ? size + 24 : 0);
|
|
115
|
+
const top =
|
|
116
|
+
own.height +
|
|
117
|
+
4 +
|
|
118
|
+
(input.direction === "DOWN" ? room(input.label.height) : 0);
|
|
119
|
+
const left =
|
|
120
|
+
16 + (input.direction === "RIGHT" ? room(input.label.width) : 0);
|
|
121
|
+
return {
|
|
122
|
+
id: service.name,
|
|
123
|
+
layoutOptions: {
|
|
124
|
+
"elk.padding": `[top=${top},left=${left},bottom=16,right=16]`,
|
|
125
|
+
"elk.nodeSize.constraints": "MINIMUM_SIZE",
|
|
126
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": betweenLayers,
|
|
127
|
+
// Wide enough for its own header. ELK applies this before rotating
|
|
128
|
+
// a top-to-bottom layout, so the axes swap.
|
|
129
|
+
"elk.nodeSize.minimum":
|
|
130
|
+
input.direction === "DOWN" ? `(0,${own.width})` : `(${own.width},0)`,
|
|
131
|
+
},
|
|
132
|
+
children: service.children.map(node),
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
const graph: ElkNode = {
|
|
136
|
+
id: "root",
|
|
137
|
+
layoutOptions: {
|
|
138
|
+
"elk.algorithm": "layered",
|
|
139
|
+
"elk.direction": input.direction,
|
|
140
|
+
"elk.edgeRouting": "ORTHOGONAL",
|
|
141
|
+
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
|
|
142
|
+
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
143
|
+
"elk.json.edgeCoords": "ROOT",
|
|
144
|
+
"elk.json.shapeCoords": "ROOT",
|
|
145
|
+
"elk.padding": "[top=8,left=8,bottom=8,right=8]",
|
|
146
|
+
"elk.spacing.nodeNode": "32",
|
|
147
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": betweenLayers,
|
|
148
|
+
"elk.spacing.edgeNode": "16",
|
|
149
|
+
"elk.spacing.edgeEdge": "12",
|
|
150
|
+
},
|
|
151
|
+
children: input.diagram.services.map(node),
|
|
152
|
+
edges: input.links.map((link): ElkExtendedEdge => ({
|
|
153
|
+
id: link.id,
|
|
154
|
+
sources: [link.a],
|
|
155
|
+
targets: [link.b],
|
|
156
|
+
})),
|
|
157
|
+
};
|
|
158
|
+
const result = await elk.layout(graph);
|
|
159
|
+
|
|
160
|
+
const boxes = new Map<string, Rect>();
|
|
161
|
+
const visit = (nodes: ElkNode[] | undefined) => {
|
|
162
|
+
for (const child of nodes ?? []) {
|
|
163
|
+
boxes.set(child.id, {
|
|
164
|
+
x: child.x!,
|
|
165
|
+
y: child.y!,
|
|
166
|
+
width: child.width!,
|
|
167
|
+
height: child.height!,
|
|
168
|
+
});
|
|
169
|
+
visit(child.children);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
visit(result.children);
|
|
173
|
+
const routes = new Map(
|
|
174
|
+
(result.edges ?? []).map((edge) => {
|
|
175
|
+
const section = edge.sections![0]!;
|
|
176
|
+
return [
|
|
177
|
+
edge.id,
|
|
178
|
+
[section.startPoint, ...(section.bendPoints ?? []), section.endPoint],
|
|
179
|
+
];
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
return {
|
|
183
|
+
width: result.width!,
|
|
184
|
+
height: result.height!,
|
|
185
|
+
sizes: input.sizes,
|
|
186
|
+
boxes,
|
|
187
|
+
routes,
|
|
188
|
+
};
|
|
189
|
+
}
|
package/vite.config.ts
CHANGED