#!/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`);