@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
@@ -1,5 +1,3 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
1
  import path from "node:path";
4
2
  import { imageSize } from "image-size";
5
3
 
@@ -52,8 +50,9 @@ function assertDimensions(bytes: Buffer): void {
52
50
  }
53
51
 
54
52
  async function readLocalCover(local: string): Promise<Buffer> {
55
- if ((await stat(local)).size > MAX_COVER_BYTES) throw new Error("cover exceeds 8 MiB");
56
- const bytes = await readFile(local);
53
+ const file = Bun.file(local);
54
+ if (file.size > MAX_COVER_BYTES) throw new Error("cover exceeds 8 MiB");
55
+ const bytes = Buffer.from(await file.bytes());
57
56
  assertDimensions(bytes);
58
57
  return bytes;
59
58
  }
@@ -71,16 +70,15 @@ async function readResponseBytes(response: Response): Promise<Buffer> {
71
70
  }
72
71
 
73
72
  async function fetchCover(url: URL, cacheFile: string): Promise<Buffer> {
74
- const cachedStat = await stat(cacheFile).catch(() => undefined);
75
- if (cachedStat && Date.now() - cachedStat.mtimeMs < COVER_CACHE_TTL_MS)
76
- return readFile(cacheFile);
73
+ const cached = Bun.file(cacheFile);
74
+ if (cached.size > 0 && Date.now() - cached.lastModified < COVER_CACHE_TTL_MS)
75
+ return Buffer.from(await cached.bytes());
77
76
  const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
78
77
  if (!(response.ok && response.headers.get("content-type")?.startsWith("image/")))
79
78
  throw new Error(`cover returned HTTP ${response.status} or non-image content`);
80
79
  const bytes = await readResponseBytes(response);
81
80
  assertDimensions(bytes);
82
- await mkdir(path.dirname(cacheFile), { recursive: true });
83
- await writeFile(cacheFile, bytes);
81
+ await Bun.write(cacheFile, bytes);
84
82
  return bytes;
85
83
  }
86
84
 
@@ -91,6 +89,10 @@ function loadCoverUncached(cover: string, paths: SocialCardPaths): Promise<Buffe
91
89
  if (!["https:", "http:"].includes(url.protocol)) throw new Error("invalid cover URL");
92
90
  return fetchCover(
93
91
  url,
94
- path.join(paths.cacheDir, "covers", createHash("sha256").update(url.href).digest("hex")),
92
+ path.join(
93
+ paths.cacheDir,
94
+ "covers",
95
+ new Bun.CryptoHasher("sha256").update(url.href).digest("hex"),
96
+ ),
95
97
  );
96
98
  }
@@ -1,5 +1,3 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
1
  import path from "node:path";
4
2
  import { Renderer } from "@takumi-rs/core";
5
3
  import { type SocialCardData, socialCardFormat } from "../utils/social-image";
@@ -23,7 +21,7 @@ function getRenderer(fontPath: string): Promise<Renderer> {
23
21
  const renderer = new Renderer({ cacheMaxBytes: 32 * 1024 * 1024 });
24
22
  await renderer.registerFont({
25
23
  name: CARD_FONT_FAMILY,
26
- data: await readFile(fontPath),
24
+ data: await Bun.file(fontPath).bytes(),
27
25
  });
28
26
  return renderer;
29
27
  })();
