-
-
Save shricodev/b444308d4330b1982d1e6ce1e5fb3cf3 to your computer and use it in GitHub Desktop.
o3 - Blog Dependency Visualizer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>NPM Dependency‑Graph Visualizer</title> | |
<!-- Basic page styling --> | |
<style> | |
* { | |
box-sizing: border-box; | |
font-family: Arial, Helvetica, sans-serif; | |
} | |
body { | |
margin: 0; | |
height: 100vh; | |
display: flex; | |
flex-direction: column; | |
} | |
header { | |
padding: 10px; | |
background: #24292f; | |
color: #fff; | |
} | |
header h1 { | |
margin: 0; | |
font-size: 18px; | |
} | |
#controls { | |
display: flex; | |
gap: 10px; | |
margin-top: 10px; | |
} | |
textarea { | |
flex: 1; | |
height: 140px; | |
width: 100%; | |
resize: vertical; | |
padding: 8px; | |
font-family: monospace; | |
} | |
#fileInput { | |
display: inline-block; | |
} | |
button { | |
padding: 8px 14px; | |
font-size: 14px; | |
cursor: pointer; | |
} | |
#status { | |
margin-left: auto; | |
color: #0dd; | |
} | |
/* graph area */ | |
#graphContainer { | |
flex: 1; | |
min-height: 200px; | |
} | |
svg { | |
width: 100%; | |
height: 100%; | |
background: #fafafa; | |
cursor: move; | |
} | |
.link { | |
stroke: #999; | |
stroke-opacity: 0.6; | |
} | |
.node circle { | |
stroke: #fff; | |
stroke-width: 1.5px; | |
cursor: pointer; | |
} | |
.node text { | |
font-size: 10px; | |
pointer-events: none; | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<h1>NPM Dependency‑Graph Visualizer (Pure HTML/CSS/JS)</h1> | |
<div id="controls"> | |
<input type="file" id="fileInput" accept="application/json" /> | |
<button id="exampleBtn">Load example</button> | |
<button id="buildBtn">Build graph</button> | |
<span id="status"></span> | |
</div> | |
</header> | |
<textarea | |
id="pkgTextarea" | |
placeholder="Paste your package.json here…" | |
></textarea> | |
<div id="graphContainer"></div> | |
<!-- D3.js (force‑simulation + zoom/pan) --> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<script> | |
/* ---------- helper UI bits ---------- */ | |
const $ = (id) => document.getElementById(id); | |
const status = (msg) => ($("status").textContent = msg); | |
$("fileInput").addEventListener("change", (e) => { | |
const file = e.target.files[0]; | |
if (!file) return; | |
const reader = new FileReader(); | |
reader.onload = (ev) => ($("pkgTextarea").value = ev.target.result); | |
reader.readAsText(file); | |
}); | |
$("exampleBtn").addEventListener("click", () => { | |
$("pkgTextarea").value = `{ | |
"name": "sample-app", | |
"version": "1.0.0", | |
"dependencies": { | |
"express": "^4.18.2", | |
"chalk": "^5.3.0" | |
}, | |
"devDependencies": { | |
"eslint": "^8.55.0" | |
} | |
}`; | |
}); | |
$("buildBtn").addEventListener("click", async () => { | |
const txt = $("pkgTextarea").value.trim(); | |
if (!txt) { | |
alert("Paste or choose a package.json first."); | |
return; | |
} | |
let pkg; | |
try { | |
pkg = JSON.parse(txt); | |
} catch (e) { | |
alert("Not valid JSON"); | |
return; | |
} | |
try { | |
status("Resolving…"); | |
const graph = await buildGraph(pkg); | |
status(`Fetched ${graph.nodes.length} pkgs`); | |
drawGraph(graph); | |
} catch (e) { | |
console.error(e); | |
alert("Error while building graph – see console"); | |
status(""); | |
} | |
}); | |
/* ---------- dependency resolution (npm registry) ---------- */ | |
async function fetchMeta(name) { | |
const res = await fetch(`https://registry.npmjs.org/${name}`); | |
if (!res.ok) throw new Error(`HTTP ${res.status} for ${name}`); | |
return res.json(); | |
} | |
async function buildGraph(rootPkg) { | |
// Merge dependencies & devDependencies | |
const rootDeps = Object.assign( | |
{}, | |
rootPkg.dependencies || {}, | |
rootPkg.devDependencies || {}, | |
); | |
const nodes = new Map(); // key -> node {id} | |
const links = []; // edge objects | |
const queue = []; // BFS queue | |
const visited = new Set(); // package names already resolved | |
for (const dep in rootDeps) | |
queue.push({ parent: rootPkg.name || "root", name: dep }); | |
nodes.set(rootPkg.name || "root", { id: rootPkg.name || "root" }); | |
// Fetch depth‑first with a cap to avoid runaway explosions | |
const MAX_NODES = 400; | |
while (queue.length && nodes.size < MAX_NODES) { | |
const { parent, name } = queue.shift(); | |
links.push({ source: parent, target: name }); | |
if (visited.has(name)) continue; // already fully resolved | |
visited.add(name); | |
nodes.set(name, { id: name }); | |
try { | |
const meta = await fetchMeta(name); | |
const latest = meta["dist-tags"]?.latest; | |
const deps = meta.versions?.[latest]?.dependencies || {}; | |
for (const child in deps) { | |
queue.push({ parent: name, name: child }); | |
} | |
} catch (err) { | |
console.warn("Could not fetch", name, err); | |
} | |
} | |
return { nodes: [...nodes.values()], links }; | |
} | |
/* ---------- graph drawing (D3) ---------- */ | |
function drawGraph({ nodes, links }) { | |
// Clear existing svg | |
d3.select("#graphContainer").selectAll("*").remove(); | |
const width = $("graphContainer").clientWidth; | |
const height = $("graphContainer").clientHeight || 500; | |
const svg = d3 | |
.select("#graphContainer") | |
.append("svg") | |
.attr("viewBox", [0, 0, width, height]); | |
// Zoom / pan | |
const g = svg.append("g"); | |
svg.call( | |
d3 | |
.zoom() | |
.scaleExtent([0.1, 4]) | |
.on("zoom", (event) => g.attr("transform", event.transform)), | |
); | |
// links | |
const link = g | |
.append("g") | |
.attr("stroke", "#999") | |
.attr("stroke-opacity", 0.6) | |
.selectAll("line") | |
.data(links) | |
.join("line") | |
.attr("class", "link") | |
.attr("stroke-width", 1.2); | |
// nodes | |
const node = g | |
.append("g") | |
.selectAll("g") | |
.data(nodes) | |
.join("g") | |
.attr("class", "node") | |
.call(drag()); | |
node | |
.append("circle") | |
.attr("r", 8) | |
.attr("fill", (d) => (d.id === "root" ? "#ff4136" : "#1f77b4")); | |
node | |
.append("text") | |
.text((d) => d.id) | |
.attr("x", 10) | |
.attr("y", 3); | |
// physics | |
const simulation = d3 | |
.forceSimulation(nodes) | |
.force( | |
"link", | |
d3 | |
.forceLink(links) | |
.id((d) => d.id) | |
.distance(50), | |
) | |
.force("charge", d3.forceManyBody().strength(-160)) | |
.force("center", d3.forceCenter(width / 2, height / 2)); | |
simulation.on("tick", () => { | |
link | |
.attr("x1", (d) => d.source.x) | |
.attr("y1", (d) => d.source.y) | |
.attr("x2", (d) => d.target.x) | |
.attr("y2", (d) => d.target.y); | |
node.attr("transform", (d) => `translate(${d.x},${d.y})`); | |
}); | |
// drag behaviour | |
function drag() { | |
function dragstarted(event, d) { | |
if (!event.active) simulation.alphaTarget(0.3).restart(); | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function dragged(event, d) { | |
d.fx = event.x; | |
d.fy = event.y; | |
} | |
function dragended(event, d) { | |
if (!event.active) simulation.alphaTarget(0); | |
d.fx = d.fy = null; | |
} | |
return d3 | |
.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended); | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment