@xingwangzhe/stalux 1.35.12 → 1.35.13
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/README.md +24 -11
- package/package.json +2 -1
- package/src/components/stalux/common/Typewriter.astro +32 -1
- package/src/components/stalux/posts/postContent.astro +7 -1
- package/src/index.ts +49 -101
- package/src/internal/font-slices.ts +125 -31
- package/src/internal/html-images.ts +2 -0
- package/src/internal/html-output.ts +98 -0
- package/src/internal/image-dimensions.ts +99 -74
- package/src/internal/page-output-cache.ts +10 -2
- package/src/internal/public-assets.ts +59 -0
- package/src/internal/runtime-cache-key.ts +14 -3
- package/src/internal/style-names.generated.ts +27 -26
- package/src/layouts/PostLayout.astro +1 -0
- package/src/layouts/Stalux.astro +3 -1
- package/src/pages/api/post.abbrlink.json.ts +0 -3
- package/src/pages/api/posts.json.ts +4 -6
- package/src/pages/openapi.json.ts +3 -44
- package/src/pages/posts/[post].md.ts +4 -40
- package/src/scripts/navigation-font.ts +28 -0
- package/src/scripts/page-runtime.ts +20 -6
- package/src/scripts/pagefind-api.ts +63 -0
- package/src/scripts/view-transitions.ts +12 -1
- package/src/scripts/webmcp.ts +385 -136
- package/src/styles/base.css +7 -0
- package/src/styles/generated.css +1 -1
- package/src/utils/post-lookup.ts +23 -0
- package/src/utils/post-markdown.ts +31 -0
- package/src/utils/site-path.ts +4 -0
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ bun run dev
|
|
|
93
93
|
- 🗺️ **Sitemap** (bundled; `.md` source endpoints auto-filtered)
|
|
94
94
|
- 🖼️ **PhotoSwipe** image lightbox
|
|
95
95
|
- 📊 **Mermaid** diagrams and flowcharts
|
|
96
|
-
Uses Merman through `@xingwangzhe/satteri-mermaid@0.8.
|
|
96
|
+
Uses Merman through `@xingwangzhe/satteri-mermaid@0.8.2`, with the `dark` theme and a transparent canvas by default. The plugin supports 11 theme presets; see the [configuration reference](https://github.com/xingwangzhe/satteri-mermaid/blob/v0.8.2/docs/configuration.md). Rendering failures throw by default.
|
|
97
97
|
- 📐 **Math formula rendering** (Temml → MathML)
|
|
98
98
|
- 💬 **Waline** comment system
|
|
99
99
|
- 🤖 **LLM discovery files** (llms.txt / llms-full.txt)
|
|
@@ -112,11 +112,13 @@ Stalux ships with LXGW WenKai and a variable code font (Google Sans Code). Durin
|
|
|
112
112
|
|
|
113
113
|
Arabic digits `0-9` are always reserved in every subset (`RESERVED_GLYPHS`), even on pages without any visible number. Digits produced at runtime by client scripts — clocks, counters, dates, injected values — never appear in the built HTML, so reserving them keeps numbers from falling back to a system font. Callers can append more characters through the optional `extraChars` argument of `writePageFontSubset`.
|
|
114
114
|
|
|
115
|
-
The subset CSS uses an exact per-character `unicode-range` and `font-display: swap`, so visible text and the downloaded glyphs stay aligned. The variable code font continues to use Astro's local Fonts API.
|
|
115
|
+
The subset CSS uses an exact per-character `unicode-range` and `font-display: swap`, so visible text and the downloaded glyphs stay aligned. The variable code font continues to use Astro's local Fonts API. A same-origin font preload in the HTML head discovers the body subset early; consumers need no manual font links.
|
|
116
116
|
|
|
117
117
|
The persistent BLAKE3 cache key includes font bytes, face index, normalized character set, and algorithm version. Cache files live under `node_modules/.astro/` and are reused across pages and incremental builds. Font inputs are read locally; builds do not fetch fonts from the network.
|
|
118
118
|
|
|
119
|
-
Multi-page builds reuse prepared font data and hashing state through `FontSubsetter`, with up to four native worker tasks for cold subsets. Font containers are decoded/extracted to SFNT before reusable HarfBuzz preprocessing preserves original outlines; Google Brotli encodes WOFF2 at quality 8. Image dimensions, image loading hints, linked CSS text, and font character extraction share one HTML parse. Shared image dimensions and CSS-generated text are also reused within each build, and
|
|
119
|
+
Multi-page builds reuse prepared font data and hashing state through `FontSubsetter`, with up to four native worker tasks for cold subsets. Font containers are decoded/extracted to SFNT before reusable HarfBuzz preprocessing preserves original outlines; Google Brotli encodes WOFF2 at quality 8. Image dimensions, image loading hints, linked CSS text, and font character extraction share one HTML parse. Shared image dimensions and CSS-generated text are also reused within each build, image and font nodes are mutated on the same parse5 AST, then changed nodes are emitted once using source locations. Untouched scripts, comments, templates and subtrees retain their original source.
|
|
120
|
+
|
|
121
|
+
Local image dimensions use `image-size`, including SVG and AVIF support. Normal file reads inspect at most 512 KiB; a JPEG compatibility fallback may read the full file when the size marker is first or beyond that window. Explicit dimensions and remote images are preserved. Referenced assets are hashed as streams, and content changes at the same URL invalidate the page cache regardless of image extension.
|
|
120
122
|
|
|
121
123
|
Processed pages are cached under `node_modules/.astro/stalux-page-output/`. The cache checks source HTML and page paths, referenced local CSS/image content, font inputs, and processing code. Changed inputs, missing font files, and corrupt records trigger reprocessing. Unchanged pages restore their processed output; Pagefind still rebuilds the full search index on every build. Preserve `node_modules/.astro/` to reuse caches across builds; removing it safely rebuilds them.
|
|
122
124
|
|
|
@@ -214,9 +216,7 @@ Full documentation and live demo: **[stalux.needhelp.icu](https://stalux.needhel
|
|
|
214
216
|
|
|
215
217
|
## 🤝 WebMCP / AI Agents
|
|
216
218
|
|
|
217
|
-
Stalux
|
|
218
|
-
`document.modelContext`, e.g. Chrome's built-in Gemini or the [Ask nekuda](https://chromewebstore.google.com/detail/ask-nekuda/amochnnbmnkjjlblolhpddkokhnalkjp)
|
|
219
|
-
extension) interact with your blog directly — **no backend required**.
|
|
219
|
+
Stalux exposes page-local WebMCP tools to compatible browser agents or extensions. All data comes from static build artifacts, so **no backend is required**. This is not an HTTP `/mcp` server that a desktop MCP client can connect to directly: ordinary static hosting cannot process MCP JSON-RPC POST requests. See the [WebMCP community-group draft](https://webmachinelearning.github.io/webmcp/).
|
|
220
220
|
|
|
221
221
|
When a WebMCP-aware browser opens your site, these read-only tools are registered:
|
|
222
222
|
|
|
@@ -227,17 +227,22 @@ When a WebMCP-aware browser opens your site, these read-only tools are registere
|
|
|
227
227
|
| `stalux_current_post` | Metadata of the post currently being viewed | `/api/posts.json` |
|
|
228
228
|
| `stalux_random_post` | Pick a random post's metadata | `/api/posts.json` |
|
|
229
229
|
| `stalux_search_posts` | Full-text search across posts | Pagefind `/pagefind/` |
|
|
230
|
-
| `stalux_read_post` |
|
|
230
|
+
| `stalux_read_post` | Read Markdown in chunks, or indexed plain text when not exported | Markdown / Pagefind |
|
|
231
231
|
| `stalux_site_info` | Site title, URL, description + pointers to `llms.txt` / `llms-full.txt` | `site.yml` (build-time) |
|
|
232
232
|
|
|
233
|
-
All tools are `readOnlyHint: true` — they
|
|
233
|
+
All tools are `readOnlyHint: true` — they do not write site data or initiate navigation.
|
|
234
|
+
|
|
235
|
+
`stalux_search_posts` merges title, tag, category and description matches from the published metadata with body hits from the [Pagefind JavaScript API](https://pagefind.app/docs/api/), using `meta.title` and `plain_excerpt`. Results include `abbrlink` and absolute URL, are deduplicated, and prioritize title matches, with up to 10 posts per call. If the full-text index is unavailable, available metadata matches are labeled `source: "metadata"`, `fullText: false`; no metadata matches yields an index-unavailable error instead of pretending the full-text search succeeded with zero hits. Highlighted HTML is not returned as the tool excerpt. Failed metadata and Pagefind module loads remain retryable.
|
|
236
|
+
|
|
237
|
+
`stalux_read_post` accepts `id`, `offset` (default 0), and `maxChars` (default and maximum 50000). Offsets use UTF-16 units and never split surrogate pairs. With `promote.export_md` enabled, the result has `format: "text/markdown"` and `markdown`. If exports are disabled or the Markdown file is unavailable, it reads Pagefind using the exact `post` filter and returns `format: "text/plain"` and `content`; Markdown syntax and non-indexed content are not preserved by this fallback. Continue with `nextOffset` until it is `null`; `totalChars` and `truncated` describe the range. Disabling both Markdown exports and Pagefind returns `CONTENT_UNAVAILABLE`.
|
|
238
|
+
|
|
239
|
+
Tools initialize the polyfill and register on `astro:page-load`, then unregister and stop the outgoing batch on `astro:before-swap`. Requests only read static resources; search terms are not sent to an external search service, no model is called, and no telemetry is added. The article filter ID and Markdown export flag are public HTML attributes used to select available sources. JSON, Markdown and OpenAPI exports are static files and do not advertise dynamic quotas or fixed `RateLimit-*` balances.
|
|
234
240
|
|
|
235
241
|
**Enabling / disabling:** the tools follow the `conformance` setting in
|
|
236
242
|
`stalux/config/ai-discovery.yml`. Set it to `disabled` to stop registering
|
|
237
243
|
tools; `essential` / `recommended` / `complete` all enable them.
|
|
238
244
|
|
|
239
|
-
**Browser support:** WebMCP is a
|
|
240
|
-
Trial). On browsers without a native `modelContext`, Stalux loads the
|
|
245
|
+
**Browser support:** WebMCP is a community-group draft; native support depends on browser and version. On browsers without a native `modelContext`, Stalux loads the
|
|
241
246
|
[`@mcp-b/webmcp-polyfill`](https://www.npmjs.com/package/@mcp-b/webmcp-polyfill)
|
|
242
247
|
so agents still work; the polyfill becomes a no-op once native support lands.
|
|
243
248
|
|
|
@@ -265,6 +270,14 @@ jobTitle: Software Engineer # Optional; emitted as JSON-LD Person.jobTitle
|
|
|
265
270
|
This is a build-time metadata field. It does not create a contact endpoint or expose private information; only publish a role that is already intended to be public.
|
|
266
271
|
|
|
267
272
|
|
|
273
|
+
## Loading and navigation stability
|
|
274
|
+
|
|
275
|
+
The root reserves scrollbar space and uses explicit line height. The homepage typewriter reserves the maximum wrapped height of its complete messages and keeps the text origin fixed; clocks reserve line boxes and use tabular digits. Existing typing, cursor and route animations remain enabled. No additional configuration is required.
|
|
276
|
+
|
|
277
|
+
A wrapper around Astro ClientRouter's `astro:before-preparation` loader warms the destination body font before the DOM swap without installing it on the old page. It waits at most one second; failures, timeout and superseding navigation do not block the route. Initial loads on slow networks may still show the system fallback. Content authors must reserve dimensions for remote images and third-party embeds. Reduced-motion and print behavior remain supported.
|
|
278
|
+
|
|
279
|
+
See [style maintenance](docs/STYLING.md) and [lifecycle and static interfaces](docs/RUNTIME.md). Astro development documentation is available through the [official MCP endpoint](https://mcp.docs.astro.build/mcp); `.codex/config.toml`, `.mcp.json` and `AGENTS.md` record the project setup. This development MCP is separate from the blog's browser WebMCP tools.
|
|
280
|
+
|
|
268
281
|
## Development diagnostics
|
|
269
282
|
|
|
270
283
|
Stalux routes integration, content-loader and rendering diagnostics through Astro's official logger. Normal mode keeps summaries, warnings and errors. Verbose mode adds configuration/routes, component overrides, font cache, Markdown analysis/cache, content statistics, feeds, asset sync and Pagefind stages.
|
|
@@ -297,7 +310,7 @@ Keep the TypeScript 6 alias for `astro check`, upgrade Vitest and its coverage p
|
|
|
297
310
|
|
|
298
311
|
Stalux precompiles its UnoCSS styles; consuming projects need no UnoCSS dependency or configuration. Shared CSS is a cacheable external stylesheet by default. Article/math, archive, category, tag and quote styles load with their components. Explicit Astro `build.inlineStylesheets: "always"` restores inline output. New utilities in consumer override components require the consumer’s own compiler. See [STYLING.md](./docs/STYLING.md) for maintenance and watch mode.
|
|
299
312
|
|
|
300
|
-
Wide article pages use 280px sidebars within a layout up to
|
|
313
|
+
Wide article pages use 280px sidebars within a layout up to 1544px wide, with 16px outer padding. Sidebars collapse below 1600px. UnoCSS remains the style builder; prose and integration styles use standard CSS, and build checks reject Tailwind variables and uncompiled directives.
|
|
301
314
|
|
|
302
315
|
The friend links list is up to 1600px wide with 16px horizontal padding. Cards fill their grid columns, with one to five columns depending on the viewport.
|
|
303
316
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.35.
|
|
3
|
+
"version": "1.35.13",
|
|
4
4
|
"description": "A powerful, modern Astro blog theme — use as template or install as plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -117,6 +117,7 @@
|
|
|
117
117
|
"astro-seo": "^1.2.0",
|
|
118
118
|
"badge-maker": "^6.0.0",
|
|
119
119
|
"css-tree": "^3.2.1",
|
|
120
|
+
"image-size": "^2.0.4",
|
|
120
121
|
"magic-string": "^1.4.3",
|
|
121
122
|
"pagefind": "^1.5.2",
|
|
122
123
|
"parse5": "^8.0.1",
|
|
@@ -12,6 +12,11 @@ const texts = normalizeMessages(messages);
|
|
|
12
12
|
{texts.length > 0 && (
|
|
13
13
|
<div class={className} data-stalux-typewriter data-messages={JSON.stringify(texts)}>
|
|
14
14
|
<span class="stalux-typewriter-accessible">{texts.join(", ")}</span>
|
|
15
|
+
<span class="stalux-typewriter-reserve" aria-hidden="true">
|
|
16
|
+
{texts.map((text) => (
|
|
17
|
+
<span>{text}</span>
|
|
18
|
+
))}
|
|
19
|
+
</span>
|
|
15
20
|
<span class="stalux-typewriter-text" data-typewriter-text aria-hidden="true">
|
|
16
21
|
{texts[0]}
|
|
17
22
|
</span>
|
|
@@ -37,9 +42,34 @@ registerPageLifecycle("typewriter", mountTypewriters);
|
|
|
37
42
|
white-space: nowrap;
|
|
38
43
|
border: 0;
|
|
39
44
|
}
|
|
45
|
+
[data-stalux-typewriter] {
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-columns: minmax(0, 1fr);
|
|
48
|
+
line-height: 1.25;
|
|
49
|
+
}
|
|
50
|
+
.stalux-typewriter-reserve,
|
|
40
51
|
.stalux-typewriter-text {
|
|
52
|
+
grid-area: 1 / 1;
|
|
53
|
+
width: 100%;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
align-self: center;
|
|
41
56
|
overflow-wrap: anywhere;
|
|
42
57
|
}
|
|
58
|
+
.stalux-typewriter-text {
|
|
59
|
+
align-self: stretch;
|
|
60
|
+
}
|
|
61
|
+
.stalux-typewriter-reserve {
|
|
62
|
+
display: grid;
|
|
63
|
+
visibility: hidden;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
user-select: none;
|
|
66
|
+
}
|
|
67
|
+
.stalux-typewriter-reserve > span {
|
|
68
|
+
grid-area: 1 / 1;
|
|
69
|
+
}
|
|
70
|
+
.stalux-typewriter-reserve > span::after {
|
|
71
|
+
content: "\00a0";
|
|
72
|
+
}
|
|
43
73
|
.stalux-typewriter-text::after {
|
|
44
74
|
content: "";
|
|
45
75
|
display: inline-block;
|
|
@@ -71,7 +101,8 @@ registerPageLifecycle("typewriter", mountTypewriters);
|
|
|
71
101
|
}
|
|
72
102
|
}
|
|
73
103
|
@media print {
|
|
74
|
-
.stalux-typewriter-text
|
|
104
|
+
.stalux-typewriter-text,
|
|
105
|
+
.stalux-typewriter-reserve {
|
|
75
106
|
display: none;
|
|
76
107
|
}
|
|
77
108
|
.stalux-typewriter-accessible {
|
|
@@ -10,6 +10,7 @@ import { toMachineDateTime } from "@utils/semantic-time";
|
|
|
10
10
|
import ArticleSummary from "./ArticleSummary.astro";
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
|
+
postId?: string | number;
|
|
13
14
|
title: string;
|
|
14
15
|
description?: string;
|
|
15
16
|
transitionName?: string;
|
|
@@ -107,7 +108,12 @@ const mdSourceUrl = exportMd ? `${Astro.url.pathname.replace(/\/$/, "")}.md` : "
|
|
|
107
108
|
<ArticleSummary description={props.description} label={t("post.summary")} />
|
|
108
109
|
|
|
109
110
|
<!-- Article Body -->
|
|
110
|
-
<section
|
|
111
|
+
<section
|
|
112
|
+
class="stalux-post-body stalux-prose"
|
|
113
|
+
data-pagefind-body
|
|
114
|
+
data-stalux-post-id={props.postId === undefined ? undefined : String(props.postId)}
|
|
115
|
+
data-pagefind-filter={props.postId === undefined ? undefined : "post[data-stalux-post-id]"}
|
|
116
|
+
>
|
|
111
117
|
<slot />
|
|
112
118
|
</section>
|
|
113
119
|
</article>
|
package/src/index.ts
CHANGED
|
@@ -12,14 +12,7 @@ import { styleNames } from "./internal/style-names.generated.ts";
|
|
|
12
12
|
* 基于 Astro 7.1.3 Integration API(astro:config:setup / injectRoute / injectScript)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
copyFileSync,
|
|
17
|
-
existsSync,
|
|
18
|
-
promises as fs,
|
|
19
|
-
mkdirSync,
|
|
20
|
-
readdirSync,
|
|
21
|
-
readFileSync,
|
|
22
|
-
} from "node:fs";
|
|
15
|
+
import { copyFileSync, existsSync, promises as fs, readdirSync, readFileSync } from "node:fs";
|
|
23
16
|
import { createRequire } from "node:module";
|
|
24
17
|
import path from "node:path";
|
|
25
18
|
import { fileURLToPath } from "node:url";
|
|
@@ -40,13 +33,16 @@ import { staluxComponentsAlias } from "./internal/components-plugin";
|
|
|
40
33
|
import {
|
|
41
34
|
clearPageFontSubsets,
|
|
42
35
|
readLinkedStylesheetText,
|
|
36
|
+
removePageFontStyles,
|
|
43
37
|
resolveFontInputs,
|
|
44
|
-
|
|
38
|
+
writePageFontSubsetIntoDocumentAsync,
|
|
45
39
|
} from "./internal/font-slices";
|
|
46
40
|
import { findHtmlImages } from "./internal/html-images";
|
|
47
|
-
import {
|
|
41
|
+
import { createHtmlMutationEmitter } from "./internal/html-output";
|
|
42
|
+
import { type ImageDimensions, updateLocalImageDimensions } from "./internal/image-dimensions";
|
|
48
43
|
import { createInjectedRoutes } from "./internal/injected-routes";
|
|
49
44
|
import { createPageOutputCache } from "./internal/page-output-cache";
|
|
45
|
+
import { syncBackgroundSvgs, syncFontAssets } from "./internal/public-assets";
|
|
50
46
|
import { createRuntimeCacheKey } from "./internal/runtime-cache-key";
|
|
51
47
|
import {
|
|
52
48
|
appendUniquePlugin,
|
|
@@ -85,60 +81,6 @@ function buildFontsConfig(codeNormal: string, codeItalic?: string) {
|
|
|
85
81
|
];
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// 背景 SVG 判别式同步
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 把主题包的 public/background/*.svg 同步到用户项目 public/background/。
|
|
94
|
-
*
|
|
95
|
-
* 判别式(非无条件覆盖):
|
|
96
|
-
* - 同名文件:内容一致则跳过(幂等);内容不同才覆盖(主题更新了 SVG)
|
|
97
|
-
* - 用户新增的文件:保留,不删除(避免误删用户自定义背景)
|
|
98
|
-
* - 不做整目录 force 复制
|
|
99
|
-
*/
|
|
100
|
-
function syncBackgroundSvgs(srcDir: string): { copied: number; skipped: number } {
|
|
101
|
-
const srcBg = path.join(srcDir, "../public/background");
|
|
102
|
-
if (!existsSync(srcBg)) return { copied: 0, skipped: 0 };
|
|
103
|
-
|
|
104
|
-
const destBg = path.resolve(process.cwd(), "public", "background");
|
|
105
|
-
mkdirSync(destBg, { recursive: true });
|
|
106
|
-
|
|
107
|
-
let copied = 0;
|
|
108
|
-
let skipped = 0;
|
|
109
|
-
for (const name of readdirSync(srcBg)) {
|
|
110
|
-
if (!name.endsWith(".svg")) continue;
|
|
111
|
-
const srcFile = path.join(srcBg, name);
|
|
112
|
-
const destFile = path.join(destBg, name);
|
|
113
|
-
if (existsSync(destFile)) {
|
|
114
|
-
const srcBuf = readFileSync(srcFile);
|
|
115
|
-
const destBuf = readFileSync(destFile);
|
|
116
|
-
if (srcBuf.equals(destBuf)) {
|
|
117
|
-
skipped++; // 内容一致,跳过
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
copyFileSync(srcFile, destFile);
|
|
122
|
-
copied++;
|
|
123
|
-
}
|
|
124
|
-
return { copied, skipped };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Sync packaged math fonts and licensing notices; Astro does not merge integration public directories. */
|
|
128
|
-
function syncFontAssets(srcDir: string): void {
|
|
129
|
-
const source = path.join(srcDir, "../public/fonts");
|
|
130
|
-
if (!existsSync(source)) return;
|
|
131
|
-
const destination = path.resolve(process.cwd(), "public/fonts");
|
|
132
|
-
mkdirSync(destination, { recursive: true });
|
|
133
|
-
for (const name of readdirSync(source)) {
|
|
134
|
-
if (!/\.(?:woff2|txt)$/u.test(name)) continue;
|
|
135
|
-
const from = path.join(source, name);
|
|
136
|
-
const to = path.join(destination, name);
|
|
137
|
-
if (existsSync(to) && readFileSync(from).equals(readFileSync(to))) continue;
|
|
138
|
-
copyFileSync(from, to);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
84
|
// ---------------------------------------------------------------------------
|
|
143
85
|
// 集成入口
|
|
144
86
|
// ---------------------------------------------------------------------------
|
|
@@ -186,7 +128,6 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
186
128
|
}) => {
|
|
187
129
|
const started = performance.now();
|
|
188
130
|
const configLogger = logger.fork("stalux/config");
|
|
189
|
-
const assetsLogger = logger.fork("stalux/assets");
|
|
190
131
|
const routesLogger = logger.fork("stalux/routes");
|
|
191
132
|
const markdownLogger = logger.fork("stalux/markdown");
|
|
192
133
|
const fontsLogger = logger.fork("stalux/fonts");
|
|
@@ -198,6 +139,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
198
139
|
path.join(srcDir, "styles"),
|
|
199
140
|
JSON.stringify([styleNames, modernCssTargets]) +
|
|
200
141
|
readFileSync(path.join(srcDir, "internal/compact-styles.ts"), "utf8"),
|
|
142
|
+
path.resolve(fileURLToPath(config.root), opt.contentDir ?? "stalux", "config"),
|
|
201
143
|
);
|
|
202
144
|
|
|
203
145
|
// 1. 注入 Vite 别名 + 组件覆盖插件 + Vue 特性标记(Waline 依赖)
|
|
@@ -240,23 +182,6 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
240
182
|
injectScript("page-ssr", `import "${srcDir}/styles/generated.css";`);
|
|
241
183
|
injectScript("page", `import "${srcDir}/scripts/core-runtime.ts";`);
|
|
242
184
|
|
|
243
|
-
// 2.5 同步背景 SVG 到用户项目 public/background/
|
|
244
|
-
// 纯客户端引入:SVG 以静态 URL(/background/pattern-*.svg)被 background.ts 硬编码引用,
|
|
245
|
-
// 不经过 Astro 资源管线(import.meta.glob 会打上每次构建随机的 __ASTRO_ASSET_IMAGE__ 占位符,
|
|
246
|
-
// 破坏增量构建依赖图 hash)。Astro 不会合并集成的 public 目录,这里显式同步一次。
|
|
247
|
-
// 判别式:内容一致跳过(幂等),内容不同才覆盖,用户新增文件保留。
|
|
248
|
-
try {
|
|
249
|
-
syncFontAssets(srcDir);
|
|
250
|
-
const { copied, skipped } = syncBackgroundSvgs(srcDir);
|
|
251
|
-
if (copied > 0) {
|
|
252
|
-
assetsLogger.info(`同步背景 SVG ${copied} 个(跳过 ${skipped} 个不变)`);
|
|
253
|
-
} else if (skipped > 0) {
|
|
254
|
-
assetsLogger.debug(`背景 SVG 无变化(跳过 ${skipped} 个)`);
|
|
255
|
-
}
|
|
256
|
-
} catch (error) {
|
|
257
|
-
assetsLogger.warn(`同步背景 SVG 失败: ${describeError(error)}`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
185
|
// 3. 注入所有页面路由(仅插件模式,源码模式下使用文件路由)
|
|
261
186
|
if (isPluginMode) {
|
|
262
187
|
const routes = createInjectedRoutes(new URL(".", import.meta.url));
|
|
@@ -393,6 +318,32 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
393
318
|
configLogger.info(`initialized in ${(performance.now() - started).toFixed(1)}ms`);
|
|
394
319
|
},
|
|
395
320
|
|
|
321
|
+
// Read final paths after every integration has finished updating configuration.
|
|
322
|
+
// Public assets must exist before Astro creates its Vite server/build asset pipeline.
|
|
323
|
+
"astro:config:done": ({ config, logger }) => {
|
|
324
|
+
const srcDir = fileURLToPath(new URL(".", import.meta.url));
|
|
325
|
+
const publicDir = fileURLToPath(config.publicDir);
|
|
326
|
+
const assetsLogger = logger.fork("stalux/assets");
|
|
327
|
+
if (pageFontData) {
|
|
328
|
+
pageFontData.outputDir = path.join(
|
|
329
|
+
fileURLToPath(config.outDir),
|
|
330
|
+
"_astro",
|
|
331
|
+
"fonts",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
syncFontAssets(srcDir, publicDir);
|
|
336
|
+
const { copied, skipped } = syncBackgroundSvgs(srcDir, publicDir);
|
|
337
|
+
if (copied > 0) {
|
|
338
|
+
assetsLogger.info(`同步背景 SVG ${copied} 个(跳过 ${skipped} 个不变)`);
|
|
339
|
+
} else if (skipped > 0) {
|
|
340
|
+
assetsLogger.debug(`背景 SVG 无变化(跳过 ${skipped} 个)`);
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
assetsLogger.warn(`同步主题静态资源失败: ${describeError(error)}`);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
|
|
396
347
|
"astro:build:done": async ({ dir, logger: rootLogger }) => {
|
|
397
348
|
const logger = rootLogger.fork("stalux/build");
|
|
398
349
|
const started = performance.now();
|
|
@@ -406,6 +357,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
406
357
|
const sourceDir = fileURLToPath(new URL(".", import.meta.url));
|
|
407
358
|
const resolveModule = createRequire(import.meta.url).resolve;
|
|
408
359
|
const parserEntry = resolveModule("parse5");
|
|
360
|
+
const imageSizeEntry = resolveModule("image-size");
|
|
409
361
|
const nativeEntry = resolveModule("@xingwangzhe/cjk-font-split-native");
|
|
410
362
|
const outputCache = await createPageOutputCache(
|
|
411
363
|
path.resolve("node_modules/.astro/stalux-page-output"),
|
|
@@ -415,6 +367,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
415
367
|
"internal/font-slices.ts",
|
|
416
368
|
"internal/image-dimensions.ts",
|
|
417
369
|
"internal/html-images.ts",
|
|
370
|
+
"internal/html-output.ts",
|
|
418
371
|
"internal/page-output-cache.ts",
|
|
419
372
|
"internal/style-names.generated.ts",
|
|
420
373
|
"plugins/feature-flags.ts",
|
|
@@ -422,11 +375,11 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
422
375
|
].map((entry) => path.join(sourceDir, entry)),
|
|
423
376
|
path.join(path.dirname(nativeEntry), "package.json"),
|
|
424
377
|
path.resolve(path.dirname(parserEntry), "../package.json"),
|
|
378
|
+
path.resolve(path.dirname(imageSizeEntry), "../../package.json"),
|
|
425
379
|
],
|
|
426
380
|
pageFontData?.fontBuffer,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
.map((entry) => path.join(outDir, entry)),
|
|
381
|
+
// Track referenced files regardless of extension, including SVG/AVIF.
|
|
382
|
+
htmlFiles.map((entry) => path.join(outDir, entry)),
|
|
430
383
|
);
|
|
431
384
|
let cacheHits = 0;
|
|
432
385
|
let optimizedImagePages = 0;
|
|
@@ -440,11 +393,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
440
393
|
const processPage = async (entry: string) => {
|
|
441
394
|
const output = path.join(outDir, entry);
|
|
442
395
|
const before = await fs.readFile(output, "utf8");
|
|
443
|
-
const
|
|
444
|
-
/<style data-stalux-page-font>[\s\S]*?<\/style>/gi,
|
|
445
|
-
"",
|
|
446
|
-
);
|
|
447
|
-
const cached = await outputCache.get(entry, withoutPageFont);
|
|
396
|
+
const cached = await outputCache.get(entry, before);
|
|
448
397
|
if (cached) {
|
|
449
398
|
if (cached.font && pageFontData) {
|
|
450
399
|
if (!referencedPageFonts.has(cached.font.filename)) {
|
|
@@ -465,34 +414,32 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
465
414
|
}
|
|
466
415
|
let cachedFont: { filename: string; sourcePath: string } | undefined;
|
|
467
416
|
const dependencies = new Set<string>();
|
|
468
|
-
const document = parse(
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
417
|
+
const document = parse(before, { sourceCodeLocationInfo: true });
|
|
418
|
+
const emitHtml = createHtmlMutationEmitter(before, document);
|
|
419
|
+
removePageFontStyles(document);
|
|
420
|
+
const images = findHtmlImages(before, document);
|
|
421
|
+
await updateLocalImageDimensions(images, outDir, {
|
|
472
422
|
cache: dimensionsCache,
|
|
473
423
|
loadingPolicy: true,
|
|
474
424
|
dependencies,
|
|
475
425
|
});
|
|
476
426
|
if (pageFontData) {
|
|
477
427
|
const linkedStylesheetText = readLinkedStylesheetText(
|
|
478
|
-
|
|
428
|
+
"",
|
|
479
429
|
output,
|
|
480
430
|
outDir,
|
|
481
431
|
linkedCssTextCache,
|
|
482
432
|
document,
|
|
483
433
|
dependencies,
|
|
484
434
|
);
|
|
485
|
-
const pageFont = await
|
|
486
|
-
|
|
435
|
+
const pageFont = await writePageFontSubsetIntoDocumentAsync(
|
|
436
|
+
document,
|
|
487
437
|
pageFontData.fontBuffer,
|
|
488
438
|
pageFontData.cacheDir,
|
|
489
439
|
"/_astro/fonts/",
|
|
490
440
|
linkedStylesheetText,
|
|
491
|
-
"",
|
|
492
|
-
document,
|
|
493
441
|
);
|
|
494
442
|
if (pageFont) {
|
|
495
|
-
after = pageFont.html;
|
|
496
443
|
pageFontPages++;
|
|
497
444
|
cachedFont = {
|
|
498
445
|
filename: pageFont.filename,
|
|
@@ -507,13 +454,14 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
507
454
|
referencedPageFonts.add(pageFont.filename);
|
|
508
455
|
}
|
|
509
456
|
}
|
|
457
|
+
const after = emitHtml();
|
|
510
458
|
if (after !== before) {
|
|
511
459
|
await fs.writeFile(output, after);
|
|
512
460
|
optimizedImagePages++;
|
|
513
461
|
}
|
|
514
462
|
await outputCache.set(
|
|
515
463
|
entry,
|
|
516
|
-
|
|
464
|
+
before,
|
|
517
465
|
{
|
|
518
466
|
html: after,
|
|
519
467
|
font: cachedFont,
|