pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
135 lines (134 loc) • 6.01 kB
JavaScript
/**
* Single source of truth for tree-sitter grammar assets: the language→wasm map,
* the CDN the wasms come from, and the (single-file) download routine.
*
* Reused by:
* - the runtime client (`tree-sitter-client.ts`), which lazily fetches a
* missing grammar on first use (pnpm/bun skip the postinstall);
* - the postinstall pre-fetch (`scripts/download-grammars.js`), which can't
* import this compiled module (it runs before the TS build), so it keeps a
* mirror — guarded against drift by `tests/clients/grammar-source.test.ts`.
*/
import * as fs from "node:fs";
import * as path from "node:path";
/** tree-sitter-wasms release the grammars are pulled from. */
export const TREE_SITTER_WASMS_VERSION = "0.1.13";
/** unpkg mirror of the tree-sitter-wasms artifacts. */
export const GRAMMAR_CDN_BASE = `https://unpkg.com/tree-sitter-wasms@${TREE_SITTER_WASMS_VERSION}/out`;
export const GRAMMAR_SOURCE_OVERRIDES = {
"tree-sitter-lua.wasm": {
package: "@tree-sitter-grammars/tree-sitter-lua",
version: "0.4.1",
url: "https://unpkg.com/@tree-sitter-grammars/tree-sitter-lua@0.4.1/tree-sitter-lua.wasm",
},
"tree-sitter-yaml.wasm": {
package: "@tree-sitter-grammars/tree-sitter-yaml",
version: "0.7.1",
url: "https://unpkg.com/@tree-sitter-grammars/tree-sitter-yaml@0.7.1/tree-sitter-yaml.wasm",
},
};
/** The URL a grammar wasm is fetched from (override if any, else the aggregator). */
export function grammarSourceUrl(filename) {
return GRAMMAR_SOURCE_OVERRIDES[filename]?.url ?? `${GRAMMAR_CDN_BASE}/${filename}`;
}
/** Language id → grammar wasm filename. */
export const LANGUAGE_TO_GRAMMAR = {
typescript: "tree-sitter-typescript.wasm",
tsx: "tree-sitter-tsx.wasm",
javascript: "tree-sitter-javascript.wasm",
python: "tree-sitter-python.wasm",
rust: "tree-sitter-rust.wasm",
go: "tree-sitter-go.wasm",
java: "tree-sitter-java.wasm",
kotlin: "tree-sitter-kotlin.wasm",
dart: "tree-sitter-dart.wasm",
c: "tree-sitter-c.wasm",
cpp: "tree-sitter-cpp.wasm",
elixir: "tree-sitter-elixir.wasm",
ruby: "tree-sitter-ruby.wasm",
bash: "tree-sitter-bash.wasm",
csharp: "tree-sitter-c_sharp.wasm",
css: "tree-sitter-css.wasm",
html: "tree-sitter-html.wasm",
json: "tree-sitter-json.wasm",
lua: "tree-sitter-lua.wasm",
ocaml: "tree-sitter-ocaml.wasm",
php: "tree-sitter-php.wasm",
swift: "tree-sitter-swift.wasm",
toml: "tree-sitter-toml.wasm",
vue: "tree-sitter-vue.wasm",
yaml: "tree-sitter-yaml.wasm",
zig: "tree-sitter-zig.wasm",
};
/** The full set of grammar wasm filenames (deduped). */
export const GRAMMAR_FILES = [
...new Set(Object.values(LANGUAGE_TO_GRAMMAR)),
];
/**
* Grammars that FATALLY crash the host runtime on specific engines/versions and
* therefore must NOT be loaded there. The crash is an uncatchable process abort
* (V8 Turboshaft WASM OOM — `Fatal process out of memory: Zone`), so it cannot be
* degraded in-process with try/catch; the only safe option is to skip the load
* entirely and degrade the feature (no structural symbols for that language).
*
* Membership is GUARD-DRIVEN, not hand-maintained: the grammar-health nightly
* (`scripts/check-grammar-load.mjs`) is what proves a grammar crashes and thus
* belongs here, and what proves a future build is safe enough to remove it.
*
* tree-sitter-swift crashes on Node >= 24 across all OSes under memory pressure
* (#423/#432); building from source (an earlier vendoring attempt, #426) does not
* reliably dodge it. bun (JavaScriptCore) and Node 20/22 are unaffected, so the
* block is gated on V8 + Node major.
*/
export const BLOCKED_GRAMMARS = {
"tree-sitter-swift.wasm": {
blocked: ({ isV8, nodeMajor }) => isV8 && nodeMajor >= 24,
reason: "tree-sitter-swift crashes the runtime on Node >= 24 (V8 Turboshaft WASM OOM, #423/#432); skipped so the process degrades gracefully instead of aborting.",
},
};
/** Runtime signals for the currently-running process. */
export function currentGrammarRuntime() {
const m = /^v?(\d+)/.exec(process.versions?.node ?? "");
return {
nodeMajor: m ? Number(m[1]) : 0,
isV8: !process.versions.bun && Boolean(process.versions.v8),
platform: process.platform,
};
}
/**
* The block reason if `filename` must NOT be loaded on `rt` (default: the running
* process), else null. Callers skip the load and degrade to "grammar unavailable".
*/
export function grammarBlockReason(filename, rt = currentGrammarRuntime()) {
// Diagnostic escape hatch (grammar-health probe only): force-load a blocked
// grammar to test whether a fresh build / newer runtime survives — the signal
// that a block can be LIFTED. Never set in normal operation: it disables the
// crash protection and can abort the process.
if (process.env.PILENS_UNSAFE_FORCE_GRAMMAR_LOAD === "1")
return null;
const block = BLOCKED_GRAMMARS[filename];
return block?.blocked(rt) ? block.reason : null;
}
/**
* Fetch one grammar wasm into `destDir` (atomic via a temp file). Returns true
* on success. Never throws — a failed fetch (offline, 4xx) degrades to "grammar
* unavailable" so callers can decide how to handle it. A grammar that crashes the
* runtime is protected at LOAD time (BLOCKED_GRAMMARS / grammarBlockReason), not
* by refusing to download it.
*/
export async function downloadGrammar(destDir, filename) {
try {
fs.mkdirSync(destDir, { recursive: true });
const res = await fetch(grammarSourceUrl(filename));
if (!res.ok)
throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
const tmp = path.join(destDir, `.${filename}.${process.pid}.tmp`);
fs.writeFileSync(tmp, data);
fs.renameSync(tmp, path.join(destDir, filename));
return true;
}
catch {
return false;
}
}