@xingwangzhe/stalux 1.54.1 → 1.55.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.54.1",
3
+ "version": "1.55.2",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -103,13 +103,12 @@
103
103
  },
104
104
  "dependencies": {
105
105
  "@astrojs/compiler": "^4.0.0",
106
- "@astrojs/markdown-satteri": "^0.4.6",
106
+ "@astrojs/markdown-satteri": "^0.4.8",
107
107
  "@astrojs/rss": "^4.0.19",
108
108
  "@astrojs/sitemap": "^3.7.4",
109
109
  "@expressive-code/plugin-line-numbers": "^0.44.3",
110
- "@lucide/astro": "^1.66.0",
111
- "@mcp-b/webmcp-polyfill": "^6.0.3",
112
- "@microsoft/clarity": "1.0.2",
110
+ "@lucide/astro": "^1.71.0",
111
+ "@microsoft/clarity": "^1.0.2",
113
112
  "@pagefind/component-ui": "^1.6.0",
114
113
  "@swup/a11y-plugin": "^5.2.1",
115
114
  "@swup/body-class-plugin": "^3.3.0",
@@ -122,7 +121,8 @@
122
121
  "@swup/route-name-plugin": "^4.1.0",
123
122
  "@swup/scripts-plugin": "^2.1.0",
124
123
  "@swup/scroll-plugin": "^4.0.0",
125
- "@takumi-rs/core": "2.19.0",
124
+ "@takumi-rs/core": "^2.20.0",
125
+ "@w3c/webmcp-polyfill": "^0.1.0",
126
126
  "@waline/client": "^3.16.2",
127
127
  "@xingwangzhe/cjk-font-split-native": "^0.4.10",
128
128
  "@xingwangzhe/satteri-mermaid": "^0.8.2",
@@ -132,39 +132,40 @@
132
132
  "astro-seo": "^1.2.0",
133
133
  "badge-maker": "^6.0.0",
134
134
  "css-tree": "^3.2.1",
135
- "image-size": "^2.0.4",
135
+ "escape-string-regexp": "^5.0.0",
136
+ "html-escaper": "^3.0.3",
137
+ "image-size": "^2.1.0",
136
138
  "magic-string": "^1.4.5",
137
139
  "pagefind": "^1.6.0",
138
- "parse5": "^8.1.0",
140
+ "parse5": "^8.1.1",
139
141
  "photoswipe": "^5.4.4",
140
- "satteri": "0.11.6",
141
- "simple-icons": "^16.36.0",
142
- "swup": "4.10.0",
142
+ "satteri": "^0.11.7",
143
+ "simple-icons": "^16.37.0",
144
+ "swup": "^4.10.0",
143
145
  "temml": "^0.14.0",
146
+ "webmcp-types": "^0.1.10",
144
147
  "yaml": "^2.9.1"
145
148
  },
146
149
  "devDependencies": {
147
150
  "@astrojs/check": "^0.9.12",
148
- "@biomejs/biome": "^2.6.5",
151
+ "@biomejs/biome": "^2.6.6",
149
152
  "@swup/debug-plugin": "^4.1.0",
150
- "@types/bun": "1.4.4",
153
+ "@types/bun": "^1.4.4",
151
154
  "@types/css-tree": "^3.2.0",
155
+ "@types/html-escaper": "^3.0.4",
152
156
  "@unocss/astro": "^66.12.0",
153
157
  "@vitest/coverage-v8": "^5.1.0",
154
- "astro": "^7.4.7",
158
+ "astro": "^7.4.10",
155
159
  "happy-dom": "^21.0.4",
156
- "knip": "^6.44.1",
157
- "sharp": "0.35.5",
160
+ "knip": "^6.45.0",
161
+ "sharp": "^0.35.5",
158
162
  "sonda": "^0.14.0",
159
163
  "typescript": "npm:@typescript/typescript6@6.0.2",
160
164
  "unocss": "^66.12.0",
161
- "vite": "^8.3.6",
165
+ "vite": "^8.3.8",
162
166
  "vitest": "^5.1.0"
163
167
  },
164
168
  "peerDependencies": {
165
- "astro": "^7.4.7"
166
- },
167
- "overrides": {
168
- "satteri": "0.11.6"
169
+ "astro": "^7.4.10"
169
170
  }
170
171
  }
package/src/env.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="astro/client" />
2
+ /// <reference types="webmcp-types" />
2
3
 
3
4
  declare module "*.css";
4
5
  declare module "@pagefind/component-ui/css";
package/src/index.ts CHANGED
@@ -61,7 +61,6 @@ import {
61
61
  } from "./internal/satteri-config";
62
62
  import { searchStylePlugin } from "./internal/search-style";
