新增: - 书库导入导出(library_import/library_export)及测试 - 图片变体生成(image_variants)与响应式图片(responsiveImage) - 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件 - 小组件运行时(widgetRuntime)与静态检查(widgetLint) - 上传缓存中间件(upload_cache)与字体 CSS 提取脚本 其它: - 后端 handlers/services 全量调整 - 前端页面、组件、库函数与配置更新
80 lines
3.4 KiB
JavaScript
80 lines
3.4 KiB
JavaScript
// 构建后处理:把 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)`
|
||
);
|