@xingwangzhe/stalux 1.51.1 → 1.52.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.
Files changed (42) hide show
  1. package/.release/cli.mjs +16 -20
  2. package/package.json +48 -45
  3. package/src/components/stalux/footer/FooterBadges.astro +2 -2
  4. package/src/components/stalux/layout/head.astro +1 -1
  5. package/src/components/stalux/posts/postContent.astro +1 -1
  6. package/src/index.ts +57 -51
  7. package/src/internal/cjk-search-index.ts +4 -7
  8. package/src/internal/components-plugin.ts +1 -3
  9. package/src/internal/font-slices.ts +36 -29
  10. package/src/internal/html-images.ts +1 -1
  11. package/src/internal/html-styles.ts +1 -1
  12. package/src/internal/image-dimensions.ts +3 -8
  13. package/src/internal/injected-routes.ts +1 -3
  14. package/src/internal/page-output-cache.ts +11 -15
  15. package/src/internal/post-cover.ts +22 -26
  16. package/src/internal/project-mode.ts +3 -2
  17. package/src/internal/public-assets.ts +42 -23
  18. package/src/internal/runtime-cache-key.ts +42 -27
  19. package/src/internal/satteri-config.ts +2 -2
  20. package/src/internal/search-style.ts +6 -7
  21. package/src/internal/social-card-cover.ts +12 -10
  22. package/src/internal/social-card-renderer.ts +8 -11
  23. package/src/internal/webmcp-mutations.ts +11 -5
  24. package/src/layouts/Stalux.astro +4 -4
  25. package/src/pages/openapi.json.ts +1 -1
  26. package/src/plugins/feature-flags.ts +2 -2
  27. package/src/schemas/collections.ts +1 -1
  28. package/src/schemas/config.ts +4 -1
  29. package/src/scripts/list-reveal.ts +7 -5
  30. package/src/scripts/swup-router.ts +1 -1
  31. package/src/scripts/weather.ts +2 -2
  32. package/src/scripts/webmcp.ts +4 -4
  33. package/src/utils/accent-color.ts +1 -1
  34. package/src/utils/ai-discovery.ts +6 -6
  35. package/src/utils/badge-generator.ts +1 -2
  36. package/src/utils/cc.ts +2 -2
  37. package/src/utils/dayjs.ts +5 -5
  38. package/src/utils/diagnostics.ts +3 -3
  39. package/src/utils/public-routes.ts +3 -3
  40. package/src/utils/seo-document.ts +2 -2
  41. package/src/utils/site-path.ts +1 -1
  42. package/src/utils/social-image.ts +1 -3
@@ -17,9 +17,7 @@ import { styleNames } from "./style-names.generated.ts";
17
17
  * shared-header/footer-*.woff2 — persistent shell glyph subsets
18
18
  */
19
19
 
20
- import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
21
20
  import { dirname, join, resolve, sep } from "node:path";
22
- import { fileURLToPath, pathToFileURL } from "node:url";
23
21
  import { FontSubsetter } from "@xingwangzhe/cjk-font-split-native";
24
22
  import type { AstroIntegrationLogger } from "astro";