63
63
  import { createViteAliases } from "./internal/vite-aliases";
64
- import { webMcpMutationsPlugin } from "./internal/webmcp-mutations";
65
64
  import { featureFlagsHast, featureFlagsMdast } from "./plugins/feature-flags";
66
65
  import { imageCaptions } from "./plugins/image-captions";
67
66
  import { imageDimensions } from "./plugins/image-dimensions";
@@ -176,7 +175,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
176
175
  },
177
176
  vite: {
178
177
  optimizeDeps: {
179
- exclude: ["@mcp-b/webmcp-polyfill"],
178
+ exclude: ["@w3c/webmcp-polyfill"],
180
179
  // Discover lazy page features before the first visit:
181
180
  // Vite re-optimization would otherwise reload the
182
181
  // entire document midway through a Swup navigation.
@@ -224,7 +223,6 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
224
223
  },
225
224
  swupDebugPlugin(command === "dev"),
226
225
  searchStylePlugin(command === "dev", config.base),
227
- webMcpMutationsPlugin(),
228
226
  ...(command === "build"
229
227
  ? [markdownPropagationPlugin(), compactStylesPlugin(srcDir)]
230
228
  : []),
@@ -9,20 +9,12 @@
9
9
  */
10
10
  import type { AstroIntegrationLogger } from "astro";
11
11
 
12
+ import { escape as escapeHtml } from "html-escaper";
12
13
  import { defineMdastPlugin, type MdastNode, type MdastVisitorContext } from "satteri";
13
14
  import temmlLib, { type Options as TemmlOptions } from "temml";
14
15
 
15
16
  const emptyOptions = {};
16
17
 
17
- function escapeHtml(value: string): string {
18
- return value
19
- .replaceAll("&", "&amp;")
20
- .replaceAll("<", "&lt;")
21
- .replaceAll(">", "&gt;")
22
- .replaceAll('"', "&quot;")
23
- .replaceAll("'", "&#39;");
24
- }
25
-
26
18
  type MathNode = Extract<MdastNode, { type: "math" | "inlineMath" }>;
27
19
 
28
20
  function renderTemmlError(value: string, error: unknown, options: TemmlOptions): string {
@@ -209,9 +209,15 @@ function initializeClarity(id: string): void {
209
209
  * Loading is gated on the same signals that mean "first paint is done" instead
210
210
  * of `window.load`, which slow third-party resources can postpone forever.
211
211
  */
212
- const GOOGLE_SDK_FLOOR_MS = 1200;
213
- const GOOGLE_SDK_LCP_QUIET_MS = 600;
214
- const GOOGLE_SDK_CEILING_MS = 6000;
212
+ /**
213
+ * GA SDK 只在三个条件之一满足后才加载:首次交互、LCP 后安静期、解析后硬顶。
214
+ * 线上实测 gtag.js 194KB、脚本执行 290ms,若在 LCP 后 600ms 就加载,长任务正好
215
+ * 落在 TBT 统计窗口内。这里把门控整体后移:无交互访客要等到 3.5s 之后,
216
+ * 有交互访客仍然立即加载(统计最完整)。代价是首屏前几秒的行为不计入 GA。
217
+ */
218
+ const GOOGLE_SDK_FLOOR_MS = 5000;
219
+ const GOOGLE_SDK_LCP_QUIET_MS = 1500;
220
+ const GOOGLE_SDK_CEILING_MS = 10_000;
215
221
  let googleSdkStarted = false;
216
222
  let googleGateInstalled = false;
217
223
  let googleGateOpen = false;
@@ -73,9 +73,19 @@ export function installSessionBackground(): void {
73
73
  },
74
74
  );
75
75
  };
76
+ /**
77
+ * 装饰背景不参与 LCP,也不该和首屏脚本/字体抢带宽。
78
+ * DOM 就绪后先让关键资源占用连接,再在空闲时请求;
79
+ * `timeout` 兜底防止页面持续繁忙时背景永远缺席。
80
+ */
81
+ const schedule = () => {
82
+ const idle = globalThis.requestIdleCallback;
83
+ if (typeof idle === "function") idle(start, { timeout: 1000 });
84
+ else setTimeout(start, 200);
85
+ };
76
86
  if (document.readyState === "loading")
77
- document.addEventListener("DOMContentLoaded", start, { once: true });
78
- else start();
87
+ document.addEventListener("DOMContentLoaded", schedule, { once: true });
88
+ else schedule();
79
89
  }
80
90
 
81
91
  installSessionBackground();
@@ -16,6 +16,11 @@ const logger = createClientLogger("route-prefetch");
16
16
  const AFTER_LOAD_DELAY_MS = 1000;
17
17
  const LOAD_CEILING_MS = 2500;
