@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.
- package/.release/cli.mjs +16 -20
- package/package.json +48 -45
- package/src/components/stalux/footer/FooterBadges.astro +2 -2
- package/src/components/stalux/layout/head.astro +1 -1
- package/src/components/stalux/posts/postContent.astro +1 -1
- package/src/index.ts +57 -51
- package/src/internal/cjk-search-index.ts +4 -7
- package/src/internal/components-plugin.ts +1 -3
- package/src/internal/font-slices.ts +36 -29
- package/src/internal/html-images.ts +1 -1
- package/src/internal/html-styles.ts +1 -1
- package/src/internal/image-dimensions.ts +3 -8
- package/src/internal/injected-routes.ts +1 -3
- package/src/internal/page-output-cache.ts +11 -15
- package/src/internal/post-cover.ts +22 -26
- package/src/internal/project-mode.ts +3 -2
- package/src/internal/public-assets.ts +42 -23
- package/src/internal/runtime-cache-key.ts +42 -27
- package/src/internal/satteri-config.ts +2 -2
- package/src/internal/search-style.ts +6 -7
- package/src/internal/social-card-cover.ts +12 -10
- package/src/internal/social-card-renderer.ts +8 -11
- package/src/internal/webmcp-mutations.ts +11 -5
- package/src/layouts/Stalux.astro +4 -4
- package/src/pages/openapi.json.ts +1 -1
- package/src/plugins/feature-flags.ts +2 -2
- package/src/schemas/collections.ts +1 -1
- package/src/schemas/config.ts +4 -1
- package/src/scripts/list-reveal.ts +7 -5
- package/src/scripts/swup-router.ts +1 -1
- package/src/scripts/weather.ts +2 -2
- package/src/scripts/webmcp.ts +4 -4
- package/src/utils/accent-color.ts +1 -1
- package/src/utils/ai-discovery.ts +6 -6
- package/src/utils/badge-generator.ts +1 -2
- package/src/utils/cc.ts +2 -2
- package/src/utils/dayjs.ts +5 -5
- package/src/utils/diagnostics.ts +3 -3
- package/src/utils/public-routes.ts +3 -3
- package/src/utils/seo-document.ts +2 -2
- package/src/utils/site-path.ts +1 -1
- 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
|
-
|
|
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 =
|
|
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*)?(
|
|
170
|
+
return [...css.matchAll(/\bcontent\s*:\s*(["'])(.*?)\1\s*(?:!important\s*)?(?:;|\})/gisu)]
|
|
172
171
|
.map(([, , value]) =>
|
|
173
|
-
(value ?? "").replace(/\\([\da-f]{1,6})\s?|\\(.)/
|
|
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(
|
|
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-[^"]+"[^}]*\}/
|
|
478
|
+
/@font-face\{[^}]*font-family:"LXGW WenKai-[^"]+"[^}]*\}/gu,
|
|
480
479
|
(face) => {
|
|
481
|
-
const unicodeRange = face.match(/unicode-range:([^;}]+)/
|
|
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]+)/
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
63
|
-
await file.
|
|
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.
|
|
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 |
|
|
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 =
|
|
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
|
|
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 =
|
|
38
|
-
for await (const chunk of
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
const names = listFiles(source, ["*.woff2", "*.txt"]);
|
|
50
71
|
const destination = path.join(publicDir, "fonts");
|
|
51
|
-
|
|
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 (
|
|
57
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return [
|
|
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
|
-
|
|
41
|
-
...
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
(
|
|
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
|
-
|
|
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",
|
|
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:
|
|
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 =
|
|
11
|
-
|
|
12
|
-
.
|
|
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(
|
|
41
|
+
return `export default ${JSON.stringify(`${base.replace(/\/$/u, "")}/@fs${stylesheet}`)};`;
|
|
43
42
|
},
|
|
44
43
|
};
|
|
45
44
|
}
|