@xingwangzhe/stalux 1.54.0 → 1.55.1

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.0",
3
+ "version": "1.55.1",
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
  : []),
@@ -1,4 +1,6 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { createRequire } from "node:module";
3
+ import { dirname, resolve } from "node:path";
2
4
  import type { Plugin } from "vite";
3
5
 
4
6
  /** Keep a source-only devDependency optional in published consumer projects. */
@@ -7,7 +9,18 @@ export function swupDebugPlugin(development: boolean): Plugin {
7
9
  let entry: string | undefined;
8
10
  if (development) {
9
11
  try {
10
- entry = createRequire(import.meta.url).resolve("@swup/debug-plugin");
12
+ // require.resolve honours the "require" condition and returns the CJS
13
+ // build. Re-exporting that from a virtual ESM module makes the browser
14
+ // fail with "does not provide an export named 'default'". Pick the
15
+ // package's ESM entry from its manifest instead.
16
+ const cjs = createRequire(import.meta.url).resolve("@swup/debug-plugin");
17
+ const manifest = resolve(dirname(cjs), "../package.json");
18
+ const pkg = JSON.parse(readFileSync(manifest, "utf8")) as {
19
+ exports?: { "."?: { import?: string } };
20
+ module?: string;
21
+ };
22
+ const esm = pkg.exports?.["."]?.import ?? pkg.module;
23
+ if (esm) entry = resolve(dirname(manifest), esm);
11
24
  } catch {
12
25
  // Consumers can install @swup/debug-plugin as their own devDependency.
13
26
  }
@@ -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 {
@@ -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();
@@ -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");
@@ -36,6 +36,7 @@ interface NavigationContext {
36
36
  incomingDocument?: Document;
37
37
  removeOutgoing?: () => void;
38
38
  obsoleteStyles: Element[];
39
+ pruneObsoleteStyles?: () => void;
39
40
  glide?: PageGlide;
40
41
  releaseOutgoing?: () => void;
41
42
  }
@@ -405,7 +406,17 @@ export function installSwupRouter(): Swup {
405
406
  // motion.start, in the same synchronous block as the entry cue: releasing
406
407
  // it here would paint the new document fully visible for one frame.
407
408
  if (context.controller.signal.aborted) return;
408
- for (const element of context.obsoleteStyles) element.remove();
409
+ // ParallelPlugin keeps the outgoing container visible while it fades out
410
+ // and only removes it at visit:end / content:remove. Its page-scoped CSS
411
+ // must outlive that DOM: pruning here paints the outgoing (and already
412
+ // overlapping) content unstyled for the whole overlap. `awaitAssets`
413
+ // only covers the incoming stylesheets, not the outgoing ones.
414
+ const pruneObsoleteStyles = once(() => {
415
+ for (const element of context.obsoleteStyles) element.remove();
416
+ });
417
+ context.pruneObsoleteStyles = pruneObsoleteStyles;
418
+ if (context.outgoingReady) void context.outgoingReady.then(pruneObsoleteStyles);
419
+ else pruneObsoleteStyles();
409
420
  const incomingStyles = [...next.head.querySelectorAll("style,link[rel=stylesheet]")];
410
421
  authoredStyles = [...document.head.querySelectorAll("style,link[rel=stylesheet]")].filter(
411
422
  (el) => incomingStyles.some((other) => el.isEqualNode(other)),
@@ -513,6 +524,9 @@ export function installSwupRouter(): Swup {
513
524
  const context = contexts.get(visit);
514
525
  context?.controller.abort();
515
526
  context?.removeOutgoing?.();
527
+ // An aborted visit never reaches visit:end, so its outgoing CSS has to
528
+ // be released here or it would linger in head forever.
529
+ context?.pruneObsoleteStyles?.();
516
530
  context?.glide?.cancel();
517
531
  // An aborted visit keeps no exit fill: without this the stage would stay
518
532
  // transparent until the next successful navigation.
@@ -528,6 +542,9 @@ export function installSwupRouter(): Swup {
528
542
  swup.hooks.on("visit:fail", cancel);
529
543
  swup.hooks.on("visit:end", (visit) => {
530
544
  const context = contexts.get(visit);
545
+ // Safety net for paths where the exit animation never settles. The prune
546
+ // is idempotent, so a completed outgoingReady does not remove twice.
547
+ context?.pruneObsoleteStyles?.();
531
548
  context?.glide?.cancel();
532
549
  context?.releaseOutgoing?.();
533
550
  context?.motion?.start(null, { skip: true });
@@ -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
- }