release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user