/** * React Stage 2 — Generates hooks + context providers from Stage 1 output. * * Generated providers subscribe to the runtime kernel for state. * The kernel owns data, status, and error. React just renders. */ 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, useRef, useSyncExternalStore, type ReactNode } from 'react'", "import { registerContext, mizanFetch, mizanCall, type ContextState } 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('') } // ── Helper hook: subscribe to kernel state ────────────────────────── lines.push('// Subscribe to kernel state via useSyncExternalStore') lines.push('function useContextState(') lines.push(' name: string,') lines.push(' params: Record,') lines.push(' fetchFn: () => Promise,') lines.push(' initialData?: T,') lines.push('): ContextState {') lines.push(' const ref = useRef | null>(null)') lines.push('') lines.push(' if (!ref.current) {') lines.push(' ref.current = registerContext(name, params, fetchFn, initialData)') lines.push(' }') lines.push('') lines.push(' const handle = ref.current') lines.push('') lines.push(' // Fetch on mount if no data') lines.push(' useEffect(() => {') lines.push(" if (handle.getState().status === 'idle') handle.refetch()") lines.push(' return () => handle.unregister()') lines.push(' }, [handle])') lines.push('') lines.push(' return useSyncExternalStore(') lines.push(' handle.subscribe,') lines.push(' handle.getState,') lines.push(' handle.getState,') lines.push(' )') lines.push('}') lines.push('') // ── Mutation hook helper ──────────────────────────────────────────── lines.push('// Mutation hook with loading/error state') lines.push('function useMutation(') lines.push(' callFn: (args: TArgs) => Promise,') lines.push('): { mutate: (args: TArgs) => Promise; isPending: boolean; error: Error | null } {') lines.push(' const [isPending, setIsPending] = useState(false)') lines.push(' const [error, setError] = useState(null)') lines.push('') lines.push(' const mutate = useCallback(async (args: TArgs) => {') lines.push(' setIsPending(true)') lines.push(' setError(null)') lines.push(' try {') lines.push(' const result = await callFn(args)') lines.push(' return result') lines.push(' } catch (e) {') lines.push(' setError(e as Error)') lines.push(' throw e') lines.push(' } finally {') lines.push(' setIsPending(false)') lines.push(' }') lines.push(' }, [callFn])') lines.push('') lines.push(' return { mutate, isPending, error }') lines.push('}') lines.push('') // ── Context hooks ─────────────────────────────────────────────────── 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('') // Hook that returns the full kernel state if (paramEntries.length > 0) { lines.push(`export function use${p}Context(params: ${p}ContextParams): ContextState<${p}ContextData> {`) lines.push(` const ssrData = typeof window !== 'undefined' ? (window as any).__MIZAN_SSR_DATA__ : null`) lines.push(` return useContextState('${ctxName}', params, () => fetch${p}Context(params), ssrData)`) } else { lines.push(`export function use${p}Context(): ContextState<${p}ContextData> {`) lines.push(` const ssrData = typeof window !== 'undefined' ? (window as any).__MIZAN_SSR_DATA__ : null`) lines.push(` return useContextState('${ctxName}', {}, () => fetch${p}Context({} as any), ssrData)`) } lines.push('}') lines.push('') // Convenience hooks for individual data fields for (const fn of ctxFunctions) { const hookPascal = pascalCase(fn.camelName) if (paramEntries.length > 0) { lines.push(`export function use${hookPascal}(params: ${p}ContextParams): ${fn.outputType} | null {`) lines.push(` const state = use${p}Context(params)`) } else { lines.push(`export function use${hookPascal}(): ${fn.outputType} | null {`) lines.push(` const state = use${p}Context()`) } lines.push(` return state.data?.${fn.name} ?? null`) lines.push('}') lines.push('') } } // ── Mutation hooks (with loading/error) ────────────────────────────── for (const fn of mutations) { const p = pascalCase(fn.camelName) if (fn.hasInput) { lines.push(`export function use${p}() {`) lines.push(` return useMutation[0], Awaited>>(call${p})`) lines.push('}') } else { lines.push(`export function use${p}() {`) lines.push(` return useMutation>>(() => call${p}() as any)`) 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 useMutation[0], Awaited>>(call${p})`) lines.push('}') } else { lines.push(`export function use${p}() {`) lines.push(` return useMutation>>(() => call${p}() as any)`) lines.push('}') } lines.push('') } // ── Re-export runtime types ───────────────────────────────────────── lines.push("export type { ContextState } from '@mizan/runtime'") lines.push("export { configure, initSession, MizanError } from '@mizan/runtime'") lines.push('') return lines.join('\n') }