/** * Component discovery utilities. * * Extracted from the docs app component-registry so that any consumer * (govern presets, CLI tools, third-party integrations) can discover * components from a module's exports without duplicating the logic. */ /** * Check if a value is likely a React component. * Functions or objects with $$typeof (forwardRef, memo) qualify. */ export function isReactComponent(value: unknown): boolean { if (!value) return false; // Function component (most common case) if (typeof value === 'function') { return true; } // ForwardRef, memo, etc. have $$typeof if (typeof value === 'object') { const obj = value as Record; if (obj.$$typeof) return true; } return false; } export interface DiscoverComponentsOptions { /** Set of export names to skip */ exclude?: Set; } /** * Discover all components and sub-components from a module's exports. * Returns a sorted array of component names including compound names (e.g. 'Card.Header'). * * @param moduleExports - The exports object from a component library (e.g. `import * as UI from '@fragments-sdk/ui'`) * @param options - Optional configuration */ export function discoverComponents( moduleExports: Record, options?: DiscoverComponentsOptions, ): string[] { const exclude = options?.exclude; const names: string[] = []; for (const [exportName, exportValue] of Object.entries(moduleExports)) { // Skip excluded exports if (exclude?.has(exportName)) continue; // Skip non-components (types, constants, etc.) if (!isReactComponent(exportValue)) continue; // Skip lowercase exports (likely utilities) if (exportName[0] !== exportName[0].toUpperCase()) continue; // Add the main component names.push(exportName); // Check for sub-components by looking at ALL properties of the component const componentObj = exportValue as Record; for (const key of Object.keys(componentObj)) { // Skip internal properties and non-component properties if (key.startsWith('_') || key.startsWith('$')) continue; if (key === 'displayName' || key === 'propTypes' || key === 'defaultProps') continue; // Skip 'render' which is a forwardRef internal, and 'Tabbed' which is a special variant if (key === 'render' || key === 'Tabbed') continue; const subComponent = componentObj[key]; if (isReactComponent(subComponent)) { names.push(`${exportName}.${key}`); } } } return names.sort(); }