(language: string)
| 56 | } |
| 57 | |
| 58 | export async function getParser(language: string): Promise<{ parser: any; language: any } | null> { |
| 59 | try { |
| 60 | await initTreeSitter(); |
| 61 | } catch (e: any) { |
| 62 | logger.warn('Tree-sitter unavailable', { err: e.message }); |
| 63 | return null; |
| 64 | } |
| 65 | |
| 66 | if (parsersByLang.has(language) && languagesByLang.has(language)) { |
| 67 | return { parser: parsersByLang.get(language)!, language: languagesByLang.get(language)! }; |
| 68 | } |
| 69 | |
| 70 | // Try to load WASM grammar |
| 71 | // First look in installed package dir, then in user qodex home |
| 72 | const grammarFile = `tree-sitter-${language}.wasm`; |
| 73 | const candidates: string[] = []; |
| 74 | |
| 75 | try { |
| 76 | const here = path.dirname(url.fileURLToPath(import.meta.url)); |
| 77 | candidates.push(path.join(here, '..', '..', '..', 'grammars', grammarFile)); |
| 78 | candidates.push(path.join(here, '..', '..', '..', '..', 'grammars', grammarFile)); |
| 79 | candidates.push(path.join(process.cwd(), 'grammars', grammarFile)); |
| 80 | } catch {} |
| 81 | |
| 82 | let lang: any = null; |
| 83 | for (const candidate of candidates) { |
| 84 | // CRITICAL: only hand Language.load a path that exists. web-tree-sitter's |
| 85 | // Emscripten loader, given a missing path, kicks off an async file read whose |
| 86 | // ENOENT rejection floats OUTSIDE the promise we await here — so a try/catch |
| 87 | // around load() does NOT catch it, and Node 24 crashes the whole process on |
| 88 | // the unhandled rejection. Pre-checking existence means load() is only ever |
| 89 | // called on a real file, so a missing grammar degrades to the regex fallback |
| 90 | // instead of taking down the CLI. |
| 91 | try { |
| 92 | await access(candidate); |
| 93 | } catch { |
| 94 | continue; // not present — try the next candidate |
| 95 | } |
| 96 | try { |
| 97 | lang = await TSLanguage.load(candidate); |
| 98 | logger.debug(`Loaded grammar: ${candidate}`); |
| 99 | break; |
| 100 | } catch (e: any) { |
| 101 | logger.warn(`Grammar present but failed to load: ${candidate}`, { err: e?.message }); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | if (!lang) { |
| 106 | logger.warn(`No grammar found for language: ${language}. Looked in: ${candidates.join(', ')}`); |
| 107 | return null; |
| 108 | } |
| 109 | |
| 110 | const parser = new TreeSitter(); |
| 111 | parser.setLanguage(lang); |
| 112 | parsersByLang.set(language, parser); |
| 113 | languagesByLang.set(language, lang); |
| 114 | |
| 115 | return { parser, language: lang }; |
no test coverage detected