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
package/dist/mod/cli.js
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Worker } from "node:worker_threads";
|
|
4
|
+
import { buildCliHelpText, buildSnapshotList, buildCliSummary, parseCliArgs, validateCliArgs, } from "./cli-options.js";
|
|
5
|
+
import { generateAiAnalysis } from "./llm-analysis.js";
|
|
6
|
+
import { listSnapshotHistory, loadSnapshotGraph, saveSnapshotToHistory, } from "./snapshot-history.js";
|
|
7
|
+
import { buildModvizLlmOutput, inferLlmOutputPaths, resolveModvizFocus, renderModvizLlmDrilldown, renderModvizLlmMarkdown, } from "./llm-output.js";
|
|
8
|
+
import { buildNodeTraceReport, buildPackageTraceReport, renderTraceReport, } from "../shared/modviz-trace.js";
|
|
9
|
+
import { buildModvizGraphComparison, renderModvizGraphComparison, } from "../shared/modviz-compare.js";
|
|
10
|
+
import { createModuleGraph } from "@astahmer/module-graph";
|
|
11
|
+
import { barrelFile } from "@astahmer/module-graph/plugins/barrel-file.js";
|
|
12
|
+
import { exports } from "@astahmer/module-graph/plugins/exports.js";
|
|
13
|
+
import { imports } from "@astahmer/module-graph/plugins/imports.js";
|
|
14
|
+
import { unusedExports, } from "@astahmer/module-graph/plugins/unused-exports.js";
|
|
15
|
+
import { findWorkspaces } from "find-workspaces";
|
|
16
|
+
import { sanitizeFileImportSuffixPlugin } from "./module-graph-plugins.js";
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const parsedArgs = parseCliArgs(args);
|
|
19
|
+
const { command, entryFile, flags, serveDataFile } = parsedArgs;
|
|
20
|
+
const validationError = validateCliArgs(parsedArgs);
|
|
21
|
+
if (validationError) {
|
|
22
|
+
console.error(`Error: ${validationError}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (flags.historyDir) {
|
|
26
|
+
process.env.MODVIZ_HISTORY_DIR = path.resolve(flags.historyDir);
|
|
27
|
+
}
|
|
28
|
+
if (flags.help || (command === "analyze" && !entryFile && !flags.serve)) {
|
|
29
|
+
console.log(buildCliHelpText());
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
if (command === "serve" || flags.serve) {
|
|
33
|
+
await launchWebUI(flags.port, serveDataFile ?? flags.graphFile, flags.open);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
if (command === "report") {
|
|
37
|
+
const reportGraph = loadGraphForReport(flags);
|
|
38
|
+
if (flags.listSnapshots) {
|
|
39
|
+
console.log(buildSnapshotList(listSnapshotHistory()));
|
|
40
|
+
}
|
|
41
|
+
if (flags.summary) {
|
|
42
|
+
console.log(buildCliSummary(reportGraph));
|
|
43
|
+
}
|
|
44
|
+
if (flags.packageQuery) {
|
|
45
|
+
console.log(renderTraceReport(buildPackageTraceReport(reportGraph, flags.packageQuery), flags.limit));
|
|
46
|
+
}
|
|
47
|
+
if (flags.nodeQuery) {
|
|
48
|
+
console.log(renderTraceReport(buildNodeTraceReport(reportGraph, flags.nodeQuery), flags.limit));
|
|
49
|
+
}
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
if (command === "diff") {
|
|
53
|
+
const [baselineTarget, currentTarget] = parsedArgs.positionals ?? [];
|
|
54
|
+
if (!baselineTarget || !currentTarget) {
|
|
55
|
+
console.error("Error: Diff command requires <baseline> and <current> graph targets");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const baselineGraph = loadGraphTarget(baselineTarget);
|
|
59
|
+
const currentGraph = loadGraphTarget(currentTarget);
|
|
60
|
+
const comparison = buildModvizGraphComparison(baselineGraph, currentGraph);
|
|
61
|
+
console.log(renderModvizGraphComparison(comparison, { limit: flags.limit }));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
if (!entryFile) {
|
|
65
|
+
console.error("Error: Entry file is required when not using --serve");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const entryFileAbsolute = path.resolve(entryFile);
|
|
69
|
+
if (!existsSync(entryFileAbsolute)) {
|
|
70
|
+
console.error(`Error: Entry file "${entryFile}" does not exist`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
console.log(`🔍 Analyzing dependency graph for: ${entryFileAbsolute}`);
|
|
74
|
+
const workspaces = findWorkspaces(entryFileAbsolute) ?? [];
|
|
75
|
+
const basePath = deriveAnalysisBasePath(entryFileAbsolute, workspaces);
|
|
76
|
+
const entryFileForGraph = normalizeRelativePath(path.relative(basePath, entryFileAbsolute));
|
|
77
|
+
const workspaceList = (workspaces ?? []).map((workspace) => ({
|
|
78
|
+
relativePath: path.relative(basePath, workspace.location),
|
|
79
|
+
absolutePath: workspace.location,
|
|
80
|
+
name: workspace.package.name,
|
|
81
|
+
imports: workspace.package.imports,
|
|
82
|
+
}));
|
|
83
|
+
const clusterizePlugin = {
|
|
84
|
+
name: "cluster-plugin",
|
|
85
|
+
analyze(module) {
|
|
86
|
+
const parts = module.path.split("/");
|
|
87
|
+
const srcIndex = parts.indexOf("src");
|
|
88
|
+
if (srcIndex !== -1 && parts.length > srcIndex + 1) {
|
|
89
|
+
const cluster = parts[srcIndex + 1];
|
|
90
|
+
if (path.extname(cluster) === "") {
|
|
91
|
+
module.cluster = cluster;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const moduleGraph = await withProgress("Analyzing dependency graph", () => createModuleGraph(entryFileForGraph, {
|
|
97
|
+
basePath,
|
|
98
|
+
// TODO configurable flag to allow this
|
|
99
|
+
exclude: flags.nodeModules ? undefined : [(importee) => importee.includes("node_modules")],
|
|
100
|
+
ignoreDynamicImport: flags.ignoreDynamic,
|
|
101
|
+
includeTypeOnlyImports: !flags.ignoreTypeOnly,
|
|
102
|
+
plugins: [
|
|
103
|
+
sanitizeFileImportSuffixPlugin,
|
|
104
|
+
imports,
|
|
105
|
+
exports,
|
|
106
|
+
unusedExports,
|
|
107
|
+
barrelFile({
|
|
108
|
+
amountOfExportsToConsiderModuleAsBarrel: flags.barrelThreshold,
|
|
109
|
+
}),
|
|
110
|
+
clusterizePlugin,
|
|
111
|
+
],
|
|
112
|
+
}));
|
|
113
|
+
const packages = workspaceList.map((workspace) => ({
|
|
114
|
+
name: workspace.name,
|
|
115
|
+
path: normalizeRelativePath(workspace.relativePath),
|
|
116
|
+
}));
|
|
117
|
+
const webGraphData = await withProgress("Preparing graph payload", () => processModuleGraphForWeb(moduleGraph, entryFileForGraph, packages, basePath, Number.isFinite(flags.limit) ? flags.limit : 5));
|
|
118
|
+
const pathFilteredGraph = applyPathFilters(webGraphData, flags.include, flags.exclude);
|
|
119
|
+
const focusOptions = {
|
|
120
|
+
packageName: flags.packageQuery,
|
|
121
|
+
nodeQuery: flags.nodeQuery,
|
|
122
|
+
limit: Number.isFinite(flags.limit) ? flags.limit : 20,
|
|
123
|
+
};
|
|
124
|
+
const shouldResolveFocus = Boolean(focusOptions.packageName || focusOptions.nodeQuery);
|
|
125
|
+
const shouldBuildLlmReport = flags.llm || flags.llmAnalyze || Boolean(flags.packageQuery) || Boolean(flags.nodeQuery);
|
|
126
|
+
const fullLlmOutput = shouldResolveFocus || shouldBuildLlmReport ? buildModvizLlmOutput(pathFilteredGraph) : undefined;
|
|
127
|
+
const focusResolution = shouldResolveFocus && fullLlmOutput ? resolveModvizFocus(fullLlmOutput, focusOptions) : undefined;
|
|
128
|
+
const outputGraphData = shouldResolveFocus && focusResolution
|
|
129
|
+
? applyGraphFocus(pathFilteredGraph, focusResolution, focusOptions)
|
|
130
|
+
: pathFilteredGraph;
|
|
131
|
+
writeFileSync(flags.outputFile, JSON.stringify(outputGraphData, null, 2));
|
|
132
|
+
console.log(`💾 Graph data saved to: ${flags.outputFile} (${outputGraphData.nodes.length} nodes out of ${outputGraphData.imports.length} imports)`);
|
|
133
|
+
if (flags.summary) {
|
|
134
|
+
console.log(buildCliSummary(outputGraphData));
|
|
135
|
+
}
|
|
136
|
+
if (focusResolution && shouldResolveFocus) {
|
|
137
|
+
if (focusResolution.matchedPackageNames.length === 0 &&
|
|
138
|
+
focusResolution.matchedNodePaths.length === 0) {
|
|
139
|
+
console.warn("⚠️ No focus match found; wrote the full graph output instead.");
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(`🎯 Applied focus filter (${outputGraphData.nodes.length} nodes kept)`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (shouldBuildLlmReport) {
|
|
146
|
+
const llmOutput = await withProgress("Building LLM companion report", () => buildModvizLlmOutput(outputGraphData));
|
|
147
|
+
const focusedDrilldown = flags.packageQuery || flags.nodeQuery
|
|
148
|
+
? renderModvizLlmDrilldown(llmOutput, {
|
|
149
|
+
packageName: flags.packageQuery,
|
|
150
|
+
nodeQuery: flags.nodeQuery,
|
|
151
|
+
limit: Number.isFinite(flags.limit) ? flags.limit : 20,
|
|
152
|
+
})
|
|
153
|
+
: undefined;
|
|
154
|
+
const llmOutputPaths = inferLlmOutputPaths(flags.outputFile);
|
|
155
|
+
const commandHints = buildLlmCommandHints(entryFileAbsolute, flags);
|
|
156
|
+
const llmMarkdown = renderModvizLlmMarkdown(llmOutput, {
|
|
157
|
+
focus: shouldResolveFocus ? focusOptions : undefined,
|
|
158
|
+
commandHints,
|
|
159
|
+
focusedDrilldown,
|
|
160
|
+
});
|
|
161
|
+
if (flags.llm || flags.llmAnalyze) {
|
|
162
|
+
writeFileSync(llmOutputPaths.json, JSON.stringify(llmOutput, null, 2));
|
|
163
|
+
writeFileSync(llmOutputPaths.markdown, llmMarkdown);
|
|
164
|
+
console.log(`🧠 LLM reports saved to: ${llmOutputPaths.json} and ${llmOutputPaths.markdown}`);
|
|
165
|
+
}
|
|
166
|
+
if (flags.llmAnalyze) {
|
|
167
|
+
const analysis = await withProgress("Generating AI analysis", () => generateAiAnalysis({
|
|
168
|
+
baseUrl: flags.llmBaseUrl,
|
|
169
|
+
markdown: llmMarkdown,
|
|
170
|
+
model: flags.llmModel,
|
|
171
|
+
outputFile: flags.outputFile,
|
|
172
|
+
}));
|
|
173
|
+
console.log(`🤖 AI analysis saved to: ${analysis.analysisPath} (model: ${analysis.modelName})`);
|
|
174
|
+
}
|
|
175
|
+
if (flags.packageQuery || flags.nodeQuery) {
|
|
176
|
+
console.log(focusedDrilldown);
|
|
177
|
+
}
|
|
178
|
+
if (flags.snapshotName) {
|
|
179
|
+
const snapshot = saveSnapshotToHistory({
|
|
180
|
+
graph: outputGraphData,
|
|
181
|
+
llm: flags.llm || flags.llmAnalyze ? llmOutput : undefined,
|
|
182
|
+
snapshotName: flags.snapshotName,
|
|
183
|
+
});
|
|
184
|
+
if (snapshot) {
|
|
185
|
+
console.log(`🗂️ Saved named snapshot: ${snapshot.id}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (flags.snapshotName) {
|
|
190
|
+
const snapshot = saveSnapshotToHistory({
|
|
191
|
+
graph: outputGraphData,
|
|
192
|
+
snapshotName: flags.snapshotName,
|
|
193
|
+
});
|
|
194
|
+
if (snapshot) {
|
|
195
|
+
console.log(`🗂️ Saved named snapshot: ${snapshot.id}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (flags.ui) {
|
|
199
|
+
await launchWebUI(flags.port, flags.outputFile, flags.open);
|
|
200
|
+
}
|
|
201
|
+
function formatDuration(milliseconds) {
|
|
202
|
+
if (milliseconds < 1000) {
|
|
203
|
+
return `${milliseconds}ms`;
|
|
204
|
+
}
|
|
205
|
+
return `${(milliseconds / 1000).toFixed(1)}s`;
|
|
206
|
+
}
|
|
207
|
+
async function withProgress(label, work) {
|
|
208
|
+
const start = Date.now();
|
|
209
|
+
let spinnerWorker;
|
|
210
|
+
if (process.stdout.isTTY) {
|
|
211
|
+
process.stdout.write(`⏳ ${label}\n`);
|
|
212
|
+
spinnerWorker = new Worker(`
|
|
213
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
214
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
215
|
+
const start = Date.now();
|
|
216
|
+
let frameIndex = 0;
|
|
217
|
+
const formatDuration = (milliseconds) => milliseconds < 1000 ? \
|
|
218
|
+
numberToString(milliseconds) + "ms" : numberToString((milliseconds / 1000).toFixed(1)) + "s";
|
|
219
|
+
function numberToString(value) { return String(value); }
|
|
220
|
+
const interval = setInterval(() => {
|
|
221
|
+
const frame = frames[frameIndex % frames.length];
|
|
222
|
+
frameIndex += 1;
|
|
223
|
+
process.stdout.write(\`\\r\${frame} \${workerData.label} (\${formatDuration(Date.now() - start)})\`);
|
|
224
|
+
}, 80);
|
|
225
|
+
parentPort.on("message", () => {
|
|
226
|
+
clearInterval(interval);
|
|
227
|
+
process.exit(0);
|
|
228
|
+
});
|
|
229
|
+
`, { eval: true, workerData: { label } });
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
console.log(`⏳ ${label}`);
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const result = await work();
|
|
236
|
+
if (spinnerWorker) {
|
|
237
|
+
spinnerWorker.postMessage("stop");
|
|
238
|
+
await spinnerWorker.terminate().catch(() => undefined);
|
|
239
|
+
process.stdout.write(`\r✅ ${label} (${formatDuration(Date.now() - start)})\n`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.log(`✅ ${label} (${formatDuration(Date.now() - start)})`);
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (spinnerWorker) {
|
|
248
|
+
spinnerWorker.postMessage("stop");
|
|
249
|
+
await spinnerWorker.terminate().catch(() => undefined);
|
|
250
|
+
process.stdout.write(`\r❌ ${label} failed after ${formatDuration(Date.now() - start)}\n`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
console.error(`❌ ${label} failed after ${formatDuration(Date.now() - start)}`);
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function processModuleGraphForWeb(moduleGraph, entryFile, workspaces, basePath, maxChainsPerNode) {
|
|
259
|
+
const nodeList = [];
|
|
260
|
+
const edgeList = new Set();
|
|
261
|
+
for (const [filePath, importees] of moduleGraph.graph) {
|
|
262
|
+
const module = moduleGraph.modules.get(filePath);
|
|
263
|
+
const imports = (module.imports ?? []);
|
|
264
|
+
const exports = (module.exports ?? []);
|
|
265
|
+
const unusedExports = (module.unusedExports ?? []);
|
|
266
|
+
const node = {
|
|
267
|
+
name: path.basename(filePath),
|
|
268
|
+
path: filePath,
|
|
269
|
+
type: getNodeType(filePath, module, entryFile),
|
|
270
|
+
package: workspaces.find((workspace) => filePath.startsWith(workspace.path)),
|
|
271
|
+
cluster: module.cluster,
|
|
272
|
+
imports,
|
|
273
|
+
exports,
|
|
274
|
+
unusedExports,
|
|
275
|
+
importees: Array.from(importees),
|
|
276
|
+
importedBy: module.importedBy,
|
|
277
|
+
isBarrelFile: module.isBarrelFile || false,
|
|
278
|
+
chain: moduleGraph.findImportChains(filePath, { maxChains: maxChainsPerNode }),
|
|
279
|
+
};
|
|
280
|
+
nodeList.push(node);
|
|
281
|
+
importees.forEach((importee) => {
|
|
282
|
+
edgeList.add(`${filePath}->${importee}`);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const uniqueModules = moduleGraph.getUniqueModules();
|
|
286
|
+
return {
|
|
287
|
+
metadata: {
|
|
288
|
+
entrypoints: moduleGraph.entrypoints.map((entrypoint) => normalizeRelativePath(entrypoint)),
|
|
289
|
+
basePath: basePath,
|
|
290
|
+
totalFiles: moduleGraph.getUniqueModules().length,
|
|
291
|
+
generatedAt: new Date().toISOString(),
|
|
292
|
+
nodeModulesCount: nodeList.filter((n) => n.path.includes("node_modules")).length,
|
|
293
|
+
packages,
|
|
294
|
+
},
|
|
295
|
+
nodes: nodeList,
|
|
296
|
+
imports: uniqueModules,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function getNodeType(filePath, module, entryPoint) {
|
|
300
|
+
if (filePath === entryPoint)
|
|
301
|
+
return "entry";
|
|
302
|
+
if (filePath.includes("node_modules"))
|
|
303
|
+
return "external";
|
|
304
|
+
if (module.isBarrelFile)
|
|
305
|
+
return "barrel";
|
|
306
|
+
return "internal";
|
|
307
|
+
}
|
|
308
|
+
async function launchWebUI(port, dataFile, open = true) {
|
|
309
|
+
const resolvedPort = port ? Number.parseInt(port, 10) : 3000;
|
|
310
|
+
console.log(`🚀 Launching production web UI on port ${resolvedPort}...`);
|
|
311
|
+
const { startProductionServer } = await import("./production-server.js");
|
|
312
|
+
await startProductionServer({
|
|
313
|
+
open,
|
|
314
|
+
outputPath: path.resolve(dataFile ?? flags.outputFile),
|
|
315
|
+
port: resolvedPort,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function deriveAnalysisBasePath(entryFileAbsolute, workspaces) {
|
|
319
|
+
const workspaceLocations = workspaces.map((workspace) => workspace.location);
|
|
320
|
+
if (workspaceLocations.length > 0) {
|
|
321
|
+
return getCommonAncestorPath([path.dirname(entryFileAbsolute), ...workspaceLocations]);
|
|
322
|
+
}
|
|
323
|
+
return findNearestPackageRoot(path.dirname(entryFileAbsolute));
|
|
324
|
+
}
|
|
325
|
+
function getCommonAncestorPath(paths) {
|
|
326
|
+
const normalizedPaths = paths.map((currentPath) => path.resolve(currentPath));
|
|
327
|
+
let commonPath = normalizedPaths[0] ?? process.cwd();
|
|
328
|
+
for (const currentPath of normalizedPaths.slice(1)) {
|
|
329
|
+
while (commonPath !== path.dirname(commonPath) &&
|
|
330
|
+
!currentPath.startsWith(`${commonPath}${path.sep}`) &&
|
|
331
|
+
currentPath !== commonPath) {
|
|
332
|
+
commonPath = path.dirname(commonPath);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return commonPath;
|
|
336
|
+
}
|
|
337
|
+
function findNearestPackageRoot(startDirectory) {
|
|
338
|
+
let currentDirectory = path.resolve(startDirectory);
|
|
339
|
+
while (true) {
|
|
340
|
+
if (existsSync(path.join(currentDirectory, "package.json"))) {
|
|
341
|
+
return currentDirectory;
|
|
342
|
+
}
|
|
343
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
344
|
+
if (parentDirectory === currentDirectory) {
|
|
345
|
+
return startDirectory;
|
|
346
|
+
}
|
|
347
|
+
currentDirectory = parentDirectory;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function normalizeRelativePath(filePath) {
|
|
351
|
+
return filePath.replace(/\\/g, "/");
|
|
352
|
+
}
|
|
353
|
+
function loadGraphForReport(flags) {
|
|
354
|
+
if (flags.snapshot) {
|
|
355
|
+
return loadSnapshotGraph(flags.snapshot);
|
|
356
|
+
}
|
|
357
|
+
const graphFile = path.resolve(flags.graphFile ?? flags.outputFile);
|
|
358
|
+
return loadGraphFile(graphFile);
|
|
359
|
+
}
|
|
360
|
+
function loadGraphTarget(target) {
|
|
361
|
+
if (target.startsWith("snapshot:")) {
|
|
362
|
+
return loadSnapshotGraph(target.slice("snapshot:".length));
|
|
363
|
+
}
|
|
364
|
+
return loadGraphFile(path.resolve(target));
|
|
365
|
+
}
|
|
366
|
+
function loadGraphFile(graphFile) {
|
|
367
|
+
if (!existsSync(graphFile)) {
|
|
368
|
+
console.error(`Error: Graph file "${graphFile}" does not exist`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
return JSON.parse(readFileSync(graphFile, "utf-8"));
|
|
372
|
+
}
|
|
373
|
+
function applyPathFilters(output, includeValue, excludeValue) {
|
|
374
|
+
const includePatterns = splitGlobPatterns(includeValue);
|
|
375
|
+
const excludePatterns = splitGlobPatterns(excludeValue);
|
|
376
|
+
if (includePatterns.length === 0 && excludePatterns.length === 0) {
|
|
377
|
+
return output;
|
|
378
|
+
}
|
|
379
|
+
const includeMatchers = includePatterns.map(createGlobMatcher);
|
|
380
|
+
const excludeMatchers = excludePatterns.map(createGlobMatcher);
|
|
381
|
+
const includedPaths = new Set(output.nodes
|
|
382
|
+
.map((node) => node.path)
|
|
383
|
+
.filter((nodePath) => {
|
|
384
|
+
const included = includeMatchers.length === 0 || includeMatchers.some((matcher) => matcher(nodePath));
|
|
385
|
+
const excluded = excludeMatchers.some((matcher) => matcher(nodePath));
|
|
386
|
+
return included && !excluded;
|
|
387
|
+
}));
|
|
388
|
+
if (includedPaths.size === output.nodes.length) {
|
|
389
|
+
return output;
|
|
390
|
+
}
|
|
391
|
+
const filteredNodes = output.nodes
|
|
392
|
+
.filter((node) => includedPaths.has(node.path))
|
|
393
|
+
.map((node) => ({
|
|
394
|
+
...node,
|
|
395
|
+
importees: node.importees.filter((importee) => includedPaths.has(importee)),
|
|
396
|
+
importedBy: node.importedBy.filter((importer) => includedPaths.has(importer)),
|
|
397
|
+
chain: dedupeChains(node.chain
|
|
398
|
+
.map((chain) => chain.filter((chainNode) => includedPaths.has(chainNode)))
|
|
399
|
+
.filter((chain) => chain.length > 0)),
|
|
400
|
+
}));
|
|
401
|
+
const entrypoints = output.metadata.entrypoints.filter((entrypoint) => includedPaths.has(entrypoint));
|
|
402
|
+
return {
|
|
403
|
+
metadata: {
|
|
404
|
+
...output.metadata,
|
|
405
|
+
entrypoints: entrypoints.length > 0
|
|
406
|
+
? entrypoints
|
|
407
|
+
: filteredNodes[0]
|
|
408
|
+
? [filteredNodes[0].path]
|
|
409
|
+
: output.metadata.entrypoints,
|
|
410
|
+
totalFiles: filteredNodes.length,
|
|
411
|
+
nodeModulesCount: filteredNodes.filter((node) => node.path.includes("node_modules")).length,
|
|
412
|
+
},
|
|
413
|
+
nodes: filteredNodes,
|
|
414
|
+
imports: Array.from(new Set(filteredNodes.flatMap((node) => [node.path, ...node.importees]))).sort((left, right) => left.localeCompare(right)),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function splitGlobPatterns(value) {
|
|
418
|
+
return (value ?? "")
|
|
419
|
+
.split(/[\n,]+/)
|
|
420
|
+
.map((pattern) => pattern.trim())
|
|
421
|
+
.filter(Boolean);
|
|
422
|
+
}
|
|
423
|
+
function createGlobMatcher(pattern) {
|
|
424
|
+
const escaped = pattern
|
|
425
|
+
.replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
|
|
426
|
+
.replace(/\*\*/g, "__DOUBLE_STAR__")
|
|
427
|
+
.replace(/\*/g, "[^/]*")
|
|
428
|
+
.replace(/__DOUBLE_STAR__/g, ".*");
|
|
429
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
430
|
+
return (candidate) => regex.test(candidate);
|
|
431
|
+
}
|
|
432
|
+
function applyGraphFocus(output, focusResolution, focusOptions) {
|
|
433
|
+
if (focusResolution.matchedPackageNames.length === 0 &&
|
|
434
|
+
focusResolution.matchedNodePaths.length === 0) {
|
|
435
|
+
return output;
|
|
436
|
+
}
|
|
437
|
+
const includedPaths = new Set(focusResolution.includedPaths);
|
|
438
|
+
for (const nodePath of focusResolution.matchedNodePaths) {
|
|
439
|
+
includeReachablePaths(nodePath, output, includedPaths);
|
|
440
|
+
}
|
|
441
|
+
const filteredNodes = output.nodes
|
|
442
|
+
.filter((node) => includedPaths.has(node.path))
|
|
443
|
+
.map((node) => ({
|
|
444
|
+
...node,
|
|
445
|
+
importees: node.importees.filter((importee) => includedPaths.has(importee)),
|
|
446
|
+
importedBy: node.importedBy.filter((importer) => includedPaths.has(importer)),
|
|
447
|
+
chain: dedupeChains(node.chain
|
|
448
|
+
.map((chain) => chain.filter((chainNode) => includedPaths.has(chainNode)))
|
|
449
|
+
.filter((chain) => chain.length > 0)),
|
|
450
|
+
}));
|
|
451
|
+
const filteredImports = Array.from(new Set(filteredNodes.flatMap((node) => [node.path, ...node.importees]))).sort((left, right) => left.localeCompare(right));
|
|
452
|
+
return {
|
|
453
|
+
metadata: {
|
|
454
|
+
...output.metadata,
|
|
455
|
+
totalFiles: filteredNodes.length,
|
|
456
|
+
nodeModulesCount: filteredNodes.filter((node) => node.path.includes("node_modules")).length,
|
|
457
|
+
focus: {
|
|
458
|
+
packageName: focusOptions.packageName,
|
|
459
|
+
nodeQuery: focusOptions.nodeQuery,
|
|
460
|
+
matchedPackageNames: focusResolution.matchedPackageNames,
|
|
461
|
+
matchedNodePaths: focusResolution.matchedNodePaths,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
nodes: filteredNodes,
|
|
465
|
+
imports: filteredImports,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function includeReachablePaths(startPath, output, includedPaths) {
|
|
469
|
+
const nodeMap = new Map(output.nodes.map((node) => [node.path, node]));
|
|
470
|
+
const stack = [startPath];
|
|
471
|
+
const visited = new Set();
|
|
472
|
+
while (stack.length > 0) {
|
|
473
|
+
const currentPath = stack.pop();
|
|
474
|
+
if (!currentPath || visited.has(currentPath)) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
visited.add(currentPath);
|
|
478
|
+
includedPaths.add(currentPath);
|
|
479
|
+
const node = nodeMap.get(currentPath);
|
|
480
|
+
if (!node) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
for (const importee of node.importees) {
|
|
484
|
+
if (!includedPaths.has(importee)) {
|
|
485
|
+
stack.push(importee);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function dedupeChains(chains) {
|
|
491
|
+
const seen = new Set();
|
|
492
|
+
const deduped = [];
|
|
493
|
+
for (const chain of chains) {
|
|
494
|
+
const key = chain.join("\u0000");
|
|
495
|
+
if (seen.has(key)) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
seen.add(key);
|
|
499
|
+
deduped.push(chain);
|
|
500
|
+
}
|
|
501
|
+
return deduped;
|
|
502
|
+
}
|
|
503
|
+
function buildLlmCommandHints(entryFileAbsolute, flags) {
|
|
504
|
+
const baseArgs = [
|
|
505
|
+
"node",
|
|
506
|
+
path.resolve(process.argv[1] ?? "mod/cli.ts"),
|
|
507
|
+
entryFileAbsolute,
|
|
508
|
+
flags.nodeModules ? "--node-modules" : undefined,
|
|
509
|
+
flags.ignoreDynamic ? "--ignore-dynamic" : undefined,
|
|
510
|
+
flags.llm || flags.llmAnalyze ? "--llm" : undefined,
|
|
511
|
+
flags.outputFile !== "./modviz.json" ? `--output-file=${flags.outputFile}` : undefined,
|
|
512
|
+
]
|
|
513
|
+
.filter(Boolean)
|
|
514
|
+
.join(" ");
|
|
515
|
+
return {
|
|
516
|
+
packageCommand: `${baseArgs} --package=<package-name> --limit=20`,
|
|
517
|
+
nodeCommand: `${baseArgs} --node=<path-or-display-path> --limit=20`,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
4
|
+
import { generateText } from "ai";
|
|
5
|
+
export async function generateAiAnalysis(options) {
|
|
6
|
+
const apiKey = process.env.MODVIZ_LLM_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
7
|
+
if (!apiKey) {
|
|
8
|
+
throw new Error("Missing MODVIZ_LLM_API_KEY or OPENAI_API_KEY. Set one before using --llm-analyze.");
|
|
9
|
+
}
|
|
10
|
+
const modelName = options.model ?? process.env.MODVIZ_LLM_MODEL ?? "gpt-4.1-mini";
|
|
11
|
+
const baseURL = options.baseUrl ?? process.env.MODVIZ_LLM_BASE_URL;
|
|
12
|
+
const openai = createOpenAI({
|
|
13
|
+
apiKey,
|
|
14
|
+
baseURL,
|
|
15
|
+
});
|
|
16
|
+
const result = await generateText({
|
|
17
|
+
model: openai(modelName),
|
|
18
|
+
temperature: 0.2,
|
|
19
|
+
system: "You are analyzing a module dependency graph. Produce a terse engineering report with these sections: Executive summary, Architectural risks, Dependency hotspots, Suggested next actions. Ground every claim in the provided report. Avoid fluff.",
|
|
20
|
+
prompt: `Analyze this module dependency report and write an actionable engineering summary.\n\n${options.markdown}`,
|
|
21
|
+
});
|
|
22
|
+
const parsed = path.parse(options.outputFile);
|
|
23
|
+
const analysisPath = path.join(parsed.dir, `${parsed.name}.llm.ai.md`);
|
|
24
|
+
writeFileSync(analysisPath, result.text);
|
|
25
|
+
return {
|
|
26
|
+
analysisPath,
|
|
27
|
+
modelName,
|
|
28
|
+
};
|
|
29
|
+
}
|