diff --git a/backends/mizan-django/generate/generator/cli.mjs b/backends/mizan-django/generate/generator/cli.mjs index f7cb625..d9158ae 100755 --- a/backends/mizan-django/generate/generator/cli.mjs +++ b/backends/mizan-django/generate/generator/cli.mjs @@ -148,6 +148,18 @@ async function generate(config, options = {}) { } } + // Append Stage 2 re-exports to index.ts so `import { useEcho, MizanContext } from './api'` works + const adapterExports = targets + .map(t => ({ react: 'react', vue: 'vue', svelte: 'svelte' })[t]) + .filter(Boolean) + .map(name => `export * from './${name}'`) + .join('\n') + if (adapterExports) { + const indexPath = path.join(fullOutputDir, 'index.ts') + const existing = await fs.readFile(indexPath, 'utf8') + await writeOutput(indexPath, `${existing}\n// Stage 2 framework adapter\n${adapterExports}\n`) + } + // Schema JSON await writeOutput( path.join(fullOutputDir, 'schema.json'), diff --git a/backends/mizan-django/generate/generator/lib/adapters/react.mjs b/backends/mizan-django/generate/generator/lib/adapters/react.mjs index b2f5442..4d4cc86 100644 --- a/backends/mizan-django/generate/generator/lib/adapters/react.mjs +++ b/backends/mizan-django/generate/generator/lib/adapters/react.mjs @@ -1,8 +1,23 @@ /** - * React Stage 2 — Generates hooks + context providers from Stage 1 output. + * React Stage 2 — Generates idiomatic React providers + hooks on top of the kernel. * - * Generated providers subscribe to the runtime kernel for state. - * The kernel owns data, status, and error. React just renders. + * The kernel (@mizan/base) owns data, status, error. This adapter wraps each + * registered context in a React Provider component so kernel subscription happens + * once per provider mount, and consumer hooks read from React Context. + * + * Output shape: + * root — calls configure(), auto-mounts global + * per-named-context provider + * + * + * + * + * useGlobalContext() full ContextState + * useCurrentUser() convenience: data field + * useUserContext() full ContextState + * useUserProfile() convenience: data field + * useEcho() mutation/plain — { mutate, isPending, error } + * useMizan() escape hatch — { call, fetch } */ function pascalCase(str) { @@ -13,21 +28,42 @@ 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 hasGlobal = !!contextGroups.global + const globalFns = 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 = [ + const lines = [] + + // ── Header + imports ───────────────────────────────────────────────── + + lines.push( "'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/base'", + "import {", + " createContext,", + " useCallback,", + " useContext,", + " useEffect,", + " useRef,", + " useState,", + " useSyncExternalStore,", + " type ReactNode,", + "} from 'react'", + "import {", + " configure,", + " initSession,", + " mizanCall,", + " mizanFetch,", + " MizanError,", + " registerContext,", + " type ContextState,", + "} from '@mizan/base'", '', - ] + ) - // Import from Stage 1 const stage1Imports = [] for (const [ctxName] of Object.entries(contextGroups)) { const p = pascalCase(ctxName) @@ -37,144 +73,226 @@ export function generateReactAdapter(schema) { stage1Imports.push(`call${pascalCase(fn.camelName)}`) } if (stage1Imports.length > 0) { - lines.push(`import { ${stage1Imports.join(', ')} } from '../index'`) - lines.push('') + lines.push(`import { ${stage1Imports.join(', ')} } from './index'`, '') } - // ── Helper hook: subscribe to kernel state ────────────────────────── + // ── Internal helper: subscribe to kernel state from a Provider ────── - 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('') + lines.push( + '// Internal — runs inside a Provider, registers with the kernel exactly once.', + 'function useContextSubscription(', + ' name: string,', + ' params: Record,', + ' fetchFn: () => Promise,', + ' initialData?: T,', + '): ContextState {', + ' const ref = useRef | null>(null)', + ' if (!ref.current) {', + ' ref.current = registerContext(name, params, fetchFn, initialData)', + ' }', + ' const handle = ref.current', + '', + ' useEffect(() => {', + " if (handle.getState().status === 'idle') handle.refetch()", + ' return () => handle.unregister()', + ' }, [handle])', + '', + ' return useSyncExternalStore(handle.subscribe, handle.getState, handle.getState)', + '}', + '', + ) - // ── Mutation hook helper ──────────────────────────────────────────── + // ── Internal helper: mutation wrapper ─────────────────────────────── - 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('') + lines.push( + '// Internal — wraps an imperative call() with isPending / error state.', + 'interface MutationHook {', + ' mutate: (args: TArgs) => Promise', + ' isPending: boolean', + ' error: Error | null', + '}', + '', + 'function useMutation(', + ' callFn: (args: TArgs) => Promise,', + '): MutationHook {', + ' const [isPending, setIsPending] = useState(false)', + ' const [error, setError] = useState(null)', + '', + ' const mutate = useCallback(async (args: TArgs) => {', + ' setIsPending(true)', + ' setError(null)', + ' try {', + ' return await callFn(args)', + ' } catch (e) {', + ' setError(e as Error)', + ' throw e', + ' } finally {', + ' setIsPending(false)', + ' }', + ' }, [callFn])', + '', + ' return { mutate, isPending, error }', + '}', + '', + ) - // ── Context hooks ─────────────────────────────────────────────────── + // ── Global context provider + hooks ───────────────────────────────── - for (const [ctxName, ctxMeta] of Object.entries(contextGroups)) { + if (hasGlobal) { + lines.push( + '// ── Global Context ──', + '', + 'const GlobalCtx = createContext | null>(null)', + '', + 'export function GlobalContextProvider({ children }: { children: ReactNode }) {', + " const ssrData = typeof window !== 'undefined' ? (window as any).__MIZAN_SSR_DATA__ : undefined", + " const state = useContextSubscription('global', {}, () => fetchGlobalContext({} as any), ssrData)", + ' return {children}', + '}', + '', + 'export function useGlobalContext(): ContextState {', + ' const ctx = useContext(GlobalCtx)', + " if (!ctx) throw new Error('useGlobalContext requires or ')", + ' return ctx', + '}', + '', + ) + + for (const fn of globalFns) { + const p = pascalCase(fn.camelName) + lines.push( + `export function use${p}(): ${fn.outputType} | null {`, + ` return useGlobalContext().data?.${fn.name} ?? null`, + '}', + '', + ) + } + } + + // ── Named context providers + hooks ───────────────────────────────── + + for (const [ctxName, ctxMeta] of namedContexts) { const p = pascalCase(ctxName) const ctxFunctions = functions.filter(fn => fn.isContext === ctxName) - const paramEntries = Object.entries(ctxMeta.params || {}) + const paramKeys = Object.keys(ctxMeta.params || {}) + const hasParams = paramKeys.length > 0 - lines.push(`// ── ${p} Context ──`) - lines.push('') + lines.push( + `// ── ${p} Context ──`, + '', + `const ${p}Ctx = createContext | null>(null)`, + '', + ) - // 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)`) + if (hasParams) { + lines.push( + `export function ${p}Context({ children, ...params }: ${p}ContextParams & { children: ReactNode }) {`, + ` const state = useContextSubscription('${ctxName}', params, () => fetch${p}Context(params))`, + ` return <${p}Ctx.Provider value={state}>{children}`, + '}', + ) } 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( + `export function ${p}Context({ children }: { children: ReactNode }) {`, + ` const state = useContextSubscription('${ctxName}', {}, () => fetch${p}Context({} as any))`, + ` return <${p}Ctx.Provider value={state}>{children}`, + '}', + ) } - lines.push('}') lines.push('') - // Convenience hooks for individual data fields + lines.push( + `export function use${p}Context(): ContextState<${p}ContextData> {`, + ` const ctx = useContext(${p}Ctx)`, + ` if (!ctx) throw new Error('use${p}Context requires <${p}Context>')`, + ' return ctx', + '}', + '', + ) + 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('') + const fnPascal = pascalCase(fn.camelName) + lines.push( + `export function use${fnPascal}(): ${fn.outputType} | null {`, + ` return use${p}Context().data?.${fn.name} ?? null`, + '}', + '', + ) } } - // ── Mutation hooks (with loading/error) ────────────────────────────── + // ── Mutation + plain function hooks ───────────────────────────────── - for (const fn of mutations) { + for (const fn of [...mutations, ...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('}') + lines.push( + `export function use${p}() {`, + ` return useMutation[0], Awaited>>(call${p})`, + '}', + '', + ) } else { - lines.push(`export function use${p}() {`) - lines.push(` return useMutation>>(() => call${p}() as any)`) - lines.push('}') + lines.push( + `export function use${p}() {`, + ` return useMutation>>(() => call${p}() as any)`, + '}', + '', + ) } - lines.push('') } - // ── Plain function hooks ──────────────────────────────────────────── + // ── Root MizanContext provider ────────────────────────────────────── - 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('') + lines.push( + '// ── MizanContext root provider ──', + '', + 'export interface MizanContextProps {', + ' /** Base URL for protocol endpoints. Defaults to "/api/mizan". */', + ' baseUrl?: string', + ' children: ReactNode', + '}', + '', + '/**', + " * Root provider — calls configure() once and mounts the global context (if defined).", + ' * Must wrap any component using Mizan-generated hooks.', + ' */', + 'export function MizanContext({ baseUrl, children }: MizanContextProps) {', + ' const configured = useRef(false)', + ' if (!configured.current) {', + ' if (baseUrl) configure({ baseUrl })', + ' configured.current = true', + ' }', + ) + if (hasGlobal) { + lines.push(' return {children}') + } else { + lines.push(' return <>{children}') } + lines.push('}', '') - // ── Re-export runtime types ───────────────────────────────────────── + // ── Escape hatch: useMizan ────────────────────────────────────────── - lines.push("export type { ContextState } from '@mizan/base'") - lines.push("export { configure, initSession, MizanError } from '@mizan/base'") - lines.push('') + lines.push( + '// ── Imperative escape hatch ──', + '', + '/**', + ' * Returns the imperative kernel API. For test harnesses or rare cases where', + ' * a typed generated hook does not fit. Most app code should use the typed hooks.', + ' */', + 'export function useMizan() {', + ' return { call: mizanCall, fetch: mizanFetch }', + '}', + '', + ) + + // ── Re-exports ────────────────────────────────────────────────────── + + lines.push( + "export type { ContextState } from '@mizan/base'", + "export { configure, initSession, MizanError } from '@mizan/base'", + '', + ) return lines.join('\n') } diff --git a/examples/django-react-site/backend/testapp/clients.py b/examples/django-react-site/backend/testapp/clients.py index 9c11444..035e3b0 100644 --- a/examples/django-react-site/backend/testapp/clients.py +++ b/examples/django-react-site/backend/testapp/clients.py @@ -13,7 +13,7 @@ from pydantic import BaseModel from mizan.client import ServerFunction, client from mizan.channels import ReactChannel -from mizan.setup.registry import register, register_form, register_as +from mizan.setup import register, register_form, register_as from mizan.channels import register as register_channel from mizan.forms import mizanFormMixin, mizanFormMeta from mizan.jwt import jwt_obtain, jwt_refresh diff --git a/examples/django-react-site/harness/django.config.mjs b/examples/django-react-site/harness/django.config.mjs index 5d5d15a..1ce8335 100644 --- a/examples/django-react-site/harness/django.config.mjs +++ b/examples/django-react-site/harness/django.config.mjs @@ -18,5 +18,5 @@ export default { }, }, - output: 'src/api/generated.ts', + output: 'src/api', } diff --git a/examples/django-react-site/harness/package.json b/examples/django-react-site/harness/package.json index 0f0c4e7..a3f1801 100644 --- a/examples/django-react-site/harness/package.json +++ b/examples/django-react-site/harness/package.json @@ -7,6 +7,7 @@ "dev": "vite --port 5174" }, "dependencies": { + "@mizan/base": "file:../../../frontends/mizan-base", "@rythazhur/mizan": "file:../../../frontends/mizan-react", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/django-react-site/harness/src/api/contexts/global.ts b/examples/django-react-site/harness/src/api/contexts/global.ts index 93c97f2..253ded6 100644 --- a/examples/django-react-site/harness/src/api/contexts/global.ts +++ b/examples/django-react-site/harness/src/api/contexts/global.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanFetch } from '@mizan/runtime' +import { mizanFetch } from '@mizan/base' import type { currentUserOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/contexts/local.ts b/examples/django-react-site/harness/src/api/contexts/local.ts index 6fb25a7..6de5c39 100644 --- a/examples/django-react-site/harness/src/api/contexts/local.ts +++ b/examples/django-react-site/harness/src/api/contexts/local.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanFetch } from '@mizan/runtime' +import { mizanFetch } from '@mizan/base' import type { greetOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/add.ts b/examples/django-react-site/harness/src/api/functions/add.ts index 5c14af1..a0efbcc 100644 --- a/examples/django-react-site/harness/src/api/functions/add.ts +++ b/examples/django-react-site/harness/src/api/functions/add.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { addInput, addOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/buggyFn.ts b/examples/django-react-site/harness/src/api/functions/buggyFn.ts index 205dc76..98e3020 100644 --- a/examples/django-react-site/harness/src/api/functions/buggyFn.ts +++ b/examples/django-react-site/harness/src/api/functions/buggyFn.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { buggyFnOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/echo.ts b/examples/django-react-site/harness/src/api/functions/echo.ts index b054957..5374efe 100644 --- a/examples/django-react-site/harness/src/api/functions/echo.ts +++ b/examples/django-react-site/harness/src/api/functions/echo.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { echoInput, echoOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/httpOnlyEcho.ts b/examples/django-react-site/harness/src/api/functions/httpOnlyEcho.ts index e0af195..b855132 100644 --- a/examples/django-react-site/harness/src/api/functions/httpOnlyEcho.ts +++ b/examples/django-react-site/harness/src/api/functions/httpOnlyEcho.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { httpOnlyEchoInput, httpOnlyEchoOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/jwtObtain.ts b/examples/django-react-site/harness/src/api/functions/jwtObtain.ts index bccc183..5d98b58 100644 --- a/examples/django-react-site/harness/src/api/functions/jwtObtain.ts +++ b/examples/django-react-site/harness/src/api/functions/jwtObtain.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { jwtObtainOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/jwtRefresh.ts b/examples/django-react-site/harness/src/api/functions/jwtRefresh.ts index c49d078..45aaf51 100644 --- a/examples/django-react-site/harness/src/api/functions/jwtRefresh.ts +++ b/examples/django-react-site/harness/src/api/functions/jwtRefresh.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { jwtRefreshInput, jwtRefreshOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/multiply.ts b/examples/django-react-site/harness/src/api/functions/multiply.ts index 4039a52..174aba9 100644 --- a/examples/django-react-site/harness/src/api/functions/multiply.ts +++ b/examples/django-react-site/harness/src/api/functions/multiply.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { multiplyInput, multiplyOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/notImplementedFn.ts b/examples/django-react-site/harness/src/api/functions/notImplementedFn.ts index 2ae0e12..8302e10 100644 --- a/examples/django-react-site/harness/src/api/functions/notImplementedFn.ts +++ b/examples/django-react-site/harness/src/api/functions/notImplementedFn.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { notImplementedFnOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/permissionCheckFn.ts b/examples/django-react-site/harness/src/api/functions/permissionCheckFn.ts index 82bfbeb..0cd3362 100644 --- a/examples/django-react-site/harness/src/api/functions/permissionCheckFn.ts +++ b/examples/django-react-site/harness/src/api/functions/permissionCheckFn.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { permissionCheckFnInput, permissionCheckFnOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/staffOnly.ts b/examples/django-react-site/harness/src/api/functions/staffOnly.ts index 38c8c25..2fa0c0a 100644 --- a/examples/django-react-site/harness/src/api/functions/staffOnly.ts +++ b/examples/django-react-site/harness/src/api/functions/staffOnly.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { staffOnlyOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/superuserOnly.ts b/examples/django-react-site/harness/src/api/functions/superuserOnly.ts index 52f7167..4e397ed 100644 --- a/examples/django-react-site/harness/src/api/functions/superuserOnly.ts +++ b/examples/django-react-site/harness/src/api/functions/superuserOnly.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { superuserOnlyOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/verifiedOnly.ts b/examples/django-react-site/harness/src/api/functions/verifiedOnly.ts index 08a2520..aedd921 100644 --- a/examples/django-react-site/harness/src/api/functions/verifiedOnly.ts +++ b/examples/django-react-site/harness/src/api/functions/verifiedOnly.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { verifiedOnlyOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/whoami.ts b/examples/django-react-site/harness/src/api/functions/whoami.ts index 0a57cc7..9e2e136 100644 --- a/examples/django-react-site/harness/src/api/functions/whoami.ts +++ b/examples/django-react-site/harness/src/api/functions/whoami.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { whoamiOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/functions/wsWhoami.ts b/examples/django-react-site/harness/src/api/functions/wsWhoami.ts index 891563f..f533875 100644 --- a/examples/django-react-site/harness/src/api/functions/wsWhoami.ts +++ b/examples/django-react-site/harness/src/api/functions/wsWhoami.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by mizan — do not edit -import { mizanCall } from '@mizan/runtime' +import { mizanCall } from '@mizan/base' import type { wsWhoamiOutput } from '../types' diff --git a/examples/django-react-site/harness/src/api/index.ts b/examples/django-react-site/harness/src/api/index.ts index 72a912f..6e61b9b 100644 --- a/examples/django-react-site/harness/src/api/index.ts +++ b/examples/django-react-site/harness/src/api/index.ts @@ -19,3 +19,6 @@ export { callPermissionCheckFn } from './functions/permissionCheckFn' export { callWsWhoami } from './functions/wsWhoami' export { callJwtObtain } from './functions/jwtObtain' export { callJwtRefresh } from './functions/jwtRefresh' + +// Stage 2 framework adapter +export * from './react' diff --git a/examples/django-react-site/harness/src/api/react.tsx b/examples/django-react-site/harness/src/api/react.tsx index 894c700..62e3cda 100644 --- a/examples/django-react-site/harness/src/api/react.tsx +++ b/examples/django-react-site/harness/src/api/react.tsx @@ -2,43 +2,59 @@ // 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 { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, + useSyncExternalStore, + type ReactNode, +} from 'react' +import { + configure, + initSession, + mizanCall, + mizanFetch, + MizanError, + registerContext, + type ContextState, +} from '@mizan/base' -import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, fetchLocalContext, type LocalContextData, type LocalContextParams, callEcho, callAdd, callWhoami, callHttpOnlyEcho, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callMultiply, callNotImplementedFn, callBuggyFn, callPermissionCheckFn, callWsWhoami, callJwtObtain, callJwtRefresh } from '../index' +import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, fetchLocalContext, type LocalContextData, type LocalContextParams, callEcho, callAdd, callWhoami, callHttpOnlyEcho, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callMultiply, callNotImplementedFn, callBuggyFn, callPermissionCheckFn, callWsWhoami, callJwtObtain, callJwtRefresh } from './index' -// Subscribe to kernel state via useSyncExternalStore -function useContextState( +// Internal — runs inside a Provider, registers with the kernel exactly once. +function useContextSubscription( name: string, params: Record, fetchFn: () => Promise, initialData?: T, ): ContextState { const ref = useRef | null>(null) - if (!ref.current) { ref.current = registerContext(name, params, fetchFn, initialData) } - const handle = ref.current - // Fetch on mount if no data useEffect(() => { if (handle.getState().status === 'idle') handle.refetch() return () => handle.unregister() }, [handle]) - return useSyncExternalStore( - handle.subscribe, - handle.getState, - handle.getState, - ) + return useSyncExternalStore(handle.subscribe, handle.getState, handle.getState) +} + +// Internal — wraps an imperative call() with isPending / error state. +interface MutationHook { + mutate: (args: TArgs) => Promise + isPending: boolean + error: Error | null } -// Mutation hook with loading/error state function useMutation( callFn: (args: TArgs) => Promise, -): { mutate: (args: TArgs) => Promise; isPending: boolean; error: Error | null } { +): MutationHook { const [isPending, setIsPending] = useState(false) const [error, setError] = useState(null) @@ -46,8 +62,7 @@ function useMutation( setIsPending(true) setError(null) try { - const result = await callFn(args) - return result + return await callFn(args) } catch (e) { setError(e as Error) throw e @@ -61,26 +76,41 @@ function useMutation( // ── Global Context ── +const GlobalCtx = createContext | null>(null) + +export function GlobalContextProvider({ children }: { children: ReactNode }) { + const ssrData = typeof window !== 'undefined' ? (window as any).__MIZAN_SSR_DATA__ : undefined + const state = useContextSubscription('global', {}, () => fetchGlobalContext({} as any), ssrData) + return {children} +} + export function useGlobalContext(): ContextState { - const ssrData = typeof window !== 'undefined' ? (window as any).__MIZAN_SSR_DATA__ : null - return useContextState('global', {}, () => fetchGlobalContext({} as any), ssrData) + const ctx = useContext(GlobalCtx) + if (!ctx) throw new Error('useGlobalContext requires or ') + return ctx } export function useCurrentUser(): currentUserOutput | null { - const state = useGlobalContext() - return state.data?.current_user ?? null + return useGlobalContext().data?.current_user ?? null } // ── Local Context ── -export function useLocalContext(params: LocalContextParams): ContextState { - const ssrData = typeof window !== 'undefined' ? (window as any).__MIZAN_SSR_DATA__ : null - return useContextState('local', params, () => fetchLocalContext(params), ssrData) +const LocalCtx = createContext | null>(null) + +export function LocalContext({ children, ...params }: LocalContextParams & { children: ReactNode }) { + const state = useContextSubscription('local', params, () => fetchLocalContext(params)) + return {children} } -export function useGreet(params: LocalContextParams): greetOutput | null { - const state = useLocalContext(params) - return state.data?.greet ?? null +export function useLocalContext(): ContextState { + const ctx = useContext(LocalCtx) + if (!ctx) throw new Error('useLocalContext requires ') + return ctx +} + +export function useGreet(): greetOutput | null { + return useLocalContext().data?.greet ?? null } export function useEcho() { @@ -139,5 +169,36 @@ export function useJwtRefresh() { return useMutation[0], Awaited>>(callJwtRefresh) } -export type { ContextState } from '@mizan/runtime' -export { configure, initSession, MizanError } from '@mizan/runtime' +// ── MizanContext root provider ── + +export interface MizanContextProps { + /** Base URL for protocol endpoints. Defaults to "/api/mizan". */ + baseUrl?: string + children: ReactNode +} + +/** + * Root provider — calls configure() once and mounts the global context (if defined). + * Must wrap any component using Mizan-generated hooks. + */ +export function MizanContext({ baseUrl, children }: MizanContextProps) { + const configured = useRef(false) + if (!configured.current) { + if (baseUrl) configure({ baseUrl }) + configured.current = true + } + return {children} +} + +// ── Imperative escape hatch ── + +/** + * Returns the imperative kernel API. For test harnesses or rare cases where + * a typed generated hook does not fit. Most app code should use the typed hooks. + */ +export function useMizan() { + return { call: mizanCall, fetch: mizanFetch } +} + +export type { ContextState } from '@mizan/base' +export { configure, initSession, MizanError } from '@mizan/base' diff --git a/examples/django-react-site/harness/src/api/svelte.ts b/examples/django-react-site/harness/src/api/svelte.ts deleted file mode 100644 index 4c9ac7a..0000000 --- a/examples/django-react-site/harness/src/api/svelte.ts +++ /dev/null @@ -1,52 +0,0 @@ -// AUTO-GENERATED by mizan — do not edit - -import { readable, type Readable } from 'svelte/store' -import { registerContext, type ContextState } from '@mizan/runtime' - -import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, fetchLocalContext, type LocalContextData, type LocalContextParams, callEcho, callAdd, callWhoami, callHttpOnlyEcho, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callMultiply, callNotImplementedFn, callBuggyFn, callPermissionCheckFn, callWsWhoami, callJwtObtain, callJwtRefresh } from '../index' - -export function createGlobalContext() { - const store = readable>( - { data: null, status: 'idle', error: null }, - (set) => { - const handle = registerContext('global', {} as any, () => fetchGlobalContext({} as any)) - const unsub = handle.subscribe(() => set(handle.getState())) - handle.refetch() - return () => { unsub(); handle.unregister() } - }, - ) - - return store -} - -export function createLocalContext(params: LocalContextParams) { - const store = readable>( - { data: null, status: 'idle', error: null }, - (set) => { - const handle = registerContext('local', params, () => fetchLocalContext(params)) - const unsub = handle.subscribe(() => set(handle.getState())) - handle.refetch() - return () => { unsub(); handle.unregister() } - }, - ) - - return store -} - -export { callEcho } from '../index' -export { callAdd } from '../index' -export { callWhoami } from '../index' -export { callHttpOnlyEcho } from '../index' -export { callStaffOnly } from '../index' -export { callSuperuserOnly } from '../index' -export { callVerifiedOnly } from '../index' -export { callMultiply } from '../index' -export { callNotImplementedFn } from '../index' -export { callBuggyFn } from '../index' -export { callPermissionCheckFn } from '../index' -export { callWsWhoami } from '../index' -export { callJwtObtain } from '../index' -export { callJwtRefresh } from '../index' - -export type { ContextState } from '@mizan/runtime' -export { configure, initSession, MizanError } from '@mizan/runtime' diff --git a/examples/django-react-site/harness/src/api/vue.ts b/examples/django-react-site/harness/src/api/vue.ts deleted file mode 100644 index b75cde2..0000000 --- a/examples/django-react-site/harness/src/api/vue.ts +++ /dev/null @@ -1,229 +0,0 @@ -// AUTO-GENERATED by mizan — do not edit - -import { ref, computed, onMounted, onUnmounted, onServerPrefetch, type ComputedRef } from 'vue' -import { registerContext, type ContextState } from '@mizan/runtime' - -import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, fetchLocalContext, type LocalContextData, type LocalContextParams, callEcho, callAdd, callWhoami, callHttpOnlyEcho, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callMultiply, callNotImplementedFn, callBuggyFn, callPermissionCheckFn, callWsWhoami, callJwtObtain, callJwtRefresh } from '../index' - -export function useGlobalContext() { - const state = ref>({ data: null, status: 'idle', error: null }) - let handle: ReturnType | null = null - - onMounted(() => { - handle = registerContext('global', {} as any, () => fetchGlobalContext({} as any)) - handle.subscribe(() => { state.value = handle!.getState() }) - handle.refetch() - }) - - onServerPrefetch(async () => { - handle = registerContext('global', {} as any, () => fetchGlobalContext({} as any)) - await handle.refetch() - state.value = handle.getState() - }) - - onUnmounted(() => { handle?.unregister() }) - - return { - state, - currentUser: computed(() => state.value.data?.current_user ?? null) as ComputedRef, - loading: computed(() => state.value.status === 'loading'), - error: computed(() => state.value.error), - } -} - -export function useLocalContext(params: LocalContextParams) { - const state = ref>({ data: null, status: 'idle', error: null }) - let handle: ReturnType | null = null - - onMounted(() => { - handle = registerContext('local', params, () => fetchLocalContext(params)) - handle.subscribe(() => { state.value = handle!.getState() }) - handle.refetch() - }) - - onServerPrefetch(async () => { - handle = registerContext('local', params, () => fetchLocalContext(params)) - await handle.refetch() - state.value = handle.getState() - }) - - onUnmounted(() => { handle?.unregister() }) - - return { - state, - greet: computed(() => state.value.data?.greet ?? null) as ComputedRef, - loading: computed(() => state.value.status === 'loading'), - error: computed(() => state.value.error), - } -} - -export function useEcho() { - const isPending = ref(false) - const error = ref(null) - async function mutate(args: Parameters[0]) { - isPending.value = true; error.value = null - try { return await callEcho(args) } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useAdd() { - const isPending = ref(false) - const error = ref(null) - async function mutate(args: Parameters[0]) { - isPending.value = true; error.value = null - try { return await callAdd(args) } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useWhoami() { - const isPending = ref(false) - const error = ref(null) - async function mutate() { - isPending.value = true; error.value = null - try { return await callWhoami() } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useHttpOnlyEcho() { - const isPending = ref(false) - const error = ref(null) - async function mutate(args: Parameters[0]) { - isPending.value = true; error.value = null - try { return await callHttpOnlyEcho(args) } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useStaffOnly() { - const isPending = ref(false) - const error = ref(null) - async function mutate() { - isPending.value = true; error.value = null - try { return await callStaffOnly() } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useSuperuserOnly() { - const isPending = ref(false) - const error = ref(null) - async function mutate() { - isPending.value = true; error.value = null - try { return await callSuperuserOnly() } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useVerifiedOnly() { - const isPending = ref(false) - const error = ref(null) - async function mutate() { - isPending.value = true; error.value = null - try { return await callVerifiedOnly() } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useMultiply() { - const isPending = ref(false) - const error = ref(null) - async function mutate(args: Parameters[0]) { - isPending.value = true; error.value = null - try { return await callMultiply(args) } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useNotImplementedFn() { - const isPending = ref(false) - const error = ref(null) - async function mutate() { - isPending.value = true; error.value = null - try { return await callNotImplementedFn() } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useBuggyFn() { - const isPending = ref(false) - const error = ref(null) - async function mutate() { - isPending.value = true; error.value = null - try { return await callBuggyFn() } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function usePermissionCheckFn() { - const isPending = ref(false) - const error = ref(null) - async function mutate(args: Parameters[0]) { - isPending.value = true; error.value = null - try { return await callPermissionCheckFn(args) } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useWsWhoami() { - const isPending = ref(false) - const error = ref(null) - async function mutate() { - isPending.value = true; error.value = null - try { return await callWsWhoami() } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useJwtObtain() { - const isPending = ref(false) - const error = ref(null) - async function mutate() { - isPending.value = true; error.value = null - try { return await callJwtObtain() } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export function useJwtRefresh() { - const isPending = ref(false) - const error = ref(null) - async function mutate(args: Parameters[0]) { - isPending.value = true; error.value = null - try { return await callJwtRefresh(args) } - catch (e) { error.value = e as Error; throw e } - finally { isPending.value = false } - } - return { mutate, isPending, error } -} - -export type { ContextState } from '@mizan/runtime' -export { configure, initSession, MizanError } from '@mizan/runtime' diff --git a/examples/django-react-site/harness/src/fixtures.tsx b/examples/django-react-site/harness/src/fixtures.tsx index 04fe4ea..6a5c2f0 100644 --- a/examples/django-react-site/harness/src/fixtures.tsx +++ b/examples/django-react-site/harness/src/fixtures.tsx @@ -23,11 +23,10 @@ import { useBuggyFn, usePermissionCheckFn, useCurrentUser, - DjangoError, + MizanError, useMizan, - useChatChannel, } from './api' -import { useContactForm, useLoginForm } from './api/generated.forms' +import { useChatChannel } from './api/channels.hooks' // ─── Fixture router ───────────────────────────────────────────────────────── @@ -55,9 +54,7 @@ export function Fixtures() { case 'permission-error': return case 'permission-success': return case 'context-current-user': return - case 'form-login-schema': return - case 'form-contact-schema': return - case 'form-contact-submit': return + // Form fixtures removed — forms codegen deferred per Blazr scope case 'channel-chat': return default: return
Harness ready. Set #hash.
} @@ -74,10 +71,10 @@ function Result({ data, error }: { data?: unknown; error?: unknown }) { {error !== undefined && error !== null && ( <>
- {error instanceof DjangoError ? 'DjangoError' : 'Error'} + {error instanceof MizanError ? 'MizanError' : 'Error'}
- {error instanceof DjangoError ? error.code : ''} + {error instanceof MizanError ? error.code : ''}
                         {error instanceof Error ? error.message : String(error)}
@@ -187,44 +184,6 @@ function ContextCurrentUser() {
     }
 }
 
-// ─── Form fixtures (using generated form hooks) ─────────────────────────────
-
-function FormLoginSchema() {
-    const form = useLoginForm()
-    if (form.loading) return 
loading...
- return
{JSON.stringify(form.schema)}
-} - -function FormContactSchema() { - const form = useContactForm() - if (form.loading) return
loading...
- return
{JSON.stringify(form.schema)}
-} - -function FormContactSubmit() { - const form = useContactForm() - const [result, setResult] = useState() - const [submitted, setSubmitted] = useState(false) - - useEffect(() => { - if (!form.loading && !submitted) { - form.set('name', 'Test User') - form.set('email', 'test@example.com') - form.set('message', 'Hello from e2e') - setSubmitted(true) - } - }, [form.loading, submitted, form]) - - useEffect(() => { - if (submitted && !result) { - form.submit().then(setResult) - } - }, [submitted, result, form]) - - if (!result) return
loading...
- return
{JSON.stringify(result)}
-} - // ─── Channel fixtures ─────────────────────────────────────────────────────── function ChannelChatFixture() {