135 lines
3.4 KiB
JavaScript
135 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Enforce valid JSON everywhere and schemaVersion markers on SF-owned contracts.
|
|
*
|
|
* Ecosystem JSON such as package.json, tsconfig.json, lockfiles, and extension
|
|
* manifests are parsed for validity but are not treated as SF data contracts.
|
|
* Their `version` fields belong to their owning tools or component release
|
|
* lifecycle. SF-owned runtime/data contracts use `schemaVersion` for shape
|
|
* compatibility.
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
|
|
const CONTRACT_EXACT_PATHS = new Set([
|
|
"src/resources/extensions/sf/workflow-templates/registry.json",
|
|
]);
|
|
|
|
const CONTRACT_PREFIXES = ["src/resources/extensions/sf/learning/data/"];
|
|
|
|
function trackedJsonFiles() {
|
|
try {
|
|
const out = execFileSync("git", ["ls-files", "*.json"], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
return out
|
|
.split("\n")
|
|
.map((line) => line.trim())
|
|
.filter((line) => line && existsSync(line));
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
throw new Error(`failed to list tracked JSON files: ${message}`);
|
|
}
|
|
}
|
|
|
|
export function isSfOwnedJsonContract(path) {
|
|
return (
|
|
CONTRACT_EXACT_PATHS.has(path) ||
|
|
CONTRACT_PREFIXES.some((prefix) => path.startsWith(prefix))
|
|
);
|
|
}
|
|
|
|
export function hasOwn(object, key) {
|
|
return Object.hasOwn(object, key);
|
|
}
|
|
|
|
export function getSchemaVersion(parsed) {
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
return false;
|
|
if (hasOwn(parsed, "schemaVersion")) return parsed.schemaVersion;
|
|
|
|
const meta = parsed._meta;
|
|
if (
|
|
meta &&
|
|
typeof meta === "object" &&
|
|
!Array.isArray(meta) &&
|
|
hasOwn(meta, "schemaVersion")
|
|
) {
|
|
return meta.schemaVersion;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function hasValidSchemaVersion(parsed) {
|
|
const schemaVersion = getSchemaVersion(parsed);
|
|
return (
|
|
typeof schemaVersion === "number" &&
|
|
Number.isInteger(schemaVersion) &&
|
|
schemaVersion >= 1
|
|
);
|
|
}
|
|
|
|
export function checkJsonPolicy(paths, readText) {
|
|
const failures = [];
|
|
let contractsChecked = 0;
|
|
let filesParsed = 0;
|
|
|
|
for (const path of paths) {
|
|
filesParsed++;
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(readText(path));
|
|
} catch (error) {
|
|
if (
|
|
error &&
|
|
typeof error === "object" &&
|
|
"code" in error &&
|
|
error.code === "ENOENT"
|
|
) {
|
|
filesParsed--;
|
|
continue;
|
|
}
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
failures.push(`${path}: invalid JSON (${message})`);
|
|
continue;
|
|
}
|
|
|
|
if (!isSfOwnedJsonContract(path)) continue;
|
|
contractsChecked++;
|
|
|
|
if (!hasValidSchemaVersion(parsed)) {
|
|
failures.push(
|
|
`${path}: missing numeric schemaVersion marker (top-level or _meta)`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return { failures, filesParsed, contractsChecked };
|
|
}
|
|
|
|
export function run() {
|
|
const result = checkJsonPolicy(trackedJsonFiles(), (path) =>
|
|
readFileSync(path, "utf8"),
|
|
);
|
|
|
|
if (result.failures.length > 0) {
|
|
console.error("Versioned JSON check failed:");
|
|
for (const failure of result.failures) {
|
|
console.error(` - ${failure}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
`Versioned JSON check passed (${result.filesParsed} JSON file${result.filesParsed === 1 ? "" : "s"} parsed, ` +
|
|
`${result.contractsChecked} SF contract${result.contractsChecked === 1 ? "" : "s"} checked).`,
|
|
);
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
run();
|
|
}
|