@@ -36,9 +34,9 @@ const fontDigests = new Map<string, Promise<string>>();
36
34
  function getFontDigest(fontPath: string): Promise<string> {
37
35
  let pending = fontDigests.get(fontPath);
38
36
  if (!pending) {
39
- pending = readFile(fontPath).then((bytes) =>
40
- createHash("sha256").update(bytes).digest("hex"),
41
- );
37
+ pending = Bun.file(fontPath)
38
+ .bytes()
39
+ .then((bytes) => new Bun.CryptoHasher("sha256").update(bytes).digest("hex"));
42
40
  fontDigests.set(fontPath, pending);
43
41
  }
44
42
  return pending;
@@ -62,13 +60,13 @@ export async function renderSocialCard(
62
60
  // always reuses the cached render, whatever the filesystem timestamps say.
63
61
  // It is checked before any raster work, so a warm build only reads bytes.
64
62
  const fontDigest = await getFontDigest(paths.fontPath);
65
- const key = createHash("sha256")
63
+ const key = new Bun.CryptoHasher("sha256")
66
64
  .update(JSON.stringify([TEMPLATE, format, data, fontDigest]))
67
65
  .update(coverBytes ?? "")
68
66
  .digest("hex");
69
67
  const cacheFile = path.join(paths.cacheDir, `${key}.${format}`);
70
- const hit = await readFile(cacheFile).catch(() => undefined);
71
- if (hit) return hit;
68
+ const cached = Bun.file(cacheFile);
69
+ if (cached.size > 0) return Buffer.from(await cached.bytes());
72
70
 
73
71
  const renderer = await getRenderer(paths.fontPath);
74
72
  const render = (withCover: boolean) =>
@@ -92,7 +90,6 @@ export async function renderSocialCard(
92
90
  cacheable = false;
93
91
  }
94
92
  if (!cacheable) return bytes;
95
- await mkdir(paths.cacheDir, { recursive: true });
96
- await writeFile(cacheFile, bytes);
93
+ await Bun.write(cacheFile, bytes);
97
94
  return bytes;
98
95
  }
@@ -1,6 +1,4 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { createRequire } from "node:module";
3
- import { pathToFileURL } from "node:url";
4
2
  import type { Plugin } from "vite";
5
3
  import { affectsDeclarativeTools } from "../scripts/webmcp-mutations";
6
4
 
@@ -21,9 +19,17 @@ export function optimizeWebMcpMutations(code: string, version: string): string |
21
19
  }
22
20
 
