// Node API surface manifests. The compat target is the repo's .node-version. // // node scripts/surface.mjs record [--node=] // Run surface-walk.cjs under real Node and write compat/node-.json. // Without --node it uses this Node, which must be the pinned version. // node scripts/surface.mjs delta // What Node added, changed or removed between two recorded versions // (e.g. `delta 24.16.0 24.17.0`): the checklist for a version bump. // node scripts/surface.mjs baseline // Rewrite compat/divergences.json from the last tests/surface.spec.ts run // (compat/observed.json): keep existing reasons, add new gaps as "untriaged" // (or "refuse: " when every gap an entry covers is a marked refusal), // drop entries the kernel now satisfies. import { spawnSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { compact, compare, covers, delta, gaps } from "./surface-compare.ts"; import { oracleEnv, PINNED } from "./pinned-node.mjs"; const ROOT = resolve(fileURLToPath(new URL("..", import.meta.url))); const GUESTS = resolve(ROOT, "tests/fixtures/guests"); const COMPAT = resolve(ROOT, "compat"); const manifestPath = (version) => resolve(COMPAT, `node-${version.replace(/^v/, "")}.json`); const readJson = (path) => JSON.parse(readFileSync(path, "utf8")); const writeJson = (path, value) => writeFileSync(path, JSON.stringify(value, null, 2) + "\n"); function record(args) { const bin = args.find((a) => a.startsWith("--node="))?.slice("--node=".length) ?? process.execPath; const info = spawnSync(bin, ["-p", "JSON.stringify([process.version, process.platform, process.arch])"], { encoding: "utf8" }); if (info.status !== 0) throw new Error(`${bin}: ${info.stderr}`); const [version, platform, arch] = JSON.parse(info.stdout); if (bin === process.execPath && version !== `v${PINNED}`) { throw new Error(`this is node ${version}, .node-version pins ${PINNED}; pass --node= to record another version`); } const r = spawnSync(bin, ["surface-walk.cjs"], { cwd: GUESTS, encoding: "utf8", maxBuffer: 64 << 20, env: oracleEnv() }); if (r.status !== 0) throw new Error(`surface-walk.cjs exited ${r.status}: ${r.stderr}`); const surface = JSON.parse(r.stdout); writeJson(manifestPath(version), { node: version, platform, arch, surface }); console.log(`${manifestPath(version)}: ${Object.keys(surface).length} paths`); } function printDelta([from, to]) { if (!from || !to) throw new Error("usage: surface.mjs delta "); const d = delta(readJson(manifestPath(from)).surface, readJson(manifestPath(to)).surface); for (const g of d.added) console.log(g.actual === undefined ? `+ ${g.path} ${g.expected}` : `~ ${g.path} ${g.actual} -> ${g.expected}`); for (const g of d.removed) console.log(`- ${g.path}`); console.log(`\n${d.added.length} added or changed, ${d.removed.length} removed (outermost paths)`); } function baseline() { const observedPath = resolve(COMPAT, "observed.json"); if (!existsSync(observedPath)) throw new Error("no compat/observed.json; run `playwright test surface` first"); const node = readJson(manifestPath(PINNED)).surface; const observed = readJson(observedPath); const divergencesPath = resolve(COMPAT, "divergences.json"); const old = existsSync(divergencesPath) ? readJson(divergencesPath) : {}; const { stale } = compare(node, observed, old); const kept = Object.fromEntries(Object.entries(old).filter(([d]) => !stale.includes(d))); const open = gaps(node, observed).filter((g) => !Object.keys(kept).some((d) => covers(d, g.path))); const added = compact(node, open); const REFUSED = "refused: "; for (const path of added) { const under = open.filter((g) => covers(path, g.path)); const reasons = [...new Set(under.map((g) => g.actual?.startsWith(REFUSED) ? g.actual.slice(REFUSED.length) : null))]; kept[path] = under.length && !reasons.includes(null) ? `refuse: ${reasons.join("; ")}` : "untriaged"; } writeJson(divergencesPath, Object.fromEntries(Object.keys(kept).sort().map((k) => [k, kept[k]]))); console.log(`divergences.json: ${Object.keys(kept).length} entries (+${added.length}, -${stale.length})`); } const [command, ...rest] = process.argv.slice(2); if (command === "record") record(rest); else if (command === "delta") printDelta(rest); else if (command === "baseline") baseline(); else { console.error("usage: surface.mjs record [--node=] | delta | baseline"); process.exit(1); }