graph-indexer 1.1.0 → 2.0.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +412 -0
  2. package/README.md +228 -350
  3. package/SECURITY.md +68 -0
  4. package/TRADEMARK.md +31 -0
  5. package/cli-ui.mjs +78 -0
  6. package/config.mjs +269 -0
  7. package/daemon-ctl.mjs +175 -0
  8. package/daemon-lock.mjs +69 -0
  9. package/embedders/python/mlx_embed_server.py +61 -0
  10. package/embedders/python/requirements-mlx.txt +5 -0
  11. package/embedders/setup-mlx.mjs +110 -0
  12. package/embeddings.mjs +489 -0
  13. package/engine/binary.mjs +399 -0
  14. package/{core-engine.mjs → engine/memory.mjs} +359 -249
  15. package/engine/sqlite.mjs +863 -0
  16. package/enrichment.mjs +426 -0
  17. package/git-signals.mjs +248 -0
  18. package/indexer.mjs +216 -77
  19. package/init.mjs +1604 -170
  20. package/layout.mjs +123 -0
  21. package/mcp/format.mjs +61 -0
  22. package/mcp/tools.mjs +1157 -0
  23. package/mcp/topology.mjs +551 -0
  24. package/mcp-server.mjs +118 -573
  25. package/package.json +56 -16
  26. package/parse/extractor.mjs +617 -0
  27. package/parse/imports.mjs +365 -0
  28. package/parse/languages.mjs +89 -0
  29. package/parse/metadata.mjs +655 -0
  30. package/parse/routes.mjs +327 -0
  31. package/prompts/CORE.md +86 -0
  32. package/prompts/DOMAIN_TEMPLATE.md +46 -0
  33. package/prompts/INTEGRATION.md +123 -0
  34. package/prompts/frameworks/ANDROID.md +17 -0
  35. package/prompts/frameworks/ASPNET_CORE.md +19 -0
  36. package/prompts/frameworks/FASTAPI_DJANGO.md +19 -0
  37. package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
  38. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +18 -0
  39. package/prompts/frameworks/RAILS.md +20 -0
  40. package/prompts/frameworks/REACT.md +20 -0
  41. package/prompts/frameworks/SPRING_BOOT.md +19 -0
  42. package/prompts/languages/BASH.md +22 -0
  43. package/prompts/languages/C.md +21 -0
  44. package/prompts/languages/CSHARP.md +24 -0
  45. package/prompts/languages/CSS_SCSS.md +22 -0
  46. package/prompts/languages/GO.md +23 -0
  47. package/prompts/languages/JAVA.md +23 -0
  48. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
  49. package/prompts/languages/KOTLIN.md +24 -0
  50. package/prompts/languages/PHP.md +23 -0
  51. package/prompts/languages/PYTHON.md +23 -0
  52. package/prompts/languages/RUBY.md +23 -0
  53. package/prompts/languages/RUST.md +21 -0
  54. package/prompts/languages/SWIFT.md +21 -0
  55. package/search-core.mjs +627 -0
  56. package/storage.mjs +68 -0
  57. package/watch-daemon.mjs +204 -69
  58. package/PROMPT.md +0 -143
  59. package/parser-utils.mjs +0 -861
