The Problem
You just joined a team that owns a 500-file open source project. No docs. The original author left. You need to understand the codebase fast — what calls what, where the dependencies are, which files are the real entry points. Existing tools require server setups, language servers, or IDE plugins. You want something that runs in your browser, works offline, and lets you ask questions like "what happens when a user clicks submit?" without reading every file manually.
Inspired by GitNexus (19k+ stars) — client-side code intelligence with zero server dependencies.
The Solution
Build a browser-based tool that parses source code using Tree-sitter compiled to WASM, extracts functions, classes, and imports into a knowledge graph, visualizes it with D3 force-directed layouts, and supports Graph RAG queries so you can ask natural language questions about the codebase. Everything runs client-side — no server needed.
ZIP/Repo Upload (browser)
↓
Tree-sitter WASM Parser
↓
AST Extraction (functions, classes, imports)
↓
Knowledge Graph (nodes + edges)
↓
┌─────────────┬──────────────────┐
│ D3 Force │ Graph RAG │
│ Visualization│ (ask questions) │
└─────────────┴──────────────────┘
Step-by-Step Walkthrough
Step 1: Parse Code in the Browser with Tree-sitter WASM
import Parser from "web-tree-sitter";
async function initParser(language) {
await Parser.init();
const parser = new Parser();
const lang = await Parser.Language.load(`/tree-sitter-${language}.wasm`);
parser.setLanguage(lang);
return parser;
}
function extractSymbols(tree, sourceCode) {
const symbols = [];
const cursor = tree.walk();
function visit() {
const node = cursor.currentNode;
if (["function_declaration", "function_definition", "method_definition",
"class_declaration", "arrow_function"].includes(node.type)) {
const nameNode = node.childForFieldName("name");
symbols.push({
type: node.type.replace("_declaration", "").replace("_definition", ""),
name: nameNode?.text || "<anonymous>",
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
body: sourceCode.slice(node.startIndex, node.endIndex),
});
}
if (cursor.gotoFirstChild()) {
do { visit(); } while (cursor.gotoNextSibling());
cursor.gotoParent();
}
}
visit();
return symbols;
}
function extractImports(tree) {
const imports = [];
const cursor = tree.walk();
function visit() {
const node = cursor.currentNode;
if (node.type === "import_statement" || node.type === "import_declaration") {
const source = node.descendantsOfType("string")[0];
if (source) imports.push(source.text.replace(/['"]/g, ""));
}
if (cursor.gotoFirstChild()) {
do { visit(); } while (cursor.gotoNextSibling());
cursor.gotoParent();
}
}
visit();
return imports;
}
Step 2: Build the Knowledge Graph
class CodeGraph {
constructor() {
this.nodes = new Map(); // id -> {type, name, file, metadata}
this.edges = []; // {source, target, type}
}
addFile(filePath, symbols, imports) {
// File node
this.nodes.set(filePath, { type: "file", name: filePath, file: filePath });
// Symbol nodes
for (const sym of symbols) {
const id = `${filePath}::${sym.name}`;
this.nodes.set(id, { type: sym.type, name: sym.name, file: filePath, ...sym });
this.edges.push({ source: filePath, target: id, type: "defines" });
}
// Import edges
for (const imp of imports) {
const resolved = resolveImport(filePath, imp);
this.edges.push({ source: filePath, target: resolved, type: "imports" });
}
}
// Find what depends on a given file
dependentsOf(filePath) {
return this.edges
.filter(e => e.target === filePath && e.type === "imports")
.map(e => e.source);
}
// Find entry points (files nothing imports)
findEntryPoints() {
const imported = new Set(this.edges.filter(e => e.type === "imports").map(e => e.target));
return [...this.nodes.values()]
.filter(n => n.type === "file" && !imported.has(n.name));
}
// Subgraph around a node (for focused visualization)
neighborhood(nodeId, depth = 2) {
const visited = new Set([nodeId]);
let frontier = [nodeId];
for (let d = 0; d < depth; d++) {
const next = [];
for (const id of frontier) {
for (const edge of this.edges) {
const neighbor = edge.source === id ? edge.target : edge.target === id ? edge.source : null;
if (neighbor && !visited.has(neighbor)) {
visited.add(neighbor);
next.push(neighbor);
}
}
}
frontier = next;
}
return {
nodes: [...visited].map(id => this.nodes.get(id)).filter(Boolean),
edges: this.edges.filter(e => visited.has(e.source) && visited.has(e.target))
};
}
}
Step 3: Visualize with D3 Force-Directed Graph
import * as d3 from "d3";
function renderGraph(container, graph) {
const width = container.clientWidth, height = container.clientHeight;
const svg = d3.select(container).append("svg").attr("width", width).attr("height", height);
const colorMap = { file: "#4A90D9", function: "#50C878", class: "#FF6B6B", method: "#FFB347" };
const sim = d3.forceSimulation(graph.nodes)
.force("link", d3.forceLink(graph.edges).id(d => d.id).distance(80))
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width / 2, height / 2));
const link = svg.selectAll("line").data(graph.edges).join("line")
.attr("stroke", "#999").attr("stroke-opacity", 0.6);
const node = svg.selectAll("circle").data(graph.nodes).join("circle")
.attr("r", d => d.type === "file" ? 8 : 5)
.attr("fill", d => colorMap[d.type] || "#ccc")
.call(d3.drag().on("start", dragStart).on("drag", dragged).on("end", dragEnd));
node.append("title").text(d => `${d.type}: ${d.name}`);
sim.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("cx", d => d.x).attr("cy", d => d.y);
});
}
Step 4: Graph RAG — Ask Questions About Code
async function queryCode(graph, question) {
// Find relevant nodes via keyword matching
const keywords = question.toLowerCase().split(/\s+/);
const relevant = [...graph.nodes.values()].filter(n =>
keywords.some(k => n.name.toLowerCase().includes(k) ||
(n.body && n.body.toLowerCase().includes(k)))
);
// Expand to neighborhood for context
const contextNodes = new Set();
for (const node of relevant.slice(0, 5)) {
const hood = graph.neighborhood(`${node.file}::${node.name}`, 1);
hood.nodes.forEach(n => contextNodes.add(n));
}
const context = [...contextNodes].map(n =>
`[${n.type}] ${n.name} (${n.file}:${n.startLine || "?"})\n${(n.body || "").slice(0, 300)}`
).join("\n---\n");
// Query AI with graph context (use any API — or run locally with WebLLM)
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({
messages: [
{ role: "system", content: "Answer questions about code using the provided graph context. Reference specific files and functions." },
{ role: "user", content: `Graph context:\n${context}\n\nQuestion: ${question}` }
]
})
});
return response.json();
}
Real-World Example
You upload a 500-file React + Express project as a ZIP. The tool parses it in 8 seconds using Tree-sitter WASM, extracting 340 functions, 45 classes, and 890 import edges. The D3 visualization immediately reveals two large clusters (frontend and backend) connected through a thin API layer. You click on src/api/client.ts and see it's imported by 28 components — the central hub. You ask "what happens when a user submits a form?" and the Graph RAG returns: "FormComponent.handleSubmit (src/components/Form.tsx:42) calls apiClient.post (src/api/client.ts:15), which hits POST /api/submissions handled by createSubmission (server/routes/submissions.ts:23), writing to the database via SubmissionModel.create." In 10 minutes you have a mental map that would have taken days of code reading.
Related Skills
- gitnexus — Client-side code intelligence and repository analysis
- understand-chat — AI-powered code understanding through conversation
What You'll Learn
- Browser-based code parsing with Tree-sitter WASM
- Building and querying knowledge graphs in JavaScript
- D3 force-directed graph visualization
- Graph RAG: combining graph structure with LLM queries
- Zero-server architecture — everything runs client-side
- Understanding large codebases through structural analysis