23
21
  export function webMcpMutationsPlugin(): Plugin {
24
- const entry = createRequire(import.meta.url).resolve("@mcp-b/webmcp-polyfill");
25
- const version = JSON.parse(
26
- readFileSync(new URL("../package.json", pathToFileURL(entry)), "utf8"),
22
+ const entry = Bun.resolveSync(
23
+ "@mcp-b/webmcp-polyfill",
24
+ Bun.fileURLToPath(new URL(".", import.meta.url)),
25
+ );
26
+ const version = (
27
+ JSON.parse(
28
+ readFileSync(
29
+ Bun.fileURLToPath(new URL("../package.json", Bun.pathToFileURL(entry))),
30
+ "utf8",
31
+ ),
32
+ ) as { version: string }
27
33
  ).version;
28
34
  return {
29
35
  name: "stalux:webmcp-mutations",
@@ -46,10 +46,10 @@ const htmlLang = siteConfig.lang || "zh-CN";
46
46
  // 保证增量构建依赖图 hash 稳定。
47
47
  const llmPromoteRaw = promoteConfig?.llm_promote ?? "";
48
48
  const llmPromote = llmPromoteRaw
49
- ?.replace(/\{author\}/g, authorConfig.name)
50
- ?.replace(/\{url\}/g, siteConfig.url)
51
- ?.replace(/\{title\}/g, siteConfig.title)
52
- ?.replace(/\{cc\}/g, "CC-BY-NC-SA-4.0");
49
+ ?.replace(/\{author\}/gu, authorConfig.name)
50
+ ?.replace(/\{url\}/gu, siteConfig.url)
51
+ ?.replace(/\{title\}/gu, siteConfig.title)
52
+ ?.replace(/\{cc\}/gu, "CC-BY-NC-SA-4.0");
53
53
  const routeKind = getRouteKind(Astro.url.pathname);
54
54
  ---
55
55
 
@@ -8,7 +8,7 @@ const responseSchema = {
8
8
  };
9
9
 
10
10
  export const GET: APIRoute = async ({ site }) => {
11
- const server = site?.toString().replace(/\/$/, "") || "";
11
+ const server = site?.toString().replace(/\/$/u, "") || "";
12
12
  const document = {
13
13
  openapi: "3.1.0",
14
14
  info: {
@@ -79,8 +79,8 @@ function getState(ctx: PluginContext): FeatureFlagsState {
79
79
  }
80
80
 
81
81
  function getFrontmatter(ctx: PluginContext): Record<string, unknown> {
82
- const astroData = ctx.data.astro;
83
- if (!astroData) throw new Error("Satteri did not provide Astro frontmatter data");
82
+ const astroData = ctx.data.astro as { frontmatter?: Record<string, unknown> } | undefined;
83
+ if (!astroData?.frontmatter) throw new Error("Satteri did not provide Astro frontmatter data");
84
84
  return astroData.frontmatter;
85
85
  }
86
86
 
@@ -61,7 +61,7 @@ export interface CollectionPaths {
61
61
  * 因此在 schema 层直接拒绝,让构建尽快失败,而不是产出错误时间。
62
62
  */
63
63
  export const PADDED_DATETIME_PATTERN =
64
- /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
64
+ /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:?\d{2})?)?$/u;
65
65
 
66
66
  const datetimeField = (label: string) =>
67
67
  z
@@ -18,7 +18,10 @@ export const siteSchema = z.object({
18
18
  description: z.string().min(1, "site.description is required"),
19
19
  accentColor: z
20
20
  .string()
21
- .regex(/^#[\da-fA-F]{6}$/, "site.accentColor must be a six-digit HEX color such as #EAB308")
21
+ .regex(
22
+ /^#[\da-fA-F]{6}$/u,
23
+ "site.accentColor must be a six-digit HEX color such as #EAB308",
24
+ )
22
25
  .optional()
23
26
  .default("#EAB308"),
24
27
  seoTitle: z.string().min(1).optional(),
@@ -274,9 +274,11 @@ declare global {
274
274
  __staluxListRevealPrime?: boolean;
275
275
  }
276
276
  }
277
- if (typeof document !== "undefined" && typeof window !== "undefined") {
278
- if (!window.__staluxListRevealPrime) {
279
- window.__staluxListRevealPrime = true;
280
- document.addEventListener("astro:after-swap", primeListReveal);
281
- }
277
+ if (
278
+ typeof document !== "undefined" &&
279
+ typeof window !== "undefined" &&
280
+ !window.__staluxListRevealPrime
281
+ ) {
282
+ window.__staluxListRevealPrime = true;
283
+ document.addEventListener("astro:after-swap", primeListReveal);
282
284
  }
@@ -111,7 +111,7 @@ export function installSwupRouter(): Swup {
111
111
  document.querySelector("base[target]")?.getAttribute("target");
112
112
  return (
113
113
  getRouteKind(url.pathname) === "other" ||
114
- /\.(?:md|json|xml|txt|html?|pdf|png|webp|avif|svg|jpe?g)$/i.test(url.pathname) ||
114
+ /\.(?:md|json|xml|txt|html?|pdf|png|webp|avif|svg|jpe?g)$/iu.test(url.pathname) ||
115
115
  Boolean(el?.closest("[data-no-swup],[data-astro-reload],[rel~=external]")) ||
116
116
  Boolean(target && target !== "_self")
117
117
  );
@@ -57,7 +57,7 @@ function parseLocation(value: unknown): ApproximateLocation {
57
57
  "ip" in value &&
58
58
  typeof value.ip === "string" &&
59
59
  value.ip.length <= 45 &&
60
- /^[0-9a-f:.]+$/i.test(value.ip);
60
+ /^[0-9a-f:.]+$/iu.test(value.ip);
61
61
  return { city: value.city.trim(), lat, lon, ...(usableIp ? { ip: value.ip as string } : {}) };
62
62
  }
63
63
 
@@ -97,7 +97,7 @@ function parseWeatherData(value: unknown): WeatherData | undefined {
97
97
  if (
98
98
  typeof value.ip !== "string" ||
99
99
  value.ip.length > 45 ||
100
- !/^[0-9a-f:.]+$/i.test(value.ip)
100
+ !/^[0-9a-f:.]+$/iu.test(value.ip)
101
101
  )
102
102
  return undefined;
103
103
  ip = value.ip;
@@ -17,8 +17,8 @@
17
17
  */
18
18
  // 浏览器无原生 document.modelContext 时由 polyfill 兜底安装;
19
19
  // 有原生实现或已存在 WebMCP-aware 扩展时,polyfill 检测到后自动 no-op。
20
- // ESM entry exports an initializer; importing it alone does not install the API.
21
- import { initializeWebMCPPolyfill } from "@mcp-b/webmcp-polyfill";
20
+ // ESM entry exports installWebMCP; importing it alone does not install the API.
21
+ import { installWebMCP } from "@mcp-b/webmcp-polyfill";
22
22
  import { createPostLookup } from "../utils/post-lookup";
23
23
  import { sitePath } from "../utils/site-path";
24
24
  import { createClientLogger } from "./logger";
@@ -211,7 +211,7 @@ function postIdFromUrl(value: string): string | null {
211
211
  const prefix = sitePath("posts/");
212
212
  if (!url.pathname.startsWith(prefix)) return null;
213
213
  const suffix = url.pathname.slice(prefix.length);
214
- const path = suffix.endsWith("/") ? suffix.slice(0, -1) : suffix.replace(/\.md$/, "");
214
+ const path = suffix.endsWith("/") ? suffix.slice(0, -1) : suffix.replace(/\.md$/u, "");
215
215
  if (!path || path.includes("/")) return null;
216
216
  return decodeURIComponent(path);
217
217
  } catch {
@@ -841,7 +841,7 @@ async function registerTools(
841
841
  registerPageLifecycle("webmcp", () => {
842
842
  // Astro can hoist processed scripts from conditional template branches.
843
843
  if (document.body?.dataset.staluxWebmcpEnabled === "false") return;
844
- initializeWebMCPPolyfill();
844
+ installWebMCP();
845
845
  const controller = new AbortController();
846
846
  void registerTools(controller.signal)
847
847
  .then((r) => {
@@ -1,6 +1,6 @@
1
1
  import { productionStyleName } from "./style-name.ts";
2
2
 
3
- const ACCENT_COLOR_PATTERN = /^#[\da-fA-F]{6}$/;
3
+ const ACCENT_COLOR_PATTERN = /^#[\da-fA-F]{6}$/u;
4
4
 
5
5
  const ACCENT_OPACITIES = [90, 85, 80, 70, 60, 50, 40, 30, 20] as const;
6
6
 
@@ -61,22 +61,22 @@ function getSiteConfig(config: ConfigMap): Record<string, unknown> {
61
61
  export function getSite(config: ConfigMap, contextSite?: string): string {
62
62
  const siteData = getSiteConfig(config);
63
63
  const configUrl = siteData.url as string | undefined;
64
- return (configUrl || contextSite || "").replace(/\/$/, "");
64
+ return (configUrl || contextSite || "").replace(/\/$/u, "");
65
65
  }
66
66
 
67
67
  /** 将任意空白压缩为单行 */
68
68
  function inlineText(text: string | undefined): string {
69
- return (text || "").replace(/\s+/g, " ").trim();
69
+ return (text || "").replace(/\s+/gu, " ").trim();
70
70
  }
71
71
 
72
72
  /** 转义 markdown 文本中的特殊字符(用于描述) */
73
73
  function escapeMd(text: string | undefined): string {
74
- return inlineText(text).replace(/([\\[*_`#])/g, "\\$1");
74
+ return inlineText(text).replace(/([\\[*_`#])/gu, "\\$1");
75
75
  }
76
76
 
77
77
  /** 转义 markdown 链接标题中的特殊字符 */
78
78
  function escapeMdLinkTitle(text: string | undefined): string {
79
- return inlineText(text).replace(/([\\[\]])/g, "\\$1");
79
+ return inlineText(text).replace(/([\\[\]])/gu, "\\$1");
80
80
  }
81
81
 
82
82
  /** 构建 markdown 列表项 */
@@ -125,7 +125,7 @@ function getMediaLinks(
125
125
  try {
126
126
  const hostname = new URL(url.startsWith("http") ? url : `https://example.com${url}`)
127
127
  .hostname;
128
- const domain = hostname.replace(/^www\./, "").split(".")[0];
128
+ const domain = hostname.replace(/^www\./u, "").split(".")[0];
129
129
  name = domain ? domain.charAt(0).toUpperCase() + domain.slice(1) : "Link";
130
130
  } catch {
131
131
  logDetail(
@@ -147,7 +147,7 @@ function getEmail(config: ConfigMap): string | undefined {
147
147
  | undefined;
148
148
  if (!items) return undefined;
149
149
  const mail = items.find((l) => l.link?.startsWith("mailto:"))?.link;
150
- if (mail) return mail.replace(/^mailto:/, "");
150
+ if (mail) return mail.replace(/^mailto:/u, "");
151
151
  return undefined;
152
152
  }
153
153
 
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  import type { AstroRuntimeLogger } from "astro";
3
2
  /**
4
3
  * 徽章生成器工具
@@ -59,7 +58,7 @@ export function generateBadge(options: BadgeOptions, logger?: AstroRuntimeLogger
59
58
 
60
59
  /** Content-addressed assets are shared by all pages and remain lazy inside details. */
61
60
  export function badgeAssetId(svg: string): string {
62
- return createHash("sha256").update(svg).digest("hex").slice(0, 24);
61
+ return new Bun.CryptoHasher("sha256").update(svg).digest("hex").slice(0, 24);
63
62
  }
64
63
 
65
64
  export function normalizeBadgeStyle(style: string | undefined): BadgeOptions["style"] {
package/src/utils/cc.ts CHANGED
@@ -11,7 +11,7 @@ export function buildCCLink(licenseCode: string): string {
11
11
  return "https://creativecommons.org/publicdomain/zero/1.0/";
12
12
  }
13
13
 
14
- const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/);
14
+ const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/u);
15
15
  if (!match) {
16
16
  return "https://creativecommons.org/licenses/by-nc-sa/4.0/";
17
17
  }
@@ -36,7 +36,7 @@ export function buildCCName(licenseCode: string, t: Translator["t"]): string {
36
36
  return t("cc.zero");
37
37
  }
38
38
 
39
- const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/);
39
+ const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/u);
40
40
  if (!match) {
41
41
  // 无法解析时回退为默认 CC-BY-NC-SA-4.0
42
42
  const elements = ["BY", "NC", "SA"].map((code) => t(`cc.${code}`)).join("-");
@@ -33,7 +33,7 @@ export const toTimestamp = (v: unknown, timeZone?: string): number =>
33
33
  * RSS pubDate、页面 datetime 全体平移 8 小时(2026-10-24 事故)。
34
34
  */
35
35
  const NAIVE_DATE_PATTERN =
36
- /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?)?$/;
36
+ /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?)?$/u;
37
37
 
38
38
  /** 用共享的 formatter 计算目标时区在给定时刻的 UTC 偏移(毫秒,东八区为 +28800000) */
39
39
  function zoneOffsetMs(date: Date, timeZone: string): number {
@@ -128,7 +128,7 @@ export function format(date: Date | string | number, formatStr: string): string
128
128
  mm: pad2(d.getMinutes()),
129
129
  ss: pad2(d.getSeconds()),
130
130
  };
131
- return formatStr.replace(/yyyy|MM|dd|HH|mm|ss/g, (t) => tokens[t] ?? t);
131
+ return formatStr.replace(/yyyy|MM|dd|HH|mm|ss/gu, (t) => tokens[t] ?? t);
132
132
  }
133
133
 
134
134
  /**
@@ -159,11 +159,11 @@ export function formatInTimeZone(
159
159
  if (offset === "GMT" || offset === "UTC") {
160
160
  offset = "+00:00";
161
161
  } else {
162
- offset = offset.replace(/^GMT/, "").replace(/^UTC/, "");
162
+ offset = offset.replace(/^GMT/u, "").replace(/^UTC/u, "");
163
163
  if (!(offset.startsWith("+") || offset.startsWith("-"))) {
164
164
  offset = `+${offset}`;
165
165
  }
166
- const m = offset.match(/^([+-])(\d{1,2})(?::?(\d{2}))?$/);
166
+ const m = offset.match(/^([+-])(\d{1,2})(?::?(\d{2}))?$/u);
167
167
  const sign = m?.[1];
168
168
  const hours = m?.[2];
169
169
  if (sign && hours) {
@@ -186,5 +186,5 @@ export function formatInTimeZone(
186
186
  ss: second,
187
187
  XXX: offset,
188
188
  };
189
- return formatStr.replace(/yyyy|MM|dd|HH|mm|ss|XXX/g, (t) => tokens[t] ?? t);
189
+ return formatStr.replace(/yyyy|MM|dd|HH|mm|ss|XXX/gu, (t) => tokens[t] ?? t);
190
190
  }
@@ -19,9 +19,9 @@ export function describeError(error: unknown): string {
19
19
  }
20
20
  const text = parts.join("\nCaused by: ");
21
21
  return text
22
- .replace(/(https?:\/\/[^\s?#]+)[?#][^\s)]+/g, "$1?[redacted]")
23
- .replace(/(Bearer\s+)[^\s]+/gi, "$1[redacted]")
24
- .replace(/((?:token|password|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+/gi, "$1[redacted]");
22
+ .replace(/(https?:\/\/[^\s?#]+)[?#][^\s)]+/gu, "$1?[redacted]")
23
+ .replace(/(Bearer\s+)[^\s]+/giu, "$1[redacted]")
24
+ .replace(/((?:token|password|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]");
25
25
  }
26
26
 
27
27
  /** Runtime loggers have no debug method. Detail is opt-in, then sent through official info. */
@@ -13,7 +13,7 @@ export type RouteKind =
13
13
  | "other";
14
14
 
15
15
  export function normalizePublicPath(pathname: string): string {
16
- const path = pathname.split(/[?#]/, 1)[0]?.replace(/\/+$/, "") ?? "";
16
+ const path = pathname.split(/[?#]/u, 1)[0]?.replace(/\/+$/u, "") ?? "";
17
17
  return path || "/";
18
18
  }
19
19
 
@@ -41,12 +41,12 @@ export function getMarkdownPath(pathname: string): string | undefined {
41
41
  if (path === "/") return "/index.md";
42
42
  if (path === "/tags" || path === "/categories") return `${path}/index.md`;
43
43
  if (["/about", "/archives", "/links", "/words"].includes(path)) return `${path}.md`;
44
- if (/^\/(?:posts|tags|categories)\/[^/]+$/.test(path)) return `${path}.md`;
44
+ if (/^\/(?:posts|tags|categories)\/[^/]+$/u.test(path)) return `${path}.md`;
45
45
  return undefined;
46
46
  }
47
47
 
48
48
  export function toPublicUrl(site: string, pathname: string, markdown = false): string {
49
- const base = site.replace(/\/+$/, "");
49
+ const base = site.replace(/\/+$/u, "");
50
50
  const normalized = normalizePublicPath(pathname);
51
51
  const path = markdown ? getMarkdownPath(normalized) : normalized;
52
52
  if (!path) return `${base}${normalized === "/" ? "/" : `${normalized}/`}`;
@@ -46,7 +46,7 @@ export interface SeoDocument {
46
46
  }
47
47
 
48
48
  function normalizeSiteUrl(value: string): string {
49
- return new URL(value).href.replace(/\/+$/, "");
49
+ return new URL(value).href.replace(/\/+$/u, "");
50
50
  }
51
51
 
52
52
  function absoluteUrl(value: string, base: string, protocol: string): string {
@@ -155,7 +155,7 @@ export function buildSeoDocument(input: SeoDocumentInput): SeoDocument {
155
155
  ]),
156
156
  ];
157
157
 
158
- const sameAs = input.mediaLinks.filter((link) => /^https?:\/\//i.test(link));
158
+ const sameAs = input.mediaLinks.filter((link) => /^https?:\/\//iu.test(link));
159
159
  const authorImage = absoluteUrl(author.avatar, siteUrl, input.protocol);
160
160
  const authorEntity = {
161
161
  "@type": "Person",
@@ -1,4 +1,4 @@
1
1
  /** Resolve a known static resource under Astro's configured deployment base. */
2
2
  export function sitePath(pathname: string, base = import.meta.env.BASE_URL): string {
3
- return `${(base || "/").replace(/\/+$/, "")}/${pathname.replace(/^\/+/, "")}`;
3
+ return `${(base || "/").replace(/\/+$/u, "")}/${pathname.replace(/^\/+/u, "")}`;
4
4
  }
@@ -1,5 +1,3 @@
1
- import { createHash } from "node:crypto";
2
-
3
1
  export interface SocialCardData {
4
2
  title: string;
5
3
  siteTitle: string;
@@ -43,7 +41,7 @@ export function socialCardFormat(data: Pick<SocialCardData, "cover">): SocialCar
43
41
 
44
42
  export function socialCardPath(data: SocialCardData, base = "/"): string {
45
43
  // Bump the template revision when layout changes. Metadata changes get a fresh URL.
46
- const hash = createHash("sha256")
44
+ const hash = new Bun.CryptoHasher("sha256")
47
45
  .update(JSON.stringify(["stalux-mono-v2", data]))
48
46
  .digest("hex")
49
47
  .slice(0, 20);