modviz 0.1.2
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 +261 -0
- package/dist/client/_shell.html +0 -0
- package/dist/client/android-chrome-192x192.png +0 -0
- package/dist/client/android-chrome-512x512.png +0 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/app-Sjrldkrg.css +2 -0
- package/dist/client/assets/button-aOWckyNs.js +1 -0
- package/dist/client/assets/check-C0EQe2S8.js +1 -0
- package/dist/client/assets/chevron-down-DrspihmT.js +1 -0
- package/dist/client/assets/chevron-right-DIJHr8AN.js +1 -0
- package/dist/client/assets/colors-CQoWjU5E.js +1 -0
- package/dist/client/assets/command-kkF7_wdz.js +45 -0
- package/dist/client/assets/compare-K6jVFsiI.js +1 -0
- package/dist/client/assets/compare-TOnoe1EP.js +2 -0
- package/dist/client/assets/configure-DnlSnhtN.js +1 -0
- package/dist/client/assets/explorer-C7NclVKg.js +2 -0
- package/dist/client/assets/explorer-Xu2X6XXF.js +1 -0
- package/dist/client/assets/external-link-B9eNA-li.js +1 -0
- package/dist/client/assets/flamegraph-CRVZSAlj.js +13 -0
- package/dist/client/assets/floating-ui.dom-DLIT5tPE.js +1 -0
- package/dist/client/assets/formatting-CiC0SYI8.js +1 -0
- package/dist/client/assets/graph-6Vr74V1k.js +2 -0
- package/dist/client/assets/graph-CVzypIGU.js +2 -0
- package/dist/client/assets/graph-command-menu-D2MoVT2B.js +4 -0
- package/dist/client/assets/graph-command-menu-VWiiW3qy.css +1 -0
- package/dist/client/assets/hierarchy-C8xxGb_u.js +2 -0
- package/dist/client/assets/hierarchy-iO7d4oSK.js +2 -0
- package/dist/client/assets/import-display-D-jRyyjM.js +5 -0
- package/dist/client/assets/imports-CPggnrs-.js +2 -0
- package/dist/client/assets/imports-CodbPyUJ.js +1 -0
- package/dist/client/assets/index-Dj_rhLdR.js +12 -0
- package/dist/client/assets/input-BCFMF0aR.js +1 -0
- package/dist/client/assets/jsx-runtime-DWSWI4JT.js +1 -0
- package/dist/client/assets/lazyRouteComponent-PTSyFp1J.js +1 -0
- package/dist/client/assets/loading-state-CyC_hrTF.js +1 -0
- package/dist/client/assets/modviz-data-BiRqoDI5.js +1 -0
- package/dist/client/assets/modviz-layout-Do93E-IB.js +1 -0
- package/dist/client/assets/modviz-sigma-Xl8qHaxK.js +312 -0
- package/dist/client/assets/portal-BgAm3V3j.js +1 -0
- package/dist/client/assets/routes-DBtN8hrZ.js +1 -0
- package/dist/client/assets/schemas-B4zfTepZ.js +39 -0
- package/dist/client/assets/search-BYHxNrYn.js +1 -0
- package/dist/client/assets/search-params-BaZRBvGI.js +1 -0
- package/dist/client/assets/setup-view-j1o0TuZz.js +1 -0
- package/dist/client/assets/summary-D703Zh3x.js +1 -0
- package/dist/client/assets/tooltip-B1VDU9HG.js +1 -0
- package/dist/client/assets/trace-B67CM5s2.js +2 -0
- package/dist/client/assets/trace-Bwwdw3AM.js +1 -0
- package/dist/client/assets/treemap-BZf2shzY.js +5 -0
- package/dist/client/assets/treemap-Csroy8Gy.js +2 -0
- package/dist/client/assets/utils-DkkZd0ys.js +1 -0
- package/dist/client/favicon-16x16.png +0 -0
- package/dist/client/favicon-32x32.png +0 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/site.webmanifest +19 -0
- package/dist/mod/cli-options.js +225 -0
- package/dist/mod/cli.js +519 -0
- package/dist/mod/index.js +3 -0
- package/dist/mod/llm-analysis.js +29 -0
- package/dist/mod/llm-output.js +742 -0
- package/dist/mod/module-graph-plugins.js +60 -0
- package/dist/mod/production-server.js +103 -0
- package/dist/mod/runtime-host.js +217 -0
- package/dist/mod/snapshot-history.js +73 -0
- package/dist/mod/types.js +3 -0
- package/dist/server/assets/__23tanstack-start-plugin-adapters-3QxJs4a0.js +5 -0
- package/dist/server/assets/_tanstack-start-manifest_v-DMytuIue.js +188 -0
- package/dist/server/assets/button-Bqnnid5i.js +41 -0
- package/dist/server/assets/colors-DhAxrYua.js +100 -0
- package/dist/server/assets/command-SdxShIbL.js +138 -0
- package/dist/server/assets/compare-BFMiiUsB.js +562 -0
- package/dist/server/assets/compare-CpOqTpYu.js +10 -0
- package/dist/server/assets/configure-Bvd45DTI.js +288 -0
- package/dist/server/assets/explorer-C7dODpSv.js +379 -0
- package/dist/server/assets/explorer-CpSb0JTa.js +20 -0
- package/dist/server/assets/flamegraph-CdW-VG6I.js +198 -0
- package/dist/server/assets/formatting-iDlL4tA-.js +4 -0
- package/dist/server/assets/graph-C1G9H5O4.js +438 -0
- package/dist/server/assets/graph-DAGFGioS.js +45 -0
- package/dist/server/assets/graph-command-menu-BV5GtOWx.js +249 -0
- package/dist/server/assets/hierarchy-B4K-Zfn9.js +16 -0
- package/dist/server/assets/hierarchy-BGpWSG-f.js +104 -0
- package/dist/server/assets/import-display-BVIOWcsm.js +124 -0
- package/dist/server/assets/imports-B6JBDl_h.js +379 -0
- package/dist/server/assets/imports-BGe5tZJT.js +28 -0
- package/dist/server/assets/input-C5r-hBix.js +19 -0
- package/dist/server/assets/loading-state-CrvCWTtw.js +23 -0
- package/dist/server/assets/modviz-data-CUyTorv0.js +197 -0
- package/dist/server/assets/modviz-layout-BAH2ogse.js +253 -0
- package/dist/server/assets/modviz-server-DoMlAyFW.js +195 -0
- package/dist/server/assets/modviz-sigma-XYxARWqd.js +1441 -0
- package/dist/server/assets/rolldown-runtime-rSIU-vHC.js +13 -0
- package/dist/server/assets/router-DYJ-zDbU.js +353 -0
- package/dist/server/assets/routes-DInCacpY.js +244 -0
- package/dist/server/assets/search-params-BNApPgkX.js +26 -0
- package/dist/server/assets/setup-view-DjI49Iqr.js +91 -0
- package/dist/server/assets/start-Ba3KII43.js +4 -0
- package/dist/server/assets/summary-z3lXkLCQ.js +208 -0
- package/dist/server/assets/tooltip-Ck0DDfF7.js +24 -0
- package/dist/server/assets/trace-ColKOf9g.js +16 -0
- package/dist/server/assets/trace-eVs-hIZO.js +578 -0
- package/dist/server/assets/treemap-BbZ9M4GF.js +17 -0
- package/dist/server/assets/treemap-CrgWFoCF.js +912 -0
- package/dist/server/assets/utils-BQZm0uva.js +8 -0
- package/dist/server/server.js +5259 -0
- package/dist/shared/modviz-compare.js +120 -0
- package/dist/shared/modviz-trace.js +244 -0
- package/package.json +135 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { t as Button } from "./button-Bqnnid5i.js";
|
|
2
|
+
import { i as TooltipTrigger, n as TooltipArrow, r as TooltipContent, t as Tooltip } from "./tooltip-Ck0DDfF7.js";
|
|
3
|
+
import { a as fetchSnapshotHistory, i as fetchSnapshotGraph, m as useModvizBundle } from "./modviz-data-CUyTorv0.js";
|
|
4
|
+
import { a as CommandInput, i as CommandGroup, n as CommandDialog, o as CommandItem, r as CommandEmpty, s as CommandList } from "./command-SdxShIbL.js";
|
|
5
|
+
import { t as Route$1 } from "./compare-CpOqTpYu.js";
|
|
6
|
+
import { t as formatNumber } from "./formatting-iDlL4tA-.js";
|
|
7
|
+
import { t as ModvizLayout } from "./modviz-layout-BAH2ogse.js";
|
|
8
|
+
import { useEffect, useMemo, useState } from "react";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
import { ArrowLeftRight, ChevronsUpDown, FileUp, History, RotateCcw } from "lucide-react";
|
|
11
|
+
//#region shared/modviz-compare.ts
|
|
12
|
+
var sortStrings = (values) => Array.from(values).sort((left, right) => left.localeCompare(right));
|
|
13
|
+
var getEdgeSet = (graph) => new Set(graph.nodes.flatMap((node) => node.importees.map((importee) => `${node.path} -> ${importee}`)));
|
|
14
|
+
var getExternalPackageName = (nodePath, packageName) => {
|
|
15
|
+
if (!nodePath.includes("node_modules")) return packageName ?? "external";
|
|
16
|
+
if (packageName && packageName !== "node_modules") return packageName;
|
|
17
|
+
const segments = nodePath.split(/[\\/]/).filter(Boolean);
|
|
18
|
+
const nodeModulesIndex = segments.lastIndexOf("node_modules");
|
|
19
|
+
if (nodeModulesIndex === -1) return "node_modules";
|
|
20
|
+
const scopeOrName = segments[nodeModulesIndex + 1];
|
|
21
|
+
const maybeName = segments[nodeModulesIndex + 2];
|
|
22
|
+
if (!scopeOrName) return "node_modules";
|
|
23
|
+
return scopeOrName.startsWith("@") && maybeName ? `${scopeOrName}/${maybeName}` : scopeOrName;
|
|
24
|
+
};
|
|
25
|
+
var getExternalPackageSet = (graph) => new Set(graph.nodes.filter((node) => node.path.includes("node_modules")).map((node) => getExternalPackageName(node.path, node.package?.name)).filter(Boolean));
|
|
26
|
+
var getNodeMap = (graph) => new Map(graph.nodes.map((node) => [node.path, node]));
|
|
27
|
+
var setDifference = (left, right) => {
|
|
28
|
+
const values = /* @__PURE__ */ new Set();
|
|
29
|
+
for (const value of left) if (!right.has(value)) values.add(value);
|
|
30
|
+
return values;
|
|
31
|
+
};
|
|
32
|
+
var buildModvizGraphComparison = (baseline, current) => {
|
|
33
|
+
const baselineNodes = getNodeMap(baseline);
|
|
34
|
+
const currentNodes = getNodeMap(current);
|
|
35
|
+
const baselineNodePaths = new Set(baselineNodes.keys());
|
|
36
|
+
const currentNodePaths = new Set(currentNodes.keys());
|
|
37
|
+
const baselineEdges = getEdgeSet(baseline);
|
|
38
|
+
const currentEdges = getEdgeSet(current);
|
|
39
|
+
const baselineWorkspacePackages = new Set(baseline.metadata.packages.map((pkg) => pkg.name));
|
|
40
|
+
const currentWorkspacePackages = new Set(current.metadata.packages.map((pkg) => pkg.name));
|
|
41
|
+
const baselineExternalPackages = getExternalPackageSet(baseline);
|
|
42
|
+
const currentExternalPackages = getExternalPackageSet(current);
|
|
43
|
+
const changedNodes = [];
|
|
44
|
+
for (const [nodePath, baselineNode] of baselineNodes.entries()) {
|
|
45
|
+
const currentNode = currentNodes.get(nodePath);
|
|
46
|
+
if (!currentNode) continue;
|
|
47
|
+
const deltaMagnitude = Math.abs(currentNode.importedBy.length - baselineNode.importedBy.length) + Math.abs(currentNode.importees.length - baselineNode.importees.length) + Math.abs(currentNode.imports.length - baselineNode.imports.length);
|
|
48
|
+
if (deltaMagnitude === 0) continue;
|
|
49
|
+
changedNodes.push({
|
|
50
|
+
path: nodePath,
|
|
51
|
+
baselineIncoming: baselineNode.importedBy.length,
|
|
52
|
+
currentIncoming: currentNode.importedBy.length,
|
|
53
|
+
baselineOutgoing: baselineNode.importees.length,
|
|
54
|
+
currentOutgoing: currentNode.importees.length,
|
|
55
|
+
baselineImports: baselineNode.imports.length,
|
|
56
|
+
currentImports: currentNode.imports.length,
|
|
57
|
+
deltaMagnitude
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
changedNodes.sort((left, right) => right.deltaMagnitude - left.deltaMagnitude || left.path.localeCompare(right.path));
|
|
61
|
+
return {
|
|
62
|
+
baselineGeneratedAt: baseline.metadata.generatedAt ?? null,
|
|
63
|
+
currentGeneratedAt: current.metadata.generatedAt ?? null,
|
|
64
|
+
summary: {
|
|
65
|
+
baselineEdges: baselineEdges.size,
|
|
66
|
+
baselineExternalPackages: baselineExternalPackages.size,
|
|
67
|
+
baselineImportStatements: baseline.nodes.reduce((sum, node) => sum + node.imports.length, 0),
|
|
68
|
+
baselineNodes: baseline.nodes.length,
|
|
69
|
+
baselineWorkspacePackages: baselineWorkspacePackages.size,
|
|
70
|
+
currentEdges: currentEdges.size,
|
|
71
|
+
currentExternalPackages: currentExternalPackages.size,
|
|
72
|
+
currentImportStatements: current.nodes.reduce((sum, node) => sum + node.imports.length, 0),
|
|
73
|
+
currentNodes: current.nodes.length,
|
|
74
|
+
currentWorkspacePackages: currentWorkspacePackages.size
|
|
75
|
+
},
|
|
76
|
+
addedEdges: sortStrings(setDifference(currentEdges, baselineEdges)),
|
|
77
|
+
addedExternalPackages: sortStrings(setDifference(currentExternalPackages, baselineExternalPackages)),
|
|
78
|
+
addedNodes: sortStrings(setDifference(currentNodePaths, baselineNodePaths)),
|
|
79
|
+
addedWorkspacePackages: sortStrings(setDifference(currentWorkspacePackages, baselineWorkspacePackages)),
|
|
80
|
+
changedNodes,
|
|
81
|
+
removedEdges: sortStrings(setDifference(baselineEdges, currentEdges)),
|
|
82
|
+
removedExternalPackages: sortStrings(setDifference(baselineExternalPackages, currentExternalPackages)),
|
|
83
|
+
removedNodes: sortStrings(setDifference(baselineNodePaths, currentNodePaths)),
|
|
84
|
+
removedWorkspacePackages: sortStrings(setDifference(baselineWorkspacePackages, currentWorkspacePackages))
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/components/modviz/compare-view.tsx
|
|
89
|
+
var isModvizOutput = (value) => {
|
|
90
|
+
if (!value || typeof value !== "object") return false;
|
|
91
|
+
const candidate = value;
|
|
92
|
+
return Boolean(candidate.metadata && Array.isArray(candidate.nodes) && Array.isArray(candidate.imports));
|
|
93
|
+
};
|
|
94
|
+
var deltaLabel = (baseline, current) => {
|
|
95
|
+
const delta = current - baseline;
|
|
96
|
+
return `${delta > 0 ? "+" : ""}${formatNumber.format(delta)}`;
|
|
97
|
+
};
|
|
98
|
+
var parseSnapshotFile = async (file) => {
|
|
99
|
+
const raw = JSON.parse(await file.text());
|
|
100
|
+
if (!isModvizOutput(raw)) throw new Error("The selected file is not a valid modviz graph JSON snapshot.");
|
|
101
|
+
return {
|
|
102
|
+
graph: raw,
|
|
103
|
+
label: file.name
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
function SnapshotHistoryCombobox(props) {
|
|
107
|
+
const [open, setOpen] = useState(false);
|
|
108
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
109
|
+
variant: "outline",
|
|
110
|
+
className: "mt-3 w-full justify-between",
|
|
111
|
+
onClick: () => setOpen(true),
|
|
112
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
113
|
+
className: "flex items-center gap-2",
|
|
114
|
+
children: [/* @__PURE__ */ jsx(History, { className: "size-4" }), "Pick from snapshot history"]
|
|
115
|
+
}), /* @__PURE__ */ jsx(ChevronsUpDown, { className: "size-4 opacity-50" })]
|
|
116
|
+
}), /* @__PURE__ */ jsxs(CommandDialog, {
|
|
117
|
+
open,
|
|
118
|
+
onOpenChange: setOpen,
|
|
119
|
+
children: [/* @__PURE__ */ jsx(CommandInput, { placeholder: "Search snapshots…" }), /* @__PURE__ */ jsxs(CommandList, { children: [/* @__PURE__ */ jsx(CommandEmpty, { children: "No snapshots found." }), /* @__PURE__ */ jsx(CommandGroup, {
|
|
120
|
+
heading: "Saved snapshots",
|
|
121
|
+
children: props.history.map((snapshot) => /* @__PURE__ */ jsx(CommandItem, {
|
|
122
|
+
value: snapshot.id + " " + (snapshot.label ?? ""),
|
|
123
|
+
onSelect: () => {
|
|
124
|
+
props.onSelect(snapshot.id);
|
|
125
|
+
setOpen(false);
|
|
126
|
+
},
|
|
127
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
128
|
+
className: "flex min-w-0 flex-col",
|
|
129
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
130
|
+
className: "truncate font-medium",
|
|
131
|
+
children: snapshot.label || snapshot.id
|
|
132
|
+
}), snapshot.generatedAt ? /* @__PURE__ */ jsx("span", {
|
|
133
|
+
className: "truncate text-xs text-slate-500",
|
|
134
|
+
children: new Date(snapshot.generatedAt).toLocaleString()
|
|
135
|
+
}) : null]
|
|
136
|
+
})
|
|
137
|
+
}, snapshot.id))
|
|
138
|
+
})] })]
|
|
139
|
+
})] });
|
|
140
|
+
}
|
|
141
|
+
function SnapshotCard(props) {
|
|
142
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
143
|
+
className: "rounded-[24px] border border-slate-200/70 bg-white/90 p-5 shadow-[0_16px_50px_-32px_rgba(15,23,42,0.55)] dark:border-slate-800 dark:bg-slate-950/70",
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ jsxs("div", {
|
|
146
|
+
className: "flex items-start justify-between gap-4",
|
|
147
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [
|
|
148
|
+
/* @__PURE__ */ jsx("p", {
|
|
149
|
+
className: "text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400",
|
|
150
|
+
children: props.title
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ jsx("h2", {
|
|
153
|
+
className: "mt-2 text-lg font-semibold text-slate-900 dark:text-slate-100",
|
|
154
|
+
children: props.snapshot?.label ?? props.label
|
|
155
|
+
}),
|
|
156
|
+
/* @__PURE__ */ jsx("p", {
|
|
157
|
+
className: "mt-2 text-sm leading-6 text-slate-500 dark:text-slate-400",
|
|
158
|
+
children: props.description
|
|
159
|
+
})
|
|
160
|
+
] }), props.secondaryAction]
|
|
161
|
+
}),
|
|
162
|
+
/* @__PURE__ */ jsxs("label", {
|
|
163
|
+
className: "mt-4 flex cursor-pointer items-center justify-center gap-2 rounded-2xl border border-dashed border-slate-300 bg-slate-50 px-4 py-5 text-sm font-medium text-slate-600 transition hover:border-sky-300 hover:bg-sky-50 hover:text-sky-700 dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300 dark:hover:border-sky-500/60 dark:hover:bg-sky-500/10 dark:hover:text-sky-200",
|
|
164
|
+
children: [
|
|
165
|
+
/* @__PURE__ */ jsx(FileUp, { className: "size-4" }),
|
|
166
|
+
/* @__PURE__ */ jsx("span", { children: "Load JSON snapshot" }),
|
|
167
|
+
/* @__PURE__ */ jsx("input", {
|
|
168
|
+
accept: "application/json,.json",
|
|
169
|
+
className: "sr-only",
|
|
170
|
+
type: "file",
|
|
171
|
+
onChange: (event) => {
|
|
172
|
+
const file = event.currentTarget.files?.[0];
|
|
173
|
+
if (file) props.onFileChange(file);
|
|
174
|
+
event.currentTarget.value = "";
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
]
|
|
178
|
+
}),
|
|
179
|
+
props.history && props.history.length > 0 && props.onHistorySelect ? /* @__PURE__ */ jsx(SnapshotHistoryCombobox, {
|
|
180
|
+
history: props.history,
|
|
181
|
+
onSelect: props.onHistorySelect
|
|
182
|
+
}) : null,
|
|
183
|
+
props.snapshot ? /* @__PURE__ */ jsxs("div", {
|
|
184
|
+
className: "mt-4 grid gap-3 sm:grid-cols-3",
|
|
185
|
+
children: [
|
|
186
|
+
/* @__PURE__ */ jsx(MetricCard, {
|
|
187
|
+
label: "Nodes",
|
|
188
|
+
value: props.snapshot.graph.nodes.length
|
|
189
|
+
}),
|
|
190
|
+
/* @__PURE__ */ jsx(MetricCard, {
|
|
191
|
+
label: "Edges",
|
|
192
|
+
value: props.snapshot.graph.nodes.reduce((sum, node) => sum + node.importees.length, 0)
|
|
193
|
+
}),
|
|
194
|
+
/* @__PURE__ */ jsx(MetricCard, {
|
|
195
|
+
label: "Generated",
|
|
196
|
+
valueLabel: new Date(props.snapshot.graph.metadata.generatedAt).toLocaleString()
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
}) : null
|
|
200
|
+
]
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function MetricCard(props) {
|
|
204
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
205
|
+
className: "rounded-[20px] bg-slate-50/90 p-4 dark:bg-slate-900/70",
|
|
206
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
207
|
+
className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400",
|
|
208
|
+
children: props.label
|
|
209
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
210
|
+
className: "mt-2 text-xl font-semibold text-slate-900 dark:text-slate-100",
|
|
211
|
+
children: props.valueLabel ?? formatNumber.format(props.value ?? 0)
|
|
212
|
+
})]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function DeltaCard(props) {
|
|
216
|
+
const delta = props.current - props.baseline;
|
|
217
|
+
const tone = delta === 0 ? "text-slate-500 dark:text-slate-400" : delta > 0 ? "text-emerald-600 dark:text-emerald-300" : "text-rose-600 dark:text-rose-300";
|
|
218
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
219
|
+
className: "rounded-[24px] border border-slate-200/70 bg-white/90 p-5 shadow-[0_16px_50px_-32px_rgba(15,23,42,0.55)] dark:border-slate-800 dark:bg-slate-950/70",
|
|
220
|
+
children: [
|
|
221
|
+
/* @__PURE__ */ jsx("p", {
|
|
222
|
+
className: "text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400",
|
|
223
|
+
children: props.label
|
|
224
|
+
}),
|
|
225
|
+
/* @__PURE__ */ jsxs("div", {
|
|
226
|
+
className: "mt-3 flex items-end justify-between gap-4",
|
|
227
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
228
|
+
className: "text-3xl font-semibold text-slate-900 dark:text-slate-100",
|
|
229
|
+
children: formatNumber.format(props.current)
|
|
230
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
231
|
+
className: "mt-2 text-sm text-slate-500 dark:text-slate-400",
|
|
232
|
+
children: ["was ", formatNumber.format(props.baseline)]
|
|
233
|
+
})] }), /* @__PURE__ */ jsx("p", {
|
|
234
|
+
className: `text-sm font-semibold ${tone}`,
|
|
235
|
+
children: deltaLabel(props.baseline, props.current)
|
|
236
|
+
})]
|
|
237
|
+
}),
|
|
238
|
+
/* @__PURE__ */ jsx("p", {
|
|
239
|
+
className: "mt-3 text-sm leading-6 text-slate-500 dark:text-slate-400",
|
|
240
|
+
children: props.note
|
|
241
|
+
})
|
|
242
|
+
]
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function ChangeList(props) {
|
|
246
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
247
|
+
className: "rounded-[24px] border border-slate-200/70 bg-white/90 p-5 shadow-[0_16px_50px_-32px_rgba(15,23,42,0.55)] dark:border-slate-800 dark:bg-slate-950/70",
|
|
248
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
249
|
+
className: "text-base font-semibold text-slate-900 dark:text-slate-100",
|
|
250
|
+
children: props.title
|
|
251
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
252
|
+
className: "mt-4 max-h-[20rem] space-y-2 overflow-auto pr-2",
|
|
253
|
+
children: props.items.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
254
|
+
className: "text-sm text-slate-500 dark:text-slate-400",
|
|
255
|
+
children: props.emptyMessage
|
|
256
|
+
}) : props.items.slice(0, 80).map((item) => /* @__PURE__ */ jsx("div", {
|
|
257
|
+
className: "rounded-2xl bg-slate-50 px-3 py-2 font-mono text-xs text-slate-700 dark:bg-slate-900/70 dark:text-slate-200",
|
|
258
|
+
children: item
|
|
259
|
+
}, item))
|
|
260
|
+
})]
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function ChangedNodesTable(props) {
|
|
264
|
+
const headerWithTooltip = (label, description, align = "left") => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
265
|
+
asChild: true,
|
|
266
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
267
|
+
type: "button",
|
|
268
|
+
className: `inline-flex items-center gap-1 ${align === "right" ? "ml-auto" : ""}`,
|
|
269
|
+
children: label
|
|
270
|
+
})
|
|
271
|
+
}), /* @__PURE__ */ jsxs(TooltipContent, { children: [/* @__PURE__ */ jsx("p", { children: description }), /* @__PURE__ */ jsx(TooltipArrow, {})] })] });
|
|
272
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
273
|
+
className: "rounded-[24px] border border-slate-200/70 bg-white/90 p-5 shadow-[0_16px_50px_-32px_rgba(15,23,42,0.55)] dark:border-slate-800 dark:bg-slate-950/70",
|
|
274
|
+
children: [
|
|
275
|
+
/* @__PURE__ */ jsx("h3", {
|
|
276
|
+
className: "text-base font-semibold text-slate-900 dark:text-slate-100",
|
|
277
|
+
children: "Modules with the largest direct-graph deltas"
|
|
278
|
+
}),
|
|
279
|
+
/* @__PURE__ */ jsx("p", {
|
|
280
|
+
className: "mt-1 text-sm text-slate-500 dark:text-slate-400",
|
|
281
|
+
children: "Inbound counts are direct importers of a file, outbound counts are direct importees, and imports are the number of import statements declared in that file."
|
|
282
|
+
}),
|
|
283
|
+
/* @__PURE__ */ jsx("div", {
|
|
284
|
+
className: "mt-4 overflow-hidden rounded-2xl border border-slate-200/70 dark:border-slate-800",
|
|
285
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
286
|
+
className: "min-w-full divide-y divide-slate-200 dark:divide-slate-800",
|
|
287
|
+
children: [/* @__PURE__ */ jsx("thead", {
|
|
288
|
+
className: "bg-slate-50/90 dark:bg-slate-900/80",
|
|
289
|
+
children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
290
|
+
/* @__PURE__ */ jsx("th", {
|
|
291
|
+
className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400",
|
|
292
|
+
children: "Path"
|
|
293
|
+
}),
|
|
294
|
+
/* @__PURE__ */ jsx("th", {
|
|
295
|
+
className: "px-4 py-3 text-right text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400",
|
|
296
|
+
children: headerWithTooltip("Inbound", "How many direct importers point at this file or module path in each snapshot.", "right")
|
|
297
|
+
}),
|
|
298
|
+
/* @__PURE__ */ jsx("th", {
|
|
299
|
+
className: "px-4 py-3 text-right text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400",
|
|
300
|
+
children: headerWithTooltip("Outbound", "How many direct importee targets this file imports in each snapshot.", "right")
|
|
301
|
+
}),
|
|
302
|
+
/* @__PURE__ */ jsx("th", {
|
|
303
|
+
className: "px-4 py-3 text-right text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400",
|
|
304
|
+
children: headerWithTooltip("Imports", "How many import statements were parsed in that file, even if multiple statements resolve to the same outbound module.", "right")
|
|
305
|
+
})
|
|
306
|
+
] })
|
|
307
|
+
}), /* @__PURE__ */ jsx("tbody", {
|
|
308
|
+
className: "divide-y divide-slate-200 bg-white dark:divide-slate-800 dark:bg-slate-950/60",
|
|
309
|
+
children: props.rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
|
|
310
|
+
colSpan: 4,
|
|
311
|
+
className: "px-4 py-6 text-sm text-slate-500 dark:text-slate-400",
|
|
312
|
+
children: "No direct node-level count changes were detected between these snapshots."
|
|
313
|
+
}) }) : props.rows.slice(0, 24).map((row) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
314
|
+
/* @__PURE__ */ jsx("td", {
|
|
315
|
+
className: "max-w-[32rem] truncate px-4 py-3 font-mono text-xs text-slate-700 dark:text-slate-200",
|
|
316
|
+
children: row.path
|
|
317
|
+
}),
|
|
318
|
+
/* @__PURE__ */ jsxs("td", {
|
|
319
|
+
className: "px-4 py-3 text-right text-sm text-slate-700 dark:text-slate-200",
|
|
320
|
+
children: [
|
|
321
|
+
row.baselineIncoming,
|
|
322
|
+
" → ",
|
|
323
|
+
row.currentIncoming
|
|
324
|
+
]
|
|
325
|
+
}),
|
|
326
|
+
/* @__PURE__ */ jsxs("td", {
|
|
327
|
+
className: "px-4 py-3 text-right text-sm text-slate-700 dark:text-slate-200",
|
|
328
|
+
children: [
|
|
329
|
+
row.baselineOutgoing,
|
|
330
|
+
" → ",
|
|
331
|
+
row.currentOutgoing
|
|
332
|
+
]
|
|
333
|
+
}),
|
|
334
|
+
/* @__PURE__ */ jsxs("td", {
|
|
335
|
+
className: "px-4 py-3 text-right text-sm text-slate-700 dark:text-slate-200",
|
|
336
|
+
children: [
|
|
337
|
+
row.baselineImports,
|
|
338
|
+
" → ",
|
|
339
|
+
row.currentImports
|
|
340
|
+
]
|
|
341
|
+
})
|
|
342
|
+
] }, row.path))
|
|
343
|
+
})]
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
]
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function CompareView(props) {
|
|
350
|
+
const [historyOverride, setHistoryOverride] = useState(null);
|
|
351
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
352
|
+
const currentServerSnapshot = useMemo(() => props.currentGraph ? {
|
|
353
|
+
graph: props.currentGraph,
|
|
354
|
+
label: "Current served snapshot"
|
|
355
|
+
} : null, [props.currentGraph]);
|
|
356
|
+
const [baseline, setBaseline] = useState(null);
|
|
357
|
+
const [currentOverride, setCurrentOverride] = useState(null);
|
|
358
|
+
const [errorMessage, setErrorMessage] = useState(null);
|
|
359
|
+
const history = historyOverride ?? props.history;
|
|
360
|
+
const current = currentOverride ?? currentServerSnapshot;
|
|
361
|
+
const assignSnapshot = (slot, snapshot) => {
|
|
362
|
+
if (slot === "baseline") {
|
|
363
|
+
setBaseline(snapshot);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
setCurrentOverride(snapshot);
|
|
367
|
+
};
|
|
368
|
+
const refreshHistory = async () => {
|
|
369
|
+
try {
|
|
370
|
+
setHistoryLoading(true);
|
|
371
|
+
setErrorMessage(null);
|
|
372
|
+
setHistoryOverride(await fetchSnapshotHistory());
|
|
373
|
+
} catch (error) {
|
|
374
|
+
setErrorMessage(error instanceof Error ? error.message : "Failed to refresh snapshot history.");
|
|
375
|
+
} finally {
|
|
376
|
+
setHistoryLoading(false);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const comparison = useMemo(() => {
|
|
380
|
+
if (!baseline || !current) return null;
|
|
381
|
+
return buildModvizGraphComparison(baseline.graph, current.graph);
|
|
382
|
+
}, [baseline, current]);
|
|
383
|
+
const loadFileInto = async (slot, file) => {
|
|
384
|
+
try {
|
|
385
|
+
setErrorMessage(null);
|
|
386
|
+
assignSnapshot(slot, await parseSnapshotFile(file));
|
|
387
|
+
} catch (error) {
|
|
388
|
+
setErrorMessage(error instanceof Error ? error.message : "Failed to load the selected snapshot.");
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const loadHistorySnapshot = async (slot, snapshotId) => {
|
|
392
|
+
try {
|
|
393
|
+
setErrorMessage(null);
|
|
394
|
+
assignSnapshot(slot, {
|
|
395
|
+
graph: await fetchSnapshotGraph(snapshotId),
|
|
396
|
+
label: snapshotId
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
setErrorMessage(error instanceof Error ? error.message : `Failed to load snapshot ${snapshotId}.`);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
if (!props.baselineSnapshotId) return;
|
|
404
|
+
loadHistorySnapshot("baseline", props.baselineSnapshotId);
|
|
405
|
+
}, [props.baselineSnapshotId]);
|
|
406
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
407
|
+
className: "space-y-6",
|
|
408
|
+
children: [
|
|
409
|
+
/* @__PURE__ */ jsxs("section", {
|
|
410
|
+
className: "grid gap-4 xl:grid-cols-2",
|
|
411
|
+
children: [/* @__PURE__ */ jsx(SnapshotCard, {
|
|
412
|
+
description: "Load the older or baseline snapshot you want to compare against.",
|
|
413
|
+
history,
|
|
414
|
+
label: "No baseline snapshot loaded yet",
|
|
415
|
+
onFileChange: (file) => void loadFileInto("baseline", file),
|
|
416
|
+
onHistorySelect: (id) => void loadHistorySnapshot("baseline", id),
|
|
417
|
+
snapshot: baseline,
|
|
418
|
+
title: "Baseline"
|
|
419
|
+
}), /* @__PURE__ */ jsx(SnapshotCard, {
|
|
420
|
+
description: "By default this uses the graph currently served by the modviz UI, but you can replace it with another JSON file.",
|
|
421
|
+
history,
|
|
422
|
+
label: current?.label ?? "No current snapshot loaded yet",
|
|
423
|
+
onFileChange: (file) => void loadFileInto("current", file),
|
|
424
|
+
onHistorySelect: (id) => void loadHistorySnapshot("current", id),
|
|
425
|
+
secondaryAction: /* @__PURE__ */ jsxs("div", {
|
|
426
|
+
className: "flex gap-2",
|
|
427
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
428
|
+
variant: "outline",
|
|
429
|
+
size: "sm",
|
|
430
|
+
onClick: () => {
|
|
431
|
+
setCurrentOverride(null);
|
|
432
|
+
setErrorMessage(null);
|
|
433
|
+
},
|
|
434
|
+
disabled: !currentServerSnapshot,
|
|
435
|
+
children: [/* @__PURE__ */ jsx(RotateCcw, { className: "size-4" }), "Use served graph"]
|
|
436
|
+
}), /* @__PURE__ */ jsxs(Button, {
|
|
437
|
+
variant: "outline",
|
|
438
|
+
size: "sm",
|
|
439
|
+
onClick: () => {
|
|
440
|
+
if (!baseline || !current) return;
|
|
441
|
+
setBaseline(current);
|
|
442
|
+
setCurrentOverride(baseline);
|
|
443
|
+
},
|
|
444
|
+
disabled: !baseline || !current,
|
|
445
|
+
children: [/* @__PURE__ */ jsx(ArrowLeftRight, { className: "size-4" }), "Swap"]
|
|
446
|
+
})]
|
|
447
|
+
}),
|
|
448
|
+
snapshot: current,
|
|
449
|
+
title: "Current"
|
|
450
|
+
})]
|
|
451
|
+
}),
|
|
452
|
+
errorMessage ? /* @__PURE__ */ jsx("section", {
|
|
453
|
+
className: "rounded-[24px] border border-rose-200/70 bg-rose-50/80 p-4 text-sm text-rose-700 shadow-[0_16px_50px_-32px_rgba(15,23,42,0.55)] dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-200",
|
|
454
|
+
children: errorMessage
|
|
455
|
+
}) : null,
|
|
456
|
+
comparison ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
457
|
+
/* @__PURE__ */ jsxs("section", {
|
|
458
|
+
className: "grid gap-4 md:grid-cols-2 xl:grid-cols-5",
|
|
459
|
+
children: [
|
|
460
|
+
/* @__PURE__ */ jsx(DeltaCard, {
|
|
461
|
+
baseline: comparison.summary.baselineNodes,
|
|
462
|
+
current: comparison.summary.currentNodes,
|
|
463
|
+
label: "Nodes",
|
|
464
|
+
note: "Module paths present in the compared snapshots."
|
|
465
|
+
}),
|
|
466
|
+
/* @__PURE__ */ jsx(DeltaCard, {
|
|
467
|
+
baseline: comparison.summary.baselineEdges,
|
|
468
|
+
current: comparison.summary.currentEdges,
|
|
469
|
+
label: "Edges",
|
|
470
|
+
note: "Direct import relationships between modules."
|
|
471
|
+
}),
|
|
472
|
+
/* @__PURE__ */ jsx(DeltaCard, {
|
|
473
|
+
baseline: comparison.summary.baselineImportStatements,
|
|
474
|
+
current: comparison.summary.currentImportStatements,
|
|
475
|
+
label: "Import statements",
|
|
476
|
+
note: "Raw import entries captured from source analysis."
|
|
477
|
+
}),
|
|
478
|
+
/* @__PURE__ */ jsx(DeltaCard, {
|
|
479
|
+
baseline: comparison.summary.baselineExternalPackages,
|
|
480
|
+
current: comparison.summary.currentExternalPackages,
|
|
481
|
+
label: "External packages",
|
|
482
|
+
note: "Distinct node_modules package names observed in the graph."
|
|
483
|
+
}),
|
|
484
|
+
/* @__PURE__ */ jsx(DeltaCard, {
|
|
485
|
+
baseline: comparison.summary.baselineWorkspacePackages,
|
|
486
|
+
current: comparison.summary.currentWorkspacePackages,
|
|
487
|
+
label: "Workspace packages",
|
|
488
|
+
note: "Monorepo package metadata embedded in the snapshot."
|
|
489
|
+
})
|
|
490
|
+
]
|
|
491
|
+
}),
|
|
492
|
+
/* @__PURE__ */ jsx(ChangedNodesTable, { rows: comparison.changedNodes }),
|
|
493
|
+
/* @__PURE__ */ jsxs("section", {
|
|
494
|
+
className: "grid gap-4 xl:grid-cols-2",
|
|
495
|
+
children: [/* @__PURE__ */ jsx(ChangeList, {
|
|
496
|
+
title: "Added modules",
|
|
497
|
+
items: comparison.addedNodes,
|
|
498
|
+
emptyMessage: "No new module paths were added."
|
|
499
|
+
}), /* @__PURE__ */ jsx(ChangeList, {
|
|
500
|
+
title: "Removed modules",
|
|
501
|
+
items: comparison.removedNodes,
|
|
502
|
+
emptyMessage: "No module paths were removed."
|
|
503
|
+
})]
|
|
504
|
+
}),
|
|
505
|
+
/* @__PURE__ */ jsxs("section", {
|
|
506
|
+
className: "grid gap-4 xl:grid-cols-2",
|
|
507
|
+
children: [/* @__PURE__ */ jsx(ChangeList, {
|
|
508
|
+
title: "Added edges",
|
|
509
|
+
items: comparison.addedEdges,
|
|
510
|
+
emptyMessage: "No direct import edges were added."
|
|
511
|
+
}), /* @__PURE__ */ jsx(ChangeList, {
|
|
512
|
+
title: "Removed edges",
|
|
513
|
+
items: comparison.removedEdges,
|
|
514
|
+
emptyMessage: "No direct import edges were removed."
|
|
515
|
+
})]
|
|
516
|
+
}),
|
|
517
|
+
/* @__PURE__ */ jsxs("section", {
|
|
518
|
+
className: "grid gap-4 xl:grid-cols-2",
|
|
519
|
+
children: [/* @__PURE__ */ jsx(ChangeList, {
|
|
520
|
+
title: "Added packages",
|
|
521
|
+
items: [...comparison.addedWorkspacePackages.map((item) => `workspace: ${item}`), ...comparison.addedExternalPackages.map((item) => `external: ${item}`)],
|
|
522
|
+
emptyMessage: "No workspace or external packages were added."
|
|
523
|
+
}), /* @__PURE__ */ jsx(ChangeList, {
|
|
524
|
+
title: "Removed packages",
|
|
525
|
+
items: [...comparison.removedWorkspacePackages.map((item) => `workspace: ${item}`), ...comparison.removedExternalPackages.map((item) => `external: ${item}`)],
|
|
526
|
+
emptyMessage: "No workspace or external packages were removed."
|
|
527
|
+
})]
|
|
528
|
+
})
|
|
529
|
+
] }) : /* @__PURE__ */ jsx("section", {
|
|
530
|
+
className: "rounded-[24px] border border-dashed border-slate-300 bg-white/80 p-6 text-sm leading-6 text-slate-500 shadow-[0_16px_50px_-32px_rgba(15,23,42,0.55)] dark:border-slate-700 dark:bg-slate-950/60 dark:text-slate-400",
|
|
531
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
532
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
533
|
+
children: [/* @__PURE__ */ jsx("p", { children: "Load a baseline snapshot to compare it with the currently served graph or another uploaded JSON file." }), /* @__PURE__ */ jsxs(Button, {
|
|
534
|
+
variant: "outline",
|
|
535
|
+
size: "sm",
|
|
536
|
+
onClick: () => void refreshHistory(),
|
|
537
|
+
disabled: historyLoading,
|
|
538
|
+
children: [/* @__PURE__ */ jsx(RotateCcw, { className: "size-4" }), historyLoading ? "Refreshing history..." : "Refresh history"]
|
|
539
|
+
})]
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
]
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/routes/compare.tsx?tsr-split=component
|
|
547
|
+
function CompareRoute() {
|
|
548
|
+
const bundle = useModvizBundle();
|
|
549
|
+
const search = Route$1.useSearch();
|
|
550
|
+
return /* @__PURE__ */ jsx(ModvizLayout, {
|
|
551
|
+
projectTitle: bundle.projectTitle,
|
|
552
|
+
title: "Snapshot Compare",
|
|
553
|
+
description: "Compare the currently served graph with another modviz JSON snapshot to see which modules, edges, and packages changed.",
|
|
554
|
+
children: /* @__PURE__ */ jsx(CompareView, {
|
|
555
|
+
baselineSnapshotId: search.baselineSnapshot,
|
|
556
|
+
currentGraph: bundle.graph,
|
|
557
|
+
history: bundle.history
|
|
558
|
+
})
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
562
|
+
export { CompareRoute as component };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";
|
|
2
|
+
//#region src/routes/compare.tsx
|
|
3
|
+
var $$splitComponentImporter = () => import("./compare-BFMiiUsB.js");
|
|
4
|
+
var Route = createFileRoute("/compare")({
|
|
5
|
+
ssr: false,
|
|
6
|
+
validateSearch: (search) => ({ baselineSnapshot: typeof search.baselineSnapshot === "string" ? search.baselineSnapshot : "" }),
|
|
7
|
+
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
8
|
+
});
|
|
9
|
+
//#endregion
|
|
10
|
+
export { Route as t };
|