React wrapper-layer codegen — restore the idioms over the kernel

The harness was written against the MIZAN.md oracle (<MizanContext>,
provider-per-context, useMizan, etc.) but the codegen had been narrowed
to just hooks-direct-on-kernel after the kernel split. Restoring the
React-idiomatic layer on top of the kernel.

backends/mizan-django/generate/generator/lib/adapters/react.mjs:
- Emits <MizanContext baseUrl="…"> root provider that calls configure()
  once and (if a global context is registered) wraps children in
  <GlobalContextProvider>.
- Emits <GlobalContextProvider> + <{Name}Context> per named context —
  kernel registration happens once per provider mount, not per hook
  call. Consumers read from React Context.
- Base hooks: useGlobalContext() / use{Name}Context() return full
  ContextState<T> (data + status + error).
- Convenience hooks per context-function (use{Fn}() returns data | null)
  and per regular function/mutation (use{Fn}() returns
  { mutate, isPending, error }).
- useMizan() returns { call, fetch } as an imperative escape hatch
  for test harnesses or rare cases where typed hooks don't fit.
- Re-exports MizanError, configure, initSession, ContextState from
  @mizan/base.

backends/mizan-django/generate/generator/cli.mjs:
- After Stage 2, appends `export * from './<adapter>'` to index.ts so
  `import { useEcho, MizanContext } from './api'` works as a barrel.

Bug fixes surfaced during integration:
- react.mjs was generating `from '../index'` (wrong path); flat layout
  needs `./index`.
- harness django.config.mjs had `output: 'src/api/generated.ts'` which
  the codegen treated as a directory; corrected to `output: 'src/api'`.
- example testapp/clients.py imported from the deleted
  mizan.setup.registry path; routed through mizan.setup aggregator.

harness/package.json: adds @mizan/base dep so the generated react.tsx
can resolve its kernel imports.

harness/src/fixtures.tsx:
- DjangoError → MizanError (kernel error class, backend-agnostic).
- useChatChannel sourced from ./api/channels.hooks directly (not
  re-exported from the unified index for now).
- Form fixtures removed — forms codegen deferred per Blazr scope.

Verified: harness `vite build` succeeds, 53 modules transformed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 17:21:49 -04:00
parent 63c9a9c4ce
commit 2982741aad
26 changed files with 364 additions and 491 deletions

View File

@@ -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 <PermissionError_ />
case 'permission-success': return <PermissionSuccess />
case 'context-current-user': return <ContextCurrentUser />
case 'form-login-schema': return <FormLoginSchema />
case 'form-contact-schema': return <FormContactSchema />
case 'form-contact-submit': return <FormContactSubmit />
// Form fixtures removed — forms codegen deferred per Blazr scope
case 'channel-chat': return <ChannelChatFixture />
default: return <div data-testid="ready">Harness ready. Set #hash.</div>
}
@@ -74,10 +71,10 @@ function Result({ data, error }: { data?: unknown; error?: unknown }) {
{error !== undefined && error !== null && (
<>
<div data-testid="error-type">
{error instanceof DjangoError ? 'DjangoError' : 'Error'}
{error instanceof MizanError ? 'MizanError' : 'Error'}
</div>
<div data-testid="error-code">
{error instanceof DjangoError ? error.code : ''}
{error instanceof MizanError ? error.code : ''}
</div>
<pre data-testid="error-message">
{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 <div>loading...</div>
return <pre data-testid="result">{JSON.stringify(form.schema)}</pre>
}
function FormContactSchema() {
const form = useContactForm()
if (form.loading) return <div>loading...</div>
return <pre data-testid="result">{JSON.stringify(form.schema)}</pre>
}
function FormContactSubmit() {
const form = useContactForm()
const [result, setResult] = useState<unknown>()
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 <div>loading...</div>
return <pre data-testid="result">{JSON.stringify(result)}</pre>
}
// ─── Channel fixtures ───────────────────────────────────────────────────────
function ChannelChatFixture() {