Files
Genarrative/scripts/check-generated-bindings.mjs
T
k88936 09f007d519 修复生成绑定校验在 Windows 上启动不了 npx
- check-generated-bindings.mjs:prettier 归一化改用平台命令名 npx.cmd / npx,spawnSync 无 shell 时 Windows 解析不到裸 npx
2026-10-02 22:26:56 +08:00

154 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`(随仓库统一走 prettier)。
*
* AGC 那两个目录是 prettier 格式化过的,而 ts-rs 输出的是双引号、不换行的原始形态;所以
* 重生成后要按仓库格式再跑一遍 prettier 才能比较,否则每次都会报"格式不同"的假漂移。
* 格式化范围只限这两个生成目录,不碰其它源码。
*/
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)),
'..',
);
// Windows 上 `npx` 以 `npx.cmd` 落地,`spawnSync` 不带 shell 时解析不到(ENOENT)。
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
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';
// ui-editor 的绑定在 `.prettierignore` 里,ts-rs 原始输出就是提交形态,不需要再格式化。
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',
],
formatDirs: [],
hint: 'cargo test -p shared-contracts --features ts-bindings 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',
],
// ts-rs 落盘后按仓库格式跑一遍 prettier,比较的才是"提交时应有的内容"
// (ui-editor 的绑定被 `.prettierignore` 忽略,不在里面)。
formatDirs: [AGC_CHAT_DIR, AGC_SERVICES_DIR],
hint: 'cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings && npx prettier --write apps/ai-game-creator-shell/src/view/project-development/chat/generated apps/ai-game-creator-shell/src/services/generated',
},
];
function snapshot(dir) {
const absolute = path.join(repoRoot, dir);
if (!fs.existsSync(absolute)) return new Map();
return new Map(
fs
.readdirSync(absolute, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => [
path.posix.join(dir, entry.name),
fs.readFileSync(path.join(absolute, entry.name)),
]),
);
}
function run(command, args) {
return spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8' });
}
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) {
console.error(
`生成绑定校验无法运行:${target.label} 的 export_bindings 未通过`,
);
if (result.stdout) console.error(result.stdout.trim());
if (result.stderr) console.error(result.stderr.trim());
process.exit(result.status ?? 1);
}
if (target.formatDirs.length > 0) {
const formatted = run(npxCommand, [
'prettier',
'--write',
...target.formatDirs,
]);
if (formatted.status !== 0) {
console.error(
`生成绑定校验无法运行:${target.label} 的 prettier 格式化失败`,
);
if (formatted.stdout) console.error(formatted.stdout.trim());
if (formatted.stderr) console.error(formatted.stderr.trim());
process.exit(formatted.status ?? 1);
}
}
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 声明一致。`);