Rename djarea to mizan and fix React casing conventions

Rename the package from djarea to mizan across the entire codebase —
Python package, React library, generators, tests, and examples. Fix
JSX/hook casing (MizanProvider, useMizan, etc.) that broke when the
original PascalCase names were lowercased during the rename.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 20:01:03 -04:00
parent bf837e598b
commit c866142770
118 changed files with 1778 additions and 1433 deletions

View File

@@ -1,11 +1,11 @@
# @rythazhur/djarea (TypeScript)
# @rythazhur/mizan (TypeScript)
React client for the Djarea framework. See the [monorepo root](../README.md) for full documentation.
React client for the mizan framework. See the [monorepo root](../README.md) for full documentation.
## Install
```bash
npm install @rythazhur/djarea@git+https://git.impactsoundworks.com/isw/djarea.git#workspace=react
npm install @rythazhur/mizan@git+https://git.impactsoundworks.com/isw/mizan.git#workspace=react
```
## Usage
@@ -30,8 +30,8 @@ export default {
### 2. Generate
```bash
npx djarea-generate # once
npx djarea-generate --watch # dev mode
npx mizan-generate # once
npx mizan-generate --watch # dev mode
```
### 3. Wrap your app
@@ -74,7 +74,7 @@ chat.messages // typed, reactive
| File | Contents |
|------|----------|
| `generated.django.tsx` | `DjangoContext` + typed hooks |
| `generated.djarea.ts` | Pydantic types |
| `generated.mizan.ts` | Pydantic types |
| `generated.forms.ts` | Form hooks with Zod |
| `generated.channels.hooks.tsx` | Channel hooks |
| `index.ts` | Re-exports everything |
@@ -83,11 +83,11 @@ chat.messages // typed, reactive
| Import | When to use |
|--------|------------|
| `@rythazhur/djarea` | Core: DjareaProvider, hooks, forms, errors |
| `@rythazhur/djarea/channels` | WebSocket channels |
| `@rythazhur/djarea/jwt` | JWT token management |
| `@rythazhur/djarea/client` | HTTP clients (CSR/SSR) |
| `@rythazhur/djarea/allauth` | Allauth UI components |
| `@rythazhur/mizan` | Core: mizanProvider, hooks, forms, errors |
| `@rythazhur/mizan/channels` | WebSocket channels |
| `@rythazhur/mizan/jwt` | JWT token management |
| `@rythazhur/mizan/client` | HTTP clients (CSR/SSR) |
| `@rythazhur/mizan/allauth` | Allauth UI components |
These are **library internals** used by the generated code. You should import from `@/api` (your generated index), not from the library directly.

View File

@@ -1,5 +1,5 @@
{
"name": "@rythazhur/djarea",
"name": "@rythazhur/mizan",
"version": "0.1.1",
"type": "module",
"main": "dist/index.js",
@@ -39,7 +39,7 @@
}
},
"bin": {
"djarea-generate": "./dist/generator/cli.mjs"
"mizan-generate": "./dist/generator/cli.mjs"
},
"scripts": {
"build": "tsc -p tsconfig.build.json && node -e \"require('fs').cpSync('src/generator','dist/generator',{recursive:true})\"",

View File

@@ -10,10 +10,10 @@
import React from 'react'
import { render, screen, waitFor, act } from '@testing-library/react'
import {
DjareaProvider,
useDjarea,
useDjareaStatus,
useDjareaCall,
MizanProvider,
useMizan,
useMizanStatus,
useMizanCall,
// Legacy aliases for backwards compatibility tests
DjangoContext,
useDjango,
@@ -27,18 +27,18 @@ import { describeIntegration, BACKEND_URL } from '../testing'
// Unit Tests (no backend required)
// ============================================================================
describe('Djarea Context (unit)', () => {
describe('useDjarea hook', () => {
describe('mizan Context (unit)', () => {
describe('useMizan hook', () => {
it('should throw when used outside provider', () => {
function TestComponent() {
useDjarea()
useMizan()
return <div>Test</div>
}
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
expect(() => render(<TestComponent />)).toThrow(
'useDjarea must be used within a DjareaProvider'
'useMizan must be used within a MizanProvider'
)
consoleSpy.mockRestore()
@@ -48,14 +48,14 @@ describe('Djarea Context (unit)', () => {
let contextValue: any = null
function TestComponent() {
contextValue = useDjarea()
contextValue = useMizan()
return <div>Test</div>
}
render(
<DjareaProvider autoConnect={false}>
<MizanProvider autoConnect={false}>
<TestComponent />
</DjareaProvider>
</MizanProvider>
)
expect(contextValue).not.toBeNull()
@@ -63,17 +63,17 @@ describe('Djarea Context (unit)', () => {
})
})
describe('useDjareaStatus hook', () => {
describe('useMizanStatus hook', () => {
it('should return disconnected when autoConnect is false', () => {
function TestComponent() {
const status = useDjareaStatus()
const status = useMizanStatus()
return <div data-testid="status">{status}</div>
}
render(
<DjareaProvider autoConnect={false}>
<MizanProvider autoConnect={false}>
<TestComponent />
</DjareaProvider>
</MizanProvider>
)
expect(screen.getByTestId('status')).toHaveTextContent('disconnected')
@@ -85,7 +85,7 @@ describe('Djarea Context (unit)', () => {
let contextValue: any = null
function TestComponent() {
contextValue = useDjarea()
contextValue = useMizan()
return <div>Test</div>
}
@@ -95,9 +95,9 @@ describe('Djarea Context (unit)', () => {
}
render(
<DjareaProvider hydration={hydration} autoConnect={false}>
<MizanProvider hydration={hydration} autoConnect={false}>
<TestComponent />
</DjareaProvider>
</MizanProvider>
)
expect(contextValue.getContext('auth_status')).toEqual({ is_authenticated: false })
@@ -110,7 +110,7 @@ describe('Djarea Context (unit)', () => {
// Integration Tests (require running backend)
// ============================================================================
describeIntegration('Djarea Context (integration)', () => {
describeIntegration('mizan Context (integration)', () => {
describe('server function calls via HTTP', () => {
it('should call echo function and get response', async () => {
let result: any = null
@@ -130,7 +130,7 @@ describeIntegration('Djarea Context (integration)', () => {
}
render(
<DjangoContext baseUrl={`${BACKEND_URL}/api/djarea`} autoConnect={false}>
<DjangoContext baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
<TestComponent />
</DjangoContext>
)
@@ -161,7 +161,7 @@ describeIntegration('Djarea Context (integration)', () => {
}
render(
<DjangoContext baseUrl={`${BACKEND_URL}/api/djarea`} autoConnect={false}>
<DjangoContext baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
<TestComponent />
</DjangoContext>
)
@@ -192,7 +192,7 @@ describeIntegration('Djarea Context (integration)', () => {
}
render(
<DjangoContext baseUrl={`${BACKEND_URL}/api/djarea`} autoConnect={false}>
<DjangoContext baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
<TestComponent />
</DjangoContext>
)
@@ -227,7 +227,7 @@ describeIntegration('Djarea Context (integration)', () => {
}
render(
<DjangoContext baseUrl={`${BACKEND_URL}/api/djarea`} autoConnect={false}>
<DjangoContext baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
<TestComponent />
</DjangoContext>
)
@@ -260,7 +260,7 @@ describeIntegration('Djarea Context (integration)', () => {
}
render(
<DjangoContext baseUrl={`${BACKEND_URL}/api/djarea`} autoConnect={false}>
<DjangoContext baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
<TestComponent />
</DjangoContext>
)
@@ -296,7 +296,7 @@ describeIntegration('Djarea Context (integration)', () => {
}
render(
<DjangoContext baseUrl={`${BACKEND_URL}/api/djarea`} autoConnect={false}>
<DjangoContext baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
<TestComponent />
</DjangoContext>
)

View File

@@ -28,7 +28,7 @@ function renderFormHook<TData extends Record<string, unknown>>(
) {
return renderHook(() => useDjangoFormCore<TData>(config), {
wrapper: ({ children }) => (
<DjangoContext baseUrl={`${BACKEND_URL}/api/djarea`} autoConnect={false}>
<DjangoContext baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
{children}
</DjangoContext>
),

View File

@@ -1,5 +1,5 @@
/**
* Cross-cutting integration tests for djarea
* Cross-cutting integration tests for mizan
*
* Tests error paths and protocol correctness across HTTP, Forms, and WebSocket.
* Requires a running backend: docker-compose up
@@ -10,22 +10,22 @@
import { renderHook, act } from '@testing-library/react'
import { ReactNode } from 'react'
import { describeIntegration, BACKEND_URL, WS_URL } from '../testing'
import { DjareaProvider, useDjarea } from '../context'
import { MizanProvider, useMizan } from '../context'
import { DjangoError } from '../errors'
import { ChannelConnection } from '../channels/connection'
import { RPCError } from '../channels/connection'
function Wrapper({ children }: { children: ReactNode }) {
return (
<DjareaProvider baseUrl={`${BACKEND_URL}/api/djarea`} autoConnect={false}>
<MizanProvider baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
{children}
</DjareaProvider>
</MizanProvider>
)
}
// Helper to get call function
function useCall() {
const { call } = useDjarea()
const { call } = useMizan()
return call
}
@@ -503,7 +503,7 @@ describeIntegration('Error code coverage', () => {
})
it('should return BAD_REQUEST for invalid JSON body', async () => {
const response = await fetch(`${BACKEND_URL}/api/djarea/call/`, {
const response = await fetch(`${BACKEND_URL}/api/mizan/call/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
@@ -515,7 +515,7 @@ describeIntegration('Error code coverage', () => {
})
it('should return BAD_REQUEST for missing fn field', async () => {
const response = await fetch(`${BACKEND_URL}/api/djarea/call/`, {
const response = await fetch(`${BACKEND_URL}/api/mizan/call/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
@@ -528,11 +528,11 @@ describeIntegration('Error code coverage', () => {
})
// ============================================================================
// Group 8: DjareaFormMixin integration
// Group 8: mizanFormMixin integration
// ============================================================================
describeIntegration('DjareaFormMixin integration', () => {
it('should return schema with title, subtitle, and submit_label from DjareaFormMeta', async () => {
describeIntegration('mizanFormMixin integration', () => {
it('should return schema with title, subtitle, and submit_label from mizanFormMeta', async () => {
const { result } = renderHook(() => useCall(), { wrapper: Wrapper })
let response: any = null

View File

@@ -1,9 +1,9 @@
/**
* Re-export RouterAdapter from djarea/client.
* Re-export RouterAdapter from mizan/client.
*
* Allauth extends this with a required getParam method.
*/
import type { RouterAdapter as BaseRouterAdapter } from 'djarea/client'
import type { RouterAdapter as BaseRouterAdapter } from 'mizan/client'
export interface RouterAdapter extends BaseRouterAdapter {
/** Get a specific route param (e.g., from /auth/[...path]) - required for allauth */

View File

@@ -6,7 +6,7 @@ import {
type DjangoFormState,
type FormOptions,
type FormErrors,
} from 'djarea'
} from 'mizan'
import { useAuthContext } from '../contexts/AuthContext'
import { useStyles } from '../contexts/StylesContext'
import { getAuthDetails, AuthDetails } from '../api'
@@ -41,7 +41,7 @@ interface AuthDjangoFormProps {
}
/**
* AuthDjangoForm renders a form from the Djarea server functions
* AuthDjangoForm renders a form from the mizan server functions
* with styling consistent with the auth UI.
*
* It fetches the form schema (including title, subtitle, fields, submit label)

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react'
import { useAllauthAPI } from '../../contexts/APIContext'
import { useStyles } from '../../contexts/StylesContext'
import { useDjangoFormCore } from 'djarea'
import { useDjangoFormCore } from 'mizan'
import { SettingsSection, SettingsItem, SettingsList, Badge, Button } from './SettingsComponents'
interface Email {

View File

@@ -1,6 +1,6 @@
'use client'
import { useDjangoFormCore } from 'djarea'
import { useDjangoFormCore } from 'mizan'
import { useStyles } from '../../contexts/StylesContext'
import { SettingsSection, Button } from './SettingsComponents'

View File

@@ -5,7 +5,7 @@
* 1. Define the base path for Django-initiated routes (must match HEADLESS_FRONTEND_URLS)
* 2. Define where to navigate for various auth events (developer controls these)
*
* For JWT-based API calls, use djarea/jwt separately.
* For JWT-based API calls, use mizan/jwt separately.
*/
export interface AllauthConfig {

View File

@@ -1,7 +1,7 @@
'use client'
import { useMemo } from 'react'
import { useDjangoCSRClient, Auth } from 'djarea/client/react'
import { useDjangoCSRClient, Auth } from 'mizan/client/react'
import { useAuthContext } from './AuthContext'
import { createAPI, AllauthAPI, BrowserFormAction } from '../api'

View File

@@ -1,7 +1,7 @@
'use client'
import { ReactNode, useEffect, useState } from 'react'
import { useDjangoCSRClient, Auth } from 'djarea/client/react'
import { useDjangoCSRClient, Auth } from 'mizan/client/react'
import type { RouterAdapter } from '../adapters/router'
import type { InitialAuth } from '../hydration'
import { AuthContext } from './AuthContext'

View File

@@ -1,8 +1,8 @@
'use client'
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { useDjangoCSRClient, Auth } from 'djarea/client/react'
import { useDjarea, useDjareaContext } from 'djarea'
import { useDjangoCSRClient, Auth } from 'mizan/client/react'
import { useMizan, useMizanContext } from 'mizan'
import { getAuthDetails, createAPI } from '../api'
import type { AllauthResponse } from '../types'
import getAuthChangeEvent from '../events'
@@ -30,7 +30,7 @@ export function AuthContext({
auth: initialAuth,
}: AuthContextProps) {
const client = useDjangoCSRClient(Auth.SESSION)
const { refreshAllContexts } = useDjarea()
const { refreshAllContexts } = useMizan()
const [auth, setAuth] = useState(initialAuth)
const [event, setEvent] = useState('')
const prevAuth = useRef(initialAuth)
@@ -100,10 +100,10 @@ export interface AllauthUser {
}
/**
* Get the current user from DjareaProvider.
* Get the current user from MizanProvider.
*
* This uses the generic djarea hook to access the 'user' context.
* The backend defines this context in lib/djarea/allauth/contexts.py:
* This uses the generic mizan hook to access the 'user' context.
* The backend defines this context in lib/mizan/allauth/contexts.py:
*
* @client(context='global')
* def user(request) -> UserOutput | None:
@@ -112,7 +112,7 @@ export interface AllauthUser {
* @typeParam T - User type (defaults to AllauthUser, products can use more specific types)
*/
export function useUser<T extends AllauthUser = AllauthUser>(): T {
const user = useDjareaContext<T>('user')
const user = useMizanContext<T>('user')
// Return empty object cast to T if user is undefined (not loaded)
// This matches the previous behavior and allows optional chaining
return (user ?? {}) as T

View File

@@ -1,4 +1,4 @@
import type { DjangoHTTPClient } from 'djarea/client'
import type { DjangoHTTPClient } from 'mizan/client'
import { createAPI } from './api'
import type { AllauthResponse } from './types'

View File

@@ -1,5 +1,5 @@
/**
* djarea/allauth
* mizan/allauth
*
* React integration for django-allauth headless API.
* Framework-agnostic - works with Next.js, Remix, React Router, etc.
@@ -9,9 +9,9 @@
* ```tsx
* // layout.tsx
* import { cookies } from 'next/headers'
* import { createDjangoSSRClient } from 'djarea/client'
* import { getInitialAuth } from 'djarea/allauth'
* import { NextAllauthContext } from 'djarea/allauth/nextjs'
* import { createDjangoSSRClient } from 'mizan/client'
* import { getInitialAuth } from 'mizan/allauth'
* import { NextAllauthContext } from 'mizan/allauth/nextjs'
*
* export default async function RootLayout({ children }) {
* const ssrClient = createDjangoSSRClient({ cookies: await cookies() })

View File

@@ -1,14 +1,14 @@
'use client'
/**
* Next.js adapter for djarea/allauth.
* Next.js adapter for mizan/allauth.
*
* Usage:
* ```tsx
* // In layout.tsx (server component)
* import { createDjangoSSRClient } from 'djarea/client'
* import { getInitialAuth } from 'djarea/allauth'
* import { NextAllauthContext } from 'djarea/allauth/nextjs'
* import { createDjangoSSRClient } from 'mizan/client'
* import { getInitialAuth } from 'mizan/allauth'
* import { NextAllauthContext } from 'mizan/allauth/nextjs'
*
* export default async function RootLayout({ children }) {
* const ssrClient = createDjangoSSRClient({ cookies: await cookies() })

View File

@@ -1,5 +1,5 @@
/**
* WebSocket connection manager for djarea/channels
* WebSocket connection manager for mizan/channels
*
* Supports both pub/sub channels AND RPC calls over the same connection.
*/

View File

@@ -1,7 +1,7 @@
'use client'
/**
* React context for djarea/channels
* React context for mizan/channels
*/
import { createContext, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'

View File

@@ -1,7 +1,7 @@
'use client'
/**
* React hooks for djarea/channels
* React hooks for mizan/channels
*
* Includes pub/sub channel hooks AND RPC hooks.
*/

View File

@@ -1,5 +1,5 @@
/**
* djarea/channels
* mizan/channels
*
* Real-time WebSocket communication with Django Channels.
* Type-safe bidirectional messaging.
@@ -8,7 +8,7 @@
*
* ```tsx
* // layout.tsx
* import { ChannelProvider } from 'djarea/channels'
* import { ChannelProvider } from 'mizan/channels'
*
* export default function Layout({ children }) {
* return (
@@ -36,7 +36,7 @@
*
* ```tsx
* // Using raw hook (for custom channels)
* import { useChannel } from 'djarea/channels'
* import { useChannel } from 'mizan/channels'
*
* function CustomChannel() {
* const channel = useChannel<

View File

@@ -1,5 +1,5 @@
/**
* Types for djarea/channels
* Types for mizan/channels
*/
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected'

View File

@@ -1,5 +1,5 @@
/**
* djarea/client
* mizan/client
*
* HTTP client factories for Django backends.
* Framework-agnostic - works with vanilla JS, React, Vue, etc.
@@ -8,7 +8,7 @@
*
* ### Client-Side (CSR)
* ```ts
* import { createDjangoCSRClient, Auth } from 'djarea/client'
* import { createDjangoCSRClient, Auth } from 'mizan/client'
*
* // Session-based (cookies + CSRF)
* const client = createDjangoCSRClient(Auth.SESSION)
@@ -21,7 +21,7 @@
*
* ### Server-Side (SSR)
* ```ts
* import { createDjangoSSRClient } from 'djarea/client'
* import { createDjangoSSRClient } from 'mizan/client'
*
* const client = createDjangoSSRClient({
* cookies: await cookies() // Next.js cookies()
@@ -34,7 +34,7 @@
*
* For React, import from `/react`:
* ```tsx
* import { useDjangoCSRClient, Auth } from 'djarea/client/react'
* import { useDjangoCSRClient, Auth } from 'mizan/client/react'
*
* const client = useDjangoCSRClient(Auth.SESSION)
* ```
@@ -250,7 +250,7 @@ export function createDjangoCSRClient(
): DjangoHTTPClient {
if (!config?.baseUrl) {
throw new Error(
'baseUrl is required. Pass it via config or use DjareaProvider which provides it automatically.'
'baseUrl is required. Pass it via config or use MizanProvider which provides it automatically.'
)
}
@@ -484,7 +484,7 @@ export async function ensureDjangoSession(config: SSRClientConfig): Promise<{
}
// No CSRF token - need to initialize session
const url = new URL('/api/djarea/session/', baseUrl)
const url = new URL('/api/mizan/session/', baseUrl)
const resp = await fetch(url, {
method: 'GET',
headers: {
@@ -496,7 +496,7 @@ export async function ensureDjangoSession(config: SSRClientConfig): Promise<{
})
if (!resp.ok) {
console.error('[djarea] Failed to initialize session:', resp.status, resp.statusText)
console.error('[mizan] Failed to initialize session:', resp.status, resp.statusText)
return { csrf: '', cookieHeader: existingCookies }
}
@@ -553,7 +553,7 @@ function getCSRClient(): DjangoHTTPClient {
*
* Uses the standard CSR client with session-based auth.
*
* @param baseUrl - Base URL for the API (e.g., '/api/djarea')
* @param baseUrl - Base URL for the API (e.g., '/api/mizan')
* @param functionName - Name of the server function
* @param input - Input data for the function
* @returns Promise resolving to the function output

View File

@@ -1,12 +1,12 @@
'use client'
/**
* Next.js adapter for djarea/jwt.
* Next.js adapter for mizan/jwt.
*
* Usage:
* ```tsx
* // In layout.tsx
* import { NextAuthContext } from 'djarea/jwt/nextjs'
* import { NextAuthContext } from 'mizan/jwt/nextjs'
*
* export default function RootLayout({ children }) {
* return (

View File

@@ -23,7 +23,7 @@ export type * from './types'
* React hook that returns a client-side Django HTTP client.
*
* For SESSION auth, creates a session-based client with CSRF handling.
* For JWT auth, automatically wires up the JWTContext from djarea/jwt.
* For JWT auth, automatically wires up the JWTContext from mizan/jwt.
*
* @param auth - Authentication strategy (Auth.SESSION or Auth.JWT)
* @param config - Optional client configuration
@@ -35,7 +35,7 @@ export type * from './types'
* const user = await client.json('GET', '/api/accounts/me/')
*
* @example
* // JWT-based (requires JWTContext from djarea/jwt)
* // JWT-based (requires JWTContext from mizan/jwt)
* const client = useDjangoCSRClient(Auth.JWT)
* const user = await client.json('GET', '/api/accounts/me/')
*/
@@ -48,7 +48,7 @@ export function useDjangoCSRClient(auth: Auth, config?: CSRClientConfig): Django
if (auth === Auth.JWT) {
if (!jwtContext?.getAccessToken) {
throw new Error(
'useDjangoCSRClient(Auth.JWT) requires JWTContext from djarea/jwt. ' +
'useDjangoCSRClient(Auth.JWT) requires JWTContext from mizan/jwt. ' +
'Wrap your component in JWTContext to use JWT authentication.'
)
}

View File

@@ -42,7 +42,7 @@ export interface JWTTokens {
export interface JWTConfig {
/** Base URL for API calls (default: '' - use relative URLs) */
baseUrl?: string
/** Djarea server function endpoint (default: /api/djarea/call/) */
/** mizan server function endpoint (default: /api/mizan/call/) */
endpoint?: string
/** Seconds before expiry to trigger refresh (default: 30) */
refreshBuffer?: number

View File

@@ -1,7 +1,7 @@
'use client'
/**
* Djarea React Context
* mizan React Context
*
* Provides server function calls via HTTP (default) or WebSocket RPC (opt-in).
* This is the core React integration for Django server functions.
@@ -12,13 +12,13 @@
* when connected, falling back to HTTP when disconnected
*
* Two layers:
* 1. DjareaProvider (this file) - Generic provider with name-based API
* - Libraries like Allauth use this: useDjarea(), useContext('current_user')
* 1. MizanProvider (this file) - Generic provider with name-based API
* - Libraries like Allauth use this: useMizan(), useContext('current_user')
*
* 2. Generated DjangoContext (in @/api) - Typed wrapper around DjareaProvider
* 2. Generated DjangoContext (in @/api) - Typed wrapper around MizanProvider
* - Product code uses this: useCurrentUser(), useUpdateProfile()
*
* The generated code wraps DjareaProvider and adds type-safe hooks.
* The generated code wraps MizanProvider and adds type-safe hooks.
*/
import {
@@ -31,12 +31,12 @@ import {
useCallback,
type ReactNode,
} from 'react'
import { ChannelConnection, RPCError } from 'djarea/channels'
import { ChannelConnection, RPCError } from 'mizan/channels'
import {
createDjangoCSRClient,
Auth,
type FunctionResponse,
} from 'djarea/client'
} from 'mizan/client'
import { useJWT } from './jwt'
import { DjangoError, type ErrorCode, type FunctionErrorResponse } from './errors'
@@ -69,17 +69,17 @@ export type PushListener<T = unknown> = (message: PushMessage<T>) => void
export type ContextStore = Record<string, unknown>
/** Hydration data for SSR - maps context names to their initial data */
export type DjareaHydration = Record<string, unknown>
export type MizanHydration = Record<string, unknown>
/** Transport mode for server function calls */
export type Transport = 'http' | 'websocket'
export interface DjareaContextValue {
export interface MizanContextValue {
/**
* Call a server function by name.
*
* Transport behavior:
* - 'http' (default): Always use HTTP POST /api/djarea/call/
* - 'http' (default): Always use HTTP POST /api/mizan/call/
* - 'websocket': Use WebSocket RPC when connected, HTTP fallback when not
*
* @param functionName - The server function name (e.g., 'echo', 'update_profile')
@@ -139,14 +139,14 @@ export interface DjareaContextValue {
whenReady: Promise<void>
}
export interface DjareaProviderProps {
export interface MizanProviderProps {
children: ReactNode
/**
* Initial hydration data for contexts (from SSR).
* Keys are context names, values are the data.
*/
hydration?: DjareaHydration
hydration?: MizanHydration
/**
* List of context names to auto-fetch if not in hydration.
@@ -156,7 +156,7 @@ export interface DjareaProviderProps {
/**
* Base URL for HTTP fallback calls.
* @default '/api/djarea'
* @default '/api/mizan'
*/
baseUrl?: string
@@ -189,24 +189,24 @@ export interface DjareaProviderProps {
// Context
// ============================================================================
const DjareaContextInternal = createContext<DjareaContextValue | null>(null)
const MizanContextInternal = createContext<MizanContextValue | null>(null)
// ============================================================================
// Provider
// ============================================================================
export function DjareaProvider({
export function MizanProvider({
children,
hydration,
contexts: contextNames = [],
baseUrl = '/api/djarea',
baseUrl = '/api/mizan',
wsUrl = '/ws/',
autoConnect = true,
reconnect = true,
reconnectDelay = 1000,
maxReconnectAttempts = 10,
connection: providedConnection,
}: DjareaProviderProps) {
}: MizanProviderProps) {
const connectionRef = useRef<ChannelConnection | null>(null)
// Push listeners: Map<topic, Set<listener>>
@@ -287,7 +287,7 @@ export function DjareaProvider({
// Connection error - fall through to HTTP
console.warn(
`[Djarea] WebSocket RPC failed for '${functionName}', falling back to HTTP:`,
`[mizan] WebSocket RPC failed for '${functionName}', falling back to HTTP:`,
e
)
}
@@ -335,14 +335,14 @@ export function DjareaProvider({
try {
listener(data)
} catch (e) {
console.error(`[Djarea] Context listener error for '${name}':`, e)
console.error(`[mizan] Context listener error for '${name}':`, e)
}
})
}
return next
})
} catch (e) {
console.error(`[Djarea] Failed to refresh context '${name}':`, e)
console.error(`[mizan] Failed to refresh context '${name}':`, e)
throw e
}
},
@@ -417,7 +417,7 @@ export function DjareaProvider({
try {
listener(message)
} catch (e) {
console.error('[Djarea] Push listener error:', e)
console.error('[mizan] Push listener error:', e)
}
})
}
@@ -443,7 +443,7 @@ export function DjareaProvider({
return
}
fetch(`${baseUrl}/session/`, { credentials: 'include' })
.catch(e => console.error('[DjareaProvider] Session init failed:', e))
.catch(e => console.error('[MizanProvider] Session init failed:', e))
.finally(() => {
setSessionReady(true)
sessionRef.current?.resolve()
@@ -466,7 +466,7 @@ export function DjareaProvider({
const isRPCAvailable = status === 'connected'
const value = useMemo<DjareaContextValue>(
const value = useMemo<MizanContextValue>(
() => ({
call,
getContext,
@@ -482,9 +482,9 @@ export function DjareaProvider({
)
return (
<DjareaContextInternal value={value}>
<MizanContextInternal value={value}>
{children}
</DjareaContextInternal>
</MizanContextInternal>
)
}
@@ -493,7 +493,7 @@ export function DjareaProvider({
// ============================================================================
/**
* Access the Djarea context.
* Access the mizan context.
*
* Provides generic name-based API for server functions and contexts.
* Libraries should use this hook, not the typed generated hooks.
@@ -501,18 +501,18 @@ export function DjareaProvider({
* @example
* ```tsx
* // Library code (e.g., Allauth)
* import { useDjarea } from 'djarea'
* import { useMizan } from 'mizan'
*
* function useUser() {
* const { getContext } = useDjarea()
* const { getContext } = useMizan()
* return getContext('current_user')
* }
* ```
*/
export function useDjarea(): DjareaContextValue {
const context = useReactContext(DjareaContextInternal)
export function useMizan(): MizanContextValue {
const context = useReactContext(MizanContextInternal)
if (!context) {
throw new Error('useDjarea must be used within a DjareaProvider')
throw new Error('useMizan must be used within a MizanProvider')
}
return context
}
@@ -526,12 +526,12 @@ export function useDjarea(): DjareaContextValue {
* ```tsx
* // In Allauth library
* function useUser() {
* return useDjareaContext('current_user')
* return useMizanContext('current_user')
* }
* ```
*/
export function useDjareaContext<T = unknown>(name: string): T | undefined {
const { getContext } = useDjarea()
export function useMizanContext<T = unknown>(name: string): T | undefined {
const { getContext } = useMizan()
return getContext<T>(name)
}
@@ -548,20 +548,20 @@ export function useDjareaContext<T = unknown>(name: string): T | undefined {
* ```tsx
* // HTTP-only function (default)
* function useUpdateProfile() {
* return useDjareaCall('update_profile')
* return useMizanCall('update_profile')
* }
*
* // WebSocket-enabled function
* function useSendMessage() {
* return useDjareaCall('send_message', 'websocket')
* return useMizanCall('send_message', 'websocket')
* }
* ```
*/
export function useDjareaCall<TInput = unknown, TOutput = unknown>(
export function useMizanCall<TInput = unknown, TOutput = unknown>(
functionName: string,
transport: Transport = 'http'
): (input?: TInput) => Promise<TOutput> {
const { call } = useDjarea()
const { call } = useMizan()
return useCallback(
(input?: TInput) => call<TInput, TOutput>(functionName, input, transport),
[call, functionName, transport]
@@ -571,8 +571,8 @@ export function useDjareaCall<TInput = unknown, TOutput = unknown>(
/**
* Get the current WebSocket connection status.
*/
export function useDjareaStatus(): ConnectionStatus {
const { status } = useDjarea()
export function useMizanStatus(): ConnectionStatus {
const { status } = useMizan()
return status
}
@@ -584,7 +584,7 @@ export function usePush<T = unknown>(
topic: string,
callback: PushListener<T>
): void {
const { onPush } = useDjarea()
const { onPush } = useMizan()
const callbackRef = useRef(callback)
useEffect(() => {
@@ -604,20 +604,20 @@ export function usePush<T = unknown>(
// Legacy Aliases (for backwards compatibility during migration)
// ============================================================================
/** @deprecated Use DjareaProvider instead */
export const DjangoContext = DjareaProvider
/** @deprecated Use MizanProvider instead */
export const DjangoContext = MizanProvider
/** @deprecated Use useDjarea instead */
export const useDjango = useDjarea
/** @deprecated Use useMizan instead */
export const useDjango = useMizan
/** @deprecated Use useDjareaStatus instead */
export const useDjangoStatus = useDjareaStatus
/** @deprecated Use useMizanStatus instead */
export const useDjangoStatus = useMizanStatus
/** @deprecated Use useDjareaCall instead */
/** @deprecated Use useMizanCall instead */
export function useServerFunction<TInput = unknown, TOutput = unknown>(
functionName: string
): (input: TInput) => Promise<TOutput> {
const { call } = useDjarea()
const { call } = useMizan()
return useCallback(
(input: TInput) => call<TInput, TOutput>(functionName, input),
[call, functionName]
@@ -625,5 +625,5 @@ export function useServerFunction<TInput = unknown, TOutput = unknown>(
}
// Re-export types for the legacy API
export type DjangoContextValue = DjareaContextValue
export type DjangoContextProps = DjareaProviderProps
export type DjangoContextValue = MizanContextValue
export type DjangoContextProps = MizanProviderProps

View File

@@ -1,10 +1,10 @@
'use client'
/**
* Djarea Forms - Typed React Form Hooks for Django Server Functions
* mizan Forms - Typed React Form Hooks for Django Server Functions
*
* This module provides the core form state management that generated
* form hooks use. It integrates with Djarea server functions for
* form hooks use. It integrates with mizan server functions for
* schema fetching, validation, and submission.
*
* Users don't use this directly - they use generated typed hooks:
@@ -23,7 +23,7 @@ import {
useMemo,
} from 'react'
import type { ZodObject, ZodRawShape, ZodError } from 'zod'
import { useDjarea } from './context'
import { useMizan } from './context'
import { DjangoError } from './errors'
// Forms always use HTTP transport because Django Allauth and other auth
@@ -421,7 +421,7 @@ export interface DjangoFormsetState<TData extends Record<string, unknown>> {
// ============================================================================
/**
* Configuration for useDjareaFormCore.
* Configuration for useMizanFormCore.
* This is used by generated hooks - not directly by users.
*/
export interface FormCoreConfig<TData extends Record<string, unknown>> {
@@ -447,7 +447,7 @@ export interface FormCoreConfig<TData extends Record<string, unknown>> {
*
* @internal
*/
export function useDjareaFormCore<TData extends Record<string, unknown>>(
export function useMizanFormCore<TData extends Record<string, unknown>>(
config: FormCoreConfig<TData>
): DjangoFormState<TData> {
const { name, zodSchema, options = {} } = config
@@ -458,7 +458,7 @@ export function useDjareaFormCore<TData extends Record<string, unknown>>(
serverValidation: serverValidationMode = 'on-submit',
} = options
const { call } = useDjarea()
const { call } = useMizan()
// State
const [data, setData] = useState<TData>({} as TData)
@@ -865,7 +865,7 @@ function transformFormsetValidation<TData extends Record<string, unknown>>(
}
/**
* Configuration for useDjareaFormsetCore.
* Configuration for useMizanFormsetCore.
*/
export interface FormsetCoreConfig<TData extends Record<string, unknown>> {
/** Form name (used for server function calls) */
@@ -892,7 +892,7 @@ export interface FormsetCoreConfig<TData extends Record<string, unknown>> {
*
* @internal
*/
export function useDjareaFormsetCore<TData extends Record<string, unknown>>(
export function useMizanFormsetCore<TData extends Record<string, unknown>>(
config: FormsetCoreConfig<TData>
): DjangoFormsetState<TData> {
const {
@@ -902,7 +902,7 @@ export function useDjareaFormsetCore<TData extends Record<string, unknown>>(
debounceMs = 350,
} = config
const { call } = useDjarea()
const { call } = useMizan()
// State
const [forms, setForms] = useState<TData[]>(

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env node
/**
* Djarea Code Generator CLI
* mizan Code Generator CLI
*
* Generate TypeScript types, React provider, and hooks from Django schemas.
*
* Usage:
* npx djarea-generate # Run once
* npx djarea-generate --watch # Watch mode
* npx mizan-generate # Run once
* npx mizan-generate --watch # Watch mode
*/
import { promises as fs } from 'fs'
import path from 'path'
import { fetchChannelsSchema, fetchDjareaSchema } from './lib/fetch.mjs'
import { generateDjareaFiles } from './lib/djarea.mjs'
import { fetchChannelsSchema, fetchMizanSchema } from './lib/fetch.mjs'
import { generateMizanFiles } from './lib/mizan.mjs'
import { generateChannelsFiles } from './lib/channels.mjs'
import { generateIndex } from './lib/index.mjs'
// Use cwd — the script runs via `npx djarea-generate` from the frontend root
// Use cwd — the script runs via `npx mizan-generate` from the frontend root
const frontendDir = process.cwd()
/**
@@ -70,21 +70,21 @@ async function writeOutput(filePath, content) {
async function generate(config, options = {}) {
const { output } = options
console.log('[djarea] Starting schema generation...')
console.log('[mizan] Starting schema generation...')
const outputPath = output || config.output || 'src/api/generated.ts'
let channelsSchema = null
let djareaSchema = null
let mizanSchema = null
// Fetch and generate channels if available
try {
console.log('[djarea] Fetching channels schema...')
console.log('[mizan] Fetching channels schema...')
channelsSchema = await fetchChannelsSchema(config.source, frontendDir)
const channelCount = channelsSchema['x-djarea-channels']?.length || 0
const channelCount = channelsSchema['x-mizan-channels']?.length || 0
if (channelCount > 0) {
console.log(`[djarea] Found ${channelCount} channels`)
console.log(`[mizan] Found ${channelCount} channels`)
const channelsTypesPath = outputPath.replace(/\.ts$/, '.channels.ts')
const fullChannelsTypesPath = path.resolve(frontendDir, channelsTypesPath)
@@ -95,85 +95,85 @@ async function generate(config, options = {}) {
const { types: channelsTypes, hooks: channelsHooks } = await generateChannelsFiles(channelsSchema)
console.log(`[djarea] Generating -> ${channelsTypesPath}`)
console.log(`[mizan] Generating -> ${channelsTypesPath}`)
await writeOutput(fullChannelsTypesPath, channelsTypes)
if (channelsHooks) {
console.log(`[djarea] Generating -> ${channelsHooksPath}`)
console.log(`[mizan] Generating -> ${channelsHooksPath}`)
await writeOutput(fullChannelsHooksPath, channelsHooks)
}
console.log(`[djarea] Generating -> ${channelsSchemaPath}`)
console.log(`[mizan] Generating -> ${channelsSchemaPath}`)
await writeOutput(fullChannelsSchemaPath, JSON.stringify(channelsSchema, null, 2))
} else {
console.log('[djarea] No channels registered, skipping channels generation')
console.log('[mizan] No channels registered, skipping channels generation')
}
} catch (err) {
console.log(`[djarea] Channels schema not available: ${err.message}`)
console.log(`[mizan] Channels schema not available: ${err.message}`)
}
// Fetch and generate djarea files
// Fetch and generate mizan files
try {
console.log('[djarea] Fetching djarea schema...')
djareaSchema = await fetchDjareaSchema(config.source, frontendDir)
console.log('[mizan] Fetching mizan schema...')
mizanSchema = await fetchMizanSchema(config.source, frontendDir)
const functionCount = djareaSchema['x-djarea-functions']?.length || 0
const functionCount = mizanSchema['x-mizan-functions']?.length || 0
if (functionCount > 0) {
console.log(`[djarea] Found ${functionCount} djarea functions`)
console.log(`[mizan] Found ${functionCount} mizan functions`)
const djareaTypesPath = outputPath.replace(/\.ts$/, '.djarea.ts')
const fullDjareaTypesPath = path.resolve(frontendDir, djareaTypesPath)
const djareaProviderPath = outputPath.replace(/\.ts$/, '.django.tsx')
const fullDjareaProviderPath = path.resolve(frontendDir, djareaProviderPath)
const djareaServerPath = outputPath.replace(/\.ts$/, '.django.server.ts')
const fullDjareaServerPath = path.resolve(frontendDir, djareaServerPath)
const djareaFormsPath = outputPath.replace(/\.ts$/, '.forms.ts')
const fullDjareaFormsPath = path.resolve(frontendDir, djareaFormsPath)
const djareaSchemaPath = outputPath.replace(/\.ts$/, '.djarea.schema.json')
const fullDjareaSchemaPath = path.resolve(frontendDir, djareaSchemaPath)
const mizanTypesPath = outputPath.replace(/\.ts$/, '.mizan.ts')
const fullMizanTypesPath = path.resolve(frontendDir, mizanTypesPath)
const mizanProviderPath = outputPath.replace(/\.ts$/, '.django.tsx')
const fullMizanProviderPath = path.resolve(frontendDir, mizanProviderPath)
const mizanServerPath = outputPath.replace(/\.ts$/, '.django.server.ts')
const fullMizanServerPath = path.resolve(frontendDir, mizanServerPath)
const mizanFormsPath = outputPath.replace(/\.ts$/, '.forms.ts')
const fullMizanFormsPath = path.resolve(frontendDir, mizanFormsPath)
const mizanSchemaPath = outputPath.replace(/\.ts$/, '.mizan.schema.json')
const fullMizanSchemaPath = path.resolve(frontendDir, mizanSchemaPath)
const hasChannels = (channelsSchema?.['x-djarea-channels']?.length || 0) > 0
const { types: djareaTypes, provider: djareaProvider, server: djareaServer, forms: djareaForms } = await generateDjareaFiles(djareaSchema, { hasChannels })
const hasChannels = (channelsSchema?.['x-mizan-channels']?.length || 0) > 0
const { types: mizanTypes, provider: mizanProvider, server: mizanServer, forms: mizanForms } = await generateMizanFiles(mizanSchema, { hasChannels })
console.log(`[djarea] Generating -> ${djareaTypesPath}`)
await writeOutput(fullDjareaTypesPath, djareaTypes)
console.log(`[mizan] Generating -> ${mizanTypesPath}`)
await writeOutput(fullMizanTypesPath, mizanTypes)
if (djareaProvider) {
console.log(`[djarea] Generating -> ${djareaProviderPath}`)
await writeOutput(fullDjareaProviderPath, djareaProvider)
if (mizanProvider) {
console.log(`[mizan] Generating -> ${mizanProviderPath}`)
await writeOutput(fullMizanProviderPath, mizanProvider)
}
if (djareaServer) {
console.log(`[djarea] Generating -> ${djareaServerPath}`)
await writeOutput(fullDjareaServerPath, djareaServer)
if (mizanServer) {
console.log(`[mizan] Generating -> ${mizanServerPath}`)
await writeOutput(fullMizanServerPath, mizanServer)
}
if (djareaForms) {
console.log(`[djarea] Generating -> ${djareaFormsPath}`)
await writeOutput(fullDjareaFormsPath, djareaForms)
if (mizanForms) {
console.log(`[mizan] Generating -> ${mizanFormsPath}`)
await writeOutput(fullMizanFormsPath, mizanForms)
}
console.log(`[djarea] Generating -> ${djareaSchemaPath}`)
await writeOutput(fullDjareaSchemaPath, JSON.stringify(djareaSchema, null, 2))
console.log(`[mizan] Generating -> ${mizanSchemaPath}`)
await writeOutput(fullMizanSchemaPath, JSON.stringify(mizanSchema, null, 2))
} else {
console.log('[djarea] No djarea functions registered, skipping djarea generation')
console.log('[mizan] No mizan functions registered, skipping mizan generation')
}
} catch (err) {
console.log(`[djarea] Djarea schema not available: ${err.message}`)
console.log(`[mizan] mizan schema not available: ${err.message}`)
}
// Generate consolidated index.ts
const indexPath = path.dirname(outputPath) + '/index.ts'
const fullIndexPath = path.resolve(frontendDir, indexPath)
console.log(`[djarea] Generating -> ${indexPath}`)
console.log(`[mizan] Generating -> ${indexPath}`)
const indexContent = generateIndex({
channelsSchema,
djareaSchema,
mizanSchema,
})
await writeOutput(fullIndexPath, indexContent)
console.log('[djarea] Generation complete!')
console.log('[mizan] Generation complete!')
}
/**
@@ -194,7 +194,7 @@ async function watch(config, options) {
try {
await generate(config, options)
} catch (err) {
console.error('[djarea] Generation failed:', err.message)
console.error('[mizan] Generation failed:', err.message)
} finally {
running = false
}
@@ -202,7 +202,7 @@ async function watch(config, options) {
await runGenerate()
console.log('[djarea] Watching for changes (press Ctrl+C to stop)...')
console.log('[mizan] Watching for changes (press Ctrl+C to stop)...')
if (config.source.django) {
const { watch: chokidarWatch } = await import('chokidar')
@@ -221,14 +221,14 @@ async function watch(config, options) {
})
watcher.on('change', (filePath) => {
console.log(`[djarea] Detected change: ${path.relative(djangoDir, filePath)}`)
console.log(`[mizan] Detected change: ${path.relative(djangoDir, filePath)}`)
if (timeout) clearTimeout(timeout)
timeout = setTimeout(runGenerate, debounce)
})
}
process.on('SIGINT', () => {
console.log('\n[djarea] Stopping watch mode...')
console.log('\n[mizan] Stopping watch mode...')
process.exit(0)
})
}
@@ -252,10 +252,10 @@ async function main() {
output = args[++i]
} else if (args[i] === '--help' || args[i] === '-h') {
console.log(`
Djarea Code Generator - Generate TypeScript from Django schemas
mizan Code Generator - Generate TypeScript from Django schemas
Usage:
npx djarea-generate [options]
npx mizan-generate [options]
Options:
-c, --config <path> Config file path (default: django.config.mjs)
@@ -278,6 +278,6 @@ Options:
}
main().catch(err => {
console.error('[djarea] Error:', err.message)
console.error('[mizan] Error:', err.message)
process.exit(1)
})

View File

@@ -16,7 +16,7 @@ export async function generateChannelsTypes(schema) {
const typesCode = astToString(ast)
const lines = [
'// AUTO-GENERATED by djarea - do not edit manually',
'// AUTO-GENERATED by mizan - do not edit manually',
'// Regenerate with: npm run schemas',
'',
'// ============================================================================',
@@ -27,8 +27,8 @@ export async function generateChannelsTypes(schema) {
'',
]
// Extract channel metadata from x-djarea-channels extension
const channels = schema['x-djarea-channels'] || []
// Extract channel metadata from x-mizan-channels extension
const channels = schema['x-mizan-channels'] || []
if (channels.length > 0) {
lines.push('// ============================================================================')
@@ -86,7 +86,7 @@ export async function generateChannelsTypes(schema) {
* Generate channel hooks from metadata.
*/
export function generateChannelsHooks(schema) {
const channels = schema['x-djarea-channels'] || []
const channels = schema['x-mizan-channels'] || []
if (channels.length === 0) {
return null
@@ -95,10 +95,10 @@ export function generateChannelsHooks(schema) {
const lines = [
"'use client'",
'',
'// AUTO-GENERATED by djarea - do not edit manually',
'// AUTO-GENERATED by mizan - do not edit manually',
'// Regenerate with: npm run schemas',
'',
"import { useChannel, type ChannelSubscription } from 'djarea/channels'",
"import { useChannel, type ChannelSubscription } from 'mizan/channels'",
'',
]

View File

@@ -1,12 +1,12 @@
/**
* Djarea Code Generator
* mizan Code Generator
*
* Generates TypeScript types and React provider from Djarea OpenAPI schema.
* Generates TypeScript types and React provider from mizan OpenAPI schema.
* Uses openapi-typescript for robust type generation.
*
* Output structure:
* - generated.djarea.ts - Types only (from OpenAPI)
* - generated.provider.tsx - Typed provider wrapping DjareaProvider + hooks
* - generated.mizan.ts - Types only (from OpenAPI)
* - generated.provider.tsx - Typed provider wrapping MizanProvider + hooks
* - generated.forms.ts - Typed form hooks with Zod schemas
*/
@@ -74,14 +74,14 @@ function buildSchemaExports(schemaNames) {
/**
* Generate the types file using openapi-typescript.
*/
export async function generateDjareaTypes(schema) {
export async function generateMizanTypes(schema) {
// Generate types using openapi-typescript
const ast = await openapiTS(schema)
const schemaNames = getSchemaNamesFromAst(ast)
const typesCode = astToString(ast)
const lines = [
'// AUTO-GENERATED by djarea - do not edit manually',
'// AUTO-GENERATED by mizan - do not edit manually',
'// Regenerate with: npm run schemas',
'',
'// ============================================================================',
@@ -104,8 +104,8 @@ export async function generateDjareaTypes(schema) {
'',
]
// Extract function metadata from x-djarea-functions extension
const functions = schema['x-djarea-functions'] || []
// Extract function metadata from x-mizan-functions extension
const functions = schema['x-mizan-functions'] || []
if (functions.length > 0) {
lines.push('export const DJANGO_FUNCTIONS = {')
@@ -128,16 +128,16 @@ export async function generateDjareaTypes(schema) {
}
/**
* Generate the React provider that wraps DjareaProvider with typed hooks.
* Generate the React provider that wraps MizanProvider with typed hooks.
*
* The generated provider:
* - Wraps DjareaProvider (from djarea library)
* - Wraps MizanProvider (from mizan library)
* - Passes context names for auto-fetch
* - Provides typed hooks for contexts and functions
*/
export function generateDjareaProvider(schema, options = {}) {
export function generateMizanProvider(schema, options = {}) {
const { hasChannels = false } = options
const functions = schema['x-djarea-functions'] || []
const functions = schema['x-mizan-functions'] || []
if (functions.length === 0) {
return null
@@ -162,31 +162,31 @@ export function generateDjareaProvider(schema, options = {}) {
const lines = [
"'use client'",
'',
'// AUTO-GENERATED by djarea - do not edit manually',
'// AUTO-GENERATED by mizan - do not edit manually',
'// Regenerate with: npm run schemas',
'',
'// This file provides typed wrappers around the djarea library.',
'// - DjangoContext: Typed provider wrapping DjareaProvider',
'// This file provides typed wrappers around the mizan library.',
'// - DjangoContext: Typed provider wrapping MizanProvider',
'// - Typed hooks: useAuthStatus(), useUser(), etc.',
'',
"import { type ReactNode, useCallback } from 'react'",
"import {",
" DjareaProvider,",
" useDjarea,",
" useDjareaContext,",
" useDjareaCall,",
" type DjareaHydration,",
" MizanProvider,",
" useMizan,",
" useMizanContext,",
" useMizanCall,",
" type MizanHydration,",
" type Transport,",
"} from 'djarea'",
"} from 'mizan'",
...(hasChannels ? [
"import { ChannelProvider, ChannelConnection } from 'djarea/channels'",
"import { ChannelProvider, ChannelConnection } from 'mizan/channels'",
"import { useRef } from 'react'",
] : []),
'',
]
if (uniqueTypeImports.length > 0) {
lines.push(`import type { ${uniqueTypeImports.join(', ')} } from './generated.djarea'`)
lines.push(`import type { ${uniqueTypeImports.join(', ')} } from './generated.mizan'`)
lines.push('')
}
@@ -208,10 +208,10 @@ export function generateDjareaProvider(schema, options = {}) {
lines.push('}')
lines.push('')
lines.push('/** Convert typed hydration to djarea format */')
lines.push('function toDjareaHydration(hydration?: DjangoHydration): DjareaHydration | undefined {')
lines.push('/** Convert typed hydration to mizan format */')
lines.push('function toMizanHydration(hydration?: DjangoHydration): MizanHydration | undefined {')
lines.push(' if (!hydration) return undefined')
lines.push(' const result: DjareaHydration = {}')
lines.push(' const result: MizanHydration = {}')
for (const ctx of contexts) {
lines.push(` if (hydration.${ctx.camelName} !== undefined) result['${ctx.name}'] = hydration.${ctx.camelName}`)
}
@@ -237,18 +237,18 @@ export function generateDjareaProvider(schema, options = {}) {
}
lines.push(' /** WebSocket URL for RPC calls (default: /ws/) */')
lines.push(' wsUrl?: string')
lines.push(' /** Base URL for HTTP fallback (default: /api/djarea) */')
lines.push(' /** Base URL for HTTP fallback (default: /api/mizan) */')
lines.push(' baseUrl?: string')
lines.push('}')
lines.push('')
// Context names array for DjareaProvider
// Context names array for MizanProvider
const contextNames = contexts.map(ctx => `'${ctx.name}'`).join(', ')
lines.push('/**')
lines.push(' * Typed Django context provider.')
lines.push(' *')
lines.push(' * Wraps DjareaProvider with:')
lines.push(' * Wraps MizanProvider with:')
lines.push(' * - Typed hydration')
lines.push(' * - Auto-fetch for registered contexts')
lines.push(' *')
@@ -275,9 +275,9 @@ export function generateDjareaProvider(schema, options = {}) {
}
lines.push(' return (')
lines.push(' <DjareaProvider')
lines.push(' <MizanProvider')
if (contexts.length > 0) {
lines.push(' hydration={toDjareaHydration(hydration)}')
lines.push(' hydration={toMizanHydration(hydration)}')
lines.push(` contexts={[${contextNames}]}`)
}
lines.push(' wsUrl={wsUrl}')
@@ -295,7 +295,7 @@ export function generateDjareaProvider(schema, options = {}) {
lines.push(' {children}')
}
lines.push(' </DjareaProvider>')
lines.push(' </MizanProvider>')
lines.push(' )')
lines.push('}')
lines.push('')
@@ -317,7 +317,7 @@ export function generateDjareaProvider(schema, options = {}) {
lines.push(` * @throws if context not loaded yet`)
lines.push(` */`)
lines.push(`export function use${pascal}(): ${ctx.outputType} {`)
lines.push(` const data = useDjareaContext<${ctx.outputType}>('${ctx.name}')`)
lines.push(` const data = useMizanContext<${ctx.outputType}>('${ctx.name}')`)
lines.push(` if (data === undefined) {`)
lines.push(` throw new Error('use${pascal}: context not loaded yet')`)
lines.push(` }`)
@@ -332,7 +332,7 @@ export function generateDjareaProvider(schema, options = {}) {
lines.push(' * Use this in components that only need to trigger refreshes.')
lines.push(' */')
lines.push('export function useDjangoRefresh() {')
lines.push(' const { refreshContext, refreshAllContexts } = useDjarea()')
lines.push(' const { refreshContext, refreshAllContexts } = useMizan()')
lines.push(' return {')
for (const ctx of contexts) {
const pascal = pascalCase(ctx.camelName)
@@ -365,7 +365,7 @@ export function generateDjareaProvider(schema, options = {}) {
lines.push(` * Transport: ${transport}`)
lines.push(` */`)
lines.push(`export function use${pascal}() {`)
lines.push(` return useDjareaCall<${fn.inputType}, ${fn.outputType}>('${fn.name}', '${transport}')`)
lines.push(` return useMizanCall<${fn.inputType}, ${fn.outputType}>('${fn.name}', '${transport}')`)
lines.push(`}`)
} else {
lines.push(`/**`)
@@ -373,7 +373,7 @@ export function generateDjareaProvider(schema, options = {}) {
lines.push(` * Transport: ${transport}`)
lines.push(` */`)
lines.push(`export function use${pascal}() {`)
lines.push(` return useDjareaCall<void, ${fn.outputType}>('${fn.name}', '${transport}')`)
lines.push(` return useMizanCall<void, ${fn.outputType}>('${fn.name}', '${transport}')`)
lines.push(`}`)
}
lines.push('')
@@ -385,11 +385,11 @@ export function generateDjareaProvider(schema, options = {}) {
// ============================================================================
lines.push('// ============================================================================')
lines.push('// Re-exports from djarea library')
lines.push('// Re-exports from mizan library')
lines.push('// ============================================================================')
lines.push('')
lines.push("export { useDjarea, useDjareaStatus, usePush, DjangoError } from 'djarea'")
lines.push("export type { ConnectionStatus, PushMessage, PushListener } from 'djarea'")
lines.push("export { useMizan, useMizanStatus, usePush, DjangoError } from 'mizan'")
lines.push("export type { ConnectionStatus, PushMessage, PushListener } from 'mizan'")
lines.push('')
return lines.join('\n')
@@ -399,8 +399,8 @@ export function generateDjareaProvider(schema, options = {}) {
* Generate server-side hydration helper (runs in Next.js server components).
* This is separate from the client file because it needs to run on the server.
*/
export function generateDjareaServer(schema) {
const functions = schema['x-djarea-functions'] || []
export function generateMizanServer(schema) {
const functions = schema['x-mizan-functions'] || []
const contexts = functions.filter(fn => fn.isContext)
if (contexts.length === 0) {
@@ -412,7 +412,7 @@ export function generateDjareaServer(schema) {
const uniqueTypeImports = [...new Set(typeImports)].sort()
const lines = [
'// AUTO-GENERATED by djarea - do not edit manually',
'// AUTO-GENERATED by mizan - do not edit manually',
'// Regenerate with: npm run schemas',
'//',
'// Server-side functions for SSR hydration.',
@@ -421,7 +421,7 @@ export function generateDjareaServer(schema) {
]
if (uniqueTypeImports.length > 0) {
lines.push(`import type { ${uniqueTypeImports.join(', ')} } from './generated.djarea'`)
lines.push(`import type { ${uniqueTypeImports.join(', ')} } from './generated.mizan'`)
lines.push('')
}
@@ -457,7 +457,7 @@ export function generateDjareaServer(schema) {
lines.push('')
lines.push(' const results = await Promise.allSettled([')
for (const ctx of contexts) {
lines.push(` client.request('POST', '/api/djarea/call/', { fn: '${ctx.name}', args: {} }),`)
lines.push(` client.request('POST', '/api/mizan/call/', { fn: '${ctx.name}', args: {} }),`)
}
lines.push(' ])')
lines.push('')
@@ -484,13 +484,13 @@ export function generateDjareaServer(schema) {
}
/**
* Generate all djarea files.
* Generate all mizan files.
*/
export async function generateDjareaFiles(schema, options = {}) {
const types = await generateDjareaTypes(schema)
const provider = generateDjareaProvider(schema, options)
const server = generateDjareaServer(schema)
const forms = generateDjareaForms(schema)
export async function generateMizanFiles(schema, options = {}) {
const types = await generateMizanTypes(schema)
const provider = generateMizanProvider(schema, options)
const server = generateMizanServer(schema)
const forms = generateMizanForms(schema)
return { types, provider, server, forms }
}
@@ -498,8 +498,8 @@ export async function generateDjareaFiles(schema, options = {}) {
/**
* Generate typed form hooks with Zod schemas.
*/
export function generateDjareaForms(schema) {
const functions = schema['x-djarea-functions'] || []
export function generateMizanForms(schema) {
const functions = schema['x-mizan-functions'] || []
// Group form functions by form name
const formFunctions = functions.filter(fn => fn.isForm)
@@ -535,7 +535,7 @@ export function generateDjareaForms(schema) {
const lines = [
"'use client'",
'',
'// AUTO-GENERATED by djarea - do not edit manually',
'// AUTO-GENERATED by mizan - do not edit manually',
'// Regenerate with: npm run schemas',
'',
'// Typed form hooks with Zod validation.',
@@ -549,7 +549,7 @@ export function generateDjareaForms(schema) {
" type DjangoFormState,",
" type DjangoFormsetState,",
" type FormOptions,",
"} from 'djarea'",
"} from 'mizan'",
'',
'// ============================================================================',
'// Zod Schemas',

View File

@@ -1,7 +1,7 @@
/**
* Schema Fetching
*
* Fetches djarea and channels schemas from Django management commands.
* Fetches mizan and channels schemas from Django management commands.
*/
import { spawn } from 'child_process'
@@ -78,11 +78,11 @@ export async function fetchChannelsSchema(source, cwd) {
}
/**
* Fetch djarea schema from Django.
* Fetch mizan schema from Django.
*/
export async function fetchDjareaSchema(source, cwd) {
export async function fetchMizanSchema(source, cwd) {
if (!source.django) {
throw new Error('Djarea schema export requires django source configuration')
throw new Error('mizan schema export requires django source configuration')
}
return runDjangoCommand(source, cwd, 'export_djarea_schema')
return runDjangoCommand(source, cwd, 'export_mizan_schema')
}

View File

@@ -6,11 +6,11 @@
*/
/**
* Extract context hooks from djarea schema.
* Extract context hooks from mizan schema.
* Returns hook names in PascalCase (e.g., useAuthStatus, useUser).
*/
function extractContextHooks(djareaSchema) {
const functions = djareaSchema?.['x-djarea-functions'] || []
function extractContextHooks(mizanSchema) {
const functions = mizanSchema?.['x-mizan-functions'] || []
const contexts = functions.filter(fn => fn.isContext)
return contexts.map(ctx => {
@@ -24,13 +24,13 @@ function extractContextHooks(djareaSchema) {
*
* @param {Object} options - Generation options
* @param {Object} options.channelsSchema - Channels schema (optional)
* @param {Object} options.djareaSchema - Djarea schema (optional)
* @param {Object} options.mizanSchema - mizan schema (optional)
* @returns {string} Generated index.ts content
*/
export function generateIndex({ channelsSchema, djareaSchema }) {
export function generateIndex({ channelsSchema, mizanSchema }) {
const lines = [
'/**',
' * Djarea API - Consolidated Exports',
' * mizan API - Consolidated Exports',
' *',
' * Import everything from here:',
' *',
@@ -46,25 +46,25 @@ export function generateIndex({ channelsSchema, djareaSchema }) {
' * ```',
' */',
'',
'// AUTO-GENERATED by djarea - do not edit manually',
'// AUTO-GENERATED by mizan - do not edit manually',
'// Regenerate with: npm run schemas',
'',
]
// ==========================================================================
// Djarea Provider & Hooks (from generated.django.tsx)
// mizan Provider & Hooks (from generated.django.tsx)
// ==========================================================================
const functions = djareaSchema?.['x-djarea-functions'] || []
const hasDjarea = functions.length > 0
const functions = mizanSchema?.['x-mizan-functions'] || []
const hasMizan = functions.length > 0
if (hasDjarea) {
const contextHooks = extractContextHooks(djareaSchema)
if (hasMizan) {
const contextHooks = extractContextHooks(mizanSchema)
const contexts = functions.filter(fn => fn.isContext)
const regularFunctions = functions.filter(fn => !fn.isContext && !fn.isForm)
lines.push('// =============================================================================')
lines.push('// Djarea Provider & Hooks')
lines.push('// mizan Provider & Hooks')
lines.push('// =============================================================================')
lines.push('')
@@ -104,9 +104,9 @@ export function generateIndex({ channelsSchema, djareaSchema }) {
}
lines.push('')
lines.push(' // Re-exports from djarea library')
lines.push(' useDjarea,')
lines.push(' useDjareaStatus,')
lines.push(' // Re-exports from mizan library')
lines.push(' useMizan,')
lines.push(' useMizanStatus,')
lines.push(' usePush,')
lines.push(' DjangoError,')
lines.push(' type ConnectionStatus,')
@@ -120,7 +120,7 @@ export function generateIndex({ channelsSchema, djareaSchema }) {
// Channel Hooks (from generated.channels.hooks.tsx)
// ==========================================================================
const channels = channelsSchema?.['x-djarea-channels'] || []
const channels = channelsSchema?.['x-mizan-channels'] || []
if (channels.length > 0) {
lines.push('// =============================================================================')

View File

@@ -1,5 +1,5 @@
/**
* Djarea - Django Server Functions Client
* mizan - Django Server Functions Client
*
* Frontend client for Django server functions.
* Server functions are the core primitive - accessed via React hooks.
@@ -9,9 +9,9 @@
* 1. Library layer (this package) - Generic name-based API
* Used by libraries like Allauth that need to call functions by name.
*
* import { useDjarea, useDjareaContext, useDjareaCall } from 'djarea'
* const user = useDjareaContext('current_user')
* const call = useDjareaCall('update_profile')
* import { useMizan, useMizanContext, useMizanCall } from 'mizan'
* const user = useMizanContext('current_user')
* const call = useMizanCall('update_profile')
*
* 2. Generated layer (@/api) - Typed project-specific API
* Used by product code for type-safe hooks.
@@ -20,7 +20,7 @@
* const user = useCurrentUser()
* const updateProfile = useUpdateProfile()
*
* The generated code wraps DjareaProvider and adds type-safe hooks.
* The generated code wraps MizanProvider and adds type-safe hooks.
*/
// ============================================================================
@@ -29,19 +29,19 @@
export {
// Provider
DjareaProvider,
type DjareaProviderProps,
type DjareaHydration,
MizanProvider,
type MizanProviderProps,
type MizanHydration,
// Hooks (generic name-based API for libraries)
useDjarea,
useDjareaContext,
useDjareaCall,
useDjareaStatus,
useMizan,
useMizanContext,
useMizanCall,
useMizanStatus,
usePush,
// Types
type DjareaContextValue,
type MizanContextValue,
type ConnectionStatus,
type PushMessage,
type PushListener,
@@ -89,9 +89,9 @@ export {
export {
// Single form
useDjareaFormCore,
useMizanFormCore,
// Legacy alias
useDjareaFormCore as useDjangoFormCore,
useMizanFormCore as useDjangoFormCore,
type DjangoFormState,
type FormSchema,
type FormErrors,
@@ -99,9 +99,9 @@ export {
type FormSubmitResult,
type FormCoreConfig,
// Formset
useDjareaFormsetCore,
useMizanFormsetCore,
// Legacy alias
useDjareaFormsetCore as useDjangoFormsetCore,
useMizanFormsetCore as useDjangoFormsetCore,
type DjangoFormsetState,
type FormsetSchema,
type FormsetErrors,

View File

@@ -21,7 +21,7 @@ const Context = createContext<JWTState | null>(null)
const DEFAULT_CONFIG: Required<JWTConfig> = {
baseUrl: '',
endpoint: '/api/djarea/call/',
endpoint: '/api/mizan/call/',
refreshBuffer: 30,
autoObtain: true,
autoRefresh: true,

View File

@@ -1,13 +1,13 @@
/**
* Contract Tests for Djarea JWT Server Functions
* Contract Tests for mizan JWT Server Functions
*
* Validates that the backend schema exports the expected JWT functions.
* These tests catch frontend/backend contract mismatches early.
*/
import djareaSchema from '@/api/generated.djarea.schema.json'
import mizanSchema from '@/api/generated.mizan.schema.json'
type DjareaFunction = {
type mizanFunction = {
name: string
camelName: string
hasInput: boolean
@@ -16,11 +16,11 @@ type DjareaFunction = {
transport: string
}
function getFunctions(): DjareaFunction[] {
return (djareaSchema as any)['x-djarea-functions'] ?? []
function getFunctions(): mizanFunction[] {
return (mizanSchema as any)['x-mizan-functions'] ?? []
}
function findFunction(name: string): DjareaFunction | undefined {
function findFunction(name: string): mizanFunction | undefined {
return getFunctions().find(fn => fn.name === name)
}
@@ -38,7 +38,7 @@ describe('JWT Server Functions Contract', () => {
})
it('should return token pair with expected fields', () => {
const schemas = (djareaSchema as any).components?.schemas
const schemas = (mizanSchema as any).components?.schemas
const output = schemas?.jwtObtainOutput
expect(output).toBeDefined()
@@ -59,7 +59,7 @@ describe('JWT Server Functions Contract', () => {
const fn = findFunction('jwt_refresh')
expect(fn?.hasInput).toBe(true)
const schemas = (djareaSchema as any).components?.schemas
const schemas = (mizanSchema as any).components?.schemas
const input = schemas?.jwtRefreshInput
expect(input).toBeDefined()
@@ -67,7 +67,7 @@ describe('JWT Server Functions Contract', () => {
})
it('should return token pair with expected fields', () => {
const schemas = (djareaSchema as any).components?.schemas
const schemas = (mizanSchema as any).components?.schemas
const output = schemas?.jwtRefreshOutput
expect(output).toBeDefined()

View File

@@ -1,7 +1,7 @@
/**
* djarea/jwt
* mizan/jwt
*
* JWT token management via djarea server functions.
* JWT token management via mizan server functions.
* Handles token lifecycle: obtain, refresh, clear.
*
* ## Quick Start
@@ -9,8 +9,8 @@
* Use JWTContext in authenticated areas (e.g., inside UserRoute):
*
* ```tsx
* import { JWTContext } from 'djarea/jwt'
* import { UserRoute } from 'djarea/allauth'
* import { JWTContext } from 'mizan/jwt'
* import { UserRoute } from 'mizan/allauth'
*
* function ProtectedPage() {
* return (
@@ -26,7 +26,7 @@
* Then use JWT-authenticated requests:
*
* ```tsx
* import { useDjangoCSRClient, Auth } from 'djarea/client/react'
* import { useDjangoCSRClient, Auth } from 'mizan/client/react'
*
* function MyProtectedContent() {
* const client = useDjangoCSRClient(Auth.JWT)
@@ -40,7 +40,7 @@
*
* ## How It Works
*
* 1. JWTContext calls jwt_obtain server function (via /api/djarea/call/)
* 1. JWTContext calls jwt_obtain server function (via /api/mizan/call/)
* 2. If not authenticated, returns FORBIDDEN (tokens stay null)
* 3. Client uses getAccessToken() for Bearer token injection
* 4. Tokens auto-refresh via jwt_refresh server function
@@ -51,7 +51,7 @@
* ```tsx
* <JWTContext
* config={{
* endpoint: '/api/djarea/call/', // default
* endpoint: '/api/mizan/call/', // default
* refreshBuffer: 30, // refresh 30s before expiry
* autoObtain: true, // obtain on mount
* autoRefresh: true, // auto-refresh before expiry
@@ -62,7 +62,7 @@
* ## Manual Token Management
*
* ```tsx
* import { useJWT } from 'djarea/jwt'
* import { useJWT } from 'mizan/jwt'
*
* function LogoutButton() {
* const jwt = useJWT()

View File

@@ -31,7 +31,7 @@ export const describeIntegration = runIntegrationTests ? describe : describe.ski
*/
export const BACKEND_URL = (() => {
if (!process.env.NEXT_PUBLIC_HOST_URL) {
console.warn('[djarea/testing] NEXT_PUBLIC_HOST_URL not set, falling back to http://localhost')
console.warn('[mizan/testing] NEXT_PUBLIC_HOST_URL not set, falling back to http://localhost')
}
return process.env.NEXT_PUBLIC_HOST_URL || 'http://localhost'
})()

View File

@@ -14,8 +14,8 @@
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"djarea": ["./src/index.ts"],
"djarea/*": ["./src/*"]
"mizan": ["./src/index.ts"],
"mizan/*": ["./src/*"]
}
},
"include": ["src"],

View File

@@ -4,14 +4,14 @@ import path from 'path'
export default defineConfig({
resolve: {
alias: {
'djarea/channels': path.resolve(__dirname, 'src/channels/index.ts'),
'djarea/client/react': path.resolve(__dirname, 'src/client/react.ts'),
'djarea/client/nextjs': path.resolve(__dirname, 'src/client/nextjs.tsx'),
'djarea/client': path.resolve(__dirname, 'src/client/index.ts'),
'djarea/jwt': path.resolve(__dirname, 'src/jwt/index.ts'),
'djarea/allauth/nextjs': path.resolve(__dirname, 'src/allauth/nextjs.tsx'),
'djarea/allauth': path.resolve(__dirname, 'src/allauth/index.ts'),
'djarea': path.resolve(__dirname, 'src/index.ts'),
'mizan/channels': path.resolve(__dirname, 'src/channels/index.ts'),
'mizan/client/react': path.resolve(__dirname, 'src/client/react.ts'),
'mizan/client/nextjs': path.resolve(__dirname, 'src/client/nextjs.tsx'),
'mizan/client': path.resolve(__dirname, 'src/client/index.ts'),
'mizan/jwt': path.resolve(__dirname, 'src/jwt/index.ts'),
'mizan/allauth/nextjs': path.resolve(__dirname, 'src/allauth/nextjs.tsx'),
'mizan/allauth': path.resolve(__dirname, 'src/allauth/index.ts'),
'mizan': path.resolve(__dirname, 'src/index.ts'),
},
},
test: {
@@ -20,7 +20,7 @@ export default defineConfig({
setupFiles: ['./vitest.setup.ts'],
include: ['src/**/*.test.{ts,tsx}'],
exclude: [
// Requires @/api/generated.djarea.schema.json from consuming project
// Requires @/api/generated.mizan.schema.json from consuming project
'src/jwt/__tests__/contract.test.ts',
],
},