Files
Genarrative/scripts/check-server-rs-ddd-boundaries.mjs
T
kdletters e9c3dc1120 退役旧创作模板业务
保留 SpacetimeDB 历史表、迁移白名单与旧业务源码
切换前端 active 入口并解除旧创作页面和路由编译链
移除旧后端路由、worker 与纯业务 crate 依赖
收敛 SpacetimeDB 模块为历史数据壳
同步 Nginx、Pingora、验证门禁与架构文档
2026-07-17 22:07:52 +08:00

340 lines
9.2 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.
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, join, relative } from 'node:path';
const repoRoot = process.cwd();
const cratesDir = join(repoRoot, 'server-rs', 'crates');
const spacetimeModuleSrcDir = join(cratesDir, 'spacetime-module', 'src');
const spacetimeMigrationPath = join(spacetimeModuleSrcDir, 'migration.rs');
const spacetimeTableCatalogPath = join(
repoRoot,
'docs',
'【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md',
);
const migrationExcludedTables = new Set([
'database_migration_operator',
'database_migration_import_chunk',
]);
const requiredModuleFiles = [
'domain.rs',
'commands.rs',
'application.rs',
'events.rs',
'errors.rs',
];
const requiredLibModules = ['domain', 'commands', 'application', 'events', 'errors'];
const retiredModuleCrates = new Set([
'module-bark-battle',
'module-big-fish',
'module-combat',
'module-creative-agent',
'module-custom-world',
'module-inventory',
'module-jump-hop',
'module-match3d',
'module-npc',
'module-progression',
'module-puzzle',
'module-puzzle-clear',
'module-quest',
'module-runtime-item',
'module-runtime-story',
'module-square-hole',
'module-story',
'module-visual-novel',
'module-wooden-fish',
]);
const retiredOriginalSpacetimeSources = new Set([
'bark_battle.rs',
'big_fish.rs',
'custom_world.rs',
'gameplay.rs',
'jump_hop.rs',
'match3d.rs',
'public_work.rs',
'puzzle.rs',
'puzzle_clear.rs',
'square_hole.rs',
'visual_novel.rs',
'wooden_fish.rs',
'big_fish/events.rs',
'big_fish/runtime.rs',
'big_fish/session.rs',
'bark_battle/types.rs',
'square_hole/types.rs',
'runtime/admin_work_visibility.rs',
'runtime/browse_history.rs',
'runtime/creation_entry_config.rs',
'runtime/profile.rs',
'runtime/settings.rs',
'runtime/snapshots.rs',
]);
const retiredOriginalSpacetimePrefixes = [
'gameplay/',
];
function isRetiredOriginalSpacetimeSource(path) {
return (
retiredOriginalSpacetimeSources.has(path) ||
retiredOriginalSpacetimePrefixes.some((prefix) => path.startsWith(prefix))
);
}
const forbiddenModuleWidePatterns = [
{
pattern: /\baxum::/u,
message: 'module-* 不允许直接依赖 Axum',
},
{
pattern: /\bspacetimedb::(?:table|reducer|procedure|ReducerContext|ProcedureContext|Table)\b/u,
message: 'module-* 不允许声明 SpacetimeDB table/reducer/procedure 或直接操作表',
},
];
const forbiddenCorePatterns = [
{
pattern: /\breqwest::/u,
message: 'DDD 核心文件不允许直接依赖 reqwest',
},
{
pattern: /\bplatform_oss::/u,
message: 'DDD 核心文件不允许直接依赖 OSS adapter',
},
{
pattern: /\bplatform_llm::/u,
message: 'DDD 核心文件不允许直接依赖 LLM adapter',
},
{
pattern: /\bspacetime_client::/u,
message: 'DDD 核心文件不允许直接依赖 SpacetimeDB client adapter',
},
{
pattern: /\bstd::fs\b/u,
message: 'DDD 核心文件不允许直接访问文件系统',
},
{
pattern: /\btokio::/u,
message: 'DDD 核心文件不允许绑定异步运行时',
},
];
function normalizePath(path) {
return path.replace(/\\/gu, '/');
}
function readText(path) {
return readFileSync(path, 'utf8');
}
function listRustFiles(dir) {
const files = [];
function walk(currentDir) {
for (const name of readdirSync(currentDir)) {
const fullPath = join(currentDir, name);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
walk(fullPath);
continue;
}
if (name.endsWith('.rs')) {
files.push(fullPath);
}
}
}
walk(dir);
return files;
}
function collectSpacetimeTables() {
if (!existsSync(spacetimeModuleSrcDir)) {
return [];
}
const tableByAccessor = new Map();
const tablePattern =
/#\[spacetimedb::table\(([\s\S]*?)\)\]\s*(?:#\[[^\]]+\]\s*)*(?:pub\s+)?struct\s+([A-Za-z0-9_]+)/gu;
for (const rustFile of listRustFiles(spacetimeModuleSrcDir)) {
const moduleRelativePath = normalizePath(relative(spacetimeModuleSrcDir, rustFile));
if (isRetiredOriginalSpacetimeSource(moduleRelativePath)) {
continue;
}
const text = readText(rustFile);
let match;
while ((match = tablePattern.exec(text)) !== null) {
const accessorMatch = /accessor\s*=\s*([A-Za-z0-9_]+)/u.exec(match[1]);
if (!accessorMatch) {
continue;
}
const accessor = accessorMatch[1];
const relativePath = normalizePath(relative(repoRoot, rustFile));
const previous = tableByAccessor.get(accessor);
if (previous) {
failures.push(
`SpacetimeDB table accessor ${accessor} 重复定义于 ${previous.path}${relativePath}`,
);
continue;
}
tableByAccessor.set(accessor, {
accessor,
structName: match[2],
path: relativePath,
});
}
}
return [...tableByAccessor.values()].sort((left, right) =>
left.accessor.localeCompare(right.accessor),
);
}
function collectMigrationTables() {
if (!existsSync(spacetimeMigrationPath)) {
return new Set();
}
const migrationText = readText(spacetimeMigrationPath);
const macroMatch =
/macro_rules!\s+migration_tables\s*\{[\s\S]*?\$macro_name!\s*\{([\s\S]*?)\n\s*\}\s*\n\s*\};\s*\n\}/u.exec(
migrationText,
);
if (!macroMatch) {
failures.push('migration.rs 无法解析 migration_tables! 白名单');
return new Set();
}
const migrationTableBody = macroMatch[1]
.replace(/\/\*[\s\S]*?\*\//gu, ' ')
.replace(/\/\/[^\r\n]*/gu, ' ');
return new Set(
[...migrationTableBody.matchAll(/\b([a-z][a-z0-9_]*)\b/gu)]
.map((match) => match[1])
.filter((name) => !['arg'].includes(name)),
);
}
function collectCatalogTables() {
if (!existsSync(spacetimeTableCatalogPath)) {
return new Set();
}
const catalogText = readText(spacetimeTableCatalogPath);
return new Set(
[...catalogText.matchAll(/^### `([^`]+)`/gmu)].map((match) => match[1]),
);
}
function checkSpacetimeTableCatalogAndMigration() {
const tables = collectSpacetimeTables();
const tableNames = new Set(tables.map((table) => table.accessor));
const migrationTables = collectMigrationTables();
const catalogTables = collectCatalogTables();
for (const table of tables) {
if (!migrationExcludedTables.has(table.accessor) && !migrationTables.has(table.accessor)) {
failures.push(
`${table.path}: SpacetimeDB 表 ${table.accessor} 缺少 migration.rs 白名单`,
);
}
if (!catalogTables.has(table.accessor)) {
failures.push(
`${table.path}: SpacetimeDB 表 ${table.accessor} 缺少后端架构文档表目录项`,
);
}
}
for (const tableName of migrationTables) {
if (!tableNames.has(tableName)) {
failures.push(`migration.rs 白名单包含不存在的 SpacetimeDB 表 ${tableName}`);
}
}
for (const tableName of catalogTables) {
if (!tableNames.has(tableName)) {
failures.push(`后端架构文档表目录包含不存在的 SpacetimeDB 表 ${tableName}`);
}
}
}
function collectModuleCrates() {
return readdirSync(cratesDir)
.filter((name) => name.startsWith('module-'))
.filter((name) => !retiredModuleCrates.has(name))
.filter((name) => existsSync(join(cratesDir, name, 'Cargo.toml')))
.sort();
}
const failures = [];
const moduleCrates = collectModuleCrates();
for (const crateName of moduleCrates) {
const crateDir = join(cratesDir, crateName);
const srcDir = join(crateDir, 'src');
const libPath = join(srcDir, 'lib.rs');
for (const fileName of requiredModuleFiles) {
const filePath = join(srcDir, fileName);
if (!existsSync(filePath)) {
failures.push(`${crateName} 缺少 DDD 落位文件 src/${fileName}`);
}
}
if (existsSync(libPath)) {
const libText = readText(libPath);
for (const moduleName of requiredLibModules) {
const moduleDeclaration = new RegExp(
`(?:^|\\n)\\s*(?:pub(?:\\([^)]*\\))?\\s+)?mod\\s+${moduleName}\\s*;`,
'u',
);
if (!moduleDeclaration.test(libText)) {
failures.push(`${crateName} 的 lib.rs 缺少模块声明 mod ${moduleName};`);
}
}
}
for (const rustFile of listRustFiles(srcDir)) {
const relativePath = normalizePath(relative(repoRoot, rustFile));
const fileName = basename(rustFile);
const text = readText(rustFile);
if (fileName === 'mapper.rs') {
failures.push(`${relativePath} 不能位于 module-*mapper 只能放在 adapter crate`);
}
for (const rule of forbiddenModuleWidePatterns) {
if (rule.pattern.test(text)) {
failures.push(`${relativePath}: ${rule.message}`);
}
}
const isDddCoreFile = requiredModuleFiles.some((name) =>
relativePath.endsWith(`/src/${name}`),
);
if (!isDddCoreFile) {
continue;
}
for (const rule of forbiddenCorePatterns) {
if (rule.pattern.test(text)) {
failures.push(`${relativePath}: ${rule.message}`);
}
}
}
}
checkSpacetimeTableCatalogAndMigration();
if (failures.length > 0) {
console.error('server-rs DDD boundary check failed:');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log(`server-rs DDD boundary check passed for ${moduleCrates.length} module crate(s).`);