/** * Mandu Island - 선언적 Islands Architecture * * @example * ```tsx * import { island } from '@mandujs/core'; * * export default island('visible', ({ name }) => { * const [count, setCount] = useState(0); * return ; * }); * ``` */ import type { ComponentType, ReactNode } from 'react'; import type { z } from 'zod'; // ============================================================================ // Types // ============================================================================ /** 하이드레이션 타이밍 */ export type IslandHydrationStrategy = | 'load' // 페이지 로드 즉시 | 'idle' // requestIdleCallback | 'visible' // IntersectionObserver | 'media' // 미디어 쿼리 매치 시 | 'never'; // SSR only, 하이드레이션 안 함 /** Island 옵션 */ export interface IslandOptions

{ /** 하이드레이션 전략 */ hydrate: IslandHydrationStrategy; /** 미디어 쿼리 (hydrate: 'media' 일 때) */ media?: string; /** SSR 폴백 컴포넌트 */ fallback?: ReactNode; /** Props 스키마 (Zod) - 런타임 검증 */ props?: z.ZodType

; /** Island 이름 (자동 생성됨) */ name?: string; } /** Island 컴포넌트 메타데이터 */ export interface IslandMeta { __island: true; __hydrate: IslandHydrationStrategy; __media?: string; __fallback?: ReactNode; __name: string; __propsSchema?: z.ZodType; } /** Island 컴포넌트 타입 */ export type IslandComponent

= ComponentType

& IslandMeta; // ============================================================================ // Island Registry (서버/클라이언트 공용) // ============================================================================ // Registry bag for heterogeneous React components. React's `ComponentType

` // is contravariant on `P` — a `ComponentType<{id:string}>` is NOT assignable // to `ComponentType>`. The correct bag type for a // heterogeneous component map is `ComponentType`; narrowing happens at // call sites via each island's own typed `IslandComponent

