Files
Genarrative/scripts/check-generated-bindings.mjs
T
k88936 2a70926a3e 小红书小工具的表单 DTO、typed 失败、路径常量与首错校验
- 新增 src-tauri/src/export/draft/xhs_minitool/dto.rs:XHSMiniToolExportForm / Field / State / RunResult 的线上形状;表单 deny_unknown_fields,TS 由 ts-rs 生成。
- 新增 error.rs:命令边界 typed 失败七变体(registryMalformed、formInvalid、buildScriptMissing、saveConflict、commandFailed、artifactMissing、exportUnavailable),带载荷变体持具名结构体,保证前端拿到可判别联合类型。
- 新增 layout.rs:.export/ 下 xhs-minitool.json 与 xhs-minitool.zip 的契约常量、npm 脚本名 build:xhs-minitool 与路径推导。
- 新增 validation.rs:名称与简介按 Unicode 字符数不超过 14 且非空,icon 必须是项目内普通文件、非符号链接、png/jpg/jpeg 且不超过 5 MiB,首个错误即返回。
- 改写 xhs_minitool/mod.rs 为模块声明与重导出,删除上一轮 xhx-* 草案常量与 xhx-minitool 拼写。
- 生成 src/view/project-development/export/generated/ 下 12 份 ts-rs 绑定。
- scripts/check-generated-bindings.mjs 把 export/generated 纳入 AGC 目标的覆盖目录。
2026-10-05 15:27:35 +08:00

150 lines
5.5 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 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 声明一致。`);