diff --git a/scripts/check-generated-bindings.mjs b/scripts/check-generated-bindings.mjs index f2a108c09..0a3140b0b 100644 --- a/scripts/check-generated-bindings.mjs +++ b/scripts/check-generated-bindings.mjs @@ -1,10 +1,18 @@ #!/usr/bin/env node /** - * 校验 ts-rs 生成的共享契约绑定与 Rust 声明一致。 + * 校验 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'; @@ -15,67 +23,124 @@ const repoRoot = path.resolve( path.dirname(fileURLToPath(import.meta.url)), '..', ); -const generatedDir = path.join( - repoRoot, - 'packages/shared/src/contracts/generated', -); -function snapshot() { - if (!fs.existsSync(generatedDir)) return new Map(); +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(generatedDir, { withFileTypes: true }) + .readdirSync(absolute, { withFileTypes: true }) .filter((entry) => entry.isFile()) .map((entry) => [ - entry.name, - fs.readFileSync(path.join(generatedDir, entry.name)), + path.posix.join(dir, entry.name), + fs.readFileSync(path.join(absolute, entry.name)), ]), ); } -const before = snapshot(); -const result = spawnSync( - 'cargo', - [ - 'test', - '--locked', - '-p', - 'shared-contracts', - '--features', - 'ts-bindings', - '--manifest-path', - 'server-rs/Cargo.toml', - 'export_bindings', - ], - { cwd: repoRoot, encoding: 'utf8' }, -); -if (result.status !== 0) { - console.error('生成绑定校验无法运行:export_bindings 未通过'); - if (result.stdout) console.error(result.stdout.trim()); - if (result.stderr) console.error(result.stderr.trim()); - process.exit(result.status ?? 1); -} -const after = snapshot(); - -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}`); +function run(command, args) { + return spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8' }); } -if (problems.length > 0) { - console.error('生成绑定与 Rust 声明不一致:'); - for (const problem of problems) console.error(` - ${problem}`); - console.error( - '请运行 `cargo test -p shared-contracts --features ts-bindings export_bindings`,并把重新生成的结果与 Rust 改动一并提交。', +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()]), ); - process.exit(1); + 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('npx', ['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} 个文件)。`); + } } -console.log( - `生成绑定校验通过:${after.size} 个文件与 Rust 声明一致(${path.relative(repoRoot, generatedDir)})。`, -); +if (failed) process.exit(1); +console.log(`生成绑定校验通过:合计 ${totalFiles} 个文件与 Rust 声明一致。`);