Build Registry Script
Introduction
scripts/build-registry.mjs is the 5,208-line powerhouse (164KB) that transforms runtime I/O into build-time static registries. This document explains how this critical script discovers, processes, and generates all 16 registry files that power the entire system.
Script location: scripts/build-registry.mjs
Size: 5,208 lines (164KB)
Purpose: Unified build-time registry generation
Performance: ~5-10 seconds build time, enables 17,255x runtime improvement
Overview
What It Does
The script is a 4-phase pipeline:
Phase 1: Discovery
├─> Scan contents/ directory
├─> Find entities, plugins, themes
├─> Discover routes, translations, templates
└─> Collect configurations
Phase 2: Processing
├─> Extract metadata from configs
├─> Resolve dependencies
├─> Build relationships (parent/child)
└─> Apply merge priority (Core > Theme > Plugin)
Phase 3: Generation
├─> Generate TypeScript imports
├─> Create static registry objects
├─> Generate helper functions
└─> Output 16 registry files
Phase 4: Watch (dev mode only)
├─> Monitor contents/ for changes
├─> Trigger rebuild on changes
└─> Notify to restart dev server
Command Line Usage
# One-time build
pnpm registry:build
# or
node scripts/build-registry.mjs
# Watch mode (automatic rebuild)
pnpm registry:build-watch
# or
node scripts/build-registry.mjs --watch
# Production build (no watch)
node scripts/build-registry.mjs --build
# Verbose logging
node scripts/build-registry.mjs --verbose
# or
node scripts/build-registry.mjs -v
Output Files
Generates 16 files in core/lib/registries/:
entity-registry.ts- Server entity registryentity-registry.client.ts- Client entity registryplugin-registry.ts- Server plugin registryplugin-registry.client.ts- Client plugin registryroute-handlers.ts- Zero-dynamic-import API routestheme-registry.ts- Theme configurationstranslation-registry.ts- i18n translationstemplate-registry.ts- Template overridestemplate-registry.client.ts- Client template registryconfig-registry.ts- App/theme/plugin configsdocs-registry.ts- Documentation metadatamiddleware-registry.ts- Theme middlewarenamespace-registry.ts- i18n namespace optimizationscope-registry.ts- Scope configurationslegacy-entity-registry.ts- Backwards compatibilityindex.ts- Unified exports
Configuration
Environment Variables
const CONFIG = {
// Output directory for generated registries
outputDir: join(rootDir, 'core', 'lib', 'registries'),
// Watch mode (auto-rebuild on changes)
watchMode: process.argv.includes('--watch') && !process.argv.includes('--build'),
// Build mode (one-time, no watch)
buildMode: process.argv.includes('--build'),
// Verbose logging
verbose: process.argv.includes('--verbose') || process.argv.includes('-v'),
// Contents directory to scan
contentsDir: join(rootDir, 'contents'),
// Active theme (from NEXT_PUBLIC_ACTIVE_THEME)
activeTheme: process.env.NEXT_PUBLIC_ACTIVE_THEME?.replace(/'/g, '')
}
Content Type Definitions
const CONTENT_TYPES = {
plugins: {
dir: 'plugins',
configPattern: 'plugin.config.ts',
generator: generatePluginRegistry,
discoverer: discoverPlugins
},
entities: {
dir: 'entities',
configPattern: '.config.ts',
generator: generateEntityRegistry,
discoverer: discoverEntities
},
themes: {
dir: 'themes',
configPattern: 'theme.config.ts',
generator: generateThemeRegistry,
discoverer: discoverThemes
},
templates: {
dir: 'themes', // Templates within themes
configPattern: null,
generator: generateTemplateRegistry,
discoverer: discoverTemplates
},
// ... more content types
}
Phase 1: Discovery
Entity Discovery
Algorithm: Recursive nested entity discovery
async function discoverNestedEntities(
basePath,
relativePath = '',
depth = 0,
parentName = null,
ownerContext = null // { type: 'plugin', name: 'ai' }
) {
const entities = []
// 1. Scan directory
const entries = await readdir(basePath, { withFileTypes: true })
// 2. Find config files directly in this directory
const configFiles = entries.filter(entry =>
entry.isFile() &&
entry.name.endsWith('.config.ts') &&
entry.name !== 'index.ts'
)
// 3. Process config files (child entities)
for (const configFile of configFiles) {
let entityName = configFile.name.replace('.config.ts', '')
const configPath = join(basePath, configFile.name)
// Extract export name (EntityConfig or ChildConfig)
const exportName = await extractExportName(configPath, [
/export\s+const\s+([a-zA-Z]+EntityConfig)\s*[:=]/,
/export\s+const\s+([a-zA-Z]+ChildConfig)\s*[:=]/
])
if (!exportName) continue
// For child entities, extract table name
if (exportName.includes('ChildConfig')) {
const configContent = await readFile(configPath, 'utf-8')
const tableMatch = configContent.match(/table:\s*['"]([^'"]+)['"]/)
if (tableMatch) {
entityName = tableMatch[1] // Use table name as entity name
}
}
// Determine import path based on owner context
let baseImportPath
if (ownerContext?.type === 'plugin') {
baseImportPath = `@/contents/plugins/${ownerContext.name}/entities/${entityName}`
} else if (ownerContext?.type === 'theme') {
baseImportPath = `@/contents/themes/${ownerContext.name}/entities/${entityName}`
} else {
baseImportPath = `@/contents/entities/${entityName}`
}
// Build entity metadata
entities.push({
name: entityName,
exportName,
configPath: `${baseImportPath}/${configFile.name.replace('.ts', '')}`,
relativePath: entityName,
depth,
parent: parentName,
children: [],
hasComponents: false,
hasHooks: false,
hasMigrations: false,
hasMessages: false,
hasAssets: false,
messagesPath: `${baseImportPath}/messages`,
pluginContext: ownerContext?.type === 'plugin' ? { pluginName: ownerContext.name } : null,
themeContext: ownerContext?.type === 'theme' ? { themeName: ownerContext.name } : null,
source: ownerContext?.type || 'theme'
})
}
// 4. Process subdirectories (entities with own directories)
const entityDirs = entries.filter(entry => entry.isDirectory())
for (const dir of entityDirs) {
const entityName = dir.name
const currentPath = join(basePath, entityName)
// Try multiple naming patterns for config files
const configPatterns = [
`${entityName}.config.ts`,
`${entityName.slice(0, -1)}.config.ts`, // Remove trailing 's'
`${entityName.replace(/s$/, '')}.config.ts`
]
let configPath = null
let actualConfigFile = null
// Find config file
for (const pattern of configPatterns) {
const testPath = join(currentPath, pattern)
if (existsSync(testPath)) {
configPath = testPath
actualConfigFile = pattern
break
}
}
// Fallback: find any .config.ts file
if (!configPath) {
const dirContents = await readdir(currentPath, { withFileTypes: true })
const configFile = dirContents.find(entry =>
entry.isFile() &&
entry.name.endsWith('.config.ts') &&
entry.name !== 'index.ts'
)
if (configFile) {
configPath = join(currentPath, configFile.name)
actualConfigFile = configFile.name
}
}
if (!configPath) {
// Check for nested entities even without config (container)
const childPath = join(currentPath, 'children')
if (existsSync(childPath)) {
const nestedEntities = await discoverNestedEntities(
childPath,
`${entityName}/children`,
depth + 1,
entityName,
ownerContext
)
entities.push(...nestedEntities)
}
continue
}
// Extract export name
const exportName = await extractExportName(configPath, [
/export\s+const\s+([a-zA-Z]+EntityConfig)\s*[:=]/,
/export\s+const\s+([a-zA-Z]+ChildConfig)\s*[:=]/
])
if (!exportName) continue
// For child entities, extract table name
let actualEntityName = entityName
if (exportName.includes('ChildConfig')) {
const configContent = await readFile(configPath, 'utf-8')
const tableMatch = configContent.match(/table:\s*['"]([^'"]+)['"]/)
if (tableMatch) {
actualEntityName = tableMatch[1]
}
}
// Check for related directories
const componentsPath = join(currentPath, 'components')
const hooksPath = join(currentPath, 'hooks')
const migrationsPath = join(currentPath, 'migrations')
const messagesPath = join(currentPath, 'messages')
const assetsPath = join(currentPath, 'assets')
const hasComponents = existsSync(componentsPath)
const hasHooks = existsSync(hooksPath)
const hasMigrations = existsSync(migrationsPath)
const hasMessages = existsSync(messagesPath)
const hasAssets = existsSync(assetsPath)
// Discover nested child entities
const children = []
const childPath = join(currentPath, 'children')
if (existsSync(childPath)) {
const nestedEntities = await discoverNestedEntities(
childPath,
`${entityName}/children`,
depth + 1,
actualEntityName,
ownerContext
)
entities.push(...nestedEntities)
nestedEntities.forEach(ne => children.push(ne.name))
}
// Build entity metadata
entities.push({
name: actualEntityName,
exportName,
configPath: `${baseImportPath}/${actualConfigFile.replace('.ts', '')}`,
actualConfigFile,
relativePath: entityName,
depth,
parent: parentName,
children,
hasComponents,
hasHooks,
hasMigrations,
hasMessages,
hasAssets,
messagesPath: `${baseImportPath}/messages`,
pluginContext: ownerContext?.type === 'plugin' ? { pluginName: ownerContext.name } : null,
themeContext: ownerContext?.type === 'theme' ? { themeName: ownerContext.name } : null,
source: ownerContext?.type || 'theme'
})
}
return entities
}
Discovery sources:
- Core entities:
core/lib/entities/core/ - Theme entities:
contents/themes/[THEME]/entities/ - Plugin entities:
contents/plugins/[PLUGIN]/entities/
Example output:
{
name: 'tasks',
exportName: 'taskEntityConfig',
configPath: '@/contents/themes/default/entities/tasks/tasks.config',
depth: 0,
parent: null,
children: [],
hasComponents: false,
hasHooks: false,
hasMigrations: true,
hasMessages: true,
hasAssets: false,
messagesPath: '@/contents/themes/default/entities/tasks/messages',
pluginContext: null,
themeContext: { themeName: 'default' },
source: 'theme'
}
Plugin Discovery
Algorithm: Plugin directory scanning + route file discovery
async function discoverPlugins() {
const pluginsDir = join(CONFIG.contentsDir, 'plugins')
const plugins = []
// Scan plugins directory
const entries = await readdir(pluginsDir, { withFileTypes: true })
const pluginDirs = entries.filter(entry => entry.isDirectory())
for (const dir of pluginDirs) {
const pluginName = dir.name
const configPath = join(pluginsDir, pluginName, 'plugin.config.ts')
// Check if plugin.config.ts exists
if (!existsSync(configPath)) continue
// Extract export name
const configContent = await readFile(configPath, 'utf8')
const exportMatch = configContent.match(/export\s+const\s+([a-zA-Z]+PluginConfig)\s*[:=]/)
const exportName = exportMatch ? exportMatch[1] : null
// Check for plugin directories
const componentsPath = join(pluginsDir, pluginName, 'components')
const routesPath = join(pluginsDir, pluginName, 'routes')
const messagesPath = join(pluginsDir, pluginName, 'messages')
const hasComponents = existsSync(componentsPath)
const hasMessages = existsSync(messagesPath)
// Discover route files
let routeFiles = []
if (existsSync(routesPath)) {
routeFiles = await discoverPluginRouteFiles(routesPath, pluginName)
}
// Discover plugin entities
const pluginEntitiesPath = join(pluginsDir, pluginName, 'entities')
let pluginEntities = []
if (existsSync(pluginEntitiesPath)) {
pluginEntities = await discoverNestedEntities(
pluginEntitiesPath,
'',
0,
null,
{ type: 'plugin', name: pluginName }
)
}
plugins.push({
name: pluginName,
exportName,
configPath: `@/contents/plugins/${pluginName}/plugin.config`,
hasComponents,
hasMessages,
componentsPath: hasComponents ? `@/contents/plugins/${pluginName}/components` : null,
messagesPath: hasMessages ? `@/contents/plugins/${pluginName}/messages` : null,
entities: pluginEntities,
routeFiles
})
}
return plugins
}
Route file discovery:
async function discoverPluginRouteFiles(routesPath, pluginName) {
const routeFiles = []
async function scanRouteDir(dirPath, relativePath = '') {
const entries = await readdir(dirPath, { withFileTypes: true })
for (const entry of entries) {
const entryPath = join(dirPath, entry.name)
if (entry.isDirectory()) {
// Recursively scan subdirectories
await scanRouteDir(entryPath, `${relativePath}/${entry.name}`)
} else if (entry.name === 'route.ts') {
// Extract HTTP methods from route.ts
const methods = await extractHttpMethods(entryPath)
routeFiles.push({
path: relativePath,
fullPath: entryPath,
importPath: entryPath.replace(CONFIG.contentsDir + '/', '@/contents/'),
methods,
pluginName
})
}
}
}
await scanRouteDir(routesPath)
return routeFiles
}
Theme Discovery
Algorithm: Active theme or all themes
async function discoverThemes() {
const themesDir = join(CONFIG.contentsDir, 'themes')
const themes = []
// Only process active theme if specified
if (!CONFIG.activeTheme) {
// Process all themes
const entries = await readdir(themesDir, { withFileTypes: true })
const themeDirs = entries.filter(entry => entry.isDirectory())
for (const dir of themeDirs) {
const themeData = await processTheme(dir.name)
if (themeData) themes.push(themeData)
}
} else {
// Process only active theme
const themeData = await processTheme(CONFIG.activeTheme)
if (themeData) themes.push(themeData)
}
return themes
}
async function processTheme(themeName) {
const themesDir = join(CONFIG.contentsDir, 'themes')
const configPath = join(themesDir, themeName, 'theme.config.ts')
if (!existsSync(configPath)) return null
// Extract theme config export name
const configContent = await readFile(configPath, 'utf8')
const exportMatch = configContent.match(/export\s+const\s+([a-zA-Z]+ThemeConfig)\s*[:=]/)
const exportName = exportMatch ? exportMatch[1] : null
// Extract plugin dependencies
const pluginsMatch = configContent.match(/plugins:\s*\[([^\]]+)\]/)
const plugins = pluginsMatch
? pluginsMatch[1].split(',').map(p => p.trim().replace(/['"]/g, '')).filter(Boolean)
: []
// Check for theme directories
const componentsPath = join(themesDir, themeName, 'components')
const stylesPath = join(themesDir, themeName, 'styles')
const assetsPath = join(themesDir, themeName, 'assets')
const messagesPath = join(themesDir, themeName, 'messages')
const hasComponents = existsSync(componentsPath)
const hasStyles = existsSync(stylesPath)
const hasAssets = existsSync(assetsPath)
const hasMessages = existsSync(messagesPath)
// Discover theme entities
const themeEntitiesPath = join(themesDir, themeName, 'entities')
let themeEntities = []
if (existsSync(themeEntitiesPath)) {
themeEntities = await discoverNestedEntities(
themeEntitiesPath,
'',
0,
null,
{ type: 'theme', name: themeName }
)
}
// Discover theme API routes
const themeApiPath = join(themesDir, themeName, 'api')
let themeRouteFiles = []
if (existsSync(themeApiPath)) {
themeRouteFiles = await discoverThemeRouteFiles(themeApiPath, themeName)
}
// Check for dashboard config
const dashboardConfigPath = join(themesDir, themeName, 'dashboard.config.ts')
const hasDashboardConfig = existsSync(dashboardConfigPath)
let dashboardConfigExportName = null
if (hasDashboardConfig) {
dashboardConfigExportName = await extractExportName(dashboardConfigPath, [
/export\s+const\s+([a-zA-Z_]+(?:DASHBOARD_CONFIG|DashboardConfig))\s*[:=]/
])
}
// Check for app config
const appConfigPath = join(themesDir, themeName, 'app.config.ts')
const hasAppConfig = existsSync(appConfigPath)
let appConfigExportName = null
if (hasAppConfig) {
appConfigExportName = await extractExportName(appConfigPath, [
/export\s+const\s+([a-zA-Z_]+(?:APP_CONFIG|AppConfig))\s*[:=]/
])
}
return {
name: themeName,
exportName,
configPath: `@/contents/themes/${themeName}/theme.config`,
hasComponents,
hasStyles,
hasAssets,
hasMessages,
hasDashboardConfig,
dashboardConfigPath: hasDashboardConfig ? `@/contents/themes/${themeName}/dashboard.config` : null,
dashboardConfigExportName,
hasAppConfig,
appConfigPath: hasAppConfig ? `@/contents/themes/${themeName}/app.config` : null,
appConfigExportName,
entities: themeEntities,
routeFiles: themeRouteFiles,
plugins // Plugin dependencies
}
}
Translation Discovery
Algorithm: Locale-based message discovery
async function discoverTranslations() {
const translations = {}
// Discover from core messages
const coreMessagesPath = join(rootDir, 'core', 'messages')
if (existsSync(coreMessagesPath)) {
await discoverTranslationsInPath(coreMessagesPath, 'core', translations)
}
// Discover from theme messages
if (CONFIG.activeTheme) {
const themeMessagesPath = join(CONFIG.contentsDir, 'themes', CONFIG.activeTheme, 'messages')
if (existsSync(themeMessagesPath)) {
await discoverTranslationsInPath(themeMessagesPath, 'theme', translations)
}
}
// Discover from plugin messages
const pluginsDir = join(CONFIG.contentsDir, 'plugins')
if (existsSync(pluginsDir)) {
const pluginDirs = await readdir(pluginsDir, { withFileTypes: true })
for (const dir of pluginDirs.filter(d => d.isDirectory())) {
const pluginMessagesPath = join(pluginsDir, dir.name, 'messages')
if (existsSync(pluginMessagesPath)) {
await discoverTranslationsInPath(pluginMessagesPath, `plugin:${dir.name}`, translations)
}
}
}
return translations
}
Phase 2: Processing & Merging
Merge Priority System
Priority order: Core > Theme > Plugin
function mergeEntities(coreEntities, themeEntities, pluginEntities) {
const merged = {}
// 1. Add plugin entities (lowest priority)
for (const entity of pluginEntities) {
merged[entity.name] = {
...entity,
source: 'plugin',
isCore: false
}
}
// 2. Override with theme entities (medium priority)
for (const entity of themeEntities) {
merged[entity.name] = {
...entity,
source: 'theme',
isCore: false
}
}
// 3. Override with core entities (highest priority)
for (const entity of coreEntities) {
merged[entity.name] = {
...entity,
source: 'core',
isCore: true // Core entities CANNOT be overridden
}
}
return Object.values(merged)
}
Why this matters:
- Core entities (e.g.,
users) are system-critical and cannot be overridden - Theme entities can customize or extend plugin entities
- Plugin entities provide default implementations
Example:
// Plugin provides default 'tasks' entity
Plugin: tasks.config.ts → { label: 'Tasks', features: { create: true } }
// Theme customizes 'tasks' entity
Theme: tasks.config.ts → { label: 'My Tasks', features: { create: true, archive: true } }
// Result: Theme wins
Merged: { label: 'My Tasks', features: { create: true, archive: true }, source: 'theme' }
// But core entities always win
Core: users.config.ts → { isCore: true, ... }
Plugin: users.config.ts → (ignored)
Result: Core users entity used, plugin version ignored
Dependency Resolution
function resolvePluginDependencies(plugins, themes) {
const resolved = new Map()
// Build dependency graph
for (const theme of themes) {
for (const pluginName of theme.plugins) {
const plugin = plugins.find(p => p.name === pluginName)
if (!plugin) {
log(`⚠️ Theme '${theme.name}' depends on plugin '${pluginName}' but it was not found`, 'warning')
continue
}
resolved.set(pluginName, plugin)
}
}
return Array.from(resolved.values())
}
Phase 3: Generation
Entity Registry Generation
async function generateEntityRegistry(entities) {
const timestamp = new Date().toISOString()
let code = `/**
* Auto-generated Entity Registry
*
* Generated at: ${timestamp}
* Entities discovered: ${entities.length}
*
* DO NOT EDIT - This file is auto-generated by scripts/build-registry.mjs
*/
`
// Generate imports
for (const entity of entities) {
code += `import { ${entity.exportName} } from '${entity.configPath}'\n`
}
code += `import type { EntityConfig, ChildEntityDefinition } from '@/core/lib/entities/types'\n\n`
// Generate types
code += `export interface EntityPluginContext {
pluginName: string
}
export interface EntityThemeContext {
themeName: string
}
export interface EntityRegistryEntry {
name: string
config: EntityConfig | ChildEntityDefinition
tableName?: string
relativePath: string
depth: number
parent: string | null
children: string[]
hasComponents: boolean
hasHooks: boolean
hasMigrations: boolean
hasMessages: boolean
hasAssets: boolean
messagesPath: string
pluginContext: EntityPluginContext | null
themeContext: EntityThemeContext | null
isCore?: boolean
source?: 'core' | 'theme' | 'plugin'
}
`
// Generate registry object
code += `export const ENTITY_REGISTRY = {\n`
for (const entity of entities) {
code += ` '${entity.name}': {
name: '${entity.name}',
config: ${entity.exportName},
tableName: ${entity.exportName}.slug || '${entity.name}',
relativePath: '${entity.relativePath}',
depth: ${entity.depth},
parent: ${entity.parent ? `'${entity.parent}'` : 'null'},
children: [${entity.children.map(c => `'${c}'`).join(', ')}],
hasComponents: ${entity.hasComponents},
hasHooks: ${entity.hasHooks},
hasMigrations: ${entity.hasMigrations},
hasMessages: ${entity.hasMessages},
hasAssets: ${entity.hasAssets},
messagesPath: '${entity.messagesPath}',
pluginContext: ${entity.pluginContext ? `{ pluginName: '${entity.pluginContext.pluginName}' }` : 'null'},
themeContext: ${entity.themeContext ? `{ themeName: '${entity.themeContext.themeName}' }` : 'null'},
isCore: ${entity.isCore || false},
source: '${entity.source}' as const
},\n`
}
code += `}\n\n`
// Generate entity name type
code += `export type EntityName = keyof typeof ENTITY_REGISTRY\n\n`
// Generate helper functions
code += generateEntityHelperFunctions()
// Write file
await writeFile(join(CONFIG.outputDir, 'entity-registry.ts'), code)
}
Route Handlers Registry Generation
Zero dynamic imports enforcement:
async function generateRouteHandlersRegistry(themes, plugins) {
let code = `/**
* Auto-generated Route Handlers Registry
*
* ZERO DYNAMIC IMPORTS - All routes statically imported
* This file enables instant route resolution without runtime I/O
*/
`
// Import all theme route files
for (const theme of themes) {
for (const routeFile of theme.routeFiles) {
const importName = generateRouteImportName(theme.name, routeFile.path)
code += `import * as ${importName} from '${routeFile.importPath}'\n`
}
}
// Import all plugin route files
for (const plugin of plugins) {
for (const routeFile of plugin.routeFiles) {
const importName = generateRouteImportName(plugin.name, routeFile.path)
code += `import * as ${importName} from '${routeFile.importPath}'\n`
}
}
code += `\n`
// Generate route handler types
code += `export interface RouteHandler {
GET?: Function
POST?: Function
PUT?: Function
PATCH?: Function
DELETE?: Function
HEAD?: Function
OPTIONS?: Function
}
`
// Generate THEME_ROUTE_HANDLERS
code += `export const THEME_ROUTE_HANDLERS: Record<string, RouteHandler> = {\n`
for (const theme of themes) {
for (const routeFile of theme.routeFiles) {
const importName = generateRouteImportName(theme.name, routeFile.path)
const routeKey = routeFile.path.replace(/^\//, '')
code += ` '${routeKey}': {\n`
for (const method of routeFile.methods) {
code += ` ${method}: ${importName}.${method},\n`
}
code += ` },\n`
}
}
code += `}\n\n`
// Generate PLUGIN_ROUTE_HANDLERS
code += `export const PLUGIN_ROUTE_HANDLERS: Record<string, RouteHandler> = {\n`
for (const plugin of plugins) {
for (const routeFile of plugin.routeFiles) {
const importName = generateRouteImportName(plugin.name, routeFile.path)
const routeKey = `${plugin.name}/${routeFile.path.replace(/^\//, '')}`
code += ` '${routeKey}': {\n`
for (const method of routeFile.methods) {
code += ` ${method}: ${importName}.${method},\n`
}
code += ` },\n`
}
}
code += `}\n\n`
// Generate helper functions
code += `
/**
* Get theme route handler (zero I/O, instant lookup)
*/
export function getThemeRouteHandler(
routeKey: string,
method: string
): Function | undefined {
const handler = THEME_ROUTE_HANDLERS[routeKey]
return handler?.[method as keyof RouteHandler]
}
/**
* Get plugin route handler (zero I/O, instant lookup)
*/
export function getPluginRouteHandler(
routeKey: string,
method: string
): Function | undefined {
const handler = PLUGIN_ROUTE_HANDLERS[routeKey]
return handler?.[method as keyof RouteHandler]
}
`
// Write file
await writeFile(join(CONFIG.outputDir, 'route-handlers.ts'), code)
}
Phase 4: Watch Mode
File Watching
async function watchMode() {
log('Watch mode enabled - monitoring contents/ for changes...', 'info')
let rebuildTimeout = null
// Watch contents directory
watch(CONFIG.contentsDir, { recursive: true }, (eventType, filename) => {
if (!filename) return
// Ignore non-relevant files
if (filename.includes('node_modules')) return
if (filename.includes('.next')) return
if (filename.endsWith('.test.ts')) return
// Debounce rebuilds (300ms)
if (rebuildTimeout) {
clearTimeout(rebuildTimeout)
}
rebuildTimeout = setTimeout(async () => {
log(`Change detected: ${filename}`, 'info')
log('Rebuilding registries...', 'build')
try {
await main() // Rebuild all registries
log('✅ Registry rebuild completed', 'success')
log('⚠️ RESTART DEV SERVER to apply changes', 'warning')
} catch (error) {
log(`❌ Registry rebuild failed: ${error.message}`, 'error')
}
}, 300)
})
log('Watching for changes... (Ctrl+C to stop)', 'info')
}
Debounce Strategy
Why 300ms debounce:
- Prevents multiple rebuilds for rapid changes (e.g., saving multiple files)
- Allows IDEs to finish writing files
- Reduces CPU usage during active development
- Still feels instant to developers
Performance Optimizations
1. Parallel Discovery
// Discover multiple content types in parallel
const [entities, plugins, themes, translations] = await Promise.all([
discoverEntities(),
discoverPlugins(),
discoverThemes(),
discoverTranslations()
])
2. File Caching
// Cache file reads to avoid redundant I/O
const fileCache = new Map()
async function cachedReadFile(path) {
if (fileCache.has(path)) {
return fileCache.get(path)
}
const content = await readFile(path, 'utf8')
fileCache.set(path, content)
return content
}
3. Minimal Parsing
// Use simple regex instead of full AST parsing
// Trade-off: Faster, but less robust for complex configs
const exportMatch = content.match(/export\s+const\s+([a-zA-Z]+Config)\s*[:=]/)
const exportName = exportMatch ? exportMatch[1] : null
4. Conditional Processing
// Only process active theme (not all themes)
if (CONFIG.activeTheme) {
const theme = await processTheme(CONFIG.activeTheme)
} else {
const themes = await processAllThemes()
}
Debugging
Verbose Mode
pnpm registry:build --verbose
Output:
🔍 Discovering content...
Theme: default (components, styles, 2 entities, 3 routes)
Plugin: ai (components, 1 entity, 5 routes)
Plugin: billing (3 routes)
Entity: tasks (config only)
Entity: ai-history (migrations, messages)
✅ Found 2 entities
✅ Found 2 plugins
✅ Found 1 theme
🏗️ Generating registries...
✅ Generated entity-registry.ts (218 lines)
✅ Generated plugin-registry.ts (475 lines)
✅ Generated route-handlers.ts (78 lines)
...
✅ Registry build completed (5.2s)
Common Issues
Issue: "No entities found"
Causes:
- No .config.ts files in contents/themes/[THEME]/entities/
- Export name doesn't match pattern (EntityConfig or ChildConfig)
- File permissions prevent reading
Debug:
- Run with --verbose to see discovered files
- Check file naming conventions
- Verify export statements in config files
Issue: "Registry build failed"
Causes:
- Syntax errors in config files
- Missing required imports
- Circular dependencies
Debug:
- Check error stack trace
- Validate config file syntax
- Run TypeScript type checking
Issue: "Registry changes not reflected"
Cause:
- Registries imported at app initialization
- Hot reload doesn't reload imports
Solution:
- Restart dev server (Ctrl+C → pnpm dev)
Output Structure
Generated File Example
entity-registry.ts (~218 lines):
/**
* Auto-generated Entity Registry
*
* Generated at: 2025-11-19T20:19:36.833Z
* Entities discovered: 2
*
* DO NOT EDIT - This file is auto-generated by scripts/build-registry.mjs
*/
import { aiHistoryEntityConfig } from '@/contents/plugins/ai/entities/ai-history/ai-history.config'
import { taskEntityConfig } from '@/contents/themes/default/entities/tasks/tasks.config'
import type { EntityConfig, ChildEntityDefinition } from '@/core/lib/entities/types'
export interface EntityRegistryEntry {
name: string
config: EntityConfig | ChildEntityDefinition
tableName?: string
relativePath: string
depth: number
parent: string | null
children: string[]
hasComponents: boolean
hasHooks: boolean
hasMigrations: boolean
hasMessages: boolean
hasAssets: boolean
messagesPath: string
pluginContext: EntityPluginContext | null
themeContext: EntityThemeContext | null
isCore?: boolean
source?: 'core' | 'theme' | 'plugin'
}
export const ENTITY_REGISTRY = {
'ai-history': {
name: 'ai-history',
config: aiHistoryEntityConfig,
tableName: 'ai-history',
// ... metadata
},
'tasks': {
name: 'tasks',
config: taskEntityConfig,
tableName: 'tasks',
// ... metadata
}
}
export type EntityName = keyof typeof ENTITY_REGISTRY
// 17 helper functions...
export function getRegisteredEntities() { /* ... */ }
export function getEntity(name: EntityName) { /* ... */ }
// ...
Summary
Build registry script is:
- ✅ 5,208 lines of discovery and generation logic
- ✅ 4-phase pipeline (Discovery → Processing → Generation → Watch)
- ✅ 16 registry files output to
core/lib/registries/ - ✅ ~5-10 seconds build time (one-time cost)
- ✅ Zero dynamic imports in generated code
- ✅ Merge priority system (Core > Theme > Plugin)
- ✅ Watch mode for automatic rebuilds
- ✅ Verbose logging for debugging
Key takeaways:
- Script runs at build time, NOT runtime
- Discovers ALL content in contents/ directory
- Generates static TypeScript imports
- Enables ~17,255x performance improvement
- Requires server restart for registry changes
- Never edit generated files manually
Next: Entity Registry - Deep dive into most common registry
Last Updated: 2025-11-19 Version: 1.0.0 Status: Complete Script Size: 5,208 lines (164KB) Generated Files: 16 registries