Close DDD refactor and remove generated asset proxy
This commit is contained in:
@@ -3,6 +3,15 @@ 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',
|
||||
@@ -79,6 +88,113 @@ function listRustFiles(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-'))
|
||||
@@ -144,6 +260,8 @@ for (const crateName of moduleCrates) {
|
||||
}
|
||||
}
|
||||
|
||||
checkSpacetimeTableCatalogAndMigration();
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('server-rs DDD boundary check failed:');
|
||||
for (const failure of failures) {
|
||||
|
||||
@@ -381,12 +381,6 @@ const indexPath = path.join(webRoot, 'index.html');
|
||||
const proxyPrefixes = [
|
||||
'/api/',
|
||||
'/api',
|
||||
'/generated-character-drafts',
|
||||
'/generated-characters',
|
||||
'/generated-animations',
|
||||
'/generated-custom-world-scenes',
|
||||
'/generated-custom-world-covers',
|
||||
'/generated-qwen-sprites',
|
||||
'/healthz',
|
||||
];
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ for (const target of selectedTargets) {
|
||||
await recreateTempDir(tempOutDir);
|
||||
|
||||
console.log(`[spacetime:generate] 生成 ${target.name} bindings 到短路径: ${tempOutDir}`);
|
||||
await run('spacetime', buildGenerateArgs(target, tempOutDir));
|
||||
await generateBindings(target, tempOutDir);
|
||||
|
||||
const fileCount = await countFiles(tempOutDir);
|
||||
if (fileCount === 0) {
|
||||
@@ -141,7 +141,79 @@ function buildGenerateArgs(target, outDir) {
|
||||
return generateArgs;
|
||||
}
|
||||
|
||||
function run(command, commandArgs) {
|
||||
async function generateBindings(target, outDir) {
|
||||
const result = await run('spacetime', buildGenerateArgs(target, outDir), {
|
||||
allowGeneratedFormatFailure: target.lang === 'rust',
|
||||
});
|
||||
|
||||
if (result.generatedFormatFailed) {
|
||||
// Windows 下 SpacetimeDB CLI 2.1.0 会把所有 Rust 文件一次性传给 formatter;
|
||||
// 这里只接管“文件已生成但 CLI 格式化失败”的尾段,并仍然只同步生成目录。
|
||||
console.warn(
|
||||
`[spacetime:generate] ${target.name} bindings 已生成,但 SpacetimeDB CLI 自带格式化失败;改用短路径分批 rustfmt。`,
|
||||
);
|
||||
await formatRustBindings(outDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function formatRustBindings(outDir) {
|
||||
const rustFiles = await collectRustFiles(outDir);
|
||||
if (rustFiles.length === 0) {
|
||||
throw new Error(`Rust bindings 未生成任何 .rs 文件,无法格式化: ${outDir}`);
|
||||
}
|
||||
|
||||
for (const chunk of chunkCommandArgs(rustFiles)) {
|
||||
await run('rustfmt', ['--edition', '2024', ...chunk]);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectRustFiles(dir) {
|
||||
const files = [];
|
||||
const entries = await readdir(dir, {withFileTypes: true});
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await collectRustFiles(entryPath)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith('.rs')) {
|
||||
files.push(entryPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function chunkCommandArgs(argsToChunk) {
|
||||
// Windows CreateProcess 受命令行长度限制;分批能避免 bindings 文件变多后再次失败。
|
||||
const maxCommandLineChars = process.platform === 'win32' ? 20_000 : 100_000;
|
||||
const chunks = [];
|
||||
let current = [];
|
||||
let currentLength = 0;
|
||||
|
||||
for (const arg of argsToChunk) {
|
||||
const argLength = arg.length + 3;
|
||||
if (current.length > 0 && currentLength + argLength > maxCommandLineChars) {
|
||||
chunks.push(current);
|
||||
current = [];
|
||||
currentLength = 0;
|
||||
}
|
||||
|
||||
current.push(arg);
|
||||
currentLength += argLength;
|
||||
}
|
||||
|
||||
if (current.length > 0) {
|
||||
chunks.push(current);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function run(command, commandArgs, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, commandArgs, {
|
||||
cwd: REPO_ROOT,
|
||||
@@ -171,13 +243,20 @@ function run(command, commandArgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (output.includes('Could not format generated files')) {
|
||||
const generatedFormatFailed = output.includes('Could not format generated files');
|
||||
|
||||
if (generatedFormatFailed && options.allowGeneratedFormatFailure) {
|
||||
resolve({generatedFormatFailed});
|
||||
return;
|
||||
}
|
||||
|
||||
if (generatedFormatFailed) {
|
||||
reject(new Error(`${command} 生成后格式化失败。`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
resolve({generatedFormatFailed: false});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user