import React, { createContext, forwardRef, Fragment, lazy, useContext, useMemo, useRef, type ComponentType, type FC, type ReactNode, } from 'react'; import type { MaybePromise } from '@wener/utils'; export interface DefineComponentOptions

{ name: string; title?: string; props?: P; schema?: any; metadata?: Record; Component?: ComponentType

; load?: () => Promise | { default: ComponentType

}>; } export interface ComponentDef { name: string; title: string; props?: Record; schema?: any; Component: ComponentType; metadata: Record; } let _components: ComponentDef[] = []; export function defineComponent

({ Component: _Component, load, ...opts }: DefineComponentOptions

): ContextComponentType

{ { const Component = createComponent({ Component: _Component as any, load, }); // if (Component === Fragment) { // console.warn(`Component ${opts.name} not resolved`); // } const def: ComponentDef = { title: opts.title || opts.name, ...opts, Component, metadata: {}, }; let last = _components.find((v) => v.name === def.name); if (last) { console.error(`Component ${def.name} already defined`); // last.schema = def.schema; // last.props = def.props; // last.metadata = def.metadata; } _components.unshift(def); } const name = opts.name; let component = Object.assign( forwardRef>((props, ref) => { return ; }), { [ComponentNamePropKey]: name, }, ) as ContextComponentType

; return component; } export function getComponents() { return _components; } export const ConsumeComponent = forwardRef>(({ $name, ...props }, ref) => { const [Component] = useComponent($name); return ; }); const ComponentNamePropKey = '$ContextComponentName'; export type ContextComponentType

= ComponentType

& { [ComponentNamePropKey]: string }; export function createContextComponent

(name: string): ContextComponentType

{ let component = Object.assign( forwardRef>((props, ref) => { return ; }), { [ComponentNamePropKey]: name, }, ) as ContextComponentType

; component.displayName = `${ComponentNamePropKey}(${name})`; return component; } type LoadableComponent

= () => MaybePromise | { default: ComponentType

}>; type ProvidedComponent

= { provide: NameLike

; Component?: ComponentType

; load?: LoadableComponent

; }; export type ComponentProviderProps = { components: Array; children?: ReactNode; }; type NameLike

= string | ContextComponentType

| ComponentType

; type ComponentContextObject = { parent?: ComponentContextObject; components: ProvidedComponent[]; useComponent:

(comp: NameLike

) => UseComponentResult

; }; type ComponentProviderState = {}; type UseComponentResult

= [ComponentType

, { found: boolean }]; const RootValue: ComponentContextObject = { get components() { return _components.map((v) => { return { provide: v.name, Component: v.Component, }; }); }, useComponent: (name) => resolveComponent(name, RootValue), }; const ComponentContext = createContext(RootValue); function resolveName

(def: NameLike

) { let name: string; if (typeof def === 'string') { name = def; } else { name = (def as ContextComponentType)[ComponentNamePropKey] || (def as ComponentType).displayName || // (def as ComponentType).name || // this is not reliable ''; } return name; } export function useComponent

(comp: NameLike

, def?: ComponentType

): UseComponentResult

{ const { useComponent } = useContext(ComponentContext); const [o, r] = useComponent

(comp); return [r.found ? o : def || o, r]; } export const ComponentProvider: FC = ({ components, children }) => { const parent = useContext(ComponentContext); const provideRef = useRef(components); const parentRef = useRef(parent); provideRef.current = components; parentRef.current = parent; const val = useMemo((): ComponentContextObject => { return { get parent() { return parentRef.current; }, get components() { return provideRef.current; }, useComponent: (comp) => { return resolveComponent(comp, val); }, }; }, []); return {children}; }; function resolveComponent

(comp: NameLike

, obj: ComponentContextObject): UseComponentResult

{ let cur: ComponentContextObject | undefined = obj; let Component = Fragment as ComponentType

; let found = false; const name = resolveName(comp); const isMatch = (provide: NameLike) => { if (provide === comp) { return true; } return name && resolveName(provide) === name; }; outer: while (cur) { for (let item of cur.components) { if (isMatch(item.provide)) { Component = createComponent(item); found = true; break outer; } } cur = cur.parent; } if (Component === Fragment || !found) { console.warn(`Component ${name || String(comp)} not found`); } return [Component, { found }]; } function createComponent({ Component, load, }: { Component?: ComponentType; load?: LoadableComponent; }): ComponentType { if (Component) { return Component; } if (load) { return lazy(async () => { try { const v = await load(); if ('default' in v) { return v as { default: ComponentType }; } if (!v) { throw new Error(`Component not found`); } return { default: v }; } catch (e) { console.error(`Failed to load component`, { load, Component }, e); return { default: Fragment }; } }); } return Fragment; }