UNPKG

@gaddario98/react-pages

Version:

A powerful, performance-optimized React component library for creating dynamic pages that work seamlessly across web (React DOM) and React Native with integrated form management, query handling, SEO metadata, lazy loading, and content rendering.

1,615 lines 122 kB
'use strict';var compilerRuntime=require('react/compiler-runtime'),utiles=require('@gaddario98/utiles'),React=require('react'),reactHookForm=require('react-hook-form'),reactQuery=require('@tanstack/react-query'),useDebounce=require('use-debounce'),jsxRuntime=require('react/jsx-runtime'),equal=require('fast-deep-equal'),reactForm=require('@gaddario98/react-form'),reactProviders=require('@gaddario98/react-providers'),reactQueries=require('@gaddario98/react-queries'),reactAuth=require('@gaddario98/react-auth'),reactI18next=require('react-i18next');function createQueryExtractor(allQuery, queryCacheRef, usedKeys) { return utiles.createExtractor(allQuery, queryCacheRef, usedKeys); } function createMutationExtractor(allMutation, mutationCacheRef, usedKeys) { return utiles.createExtractor(allMutation, mutationCacheRef, usedKeys); } function createFormValuesExtractor(formValues, formValuesCacheRef, usedKeys) { return utiles.createExtractor(formValues, formValuesCacheRef, usedKeys); } function useDataExtractor(t0) { const $ = compilerRuntime.c(15); const { allQuery, allMutation, formValues } = t0; let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t1 = new Map(); $[0] = t1; } else { t1 = $[0]; } const queryCacheRef = React.useRef(t1); let t2; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { t2 = new Map(); $[1] = t2; } else { t2 = $[1]; } const mutationCacheRef = React.useRef(t2); let t3; if ($[2] === Symbol.for("react.memo_cache_sentinel")) { t3 = new Map(); $[2] = t3; } else { t3 = $[2]; } const formValuesCacheRef = React.useRef(t3); let t4; if ($[3] !== allQuery) { t4 = usedKeys => createQueryExtractor(allQuery, queryCacheRef.current, usedKeys); $[3] = allQuery; $[4] = t4; } else { t4 = $[4]; } const extractQuery = t4; let t5; if ($[5] !== allMutation) { t5 = usedKeys_0 => createMutationExtractor(allMutation, mutationCacheRef.current, usedKeys_0); $[5] = allMutation; $[6] = t5; } else { t5 = $[6]; } const extractMutations = t5; let t6; if ($[7] !== formValues) { t6 = usedKeys_1 => createFormValuesExtractor(formValues, formValuesCacheRef.current, usedKeys_1); $[7] = formValues; $[8] = t6; } else { t6 = $[8]; } const extractFormValues = t6; let t7; if ($[9] === Symbol.for("react.memo_cache_sentinel")) { t7 = () => { queryCacheRef.current.clear(); mutationCacheRef.current.clear(); formValuesCacheRef.current.clear(); }; $[9] = t7; } else { t7 = $[9]; } const clearCache = t7; let t8; if ($[10] === Symbol.for("react.memo_cache_sentinel")) { t8 = { queryCacheRef, mutationCacheRef, formValuesCacheRef }; $[10] = t8; } else { t8 = $[10]; } let t9; if ($[11] !== extractFormValues || $[12] !== extractMutations || $[13] !== extractQuery) { t9 = { extractQuery, extractMutations, extractFormValues, clearCache, cacheRefs: t8 }; $[11] = extractFormValues; $[12] = extractMutations; $[13] = extractQuery; $[14] = t9; } else { t9 = $[14]; } return t9; }const useFormPage = ({ form }) => { var _a_0, _b_0, _c, _d; const queryClient = reactQuery.useQueryClient(); const [defaultValueQuery, setDefaultValueQuery] = React.useState(form === null || form === void 0 ? void 0 : form.defaultValues); React.useEffect(() => { if (!(form === null || form === void 0 ? void 0 : form.defaultValueQueryKey)) { setDefaultValueQuery(form === null || form === void 0 ? void 0 : form.defaultValues); return; } const initialData = queryClient.getQueryData(form.defaultValueQueryKey); if (initialData) { setDefaultValueQuery(initialData); } const observer = new reactQuery.QueryObserver(queryClient, { queryKey: form.defaultValueQueryKey, enabled: true, notifyOnChangeProps: ["data"], refetchOnWindowFocus: false }); const unsubscribe = observer.subscribe(result => { if (result.data !== undefined) { setDefaultValueQuery(result.data); } }); return () => unsubscribe(); }, [form === null || form === void 0 ? void 0 : form.defaultValueQueryKey, form === null || form === void 0 ? void 0 : form.defaultValues, queryClient]); const defaultValues = React.useMemo(() => { var _a, _b; return Object.assign(Object.assign({}, defaultValueQuery !== null && defaultValueQuery !== void 0 ? defaultValueQuery : {}), (_b = (_a = form === null || form === void 0 ? void 0 : form.defaultValueQueryMap) === null || _a === void 0 ? void 0 : _a.call(form, defaultValueQuery)) !== null && _b !== void 0 ? _b : {}); }, [defaultValueQuery, form]); const formControl = reactHookForm.useForm(Object.assign(Object.assign({ mode: "all" }, (_a_0 = form === null || form === void 0 ? void 0 : form.formSettings) !== null && _a_0 !== void 0 ? _a_0 : {}), { defaultValues, resetOptions: Object.assign({ keepDirtyValues: true, keepDefaultValues: false }, (_c = (_b_0 = form === null || form === void 0 ? void 0 : form.formSettings) === null || _b_0 === void 0 ? void 0 : _b_0.resetOptions) !== null && _c !== void 0 ? _c : {}) })); // Memoize formControl to avoid unnecessary re-renders const stableFormControl = React.useMemo(() => formControl, []); React.useEffect(() => { stableFormControl.reset(defaultValues, { keepDirtyValues: true, keepDefaultValues: false }); }, [defaultValues, stableFormControl]); // Watch form values (raw, updates on every keystroke) const rawFormValues = stableFormControl.watch(); // NEW IN 2.0: Debounce form values to reduce re-render cascades // Default 300ms delay - reduces re-renders by ~80% during rapid typing // Components using formValues will only re-render after user stops typing const [debouncedFormValues] = useDebounce.useDebounce(rawFormValues, (_d = form === null || form === void 0 ? void 0 : form.debounceDelay) !== null && _d !== void 0 ? _d : 300); const setValueAndTrigger = React.useCallback(async (name, value, options) => { stableFormControl.setValue(name, value, options); await stableFormControl.trigger(name); }, [stableFormControl]); return { formValues: debouncedFormValues, // Return debounced values rawFormValues, // Also expose raw values for immediate updates (e.g., input controlled components) formControl: stableFormControl, setValue: setValueAndTrigger }; };/** * Dependency Graph Module * Tracks component dependencies for selective re-rendering optimization * * @module utils/dependencyGraph */ /** * Manages component dependencies for efficient selective re-rendering * * @example * ```typescript * const graph = new DependencyGraph(); * * // Register a component and its dependencies * graph.addNode({ * componentId: 'item-1', * usedQueries: ['getUser', 'getPosts'], * usedFormValues: ['username'], * usedMutations: ['updateProfile'], * parentComponent: null, * childComponents: [], * }); * * // Find affected components when query updates * const affected = graph.getAffectedComponents(['getUser']); * // Returns: ['item-1'] * ``` */ class DependencyGraph { constructor() { this.nodes = new Map(); } /** * Register a component and its dependencies in the graph * @param node The dependency node to add */ addNode(node) { this.nodes.set(node.componentId, node); // Update parent's children list if parent exists if (node.parentComponent) { const parent = this.nodes.get(node.parentComponent); if (parent && !parent.childComponents.includes(node.componentId)) { parent.childComponents.push(node.componentId); } } } /** * Retrieve a dependency node by component ID * @param componentId The component ID to look up * @returns The dependency node or undefined if not found */ getNode(componentId) { return this.nodes.get(componentId); } /** * Find all components affected by changed data keys * Used to determine which components need re-rendering * * @param changedKeys Array of changed query keys or form field keys * @returns Array of component IDs that need re-rendering */ getAffectedComponents(changedKeys) { const affected = []; for (const [componentId, node] of this.nodes.entries()) { // Check if any changed key matches this component's dependencies const hasAffectedQuery = node.usedQueries.some(q => changedKeys.includes(q)); const hasAffectedFormValue = node.usedFormValues.some(f => changedKeys.includes(f)); const hasAffectedMutation = node.usedMutations.some(m => changedKeys.includes(m)); if (hasAffectedQuery || hasAffectedFormValue || hasAffectedMutation) { affected.push(componentId); } } return affected; } /** * Detect circular dependencies in the graph * Helps identify configuration errors that could cause infinite loops * * @returns Array of circular dependency paths for debugging */ detectCircularDependencies() { const cycles = []; const visited = new Set(); const stack = new Set(); const dfs = (nodeId, path) => { if (stack.has(nodeId)) { // Found a cycle const cycleStart = path.indexOf(nodeId); if (cycleStart >= 0) { cycles.push(path.slice(cycleStart).concat(nodeId)); } return; } if (visited.has(nodeId)) return; visited.add(nodeId); stack.add(nodeId); path.push(nodeId); const node = this.nodes.get(nodeId); if (node) { for (const childId of node.childComponents) { dfs(childId, [...path]); } } stack.delete(nodeId); }; // Check for cycles starting from each unvisited node for (const nodeId of this.nodes.keys()) { if (!visited.has(nodeId)) { dfs(nodeId, []); } } return cycles; } /** * Get all nodes in the graph * @returns Map of all dependency nodes */ getAllNodes() { return new Map(this.nodes); } /** * Clear all nodes from the graph */ clear() { this.nodes.clear(); } /** * Get the size of the graph (number of nodes) */ size() { return this.nodes.size; } /** * Check if a node exists in the graph */ hasNode(componentId) { return this.nodes.has(componentId); } /** * Remove a node from the graph * @param componentId The component ID to remove */ removeNode(componentId) { const node = this.nodes.get(componentId); if (!node) return; // Update parent's children list if (node.parentComponent) { const parent = this.nodes.get(node.parentComponent); if (parent) { parent.childComponents = parent.childComponents.filter(id => id !== componentId); } } // Update children's parent reference for (const childId of node.childComponents) { const child = this.nodes.get(childId); if (child) { child.parentComponent = null; } } this.nodes.delete(componentId); } /** * Get the depth of a component in the dependency tree * @param componentId The component ID * @returns The depth (0 for root) */ getDepth(componentId) { const node = this.nodes.get(componentId); if (!node || !node.parentComponent) return 0; let depth = 1; let parent = this.nodes.get(node.parentComponent); while (parent && parent.parentComponent) { depth++; parent = this.nodes.get(parent.parentComponent); } return depth; } /** * Get all leaf nodes (components with no children) */ getLeafNodes() { return Array.from(this.nodes.values()).filter(node => node.childComponents.length === 0); } /** * Get all root nodes (components with no parent) */ getRootNodes() { return Array.from(this.nodes.values()).filter(node => node.parentComponent === null); } }/** * Hook for managing a dependency graph within a page * Provides methods to register components and find affected components * * @example * ```typescript * function PageRenderer() { * const { * graph, * registerComponent, * getAffectedComponents, * detectCircularDependencies * } = useDependencyGraph(); * * // Register content items * useEffect(() => { * contentItems.forEach((item, index) => { * registerComponent({ * componentId: `item-${index}`, * usedQueries: item.usedQueries || [], * usedFormValues: item.usedFormValues || [], * usedMutations: [], * parentComponent: null, * childComponents: [], * }); * }); * * // Check for circular dependencies * const cycles = detectCircularDependencies(); * if (cycles.length > 0) { * console.warn('[DependencyGraph] Circular dependencies:', cycles); * } * }, [contentItems]); * * // When query updates * const handleQueryUpdate = useCallback((queryKey: string) => { * const affected = getAffectedComponents([queryKey]); * // Re-render only affected components * }, [getAffectedComponents]); * } * ``` */ function useDependencyGraph() { const graphRef = React.useRef(new DependencyGraph()); /** * Register a component and its dependencies */ const registerComponent = React.useCallback(node => { graphRef.current.addNode(node); }, []); /** * Get a specific node from the graph */ const getNode = React.useCallback(componentId => { return graphRef.current.getNode(componentId); }, []); /** * Find all components affected by changed keys */ const getAffectedComponents = React.useCallback(changedKeys => { return graphRef.current.getAffectedComponents(changedKeys); }, []); /** * Detect circular dependencies */ const detectCircularDependencies = React.useCallback(() => { return graphRef.current.detectCircularDependencies(); }, []); /** * Clear all nodes from the graph */ const clear = React.useCallback(() => { graphRef.current.clear(); }, []); /** * Remove a specific component from the graph */ const removeComponent = React.useCallback(componentId_0 => { graphRef.current.removeNode(componentId_0); }, []); /** * Check if a component is registered */ const hasComponent = React.useCallback(componentId_1 => { return graphRef.current.hasNode(componentId_1); }, []); /** * Get graph statistics */ const getStats = React.useCallback(() => { return { totalNodes: graphRef.current.size(), rootNodes: graphRef.current.getRootNodes().length, leafNodes: graphRef.current.getLeafNodes().length }; }, []); // eslint-disable-next-line react-hooks/refs return React.useMemo(() => ({ // eslint-disable-next-line react-hooks/refs graph: graphRef.current, registerComponent, getNode, getAffectedComponents, detectCircularDependencies, clear, removeComponent, hasComponent, getStats }), [registerComponent, getNode, getAffectedComponents, detectCircularDependencies, clear, removeComponent, hasComponent, getStats]); } /** * Hook variant that automatically registers components from a list * Simplifies common use case of registering content items */ function useAutoRegisterDependencies(items, t0) { const $ = compilerRuntime.c(3); const idPrefix = t0 === undefined ? "item" : t0; const { graph, registerComponent, getAffectedComponents, detectCircularDependencies, clear } = useDependencyGraph(); clear(); items.forEach((item, index) => { const componentId = item.key || `${idPrefix}-${index}`; registerComponent({ componentId, usedQueries: item.usedQueries || [], usedFormValues: item.usedFormValues || [], usedMutations: [], parentComponent: null, childComponents: [] }); }); const cycles = detectCircularDependencies(); if (cycles.length > 0 && typeof console !== "undefined") { console.warn("[useDependencyGraph] Circular dependencies detected:", cycles); } let t1; if ($[0] !== getAffectedComponents || $[1] !== graph) { t1 = { graph, getAffectedComponents }; $[0] = getAffectedComponents; $[1] = graph; $[2] = t1; } else { t1 = $[2]; } return t1; }const useGenerateContentRender = ({ pageId, ns = "", contents = [], allMutation, allQuery, formValues, isAllQueryMapped, formData, setValue, renderComponent }) => { var _a; const memorizedContentsRef = React.useRef([]); const contentsWithQueriesDeps = React.useMemo(() => { if (typeof contents === "function" && isAllQueryMapped) { return contents({ formValues, allMutation, allQuery, setValue }); } return Array.isArray(contents) ? contents : []; }, [contents, isAllQueryMapped, formValues, allMutation, allQuery, setValue]); const filteredContents = React.useMemo(() => { if (typeof contents === "function") { return contentsWithQueriesDeps.filter(el => !(el === null || el === void 0 ? void 0 : el.hidden)); } else { return contents.filter(el => !(el === null || el === void 0 ? void 0 : el.hidden)); } }, [contents, contentsWithQueriesDeps]); // Register content items with dependency graph for selective re-rendering const { getAffectedComponents } = useAutoRegisterDependencies(filteredContents, `${pageId}-content`); const { extractFormValues, extractMutations, extractQuery } = useDataExtractor({ allMutation, allQuery, formValues }); const memorizedContents = React.useMemo(() => { if (!isAllQueryMapped) return []; const getStableKey = (content, index) => { var _a; return (_a = content.key) !== null && _a !== void 0 ? _a : `content-${index}`; }; const dynamicElements = filteredContents.map((content, index) => { var _a, _b, _c, _d; const stableKey = getStableKey(content, index); return { element: renderComponent({ content, ns, formValues: extractFormValues((_a = content.usedFormValues) !== null && _a !== void 0 ? _a : []), pageId, allMutation: extractMutations((_b = content.usedQueries) !== null && _b !== void 0 ? _b : []), allQuery: extractQuery((_c = content.usedQueries) !== null && _c !== void 0 ? _c : []), setValue, key: stableKey }), index: (_d = content.index) !== null && _d !== void 0 ? _d : index, renderInFooter: !!content.renderInFooter, renderInHeader: !!content.renderInHeader, key: stableKey }; }); let formElementsWithKey = []; if (formData && Array.isArray(formData.elements)) { formElementsWithKey = formData.elements.map((el, idx) => { var _a, _b; return Object.assign(Object.assign({}, el), { key: (_a = el.key) !== null && _a !== void 0 ? _a : `form-element-${(_b = el.index) !== null && _b !== void 0 ? _b : idx}` }); }); } const next = [...dynamicElements, ...formElementsWithKey].sort((a, b) => a.index - b.index || String(a.key).localeCompare(String(b.key))); const prev = memorizedContentsRef.current; // eslint-disable-next-line react-hooks/refs const merged = next.map(el => { const found = prev.find(e => e.key === el.key); if (found) { return Object.assign(Object.assign(Object.assign({}, found), el), { element: el.element }); } return el; }); // eslint-disable-next-line react-hooks/refs memorizedContentsRef.current = merged; return next; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAllQueryMapped, filteredContents, ns, pageId, setValue, extractFormValues, extractMutations, extractQuery, formData]); return { components: memorizedContents, allContents: [...filteredContents, ...(!formData ? [] : (_a = formData === null || formData === void 0 ? void 0 : formData.formContents) !== null && _a !== void 0 ? _a : [])], // Expose dependency graph utilities for selective re-rendering getAffectedComponents }; };/** * Custom Metadata Configuration System * Replaces react-helmet-async with ~1KB native implementation * Platform-agnostic: Web, React Native, and SSR support */ // Store current metadata for SSR and getMetadata() let currentMetadata = {}; // Platform detection const isWeb = typeof document !== "undefined"; /** * Apply metadata configuration to the page * @param config - Metadata configuration object */ const setMetadata = config => { // Store for getMetadata() currentMetadata = Object.assign(Object.assign({}, currentMetadata), config); // SSR or React Native - just store, don't manipulate DOM if (!isWeb) { return; } // Set document title if (config.title && typeof config.title === 'string') { document.title = config.title; } // Helper to update or create meta tags const updateOrCreateMeta = (selector, content, attributes = {}) => { let element = document.querySelector(selector); if (!element) { element = document.createElement("meta"); Object.entries(attributes).forEach(([key, value]) => { element.setAttribute(key, value); }); document.head.appendChild(element); } element.setAttribute("content", content); }; // Set standard meta tags if (config.description && typeof config.description === 'string') { updateOrCreateMeta('meta[name="description"]', config.description, { name: "description" }); } if (config.keywords && Array.isArray(config.keywords)) { updateOrCreateMeta('meta[name="keywords"]', config.keywords.join(", "), { name: "keywords" }); } if (config.author && typeof config.author === 'string') { updateOrCreateMeta('meta[name="author"]', config.author, { name: "author" }); } if (config.viewport) { updateOrCreateMeta('meta[name="viewport"]', config.viewport, { name: "viewport" }); } if (config.themeColor) { updateOrCreateMeta('meta[name="theme-color"]', config.themeColor, { name: "theme-color" }); } // Set Open Graph meta tags (T059) if (config.openGraph) { const og = config.openGraph; if (og.title && typeof og.title === 'string') { updateOrCreateMeta('meta[property="og:title"]', og.title, { property: "og:title" }); } if (og.description && typeof og.description === 'string') { updateOrCreateMeta('meta[property="og:description"]', og.description, { property: "og:description" }); } if (og.image && typeof og.image === 'string') { updateOrCreateMeta('meta[property="og:image"]', og.image, { property: "og:image" }); } if (og.url && typeof og.url === 'string') { updateOrCreateMeta('meta[property="og:url"]', og.url, { property: "og:url" }); } if (og.type && typeof og.type === 'string') { updateOrCreateMeta('meta[property="og:type"]', og.type, { property: "og:type" }); } if (og.siteName && typeof og.siteName === 'string') { updateOrCreateMeta('meta[property="og:site_name"]', og.siteName, { property: "og:site_name" }); } if (og.locale && typeof og.locale === 'string') { updateOrCreateMeta('meta[property="og:locale"]', og.locale, { property: "og:locale" }); } } // Backward compatibility: legacy ogImage, ogTitle (ogDescription handled via description) const configWithLegacy = config; if (configWithLegacy.ogImage) { updateOrCreateMeta('meta[property="og:image"]', configWithLegacy.ogImage, { property: "og:image" }); } if (configWithLegacy.ogTitle) { updateOrCreateMeta('meta[property="og:title"]', configWithLegacy.ogTitle, { property: "og:title" }); } if (configWithLegacy.ogDescription) { updateOrCreateMeta('meta[property="og:description"]', configWithLegacy.ogDescription, { property: "og:description" }); } // Set robots directive (T062) if (config.robots) { const robotsValue = typeof config.robots === 'string' ? config.robots : [config.robots.noindex ? 'noindex' : 'index', config.robots.nofollow ? 'nofollow' : 'follow', config.robots.noarchive && 'noarchive', config.robots.nosnippet && 'nosnippet', config.robots.maxImagePreview && `max-image-preview:${config.robots.maxImagePreview}`, config.robots.maxSnippet && `max-snippet:${config.robots.maxSnippet}`].filter(Boolean).join(', '); updateOrCreateMeta('meta[name="robots"]', robotsValue, { name: "robots" }); } // Set structured data JSON-LD (T060) if (config.structuredData) { const schemaScriptId = 'react-pages-schema-org'; let scriptElement = document.querySelector(`script[id="${schemaScriptId}"]`); if (!scriptElement) { scriptElement = document.createElement('script'); scriptElement.type = 'application/ld+json'; scriptElement.id = schemaScriptId; document.head.appendChild(scriptElement); } scriptElement.textContent = JSON.stringify(config.structuredData); } // Set AI crawler hints (T061) if (config.aiHints) { const hints = config.aiHints; if (hints.contentClassification && typeof hints.contentClassification === 'string') { updateOrCreateMeta('meta[name="ai-content-classification"]', hints.contentClassification, { name: 'ai-content-classification' }); } if (hints.modelHints) { const modelHints = Array.isArray(hints.modelHints) ? hints.modelHints.join(', ') : typeof hints.modelHints === 'string' ? hints.modelHints : undefined; if (modelHints) { updateOrCreateMeta('meta[name="ai-model-hints"]', modelHints, { name: 'ai-model-hints' }); } } if (hints.contextualInfo && typeof hints.contextualInfo === 'string') { updateOrCreateMeta('meta[name="ai-context"]', hints.contextualInfo, { name: 'ai-context' }); } if (hints.excludeFromIndexing) { updateOrCreateMeta('meta[name="ai-exclude-from-indexing"]', 'true', { name: 'ai-exclude-from-indexing' }); } } // Set canonical link if (config.canonical) { let link = document.querySelector('link[rel="canonical"]'); if (!link) { link = document.createElement("link"); link.rel = "canonical"; document.head.appendChild(link); } link.href = config.canonical; } // Set language if (config.lang) { document.documentElement.lang = config.lang; } // Set custom meta tags (T063) if (config.customMeta && Array.isArray(config.customMeta)) { config.customMeta.forEach(tag => { const selector = tag.id ? `meta[id="${tag.id}"]` : tag.name ? `meta[name="${tag.name}"]` : tag.property ? `meta[property="${tag.property}"]` : `meta[http-equiv="${tag.httpEquiv}"]`; const attributes = tag.name ? { name: tag.name } : tag.property ? { property: tag.property } : tag.httpEquiv ? { "http-equiv": tag.httpEquiv } : {}; if (tag.id) attributes.id = tag.id; updateOrCreateMeta(selector, tag.content, attributes); }); } }; /** * Get current metadata configuration (T073) * Useful for SSR framework integration, testing, and debugging * * @example SSR with Next.js App Router * ```typescript * import { getMetadata } from '@gaddario98/react-pages'; * * export async function generateMetadata() { * const metadata = getMetadata(); * return { * title: metadata.title, * description: metadata.description, * openGraph: { * title: metadata.openGraph?.title, * description: metadata.openGraph?.description, * images: metadata.openGraph?.image ? [metadata.openGraph.image] : undefined, * url: metadata.openGraph?.url, * type: metadata.openGraph?.type as any, * locale: metadata.openGraph?.locale, * siteName: metadata.openGraph?.siteName, * }, * }; * } * ``` * * @example SSR with Remix * ```typescript * import { getMetadata } from '@gaddario98/react-pages'; * * export const meta: MetaFunction = () => { * const metadata = getMetadata(); * return [ * { title: metadata.title }, * { name: 'description', content: metadata.description }, * { property: 'og:title', content: metadata.openGraph?.title }, * { property: 'og:description', content: metadata.openGraph?.description }, * ]; * }; * ``` */ const getMetadata = () => { return Object.assign({}, currentMetadata); }; /** * Reset all metadata to defaults * Removes dynamically-added meta tags on web */ const resetMetadata = () => { currentMetadata = {}; if (!isWeb) { return; } // Remove dynamically-added meta tags (with data-react-pages attribute) const metaTags = document.querySelectorAll('meta[name], meta[property], meta[http-equiv], link[rel="canonical"]'); metaTags.forEach(tag => { // Only remove tags we created (not hardcoded in HTML) // We can't reliably detect this, so we'll just reset the stored config // and let setMetadata() handle re-applying defaults }); // Reset document title to empty (or leave as-is) // document.title = ''; };const DefaultContainer = ({ children }) => { return children; }; // Lazy initialization to avoid side effects at module load time // This ensures tree-shaking works correctly by deferring singleton creation let _pageConfig; /** * Get or initialize the page configuration singleton * Uses lazy initialization to avoid module-level side effects for better tree-shaking */ function initializePageConfig() { if (!_pageConfig) { _pageConfig = { HeaderContainer: DefaultContainer, FooterContainer: DefaultContainer, BodyContainer: DefaultContainer, authPageImage: "", authPageProps: { id: "auth-page" }, isLogged: val => !!(val === null || val === void 0 ? void 0 : val.id) && !!(val === null || val === void 0 ? void 0 : val.isLogged), ItemsContainer: ({ children }) => children, PageContainer: ({ children }) => children, meta: { title: "", description: "" }, // Metadata configuration defaultMetadata: {}, setMetadata, getMetadata, resetMetadata, // Lazy loading configuration lazyLoading: { enabled: true, preloadOnHover: false, preloadOnFocus: false, timeout: 30000, logMetrics: process.env.NODE_ENV === 'development' } }; } return _pageConfig; } // Getter for pageConfig - initializes on first access function getPageConfig() { return initializePageConfig(); } // Legacy export for backward compatibility const pageConfig = new Proxy({}, { get: (target, prop) => { return initializePageConfig()[prop]; }, set: (target, prop, value) => { initializePageConfig()[prop] = value; return true; } }); const setPageConfig = config => { const current = initializePageConfig(); Object.assign(current, config); };/** * Optimized shallow equality check for objects and functions * @param objA - First object to compare * @param objB - Second object to compare * @returns True if objects are shallow equal */ function shallowEqual(objA, objB) { if (objA === objB) return true; if (!objA || !objB) return false; if (typeof objA !== 'object' || typeof objB !== 'object') { return objA === objB; } if (typeof objA === 'function' && typeof objB === 'function') { return objA.name === objB.name && objA.toString() === objB.toString(); } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!keysB.includes(key)) return false; const valA = objA[key]; const valB = objB[key]; if (typeof valA === 'function' && typeof valB === 'function') { if (valA.name !== valB.name || valA.toString() !== valB.toString()) { return false; } continue; } if (valA !== valB) return false; } return true; } /** * Checks if a value is stable for React dependency arrays * @param value - Value to check for stability * @returns True if value is considered stable */ function isStableValue(value) { if (value === null || value === undefined) return true; if (typeof value !== 'object' && typeof value !== 'function') return true; if (typeof value === 'function') return value.toString().length < 1000; return false; } /** * Creates an optimized dependency array by filtering unstable values * @param deps - Array of dependencies to optimize * @returns Filtered array of stable dependencies */ function optimizeDeps(deps) { return deps.filter(dep => isStableValue(dep) || typeof dep === 'object'); } /** * Custom prop comparator for React.memo() to prevent unnecessary re-renders * Compares props shallowly and ignores function references if they have the same name * @param prevProps - Previous component props * @param nextProps - Next component props * @returns True if props are equal (component should NOT re-render) */ function memoPropsComparator(prevProps, nextProps) { return shallowEqual(prevProps, nextProps); } /** * Deep equality check for complex objects * Use sparingly - prefer shallow equality for performance * Uses fast-deep-equal library for optimized deep comparison with circular reference protection * @param objA - First object * @param objB - Second object * @returns True if objects are deeply equal */ function deepEqual(objA, objB) { return equal(objA, objB); } /** * Memoization cache for expensive computations * Simple LRU cache with configurable size */ class MemoizationCache { constructor(maxSize = 100) { this.cache = new Map(); this.maxSize = maxSize; } get(key) { const value = this.cache.get(key); if (value !== undefined) { // Move to end (most recently used) this.cache.delete(key); this.cache.set(key, value); } return value; } set(key, value) { // Delete oldest entry if cache is full if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey !== undefined) { this.cache.delete(firstKey); } } this.cache.set(key, value); } has(key) { return this.cache.has(key); } clear() { this.cache.clear(); } } /** * Creates a memoized function with custom cache key generator * @param fn - Function to memoize * @param cacheKeyFn - Optional function to generate cache key from arguments * @returns Memoized function */ function memoize(fn, cacheKeyFn) { const cache = new MemoizationCache(); return (...args) => { const cacheKey = cacheKeyFn ? cacheKeyFn(...args) : JSON.stringify(args); if (cache.has(cacheKey)) { return cache.get(cacheKey); } const result = fn(...args); cache.set(cacheKey, result); return result; }; }// Internal component implementation const RenderComponentImpl = t0 => { const $ = compilerRuntime.c(6); const { content, formValues, allMutation, allQuery, setValue } = t0; const { component } = content; let t1; bb0: { if (typeof component === "function") { let t2; if ($[0] !== allMutation || $[1] !== allQuery || $[2] !== component || $[3] !== formValues || $[4] !== setValue) { t2 = component({ allQuery, allMutation, formValues, setValue }); $[0] = allMutation; $[1] = allQuery; $[2] = component; $[3] = formValues; $[4] = setValue; $[5] = t2; } else { t2 = $[5]; } t1 = t2; break bb0; } t1 = component; } return t1; }; // Export with React.memo and fast-deep-equal comparator for optimal performance const RenderComponent = React.memo(RenderComponentImpl, (prevProps, nextProps) => { // Return true if props are equal (component should NOT re-render) return deepEqual(prevProps, nextProps); });const ContainerImpl = ({ content, ns, pageId, allMutation, allQuery, formValues, setValue }) => { const { components } = useGenerateContentRender({ allMutation, allQuery, formValues, pageId, isAllQueryMapped: true, formData: false, contents: content.items, ns, setValue, renderComponent: props => { if (props.content.type === "container") { return jsxRuntime.jsx(Container, { content: props.content, ns: props.ns, pageId: props.pageId, allMutation: props.allMutation, allQuery: props.allQuery, formValues: props.formValues, setValue: props.setValue }, props.key); } return jsxRuntime.jsx(RenderComponent, { content: props.content, ns: props.ns, formValues: props.formValues, pageId: props.pageId, allMutation: props.allMutation, allQuery: props.allQuery, setValue: props.setValue }, props.key); } }); const Layout = React.useMemo(() => { var _a; return (_a = content === null || content === void 0 ? void 0 : content.component) !== null && _a !== void 0 ? _a : pageConfig.ItemsContainer; }, [content === null || content === void 0 ? void 0 : content.component]); return jsxRuntime.jsx(Layout, { children: components === null || components === void 0 ? void 0 : components.map(el => el.element) }); }; // Export with React.memo and fast-deep-equal comparator for optimal performance const Container = React.memo(ContainerImpl, (prevProps, nextProps) => { // Return true if props are equal (component should NOT re-render) return deepEqual(prevProps, nextProps); });const useGenerateContent = t0 => { const $ = compilerRuntime.c(23); const { pageId, ns: t1, contents: t2, pageConfig } = t0; const ns = t1 === undefined ? "" : t1; let t3; if ($[0] !== t2) { t3 = t2 === undefined ? [] : t2; $[0] = t2; $[1] = t3; } else { t3 = $[1]; } const contents = t3; const { allMutation, allQuery, formData, formValues, isAllQueryMapped, setValue } = pageConfig; let t4; if ($[2] !== allMutation || $[3] !== allQuery || $[4] !== contents || $[5] !== formData || $[6] !== formValues || $[7] !== isAllQueryMapped || $[8] !== ns || $[9] !== pageId || $[10] !== setValue) { t4 = { allMutation, allQuery, formData, formValues, pageId, contents, isAllQueryMapped, setValue, ns, renderComponent: _temp$3 }; $[2] = allMutation; $[3] = allQuery; $[4] = contents; $[5] = formData; $[6] = formValues; $[7] = isAllQueryMapped; $[8] = ns; $[9] = pageId; $[10] = setValue; $[11] = t4; } else { t4 = $[11]; } const { allContents, components } = useGenerateContentRender(t4); let t5; if ($[12] !== components) { t5 = components.filter(_temp2$2).map(_temp3$1); $[12] = components; $[13] = t5; } else { t5 = $[13]; } const body = t5; let t6; if ($[14] !== components) { t6 = components.filter(_temp4$1).map(_temp5$1); $[14] = components; $[15] = t6; } else { t6 = $[15]; } const header = t6; let t7; if ($[16] !== components) { t7 = components.filter(_temp6).map(_temp7); $[16] = components; $[17] = t7; } else { t7 = $[17]; } const footer = t7; let t8; if ($[18] !== allContents || $[19] !== body || $[20] !== footer || $[21] !== header) { t8 = { header, body, footer, allContents }; $[18] = allContents; $[19] = body; $[20] = footer; $[21] = header; $[22] = t8; } else { t8 = $[22]; } return t8; }; function _temp$3(props) { if (props.content.type === "container") { return jsxRuntime.jsx(Container, { content: props.content, ns: props.ns, pageId: props.pageId, allMutation: props.allMutation, allQuery: props.allQuery, formValues: props.formValues, setValue: props.setValue }, props.key); } return jsxRuntime.jsx(RenderComponent, { content: props.content, ns: props.ns, formValues: props.formValues, pageId: props.pageId, allMutation: props.allMutation, allQuery: props.allQuery, setValue: props.setValue }, props.key); } function _temp2$2(el) { return !el.renderInFooter && !el.renderInHeader; } function _temp3$1(item) { return item.element; } function _temp4$1(el_0) { return el_0.renderInHeader; } function _temp5$1(item_0) { return item_0.element; } function _temp6(el_1) { return el_1.renderInFooter; } function _temp7(item_1) { return item_1.element; }/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ /** * useMemoizedProps Hook * Provides stable MappedProps memoization for preventing unnecessary re-renders * * @module hooks/useMemoizedProps */ /** * Hook that provides stable MappedProps object across renders * Prevents unnecessary re-renders by memoizing the props object * * @example * ```typescript * function PageComponent({ queries, formValues, setValue }: PageProps) { * const allQuery = useQueries(queries); * const allMutation = useMutations(queries); * * const mappedProps = useMemoizedProps({ * formValues, * setValue, * allQuery, * allMutation, * }); * * // mappedProps is stable - won't change unless dependencies actually change * return <ContentRenderer mappedProps={mappedProps} />; * } * ``` */ function useMemoizedProps(props, options = {}) { const { deepEqual: useDeepEqual = true } = options; // Memoize formValues with deep equality const stableFormValues = React.useMemo(() => { return props.formValues; }, [useDeepEqual ? JSON.stringify(props.formValues) : props.formValues]); // Memoize allQuery with deep equality check on data const stableAllQuery = React.useMemo(() => { return props.allQuery; }, [useDeepEqual && props.allQuery ? Object.entries(props.allQuery).map(([key, value]) => ({ key, data: value === null || value === void 0 ? void 0 : value.data, isLoading: value === null || value === void 0 ? void 0 : value.isLoading, error: value === null || value === void 0 ? void 0 : value.error })) : props.allQuery]); // Memoize allMutation (usually stable already) const stableAllMutation = React.useMemo(() => { return props.allMutation; }, [props.allMutation]); // setValue is already stable from react-hook-form, but wrap for consistency const stableSetValue = React.useMemo(() => { return props.setValue; }, [props.setValue]); // Combine into final stable MappedProps object const mappedProps = React.useMemo(() => ({ formValues: stableFormValues, setValue: stableSetValue, allQuery: stableAllQuery, allMutation: stableAllMutation }), [stableFormValues, stableSetValue, stableAllQuery, stableAllMutation]); return mappedProps; }/** * Specialized hook for managing queries and mutations * Handles query processing, loading states, and key mapping * Enhanced in 2.0: Uses useMemoizedProps for stable query/mutation references * @param queries - Array of query configurations * @param formValues - Current form values * @param setValue - Form setValue function * @returns Query management state and utilities with stable references */ function usePageQueries(t0) { const $ = compilerRuntime.c(38); const { queries: t1, formValues, setValue } = t0; let t2; if ($[0] !== t1) { t2 = t1 === undefined ? [] : t1; $[0] = t1; $[1] = t2; } else { t2 = $[1]; } const queries = t2; let t3; if ($[2] !== formValues || $[3] !== queries || $[4] !== setValue) { let t4; if ($[6] !== formValues || $[7] !== setValue) { t4 = q => { if (q.type === "mutation") { const mutationConfig = typeof q.mutationConfig === "function" ? q.mutationConfig({ formValues, setValue }) : q.mutationConfig; return Object.assign(Object.assign({}, q), { mutationConfig }); } if (q.type === "query") { const queryConfig = typeof q.queryConfig === "function" ? q.queryConfig({ formValues, setValue }) : q.queryConfig; return Object.assign(Object.assign({}, q), { queryConfig }); } return q; }; $[6] = formValues; $[7] = setValue; $[8] = t4; } else { t4 = $[8]; } t3 = queries.map(t4); $[2] = formValues; $[3] = queries; $[4] = setValue; $[5] = t3; } else { t3 = $[5]; } const processedQueries = t3; const { allMutation, allQuery } = reactQueries.useApi(processedQueries); let t4; if ($[9] !== allMutation || $[10] !== allQuery || $[11] !== formValues || $[12] !== setValue) { t4 = { allQuery, allMutation, formValues, setValue }; $[9] = allMutation; $[10] = allQuery; $[11] = formValues; $[12] = setValue; $[13] = t4; } else { t4 = $[13]; } const stableMappedProps = useMemoizedProps(t4); const stableAllQuery = stableMappedProps.allQuery; const stableAllMutation = stableMappedProps.allMutation; let t5; if ($[14] !== stableAllQuery) { t5 = Object.keys(stableAllQuery !== null && stableAllQuery !== void 0 ? stableAllQuery : {}); $[14] = stableAllQuery; $[15] = t5; } else { t5 = $[15]; } let t6; if ($[16] !== stableAllMutation || $[17] !== t5) { let t7; if ($[19] !== stableAllMutation) { t7 = Object.keys(stableAllMutation !== null && stableAllMutation !== void 0 ? stableAllMutation : {}); $[19] = stableAllMutation; $[20] = t7; } else { t7 = $[20]; } t6 = t5.concat(t7); $[16] = stableAllMutation; $[17] = t5; $[18] = t6; } else { t6 = $[18]; } const queriesKeys = t6; let t7; bb0: { if (!queries.length) { t7 = true; break bb0; } let t8; if ($[21] !== queries || $[22] !== queriesKeys) { let t9; if ($[24] !== queriesKeys) { t9 = el_0 => queriesKeys.includes(el_0); $[24] = queriesKeys; $[25] = t9; } else { t9 = $[25]; } t8 = queries.map(_temp$2).every(t9); $[21] = queries; $[22] = queriesKeys; $[23] = t8; } else { t8 = $[23]; } t7 = t8; } const isAllQueryMapped = t7; const isLoading = Object.values(stableAllQuery !== null && stableAllQuery !== void 0 ? stableAllQuery : {}).some(_temp2$1); let t8; if ($[26] !== processedQueries) { t8 = processedQueries.filter(_temp3).map(_temp4).filter(Boolean); $[26] = processedQueries; $[27] = t8; } else { t8 = $[27]; } const queryKeys = t8; let t9; if ($[28] !== queries) { t9 = queries.some(_temp5); $[28] = queries; $[29] = t9; } else { t9 = $[29]; } const hasQueries = t9; let t10; if ($[30] !== hasQueries || $[31] !== isAllQueryMapped || $[32] !== isLoading || $[33] !== queriesKeys || $[34] !== queryKeys || $[35] !== stableAllMutation || $[36] !== stableAllQuery) { t10 = { allMutation: stableAllMutation, allQuery: stableAllQuery, isAllQueryMapped, isLoading, queryKeys, hasQueries, queriesKeys }; $[30] = hasQueries; $[31] = isAllQueryMapped; $[32] = isLoading; $[33] = queriesKeys; $[34] = queryKeys; $[35] = stableAllMutation; $[36] = stableAllQuery; $[37] = t10; } else { t10 = $[37]; } return t10; } function _temp5(q_0) { return q_0.type === "query"; } function _temp4(el_3) { const queryConfig_0 = el_3.queryConfig; return queryConfig_0 === null || queryConfig_0 === void 0 ? void 0 : queryConfig_0.queryKey; } function _temp3(el_2) { return (el_2 === null || el_2 === void 0 ? void 0 : el_2.type) === "query"; } function _temp2$1(el_1) { return typeof el_1 !== "boolean" && (el_1 === null || el_1 === void 0 ? void 0 : el_1.isLoadingMapped) === true && !el_1.data; } function _temp$2(el) { return el.key; }/* eslint-disable react-hooks/refs */ /** * Specialized hook for managing view settings * Optimized to prevent unnecessary re-renders * @param viewSettings - View settings configuration (static or function) * @param allQuery - All query results * @param allMutation - All mutation handlers * @param formValues - Current form values * @param setValue - Form setValue function * @returns Processed view settings */ function useViewSettings({ viewSettings = {}, allQuery, allMutation, formValues, setValue }) { const prevViewSettingsRef = React.useRef(undefined); const mappedViewSettings = React.useMemo(() => { let next; if (typeof viewSettings === 'function') { next = viewSettings({ allQuery, allMutation, formValues, setValue }); } else { next = viewSettings; } if (prevViewSettingsRef.current && shallowEqual(prevViewSettingsRef.current, next)) { return prevViewSettingsRef.current; } prevViewSettingsRef.current = next; return next; }, [viewSettings, allQuery, allMutation, formValues, setValue]); return mappedViewSettings; }/** * Configuration Merge Utility (T084) * Provides deep merging of configuration objects with proper precedence * Handles arrays, objects, and primitives correctly * * @module utils/merge */ /** * Deep merge of multiple objects with precedence from le