universal-ast-mapper 1.6.0 → 1.7.0
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 +2 -0
- package/dist/cli.js +23 -0
- package/dist/explorer.js +69 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ ast-map complexity <path> [alias: cx] [--min N]
|
|
|
98
98
|
ast-map unused-params <path> [alias: unused]
|
|
99
99
|
ast-map trace-type <type> [dir] [alias: flow]
|
|
100
100
|
ast-map workspace [dir] [alias: ws]
|
|
101
|
+
ast-map explore [dir] [-o out.html]
|
|
101
102
|
ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
|
|
102
103
|
ast-map deps <file> [--scan <dir>]
|
|
103
104
|
ast-map top <dir> [-n 10]
|
|
@@ -617,6 +618,7 @@ Not part of the public API: the internal `src/` module layout and the generated
|
|
|
617
618
|
|
|
618
619
|
| Version | What changed |
|
|
619
620
|
|---------|--------------|
|
|
621
|
+
| **1.7.0** | **Web UI graph explorer** — `ast-map explore [dir]` writes a self-contained, dependency-free interactive HTML: a force-directed file dependency graph (drag, zoom, click-to-highlight neighbours, filter by name). No build step, no external scripts — just open it in a browser. |
|
|
620
622
|
| **1.6.0** | **MCP resource endpoints** — the server now exposes browseable resources: `ast://languages`, `ast://skeleton/{path}` (templated, one per source file via `resources/list`), and `ast://graph`. Agents can list and read codebase structure as resources, not just call tools. |
|
|
621
623
|
| **1.5.0** | **`.d.ts` / ambient declarations** — `declare function/const/class`, `declare module "x"`, and `declare namespace` (and plain `namespace`) are now extracted (previously a `.d.ts` yielded 0 symbols). Adds a `namespace` symbol kind; declared APIs surface as exported, nested under their module/namespace. |
|
|
622
624
|
| **1.4.0** | **Dynamic import tracking** — dynamic `import("...")` and CommonJS `require("...")` calls (anywhere in a file) are now captured as imports with an `isDynamic` flag. Relative dynamic imports resolve and draw graph edges like static ones, so lazy-loaded routes/modules show up in the dependency graph. |
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { computeFileComplexity } from "./complexity.js";
|
|
|
14
14
|
import { findUnusedParams } from "./unused-params.js";
|
|
15
15
|
import { traceTypeInFile } from "./typeflow.js";
|
|
16
16
|
import { discoverWorkspace, findPackageCycles } from "./workspace.js";
|
|
17
|
+
import { buildExplorerHtml } from "./explorer.js";
|
|
17
18
|
import { buildCallGraph } from "./callgraph.js";
|
|
18
19
|
import { searchSymbols } from "./search.js";
|
|
19
20
|
const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
|
|
@@ -370,6 +371,28 @@ program
|
|
|
370
371
|
}
|
|
371
372
|
console.log();
|
|
372
373
|
});
|
|
374
|
+
// ─── Command: explore ─────────────────────────────────────────────────────────
|
|
375
|
+
program
|
|
376
|
+
.command("explore [dir]")
|
|
377
|
+
.description("Generate an interactive HTML dependency-graph explorer")
|
|
378
|
+
.option("-o, --out <file>", "Output HTML path")
|
|
379
|
+
.action(async (dir, opts) => {
|
|
380
|
+
const { abs, rel } = resolveArg(dir ?? ".");
|
|
381
|
+
if (!fs.statSync(abs).isDirectory())
|
|
382
|
+
die(`"${rel}" is not a directory`);
|
|
383
|
+
const skeletons = await gatherSkeletons(abs);
|
|
384
|
+
const graph = buildSymbolGraph(skeletons, ROOT);
|
|
385
|
+
const html = buildExplorerHtml(graph, abs);
|
|
386
|
+
const outPath = opts.out
|
|
387
|
+
? path.resolve(process.cwd(), opts.out)
|
|
388
|
+
: path.join(abs, "ast-explorer.html");
|
|
389
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
390
|
+
fs.writeFileSync(outPath, html, "utf8");
|
|
391
|
+
header(`Graph Explorer — ${rel}/ ${dim(`(${skeletons.length} files)`)}`);
|
|
392
|
+
console.log(indent(green("✓ wrote " + path.relative(process.cwd(), outPath))));
|
|
393
|
+
console.log(indent(dim("open it in a browser — drag nodes, scroll to zoom, click to highlight, filter by name")));
|
|
394
|
+
console.log();
|
|
395
|
+
});
|
|
373
396
|
// ─── Command: workspace ───────────────────────────────────────────────────────
|
|
374
397
|
program
|
|
375
398
|
.command("workspace [dir]")
|
package/dist/explorer.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Derive a file-level dependency graph (nodes = files, edges = imports). */
|
|
2
|
+
function deriveFileGraph(graph) {
|
|
3
|
+
const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
4
|
+
const nodes = [];
|
|
5
|
+
for (const n of graph.nodes) {
|
|
6
|
+
if (n.nodeType !== "file")
|
|
7
|
+
continue;
|
|
8
|
+
const f = n;
|
|
9
|
+
const parts = f.id.split("/");
|
|
10
|
+
nodes.push({ id: f.id, symbols: f.symbolCount, group: parts.length > 1 ? parts[0] : "(root)", lang: f.language });
|
|
11
|
+
}
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const links = [];
|
|
14
|
+
for (const e of graph.edges) {
|
|
15
|
+
if (e.edgeType !== "imports")
|
|
16
|
+
continue;
|
|
17
|
+
const to = nodeMap.get(e.to);
|
|
18
|
+
const toFile = to ? (to.nodeType === "file" ? to.id : to.file) : null;
|
|
19
|
+
if (!toFile || e.from === toFile)
|
|
20
|
+
continue;
|
|
21
|
+
const key = e.from + "|" + toFile;
|
|
22
|
+
if (seen.has(key))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(key);
|
|
25
|
+
links.push({ source: e.from, target: toFile });
|
|
26
|
+
}
|
|
27
|
+
return { nodes, links };
|
|
28
|
+
}
|
|
29
|
+
const STYLE = "body{margin:0;font-family:system-ui,sans-serif;color:#222;background:#fafafa}" +
|
|
30
|
+
"#bar{position:fixed;top:0;left:0;right:0;height:48px;display:flex;align-items:center;gap:12px;padding:0 14px;background:#fff;border-bottom:1px solid #e5e5e5;z-index:2;box-sizing:border-box}" +
|
|
31
|
+
"#bar h1{font-size:14px;margin:0;font-weight:600}#bar .muted{color:#888;font-size:12px}" +
|
|
32
|
+
"#q{flex:0 0 220px;padding:6px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px}" +
|
|
33
|
+
"#cv{position:fixed;top:48px;left:0;right:0;bottom:0;display:block;cursor:grab}" +
|
|
34
|
+
"#tip{position:fixed;pointer-events:none;background:#222;color:#fff;font-size:12px;padding:4px 8px;border-radius:5px;display:none;z-index:3}" +
|
|
35
|
+
"@media(prefers-color-scheme:dark){body{color:#ddd;background:#161616}#bar{background:#1e1e1e;border-color:#333}#q{background:#2a2a2a;border-color:#444;color:#ddd}}";
|
|
36
|
+
const CLIENT = "var c=document.getElementById('cv'),ctx=c.getContext('2d'),tip=document.getElementById('tip');" +
|
|
37
|
+
"var W,H;function resize(){var r=devicePixelRatio||1;W=c.clientWidth;H=c.clientHeight;c.width=W*r;c.height=H*r;ctx.setTransform(r,0,0,r,0,0);}addEventListener('resize',resize);resize();" +
|
|
38
|
+
"var nodes=DATA.nodes,links=DATA.links,byId={};nodes.forEach(function(n){n.x=Math.random()*W;n.y=Math.random()*H;n.vx=0;n.vy=0;byId[n.id]=n;});" +
|
|
39
|
+
"var groups={},gi=0;function color(g){if(groups[g]==null)groups[g]=gi++;return 'hsl('+((groups[g]*67)%360)+',58%,55%)';}" +
|
|
40
|
+
"var adj={};links.forEach(function(l){(adj[l.source]=adj[l.source]||[]).push(l.target);(adj[l.target]=adj[l.target]||[]).push(l.source);});" +
|
|
41
|
+
"var view={x:0,y:0,k:1},sel=null,hover=null,drag=null,pan=null,q='';" +
|
|
42
|
+
"function radius(n){return 4+Math.sqrt(n.symbols||0)*1.7;}" +
|
|
43
|
+
"function tick(){var k=0.0007;for(var i=0;i<nodes.length;i++){var a=nodes[i];a.vx+=(W/2-a.x)*k;a.vy+=(H/2-a.y)*k;for(var j=i+1;j<nodes.length;j++){var b=nodes[j];var dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2),f=1400/d2,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;}}" +
|
|
44
|
+
"links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)+0.01,f=(d-95)*0.012,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;});" +
|
|
45
|
+
"for(var i=0;i<nodes.length;i++){var n=nodes[i];if(n===drag)continue;n.vx*=0.86;n.vy*=0.86;n.x+=n.vx;n.y+=n.vy;}}" +
|
|
46
|
+
"function draw(){ctx.clearRect(0,0,W,H);ctx.save();ctx.translate(view.x,view.y);ctx.scale(view.k,view.k);ctx.lineWidth=0.7;" +
|
|
47
|
+
"links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var on=sel&&(l.source===sel.id||l.target===sel.id);ctx.strokeStyle=on?'rgba(110,110,240,0.85)':'rgba(140,140,140,0.16)';ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();});" +
|
|
48
|
+
"nodes.forEach(function(n){var dim=(sel&&n!==sel&&(adj[sel.id]||[]).indexOf(n.id)<0)||(q&&n.id.toLowerCase().indexOf(q)<0);ctx.globalAlpha=dim?0.16:1;ctx.beginPath();ctx.arc(n.x,n.y,radius(n),0,6.2832);ctx.fillStyle=color(n.group);ctx.fill();if(n===sel||n===hover){ctx.lineWidth=2;ctx.strokeStyle='#111';ctx.stroke();ctx.lineWidth=0.7;}});" +
|
|
49
|
+
"ctx.globalAlpha=1;ctx.fillStyle=getComputedStyle(document.body).color;ctx.font='11px system-ui';nodes.forEach(function(n){if(n===sel||n===hover||n.symbols>=14){ctx.fillText(n.id.split('/').pop(),n.x+radius(n)+3,n.y+3);}});ctx.restore();}" +
|
|
50
|
+
"function loop(){tick();tick();draw();requestAnimationFrame(loop);}" +
|
|
51
|
+
"function world(e){return{x:(e.clientX-view.x)/view.k,y:(e.clientY-48-view.y)/view.k};}" +
|
|
52
|
+
"function pick(p){for(var i=nodes.length-1;i>=0;i--){var n=nodes[i];if((p.x-n.x)*(p.x-n.x)+(p.y-n.y)*(p.y-n.y)<=radius(n)*radius(n)+12)return n;}return null;}" +
|
|
53
|
+
"c.addEventListener('mousedown',function(e){var n=pick(world(e));if(n){drag=n;sel=n;}else{pan={x:e.clientX-view.x,y:e.clientY-view.y};sel=null;}});" +
|
|
54
|
+
"addEventListener('mousemove',function(e){var p=world(e);if(drag){drag.x=p.x;drag.y=p.y;drag.vx=0;drag.vy=0;}else if(pan){view.x=e.clientX-pan.x;view.y=e.clientY-pan.y;}else{hover=pick(p);if(hover){tip.style.display='block';tip.style.left=(e.clientX+12)+'px';tip.style.top=(e.clientY+12)+'px';tip.textContent=hover.id+' · '+hover.symbols+' symbols · '+hover.lang;}else tip.style.display='none';}});" +
|
|
55
|
+
"addEventListener('mouseup',function(){drag=null;pan=null;});" +
|
|
56
|
+
"c.addEventListener('wheel',function(e){e.preventDefault();var s=e.deltaY<0?1.1:0.9;var mx=e.clientX,my=e.clientY-48;view.x=mx-(mx-view.x)*s;view.y=my-(my-view.y)*s;view.k*=s;},{passive:false});" +
|
|
57
|
+
"document.getElementById('q').addEventListener('input',function(e){q=e.target.value.toLowerCase();});loop();";
|
|
58
|
+
/** Build a self-contained, dependency-free HTML graph explorer. */
|
|
59
|
+
export function buildExplorerHtml(graph, root) {
|
|
60
|
+
const data = deriveFileGraph(graph);
|
|
61
|
+
const dataJson = JSON.stringify(data);
|
|
62
|
+
const title = root.split(/[\\/]/).filter(Boolean).pop() || "project";
|
|
63
|
+
return ("<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" +
|
|
64
|
+
"<title>AST-MCP — " + title + " graph</title><style>" + STYLE + "</style></head><body>" +
|
|
65
|
+
"<div id=\"bar\"><h1>AST-MCP graph</h1><span class=\"muted\">" + data.nodes.length + " files · " + data.links.length + " edges · drag / scroll / click</span>" +
|
|
66
|
+
"<input id=\"q\" placeholder=\"filter files…\" /></div>" +
|
|
67
|
+
"<canvas id=\"cv\"></canvas><div id=\"tip\"></div>" +
|
|
68
|
+
"<script>var DATA=" + dataJson + ";</script><script>" + CLIENT + "</script></body></html>");
|
|
69
|
+
}
|
package/package.json
CHANGED