Files
jiang13-bbs/frontend/scripts/extract-font-css.mjs
freefire ff2ab286fb feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

80 lines
3.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 构建后处理:把 next/font 生成的 Noto Sans SC 字体声明(6 字重 × ~101 个
// unicode-range 切片 ≈ 606 条 @font-face,压缩后 ~200 KiB)从渲染阻塞的 CSS
// chunk 中抽到 public/fonts/noto-sans-sc.css,由 app/layout.tsx 注入的脚本异步
// 加载。next/font 机制原样保留:woff2 仍在 /_next/static/media/(immutable),
// 度量校正 fallback 面与 Jakarta/JetBrains 声明仍留在阻塞 CSS。
//
// 挂载点:package.json 的 postbuild(构建后必跑)与 predev(有上次构建产物时
// 让 dev 也有字体;无产物则静默跳过)。
//
// 用法:node scripts/extract-font-css.mjs
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const staticDir = join(root, ".next", "static");
const outDir = join(root, "public", "fonts");
const outFile = join(outDir, "noto-sans-sc.css");
// 递归收集 .next/static 下所有产物 CSS(chunks 与 css 目录都扫,与 bundler 无关)
function collectCssFiles(dir) {
if (!existsSync(dir)) return [];
const out = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, entry.name);
if (entry.isDirectory()) out.push(...collectCssFiles(p));
else if (entry.isFile() && entry.name.endsWith(".css")) out.push(p);
}
return out;
}
// @font-face 内无嵌套花括号(压缩产物),逐块提取
const FACE_RE = /@font-face\{[^{}]*\}/g;
// font-family 值精确等于 "Noto Sans SC"(产物中可能不带引号);
// "Noto Sans SC Fallback" 因值不等而自然排除,必须留在阻塞 CSS 兜度量
function isNotoFace(block) {
const m = block.match(/font-family\s*:\s*([^;}]+)/);
if (!m) return false;
return m[1].trim().replace(/^["']|["']$/g, "") === "Noto Sans SC";
}
const cssFiles = collectCssFiles(staticDir);
let extracted = [];
const touched = new Map(); // file -> 重写后的内容
for (const file of cssFiles) {
const content = readFileSync(file, "utf8");
const faces = content.match(FACE_RE);
if (!faces) continue;
const noto = faces.filter(isNotoFace);
if (noto.length === 0) continue;
// 抽出块里的相对 URL(../media/)改为绝对路径:CSS 将从 /fonts/ 引用
extracted.push(...noto.map((b) => b.replaceAll("../media/", "/_next/static/media/")));
let next = content;
for (const b of noto) next = next.replace(b, "");
touched.set(file, next);
}
if (extracted.length === 0) {
if (existsSync(outFile) || !existsSync(staticDir)) {
// 已抽取过 / 上游产物无字体声明(无构建产物时的 predev 场景):静默跳过
console.log("[extract-font-css] 无可抽取的 Noto Sans SC 声明,跳过");
process.exit(0);
}
// 有构建产物却扫不到字体声明:bundler 行为可能变化,必须响亮失败
console.error(
"[extract-font-css] 构建产物中未找到 Noto Sans SC @font-face,字体加载机制可能已变化,请人工检查 .next/static 下的 CSS"
);
process.exit(1);
}
mkdirSync(outDir, { recursive: true });
writeFileSync(outFile, extracted.join("\n") + "\n");
for (const [file, content] of touched) writeFileSync(file, content);
console.log(
`[extract-font-css] 已抽出 ${extracted.length} 条 @font-face → public/fonts/noto-sans-sc.css(涉及 ${touched.size} 个 chunk)`
);