Files
xiaoluolyg--god/scripts/validate-public-site.mjs
wehub-resource-sync aa1a5d5763
CI / Release hygiene (push) Has been cancelled
CI / Python targeted tests (push) Has been cancelled
CI / Frontend build (push) Has been cancelled
Pages / Build and deploy public pages (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:17 +08:00

249 lines
7.7 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import vm from "node:vm";
const DEFAULT_BASE = "docs/site";
function usage() {
console.log(
[
"Usage: node scripts/validate-public-site.mjs [docs/site|https://example.github.io/GOD/]",
"",
"Validates GOD public-site package preview pages and their key assets."
].join("\n")
);
}
function isRemoteBase(base) {
return /^https?:\/\//i.test(base);
}
function joinUrl(base, target) {
return new URL(target, base.endsWith("/") ? base : base + "/").toString();
}
async function readText(base, target) {
if (isRemoteBase(base)) {
const response = await fetch(joinUrl(base, target), { redirect: "follow" });
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return response.text();
}
return fs.readFile(path.join(base, target), "utf8");
}
async function checkExists(base, target) {
if (!target || target === "#") return false;
if (/^https?:\/\//i.test(target)) {
const response = await fetch(target, { method: "HEAD", redirect: "follow" });
return response.ok;
}
if (isRemoteBase(base)) {
const response = await fetch(joinUrl(base, target), { method: "HEAD", redirect: "follow" });
return response.ok;
}
try {
await fs.access(path.join(base, target));
return true;
} catch {
return false;
}
}
function parseCatalog(source) {
const sandbox = { window: {} };
vm.runInNewContext(source, sandbox, { filename: "catalog.js" });
if (!sandbox.window.GOD_CATALOG) {
throw new Error("catalog.js did not define window.GOD_CATALOG");
}
return sandbox.window.GOD_CATALOG;
}
function assertContains(errors, html, needle, label, page) {
if (!html.includes(needle)) {
errors.push(`${page}: missing ${label}`);
}
}
function hasHan(value) {
return /[\u3400-\u9fff]/.test(String(value || ""));
}
function assetPathFromManifest(slug, manifest) {
if (!manifest || !manifest.preview_url) return "";
return `public-data/map-packs/${slug}/${manifest.preview_url}`;
}
async function validateMapPack(base, entry) {
const errors = [];
const slug = entry.slug;
const page = `map-packs/${slug}/index.html`;
const html = await readText(base, page).catch((error) => {
errors.push(`${page}: ${error.message}`);
return "";
});
if (html) {
assertContains(errors, html, `data-map-pack="${slug}"`, "data-map-pack marker", page);
assertContains(errors, html, 'class="page-hero"', "page hero", page);
assertContains(errors, html, "data-map-image", "map preview image", page);
assertContains(errors, html, "data-map-stats", "map stats target", page);
assertContains(errors, html, "data-map-locations", "map locations target", page);
assertContains(errors, html, "map-pack-viewer.js", "map viewer script", page);
assertContains(errors, html, "nav-links", "navigation links", page);
}
const manifestPath = `public-data/map-packs/${slug}/map_pack.json`;
const manifest = await readText(base, manifestPath)
.then(JSON.parse)
.catch((error) => {
errors.push(`${manifestPath}: ${error.message}`);
return null;
});
for (const location of manifest?.locations || []) {
const name = location?.localized?.en?.name || "";
if (!name || hasHan(name)) {
errors.push(`${manifestPath}: location ${location.id || "unknown"} needs an English localized name`);
}
}
const preview = entry.image || assetPathFromManifest(slug, manifest);
if (preview && !(await checkExists(base, preview))) {
errors.push(`${page}: missing preview asset ${preview}`);
}
return errors;
}
async function validateAgentPack(base, entry) {
const errors = [];
const slug = entry.slug;
const page = `agent-packs/${slug}/index.html`;
const html = await readText(base, page).catch((error) => {
errors.push(`${page}: ${error.message}`);
return "";
});
if (html) {
assertContains(errors, html, `data-agent-pack="${slug}"`, "data-agent-pack marker", page);
assertContains(errors, html, 'class="page-hero"', "page hero", page);
assertContains(errors, html, "data-agent-stats", "agent stats target", page);
assertContains(errors, html, "data-agent-grid", "agent grid target", page);
assertContains(errors, html, "agent-pack-viewer.js", "agent viewer script", page);
assertContains(errors, html, "nav-links", "navigation links", page);
}
const manifestPath = `public-data/agent-packs/${slug}/agent_pack.json`;
const manifest = await readText(base, manifestPath)
.then(JSON.parse)
.catch((error) => {
errors.push(`${manifestPath}: ${error.message}`);
return null;
});
for (const agent of manifest?.agents || []) {
const display = agent?.localized?.en || {};
if (!display.name || hasHan(display.name)) {
errors.push(`${manifestPath}: agent ${agent.id || "unknown"} needs an English localized name`);
}
if (!display.summary || hasHan(display.summary)) {
errors.push(`${manifestPath}: agent ${agent.id || "unknown"} needs an English localized summary`);
}
}
const firstSprite = manifest?.agents?.find((agent) => agent.sprite?.path)?.sprite?.path;
if (firstSprite) {
const spritePath = `public-data/agent-packs/${slug}/${firstSprite}`;
if (!(await checkExists(base, spritePath))) {
errors.push(`${page}: missing sprite asset ${spritePath}`);
}
}
return errors;
}
async function validateReplay(base, entry) {
const errors = [];
const slug = entry.slug;
const page = `replays/${slug}/index.html`;
const html = await readText(base, page).catch((error) => {
errors.push(`${page}: ${error.message}`);
return "";
});
if (html) {
assertContains(errors, html, `data-replay-slug="${slug}"`, "data-replay-slug marker", page);
assertContains(errors, html, "data-language-toggle", "language toggle", page);
assertContains(errors, html, "data-step-range", "timeline range", page);
assertContains(
errors,
html,
"Local setup gives the best experience. Online replays are static previews with limited live-control features.",
"local-experience note",
page
);
}
const timelinePath = `public-data/replays/${slug}/timeline.json`;
const timeline = await readText(base, timelinePath)
.then(JSON.parse)
.catch((error) => {
errors.push(`${timelinePath}: ${error.message}`);
return [];
});
if (!Array.isArray(timeline) || timeline.length === 0) {
errors.push(`${timelinePath}: expected at least one timeline frame`);
return errors;
}
const firstFrame = timeline[0]?.frame_url;
if (firstFrame && !(await checkExists(base, `public-data/replays/${slug}/${firstFrame}`))) {
errors.push(`${timelinePath}: missing first frame ${firstFrame}`);
}
return errors;
}
async function main() {
const arg = process.argv[2] || DEFAULT_BASE;
if (arg === "-h" || arg === "--help") {
usage();
return;
}
const base = arg;
const catalog = parseCatalog(await readText(base, "data/catalog.js"));
const allErrors = [];
for (const entry of catalog.mapPacks || []) {
allErrors.push(...(await validateMapPack(base, entry)));
}
for (const entry of catalog.agentPacks || []) {
allErrors.push(...(await validateAgentPack(base, entry)));
}
for (const entry of catalog.replays || []) {
allErrors.push(...(await validateReplay(base, entry)));
}
if (allErrors.length) {
console.error(`Public site validation failed for ${base}`);
for (const error of allErrors) {
console.error(`- ${error}`);
}
process.exitCode = 1;
return;
}
console.log(`Public site validation passed for ${base}`);
}
main().catch((error) => {
console.error(error.stack || error.message);
process.exitCode = 1;
});