406 lines
21 KiB
JavaScript
406 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { spawnSync } from "node:child_process";
|
|
import {
|
|
lstatSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
realpathSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const METADATA_EXCLUDES = ["**/._*", "**/._*/**"];
|
|
const SENTINELS = ["src/metadata-discovery-sentinel.test.ts", "src/ordinary._fragment.test.ts"];
|
|
const SENTINEL = SENTINELS[0];
|
|
const METADATA_FILE = "src/._appledouble-file.test.ts";
|
|
const METADATA_NESTED_FILE = "src/._appledouble-directory/nested-metadata.test.ts";
|
|
|
|
function sha256(value) {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
function sha256File(filePath) {
|
|
assertRegularNoSymlink(filePath);
|
|
return sha256(readFileSync(filePath));
|
|
}
|
|
|
|
function assertRegularNoSymlink(filePath) {
|
|
const stat = lstatSync(filePath);
|
|
assert.equal(stat.isSymbolicLink(), false, `symbolic link is not accepted: ${filePath}`);
|
|
assert.equal(stat.isFile(), true, `expected a regular file: ${filePath}`);
|
|
}
|
|
|
|
function assertNoSymlinkComponents(targetPath) {
|
|
const absolute = path.resolve(targetPath);
|
|
const parsed = path.parse(absolute);
|
|
let current = parsed.root;
|
|
for (const component of absolute.slice(parsed.root.length).split(path.sep).filter(Boolean)) {
|
|
current = path.join(current, component);
|
|
try {
|
|
const stat = lstatSync(current);
|
|
assert.equal(stat.isSymbolicLink(), false, `symbolic path component is not accepted: ${current}`);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") break;
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
function hasPathPrefix(parent, candidate) {
|
|
const relative = path.relative(parent, candidate);
|
|
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
}
|
|
|
|
function canonicalizeProspectivePath(targetPath) {
|
|
let current = path.resolve(targetPath);
|
|
const suffix = [];
|
|
while (true) {
|
|
try {
|
|
const canonicalAncestor = realpathSync(current);
|
|
return path.resolve(canonicalAncestor, ...suffix.reverse());
|
|
} catch (error) {
|
|
if (error.code !== "ENOENT") throw error;
|
|
const parent = path.dirname(current);
|
|
assert.notEqual(parent, current, `no existing ancestor for evidence path: ${targetPath}`);
|
|
suffix.push(path.basename(current));
|
|
current = parent;
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNoAppleDoublePathComponents(targetPath) {
|
|
const absolute = path.resolve(targetPath);
|
|
const parsed = path.parse(absolute);
|
|
const components = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean);
|
|
for (const component of components) {
|
|
assert.equal(component.startsWith("._"), false, `AppleDouble path component is not accepted: ${component}`);
|
|
}
|
|
}
|
|
|
|
function assertFreshEvidenceDirectory(targetPath) {
|
|
assert.equal(path.isAbsolute(targetPath), true, "--evidence-dir must be absolute");
|
|
const absolute = path.resolve(targetPath);
|
|
assert.equal(absolute, targetPath, "--evidence-dir must be normalized");
|
|
assertNoAppleDoublePathComponents(absolute);
|
|
assertNoSymlinkComponents(absolute);
|
|
const canonicalProjectRoot = realpathSync(PROJECT_ROOT);
|
|
const canonicalEvidence = canonicalizeProspectivePath(absolute);
|
|
assert.equal(hasPathPrefix(canonicalProjectRoot, canonicalEvidence) || hasPathPrefix(canonicalEvidence, canonicalProjectRoot), false,
|
|
"evidence directory must not overlap the checkout");
|
|
try {
|
|
lstatSync(absolute);
|
|
assert.fail(`evidence directory already exists; use a fresh path: ${absolute}`);
|
|
} catch (error) {
|
|
if (error.code !== "ENOENT") throw error;
|
|
}
|
|
mkdirSync(path.dirname(absolute), { recursive: true });
|
|
mkdirSync(absolute, { recursive: false });
|
|
return absolute;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const token = argv[index];
|
|
if (token !== "--evidence-dir" || !argv[index + 1]) {
|
|
throw new Error("usage: verify-appledouble-metadata-discovery.mjs --evidence-dir ABSOLUTE_FRESH_PATH");
|
|
}
|
|
if (args.evidenceDir) throw new Error("--evidence-dir may be provided once");
|
|
args.evidenceDir = argv[index + 1];
|
|
index += 1;
|
|
}
|
|
if (!args.evidenceDir || !path.isAbsolute(args.evidenceDir)) {
|
|
throw new Error("--evidence-dir must be an absolute path");
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function runNode(args, cwd, stdoutPath, stderrPath) {
|
|
const result = spawnSync(process.execPath, args, {
|
|
cwd,
|
|
encoding: "utf8",
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
env: { ...process.env, CI: "1" },
|
|
});
|
|
writeFileSync(stdoutPath, result.stdout ?? "", "utf8");
|
|
writeFileSync(stderrPath, result.stderr ?? "", "utf8");
|
|
if (result.error) throw result.error;
|
|
return {
|
|
argv: [process.execPath, ...args],
|
|
cwd,
|
|
exitCode: result.status,
|
|
signal: result.signal,
|
|
stdout: { path: stdoutPath, sha256: sha256File(stdoutPath) },
|
|
stderr: { path: stderrPath, sha256: sha256File(stderrPath) },
|
|
error: result.error?.message ?? null,
|
|
};
|
|
}
|
|
|
|
function readGitHead() {
|
|
const result = spawnSync("git", ["rev-parse", "HEAD"], {
|
|
cwd: PROJECT_ROOT,
|
|
encoding: "utf8",
|
|
});
|
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
}
|
|
|
|
function generatedTest(importPath, name, body) {
|
|
return `import { expect, it } from ${JSON.stringify(importPath)};\n\nit(${JSON.stringify(name)}, () => {\n${body}\n});\n`;
|
|
}
|
|
|
|
function readVitestFiles(jsonPath, fixtureRoot) {
|
|
const report = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
const testResults = report.testResults;
|
|
assert.ok(Array.isArray(testResults), "Vitest JSON reporter must provide testResults");
|
|
return testResults.map((result) => path.relative(fixtureRoot, result.name).split(path.sep).join("/")).sort();
|
|
}
|
|
|
|
function writeJson(filePath, value) {
|
|
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
let evidenceRootForFailure = null;
|
|
|
|
async function main() {
|
|
const { evidenceDir } = parseArgs(process.argv.slice(2));
|
|
const absoluteEvidence = assertFreshEvidenceDirectory(evidenceDir);
|
|
evidenceRootForFailure = absoluteEvidence;
|
|
|
|
const reportPath = path.join(absoluteEvidence, "report.json");
|
|
const R = createRequire(path.join(PROJECT_ROOT, "package.json"));
|
|
const vitestRunner = R.resolve("vitest/vitest.mjs");
|
|
const vitestEntry = R.resolve("vitest/dist/index.js");
|
|
const vitestConfigEntry = R.resolve("vitest/dist/config.js");
|
|
const viteEntry = R.resolve("vite");
|
|
const tsEntry = R.resolve("typescript");
|
|
const vitestPackage = JSON.parse(readFileSync(path.join(PROJECT_ROOT, "node_modules", "vitest", "package.json"), "utf8"));
|
|
const vitePackage = JSON.parse(readFileSync(path.join(PROJECT_ROOT, "node_modules", "vite", "package.json"), "utf8"));
|
|
const tsPackage = JSON.parse(readFileSync(path.join(PROJECT_ROOT, "node_modules", "typescript", "package.json"), "utf8"));
|
|
const configInfo = {};
|
|
for (const name of ["vite.config.ts", "tsconfig.app.json"]) {
|
|
const actualPath = path.join(PROJECT_ROOT, name);
|
|
const actualHash = sha256File(actualPath);
|
|
configInfo[name] = { path: actualPath, sha256: actualHash };
|
|
}
|
|
|
|
const vite = await import(pathToFileURL(viteEntry).href);
|
|
const { configDefaults } = await import(pathToFileURL(vitestConfigEntry).href);
|
|
const loaded = await vite.loadConfigFromFile({ command: "serve", mode: "test" }, path.join(PROJECT_ROOT, "vite.config.ts"), PROJECT_ROOT);
|
|
assert.ok(loaded?.config, "Vite must load the actual project config");
|
|
const testConfig = loaded.config.test;
|
|
assert.deepEqual(testConfig.include, ["src/**/*.test.ts"], "candidate must preserve the normal Vitest include");
|
|
assert.ok(Array.isArray(testConfig.exclude), "project Vitest config must define exclude patterns");
|
|
for (const defaultExclude of configDefaults.exclude) {
|
|
assert.ok(testConfig.exclude.includes(defaultExclude), `Vitest built-in exclusion was dropped: ${defaultExclude}`);
|
|
}
|
|
for (const metadataExclude of METADATA_EXCLUDES) {
|
|
assert.ok(testConfig.exclude.includes(metadataExclude), `metadata exclusion is missing: ${metadataExclude}`);
|
|
}
|
|
const baselineTestConfig = {
|
|
...testConfig,
|
|
exclude: testConfig.exclude.filter((pattern) => !METADATA_EXCLUDES.includes(pattern)),
|
|
};
|
|
|
|
const fixtureRoot = path.join(absoluteEvidence, "fixture-project");
|
|
const srcRoot = path.join(fixtureRoot, "src");
|
|
mkdirSync(srcRoot, { recursive: true });
|
|
writeFileSync(path.join(srcRoot, "._appledouble-file.test.ts"),
|
|
generatedTest(vitestEntry, "AppleDouble file fixture is discoverable without the scoped exclusion", " expect(true).toBe(true);\n"), "utf8");
|
|
mkdirSync(path.join(srcRoot, "._appledouble-directory"), { recursive: true });
|
|
writeFileSync(path.join(srcRoot, "._appledouble-directory", "nested-metadata.test.ts"),
|
|
generatedTest(vitestEntry, "AppleDouble nested-directory fixture is discoverable without the scoped exclusion", " expect(true).toBe(true);\n"), "utf8");
|
|
writeFileSync(path.join(srcRoot, "metadata-discovery-sentinel.test.ts"),
|
|
generatedTest(vitestEntry, "ordinary source sentinel runs", " expect(2 + 2).toBe(4);\n"), "utf8");
|
|
writeFileSync(path.join(srcRoot, "ordinary._fragment.test.ts"),
|
|
generatedTest(vitestEntry, "ordinary filename containing dot underscore remains included", " expect(" +
|
|
"'ordinary._fragment.test.ts'.startsWith('._')).toBe(false);\n"), "utf8");
|
|
// This control is derived from the actual loaded config and removes only the two issue filters.
|
|
const baselineConfig = path.join(absoluteEvidence, "baseline-vitest.config.mjs");
|
|
writeFileSync(baselineConfig, `export default ${JSON.stringify({ test: baselineTestConfig }, null, 2)};\n`, "utf8");
|
|
|
|
const baselineJson = path.join(absoluteEvidence, "vitest-baseline-results.json");
|
|
const baseline = runNode([vitestRunner, "run", "--root", fixtureRoot, "--config", baselineConfig,
|
|
"--reporter=json", `--outputFile=${baselineJson}`, "--maxWorkers=1"], PROJECT_ROOT,
|
|
path.join(absoluteEvidence, "vitest-baseline.stdout.log"), path.join(absoluteEvidence, "vitest-baseline.stderr.log"));
|
|
const baselineFiles = readVitestFiles(baselineJson, fixtureRoot);
|
|
for (const sentinel of SENTINELS) {
|
|
assert.ok(baselineFiles.includes(sentinel), `baseline Vitest control must execute ordinary file: ${sentinel}`);
|
|
}
|
|
assert.ok(baselineFiles.includes(METADATA_FILE), "baseline control must discover the AppleDouble file fixture");
|
|
assert.ok(baselineFiles.includes(METADATA_NESTED_FILE), "baseline control must discover the nested AppleDouble fixture");
|
|
const baselineJsonResult = JSON.parse(readFileSync(baselineJson, "utf8"));
|
|
const baselineFailures = baselineJsonResult.testResults
|
|
.filter((result) => result.status === "failed")
|
|
.map((result) => ({
|
|
path: path.relative(fixtureRoot, result.name).split(path.sep).join("/"),
|
|
message: result.message,
|
|
}));
|
|
assert.ok(baseline.exitCode === 0 || baseline.exitCode === 1, "baseline must exit normally, not crash");
|
|
for (const failure of baselineFailures) {
|
|
assert.ok(path.posix.basename(failure.path).startsWith("._"),
|
|
`baseline may fail only while transforming an AppleDouble sidecar: ${failure.path}`);
|
|
assert.ok(failure.message.includes("Unexpected") && failure.message.includes("\\x00"),
|
|
`baseline AppleDouble transform failure should identify binary metadata: ${failure.path}`);
|
|
}
|
|
|
|
const candidateJson = path.join(absoluteEvidence, "vitest-candidate-results.json");
|
|
const candidate = runNode([vitestRunner, "run", "--root", fixtureRoot,
|
|
"--config", path.join(PROJECT_ROOT, "vite.config.ts"), "--mode", "test", "--reporter=json",
|
|
`--outputFile=${candidateJson}`, "--maxWorkers=1"], PROJECT_ROOT,
|
|
path.join(absoluteEvidence, "vitest-candidate.stdout.log"), path.join(absoluteEvidence, "vitest-candidate.stderr.log"));
|
|
assert.equal(candidate.exitCode, 0, "candidate Vitest discovery must pass");
|
|
const candidateFiles = readVitestFiles(candidateJson, fixtureRoot);
|
|
assert.deepEqual(candidateFiles, [...SENTINELS].sort(), "candidate discovery must keep ordinary names and exclude dot-prefix metadata");
|
|
|
|
const ts = R("typescript");
|
|
const tsconfigSource = path.join(PROJECT_ROOT, "tsconfig.app.json");
|
|
const tsconfigPath = path.join(fixtureRoot, "tsconfig.app.json");
|
|
const tsconfigBytes = readFileSync(tsconfigSource);
|
|
writeFileSync(tsconfigPath, tsconfigBytes);
|
|
const tsconfigJson = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
assert.equal(tsconfigJson.error, undefined, "TypeScript must read the copied app config");
|
|
const parsedTsconfig = ts.parseJsonConfigFileContent(tsconfigJson.config, ts.sys, fixtureRoot, undefined, tsconfigPath);
|
|
assert.deepEqual(parsedTsconfig.errors, [], "TypeScript config must parse without diagnostics");
|
|
assert.deepEqual(tsconfigJson.config.include, ["src"], "TypeScript include must retain normal src scope");
|
|
assert.deepEqual(tsconfigJson.config.compilerOptions.types, ["vite/client", "vitest/globals"],
|
|
"TypeScript configured test/browser globals must remain unchanged");
|
|
const tsFiles = parsedTsconfig.fileNames.map((filePath) => path.relative(fixtureRoot, filePath).split(path.sep).join("/")).sort();
|
|
assert.deepEqual(tsFiles, [...SENTINELS].sort(), "TypeScript must keep ordinary names and exclude dot-prefix metadata");
|
|
const expectedPackageExcludes = ["node_modules", "bower_components", "jspm_packages"];
|
|
for (const exclude of expectedPackageExcludes) {
|
|
assert.ok(tsconfigJson.config.exclude.includes(exclude), `TypeScript package exclusion was dropped: ${exclude}`);
|
|
}
|
|
for (const exclude of METADATA_EXCLUDES) {
|
|
assert.ok(tsconfigJson.config.exclude.includes(exclude), `TypeScript metadata exclusion is missing: ${exclude}`);
|
|
}
|
|
|
|
const tscPath = path.resolve(path.dirname(tsEntry), "../bin/tsc");
|
|
const baselineTsconfigPath = path.join(fixtureRoot, "tsconfig.app.without-appledouble-excludes.json");
|
|
const baselineTsconfigObject = JSON.parse(tsconfigBytes.toString("utf8"));
|
|
baselineTsconfigObject.exclude = baselineTsconfigObject.exclude.filter((pattern) => !METADATA_EXCLUDES.includes(pattern));
|
|
writeFileSync(baselineTsconfigPath, `${JSON.stringify(baselineTsconfigObject, null, 2)}\n`, "utf8");
|
|
const baselineTsconfigJson = ts.readConfigFile(baselineTsconfigPath, ts.sys.readFile);
|
|
assert.equal(baselineTsconfigJson.error, undefined, "TypeScript must read the filtered-control app config");
|
|
const baselineParsedTsconfig = ts.parseJsonConfigFileContent(
|
|
baselineTsconfigJson.config, ts.sys, fixtureRoot, undefined, baselineTsconfigPath,
|
|
);
|
|
assert.deepEqual(baselineParsedTsconfig.errors, [], "filtered-control TypeScript config must parse without diagnostics");
|
|
const baselineTsFiles = baselineParsedTsconfig.fileNames
|
|
.map((filePath) => path.relative(fixtureRoot, filePath).split(path.sep).join("/")).sort();
|
|
assert.deepEqual(baselineTsFiles, [...SENTINELS].sort(),
|
|
"TypeScript 5.8 should continue to skip dot paths even without the explicit filters");
|
|
const baselineTsc = runNode([tscPath, "--showConfig", "--project", baselineTsconfigPath], PROJECT_ROOT,
|
|
path.join(absoluteEvidence, "tsc-control-show-config.stdout.json"), path.join(absoluteEvidence, "tsc-control-show-config.stderr.log"));
|
|
assert.equal(baselineTsc.exitCode, 0, "the filtered-control TypeScript CLI --showConfig must accept the fixture config");
|
|
const baselineTscJson = JSON.parse(readFileSync(path.join(absoluteEvidence, "tsc-control-show-config.stdout.json"), "utf8"));
|
|
const baselineTscFiles = baselineTscJson.files
|
|
.map((filePath) => path.relative(fixtureRoot, path.resolve(fixtureRoot, filePath)).split(path.sep).join("/")).sort();
|
|
assert.deepEqual(baselineTscFiles, [...SENTINELS].sort());
|
|
assert.deepEqual(baselineTscFiles, baselineTsFiles,
|
|
"TypeScript compiler API and CLI must agree on filtered-control src inputs");
|
|
const tsc = runNode([tscPath, "--showConfig", "--project", tsconfigPath], PROJECT_ROOT,
|
|
path.join(absoluteEvidence, "tsc-show-config.stdout.json"), path.join(absoluteEvidence, "tsc-show-config.stderr.log"));
|
|
assert.equal(tsc.exitCode, 0, "the actual TypeScript CLI --showConfig must accept the fixture config");
|
|
const tscJson = JSON.parse(readFileSync(path.join(absoluteEvidence, "tsc-show-config.stdout.json"), "utf8"));
|
|
const tscFiles = tscJson.files.map((filePath) => path.relative(fixtureRoot, path.resolve(fixtureRoot, filePath)).split(path.sep).join("/")).sort();
|
|
assert.deepEqual(tscFiles, [...SENTINELS].sort(), "the TypeScript CLI must resolve the same two ordinary inputs");
|
|
|
|
const report = {
|
|
schemaVersion: 1,
|
|
passed: true,
|
|
issue: 11,
|
|
node: { executable: process.execPath, version: process.version },
|
|
invocationCwd: process.cwd(),
|
|
tool: { path: fileURLToPath(import.meta.url), sha256: sha256File(fileURLToPath(import.meta.url)) },
|
|
project: {
|
|
root: PROJECT_ROOT,
|
|
gitHead: readGitHead(),
|
|
configHashes: configInfo,
|
|
},
|
|
packageVersions: { vitest: vitestPackage.version, vite: vitePackage.version, typescript: tsPackage.version },
|
|
fixtures: {
|
|
root: fixtureRoot,
|
|
ordinarySentinel: { path: SENTINEL, sha256: sha256File(path.join(fixtureRoot, SENTINEL)) },
|
|
ordinaryDotUnderscoreFragment: {
|
|
path: SENTINELS[1],
|
|
sha256: sha256File(path.join(fixtureRoot, SENTINELS[1])),
|
|
reasonIncluded: "the basename contains ._ but does not begin with the AppleDouble component prefix ._",
|
|
},
|
|
metadataFile: { path: METADATA_FILE, sha256: sha256File(path.join(fixtureRoot, METADATA_FILE)) },
|
|
metadataNestedFile: { path: METADATA_NESTED_FILE, sha256: sha256File(path.join(fixtureRoot, METADATA_NESTED_FILE)) },
|
|
},
|
|
vitest: {
|
|
unfilteredControl: {
|
|
description: "actual loaded test config with only the two issue-owned excludes removed; all other settings and Vitest built-in defaults are retained",
|
|
configSha256: sha256File(baselineConfig),
|
|
removedExcludes: METADATA_EXCLUDES,
|
|
command: baseline,
|
|
jsonReport: { path: baselineJson, sha256: sha256File(baselineJson) },
|
|
exitCode: baseline.exitCode,
|
|
failures: baselineFailures,
|
|
discoveredFiles: baselineFiles,
|
|
ordinarySentinelsExecuted: SENTINELS.every((name) => baselineFiles.includes(name)),
|
|
metadataFixturesDiscovered: [METADATA_FILE, METADATA_NESTED_FILE].every((name) => baselineFiles.includes(name)),
|
|
completedNormally: baseline.exitCode === 0 || baseline.exitCode === 1,
|
|
},
|
|
candidate: {
|
|
command: candidate,
|
|
jsonReport: { path: candidateJson, sha256: sha256File(candidateJson) },
|
|
exitCode: candidate.exitCode,
|
|
discoveredFiles: candidateFiles,
|
|
expectedOrdinaryFiles: [...SENTINELS].sort(),
|
|
include: testConfig.include,
|
|
builtinExcludesPreserved: configDefaults.exclude.every((name) => testConfig.exclude.includes(name)),
|
|
metadataExcludes: METADATA_EXCLUDES,
|
|
},
|
|
},
|
|
typescript: {
|
|
compilerApi: {
|
|
parsedConfigPath: tsconfigPath,
|
|
parsedConfigSha256: sha256(tsconfigBytes),
|
|
diagnostics: parsedTsconfig.errors.length,
|
|
},
|
|
baseline: {
|
|
configPath: baselineTsconfigPath,
|
|
removedExcludes: METADATA_EXCLUDES,
|
|
parsedFiles: baselineTsFiles,
|
|
metadataFileIncludedBeforeExplicitExcludes: baselineTsFiles.includes(METADATA_FILE),
|
|
nestedMetadataIncludedBeforeExplicitExcludes: baselineTsFiles.includes(METADATA_NESTED_FILE),
|
|
note: "TypeScript 5.8 already skips dot-prefixed paths during include globbing; the candidate also retains explicit excludes for portable input hygiene.",
|
|
cliCommand: baselineTsc,
|
|
cliJsonSha256: sha256File(path.join(absoluteEvidence, "tsc-control-show-config.stdout.json")),
|
|
},
|
|
cliCommand: tsc,
|
|
cliJsonSha256: sha256File(path.join(absoluteEvidence, "tsc-show-config.stdout.json")),
|
|
parsedFiles: tsFiles,
|
|
cliParsedFiles: tscFiles,
|
|
include: tsconfigJson.config.include,
|
|
types: tsconfigJson.config.compilerOptions.types,
|
|
packageExcludesPreserved: expectedPackageExcludes.every((name) => tsconfigJson.config.exclude.includes(name)),
|
|
metadataExcludes: METADATA_EXCLUDES,
|
|
copiedConfigSha256: sha256(tsconfigBytes),
|
|
},
|
|
logs: ["vitest-baseline.stdout.log", "vitest-baseline.stderr.log", "vitest-candidate.stdout.log",
|
|
"vitest-candidate.stderr.log", "tsc-control-show-config.stderr.log", "tsc-show-config.stderr.log"].map((name) => ({
|
|
path: path.join(absoluteEvidence, name),
|
|
sha256: sha256File(path.join(absoluteEvidence, name)),
|
|
})),
|
|
};
|
|
writeJson(reportPath, report);
|
|
process.stdout.write(`${reportPath}\n`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
const safe = { passed: false, error: `${error.name}: ${error.message}` };
|
|
if (evidenceRootForFailure) {
|
|
try { writeJson(path.join(evidenceRootForFailure, "failure.json"), safe); } catch { /* preserve original failure */ }
|
|
}
|
|
process.stderr.write(`${safe.error}\n`);
|
|
process.exitCode = 1;
|
|
});
|