import type { ReactElement, ReactNode, RefObject } from "react"; interface IReactElementLike { $$typeof?: symbol; props?: Record; } function isReactChildren(obj: unknown): obj is ReactElement | ReactNode { if (!obj || typeof obj !== "object") { return false; } // Проверка на React элемент через Symbol const reactElementSymbol = Symbol.for("react.element"); const reactFragmentSymbol = Symbol.for("react.fragment"); const reactObj = obj as IReactElementLike; if (reactObj.$$typeof === reactElementSymbol || reactObj.$$typeof === reactFragmentSymbol) { return true; } // Проверка на массив React элементов if (Array.isArray(reactObj)) { return reactObj.some((item) => isReactChildren(item)); } // Проверка на объект с React-подобными свойствами if (reactObj.props && typeof reactObj.props === "object") { return true; } return false; } function isReactRef(obj: unknown): obj is RefObject { if (!obj || typeof obj !== "object") { return false; } // Проверяем специфическую структуру ref объекта const isRefLike = ( Object.keys(obj).length === 1 && "current" in obj && Object.getOwnPropertyDescriptor(obj, "current")?.writable === true ); return isRefLike; } type TCleanedValue = | string | number | boolean | null | TCleanedValue[] | { [key: string]: TCleanedValue; }; const BLOCKED_MARKERS = [ "[Circular]", "[React Children]", "[React Ref]", ] as const; const hasBlockedMarker = (value: unknown): boolean => { if (typeof value !== "string") { return false; } return BLOCKED_MARKERS.some((marker) => value.includes(marker)); }; /** * Получает JSON-конфигурацию из объекта, очищая его от неподдерживаемых типов данных * @internal * @returns {String} Возвращает сериализованную JSON-строку с очищенной конфигурацией для статической разметки. * @example * const cfg = _getComponentCfgForStaticMarkup({ * id: "card", * children: "[React Children]", * nested: { value: 10 }, * }); */ export const _getComponentCfgForStaticMarkup = (obj: unknown = {}, isClear: boolean = true): string => { try { // Используем WeakSet для отслеживания посещенных объектов const seen = new WeakSet(); const clean = (currentObj: unknown): TCleanedValue | undefined => { // Обрабатываем null и undefined if (currentObj === null || currentObj === undefined) { return null; } // Обрабатываем примитивные типы const type = typeof currentObj; if (type !== "object" && type !== "function") { if (isClear && hasBlockedMarker(currentObj)) { return undefined; } return currentObj as string | number | boolean; } // Проверяем на циклические ссылки if (seen.has(currentObj)) { return isClear ? undefined : "[Circular]"; } // Проверяем на React-элемента if (isReactChildren(currentObj)) { return isClear ? undefined : "[React Children]"; } // Проверяем на React-ref if (isReactRef(currentObj)) { return isClear ? undefined : "[React Ref]"; } // Добавляем объект в множество посещенных seen.add(currentObj); try { // Пропускаем функции if (typeof currentObj === "function") { return undefined; } // Обрабатываем специальные объекты if (currentObj instanceof Date) { return currentObj.toISOString(); } if (currentObj instanceof RegExp) { return currentObj.toString(); } if (currentObj instanceof Map) { const result: Record = {}; for (const [ key, value ] of currentObj.entries()) { // Для ключей Map используем отдельную очистку без добавления в seen const cleanedKey = typeof key === "object" && key !== null ? JSON.stringify(key) : String(key); if (isClear && hasBlockedMarker(cleanedKey)) { continue; } const cleanedValue = clean(value); if (cleanedValue !== undefined && !(isClear && hasBlockedMarker(cleanedValue))) { result[cleanedKey] = cleanedValue; } } return result; } if (currentObj instanceof Set) { return Array.from(currentObj).map((item) => clean(item)) .filter((item) => item !== undefined && !(isClear && hasBlockedMarker(item))) as TCleanedValue[]; } // Обрабатываем массивы if (Array.isArray(currentObj)) { return currentObj.map((item) => clean(item)) .filter((item) => item !== undefined && !(isClear && hasBlockedMarker(item))) as TCleanedValue[]; } // Обрабатываем обычные объекты const result: Record = {}; for (const [ key, value ] of Object.entries(currentObj)) { // Пропускаем символьные ключи if (typeof key === "symbol") { continue; } if (isClear && hasBlockedMarker(key)) { continue; } const cleanedValue = clean(value); if (cleanedValue !== undefined && !(isClear && hasBlockedMarker(cleanedValue))) { result[key] = cleanedValue; } } return Object.keys(result).length === 0 ? undefined : result; } finally { seen.delete(currentObj); } }; const result = clean(obj); return JSON.stringify(result !== undefined ? (result as Record) : {}); } catch (e) { console.error(`[getComponentCfgForStaticMarkup] Can't parse config for serialization`, e, obj); return ""; } };