81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { existsSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import {
|
|
ContractRegistry,
|
|
checkContractCompatibility,
|
|
type ContractSnapshot,
|
|
} from "@wrnexus/validation";
|
|
|
|
const CURRENT_FILE = "wrnexus.contracts.json";
|
|
const BASELINE_FILE = join(".wrnexus", "contracts.json");
|
|
|
|
function validateSnapshot(value: unknown, source: string): ContractSnapshot {
|
|
const snapshot = value as Partial<ContractSnapshot>;
|
|
if (snapshot?.format !== 1 || !Array.isArray(snapshot.contracts)) {
|
|
throw new Error(`WRN-CONTRACT-FORMAT: ${source} is not a version 1 contract snapshot.`);
|
|
}
|
|
return snapshot as ContractSnapshot;
|
|
}
|
|
|
|
async function currentSnapshot(appRoot: string): Promise<ContractSnapshot> {
|
|
const modulePath = join(appRoot, "app", "contracts.ts");
|
|
if (existsSync(modulePath)) {
|
|
const imported = (await import(`${modulePath}?t=${Date.now()}`)) as {
|
|
default?: ContractRegistry | ContractSnapshot;
|
|
contracts?: ContractRegistry | ContractSnapshot;
|
|
};
|
|
const value = imported.contracts ?? imported.default;
|
|
if (value instanceof ContractRegistry) return value.snapshot();
|
|
return validateSnapshot(value, modulePath);
|
|
}
|
|
const jsonPath = join(appRoot, CURRENT_FILE);
|
|
if (!existsSync(jsonPath)) {
|
|
throw new Error(
|
|
`WRN-CONTRACT-SOURCE: create app/contracts.ts exporting a ContractRegistry, or ${CURRENT_FILE}.`,
|
|
);
|
|
}
|
|
return validateSnapshot(JSON.parse(await readFile(jsonPath, "utf8")), jsonPath);
|
|
}
|
|
|
|
export interface ContractCommandResult {
|
|
ok: boolean;
|
|
issueCount: number;
|
|
baseline: string;
|
|
}
|
|
|
|
export async function runContractsCommand(
|
|
root: string,
|
|
command = "check",
|
|
args: string[] = [],
|
|
): Promise<ContractCommandResult> {
|
|
const appRoot = resolve(root);
|
|
const baseline = join(appRoot, BASELINE_FILE);
|
|
const current = await currentSnapshot(appRoot);
|
|
if (command === "snapshot") {
|
|
await mkdir(dirname(baseline), { recursive: true });
|
|
await writeFile(baseline, `${JSON.stringify(current, null, 2)}\n`, "utf8");
|
|
if (args.includes("--json")) console.log(JSON.stringify({ ok: true, baseline }));
|
|
else console.log(`✓ Contract baseline written: ${baseline}`);
|
|
return { ok: true, issueCount: 0, baseline };
|
|
}
|
|
if (command !== "check") throw new Error(`WRN-CONTRACT-COMMAND: unknown command '${command}'.`);
|
|
if (!existsSync(baseline)) {
|
|
throw new Error(`WRN-CONTRACT-BASELINE: no baseline found; run 'wrnexus contracts snapshot'.`);
|
|
}
|
|
const previous = validateSnapshot(JSON.parse(await readFile(baseline, "utf8")), baseline);
|
|
const issues = checkContractCompatibility(previous, current);
|
|
if (args.includes("--json")) {
|
|
console.log(JSON.stringify({ ok: issues.length === 0, issues, baseline }, null, 2));
|
|
} else if (issues.length === 0) {
|
|
console.log(`✓ ${current.contracts.length} contracts are backward compatible.`);
|
|
} else {
|
|
console.error(`Breaking contract changes detected (${issues.length}):`);
|
|
for (const issue of issues) {
|
|
console.error(` ${issue.code} ${issue.contract}: ${issue.message}`);
|
|
if (issue.consumers.length) console.error(` Consumers: ${issue.consumers.join(", ")}`);
|
|
}
|
|
}
|
|
return { ok: issues.length === 0, issueCount: issues.length, baseline };
|
|
}
|