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.

852 lines (844 loc) 25.5 kB
'use strict';var equal=require('fast-deep-equal'),compilerRuntime=require('react/compiler-runtime'),jsxRuntime=require('react/jsx-runtime'),React=require('react');/** * 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; }; }/** * 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 left to right * Later objects override earlier ones, with special handling for nested objects * * @param objects - Objects to merge (left to right precedence) * @param options - Merge options * @returns Merged object * * @example * ```typescript * const defaults = { theme: 'light', nested: { color: 'black' } }; * const overrides = { theme: 'dark', nested: { fontSize: 14 } }; * const result = deepMerge(defaults, overrides); * // Result: { theme: 'dark', nested: { color: 'black', fontSize: 14 } } * ``` */ function deepMerge(...objects) { return deepMergeWithOptions({}, objects.filter(obj => obj != null), { maxDepth: 10 }); } /** * Deep merge with custom options * @param target - Target object to merge into * @param sources - Source objects to merge * @param options - Merge options * @returns Merged object */ function deepMergeWithOptions(target, sources, options = {}) { const { concatArrays = false, overwriteWithEmpty = false, maxDepth = 10, skipKeys = new Set() } = options; function merge(target, source, depth = 0) { // Prevent infinite recursion if (depth > maxDepth) { console.warn('[deepMerge] Maximum recursion depth exceeded'); return source; } // Handle null/undefined if (source == null) { return overwriteWithEmpty ? source : target; } if (target == null) { return source; } // Handle primitives and functions if (typeof source !== 'object' || typeof target !== 'object') { return source; } // Handle arrays if (Array.isArray(source)) { if (!Array.isArray(target)) { return source; } if (concatArrays) { return [...target, ...source]; } return source; } // Handle non-array objects if (Array.isArray(target)) { return source; // Source object replaces target array } // Deep merge objects const result = Object.assign({}, target); for (const key in source) { if (!source.hasOwnProperty(key) || skipKeys.has(key)) { continue; } const sourceValue = source[key]; const targetValue = result[key]; if (sourceValue == null) { if (overwriteWithEmpty) { result[key] = sourceValue; } continue; } // Recursively merge nested objects if (typeof sourceValue === 'object' && typeof targetValue === 'object' && !Array.isArray(sourceValue) && !Array.isArray(targetValue)) { result[key] = merge(targetValue, sourceValue, depth + 1); } else { result[key] = sourceValue; } } return result; } return sources.reduce((acc, source) => merge(acc, source), target); } /** * Shallow merge of objects (only top-level keys) * @param objects - Objects to merge * @returns Merged object */ function shallowMerge(...objects) { const filtered = objects.filter(obj => obj != null); return Object.assign({}, ...filtered); } /** * Merge arrays of objects by a key (useful for config arrays) * @param baseArray - Base array * @param overrideArray - Array to merge in * @param keyName - Key to match on * @returns Merged array * * @example * ```typescript * const base = [{ id: 'a', name: 'Alice' }, { id: 'b', name: 'Bob' }]; * const override = [{ id: 'a', name: 'Alicia' }, { id: 'c', name: 'Charlie' }]; * const result = mergeArraysByKey(base, override, 'id'); * // Result: [{ id: 'a', name: 'Alicia' }, { id: 'b', name: 'Bob' }, { id: 'c', name: 'Charlie' }] * ``` */ function mergeArraysByKey(baseArray, overrideArray, keyName) { const baseMap = new Map(); const result = []; // Add base array items to map baseArray.forEach(item => { const key = item[keyName]; baseMap.set(key, item); }); // Track which keys we've seen const seenKeys = new Set(); // Merge overrides overrideArray.forEach(item => { const key = item[keyName]; seenKeys.add(key); if (baseMap.has(key)) { // Deep merge if key exists const merged = deepMerge(baseMap.get(key), item); baseMap.set(key, merged); } else { // Add new item baseMap.set(key, item); } }); // Build result maintaining base order, then add new items baseArray.forEach(item => { const key = item[keyName]; result.push(baseMap.get(key)); }); // Add new items from override overrideArray.forEach(item => { const key = item[keyName]; if (!baseArray.some(baseItem => baseItem[keyName] === key)) { result.push(item); } }); return result; } /** * Check if two objects are deeply equal * Useful for detecting if a merge actually changed anything * Note: Use the fast-deep-equal version from optimization.ts instead * @deprecated - Use deepEqual from utils/optimization.ts instead * @param obj1 - First object * @param obj2 - Second object * @returns True if objects are deeply equal */ function deepEqualFallback(obj1, obj2) { if (obj1 === obj2) { return true; } if (obj1 == null || obj2 == null) { return obj1 === obj2; } if (typeof obj1 !== 'object' || typeof obj2 !== 'object') { return false; } const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { if (!keys2.includes(key)) { return false; } if (!deepEqualFallback(obj1[key], obj2[key])) { return false; } } return true; } /** * Stable Cache for memoizing objects across renders * Prevents unnecessary object creation in hooks * * @example * ```typescript * const cache = useRef(new StableCache<FormConfig>()); * const formConfig = cache.current.getOrSet('key', () => createConfig()); * ``` */ class StableCache { constructor() { this.cache = new Map(); } /** * Get value from cache, or set it if not present * @param key - Cache key * @param factory - Function to create value if not cached * @returns Cached or newly created value */ getOrSet(key, factory) { if (this.cache.has(key)) { return this.cache.get(key); } const value = factory(); this.cache.set(key, value); return value; } /** * Get value from cache * @param key - Cache key * @returns Cached value or undefined */ get(key) { return this.cache.get(key); } /** * Set value in cache * @param key - Cache key * @param value - Value to cache */ set(key, value) { this.cache.set(key, value); } /** * Check if key exists in cache * @param key - Cache key * @returns True if key exists */ has(key) { return this.cache.has(key); } /** * Clear all cached values */ clear() { this.cache.clear(); } /** * Get cache size * @returns Number of cached items */ size() { return this.cache.size; } }/****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; };/** * Enhanced lazy wrapper with optional preloading capability * * Wraps React.lazy() with a Suspense boundary and optional preload support. * Helps reduce initial bundle size by code-splitting components. * * @example * ```tsx * // Basic lazy loading * const HeavyComponent = lazyWithPreload(() => import('./Heavy')); * * function App() { * return ( * <lazyWithPreload.Suspense fallback={<Loader />}> * <HeavyComponent /> * </lazyWithPreload.Suspense> * ); * } * ``` * * @example * ```tsx * // With preloading on hover * const LazyModal = lazyWithPreload( * () => import('./Modal'), * { preloadOnHover: true } * ); * * function Button() { * return ( * <button onMouseEnter={() => LazyModal.preload?.()}> * Open Modal * </button> * ); * } * ``` */ function lazyWithPreload(importFunc, config) { // Create lazy component const LazyComponent = React.lazy(importFunc); // Store preload function for optional use let preloadPromise = null; /** * Preload the component by initiating the import */ const preload = () => { if (!preloadPromise) { preloadPromise = importFunc(); } return preloadPromise; }; /** * Wrapper component that handles preloading on hover */ const LazyWrapper = props => { const { onMouseEnter, onMouseLeave } = props, restProps = __rest(props, ["onMouseEnter", "onMouseLeave"]); const handleMouseEnter = () => { if (config === null || config === void 0 ? void 0 : config.preloadOnHover) { preload(); } onMouseEnter === null || onMouseEnter === void 0 ? void 0 : onMouseEnter(); }; return jsxRuntime.jsx("div", { onMouseEnter: handleMouseEnter, onMouseLeave: onMouseLeave, style: { display: 'contents' }, children: jsxRuntime.jsx(LazyComponent, Object.assign({}, restProps)) }); }; // Attach preload function to component for external access LazyWrapper.preload = preload; // Attach Suspense for convenience LazyWrapper.Suspense = React.Suspense; // Default fallback const defaultFallback = (config === null || config === void 0 ? void 0 : config.suspenseFallback) || jsxRuntime.jsx("div", { children: "Loading..." }); /** * Wrapped component with integrated Suspense boundary */ const WithSuspense = props => jsxRuntime.jsx(React.Suspense, { fallback: defaultFallback, children: jsxRuntime.jsx(LazyWrapper, Object.assign({}, props)) }); WithSuspense.preload = preload; WithSuspense.Suspense = React.Suspense; return WithSuspense; } /** * Create a batch of lazy components with shared preloading strategy * * Useful for code-splitting a route or feature module * * @example * ```tsx * const lazyPages = lazyBatch({ * UserList: () => import('./pages/UserList'), * UserDetail: () => import('./pages/UserDetail'), * UserForm: () => import('./pages/UserForm'), * }, { * preloadOnHover: true, * suspenseFallback: <PageLoader /> * }); * * function App() { * return ( * <Routes> * <Route path="/users" element={<lazyPages.UserList />} /> * <Route path="/users/:id" element={<lazyPages.UserDetail />} /> * </Routes> * ); * } * ``` */ function lazyBatch(modules, config) { return Object.entries(modules).reduce((acc, [key, importFunc]) => { acc[key] = lazyWithPreload(importFunc, config); return acc; }, {}); } /** * Preload multiple components in parallel * * Useful for preloading components before user interaction * * @example * ```tsx * // Preload critical components on app mount * useEffect(() => { * preloadComponents([UserComponent.preload, SettingsComponent.preload]); * }, []); * ``` */ function preloadComponents(preloaders) { return Promise.all(preloaders.map(preload => preload())); } /** * Higher-order component to wrap lazy-loaded components with error boundary * * @example * ```tsx * const SafeLazyComponent = withLazyErrorBoundary(LazyComponent, { * fallback: <ErrorMessage /> * }); * ``` */ function withLazyErrorBoundary(Component, config) { const Wrapper = props => { const $ = compilerRuntime.c(3); const [error, setError] = React.useState(null); if (error) { let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = (config === null || config === void 0 ? void 0 : config.fallback) || jsxRuntime.jsx("div", { children: "Error loading component" }); $[0] = t0; } else { t0 = $[0]; } return t0; } let t0; if ($[1] !== props) { t0 = jsxRuntime.jsx(ErrorBoundary, { onError: setError, children: jsxRuntime.jsx(Component, Object.assign({}, props)) }); $[1] = props; $[2] = t0; } else { t0 = $[2]; } return t0; }; return Wrapper; } /** * Simple error boundary implementation */ class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error) { this.props.onError(error); } render() { if (this.state.hasError) { return null; } return this.props.children; } } /** * T095: Hook to preload a lazy component on demand * Useful for imperative preloading scenarios * * @example * ```tsx * function MyComponent() { * const preload = usePreloadLazy(HeavyComponent); * * return ( * <button onMouseEnter={preload}> * Hover to preload * </button> * ); * } * ``` */ function usePreloadLazy(component) { const $ = compilerRuntime.c(2); let t0; if ($[0] !== component) { t0 = () => { if (component.preload) { component.preload(); } }; $[0] = component; $[1] = t0; } else { t0 = $[1]; } return t0; } /** * T095: Hook to preload multiple lazy components * Useful for preloading a set of components before user navigation * * @example * ```tsx * function Navigation() { * usePreloadLazyBatch([UserList, UserDetail, UserForm]); * * return <nav>...</nav>; * } * ``` */ function usePreloadLazyBatch(components) { const $ = compilerRuntime.c(2); let t0; if ($[0] !== components) { t0 = () => { components.forEach(_temp); }; $[0] = components; $[1] = t0; } else { t0 = $[1]; } return t0; } /** * T095: Hook to preload lazy components on viewport intersection * Useful for preloading components that are likely to be scrolled into view * * @example * ```tsx * function LazySection() { * const ref = usePreloadOnViewport(ExpensiveComponent); * * return <div ref={ref}>Content goes here</div>; * } * ``` */ function _temp(component) { if (component.preload) { component.preload(); } } function usePreloadOnViewport(component) { const ref = React.useRef(null); React.useEffect(() => { if (!ref.current || !component.preload) return; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { var _a; if (entry.isIntersecting) { (_a = component.preload) === null || _a === void 0 ? void 0 : _a.call(component); observer.unobserve(entry.target); } }); }, { threshold: 0.1 }); observer.observe(ref.current); return () => observer.disconnect(); }, [component]); return ref; }/** * Deprecation warnings for v1.x → v2.0 migration * This file manages deprecation notices and warnings for removed/changed v1.x APIs * * @module utils/deprecations */ /** * Set of deprecated features that have been warned about * Prevents duplicate warnings in development mode */ const warnedFeatures = new Set(); /** * Log a deprecation warning (only in development, only once per feature) * @param featureName - Name of the deprecated feature * @param message - Deprecation message with migration guidance * @param replacement - Recommended replacement code/feature */ function deprecationWarning(featureName, message, replacement) { if (process.env.NODE_ENV !== 'development') { return; } if (warnedFeatures.has(featureName)) { return; // Already warned about this feature } warnedFeatures.add(featureName); const fullMessage = [`⚠️ [react-pages v2] Deprecation Warning`, `Feature: ${featureName}`, `${message}`, replacement ? `Recommended replacement:\n${replacement}` : '', 'See https://github.com/gaddario98/react-pages#migration for details'].filter(Boolean).join('\n'); console.warn(fullMessage); } /** * Warnings for specific v1.x API patterns */ const V1_DEPRECATIONS = { HELMET_PROVIDER: { name: 'react-helmet-async wrapper', message: 'The <HelmetProvider> wrapper is no longer needed. Metadata injection is now automatic in PageGenerator.', replacement: `Remove <HelmetProvider> from your app: // Before (v1.x): <HelmetProvider> <PageGenerator {...pageProps} /> </HelmetProvider> // After (v2.x): <PageGenerator {...pageProps} />` }, INLINE_METADATA: { name: 'Inline metadata via viewSettings', message: 'Setting metadata directly in viewSettings (metaTitle, metaDescription) is deprecated. Use the new PageProps.meta field instead.', replacement: `Update your PageProps: // Before (v1.x): { id: "page", viewSettings: { metaTitle: "...", metaDescription: "..." } } // After (v2.x): { id: "page", meta: { title: "...", description: "..." } }` }, MANUAL_LAZY_LOADING: { name: 'Manual React.lazy() for content', message: 'Wrapping content in React.lazy() and Suspense manually is no longer needed. Use the ContentItem.lazy configuration instead.', replacement: `// Before (v1.x): const Component = lazy(() => import('./Heavy')); <Suspense fallback={<div>Loading...</div>}> <Component /> </Suspense> // After (v2.x): { type: "custom", component: <HeavyComponent />, lazy: true, lazyTrigger: "viewport" }` }, CUSTOM_FORM_DEBOUNCE: { name: 'Custom form debouncing', message: 'Custom debouncing logic in form handlers is now built-in. Use the form.debounceDelay property instead.', replacement: `// Before (v1.x): You had to implement debouncing manually // After (v2.x): { form: { fields: [...], debounceDelay: 300 // Built-in debouncing } }` }, QUERY_DEPENDENCY_TRACKING: { name: 'Implicit query dependency tracking', message: 'Components that depend on specific queries should declare those dependencies explicitly using ContentItem.usedQueries for better performance.', replacement: `// Before (v1.x): All content items re-rendered on any query change // After (v2.x): Declare specific dependencies { type: "custom", component: <MyComponent />, usedQueries: ["user", "posts"] // Only re-render when these change }` }, HELMET_IMPORT: { name: 'Direct react-helmet-async import', message: 'Importing react-helmet-async directly is no longer recommended. The library handles metadata automatically.', replacement: `// Before (v1.x): import { Helmet } from 'react-helmet-async'; <Helmet> <title>Page Title</title> </Helmet> // After (v2.x): const pageProps = { meta: { title: "Page Title" } }` } }; /** * Check if the app is using a deprecated pattern and warn * @param pattern - The deprecated pattern to check */ function warnIfUsingDeprecatedPattern(pattern) { const deprecation = V1_DEPRECATIONS[pattern]; deprecationWarning(deprecation.name, deprecation.message, deprecation.replacement); } /** * Reset deprecation warnings (useful for testing) */ function resetDeprecationWarnings() { warnedFeatures.clear(); }exports.MemoizationCache=MemoizationCache;exports.StableCache=StableCache;exports.V1_DEPRECATIONS=V1_DEPRECATIONS;exports.deepEqual=deepEqual;exports.deepEqualFallback=deepEqualFallback;exports.deepMerge=deepMerge;exports.deepMergeWithOptions=deepMergeWithOptions;exports.deprecationWarning=deprecationWarning;exports.isStableValue=isStableValue;exports.lazyBatch=lazyBatch;exports.lazyWithPreload=lazyWithPreload;exports.memoPropsComparator=memoPropsComparator;exports.memoize=memoize;exports.mergeArraysByKey=mergeArraysByKey;exports.optimizeDeps=optimizeDeps;exports.preloadComponents=preloadComponents;exports.resetDeprecationWarnings=resetDeprecationWarnings;exports.shallowEqual=shallowEqual;exports.shallowMerge=shallowMerge;exports.usePreloadLazy=usePreloadLazy;exports.usePreloadLazyBatch=usePreloadLazyBatch;exports.usePreloadOnViewport=usePreloadOnViewport;exports.warnIfUsingDeprecatedPattern=warnIfUsingDeprecatedPattern;exports.withLazyErrorBoundary=withLazyErrorBoundary;//# sourceMappingURL=index.js.map