lattice-graph 0.1.0 → 0.3.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.
@@ -0,0 +1,211 @@
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import type {
4
+ CallHierarchyItem,
5
+ CallHierarchyOutgoingCall,
6
+ DocumentSymbol,
7
+ JsonRpcMessage,
8
+ Location,
9
+ } from "./types.ts";
10
+
11
+ /** Options for creating an LSP client. */
12
+ type LspClientOptions = {
13
+ readonly command: string;
14
+ readonly args: readonly string[];
15
+ readonly rootUri: string;
16
+ readonly languageId?: string;
17
+ };
18
+
19
+ /** An LSP client that communicates with a language server over stdio. */
20
+ type LspClient = {
21
+ /** Waits for the server to be ready by probing a known file. */
22
+ waitForReady(filePath: string): Promise<void>;
23
+ /** Returns all symbols in a file. */
24
+ documentSymbol(filePath: string): Promise<readonly DocumentSymbol[]>;
25
+ /** Prepares a call hierarchy item at a position. */
26
+ prepareCallHierarchy(
27
+ filePath: string,
28
+ line: number,
29
+ character: number,
30
+ ): Promise<readonly CallHierarchyItem[]>;
31
+ /** Returns outgoing calls from a call hierarchy item. */
32
+ outgoingCalls(item: CallHierarchyItem): Promise<readonly CallHierarchyOutgoingCall[]>;
33
+ /** Returns all reference locations for a symbol at a position. */
34
+ references(filePath: string, line: number, character: number): Promise<readonly Location[]>;
35
+ /** Shuts down the language server and kills the process. */
36
+ shutdown(): Promise<void>;
37
+ };
38
+
39
+ /**
40
+ * Creates an LSP client by spawning a language server process.
41
+ * Sends initialize/initialized, waits for readiness, then returns the client handle.
42
+ *
43
+ * @param opts - Server command, args, and project root URI
44
+ * @returns A ready LSP client
45
+ */
46
+ async function createLspClient(opts: LspClientOptions): Promise<LspClient> {
47
+ let nextId = 1;
48
+ const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
49
+ let buffer = "";
50
+ const openedFiles = new Set<string>();
51
+
52
+ const proc: ChildProcess = spawn(opts.command, [...opts.args], {
53
+ stdio: ["pipe", "pipe", "pipe"],
54
+ });
55
+
56
+ if (!proc.stdin || !proc.stdout) {
57
+ throw new Error(`Failed to spawn ${opts.command}`);
58
+ }
59
+
60
+ // Parse incoming JSON-RPC messages from stdout
61
+ proc.stdout.on("data", (chunk: Buffer) => {
62
+ buffer += chunk.toString();
63
+ while (true) {
64
+ const headerEnd = buffer.indexOf("\r\n\r\n");
65
+ if (headerEnd === -1) break;
66
+
67
+ const header = buffer.slice(0, headerEnd);
68
+ const match = header.match(/Content-Length:\s*(\d+)/i);
69
+ if (!match) {
70
+ buffer = buffer.slice(headerEnd + 4);
71
+ continue;
72
+ }
73
+
74
+ const contentLength = Number.parseInt(match[1] as string, 10);
75
+ const bodyStart = headerEnd + 4;
76
+ if (buffer.length < bodyStart + contentLength) break;
77
+
78
+ const body = buffer.slice(bodyStart, bodyStart + contentLength);
79
+ buffer = buffer.slice(bodyStart + contentLength);
80
+
81
+ try {
82
+ const msg = JSON.parse(body) as JsonRpcMessage;
83
+ if (msg.id !== undefined && pending.has(msg.id)) {
84
+ const handler = pending.get(msg.id);
85
+ pending.delete(msg.id);
86
+ if (msg.error) {
87
+ handler?.reject(new Error(`LSP error: ${msg.error.message}`));
88
+ } else {
89
+ handler?.resolve(msg.result);
90
+ }
91
+ }
92
+ } catch {
93
+ // Ignore malformed messages
94
+ }
95
+ }
96
+ });
97
+
98
+ function sendRequest(method: string, params: unknown): Promise<unknown> {
99
+ const id = nextId++;
100
+ const msg: JsonRpcMessage = { jsonrpc: "2.0", id, method, params };
101
+ const body = JSON.stringify(msg);
102
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
103
+ proc.stdin?.write(header + body);
104
+
105
+ return new Promise((resolve, reject) => {
106
+ pending.set(id, { resolve, reject });
107
+ });
108
+ }
109
+
110
+ function sendNotification(method: string, params: unknown): void {
111
+ const msg = { jsonrpc: "2.0", method, params };
112
+ const body = JSON.stringify(msg);
113
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
114
+ proc.stdin?.write(header + body);
115
+ }
116
+
117
+ const langId = opts.languageId ?? "typescript";
118
+
119
+ function openFile(filePath: string): void {
120
+ const uri = `file://${filePath}`;
121
+ if (openedFiles.has(uri)) return;
122
+ openedFiles.add(uri);
123
+
124
+ const text = readFileSync(filePath, "utf-8");
125
+ sendNotification("textDocument/didOpen", {
126
+ textDocument: { uri, languageId: langId, version: 1, text },
127
+ });
128
+ }
129
+
130
+ // Initialize
131
+ await sendRequest("initialize", {
132
+ processId: process.pid,
133
+ rootUri: opts.rootUri,
134
+ capabilities: {
135
+ textDocument: {
136
+ documentSymbol: { hierarchicalDocumentSymbolSupport: true },
137
+ callHierarchy: { dynamicRegistration: false },
138
+ },
139
+ },
140
+ });
141
+
142
+ sendNotification("initialized", {});
143
+
144
+ const client: LspClient = {
145
+ async waitForReady(probePath: string): Promise<void> {
146
+ openFile(probePath);
147
+ const delays = [100, 200, 400, 800, 1600, 3200];
148
+ for (const delay of delays) {
149
+ const result = (await sendRequest("textDocument/documentSymbol", {
150
+ textDocument: { uri: `file://${probePath}` },
151
+ })) as readonly DocumentSymbol[] | null;
152
+ if (result && result.length > 0) return;
153
+ await new Promise((r) => setTimeout(r, delay));
154
+ }
155
+ },
156
+
157
+ async documentSymbol(filePath: string): Promise<readonly DocumentSymbol[]> {
158
+ openFile(filePath);
159
+ const result = await sendRequest("textDocument/documentSymbol", {
160
+ textDocument: { uri: `file://${filePath}` },
161
+ });
162
+ return (result as readonly DocumentSymbol[]) ?? [];
163
+ },
164
+
165
+ async prepareCallHierarchy(
166
+ filePath: string,
167
+ line: number,
168
+ character: number,
169
+ ): Promise<readonly CallHierarchyItem[]> {
170
+ openFile(filePath);
171
+ const result = await sendRequest("textDocument/prepareCallHierarchy", {
172
+ textDocument: { uri: `file://${filePath}` },
173
+ position: { line, character },
174
+ });
175
+ return (result as readonly CallHierarchyItem[]) ?? [];
176
+ },
177
+
178
+ async outgoingCalls(item: CallHierarchyItem): Promise<readonly CallHierarchyOutgoingCall[]> {
179
+ const result = await sendRequest("callHierarchy/outgoingCalls", { item });
180
+ return (result as readonly CallHierarchyOutgoingCall[]) ?? [];
181
+ },
182
+
183
+ async references(
184
+ filePath: string,
185
+ line: number,
186
+ character: number,
187
+ ): Promise<readonly Location[]> {
188
+ openFile(filePath);
189
+ const result = await sendRequest("textDocument/references", {
190
+ textDocument: { uri: `file://${filePath}` },
191
+ position: { line, character },
192
+ context: { includeDeclaration: false },
193
+ });
194
+ return (result as readonly Location[]) ?? [];
195
+ },
196
+
197
+ async shutdown(): Promise<void> {
198
+ try {
199
+ await sendRequest("shutdown", null);
200
+ sendNotification("exit", null);
201
+ } catch {
202
+ // Server may already be dead
203
+ }
204
+ proc.kill();
205
+ },
206
+ };
207
+
208
+ return client;
209
+ }
210
+
211
+ export { createLspClient, type LspClient, type LspClientOptions };
@@ -0,0 +1,146 @@
1
+ import type { Node, NodeKind } from "../types/graph.ts";
2
+ import { type DocumentSymbol, SymbolKind } from "./types.ts";
3
+
4
+ /**
5
+ * Converts LSP DocumentSymbol responses into Lattice Node objects.
6
+ * Flattens the hierarchy and builds qualified names (e.g., ClassName.methodName).
7
+ *
8
+ * @param symbols - DocumentSymbol array from LSP
9
+ * @param filePath - Relative file path for node IDs
10
+ * @param language - Language identifier
11
+ * @param isTest - Whether this file is in a test directory
12
+ * @returns Flat array of Lattice Nodes
13
+ */
14
+ function documentSymbolsToNodes(
15
+ symbols: readonly DocumentSymbol[],
16
+ filePath: string,
17
+ language: string,
18
+ isTest: boolean,
19
+ ): readonly Node[] {
20
+ const nodes: Node[] = [];
21
+ flattenSymbols(symbols, filePath, language, isTest, [], nodes);
22
+ return nodes;
23
+ }
24
+
25
+ function flattenSymbols(
26
+ symbols: readonly DocumentSymbol[],
27
+ filePath: string,
28
+ language: string,
29
+ isTest: boolean,
30
+ parentNames: readonly string[],
31
+ results: Node[],
32
+ ): void {
33
+ for (const sym of symbols) {
34
+ const kind = symbolKindToNodeKind(sym.kind);
35
+ if (!kind) {
36
+ // Still recurse into children for non-matching kinds (e.g., modules)
37
+ if (sym.children) {
38
+ flattenSymbols(sym.children, filePath, language, isTest, parentNames, results);
39
+ }
40
+ continue;
41
+ }
42
+
43
+ const qualifiedName = [...parentNames, sym.name].join(".");
44
+ const id = `${filePath}::${qualifiedName}`;
45
+
46
+ results.push({
47
+ id,
48
+ kind,
49
+ name: sym.name,
50
+ file: filePath,
51
+ lineStart: sym.range.start.line + 1,
52
+ lineEnd: sym.range.end.line + 1,
53
+ language,
54
+ signature: undefined,
55
+ isTest,
56
+ metadata: undefined,
57
+ });
58
+
59
+ if (sym.children) {
60
+ flattenSymbols(sym.children, filePath, language, isTest, [...parentNames, sym.name], results);
61
+ }
62
+ }
63
+ }
64
+
65
+ function symbolKindToNodeKind(kind: number): NodeKind | undefined {
66
+ if (kind === SymbolKind.Function) return "function";
67
+ if (kind === SymbolKind.Method) return "method";
68
+ if (kind === SymbolKind.Constructor) return "method";
69
+ if (kind === SymbolKind.Class) return "class";
70
+ if (kind === SymbolKind.Interface) return "type";
71
+ return undefined;
72
+ }
73
+
74
+ /** A Lattice Node paired with the LSP selectionRange position for call hierarchy queries. */
75
+ type NodeWithPosition = {
76
+ readonly node: Node;
77
+ readonly selectionLine: number;
78
+ readonly selectionCharacter: number;
79
+ };
80
+
81
+ /**
82
+ * Like documentSymbolsToNodes but also returns the selectionRange position
83
+ * needed for prepareCallHierarchy requests.
84
+ */
85
+ function documentSymbolsToNodesWithPositions(
86
+ symbols: readonly DocumentSymbol[],
87
+ filePath: string,
88
+ language: string,
89
+ isTest: boolean,
90
+ ): readonly NodeWithPosition[] {
91
+ const results: NodeWithPosition[] = [];
92
+ flattenSymbolsWithPositions(symbols, filePath, language, isTest, [], results);
93
+ return results;
94
+ }
95
+
96
+ function flattenSymbolsWithPositions(
97
+ symbols: readonly DocumentSymbol[],
98
+ filePath: string,
99
+ language: string,
100
+ isTest: boolean,
101
+ parentNames: readonly string[],
102
+ results: NodeWithPosition[],
103
+ ): void {
104
+ for (const sym of symbols) {
105
+ const kind = symbolKindToNodeKind(sym.kind);
106
+ if (!kind) {
107
+ if (sym.children) {
108
+ flattenSymbolsWithPositions(sym.children, filePath, language, isTest, parentNames, results);
109
+ }
110
+ continue;
111
+ }
112
+
113
+ const qualifiedName = [...parentNames, sym.name].join(".");
114
+ const id = `${filePath}::${qualifiedName}`;
115
+
116
+ results.push({
117
+ node: {
118
+ id,
119
+ kind,
120
+ name: sym.name,
121
+ file: filePath,
122
+ lineStart: sym.range.start.line + 1,
123
+ lineEnd: sym.range.end.line + 1,
124
+ language,
125
+ signature: undefined,
126
+ isTest,
127
+ metadata: undefined,
128
+ },
129
+ selectionLine: sym.selectionRange.start.line,
130
+ selectionCharacter: sym.selectionRange.start.character,
131
+ });
132
+
133
+ if (sym.children) {
134
+ flattenSymbolsWithPositions(
135
+ sym.children,
136
+ filePath,
137
+ language,
138
+ isTest,
139
+ [...parentNames, sym.name],
140
+ results,
141
+ );
142
+ }
143
+ }
144
+ }
145
+
146
+ export { documentSymbolsToNodes, documentSymbolsToNodesWithPositions, type NodeWithPosition };
@@ -0,0 +1,73 @@
1
+ /** JSON-RPC message envelope. */
2
+ type JsonRpcMessage = {
3
+ readonly jsonrpc: "2.0";
4
+ readonly id?: number;
5
+ readonly method?: string;
6
+ readonly params?: unknown;
7
+ readonly result?: unknown;
8
+ readonly error?: { readonly code: number; readonly message: string; readonly data?: unknown };
9
+ };
10
+
11
+ /** Position in a text document (0-based). */
12
+ type Position = {
13
+ readonly line: number;
14
+ readonly character: number;
15
+ };
16
+
17
+ /** Range in a text document. */
18
+ type Range = {
19
+ readonly start: Position;
20
+ readonly end: Position;
21
+ };
22
+
23
+ /** LSP SymbolKind values (subset Lattice uses). */
24
+ const SymbolKind = {
25
+ Function: 12,
26
+ Method: 6,
27
+ Class: 5,
28
+ Interface: 11,
29
+ Constructor: 9,
30
+ Variable: 13,
31
+ Property: 7,
32
+ } as const;
33
+
34
+ /** A symbol in a document, returned by textDocument/documentSymbol. */
35
+ type DocumentSymbol = {
36
+ readonly name: string;
37
+ readonly kind: number;
38
+ readonly range: Range;
39
+ readonly selectionRange: Range;
40
+ readonly children?: readonly DocumentSymbol[];
41
+ };
42
+
43
+ /** A call hierarchy item, returned by textDocument/prepareCallHierarchy. */
44
+ type CallHierarchyItem = {
45
+ readonly name: string;
46
+ readonly kind: number;
47
+ readonly uri: string;
48
+ readonly range: Range;
49
+ readonly selectionRange: Range;
50
+ };
51
+
52
+ /** An outgoing call, returned by callHierarchy/outgoingCalls. */
53
+ type CallHierarchyOutgoingCall = {
54
+ readonly to: CallHierarchyItem;
55
+ readonly fromRanges: readonly Range[];
56
+ };
57
+
58
+ /** A location in a document, returned by textDocument/references. */
59
+ type Location = {
60
+ readonly uri: string;
61
+ readonly range: Range;
62
+ };
63
+
64
+ export {
65
+ type CallHierarchyItem,
66
+ type CallHierarchyOutgoingCall,
67
+ type DocumentSymbol,
68
+ type JsonRpcMessage,
69
+ type Location,
70
+ type Position,
71
+ type Range,
72
+ SymbolKind,
73
+ };
package/src/main.ts CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  import type { Node } from "./types/graph.ts";
38
38
  import { isOk, unwrap } from "./types/result.ts";
