import type { JSX, JSXElementConstructor, PropsWithChildren, ReactNode } from 'react'; import React, { Children, isValidElement } from 'react'; import type { ExplicitAny } from '@shoptet/utils'; import { isTruthyReactNode } from './isTruthyNode'; /** * Function that restricts the components that can be rendered * as children. * * @param children - The children to parse. * @returns The parsed children. */ export type OnlyAllowedComponents = (children: ReactNode) => ReactNode; type AnyFunction = (...args: ExplicitAny[]) => ExplicitAny; function getDisplayName(value: AnyFunction | object): string | undefined { if ('displayName' in value && typeof value.displayName === 'string' && value.displayName.trim() !== '') { return value.displayName; } else { return undefined; } } function getName(value: AnyFunction | object): string | undefined { if ('name' in value && typeof value.name === 'string' && value.name.trim() !== '') { return value.name; } else { return undefined; } } function getRenderComponentName(value: AnyFunction | object): string | undefined { if ('render' in value && typeof value.render === 'function') { return getName(value.render) ?? getDisplayName(value.render); } else { return undefined; } } function getComponentName(Component: unknown): string { if (typeof Component === 'string') { return Component; } else if (Component && (typeof Component === 'function' || typeof Component === 'object')) { return getDisplayName(Component) ?? getName(Component) ?? getRenderComponentName(Component) ?? '(Anonymous)'; } else { throw new TypeError('Invalid component type'); } } /** * Function that restricts the components that can be rendered * as children. * * @param children - The children to parse. * @param allowedComponents - The Set of components that are allowed. * @param SafeWrapper - Optional component to wrap the children in. * @returns The parsed children. */ export function onlyAllowedComponents( children: ReactNode, allowedComponents: Set, SafeWrapper?: JSXElementConstructor ): ReactNode { if (!isTruthyReactNode(children)) { return children; } if (process.env.NODE_ENV === 'production') { if (!isValidElement(children) && SafeWrapper) { return {children}; } else { return children; } } else { if (isValidElement(children)) { Children.only(children); if (children.type === React.Fragment) { Children.forEach(children.props.children, child => onlyAllowedComponents(child, allowedComponents, SafeWrapper) ); return children; } if (allowedComponents.has(children.type)) { return children; } } else if (SafeWrapper) { return {children}; } console.error('Invalid child component:', children); throw new Error( 'Invalid child component, see error above. Allowed components: ' + [...allowedComponents].map(component => getComponentName(component)).join(', '), { cause: children } ); } } /** * Factory function that creates a function that restricts the components * @param allowedComponents - The Set of components that are allowed. * @param SafeWrapper - Optional component to wrap the children in. * @returns The function that restricts the components. */ export function onlyAllowedComponentsFactory( allowedComponents: Set, SafeWrapper?: JSXElementConstructor ): OnlyAllowedComponents { return function parseAllowedComponents(children: ReactNode): ReactNode { if (isTruthyReactNode(children)) { return <>{Children.map(children, child => onlyAllowedComponents(child, allowedComponents, SafeWrapper))}; } else { return null; } }; }