Something went wrong. Try again.
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677/* eslint-disable @typescript-eslint/no-explicit-any */import { readFileSync, statSync } from 'node:fs';import { dirname, join, resolve } from 'node:path';import Parser from 'web-tree-sitter';import type { Language } from 'web-tree-sitter';import { adapterFor, grammarPath, runtimeWasmPath } from './language-registry';import { CodeIndexError, type ParsedFile } from './types';
export class ParseCache { private readonly entries = new Map<string, ParsedFile>(); private readonly grammars = new Map<string, Language>(); constructor(private readonly cwd: string) {}
async initialize(): Promise<void> { // Pin the emscripten runtime wasm (`tree-sitter.wasm`) to the release asset // shipped beside the binary. Without this, `web-tree-sitter`'s default // `locateFile` resolves it against the bundled module's `__dirname`, which a // `bun --compile` binary freezes at the BUILD-TIME path // (`…/node_modules/web-tree-sitter/`); on a deployed host that directory is // gone and `Parser.init()` aborts with ENOENT — killing every spawned agent. // `locateFile` receives only the bare filename (`tree-sitter.wasm`); return an // absolute path so `fs.readFileSync` reads the shipped asset regardless of cwd. const runtimeWasm = runtimeWasmPath(); await Parser.init({ locateFile: (file: string) => (file === 'tree-sitter.wasm' ? runtimeWasm : join(dirname(runtimeWasm), file)), }); for (const name of ['typescript', 'tsx', 'javascript', 'python', 'go', 'rust', 'html']) { this.grammars.set(name, await Parser.Language.load(grammarPath(name))); } }
get(inputPath: string): ParsedFile { const path = resolve(this.cwd, inputPath); const adapter = adapterFor(path); if (!adapter) throw new CodeIndexError('unsupported', `Unsupported language: ${path}`); let stats: ReturnType<typeof statSync>; try { stats = statSync(path); } catch { throw new CodeIndexError('missing', `File not found: ${path}`); } const fingerprint = `${stats.mtimeMs}:${stats.size}`; const current = this.entries.get(path); if (current?.fingerprint === fingerprint) return current; if (current) this.deleteTree(current); let source: string; try { source = readFileSync(path, 'utf8'); } catch { throw new CodeIndexError('unreadable', `Unable to read: ${path}`); } const grammar = this.grammars.get(adapter.grammarName); if (!grammar) throw new CodeIndexError('unsupported', `Grammar is not initialized: ${adapter.grammarName}`); const parser = new Parser(); parser.setLanguage(grammar); const tree = parser.parse(source); const topLevelGroups = (tree?.rootNode.namedChildren ?? []) .map((node: any) => adapter.symbols(node, source)); const symbols = topLevelGroups.flat(); const signatures = topLevelGroups .flatMap((group) => group.slice(0, 1)) .flatMap((symbol) => symbol.signature ? [symbol.signature] : []); parser.delete(); const parsed = { path, source, fingerprint, tree, symbols, signatures }; this.entries.set(path, parsed); return parsed; }
invalidate(inputPath: string): void { const path = resolve(this.cwd, inputPath); const entry = this.entries.get(path); if (entry) { this.deleteTree(entry); this.entries.delete(path); } }
clear(): void { for (const entry of this.entries.values()) this.deleteTree(entry); this.entries.clear(); }
private deleteTree(entry: ParsedFile): void { const tree = entry.tree; if (tree) { entry.tree = undefined; tree.delete?.(); } }}