universal-ast-mapper 1.28.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.
@@ -0,0 +1,176 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ // ─── Format detection ─────────────────────────────────────────────────────────
4
+ export function detectFormat(reportPath) {
5
+ const ext = path.extname(reportPath).toLowerCase();
6
+ const base = path.basename(reportPath).toLowerCase();
7
+ if (ext === ".json") {
8
+ try {
9
+ const raw = JSON.parse(fs.readFileSync(reportPath, "utf8"));
10
+ if ("total" in raw && typeof raw.total === "object")
11
+ return "istanbul";
12
+ if ("version" in raw)
13
+ return "clover";
14
+ }
15
+ catch { /* fall through */ }
16
+ return "istanbul";
17
+ }
18
+ if (ext === ".lcov" || base.endsWith(".info") || base === "lcov.info")
19
+ return "lcov";
20
+ if (base.includes("clover"))
21
+ return "clover";
22
+ if (base.includes("cobertura"))
23
+ return "cobertura";
24
+ return "istanbul";
25
+ }
26
+ function parseIstanbul(reportPath) {
27
+ const raw = JSON.parse(fs.readFileSync(reportPath, "utf8"));
28
+ const results = [];
29
+ for (const [file, data] of Object.entries(raw)) {
30
+ if (file === "total")
31
+ continue;
32
+ const d = data;
33
+ results.push({
34
+ file: normalizeFile(file),
35
+ lineCoverage: (d.lines?.pct ?? 0) / 100,
36
+ branchCoverage: d.branches ? d.branches.pct / 100 : undefined,
37
+ functionCoverage: d.functions ? d.functions.pct / 100 : undefined,
38
+ lines: d.lines?.total,
39
+ coveredLines: d.lines?.covered,
40
+ });
41
+ }
42
+ return results;
43
+ }
44
+ // ─── lcov parser ─────────────────────────────────────────────────────────────
45
+ function parseLcov(reportPath) {
46
+ const text = fs.readFileSync(reportPath, "utf8");
47
+ const results = [];
48
+ let file = "";
49
+ let linesFound = 0, linesHit = 0, branchFound = 0, branchHit = 0;
50
+ for (const line of text.split("\n")) {
51
+ const l = line.trim();
52
+ if (l.startsWith("SF:")) {
53
+ file = normalizeFile(l.slice(3));
54
+ }
55
+ else if (l.startsWith("LF:")) {
56
+ linesFound = parseInt(l.slice(3), 10) || 0;
57
+ }
58
+ else if (l.startsWith("LH:")) {
59
+ linesHit = parseInt(l.slice(3), 10) || 0;
60
+ }
61
+ else if (l.startsWith("BRF:")) {
62
+ branchFound = parseInt(l.slice(4), 10) || 0;
63
+ }
64
+ else if (l.startsWith("BRH:")) {
65
+ branchHit = parseInt(l.slice(4), 10) || 0;
66
+ }
67
+ else if (l === "end_of_record" && file) {
68
+ results.push({
69
+ file,
70
+ lineCoverage: linesFound > 0 ? linesHit / linesFound : 0,
71
+ branchCoverage: branchFound > 0 ? branchHit / branchFound : undefined,
72
+ lines: linesFound,
73
+ coveredLines: linesHit,
74
+ });
75
+ file = "";
76
+ linesFound = 0;
77
+ linesHit = 0;
78
+ branchFound = 0;
79
+ branchHit = 0;
80
+ }
81
+ }
82
+ return results;
83
+ }
84
+ // ─── Cobertura / Clover XML parser (minimal) ──────────────────────────────────
85
+ function parseXmlCoverage(reportPath) {
86
+ const text = fs.readFileSync(reportPath, "utf8");
87
+ const results = [];
88
+ // Match <class filename="..." line-rate="..." branch-rate="...">
89
+ const classRe = /(?:filename|name)="([^"]+)"[^>]*(?:line-rate|lineRate)="([^"]+)"(?:[^>]*(?:branch-rate|branchRate)="([^"]+)")?/g;
90
+ let m;
91
+ while ((m = classRe.exec(text)) !== null) {
92
+ results.push({
93
+ file: normalizeFile(m[1]),
94
+ lineCoverage: parseFloat(m[2]) || 0,
95
+ branchCoverage: m[3] ? parseFloat(m[3]) : undefined,
96
+ });
97
+ }
98
+ return results;
99
+ }
100
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
101
+ function normalizeFile(f) {
102
+ return f.replace(/\\/g, "/").replace(/^\.\//, "");
103
+ }
104
+ function parseReport(reportPath, format) {
105
+ const effectiveFormat = format === "auto" ? detectFormat(reportPath) : format;
106
+ if (effectiveFormat === "lcov")
107
+ return parseLcov(reportPath);
108
+ if (effectiveFormat === "clover" || effectiveFormat === "cobertura")
109
+ return parseXmlCoverage(reportPath);
110
+ return parseIstanbul(reportPath);
111
+ }
112
+ // ─── Merge ────────────────────────────────────────────────────────────────────
113
+ export function mergeCoverage(reportPath, structuralMap, root, format = "auto") {
114
+ const actual = parseReport(reportPath, format === "auto" ? detectFormat(reportPath) : format);
115
+ const effectiveFormat = format === "auto" ? detectFormat(reportPath) : format;
116
+ // Index actual by normalised file path
117
+ const actualByFile = new Map();
118
+ for (const fc of actual) {
119
+ // Try multiple key forms: absolute, root-relative, basename
120
+ actualByFile.set(fc.file, fc);
121
+ actualByFile.set(path.relative(root, fc.file).replace(/\\/g, "/"), fc);
122
+ actualByFile.set(path.basename(fc.file), fc);
123
+ }
124
+ const testedSet = new Set(structuralMap.tested.map((f) => f.file));
125
+ const untestedSet = new Set(structuralMap.untested.map((f) => f.file));
126
+ const enriched = [];
127
+ const deadTests = [];
128
+ const uncovered = [];
129
+ for (const src of structuralMap.tested) {
130
+ const fc = actualByFile.get(src.file)
131
+ ?? actualByFile.get(path.relative(root, src.file))
132
+ ?? actualByFile.get(path.basename(src.file));
133
+ const lineCov = fc?.lineCoverage ?? 0;
134
+ if (lineCov === 0 && fc)
135
+ deadTests.push(src.file);
136
+ enriched.push({
137
+ file: src.file,
138
+ hasTests: true,
139
+ lineCoverage: lineCov,
140
+ branchCoverage: fc?.branchCoverage,
141
+ });
142
+ }
143
+ for (const src of structuralMap.untested) {
144
+ const fc = actualByFile.get(src.file)
145
+ ?? actualByFile.get(path.relative(root, src.file))
146
+ ?? actualByFile.get(path.basename(src.file));
147
+ enriched.push({
148
+ file: src.file,
149
+ hasTests: false,
150
+ lineCoverage: fc?.lineCoverage ?? 0,
151
+ branchCoverage: fc?.branchCoverage,
152
+ });
153
+ if (!fc || fc.lineCoverage === 0)
154
+ uncovered.push(src.file);
155
+ }
156
+ const totalLines = actual.reduce((s, f) => s + (f.lineCoverage ?? 0), 0);
157
+ const avgLineCoverage = actual.length > 0 ? totalLines / actual.length : 0;
158
+ const branchEntries = actual.filter((f) => f.branchCoverage !== undefined);
159
+ const avgBranchCoverage = branchEntries.length > 0
160
+ ? branchEntries.reduce((s, f) => s + (f.branchCoverage ?? 0), 0) / branchEntries.length
161
+ : undefined;
162
+ return {
163
+ format: effectiveFormat,
164
+ reportPath,
165
+ actual,
166
+ summary: {
167
+ totalFiles: actual.length,
168
+ coveredFiles: actual.filter((f) => f.lineCoverage > 0).length,
169
+ avgLineCoverage,
170
+ avgBranchCoverage,
171
+ },
172
+ enriched,
173
+ deadTests,
174
+ uncovered,
175
+ };
176
+ }
@@ -0,0 +1,259 @@
1
+ function safeJson(obj) {
2
+ return JSON.stringify(obj)
3
+ .replace(/</g, "\\u003c")
4
+ .replace(/>/g, "\\u003e")
5
+ .replace(/&/g, "\\u0026");
6
+ }
7
+ function flattenSymbols(skeletons) {
8
+ const out = [];
9
+ const walk = (syms, file) => {
10
+ for (const s of syms) {
11
+ out.push({ name: s.name, kind: s.kind, file, startLine: s.range.startLine, endLine: s.range.endLine, exported: s.exported ?? false });
12
+ if (s.children.length)
13
+ walk(s.children, file);
14
+ }
15
+ };
16
+ for (const sk of skeletons)
17
+ walk(sk.symbols, sk.file);
18
+ return out;
19
+ }
20
+ const KIND_COLORS = {
21
+ class: "#7c3aed", interface: "#0ea5e9", struct: "#0d9488",
22
+ function: "#2563eb", method: "#4f46e5", type: "#db2777",
23
+ enum: "#ea580c", const: "#65a30d", var: "#ca8a04",
24
+ field: "#64748b", namespace: "#9333ea",
25
+ };
26
+ export function buildDashboardHtml(reportHtml, skeletonHtml, explorerHtml, skeletons, title, liveReloadPort) {
27
+ const symbols = flattenSymbols(skeletons);
28
+ const totalSymbols = skeletons.reduce((n, s) => n + s.symbolCount, 0);
29
+ const tabsData = safeJson({ overview: reportHtml, files: skeletonHtml, graph: explorerHtml });
30
+ const symData = safeJson(symbols);
31
+ const kindColorsData = safeJson(KIND_COLORS);
32
+ const liveReloadScript = liveReloadPort
33
+ ? `<script>
34
+ (function(){
35
+ var es=new EventSource('http://localhost:${liveReloadPort ?? 0}/events');
36
+ es.addEventListener('reload',function(){location.reload();});
37
+ es.onerror=function(){setTimeout(function(){location.reload();},2000);};
38
+ })();
39
+ </script>`
40
+ : "";
41
+ return `<!doctype html>
42
+ <html lang="en">
43
+ <head>
44
+ <meta charset="utf-8">
45
+ <meta name="viewport" content="width=device-width,initial-scale=1">
46
+ <title>AST Map Dashboard — ${title}</title>
47
+ <style>
48
+ :root{color-scheme:light dark;--bg:#0d1117;--bg2:#161b22;--fg:#e6edf3;--fg2:#7d8590;--bdr:#21262d;--accent:#6366f1;--accent2:#8b5cf6;--tab-h:44px;}
49
+ @media(prefers-color-scheme:light){:root{--bg:#f6f8fa;--bg2:#fff;--fg:#0f172a;--fg2:#64748b;--bdr:#e2e8f0;}}
50
+ *{box-sizing:border-box;margin:0;padding:0;}
51
+ body{font:13px/1.5 ui-sans-serif,system-ui,sans-serif;background:var(--bg);color:var(--fg);display:flex;flex-direction:column;height:100vh;overflow:hidden;}
52
+ /* Topbar */
53
+ .topbar{display:flex;align-items:center;gap:10px;padding:0 16px;height:48px;background:var(--bg2);border-bottom:1px solid var(--bdr);flex-shrink:0;box-shadow:0 1px 3px rgba(0,0,0,.15);}
54
+ .topbar-logo{display:flex;align-items:center;gap:7px;font-weight:700;font-size:14px;color:var(--accent);text-decoration:none;}
55
+ .topbar-sep{width:1px;height:18px;background:var(--bdr);}
56
+ .topbar-title{font-size:13px;font-weight:600;}
57
+ .topbar-meta{font-size:11px;color:var(--fg2);flex:1;}
58
+ .live-dot{width:7px;height:7px;border-radius:50%;background:#22c55e;display:${liveReloadPort ? "inline-block" : "none"};animation:pulse 2s infinite;}
59
+ @keyframes pulse{0%,100%{opacity:1;}50%{opacity:.4;}}
60
+ /* Tabs */
61
+ .tabs{display:flex;align-items:stretch;background:var(--bg2);border-bottom:1px solid var(--bdr);flex-shrink:0;height:var(--tab-h);padding:0 12px;gap:2px;}
62
+ .tab-btn{font:13px/1 ui-sans-serif,sans-serif;cursor:pointer;border:none;background:transparent;color:var(--fg2);padding:0 14px;border-bottom:2px solid transparent;transition:color .15s,border-color .15s;display:flex;align-items:center;gap:6px;font-weight:500;}
63
+ .tab-btn:hover{color:var(--fg);}
64
+ .tab-btn.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600;}
65
+ .tab-icon{font-size:14px;}
66
+ /* Content */
67
+ .tab-content{flex:1;display:none;min-height:0;}
68
+ .tab-content.active{display:flex;flex-direction:column;}
69
+ iframe{border:none;flex:1;width:100%;height:100%;}
70
+ /* Symbols tab */
71
+ .sym-pane{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden;}
72
+ .sym-toolbar{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid var(--bdr);background:var(--bg2);flex-shrink:0;flex-wrap:wrap;}
73
+ .sym-search{font:12px ui-monospace,monospace;padding:5px 10px;border:1px solid var(--bdr);border-radius:8px;background:var(--bg);color:var(--fg);outline:none;width:220px;transition:border-color .15s;}
74
+ .sym-search:focus{border-color:var(--accent);}
75
+ .kind-sel{font:12px ui-sans-serif,sans-serif;padding:4px 8px;border:1px solid var(--bdr);border-radius:8px;background:var(--bg);color:var(--fg);outline:none;cursor:pointer;}
76
+ .sym-count{font-size:11px;color:var(--fg2);margin-left:auto;}
77
+ .sym-table-wrap{flex:1;overflow-y:auto;scrollbar-width:thin;}
78
+ table{width:100%;border-collapse:collapse;font-size:12px;}
79
+ th{position:sticky;top:0;background:var(--bg2);border-bottom:2px solid var(--bdr);padding:7px 12px;text-align:left;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--fg2);cursor:pointer;user-select:none;white-space:nowrap;}
80
+ th:hover{color:var(--fg);}
81
+ th .sort-arrow{opacity:.5;margin-left:4px;}
82
+ td{padding:5px 12px;border-bottom:1px solid var(--bdr);vertical-align:middle;}
83
+ tr:hover td{background:color-mix(in srgb,var(--accent) 4%,var(--bg));}
84
+ .mono{font-family:ui-monospace,monospace;font-weight:600;}
85
+ .kind-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:999px;letter-spacing:.04em;}
86
+ .exp-badge{font-size:10px;color:#16a34a;background:#dcfce7;padding:1px 6px;border-radius:5px;}
87
+ .file-cell{color:var(--fg2);font-size:11px;font-family:ui-monospace,monospace;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
88
+ .line-cell{color:var(--fg2);font-family:ui-monospace,monospace;font-size:11px;white-space:nowrap;}
89
+ .no-results{padding:48px;text-align:center;color:var(--fg2);}
90
+ @media(prefers-color-scheme:dark){.exp-badge{background:#14532d;color:#4ade80;}}
91
+ </style>
92
+ </head>
93
+ <body>
94
+ ${liveReloadScript}
95
+ <header class="topbar">
96
+ <div class="topbar-logo">
97
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
98
+ AST Map
99
+ </div>
100
+ <div class="topbar-sep"></div>
101
+ <span class="topbar-title">${title}</span>
102
+ <span class="topbar-meta">${skeletons.length} files &middot; ${totalSymbols} symbols</span>
103
+ <span class="live-dot" title="Live reload active"></span>
104
+ </header>
105
+
106
+ <nav class="tabs">
107
+ <button class="tab-btn active" data-tab="overview"><span class="tab-icon">📊</span>Overview</button>
108
+ <button class="tab-btn" data-tab="files"><span class="tab-icon">📁</span>Files</button>
109
+ <button class="tab-btn" data-tab="graph"><span class="tab-icon">🕸</span>Dependencies</button>
110
+ <button class="tab-btn" data-tab="symbols"><span class="tab-icon">⬡</span>Symbols</button>
111
+ </nav>
112
+
113
+ <div id="tc-overview" class="tab-content active">
114
+ <iframe id="fr-overview" title="Overview"></iframe>
115
+ </div>
116
+ <div id="tc-files" class="tab-content">
117
+ <iframe id="fr-files" title="Files"></iframe>
118
+ </div>
119
+ <div id="tc-graph" class="tab-content">
120
+ <iframe id="fr-graph" title="Dependencies"></iframe>
121
+ </div>
122
+ <div id="tc-symbols" class="tab-content">
123
+ <div class="sym-pane">
124
+ <div class="sym-toolbar">
125
+ <input class="sym-search" id="sym-q" type="search" placeholder="Search symbols…" autocomplete="off">
126
+ <select class="kind-sel" id="kind-filter">
127
+ <option value="">All kinds</option>
128
+ <option>class</option><option>interface</option><option>struct</option>
129
+ <option>function</option><option>method</option><option>type</option>
130
+ <option>enum</option><option>const</option><option>var</option>
131
+ <option>field</option><option>namespace</option>
132
+ </select>
133
+ <label style="font-size:11px;color:var(--fg2);display:flex;align-items:center;gap:5px;">
134
+ <input type="checkbox" id="exp-only"> Exported only
135
+ </label>
136
+ <span class="sym-count" id="sym-count"></span>
137
+ </div>
138
+ <div class="sym-table-wrap">
139
+ <table>
140
+ <thead>
141
+ <tr>
142
+ <th data-col="name">Symbol <span class="sort-arrow" id="sa-name"></span></th>
143
+ <th data-col="kind">Kind <span class="sort-arrow" id="sa-kind"></span></th>
144
+ <th data-col="file">File <span class="sort-arrow" id="sa-file"></span></th>
145
+ <th data-col="startLine">Line <span class="sort-arrow" id="sa-startLine"></span></th>
146
+ <th>Export</th>
147
+ </tr>
148
+ </thead>
149
+ <tbody id="sym-tbody"></tbody>
150
+ </table>
151
+ <div id="sym-empty" class="no-results" style="display:none">No symbols match.</div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <script>
157
+ (function(){
158
+ 'use strict';
159
+ const TABS=${tabsData};
160
+ const SYMS=${symData};
161
+ const KIND_COLORS=${kindColorsData};
162
+
163
+ // ── Tab switching ───────────────────────────────────────────
164
+ const loaded={};
165
+
166
+ function showTab(name){
167
+ document.querySelectorAll('.tab-btn').forEach(b=>b.classList.toggle('active',b.dataset.tab===name));
168
+ document.querySelectorAll('.tab-content').forEach(tc=>tc.classList.toggle('active',tc.id==='tc-'+name));
169
+ if(name!=='symbols'){
170
+ if(!loaded[name]){
171
+ const fr=document.getElementById('fr-'+name);
172
+ if(fr){fr.srcdoc=TABS[name]||'';loaded[name]=true;}
173
+ }
174
+ }else{
175
+ renderSymbols();
176
+ }
177
+ }
178
+
179
+ document.querySelectorAll('.tab-btn').forEach(btn=>{
180
+ btn.addEventListener('click',()=>showTab(btn.dataset.tab));
181
+ });
182
+
183
+ // Load overview immediately
184
+ showTab('overview');
185
+
186
+ // ── Symbols table ───────────────────────────────────────────
187
+ let sortCol='name',sortAsc=true,filtered=[];
188
+
189
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
190
+
191
+ function renderSymbols(){
192
+ const q=document.getElementById('sym-q').value.trim().toLowerCase();
193
+ const kind=document.getElementById('kind-filter').value;
194
+ const expOnly=document.getElementById('exp-only').checked;
195
+
196
+ filtered=SYMS.filter(s=>{
197
+ if(kind&&s.kind!==kind)return false;
198
+ if(expOnly&&!s.exported)return false;
199
+ if(q&&!s.name.toLowerCase().includes(q)&&!s.file.toLowerCase().includes(q))return false;
200
+ return true;
201
+ });
202
+
203
+ filtered.sort((a,b)=>{
204
+ const va=String(a[sortCol]),vb=String(b[sortCol]);
205
+ if(sortCol==='startLine')return sortAsc?(a.startLine-b.startLine):(b.startLine-a.startLine);
206
+ return sortAsc?va.localeCompare(vb):vb.localeCompare(va);
207
+ });
208
+
209
+ const tbody=document.getElementById('sym-tbody');
210
+ const c=KIND_COLORS;
211
+ tbody.innerHTML=filtered.slice(0,500).map(s=>{
212
+ const col=c[s.kind]||'#64748b';
213
+ return \`<tr>
214
+ <td class="mono">\${esc(s.name)}</td>
215
+ <td><span class="kind-badge" style="background:\${col}1a;color:\${col};border:1px solid \${col}44">\${s.kind}</span></td>
216
+ <td class="file-cell" title="\${esc(s.file)}">\${esc(s.file)}</td>
217
+ <td class="line-cell">L\${s.startLine}</td>
218
+ <td>\${s.exported?'<span class="exp-badge">exp</span>':''}</td>
219
+ </tr>\`;
220
+ }).join('');
221
+
222
+ const extra=filtered.length>500?' (showing 500 of '+filtered.length+')'+'':'';
223
+ document.getElementById('sym-count').textContent=filtered.length+' symbol(s)'+extra;
224
+ document.getElementById('sym-empty').style.display=filtered.length?'none':'block';
225
+ tbody.style.display=filtered.length?'':'none';
226
+
227
+ // Update sort arrows
228
+ ['name','kind','file','startLine'].forEach(col=>{
229
+ const el=document.getElementById('sa-'+col);
230
+ if(el)el.textContent=col===sortCol?(sortAsc?'↑':'↓'):'';
231
+ });
232
+ }
233
+
234
+ document.getElementById('sym-q').addEventListener('input',renderSymbols);
235
+ document.getElementById('kind-filter').addEventListener('change',renderSymbols);
236
+ document.getElementById('exp-only').addEventListener('change',renderSymbols);
237
+ document.querySelectorAll('th[data-col]').forEach(th=>{
238
+ th.addEventListener('click',()=>{
239
+ const col=th.dataset.col;
240
+ if(sortCol===col)sortAsc=!sortAsc;else{sortCol=col;sortAsc=true;}
241
+ renderSymbols();
242
+ });
243
+ });
244
+
245
+ // Keyboard shortcut
246
+ document.addEventListener('keydown',ev=>{
247
+ if(ev.key==='/'&&document.activeElement?.tagName!=='INPUT'&&document.activeElement?.tagName!=='SELECT'){
248
+ const q=document.getElementById('sym-q');
249
+ if(document.getElementById('tc-symbols').classList.contains('active')){
250
+ ev.preventDefault();q.focus();q.select();
251
+ }
252
+ }
253
+ });
254
+
255
+ })();
256
+ </script>
257
+ </body>
258
+ </html>`;
259
+ }