- 默认零第三方自动请求:移除 21 个模板的 preconnect/dns-prefetch、PrismJS+autoloader、qrcode-generator 及内置一言/IP 定位 API - 一言与代码高亮配色改为自托管;欢迎弹窗自定义 IP 接口改为 opt-in;微信分享改为复制链接;LightGallery 按 GPLv3 使用 - README 新增「外部资源与隐私」「验证与回归测试」章节,修正无效链接并补充字体 OFL 来源;同步 docs 与修正虚构的目录树 - friends 等 12 处 #strings.substring 加空值保护;logo 由两份 1.5MB 压缩去重为单份 146KB - 新增 pnpm audit:external 与打包前置合规审计;制品补齐 34 个 docs 文档与预览图
83 lines
3.2 KiB
JavaScript
83 lines
3.2 KiB
JavaScript
/**
|
||
* 构建后置处理:
|
||
* 1. 校验主题 logo 存在且只保留一份(public/assets/logo.png → templates/assets/logo.png)
|
||
* 2. 合并 compat/assets 下的历史版本静态资源,避免站点/代理缓存中的旧 HTML
|
||
* 仍然引用上一版本的哈希文件名时出现 500(FileNotFoundException)
|
||
* 3. 校验所有 templates/*.html 引用的静态资源真实存在,缺失即构建失败
|
||
*/
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||
const assetsDir = path.join(root, "templates", "assets");
|
||
const compatDir = path.join(root, "compat", "assets");
|
||
const templatesDir = path.join(root, "templates");
|
||
|
||
fs.mkdirSync(assetsDir, { recursive: true });
|
||
|
||
// 1. logo:旧实现把 templates/logo.png 再复制一份到 assets,安装包里会有两份 1.5MB 大图。
|
||
// 现在源文件统一放在 public/assets/logo.png(256x256 压缩版),这里只做存在性校验。
|
||
if (!fs.existsSync(path.join(assetsDir, "logo.png"))) {
|
||
console.error("[postbuild] 未找到 templates/assets/logo.png(public/assets/logo.png 应随构建复制)");
|
||
process.exit(1);
|
||
}
|
||
console.log("[postbuild] logo.png 已去重,仅保留 templates/assets/logo.png");
|
||
|
||
// 2. 历史资源合并(已存在的同名文件以当前构建为准,不覆盖)
|
||
if (fs.existsSync(compatDir)) {
|
||
let copied = 0;
|
||
let skipped = 0;
|
||
for (const entry of fs.readdirSync(compatDir, { withFileTypes: true })) {
|
||
if (!entry.isFile()) continue;
|
||
const src = path.join(compatDir, entry.name);
|
||
const dst = path.join(assetsDir, entry.name);
|
||
if (fs.existsSync(dst)) {
|
||
skipped++;
|
||
continue;
|
||
}
|
||
fs.copyFileSync(src, dst);
|
||
copied++;
|
||
}
|
||
console.log(`[compat] 历史静态资源:复制 ${copied} 个,当前构建已覆盖 ${skipped} 个`);
|
||
}
|
||
|
||
// 3. 资源引用校验
|
||
const missing = [];
|
||
const checked = new Set();
|
||
/** 主题静态资源前缀,必须与 astro.config.mjs 的 base 保持一致 */
|
||
const prefixes = ["/themes/warm-island/assets/"];
|
||
|
||
/** Thymeleaf 的 @{...} 链接表达式同样需要校验 */
|
||
const REFERENCE_RE =
|
||
/(?:href|src)=["']([^"']*?\/(?:themes\/[^/]+)\/assets\/[^"']+?)["']/g;
|
||
|
||
for (const file of fs.readdirSync(templatesDir)) {
|
||
if (!file.endsWith(".html")) continue;
|
||
const html = fs.readFileSync(path.join(templatesDir, file), "utf8");
|
||
REFERENCE_RE.lastIndex = 0;
|
||
let match;
|
||
while ((match = REFERENCE_RE.exec(html)) !== null) {
|
||
let ref = match[1];
|
||
if (ref.includes("@{")) continue; // 由 Halo 运行时解析,跳过
|
||
for (const prefix of prefixes) {
|
||
if (!ref.startsWith(prefix)) continue;
|
||
const rel = ref.slice(prefix.length).split("#")[0].split("?")[0];
|
||
const key = `${file}:${rel}`;
|
||
if (checked.has(key)) continue;
|
||
checked.add(key);
|
||
if (!fs.existsSync(path.join(assetsDir, rel))) {
|
||
missing.push(`${file} -> ${ref}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (missing.length > 0) {
|
||
console.error(`[verify] 发现 ${missing.length} 处静态资源缺失:`);
|
||
for (const item of missing) console.error(` - ${item}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(`[verify] 静态资源引用校验通过(${checked.size} 条引用)`);
|