Files
Genarrative/scripts/check-generated-bindings.mjs
T
k88936 0e313405d6
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m54s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m36s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 4m44s
Project CI / Backend tests (pull_request) Successful in 5m46s
Project CI / Frontend tests (pull_request) Successful in 2m50s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m41s
Project CI / Native shell tests (pull_request) Successful in 6m39s
Project CI / Repository checks (pull_request) Successful in 6m12s
修复:生成绑定校验不再用 prettier 改写生成文件
- scripts/check-generated-bindings.mjs:删掉生成后的 prettier 归一化,直接逐字节比较 ts-rs 原始输出
- .prettierignore 与 .eslintrc.cjs:四个 ts-rs 生成目录统一排除,禁止格式化工具二次改写
- .prettierrc.json:删除 contracts/generated 的 printWidth / singleQuote 覆盖
- .gitattributes:生成目录标 linguist-generated 并豁免 trailing-space,保证 git diff --check 通过
- 重新生成 AGC chat/generated 与 services/generated 的 51 个绑定,改为提交 ts-rs 原始输出(纯格式,语义不变)
- docs/project-memory:记录生成绑定不再经 prettier 的口径,修正旧流程说明
2026-10-03 15:40:21 +08:00

148 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 校验 ts-rs 生成的绑定与 Rust 声明一致。
*
* 做法是「重新生成一遍并与工作区现有内容逐字节比较」:只比 git diff 会漏掉「Rust 改了但
* 生成文件没重新跑」以外的情形,也很容易被本地未提交改动掩盖;比较生成前后快照更直接——
* 只要重新生成后的内容与现有内容不同,就说明仓库里的绑定是陈旧的。
*
* 覆盖两套 crate、四个目录:
* - `shared-contracts` → `packages/shared/src/contracts/generated`;
* - AGC shell → `chat/generated` + `services/generated` + `features/ui-editor/types`。
*
* 生成文件一律以 ts-rs 原始输出提交,不接受 prettier / eslint 等工具二次改写:这些目录
* 同时登记在 `.prettierignore` 与 `.eslintrc.cjs` 的 `ignorePatterns`,所以这里直接逐字节
* 比较原始输出即可,不需要(也不允许)再跑格式化。
*/
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
);
const SHARED_CONTRACTS_DIR = 'packages/shared/src/contracts/generated';
const AGC_CHAT_DIR =
'apps/ai-game-creator-shell/src/view/project-development/chat/generated';
const AGC_SERVICES_DIR = 'apps/ai-game-creator-shell/src/services/generated';
const AGC_UI_EDITOR_DIR =
'apps/ai-game-creator-shell/src/features/ui-editor/types';
/** 一套绑定的生成入口、覆盖目录与"怎么重新生成"的提示。 */
const targets = [
{
label: 'shared-contracts',
dirs: [SHARED_CONTRACTS_DIR],
cargoArgs: [
'test',
'--locked',
'-p',
'shared-contracts',
'--features',
'ts-bindings',
'--manifest-path',
'server-rs/Cargo.toml',
'export_bindings',
],
hint: 'cargo test --locked -p shared-contracts --features ts-bindings --manifest-path server-rs/Cargo.toml export_bindings',
},
{
label: 'ai-game-creator-shell',
dirs: [AGC_CHAT_DIR, AGC_SERVICES_DIR, AGC_UI_EDITOR_DIR],
cargoArgs: [
'test',
'--locked',
'--manifest-path',
'apps/ai-game-creator-shell/src-tauri/Cargo.toml',
'export_bindings',
],
hint: 'cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings',
},
];
function snapshot(dir) {
const absolute = path.join(repoRoot, dir);
if (!fs.existsSync(absolute)) {
// 目录不存在时旧实现返回空 Map:若前后快照都为空,diff 什么都不报,这个 target 会以
// "0 个文件"静默通过——`export_to` 路径被改名或生成文件被整体删除时,校验形同虚设。
throw new Error(`生成绑定目录不存在,无法校验:${dir}`);
}
// 四个生成目录都是平铺的(ts-rs 的 `export_to` 直接落文件,不再嵌套子目录);
// 不用 `recursive`,避免依赖 Node 18.17 / 20.1 才有的选项(旧版本会静默退化成只读顶层)。
const entries = fs.readdirSync(absolute);
return new Map(
entries.flatMap((entry) => {
const absoluteFile = path.join(absolute, entry);
if (!fs.lstatSync(absoluteFile).isFile()) return [];
const name = path.posix.join(dir, entry.split(path.sep).join('/'));
return [[name, fs.readFileSync(absoluteFile)]];
}),
);
}
function run(command, args) {
// Windows 上 `.cmd` / `.bat` 不带 shell 无法执行(Node 修 CVE-2024-27980 后直接 EINVAL);
// 仓库既有脚本统一用 `shell: process.platform === 'win32'`。
const result = spawnSync(command, args, {
cwd: repoRoot,
encoding: 'utf8',
shell: process.platform === 'win32',
});
if (result.error) {
console.error(`无法启动 ${command}:${result.error.message}`);
}
return result;
}
function diff(before, after) {
const problems = [];
for (const [name, content] of after) {
const previous = before.get(name);
if (!previous) problems.push(`新增 ${name}`);
else if (!previous.equals(content)) problems.push(`内容变化 ${name}`);
}
for (const name of before.keys()) {
if (!after.has(name)) problems.push(`删除 ${name}`);
}
return problems;
}
let totalFiles = 0;
let failed = false;
for (const target of targets) {
const before = new Map(
target.dirs.flatMap((dir) => [...snapshot(dir).entries()]),
);
const result = run('cargo', target.cargoArgs);
if (result.status !== 0) {
failed = true;
console.error(
`生成绑定校验无法运行:${target.label} 的 export_bindings 未通过`,
);
if (result.stdout) console.error(result.stdout.trim());
if (result.stderr) console.error(result.stderr.trim());
continue;
}
const after = new Map(
target.dirs.flatMap((dir) => [...snapshot(dir).entries()]),
);
const problems = diff(before, after);
if (problems.length > 0) {
failed = true;
console.error(`生成绑定与 Rust 声明不一致(${target.label}):`);
for (const problem of problems) console.error(` - ${problem}`);
console.error(
`请运行 \`${target.hint}\`,并把重新生成的结果与 Rust 改动一并提交。`,
);
} else {
totalFiles += after.size;
console.log(`生成绑定校验通过:${target.label}(${after.size} 个文件)。`);
}
}
if (failed) process.exit(1);
console.log(`生成绑定校验通过:合计 ${totalFiles} 个文件与 Rust 声明一致。`);