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,120 @@
|
|
|
1
|
+
const sortStrings = (values) => Array.from(values).sort((left, right) => left.localeCompare(right));
|
|
2
|
+
const getEdgeSet = (graph) => new Set(graph.nodes.flatMap((node) => node.importees.map((importee) => `${node.path} -> ${importee}`)));
|
|
3
|
+
const getExternalPackageName = (nodePath, packageName) => {
|
|
4
|
+
if (!nodePath.includes("node_modules")) {
|
|
5
|
+
return packageName ?? "external";
|
|
6
|
+
}
|
|
7
|
+
if (packageName && packageName !== "node_modules") {
|
|
8
|
+
return packageName;
|
|
9
|
+
}
|
|
10
|
+
const segments = nodePath.split(/[\\/]/).filter(Boolean);
|
|
11
|
+
const nodeModulesIndex = segments.lastIndexOf("node_modules");
|
|
12
|
+
if (nodeModulesIndex === -1) {
|
|
13
|
+
return "node_modules";
|
|
14
|
+
}
|
|
15
|
+
const scopeOrName = segments[nodeModulesIndex + 1];
|
|
16
|
+
const maybeName = segments[nodeModulesIndex + 2];
|
|
17
|
+
if (!scopeOrName) {
|
|
18
|
+
return "node_modules";
|
|
19
|
+
}
|
|
20
|
+
return scopeOrName.startsWith("@") && maybeName ? `${scopeOrName}/${maybeName}` : scopeOrName;
|
|
21
|
+
};
|
|
22
|
+
const getExternalPackageSet = (graph) => new Set(graph.nodes
|
|
23
|
+
.filter((node) => node.path.includes("node_modules"))
|
|
24
|
+
.map((node) => getExternalPackageName(node.path, node.package?.name))
|
|
25
|
+
.filter(Boolean));
|
|
26
|
+
const getNodeMap = (graph) => new Map(graph.nodes.map((node) => [node.path, node]));
|
|
27
|
+
const setDifference = (left, right) => {
|
|
28
|
+
const values = new Set();
|
|
29
|
+
for (const value of left) {
|
|
30
|
+
if (!right.has(value)) {
|
|
31
|
+
values.add(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return values;
|
|
35
|
+
};
|
|
36
|
+
export const buildModvizGraphComparison = (baseline, current) => {
|
|
37
|
+
const baselineNodes = getNodeMap(baseline);
|
|
38
|
+
const currentNodes = getNodeMap(current);
|
|
39
|
+
const baselineNodePaths = new Set(baselineNodes.keys());
|
|
40
|
+
const currentNodePaths = new Set(currentNodes.keys());
|
|
41
|
+
const baselineEdges = getEdgeSet(baseline);
|
|
42
|
+
const currentEdges = getEdgeSet(current);
|
|
43
|
+
const baselineWorkspacePackages = new Set(baseline.metadata.packages.map((pkg) => pkg.name));
|
|
44
|
+
const currentWorkspacePackages = new Set(current.metadata.packages.map((pkg) => pkg.name));
|
|
45
|
+
const baselineExternalPackages = getExternalPackageSet(baseline);
|
|
46
|
+
const currentExternalPackages = getExternalPackageSet(current);
|
|
47
|
+
const changedNodes = [];
|
|
48
|
+
for (const [nodePath, baselineNode] of baselineNodes.entries()) {
|
|
49
|
+
const currentNode = currentNodes.get(nodePath);
|
|
50
|
+
if (!currentNode) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const deltaMagnitude = Math.abs(currentNode.importedBy.length - baselineNode.importedBy.length) +
|
|
54
|
+
Math.abs(currentNode.importees.length - baselineNode.importees.length) +
|
|
55
|
+
Math.abs(currentNode.imports.length - baselineNode.imports.length);
|
|
56
|
+
if (deltaMagnitude === 0) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
changedNodes.push({
|
|
60
|
+
path: nodePath,
|
|
61
|
+
baselineIncoming: baselineNode.importedBy.length,
|
|
62
|
+
currentIncoming: currentNode.importedBy.length,
|
|
63
|
+
baselineOutgoing: baselineNode.importees.length,
|
|
64
|
+
currentOutgoing: currentNode.importees.length,
|
|
65
|
+
baselineImports: baselineNode.imports.length,
|
|
66
|
+
currentImports: currentNode.imports.length,
|
|
67
|
+
deltaMagnitude,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
changedNodes.sort((left, right) => right.deltaMagnitude - left.deltaMagnitude || left.path.localeCompare(right.path));
|
|
71
|
+
return {
|
|
72
|
+
baselineGeneratedAt: baseline.metadata.generatedAt ?? null,
|
|
73
|
+
currentGeneratedAt: current.metadata.generatedAt ?? null,
|
|
74
|
+
summary: {
|
|
75
|
+
baselineEdges: baselineEdges.size,
|
|
76
|
+
baselineExternalPackages: baselineExternalPackages.size,
|
|
77
|
+
baselineImportStatements: baseline.nodes.reduce((sum, node) => sum + node.imports.length, 0),
|
|
78
|
+
baselineNodes: baseline.nodes.length,
|
|
79
|
+
baselineWorkspacePackages: baselineWorkspacePackages.size,
|
|
80
|
+
currentEdges: currentEdges.size,
|
|
81
|
+
currentExternalPackages: currentExternalPackages.size,
|
|
82
|
+
currentImportStatements: current.nodes.reduce((sum, node) => sum + node.imports.length, 0),
|
|
83
|
+
currentNodes: current.nodes.length,
|
|
84
|
+
currentWorkspacePackages: currentWorkspacePackages.size,
|
|
85
|
+
},
|
|
86
|
+
addedEdges: sortStrings(setDifference(currentEdges, baselineEdges)),
|
|
87
|
+
addedExternalPackages: sortStrings(setDifference(currentExternalPackages, baselineExternalPackages)),
|
|
88
|
+
addedNodes: sortStrings(setDifference(currentNodePaths, baselineNodePaths)),
|
|
89
|
+
addedWorkspacePackages: sortStrings(setDifference(currentWorkspacePackages, baselineWorkspacePackages)),
|
|
90
|
+
changedNodes,
|
|
91
|
+
removedEdges: sortStrings(setDifference(baselineEdges, currentEdges)),
|
|
92
|
+
removedExternalPackages: sortStrings(setDifference(baselineExternalPackages, currentExternalPackages)),
|
|
93
|
+
removedNodes: sortStrings(setDifference(baselineNodePaths, currentNodePaths)),
|
|
94
|
+
removedWorkspacePackages: sortStrings(setDifference(baselineWorkspacePackages, currentWorkspacePackages)),
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
export const renderModvizGraphComparison = (comparison, options = {}) => {
|
|
98
|
+
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.trunc(options.limit ?? 10)) : 10;
|
|
99
|
+
const changedNodes = comparison.changedNodes.slice(0, limit);
|
|
100
|
+
return [
|
|
101
|
+
"",
|
|
102
|
+
"Graph diff",
|
|
103
|
+
`- Nodes: ${comparison.summary.baselineNodes} -> ${comparison.summary.currentNodes} (${comparison.summary.currentNodes - comparison.summary.baselineNodes >= 0 ? "+" : ""}${comparison.summary.currentNodes - comparison.summary.baselineNodes})`,
|
|
104
|
+
`- Edges: ${comparison.summary.baselineEdges} -> ${comparison.summary.currentEdges} (${comparison.summary.currentEdges - comparison.summary.baselineEdges >= 0 ? "+" : ""}${comparison.summary.currentEdges - comparison.summary.baselineEdges})`,
|
|
105
|
+
`- Import statements: ${comparison.summary.baselineImportStatements} -> ${comparison.summary.currentImportStatements} (${comparison.summary.currentImportStatements - comparison.summary.baselineImportStatements >= 0 ? "+" : ""}${comparison.summary.currentImportStatements - comparison.summary.baselineImportStatements})`,
|
|
106
|
+
`- Workspace packages: ${comparison.summary.baselineWorkspacePackages} -> ${comparison.summary.currentWorkspacePackages}`,
|
|
107
|
+
`- External packages: ${comparison.summary.baselineExternalPackages} -> ${comparison.summary.currentExternalPackages}`,
|
|
108
|
+
`- Added modules: ${comparison.addedNodes.length}`,
|
|
109
|
+
`- Removed modules: ${comparison.removedNodes.length}`,
|
|
110
|
+
`- Added edges: ${comparison.addedEdges.length}`,
|
|
111
|
+
`- Removed edges: ${comparison.removedEdges.length}`,
|
|
112
|
+
changedNodes.length > 0
|
|
113
|
+
? [
|
|
114
|
+
"",
|
|
115
|
+
"Largest direct node deltas",
|
|
116
|
+
...changedNodes.map((node) => `- ${node.path}: inbound ${node.baselineIncoming}->${node.currentIncoming}, outbound ${node.baselineOutgoing}->${node.currentOutgoing}, imports ${node.baselineImports}->${node.currentImports}`),
|
|
117
|
+
].join("\n")
|
|
118
|
+
: "",
|
|
119
|
+
].filter(Boolean).join("\n");
|
|
120
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
const chainCache = new WeakMap();
|
|
2
|
+
const packageReportCache = new WeakMap();
|
|
3
|
+
const nodeReportCache = new WeakMap();
|
|
4
|
+
const normalizeForSearch = (value) => value.trim().toLowerCase();
|
|
5
|
+
const isExternalPath = (value) => value.includes("node_modules");
|
|
6
|
+
const uniqueSorted = (values) => Array.from(new Set(values))
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.sort((left, right) => left.localeCompare(right));
|
|
9
|
+
export const getTracePackageName = (node) => {
|
|
10
|
+
if (!node.path.includes("node_modules")) {
|
|
11
|
+
return node.package?.name ?? null;
|
|
12
|
+
}
|
|
13
|
+
if (node.package?.name && node.package.name !== "node_modules") {
|
|
14
|
+
return node.package.name;
|
|
15
|
+
}
|
|
16
|
+
const segments = node.path.split(/[\\/]/).filter(Boolean);
|
|
17
|
+
const nodeModulesIndex = segments.lastIndexOf("node_modules");
|
|
18
|
+
if (nodeModulesIndex === -1) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const scopeOrName = segments[nodeModulesIndex + 1];
|
|
22
|
+
const maybeName = segments[nodeModulesIndex + 2];
|
|
23
|
+
if (!scopeOrName) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return scopeOrName.startsWith("@") && maybeName ? `${scopeOrName}/${maybeName}` : scopeOrName;
|
|
27
|
+
};
|
|
28
|
+
const uniqueChains = (chains) => {
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const nextChains = [];
|
|
31
|
+
for (const chain of chains) {
|
|
32
|
+
const key = chain.join(" -> ");
|
|
33
|
+
if (seen.has(key)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
seen.add(key);
|
|
37
|
+
nextChains.push(chain);
|
|
38
|
+
}
|
|
39
|
+
return nextChains.sort((left, right) => {
|
|
40
|
+
const byLength = left.length - right.length;
|
|
41
|
+
return byLength !== 0 ? byLength : left.join("/").localeCompare(right.join("/"));
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const buildUpstreamChains = (graph, targetPath, maxChains = 40, maxDepth = 28) => {
|
|
45
|
+
const cacheKey = `${targetPath}::${maxChains}::${maxDepth}`;
|
|
46
|
+
const cachedGraphChains = chainCache.get(graph);
|
|
47
|
+
if (cachedGraphChains?.has(cacheKey)) {
|
|
48
|
+
return cachedGraphChains.get(cacheKey);
|
|
49
|
+
}
|
|
50
|
+
const nodeByPath = new Map(graph.nodes.map((node) => [node.path, node]));
|
|
51
|
+
const entrypoints = new Set(graph.metadata.entrypoints);
|
|
52
|
+
const results = [];
|
|
53
|
+
const queue = [[targetPath]];
|
|
54
|
+
let queueIndex = 0;
|
|
55
|
+
while (queueIndex < queue.length && results.length < maxChains) {
|
|
56
|
+
const reverseChain = queue[queueIndex];
|
|
57
|
+
queueIndex += 1;
|
|
58
|
+
if (!reverseChain) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const currentPath = reverseChain[0];
|
|
62
|
+
const currentNode = nodeByPath.get(currentPath);
|
|
63
|
+
const importers = uniqueSorted((currentNode?.importedBy ?? []).filter((importerPath) => !reverseChain.includes(importerPath) && nodeByPath.has(importerPath)));
|
|
64
|
+
if (!currentNode ||
|
|
65
|
+
importers.length === 0 ||
|
|
66
|
+
entrypoints.has(currentPath) ||
|
|
67
|
+
reverseChain.length >= maxDepth) {
|
|
68
|
+
results.push([...reverseChain].reverse());
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
importers
|
|
72
|
+
.sort((left, right) => {
|
|
73
|
+
const leftExternal = isExternalPath(left);
|
|
74
|
+
const rightExternal = isExternalPath(right);
|
|
75
|
+
if (leftExternal !== rightExternal) {
|
|
76
|
+
return leftExternal ? 1 : -1;
|
|
77
|
+
}
|
|
78
|
+
return left.localeCompare(right);
|
|
79
|
+
})
|
|
80
|
+
.forEach((importerPath) => {
|
|
81
|
+
queue.push([importerPath, ...reverseChain]);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const resolvedChains = uniqueChains(results);
|
|
85
|
+
const nextCache = cachedGraphChains ?? new Map();
|
|
86
|
+
nextCache.set(cacheKey, resolvedChains);
|
|
87
|
+
if (!cachedGraphChains) {
|
|
88
|
+
chainCache.set(graph, nextCache);
|
|
89
|
+
}
|
|
90
|
+
return resolvedChains;
|
|
91
|
+
};
|
|
92
|
+
const getWorkspaceOrigin = (chain) => chain.find((segment) => !isExternalPath(segment)) ?? chain[0] ?? null;
|
|
93
|
+
const getIntroducedThrough = (chain) => {
|
|
94
|
+
for (let index = 0; index < chain.length; index += 1) {
|
|
95
|
+
if (!isExternalPath(chain[index])) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
return chain[index - 1] ?? chain[index] ?? null;
|
|
99
|
+
}
|
|
100
|
+
return chain.at(-1) ?? null;
|
|
101
|
+
};
|
|
102
|
+
const createTraceMatch = (node, chains) => ({
|
|
103
|
+
path: node.path,
|
|
104
|
+
label: node.name,
|
|
105
|
+
packageName: getTracePackageName(node),
|
|
106
|
+
type: node.type,
|
|
107
|
+
targetPaths: [node.path],
|
|
108
|
+
directImporters: uniqueSorted(node.importedBy),
|
|
109
|
+
chains,
|
|
110
|
+
workspaceOrigins: uniqueSorted(chains.map((chain) => getWorkspaceOrigin(chain)).filter(Boolean)),
|
|
111
|
+
introducedThrough: uniqueSorted(chains.map((chain) => getIntroducedThrough(chain)).filter(Boolean)),
|
|
112
|
+
});
|
|
113
|
+
export const buildPackageTraceReport = (graph, packageQuery, options = {}) => {
|
|
114
|
+
const { maxChainsPerTarget = 40, maxDepth = 28, maxNodesPerPackage = Number.POSITIVE_INFINITY, } = options;
|
|
115
|
+
const cacheSuffix = `::${maxChainsPerTarget}::${maxDepth}::${maxNodesPerPackage}`;
|
|
116
|
+
const normalizedQuery = normalizeForSearch(packageQuery);
|
|
117
|
+
const cacheKey = `${normalizedQuery}${cacheSuffix}`;
|
|
118
|
+
const cachedReports = packageReportCache.get(graph);
|
|
119
|
+
if (cachedReports?.has(cacheKey)) {
|
|
120
|
+
return cachedReports.get(cacheKey);
|
|
121
|
+
}
|
|
122
|
+
const groupedMatches = new Map();
|
|
123
|
+
const tracedNodeCountByPackage = new Map();
|
|
124
|
+
graph.nodes
|
|
125
|
+
.filter((node) => node.path.includes("node_modules"))
|
|
126
|
+
.filter((node) => {
|
|
127
|
+
const packageName = getTracePackageName(node);
|
|
128
|
+
return packageName ? normalizeForSearch(packageName).includes(normalizedQuery) : false;
|
|
129
|
+
})
|
|
130
|
+
.forEach((node) => {
|
|
131
|
+
const packageName = getTracePackageName(node) ?? node.path;
|
|
132
|
+
const tracedCount = tracedNodeCountByPackage.get(packageName) ?? 0;
|
|
133
|
+
const canTraceNode = tracedCount < maxNodesPerPackage;
|
|
134
|
+
const chains = canTraceNode
|
|
135
|
+
? buildUpstreamChains(graph, node.path, maxChainsPerTarget, maxDepth)
|
|
136
|
+
: [];
|
|
137
|
+
if (canTraceNode) {
|
|
138
|
+
tracedNodeCountByPackage.set(packageName, tracedCount + 1);
|
|
139
|
+
}
|
|
140
|
+
const existing = groupedMatches.get(packageName);
|
|
141
|
+
if (!existing) {
|
|
142
|
+
groupedMatches.set(packageName, {
|
|
143
|
+
path: packageName,
|
|
144
|
+
label: packageName,
|
|
145
|
+
packageName,
|
|
146
|
+
type: "package",
|
|
147
|
+
targetPaths: [node.path],
|
|
148
|
+
directImporters: uniqueSorted(node.importedBy),
|
|
149
|
+
chains,
|
|
150
|
+
workspaceOrigins: uniqueSorted(chains.map((chain) => getWorkspaceOrigin(chain)).filter(Boolean)),
|
|
151
|
+
introducedThrough: uniqueSorted(chains.map((chain) => getIntroducedThrough(chain)).filter(Boolean)),
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
existing.targetPaths = uniqueSorted([...existing.targetPaths, node.path]);
|
|
156
|
+
existing.directImporters = uniqueSorted([...existing.directImporters, ...node.importedBy]);
|
|
157
|
+
existing.chains = uniqueChains([...existing.chains, ...chains]);
|
|
158
|
+
existing.workspaceOrigins = uniqueSorted([
|
|
159
|
+
...existing.workspaceOrigins,
|
|
160
|
+
...chains.map((chain) => getWorkspaceOrigin(chain)).filter(Boolean),
|
|
161
|
+
]);
|
|
162
|
+
existing.introducedThrough = uniqueSorted([
|
|
163
|
+
...existing.introducedThrough,
|
|
164
|
+
...chains.map((chain) => getIntroducedThrough(chain)).filter(Boolean),
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
const matches = Array.from(groupedMatches.values()).sort((left, right) => left.label.localeCompare(right.label));
|
|
168
|
+
const report = {
|
|
169
|
+
kind: "package",
|
|
170
|
+
query: packageQuery,
|
|
171
|
+
matchedLabels: Array.from(new Set(matches.map((match) => match.packageName).filter(Boolean))).sort((left, right) => left.localeCompare(right)),
|
|
172
|
+
matches,
|
|
173
|
+
totalChains: matches.reduce((sum, match) => sum + match.chains.length, 0),
|
|
174
|
+
};
|
|
175
|
+
const nextCache = cachedReports ?? new Map();
|
|
176
|
+
nextCache.set(cacheKey, report);
|
|
177
|
+
if (!cachedReports) {
|
|
178
|
+
packageReportCache.set(graph, nextCache);
|
|
179
|
+
}
|
|
180
|
+
return report;
|
|
181
|
+
};
|
|
182
|
+
export const buildNodeTraceReport = (graph, nodeQuery, options = {}) => {
|
|
183
|
+
const { maxChainsPerTarget = 40, maxDepth = 28, maxNodeMatches = Number.POSITIVE_INFINITY, } = options;
|
|
184
|
+
const cacheSuffix = `::${maxChainsPerTarget}::${maxDepth}::${maxNodeMatches}`;
|
|
185
|
+
const normalizedQuery = normalizeForSearch(nodeQuery);
|
|
186
|
+
const cacheKey = `${normalizedQuery}${cacheSuffix}`;
|
|
187
|
+
const cachedReports = nodeReportCache.get(graph);
|
|
188
|
+
if (cachedReports?.has(cacheKey)) {
|
|
189
|
+
return cachedReports.get(cacheKey);
|
|
190
|
+
}
|
|
191
|
+
const matches = graph.nodes
|
|
192
|
+
.filter((node) => {
|
|
193
|
+
return [node.path, node.name, node.package?.name, node.cluster]
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.some((value) => normalizeForSearch(String(value)).includes(normalizedQuery));
|
|
196
|
+
})
|
|
197
|
+
.slice(0, maxNodeMatches)
|
|
198
|
+
.map((node) => createTraceMatch(node, buildUpstreamChains(graph, node.path, maxChainsPerTarget, maxDepth)));
|
|
199
|
+
const report = {
|
|
200
|
+
kind: "node",
|
|
201
|
+
query: nodeQuery,
|
|
202
|
+
matchedLabels: matches.map((match) => match.path),
|
|
203
|
+
matches,
|
|
204
|
+
totalChains: matches.reduce((sum, match) => sum + match.chains.length, 0),
|
|
205
|
+
};
|
|
206
|
+
const nextCache = cachedReports ?? new Map();
|
|
207
|
+
nextCache.set(cacheKey, report);
|
|
208
|
+
if (!cachedReports) {
|
|
209
|
+
nodeReportCache.set(graph, nextCache);
|
|
210
|
+
}
|
|
211
|
+
return report;
|
|
212
|
+
};
|
|
213
|
+
export const renderTraceReport = (report, limit = 10) => {
|
|
214
|
+
const lines = [
|
|
215
|
+
`${report.kind === "package" ? "Package" : "Node"} trace for: ${report.query}`,
|
|
216
|
+
`Matched ${report.matches.length} node(s) across ${report.totalChains} origin chain(s).`,
|
|
217
|
+
"",
|
|
218
|
+
];
|
|
219
|
+
if (report.matches.length === 0) {
|
|
220
|
+
lines.push("No matching nodes were found.");
|
|
221
|
+
return `${lines.join("\n")}\n`;
|
|
222
|
+
}
|
|
223
|
+
for (const match of report.matches.slice(0, limit)) {
|
|
224
|
+
lines.push(`- ${match.path} (${match.type})`);
|
|
225
|
+
if (match.packageName) {
|
|
226
|
+
lines.push(` Package: ${match.packageName}`);
|
|
227
|
+
}
|
|
228
|
+
lines.push(` Matching files: ${match.targetPaths.length}`);
|
|
229
|
+
lines.push(` Workspace origins: ${match.workspaceOrigins.join(", ") || "none"}`);
|
|
230
|
+
lines.push(` Introduced through: ${match.introducedThrough.join(", ") || "none"}`);
|
|
231
|
+
lines.push(` Direct importers: ${match.directImporters.join(", ") || "none"}`);
|
|
232
|
+
for (const chain of match.chains.slice(0, limit)) {
|
|
233
|
+
lines.push(` Chain: ${chain.join(" -> ")}`);
|
|
234
|
+
}
|
|
235
|
+
if (match.chains.length > limit) {
|
|
236
|
+
lines.push(` +${match.chains.length - limit} more chain(s)`);
|
|
237
|
+
}
|
|
238
|
+
lines.push("");
|
|
239
|
+
}
|
|
240
|
+
if (report.matches.length > limit) {
|
|
241
|
+
lines.push(`+${report.matches.length - limit} more matching node(s)`);
|
|
242
|
+
}
|
|
243
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
244
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "modviz",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.2",
|
|
5
|
+
"description": "CLI and web UI for analyzing and visualizing TypeScript and JavaScript module dependency graphs.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"module",
|
|
8
|
+
"graph",
|
|
9
|
+
"cli",
|
|
10
|
+
"transitive",
|
|
11
|
+
"import",
|
|
12
|
+
"export",
|
|
13
|
+
"dependency-graph",
|
|
14
|
+
"import-analysis",
|
|
15
|
+
"javascript",
|
|
16
|
+
"module-graph",
|
|
17
|
+
"typescript",
|
|
18
|
+
"visualization"
|
|
19
|
+
],
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"author": "Alexandre Stahmer",
|
|
22
|
+
"homepage": "https://github.com/astahmer/modviz#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/astahmer/modviz/issues"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/astahmer/modviz.git"
|
|
29
|
+
},
|
|
30
|
+
"packageManager": "pnpm@10.33.0",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.12.0",
|
|
33
|
+
"pnpm": ">=10.0.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"main": "./dist/mod/index.js",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": "./dist/mod/index.js",
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"bin": {
|
|
44
|
+
"modviz": "./dist/mod/cli.js"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"README.md",
|
|
48
|
+
"dist"
|
|
49
|
+
],
|
|
50
|
+
"sideEffects": false,
|
|
51
|
+
"scripts": {
|
|
52
|
+
"cli": "node mod/cli.ts",
|
|
53
|
+
"cli:build": "tsgo --ignoreConfig --skipLibCheck mod/cli.ts mod/index.ts --types node --outDir dist --target es2022 --module nodenext --moduleResolution nodenext --allowImportingTsExtensions --rewriteRelativeImportExtensions",
|
|
54
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
55
|
+
"fmt": "oxfmt --write .",
|
|
56
|
+
"fmt:check": "oxfmt --check .",
|
|
57
|
+
"lint": "oxlint --import-plugin --react-plugin --vitest-plugin --jsx-a11y-plugin --node-plugin .",
|
|
58
|
+
"lint:fix": "pnpm run lint -- --fix",
|
|
59
|
+
"dev": "vite dev",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"typecheck": "tsgo -p tsconfig.json --noEmit",
|
|
62
|
+
"typecheck:tsgo": "tsgo -p tsconfig.json --noEmit",
|
|
63
|
+
"check": "pnpm run lint && pnpm run typecheck",
|
|
64
|
+
"build": "pnpm run clean && pnpm vite build && pnpm run cli:build && pnpm run typecheck",
|
|
65
|
+
"prepublishOnly": "pnpm run check && pnpm run test",
|
|
66
|
+
"prepack": "pnpm run build",
|
|
67
|
+
"release": "release-it",
|
|
68
|
+
"release:dry-run": "release-it --dry-run --no-npm.publish --git.requireCleanWorkingDir=false --git.requireUpstream=false --git.push=false",
|
|
69
|
+
"start": "node dist/mod/runtime-host.js"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@ai-sdk/openai": "3.0.52",
|
|
73
|
+
"@ark-ui/react": "5.36.0",
|
|
74
|
+
"@astahmer/module-graph": "0.14.2",
|
|
75
|
+
"@chenglou/pretext": "0.0.5",
|
|
76
|
+
"@radix-ui/react-dialog": "1.1.15",
|
|
77
|
+
"@radix-ui/react-popover": "1.1.15",
|
|
78
|
+
"@radix-ui/react-slot": "1.2.4",
|
|
79
|
+
"@react-sigma/core": "5.0.6",
|
|
80
|
+
"@react-sigma/graph-search": "5.0.6",
|
|
81
|
+
"@react-sigma/layout-circlepack": "5.0.6",
|
|
82
|
+
"@react-sigma/layout-circular": "5.0.6",
|
|
83
|
+
"@react-sigma/layout-core": "5.0.6",
|
|
84
|
+
"@react-sigma/layout-force": "5.0.6",
|
|
85
|
+
"@react-sigma/layout-forceatlas2": "5.0.6",
|
|
86
|
+
"@react-sigma/layout-noverlap": "5.0.6",
|
|
87
|
+
"@react-sigma/layout-random": "5.0.6",
|
|
88
|
+
"@react-sigma/minimap": "5.0.6",
|
|
89
|
+
"@sigma/utils": "3.0.0",
|
|
90
|
+
"@tanstack/react-router": "1.168.19",
|
|
91
|
+
"@tanstack/react-router-devtools": "1.166.13",
|
|
92
|
+
"@tanstack/react-start": "1.167.35",
|
|
93
|
+
"@tanstack/react-virtual": "3.13.23",
|
|
94
|
+
"@xstate/store": "3.17.1",
|
|
95
|
+
"ai": "6.0.159",
|
|
96
|
+
"class-variance-authority": "0.7.1",
|
|
97
|
+
"clsx": "2.1.1",
|
|
98
|
+
"cmdk": "1.1.1",
|
|
99
|
+
"find-workspaces": "0.3.1",
|
|
100
|
+
"graphology": "0.26.0",
|
|
101
|
+
"graphology-communities-louvain": "2.0.2",
|
|
102
|
+
"graphology-types": "0.24.8",
|
|
103
|
+
"iwanthue": "2.0.0",
|
|
104
|
+
"leva": "0.10.1",
|
|
105
|
+
"lucide-react": "1.8.0",
|
|
106
|
+
"magic-string": "0.30.21",
|
|
107
|
+
"nanovis": "1.0.0",
|
|
108
|
+
"react": "19.2.5",
|
|
109
|
+
"react-dom": "19.2.5",
|
|
110
|
+
"react-icons": "5.6.0",
|
|
111
|
+
"resolve-pkg-maps": "1.0.0",
|
|
112
|
+
"sigma": "3.0.2",
|
|
113
|
+
"tailwind-merge": "3.5.0",
|
|
114
|
+
"vite": "8.0.8",
|
|
115
|
+
"zod": "4.3.6"
|
|
116
|
+
},
|
|
117
|
+
"devDependencies": {
|
|
118
|
+
"@tailwindcss/vite": "4.2.2",
|
|
119
|
+
"@types/node": "25.6.0",
|
|
120
|
+
"@types/react": "19.2.14",
|
|
121
|
+
"@types/react-dom": "19.2.3",
|
|
122
|
+
"@typescript/native-preview": "7.0.0-dev.20260413.1",
|
|
123
|
+
"@vitejs/plugin-react": "6.0.1",
|
|
124
|
+
"autoprefixer": "10.4.27",
|
|
125
|
+
"oxfmt": "0.45.0",
|
|
126
|
+
"oxlint": "1.60.0",
|
|
127
|
+
"postcss": "8.5.9",
|
|
128
|
+
"release-it": "19.2.4",
|
|
129
|
+
"shadcn": "4.2.0",
|
|
130
|
+
"tailwindcss": "4.2.2",
|
|
131
|
+
"tw-animate-css": "1.4.0",
|
|
132
|
+
"typescript": "6.0.2",
|
|
133
|
+
"vitest": "4.1.4"
|
|
134
|
+
}
|
|
135
|
+
}
|