universal-ast-mapper 2.0.0 → 2.0.1
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/CHANGELOG.md +9 -0
- package/README.md +261 -12
- package/dist/ai-refactor.js +185 -0
- package/dist/ai-testgen.js +105 -0
- package/dist/analysis.js +134 -0
- package/dist/arch-rules.js +82 -0
- package/dist/callgraph.js +467 -0
- package/dist/check.js +112 -0
- package/dist/cli.js +2284 -0
- package/dist/complexity.js +98 -0
- package/dist/config.js +53 -0
- package/dist/contextpack.js +79 -0
- package/dist/coupling.js +35 -0
- package/dist/covmerge.js +176 -0
- package/dist/crosslang.js +425 -0
- package/dist/dashboard.js +259 -0
- package/dist/diagram.js +264 -0
- package/dist/diskcache.js +97 -0
- package/dist/docgen.js +156 -0
- package/dist/embeddings.js +136 -0
- package/dist/explain.js +123 -0
- package/dist/explorer.js +123 -0
- package/dist/extractors/c.js +204 -0
- package/dist/extractors/common.js +56 -0
- package/dist/extractors/cpp.js +272 -0
- package/dist/extractors/csharp.js +209 -0
- package/dist/extractors/go.js +212 -0
- package/dist/extractors/java.js +152 -0
- package/dist/extractors/kotlin.js +159 -0
- package/dist/extractors/php.js +208 -0
- package/dist/extractors/python.js +153 -0
- package/dist/extractors/ruby.js +146 -0
- package/dist/extractors/rust.js +249 -0
- package/dist/extractors/swift.js +192 -0
- package/dist/extractors/typescript.js +577 -0
- package/dist/fix.js +92 -0
- package/dist/gitdiff.js +178 -0
- package/dist/graph-analysis.js +279 -0
- package/dist/graph.js +165 -0
- package/dist/history.js +36 -0
- package/dist/html.js +658 -0
- package/dist/incremental.js +122 -0
- package/dist/index.js +1945 -0
- package/dist/indexstore.js +105 -0
- package/dist/layers.js +36 -0
- package/dist/lsp.js +238 -0
- package/dist/modulecoupling.js +0 -0
- package/dist/parser.js +84 -0
- package/dist/patch.js +199 -0
- package/dist/plugins.js +88 -0
- package/dist/pool.js +114 -0
- package/dist/prompts.js +67 -0
- package/dist/registry.js +87 -0
- package/dist/report.js +441 -0
- package/dist/resolver.js +222 -0
- package/dist/roots.js +47 -0
- package/dist/search.js +68 -0
- package/dist/security.js +178 -0
- package/dist/semantic.js +365 -0
- package/dist/serve.js +185 -0
- package/dist/sfc.js +27 -0
- package/dist/similar.js +98 -0
- package/dist/skeleton.js +132 -0
- package/dist/smells.js +285 -0
- package/dist/sourcemap.js +60 -0
- package/dist/testgen.js +280 -0
- package/dist/testmap.js +167 -0
- package/dist/tsconfig.js +212 -0
- package/dist/typeflow.js +124 -0
- package/dist/types.js +5 -0
- package/dist/unused-params.js +127 -0
- package/dist/webapp.js +341 -0
- package/dist/worker.js +27 -0
- package/dist/workspace.js +330 -0
- package/package.json +2 -1
package/dist/webapp.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/** Generate the self-contained SPA HTML served by `ast-map serve`. */
|
|
2
|
+
export function webAppHtml(port) {
|
|
3
|
+
return `<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<title>AST Map — Live Dashboard</title>
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #0f1117; --surface: #1a1d27; --border: #2d3142;
|
|
13
|
+
--text: #e2e8f0; --muted: #94a3b8; --accent: #7c3aed;
|
|
14
|
+
--green: #22c55e; --yellow: #eab308; --red: #ef4444; --blue: #3b82f6;
|
|
15
|
+
}
|
|
16
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
17
|
+
body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; display: flex; height: 100vh; overflow: hidden; }
|
|
18
|
+
#sidebar { width: 220px; background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; padding: 16px 0; flex-shrink: 0; }
|
|
19
|
+
.logo { padding: 0 16px 16px; font-size: 14px; font-weight: 700; color: var(--accent); letter-spacing: 1px; border-bottom: 1px solid var(--border); }
|
|
20
|
+
.logo span { color: var(--muted); font-weight: 400; }
|
|
21
|
+
.nav-item { padding: 10px 16px; cursor: pointer; font-size: 13px; color: var(--muted); transition: all .15s; border-left: 3px solid transparent; }
|
|
22
|
+
.nav-item:hover { color: var(--text); background: rgba(124,58,237,.1); }
|
|
23
|
+
.nav-item.active { color: var(--text); border-left-color: var(--accent); background: rgba(124,58,237,.15); }
|
|
24
|
+
.nav-section { padding: 12px 16px 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-top: 8px; }
|
|
25
|
+
#main { flex: 1; overflow-y: auto; padding: 24px; }
|
|
26
|
+
.page { display: none; } .page.active { display: block; }
|
|
27
|
+
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
|
|
28
|
+
.subtitle { color: var(--muted); font-size: 13px; margin-bottom: 20px; }
|
|
29
|
+
.grid { display: grid; gap: 16px; }
|
|
30
|
+
.grid-4 { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
|
|
31
|
+
.grid-2 { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }
|
|
32
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
33
|
+
.stat-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px; }
|
|
34
|
+
.stat-value { font-size: 28px; font-weight: 700; line-height: 1; }
|
|
35
|
+
.stat-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
36
|
+
.score-ring { width: 80px; height: 80px; margin: 0 auto 8px; }
|
|
37
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
|
38
|
+
.badge-green { background: rgba(34,197,94,.15); color: var(--green); }
|
|
39
|
+
.badge-yellow { background: rgba(234,179,8,.15); color: var(--yellow); }
|
|
40
|
+
.badge-red { background: rgba(239,68,68,.15); color: var(--red); }
|
|
41
|
+
.badge-blue { background: rgba(59,130,246,.15); color: var(--blue); }
|
|
42
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
43
|
+
th { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--border); color: var(--muted); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; }
|
|
44
|
+
td { padding: 8px 12px; border-bottom: 1px solid rgba(45,49,66,.5); }
|
|
45
|
+
tr:hover td { background: rgba(124,58,237,.05); }
|
|
46
|
+
.search { width: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; color: var(--text); font-size: 13px; margin-bottom: 16px; outline: none; }
|
|
47
|
+
.search:focus { border-color: var(--accent); }
|
|
48
|
+
#graph-canvas { width: 100%; height: calc(100vh - 140px); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; }
|
|
49
|
+
.tooltip { position: fixed; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; font-size: 12px; pointer-events: none; z-index: 9999; max-width: 260px; display: none; }
|
|
50
|
+
.sparkline { display: inline-block; width: 80px; height: 24px; vertical-align: middle; }
|
|
51
|
+
.refresh-btn { margin-left: auto; padding: 6px 14px; background: var(--accent); border: none; border-radius: 6px; color: #fff; font-size: 12px; cursor: pointer; }
|
|
52
|
+
.refresh-btn:hover { opacity: .85; }
|
|
53
|
+
.header-row { display: flex; align-items: center; margin-bottom: 16px; }
|
|
54
|
+
.pill { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; background: var(--border); color: var(--muted); margin-left: 6px; }
|
|
55
|
+
.error-box { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3); border-radius: 6px; padding: 12px 16px; color: var(--red); font-size: 13px; margin-top: 8px; }
|
|
56
|
+
.loading { color: var(--muted); font-size: 13px; padding: 32px; text-align: center; }
|
|
57
|
+
.timeline { height: 120px; width: 100%; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<div id="sidebar">
|
|
62
|
+
<div class="logo">AST Map <span>v2</span></div>
|
|
63
|
+
<div class="nav-section">Overview</div>
|
|
64
|
+
<div class="nav-item active" data-page="overview">📊 Dashboard</div>
|
|
65
|
+
<div class="nav-item" data-page="timeline">📈 History</div>
|
|
66
|
+
<div class="nav-section">Analysis</div>
|
|
67
|
+
<div class="nav-item" data-page="files">📁 Files</div>
|
|
68
|
+
<div class="nav-item" data-page="symbols">🔷 Symbols</div>
|
|
69
|
+
<div class="nav-item" data-page="deps">🕸️ Dependency Graph</div>
|
|
70
|
+
<div class="nav-section">Issues</div>
|
|
71
|
+
<div class="nav-item" data-page="smells">🤢 Code Smells</div>
|
|
72
|
+
<div class="nav-item" data-page="security">🔒 Security</div>
|
|
73
|
+
<div class="nav-item" data-page="dead">💀 Dead Code</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div id="main">
|
|
77
|
+
<!-- OVERVIEW -->
|
|
78
|
+
<div class="page active" id="page-overview">
|
|
79
|
+
<div class="header-row"><h1>Dashboard</h1><button class="refresh-btn" onclick="loadAll()">↺ Refresh</button></div>
|
|
80
|
+
<div class="subtitle" id="root-label">Loading…</div>
|
|
81
|
+
<div class="grid grid-4" id="stat-cards" style="margin-bottom:20px"></div>
|
|
82
|
+
<div class="grid grid-2">
|
|
83
|
+
<div class="card"><div class="stat-label">Top imported symbols</div><div id="top-syms"></div></div>
|
|
84
|
+
<div class="card"><div class="stat-label">Recent issues</div><div id="recent-issues"></div></div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- HISTORY -->
|
|
89
|
+
<div class="page" id="page-timeline">
|
|
90
|
+
<h1>Health Score History</h1>
|
|
91
|
+
<div class="subtitle">Score trend over time</div>
|
|
92
|
+
<div class="card" style="margin-bottom:16px"><svg class="timeline" id="timeline-svg"></svg></div>
|
|
93
|
+
<div class="card"><table><thead><tr><th>Date</th><th>Score</th><th>Grade</th><th>Files</th><th>Dead</th><th>Cycles</th></tr></thead><tbody id="history-table"></tbody></table></div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- FILES -->
|
|
97
|
+
<div class="page" id="page-files">
|
|
98
|
+
<h1>Files</h1>
|
|
99
|
+
<input class="search" id="file-search" placeholder="Filter files…" oninput="filterFiles()">
|
|
100
|
+
<div class="card"><table><thead><tr><th>File</th><th>Lang</th><th>Symbols</th><th>Lines</th></tr></thead><tbody id="file-table"></tbody></table></div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- SYMBOLS -->
|
|
104
|
+
<div class="page" id="page-symbols">
|
|
105
|
+
<h1>Symbols</h1>
|
|
106
|
+
<input class="search" id="sym-search" placeholder="Search symbols…" oninput="filterSymbols()">
|
|
107
|
+
<div class="card"><table><thead><tr><th>Symbol</th><th>Kind</th><th>File</th><th>Exported</th></tr></thead><tbody id="sym-table"></tbody></table></div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- DEPS GRAPH -->
|
|
111
|
+
<div class="page" id="page-deps">
|
|
112
|
+
<h1>Dependency Graph</h1>
|
|
113
|
+
<div class="subtitle">File-level import relationships — drag to explore</div>
|
|
114
|
+
<svg id="graph-canvas"></svg>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- SMELLS -->
|
|
118
|
+
<div class="page" id="page-smells">
|
|
119
|
+
<h1>Code Smells</h1>
|
|
120
|
+
<div id="smells-content"></div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- SECURITY -->
|
|
124
|
+
<div class="page" id="page-security">
|
|
125
|
+
<h1>Security Issues</h1>
|
|
126
|
+
<div id="security-content"></div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- DEAD CODE -->
|
|
130
|
+
<div class="page" id="page-dead">
|
|
131
|
+
<h1>Dead Exports</h1>
|
|
132
|
+
<div class="subtitle">Exported symbols with no known importers inside the scanned directory</div>
|
|
133
|
+
<div class="card"><table><thead><tr><th>Symbol</th><th>Kind</th><th>File</th><th>Confidence</th></tr></thead><tbody id="dead-table"></tbody></table></div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="tooltip" id="tooltip"></div>
|
|
138
|
+
|
|
139
|
+
<script>
|
|
140
|
+
const API = 'http://localhost:${port}/api';
|
|
141
|
+
let state = { report: null, graph: null, dead: [], history: [], skeletons: [], smells: [], security: [] };
|
|
142
|
+
|
|
143
|
+
// ─── Navigation ───────────────────────────────────────────────────────────────
|
|
144
|
+
document.querySelectorAll('.nav-item').forEach(el => {
|
|
145
|
+
el.addEventListener('click', () => {
|
|
146
|
+
document.querySelectorAll('.nav-item').forEach(e => e.classList.remove('active'));
|
|
147
|
+
document.querySelectorAll('.page').forEach(e => e.classList.remove('active'));
|
|
148
|
+
el.classList.add('active');
|
|
149
|
+
const page = document.getElementById('page-' + el.dataset.page);
|
|
150
|
+
if (page) { page.classList.add('active'); renderPage(el.dataset.page); }
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── Data loading ─────────────────────────────────────────────────────────────
|
|
155
|
+
async function fetchJson(path) {
|
|
156
|
+
const r = await fetch(API + path);
|
|
157
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
158
|
+
return r.json();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function loadAll() {
|
|
162
|
+
try {
|
|
163
|
+
const [report, graph, dead, history, skeletons, smells, security] = await Promise.all([
|
|
164
|
+
fetchJson('/report'), fetchJson('/graph'), fetchJson('/dead'),
|
|
165
|
+
fetchJson('/history'), fetchJson('/skeletons'), fetchJson('/smells'), fetchJson('/security'),
|
|
166
|
+
]);
|
|
167
|
+
state = { report, graph, dead, history, skeletons, smells, security };
|
|
168
|
+
renderPage(document.querySelector('.nav-item.active')?.dataset.page ?? 'overview');
|
|
169
|
+
} catch(e) {
|
|
170
|
+
document.getElementById('root-label').textContent = 'Error: ' + e.message;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Renderers ────────────────────────────────────────────────────────────────
|
|
175
|
+
function renderPage(name) {
|
|
176
|
+
if (name === 'overview') renderOverview();
|
|
177
|
+
else if (name === 'timeline') renderTimeline();
|
|
178
|
+
else if (name === 'files') renderFiles();
|
|
179
|
+
else if (name === 'symbols') renderSymbols();
|
|
180
|
+
else if (name === 'deps') renderGraph();
|
|
181
|
+
else if (name === 'smells') renderSmells();
|
|
182
|
+
else if (name === 'security') renderSecurity();
|
|
183
|
+
else if (name === 'dead') renderDead();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function grade(s) { return s >= 90 ? 'A' : s >= 80 ? 'B' : s >= 70 ? 'C' : s >= 60 ? 'D' : 'F'; }
|
|
187
|
+
function gradeClass(s) { return s >= 80 ? 'badge-green' : s >= 60 ? 'badge-yellow' : 'badge-red'; }
|
|
188
|
+
|
|
189
|
+
function renderOverview() {
|
|
190
|
+
const r = state.report;
|
|
191
|
+
if (!r) return;
|
|
192
|
+
document.getElementById('root-label').textContent = r.directory ?? '.';
|
|
193
|
+
const score = r.score ?? 0;
|
|
194
|
+
document.getElementById('stat-cards').innerHTML = [
|
|
195
|
+
{ label: 'Health Score', value: score, sub: 'Grade ' + (r.grade ?? grade(score)), cls: gradeClass(score) },
|
|
196
|
+
{ label: 'Files', value: r.files ?? 0, sub: '' },
|
|
197
|
+
{ label: 'Symbols', value: r.symbols ?? 0, sub: '' },
|
|
198
|
+
{ label: 'Dead Exports', value: r.deadExports ?? state.dead.length, sub: '', cls: (r.deadExports || state.dead.length) > 0 ? 'badge-yellow' : 'badge-green' },
|
|
199
|
+
{ label: 'Circular Deps', value: r.cyclicGroups ?? 0, sub: '', cls: (r.cyclicGroups ?? 0) > 0 ? 'badge-red' : 'badge-green' },
|
|
200
|
+
{ label: 'Max Complexity', value: r.maxComplexity ?? 0, sub: '', cls: (r.maxComplexity ?? 0) > 20 ? 'badge-red' : (r.maxComplexity ?? 0) > 10 ? 'badge-yellow' : 'badge-green' },
|
|
201
|
+
{ label: 'Smells', value: state.smells.length, sub: '', cls: state.smells.length > 0 ? 'badge-yellow' : 'badge-green' },
|
|
202
|
+
{ label: 'Security', value: state.security.length, sub: '', cls: state.security.length > 0 ? 'badge-red' : 'badge-green' },
|
|
203
|
+
].map(s => \`<div class="card"><div class="stat-label">\${s.label}</div><div class="stat-value"><span class="badge \${s.cls || ''}">\${s.value}</span></div><div class="stat-sub">\${s.sub}</div></div>\`).join('');
|
|
204
|
+
|
|
205
|
+
const topNodes = (state.graph?.nodes ?? []).filter(n => n.nodeType === 'symbol').sort((a, b) => (b.inDegree ?? 0) - (a.inDegree ?? 0)).slice(0, 8);
|
|
206
|
+
document.getElementById('top-syms').innerHTML = topNodes.map(n =>
|
|
207
|
+
\`<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)"><span>\${n.id?.split('::').pop() ?? n.id}</span><span class="badge badge-blue">\${n.inDegree ?? 0}</span></div>\`
|
|
208
|
+
).join('') || '<div style="color:var(--muted);font-size:12px">No data</div>';
|
|
209
|
+
|
|
210
|
+
const recent = [...state.smells.slice(0, 3).map(s => ({ type: '🤢', msg: (s.symbol ?? s.smell), file: s.file })),
|
|
211
|
+
...state.security.slice(0, 3).map(s => ({ type: '🔒', msg: s.rule, file: s.file }))];
|
|
212
|
+
document.getElementById('recent-issues').innerHTML = recent.map(i =>
|
|
213
|
+
\`<div style="display:flex;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)"><span>\${i.type}</span><div><div>\${i.msg}</div><div style="color:var(--muted)">\${i.file}</div></div></div>\`
|
|
214
|
+
).join('') || '<div style="color:var(--green);font-size:12px">No issues 🎉</div>';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderTimeline() {
|
|
218
|
+
const hist = state.history;
|
|
219
|
+
const tbody = document.getElementById('history-table');
|
|
220
|
+
tbody.innerHTML = [...hist].reverse().map(h =>
|
|
221
|
+
\`<tr><td>\${h.date}</td><td><span class="badge \${gradeClass(h.score)}">\${h.score}</span></td><td>\${h.grade}</td><td>\${h.files}</td><td>\${h.dead}</td><td>\${h.cycles}</td></tr>\`
|
|
222
|
+
).join('');
|
|
223
|
+
|
|
224
|
+
if (hist.length < 2) return;
|
|
225
|
+
const svg = document.getElementById('timeline-svg');
|
|
226
|
+
const W = svg.clientWidth || 600, H = 120, M = { t: 10, r: 20, b: 30, l: 40 };
|
|
227
|
+
const iW = W - M.l - M.r, iH = H - M.t - M.b;
|
|
228
|
+
const xs = d3.scalePoint().domain(hist.map(h => h.date)).range([0, iW]);
|
|
229
|
+
const ys = d3.scaleLinear().domain([0, 100]).range([iH, 0]);
|
|
230
|
+
const line = d3.line().x(h => xs(h.date) ?? 0).y(h => ys(h.score));
|
|
231
|
+
svg.innerHTML = \`<g transform="translate(\${M.l},\${M.t})">
|
|
232
|
+
<g transform="translate(0,\${iH})">\${d3.axisBottom(xs).ticks(5)(d3.select(document.createElementNS('http://www.w3.org/2000/svg','g'))?.node?.() ?? document.createElementNS('http://www.w3.org/2000/svg','g'))?.outerHTML ?? ''}</g>
|
|
233
|
+
<path d="\${line(hist)}" fill="none" stroke="#7c3aed" stroke-width="2"/>
|
|
234
|
+
\${hist.map(h => \`<circle cx="\${xs(h.date)}" cy="\${ys(h.score)}" r="4" fill="#7c3aed"/>\`).join('')}
|
|
235
|
+
</g>\`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let allFiles = [];
|
|
239
|
+
function renderFiles() {
|
|
240
|
+
allFiles = state.skeletons;
|
|
241
|
+
filterFiles();
|
|
242
|
+
}
|
|
243
|
+
function filterFiles() {
|
|
244
|
+
const q = document.getElementById('file-search')?.value?.toLowerCase() ?? '';
|
|
245
|
+
const rows = allFiles.filter(s => !q || s.file.toLowerCase().includes(q));
|
|
246
|
+
document.getElementById('file-table').innerHTML = rows.map(s =>
|
|
247
|
+
\`<tr><td style="font-family:monospace">\${s.file}</td><td><span class="pill">\${s.language}</span></td><td>\${s.symbolCount ?? s.symbols?.length ?? 0}</td><td>\${s.lineCount ?? '?'}</td></tr>\`
|
|
248
|
+
).join('');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let allSymbols = [];
|
|
252
|
+
function renderSymbols() {
|
|
253
|
+
allSymbols = state.skeletons.flatMap(s => flattenSyms(s.symbols, s.file));
|
|
254
|
+
filterSymbols();
|
|
255
|
+
}
|
|
256
|
+
function flattenSyms(syms, file, out = []) {
|
|
257
|
+
for (const s of syms) { out.push({ ...s, file }); flattenSyms(s.children ?? [], file, out); }
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
function filterSymbols() {
|
|
261
|
+
const q = document.getElementById('sym-search')?.value?.toLowerCase() ?? '';
|
|
262
|
+
const rows = allSymbols.filter(s => !q || s.name.toLowerCase().includes(q) || s.kind.includes(q));
|
|
263
|
+
document.getElementById('sym-table').innerHTML = rows.slice(0, 200).map(s =>
|
|
264
|
+
\`<tr><td><b>\${esc(s.name)}</b></td><td><span class="pill">\${s.kind}</span></td><td style="font-family:monospace;font-size:11px">\${esc(s.file)}</td><td>\${s.exported ? '✓' : ''}</td></tr>\`
|
|
265
|
+
).join('');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderGraph() {
|
|
269
|
+
const g = state.graph;
|
|
270
|
+
if (!g) return;
|
|
271
|
+
const svg = d3.select('#graph-canvas');
|
|
272
|
+
svg.selectAll('*').remove();
|
|
273
|
+
const W = document.getElementById('graph-canvas').clientWidth || 800;
|
|
274
|
+
const H = document.getElementById('graph-canvas').clientHeight || 500;
|
|
275
|
+
const fileNodes = g.nodes.filter(n => n.nodeType === 'file').slice(0, 60);
|
|
276
|
+
const nodeIds = new Set(fileNodes.map(n => n.id));
|
|
277
|
+
const links = g.edges.filter(e => e.edgeType === 'imports' && nodeIds.has(e.from) && nodeIds.has(e.to))
|
|
278
|
+
.map(e => ({ source: e.from, target: e.to }));
|
|
279
|
+
|
|
280
|
+
const sim = d3.forceSimulation(fileNodes)
|
|
281
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(80))
|
|
282
|
+
.force('charge', d3.forceManyBody().strength(-120))
|
|
283
|
+
.force('center', d3.forceCenter(W / 2, H / 2));
|
|
284
|
+
|
|
285
|
+
const link = svg.append('g').selectAll('line').data(links).join('line')
|
|
286
|
+
.attr('stroke', '#2d3142').attr('stroke-width', 1);
|
|
287
|
+
const node = svg.append('g').selectAll('circle').data(fileNodes).join('circle')
|
|
288
|
+
.attr('r', 6).attr('fill', '#7c3aed').attr('cursor', 'pointer')
|
|
289
|
+
.call(d3.drag().on('start', e => { if (!e.active) sim.alphaTarget(.3).restart(); e.subject.fx = e.subject.x; e.subject.fy = e.subject.y; })
|
|
290
|
+
.on('drag', e => { e.subject.fx = e.x; e.subject.fy = e.y; })
|
|
291
|
+
.on('end', e => { if (!e.active) sim.alphaTarget(0); e.subject.fx = null; e.subject.fy = null; }))
|
|
292
|
+
.on('mouseover', (ev, d) => { const t = document.getElementById('tooltip'); t.style.display='block'; t.style.left=ev.clientX+12+'px'; t.style.top=ev.clientY+'px'; t.textContent=d.id; })
|
|
293
|
+
.on('mouseout', () => { document.getElementById('tooltip').style.display='none'; });
|
|
294
|
+
|
|
295
|
+
sim.on('tick', () => {
|
|
296
|
+
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
297
|
+
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
298
|
+
node.attr('cx', d => d.x).attr('cy', d => d.y);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderSmells() {
|
|
303
|
+
const s = state.smells;
|
|
304
|
+
document.getElementById('smells-content').innerHTML = s.length === 0
|
|
305
|
+
? '<div class="card" style="color:var(--green)">No smells detected 🎉</div>'
|
|
306
|
+
: \`<div class="card"><table><thead><tr><th>Smell</th><th>Symbol</th><th>File</th><th>Line</th><th>Severity</th></tr></thead><tbody>\${s.map(i =>
|
|
307
|
+
\`<tr><td><span class="badge badge-yellow">\${esc(i.smell)}</span></td><td>\${esc(i.symbol??'')}</td><td style="font-size:11px">\${esc(i.file)}</td><td>\${i.line??''}</td><td>\${i.severity}</td></tr>\`
|
|
308
|
+
).join('')}</tbody></table></div>\`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function renderSecurity() {
|
|
312
|
+
const s = state.security;
|
|
313
|
+
document.getElementById('security-content').innerHTML = s.length === 0
|
|
314
|
+
? '<div class="card" style="color:var(--green)">No security issues detected 🎉</div>'
|
|
315
|
+
: \`<div class="card"><table><thead><tr><th>Rule</th><th>Severity</th><th>File</th><th>Line</th><th>Message</th></tr></thead><tbody>\${s.map(i =>
|
|
316
|
+
\`<tr><td><span class="badge \${i.severity==='critical'||i.severity==='high'?'badge-red':'badge-yellow'}">\${esc(i.rule)}</span></td><td>\${esc(i.severity)}</td><td style="font-size:11px">\${esc(i.file)}</td><td>\${i.line}</td><td style="font-size:11px">\${esc(i.message)}</td></tr>\`
|
|
317
|
+
).join('')}</tbody></table></div>\`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function renderDead() {
|
|
321
|
+
const d = state.dead;
|
|
322
|
+
document.getElementById('dead-table').innerHTML = d.map(i =>
|
|
323
|
+
\`<tr><td><b>\${esc(i.symbol)}</b></td><td><span class="pill">\${esc(i.kind)}</span></td><td style="font-family:monospace;font-size:11px">\${esc(i.file)}</td><td><span class="badge \${i.confidence==='high'?'badge-red':'badge-yellow'}">\${esc(i.confidence)}</span></td></tr>\`
|
|
324
|
+
).join('');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function esc(s) { return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
328
|
+
|
|
329
|
+
// ─── Bootstrap ────────────────────────────────────────────────────────────────
|
|
330
|
+
loadAll();
|
|
331
|
+
|
|
332
|
+
// ─── Live reload via SSE ──────────────────────────────────────────────────────
|
|
333
|
+
(function connectSSE() {
|
|
334
|
+
const es = new EventSource('http://localhost:${port}/events');
|
|
335
|
+
es.addEventListener('change', () => loadAll());
|
|
336
|
+
es.addEventListener('error', () => { es.close(); setTimeout(connectSSE, 3000); });
|
|
337
|
+
})();
|
|
338
|
+
</script>
|
|
339
|
+
</body>
|
|
340
|
+
</html>`;
|
|
341
|
+
}
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Worker-thread entry: builds skeletons (and optionally per-file complexity)
|
|
2
|
+
// off the main thread. Spawned by pool.ts with { cacheDir } as workerData.
|
|
3
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
4
|
+
import { buildSkeleton } from "./skeleton.js";
|
|
5
|
+
import { computeFileComplexity } from "./complexity.js";
|
|
6
|
+
import { initDiskCache } from "./diskcache.js";
|
|
7
|
+
const data = (workerData ?? {});
|
|
8
|
+
if (data.cacheDir)
|
|
9
|
+
initDiskCache(data.cacheDir);
|
|
10
|
+
parentPort.on("message", (msg) => {
|
|
11
|
+
void (async () => {
|
|
12
|
+
try {
|
|
13
|
+
const skel = await buildSkeleton(msg.abs, msg.rel, msg.opts);
|
|
14
|
+
const complexity = msg.withComplexity
|
|
15
|
+
? await computeFileComplexity(msg.abs, msg.rel)
|
|
16
|
+
: undefined;
|
|
17
|
+
parentPort.postMessage({ id: msg.id, ok: true, skel, complexity });
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
parentPort.postMessage({
|
|
21
|
+
id: msg.id,
|
|
22
|
+
ok: false,
|
|
23
|
+
error: e instanceof Error ? e.message : String(e),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
});
|