feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增: - 书库导入导出(library_import/library_export)及测试 - 图片变体生成(image_variants)与响应式图片(responsiveImage) - 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件 - 小组件运行时(widgetRuntime)与静态检查(widgetLint) - 上传缓存中间件(upload_cache)与字体 CSS 提取脚本 其它: - 后端 handlers/services 全量调整 - 前端页面、组件、库函数与配置更新
This commit is contained in:
79
frontend/scripts/extract-font-css.mjs
Normal file
79
frontend/scripts/extract-font-css.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
// 构建后处理:把 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)`
|
||||
);
|
||||
Reference in New Issue
Block a user