39
39
 
40
- const VERSION = "0.1.0";
40
+ const VERSION = "0.3.0";
41
41
 
42
42
  const program = new Command();
43
43
  program.name("lattice").description("Knowledge graph CLI for coding agents").version(VERSION);
@@ -109,8 +109,7 @@ program
109
109
  .command("lint")
110
110
  .description("Validate tags: syntax, typos, orphans, missing tags")
111
111
  .option("--strict", "Treat warnings as errors")
112
- .option("--unresolved", "Show detailed unresolved references")
113
- .action((opts: { strict?: boolean; unresolved?: boolean }) => {
112
+ .action((opts: { strict?: boolean }) => {
114
113
  const cwd = process.cwd();
115
114
  const configResult = loadConfig(cwd);
116
115
  if (!isOk(configResult)) {
@@ -147,21 +146,6 @@ program
147
146
  console.log(
148
147
  `Info:\n Coverage: ${result.coverage.tagged}/${result.coverage.total} entry points tagged (${result.coverage.total > 0 ? Math.round((result.coverage.tagged / result.coverage.total) * 100) : 0}%)`,
149
148
  );
150
- console.log(` Unresolved references: ${result.unresolvedCount}`);
151
-
152
- // Unresolved details
153
- if (opts.unresolved) {
154
- const refs = db
155
- .query("SELECT file, line, expression, reason FROM unresolved ORDER BY file, line")
156
- .all() as { file: string; line: number; expression: string; reason: string }[];
157
- if (refs.length > 0) {
158
- console.log("\nUnresolved references:");
159
- for (const ref of refs) {
160
- console.log(` ${ref.file}:${ref.line} ${ref.expression} (${ref.reason})`);
161
- }
162
- }
163
- }
164
-
165
149
  db.close();
166
150
 
167
151
  // Exit code
@@ -2,7 +2,6 @@
2
2
  type PythonConfig = {
3
3
  readonly sourceRoots: readonly string[];
4
4
  readonly testPaths: readonly string[];
5
- readonly frameworks: readonly string[];
6
5
  };
7
6
 
8
7
  /** Configuration for a TypeScript language section. */
@@ -10,7 +9,6 @@ type TypeScriptConfig = {
10
9
  readonly sourceRoots: readonly string[];
11
10
  readonly testPaths: readonly string[];
12
11
  readonly tsconfig: string | undefined;
13
- readonly frameworks: readonly string[];
14
12
  };
15
13
 
16
14
  /** Lint-specific configuration. */
@@ -10,19 +10,6 @@ type EdgeKind = (typeof EDGE_KINDS)[number];
10
10
  const TAG_KINDS = ["flow", "boundary", "emits", "handles"] as const;
11
11
  type TagKind = (typeof TAG_KINDS)[number];
12
12
 
13
- /** Certainty level of an edge relationship. */
14
- const CERTAINTY_LEVELS = ["certain", "uncertain"] as const;
15
- type Certainty = (typeof CERTAINTY_LEVELS)[number];
16
-
17
- /** Reasons an extractor may fail to resolve a reference. */
18
- const UNRESOLVED_REASONS = [
19
- "dynamic_dispatch",
20
- "unknown_module",
21
- "computed_property",
22
- "untyped_call",
23
- ] as const;
24
- type UnresolvedReason = (typeof UNRESOLVED_REASONS)[number];
25
-
26
13
  /** A symbol in the codebase (function, class, method, type, module). */
27
14
  type Node = {
28
15
  readonly id: string;
@@ -42,7 +29,6 @@ type Edge = {
42
29
  readonly sourceId: string;
43
30
  readonly targetId: string;
44
31
  readonly kind: EdgeKind;
45
- readonly certainty: Certainty;
46
32
  };
47
33
 
48
34
  /** A lattice annotation on a node. */
@@ -52,36 +38,22 @@ type Tag = {
52
38
  readonly value: string;
53
39
  };
54
40
 
55
- /** A reference that could not be resolved during extraction. */
56
- type UnresolvedReference = {
57
- readonly file: string;
58
- readonly line: number;
59
- readonly expression: string;
60
- readonly reason: UnresolvedReason;
61
- };
62
-
63
- /** The complete output of extracting a single file. */
64
- type ExtractionResult = {
65
- readonly nodes: readonly Node[];
66
- readonly edges: readonly Edge[];
67
- readonly tags: readonly Tag[];
68
- readonly unresolved: readonly UnresolvedReference[];
41
+ /** A call from a project function to an external package. */
42
+ type ExternalCall = {
43
+ readonly nodeId: string;
44
+ readonly package: string;
45
+ readonly symbol: string;
69
46
  };
70
47
 
71
48
  export {
72
- CERTAINTY_LEVELS,
73
- type Certainty,
74
49
  EDGE_KINDS,
75
50
  type Edge,
76
51
  type EdgeKind,
77
- type ExtractionResult,
52
+ type ExternalCall,
78
53
  NODE_KINDS,
79
54
  type Node,
80
55
  type NodeKind,
81
56
  TAG_KINDS,
82
57
  type Tag,
83
58
  type TagKind,
84
- UNRESOLVED_REASONS,
85
- type UnresolvedReason,
86
- type UnresolvedReference,
87
59
  };
package/src/types/lint.ts CHANGED
@@ -15,7 +15,6 @@ type LintIssue = {
15
15
  type LintResult = {
16
16
  readonly issues: readonly LintIssue[];
17
17
  readonly coverage: { readonly tagged: number; readonly total: number };
18
- readonly unresolvedCount: number;
19
18
  };
20
19
 
21
20
  export type { LintIssue, LintResult, LintSeverity };
@@ -1,13 +0,0 @@
1
- import type { ExtractionResult } from "../types/graph.ts";
2
-
3
- /**
4
- * Language-specific extractor that produces nodes, edges, and tags from source files.
5
- * Each supported language implements this type.
6
- */
7
- type Extractor = {
8
- readonly language: string;
9
- readonly fileExtensions: readonly string[];
10
- readonly extract: (filePath: string, source: string) => Promise<ExtractionResult>;
11
- };
12
-
13
- export type { Extractor };
@@ -1,117 +0,0 @@
1
- /**
2
- * Shared tree-sitter parser initialization.
3
- * Uses web-tree-sitter with WASM grammars from tree-sitter-wasms.
4
- */
5
-
6
- // web-tree-sitter 0.24.x uses CJS exports
7
- // biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter 0.24 has no ESM types
8
- const TreeSitter = require("web-tree-sitter") as any;
9
-
10
- type TreeSitterParser = {
11
- setLanguage(lang: TreeSitterLanguage): void;
12
- parse(input: string): TreeSitterTree;
13
- };
14
-
15
- type TreeSitterLanguage = {
16
- readonly version: number;
17
- };
18
-
19
- type TreeSitterTree = {
20
- readonly rootNode: TreeSitterNode;
21
- };
22
-
23
- type TreeSitterNode = {
24
- readonly type: string;
25
- readonly text: string;
26
- readonly startPosition: { row: number; column: number };
27
- readonly endPosition: { row: number; column: number };
28
- readonly startIndex: number;
29
- readonly endIndex: number;
30
- readonly childCount: number;
31
- readonly children: readonly TreeSitterNode[];
32
- readonly parent: TreeSitterNode | null;
33
- readonly firstChild: TreeSitterNode | null;
34
- readonly lastChild: TreeSitterNode | null;
35
- readonly nextSibling: TreeSitterNode | null;
36
- readonly previousSibling: TreeSitterNode | null;
37
- childForFieldName(fieldName: string): TreeSitterNode | null;
38
- childrenForFieldName(fieldName: string): readonly TreeSitterNode[];
39
- descendantsOfType(type: string | readonly string[]): readonly TreeSitterNode[];
40
- };
41
-
42
- let initialized = false;
43
- const languageCache = new Map<string, TreeSitterLanguage>();
44
-
45
- /**
46
- * Resolves the WASM file path for a language grammar.
47
- * Tries multiple locations to support both dev and compiled binary modes:
48
- * 1. Relative to this source file (dev: bun src/main.ts)
49
- * 2. Relative to the compiled binary (binary: ./lattice)
50
- * 3. Relative to CWD (when node_modules exists in working dir)
51
- */
52
- function grammarPath(language: string): string {
53
- const { existsSync } = require("node:fs");
54
- const { dirname, join } = require("node:path");
55
- const filename = `tree-sitter-${language}.wasm`;
56
- const subpath = `node_modules/tree-sitter-wasms/out/${filename}`;
57
-
58
- // Try relative to this source file (dev mode)
59
- const packageRoot = new URL("../../", import.meta.url).pathname;
60
- const devPath = join(packageRoot, subpath);
61
- if (existsSync(devPath)) return devPath;
62
-
63
- // Try relative to the binary's location (compiled mode)
64
- const binDir = dirname(process.execPath);
65
- const binPath = join(binDir, subpath);
66
- if (existsSync(binPath)) return binPath;
67
-
68
- // Try relative to CWD
69
- const cwdPath = join(process.cwd(), subpath);
70
- if (existsSync(cwdPath)) return cwdPath;
71
-
72
- // Fall back — will produce a clear error
73
- return devPath;
74
- }
75
-
76
- /**
77
- * Initializes tree-sitter WASM runtime. Must be called once before parsing.
78
- * Safe to call multiple times — subsequent calls are no-ops.
79
- */
80
- async function initTreeSitter(): Promise<void> {
81
- if (initialized) return;
82
- await TreeSitter.init();
83
- initialized = true;
84
- }
85
-
86
- /**
87
- * Creates a parser configured for the given language.
88
- *
89
- * @param language - Language name matching the WASM grammar file (e.g., "python", "typescript")
90
- * @returns A configured parser ready to parse source code
91
- */
92
- async function createParser(language: string): Promise<TreeSitterParser> {
93
- await initTreeSitter();
94
-
95
- const cached = languageCache.get(language);
96
- const lang = cached ?? (await loadAndCacheLanguage(language));
97
-
98
- const parser = new TreeSitter();
99
- parser.setLanguage(lang);
100
- return parser as TreeSitterParser;
101
- }
102
-
103
- /** Loads a language grammar from WASM and caches it. */
104
- async function loadAndCacheLanguage(language: string): Promise<TreeSitterLanguage> {
105
- const lang = (await TreeSitter.Language.load(grammarPath(language))) as TreeSitterLanguage;
106
- languageCache.set(language, lang);
107
- return lang;
108
- }
109
-
110
- export {
111
- createParser,
112
- initTreeSitter,
113
- type TreeSitterLanguage,
114
- type TreeSitterNode,
115
- type TreeSitterParser,
116
- type TreeSitterTree,
117
- };