#!/usr/bin/env node /** * 校验 ts-rs 生成的共享契约绑定与 Rust 声明一致。 * * 做法是「重新生成一遍并与工作区现有内容逐字节比较」:只比 git diff 会漏掉「Rust 改了但 * 生成文件没重新跑」以外的情形,也很容易被本地未提交改动掩盖;比较生成前后快照更直接—— * 只要重新生成后的内容与现有内容不同,就说明仓库里的绑定是陈旧的。 */ 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 generatedDir = path.join( repoRoot, 'packages/shared/src/contracts/generated', ); function snapshot() { if (!fs.existsSync(generatedDir)) return new Map(); return new Map( fs .readdirSync(generatedDir, { withFileTypes: true }) .filter((entry) => entry.isFile()) .map((entry) => [ entry.name, fs.readFileSync(path.join(generatedDir, 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}`); } 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 改动一并提交。', ); process.exit(1); } console.log( `生成绑定校验通过:${after.size} 个文件与 Rust 声明一致(${path.relative(repoRoot, generatedDir)})。`, );