#!/usr/bin/env tsx import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs'; import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface SchemaExport { name: string; content: string; source: 'auto' | 'manual'; } function extractExports(filePath: string, source: 'auto' | 'manual'): SchemaExport[] { if (!existsSync(filePath)) return []; const content = readFileSync(filePath, 'utf-8'); const exports: SchemaExport[] = []; const exportMatches = content.matchAll(/export\s+(?:const|type)\s+(\w+)\s*[=:]/g); for (const match of exportMatches) { const name = match[1]; const lines = content.split('\n'); let exportContent = ''; let inExport = false; let braceCount = 0; for (const line of lines) { if (line.match(new RegExp(`export\\s+(?:const|type)\\s+${name}\\s*[=:]`))) { inExport = true; exportContent = line; braceCount = (line.match(/{/g) || []).length - (line.match(/}/g) || []).length; if (braceCount <= 0 && !line.includes('{')) { break; } } else if (inExport) { exportContent += '\n' + line; braceCount += (line.match(/{/g) || []).length - (line.match(/}/g) || []).length; if (braceCount <= 0) { break; } } } if (exportContent) { exports.push({ name, content: exportContent, source }); } } return exports; } function main() { console.log('🔄 Merging database-generated and source Zod schemas...'); const fromDatabaseDir = resolve(__dirname, '../from-database'); const srcDir = resolve(__dirname, '../src'); const outputDir = resolve(__dirname, '../output/zod'); mkdirSync(outputDir, { recursive: true }); const allExports = new Map(); const conflicts: string[] = []; if (existsSync(fromDatabaseDir)) { const fromDatabaseFiles = readdirSync(fromDatabaseDir).filter(f => f.endsWith('.ts')); for (const file of fromDatabaseFiles) { const exports = extractExports(join(fromDatabaseDir, file), 'auto'); for (const exp of exports) { allExports.set(exp.name, exp); } } } if (existsSync(srcDir)) { const srcFiles = readdirSync(srcDir).filter(f => f.endsWith('.ts')); for (const file of srcFiles) { const exports = extractExports(join(srcDir, file), 'manual'); for (const exp of exports) { if (allExports.has(exp.name)) { conflicts.push(exp.name); console.error( `⚠️ CONFLICT: "${exp.name}" exists in both from-database and src. Using from-database version.` ); } else { allExports.set(exp.name, exp); } } } } const schemaExports = Array.from(allExports.values()).filter(e => e.name.endsWith('Schema')); const typeExports = Array.from(allExports.values()).filter(e => !e.name.endsWith('Schema')); // Group schemas by table name, ensuring we only have one schema per table const schemasByTable = new Map(); for (const schema of schemaExports) { // Extract table name from schema name (e.g., PostsSchema -> posts) const tableName = schema.name.replace('Schema', '').toLowerCase(); // If we already have a schema for this table, keep the one from from-database (auto-generated) if (!schemasByTable.has(tableName) || schema.source === 'auto') { schemasByTable.set(tableName, schema); } } const filesByTable = new Map }>(); // Set up files with their single schema for (const [tableName, schema] of schemasByTable) { filesByTable.set(tableName, { schema, types: new Set() }); } // Match types to their corresponding tables for (const typeExport of typeExports) { const typeName = typeExport.name.toLowerCase(); for (const [tableName] of filesByTable) { if ( typeName === tableName || typeName + 's' === tableName || typeName === tableName.slice(0, -1) || tableName.startsWith(typeName) ) { filesByTable.get(tableName)!.types.add(typeExport.content); break; } } } // Generate files with single schema and associated types for (const [tableName, { schema, types }] of filesByTable) { const typeContents = Array.from(types).join('\n\n'); const fileContent = `import { z } from 'zod';\n\n${schema.content}${typeContents ? '\n\n' + typeContents : ''}\n`; writeFileSync(join(outputDir, `${tableName}.ts`), fileContent); } const indexImports = Array.from(schemasByTable.keys()) .map(table => `export * from './${table}';`) .join('\n'); const indexContent = `${indexImports}\n`; writeFileSync(join(outputDir, 'index.ts'), indexContent); console.log(`✅ Merged ${allExports.size} exports into output/zod/`); if (conflicts.length > 0) { console.log(`⚠️ ${conflicts.length} conflicts detected and resolved (from-database priority)`); console.log(' Please review your source schemas and remove or rename conflicting exports:'); conflicts.forEach(name => console.log(` - ${name}`)); } } if (import.meta.url === `file://${process.argv[1]}`) { main(); }