pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
175 lines (164 loc) • 6.79 kB
JavaScript
/**
* Install self-test — verifies pi-lens's runtime dependency graph actually
* resolves *as installed*, independent of the host model or auth.
*
* WHY THIS EXISTS
* Issues #285 (pnpm symlink store) and #335 (nested npm install) reported
* `ResolveMessage: Cannot find package '<dep>' from ...` — pi-lens failing to
* load because a third-party RUNTIME dep was unreachable under a non-default
* package-manager layout. (The original report named `typescript`; it's since a
* devDependency — #402 — so the probe now covers the remaining runtime deps.)
* Those did not reproduce on current bun/pi, but the failure class is real and
* silent in normal dev (a flat `node_modules` always resolves). This probe makes
* it loud and CI-catchable.
*
* WHAT IT DOES
* Force-imports the modules whose TOP-LEVEL bare imports are the documented
* failure points, plus the bare specifiers directly, and checks the two
* build-script-provided assets (ast-grep CLI binary + tree-sitter grammars)
* that pnpm/bun skip by default. It runs no model and needs no credentials.
*
* USAGE
* bun scripts/install-selftest.mjs # faithful: pi's runtime is bun
* node scripts/install-selftest.mjs # also works
* Exit 0 = all critical checks passed; non-zero = at least one failed.
* `--allow-soft` downgrades the build-script-asset checks (ast-grep/grammars)
* to warnings, so a pure *resolution* regression is still a hard failure.
*/
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import * as path from "node:path";
import * as fs from "node:fs";
const here = path.dirname(fileURLToPath(import.meta.url));
const pkgRoot = path.resolve(here, "..");
const require = createRequire(import.meta.url);
const allowSoft = process.argv.includes("--allow-soft");
const results = [];
const record = (name, kind, ok, detail = "") =>
results.push({ name, kind, ok, detail });
/** Import a module by URL and record whether its (eager) dep graph resolved. */
async function probeImport(name, relPath) {
const url = new URL(
`file://${path.resolve(pkgRoot, relPath).replace(/\\/g, "/")}`,
);
try {
await import(url.href);
record(name, "resolve", true);
} catch (err) {
record(name, "resolve", false, `${err?.code || ""} ${err?.message || err}`.trim());
}
}
/** Resolve a bare specifier the way the package itself would. */
function probeResolve(spec) {
try {
require.resolve(spec);
record(spec, "resolve", true);
} catch (err) {
record(spec, "resolve", false, `${err?.code || ""} ${err?.message || err}`.trim());
}
}
// --- 1. The documented failure-point modules (eager bare imports) ----------
await probeImport("dist/index.js (entry)", "dist/index.js");
await probeImport("clients/file-utils.js (→ minimatch)", "dist/clients/file-utils.js");
await probeImport("clients/complexity-client.js (→ tree-sitter)", "dist/clients/complexity-client.js");
await probeImport("clients/bootstrap.js (→ all analyzers)", "dist/clients/bootstrap.js");
// --- 2. Direct bare-specifier resolution -----------------------------------
for (const spec of [
"minimatch",
"typebox",
"js-yaml",
"vscode-jsonrpc",
"web-tree-sitter",
"@ast-grep/napi",
]) {
probeResolve(spec);
}
// --- 3. Spawned-binary tools (the universal net) ----------------------------
// For every tool that ships its binary in a per-platform npm package
// (platformPackage: ast-grep, biome, …), if it's actually a dependency of this
// install, resolve the native binary and RUN it. Reuses the installer's own
// resolver + TOOLS registry, so any future platform-CLI tool is covered with no
// change here. This is the exact class that pnpm/bun break (skipped postinstall /
// symlink store) — the binary exists but the launcher can't reach it.
try {
const installerUrl = `file://${path
.resolve(pkgRoot, "dist/clients/installer/index.js")
.replace(/\\/g, "/")}`;
const { TOOLS, resolvePlatformPackageBinary } = await import(installerUrl);
for (const tool of TOOLS.filter((t) => t.platformPackage)) {
const label = `tool ${tool.id} binary`;
// Only probe tools whose main package is installed in THIS layout.
try {
require.resolve(`${tool.packageName}/package.json`);
} catch {
record(label, "tool", true, "not a dep of this install — skipped");
continue;
}
const bin = resolvePlatformPackageBinary(tool);
if (!bin) {
record(
label,
"tool",
false,
`platform binary not resolved (${process.platform}-${process.arch})`,
);
continue;
}
try {
execFileSync(bin, tool.checkArgs ?? ["--version"], {
stdio: "ignore",
timeout: 15000,
});
record(label, "tool", true, bin);
} catch (err) {
record(label, "tool", false, `${bin} failed to run: ${err?.message || err}`);
}
}
} catch (err) {
record("spawned-tool probe", "tool", false, `installer load failed: ${err?.message || err}`);
}
// tree-sitter grammars — download-grammars.js postinstall writes them into
// node_modules/web-tree-sitter/grammars/. Locate that dir via the resolved
// web-tree-sitter package (matches tree-sitter-client's runtime strategy).
let grammarDetail = "tree-sitter-*.wasm missing (postinstall skipped?)";
let hasCoreGrammar = false;
try {
// web-tree-sitter restricts `exports`, so resolve its main entry and walk
// up to the package root (the dir literally named web-tree-sitter), where
// download-grammars.js writes grammars/.
let dir = path.dirname(require.resolve("web-tree-sitter"));
while (
path.basename(dir) !== "web-tree-sitter" &&
dir !== path.dirname(dir)
) {
dir = path.dirname(dir);
}
const grammarDir = path.join(dir, "grammars");
hasCoreGrammar = fs.existsSync(
path.join(grammarDir, "tree-sitter-typescript.wasm"),
);
if (hasCoreGrammar) grammarDetail = grammarDir;
} catch (err) {
grammarDetail = `web-tree-sitter unresolved: ${err?.message || err}`;
}
record("tree-sitter grammars", "asset", hasCoreGrammar, grammarDetail);
// --- Report ----------------------------------------------------------------
const pad = Math.max(...results.map((r) => r.name.length));
let hardFail = 0;
let softFail = 0;
for (const r of results) {
const soft = (r.kind === "asset" || r.kind === "tool") && allowSoft;
const status = r.ok ? "PASS" : soft ? "WARN" : "FAIL";
if (!r.ok) soft ? softFail++ : hardFail++;
console.log(
` [${status}] ${r.name.padEnd(pad)} ${r.detail ? "— " + r.detail : ""}`,
);
}
console.log(
`\nselftest: ${results.length - hardFail - softFail} passed, ${hardFail} failed${
softFail ? `, ${softFail} warned` : ""
} (runtime: ${process.versions.bun ? "bun " + process.versions.bun : "node " + process.versions.node})`,
);
process.exit(hardFail > 0 ? 1 : 0);