@lolyjs/core 0.2.0-alpha.15 → 0.2.0-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../modules/react/components/index.ts","../../modules/react/components/Image/index.tsx","../../modules/react/cache/client-data-cache/index.ts","../../modules/react/components/Link/index.tsx"],"sourcesContent":["export { Image } from \"./Image\";\r\nexport { Link } from \"./Link\";","import React from 'react';\r\n\r\nconst BASE_DIR = \"/static/assets\";\r\n\r\ninterface ImageProps\r\n extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"alt\"> {\r\n src: string;\r\n alt: string;\r\n width?: number;\r\n height?: number;\r\n className?: string;\r\n}\r\n\r\n/**\r\n * Image component that handles static asset paths.\r\n *\r\n * Automatically prefixes relative paths with the static assets directory.\r\n *\r\n * @param props - Image component props\r\n * @returns Image element\r\n */\r\nexport function Image({\r\n src,\r\n alt,\r\n width,\r\n height,\r\n className,\r\n ...rest\r\n}: ImageProps) {\r\n const style: React.CSSProperties = {};\r\n\r\n if (width) style.width = width;\r\n if (height) style.height = height;\r\n\r\n const isRelative = !src.startsWith('http');\r\n const FULL_ROUTE = isRelative ? `${BASE_DIR}/${src}` : src;\r\n\r\n return (\r\n <img\r\n src={FULL_ROUTE}\r\n alt={alt}\r\n width={width}\r\n height={height}\r\n className={className}\r\n style={style}\r\n {...rest}\r\n />\r\n );\r\n}\r\n","type RouteData = {\n ok: boolean;\n status: number;\n json: any;\n};\n\ntype CacheEntry =\n | { status: \"pending\"; promise: Promise<RouteData> }\n | { status: \"fulfilled\"; value: RouteData }\n | { status: \"rejected\"; error: any };\n\n// Use window to guarantee a single shared cache instance\n// across all bundles/modules\nconst CACHE_KEY = \"__FW_DATA_CACHE__\";\n\n// Maximum number of entries in the cache (LRU)\nconst MAX_CACHE_SIZE = 100;\n\ntype CacheStore = {\n data: Map<string, CacheEntry>;\n index: Map<string, Set<string>>; // pathBase -> Set of keys\n lru: string[]; // Ordered list: most recent at end, oldest at start\n};\n\nfunction getCacheStore(): CacheStore {\n if (typeof window !== \"undefined\") {\n if (!(window as any)[CACHE_KEY]) {\n (window as any)[CACHE_KEY] = {\n data: new Map<string, CacheEntry>(),\n index: new Map<string, Set<string>>(),\n lru: [],\n };\n }\n return (window as any)[CACHE_KEY];\n }\n // Fallback for SSR (though this shouldn't be used on the client)\n return {\n data: new Map<string, CacheEntry>(),\n index: new Map<string, Set<string>>(),\n lru: [],\n };\n}\n\nconst cacheStore = getCacheStore();\nconst dataCache = cacheStore.data;\nconst pathIndex = cacheStore.index;\nconst lru = cacheStore.lru;\n\n// Helper functions for cache management\n\n/**\n * Extract base path from a cache key (removes query params)\n */\nfunction extractPathBase(key: string): string {\n return key.split(\"?\")[0];\n}\n\n/**\n * Add key to path index\n */\nfunction addToIndex(key: string): void {\n const pathBase = extractPathBase(key);\n if (!pathIndex.has(pathBase)) {\n pathIndex.set(pathBase, new Set());\n }\n pathIndex.get(pathBase)!.add(key);\n}\n\n/**\n * Remove key from path index\n */\nfunction removeFromIndex(key: string): void {\n const pathBase = extractPathBase(key);\n const keys = pathIndex.get(pathBase);\n if (keys) {\n keys.delete(key);\n if (keys.size === 0) {\n pathIndex.delete(pathBase);\n }\n }\n}\n\n/**\n * Update LRU order - move key to end (most recent)\n */\nfunction updateLRU(key: string): void {\n const index = lru.indexOf(key);\n if (index !== -1) {\n lru.splice(index, 1);\n }\n lru.push(key);\n}\n\n/**\n * Remove oldest entries if cache exceeds MAX_CACHE_SIZE\n */\nfunction evictOldest(): void {\n while (lru.length >= MAX_CACHE_SIZE && lru.length > 0) {\n const oldestKey = lru.shift()!;\n dataCache.delete(oldestKey);\n removeFromIndex(oldestKey);\n }\n}\n\n/**\n * Set cache entry and maintain indexes\n */\nfunction setCacheEntry(key: string, entry: CacheEntry): void {\n const existingEntry = dataCache.get(key);\n const wasFulfilled = existingEntry?.status === \"fulfilled\";\n \n dataCache.set(key, entry);\n \n // Only track fulfilled entries in LRU and index (not pending/rejected)\n if (entry.status === \"fulfilled\") {\n // Add to index if it wasn't already fulfilled (new entry or transition from pending/rejected)\n if (!wasFulfilled) {\n addToIndex(key);\n }\n updateLRU(key);\n evictOldest();\n } else if (wasFulfilled) {\n // If entry was fulfilled and now isn't (transitioning to pending/rejected), remove from index\n removeFromIndex(key);\n }\n}\n\n/**\n * Delete cache entry and clean up indexes\n */\nfunction deleteCacheEntry(key: string): void {\n if (dataCache.has(key)) {\n dataCache.delete(key);\n removeFromIndex(key);\n const lruIndex = lru.indexOf(key);\n if (lruIndex !== -1) {\n lru.splice(lruIndex, 1);\n }\n }\n}\n\nfunction buildDataUrl(url: string): string {\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + \"__fw_data=1\";\n}\n\nasync function fetchRouteDataOnce(url: string): Promise<RouteData> {\n const dataUrl = buildDataUrl(url);\n\n const res = await fetch(dataUrl, {\n headers: {\n \"x-fw-data\": \"1\",\n Accept: \"application/json\",\n },\n });\n\n let json: any = {};\n\n try {\n const text = await res.text();\n if (text) {\n json = JSON.parse(text);\n }\n } catch (parseError) {\n console.error(\n \"[client][cache] Failed to parse response as JSON:\",\n parseError\n );\n }\n\n const result: RouteData = {\n ok: res.ok,\n status: res.status,\n json,\n };\n\n return result;\n}\n\n/**\n * Revalidates route data by removing it from the cache.\n * The next time you navigate to this route, fresh data will be fetched from the server.\n * This is a client-side function and does not require a server-side revalidation.\n *\n * @param path - The route path to revalidate (e.g., '/posts/1' or '/posts/1?page=2')\n * If query params are not included, revalidates all variants of that route.\n *\n * @example\n * ```ts\n * // After saving something to the DB, revalidate the route\n * await saveToDatabase(data);\n * revalidatePath('/posts');\n * \n * // Revalidate a specific route with query params\n * revalidatePath('/posts?page=2');\n * ```\n */\nexport function revalidatePath(path: string): void {\n // Normalize the base path (without query params)\n const normalizedPath = path.split(\"?\")[0];\n const hasQueryParams = path.includes(\"?\");\n \n // Get all keys for this path base from index (O(1) lookup)\n const keysForPath = pathIndex.get(normalizedPath);\n \n if (!keysForPath || keysForPath.size === 0) {\n return; // No entries to revalidate\n }\n \n // If the path includes specific query params, extract them\n let specificQueryParams: string | undefined;\n if (hasQueryParams) {\n const queryPart = path.split(\"?\")[1];\n // Sort query params for consistent comparison\n specificQueryParams = queryPart\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n }\n \n // Iterate only over keys for this path (much smaller set)\n const keysToDelete: string[] = [];\n for (const key of keysForPath) {\n // If specific query params were specified, check if they match\n if (hasQueryParams && specificQueryParams) {\n const [, keyQuery = \"\"] = key.split(\"?\");\n const keyQueryParams = keyQuery\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n \n if (keyQueryParams === specificQueryParams) {\n keysToDelete.push(key);\n }\n } else {\n // If no specific query params, revalidate all variants\n keysToDelete.push(key);\n }\n }\n \n // Delete matching entries\n keysToDelete.forEach((key) => {\n deleteCacheEntry(key);\n });\n \n // If the revalidated path matches the current route, automatically refresh data\n if (typeof window !== \"undefined\") {\n const currentPathname = window.location.pathname;\n const currentSearch = window.location.search;\n const matchesCurrentPath = normalizedPath === currentPathname;\n \n if (matchesCurrentPath) {\n if (hasQueryParams && specificQueryParams) {\n const currentQueryParams = currentSearch\n .replace(\"?\", \"\")\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n \n if (currentQueryParams === specificQueryParams) {\n revalidate().catch((err) => {\n console.error(\n \"[client][cache] Error revalidating current route:\",\n err\n );\n });\n }\n } else {\n revalidate().catch((err) => {\n console.error(\n \"[client][cache] Error revalidating current route:\",\n err\n );\n });\n }\n }\n }\n}\n\n/**\n * Revalidates and refreshes the current page data.\n * Similar to Next.js's `router.refresh()`.\n * \n * This function:\n * 1. Removes the current route from cache\n * 2. Fetches fresh data from the server\n * 3. Updates window.__FW_DATA__ with the new data\n * 4. Dispatches a 'fw-data-refresh' event for components to listen to\n * \n * @returns Promise that resolves with the fresh route data\n * \n * @example\n * ```ts\n * // Refresh current page data after a mutation\n * await revalidate();\n * ```\n */\nexport async function revalidate(): Promise<RouteData> {\n if (typeof window === \"undefined\") {\n throw new Error(\"revalidate() can only be called on the client\");\n }\n\n const pathname = window.location.pathname + window.location.search;\n \n // Revalidate the path (remove from cache)\n revalidatePath(pathname);\n \n // Fetch fresh data\n const freshData = await getRouteData(pathname, { revalidate: true });\n \n // Update window.__FW_DATA__ if it exists\n if ((window as any).__FW_DATA__ && freshData.ok && freshData.json) {\n const currentData = (window as any).__FW_DATA__;\n (window as any).__FW_DATA__ = {\n ...currentData,\n pathname: pathname.split(\"?\")[0],\n params: freshData.json.params || currentData.params || {},\n props: freshData.json.props || currentData.props || {},\n metadata: freshData.json.metadata ?? currentData.metadata ?? null,\n notFound: freshData.json.notFound ?? false,\n error: freshData.json.error ?? false,\n };\n \n // Dispatch event for components to listen to\n window.dispatchEvent(new CustomEvent(\"fw-data-refresh\", {\n detail: { data: freshData },\n }));\n }\n \n return freshData;\n}\n\n/**\n * @deprecated Use `revalidatePath()` instead. This function is kept for backwards compatibility.\n */\nexport function revalidateRouteData(url: string): void {\n revalidatePath(url);\n}\n\nexport function prefetchRouteData(url: string): void {\n const key = buildDataUrl(url);\n\n const cached = dataCache.get(key);\n\n if (cached && cached.status !== \"rejected\") {\n // Update LRU if it exists and is fulfilled\n if (cached.status === \"fulfilled\") {\n updateLRU(key);\n }\n return;\n }\n\n const promise = fetchRouteDataOnce(url)\n .then((value) => {\n setCacheEntry(key, { status: \"fulfilled\", value });\n return value;\n })\n .catch((error) => {\n console.error(\"[client][cache] Error prefetching route data:\", error);\n dataCache.set(key, { status: \"rejected\", error });\n throw error;\n });\n\n dataCache.set(key, { status: \"pending\", promise });\n}\n\nexport type GetRouteDataOptions = {\n /**\n * If true, forces revalidation of route data,\n * ignoring the cache and fetching fresh data from the server.\n * Similar to Next.js's `router.refresh()` behavior.\n */\n revalidate?: boolean;\n};\n\nexport async function getRouteData(\n url: string,\n options?: GetRouteDataOptions\n): Promise<RouteData> {\n const key = buildDataUrl(url);\n\n // If revalidation is requested, remove the entry from cache\n if (options?.revalidate) {\n deleteCacheEntry(key);\n }\n\n const entry = dataCache.get(key);\n\n if (entry) {\n if (entry.status === \"fulfilled\") {\n // Update LRU: mark as recently used\n updateLRU(key);\n return entry.value;\n }\n if (entry.status === \"pending\") {\n return entry.promise;\n }\n }\n\n // No entry in cache, fetch it\n const promise = fetchRouteDataOnce(url)\n .then((value) => {\n setCacheEntry(key, { status: \"fulfilled\", value });\n return value;\n })\n .catch((error) => {\n console.error(\"[client][cache] Error fetching route data:\", error);\n dataCache.set(key, { status: \"rejected\", error });\n throw error;\n });\n\n dataCache.set(key, { status: \"pending\", promise });\n return promise;\n}\n","import React from \"react\";\r\nimport { prefetchRouteData } from \"../../cache/index\";\r\n\r\ninterface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\r\n href: string;\r\n children: React.ReactNode;\r\n prefetch?: boolean;\r\n}\r\n\r\nimport { useEffect, useRef } from \"react\"; \r\n\r\nfunction isExternal(href: string) {\r\n try {\r\n const url = new URL(href, window.location.href);\r\n return url.origin !== window.location.origin;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nexport function Link({\r\n href,\r\n prefetch = true,\r\n children,\r\n ...rest\r\n}: React.PropsWithChildren<{\r\n href: string;\r\n prefetch?: boolean;\r\n}> &\r\n React.AnchorHTMLAttributes<HTMLAnchorElement>) {\r\n const ref = useRef<HTMLAnchorElement | null>(null);\r\n\r\n useEffect(() => {\r\n if (!prefetch || !ref.current || isExternal(href)) return;\r\n\r\n const el = ref.current;\r\n let prefetched = false;\r\n\r\n const io = new IntersectionObserver(([entry]) => {\r\n if (entry.isIntersecting && !prefetched) {\r\n prefetched = true;\r\n prefetchRouteData(href);\r\n }\r\n });\r\n\r\n io.observe(el);\r\n return () => io.disconnect();\r\n }, [href, prefetch]);\r\n\r\n return (\r\n <a ref={ref} href={href} {...rest}>\r\n {children}\r\n </a>\r\n );\r\n}\r\n"],"mappings":"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","names":["import_jsx_runtime"]}
1
+ {"version":3,"sources":["../../modules/react/components/index.ts","../../modules/react/components/Image/index.tsx","../../modules/react/cache/client-data-cache/index.ts","../../modules/react/components/Link/index.tsx"],"sourcesContent":["export { Image } from \"./Image\";\r\nexport { Link } from \"./Link\";","import React from 'react';\r\n\r\nconst BASE_DIR = \"/static/assets\";\r\n\r\ninterface ImageProps\r\n extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"alt\"> {\r\n src: string;\r\n alt: string;\r\n width?: number;\r\n height?: number;\r\n className?: string;\r\n}\r\n\r\n/**\r\n * Image component that handles static asset paths.\r\n *\r\n * Automatically prefixes relative paths with the static assets directory.\r\n *\r\n * @param props - Image component props\r\n * @returns Image element\r\n */\r\nexport function Image({\r\n src,\r\n alt,\r\n width,\r\n height,\r\n className,\r\n ...rest\r\n}: ImageProps) {\r\n const style: React.CSSProperties = {};\r\n\r\n if (width) style.width = width;\r\n if (height) style.height = height;\r\n\r\n const isRelative = !src.startsWith('http');\r\n const FULL_ROUTE = isRelative ? `${BASE_DIR}/${src}` : src;\r\n\r\n return (\r\n <img\r\n src={FULL_ROUTE}\r\n alt={alt}\r\n width={width}\r\n height={height}\r\n className={className}\r\n style={style}\r\n {...rest}\r\n />\r\n );\r\n}\r\n","import type { PageMetadata } from \"@router/index\";\n\n/**\n * Response data structure from server for route data requests\n */\nexport type RouteDataResponse = {\n props?: Record<string, unknown>;\n metadata?: PageMetadata | null;\n theme?: string;\n redirect?: { destination: string; permanent?: boolean };\n notFound?: boolean;\n error?: boolean;\n message?: string;\n params?: Record<string, string>;\n};\n\ntype RouteData = {\n ok: boolean;\n status: number;\n json: RouteDataResponse;\n};\n\ntype CacheEntry =\n | { status: \"pending\"; promise: Promise<RouteData> }\n | { status: \"fulfilled\"; value: RouteData }\n | { status: \"rejected\"; error: any };\n\n// Use window to guarantee a single shared cache instance\n// across all bundles/modules\nconst CACHE_KEY = \"__FW_DATA_CACHE__\";\n\n// Maximum number of entries in the cache (LRU)\nconst MAX_CACHE_SIZE = 100;\n\ntype CacheStore = {\n data: Map<string, CacheEntry>;\n index: Map<string, Set<string>>; // pathBase -> Set of keys\n lru: string[]; // Ordered list: most recent at end, oldest at start\n};\n\nfunction getCacheStore(): CacheStore {\n if (typeof window !== \"undefined\") {\n if (!(window as any)[CACHE_KEY]) {\n (window as any)[CACHE_KEY] = {\n data: new Map<string, CacheEntry>(),\n index: new Map<string, Set<string>>(),\n lru: [],\n };\n }\n return (window as any)[CACHE_KEY];\n }\n // Fallback for SSR (though this shouldn't be used on the client)\n return {\n data: new Map<string, CacheEntry>(),\n index: new Map<string, Set<string>>(),\n lru: [],\n };\n}\n\nconst cacheStore = getCacheStore();\nconst dataCache = cacheStore.data;\nconst pathIndex = cacheStore.index;\nconst lru = cacheStore.lru;\n\n// Helper functions for cache management\n\n/**\n * Extract base path from a cache key (removes query params)\n */\nfunction extractPathBase(key: string): string {\n return key.split(\"?\")[0];\n}\n\n/**\n * Add key to path index\n */\nfunction addToIndex(key: string): void {\n const pathBase = extractPathBase(key);\n if (!pathIndex.has(pathBase)) {\n pathIndex.set(pathBase, new Set());\n }\n pathIndex.get(pathBase)!.add(key);\n}\n\n/**\n * Remove key from path index\n */\nfunction removeFromIndex(key: string): void {\n const pathBase = extractPathBase(key);\n const keys = pathIndex.get(pathBase);\n if (keys) {\n keys.delete(key);\n if (keys.size === 0) {\n pathIndex.delete(pathBase);\n }\n }\n}\n\n/**\n * Update LRU order - move key to end (most recent)\n */\nfunction updateLRU(key: string): void {\n const index = lru.indexOf(key);\n if (index !== -1) {\n lru.splice(index, 1);\n }\n lru.push(key);\n}\n\n/**\n * Remove oldest entries if cache exceeds MAX_CACHE_SIZE\n */\nfunction evictOldest(): void {\n while (lru.length >= MAX_CACHE_SIZE && lru.length > 0) {\n const oldestKey = lru.shift()!;\n dataCache.delete(oldestKey);\n removeFromIndex(oldestKey);\n }\n}\n\n/**\n * Set cache entry and maintain indexes\n */\nfunction setCacheEntry(key: string, entry: CacheEntry): void {\n const existingEntry = dataCache.get(key);\n const wasFulfilled = existingEntry?.status === \"fulfilled\";\n \n dataCache.set(key, entry);\n \n // Only track fulfilled entries in LRU and index (not pending/rejected)\n if (entry.status === \"fulfilled\") {\n // Add to index if it wasn't already fulfilled (new entry or transition from pending/rejected)\n if (!wasFulfilled) {\n addToIndex(key);\n }\n updateLRU(key);\n evictOldest();\n } else if (wasFulfilled) {\n // If entry was fulfilled and now isn't (transitioning to pending/rejected), remove from index\n removeFromIndex(key);\n }\n}\n\n/**\n * Delete cache entry and clean up indexes\n */\nfunction deleteCacheEntry(key: string): void {\n if (dataCache.has(key)) {\n dataCache.delete(key);\n removeFromIndex(key);\n const lruIndex = lru.indexOf(key);\n if (lruIndex !== -1) {\n lru.splice(lruIndex, 1);\n }\n }\n}\n\nfunction buildDataUrl(url: string): string {\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + \"__fw_data=1\";\n}\n\nasync function fetchRouteDataOnce(url: string): Promise<RouteData> {\n const dataUrl = buildDataUrl(url);\n\n const res = await fetch(dataUrl, {\n headers: {\n \"x-fw-data\": \"1\",\n Accept: \"application/json\",\n },\n });\n\n let json: any = {};\n\n try {\n const text = await res.text();\n if (text) {\n json = JSON.parse(text);\n }\n } catch (parseError) {\n console.error(\n \"[client][cache] Failed to parse response as JSON:\",\n parseError\n );\n }\n\n const result: RouteData = {\n ok: res.ok,\n status: res.status,\n json,\n };\n\n return result;\n}\n\n/**\n * Revalidates route data by removing it from the cache.\n * The next time you navigate to this route, fresh data will be fetched from the server.\n * This is a client-side function and does not require a server-side revalidation.\n *\n * @param path - The route path to revalidate (e.g., '/posts/1' or '/posts/1?page=2')\n * If query params are not included, revalidates all variants of that route.\n *\n * @example\n * ```ts\n * // After saving something to the DB, revalidate the route\n * await saveToDatabase(data);\n * revalidatePath('/posts');\n * \n * // Revalidate a specific route with query params\n * revalidatePath('/posts?page=2');\n * ```\n */\nexport function revalidatePath(path: string): void {\n // Normalize the base path (without query params)\n const normalizedPath = path.split(\"?\")[0];\n const hasQueryParams = path.includes(\"?\");\n \n // Get all keys for this path base from index (O(1) lookup)\n const keysForPath = pathIndex.get(normalizedPath);\n \n if (!keysForPath || keysForPath.size === 0) {\n return; // No entries to revalidate\n }\n \n // If the path includes specific query params, extract them\n let specificQueryParams: string | undefined;\n if (hasQueryParams) {\n const queryPart = path.split(\"?\")[1];\n // Sort query params for consistent comparison\n specificQueryParams = queryPart\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n }\n \n // Iterate only over keys for this path (much smaller set)\n const keysToDelete: string[] = [];\n for (const key of keysForPath) {\n // If specific query params were specified, check if they match\n if (hasQueryParams && specificQueryParams) {\n const [, keyQuery = \"\"] = key.split(\"?\");\n const keyQueryParams = keyQuery\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n \n if (keyQueryParams === specificQueryParams) {\n keysToDelete.push(key);\n }\n } else {\n // If no specific query params, revalidate all variants\n keysToDelete.push(key);\n }\n }\n \n // Delete matching entries\n keysToDelete.forEach((key) => {\n deleteCacheEntry(key);\n });\n \n // If the revalidated path matches the current route, automatically refresh data\n if (typeof window !== \"undefined\") {\n const currentPathname = window.location.pathname;\n const currentSearch = window.location.search;\n const matchesCurrentPath = normalizedPath === currentPathname;\n \n if (matchesCurrentPath) {\n if (hasQueryParams && specificQueryParams) {\n const currentQueryParams = currentSearch\n .replace(\"?\", \"\")\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n \n if (currentQueryParams === specificQueryParams) {\n revalidate().catch((err) => {\n console.error(\n \"[client][cache] Error revalidating current route:\",\n err\n );\n });\n }\n } else {\n revalidate().catch((err) => {\n console.error(\n \"[client][cache] Error revalidating current route:\",\n err\n );\n });\n }\n }\n }\n}\n\n/**\n * Revalidates and refreshes the current page data.\n * Similar to Next.js's `router.refresh()`.\n * \n * This function:\n * 1. Removes the current route from cache\n * 2. Fetches fresh data from the server\n * 3. Updates window.__FW_DATA__ with the new data\n * 4. Dispatches a 'fw-data-refresh' event for components to listen to\n * \n * @returns Promise that resolves with the fresh route data\n * \n * @example\n * ```ts\n * // Refresh current page data after a mutation\n * await revalidate();\n * ```\n */\nexport async function revalidate(): Promise<RouteData> {\n if (typeof window === \"undefined\") {\n throw new Error(\"revalidate() can only be called on the client\");\n }\n\n const pathname = window.location.pathname + window.location.search;\n \n // Revalidate the path (remove from cache)\n revalidatePath(pathname);\n \n // Fetch fresh data\n const freshData = await getRouteData(pathname, { revalidate: true });\n \n // Update window.__FW_DATA__ if it exists\n if ((window as any).__FW_DATA__ && freshData.ok && freshData.json) {\n const currentData = (window as any).__FW_DATA__;\n (window as any).__FW_DATA__ = {\n ...currentData,\n pathname: pathname.split(\"?\")[0],\n params: freshData.json.params || currentData.params || {},\n props: freshData.json.props || currentData.props || {},\n metadata: freshData.json.metadata ?? currentData.metadata ?? null,\n notFound: freshData.json.notFound ?? false,\n error: freshData.json.error ?? false,\n };\n \n // Dispatch event for components to listen to\n window.dispatchEvent(new CustomEvent(\"fw-data-refresh\", {\n detail: { data: freshData },\n }));\n }\n \n return freshData;\n}\n\n/**\n * @deprecated Use `revalidatePath()` instead. This function is kept for backwards compatibility.\n */\nexport function revalidateRouteData(url: string): void {\n revalidatePath(url);\n}\n\nexport function prefetchRouteData(url: string): void {\n const key = buildDataUrl(url);\n\n const cached = dataCache.get(key);\n\n if (cached && cached.status !== \"rejected\") {\n // Update LRU if it exists and is fulfilled\n if (cached.status === \"fulfilled\") {\n updateLRU(key);\n }\n return;\n }\n\n const promise = fetchRouteDataOnce(url)\n .then((value) => {\n setCacheEntry(key, { status: \"fulfilled\", value });\n return value;\n })\n .catch((error) => {\n console.error(\"[client][cache] Error prefetching route data:\", error);\n dataCache.set(key, { status: \"rejected\", error });\n throw error;\n });\n\n dataCache.set(key, { status: \"pending\", promise });\n}\n\nexport type GetRouteDataOptions = {\n /**\n * If true, forces revalidation of route data,\n * ignoring the cache and fetching fresh data from the server.\n * Similar to Next.js's `router.refresh()` behavior.\n */\n revalidate?: boolean;\n};\n\nexport async function getRouteData(\n url: string,\n options?: GetRouteDataOptions\n): Promise<RouteData> {\n const key = buildDataUrl(url);\n\n // If revalidation is requested, remove the entry from cache\n if (options?.revalidate) {\n deleteCacheEntry(key);\n }\n\n const entry = dataCache.get(key);\n\n if (entry) {\n if (entry.status === \"fulfilled\") {\n // Update LRU: mark as recently used\n updateLRU(key);\n return entry.value;\n }\n if (entry.status === \"pending\") {\n return entry.promise;\n }\n }\n\n // No entry in cache, fetch it\n const promise = fetchRouteDataOnce(url)\n .then((value) => {\n setCacheEntry(key, { status: \"fulfilled\", value });\n return value;\n })\n .catch((error) => {\n console.error(\"[client][cache] Error fetching route data:\", error);\n dataCache.set(key, { status: \"rejected\", error });\n throw error;\n });\n\n dataCache.set(key, { status: \"pending\", promise });\n return promise;\n}\n","import React from \"react\";\r\nimport { prefetchRouteData } from \"../../cache/index\";\r\n\r\ninterface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\r\n href: string;\r\n children: React.ReactNode;\r\n prefetch?: boolean;\r\n}\r\n\r\nimport { useEffect, useRef } from \"react\"; \r\n\r\nfunction isExternal(href: string) {\r\n try {\r\n const url = new URL(href, window.location.href);\r\n return url.origin !== window.location.origin;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nexport function Link({\r\n href,\r\n prefetch = true,\r\n children,\r\n ...rest\r\n}: React.PropsWithChildren<{\r\n href: string;\r\n prefetch?: boolean;\r\n}> &\r\n React.AnchorHTMLAttributes<HTMLAnchorElement>) {\r\n const ref = useRef<HTMLAnchorElement | null>(null);\r\n\r\n useEffect(() => {\r\n if (!prefetch || !ref.current || isExternal(href)) return;\r\n\r\n const el = ref.current;\r\n let prefetched = false;\r\n\r\n const io = new IntersectionObserver(([entry]) => {\r\n if (entry.isIntersecting && !prefetched) {\r\n prefetched = true;\r\n prefetchRouteData(href);\r\n }\r\n });\r\n\r\n io.observe(el);\r\n return () => io.disconnect();\r\n }, [href, prefetch]);\r\n\r\n return (\r\n <a ref={ref} href={href} {...rest}>\r\n {children}\r\n </a>\r\n );\r\n}\r\n"],"mappings":"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","names":["import_jsx_runtime"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../modules/react/components/Image/index.tsx","../../modules/react/cache/client-data-cache/index.ts","../../modules/react/components/Link/index.tsx"],"sourcesContent":["import React from 'react';\r\n\r\nconst BASE_DIR = \"/static/assets\";\r\n\r\ninterface ImageProps\r\n extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"alt\"> {\r\n src: string;\r\n alt: string;\r\n width?: number;\r\n height?: number;\r\n className?: string;\r\n}\r\n\r\n/**\r\n * Image component that handles static asset paths.\r\n *\r\n * Automatically prefixes relative paths with the static assets directory.\r\n *\r\n * @param props - Image component props\r\n * @returns Image element\r\n */\r\nexport function Image({\r\n src,\r\n alt,\r\n width,\r\n height,\r\n className,\r\n ...rest\r\n}: ImageProps) {\r\n const style: React.CSSProperties = {};\r\n\r\n if (width) style.width = width;\r\n if (height) style.height = height;\r\n\r\n const isRelative = !src.startsWith('http');\r\n const FULL_ROUTE = isRelative ? `${BASE_DIR}/${src}` : src;\r\n\r\n return (\r\n <img\r\n src={FULL_ROUTE}\r\n alt={alt}\r\n width={width}\r\n height={height}\r\n className={className}\r\n style={style}\r\n {...rest}\r\n />\r\n );\r\n}\r\n","type RouteData = {\n ok: boolean;\n status: number;\n json: any;\n};\n\ntype CacheEntry =\n | { status: \"pending\"; promise: Promise<RouteData> }\n | { status: \"fulfilled\"; value: RouteData }\n | { status: \"rejected\"; error: any };\n\n// Use window to guarantee a single shared cache instance\n// across all bundles/modules\nconst CACHE_KEY = \"__FW_DATA_CACHE__\";\n\n// Maximum number of entries in the cache (LRU)\nconst MAX_CACHE_SIZE = 100;\n\ntype CacheStore = {\n data: Map<string, CacheEntry>;\n index: Map<string, Set<string>>; // pathBase -> Set of keys\n lru: string[]; // Ordered list: most recent at end, oldest at start\n};\n\nfunction getCacheStore(): CacheStore {\n if (typeof window !== \"undefined\") {\n if (!(window as any)[CACHE_KEY]) {\n (window as any)[CACHE_KEY] = {\n data: new Map<string, CacheEntry>(),\n index: new Map<string, Set<string>>(),\n lru: [],\n };\n }\n return (window as any)[CACHE_KEY];\n }\n // Fallback for SSR (though this shouldn't be used on the client)\n return {\n data: new Map<string, CacheEntry>(),\n index: new Map<string, Set<string>>(),\n lru: [],\n };\n}\n\nconst cacheStore = getCacheStore();\nconst dataCache = cacheStore.data;\nconst pathIndex = cacheStore.index;\nconst lru = cacheStore.lru;\n\n// Helper functions for cache management\n\n/**\n * Extract base path from a cache key (removes query params)\n */\nfunction extractPathBase(key: string): string {\n return key.split(\"?\")[0];\n}\n\n/**\n * Add key to path index\n */\nfunction addToIndex(key: string): void {\n const pathBase = extractPathBase(key);\n if (!pathIndex.has(pathBase)) {\n pathIndex.set(pathBase, new Set());\n }\n pathIndex.get(pathBase)!.add(key);\n}\n\n/**\n * Remove key from path index\n */\nfunction removeFromIndex(key: string): void {\n const pathBase = extractPathBase(key);\n const keys = pathIndex.get(pathBase);\n if (keys) {\n keys.delete(key);\n if (keys.size === 0) {\n pathIndex.delete(pathBase);\n }\n }\n}\n\n/**\n * Update LRU order - move key to end (most recent)\n */\nfunction updateLRU(key: string): void {\n const index = lru.indexOf(key);\n if (index !== -1) {\n lru.splice(index, 1);\n }\n lru.push(key);\n}\n\n/**\n * Remove oldest entries if cache exceeds MAX_CACHE_SIZE\n */\nfunction evictOldest(): void {\n while (lru.length >= MAX_CACHE_SIZE && lru.length > 0) {\n const oldestKey = lru.shift()!;\n dataCache.delete(oldestKey);\n removeFromIndex(oldestKey);\n }\n}\n\n/**\n * Set cache entry and maintain indexes\n */\nfunction setCacheEntry(key: string, entry: CacheEntry): void {\n const existingEntry = dataCache.get(key);\n const wasFulfilled = existingEntry?.status === \"fulfilled\";\n \n dataCache.set(key, entry);\n \n // Only track fulfilled entries in LRU and index (not pending/rejected)\n if (entry.status === \"fulfilled\") {\n // Add to index if it wasn't already fulfilled (new entry or transition from pending/rejected)\n if (!wasFulfilled) {\n addToIndex(key);\n }\n updateLRU(key);\n evictOldest();\n } else if (wasFulfilled) {\n // If entry was fulfilled and now isn't (transitioning to pending/rejected), remove from index\n removeFromIndex(key);\n }\n}\n\n/**\n * Delete cache entry and clean up indexes\n */\nfunction deleteCacheEntry(key: string): void {\n if (dataCache.has(key)) {\n dataCache.delete(key);\n removeFromIndex(key);\n const lruIndex = lru.indexOf(key);\n if (lruIndex !== -1) {\n lru.splice(lruIndex, 1);\n }\n }\n}\n\nfunction buildDataUrl(url: string): string {\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + \"__fw_data=1\";\n}\n\nasync function fetchRouteDataOnce(url: string): Promise<RouteData> {\n const dataUrl = buildDataUrl(url);\n\n const res = await fetch(dataUrl, {\n headers: {\n \"x-fw-data\": \"1\",\n Accept: \"application/json\",\n },\n });\n\n let json: any = {};\n\n try {\n const text = await res.text();\n if (text) {\n json = JSON.parse(text);\n }\n } catch (parseError) {\n console.error(\n \"[client][cache] Failed to parse response as JSON:\",\n parseError\n );\n }\n\n const result: RouteData = {\n ok: res.ok,\n status: res.status,\n json,\n };\n\n return result;\n}\n\n/**\n * Revalidates route data by removing it from the cache.\n * The next time you navigate to this route, fresh data will be fetched from the server.\n * This is a client-side function and does not require a server-side revalidation.\n *\n * @param path - The route path to revalidate (e.g., '/posts/1' or '/posts/1?page=2')\n * If query params are not included, revalidates all variants of that route.\n *\n * @example\n * ```ts\n * // After saving something to the DB, revalidate the route\n * await saveToDatabase(data);\n * revalidatePath('/posts');\n * \n * // Revalidate a specific route with query params\n * revalidatePath('/posts?page=2');\n * ```\n */\nexport function revalidatePath(path: string): void {\n // Normalize the base path (without query params)\n const normalizedPath = path.split(\"?\")[0];\n const hasQueryParams = path.includes(\"?\");\n \n // Get all keys for this path base from index (O(1) lookup)\n const keysForPath = pathIndex.get(normalizedPath);\n \n if (!keysForPath || keysForPath.size === 0) {\n return; // No entries to revalidate\n }\n \n // If the path includes specific query params, extract them\n let specificQueryParams: string | undefined;\n if (hasQueryParams) {\n const queryPart = path.split(\"?\")[1];\n // Sort query params for consistent comparison\n specificQueryParams = queryPart\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n }\n \n // Iterate only over keys for this path (much smaller set)\n const keysToDelete: string[] = [];\n for (const key of keysForPath) {\n // If specific query params were specified, check if they match\n if (hasQueryParams && specificQueryParams) {\n const [, keyQuery = \"\"] = key.split(\"?\");\n const keyQueryParams = keyQuery\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n \n if (keyQueryParams === specificQueryParams) {\n keysToDelete.push(key);\n }\n } else {\n // If no specific query params, revalidate all variants\n keysToDelete.push(key);\n }\n }\n \n // Delete matching entries\n keysToDelete.forEach((key) => {\n deleteCacheEntry(key);\n });\n \n // If the revalidated path matches the current route, automatically refresh data\n if (typeof window !== \"undefined\") {\n const currentPathname = window.location.pathname;\n const currentSearch = window.location.search;\n const matchesCurrentPath = normalizedPath === currentPathname;\n \n if (matchesCurrentPath) {\n if (hasQueryParams && specificQueryParams) {\n const currentQueryParams = currentSearch\n .replace(\"?\", \"\")\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n \n if (currentQueryParams === specificQueryParams) {\n revalidate().catch((err) => {\n console.error(\n \"[client][cache] Error revalidating current route:\",\n err\n );\n });\n }\n } else {\n revalidate().catch((err) => {\n console.error(\n \"[client][cache] Error revalidating current route:\",\n err\n );\n });\n }\n }\n }\n}\n\n/**\n * Revalidates and refreshes the current page data.\n * Similar to Next.js's `router.refresh()`.\n * \n * This function:\n * 1. Removes the current route from cache\n * 2. Fetches fresh data from the server\n * 3. Updates window.__FW_DATA__ with the new data\n * 4. Dispatches a 'fw-data-refresh' event for components to listen to\n * \n * @returns Promise that resolves with the fresh route data\n * \n * @example\n * ```ts\n * // Refresh current page data after a mutation\n * await revalidate();\n * ```\n */\nexport async function revalidate(): Promise<RouteData> {\n if (typeof window === \"undefined\") {\n throw new Error(\"revalidate() can only be called on the client\");\n }\n\n const pathname = window.location.pathname + window.location.search;\n \n // Revalidate the path (remove from cache)\n revalidatePath(pathname);\n \n // Fetch fresh data\n const freshData = await getRouteData(pathname, { revalidate: true });\n \n // Update window.__FW_DATA__ if it exists\n if ((window as any).__FW_DATA__ && freshData.ok && freshData.json) {\n const currentData = (window as any).__FW_DATA__;\n (window as any).__FW_DATA__ = {\n ...currentData,\n pathname: pathname.split(\"?\")[0],\n params: freshData.json.params || currentData.params || {},\n props: freshData.json.props || currentData.props || {},\n metadata: freshData.json.metadata ?? currentData.metadata ?? null,\n notFound: freshData.json.notFound ?? false,\n error: freshData.json.error ?? false,\n };\n \n // Dispatch event for components to listen to\n window.dispatchEvent(new CustomEvent(\"fw-data-refresh\", {\n detail: { data: freshData },\n }));\n }\n \n return freshData;\n}\n\n/**\n * @deprecated Use `revalidatePath()` instead. This function is kept for backwards compatibility.\n */\nexport function revalidateRouteData(url: string): void {\n revalidatePath(url);\n}\n\nexport function prefetchRouteData(url: string): void {\n const key = buildDataUrl(url);\n\n const cached = dataCache.get(key);\n\n if (cached && cached.status !== \"rejected\") {\n // Update LRU if it exists and is fulfilled\n if (cached.status === \"fulfilled\") {\n updateLRU(key);\n }\n return;\n }\n\n const promise = fetchRouteDataOnce(url)\n .then((value) => {\n setCacheEntry(key, { status: \"fulfilled\", value });\n return value;\n })\n .catch((error) => {\n console.error(\"[client][cache] Error prefetching route data:\", error);\n dataCache.set(key, { status: \"rejected\", error });\n throw error;\n });\n\n dataCache.set(key, { status: \"pending\", promise });\n}\n\nexport type GetRouteDataOptions = {\n /**\n * If true, forces revalidation of route data,\n * ignoring the cache and fetching fresh data from the server.\n * Similar to Next.js's `router.refresh()` behavior.\n */\n revalidate?: boolean;\n};\n\nexport async function getRouteData(\n url: string,\n options?: GetRouteDataOptions\n): Promise<RouteData> {\n const key = buildDataUrl(url);\n\n // If revalidation is requested, remove the entry from cache\n if (options?.revalidate) {\n deleteCacheEntry(key);\n }\n\n const entry = dataCache.get(key);\n\n if (entry) {\n if (entry.status === \"fulfilled\") {\n // Update LRU: mark as recently used\n updateLRU(key);\n return entry.value;\n }\n if (entry.status === \"pending\") {\n return entry.promise;\n }\n }\n\n // No entry in cache, fetch it\n const promise = fetchRouteDataOnce(url)\n .then((value) => {\n setCacheEntry(key, { status: \"fulfilled\", value });\n return value;\n })\n .catch((error) => {\n console.error(\"[client][cache] Error fetching route data:\", error);\n dataCache.set(key, { status: \"rejected\", error });\n throw error;\n });\n\n dataCache.set(key, { status: \"pending\", promise });\n return promise;\n}\n","import React from \"react\";\r\nimport { prefetchRouteData } from \"../../cache/index\";\r\n\r\ninterface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\r\n href: string;\r\n children: React.ReactNode;\r\n prefetch?: boolean;\r\n}\r\n\r\nimport { useEffect, useRef } from \"react\"; \r\n\r\nfunction isExternal(href: string) {\r\n try {\r\n const url = new URL(href, window.location.href);\r\n return url.origin !== window.location.origin;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nexport function Link({\r\n href,\r\n prefetch = true,\r\n children,\r\n ...rest\r\n}: React.PropsWithChildren<{\r\n href: string;\r\n prefetch?: boolean;\r\n}> &\r\n React.AnchorHTMLAttributes<HTMLAnchorElement>) {\r\n const ref = useRef<HTMLAnchorElement | null>(null);\r\n\r\n useEffect(() => {\r\n if (!prefetch || !ref.current || isExternal(href)) return;\r\n\r\n const el = ref.current;\r\n let prefetched = false;\r\n\r\n const io = new IntersectionObserver(([entry]) => {\r\n if (entry.isIntersecting && !prefetched) {\r\n prefetched = true;\r\n prefetchRouteData(href);\r\n }\r\n });\r\n\r\n io.observe(el);\r\n return () => io.disconnect();\r\n }, [href, prefetch]);\r\n\r\n return (\r\n <a ref={ref} href={href} {...rest}>\r\n {children}\r\n </a>\r\n );\r\n}\r\n"],"mappings":"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","names":["jsx"]}
1
+ {"version":3,"sources":["../../modules/react/components/Image/index.tsx","../../modules/react/cache/client-data-cache/index.ts","../../modules/react/components/Link/index.tsx"],"sourcesContent":["import React from 'react';\r\n\r\nconst BASE_DIR = \"/static/assets\";\r\n\r\ninterface ImageProps\r\n extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"alt\"> {\r\n src: string;\r\n alt: string;\r\n width?: number;\r\n height?: number;\r\n className?: string;\r\n}\r\n\r\n/**\r\n * Image component that handles static asset paths.\r\n *\r\n * Automatically prefixes relative paths with the static assets directory.\r\n *\r\n * @param props - Image component props\r\n * @returns Image element\r\n */\r\nexport function Image({\r\n src,\r\n alt,\r\n width,\r\n height,\r\n className,\r\n ...rest\r\n}: ImageProps) {\r\n const style: React.CSSProperties = {};\r\n\r\n if (width) style.width = width;\r\n if (height) style.height = height;\r\n\r\n const isRelative = !src.startsWith('http');\r\n const FULL_ROUTE = isRelative ? `${BASE_DIR}/${src}` : src;\r\n\r\n return (\r\n <img\r\n src={FULL_ROUTE}\r\n alt={alt}\r\n width={width}\r\n height={height}\r\n className={className}\r\n style={style}\r\n {...rest}\r\n />\r\n );\r\n}\r\n","import type { PageMetadata } from \"@router/index\";\n\n/**\n * Response data structure from server for route data requests\n */\nexport type RouteDataResponse = {\n props?: Record<string, unknown>;\n metadata?: PageMetadata | null;\n theme?: string;\n redirect?: { destination: string; permanent?: boolean };\n notFound?: boolean;\n error?: boolean;\n message?: string;\n params?: Record<string, string>;\n};\n\ntype RouteData = {\n ok: boolean;\n status: number;\n json: RouteDataResponse;\n};\n\ntype CacheEntry =\n | { status: \"pending\"; promise: Promise<RouteData> }\n | { status: \"fulfilled\"; value: RouteData }\n | { status: \"rejected\"; error: any };\n\n// Use window to guarantee a single shared cache instance\n// across all bundles/modules\nconst CACHE_KEY = \"__FW_DATA_CACHE__\";\n\n// Maximum number of entries in the cache (LRU)\nconst MAX_CACHE_SIZE = 100;\n\ntype CacheStore = {\n data: Map<string, CacheEntry>;\n index: Map<string, Set<string>>; // pathBase -> Set of keys\n lru: string[]; // Ordered list: most recent at end, oldest at start\n};\n\nfunction getCacheStore(): CacheStore {\n if (typeof window !== \"undefined\") {\n if (!(window as any)[CACHE_KEY]) {\n (window as any)[CACHE_KEY] = {\n data: new Map<string, CacheEntry>(),\n index: new Map<string, Set<string>>(),\n lru: [],\n };\n }\n return (window as any)[CACHE_KEY];\n }\n // Fallback for SSR (though this shouldn't be used on the client)\n return {\n data: new Map<string, CacheEntry>(),\n index: new Map<string, Set<string>>(),\n lru: [],\n };\n}\n\nconst cacheStore = getCacheStore();\nconst dataCache = cacheStore.data;\nconst pathIndex = cacheStore.index;\nconst lru = cacheStore.lru;\n\n// Helper functions for cache management\n\n/**\n * Extract base path from a cache key (removes query params)\n */\nfunction extractPathBase(key: string): string {\n return key.split(\"?\")[0];\n}\n\n/**\n * Add key to path index\n */\nfunction addToIndex(key: string): void {\n const pathBase = extractPathBase(key);\n if (!pathIndex.has(pathBase)) {\n pathIndex.set(pathBase, new Set());\n }\n pathIndex.get(pathBase)!.add(key);\n}\n\n/**\n * Remove key from path index\n */\nfunction removeFromIndex(key: string): void {\n const pathBase = extractPathBase(key);\n const keys = pathIndex.get(pathBase);\n if (keys) {\n keys.delete(key);\n if (keys.size === 0) {\n pathIndex.delete(pathBase);\n }\n }\n}\n\n/**\n * Update LRU order - move key to end (most recent)\n */\nfunction updateLRU(key: string): void {\n const index = lru.indexOf(key);\n if (index !== -1) {\n lru.splice(index, 1);\n }\n lru.push(key);\n}\n\n/**\n * Remove oldest entries if cache exceeds MAX_CACHE_SIZE\n */\nfunction evictOldest(): void {\n while (lru.length >= MAX_CACHE_SIZE && lru.length > 0) {\n const oldestKey = lru.shift()!;\n dataCache.delete(oldestKey);\n removeFromIndex(oldestKey);\n }\n}\n\n/**\n * Set cache entry and maintain indexes\n */\nfunction setCacheEntry(key: string, entry: CacheEntry): void {\n const existingEntry = dataCache.get(key);\n const wasFulfilled = existingEntry?.status === \"fulfilled\";\n \n dataCache.set(key, entry);\n \n // Only track fulfilled entries in LRU and index (not pending/rejected)\n if (entry.status === \"fulfilled\") {\n // Add to index if it wasn't already fulfilled (new entry or transition from pending/rejected)\n if (!wasFulfilled) {\n addToIndex(key);\n }\n updateLRU(key);\n evictOldest();\n } else if (wasFulfilled) {\n // If entry was fulfilled and now isn't (transitioning to pending/rejected), remove from index\n removeFromIndex(key);\n }\n}\n\n/**\n * Delete cache entry and clean up indexes\n */\nfunction deleteCacheEntry(key: string): void {\n if (dataCache.has(key)) {\n dataCache.delete(key);\n removeFromIndex(key);\n const lruIndex = lru.indexOf(key);\n if (lruIndex !== -1) {\n lru.splice(lruIndex, 1);\n }\n }\n}\n\nfunction buildDataUrl(url: string): string {\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + \"__fw_data=1\";\n}\n\nasync function fetchRouteDataOnce(url: string): Promise<RouteData> {\n const dataUrl = buildDataUrl(url);\n\n const res = await fetch(dataUrl, {\n headers: {\n \"x-fw-data\": \"1\",\n Accept: \"application/json\",\n },\n });\n\n let json: any = {};\n\n try {\n const text = await res.text();\n if (text) {\n json = JSON.parse(text);\n }\n } catch (parseError) {\n console.error(\n \"[client][cache] Failed to parse response as JSON:\",\n parseError\n );\n }\n\n const result: RouteData = {\n ok: res.ok,\n status: res.status,\n json,\n };\n\n return result;\n}\n\n/**\n * Revalidates route data by removing it from the cache.\n * The next time you navigate to this route, fresh data will be fetched from the server.\n * This is a client-side function and does not require a server-side revalidation.\n *\n * @param path - The route path to revalidate (e.g., '/posts/1' or '/posts/1?page=2')\n * If query params are not included, revalidates all variants of that route.\n *\n * @example\n * ```ts\n * // After saving something to the DB, revalidate the route\n * await saveToDatabase(data);\n * revalidatePath('/posts');\n * \n * // Revalidate a specific route with query params\n * revalidatePath('/posts?page=2');\n * ```\n */\nexport function revalidatePath(path: string): void {\n // Normalize the base path (without query params)\n const normalizedPath = path.split(\"?\")[0];\n const hasQueryParams = path.includes(\"?\");\n \n // Get all keys for this path base from index (O(1) lookup)\n const keysForPath = pathIndex.get(normalizedPath);\n \n if (!keysForPath || keysForPath.size === 0) {\n return; // No entries to revalidate\n }\n \n // If the path includes specific query params, extract them\n let specificQueryParams: string | undefined;\n if (hasQueryParams) {\n const queryPart = path.split(\"?\")[1];\n // Sort query params for consistent comparison\n specificQueryParams = queryPart\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n }\n \n // Iterate only over keys for this path (much smaller set)\n const keysToDelete: string[] = [];\n for (const key of keysForPath) {\n // If specific query params were specified, check if they match\n if (hasQueryParams && specificQueryParams) {\n const [, keyQuery = \"\"] = key.split(\"?\");\n const keyQueryParams = keyQuery\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n \n if (keyQueryParams === specificQueryParams) {\n keysToDelete.push(key);\n }\n } else {\n // If no specific query params, revalidate all variants\n keysToDelete.push(key);\n }\n }\n \n // Delete matching entries\n keysToDelete.forEach((key) => {\n deleteCacheEntry(key);\n });\n \n // If the revalidated path matches the current route, automatically refresh data\n if (typeof window !== \"undefined\") {\n const currentPathname = window.location.pathname;\n const currentSearch = window.location.search;\n const matchesCurrentPath = normalizedPath === currentPathname;\n \n if (matchesCurrentPath) {\n if (hasQueryParams && specificQueryParams) {\n const currentQueryParams = currentSearch\n .replace(\"?\", \"\")\n .split(\"&\")\n .filter((p) => !p.startsWith(\"__fw_data=\"))\n .sort()\n .join(\"&\");\n \n if (currentQueryParams === specificQueryParams) {\n revalidate().catch((err) => {\n console.error(\n \"[client][cache] Error revalidating current route:\",\n err\n );\n });\n }\n } else {\n revalidate().catch((err) => {\n console.error(\n \"[client][cache] Error revalidating current route:\",\n err\n );\n });\n }\n }\n }\n}\n\n/**\n * Revalidates and refreshes the current page data.\n * Similar to Next.js's `router.refresh()`.\n * \n * This function:\n * 1. Removes the current route from cache\n * 2. Fetches fresh data from the server\n * 3. Updates window.__FW_DATA__ with the new data\n * 4. Dispatches a 'fw-data-refresh' event for components to listen to\n * \n * @returns Promise that resolves with the fresh route data\n * \n * @example\n * ```ts\n * // Refresh current page data after a mutation\n * await revalidate();\n * ```\n */\nexport async function revalidate(): Promise<RouteData> {\n if (typeof window === \"undefined\") {\n throw new Error(\"revalidate() can only be called on the client\");\n }\n\n const pathname = window.location.pathname + window.location.search;\n \n // Revalidate the path (remove from cache)\n revalidatePath(pathname);\n \n // Fetch fresh data\n const freshData = await getRouteData(pathname, { revalidate: true });\n \n // Update window.__FW_DATA__ if it exists\n if ((window as any).__FW_DATA__ && freshData.ok && freshData.json) {\n const currentData = (window as any).__FW_DATA__;\n (window as any).__FW_DATA__ = {\n ...currentData,\n pathname: pathname.split(\"?\")[0],\n params: freshData.json.params || currentData.params || {},\n props: freshData.json.props || currentData.props || {},\n metadata: freshData.json.metadata ?? currentData.metadata ?? null,\n notFound: freshData.json.notFound ?? false,\n error: freshData.json.error ?? false,\n };\n \n // Dispatch event for components to listen to\n window.dispatchEvent(new CustomEvent(\"fw-data-refresh\", {\n detail: { data: freshData },\n }));\n }\n \n return freshData;\n}\n\n/**\n * @deprecated Use `revalidatePath()` instead. This function is kept for backwards compatibility.\n */\nexport function revalidateRouteData(url: string): void {\n revalidatePath(url);\n}\n\nexport function prefetchRouteData(url: string): void {\n const key = buildDataUrl(url);\n\n const cached = dataCache.get(key);\n\n if (cached && cached.status !== \"rejected\") {\n // Update LRU if it exists and is fulfilled\n if (cached.status === \"fulfilled\") {\n updateLRU(key);\n }\n return;\n }\n\n const promise = fetchRouteDataOnce(url)\n .then((value) => {\n setCacheEntry(key, { status: \"fulfilled\", value });\n return value;\n })\n .catch((error) => {\n console.error(\"[client][cache] Error prefetching route data:\", error);\n dataCache.set(key, { status: \"rejected\", error });\n throw error;\n });\n\n dataCache.set(key, { status: \"pending\", promise });\n}\n\nexport type GetRouteDataOptions = {\n /**\n * If true, forces revalidation of route data,\n * ignoring the cache and fetching fresh data from the server.\n * Similar to Next.js's `router.refresh()` behavior.\n */\n revalidate?: boolean;\n};\n\nexport async function getRouteData(\n url: string,\n options?: GetRouteDataOptions\n): Promise<RouteData> {\n const key = buildDataUrl(url);\n\n // If revalidation is requested, remove the entry from cache\n if (options?.revalidate) {\n deleteCacheEntry(key);\n }\n\n const entry = dataCache.get(key);\n\n if (entry) {\n if (entry.status === \"fulfilled\") {\n // Update LRU: mark as recently used\n updateLRU(key);\n return entry.value;\n }\n if (entry.status === \"pending\") {\n return entry.promise;\n }\n }\n\n // No entry in cache, fetch it\n const promise = fetchRouteDataOnce(url)\n .then((value) => {\n setCacheEntry(key, { status: \"fulfilled\", value });\n return value;\n })\n .catch((error) => {\n console.error(\"[client][cache] Error fetching route data:\", error);\n dataCache.set(key, { status: \"rejected\", error });\n throw error;\n });\n\n dataCache.set(key, { status: \"pending\", promise });\n return promise;\n}\n","import React from \"react\";\r\nimport { prefetchRouteData } from \"../../cache/index\";\r\n\r\ninterface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\r\n href: string;\r\n children: React.ReactNode;\r\n prefetch?: boolean;\r\n}\r\n\r\nimport { useEffect, useRef } from \"react\"; \r\n\r\nfunction isExternal(href: string) {\r\n try {\r\n const url = new URL(href, window.location.href);\r\n return url.origin !== window.location.origin;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nexport function Link({\r\n href,\r\n prefetch = true,\r\n children,\r\n ...rest\r\n}: React.PropsWithChildren<{\r\n href: string;\r\n prefetch?: boolean;\r\n}> &\r\n React.AnchorHTMLAttributes<HTMLAnchorElement>) {\r\n const ref = useRef<HTMLAnchorElement | null>(null);\r\n\r\n useEffect(() => {\r\n if (!prefetch || !ref.current || isExternal(href)) return;\r\n\r\n const el = ref.current;\r\n let prefetched = false;\r\n\r\n const io = new IntersectionObserver(([entry]) => {\r\n if (entry.isIntersecting && !prefetched) {\r\n prefetched = true;\r\n prefetchRouteData(href);\r\n }\r\n });\r\n\r\n io.observe(el);\r\n return () => io.disconnect();\r\n }, [href, prefetch]);\r\n\r\n return (\r\n <a ref={ref} href={href} {...rest}>\r\n {children}\r\n </a>\r\n );\r\n}\r\n"],"mappings":"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","names":["jsx"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../modules/react/hooks/index.ts","../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/useRouter/index.ts","../../modules/runtime/client/RouterContext.tsx","../../modules/runtime/client/constants.ts","../../modules/runtime/client/window-data.ts"],"sourcesContent":["export { useBroadcastChannel } from \"./useBroadcastChannel\";\r\nexport { useRouter } from \"./useRouter\";\r\nexport type { Router } from \"./useRouter\";\r\n","import React, { useEffect, useState, useRef, useCallback } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channelRef = useRef<BroadcastChannel | null>(null);\r\n\r\n useEffect(() => {\r\n // Create channel only once, inside useEffect\r\n if (!channelRef.current && typeof window !== \"undefined\") {\r\n channelRef.current = new BroadcastChannel(channelName);\r\n }\r\n\r\n const channel = channelRef.current;\r\n if (!channel) return;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n if (channelRef.current) {\r\n channelRef.current.close();\r\n channelRef.current = null;\r\n }\r\n };\r\n }, [channelName]);\r\n\r\n const sendMessage = useCallback((msg: unknown) => {\r\n if (channelRef.current) {\r\n channelRef.current.postMessage(msg);\r\n }\r\n }, []);\r\n\r\n return { message, sendMessage };\r\n};\r\n","import { useState, useEffect, useCallback, useContext, useRef } from \"react\";\r\nimport { RouterContext } from \"../../../runtime/client/RouterContext\";\r\nimport { getWindowData, getRouterData } from \"../../../runtime/client/window-data\";\r\nimport { ROUTER_NAVIGATE_KEY } from \"../../../runtime/client/constants\";\r\n\r\nexport interface Router {\r\n /**\r\n * Navigate to a new route.\r\n * @param url - The URL to navigate to (e.g., \"/about\" or \"/blog/[slug]\" with params)\r\n * @param options - Navigation options\r\n */\r\n push: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Replace the current route without adding to history.\r\n * @param url - The URL to navigate to\r\n * @param options - Navigation options\r\n */\r\n replace: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Go back in the browser history.\r\n */\r\n back: () => void;\r\n \r\n /**\r\n * Refresh the current route data by revalidating.\r\n */\r\n refresh: () => Promise<void>;\r\n \r\n /**\r\n * Current pathname (e.g., \"/blog/my-post\")\r\n */\r\n pathname: string;\r\n \r\n /**\r\n * Query parameters from the URL (e.g., ?id=123&name=test)\r\n * Alias for searchParams for backward compatibility\r\n */\r\n query: Record<string, string>;\r\n \r\n /**\r\n * Search parameters from the URL (e.g., ?id=123&name=test)\r\n */\r\n searchParams: Record<string, unknown>;\r\n \r\n /**\r\n * Dynamic route parameters (e.g., { slug: \"my-post\" } for /blog/[slug])\r\n */\r\n params: Record<string, string>;\r\n}\r\n\r\n/**\r\n * Hook to access router functionality and current route information.\r\n * \r\n * Provides methods to navigate programmatically and access current route data.\r\n * \r\n * @returns Router object with navigation methods and route information\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const router = useRouter();\r\n * \r\n * const handleClick = () => {\r\n * router.push(\"/about\");\r\n * };\r\n * \r\n * return (\r\n * <div>\r\n * <p>Current path: {router.pathname}</p>\r\n * <p>Params: {JSON.stringify(router.params)}</p>\r\n * <button onClick={handleClick}>Go to About</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```tsx\r\n * // Navigate with dynamic params\r\n * router.push(\"/blog/my-post\");\r\n * \r\n * // Replace current route\r\n * router.replace(\"/login\");\r\n * \r\n * // Refresh current route data\r\n * await router.refresh();\r\n * ```\r\n */\r\nexport function useRouter(): Router {\r\n // Try to get context, but don't throw if it's not available (SSR)\r\n const context = useContext(RouterContext);\r\n const navigate = context?.navigate;\r\n \r\n // Use a ref to store navigate so we can access it in callbacks even if context updates\r\n // Initialize with current navigate value\r\n const navigateRef = useRef(navigate);\r\n \r\n // Update ref when navigate changes (this ensures we always have the latest value)\r\n useEffect(() => {\r\n navigateRef.current = navigate;\r\n }, [navigate]);\r\n \r\n const [routeData, setRouteData] = useState(() => {\r\n // During SSR, return empty/default values\r\n if (typeof window === \"undefined\") {\r\n return {\r\n pathname: \"\",\r\n query: {},\r\n searchParams: {},\r\n params: {},\r\n };\r\n }\r\n \r\n // On client, get data from window\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n \r\n // Parse search params from URL if routerData is not available\r\n const searchParams = routerData?.searchParams || parseQueryString(window.location.search);\r\n \r\n return {\r\n pathname: routerData?.pathname || data?.pathname || window.location.pathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n };\r\n });\r\n\r\n // Listen for route changes (only on client)\r\n useEffect(() => {\r\n if (typeof window === \"undefined\") return;\r\n \r\n const handleDataRefresh = () => {\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n \r\n const searchParams = routerData?.searchParams || parseQueryString(currentSearch);\r\n \r\n setRouteData({\r\n pathname: routerData?.pathname || data?.pathname || currentPathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n });\r\n };\r\n\r\n // Listen for navigation events\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.addEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n \r\n // Also listen for popstate (browser back/forward)\r\n const handlePopState = () => {\r\n handleDataRefresh();\r\n };\r\n window.addEventListener(\"popstate\", handlePopState);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"popstate\", handlePopState);\r\n };\r\n }, []);\r\n\r\n const push = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n /**\r\n * SOLUTION: Multi-source navigate function resolution\r\n * \r\n * During React hydration, RouterContext may not be available immediately.\r\n * We try three sources in order:\r\n * 1. navigateRef.current - Most up-to-date, updated via useEffect\r\n * 2. navigate from context - Direct context access\r\n * 3. window.__LOLY_ROUTER_NAVIGATE__ - Global fallback exposed by AppShell\r\n * \r\n * This ensures SPA navigation works even during hydration timing issues.\r\n */\r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return; // SSR\r\n }\r\n \r\n // Wait for context during hydration (up to 100ms)\r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n // Final fallback: full page reload if navigate is still unavailable\r\n if (!currentNavigate) {\r\n window.location.href = fullUrl;\r\n return;\r\n }\r\n \r\n // Check if we're already on this URL (same as link handler)\r\n const currentUrl = window.location.pathname + window.location.search;\r\n if (fullUrl === currentUrl) {\r\n return; // Already on this route, no need to navigate\r\n }\r\n \r\n // Update URL in browser history (same as link handler does)\r\n // This is done BEFORE navigation to match link behavior\r\n window.history.pushState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation (same as link handler)\r\n // If navigation fails, navigate() will handle the reload internally\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate] // Include navigate in dependencies so it updates when context becomes available\r\n );\r\n\r\n const replace = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.replace(fullUrl);\r\n return;\r\n }\r\n \r\n // Update URL in browser history using replace (doesn't add to history)\r\n window.history.replaceState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const back = useCallback(() => {\r\n if (typeof window !== \"undefined\") {\r\n window.history.back();\r\n }\r\n }, []);\r\n\r\n const refresh = useCallback(async () => {\r\n const currentUrl = typeof window !== \"undefined\" \r\n ? window.location.pathname + window.location.search \r\n : routeData.pathname;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.reload();\r\n return;\r\n }\r\n \r\n await currentNavigate(currentUrl, { revalidate: true });\r\n }, [navigate, routeData.pathname]);\r\n\r\n return {\r\n push,\r\n replace,\r\n back,\r\n refresh,\r\n pathname: routeData.pathname,\r\n query: routeData.query,\r\n searchParams: routeData.searchParams,\r\n params: routeData.params,\r\n };\r\n}\r\n\r\n/**\r\n * Parse query string into an object.\r\n * @param search - Query string (e.g., \"?id=123&name=test\")\r\n * @returns Object with query parameters\r\n */\r\nfunction parseQueryString(search: string): Record<string, string> {\r\n const params: Record<string, string> = {};\r\n if (!search || search.length === 0) return params;\r\n \r\n const queryString = search.startsWith(\"?\") ? search.slice(1) : search;\r\n const pairs = queryString.split(\"&\");\r\n \r\n for (const pair of pairs) {\r\n const [key, value] = pair.split(\"=\");\r\n if (key) {\r\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\r\n }\r\n }\r\n \r\n return params;\r\n}\r\n","import { createContext, useContext } from \"react\";\r\n\r\nexport type NavigateFunction = (\r\n url: string,\r\n options?: { revalidate?: boolean; replace?: boolean }\r\n) => Promise<void>;\r\n\r\nexport interface RouterContextValue {\r\n navigate: NavigateFunction;\r\n}\r\n\r\nexport const RouterContext = createContext<RouterContextValue | null>(null);\r\n\r\nexport function useRouterContext(): RouterContextValue {\r\n const context = useContext(RouterContext);\r\n if (!context) {\r\n throw new Error(\r\n \"useRouter must be used within a RouterProvider. Make sure you're using it inside a Loly app.\"\r\n );\r\n }\r\n return context;\r\n}\r\n","// Client-side constants (hardcoded to avoid alias resolution issues in Rspack)\r\nexport const WINDOW_DATA_KEY = \"__FW_DATA__\";\r\nexport const ROUTER_DATA_KEY = \"__LOLY_ROUTER_DATA__\";\r\nexport const APP_CONTAINER_ID = \"__app\";\r\n// Global key for navigate function fallback (exposed by AppShell for hydration timing issues)\r\nexport const ROUTER_NAVIGATE_KEY = \"__LOLY_ROUTER_NAVIGATE__\";\r\n\r\n","import { WINDOW_DATA_KEY, ROUTER_DATA_KEY } from \"./constants\";\r\nimport type { InitialData, RouterData } from \"./types\";\r\n\r\nexport function getWindowData(): InitialData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return ((window as any)[WINDOW_DATA_KEY] as InitialData | undefined) ?? null;\r\n}\r\n\r\nexport function getRouterData(): RouterData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return ((window as any)[ROUTER_DATA_KEY] as RouterData | undefined) ?? null;\r\n}\r\n\r\nexport function setWindowData(data: InitialData): void {\r\n (window as any)[WINDOW_DATA_KEY] = data;\r\n \r\n // Dispatch event for components to listen to (e.g. ThemeProvider)\r\n // This ensures components update when navigating in SPA mode\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function setRouterData(data: RouterData): void {\r\n (window as any)[ROUTER_DATA_KEY] = data;\r\n \r\n // Dispatch event for router data updates\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-router-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function getCurrentTheme(): string | null {\r\n return getWindowData()?.theme ?? null;\r\n}\r\n\r\n"],"mappings":"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","names":["import_react","import_react"]}
1
+ {"version":3,"sources":["../../modules/react/hooks/index.ts","../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/useRouter/index.ts","../../modules/runtime/client/RouterContext.tsx","../../modules/runtime/client/constants.ts","../../modules/runtime/client/window-data.ts"],"sourcesContent":["export { useBroadcastChannel } from \"./useBroadcastChannel\";\r\nexport { useRouter } from \"./useRouter\";\r\nexport type { Router } from \"./useRouter\";\r\n","import React, { useEffect, useState, useRef, useCallback } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channelRef = useRef<BroadcastChannel | null>(null);\r\n\r\n useEffect(() => {\r\n // Create channel only once, inside useEffect\r\n if (!channelRef.current && typeof window !== \"undefined\") {\r\n channelRef.current = new BroadcastChannel(channelName);\r\n }\r\n\r\n const channel = channelRef.current;\r\n if (!channel) return;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n if (channelRef.current) {\r\n channelRef.current.close();\r\n channelRef.current = null;\r\n }\r\n };\r\n }, [channelName]);\r\n\r\n const sendMessage = useCallback((msg: unknown) => {\r\n if (channelRef.current) {\r\n channelRef.current.postMessage(msg);\r\n }\r\n }, []);\r\n\r\n return { message, sendMessage };\r\n};\r\n","import { useState, useEffect, useCallback, useContext, useRef } from \"react\";\r\nimport { RouterContext } from \"../../../runtime/client/RouterContext\";\r\nimport { getWindowData, getRouterData } from \"../../../runtime/client/window-data\";\r\nimport { ROUTER_NAVIGATE_KEY } from \"../../../runtime/client/constants\";\r\n\r\nexport interface Router {\r\n /**\r\n * Navigate to a new route.\r\n * @param url - The URL to navigate to (e.g., \"/about\" or \"/blog/[slug]\" with params)\r\n * @param options - Navigation options\r\n */\r\n push: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Replace the current route without adding to history.\r\n * @param url - The URL to navigate to\r\n * @param options - Navigation options\r\n */\r\n replace: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Go back in the browser history.\r\n */\r\n back: () => void;\r\n \r\n /**\r\n * Refresh the current route data by revalidating.\r\n */\r\n refresh: () => Promise<void>;\r\n \r\n /**\r\n * Current pathname (e.g., \"/blog/my-post\")\r\n */\r\n pathname: string;\r\n \r\n /**\r\n * Query parameters from the URL (e.g., ?id=123&name=test)\r\n * Alias for searchParams for backward compatibility\r\n */\r\n query: Record<string, string>;\r\n \r\n /**\r\n * Search parameters from the URL (e.g., ?id=123&name=test)\r\n */\r\n searchParams: Record<string, unknown>;\r\n \r\n /**\r\n * Dynamic route parameters (e.g., { slug: \"my-post\" } for /blog/[slug])\r\n */\r\n params: Record<string, string>;\r\n}\r\n\r\n/**\r\n * Hook to access router functionality and current route information.\r\n * \r\n * Provides methods to navigate programmatically and access current route data.\r\n * \r\n * @returns Router object with navigation methods and route information\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const router = useRouter();\r\n * \r\n * const handleClick = () => {\r\n * router.push(\"/about\");\r\n * };\r\n * \r\n * return (\r\n * <div>\r\n * <p>Current path: {router.pathname}</p>\r\n * <p>Params: {JSON.stringify(router.params)}</p>\r\n * <button onClick={handleClick}>Go to About</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```tsx\r\n * // Navigate with dynamic params\r\n * router.push(\"/blog/my-post\");\r\n * \r\n * // Replace current route\r\n * router.replace(\"/login\");\r\n * \r\n * // Refresh current route data\r\n * await router.refresh();\r\n * ```\r\n */\r\nexport function useRouter(): Router {\r\n // Try to get context, but don't throw if it's not available (SSR)\r\n const context = useContext(RouterContext);\r\n const navigate = context?.navigate;\r\n \r\n // Use a ref to store navigate so we can access it in callbacks even if context updates\r\n // Initialize with current navigate value\r\n const navigateRef = useRef(navigate);\r\n \r\n // Update ref when navigate changes (this ensures we always have the latest value)\r\n useEffect(() => {\r\n navigateRef.current = navigate;\r\n }, [navigate]);\r\n \r\n const [routeData, setRouteData] = useState(() => {\r\n // During SSR, return empty/default values\r\n if (typeof window === \"undefined\") {\r\n return {\r\n pathname: \"\",\r\n query: {},\r\n searchParams: {},\r\n params: {},\r\n };\r\n }\r\n \r\n // On client, get data from window\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n \r\n // Parse search params from URL if routerData is not available\r\n const searchParams = routerData?.searchParams || parseQueryString(window.location.search);\r\n \r\n return {\r\n pathname: routerData?.pathname || data?.pathname || window.location.pathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n };\r\n });\r\n\r\n // Listen for route changes (only on client)\r\n useEffect(() => {\r\n if (typeof window === \"undefined\") return;\r\n \r\n const handleDataRefresh = () => {\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n \r\n const searchParams = routerData?.searchParams || parseQueryString(currentSearch);\r\n \r\n setRouteData({\r\n pathname: routerData?.pathname || data?.pathname || currentPathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n });\r\n };\r\n\r\n // Listen for navigation events\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.addEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n \r\n // Also listen for popstate (browser back/forward)\r\n const handlePopState = () => {\r\n handleDataRefresh();\r\n };\r\n window.addEventListener(\"popstate\", handlePopState);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"popstate\", handlePopState);\r\n };\r\n }, []);\r\n\r\n const push = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n /**\r\n * SOLUTION: Multi-source navigate function resolution\r\n * \r\n * During React hydration, RouterContext may not be available immediately.\r\n * We try three sources in order:\r\n * 1. navigateRef.current - Most up-to-date, updated via useEffect\r\n * 2. navigate from context - Direct context access\r\n * 3. window.__LOLY_ROUTER_NAVIGATE__ - Global fallback exposed by AppShell\r\n * \r\n * This ensures SPA navigation works even during hydration timing issues.\r\n */\r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return; // SSR\r\n }\r\n \r\n // Wait for context during hydration (up to 100ms)\r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n // Final fallback: full page reload if navigate is still unavailable\r\n if (!currentNavigate) {\r\n window.location.href = fullUrl;\r\n return;\r\n }\r\n \r\n // Check if we're already on this URL (same as link handler)\r\n const currentUrl = window.location.pathname + window.location.search;\r\n if (fullUrl === currentUrl) {\r\n return; // Already on this route, no need to navigate\r\n }\r\n \r\n // Update URL in browser history (same as link handler does)\r\n // This is done BEFORE navigation to match link behavior\r\n window.history.pushState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation (same as link handler)\r\n // If navigation fails, navigate() will handle the reload internally\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate] // Include navigate in dependencies so it updates when context becomes available\r\n );\r\n\r\n const replace = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.replace(fullUrl);\r\n return;\r\n }\r\n \r\n // Update URL in browser history using replace (doesn't add to history)\r\n window.history.replaceState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const back = useCallback(() => {\r\n if (typeof window !== \"undefined\") {\r\n window.history.back();\r\n }\r\n }, []);\r\n\r\n const refresh = useCallback(async () => {\r\n const currentUrl = typeof window !== \"undefined\" \r\n ? window.location.pathname + window.location.search \r\n : routeData.pathname;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.reload();\r\n return;\r\n }\r\n \r\n await currentNavigate(currentUrl, { revalidate: true });\r\n }, [navigate, routeData.pathname]);\r\n\r\n return {\r\n push,\r\n replace,\r\n back,\r\n refresh,\r\n pathname: routeData.pathname,\r\n query: routeData.query,\r\n searchParams: routeData.searchParams,\r\n params: routeData.params,\r\n };\r\n}\r\n\r\n/**\r\n * Parse query string into an object.\r\n * @param search - Query string (e.g., \"?id=123&name=test\")\r\n * @returns Object with query parameters\r\n */\r\nfunction parseQueryString(search: string): Record<string, string> {\r\n const params: Record<string, string> = {};\r\n if (!search || search.length === 0) return params;\r\n \r\n const queryString = search.startsWith(\"?\") ? search.slice(1) : search;\r\n const pairs = queryString.split(\"&\");\r\n \r\n for (const pair of pairs) {\r\n const [key, value] = pair.split(\"=\");\r\n if (key) {\r\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\r\n }\r\n }\r\n \r\n return params;\r\n}\r\n","import { createContext, useContext } from \"react\";\r\n\r\nexport type NavigateFunction = (\r\n url: string,\r\n options?: { revalidate?: boolean; replace?: boolean }\r\n) => Promise<void>;\r\n\r\nexport interface RouterContextValue {\r\n navigate: NavigateFunction;\r\n}\r\n\r\nexport const RouterContext = createContext<RouterContextValue | null>(null);\r\n\r\nexport function useRouterContext(): RouterContextValue {\r\n const context = useContext(RouterContext);\r\n if (!context) {\r\n throw new Error(\r\n \"useRouter must be used within a RouterProvider. Make sure you're using it inside a Loly app.\"\r\n );\r\n }\r\n return context;\r\n}\r\n","// Client-side constants (hardcoded to avoid alias resolution issues in Rspack)\r\nexport const WINDOW_DATA_KEY = \"__FW_DATA__\";\r\nexport const ROUTER_DATA_KEY = \"__LOLY_ROUTER_DATA__\";\r\nexport const APP_CONTAINER_ID = \"__app\";\r\n// Global key for navigate function fallback (exposed by AppShell for hydration timing issues)\r\nexport const ROUTER_NAVIGATE_KEY = \"__LOLY_ROUTER_NAVIGATE__\";\r\n\r\n","import { WINDOW_DATA_KEY, ROUTER_DATA_KEY } from \"./constants\";\r\nimport type { InitialData, RouterData } from \"./types\";\r\n\r\nexport function getWindowData(): InitialData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return (window[WINDOW_DATA_KEY] as InitialData | undefined) ?? null;\r\n}\r\n\r\nexport function getRouterData(): RouterData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return (window[ROUTER_DATA_KEY] as RouterData | undefined) ?? null;\r\n}\r\n\r\nexport function setWindowData(data: InitialData): void {\r\n window[WINDOW_DATA_KEY] = data;\r\n \r\n // Dispatch event for components to listen to (e.g. ThemeProvider)\r\n // This ensures components update when navigating in SPA mode\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function setRouterData(data: RouterData): void {\r\n window[ROUTER_DATA_KEY] = data;\r\n \r\n // Dispatch event for router data updates\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-router-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function getCurrentTheme(): string | null {\r\n return getWindowData()?.theme ?? null;\r\n}\r\n\r\n"],"mappings":"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","names":["import_react","import_react"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/useRouter/index.ts","../../modules/runtime/client/RouterContext.tsx","../../modules/runtime/client/constants.ts","../../modules/runtime/client/window-data.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channelRef = useRef<BroadcastChannel | null>(null);\r\n\r\n useEffect(() => {\r\n // Create channel only once, inside useEffect\r\n if (!channelRef.current && typeof window !== \"undefined\") {\r\n channelRef.current = new BroadcastChannel(channelName);\r\n }\r\n\r\n const channel = channelRef.current;\r\n if (!channel) return;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n if (channelRef.current) {\r\n channelRef.current.close();\r\n channelRef.current = null;\r\n }\r\n };\r\n }, [channelName]);\r\n\r\n const sendMessage = useCallback((msg: unknown) => {\r\n if (channelRef.current) {\r\n channelRef.current.postMessage(msg);\r\n }\r\n }, []);\r\n\r\n return { message, sendMessage };\r\n};\r\n","import { useState, useEffect, useCallback, useContext, useRef } from \"react\";\r\nimport { RouterContext } from \"../../../runtime/client/RouterContext\";\r\nimport { getWindowData, getRouterData } from \"../../../runtime/client/window-data\";\r\nimport { ROUTER_NAVIGATE_KEY } from \"../../../runtime/client/constants\";\r\n\r\nexport interface Router {\r\n /**\r\n * Navigate to a new route.\r\n * @param url - The URL to navigate to (e.g., \"/about\" or \"/blog/[slug]\" with params)\r\n * @param options - Navigation options\r\n */\r\n push: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Replace the current route without adding to history.\r\n * @param url - The URL to navigate to\r\n * @param options - Navigation options\r\n */\r\n replace: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Go back in the browser history.\r\n */\r\n back: () => void;\r\n \r\n /**\r\n * Refresh the current route data by revalidating.\r\n */\r\n refresh: () => Promise<void>;\r\n \r\n /**\r\n * Current pathname (e.g., \"/blog/my-post\")\r\n */\r\n pathname: string;\r\n \r\n /**\r\n * Query parameters from the URL (e.g., ?id=123&name=test)\r\n * Alias for searchParams for backward compatibility\r\n */\r\n query: Record<string, string>;\r\n \r\n /**\r\n * Search parameters from the URL (e.g., ?id=123&name=test)\r\n */\r\n searchParams: Record<string, unknown>;\r\n \r\n /**\r\n * Dynamic route parameters (e.g., { slug: \"my-post\" } for /blog/[slug])\r\n */\r\n params: Record<string, string>;\r\n}\r\n\r\n/**\r\n * Hook to access router functionality and current route information.\r\n * \r\n * Provides methods to navigate programmatically and access current route data.\r\n * \r\n * @returns Router object with navigation methods and route information\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const router = useRouter();\r\n * \r\n * const handleClick = () => {\r\n * router.push(\"/about\");\r\n * };\r\n * \r\n * return (\r\n * <div>\r\n * <p>Current path: {router.pathname}</p>\r\n * <p>Params: {JSON.stringify(router.params)}</p>\r\n * <button onClick={handleClick}>Go to About</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```tsx\r\n * // Navigate with dynamic params\r\n * router.push(\"/blog/my-post\");\r\n * \r\n * // Replace current route\r\n * router.replace(\"/login\");\r\n * \r\n * // Refresh current route data\r\n * await router.refresh();\r\n * ```\r\n */\r\nexport function useRouter(): Router {\r\n // Try to get context, but don't throw if it's not available (SSR)\r\n const context = useContext(RouterContext);\r\n const navigate = context?.navigate;\r\n \r\n // Use a ref to store navigate so we can access it in callbacks even if context updates\r\n // Initialize with current navigate value\r\n const navigateRef = useRef(navigate);\r\n \r\n // Update ref when navigate changes (this ensures we always have the latest value)\r\n useEffect(() => {\r\n navigateRef.current = navigate;\r\n }, [navigate]);\r\n \r\n const [routeData, setRouteData] = useState(() => {\r\n // During SSR, return empty/default values\r\n if (typeof window === \"undefined\") {\r\n return {\r\n pathname: \"\",\r\n query: {},\r\n searchParams: {},\r\n params: {},\r\n };\r\n }\r\n \r\n // On client, get data from window\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n \r\n // Parse search params from URL if routerData is not available\r\n const searchParams = routerData?.searchParams || parseQueryString(window.location.search);\r\n \r\n return {\r\n pathname: routerData?.pathname || data?.pathname || window.location.pathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n };\r\n });\r\n\r\n // Listen for route changes (only on client)\r\n useEffect(() => {\r\n if (typeof window === \"undefined\") return;\r\n \r\n const handleDataRefresh = () => {\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n \r\n const searchParams = routerData?.searchParams || parseQueryString(currentSearch);\r\n \r\n setRouteData({\r\n pathname: routerData?.pathname || data?.pathname || currentPathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n });\r\n };\r\n\r\n // Listen for navigation events\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.addEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n \r\n // Also listen for popstate (browser back/forward)\r\n const handlePopState = () => {\r\n handleDataRefresh();\r\n };\r\n window.addEventListener(\"popstate\", handlePopState);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"popstate\", handlePopState);\r\n };\r\n }, []);\r\n\r\n const push = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n /**\r\n * SOLUTION: Multi-source navigate function resolution\r\n * \r\n * During React hydration, RouterContext may not be available immediately.\r\n * We try three sources in order:\r\n * 1. navigateRef.current - Most up-to-date, updated via useEffect\r\n * 2. navigate from context - Direct context access\r\n * 3. window.__LOLY_ROUTER_NAVIGATE__ - Global fallback exposed by AppShell\r\n * \r\n * This ensures SPA navigation works even during hydration timing issues.\r\n */\r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return; // SSR\r\n }\r\n \r\n // Wait for context during hydration (up to 100ms)\r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n // Final fallback: full page reload if navigate is still unavailable\r\n if (!currentNavigate) {\r\n window.location.href = fullUrl;\r\n return;\r\n }\r\n \r\n // Check if we're already on this URL (same as link handler)\r\n const currentUrl = window.location.pathname + window.location.search;\r\n if (fullUrl === currentUrl) {\r\n return; // Already on this route, no need to navigate\r\n }\r\n \r\n // Update URL in browser history (same as link handler does)\r\n // This is done BEFORE navigation to match link behavior\r\n window.history.pushState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation (same as link handler)\r\n // If navigation fails, navigate() will handle the reload internally\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate] // Include navigate in dependencies so it updates when context becomes available\r\n );\r\n\r\n const replace = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.replace(fullUrl);\r\n return;\r\n }\r\n \r\n // Update URL in browser history using replace (doesn't add to history)\r\n window.history.replaceState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const back = useCallback(() => {\r\n if (typeof window !== \"undefined\") {\r\n window.history.back();\r\n }\r\n }, []);\r\n\r\n const refresh = useCallback(async () => {\r\n const currentUrl = typeof window !== \"undefined\" \r\n ? window.location.pathname + window.location.search \r\n : routeData.pathname;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.reload();\r\n return;\r\n }\r\n \r\n await currentNavigate(currentUrl, { revalidate: true });\r\n }, [navigate, routeData.pathname]);\r\n\r\n return {\r\n push,\r\n replace,\r\n back,\r\n refresh,\r\n pathname: routeData.pathname,\r\n query: routeData.query,\r\n searchParams: routeData.searchParams,\r\n params: routeData.params,\r\n };\r\n}\r\n\r\n/**\r\n * Parse query string into an object.\r\n * @param search - Query string (e.g., \"?id=123&name=test\")\r\n * @returns Object with query parameters\r\n */\r\nfunction parseQueryString(search: string): Record<string, string> {\r\n const params: Record<string, string> = {};\r\n if (!search || search.length === 0) return params;\r\n \r\n const queryString = search.startsWith(\"?\") ? search.slice(1) : search;\r\n const pairs = queryString.split(\"&\");\r\n \r\n for (const pair of pairs) {\r\n const [key, value] = pair.split(\"=\");\r\n if (key) {\r\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\r\n }\r\n }\r\n \r\n return params;\r\n}\r\n","import { createContext, useContext } from \"react\";\r\n\r\nexport type NavigateFunction = (\r\n url: string,\r\n options?: { revalidate?: boolean; replace?: boolean }\r\n) => Promise<void>;\r\n\r\nexport interface RouterContextValue {\r\n navigate: NavigateFunction;\r\n}\r\n\r\nexport const RouterContext = createContext<RouterContextValue | null>(null);\r\n\r\nexport function useRouterContext(): RouterContextValue {\r\n const context = useContext(RouterContext);\r\n if (!context) {\r\n throw new Error(\r\n \"useRouter must be used within a RouterProvider. Make sure you're using it inside a Loly app.\"\r\n );\r\n }\r\n return context;\r\n}\r\n","// Client-side constants (hardcoded to avoid alias resolution issues in Rspack)\r\nexport const WINDOW_DATA_KEY = \"__FW_DATA__\";\r\nexport const ROUTER_DATA_KEY = \"__LOLY_ROUTER_DATA__\";\r\nexport const APP_CONTAINER_ID = \"__app\";\r\n// Global key for navigate function fallback (exposed by AppShell for hydration timing issues)\r\nexport const ROUTER_NAVIGATE_KEY = \"__LOLY_ROUTER_NAVIGATE__\";\r\n\r\n","import { WINDOW_DATA_KEY, ROUTER_DATA_KEY } from \"./constants\";\r\nimport type { InitialData, RouterData } from \"./types\";\r\n\r\nexport function getWindowData(): InitialData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return ((window as any)[WINDOW_DATA_KEY] as InitialData | undefined) ?? null;\r\n}\r\n\r\nexport function getRouterData(): RouterData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return ((window as any)[ROUTER_DATA_KEY] as RouterData | undefined) ?? null;\r\n}\r\n\r\nexport function setWindowData(data: InitialData): void {\r\n (window as any)[WINDOW_DATA_KEY] = data;\r\n \r\n // Dispatch event for components to listen to (e.g. ThemeProvider)\r\n // This ensures components update when navigating in SPA mode\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function setRouterData(data: RouterData): void {\r\n (window as any)[ROUTER_DATA_KEY] = data;\r\n \r\n // Dispatch event for router data updates\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-router-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function getCurrentTheme(): string | null {\r\n return getWindowData()?.theme ?? null;\r\n}\r\n\r\n"],"mappings":"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","names":["useState","useEffect","useCallback","useContext","useRef","useContext","useRef","useEffect","useState","useCallback"]}
1
+ {"version":3,"sources":["../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/useRouter/index.ts","../../modules/runtime/client/RouterContext.tsx","../../modules/runtime/client/constants.ts","../../modules/runtime/client/window-data.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channelRef = useRef<BroadcastChannel | null>(null);\r\n\r\n useEffect(() => {\r\n // Create channel only once, inside useEffect\r\n if (!channelRef.current && typeof window !== \"undefined\") {\r\n channelRef.current = new BroadcastChannel(channelName);\r\n }\r\n\r\n const channel = channelRef.current;\r\n if (!channel) return;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n if (channelRef.current) {\r\n channelRef.current.close();\r\n channelRef.current = null;\r\n }\r\n };\r\n }, [channelName]);\r\n\r\n const sendMessage = useCallback((msg: unknown) => {\r\n if (channelRef.current) {\r\n channelRef.current.postMessage(msg);\r\n }\r\n }, []);\r\n\r\n return { message, sendMessage };\r\n};\r\n","import { useState, useEffect, useCallback, useContext, useRef } from \"react\";\r\nimport { RouterContext } from \"../../../runtime/client/RouterContext\";\r\nimport { getWindowData, getRouterData } from \"../../../runtime/client/window-data\";\r\nimport { ROUTER_NAVIGATE_KEY } from \"../../../runtime/client/constants\";\r\n\r\nexport interface Router {\r\n /**\r\n * Navigate to a new route.\r\n * @param url - The URL to navigate to (e.g., \"/about\" or \"/blog/[slug]\" with params)\r\n * @param options - Navigation options\r\n */\r\n push: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Replace the current route without adding to history.\r\n * @param url - The URL to navigate to\r\n * @param options - Navigation options\r\n */\r\n replace: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Go back in the browser history.\r\n */\r\n back: () => void;\r\n \r\n /**\r\n * Refresh the current route data by revalidating.\r\n */\r\n refresh: () => Promise<void>;\r\n \r\n /**\r\n * Current pathname (e.g., \"/blog/my-post\")\r\n */\r\n pathname: string;\r\n \r\n /**\r\n * Query parameters from the URL (e.g., ?id=123&name=test)\r\n * Alias for searchParams for backward compatibility\r\n */\r\n query: Record<string, string>;\r\n \r\n /**\r\n * Search parameters from the URL (e.g., ?id=123&name=test)\r\n */\r\n searchParams: Record<string, unknown>;\r\n \r\n /**\r\n * Dynamic route parameters (e.g., { slug: \"my-post\" } for /blog/[slug])\r\n */\r\n params: Record<string, string>;\r\n}\r\n\r\n/**\r\n * Hook to access router functionality and current route information.\r\n * \r\n * Provides methods to navigate programmatically and access current route data.\r\n * \r\n * @returns Router object with navigation methods and route information\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const router = useRouter();\r\n * \r\n * const handleClick = () => {\r\n * router.push(\"/about\");\r\n * };\r\n * \r\n * return (\r\n * <div>\r\n * <p>Current path: {router.pathname}</p>\r\n * <p>Params: {JSON.stringify(router.params)}</p>\r\n * <button onClick={handleClick}>Go to About</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```tsx\r\n * // Navigate with dynamic params\r\n * router.push(\"/blog/my-post\");\r\n * \r\n * // Replace current route\r\n * router.replace(\"/login\");\r\n * \r\n * // Refresh current route data\r\n * await router.refresh();\r\n * ```\r\n */\r\nexport function useRouter(): Router {\r\n // Try to get context, but don't throw if it's not available (SSR)\r\n const context = useContext(RouterContext);\r\n const navigate = context?.navigate;\r\n \r\n // Use a ref to store navigate so we can access it in callbacks even if context updates\r\n // Initialize with current navigate value\r\n const navigateRef = useRef(navigate);\r\n \r\n // Update ref when navigate changes (this ensures we always have the latest value)\r\n useEffect(() => {\r\n navigateRef.current = navigate;\r\n }, [navigate]);\r\n \r\n const [routeData, setRouteData] = useState(() => {\r\n // During SSR, return empty/default values\r\n if (typeof window === \"undefined\") {\r\n return {\r\n pathname: \"\",\r\n query: {},\r\n searchParams: {},\r\n params: {},\r\n };\r\n }\r\n \r\n // On client, get data from window\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n \r\n // Parse search params from URL if routerData is not available\r\n const searchParams = routerData?.searchParams || parseQueryString(window.location.search);\r\n \r\n return {\r\n pathname: routerData?.pathname || data?.pathname || window.location.pathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n };\r\n });\r\n\r\n // Listen for route changes (only on client)\r\n useEffect(() => {\r\n if (typeof window === \"undefined\") return;\r\n \r\n const handleDataRefresh = () => {\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n \r\n const searchParams = routerData?.searchParams || parseQueryString(currentSearch);\r\n \r\n setRouteData({\r\n pathname: routerData?.pathname || data?.pathname || currentPathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n });\r\n };\r\n\r\n // Listen for navigation events\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.addEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n \r\n // Also listen for popstate (browser back/forward)\r\n const handlePopState = () => {\r\n handleDataRefresh();\r\n };\r\n window.addEventListener(\"popstate\", handlePopState);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"popstate\", handlePopState);\r\n };\r\n }, []);\r\n\r\n const push = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n /**\r\n * SOLUTION: Multi-source navigate function resolution\r\n * \r\n * During React hydration, RouterContext may not be available immediately.\r\n * We try three sources in order:\r\n * 1. navigateRef.current - Most up-to-date, updated via useEffect\r\n * 2. navigate from context - Direct context access\r\n * 3. window.__LOLY_ROUTER_NAVIGATE__ - Global fallback exposed by AppShell\r\n * \r\n * This ensures SPA navigation works even during hydration timing issues.\r\n */\r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return; // SSR\r\n }\r\n \r\n // Wait for context during hydration (up to 100ms)\r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n // Final fallback: full page reload if navigate is still unavailable\r\n if (!currentNavigate) {\r\n window.location.href = fullUrl;\r\n return;\r\n }\r\n \r\n // Check if we're already on this URL (same as link handler)\r\n const currentUrl = window.location.pathname + window.location.search;\r\n if (fullUrl === currentUrl) {\r\n return; // Already on this route, no need to navigate\r\n }\r\n \r\n // Update URL in browser history (same as link handler does)\r\n // This is done BEFORE navigation to match link behavior\r\n window.history.pushState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation (same as link handler)\r\n // If navigation fails, navigate() will handle the reload internally\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate] // Include navigate in dependencies so it updates when context becomes available\r\n );\r\n\r\n const replace = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.replace(fullUrl);\r\n return;\r\n }\r\n \r\n // Update URL in browser history using replace (doesn't add to history)\r\n window.history.replaceState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const back = useCallback(() => {\r\n if (typeof window !== \"undefined\") {\r\n window.history.back();\r\n }\r\n }, []);\r\n\r\n const refresh = useCallback(async () => {\r\n const currentUrl = typeof window !== \"undefined\" \r\n ? window.location.pathname + window.location.search \r\n : routeData.pathname;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.reload();\r\n return;\r\n }\r\n \r\n await currentNavigate(currentUrl, { revalidate: true });\r\n }, [navigate, routeData.pathname]);\r\n\r\n return {\r\n push,\r\n replace,\r\n back,\r\n refresh,\r\n pathname: routeData.pathname,\r\n query: routeData.query,\r\n searchParams: routeData.searchParams,\r\n params: routeData.params,\r\n };\r\n}\r\n\r\n/**\r\n * Parse query string into an object.\r\n * @param search - Query string (e.g., \"?id=123&name=test\")\r\n * @returns Object with query parameters\r\n */\r\nfunction parseQueryString(search: string): Record<string, string> {\r\n const params: Record<string, string> = {};\r\n if (!search || search.length === 0) return params;\r\n \r\n const queryString = search.startsWith(\"?\") ? search.slice(1) : search;\r\n const pairs = queryString.split(\"&\");\r\n \r\n for (const pair of pairs) {\r\n const [key, value] = pair.split(\"=\");\r\n if (key) {\r\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\r\n }\r\n }\r\n \r\n return params;\r\n}\r\n","import { createContext, useContext } from \"react\";\r\n\r\nexport type NavigateFunction = (\r\n url: string,\r\n options?: { revalidate?: boolean; replace?: boolean }\r\n) => Promise<void>;\r\n\r\nexport interface RouterContextValue {\r\n navigate: NavigateFunction;\r\n}\r\n\r\nexport const RouterContext = createContext<RouterContextValue | null>(null);\r\n\r\nexport function useRouterContext(): RouterContextValue {\r\n const context = useContext(RouterContext);\r\n if (!context) {\r\n throw new Error(\r\n \"useRouter must be used within a RouterProvider. Make sure you're using it inside a Loly app.\"\r\n );\r\n }\r\n return context;\r\n}\r\n","// Client-side constants (hardcoded to avoid alias resolution issues in Rspack)\r\nexport const WINDOW_DATA_KEY = \"__FW_DATA__\";\r\nexport const ROUTER_DATA_KEY = \"__LOLY_ROUTER_DATA__\";\r\nexport const APP_CONTAINER_ID = \"__app\";\r\n// Global key for navigate function fallback (exposed by AppShell for hydration timing issues)\r\nexport const ROUTER_NAVIGATE_KEY = \"__LOLY_ROUTER_NAVIGATE__\";\r\n\r\n","import { WINDOW_DATA_KEY, ROUTER_DATA_KEY } from \"./constants\";\r\nimport type { InitialData, RouterData } from \"./types\";\r\n\r\nexport function getWindowData(): InitialData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return (window[WINDOW_DATA_KEY] as InitialData | undefined) ?? null;\r\n}\r\n\r\nexport function getRouterData(): RouterData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return (window[ROUTER_DATA_KEY] as RouterData | undefined) ?? null;\r\n}\r\n\r\nexport function setWindowData(data: InitialData): void {\r\n window[WINDOW_DATA_KEY] = data;\r\n \r\n // Dispatch event for components to listen to (e.g. ThemeProvider)\r\n // This ensures components update when navigating in SPA mode\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function setRouterData(data: RouterData): void {\r\n window[ROUTER_DATA_KEY] = data;\r\n \r\n // Dispatch event for router data updates\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-router-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function getCurrentTheme(): string | null {\r\n return getWindowData()?.theme ?? null;\r\n}\r\n\r\n"],"mappings":"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","names":["useState","useEffect","useCallback","useContext","useRef","useContext","useRef","useEffect","useState","useCallback"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../modules/react/sockets/index.ts"],"sourcesContent":["import { io, ManagerOptions, Socket, SocketOptions } from \"socket.io-client\";\r\n\r\n/**\r\n * Creates a Socket.IO client connection to a specific namespace.\r\n * \r\n * This helper function simplifies Socket.IO client setup by handling:\r\n * - Namespace normalization (ensures leading slash)\r\n * - Base URL resolution (from environment or current origin)\r\n * - Default Socket.IO configuration (path: '/wss', transports: ['websocket', 'polling'])\r\n * \r\n * @param namespace - The namespace to connect to (e.g., '/chat' or 'chat').\r\n * The namespace will be normalized to always start with '/'.\r\n * Must match the namespace pattern defined in your server's WSS routes.\r\n * @param opts - Optional Socket.IO client options that will override the defaults.\r\n * \r\n * @returns A Socket.IO client instance connected to the specified namespace.\r\n * \r\n * @example\r\n * ```ts\r\n * const socket = lolySocket('/chat');\r\n * socket.on('message', (data) => {\r\n * console.log('Received:', data);\r\n * });\r\n * socket.emit('message', { text: 'Hello' });\r\n * ```\r\n */\r\nexport const lolySocket = (\r\n namespace: string,\r\n opts?: Partial<ManagerOptions & SocketOptions>\r\n): Socket => {\r\n // @ts-ignore - process.env.PUBLIC_WS_BASE_URL is replaced by DefinePlugin at build time with literal value\r\n // DefinePlugin replaces process.env.PUBLIC_WS_BASE_URL with the actual value (or undefined if not set)\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const wsBaseUrl: string | undefined = (process as any).env?.PUBLIC_WS_BASE_URL;\r\n \r\n // Get base URL: use PUBLIC_WS_BASE_URL if set, otherwise use window.location.origin (client-side only)\r\n // For SSR, if window is not available, we'll need to handle it differently\r\n let baseUrl: string;\r\n if (wsBaseUrl && wsBaseUrl.trim() !== \"\") {\r\n baseUrl = wsBaseUrl;\r\n } else if (typeof window !== \"undefined\") {\r\n // Client-side: use current origin\r\n baseUrl = window.location.origin;\r\n } else {\r\n // Server-side rendering: throw error or use a default\r\n // This should rarely happen as sockets are typically client-only\r\n throw new Error(\r\n \"[loly:socket] Cannot determine base URL in server-side context. \" +\r\n \"Either set PUBLIC_WS_BASE_URL in your .env file or ensure lolySocket is only called on the client.\"\r\n );\r\n }\r\n\r\n // Normalize namespace to always start with '/'\r\n const normalizedNamespace = namespace.startsWith(\"/\") ? namespace : `/${namespace}`;\r\n\r\n // In Socket.IO, when using a custom path, the namespace is specified in the URL:\r\n // baseUrl + namespace. The path '/wss' is the HTTP route where Socket.IO listens.\r\n const fullUrl = `${baseUrl}${normalizedNamespace}`;\r\n\r\n const socket = io(fullUrl, {\r\n path: \"/wss\",\r\n transports: [\"websocket\", \"polling\"],\r\n autoConnect: true,\r\n ...opts,\r\n });\r\n\r\n return socket;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA0D;AA0BnD,IAAM,aAAa,CACxB,WACA,SACW;AAIX,QAAM,YAAiC,QAAgB,KAAK;AAI5D,MAAI;AACJ,MAAI,aAAa,UAAU,KAAK,MAAM,IAAI;AACxC,cAAU;AAAA,EACZ,WAAW,OAAO,WAAW,aAAa;AAExC,cAAU,OAAO,SAAS;AAAA,EAC5B,OAAO;AAGL,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAGA,QAAM,sBAAsB,UAAU,WAAW,GAAG,IAAI,YAAY,IAAI,SAAS;AAIjF,QAAM,UAAU,GAAG,OAAO,GAAG,mBAAmB;AAEhD,QAAM,aAAS,kBAAG,SAAS;AAAA,IACzB,MAAM;AAAA,IACN,YAAY,CAAC,aAAa,SAAS;AAAA,IACnC,aAAa;AAAA,IACb,GAAG;AAAA,EACL,CAAC;AAED,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../modules/react/sockets/index.ts"],"sourcesContent":["import { io, ManagerOptions, Socket, SocketOptions } from \"socket.io-client\";\r\n\r\n/**\r\n * Creates a Socket.IO client connection to a specific namespace.\r\n * \r\n * This helper function simplifies Socket.IO client setup by handling:\r\n * - Namespace normalization (ensures leading slash)\r\n * - Base URL resolution (from environment or current origin)\r\n * - Default Socket.IO configuration (path: '/wss', transports: ['websocket', 'polling'])\r\n * \r\n * @param namespace - The namespace to connect to (e.g., '/chat' or 'chat').\r\n * The namespace will be normalized to always start with '/'.\r\n * Must match the namespace pattern defined in your server's WSS routes.\r\n * @param opts - Optional Socket.IO client options that will override the defaults.\r\n * \r\n * @returns A Socket.IO client instance connected to the specified namespace.\r\n * \r\n * @example\r\n * ```ts\r\n * const socket = lolySocket('/chat');\r\n * socket.on('message', (data) => {\r\n * console.log('Received:', data);\r\n * });\r\n * socket.emit('message', { text: 'Hello' });\r\n * ```\r\n */\r\nexport const lolySocket = (\r\n namespace: string,\r\n opts?: Partial<ManagerOptions & SocketOptions>\r\n): Socket => {\r\n // @ts-ignore - process.env.PUBLIC_WS_BASE_URL is replaced by DefinePlugin at build time\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const wsBaseUrl: string | undefined = (process as any).env?.PUBLIC_WS_BASE_URL;\r\n \r\n let baseUrl: string;\r\n if (wsBaseUrl && wsBaseUrl.trim() !== \"\") {\r\n baseUrl = wsBaseUrl;\r\n } else if (typeof window !== \"undefined\") {\r\n baseUrl = window.location.origin;\r\n } else {\r\n throw new Error(\r\n \"[loly:socket] Cannot determine base URL in server-side context. \" +\r\n \"Either set PUBLIC_WS_BASE_URL in your .env file or ensure lolySocket is only called on the client.\"\r\n );\r\n }\r\n\r\n const normalizedNamespace = namespace.startsWith(\"/\") ? namespace : `/${namespace}`;\r\n const fullUrl = `${baseUrl}${normalizedNamespace}`;\r\n\r\n const socket = io(fullUrl, {\r\n path: \"/wss\",\r\n transports: [\"websocket\", \"polling\"],\r\n autoConnect: true,\r\n ...opts,\r\n });\r\n\r\n return socket;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA0D;AA0BnD,IAAM,aAAa,CACxB,WACA,SACW;AAGX,QAAM,YAAiC,QAAgB,KAAK;AAE5D,MAAI;AACJ,MAAI,aAAa,UAAU,KAAK,MAAM,IAAI;AACxC,cAAU;AAAA,EACZ,WAAW,OAAO,WAAW,aAAa;AACxC,cAAU,OAAO,SAAS;AAAA,EAC5B,OAAO;AACL,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAEA,QAAM,sBAAsB,UAAU,WAAW,GAAG,IAAI,YAAY,IAAI,SAAS;AACjF,QAAM,UAAU,GAAG,OAAO,GAAG,mBAAmB;AAEhD,QAAM,aAAS,kBAAG,SAAS;AAAA,IACzB,MAAM;AAAA,IACN,YAAY,CAAC,aAAa,SAAS;AAAA,IACnC,aAAa;AAAA,IACb,GAAG;AAAA,EACL,CAAC;AAED,SAAO;AACT;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../modules/react/sockets/index.ts"],"sourcesContent":["import { io, ManagerOptions, Socket, SocketOptions } from \"socket.io-client\";\r\n\r\n/**\r\n * Creates a Socket.IO client connection to a specific namespace.\r\n * \r\n * This helper function simplifies Socket.IO client setup by handling:\r\n * - Namespace normalization (ensures leading slash)\r\n * - Base URL resolution (from environment or current origin)\r\n * - Default Socket.IO configuration (path: '/wss', transports: ['websocket', 'polling'])\r\n * \r\n * @param namespace - The namespace to connect to (e.g., '/chat' or 'chat').\r\n * The namespace will be normalized to always start with '/'.\r\n * Must match the namespace pattern defined in your server's WSS routes.\r\n * @param opts - Optional Socket.IO client options that will override the defaults.\r\n * \r\n * @returns A Socket.IO client instance connected to the specified namespace.\r\n * \r\n * @example\r\n * ```ts\r\n * const socket = lolySocket('/chat');\r\n * socket.on('message', (data) => {\r\n * console.log('Received:', data);\r\n * });\r\n * socket.emit('message', { text: 'Hello' });\r\n * ```\r\n */\r\nexport const lolySocket = (\r\n namespace: string,\r\n opts?: Partial<ManagerOptions & SocketOptions>\r\n): Socket => {\r\n // @ts-ignore - process.env.PUBLIC_WS_BASE_URL is replaced by DefinePlugin at build time with literal value\r\n // DefinePlugin replaces process.env.PUBLIC_WS_BASE_URL with the actual value (or undefined if not set)\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const wsBaseUrl: string | undefined = (process as any).env?.PUBLIC_WS_BASE_URL;\r\n \r\n // Get base URL: use PUBLIC_WS_BASE_URL if set, otherwise use window.location.origin (client-side only)\r\n // For SSR, if window is not available, we'll need to handle it differently\r\n let baseUrl: string;\r\n if (wsBaseUrl && wsBaseUrl.trim() !== \"\") {\r\n baseUrl = wsBaseUrl;\r\n } else if (typeof window !== \"undefined\") {\r\n // Client-side: use current origin\r\n baseUrl = window.location.origin;\r\n } else {\r\n // Server-side rendering: throw error or use a default\r\n // This should rarely happen as sockets are typically client-only\r\n throw new Error(\r\n \"[loly:socket] Cannot determine base URL in server-side context. \" +\r\n \"Either set PUBLIC_WS_BASE_URL in your .env file or ensure lolySocket is only called on the client.\"\r\n );\r\n }\r\n\r\n // Normalize namespace to always start with '/'\r\n const normalizedNamespace = namespace.startsWith(\"/\") ? namespace : `/${namespace}`;\r\n\r\n // In Socket.IO, when using a custom path, the namespace is specified in the URL:\r\n // baseUrl + namespace. The path '/wss' is the HTTP route where Socket.IO listens.\r\n const fullUrl = `${baseUrl}${normalizedNamespace}`;\r\n\r\n const socket = io(fullUrl, {\r\n path: \"/wss\",\r\n transports: [\"websocket\", \"polling\"],\r\n autoConnect: true,\r\n ...opts,\r\n });\r\n\r\n return socket;\r\n};\r\n"],"mappings":";AAAA,SAAS,UAAiD;AA0BnD,IAAM,aAAa,CACxB,WACA,SACW;AAIX,QAAM,YAAiC,QAAgB,KAAK;AAI5D,MAAI;AACJ,MAAI,aAAa,UAAU,KAAK,MAAM,IAAI;AACxC,cAAU;AAAA,EACZ,WAAW,OAAO,WAAW,aAAa;AAExC,cAAU,OAAO,SAAS;AAAA,EAC5B,OAAO;AAGL,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAGA,QAAM,sBAAsB,UAAU,WAAW,GAAG,IAAI,YAAY,IAAI,SAAS;AAIjF,QAAM,UAAU,GAAG,OAAO,GAAG,mBAAmB;AAEhD,QAAM,SAAS,GAAG,SAAS;AAAA,IACzB,MAAM;AAAA,IACN,YAAY,CAAC,aAAa,SAAS;AAAA,IACnC,aAAa;AAAA,IACb,GAAG;AAAA,EACL,CAAC;AAED,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../modules/react/sockets/index.ts"],"sourcesContent":["import { io, ManagerOptions, Socket, SocketOptions } from \"socket.io-client\";\r\n\r\n/**\r\n * Creates a Socket.IO client connection to a specific namespace.\r\n * \r\n * This helper function simplifies Socket.IO client setup by handling:\r\n * - Namespace normalization (ensures leading slash)\r\n * - Base URL resolution (from environment or current origin)\r\n * - Default Socket.IO configuration (path: '/wss', transports: ['websocket', 'polling'])\r\n * \r\n * @param namespace - The namespace to connect to (e.g., '/chat' or 'chat').\r\n * The namespace will be normalized to always start with '/'.\r\n * Must match the namespace pattern defined in your server's WSS routes.\r\n * @param opts - Optional Socket.IO client options that will override the defaults.\r\n * \r\n * @returns A Socket.IO client instance connected to the specified namespace.\r\n * \r\n * @example\r\n * ```ts\r\n * const socket = lolySocket('/chat');\r\n * socket.on('message', (data) => {\r\n * console.log('Received:', data);\r\n * });\r\n * socket.emit('message', { text: 'Hello' });\r\n * ```\r\n */\r\nexport const lolySocket = (\r\n namespace: string,\r\n opts?: Partial<ManagerOptions & SocketOptions>\r\n): Socket => {\r\n // @ts-ignore - process.env.PUBLIC_WS_BASE_URL is replaced by DefinePlugin at build time\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const wsBaseUrl: string | undefined = (process as any).env?.PUBLIC_WS_BASE_URL;\r\n \r\n let baseUrl: string;\r\n if (wsBaseUrl && wsBaseUrl.trim() !== \"\") {\r\n baseUrl = wsBaseUrl;\r\n } else if (typeof window !== \"undefined\") {\r\n baseUrl = window.location.origin;\r\n } else {\r\n throw new Error(\r\n \"[loly:socket] Cannot determine base URL in server-side context. \" +\r\n \"Either set PUBLIC_WS_BASE_URL in your .env file or ensure lolySocket is only called on the client.\"\r\n );\r\n }\r\n\r\n const normalizedNamespace = namespace.startsWith(\"/\") ? namespace : `/${namespace}`;\r\n const fullUrl = `${baseUrl}${normalizedNamespace}`;\r\n\r\n const socket = io(fullUrl, {\r\n path: \"/wss\",\r\n transports: [\"websocket\", \"polling\"],\r\n autoConnect: true,\r\n ...opts,\r\n });\r\n\r\n return socket;\r\n};\r\n"],"mappings":";AAAA,SAAS,UAAiD;AA0BnD,IAAM,aAAa,CACxB,WACA,SACW;AAGX,QAAM,YAAiC,QAAgB,KAAK;AAE5D,MAAI;AACJ,MAAI,aAAa,UAAU,KAAK,MAAM,IAAI;AACxC,cAAU;AAAA,EACZ,WAAW,OAAO,WAAW,aAAa;AACxC,cAAU,OAAO,SAAS;AAAA,EAC5B,OAAO;AACL,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAEA,QAAM,sBAAsB,UAAU,WAAW,GAAG,IAAI,YAAY,IAAI,SAAS;AACjF,QAAM,UAAU,GAAG,OAAO,GAAG,mBAAmB;AAEhD,QAAM,SAAS,GAAG,SAAS;AAAA,IACzB,MAAM;AAAA,IACN,YAAY,CAAC,aAAa,SAAS;AAAA,IACnC,aAAa;AAAA,IACb,GAAG;AAAA,EACL,CAAC;AAED,SAAO;AACT;","names":[]}