辅助脚本与站点配置读取
Mizuki 主题仓库在 scripts/ 目录下维护了一组由 node 直接运行的构建辅助脚本(字体子集化、图片转换、内容同步、IndexNow 推送等)。由于这些脚本运行在构建链路而非 Astro 运行时中,无法 import TypeScript 配置模块,因此仓库提供了两个专门的辅助模块:scripts/read-site-config.mjs(以正则方式读取 siteConfig.ts 的顶层配置块)与 scripts/load-env.js(手工解析 .env 文件并注入 process.env)。本文页覆盖这两个辅助模块的完整实现机制,以及它们在构建工具链中的定位。
Purpose and Scope
本页覆盖的内容:
scripts/read-site-config.mjs:站点配置取值助手,包含extractBlock、matchInBlock、matchSiteConfig三个导出函数及其「覆盖 → 默认」两级回退逻辑。scripts/load-env.js:.env文件加载器,为纯 Node 脚本提供import.meta.env的等价能力。- 上述两个模块被构建脚本消费的方式,以及为什么采用正则/手工解析而非真正 import 配置文件。
本页不覆盖、留给兄弟页面的内容:
scripts/下具体业务脚本(字体子集化流水线scripts/compress-fonts/*、图片转换scripts/convert-images.js、内容同步scripts/sync-content.js等)各自的实现细节属于独立的脚本页。src/config/siteConfig.ts本身的配置项语义,属于站点配置页。- Astro 运行时读取配置的方式(Vite/Astro 自身的加载链路),不在本页范围内。
Overview
scripts/ 目录中的脚本(如 prepare-fonts.mjs、update-anime.mjs、indexnow-submit.js、sync-content.js 等)在 pnpm 脚本阶段由 node 直接执行。这一执行环境有两个天然限制:
- Node 原生不能直接 import TypeScript 文件——
src/config/siteConfig.ts及其覆盖文件src/config/overrides/siteConfig.ts都是 TS 模块,纯 Node 脚本无法复用。 - 没有 Vite 的 env 注入——Astro 组件里常见的
import.meta.env.PUBLIC_XXX在纯 Node 上下文不可用,需要自己解析.env。
为此仓库提供了两个轻量辅助模块,均为零依赖、单文件实现,避免给构建链引入额外的转译或 dotenv 依赖:
| 模块 | 职责 | 读取对象 |
|---|---|---|
scripts/read-site-config.mjs | 从 TS 配置文本中按「块」提取字段值 | src/config/overrides/siteConfig.ts → src/config/siteConfig.ts |
scripts/load-env.js | 解析 .env 并写入 process.env | 仓库根目录 .env |
核心使用场景:
- 字体子集化:需要读取
font配置块(字体模式、字体列表等)决定子集化策略。 - 番剧页更新:需要读取
anime配置块获得站点域名等信息。 - IndexNow 提交:需要
.env中的INDEXNOW_KEY等密钥。 - 内容同步 / 初始化:需要站点基础配置确定内容仓库映射。
Architecture
下图展示两个辅助模块在构建工具链中的位置与数据流向:
设计意图解读:
- 为什么是正则而不是 import:脚本由
node直接运行,Node 原生不解析 TS。与其为几个脚本引入 esbuild/tsx 转译依赖,不如沿用已有的正则读取方式,保持构建链零额外开销。 - 为什么有覆盖优先级:
src/config/overrides/siteConfig.ts是sync-content从内容仓库同步而来的部分覆盖配置(键序任意、字段不全),必须与上游默认值src/config/siteConfig.ts组合成「覆盖优先、缺失回退」的读取链。 - 为什么 loadEnv 不用 dotenv:
.env语法极简(KEY=VALUE+ 注释 + 可选引号),手工解析约 20 行即可完成,避免运行时依赖。
Main Content:read-site-config.mjs 实现机制
模块加载与配置源定位
模块在导入时即确定两个配置源路径,优先级为覆盖文件在前、默认文件在后:
1const rootDir = path.resolve(
2 path.dirname(fileURLToPath(import.meta.url)),
3 "..",
4);
5
6const SOURCE_PATHS = [
7 path.join(rootDir, "src/config/overrides/siteConfig.ts"),
8 path.join(rootDir, "src/config/siteConfig.ts"),
9];Source: read-site-config.mjs
rootDir 通过 import.meta.url 反推仓库根目录(脚本位于 scripts/ 下,向上退一级),因此脚本无论从哪个工作目录被调用,配置路径都稳定指向仓库内的真实文件。
源文件读取与模块级缓存
1let cachedSources = null;
2
3function readSources() {
4 if (!cachedSources) {
5 cachedSources = SOURCE_PATHS.filter((source) => fs.existsSync(source)).map(
6 (source) => fs.readFileSync(source, "utf-8"),
7 );
8 }
9 return cachedSources;
10}Source: read-site-config.mjs
三个值得注意的细节:
- 存在性过滤:覆盖文件是可选的(由
sync-content按需生成),fs.existsSync过滤后,缓存数组里只剩实际存在的文件。 - 一次性缓存:
cachedSources在首次读取后被固化。脚本生命周期内配置不会变化,缓存既省 I/O,也保证多次取值的一致性。 - 顺序即优先级:数组顺序「覆盖 → 默认」直接构成后续查找的优先顺序。
extractBlock:花括号配平截取配置块
这是模块的核心算法。文件头注释解释了为什么需要它——覆盖文件是部分配置且键序任意,如果沿用旧版「块名后面第一个字段」的松散匹配,anime: {}(空块)后面紧跟的 font: { mode: ... } 会被误读成番剧模式。因此必须用花括号配平把搜索范围钉死在目标块内:
1export function extractBlock(content, blockKey) {
2 const opener = content.match(new RegExp(`\\b${blockKey}\\s*:\\s*\\{`));
3 if (!opener) {
4 return null;
5 }
6
7 let index = opener.index + opener[0].length;
8 let depth = 1;
9 const start = index;
10
11 while (index < content.length && depth > 0) {
12 const char = content[index];
13 if (char === "{") depth++;
14 else if (char === "}") depth--;
15 index++;
16 }
17
18 return depth === 0 ? content.slice(start, index - 1) : null;
19}Source: read-site-config.mjs
执行过程分两步:
- 定位块头:正则
\b${blockKey}\s*:\s*\{匹配font: {这样的顶层键声明。\b词边界防止myfont:误命中font:。 - 配平扫描:从
{之后开始逐字符扫描,遇到{深度加一、遇到}深度减一,深度归零时结束。返回值是块内文本(不含首尾花括号)。
若扫描到文件末尾深度仍大于 0(配置文件花括号不闭合),返回 null 而非截断文本——宁可读不到,也不返回可能越界的错误内容。
matchInBlock:多源顺序查找与字段回退
1export function matchInBlock(sources, blockKey, pattern) {
2 for (const content of sources) {
3 const block = extractBlock(content, blockKey);
4 if (block === null) {
5 continue;
6 }
7 const match = block.match(pattern);
8 if (match) {
9 return match[1];
10 }
11 }
12 return null;
13}Source: read-site-config.mjs
这里有一个容易被忽略但注释里明确说明的关键行为:当覆盖文件中存在该块、但没写这个字段时,继续往后找默认文件,而不是就地返回空值。这就是「未覆盖字段回退到默认配置」的实现——循环不因「块存在」而终止,只因「字段命中」而终止。match[1] 表明调用方传入的 pattern 必须包含一个捕获组。
matchSiteConfig:面向调用方的门面
export function matchSiteConfig(blockKey, pattern) {
return matchInBlock(readSources(), blockKey, pattern);
}Source: read-site-config.mjs
matchSiteConfig 是唯一被业务脚本使用的入口(另两个函数导出用于测试或更细粒度的控制),它把源加载 + 缓存 + 顺序匹配封装成一次调用。取值一律限定在指定的顶层配置块内,避免跨块串读。
Main Content:load-env.js 实现机制
load-env.js 解决的是另一个问题:纯 Node 脚本没有 Vite 的 import.meta.env 注入,需要自己把 .env 读进 process.env:
1const __filename = fileURLToPath(import.meta.url);
2const __dirname = path.dirname(__filename);
3const rootDir = path.resolve(__dirname, "..");
4
5// 加载 .env 文件
6export function loadEnv() {
7 const envPath = path.join(rootDir, ".env");
8 if (fs.existsSync(envPath)) {
9 const envContent = fs.readFileSync(envPath, "utf-8");
10 envContent.split("\n").forEach((line) => {
11 const line_ = line.trim();
12 // 跳过注释和空行
13 if (!line_ || line_.startsWith("#")) return;
14
15 const match = line_.match(/^([^=]+)=(.*)$/);
16 if (match) {
17 const key = match[1].trim();
18 let value = match[2].trim();
19 // 移除引号
20 value = value.replace(/^["']|["']$/g, "");
21 process.env[key] = value;
22 }
23 });
24 }
25}Source: load-env.js
实现要点:
- 路径定位:与
read-site-config.mjs相同的import.meta.url→ 根目录反推手法,.env固定读取仓库根目录。 - 行级解析:按
\n拆行,trim后跳过空行与#注释行。 - 键值正则:
^([^=]+)=(.*)$要求行内至少含一个=;key 与 value 各自trim。 - 引号剥离:
^["']|["']$去掉首尾成对的单/双引号,兼容KEY="value"与KEY='value'写法。 - 直接覆写
process.env:后续脚本通过process.env.INDEXNOW_KEY等直接取值,与 Node 生态惯例一致。 - 无导出前缀无副作用:只有显式调用
loadEnv()才发生读取,模块导入本身零开销。
Core Flow:一次配置读取的完整时序
以字体准备脚本读取 font 块中某字段为例:
整个链路的设计取舍是:用一次全文件读入 + 模块级缓存换取后续取值的零 I/O,同时通过「块存在但字段缺失继续找」的循环语义,让部分覆盖配置无需补全所有字段即可生效。
Usage Examples
读取站点配置块内字段(典型调用形态)
调用方传入块名与带捕获组的正则,即可按「覆盖 → 默认」顺序取值。以下形态与 font / anime 块的实际结构对应:
1import { matchSiteConfig } from "./read-site-config.mjs";
2
3// 读取 font 块内的 mode 字段(覆盖文件未写时回退默认文件)
4const fontMode = matchSiteConfig("font", /mode:\s*"([^"]+)"/);
5
6// 读取 anime 块内嵌套字段的判断
7const animeMode = matchSiteConfig("anime", /mode:\s*"([^"]+)"/);以上调用形态依据 read-site-config.mjs 的函数签名与文件头注释描述的
anime/font块使用场景整理;scripts/下各业务脚本对这些函数的具体消费代码见各脚本自身。
在脚本中加载 .env 密钥
1import { loadEnv } from "./load-env.js";
2
3loadEnv();
4// 此后可直接读取 process.env 中的键值
5const indexNowKey = process.env.INDEXNOW_KEY;Source: load-env.js
(示例中的键名仅为 process.env 取值方式演示,实际可用键以仓库 .env 内容为准。)
API Reference
read-site-config.mjs
extractBlock(content: string, blockKey: string): string | null
截取 blockKey: { ... } 的块内文本(不含首尾花括号)。
- 参数
content(string):完整配置文件文本;blockKey(string):顶层块名,用于构造\b${blockKey}\s*:\s*\{匹配。 - 返回:块内文本;块不存在或花括号未配平(文件末尾深度仍 > 0)时返回
null。 - 设计要点:以字符级配平代替松散的后向匹配,防止键序任意的部分覆盖配置造成跨块误读(如
anime: {}后的font块被读成番剧配置)。
matchInBlock(sources: string[], blockKey: string, pattern: RegExp): string | null
在若干份配置文本里按顺序查找 blockKey 块内的字段。
- 参数:
sources(string[]) 按优先级排列的文本数组;blockKey(string);pattern(RegExp) 必须含第 1 捕获组。 - 返回:第一个命中的捕获组;块存在但字段缺失时继续下一份源;全部未命中返回
null。
matchSiteConfig(blockKey: string, pattern: RegExp): string | null
按「覆盖 → 默认」顺序读取 siteConfig 某个块内的字段(业务脚本的主要入口)。
- 参数:
blockKey(string) 顶层配置块名;pattern(RegExp) 含第 1 捕获组的字段正则。 - 返回:命中字段值或
null。 - 副作用:首次调用触发配置文件读取并写入模块级缓存,之后调用零 I/O。
load-env.js
loadEnv(): void
解析仓库根目录 .env 并把键值写入 process.env。
- 参数:无。
- 返回:无。
.env不存在时静默返回。 - 行为:跳过空行与
#注释行;按^([^=]+)=(.*)$提取键值并各自trim;剥离首尾成对单/双引号;直接覆写同名process.env键。
Failure Modes, Edge Cases & Concurrency
| 场景 | 行为 | 说明 |
|---|---|---|
| 覆盖文件不存在 | SOURCE_PATHS 过滤后只剩默认文件 | 覆盖文件由 sync-content 按需生成,缺失是正常状态 |
| 块在覆盖中存在但字段未写 | 跳过该源继续找默认文件 | 「未覆盖字段回退默认」的核心语义,见 matchInBlock 循环 |
| 配置文件花括号不闭合 | extractBlock 返回 null | 不返回截断文本,避免越界误读 |
blockKey 是其他键的子串 | \b 词边界保护 | myfont: 不会命中 font: 的匹配 |
| 同名块出现多次 | 命中第一个 | String.match 只取首个匹配 |
.env 文件缺失 | loadEnv 静默返回 | 不抛错,脚本继续以现有 env 运行 |
.env 值含成对引号 | 首尾引号被剥离 | `^["'] |
行内无 = | 整行被忽略 | 正则要求至少一个 = |
重复调用 loadEnv | 每次重读文件并覆写 | 无缓存;但 readSources 有缓存,二者行为不同 |
多次调用 matchSiteConfig | 复用首次读取结果 | 模块级缓存;若脚本运行中配置文件被外部修改,不会感知 |
并发说明:这些模块只在单进程 Node 脚本内使用,无并发场景;缓存是在模块作用域上的单例,跨脚本进程不共享(每个 node 进程各自加载)。
Performance & Operational Notes / Extension Points
- 性能:
extractBlock是 O(块长度) 的字符扫描,且每次matchInBlock对每份源都会重新截块;由于配置文件通常只有几百行、取值次数有限,加上readSources缓存避免了重复磁盘 I/O,实际开销可忽略。 - 可维护性约束:正则读取强依赖
siteConfig.ts的文本形态(顶层块 + 字面量值)。若配置改用复杂表达式(函数、模板字符串、条件值),正则会静默读不到并返回null——这是该方案的固有边界。 - 扩展点 1(新增配置块):直接用新的
blockKey调用matchSiteConfig即可,无需改动辅助模块;只要目标块保持在顶层花括号结构内。 - 扩展点 2(新增环境变量):在
.env写入KEY=VALUE后调用loadEnv(),再从process.env.KEY取值;无需修改load-env.js。 - 扩展点 3(更多配置源):向
SOURCE_PATHS追加路径即可扩大回退链,顺序即优先级。 - 若未来需要真正 import TS 配置:需要引入 esbuild/tsx 转译或改由 Astro 集成在构建期导出 JSON,这会改变当前「零依赖纯 Node」的定位,属于构建链结构调整。
Related Links
- read-site-config.mjs — 站点配置取值助手源文件
- load-env.js —
.env加载器源文件 - sync-content.js — 生成
src/config/overrides/siteConfig.ts覆盖文件的上游脚本 - prepare-fonts.mjs、update-anime.mjs、indexnow-submit.js — 消费这两个辅助模块的典型业务脚本
src/config/siteConfig.ts/src/config/overrides/siteConfig.ts— 被读取的两级配置源(站点配置语义详见对应配置页)