import { cloneElement, type FC, isValidElement, type ReactElement, type ReactNode } from 'react'; import innerText from 'react-innertext'; import deepmergeFactory from '@fastify/deepmerge'; import is from 'is-lite'; import type { AnyObject, NarrowPlainObject, PlainObject, Simplify } from '~/types'; type RemoveType = { [Key in keyof TObject as TObject[Key] extends TExclude ? never : Key]: TObject[Key]; }; interface GetReactNodeTextOptions { defaultValue?: any; step?: number; steps?: number; } /** * Remove properties with undefined value from an object */ export function cleanUpObject(input: T) { const output: Record = {}; for (const key in input) { if (input[key] !== undefined) { output[key] = input[key]; } } return output as RemoveType; } export function deepMerge(...objects: object[]): T { return deepmergeFactory({ all: true, isMergeableObject: (value): value is object => !(!is.plainObject(value) || isValidElement(value)), })(...objects) as T; } /** * Get Object type */ export function getObjectType(value: unknown): string { return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); } export function getReactNodeText(input: ReactNode, options: GetReactNodeTextOptions = {}): string { const { defaultValue, step, steps } = options; let text = innerText(input); if (!text) { if ( isValidElement(input) && !Object.values(input.props as Record).length && getObjectType(input.type) === 'function' ) { try { const component = (input.type as FC)({}) as ReactNode; text = getReactNodeText(component, options); } catch { text = innerText(defaultValue); } } else { text = innerText(defaultValue); } } else if ((text.includes('{current}') || text.includes('{total}')) && step && steps) { text = text.replace('{current}', step.toString()).replace('{total}', steps.toString()); } return text; } /** * Log method calls if debug is enabled */ export function log(debug: boolean, scope: string, title: string, ...data: unknown[]): void { if (!debug) { return; } const now = new Date(); const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}`; // eslint-disable-next-line no-console console.log( `${scope} %c${title}%c ${time}`, 'font-weight: bold', 'color: gray; font-weight: normal', ...data, ); } /** * Merges the defaultProps with literal values with the incoming props, removing undefined values from it that would override the defaultProps. * The result is a type-safe object with the defaultProps as required properties. */ export function mergeProps, TProps extends PlainObject>( defaultProps: TDefaultProps, props: TProps, ) { const cleanProps = cleanUpObject(props); return { ...defaultProps, ...cleanProps } as unknown as Simplify< TProps & Required> >; } /** * A function that does nothing. */ export function noop() { return undefined; } /** * Type-safe Object.keys() */ export function objectKeys(input: T) { return Object.keys(input) as Array; } /** * Remove properties from an object */ export function omit, K extends keyof T>( input: NarrowPlainObject, ...filter: K[] ) { if (!is.plainObject(input)) { throw new TypeError('Expected an object'); } const output: any = {}; for (const key in input) { /* istanbul ignore else */ if ({}.hasOwnProperty.call(input, key) && !filter.includes(key as unknown as K)) { output[key] = input[key]; } } return output as Omit; } /** * Select properties from an object */ export function pick, K extends keyof T>( input: NarrowPlainObject, ...filter: K[] ) { if (!is.plainObject(input)) { throw new TypeError('Expected an object'); } if (!filter.length) { return input; } const output: any = {}; for (const key in input) { /* istanbul ignore else */ if ({}.hasOwnProperty.call(input, key) && filter.includes(key as unknown as K)) { output[key] = input[key]; } } return output as Pick; } export function replaceLocaleContent(input: ReactNode, step: number, steps: number): ReactNode { const replacer = (text: string) => text.replace('{current}', String(step)).replace('{total}', String(steps)); if (getObjectType(input) === 'string') { return replacer(input as string); } if (!isValidElement(input)) { return input; } const { children } = input.props as { children?: ReactNode }; if (is.string(children) && children.includes('{current}')) { return cloneElement(input as ReactElement<{ children?: ReactNode }>, { children: replacer(children), }); } if (Array.isArray(children)) { return cloneElement(input as ReactElement<{ children?: ReactNode }>, { children: children.map((child: ReactNode) => { if (typeof child === 'string') { return replacer(child); } return replaceLocaleContent(child, step, steps); }), }); } if (is.function(input.type) && !Object.values(input.props as Record).length) { try { const component = (input.type as FC)({}) as ReactNode; return replaceLocaleContent(component, step, steps); } catch { return input; } } return input; } /** * Sort object keys */ export function sortObjectKeys(input: T) { return objectKeys(input) .sort() .reduce((acc, key) => { acc[key] = input[key]; return acc; }, {} as T); }