Files
Genarrative/scripts/check-generated-bindings.mjs
T
k88936 677c477f36 修正生成绑定校验脚本头部过期的目录清单
- check-generated-bindings.mjs 文件头从「两套 crate、四个目录」更新为「五个目录」,补上 AGC export/generated
2026-10-06 18:48:24 +08:00

151 lines
5.6 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` +
* `export/generated`。
*
* 生成文件一律以 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 AGC_EXPORT_DIR =
'apps/ai-game-creator-shell/src/view/project-development/export/generated';
/** 一套绑定的生成入口、覆盖目录与"怎么重新生成"的提示。 */
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, AGC_EXPORT_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 声明一致。`);