pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
142 lines (134 loc) • 5.91 kB
JavaScript
/**
* Where web-tree-sitter is installed — ONE ladder, for every caller that needs
* the answer (#3409).
*
* Callers: `clients/tree-sitter-client.ts` (the grammar read path and the
* lazy-fetch write target), `clients/install-diagnostics.ts` (the pasted
* environment fingerprint) and `scripts/install-selftest.mjs` (the installed
* host's own diagnostic). Each used to spell its own rungs, and the two that led
* with a BARE specifier were unconditionally broken on the host pi ships: inside
* a `bun build --compile` binary `require.resolve("web-tree-sitter")` throws
* MODULE_NOT_FOUND while `require.resolve("web-tree-sitter/tree-sitter.wasm")` —
* an explicit file subpath — still resolves.
*
* It lives in `scripts/lib/` rather than `clients/` because the installed
* selftest is packaged (`files[]`) without `clients/`, and this is the house
* pattern for logic a packaged .mjs script and the TypeScript runtime share
* (`scripts/lib/skills-predicate.mjs`, `scripts/lib/process-scan.mjs`). It
* imports nothing but `node:fs` and `node:path`, so it loads even on the hosts
* whose broken dependency graph the selftest exists to report.
*/
import * as fs from "node:fs";
import * as path from "node:path";
/** The package this module is about. Also the name its manifest must carry. */
const WEB_TREE_SITTER = "web-tree-sitter";
/**
* The wasm the runtime loads, at the package root — where 0.25.10's `exports`
* map puts it (`"./tree-sitter.wasm": "./tree-sitter.wasm"`). If a later release
* moves it (#381's 0.26 migration), the two FALLBACK rungs below stop matching
* and the resolver rungs, which ask Node for that same subpath, keep working;
* this constant is then the one line to update.
*/
const RUNTIME_WASM = "tree-sitter.wasm";
/**
* True when `dir` IS an installed web-tree-sitter package: its own manifest
* names it, and the wasm the runtime loads is in it.
*
* Round 1 review of #3409 (R3418-1): the constructed rungs used to accept any
* directory that merely EXISTED at `<base>/node_modules/web-tree-sitter`, so an
* empty or foreign directory of that name became the runtime fetch's write
* target and a downloaded grammar would land in an unrelated tree. Existence is
* not identity. The manifest name is the identity; the wasm is what makes the
* directory the right place to put grammars, since a grammar is only loadable by
* the web-tree-sitter runtime sitting next to it (the ABI drift #1564 is about).
*/
export function isWebTreeSitterPackageDir(dir) {
try {
if (!fs.existsSync(path.join(dir, RUNTIME_WASM))) return false;
const manifest = JSON.parse(
fs.readFileSync(path.join(dir, "package.json"), "utf8"),
);
return manifest?.name === WEB_TREE_SITTER;
} catch {
return false;
}
}
/**
* The package directory owning `resolvedFile`: walk up until a directory passes
* the identity check, or the filesystem root is reached. The walk exists because
* an `exports` map may point into a subdirectory, so the file's own parent is
* not necessarily the package; the identity check is what stops the walk, so a
* resolver that answers outside any web-tree-sitter package yields nothing
* rather than an unrelated ancestor.
*/
function packageDirOf(resolvedFile) {
let dir = path.dirname(resolvedFile);
for (;;) {
if (isWebTreeSitterPackageDir(dir)) return dir;
const parent = path.dirname(dir);
if (parent === dir) return undefined;
dir = parent;
}
}
/**
* web-tree-sitter's installed package directory, or undefined.
*
* Rung order, most authoritative first:
* 1. the `tree-sitter.wasm` SUBPATH, which resolves on a compiled host as well
* as a plain one. This is the rung that makes the non-core grammars
* fetchable there at all.
* 2. the BARE specifier, for a future web-tree-sitter whose exports map no
* longer carries `./tree-sitter.wasm`.
* 3. pi-lens's own package root, for the temp-dir compile layout (#20) where
* the resolver's context is a temp directory but the package root still has
* the right `node_modules`.
* 4. the working directory, the pre-#3409 last resort.
*
* Rungs 1-2 are the resolver's answer and outrank the constructed guesses: the
* grammars must come from the SAME package whose wasm the runtime loaded. Every
* rung's candidate passes `isWebTreeSitterPackageDir` before it is returned.
*
* `resolve`, `packageRoot` and `cwd` are injected, never defaulted: the caller's
* resolver context and package root are what differ on a compiled host, so they
* must not be silently replaced by this module's — and a rung nothing can vary
* is a rung no test can drive.
*/
export function resolveWebTreeSitterPackageDir(deps) {
for (const specifier of [
`${WEB_TREE_SITTER}/${RUNTIME_WASM}`,
WEB_TREE_SITTER,
]) {
try {
const dir = packageDirOf(deps.resolve(specifier));
if (dir) return dir;
} catch {
/* next rung */
}
}
for (const base of [deps.packageRoot, deps.cwd]) {
try {
const candidate = path.join(base(), "node_modules", WEB_TREE_SITTER);
if (isWebTreeSitterPackageDir(candidate)) return candidate;
} catch {
/* next rung */
}
}
return undefined;
}
/**
* The directory holding the core grammars an INSTALLED pi-lens can load, or
* undefined (#1185). The tarball ships them in `<packageRoot>/grammars` and the
* runtime reads that dir first (`bundledGrammarsDir` in
* clients/tree-sitter-client.ts); `<web-tree-sitter>/grammars` is only the
* lazy-fetch write target and nothing at install time fills it. The selftest
* used to probe only the latter, so it warned in every layout.
*/
export function findCoreGrammarDir({ packageRoot, webTreeSitterDir }) {
return [
path.join(packageRoot, "grammars"),
webTreeSitterDir && path.join(webTreeSitterDir, "grammars"),
].find(
(dir) =>
dir && fs.existsSync(path.join(dir, "tree-sitter-typescript.wasm")),
);
}