@xingwangzhe/stalux 1.4.3 → 1.4.5

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.3",
3
+ "version": "1.4.5",
4
4
  "description": "A powerful, modern Astro blog theme \u2014 use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -78,7 +78,7 @@
78
78
  "@expressive-code/plugin-line-numbers": "^0.44.1",
79
79
  "@lucide/astro": "^1.27.0",
80
80
  "@pagefind/component-ui": "^1.5.2",
81
- "@waline/client": "3.15.2",
81
+ "@waline/client": "^3.15.2",
82
82
  "@xingwangzhe/satteri-mermaid": "0.7.1",
83
83
  "@xingwangzhe/satteri-photoswipe": "^0.2.0",
84
84
  "@xingwangzhe/tags-cloud": "^1.2.4",
package/src/index.ts CHANGED
@@ -9,7 +9,9 @@
9
9
  */
10
10
 
11
11
  import path from "node:path";
12
+ import { resolve } from "node:path";
12
13
  import { fileURLToPath } from "node:url";
14
+ import { existsSync, readdirSync } from "node:fs";
13
15
 
14
16
  import type { AstroIntegration } from "astro";
15
17
  // pagefind 是 ESM-only 包,需要在模块顶层导入
@@ -212,13 +214,27 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration {
212
214
  // 7. 字体裁剪(开发模式,生成 common 子集 + 按需路由切片)
213
215
  try {
214
216
  const projectRoot = process.cwd();
217
+ // 清除旧的字体缓存
218
+ const oldFontDir = resolve(projectRoot, "public/fonts");
219
+ if (existsSync(oldFontDir)) {
220
+ // 删除旧的 subset-*.css 和 common-*.woff2,让按需中间件重新生成
221
+ for (const f of readdirSync(oldFontDir)) {
222
+ if (f.startsWith("subset-") || f.startsWith("common-")) continue;
223
+ }
224
+ }
215
225
  // 先生成 common 子集
216
226
  await runFontSubsetting(projectRoot, logger, true);
217
- // 再添加按需路由切片中间件
227
+ // 再添加按需路由切片中间件(放在最前面,优先拦截字体请求)
218
228
  const { createDevFontMiddleware } = await import(
219
229
  new URL("./internal/font-subset-dev.ts", import.meta.url).href
220
230
  );
221
- server.middlewares.use(createDevFontMiddleware(projectRoot, logger));
231
+ const fontMiddleware = createDevFontMiddleware(projectRoot, logger);
232
+ server.middlewares.use((req: any, res: any, next: any) => {
233
+ if (req.url?.startsWith("/fonts/")) {
234
+ return fontMiddleware(req, res, next);
235
+ }
236
+ next();
237
+ });
222
238
  } catch (error) {
223
239
  logger.debug(`Font subsetting deferred in dev: ${String(error)}`);
224
240
  }
@@ -10,6 +10,7 @@
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
11
11
  import { resolve, join, dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
+
13
14
  import type { AstroIntegrationLogger } from "astro";
14
15
  import subsetFontFn from "subset-font";
15
16
 
@@ -22,61 +23,62 @@ const FONT_OUT_DIR = "public/fonts";
22
23
  * Returns null if route is unknown.
23
24
  */
24
25
  function getRouteChars(projectRoot: string, subsetId: string): string | null {
25
- const contentRoot = resolve(projectRoot, CONTENT_ROOT);
26
-
27
- // ── Posts ──────────────────────────────────────────────
28
- if (subsetId.startsWith("posts-")) {
29
- const abbrlink = subsetId.slice(6);
30
- const postsDir = join(contentRoot, "posts");
31
- if (!existsSync(postsDir)) return null;
32
- for (const f of readdirSync(postsDir)) {
33
- if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
34
- const content = readFileSync(join(postsDir, f), "utf-8");
35
- if (content.includes(`abbrlink: ${abbrlink}`)) return content;
36
- }
26
+ const contentRoot = resolve(projectRoot, CONTENT_ROOT);
27
+
28
+ // ── Posts ──────────────────────────────────────────────
29
+ if (subsetId.startsWith("posts-")) {
30
+ const abbrlink = subsetId.slice(6);
31
+ const postsDir = join(contentRoot, "posts");
32
+ if (!existsSync(postsDir)) return null;
33
+ for (const f of readdirSync(postsDir)) {
34
+ if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
35
+ const content = readFileSync(join(postsDir, f), "utf-8");
36
+ if (content.includes(`abbrlink: ${abbrlink}`)) return content;
37
+ }
38
+ }
39
+ return null;
37
40
  }
38
- return null;
39
- }
40
-
41
- // ── About ──────────────────────────────────────────────
42
- if (subsetId === "about") {
43
- const aboutDir = join(contentRoot, "about");
44
- if (!existsSync(aboutDir)) return null;
45
- for (const f of readdirSync(aboutDir)) {
46
- if (f.endsWith(".md") || f.endsWith(".mdx")) return readFileSync(join(aboutDir, f), "utf-8");
41
+
42
+ // ── About ──────────────────────────────────────────────
43
+ if (subsetId === "about") {
44
+ const aboutDir = join(contentRoot, "about");
45
+ if (!existsSync(aboutDir)) return null;
46
+ for (const f of readdirSync(aboutDir)) {
47
+ if (f.endsWith(".md") || f.endsWith(".mdx"))
48
+ return readFileSync(join(aboutDir, f), "utf-8");
49
+ }
50
+ return null;
47
51
  }
48
- return null;
49
- }
50
-
51
- // ── Words ──────────────────────────────────────────────
52
- if (subsetId === "words") {
53
- const wordsDir = join(contentRoot, "words");
54
- if (!existsSync(wordsDir)) return null;
55
- const all: string[] = [];
56
- for (const f of readdirSync(wordsDir)) {
57
- if (f.endsWith(".md") && !f.startsWith("_")) {
58
- all.push(readFileSync(join(wordsDir, f), "utf-8"));
59
- }
52
+
53
+ // ── Words ──────────────────────────────────────────────
54
+ if (subsetId === "words") {
55
+ const wordsDir = join(contentRoot, "words");
56
+ if (!existsSync(wordsDir)) return null;
57
+ const all: string[] = [];
58
+ for (const f of readdirSync(wordsDir)) {
59
+ if (f.endsWith(".md") && !f.startsWith("_")) {
60
+ all.push(readFileSync(join(wordsDir, f), "utf-8"));
61
+ }
62
+ }
63
+ return all.join("\n") || null;
60
64
  }
61
- return all.join("\n") || null;
62
- }
63
-
64
- // ── Other HTML routes (home, archives, tags, categories, links, 404) ──
65
- // These pages use i18n text + config YAML text, which are already
66
- // covered by the common subset. Return config text for extra safety.
67
- if (["home", "archives", "tags", "categories", "links", "common"].includes(subsetId)) {
68
- const configDir = join(contentRoot, "config");
69
- if (!existsSync(configDir)) return null;
70
- const all: string[] = [];
71
- for (const f of readdirSync(configDir)) {
72
- if (f.endsWith(".yml") || f.endsWith(".yaml")) {
73
- all.push(readFileSync(join(configDir, f), "utf-8"));
74
- }
65
+
66
+ // ── Other HTML routes (home, archives, tags, categories, links, 404) ──
67
+ // These pages use i18n text + config YAML text, which are already
68
+ // covered by the common subset. Return config text for extra safety.
69
+ if (["home", "archives", "tags", "categories", "links", "common"].includes(subsetId)) {
70
+ const configDir = join(contentRoot, "config");
71
+ if (!existsSync(configDir)) return null;
72
+ const all: string[] = [];
73
+ for (const f of readdirSync(configDir)) {
74
+ if (f.endsWith(".yml") || f.endsWith(".yaml")) {
75
+ all.push(readFileSync(join(configDir, f), "utf-8"));
76
+ }
77
+ }
78
+ return all.join("\n") || null;
75
79
  }
76
- return all.join("\n") || null;
77
- }
78
80
 
79
- return null;
81
+ return null;
80
82
  }
81
83
 
82
84
  /**
@@ -84,62 +86,63 @@ function getRouteChars(projectRoot: string, subsetId: string): string | null {
84
86
  * Returns the CSS content to be served.
85
87
  */
86
88
  async function generateSubsetOnDemand(
87
- projectRoot: string,
88
- subsetId: string,
89
- logger: AstroIntegrationLogger,
89
+ projectRoot: string,
90
+ subsetId: string,
91
+ logger: AstroIntegrationLogger,
90
92
  ): Promise<string | null> {
91
- const chars = getRouteChars(projectRoot, subsetId);
92
- if (!chars) return null;
93
-
94
- // Extract unique characters
95
- const charSet = new Set<string>();
96
- for (const ch of chars) charSet.add(ch);
97
- if (charSet.size === 0) return null;
98
-
99
- // Deduplicate against common subset: read common.css to find its referenced WOFF2
100
- const outDir = resolve(projectRoot, FONT_OUT_DIR);
101
- const commonCssPath = join(outDir, "common.css");
102
- const commonCharSet = new Set<string>();
103
-
104
- // We can't precisely know which chars common covers without re-extracting.
105
- // But we can read the i18n + config source for a rough estimate.
106
- // For now, generate the full subset - the WOFF2 will be small either way.
107
- // (Most chars are already in common, so the delta is tiny.)
108
-
109
- // Locate font file
110
- const fontPaths = [
111
- resolve(projectRoot, FONT_INPUT),
112
- resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", FONT_INPUT),
113
- resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", FONT_INPUT),
114
- ];
115
- let fontPath: string | undefined;
116
- for (const p of fontPaths) {
117
- if (existsSync(p)) { fontPath = p; break; }
118
- }
119
- if (!fontPath) return null;
120
-
121
- const fontBuffer = readFileSync(fontPath);
122
- mkdirSync(outDir, { recursive: true });
123
-
124
- // Generate subset WOFF2
125
- const charsStr = [...charSet].sort().join("");
126
- const hash = [...charsStr].slice(0, 12).join("");
127
- const filename = `subset-${subsetId}-${hash}.woff2`;
128
- const outPath = join(outDir, filename);
129
-
130
- if (!existsSync(outPath)) {
131
- try {
132
- const data = await subsetFontFn(fontBuffer, charsStr, { targetFormat: "woff2" });
133
- writeFileSync(outPath, data);
134
- logger.info(
135
- ` on-demand subset: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
136
- );
137
- } catch {
138
- return null;
93
+ const chars = getRouteChars(projectRoot, subsetId);
94
+ if (!chars) return null;
95
+
96
+ // Extract unique characters
97
+ const charSet = new Set<string>();
98
+ for (const ch of chars) charSet.add(ch);
99
+ if (charSet.size === 0) return null;
100
+
101
+ // Deduplicate against common subset: read common.css to find its referenced WOFF2
102
+ const outDir = resolve(projectRoot, FONT_OUT_DIR);
103
+ const commonCssPath = join(outDir, "common.css");
104
+ const commonCharSet = new Set<string>();
105
+
106
+ // We can't precisely know which chars common covers without re-extracting.
107
+ // But we can read the i18n + config source for a rough estimate.
108
+ // For now, generate the full subset - the WOFF2 will be small either way.
109
+ // (Most chars are already in common, so the delta is tiny.)
110
+
111
+ // Locate font file
112
+ const fontPaths = [
113
+ resolve(projectRoot, FONT_INPUT),
114
+ resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", FONT_INPUT),
115
+ resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", FONT_INPUT),
116
+ ];
117
+ let fontPath: string | undefined;
118
+ for (const p of fontPaths) {
119
+ if (existsSync(p)) {
120
+ fontPath = p;
121
+ break;
122
+ }
123
+ }
124
+ if (!fontPath) return null;
125
+
126
+ const fontBuffer = readFileSync(fontPath);
127
+ mkdirSync(outDir, { recursive: true });
128
+
129
+ // Generate subset WOFF2
130
+ const charsStr = [...charSet].sort().join("");
131
+ const hash = [...charsStr].slice(0, 12).join("");
132
+ const filename = `subset-${subsetId}-${hash}.woff2`;
133
+ const outPath = join(outDir, filename);
134
+
135
+ if (!existsSync(outPath)) {
136
+ try {
137
+ const data = await subsetFontFn(fontBuffer, charsStr, { targetFormat: "woff2" });
138
+ writeFileSync(outPath, data);
139
+ logger.info(` on-demand subset: ${(data.length / 1024).toFixed(1)} KB → ${filename}`);
140
+ } catch {
141
+ return null;
142
+ }
139
143
  }
140
- }
141
144
 
142
- return `@font-face {
145
+ return `@font-face {
143
146
  font-family: "LXGW WenKai Subset";
144
147
  src: url("/fonts/${filename}") format("woff2");
145
148
  font-display: swap;
@@ -150,32 +153,33 @@ async function generateSubsetOnDemand(
150
153
  * Create Vite middleware for on-demand font subsetting in dev mode.
151
154
  */
152
155
  export function createDevFontMiddleware(projectRoot: string, logger: AstroIntegrationLogger) {
153
- return async (req: any, res: any, next: any) => {
154
- const url = req.url || "";
155
- const match = url.match(/^\/fonts\/subset-(.+)\.css$/);
156
- if (!match) return next();
157
-
158
- const subsetId = match[1];
159
-
160
- // Check if file already exists (cached from previous on-demand generation)
161
- const cssPath = resolve(projectRoot, FONT_OUT_DIR, `subset-${subsetId}.css`);
162
- if (existsSync(cssPath)) {
163
- res.statusCode = 200;
164
- res.setHeader("Content-Type", "text/css");
165
- res.end(readFileSync(cssPath, "utf-8"));
166
- return;
167
- }
168
-
169
- // Generate on-demand
170
- logger.info(`Font subset: on-demand generating for "${subsetId}"...`);
171
- const css = await generateSubsetOnDemand(projectRoot, subsetId, logger);
172
- if (css) {
173
- writeFileSync(cssPath, css); // cache it
174
- res.statusCode = 200;
175
- res.setHeader("Content-Type", "text/css");
176
- res.end(css);
177
- } else {
178
- next();
179
- }
180
- };
156
+ return async (req: any, res: any, next: any) => {
157
+ const url = req.url || "";
158
+ const pathname = url.split("?")[0].split("#")[0];
159
+ const match = pathname.match(/^\/fonts\/subset-(.+)\.css$/);
160
+ if (!match) return next();
161
+
162
+ const subsetId = match[1];
163
+
164
+ // Check if file already exists (cached from previous on-demand generation)
165
+ const cssPath = resolve(projectRoot, FONT_OUT_DIR, `subset-${subsetId}.css`);
166
+ if (existsSync(cssPath)) {
167
+ res.statusCode = 200;
168
+ res.setHeader("Content-Type", "text/css");
169
+ res.end(readFileSync(cssPath, "utf-8"));
170
+ return;
171
+ }
172
+
173
+ // Generate on-demand
174
+ logger.info(`Font subset: on-demand generating for "${subsetId}"...`);
175
+ const css = await generateSubsetOnDemand(projectRoot, subsetId, logger);
176
+ if (css) {
177
+ writeFileSync(cssPath, css); // cache it
178
+ res.statusCode = 200;
179
+ res.setHeader("Content-Type", "text/css");
180
+ res.end(css);
181
+ } else {
182
+ next();
183
+ }
184
+ };
181
185
  }
@@ -263,6 +263,16 @@ export async function runFontSubsetting(
263
263
  }
264
264
  const fontBuffer = readFileSync(fontPath);
265
265
 
266
+ // Clean old generated font files to avoid stale cached references
267
+ const outDir = resolve(projectRoot, FONT_OUT_DIR);
268
+ if (existsSync(outDir)) {
269
+ for (const f of readdirSync(outDir)) {
270
+ if (f.startsWith("common-") || f === "common.css" || f.startsWith("subset-")) {
271
+ try { writeFileSync(join(outDir, f), ""); } catch { /* skip */ }
272
+ }
273
+ }
274
+ }
275
+
266
276
  // 2. Scan content files
267
277
  const contentFiles = scanContent(projectRoot);
268
278
  logger.info(`Font subsetting: found ${contentFiles.length} content files`);
@@ -289,7 +299,6 @@ export async function runFontSubsetting(
289
299
  }
290
300
 
291
301
  // 5. Deduplicate and generate subsets
292
- const outDir = resolve(projectRoot, FONT_OUT_DIR);
293
302
  mkdirSync(outDir, { recursive: true });
294
303
 
295
304
  // Dynamic import subset-font (ESM only)