pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
236 lines (222 loc) • 7.77 kB
JavaScript
// scripts/warm-loader-cache.mjs (#1926)
//
// Pay pi's jiti transform of dist/index.js at install time, so no interactive
// session pays it.
//
// pi loads an extension with jiti, which Babel-transforms the entry and caches
// the result under <tmpdir>/jiti. pi-lens's entry is a ~4MB esbuild bundle, so
// the transform is the dominant startup cost exactly once per build: the first
// session after a `git:` install or update measured 4847ms of `module import`,
// against 138ms once the cache is warm. `prepare` has just built that bundle,
// so this script transforms it through the same jiti and writes the same cache
// entry pi will read.
//
// See scripts/lib/warm-loader-cache.mjs for why the entry this writes is the
// entry pi reads — the cache key, the transform-version marker that makes a
// mismatch a miss instead of a corruption, and the measurement that proved the
// output byte-identical to pi's own.
//
// POSTURE: best-effort, last in the chain, always exit 0. `prepare` also runs
// `build:dist` and `download-grammars`, which consumers depend on and which
// MUST fail loudly. A cache warm is an optimisation and must never share their
// failure path — the same split `scripts/setup-git-hooks.mjs` makes (#1804).
import * as fs from "node:fs";
import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { HOST_PROVIDED_PACKAGES } from "./lib/host-provided-deps.mjs";
import {
appendBounded,
buildStubAliases,
expectedCacheFileName,
resolveJitiCacheDir,
verifyCacheEntry,
warmSkipReason,
} from "./lib/warm-loader-cache.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const require = createRequire(import.meta.url);
function log(message) {
process.stderr.write(`[warm-loader-cache] ${message}\n`);
}
/**
* Append one JSONL record to PI_LENS_INSTALL_LOG, or install.log under
* PI_LENS_HOME (falling back to ~/.pi-lens when neither is set).
*
* pi's install output scrolls away, so without this there is no record that the
* warm ran, was skipped, or failed. One bounded line per install keeps the
* question answerable after the fact. `PI_LENS_INSTALL_LOG` redirects the file,
* which is how the tests read the record back.
*/
function record(entry) {
try {
const override = process.env.PI_LENS_INSTALL_LOG;
const home = process.env.PI_LENS_HOME?.trim();
const file =
typeof override === "string" && override.length > 0
? override
: path.join(home || path.join(os.homedir(), ".pi-lens"), "install.log");
fs.mkdirSync(path.dirname(file), { recursive: true });
const existing = fs.existsSync(file)
? fs.readFileSync(file, "utf8").split("\n")
: [];
const line = JSON.stringify({
ts: new Date().toISOString(),
event: "warm_loader_cache",
...entry,
});
fs.writeFileSync(file, `${appendBounded(existing, line).join("\n")}\n`);
} catch {
// The log is diagnostic. Losing it must not change the install outcome.
}
}
/** Entries pi will load, read from the manifest rather than hardcoded. */
function extensionEntries() {
try {
const pkg = JSON.parse(
fs.readFileSync(path.join(root, "package.json"), "utf8"),
);
return (pkg.pi?.extensions ?? []).map((entry) => path.resolve(root, entry));
} catch {
return [];
}
}
async function loadCreateJiti() {
// `jiti/static` is the subpath pi's loader imports, and it is export-mapped
// for `import` only — `require.resolve` reports it as not exported. Resolve
// it the ESM way, and fall back to the package root for older runtimes.
for (const specifier of ["jiti/static", "jiti"]) {
try {
const url = import.meta.resolve
? import.meta.resolve(specifier)
: pathToFileURL(require.resolve(specifier)).href;
const mod = await import(url);
if (typeof mod.createJiti === "function") return mod.createJiti;
} catch {
// Try the next specifier; absence is a skip, not a failure.
}
}
return undefined;
}
/**
* The installed jiti's version, so a host jiti bump — which changes the marker
* and silently turns every warm into a cold session — is diagnosable from the
* record rather than from a bisect.
*/
function jitiVersion() {
try {
const url = import.meta.resolve
? import.meta.resolve("jiti/package.json")
: pathToFileURL(require.resolve("jiti/package.json")).href;
return JSON.parse(fs.readFileSync(fileURLToPath(url), "utf8")).version;
} catch {
return null;
}
}
/** Real-filesystem probes for `verifyCacheEntry`. */
const fsDeps = {
existsSync: (p) => fs.existsSync(p),
readFileSync: (p) => fs.readFileSync(p, "utf8"),
isWritable: (p) => {
try {
fs.accessSync(p, fs.constants.W_OK);
return true;
} catch {
return false;
}
},
};
export async function main() {
const entries = extensionEntries();
const createJiti = await loadCreateJiti();
const skip = warmSkipReason({
env: process.env,
distEntryExists:
entries.length > 0 && entries.every((e) => fs.existsSync(e)),
jitiResolvable: typeof createJiti === "function",
});
if (skip) {
log(`skipped: ${skip}`);
record({ status: "skipped", reason: skip });
return;
}
const cacheDir = resolveJitiCacheDir({
tmpdir: os.tmpdir,
env: process.env,
cwd: process.cwd,
join: path.join,
});
const alias = buildStubAliases(HOST_PROVIDED_PACKAGES);
const version = jitiVersion();
for (const entry of entries) {
const started = Date.now();
const cacheFile = expectedCacheFileName(entry);
const jiti = createJiti(pathToFileURL(entry).href, {
// Mirror pi's loader: no in-process module cache, aliases present.
// Neither affects the transform or the cache key, but staying close to
// pi's call keeps the two easy to compare.
moduleCache: false,
fsCache: cacheDir,
alias,
});
try {
// jiti writes the cache entry BEFORE it evaluates the module, so the
// warm is complete even though evaluation then fails on the first
// host-provided import — those only resolve inside pi.
await jiti.import(entry, { default: true });
} catch {
// Expected. Evaluation is not the goal; the transform is.
}
const ms = Date.now() - started;
const relative = path.relative(root, entry);
// The import returning proves nothing. Several exit-0 paths leave no usable
// entry — an unwritable cache directory, an import that never reached the
// transform, a stale file that fails jiti's marker check — so read the file
// back before claiming anything was cached.
const verdict = verifyCacheEntry({
cacheDir,
fileName: cacheFile,
source: fs.readFileSync(entry, "utf8"),
fsDeps,
});
const common = {
entry: relative,
ms,
cacheDir,
cacheFile,
jitiVersion: version,
};
if (verdict.ok) {
log(`warmed ${relative} in ${ms}ms (cache: ${cacheDir}/${cacheFile})`);
record({
status: "warmed",
...common,
transformVersion: verdict.transformVersion,
});
continue;
}
log(`not cached: ${relative} after ${ms}ms — ${verdict.reason}`);
record({ status: "not_cached", ...common, reason: verdict.reason });
}
}
/**
* Run `work`, absorbing any failure. Exported so a test can inject a throwing
* body and assert the process still exits 0 and still leaves a record.
*/
export async function run(work = main) {
try {
await work();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
log(`failed: ${message}`);
record({ status: "failed", reason: message });
}
}
const invokedPath = process.argv[1];
const invokedDirectly =
typeof invokedPath === "string" &&
pathToFileURL(path.resolve(invokedPath)).href === import.meta.url;
if (invokedDirectly) {
await run();
}