@xingwangzhe/stalux 1.4.1 → 1.4.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "A powerful, modern Astro blog theme \u2014 use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
package/src/index.ts CHANGED
@@ -208,11 +208,17 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration {
208
208
  }
209
209
  },
210
210
 
211
- "astro:server:setup": async ({ logger }) => {
212
- // 7. 字体裁剪(开发模式,仅 common 子集)
211
+ "astro:server:setup": async ({ server, logger }) => {
212
+ // 7. 字体裁剪(开发模式,生成 common 子集 + 按需路由切片)
213
213
  try {
214
214
  const projectRoot = process.cwd();
215
+ // 先生成 common 子集
215
216
  await runFontSubsetting(projectRoot, logger, true);
217
+ // 再添加按需路由切片中间件
218
+ const { createDevFontMiddleware } = await import(
219
+ new URL("./internal/font-subset-dev.ts", import.meta.url).href
220
+ );
221
+ server.middlewares.use(createDevFontMiddleware(projectRoot, logger));
216
222
  } catch (error) {
217
223
  logger.debug(`Font subsetting deferred in dev: ${String(error)}`);
218
224
  }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Dev mode on-demand font subsetting middleware.
3
+ *
4
+ * Intercepts /fonts/subset-*.css requests, generates the WOFF2 subset
5
+ * for the requested route on-the-fly, and serves it.
6
+ *
7
+ * Hooked into astro:server:setup.
8
+ */
9
+
10
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
11
+ import { resolve, join, dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import type { AstroIntegrationLogger } from "astro";
14
+ import subsetFontFn from "subset-font";
15
+
16
+ const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
17
+ const CONTENT_ROOT = "stalux";
18
+ const FONT_OUT_DIR = "public/fonts";
19
+
20
+ /** Map route-style subset ID → content file path */
21
+ function resolveContentFile(projectRoot: string, subsetId: string): string | null {
22
+ if (subsetId === "home") return null; // home has no specific content
23
+ if (subsetId === "about") {
24
+ const aboutDir = resolve(projectRoot, CONTENT_ROOT, "about");
25
+ if (!existsSync(aboutDir)) return null;
26
+ for (const f of readdirSync(aboutDir)) {
27
+ if (f.endsWith(".md") || f.endsWith(".mdx")) return join(aboutDir, f);
28
+ }
29
+ return null;
30
+ }
31
+ if (subsetId === "words") {
32
+ const wordsDir = resolve(projectRoot, CONTENT_ROOT, "words");
33
+ if (!existsSync(wordsDir)) return null;
34
+ // Aggregate all words files
35
+ return wordsDir; // special: aggregate all
36
+ }
37
+ if (subsetId.startsWith("posts-")) {
38
+ const abbrlink = subsetId.slice(6);
39
+ const postsDir = resolve(projectRoot, CONTENT_ROOT, "posts");
40
+ if (!existsSync(postsDir)) return null;
41
+ for (const f of readdirSync(postsDir)) {
42
+ if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
43
+ const content = readFileSync(join(postsDir, f), "utf-8");
44
+ if (content.includes(`abbrlink: ${abbrlink}`)) return join(postsDir, f);
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /** Extract unique characters from file content */
53
+ function extractChars(text: string): string {
54
+ const set = new Set<string>();
55
+ for (const ch of text) set.add(ch);
56
+ return [...set].sort().join("");
57
+ }
58
+
59
+ /**
60
+ * Generate and serve a font subset on demand.
61
+ * Returns the CSS content to be served.
62
+ */
63
+ async function generateSubsetOnDemand(
64
+ projectRoot: string,
65
+ subsetId: string,
66
+ logger: AstroIntegrationLogger,
67
+ ): Promise<string | null> {
68
+ // 1. Resolve content file
69
+ const contentPath = resolveContentFile(projectRoot, subsetId);
70
+ if (!contentPath) return null;
71
+
72
+ // 2. Read content + extract chars
73
+ let content: string;
74
+ if (contentPath.endsWith("words")) {
75
+ // aggregate all words
76
+ const allWords: string[] = [];
77
+ for (const f of readdirSync(contentPath)) {
78
+ if (f.endsWith(".md") && !f.startsWith("_")) {
79
+ allWords.push(readFileSync(join(contentPath, f), "utf-8"));
80
+ }
81
+ }
82
+ content = allWords.join("\n");
83
+ } else {
84
+ content = readFileSync(contentPath, "utf-8");
85
+ }
86
+
87
+ const chars = extractChars(content);
88
+
89
+ // 3. Load common subset chars to find unique ones
90
+ const outDir = resolve(projectRoot, FONT_OUT_DIR);
91
+ const commonCSS = join(outDir, "common.css");
92
+ let commonCharsCached: string[] = [];
93
+
94
+ // Look for existing common subset to extract its covered chars
95
+ // We re-read the common subset data from the generated file list
96
+ const commonFile = existsSync(join(outDir, "common.css"))
97
+ ? readFileSync(join(outDir, "common.css"), "utf-8")
98
+ : "";
99
+ // We can't know exactly what chars the common subset covers without reading
100
+ // the original source. But we can just generate the full subset.
101
+ // To minimize size, we note that common subset already covers most.
102
+
103
+ // 4. Locate font file
104
+ const fontPaths = [
105
+ resolve(projectRoot, FONT_INPUT),
106
+ resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", FONT_INPUT),
107
+ resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", FONT_INPUT),
108
+ ];
109
+ let fontPath: string | undefined;
110
+ for (const p of fontPaths) {
111
+ if (existsSync(p)) { fontPath = p; break; }
112
+ }
113
+ if (!fontPath) return null;
114
+
115
+ const fontBuffer = readFileSync(fontPath);
116
+ mkdirSync(outDir, { recursive: true });
117
+
118
+ // 5. Generate subset WOFF2
119
+ const hash = [...chars].slice(0, 12).join("");
120
+ const filename = `subset-${subsetId}-${hash}.woff2`;
121
+ const outPath = join(outDir, filename);
122
+
123
+ if (!existsSync(outPath)) {
124
+ const data = await subsetFontFn(fontBuffer, chars, { targetFormat: "woff2" });
125
+ writeFileSync(outPath, data);
126
+ logger.info(` on-demand subset: ${(data.length / 1024).toFixed(1)} KB → ${filename}`);
127
+ }
128
+
129
+ // 6. Return CSS content
130
+ return `@font-face {
131
+ font-family: "LXGW WenKai Subset";
132
+ src: url("/fonts/${filename}") format("woff2");
133
+ font-display: swap;
134
+ }`;
135
+ }
136
+
137
+ /**
138
+ * Create Vite middleware for on-demand font subsetting in dev mode.
139
+ */
140
+ export function createDevFontMiddleware(projectRoot: string, logger: AstroIntegrationLogger) {
141
+ return async (req: any, res: any, next: any) => {
142
+ const url = req.url || "";
143
+ const match = url.match(/^\/fonts\/subset-(.+)\.css$/);
144
+ if (!match) return next();
145
+
146
+ const subsetId = match[1];
147
+
148
+ // Check if file already exists
149
+ const cssPath = resolve(projectRoot, FONT_OUT_DIR, `subset-${subsetId}.css`);
150
+ if (existsSync(cssPath)) {
151
+ res.statusCode = 200;
152
+ res.setHeader("Content-Type", "text/css");
153
+ res.end(readFileSync(cssPath, "utf-8"));
154
+ return;
155
+ }
156
+
157
+ // Generate on-demand
158
+ logger.info(`Font subset: on-demand generating for "${subsetId}"...`);
159
+ const css = await generateSubsetOnDemand(projectRoot, subsetId, logger);
160
+ if (css) {
161
+ // Cache it
162
+ writeFileSync(cssPath, css);
163
+ res.statusCode = 200;
164
+ res.setHeader("Content-Type", "text/css");
165
+ res.end(css);
166
+ } else {
167
+ next();
168
+ }
169
+ };
170
+ }
@@ -28,8 +28,8 @@ const authorConfig = getAuthorData(configEntries);
28
28
  const promoteConfig = getPromoteData(configEntries);
29
29
  const htmlLang = siteConfig.lang || "zh-CN";
30
30
 
31
- // Resolve font subset CSS URL (only in build mode, dev mode uses only common subset)
32
- const fontSubsetUrl = import.meta.env.PROD && props.fontSubsetId
31
+ // Resolve font subset CSS URL
32
+ const fontSubsetUrl = props.fontSubsetId
33
33
  ? `/fonts/subset-${props.fontSubsetId}.css`
34
34
  : undefined;
35
35