Files
Genarrative/scripts/check-server-rs-ddd-boundaries.mjs

274 lines
7.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.
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',
'technical',
'SPACETIMEDB_TABLE_CATALOG.md',
);
const migrationExcludedTables = new Set(['database_migration_operator']);
const requiredModuleFiles = [
'domain.rs',
'commands.rs',
'application.rs',
'events.rs',
'errors.rs',
];
const requiredLibModules = ['domain', 'commands', 'application', 'events', 'errors'];
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 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();
}
return new Set(
[...macroMatch[1].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} 缺少 SPACETIMEDB_TABLE_CATALOG.md 目录项`,
);
}
}
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_TABLE_CATALOG.md 包含不存在的 SpacetimeDB 表 ${tableName}`);
}
}
}
function collectModuleCrates() {
return readdirSync(cratesDir)
.filter((name) => name.startsWith('module-'))
.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).`);