grub-basket_SP/scripts/validate-release.mjs

96 lines
4.6 KiB
JavaScript

// Release validation — a cheap structural gate that runs on every production
// build (see package.json "build"), so a broken artifact never ships a version.
//
// Why this exists: 0.122.9 shipped a CSS comment containing "mod-*/is-frameless".
// The "*/" closed the comment early; the leftover text ran into the next rule's
// selector and CSS parsers silently dropped that rule (Windows/Linux frameless
// padding). Nothing caught it — typecheck only sees TS, eslint only lints src/,
// and styles.css is merely copied to the vault, never parsed.
//
// The class of bug is "syntax that's technically balanced but corrupts a rule",
// which a brace/comment-counter would miss — so we run a REAL CSS parse via
// esbuild (already a dependency) and fail on any warning. Plus JSON validity,
// version agreement, and a built-artifact sanity check.
import esbuild from "esbuild";
import { readFileSync, statSync } from "fs";
import vm from "vm";
let failed = false;
const fail = (m) => { console.error(" ✗ " + m); failed = true; };
const pass = (m) => console.log(" ✓ " + m);
const fmtLoc = (loc) => (loc ? ` (line ${loc.line}, col ${loc.column})` : "");
// 1. CSS — real parse. esbuild reports parse problems as warnings (and hard
// syntax errors as thrown errors); treat either as a release blocker.
console.log("styles.css");
try {
const css = readFileSync("styles.css", "utf8");
const res = await esbuild.transform(css, { loader: "css" });
if (res.warnings.length) {
for (const w of res.warnings) fail(`styles.css: ${w.text}${fmtLoc(w.location)}`);
} else {
pass("parses clean");
}
} catch (e) {
const errs = e.errors ?? [{ text: e.message }];
for (const m of errs) fail(`styles.css: ${m.text}${fmtLoc(m.location)}`);
}
// 2. manifest.json — valid JSON + required Obsidian fields present and
// non-empty + version agrees with package.json. A manifest that's valid
// JSON but missing/blank a required field still loads-fails or trips store
// review, so JSON-validity alone isn't enough.
console.log("manifest.json");
let manifest = null, pkgVer = null;
try { manifest = JSON.parse(readFileSync("manifest.json", "utf8")); pass("valid JSON"); }
catch (e) { fail(`manifest.json: ${e.message}`); }
if (manifest) {
let fieldsOk = true;
const bad = (m) => { fail(m); fieldsOk = false; };
// Obsidian's required manifest keys (string, non-empty).
for (const k of ["id", "name", "version", "minAppVersion", "description", "author"]) {
if (typeof manifest[k] !== "string" || manifest[k].trim() === "") bad(`manifest.json: "${k}" must be a non-empty string`);
}
// isDesktopOnly is optional but, if present, must be a boolean (a stray
// "true"/0 silently changes platform gating).
if ("isDesktopOnly" in manifest && typeof manifest.isDesktopOnly !== "boolean") bad(`manifest.json: "isDesktopOnly" must be a boolean`);
// version should look like a plain semver (no leading "v" — Obsidian's
// tag convention — and store review rejects odd version strings).
if (typeof manifest.version === "string" && !/^\d+\.\d+\.\d+(\.\d+)?$/.test(manifest.version)) {
bad(`manifest.json: version "${manifest.version}" is not a plain N.N.N(.N) string`);
}
if (fieldsOk) pass(`required fields present (id "${manifest.id}", v${manifest.version}, minApp ${manifest.minAppVersion})`);
}
try { pkgVer = JSON.parse(readFileSync("package.json", "utf8")).version; pass(`package.json valid (v${pkgVer})`); }
catch (e) { fail(`package.json: ${e.message}`); }
if (manifest?.version && pkgVer) {
if (manifest.version !== pkgVer) fail(`version mismatch: manifest ${manifest.version} vs package ${pkgVer}`);
else pass("manifest/package versions match");
}
// 3. main.js — size floor AND a real parse. The size floor catches a grossly
// empty/half-written file; the parse (vm.Script compiles WITHOUT executing)
// catches truncation/corruption that left syntactically broken JS — e.g. a
// build killed mid-write. Parsing ≠ running, so this is side-effect-free.
console.log("main.js");
try {
const js = readFileSync("main.js", "utf8");
if (js.length < 1024) {
fail(`main.js is only ${js.length} bytes — did the production build run?`);
} else {
try {
new vm.Script(js, { filename: "main.js" }); // throws on a syntax/parse error
pass(`present + parses (${Math.round(js.length / 1024)} KB)`);
} catch (e) {
fail(`main.js failed to parse (truncated/corrupt build?): ${e.message}`);
}
}
} catch { fail("main.js missing — run `pnpm run build` first"); }
if (failed) {
console.error("\nRelease validation FAILED — fix the above before shipping a version.");
process.exit(1);
}
console.log("\nRelease validation passed.");