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', 'database_migration_import_chunk', ]); 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).`);