#!/usr/bin/env node /** * mizan Code Generator CLI * * Two-stage codegen: * Stage 1: Framework-agnostic types + fetch/mutation functions * Stage 2: Framework-specific wrappers (React hooks, Vue composables, Svelte stores) * * Usage: * npx mizan-generate # React (default) * npx mizan-generate --target vue # Vue * npx mizan-generate --target react,vue,svelte # All three */ import { promises as fs } from 'fs' import path from 'path' import { fetchChannelsSchema, fetchMizanSchema } from './lib/fetch.mjs' import { generateTypes, generateContextFile, generateMutationFile, generateFunctionFile, generateStage1Index } from './lib/stage1.mjs' import { generateReactAdapter } from './lib/adapters/react.mjs' import { generateVueAdapter } from './lib/adapters/vue.mjs' import { generateSvelteAdapter } from './lib/adapters/svelte.mjs' import { generateChannelsFiles } from './lib/channels.mjs' const frontendDir = process.cwd() async function loadConfig(configPath) { const fullPath = path.resolve(frontendDir, configPath) try { await fs.access(fullPath) } catch { throw new Error(`Config not found: ${fullPath}`) } const fileUrl = new URL(`file://${fullPath.replace(/\\/g, '/')}`) const module = await import(fileUrl) return module.default } async function writeOutput(filePath, content) { const dir = path.dirname(filePath) await fs.mkdir(dir, { recursive: true }) await fs.writeFile(filePath, content, 'utf8') } function pascalCase(str) { return str.split(/[.\-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('') } async function generate(config, options = {}) { const { output, target: targetFlag } = options const outputDir = output || config.output || 'src/api' const targets = (targetFlag || config.target || 'react').split(',').map(t => t.trim()) console.log(`[mizan] Starting generation (targets: ${targets.join(', ')})...`) const fullOutputDir = path.resolve(frontendDir, outputDir) let mizanSchema = null let channelsSchema = null // ── Channels (React-only for now) ─────────────────────────────────── try { console.log('[mizan] Fetching channels schema...') channelsSchema = await fetchChannelsSchema(config.source, frontendDir) const channelCount = channelsSchema['x-mizan-channels']?.length || 0 if (channelCount > 0 && targets.includes('react')) { console.log(`[mizan] Found ${channelCount} channels`) const { types, hooks } = await generateChannelsFiles(channelsSchema) await writeOutput(path.join(fullOutputDir, 'channels.ts'), types) if (hooks) await writeOutput(path.join(fullOutputDir, 'channels.hooks.tsx'), hooks) } } catch (err) { console.log(`[mizan] Channels not available: ${err.message}`) } // ── Mizan functions ───────────────────────────────────────────────── try { console.log('[mizan] Fetching mizan schema...') mizanSchema = await fetchMizanSchema(config.source, frontendDir) const functions = mizanSchema['x-mizan-functions'] || [] const contextGroups = mizanSchema['x-mizan-contexts'] || {} if (functions.length === 0) { console.log('[mizan] No functions registered') return } console.log(`[mizan] Found ${functions.length} functions`) // ── Stage 1: Framework-agnostic ───────────────────────────────── // Types const types = await generateTypes(mizanSchema) await writeOutput(path.join(fullOutputDir, 'types.ts'), types) console.log('[mizan] Stage 1 -> types.ts') // Context files await fs.mkdir(path.join(fullOutputDir, 'contexts'), { recursive: true }) for (const [ctxName, ctxMeta] of Object.entries(contextGroups)) { const content = generateContextFile(ctxName, ctxMeta, functions) await writeOutput(path.join(fullOutputDir, 'contexts', `${ctxName}.ts`), content) console.log(`[mizan] Stage 1 -> contexts/${ctxName}.ts`) } // Mutation + function files const regularFns = functions.filter(fn => !fn.isContext && !fn.isForm) if (regularFns.length > 0) { await fs.mkdir(path.join(fullOutputDir, 'mutations'), { recursive: true }) await fs.mkdir(path.join(fullOutputDir, 'functions'), { recursive: true }) for (const fn of regularFns) { const dir = fn.affects ? 'mutations' : 'functions' const content = fn.affects ? generateMutationFile(fn) : generateFunctionFile(fn) await writeOutput(path.join(fullOutputDir, dir, `${fn.camelName}.ts`), content) console.log(`[mizan] Stage 1 -> ${dir}/${fn.camelName}.ts`) } } // Stage 1 index const stage1Index = generateStage1Index(mizanSchema) await writeOutput(path.join(fullOutputDir, 'index.ts'), stage1Index) console.log('[mizan] Stage 1 -> index.ts') // ── Stage 2: Framework-specific ───────────────────────────────── for (const target of targets) { let content let filename switch (target) { case 'react': content = generateReactAdapter(mizanSchema) filename = 'react.tsx' break case 'vue': content = generateVueAdapter(mizanSchema) filename = 'vue.ts' break case 'svelte': content = generateSvelteAdapter(mizanSchema) filename = 'svelte.ts' break default: console.warn(`[mizan] Unknown target: ${target}`) continue } if (content) { await writeOutput(path.join(fullOutputDir, filename), content) console.log(`[mizan] Stage 2 -> ${filename}`) } } // Schema JSON await writeOutput( path.join(fullOutputDir, 'schema.json'), JSON.stringify(mizanSchema, null, 2), ) } catch (err) { console.log(`[mizan] Schema not available: ${err.message}`) } console.log('[mizan] Generation complete!') } async function main() { const args = process.argv.slice(2) let configPath = 'django.config.mjs' let watchMode = false let output = null let target = 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] === '--target' || args[i] === '-t') target = args[++i] else if (args[i] === '--help' || args[i] === '-h') { console.log(` mizan Code Generator Usage: npx mizan-generate [options] Options: -c, --config Config file (default: django.config.mjs) -t, --target Comma-separated: react,vue,svelte (default: react) -o, --output Output directory (default: src/api) -w, --watch Watch mode -h, --help Show help `) process.exit(0) } } const config = await loadConfig(configPath) const options = { output, target } if (watchMode) { await generate(config, options) console.log('[mizan] Watching for changes...') const { watch: chokidarWatch } = await import('chokidar') if (config.source.django) { const djangoDir = path.resolve(frontendDir, path.dirname(config.source.django.managePath)) let timeout = null const watcher = chokidarWatch([path.join(djangoDir, '**/*.py')], { ignored: ['**/node_modules/**', '**/__pycache__/**', '**/migrations/**'], ignoreInitial: true, }) watcher.on('change', () => { if (timeout) clearTimeout(timeout) timeout = setTimeout(() => generate(config, options), 1000) }) } process.on('SIGINT', () => process.exit(0)) } else { await generate(config, options) } } main().catch(err => { console.error('[mizan] Error:', err.message) process.exit(1) })