` signature. // Same pattern used in client/island.ts `createPartialGroup()`. // oxlint-disable no-explicit-any -- heterogeneous React component registry type AnyIslandComponent = IslandComponent; const islandRegistry = new Map(); let islandCounter = 0; export function registerIsland(name: string, component: AnyIslandComponent): void { islandRegistry.set(name, component); } export function getIsland(name: string): AnyIslandComponent | undefined { return islandRegistry.get(name); } export function getAllIslands(): Map { return islandRegistry; } // oxlint-enable no-explicit-any // ============================================================================ // Client Island Types (setup/render 패턴) // ============================================================================ /** * Client Island 정의 타입 (setup/render 패턴) * @template TServerData - SSR에서 전달받는 서버 데이터 타입 * @template TSetupResult - setup 함수가 반환하는 결과 타입 */ export interface ClientIslandDefinition { setup: (serverData: TServerData) => TSetupResult; render: (props: TSetupResult) => ReactNode; errorBoundary?: (error: Error, reset: () => void) => ReactNode; loading?: () => ReactNode; } /** Compiled Client Island */ export interface CompiledClientIsland { definition: ClientIslandDefinition; __mandu_island: true; __mandu_island_id?: string; } /** Type guard: is this a ClientIslandDefinition? */ function isClientIslandDefinition(arg: unknown): arg is ClientIslandDefinition { if (arg === null || typeof arg !== 'object') return false; if (!('setup' in arg) || !('render' in arg)) return false; const candidate = arg as { setup?: unknown; render?: unknown }; return typeof candidate.setup === 'function' && typeof candidate.render === 'function'; } // ============================================================================ // island() - 선언적 Island 생성 + Client Island 패턴 // ============================================================================ /** * Island 컴포넌트 생성 (두 가지 패턴 지원) * * @example * // 패턴 1: 선언적 (컴포넌트 래핑) * export default island('visible', ({ name }) =>

{name}
); * * @example * // 패턴 2: Setup/Render (클라이언트 하이드레이션) * export default island({ * setup: (serverData) => { * const [todos, setTodos] = useState(serverData.todos); * return { todos, setTodos }; * }, * render: ({ todos }) => , * }); * * @example * // 패턴 1: 옵션과 함께 * export default island({ * hydrate: 'idle', * fallback: , * props: z.object({ userId: z.string() }), * }, ({ userId }) => { * // ... * }); */ // Using `unknown` generic arguments on the internal impl types keeps the // overloads below as the single source of truth for external type inference // while avoiding `any` in the implementation. Callers always hit one of // the overloads and never see this input/output type. type IslandImplInput = | IslandHydrationStrategy | IslandOptions | ClientIslandDefinition; // Overload 1: Setup/Render client island pattern export function island( definition: ClientIslandDefinition ): CompiledClientIsland; // Overload 2: Declarative with strategy string export function island

>( strategy: IslandHydrationStrategy, Component: ComponentType

): IslandComponent

; // Overload 3: Declarative with options export function island

>( options: IslandOptions

, Component: ComponentType

): IslandComponent

; // Implementation export function island( strategyOrOptionsOrDefinition: IslandImplInput, Component?: ComponentType, ): AnyIslandComponent | CompiledClientIsland { // Pattern 2: Setup/Render client island if (Component === undefined && isClientIslandDefinition(strategyOrOptionsOrDefinition)) { return { definition: strategyOrOptionsOrDefinition, __mandu_island: true, }; } // Pattern 1: Declarative island wrapping if (!Component) { throw new Error('[Mandu Island] Component is required for declarative island pattern'); } const options: IslandOptions = typeof strategyOrOptionsOrDefinition === 'string' ? { hydrate: strategyOrOptionsOrDefinition } : (strategyOrOptionsOrDefinition as IslandOptions); const name = options.name || `island_${++islandCounter}_${Component.name || 'Anonymous'}`; // Island 메타데이터 부착 — mutate the component in-place so the caller's // reference carries the attached `IslandMeta` fields. const IslandWrapper = Component as unknown as AnyIslandComponent; IslandWrapper.__island = true; IslandWrapper.__hydrate = options.hydrate; IslandWrapper.__media = options.media; IslandWrapper.__fallback = options.fallback; IslandWrapper.__name = name; IslandWrapper.__propsSchema = options.props as IslandMeta['__propsSchema']; // 레지스트리에 등록 registerIsland(name, IslandWrapper); return IslandWrapper; } // ============================================================================ // isIsland() - Island 컴포넌트 체크 // ============================================================================ export function isIsland(component: unknown): component is AnyIslandComponent { return ( typeof component === 'function' && (component as Partial).__island === true ); } // ============================================================================ // serializeIslandProps() - Props 직렬화 // ============================================================================ export function serializeIslandProps(props: Record): string { return JSON.stringify(props, (_, value) => { // Date 처리 if (value instanceof Date) { return { __type: 'Date', value: value.toISOString() }; } // Map 처리 if (value instanceof Map) { return { __type: 'Map', value: Array.from(value.entries()) }; } // Set 처리 if (value instanceof Set) { return { __type: 'Set', value: Array.from(value) }; } // 함수는 직렬화 불가 if (typeof value === 'function') { console.warn('[Mandu Island] Functions cannot be serialized as props'); return undefined; } return value; }); } // ============================================================================ // deserializeIslandProps() - Props 역직렬화 // ============================================================================ export function deserializeIslandProps(json: string): Record { return JSON.parse(json, (_, value) => { if (value && typeof value === 'object' && '__type' in value) { switch (value.__type) { case 'Date': return new Date(value.value); case 'Map': return new Map(value.value); case 'Set': return new Set(value.value); } } return value; }); } // ============================================================================ // createIslandPlaceholder() - SSR용 플레이스홀더 생성 // ============================================================================ export interface IslandPlaceholderProps { name: string; props: Record; hydrate: IslandHydrationStrategy; media?: string; fallback?: ReactNode; } export function createIslandPlaceholder({ name, props, hydrate, media, fallback, }: IslandPlaceholderProps): string { const serializedProps = serializeIslandProps(props); const fallbackHtml = fallback ? renderFallback(fallback) : '

Loading...
'; return `
${fallbackHtml}
`; } function escapeHtml(str: string): string { return str .replace(/&/g, '&') .replace(/'/g, ''') .replace(/"/g, '"') .replace(//g, '>'); } function renderFallback(fallback: ReactNode): string { // 간단한 fallback 처리 (실제로는 react-dom/server 사용) if (typeof fallback === 'string') return fallback; if (fallback === null || fallback === undefined) return ''; return '
Loading...
'; } // ============================================================================ // Client Hydration Script // ============================================================================ export const ISLAND_HYDRATION_SCRIPT = ` `; // ============================================================================ // Exports // ============================================================================ export type { ComponentType, ReactNode };