#!/usr/bin/env node /** * mizan Code Generator CLI * * Generate TypeScript types, React provider, and hooks from Django schemas. * * Usage: * npx mizan-generate # Run once * npx mizan-generate --watch # Watch mode */ import { promises as fs } from 'fs' import path from 'path' import { fetchChannelsSchema, fetchMizanSchema } from './lib/fetch.mjs' import { generateMizanFiles } from './lib/mizan.mjs' import { generateChannelsFiles } from './lib/channels.mjs' import { generateIndex } from './lib/index.mjs' // Use cwd — the script runs via `npx mizan-generate` from the frontend root const frontendDir = process.cwd() /** * Load configuration from django.config.mjs */ async function loadConfig(configPath) { const fullPath = path.resolve(frontendDir, configPath) try { await fs.access(fullPath) } catch { throw new Error(`Config file not found: ${fullPath}`) } // Convert to file:// URL for Windows compatibility const fileUrl = new URL(`file://${fullPath.replace(/\\/g, '/')}`) if (configPath.endsWith('.mjs') || configPath.endsWith('.js')) { const module = await import(fileUrl) return module.default } if (configPath.endsWith('.ts')) { try { const module = await import(fileUrl) return module.default } catch { throw new Error( `Cannot load TypeScript config directly. Either:\n` + ` 1. Install tsx: npm install -D tsx\n` + ` 2. Use django.config.mjs instead` ) } } throw new Error(`Unsupported config file format: ${configPath}`) } /** * Write generated code to file. */ async function writeOutput(filePath, content) { const dir = path.dirname(filePath) await fs.mkdir(dir, { recursive: true }) await fs.writeFile(filePath, content, 'utf8') } /** * Run schema generation. */ async function generate(config, options = {}) { const { output } = options console.log('[mizan] Starting schema generation...') const outputPath = output || config.output || 'src/api/generated.ts' let channelsSchema = null let mizanSchema = null // Fetch and generate channels if available try { console.log('[mizan] Fetching channels schema...') channelsSchema = await fetchChannelsSchema(config.source, frontendDir) const channelCount = channelsSchema['x-mizan-channels']?.length || 0 if (channelCount > 0) { console.log(`[mizan] Found ${channelCount} channels`) const channelsTypesPath = outputPath.replace(/\.ts$/, '.channels.ts') const fullChannelsTypesPath = path.resolve(frontendDir, channelsTypesPath) const channelsHooksPath = outputPath.replace(/\.ts$/, '.channels.hooks.tsx') const fullChannelsHooksPath = path.resolve(frontendDir, channelsHooksPath) const channelsSchemaPath = outputPath.replace(/\.ts$/, '.channels.schema.json') const fullChannelsSchemaPath = path.resolve(frontendDir, channelsSchemaPath) const { types: channelsTypes, hooks: channelsHooks } = await generateChannelsFiles(channelsSchema) console.log(`[mizan] Generating -> ${channelsTypesPath}`) await writeOutput(fullChannelsTypesPath, channelsTypes) if (channelsHooks) { console.log(`[mizan] Generating -> ${channelsHooksPath}`) await writeOutput(fullChannelsHooksPath, channelsHooks) } console.log(`[mizan] Generating -> ${channelsSchemaPath}`) await writeOutput(fullChannelsSchemaPath, JSON.stringify(channelsSchema, null, 2)) } else { console.log('[mizan] No channels registered, skipping channels generation') } } catch (err) { console.log(`[mizan] Channels schema not available: ${err.message}`) } // Fetch and generate mizan files try { console.log('[mizan] Fetching mizan schema...') mizanSchema = await fetchMizanSchema(config.source, frontendDir) const functionCount = mizanSchema['x-mizan-functions']?.length || 0 if (functionCount > 0) { console.log(`[mizan] Found ${functionCount} mizan functions`) const mizanTypesPath = outputPath.replace(/\.ts$/, '.mizan.ts') const fullMizanTypesPath = path.resolve(frontendDir, mizanTypesPath) const mizanProviderPath = outputPath.replace(/\.ts$/, '.provider.tsx') const fullMizanProviderPath = path.resolve(frontendDir, mizanProviderPath) const mizanServerPath = outputPath.replace(/\.ts$/, '.server.ts') const fullMizanServerPath = path.resolve(frontendDir, mizanServerPath) const mizanFormsPath = outputPath.replace(/\.ts$/, '.forms.ts') const fullMizanFormsPath = path.resolve(frontendDir, mizanFormsPath) const mizanSchemaPath = outputPath.replace(/\.ts$/, '.mizan.schema.json') const fullMizanSchemaPath = path.resolve(frontendDir, mizanSchemaPath) const hasChannels = (channelsSchema?.['x-mizan-channels']?.length || 0) > 0 const { types: mizanTypes, provider: mizanProvider, server: mizanServer, forms: mizanForms } = await generateMizanFiles(mizanSchema, { hasChannels }) console.log(`[mizan] Generating -> ${mizanTypesPath}`) await writeOutput(fullMizanTypesPath, mizanTypes) if (mizanProvider) { console.log(`[mizan] Generating -> ${mizanProviderPath}`) await writeOutput(fullMizanProviderPath, mizanProvider) } if (mizanServer) { console.log(`[mizan] Generating -> ${mizanServerPath}`) await writeOutput(fullMizanServerPath, mizanServer) } if (mizanForms) { console.log(`[mizan] Generating -> ${mizanFormsPath}`) await writeOutput(fullMizanFormsPath, mizanForms) } console.log(`[mizan] Generating -> ${mizanSchemaPath}`) await writeOutput(fullMizanSchemaPath, JSON.stringify(mizanSchema, null, 2)) } else { console.log('[mizan] No mizan functions registered, skipping mizan generation') } } catch (err) { console.log(`[mizan] mizan schema not available: ${err.message}`) } // Generate consolidated index.ts const indexPath = path.dirname(outputPath) + '/index.ts' const fullIndexPath = path.resolve(frontendDir, indexPath) console.log(`[mizan] Generating -> ${indexPath}`) const indexContent = generateIndex({ channelsSchema, mizanSchema, }) await writeOutput(fullIndexPath, indexContent) console.log('[mizan] Generation complete!') } /** * Watch for changes and regenerate. */ async function watch(config, options) { const debounce = config.watch?.debounce || 1000 let timeout = null let running = false async function runGenerate() { if (running) { timeout = setTimeout(runGenerate, debounce) return } running = true try { await generate(config, options) } catch (err) { console.error('[mizan] Generation failed:', err.message) } finally { running = false } } await runGenerate() console.log('[mizan] Watching for changes (press Ctrl+C to stop)...') if (config.source.django) { const { watch: chokidarWatch } = await import('chokidar') const djangoDir = path.resolve(frontendDir, path.dirname(config.source.django.managePath)) const watcher = chokidarWatch([ path.join(djangoDir, '**/*.py'), ], { ignored: [ '**/node_modules/**', '**/__pycache__/**', '**/migrations/**', '**/.venv/**', ], ignoreInitial: true, }) watcher.on('change', (filePath) => { console.log(`[mizan] Detected change: ${path.relative(djangoDir, filePath)}`) if (timeout) clearTimeout(timeout) timeout = setTimeout(runGenerate, debounce) }) } process.on('SIGINT', () => { console.log('\n[mizan] Stopping watch mode...') process.exit(0) }) } /** * Main entry point. */ async function main() { const args = process.argv.slice(2) let configPath = 'django.config.mjs' let watchMode = false let output = null for (let i = 0; i < args.length; i++) { if (args[i] === '--config' || args[i] === '-c') { configPath = args[++i] } else if (args[i] === '--watch' || args[i] === '-w') { watchMode = true } else if (args[i] === '--output' || args[i] === '-o') { output = args[++i] } else if (args[i] === '--help' || args[i] === '-h') { console.log(` mizan Code Generator - Generate TypeScript from Django schemas Usage: npx mizan-generate [options] Options: -c, --config Config file path (default: django.config.mjs) -w, --watch Watch mode - regenerate on changes -o, --output Output file path (overrides config) -h, --help Show this help message `) process.exit(0) } } const config = await loadConfig(configPath) const options = { output } if (watchMode) { await watch(config, options) } else { await generate(config, options) } } main().catch(err => { console.error('[mizan] Error:', err.message) process.exit(1) })