代码生成与格式化流水线
SpinningMomo 仓库中的代码生成与格式化流水线由三部分组成:Node 驱动的代码生成脚本(迁移代码、内嵌本地化、地图注入代码)、pnpm 顶层构建/发布脚本链(C++ / Web / Android / 安装包),以及基于 husky + lint-staged 的提交前格式化钩子(C++ 与 Web 各自的格式化入口)。三者共同保证「源文件变更 → 重新生成派生产物 → 提交时自动格式化 → 构建发布」这条链路可重复执行。
Purpose and Scope(目的与范围)
本页面覆盖以下内容:
- 三个代码生成脚本的职责、触发条件与源/目标文件关系:
scripts/generate-migrations.js、scripts/generate-embedded-locales.js、scripts/generate-map-injection-cpp.js。 - 仓库根
package.json中的顶层构建、发布、格式化脚本及其相互调用关系(build→build:cpp/build:web/build:android/build:dist,release→ 完整发布链)。 - 提交前格式化机制:husky + lint-staged 如何在 commit 时对 C++ 源码与 Web 前端源码分别调用格式化脚本。
- AGENTS.md 中与生成/格式化强相关的工程约定:C++ 头文件包含顺序、命名规范、注释规范——这些正是格式化与生成产物需要遵守的约束。
有意留给兄弟页面的内容(本页不展开):
- C++ 后端的具体 xmake 构建/链接细节与
tasks/自定义任务 → 见「构建与发布」相关页面。 - Android 捕获守护进程(momo-capture)的源码架构 → 见
android/capture/README.md所在的 Android 页面。 - 安装包(MSI / WiX bundle)打包细节 → 见安装器相关页面。
- 代码生成脚本读取的领域内容本身(数据库迁移 SQL、i18n 词条、地图注入 JS 的业务逻辑)→ 见各自的功能页面。
Overview(概述)
SpinningMomo 是一个 Win32 C++ 后端 + Vue 3 Web 前端的双进程应用,另含 Android 捕获守护进程。这个多语言组合带来了一个典型问题:部分 C++/编译期产物并非手写,而是由 Node 脚本从源头文件生成的。如果开发者修改了源文件却忘记重新生成,就会出现「运行时数据与源码不一致」的隐蔽 bug。因此 AGENTS.md 明确列出必须重新运行的生成脚本清单。
仓库通过以下命令组织整个流水线:
1# C++ backend — debug
2xmake build
3
4# C++ backend — release
5xmake release
6
7# Web frontend
8pnpm run build:web
9
10# Android capture service
11pnpm run build:androidSource: AGENTS.md
格式化方面,仓库采用「手动全量格式化命令 + 提交时增量格式化」的双层策略:根目录提供 format:cpp 与 format:web 两个脚本入口,同时通过 husky 挂载的 lint-staged 在每次 git commit 时对本次改动的文件按语言分别格式化。
Architecture(架构)
图中各层职责说明:
- sg_Sources → sg_Codegen → sg_Generated:单向数据流。三个生成脚本分别消费 SQL 迁移、本地化 JSON、地图注入 JS 源文件,产出供 C++ 编译使用的生成物。
generate-map-injection-cpp.js会同时「重新生成压缩后的 JS 和对应的 C++ 头文件」。 - sg_Build:
pnpm run build是一个聚合脚本,按build:cpp → build:web → build:android → build:dist顺序串联;build:cpp内部先以 release 模式配置再切回 debug 模式。 - sg_Format:husky 提供 git 钩子基础设施,lint-staged 负责「只格式化本次暂存区命中的文件」,分别路由到 C++ 格式化脚本与 Web 格式化脚本。
- sg_Release:
pnpm run release在build基础上叠加便携版、安装包与校验和生成。
构建脚本链的依赖关系
构建脚本链定义在仓库根 package.json 中:
1{
2 "scripts": {
3 "prepare": "husky",
4 "build": "pnpm run build:cpp && pnpm run build:web && pnpm run build:android && pnpm run build:dist",
5 "build:cpp": "xmake config -m release && xmake build && xmake config -m debug",
6 "build:android": "node scripts/build-android.js",
7 "build:web": "pnpm --filter web run build",
8 "build:dist": "node scripts/prepare-dist.js",
9 "build:portable": "node scripts/build-portable.js",
10 "build:installer": "node scripts/build-installer.js",
11 "build:checksums": "node scripts/generate-checksums.js",
12 "release": "pnpm run build && pnpm run build:portable && pnpm run build:installer && pnpm run build:checksums",
13 "release:version": "node scripts/release-version.js",
14 "format:cpp": "node scripts/format-cpp.js",
15 "format:web": "pnpm --filter web exec prettier --write ."
16 }
17}Source: package.json
要点:
build使用&&串联而非并行,保证 C++ 后端先行编译——Web 产物与 dist 准备在其后进行,任一环节失败即中止整链。build:cpp在一条命令内完成「release 编译 + 恢复 debug 配置」,避免开发者在 release 构建后忘记切回,导致后续调试构建模式错乱。这是一个有意的状态复原设计。format:web通过pnpm --filter web exec在web/子包上下文中执行 prettier,而不是在仓库根执行——格式化规则归属前端工作区,与根目录的 C++ 格式化互不干扰。prepare脚本在pnpm install时安装 husky,是格式化钩子生效的前提。
代码生成脚本
AGENTS.md 中「Code Generation Scripts」一节是本流水线的核心约束:必须在其源文件变更后重新运行对应脚本。
These must be re-run when their source files change:
node scripts/generate-migrations.js— after modifyingsrc/migrations/*.sqlnode scripts/generate-embedded-locales.js— after modifyingsrc/locales/*.json(zh-CN / en-US)node scripts/generate-map-injection-cpp.js— after modifyingweb/src/features/map/injection/source/*.js(regenerates minified JS and its C++ header)
| 脚本 | 触发源文件 | 生成产物 | 说明 |
|---|---|---|---|
scripts/generate-migrations.js | src/migrations/*.sql | 自动生成的数据库迁移代码 | AGENTS.md 提到数据库迁移系统为「auto-generated migration system」 |
scripts/generate-embedded-locales.js | src/locales/*.json | 内嵌本地化(zh-CN / en-US)数据 | 将 JSON 词条编译进 C++ 侧 |
scripts/generate-map-injection-cpp.js | web/src/features/map/injection/source/*.js | 压缩(minified) JS + 对应 C++ 头文件 | 前端注入脚本被嵌入后端 |
Source: AGENTS.md
为什么需要「源变更即重新生成」
SpinningMomo 的数据库层使用「SQLite + SQLiteCpp + 线程本地连接 + DataMapper(ORM 风格行映射)+ 自动生成迁移系统」的组合;本地化数据与地图注入脚本同样被嵌入 C++ 编译产物。这些派生文件一旦与源文件脱节,编译仍会成功,但运行时行为(数据库 schema、界面语言、地图注入逻辑)与仓库中手写的源不一致,属于难以察觉的一致性缺陷。因此流水线把「重新生成」定义为开发者的硬性义务,而不是构建系统自动完成的步骤——AGENTS.md 同时声明了 Build Policy:不要自动运行构建,由用户确认或手动执行,这与生成脚本的显式触发策略保持一致。
生成脚本在构建中的位置
三个生成脚本不在 pnpm run build 聚合链中,它们是构建前的前置步骤。build:cpp(xmake)编译时消费其产物:
提交前格式化(husky + lint-staged)
仓库使用 husky 承载 git 钩子,lint-staged 决定「哪些暂存文件交给哪个格式化器」:
1{
2 "devDependencies": {
3 "esbuild": "^0.28.2",
4 "fast-glob": "^3.3.2",
5 "husky": "^9.1.7",
6 "lint-staged": "^16.2.4"
7 },
8 "lint-staged": {
9 "src/**/*.{cpp,h,hpp}": [
10 "node scripts/format-cpp.js --files"
11 ],
12 "web/**/*.{js,ts,vue,json,css,md}": [
13 "node scripts/format-web.js"
14 ]
15 }
16}Source: package.json
两条格式化路由
| 暂存文件模式 | 格式化命令 | 说明 |
|---|---|---|
src/**/*.{cpp,h,hpp} | node scripts/format-cpp.js --files | C++ 后端源码;--files 表示以传入文件列表模式运行(与全量 format:cpp 区分) |
web/**/*.{js,ts,vue,json,css,md} | node scripts/format-web.js | Web 前端源码,包装脚本在 web 工作区上下文执行 prettier |
设计意图:
- 按语言分治:C++ 与 Web(TS/Vue/JSON/CSS/MD)使用完全不同的工具链,lint-staged 的 glob 把每类文件路由到各自的格式化器,避免在错误的工具上浪费时间或产生不兼容的格式改写。
- 增量而非全量:只格式化本次提交涉及的文件,保证提交速度快,也避免无关文件被无关紧要的格式变更污染 diff。
--files双模式:scripts/format-cpp.js同时是全量入口(pnpm run format:cpp,不带参数时全仓库扫描)与增量入口(lint-staged 传入文件列表并附带--files标志)。这是同一个脚本服务两种调用方的典型设计。esbuild+fast-glob作为 devDependencies 出现在根目录,与 Node 脚本族的实现高度相关:fast-glob用于快速枚举待处理源文件,esbuild用于generate-map-injection-cpp.js中的 JS 压缩环节(生成 minified JS)。
实现细节说明:
scripts/format-cpp.js、scripts/format-web.js及三个生成脚本的内部实现未在本页读取范围内(源文件探索预算已用尽),上文对其行为描述仅基于 package.json 中的脚本定义与 AGENTS.md 中的官方约定。脚本内部的 CLI 参数、格式化规则文件位置等细节请直接查阅对应脚本。
格式化必须遵守的代码约定
格式化脚本输出的是「符合仓库约定」的代码,AGENTS.md 对这些约定有明确定义,理解它们有助于判断格式化结果的正确性:
C++ 头文件包含顺序
The matching header first in
.cpp, thenvendor/std.hpp, remaining vendor headers, and project headers.
即 .cpp 文件中:① 与之同名的头文件;② vendor/std.hpp;③ 其余 vendor 头;④ 项目头。这一顺序与「每个项目头必须不依赖 PCH 即可自包含」的约束配合工作。
命名规范
| 类别 | 规范 | 示例 |
|---|---|---|
| C++ 命名空间 | lower snake case | features::gallery、core::http_server |
| C++ 类型 | PascalCase | GalleryState、RpcRequest |
| C++ 文件/函数 | snake_case | gallery.hpp、initialize() |
| 前端组件 | PascalCase | GalleryPage.vue |
| 前端模块 | camelCase | galleryApi.ts |
Source: AGENTS.md
注释规范
Comments should describe intent and logic (why / what), not restate what the code already shows (how). When changing code, update related comments so they stay in sync with the implementation.
即:注释描述意图与逻辑(为什么/做什么),而非复述代码已展示的实现方式;改代码时必须同步更新相关注释。这条规范与生成脚本紧密相关——生成的迁移/本地化/注入代码同样不应携带复述式注释。
发布链与生成/格式化的衔接
pnpm run release 定义了完整发布序列:
release = build && build:portable && build:installer && build:checksums
其中:
| 脚本 | 实现 | 说明 |
|---|---|---|
build | 聚合脚本 | build:cpp → build:web → build:android → build:dist 顺序执行 |
build:portable | scripts/build-portable.js | 生成便携版 |
build:installer | scripts/build-installer.js | 构建 MSI 与 WiX bundle 安装包,输出至 dist/;支持 --msi-only 与 --version X.Y.Z 覆盖 version.json |
build:checksums | scripts/generate-checksums.js | 生成发布产物校验和 |
release:version | scripts/release-version.js | 版本号管理独立入口 |
构建输出目录约定(AGENTS.md「Build Output」节):Release 为 build\windows\x64\release\,Debug 为 build\windows\x64\debug\,Android 守护进程为 build\android\momo-capture.jar,分发产物在 dist/。
注意图中的关键点:生成脚本的执行发生在开发阶段而非发布脚本内部。发布链假设生成产物已是最新;这正是 AGENTS.md 把「必须重新运行」写成显式清单的原因——它是流水线中唯一依赖人工纪律的环节。
Failure Modes(失败模式与边界情况)
基于已收集的源证据,本流水线存在以下需要注意的失败模式:
- 忘跑生成脚本(最主要风险):修改
src/migrations/*.sql、src/locales/*.json或web/src/features/map/injection/source/*.js后未运行对应脚本。编译不会失败,但运行时行为与源文件不一致。缓解方式是 AGENTS.md 的显式清单 + code review 约定。 - 构建模式残留:
build:cpp在 release 编译后自动xmake config -m debug复原配置,防止开发者直接跑xmake build时仍处于 release 模式。绕过build:cpp手动执行xmake config -m release && xmake build则可能把配置残留在 release。 &&短路中止:聚合脚本使用&&串联,任何一个环节非零退出都会中止整链。这是有意的 fail-fast 设计,但对「部分成功状态」(例如 cpp 已构建而 web 未构建)需要使用者自行感知。- 场景测试依赖 Release 构建:AGENTS.md 明确「content-hash 语义只在 Release 下成立」,跑
pnpm run test:scenarios前必须先xmake release,否则测试语义不成立。这也解释了为何build:cpp先编译 release。 - 格式化钩子依赖
pnpm install:husky 通过prepare脚本安装;未执行过 install 的克隆仓库不会触发提交前格式化,C++ 与 Web 源码可能绕过格式约束。 - lint-staged 文件类型边界:lint-staged 只覆盖
src/**/*.{cpp,h,hpp}与web/**/*.{js,ts,vue,json,css,md}。根目录脚本、scripts/*.js、其他目录的代码不会被提交时格式化。
Configuration Options(配置项)
| 配置项 | 位置 | 默认值/示例 | 说明 |
|---|---|---|---|
| Node 版本要求 | package.json engines.node | >=22.13.0 | 所有生成/构建/格式化脚本的运行时前提 |
| 包管理器 | package.json packageManager | pnpm@11.21.0 | 使用 corepack 固定的 pnpm 版本 |
| C++ 格式化 glob | package.json lint-staged | src/**/*.{cpp,h,hpp} | 提交时进入 format-cpp.js --files 的文件集 |
| Web 格式化 glob | package.json lint-staged | web/**/*.{js,ts,vue,json,css,md} | 提交时进入 format-web.js 的文件集 |
| husky 启用 | package.json prepare | husky | pnpm install 时自动安装钩子 |
| 安装包版本覆盖 | build-installer.js | --version X.Y.Z | 覆盖 version.json;--msi-only 跳过 bundle |
| 生成脚本触发源 | AGENTS.md Code Generation Scripts 节 | 见上文表格 | 源文件变更 → 必须重跑的对应关系 |
API / 命令参考
本流水线的对外接口是 pnpm 脚本与 Node CLI 脚本,而非函数 API。
pnpm run build
聚合构建:build:cpp → build:web → build:android → build:dist,&& 串联 fail-fast。
pnpm run release
发布链:build → build:portable → build:installer → build:checksums,产物输出至 dist/。
node scripts/generate-migrations.js
修改 src/migrations/*.sql 后必须重新运行,生成数据库迁移代码。参数与内部行为未在已读取源范围内。
node scripts/generate-embedded-locales.js
修改 src/locales/*.json(zh-CN / en-US)后必须重新运行,生成内嵌本地化数据。
node scripts/generate-map-injection-cpp.js
修改 web/src/features/map/injection/source/*.js 后必须重新运行,同时重新生成压缩后的 JS 与其 C++ 头文件。
pnpm run format:cpp / node scripts/format-cpp.js --files
C++ 格式化双模式入口:不带参数(经 format:cpp)时全量执行;lint-staged 调用时带 --files 并传入暂存文件列表做增量格式化。
pnpm run format:web / node scripts/format-web.js
Web 格式化:format:web 在 web/ 工作区以 prettier 全量写入;format-web.js 为 lint-staged 的增量包装。
说明:各 Node 脚本的完整参数签名(如 format-cpp.js --files 之外是否还有其他标志)在本次未读取的 scripts/*.js 实现中,此处不臆测。
Related Links(相关链接)
- AGENTS.md — 仓库工程约定总纲:构建策略、代码生成脚本清单、注释与命名规范、测试策略。
- package.json — 顶层脚本链与 lint-staged/husky 配置。
- docs/developer/architecture.md — 完整环境搭建步骤(AGENTS.md 指引的权威来源)。
- 构建与发布、Android 捕获守护进程、安装器打包等主题由对应兄弟页面展开。