import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const siteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repositoryRoot = path.resolve(siteRoot, "..");
const errors = [];
const requiredFiles = [
"index.html",
"styles.css",
"app.js",
"site.config.js",
"assets/favicon.svg",
"README.md",
];
const report = (condition, message) => {
if (!condition) {
errors.push(message);
}
};
const readSiteFile = async (relativePath) => {
try {
return await readFile(path.join(siteRoot, relativePath), "utf8");
} catch {
errors.push(`缺少文件:${relativePath}`);
return "";
}
};
await Promise.all(
requiredFiles.map(async (relativePath) => {
try {
const fileStats = await stat(path.join(siteRoot, relativePath));
report(fileStats.isFile(), `不是普通文件:${relativePath}`);
} catch {
errors.push(`缺少文件:${relativePath}`);
}
}),
);
const [html, css, appJs, configJs] = await Promise.all([
readSiteFile("index.html"),
readSiteFile("styles.css"),
readSiteFile("app.js"),
readSiteFile("site.config.js"),
]);
let packageVersion = "";
try {
packageVersion = JSON.parse(
await readFile(path.join(repositoryRoot, "package.json"), "utf8"),
).version;
} catch {
errors.push("无法读取 package.json 版本");
}
for (const [relativePath, content] of [
["index.html", html],
["styles.css", css],
["app.js", appJs],
["site.config.js", configJs],
]) {
report(!/[ \t]+$/m.test(content), `${relativePath} 包含行尾空白`);
report(!content.includes("\t"), `${relativePath} 包含 Tab 缩进`);
}
report(//.test(html), "页面语言必须是 zh-CN");
report(/]/g) ?? []).length === 1, "页面必须且只能包含一个 h1");
report(/class="skip-link"\s+href="#main-content"/.test(html), "缺少跳到主要内容链接");
report(//.test(html), "缺少 main-content 主区域");
report(/aria-label="主导航"/.test(html), "主导航缺少可访问名称");
report(/data-theme-toggle/.test(html), "缺少主题切换控件");
report(/prefers-reduced-motion:\s*reduce/.test(css), "缺少减少动态效果规则");
report(/\[data-theme="dark"\]/.test(css), "缺少深色主题令牌");
for (const breakpoint of ["1199px", "959px", "719px"]) {
report(css.includes(`max-width: ${breakpoint}`), `缺少 ${breakpoint} 响应式断点`);
}
const requiredCopy = [
"Subagent 与智能路由",
"钉钉与企业微信以开发者预览提供",
"个人微信处于实验性边界",
"单次最多添加 8 个附件,支持同时传入 5 张图片",
"auto、low、medium、high",
"当前按单张结果呈现,不承诺批量多图生成",
"发布后开放",
"安全不是开关",
];
for (const copy of requiredCopy) {
report(html.includes(copy), `缺少准确文案:${copy}`);
}
report(
configJs.includes(`version: "${packageVersion}"`),
`site.config.js 版本必须与 package.json 的 ${packageVersion} 一致`,
);
report(
/releasePublished:\s*true/.test(configJs),
`v${packageVersion} Release 发布后 releasePublished 必须为 true`,
);
report(
configJs.includes(
`releaseUrl: "https://github.com/mesalogo/goodbuddy/releases/tag/v${packageVersion}"`,
),
`v${packageVersion} Release URL 配置不正确`,
);
report(
appJs.includes("config?.releasePublished === true"),
"下载链接必须受 releasePublished 配置保护",
);
report(
appJs.includes("config.releaseUrl === expectedReleaseUrl"),
"下载链接必须与配置版本对应的 GitHub Release 地址一致",
);
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index);
report(duplicateIds.length === 0, `存在重复 id:${[...new Set(duplicateIds)].join(", ")}`);
const attributes = [...html.matchAll(/\s(?:href|src)="([^"]+)"/g)].map((match) => match[1]);
const fragmentLinks = attributes.filter((value) => value.startsWith("#") && value.length > 1);
for (const fragment of fragmentLinks) {
report(ids.includes(fragment.slice(1)), `页内链接目标不存在:${fragment}`);
}
const localAssets = attributes.filter(
(value) =>
!value.startsWith("#") &&
!value.startsWith("https://") &&
!value.startsWith("http://") &&
!value.startsWith("mailto:") &&
!value.startsWith("data:"),
);
for (const asset of localAssets) {
const cleanAsset = asset.split(/[?#]/, 1)[0].replace(/^\.\//, "");
try {
const assetStats = await stat(path.join(siteRoot, cleanAsset));
report(assetStats.isFile(), `本地资源不是文件:${asset}`);
} catch {
errors.push(`本地资源不存在:${asset}`);
}
}
const externalBlankLinks = [
...html.matchAll(/]*target="_blank")[^>]*>/g),
].map((match) => match[0]);
for (const link of externalBlankLinks) {
report(/rel="[^"]*noreferrer[^"]*"/.test(link), `新窗口链接缺少 noreferrer:${link}`);
}
report(
!/]*href="[^"]+\.(?:exe|dmg|zip|AppImage|deb)(?:[?#][^"]*)?"/i.test(html),
"具体安装资产链接应由 Release 页面统一提供",
);
report(
!/(?:react|vue|angular|bootstrap|tailwind)(?:\.min)?\.(?:js|css)/i.test(html),
"静态官网不得引入额外框架资源",
);
if (errors.length > 0) {
console.error(`官网静态检查失败(${errors.length} 项):`);
for (const error of errors) {
console.error(`- ${error}`);
}
process.exitCode = 1;
} else {
console.log(
`官网静态检查通过:${requiredFiles.length} 个必需文件,${ids.length} 个唯一 id,${localAssets.length} 个本地资源引用。`,
);
}