@@ -0,0 +1,365 @@
1
+ /**
2
+ * @file parse/imports.mjs
3
+ * @description Import resolution, barrel export expansion, embedding payload
4
+ * builders, and legacy Ollama embedding helpers.
5
+ * @author MaquinaTech <https://github.com/MaquinaTech>
6
+ * @copyright (c) 2026 MaquinaTech. All rights reserved.
7
+ * @license MIT
8
+ */
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { getParserForFile, EXTENSIONS } from './languages.mjs';
12
+ import { truncateForEmbedding } from '../search-core.mjs';
13
+
14
+ // JS-family import-specifier extensions. Under TS node16/nodenext these appear in
15
+ // specifiers (`./x.js`) while the source on disk is `.ts`/`.tsx` — so when the literal
16
+ // path is missing we strip one of these and re-probe the real source extensions.
17
+ const JS_FAMILY_EXT = new Set(['.js', '.jsx', '.mjs', '.cjs']);
18
+
19
+ // ─── Ollama host / model resolution ──────────────────────────────────────────
20
+ // Priority: caller override → OLLAMA_HOST env var → PROJECT .graph-indexer.json
21
+ // (MCP_PROJECT_ROOT or cwd — NOT the package directory: when graph-indexer is
22
+ // installed as a dependency, the user's config lives in their project root) →
23
+ // default. Mirrors config.mjs precedence so every entry point agrees.
24
+ //
25
+ // OLLAMA_HOST in the shell is Ollama's binding address (e.g. "0.0.0.0:11434"), not an
26
+ // HTTP client URL. We normalise bare "host:port" strings by adding http:// and translating
27
+ // 0.0.0.0 → localhost so fetches work in both formats.
28
+
29
+ let _cachedHost = null;
30
+ let _cachedEmbedModel = null;
31
+ function _normalizeOllamaHost(raw) {
32
+ if (!raw) return null;
33
+ if (raw.startsWith('http://') || raw.startsWith('https://')) return raw;
34
+ return 'http://' + raw.replace(/^0\.0\.0\.0/, 'localhost');
35
+ }
36
+ function _readProjectConfig() {
37
+ const root = process.env.MCP_PROJECT_ROOT || process.cwd();
38
+ try {
39
+ return JSON.parse(fs.readFileSync(path.join(root, '.graph-indexer.json'), 'utf8'));
40
+ } catch { return null; }
41
+ }
42
+ function _resolveOllamaHost(override) {
43
+ if (override) return _normalizeOllamaHost(override) || 'http://localhost:11434';
44
+ if (_cachedHost) return _cachedHost;
45
+ if (process.env.OLLAMA_HOST) {
46
+ _cachedHost = _normalizeOllamaHost(process.env.OLLAMA_HOST) || 'http://localhost:11434';
47
+ return _cachedHost;
48
+ }
49
+ const cfg = _readProjectConfig();
50
+ _cachedHost = _normalizeOllamaHost(cfg?.ollamaHost) || 'http://localhost:11434';
51
+ return _cachedHost;
52
+ }
53
+ function _resolveEmbedModel(override) {
54
+ if (override) return override;
55
+ if (_cachedEmbedModel) return _cachedEmbedModel;
56
+ const cfg = _readProjectConfig();
57
+ _cachedEmbedModel = cfg?.embedModel || 'nomic-embed-text';
58
+ return _cachedEmbedModel;
59
+ }
60
+
61
+ // ─── Barrel export resolution ─────────────────────────────────────────────────
62
+
63
+ // Module-level cache: barrelAbsPath → Map<exportedName, sourceRelPath>
64
+ const _barrelCache = new Map();
65
+
66
+ /**
67
+ * Parses a barrel file (index.ts / index.js) and returns a map of
68
+ * exportedName → sourceFilePath (relative to projectRoot).
69
+ * e.g. { useAuthStore: 'src/stores/authStore.ts' }
70
+ */
71
+ export function resolveBarrelExports(barrelAbsPath, projectRoot) {
72
+ if (_barrelCache.has(barrelAbsPath)) return _barrelCache.get(barrelAbsPath);
73
+
74
+ const result = new Map();
75
+ _barrelCache.set(barrelAbsPath, result);
76
+
77
+ const ext = path.extname(barrelAbsPath);
78
+ if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) return result;
79
+
80
+ let content;
81
+ try { content = fs.readFileSync(barrelAbsPath, 'utf-8'); } catch { return result; }
82
+
83
+ const parser = getParserForFile(ext);
84
+ if (!parser) return result;
85
+
86
+ let tree;
87
+ try { tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null); } catch { return result; }
88
+
89
+ const barrelDir = path.dirname(barrelAbsPath);
90
+
91
+ function walk(node) {
92
+ if (node.type === 'export_statement') {
93
+ const fromNode = node.children.find(c => c.type === 'string');
94
+ if (!fromNode) { node.children.forEach(walk); return; }
95
+
96
+ const rawSource = fromNode.text.replace(/['"]/g, '');
97
+ if (!rawSource.startsWith('.')) { node.children.forEach(walk); return; }
98
+
99
+ const absSource = path.resolve(barrelDir, rawSource);
100
+ let finalAbs = null;
101
+ if (EXTENSIONS.has(path.extname(absSource)) && fs.existsSync(absSource)) {
102
+ finalAbs = absSource;
103
+ } else {
104
+ for (const e of EXTENSIONS) {
105
+ if (fs.existsSync(absSource + e)) { finalAbs = absSource + e; break; }
106
+ const idx = path.join(absSource, 'index' + e);
107
+ if (fs.existsSync(idx)) { finalAbs = idx; break; }
108
+ }
109
+ }
110
+ if (!finalAbs) { node.children.forEach(walk); return; }
111
+
112
+ const relSource = path.relative(projectRoot, finalAbs).replace(/\\/g, '/');
113
+
114
+ const namedExports = node.children.find(c => c.type === 'named_imports' || c.type === 'export_clause');
115
+ if (namedExports) {
116
+ for (const child of namedExports.children) {
117
+ if (child.type === 'import_specifier' || child.type === 'export_specifier') {
118
+ const names = child.children.filter(c => c.type === 'identifier');
119
+ if (names.length > 0) {
120
+ result.set(names[names.length - 1].text, relSource);
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ const starNode = node.children.find(c => c.text === '*');
127
+ if (starNode) {
128
+ result.set('*', relSource);
129
+ }
130
+ }
131
+ node.children.forEach(walk);
132
+ }
133
+ walk(tree.rootNode);
134
+ return result;
135
+ }
136
+
137
+ // ─── Go module-name cache (reads go.mod once per project root) ──────────────
138
+ const _goModCache = new Map();
139
+ function _readGoModuleName(projectRoot) {
140
+ if (_goModCache.has(projectRoot)) return _goModCache.get(projectRoot);
141
+ const modFile = path.join(projectRoot, 'go.mod');
142
+ let name = null;
143
+ if (fs.existsSync(modFile)) {
144
+ try {
145
+ const first = fs.readFileSync(modFile, 'utf-8').split('\n').find(l => l.trimStart().startsWith('module '));
146
+ if (first) name = first.trim().replace(/^module\s+/, '').split(/\s/)[0];
147
+ } catch { /* ignore */ }
148
+ }
149
+ _goModCache.set(projectRoot, name);
150
+ return name;
151
+ }
152
+
153
+ export function resolveLocalImports(rawImports, fromFileRelPath, projectRoot) {
154
+ const fileDir = path.dirname(path.join(projectRoot, fromFileRelPath));
155
+ const ext = path.extname(fromFileRelPath);
156
+ const resolved = [];
157
+ for (const raw of rawImports) {
158
+ if (raw.startsWith('.')) {
159
+ const absResolved = path.resolve(fileDir, raw);
160
+ const existingExt = path.extname(absResolved);
161
+ let finalAbs = null;
162
+ if (existingExt && EXTENSIONS.has(existingExt) && fs.existsSync(absResolved)) {
163
+ finalAbs = absResolved;
164
+ } else {
165
+ // TypeScript ESM convention (node16/nodenext moduleResolution): the import
166
+ // specifier carries a JS-family extension (`./Fade.js`) but the source file on
167
+ // disk is `.ts`/`.tsx`. The literal path doesn't exist, so strip that extension
168
+ // and re-probe the candidate source extensions — otherwise EVERY relative import
169
+ // in such a project drops and the file gets empty Deps:/Used by: topology.
170
+ const probeBase = (existingExt && JS_FAMILY_EXT.has(existingExt))
171
+ ? absResolved.slice(0, -existingExt.length)
172
+ : absResolved;
173
+ for (const e of EXTENSIONS) {
174
+ if (fs.existsSync(probeBase + e)) { finalAbs = probeBase + e; break; }
175
+ const idx = path.join(probeBase, 'index' + e);
176
+ if (fs.existsSync(idx)) { finalAbs = idx; break; }
177
+ // Python: also try __init__.py for package directories
178
+ if (e === '.py') {
179
+ const init = path.join(probeBase, '__init__.py');
180
+ if (fs.existsSync(init)) { finalAbs = init; break; }
181
+ }
182
+ }
183
+ }
184
+ if (finalAbs) {
185
+ const relPath = path.relative(projectRoot, finalAbs).replace(/\\/g, '/');
186
+ const baseName = path.basename(finalAbs, path.extname(finalAbs));
187
+ if (baseName === 'index' && ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(path.extname(finalAbs))) {
188
+ const barrelMap = resolveBarrelExports(finalAbs, projectRoot);
189
+ if (barrelMap.size > 0) {
190
+ const sources = new Set(barrelMap.values());
191
+ for (const src of sources) {
192
+ if (!resolved.includes(src)) resolved.push(src);
193
+ }
194
+ } else {
195
+ if (!resolved.includes(relPath)) resolved.push(relPath);
196
+ }
197
+ } else {
198
+ if (!resolved.includes(relPath)) resolved.push(relPath);
199
+ }
200
+ }
201
+ }
202
+ // ── Go intra-module imports: module-prefix paths mapped to local dirs ──
203
+ else if (ext === '.go') {
204
+ const modName = _readGoModuleName(projectRoot);
205
+ if (modName && raw.startsWith(modName + '/')) {
206
+ const subPkg = raw.slice(modName.length + 1);
207
+ const absDir = path.join(projectRoot, subPkg);
208
+ if (fs.existsSync(absDir)) {
209
+ try {
210
+ const goFiles = fs.readdirSync(absDir)
211
+ .filter(f => f.endsWith('.go') && !f.includes('_test'))
212
+ .slice(0, 5); // cap to avoid fanning out too many deps per package
213
+ for (const gof of goFiles) {
214
+ const rel = path.relative(projectRoot, path.join(absDir, gof)).replace(/\\/g, '/');
215
+ if (!resolved.includes(rel)) resolved.push(rel);
216
+ }
217
+ } catch { /* directory unreadable — skip */ }
218
+ }
219
+ }
220
+ }
221
+ // ── Rust crate-local: crate::module::item → src/module.rs ──────────
222
+ else if (ext === '.rs' && raw.startsWith('crate::')) {
223
+ const parts = raw.slice('crate::'.length).split('::').filter(Boolean);
224
+ for (let depth = parts.length; depth >= 1; depth--) {
225
+ const subPath = parts.slice(0, depth).join('/');
226
+ const candidates = [
227
+ path.join(projectRoot, 'src', subPath + '.rs'),
228
+ path.join(projectRoot, 'src', subPath, 'mod.rs'),
229
+ ];
230
+ let found = false;
231
+ for (const c of candidates) {
232
+ if (fs.existsSync(c)) {
233
+ resolved.push(path.relative(projectRoot, c).replace(/\\/g, '/'));
234
+ found = true; break;
235
+ }
236
+ }
237
+ if (found) break;
238
+ }
239
+ }
240
+ // ── C / Bash non-dotted relative includes ──────────────────────────
241
+ // C `#include "net/socket.h"` and Bash `source lib/util.sh` are resolved
242
+ // relative to the including file's directory, then to the project root.
243
+ else if ((ext === '.c' || ext === '.h' || ext === '.sh' || ext === '.bash')) {
244
+ for (const abs of [path.resolve(fileDir, raw), path.join(projectRoot, raw)]) {
245
+ if (EXTENSIONS.has(path.extname(abs)) && fs.existsSync(abs)) {
246
+ const rel = path.relative(projectRoot, abs).replace(/\\/g, '/');
247
+ if (!rel.startsWith('..') && !resolved.includes(rel)) resolved.push(rel);
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ return resolved;
254
+ }
255
+
256
+ /**
257
+ * Build the text payload sent to the embedding model for a chunk.
258
+ *
259
+ * Shared by the bootstrap indexer (indexer.mjs) and the watch daemon
260
+ * (watch-daemon.mjs) so a chunk yields the SAME embedding regardless of which
261
+ * path embedded it first. This matters because the cache key (content_hash) is
262
+ * derived from code + docstring only — it does NOT include this payload — so two
263
+ * divergent payloads for the same hash would silently produce inconsistent
264
+ * embeddings across a full re-index vs. an incremental update.
265
+ *
266
+ * @param {object} chunk Semantic chunk: { file_path, node_type, name, docstring, type_refs, code_snippet }.
267
+ * @param {string[]} depRelPaths Resolved local imports of the chunk's file (project-relative paths).
268
+ * @returns {string}
269
+ */
270
+ export function buildEmbeddingPayload(chunk, depRelPaths = [], bodyOverride = null) {
271
+ const neighbors = depRelPaths
272
+ .map(d => path.basename(d, path.extname(d)))
273
+ .filter(Boolean);
274
+ const topologicalContext = neighbors.length
275
+ ? `This code architectural neighborhood connects with: ${neighbors.join(', ')}.`
276
+ : '';
277
+ // Decorators and inheritance edges are NOT included here (A/B-tested: neutral
278
+ // on vector recall, regression on BM25 — kept as metadata only).
279
+ // LLM summary leads: declarative voice aligns with nomic's search_document training
280
+ // objective. hyde/questions are excluded — stopword noise dilutes the code fingerprint.
281
+ // Concept keywords (chunk.hyde) go to BM25 only via buildLexicalDocument.
282
+ return [
283
+ chunk.summary || '',
284
+ `File Location: ${chunk.file_path}`,
285
+ `Symbol Name: ${chunk.node_type} -> ${chunk.name}`,
286
+ chunk.docstring ? `Developer Documentation: ${chunk.docstring}` : '',
287
+ chunk.type_refs?.length ? `Type References: ${chunk.type_refs.join(', ')}` : '',
288
+ topologicalContext,
289
+ `--- Source Code ---`,
290
+ // bodyOverride carries the full source for oversized chunks (>3000 chars); BM25
291
+ // keeps using code_snippet so lexical and dense channels stay independently capped.
292
+ bodyOverride || chunk.code_snippet,
293
+ ].filter(Boolean).join('\n');
294
+ }
295
+
296
+ /**
297
+ * The full source body of a chunk whose code_snippet was truncated by the 3000-char
298
+ * cap — sliced from the file content by line range — so the dense channel can window
299
+ * the WHOLE definition (the lexical/BM25 path stays capped, byte-identical). Returns
300
+ * null when the chunk is not truncated (snippet == body) or is a god-class skeleton
301
+ * (its methods are already their own chunks). Used at embed time by the indexer and
302
+ * the watch daemon, both of which hold the source content.
303
+ *
304
+ * @param {object} chunk A semantic chunk (needs code_snippet, start_line, end_line).
305
+ * @param {string} fileContent The chunk's full file source.
306
+ * @returns {string|null}
307
+ */
308
+ export function fullBodyForEmbedding(chunk, fileContent) {
309
+ if (!chunk || !fileContent) return null;
310
+ if ((chunk.code_snippet?.length || 0) < 3000) return null; // not truncated
311
+ if (chunk.code_snippet.includes('Large class:')) return null; // god-class skeleton
312
+ if (!chunk.start_line || !chunk.end_line) return null;
313
+ const body = fileContent.split('\n').slice(chunk.start_line - 1, chunk.end_line).join('\n');
314
+ return body.length > chunk.code_snippet.length ? body : null;
315
+ }
316
+
317
+ export async function getLocalEmbedding(text, graceful = true, { ollamaHost, model } = {}) {
318
+ if (process.env.INDEXER_EMBEDDINGS === 'off') return null; // lexical-only mode
319
+ const host = _resolveOllamaHost(ollamaHost);
320
+ const embedModel = _resolveEmbedModel(model);
321
+ const MAX_RETRIES = 3;
322
+ const safeText = "search_query: " + truncateForEmbedding(text);
323
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
324
+ try {
325
+ const res = await fetch(`${host}/api/embeddings`, {
326
+ method: "POST", headers: { "Content-Type": "application/json" },
327
+ body: JSON.stringify({ model: embedModel, prompt: safeText }),
328
+ signal: AbortSignal.timeout(15000),
329
+ });
330
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
331
+ const data = await res.json();
332
+ return data.embedding;
333
+ } catch (err) {
334
+ if (attempt < MAX_RETRIES) await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
335
+ else if (!graceful) throw err;
336
+ }
337
+ }
338
+ return null;
339
+ }
340
+
341
+ export async function getLocalEmbeddingsBatch(texts, graceful = true, { ollamaHost, model } = {}) {
342
+ if (!texts || texts.length === 0) return [];
343
+ if (process.env.INDEXER_EMBEDDINGS === 'off') return null; // lexical-only mode
344
+ const host = _resolveOllamaHost(ollamaHost);
345
+ const embedModel = _resolveEmbedModel(model);
346
+ const MAX_RETRIES = 3;
347
+ const safeTexts = texts.map(t => "search_document: " + (t.length > 8000 ? t.slice(0, 8000) : t));
348
+
349
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
350
+ try {
351
+ const res = await fetch(`${host}/api/embed`, {
352
+ method: "POST", headers: { "Content-Type": "application/json" },
353
+ body: JSON.stringify({ model: embedModel, input: safeTexts }),
354
+ signal: AbortSignal.timeout(60000),
355
+ });
356
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
357
+ const data = await res.json();
358
+ return data.embeddings;
359
+ } catch (err) {
360
+ if (attempt < MAX_RETRIES) await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
361
+ else if (!graceful) throw err;
362
+ }
363
+ }
364
+ return null;
365
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @file parse/languages.mjs
3
+ * @description Language registry: dynamic Tree-sitter grammar loading, LANGUAGE_MAP,
4
+ * EXTENSIONS, and getParserForFile.
5
+ * @author MaquinaTech <https://github.com/MaquinaTech>
6
+ * @copyright (c) 2026 MaquinaTech. All rights reserved.
7
+ * @license MIT
8
+ */
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import ignore from 'ignore';
12
+ import Parser from 'tree-sitter';
13
+
14
+ export const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
15
+
16
+ // ─── Dynamic Language Loading ─────────────────────────────────────────────────
17
+
18
+ function _loadProjectConfig() {
19
+ const configPath = path.join(process.env.MCP_PROJECT_ROOT || process.cwd(), '.graph-indexer.json');
20
+ try {
21
+ if (fs.existsSync(configPath)) return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
22
+ } catch { /* ignore */ }
23
+ return null;
24
+ }
25
+
26
+ async function _tryLang(pkg, enabledLangs, key) {
27
+ if (enabledLangs && !enabledLangs.includes(key)) return null;
28
+ try {
29
+ return (await import(pkg)).default;
30
+ } catch {
31
+ process.stderr.write(`[graph-indexer] WARNING: ${pkg} not installed — ${key} files will be skipped\n`);
32
+ return null;
33
+ }
34
+ }
35
+
36
+ const _cfg = _loadProjectConfig();
37
+ const _enabled = _cfg?.languages ?? null; // null = all languages
38
+
39
+ const [
40
+ TypeScript, JavaScript, CSS, SCSS, Python, Rust,
41
+ Go, PHP, Java, Kotlin, CSharp, Ruby, C, Bash, Swift
42
+ ] = await Promise.all([
43
+ _tryLang('tree-sitter-typescript', _enabled, 'typescript'),
44
+ _tryLang('tree-sitter-javascript', _enabled, 'javascript'),
45
+ _tryLang('tree-sitter-css', _enabled, 'css'),
46
+ // SCSS shares the 'css' enable-key — init records .css and .scss under one
47
+ // 'css' language, so the SCSS grammar must load whenever CSS is enabled.
48
+ _tryLang('tree-sitter-scss', _enabled, (_enabled && _enabled.includes('css')) ? 'css' : 'scss'),
49
+ _tryLang('tree-sitter-python', _enabled, 'python'),
50
+ _tryLang('tree-sitter-rust', _enabled, 'rust'),
51
+ _tryLang('tree-sitter-go', _enabled, 'go'),
52
+ _tryLang('tree-sitter-php', _enabled, 'php'),
53
+ _tryLang('tree-sitter-java', _enabled, 'java'),
54
+ _tryLang('tree-sitter-kotlin', _enabled, 'kotlin'),
55
+ _tryLang('tree-sitter-c-sharp', _enabled, 'csharp'),
56
+ _tryLang('tree-sitter-ruby', _enabled, 'ruby'),
57
+ _tryLang('tree-sitter-c', _enabled, 'c'),
58
+ _tryLang('tree-sitter-bash', _enabled, 'bash'),
59
+ _tryLang('tree-sitter-swift', _enabled, 'swift'),
60
+ ]);
61
+
62
+ export const LANGUAGE_MAP = {
63
+ ...(TypeScript ? { '.ts': TypeScript.typescript, '.tsx': TypeScript.tsx } : {}),
64
+ ...(JavaScript ? { '.js': JavaScript, '.jsx': JavaScript, '.mjs': JavaScript, '.cjs': JavaScript } : {}),
65
+ ...(CSS ? { '.css': CSS } : {}),
66
+ ...(SCSS ? { '.scss': SCSS } : (CSS ? { '.scss': CSS } : {})),
67
+ ...(Python ? { '.py': Python } : {}),
68
+ ...(Rust ? { '.rs': Rust } : {}),
69
+ ...(Go ? { '.go': Go } : {}),
70
+ ...(PHP ? { '.php': PHP.php } : {}),
71
+ ...(Java ? { '.java': Java } : {}),
72
+ ...(Kotlin ? { '.kt': Kotlin, '.kts': Kotlin } : {}),
73
+ ...(CSharp ? { '.cs': CSharp } : {}),
74
+ ...(Ruby ? { '.rb': Ruby } : {}),
75
+ ...(C ? { '.c': C, '.h': C } : {}),
76
+ // Shebang-only extensionless scripts are not keyed.
77
+ ...(Bash ? { '.sh': Bash, '.bash': Bash } : {}),
78
+ ...(Swift ? { '.swift': Swift } : {}),
79
+ };
80
+
81
+ export const EXTENSIONS = new Set(Object.keys(LANGUAGE_MAP));
82
+
83
+ export function getParserForFile(ext) {
84
+ const language = LANGUAGE_MAP[ext];
85
+ if (!language) return null;
86
+ const parser = new Parser();
87
+ parser.setLanguage(language);
88
+ return parser;
89
+ }