/* npm packages compiled natively: every program under tests/fixtures/npm/cases * (plus the commander fixtures in npm-cases.ts) builds with the default * static npm compile and runs against Node. A case matches Node (stdout and * exit status for every argv list, stderr when it opts in with * `// @stderr`) unless KNOWN records how it differs: * * - build: the compile refuses it with this diagnostic code; * - run: it compiles, and the program's output names this diagnostic code * (a refusal deferred to the operation that reached it), for every argv * list or for the listed `argvs`, while the other lists match Node; * - divergence: it runs but does not match Node yet. These run as expected * failures, so a fix that makes one match fails here until KNOWN drops it. * * The fixture node_modules are committed test data. Binaries compile the * package sources at build time and read nothing from node_modules at run * time. */ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { globSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; import { describe, expect, test } from "vitest"; import { compile } from "@scriptc/compiler"; import { npmCases, type NpmCase } from "./npm-cases.js"; import { shardSelect, shardSuffix } from "./shard.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); const fixturesRoot = join(repoRoot, "tests/fixtures"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; type Known = { build: string } | { run: string; argvs?: string[][] } | { divergence: string }; const KNOWN: Record = { "any-chaining": { build: "SC2003" }, "assert-shims": { run: "SC1043" }, "buffer-shims": { run: "SC2020" }, "bytes-exit": { build: "SC2004" }, "cjs-esm-named": { build: "SC2013" }, "cjs-lexer-aligned": { build: "SC2013" }, "commander-calc": { run: "SC1090", argvs: [["--help"]] }, "commander-calc-async": { run: "SC1090", argvs: [["--help"]] }, "create-require": { run: "SC1090" }, "dotfolder": { run: "SC1090" }, "dual-package-kinds": { build: "SC2013" }, "dynamic-import": { build: "SC2012" }, "dynamic-import-local": { build: "SC2012" }, "esbuild-require": { build: "SC2013" }, "esm-missing-default": { build: "SC1090" }, "esm-missing-export": { build: "SC1090" }, "fetch-destructure": { run: "SC1031" }, "fs-shims": { run: "SC1101" }, "import-refusal": { build: "SC2020" }, "island-registry": { build: "SC2011" }, "island-timers": { divergence: "Timeout handles are numbers (typeof) and lose their ref flag once fired or cleared", }, "island-web-plumbing": { run: "SC2020" }, "lazy-traps": { build: "SC2013" }, "misc-shims": { run: "SC2020" }, "namespace": { build: "SC1013" }, "node24-async-events": { run: "SC2020" }, "node24-commonjs-metadata": { run: "SC2020" }, "node24-crypto-kdf": { run: "SC2020" }, "node24-deep-equality": { run: "SC2020" }, "node24-diagnostics-async": { run: "SC1090" }, "node24-diagnostics-tracing": { run: "SC1090" }, "node24-env": { run: "SC1090" }, "node24-esm-metadata": { run: "SC1090" }, "node24-event-emitter": { run: "SC2020" }, "node24-file-url-conversion": { build: "SC1090" }, "node24-fs-descriptors": { run: "SC1090" }, "node24-mime": { run: "SC2020" }, "node24-mime-errors": { run: "SC1090" }, "node24-no-error-assertions": { run: "SC2020" }, "node24-parseargs": { run: "SC1090" }, "node24-promise-timers": { run: "SC2020" }, "node24-string-decoder": { run: "SC2020" }, "node24-style-text": { run: "SC2020" }, "node24-system-errors": { run: "SC1090" }, "node24-text-encoding": { run: "SC2020" }, "node24-text-utilities": { run: "SC2020" }, "node24-timers-unref": { run: "SC2020" }, "node24-url-factories": { run: "SC2020" }, "node24-url-mutation": { run: "SC1090" }, "node24-util-adapters": { run: "Object.create with this value is not supported yet" }, "node24-util-types": { run: "SC1090" }, "node24-web-stream-encoding": { run: "SC1090" }, "node24-web-stream-errors": { run: "SC1090" }, "node24-web-stream-lifecycle": { run: "SC1090" }, "node24-web-stream-pipe-abort": { run: "SC2020" }, "node24-web-stream-pipe-errors": { run: "SC1090" }, "node24-web-stream-pipes": { run: "SC1090" }, "node24-web-stream-queues": { run: "SC1090" }, "node24-web-stream-readable-lifecycle": { run: "SC2020" }, "node24-web-stream-readers": { run: "SC1090" }, "node24-web-stream-tee": { run: "SC2020" }, "node24-web-stream-transforms": { run: "SC1090" }, "package-imports": { build: "SC2013" }, "promise-bridge": { build: "SC1090" }, "self-name": { build: "SC2013" }, "stream-shims": { run: "SC1090" }, "util-shims": { run: "SC2020" }, "web-streams": { run: "SC1090" }, "workspace-copied": { build: "SC2013" }, "workspace-unclean": { build: "SC2013" }, }; const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name); /** Per-case timeouts past the default 120 s: the undici cases compile the * built-in undici implementation's module graph, which a sanitized build * on a CI runner takes minutes to compile. */ const TIMEOUTS: Record = { undici5: 600_000, undici7: 600_000 }; interface RunResult { stdout: Buffer; stderr: Buffer; exitCode: number; } function programStderr(stderr: Buffer): Buffer { if (!sanitize) return stderr; // Linux ASan emits this instrumentation warning at the first fiber swap. // Latin-1 preserves every other byte, including non-UTF-8 program output. return Buffer.from( stderr .toString("latin1") .replace( /^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm, "", ), "latin1", ); } async function runBinary(cmd: string, args: string[], cwd: string): Promise { try { // stdin closes immediately, as in differential.test.ts: a program that // reads fd 0 to EOF must not hang on an open pipe. const pending = execFileAsync(cmd, args, { cwd, encoding: "buffer" }); pending.child.stdin?.end(); const { stdout, stderr } = await pending; return { stdout, stderr, exitCode: 0 }; } catch (err) { const e = err as { code?: unknown; signal?: unknown; stdout?: Buffer; stderr?: Buffer }; if (!Buffer.isBuffer(e.stdout) || !Buffer.isBuffer(e.stderr)) throw err; const exitCode = typeof e.code === "number" ? e.code : -1; if (exitCode === -1 && typeof e.signal !== "string") throw err; return { stdout: e.stdout, stderr: e.stderr, exitCode }; } } type Built = { ok: true; binary: string } | { ok: false; codes: string[]; text: string }; /** Compile once per (entry, lane); the cache key hashes the program and the * fixture packages so edits to either rebuild. */ async function build(entry: string): Promise { const hash = createHash("sha256"); const inputs = [ entry, ...globSync(join(dirname(entry), "*.{ts,mts,cts,js,mjs,cjs,json}")), ...globSync(join(fixturesRoot, "npm/node_modules/**/*.{js,mjs,cjs,json,d.ts,node}")), ].sort(); for (const f of inputs) hash.update(f).update(readFileSync(f)); const key = hash .update(sanitize ? "san" : "plain") .digest("hex") .slice(0, 16); const outDir = join(cacheDir, `npm-${key}`); mkdirSync(outDir, { recursive: true }); const result = await compile(entry, { outPath: join(outDir, "program"), outDir, sanitize }); if (result.ok) return { ok: true, binary: result.binaryPath }; return { ok: false, codes: result.diagnostics.map((d) => d.code), text: result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), }; } async function expectMatchesNode(c: NpmCase, binary: string): Promise { const cwd = dirname(c.entry); for (const argv of c.argvs ?? [[]]) { const [nodeRes, nativeRes] = await Promise.all([ runBinary(process.execPath, [c.entry, ...argv], cwd), runBinary(binary, argv, cwd), ]); const label = argv.join(" "); if (!nodeRes.stdout.equals(nativeRes.stdout)) { expect(nativeRes.stdout.toString("utf8"), label).toBe(nodeRes.stdout.toString("utf8")); expect.unreachable("stdout differed at byte level but not after utf8 decode"); } expect(nativeRes.exitCode, label).toBe(nodeRes.exitCode); if (c.compareStderr) expect(programStderr(nativeRes.stderr), label).toEqual(nodeRes.stderr); } } test("every KNOWN entry names a case", () => { const names = new Set(npmCases(fixturesRoot).map((c) => c.name)); expect(Object.keys(KNOWN).filter((name) => !names.has(name))).toEqual([]); }); describe(`npm native (${cases.length} programs${sanitize ? ", sanitized" : ""}${shardSuffix()})`, () => { for (const c of cases) { const known = KNOWN[c.name]; if (known !== undefined && "divergence" in known) { test.fails(`${c.name} (diverges: ${known.divergence})`, async () => { const built = await build(c.entry); if (!built.ok) throw new Error(built.text); await expectMatchesNode(c, built.binary); }, 120_000); continue; } test( c.name, async () => { const built = await build(c.entry); if (known !== undefined && "build" in known) { expect(built.ok, "the compile refuses the program").toBe(false); if (!built.ok) expect(built.codes, built.text).toContain(known.build); return; } if (!built.ok) throw new Error(`npm fixture failed to compile:\n${built.text}`); if (known !== undefined && "run" in known) { for (const argv of known.argvs ?? c.argvs ?? [[]]) { const res = await runBinary(built.binary, argv, dirname(c.entry)); const output = res.stdout.toString("utf8") + res.stderr.toString("utf8"); expect(output, `${argv.join(" ")}: the deferred refusal`).toContain(known.run); } if (known.argvs === undefined) return; const refused = new Set(known.argvs.map((argv) => JSON.stringify(argv))); const argvs = (c.argvs ?? [[]]).filter((argv) => !refused.has(JSON.stringify(argv))); await expectMatchesNode({ ...c, argvs }, built.binary); return; } await expectMatchesNode(c, built.binary); }, TIMEOUTS[c.name] ?? 120_000, ); } });