18
18
  const IDLE_TIMEOUT_MS = 2000;
19
+ /**
20
+ * 首页导航有 6~8 个路由,全量预热在线上实测约 122KB(`/archives/` 单页 48KB)。
21
+ * 只预热导航顺序最靠前的几条,把带宽留给用户当下的交互。
22
+ */
23
+ const MAX_PREFETCH_ROUTES = 3;
19
24
 
20
25
  export function collectMainRouteUrls(
21
26
  documentLike: Pick<Document, "querySelectorAll"> = document,
@@ -57,7 +62,7 @@ export function mountRoutePrefetch(): (() => void) | undefined {
57
62
  logger.debug("swup preload API unavailable; skipping route prefetch");
58
63
  return;
59
64
  }
60
- const urls = collectMainRouteUrls();
65
+ const urls = collectMainRouteUrls().slice(0, MAX_PREFETCH_ROUTES);
61
66
  if (!urls.length) return;
62
67
  logger.debug(`prefetching ${urls.length} main routes`);
63
68
  // One document at a time: keep the prefetch invisible to the current
@@ -1,3 +1,5 @@
1
+ import escapeStringRegexp from "escape-string-regexp";
2
+ import { escape as escapeHtml } from "html-escaper";
1
3
  import {
2
4
  CJK,
3
5
  type CjkDocument,
@@ -37,19 +39,12 @@ export interface NativeSearch {
37
39
  mergeIndex: (url: string, options?: Record<string, unknown>) => Promise<void>;
38
40
  }
39
41
  const logger = createClientLogger("search/cjk");
40
- const escapeHtml = (text: string) =>
41
- text.replace(
42
- /[&<>"']/gu,
43
- (char) =>
44
- ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char] ?? char,
45
- );
46
-
47
42
  function excerpt(content: string, terms: string[]) {
48
43
  const position = Math.max(0, normalizeSearch(content).indexOf(terms[0] ?? ""));
49
44
  const start = Math.max(0, position - 45);
50
45
  const plain = `${start ? "…" : ""}${content.slice(start, start + 180)}${content.length > start + 180 ? "…" : ""}`;
51
46
  const pattern = terms
52
- .map((term) => term.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"))
47
+ .map((term) => escapeStringRegexp(term))
53
48
  .sort((a, b) => b.length - a.length)
54
49
  .join("|");
55
50
  const regex = new RegExp(pattern, "giu");
@@ -10,7 +10,7 @@
10
10
  * 工具描述跟随站点语言(site.yml 的 lang):中文站显示中文说明,
11
11
  * 英文站显示英文说明,保证 AI 代理与评审(如 webmcp.com)能读懂。
12
12
  *
13
- * 浏览器无原生 modelContext 时,由布局注入的 @mcp-b/webmcp-polyfill
13
+ * 浏览器无原生 modelContext 时,由官方 @w3c/webmcp-polyfill
14
14
  * 提供兜底实现;两者都没有则静默跳过注册,不影响普通访问者。
15
15
  *
16
16
  * Spec: https://github.com/webmachinelearning/webmcp
@@ -18,7 +18,8 @@
18
18
  // 浏览器无原生 document.modelContext 时由 polyfill 兜底安装;
19
19
  // 有原生实现或已存在 WebMCP-aware 扩展时,polyfill 检测到后自动 no-op。
20
20
  // ESM entry exports installWebMCP; importing it alone does not install the API.
21
- import { installWebMCP } from "@mcp-b/webmcp-polyfill";
21
+ import { installWebMCP } from "@w3c/webmcp-polyfill";
22
+ import type { WebMCP } from "webmcp-types";
22
23
  import { createPostLookup } from "../utils/post-lookup";
23
24
  import { sitePath } from "../utils/site-path";
24
25
  import { createClientLogger } from "./logger";
@@ -38,27 +39,8 @@ declare global {
38
39
  }
39
40
  }
40
41
 
41
- /** 规范(index.bs)中 ModelContextTool 字典的最小类型子集 */
42
- interface WebMCPTool {
43
- name: string;
44
- title?: string;
45
- description: string;
46
- inputSchema?: object;
47
- annotations?: {
48
- readOnlyHint?: boolean;
49
- untrustedContentHint?: boolean;
50
- };
51
- execute: (
52
- input: Record<string, unknown>,
53
- options?: { signal?: AbortSignal },
54
- ) => Promise<unknown>;
55
- }
56
-
57
- interface ModelContext {
58
- registerTool: (tool: WebMCPTool, options?: { signal?: AbortSignal }) => Promise<undefined>;
59
- }
60
-
61
- type DocumentWithModelContext = { modelContext?: ModelContext };
42
+ /** 工具定义直接使用官方 webmcp-types 的 ModelContextTool,不自写最小接口。 */
43
+ type WebMCPTool = WebMCP.ModelContextTool;
62
44
 
63
45
  // ---------------------------------------------------------------------------
64
46
  // 语言选择(跟随站点 site.yml 的 lang)
@@ -763,7 +745,10 @@ function buildTools(): WebMCPTool[] {
763
745
  const execute = tool.execute;
764
746
  return {
765
747
  ...tool,
766
- execute: async (input: Record<string, unknown>, options?: { signal?: AbortSignal }) => {
748
+ execute: async (
749
+ input: Record<string, unknown>,
750
+ options?: WebMCP.ToolExecuteCallbackOptions,
751
+ ) => {
767
752
  if (!input || typeof input !== "object" || Array.isArray(input))
768
753
  return err("BAD_INPUT", pick("参数必须是对象", "Input must be an object"));
769
754
  const properties =
@@ -773,7 +758,10 @@ function buildTools(): WebMCPTool[] {
773
758
  return err("BAD_INPUT", pick("包含未知参数", "Unknown input property"));
774
759
  if (options?.signal?.aborted) throw options.signal.reason;
775
760
  try {
776
- const result = await execute(input, options);
761
+ const result = await execute(
762
+ input,
763
+ options ?? { signal: new AbortController().signal },
764
+ );
777
765
  options?.signal?.throwIfAborted();
778
766
  return result;
779
767
  } catch (error) {
@@ -799,8 +787,7 @@ function buildTools(): WebMCPTool[] {
799
787
  async function registerTools(
800
788
  signal: AbortSignal,
801
789
  ): Promise<{ registered: string[]; reason?: string }> {
802
- const doc = document as DocumentWithModelContext;
803
- const ctx = doc.modelContext;
790
+ const ctx = document.modelContext;
804
791
  if (!ctx || typeof ctx.registerTool !== "function") {
805
792
  logger.debug("modelContext unavailable; registration skipped");
806
793
  return { registered: [], reason: "unsupported" };
@@ -1,43 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import type { Plugin } from "vite";
3
- import { affectsDeclarativeTools } from "../scripts/webmcp-mutations";
4
-
5
- const observer = "const observer = new MutationObserver(sync);";
6
-
7
- /** Upstream 5.1.0 rescans every element even for unrelated clock/background mutations. */
8
- export function optimizeWebMcpMutations(code: string, version: string): string | undefined {
9
- if (
10
- version !== "5.1.0" ||
11
- !code.includes("function installDeclarativeForms(document, context)") ||
12
- code.split(observer).length !== 2
13
- )
14
- return;
15
- return code.replace(
16
- observer,
17
- `const relevant = ${affectsDeclarativeTools.toString()};\nconst observer = new MutationObserver((records) => { if (relevant(records)) sync(); });`,
18
- );
19
- }
20
-
21
- export function webMcpMutationsPlugin(): Plugin {
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 }
33
- ).version;
34
- return {
35
- name: "stalux:webmcp-mutations",
36
- enforce: "pre",
37
- transform(code, id) {
38
- if (!id.replaceAll("\\", "/").endsWith("/@mcp-b/webmcp-polyfill/dist/index.js")) return;
39
- const optimized = optimizeWebMcpMutations(code, version);
40
- if (optimized) return { code: optimized, map: null };
41
- },
42
- };
43
- }
@@ -1,27 +0,0 @@
1
- /** A clock tick or background fade cannot change declarative tool definitions. */
2
- export function affectsDeclarativeTools(records: MutationRecord[]): boolean {
3
- const selector = "form[toolname],[form],label,slot";
4
- const containsTools = (node: Node): boolean => {
5
- if (!(node instanceof Element)) return false;
6
- if (Element.prototype.matches.call(node, selector)) return true;
7
- if (Element.prototype.querySelector.call(node, selector)) return true;
8
- // An already-created open shadow tree can be inserted along with its host.
9
- for (const element of [node, ...Element.prototype.querySelectorAll.call(node, "*")]) {
10
- if (Reflect.get(Element.prototype, "shadowRoot", element)) return true;
11
- }
12
- return false;
13
- };
14
- return records.some((record) => {
15
- if (
16
- record.type === "attributes" &&
17
- (record.attributeName === "form" || record.attributeName === "toolname")
18
- )
19
- return true;
20
- const element =
21
- record.target instanceof Element ? record.target : record.target.parentElement;
22
- if (element && Element.prototype.closest.call(element, selector)) return true;
23
- if (record.type === "childList")
24
- return [...record.addedNodes, ...record.removedNodes].some(containsTools);
25
- return false;
26
- });
27
- }