#!/usr/bin/env node
/**
* verify-site-routing.mjs — 校验组件端入口、SPA 路由契约和 sitemap 覆盖。
* 只使用 Node 内置模块,不启动服务、不修改文件。
*/
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
const ROOT = process.cwd();
const SITE = join(ROOT, 'site');
const COMPONENTS = join(SITE, 'components');
const app = readFileSync(join(SITE, 'app.js'), 'utf8');
const data = JSON.parse(readFileSync(join(SITE, 'data.json'), 'utf8'));
const sitemap = readFileSync(join(ROOT, 'sitemap.xml'), 'utf8');
const platforms = ['h5', 'react', 'vue2', 'vue3'];
const failures = [];
const check = (ok, message) => { if (!ok) failures.push(message); };
const components = data.components || [];
check(components.length === JSON.parse(readFileSync(join(ROOT, '.design_library', 'kole-ui', 'components', 'index.json'), 'utf8')).components.length, `component count differs from index: ${components.length}`);
check(app.includes("(?:\\/([a-z0-9-]+))?"), 'SPA platform route parser is missing');
check(app.includes("PLATFORM_KINDS = { h5: 'html', react: 'jsx', vue2: 'vue2', vue3: 'vue3' }"), 'platform kind map is missing');
check(app.includes("a.href = componentPageHref(c, platform)"), 'other-platform links do not use component pages');
/* ---- 路由形态:History API(URL 里没有 #) ----
站点根由 pathname 里第一段 "/site/" 反推,site/index.html 的内联脚本与 app.js
各算一次,两处必须用同一条规则;服务器(nginx / dev-server)与 Pages 发布包
必须有对应的深链兜底,否则深层路径直接 404。 */
const indexHtml = readFileSync(join(SITE, 'index.html'), 'utf8');
const devServer = readFileSync(join(SITE, 'dev-server.js'), 'utf8');
const nginx = readFileSync(join(ROOT, 'nginx.conf'), 'utf8');
const pagesWorkflow = readFileSync(join(ROOT, '.github/workflows/deploy-pages.yml'), 'utf8');
check(app.includes("p.indexOf('/site/')"), 'app.js SITE_BASE rule is missing');
check(indexHtml.includes("p.indexOf('/site/')"), 'index.html SITE_BASE rule is missing');
check(indexHtml.includes("document.write('/site/…,tools/snapshot-site.mjs 产出) ----
站点根是按 pathname 里第一段 "/site/" 反推的,所以快照目录名必须叫 site;
服务器两侧都要有对应的深链兜底,否则版本页一刷新就 404。 */
check(/\^\/\(\[0-9\]\+\\\.\[0-9\]\+\\\.\[0-9\]\+\)\/site\//.test(nginx), 'nginx.conf lacks the versioned SPA fallback');
check(nginx.includes('site/versions\\.json'), 'nginx.conf lacks the versions.json cache rule');
check(/VERSION_DIR = \/\^\\d\+\\\.\\d\+\\\.\\d\+\$\//.test(devServer), 'dev-server.js does not allow version snapshot directories');
check(/spaMatch\[1\]/.test(devServer), 'dev-server.js does not resolve the versioned SPA shell');
check(app.includes("versionsManifestUrl"), 'app.js version manifest loader is missing');
check(indexHtml.includes('id="version-trigger"') && indexHtml.includes('id="version-menu"'), 'index.html version switcher markup is missing');
let shellCount = 0;
for (const component of components) {
const slug = component.slug;
const legacy = join(COMPONENTS, `${slug}.html`);
check(existsSync(legacy), `missing legacy shell: ${slug}.html`);
if (existsSync(legacy)) {
const legacyText = readFileSync(legacy, 'utf8');
check(!legacyText.includes('kole-ui/"site/components'), `legacy canonical is malformed: ${slug}`);
check(legacyText.includes('url=../component/' + slug), `wrong legacy route target: ${slug}.html`);
check(!legacyText.includes('#/'), `legacy shell still uses a hash route: ${slug}.html`);
}
for (const platform of platforms) {
const shell = join(COMPONENTS, slug, `${platform}.html`);
shellCount += 1;
check(existsSync(shell), `missing platform shell: ${slug}/${platform}.html`);
if (!existsSync(shell)) continue;
const text = readFileSync(shell, 'utf8');
check(text.includes(`url=../../component/${slug}/${platform}`), `wrong route target: ${slug}/${platform}`);
check(!text.includes('#/'), `shell still uses a hash route: ${slug}/${platform}`);
check(text.includes(`/site/components/${slug}/${platform}.html`), `wrong canonical: ${slug}/${platform}`);
check(!text.includes('kole-ui/"site/components'), `malformed canonical: ${slug}/${platform}`);
}
}
const urlCount = (sitemap.match(//g) || []).length;
/* 场景页(S4-P11 模板页库)也在 sitemap 里,按磁盘实际存在的 .html 逐个核对 ——
比硬编码 5 个文件名更强:新增/删除模板页而忘了同步 build-site.ps1 时这里会红。 */
const scenarioDir = join(SITE, 'scenario');
const scenarioPages = existsSync(scenarioDir)
? readdirSync(scenarioDir).filter((f) => f.endsWith('.html'))
: [];
check(scenarioPages.length > 0, 'site/scenario has no template pages');
for (const f of scenarioPages) {
check(sitemap.includes(`/site/scenario/${f}`), `scenario page missing from sitemap: ${f}`);
}
check(urlCount === 1 + components.length * (1 + platforms.length) + scenarioPages.length,
`sitemap URL count is ${urlCount}, expected ${1 + components.length * (1 + platforms.length) + scenarioPages.length}`);
/* 基址由 SITE_URL_BASE 决定(build-site.ps1),故这里只校验「索引条目存在且路径正确」,
不锁定主机名——旧写法写死了占位域名,传入真实基址后必然误报。
反例:SITE_URL_BASE=https://kole-ui.mymoyu.top/ 时旧写法报 'site index is missing from sitemap',
而 sitemap 里确有 https://kole-ui.mymoyu.top/site/index.html。 */
const indexLoc = (sitemap.match(/([^<]*\/site\/index\.html)<\/loc>/) || [])[1];
check(Boolean(indexLoc), 'site index is missing from sitemap');
/* 占位域名不得随包发布(tools/release.mjs 的既有约定)——这里由误报改为真实断言 */
check(!sitemap.includes('YOUR-ACCOUNT'), 'sitemap still uses the placeholder domain; set SITE_URL_BASE before packaging');
const dirs = readdirSync(COMPONENTS).filter((name) => {
const path = join(COMPONENTS, name);
return existsSync(path) && statSync(path).isDirectory();
});
check(dirs.length === components.length, `platform shell directories are ${dirs.length}, expected ${components.length}`);
check(shellCount === components.length * platforms.length, `platform shell count is ${shellCount}`);
if (failures.length) {
console.error(`[routing] FAIL (${failures.length})`);
failures.forEach((failure) => console.error(`- ${failure}`));
process.exit(1);
}
console.log(`[routing] OK: ${components.length} components / ${shellCount} platform shells / ${urlCount} sitemap URLs`);