/** * Vue Stage 2 — Generates composables from Stage 1 output. */ function pascalCase(str) { return str.split(/[.\-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('') } export function generateVueAdapter(schema) { const functions = schema['x-mizan-functions'] || [] const contextGroups = schema['x-mizan-contexts'] || {} const mutations = functions.filter(fn => !fn.isContext && !fn.isForm && fn.affects) const plainFns = functions.filter(fn => !fn.isContext && !fn.isForm && !fn.affects) const lines = [ '// AUTO-GENERATED by mizan — do not edit', '', "import { ref, computed, watch, onMounted, onUnmounted, provide, inject, type Ref, type ComputedRef, type InjectionKey } from 'vue'", "import { registerContext } from '@mizan/runtime'", '', ] // Stage 1 imports const stage1Imports = [] for (const [ctxName] of Object.entries(contextGroups)) { const p = pascalCase(ctxName) stage1Imports.push(`fetch${p}Context`, `type ${p}ContextData`, `type ${p}ContextParams`) } for (const fn of [...mutations, ...plainFns]) { stage1Imports.push(`call${pascalCase(fn.camelName)}`) } if (stage1Imports.length > 0) { lines.push(`import { ${stage1Imports.join(', ')} } from '../index'`) lines.push('') } // ── Context composables ───────────────────────────────────────────── for (const [ctxName, ctxMeta] of Object.entries(contextGroups)) { const p = pascalCase(ctxName) const ctxFunctions = functions.filter(fn => fn.isContext === ctxName) const paramEntries = Object.entries(ctxMeta.params || {}) lines.push(`// ${p} context`) lines.push(`const ${p}Key: InjectionKey<{ data: Ref<${p}ContextData | null>, loading: Ref }> = Symbol('${ctxName}')`) lines.push('') // Provider composable if (paramEntries.length > 0) { lines.push(`export function provide${p}Context(params: { ${paramEntries.map(([k, v]) => `${k}: ${v.type === 'integer' || v.type === 'number' ? 'number' : 'string'}`).join(', ')} }) {`) } else { lines.push(`export function provide${p}Context() {`) } lines.push(` const data = ref<${p}ContextData | null>(null)`) lines.push(` const loading = ref(true)`) lines.push('') lines.push(` const refetch = async () => {`) lines.push(` loading.value = true`) lines.push(` try {`) if (paramEntries.length > 0) { lines.push(` data.value = await fetch${p}Context(params as any)`) } else { lines.push(` data.value = await fetch${p}Context({} as any)`) } lines.push(` } catch (e) { console.error('[mizan] ${ctxName} fetch failed:', e) }`) lines.push(` loading.value = false`) lines.push(` }`) lines.push('') lines.push(` let unregister: (() => void) | null = null`) lines.push(` onMounted(() => {`) lines.push(` refetch()`) if (paramEntries.length > 0) { lines.push(` unregister = registerContext('${ctxName}', params, refetch)`) } else { lines.push(` unregister = registerContext('${ctxName}', {}, refetch)`) } lines.push(` })`) lines.push(` onUnmounted(() => { unregister?.() })`) lines.push('') lines.push(` provide(${p}Key, { data, loading })`) lines.push('}') lines.push('') // Consumer composables for (const fn of ctxFunctions) { const hookPascal = pascalCase(fn.camelName) lines.push(`export function use${hookPascal}(): ComputedRef<${fn.outputType} | null> {`) lines.push(` const ctx = inject(${p}Key)`) lines.push(` if (!ctx) throw new Error('use${hookPascal} requires provide${p}Context in a parent')`) lines.push(` return computed(() => ctx.data.value?.${fn.name} ?? null)`) lines.push('}') lines.push('') } } // ── Mutation composables ──────────────────────────────────────────── for (const fn of [...mutations, ...plainFns]) { const p = pascalCase(fn.camelName) lines.push(`export const use${p} = call${p}`) lines.push('') } return lines.join('\n') }