packages/ flattens into: backends/ server protocol adapters (mizan-django, mizan-ts) frontends/ client kernel + framework adapters (mizan-base, mizan-react, mizan-vue, mizan-svelte) workers/ runtime workers (mizan-ssr) cores/ shared language-level primitives (empty for now; mizan-python forthcoming) The frontend kernel (was packages/mizan-runtime, now frontends/mizan-base) is renamed to reflect its role — it's the shared base that frontend adapters depend on directly. Reflects the substrate position that per-framework adapters wrap a single shared kernel; codegen targets the adapter, not the raw kernel. Path updates landed in: Makefile, two Gitea workflows, Dockerfile.test, four example/harness config files, .claude/settings.local.json, four docs (CLAUDE/ISSUES/ROADMAP/AFI_ARCHITECTURE), four codegen templates (stage1 + react/vue/svelte adapters), and three package.jsons (the mizan-base rename plus mizan-vue/svelte peerDeps). Generated files under examples/django-react-site/harness/src/api/ still reference @mizan/runtime — left as-is; they're regenerated artifacts and the harness is non-functional pending the React wrapper-layer codegen. Also folded in a pre-existing fix: the Gitea workflows had working-directory: react / django pointing at a layout that predates packages/, never updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
224 lines
8.6 KiB
JavaScript
Executable File
224 lines
8.6 KiB
JavaScript
Executable File
#!/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 <path> Config file (default: django.config.mjs)
|
|
-t, --target <targets> Comma-separated: react,vue,svelte (default: react)
|
|
-o, --output <dir> 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)
|
|
})
|