25
23
  import {
@@ -75,13 +73,14 @@ export interface FontInputs {
75
73
  // ---------------------------------------------------------------------------
76
74
 
77
75
  /** Locate a font file: project root → stalux package → this package. */
78
- function findFont(projectRoot: string, rel: string): string | undefined {
76
+ async function findFont(projectRoot: string, rel: string): Promise<string | undefined> {
79
77
  const candidates = [
80
78
  resolve(projectRoot, rel),
81
79
  resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", rel),
82
- resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", rel),
80
+ resolve(dirname(Bun.fileURLToPath(import.meta.url)), "..", "..", rel),
83
81
  ];
84
- return candidates.find((p) => existsSync(p));
82
+ for (const candidate of candidates) if (await Bun.file(candidate).exists()) return candidate;
83
+ return undefined;
85
84
  }
86
85
 
87
86
  // ---------------------------------------------------------------------------
@@ -102,19 +101,19 @@ export async function resolveFontInputs(
102
101
  const started = performance.now();
103
102
  logger.debug("locating font inputs");
104
103
  // 1. Locate fonts (check project root first, then stalux package dir)
105
- const fontPath = findFont(projectRoot, FONT_INPUT);
104
+ const fontPath = await findFont(projectRoot, FONT_INPUT);
106
105
  if (!fontPath) {
107
106
  logger.warn(`Body font not found at ${FONT_INPUT}, skipping font injection`);
108
107
  return null;
109
108
  }
110
- const codeNormal = findFont(projectRoot, CODE_FONT_INPUT);
109
+ const codeNormal = await findFont(projectRoot, CODE_FONT_INPUT);
111
110
  if (!codeNormal) {
112
111
  logger.warn(`Code font not found at ${CODE_FONT_INPUT}, skipping font injection`);
113
112
  return null;
114
113
  }
115
- const codeItalic = findFont(projectRoot, CODE_FONT_ITALIC_INPUT);
114
+ const codeItalic = await findFont(projectRoot, CODE_FONT_ITALIC_INPUT);
116
115
 
117
- const fontBuffer = readFileSync(fontPath);
116
+ const fontBuffer = Buffer.from(await Bun.file(fontPath).bytes());
118
117
  logger.debug(`font inputs located in ${(performance.now() - started).toFixed(1)}ms`);
119
118
  logger.info(
120
119
  `Native page-font source ready (LXGW ${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB)`,
@@ -168,9 +167,9 @@ const VISUALLY_HIDDEN_CLASSES = new Set([
168
167
  ]);
169
168
 
170
169
  function cssGeneratedText(css: string): string {
171
- return [...css.matchAll(/\bcontent\s*:\s*(["'])(.*?)\1\s*(?:!important\s*)?(?:;|})/gis)]
170
+ return [...css.matchAll(/\bcontent\s*:\s*(["'])(.*?)\1\s*(?:!important\s*)?(?:;|\})/gisu)]
172
171
  .map(([, , value]) =>
173
- (value ?? "").replace(/\\([\da-f]{1,6})\s?|\\(.)/gi, (_, hex, char) =>
172
+ (value ?? "").replace(/\\([\da-f]{1,6})\s?|\\(.)/giu, (_, hex, char) =>
174
173
  hex ? String.fromCodePoint(Number.parseInt(hex, 16)) : char,
175
174
  ),
176
175
  )
@@ -191,14 +190,14 @@ function collectStylesheetLinks(node: HtmlNode, output: string[]): void {
191
190
  }
192
191
 
193
192
  /** Read local CSS files linked from an emitted page and return generated text strings. */
194
- export function readLinkedStylesheetText(
193
+ export async function readLinkedStylesheetText(
195
194
  html: string,
196
195
  pagePath: string,
197
196
  outputDir: string,
198
197
  cache = new Map<string, string>(),
199
198
  document = parse(html),
200
199
  dependencies?: Set<string>,
201
- ): string {
200
+ ): Promise<string> {
202
201
  const root = resolve(outputDir);
203
202
  const links: string[] = [];
204
203
  collectStylesheetLinks(document, links);
@@ -209,7 +208,7 @@ export function readLinkedStylesheetText(
209
208
  try {
210
209
  cssPath = href.startsWith("/")
211
210
  ? resolve(root, `.${new URL(href, "https://stalux.local").pathname}`)
212
- : fileURLToPath(new URL(href, pathToFileURL(pagePath)));
211
+ : Bun.fileURLToPath(new URL(href, Bun.pathToFileURL(pagePath)));
213
212
  } catch {
214
213
  continue;
215
214
  }
@@ -217,7 +216,7 @@ export function readLinkedStylesheetText(
217
216
  dependencies?.add(cssPath);
218
217
  if (!cache.has(cssPath)) {
219
218
  try {
220
- cache.set(cssPath, cssGeneratedText(readFileSync(cssPath, "utf8")));
219
+ cache.set(cssPath, cssGeneratedText(await Bun.file(cssPath).text()));
221
220
  } catch {
222
221
  cache.set(cssPath, "");
223
222
  }
@@ -476,11 +475,11 @@ function renderPageFont(
476
475
  for (const node of style.childNodes) {
477
476
  if (node.nodeName !== "#text" || !("value" in node)) continue;
478
477
  node.value = node.value.replace(
479
- /@font-face\{[^}]*font-family:"LXGW WenKai-[^"]+"[^}]*\}/g,
478
+ /@font-face\{[^}]*font-family:"LXGW WenKai-[^"]+"[^}]*\}/gu,
480
479
  (face) => {
481
- const unicodeRange = face.match(/unicode-range:([^;}]+)/i)?.[1] ?? "";
480
+ const unicodeRange = face.match(/unicode-range:([^;}]+)/iu)?.[1] ?? "";
482
481
  const hasCjkRange = unicodeRange.split(",").some((range) => {
483
- const start = range.match(/U\+([\da-f]+)/i)?.[1];
482
+ const start = range.match(/U\+([\da-f]+)/iu)?.[1];
484
483
  if (!start) return false;
485
484
  const point = Number.parseInt(start, 16);
486
485
  return point >= 0x4e00 && point <= 0x9fff;
@@ -595,18 +594,26 @@ export async function writePageFontSubsetIntoDocumentAsync(
595
594
  }
596
595
 
597
596
  /** Remove stale page subsets so deploy artifacts only contain the current build's referenced files. */
598
- export function clearPageFontSubsets(
597
+ export async function clearPageFontSubsets(
599
598
  outputDir: string,
600
599
  referencedFiles: Set<string> = new Set(),
601
- ): void {
602
- if (!existsSync(outputDir)) return;
603
- for (const name of readdirSync(outputDir)) {
604
- const isSubset =
605
- name.startsWith("page-") ||
606
- name.startsWith("shared-header-") ||
607
- name.startsWith("shared-footer-");
608
- if (isSubset && name.endsWith(".woff2") && !referencedFiles.has(name)) {
609
- unlinkSync(join(outputDir, name));
610
- }
600
+ ): Promise<void> {
601
+ let names: string[];
602
+ try {
603
+ names = [...new Bun.Glob("**/*.woff2").scanSync({ cwd: outputDir, onlyFiles: true })];
604
+ } catch {
605
+ return;
611
606
  }
607
+ await Promise.all(
608
+ names
609
+ .filter(
610
+ (name) =>
611
+ (name.startsWith("page-") ||
612
+ name.startsWith("shared-header-") ||
613
+ name.startsWith("shared-footer-")) &&
614
+ name.endsWith(".woff2") &&
615
+ !referencedFiles.has(name),
616
+ )
617
+ .map((name) => Bun.file(join(outputDir, name)).delete()),
618
+ );
612
619
  }
@@ -81,7 +81,7 @@ export function updateImageTag(image: HtmlImage, updates: Record<string, string>
81
81
  tag = tag.slice(0, edit.start) + edit.value + tag.slice(edit.end);
82
82
  if (additions.length)
83
83
  tag = tag.replace(
84
- /\s*\/?\s*>$/,
84
+ /\s*\/?\s*>$/u,
85
85
  (ending) => ` ${additions.join(" ")}${ending.trimStart()}`,
86
86
  );
87
87
  return tag;
@@ -11,7 +11,7 @@ export function promoteCodeStyles(document: DefaultTreeAdapterMap["document"]):
11
11
  if (node.tagName === "template" || node.tagName === "noscript") return;
12
12
  if (node.tagName === "head") head = node;
13
13
  inCode ||= (
14
- node.attrs.find((a) => a.name === "class")?.value.split(/\s+/) ?? []
14
+ node.attrs.find((a) => a.name === "class")?.value.split(/\s+/u) ?? []
15
15
  ).includes("expressive-code");
16
16
  if (
17
17
  inCode &&
@@ -1,4 +1,3 @@
1
- import { open } from "node:fs/promises";
2
1
  import path from "node:path";
3
2
  import { imageSize } from "image-size";
4
3
  import { imageSizeFromFile } from "image-size/fromFile";
@@ -56,17 +55,13 @@ async function readLocalImageDimensions(filename: string): Promise<ImageDimensio
56
55
  } catch {
57
56
  // Preserve previously supported JPEGs whose SOF is first or beyond the header window.
58
57
  // Only this compatibility path reads a whole image, never an unknown or remote format.
59
- const file = await open(filename, "r").catch(() => undefined);
60
- if (!file) return undefined;
61
58
  try {
62
- const signature = Buffer.alloc(2);
63
- await file.read(signature, 0, 2, 0);
59
+ const file = Bun.file(filename);
60
+ const signature = new Uint8Array(await file.slice(0, 2).arrayBuffer());
64
61
  if (signature[0] !== 0xff || signature[1] !== 0xd8) return undefined;
65
- return readImageDimensions(await file.readFile());
62
+ return readImageDimensions(Buffer.from(await file.bytes()));
66
63
  } catch {
67
64
  return undefined;
68
- } finally {
69
- await file.close();
70
65
  }
71
66
  }
72
67
  }
@@ -1,5 +1,3 @@
1
- import { fileURLToPath } from "node:url";
2
-
3
1
  import { ROUTE_DEFINITIONS } from "../utils/route-catalog";
4
2
 
5
3
  export interface InjectedRoute {
@@ -10,6 +8,6 @@ export interface InjectedRoute {
10
8
  export function createInjectedRoutes(baseDir: URL): InjectedRoute[] {
11
9
  return ROUTE_DEFINITIONS.map(({ pattern, source }) => ({
12
10
  pattern,
13
- entrypoint: fileURLToPath(new URL(source, baseDir)),
11
+ entrypoint: Bun.fileURLToPath(new URL(source, baseDir)),
14
12
  }));
15
13
  }
@@ -1,5 +1,3 @@
1
- import { createHash } from "node:crypto";
2
- import { createReadStream, promises as fs } from "node:fs";
3
1
  import path from "node:path";
4
2
 
5
3
  export interface CachedPageOutput {
@@ -7,7 +5,8 @@ export interface CachedPageOutput {
7
5
  font?: { filename: string; sourcePath: string };
8
6
  }
9
7
 
10
- const digest = (value: string | Buffer) => createHash("sha256").update(value).digest("hex");
8
+ const digest = (value: string | Uint8Array) =>
9
+ new Bun.CryptoHasher("sha256").update(value).digest("hex");
11
10
 
12
11
  /** Content hashes include dependencies even when their public URL has not changed. */
13
12
  export async function createPageOutputCache(
@@ -17,15 +16,14 @@ export async function createPageOutputCache(
17
16
  assets: readonly string[] = [],
18
17
  contentKey = "",
19
18
  ) {
20
- const hash = createHash("sha256").update("stalux-page-output-v2\0");
19
+ const hash = new Bun.CryptoHasher("sha256").update("stalux-page-output-v2\0");
21
20
  if (font) hash.update(font);
22
21
  if (contentKey) hash.update("\0content-key\0").update(contentKey);
23
22
  for (const dependency of [...dependencies].sort()) {
24
23
  hash.update(dependency).update("\0");
25
- hash.update(await fs.readFile(dependency)).update("\0");
24
+ hash.update(await Bun.file(dependency).bytes()).update("\0");
26
25
  }
27
26
  const namespace = path.join(directory, hash.digest("hex"));
28
- await fs.mkdir(namespace, { recursive: true });
29
27
  const knownAssets = new Set(assets);
30
28
  const assetDigests = new Map<string, Promise<string | null>>();
31
29
  const assetDigest = (asset: string): Promise<string | null> => {
@@ -34,8 +32,8 @@ export async function createPageOutputCache(
34
32
  if (!pending) {
35
33
  pending = (async () => {
36
34
  try {
37
- const hash = createHash("sha256");
38
- for await (const chunk of createReadStream(asset)) hash.update(chunk);
35
+ const hash = new Bun.CryptoHasher("sha256");
36
+ for await (const chunk of Bun.file(asset).stream()) hash.update(chunk);
39
37
  return hash.digest("hex");
40
38
  } catch {
41
39
  return null;
@@ -46,12 +44,12 @@ export async function createPageOutputCache(
46
44
  return pending;
47
45
  };
48
46
  const key = (entry: string, html: string) =>
49
- createHash("sha256").update(entry).update("\0").update(html).digest("hex");
47
+ new Bun.CryptoHasher("sha256").update(entry).update("\0").update(html).digest("hex");
50
48
  return {
51
49
  async get(entry: string, html: string): Promise<CachedPageOutput | undefined> {
52
50
  try {
53
51
  const record = JSON.parse(
54
- await fs.readFile(path.join(namespace, `${key(entry, html)}.json`), "utf8"),
52
+ await Bun.file(path.join(namespace, `${key(entry, html)}.json`)).text(),
55
53
  );
56
54
  if (typeof record.html !== "string" || digest(record.html) !== record.digest)
57
55
  return undefined;
@@ -63,7 +61,7 @@ export async function createPageOutputCache(
63
61
  record.font &&
64
62
  (!/^page-[a-f\d]+\.woff2$/u.test(record.font.filename) ||
65
63
  typeof record.font.sourcePath !== "string" ||
66
- !(await fs.stat(record.font.sourcePath)).isFile())
64
+ !(await Bun.file(record.font.sourcePath).exists()))
67
65
  )
68
66
  return undefined;
69
67
  return { html: record.html, font: record.font };
@@ -79,9 +77,8 @@ export async function createPageOutputCache(
79
77
  referencedAssets: readonly string[] = [],
80
78
  ): Promise<void> {
81
79
  const target = path.join(namespace, `${key(entry, html)}.json`);
82
- const temporary = `${target}.${process.pid}.tmp`;
83
- await fs.writeFile(
84
- temporary,
80
+ await Bun.write(
81
+ target,
85
82
  JSON.stringify({
86
83
  ...output,
87
84
  digest: digest(output.html),
@@ -90,7 +87,6 @@ export async function createPageOutputCache(
90
87
  ),
91
88
  }),
92
89
  );
93
- await fs.rename(temporary, target);
94
90
  },
95
91
  };
96
92
  }
@@ -1,12 +1,3 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- copyFileSync,
4
- existsSync,
5
- mkdirSync,
6
- readdirSync,
7
- readFileSync,
8
- writeFileSync,
9
- } from "node:fs";
10
1
  import path from "node:path";
11
2
  import type { HtmlImage } from "./html-images";
12
3
 
@@ -60,8 +51,8 @@ export interface CoverContext {
60
51
  logger?: { warn: (message: string) => void };
61
52
  }
62
53
 
63
- const digest = (parts: readonly (string | Buffer)[]): string => {
64
- const hash = createHash("sha256");
54
+ const digest = (parts: readonly (string | Buffer | Uint8Array)[]): string => {
55
+ const hash = new Bun.CryptoHasher("sha256");
65
56
  for (const part of parts) hash.update(part).update("\0");
66
57
  return hash.digest("hex");
67
58
  };
@@ -96,27 +87,26 @@ export async function upgradeCoverImages(
96
87
  if (!CONVERTIBLE.test(pathname)) continue;
97
88
  const source = path.resolve(root, `.${pathname}`);
98
89
  if (!source.startsWith(`${root}${path.sep}`)) continue;
99
- if (!existsSync(source)) continue;
90
+ if (!(await Bun.file(source).exists())) continue;
100
91
  context.dependencies?.add(source);
101
92
 
102
93
  let bytes: Buffer;
103
94
  try {
104
- bytes = readFileSync(source);
95
+ bytes = Buffer.from(await Bun.file(source).bytes());
105
96
  } catch {
106
97
  continue;
107
98
  }
108
99
  const hash = digest([RECIPE, "webp", String(MAX_WIDTH), String(QUALITY), bytes]);
109
100
  const filename = `${hash.slice(0, 16)}.webp`;
110
101
  const cached = path.join(cacheDir, filename);
111
- if (!existsSync(cached)) {
102
+ if (!(await Bun.file(cached).exists())) {
112
103
  try {
113
104
  const encoded = await context
114
105
  .sharp(bytes)
115
106
  .resize({ width: MAX_WIDTH, withoutEnlargement: true })
116
107
  .webp({ quality: QUALITY, effort: 5 })
117
108
  .toBuffer();
118
- mkdirSync(cacheDir, { recursive: true });
119
- writeFileSync(cached, encoded);
109
+ await Bun.write(cached, encoded);
120
110
  } catch (error) {
121
111
  // One malformed source must not fail the page it appears on.
122
112
  context.logger?.warn(`封面转 WebP 失败(${pathname}):${String(error)}`);
@@ -132,16 +122,22 @@ export async function upgradeCoverImages(
132
122
  }
133
123
 
134
124
  /** Copy every cached cover into the build output so cached pages still resolve. */
135
- export function restoreCoverFiles(context: { outDir: string; cacheDir: string }): number {
125
+ export async function restoreCoverFiles(context: {
126
+ outDir: string;
127
+ cacheDir: string;
128
+ }): Promise<number> {
136
129
  const cacheDir = path.join(context.cacheDir, RECIPE);
137
- if (!existsSync(cacheDir)) return 0;
138
- const target = path.join(context.outDir, COVER_DIR);
139
- mkdirSync(target, { recursive: true });
140
- let copied = 0;
141
- for (const name of readdirSync(cacheDir)) {
142
- if (!name.endsWith(".webp")) continue;
143
- copyFileSync(path.join(cacheDir, name), path.join(target, name));
144
- copied++;
130
+ let names: string[];
131
+ try {
132
+ names = [...new Bun.Glob("*.webp").scanSync({ cwd: cacheDir, onlyFiles: true })];
133
+ } catch {
134
+ return 0;
145
135
  }
146
- return copied;
136
+ const target = path.join(context.outDir, COVER_DIR);
137
+ await Promise.all(
138
+ names.map((name) =>
139
+ Bun.write(path.join(target, name), Bun.file(path.join(cacheDir, name))),
140
+ ),
141
+ );
142
+ return names.length;
147
143
  }
@@ -1,7 +1,8 @@
1
1
  import { realpathSync } from "node:fs";
2
- import { fileURLToPath } from "node:url";
3
2
 
4
3
  /** Linked consumers still need injected routes; only the theme's own root has file routes. */
5
4
  export function isPluginProject(projectRoot: URL, themeRoot: URL): boolean {
6
- return realpathSync(fileURLToPath(projectRoot)) !== realpathSync(fileURLToPath(themeRoot));
5
+ return (
6
+ realpathSync(Bun.fileURLToPath(projectRoot)) !== realpathSync(Bun.fileURLToPath(themeRoot))
7
+ );
7
8
  }
@@ -1,6 +1,33 @@
1
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
1
  import path from "node:path";
3
2
 
3
+ /** Bun.Glob reports a missing directory as an empty scan instead of throwing here. */
4
+ function listFiles(directory: string, patterns: readonly string[]): string[] {
5
+ const names = new Set<string>();
6
+ for (const pattern of patterns) {
7
+ try {
8
+ for (const name of new Bun.Glob(pattern).scanSync({
9
+ cwd: directory,
10
+ onlyFiles: true,
11
+ }))
12
+ names.add(name);
13
+ } catch {
14
+ // Missing source directory: nothing to sync.
15
+ }
16
+ }
17
+ return [...names].sort();
18
+ }
19
+
20
+ async function sameContents(left: string, right: string): Promise<boolean> {
21
+ const [leftBytes, rightBytes] = await Promise.all([
22
+ Bun.file(left).bytes(),
23
+ Bun.file(right).bytes(),
24
+ ]);
25
+ if (leftBytes.length !== rightBytes.length) return false;
26
+ for (let index = 0; index < leftBytes.length; index++)
27
+ if (leftBytes[index] !== rightBytes[index]) return false;
28
+ return true;
29
+ }
30
+
4
31
  // ---------------------------------------------------------------------------
5
32
  // 背景资源判别式同步
6
33
  // ---------------------------------------------------------------------------
@@ -13,47 +40,39 @@ import path from "node:path";
13
40
  * - 用户新增的文件:保留,不删除(避免误删用户自定义背景)
14
41
  * - 不做整目录 force 复制
15
42
  */
16
- export function syncBackgroundSvgs(
43
+ export async function syncBackgroundSvgs(
17
44
  srcDir: string,
18
45
  publicDir: string,
19
- ): { copied: number; skipped: number } {
46
+ ): Promise<{ copied: number; skipped: number }> {
20
47
  const srcBg = path.join(srcDir, "../public/background");
21
- if (!existsSync(srcBg)) return { copied: 0, skipped: 0 };
48
+ const names = listFiles(srcBg, ["*.svg", "*.webp"]);
49
+ if (names.length === 0) return { copied: 0, skipped: 0 };
22
50
 
23
51
  const destBg = path.join(publicDir, "background");
24
- mkdirSync(destBg, { recursive: true });
25
-
26
52
  let copied = 0;
27
53
  let skipped = 0;
28
- for (const name of readdirSync(srcBg)) {
29
- if (!/\.(?:svg|webp)$/u.test(name)) continue;
54
+ for (const name of names) {
30
55
  const srcFile = path.join(srcBg, name);
31
56
  const destFile = path.join(destBg, name);
32
- if (existsSync(destFile)) {
33
- const srcBuf = readFileSync(srcFile);
34
- const destBuf = readFileSync(destFile);
35
- if (srcBuf.equals(destBuf)) {
36
- skipped++; // 内容一致,跳过
37
- continue;
38
- }
57
+ if ((await Bun.file(destFile).exists()) && (await sameContents(srcFile, destFile))) {
58
+ skipped++; // 内容一致,跳过
59
+ continue;
39
60
  }
40
- copyFileSync(srcFile, destFile);
61
+ await Bun.write(destFile, Bun.file(srcFile));
41
62
  copied++;
42
63
  }
43
64
  return { copied, skipped };
44
65
  }
45
66
 
46
67
  /** Sync packaged math fonts and licensing notices; Astro does not merge integration public directories. */
47
- export function syncFontAssets(srcDir: string, publicDir: string): void {
68
+ export async function syncFontAssets(srcDir: string, publicDir: string): Promise<void> {
48
69
  const source = path.join(srcDir, "../public/fonts");
49
- if (!existsSync(source)) return;
70
+ const names = listFiles(source, ["*.woff2", "*.txt"]);
50
71
  const destination = path.join(publicDir, "fonts");
51
- mkdirSync(destination, { recursive: true });
52
- for (const name of readdirSync(source)) {
53
- if (!/\.(?:woff2|txt)$/u.test(name)) continue;
72
+ for (const name of names) {
54
73
  const from = path.join(source, name);
55
74
  const to = path.join(destination, name);
56
- if (existsSync(to) && readFileSync(from).equals(readFileSync(to))) continue;
57
- copyFileSync(from, to);
75
+ if ((await Bun.file(to).exists()) && (await sameContents(from, to))) continue;
76
+ await Bun.write(to, Bun.file(from));
58
77
  }
59
78
  }
@@ -1,11 +1,9 @@
1
- import { createHash } from "node:crypto";
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
3
1
  import path from "node:path";
4
2
 
5
3
  declare const __STALUX_RUNTIME_CACHE_KEY__: string;
6
4
 
7
5
  export function hashRuntimeSources(sources: ReadonlyArray<readonly [string, string]>): string {
8
- const hash = createHash("sha256");
6
+ const hash = new Bun.CryptoHasher("sha256");
9
7
  for (const [name, contents] of [...sources].sort(([left], [right]) =>
10
8
  left.localeCompare(right),
11
9
  )) {
@@ -17,43 +15,60 @@ export function hashRuntimeSources(sources: ReadonlyArray<readonly [string, stri
17
15
  return hash.digest("hex").slice(0, 16);
18
16
  }
19
17
 
20
- function collectFiles(
18
+ /** Bun.Glob keeps directory walks in the runtime instead of node:fs. */
19
+ async function collectFiles(
21
20
  directory: string,
22
21
  root = directory,
23
22
  include = (_name: string) => true,
24
- ): Array<readonly [string, string]> {
25
- return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
26
- const target = path.join(directory, entry.name);
27
- if (entry.isDirectory()) return collectFiles(target, root, include);
28
- if (!include(entry.name)) return [];
29
- return [[path.relative(root, target), readFileSync(target, "utf8")] as const];
30
- });
23
+ ): Promise<Array<readonly [string, string]>> {
24
+ let names: string[];
25
+ try {
26
+ names = [...new Bun.Glob("**/*").scanSync({ cwd: directory, onlyFiles: true })];
27
+ } catch {
28
+ return [];
29
+ }
30
+ return Promise.all(
31
+ names
32
+ .filter((name) => include(path.basename(name)))
33
+ .map(async (name): Promise<readonly [string, string]> => {
34
+ const target = path.join(directory, name);
35
+ return [
36
+ path.relative(root, target).replaceAll(path.sep, "/"),
37
+ await Bun.file(target).text(),
38
+ ];
39
+ }),
40
+ );
31
41
  }
32
42
 
33
- export function createRuntimeCacheKey(
43
+ export async function createRuntimeCacheKey(
34
44
  scriptsDirectory: string,
35
45
  packageJson: string,
36
46
  stylesDirectory?: string,
37
47
  styleMapping?: string,
38
48
  configDirectory?: string,
39
- ): string {
40
- return hashRuntimeSources([
41
- ...collectFiles(scriptsDirectory),
42
- ...(configDirectory && existsSync(configDirectory)
43
- ? collectFiles(configDirectory, configDirectory, (name) => /\.ya?ml$/iu.test(name)).map(
44
- ([name, contents]) => [`config/${name}`, contents] as const,
45
- )
46
- : []),
47
- ...(stylesDirectory
48
- ? readdirSync(stylesDirectory)
49
- .filter((name) => /^generated(?:-[a-z]+)?\.css$/u.test(name))
50
- .map((name): readonly [string, string] => [
49
+ ): Promise<string> {
50
+ const styleNames = stylesDirectory
51
+ ? [...new Bun.Glob("generated*.css").scanSync({ cwd: stylesDirectory })]
52
+ .filter((name) => /^generated(?:-[a-z]+)?\.css$/u.test(name))
53
+ .map(
54
+ async (name): Promise<readonly [string, string]> => [
51
55
  `styles/${name}`,
52
- readFileSync(path.join(stylesDirectory, name), "utf8"),
53
- ])
56
+ await Bun.file(path.join(stylesDirectory, name)).text(),
57
+ ],
58
+ )
59
+ : [];
60
+ return hashRuntimeSources([
61
+ ...(await collectFiles(scriptsDirectory)),
62
+ ...(configDirectory
63
+ ? (
64
+ await collectFiles(configDirectory, configDirectory, (name) =>
65
+ /\.ya?ml$/iu.test(name),
66
+ )
67
+ ).map(([name, contents]) => [`config/${name}`, contents] as const)
54
68
  : []),
69
+ ...(await Promise.all(styleNames)),
55
70
  ...(styleMapping ? [["style-names", styleMapping] as const] : []),
56
- ["package.json", readFileSync(packageJson, "utf8")],
71
+ ["package.json", await Bun.file(packageJson).text()],
57
72
  ]);
58
73
  }
59
74
 
@@ -23,13 +23,13 @@ function pluginName(plugin: unknown): string | undefined {
23
23
  return undefined;
24
24
  }
25
25
 
26
- export function appendUniquePlugin<T>(list: T[], plugin: T, seen: Set<string>): void {
26
+ export function appendUniquePlugin<T>(list: T[], plugin: unknown, seen: Set<string>): void {
27
27
  const name = pluginName(plugin);
28
28
  if (name) {
29
29
  if (seen.has(name)) return;
30
30
  seen.add(name);
31
31
  }
32
- list.push(plugin);
32
+ list.push(plugin as T);
33
33
  }
34
34
 
35
35
  export function prepareSatteriProcessor(processor: unknown): SatteriProcessorOptions | undefined {
@@ -1,5 +1,3 @@
1
- import { createRequire } from "node:module";
2
- import { fileURLToPath } from "node:url";
3
1
  import type { Plugin } from "vite";
4
2
 
5
3
  const publicId = "virtual:stalux-search-style";
@@ -7,9 +5,10 @@ const virtualId = `\0${publicId}`;
7
5
 
8
6
  /** Astro dev treats a direct CSS ?url import as a module script; keep it a URL-only leaf. */
9
7
  export function searchStylePlugin(development: boolean, base: string): Plugin {
10
- const stylesheet = createRequire(import.meta.url)
11
- .resolve("@pagefind/component-ui/css")
12
- .replaceAll("\\", "/");
8
+ const stylesheet = Bun.resolveSync(
9
+ "@pagefind/component-ui/css",
10
+ Bun.fileURLToPath(new URL(".", import.meta.url)),
11
+ ).replaceAll("\\", "/");
13
12
  let building = false;
14
13
  return {
15
14
  name: "stalux:search-style",
@@ -20,7 +19,7 @@ export function searchStylePlugin(development: boolean, base: string): Plugin {
20
19
  if (building && this.environment.name === "client")
21
20
  this.emitFile({
22
21
  type: "chunk",
23
- id: fileURLToPath(
22
+ id: Bun.fileURLToPath(
24
23
  new URL("../scripts/search-engine-entry.ts", import.meta.url),
25
24
  ),
26
25
  fileName: "pagefind-enhanced/pagefind.js",
@@ -39,7 +38,7 @@ export function searchStylePlugin(development: boolean, base: string): Plugin {
39
38
  },
40
39
  load(id) {
41
40
  if (id !== virtualId) return;
42
- return `export default ${JSON.stringify(`${base.replace(/\/$/, "")}/@fs${stylesheet}`)};`;
41
+ return `export default ${JSON.stringify(`${base.replace(/\/$/u, "")}/@fs${stylesheet}`)};`;
43
42
  },
44
43
  };
45
44
  }