@xingwangzhe/stalux 1.22.7 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,619 +0,0 @@
1
- /**
2
- * Per-route font subsetting engine.
3
- *
4
- * Scans each content file individually, extracts its unique character set,
5
- * and generates a minimal TTF subset per route. Also generates a "common"
6
- * subset for UI text (navs, i18n, layout).
7
- *
8
- * Output layout (under public/fonts/):
9
- * fonts/
10
- * common-{hash}.ttf — UI text subset (ASCII, nav, i18n)
11
- * subset-{hash}.ttf — per-route subset (shared by routes with same chars)
12
- * common.css — @font-face for common subset
13
- * subset-{route}.css — per-route @font-face
14
- * manifest.json — route → subset file mapping
15
- */
16
-
17
- import { createHash } from "node:crypto";
18
- import {
19
- existsSync,
20
- mkdirSync,
21
- readFileSync,
22
- writeFileSync,
23
- readdirSync,
24
- unlinkSync,
25
- } from "node:fs";
26
- import { resolve, basename, extname, join, dirname } from "node:path";
27
- import { fileURLToPath } from "node:url";
28
-
29
- import type { AstroIntegrationLogger } from "astro";
30
- // 与 Astro 内容集合同款的 YAML 解析器(astro 依赖 js-yaml 解析 frontmatter,
31
- // 见 node_modules/astro/dist/content/loaders/file.js)。用它解析 frontmatter
32
- // 得到的结果与 getCollection 的 data 一致:引号剥离、类型还原、结构完整。
33
- import { load as parseYaml } from "js-yaml";
34
- import subsetFont from "subset-font";
35
-
36
- // ---------------------------------------------------------------------------
37
- // Configuration
38
- // ---------------------------------------------------------------------------
39
-
40
- /** Content root directory (relative to project root) */
41
- const CONTENT_ROOT = "stalux";
42
-
43
- /** Full font input path (relative to project root or package) */
44
- const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
45
-
46
- /** Code font input path (Google Sans Code, deterministic static @font-face) */
47
- const CODE_FONT_INPUT = "src/assets/fonts/GoogleSansCode.woff2";
48
-
49
- /** Output directory for generated subsets */
50
- const FONT_OUT_DIR = "public/fonts";
51
-
52
- /** Characters always included in common subset */
53
- const ALWAYS_INCLUDE_CHARS = new Set(
54
- // ASCII printable
55
- [...Array(0x7e - 0x20 + 1)]
56
- .map((_, i) => String.fromCodePoint(0x20 + i))
57
- .join("")
58
- // Basic CJK punctuation
59
- .concat(
60
- "\u3000\u3001\u3002\u3008\u3009\u300a\u300b\u300c\u300d\u3010\u3011" +
61
- "\uff01\uff08\uff09\uff0c\uff0e\uff1a\uff1b\uff1f\uff5e\u2013\u2014" +
62
- "\u2018\u2019\u201c\u201d\u2026\uff09\uff08\u00b7",
63
- ),
64
- );
65
-
66
- // ---------------------------------------------------------------------------
67
- // Types
68
- // ---------------------------------------------------------------------------
69
-
70
- interface RouteCharSet {
71
- route: string;
72
- chars: string;
73
- hash: string;
74
- }
75
-
76
- interface FontManifest {
77
- common: string; // filename of common subset TTF
78
- commonCss: string; // filename of common subset CSS (content-addressed)
79
- routes: Record<string, string>; // route → subset filename
80
- /** Per-post mapping: abbrlink → subset filename */
81
- posts: Record<string, string>;
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Character extraction
86
- // ---------------------------------------------------------------------------
87
-
88
- function extractChars(text: string): string {
89
- const set = new Set<string>();
90
- for (const ch of text) set.add(ch);
91
- return [...set].sort().join("");
92
- }
93
-
94
- function hashChars(chars: string): string {
95
- return createHash("md5").update(chars).digest("hex").slice(0, 12);
96
- }
97
-
98
- // ---------------------------------------------------------------------------
99
- // Content scanning
100
- // ---------------------------------------------------------------------------
101
-
102
- interface ContentFile {
103
- route: string;
104
- filePath: string;
105
- abbrlink?: string;
106
- }
107
-
108
- /**
109
- * 用 js-yaml(内容集合同款解析器)解析 frontmatter。
110
- *
111
- * 结果与 getCollection("posts") 的 data 一致:
112
- * - abbrlink:number 转 string、引号剥离(YAML 解析天然处理)
113
- * - tags/categories:单行字符串 "Bing" 按 schema preprocess 转 ["Bing"]
114
- */
115
- function parsePostFrontmatter(content: string): {
116
- abbrlink?: string;
117
- tags: string[];
118
- categories: string[];
119
- } {
120
- const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
121
- if (!m) return { abbrlink: undefined, tags: [], categories: [] };
122
-
123
- let data: Record<string, unknown>;
124
- try {
125
- data = (parseYaml(m[1]) as Record<string, unknown>) ?? {};
126
- } catch {
127
- return { abbrlink: undefined, tags: [], categories: [] };
128
- }
129
-
130
- // 复刻 posts schema 的 preprocess:单行字符串转数组
131
- const toList = (v: unknown): string[] => {
132
- if (Array.isArray(v)) return v.map((x) => String(x));
133
- if (typeof v === "string") return v.trim() ? [v.trim()] : [];
134
- return [];
135
- };
136
-
137
- const abbr = data.abbrlink;
138
- return {
139
- abbrlink: abbr === undefined || abbr === null ? undefined : String(abbr),
140
- tags: toList(data.tags),
141
- categories: toList(data.categories),
142
- };
143
- }
144
-
145
- function scanContent(projectRoot: string): ContentFile[] {
146
- const files: ContentFile[] = [];
147
- const contentRoot = resolve(projectRoot, CONTENT_ROOT);
148
-
149
- // Posts
150
- const postsDir = join(contentRoot, "posts");
151
- const allPostContents: {
152
- abbrlink: string;
153
- tags: string[];
154
- categories: string[];
155
- content: string;
156
- }[] = [];
157
- if (existsSync(postsDir)) {
158
- for (const f of readdirSync(postsDir)) {
159
- if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
160
- const content = readFileSync(join(postsDir, f), "utf-8");
161
- const fm = parsePostFrontmatter(content);
162
-
163
- // Per-post routes:与内容集合 glob(*.{md,mdx},含 _ 前缀文件,
164
- // 见 schemas/collections.ts)保持一致——这些文件都会生成页面,
165
- // 字体子集必须覆盖,否则页面 subset css 缺失 → 404。
166
- const abbrlink = fm.abbrlink ?? basename(f, extname(f));
167
- files.push({
168
- route: `posts/${abbrlink}`,
169
- filePath: join(postsDir, f),
170
- abbrlink,
171
- });
172
- // Virtual routes (archives/tags/categories): include ALL posts including templates
173
- // because content collections load them and they render on those pages
174
- allPostContents.push({
175
- abbrlink,
176
- tags: fm.tags,
177
- categories: fm.categories,
178
- content,
179
- });
180
- }
181
- }
182
-
183
- // Build virtual routes for archives, tags, categories (aggregated pages)
184
- // These pages render content from ALL posts
185
- if (allPostContents.length > 0) {
186
- const allContent = allPostContents.map((p) => p.content).join("\n");
187
-
188
- // Archives: all posts' content (titles, dates, etc.)
189
- virtualRouteCache.set("archives", extractChars(allContent));
190
-
191
- // Tags index: all tags + all posts' content
192
- const allTags = [...new Set(allPostContents.flatMap((p) => p.tags))].join(" ");
193
- virtualRouteCache.set("tags", extractChars(allContent + allTags));
194
-
195
- // Categories index: all categories + all posts' content
196
- const allCategories = [...new Set(allPostContents.flatMap((p) => p.categories))].join(" ");
197
- virtualRouteCache.set("categories", extractChars(allContent + allCategories));
198
-
199
- // Per-tag routes: tag name + posts with that tag
200
- const tagPosts = new Map<string, string[]>();
201
- for (const p of allPostContents) {
202
- for (const tag of p.tags) {
203
- const list = tagPosts.get(tag) ?? [];
204
- list.push(p.content);
205
- tagPosts.set(tag, list);
206
- }
207
- }
208
- for (const [tag, contents] of tagPosts) {
209
- const tagContent = tag + " " + contents.join("\n");
210
- virtualRouteCache.set(`tags-${tag}`, extractChars(tagContent));
211
- }
212
-
213
- // Per-category routes: category name + posts in that category
214
- const catPosts = new Map<string, string[]>();
215
- for (const p of allPostContents) {
216
- for (const cat of p.categories) {
217
- const list = catPosts.get(cat) ?? [];
218
- list.push(p.content);
219
- catPosts.set(cat, list);
220
- }
221
- }
222
- for (const [cat, contents] of catPosts) {
223
- const catContent = cat + " " + contents.join("\n");
224
- virtualRouteCache.set(`categories-${cat}`, extractChars(catContent));
225
- }
226
- }
227
-
228
- // Build virtual routes for config-driven pages (home, links)
229
- // These pages render text from config YAML files (nav titles, site title, etc.)
230
- const configDir = join(contentRoot, "config");
231
- if (existsSync(configDir)) {
232
- const allConfigContent: string[] = [];
233
- for (const f of readdirSync(configDir)) {
234
- if (f.endsWith(".yml") || f.endsWith(".yaml")) {
235
- allConfigContent.push(readFileSync(join(configDir, f), "utf-8"));
236
- }
237
- }
238
- if (allConfigContent.length > 0) {
239
- const configText = allConfigContent.join("\n");
240
- // Home page: site title, author, navs, typetexts, social media, footer
241
- virtualRouteCache.set("home", extractChars(configText));
242
- // Links page: links config + navs + footer
243
- virtualRouteCache.set("links", extractChars(configText));
244
- }
245
- }
246
-
247
- // About
248
- const aboutDir = join(contentRoot, "about");
249
- if (existsSync(aboutDir)) {
250
- for (const f of readdirSync(aboutDir)) {
251
- if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
252
- if (f.startsWith("_")) continue;
253
- files.push({
254
- route: "about",
255
- filePath: join(aboutDir, f),
256
- });
257
- }
258
- }
259
-
260
- // Words - aggregate all words files' characters into one
261
- const wordsDir = join(contentRoot, "words");
262
- if (existsSync(wordsDir)) {
263
- const allWordsContent: string[] = [];
264
- for (const f of readdirSync(wordsDir)) {
265
- if (!f.endsWith(".md")) continue;
266
- if (f.startsWith("_")) continue;
267
- try {
268
- allWordsContent.push(readFileSync(join(wordsDir, f), "utf-8"));
269
- } catch {
270
- /* skip */
271
- }
272
- }
273
- if (allWordsContent.length > 0) {
274
- files.push({
275
- route: "words",
276
- filePath: join(wordsDir, "words-combined.md"),
277
- // We'll handle this virtual file specially in char extraction
278
- });
279
- // Store combined content in a map for later extraction
280
- wordCharsCache.set("words", extractChars(allWordsContent.join("\n")));
281
- }
282
- }
283
-
284
- // Add virtual routes (archives, tags, categories) as content files
285
- for (const [route] of virtualRouteCache) {
286
- files.push({
287
- route,
288
- filePath: resolve(projectRoot, CONTENT_ROOT, `_virtual/${route}.md`),
289
- });
290
- }
291
-
292
- return files;
293
- }
294
-
295
- // Cache for virtual route char sets
296
- const wordCharsCache = new Map<string, string>();
297
- const virtualRouteCache = new Map<string, string>();
298
-
299
- // ---------------------------------------------------------------------------
300
- // Common subset: extract chars from UI source files
301
- // ---------------------------------------------------------------------------
302
-
303
- function collectCommonChars(projectRoot: string): string {
304
- const chars = new Set(ALWAYS_INCLUDE_CHARS);
305
-
306
- // Scan all source directories for UI text characters
307
- const srcDir = resolve(projectRoot, "src");
308
-
309
- function scanDir(dir: string) {
310
- if (!existsSync(dir)) return;
311
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
312
- const fullPath = join(dir, entry.name);
313
- if (entry.isDirectory()) {
314
- scanDir(fullPath);
315
- } else if (
316
- entry.name.endsWith(".astro") ||
317
- entry.name.endsWith(".ts") ||
318
- entry.name.endsWith(".json")
319
- ) {
320
- try {
321
- const content = readFileSync(fullPath, "utf-8");
322
- // Only extract string literals and text content (skip code)
323
- for (const ch of content) chars.add(ch);
324
- } catch {
325
- /* skip */
326
- }
327
- }
328
- }
329
- }
330
-
331
- scanDir(srcDir);
332
-
333
- // Also scan user config YAML files for nav titles, descriptions, etc.
334
- const configDir = resolve(projectRoot, CONTENT_ROOT, "config");
335
- if (existsSync(configDir)) {
336
- for (const f of readdirSync(configDir)) {
337
- if (!f.endsWith(".yml") && !f.endsWith(".yaml")) continue;
338
- try {
339
- const content = readFileSync(join(configDir, f), "utf-8");
340
- for (const ch of content) chars.add(ch);
341
- } catch {
342
- /* skip */
343
- }
344
- }
345
- }
346
-
347
- // Also scan post frontmatter for tags, categories so those pages' chars are covered
348
- const postsDir = resolve(projectRoot, CONTENT_ROOT, "posts");
349
- if (existsSync(postsDir)) {
350
- for (const f of readdirSync(postsDir)) {
351
- if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
352
- if (f.startsWith("_")) continue;
353
- try {
354
- const content = readFileSync(join(postsDir, f), "utf-8");
355
- for (const ch of content) chars.add(ch);
356
- } catch {
357
- /* skip */
358
- }
359
- }
360
- }
361
-
362
- return [...chars].sort().join("");
363
- }
364
-
365
- // ---------------------------------------------------------------------------
366
- // Deduplication: merge files with identical char sets
367
- // ---------------------------------------------------------------------------
368
-
369
- function deduplicate(routes: RouteCharSet[]): Map<string, string[]> {
370
- // hash → [route1, route2, ...]
371
- const groups = new Map<string, string[]>();
372
- for (const r of routes) {
373
- const list = groups.get(r.hash) ?? [];
374
- list.push(r.route);
375
- groups.set(r.hash, list);
376
- }
377
- return groups;
378
- }
379
-
380
- // ---------------------------------------------------------------------------
381
- // Main entry
382
- // ---------------------------------------------------------------------------
383
-
384
- /**
385
- * Run font subsetting during build.
386
- * Called from integration hooks.
387
- *
388
- * @param projectRoot - Project root directory
389
- * @param logger - Astro integration logger
390
- * @param skipPerRoute - Skip per-route subsets (for dev mode, only generate common)
391
- */
392
- export async function runFontSubsetting(
393
- projectRoot: string,
394
- logger: AstroIntegrationLogger,
395
- skipPerRoute = false,
396
- ): Promise<void> {
397
- logger.info("Font subsetting: scanning content...");
398
-
399
- // 1. Locate font file (check project root first, then stalux package dir)
400
- const fontPaths = [
401
- resolve(projectRoot, FONT_INPUT),
402
- resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", FONT_INPUT),
403
- resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", FONT_INPUT),
404
- ];
405
-
406
- let fontPath: string | undefined;
407
- for (const p of fontPaths) {
408
- if (existsSync(p)) {
409
- fontPath = p;
410
- break;
411
- }
412
- }
413
-
414
- if (!fontPath) {
415
- logger.warn(`Font not found at ${FONT_INPUT}, skipping subsetting`);
416
- return;
417
- }
418
- const fontBuffer = readFileSync(fontPath);
419
-
420
- // Clean old generated font files to avoid stale cached references
421
- const outDir = resolve(projectRoot, FONT_OUT_DIR);
422
- if (existsSync(outDir)) {
423
- for (const f of readdirSync(outDir)) {
424
- if (f.startsWith("common-") || f.startsWith("subset-") || f === "common.css") {
425
- try {
426
- unlinkSync(join(outDir, f));
427
- } catch {
428
- /* skip */
429
- }
430
- }
431
- }
432
- }
433
-
434
- // 1.5 代码字体(Google Sans Code)— 确定性静态 @font-face,不经过 Astro 字体管线
435
- // Astro 的 <Font> 组件会在 prerender 阶段启动一个随机端口的 HTTP server,
436
- // 端口号会嵌进 font-file-url-resolver 虚拟模块的编译产物,破坏增量构建依赖图 hash。
437
- // 这里直接复制 woff2 到 public/fonts/ 并生成静态 code.css,完全绕开该管线。
438
- try {
439
- const codeFontPaths = [
440
- resolve(projectRoot, CODE_FONT_INPUT),
441
- resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", CODE_FONT_INPUT),
442
- resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", CODE_FONT_INPUT),
443
- ];
444
- const codeFontPath = codeFontPaths.find((p) => existsSync(p));
445
- if (codeFontPath) {
446
- const codeFontBuffer = readFileSync(codeFontPath);
447
- const codeFontOut = join(outDir, "GoogleSansCode.woff2");
448
- if (!existsSync(codeFontOut) || !readFileSync(codeFontOut).equals(codeFontBuffer)) {
449
- writeFileSync(codeFontOut, codeFontBuffer);
450
- logger.info(
451
- ` code font: ${codeFontBuffer.length / 1024} KB → GoogleSansCode.woff2`,
452
- );
453
- }
454
- // 静态 CSS:@font-face + --font-code 变量(确定性内容,无 hash)
455
- const codeCSS = `/* Auto-generated code font (deterministic, no Astro font pipeline) */
456
- :root {
457
- --font-code: "Google Sans Code", "JetBrains Mono", "Fira Code", "Consolas", "Courier New", monospace;
458
- }
459
- @font-face {
460
- font-family: "Google Sans Code";
461
- src: url("/fonts/GoogleSansCode.woff2") format("woff2");
462
- font-display: swap;
463
- }
464
- `;
465
- writeFileSync(join(outDir, "code.css"), codeCSS);
466
- } else {
467
- logger.warn(
468
- `Code font not found at ${CODE_FONT_INPUT}, --font-code falls back to system monospace`,
469
- );
470
- }
471
- } catch (error) {
472
- logger.warn(`Code font setup failed: ${String(error)}`);
473
- }
474
-
475
- // 2. Scan content files
476
- const contentFiles = scanContent(projectRoot);
477
- logger.info(`Font subsetting: found ${contentFiles.length} content files`);
478
-
479
- // 3. Collect common chars (UI, nav, i18n)
480
- const commonChars = collectCommonChars(projectRoot);
481
- const commonHash = hashChars(commonChars);
482
-
483
- // 4. Extract per-route char sets
484
- const routeSets: RouteCharSet[] = [];
485
- for (const file of contentFiles) {
486
- let content: string;
487
- if (file.route === "words" && wordCharsCache.has("words")) {
488
- content = wordCharsCache.get("words")!;
489
- } else if (virtualRouteCache.has(file.route)) {
490
- content = virtualRouteCache.get(file.route)!;
491
- } else {
492
- content = readFileSync(file.filePath, "utf-8");
493
- }
494
- const chars = extractChars(content);
495
- routeSets.push({
496
- route: file.route,
497
- chars,
498
- hash: hashChars(chars),
499
- });
500
- }
501
-
502
- // 5. Deduplicate and generate subsets using subset-font (WASM)
503
- mkdirSync(outDir, { recursive: true });
504
-
505
- // Helper: subset using subset-font (async WASM)
506
- async function doSubset(chars: string): Promise<Uint8Array> {
507
- if (!chars) return new Uint8Array(0);
508
- try {
509
- const result = await subsetFont(fontBuffer, chars, {
510
- targetFormat: "sfnt", // "sfnt" = TTF output
511
- });
512
- return result;
513
- } catch {
514
- return new Uint8Array(0);
515
- }
516
- }
517
-
518
- // Generate common subset
519
- const commonFilename = `common-${commonHash}.ttf`;
520
- const commonOutPath = join(outDir, commonFilename);
521
- if (!existsSync(commonOutPath)) {
522
- const data = await doSubset(commonChars);
523
- if (data.length > 0) {
524
- writeFileSync(commonOutPath, data);
525
- logger.info(
526
- ` common subset: ${(data.length / 1024).toFixed(1)} KB → ${commonFilename}`,
527
- );
528
- }
529
- }
530
-
531
- // Generate common CSS (content-addressed filename: 字符集变化 → hash 变化 →
532
- // 文件名变化,浏览器必然拉新 CSS,避免缓存旧 CSS 引用已删除的旧 TTF 导致 404)
533
- // dev 模式(skipPerRoute)用固定名 common.css,由 dev 中间件场景复用;
534
- // 生产构建用 common-<hash>.css,HTML 从 manifest 读取引用。
535
- const commonCssName = skipPerRoute ? "common.css" : `common-${commonHash}.css`;
536
- const commonCSS = `/* Auto-generated common font subset */
537
- @font-face {
538
- font-family: "LXGW WenKai Subset";
539
- src: url("/fonts/${commonFilename}") format("truetype");
540
- font-display: swap;
541
- }
542
- `;
543
- writeFileSync(join(outDir, commonCssName), commonCSS);
544
-
545
- const groups = deduplicate(routeSets);
546
- const routeMap: Record<string, string> = {};
547
- const postMap: Record<string, string> = {};
548
-
549
- let subsetCount = 0;
550
-
551
- if (!skipPerRoute) {
552
- for (const [hash, routes] of groups) {
553
- const filename = `subset-${hash}.ttf`;
554
- const outPath = join(outDir, filename);
555
- if (!existsSync(outPath)) {
556
- const route = routeSets.find((r) => r.hash === hash);
557
- const chars = route?.chars ?? "";
558
- const uniqueChars = chars
559
- .split("")
560
- .filter((ch) => !commonChars.includes(ch))
561
- .join("");
562
- if (uniqueChars.length > 0) {
563
- const data = await doSubset(uniqueChars);
564
- if (data.length > 0) {
565
- writeFileSync(outPath, data);
566
- subsetCount++;
567
- logger.info(
568
- ` subset ${subsetCount}: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
569
- );
570
- }
571
- }
572
- }
573
-
574
- // Generate per-route CSS (each route gets its own CSS file)
575
- // CSS 文件名内容寻址:内容只引用一个 TTF,文件名带上该 TTF 的 hash,
576
- // 内容变化 → 文件名变化 → 浏览器必然拉新,避免缓存旧 CSS 引用已删除 TTF。
577
- // If no unique chars, reference the common TTF as fallback
578
- const refTtf = existsSync(outPath) ? filename : commonFilename;
579
- const refHash = existsSync(outPath) ? hash : commonHash;
580
- for (const route of routes) {
581
- const cssContent = `/* Auto-generated font subset for route: ${route} */
582
- @font-face {
583
- font-family: "LXGW WenKai Subset";
584
- src: url("/fonts/${refTtf}") format("truetype");
585
- font-display: swap;
586
- }
587
- `;
588
- const cssName = route.replace("/", "-");
589
- const cssFilename = `subset-${cssName}-${refHash}.css`;
590
- writeFileSync(join(outDir, cssFilename), cssContent);
591
- routeMap[route] = cssFilename;
592
-
593
- if (route.startsWith("posts/")) {
594
- const abbrlink = route.replace("posts/", "");
595
- postMap[abbrlink] = cssFilename;
596
- }
597
- }
598
- }
599
- }
600
-
601
- // 6. Generate manifest(dev 也生成:routes/posts 为空,供 Stalux.astro import)
602
- const manifest: FontManifest = {
603
- common: commonFilename,
604
- commonCss: commonCssName,
605
- routes: routeMap,
606
- posts: postMap,
607
- };
608
- writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
609
-
610
- // 副本写入包内 generated/:Stalux.astro import 它,让 manifest 进入 Astro
611
- // 依赖图——manifest 变化时页面必然重建,增量构建 restored 的页面不会引用旧资源。
612
- const genDir = resolve(dirname(fileURLToPath(import.meta.url)), "generated");
613
- mkdirSync(genDir, { recursive: true });
614
- writeFileSync(join(genDir, "fonts-manifest.json"), JSON.stringify(manifest));
615
-
616
- logger.info(
617
- `Font subsetting done: ${subsetCount} route subsets + common (${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → variable)`,
618
- );
619
- }