/** * React Stage 2 — Generates hooks + context providers from Stage 1 output. */ function pascalCase(str) { return str.split(/[.\-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('') } export function generateReactAdapter(schema) { const functions = schema['x-mizan-functions'] || [] const contextGroups = schema['x-mizan-contexts'] || {} const namedContexts = Object.entries(contextGroups).filter(([n]) => n !== 'global') const globalContexts = functions.filter(fn => fn.isContext === 'global') const mutations = functions.filter(fn => !fn.isContext && !fn.isForm && fn.affects) const plainFns = functions.filter(fn => !fn.isContext && !fn.isForm && !fn.affects) const lines = [ "'use client'", '', '// AUTO-GENERATED by mizan — do not edit', '', "import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'", "import { registerContext, mizanCall, mizanFetch } from '@mizan/runtime'", '', ] // Import from Stage 1 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('') } // ── Global context hooks ──────────────────────────────────────────── if (globalContexts.length > 0) { const p = pascalCase('global') lines.push(`// Global context — fetched once at app init`) lines.push(`const GlobalCtx = createContext<${p}ContextData | null>(null)`) lines.push('') lines.push(`export function GlobalContextProvider({ children }: { children: ReactNode }) {`) lines.push(` const [data, setData] = useState<${p}ContextData | null>(() => {`) lines.push(` if (typeof window === 'undefined') return null`) lines.push(` const ssr = (window as any).__MIZAN_SSR_DATA__`) lines.push(` return ssr ?? null`) lines.push(` })`) lines.push('') lines.push(` const refetch = useCallback(async () => {`) lines.push(` const result = await fetch${p}Context({} as any)`) lines.push(` setData(result)`) lines.push(` }, [])`) lines.push('') lines.push(` useEffect(() => { if (!data) refetch() }, [data, refetch])`) lines.push(` useEffect(() => registerContext('global', {}, refetch), [refetch])`) lines.push('') lines.push(` return {children}`) lines.push('}') lines.push('') for (const fn of globalContexts) { const hookPascal = pascalCase(fn.camelName) lines.push(`export function use${hookPascal}(): ${fn.outputType} {`) lines.push(` const ctx = useContext(GlobalCtx)`) lines.push(` if (!ctx) throw new Error('use${hookPascal} requires GlobalContextProvider')`) lines.push(` return ctx.${fn.name}`) lines.push('}') lines.push('') } } // ── Named context providers ───────────────────────────────────────── for (const [ctxName, ctxMeta] of namedContexts) { 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}Ctx = createContext<${p}ContextData | null>(null)`) lines.push('') // Provider lines.push(`export function ${p}Context({ children, ...params }: ${p}ContextParams & { children: ReactNode }) {`) lines.push(` const [data, setData] = useState<${p}ContextData | null>(() => {`) lines.push(` if (typeof window === 'undefined') return null`) lines.push(` const ssr = (window as any).__MIZAN_SSR_DATA__`) if (ctxFunctions.length > 0) { lines.push(` if (ssr?.${ctxFunctions[0].name} !== undefined) return ssr`) } lines.push(` return null`) lines.push(` })`) lines.push('') lines.push(` const refetch = useCallback(async () => {`) lines.push(` const result = await fetch${p}Context(params)`) lines.push(` setData(result)`) const deps = paramEntries.map(([pName]) => `params.${pName}`) lines.push(` }, [${deps.join(', ')}])`) lines.push('') lines.push(` useEffect(() => { if (!data) refetch() }, [data, refetch])`) lines.push(` useEffect(() => registerContext('${ctxName}', params, refetch), [${deps.join(', ')}, refetch])`) lines.push('') lines.push(` return <${p}Ctx.Provider value={data}>{children}`) lines.push('}') lines.push('') // Hooks for (const fn of ctxFunctions) { const hookPascal = pascalCase(fn.camelName) lines.push(`export function use${hookPascal}(): ${fn.outputType} | null {`) lines.push(` const ctx = useContext(${p}Ctx)`) lines.push(` return ctx?.${fn.name} ?? null`) lines.push('}') lines.push('') } } // ── Mutation hooks ────────────────────────────────────────────────── for (const fn of mutations) { const p = pascalCase(fn.camelName) if (fn.hasInput) { lines.push(`export function use${p}() {`) lines.push(` return useCallback((args: Parameters[0]) => call${p}(args), [])`) lines.push('}') } else { lines.push(`export function use${p}() {`) lines.push(` return useCallback(() => call${p}(), [])`) lines.push('}') } lines.push('') } // ── Plain function hooks ──────────────────────────────────────────── for (const fn of plainFns) { const p = pascalCase(fn.camelName) if (fn.hasInput) { lines.push(`export function use${p}() {`) lines.push(` return useCallback((args: Parameters[0]) => call${p}(args), [])`) lines.push('}') } else { lines.push(`export function use${p}() {`) lines.push(` return useCallback(() => call${p}(), [])`) lines.push('}') } lines.push('') } return lines.join('\n') }