@lolyjs/core 0.2.0-alpha.3 → 0.2.0-alpha.30

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.
Files changed (46) hide show
  1. package/README.md +1463 -761
  2. package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
  3. package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
  4. package/dist/cli.cjs +15701 -2448
  5. package/dist/cli.cjs.map +1 -1
  6. package/dist/cli.js +15704 -2441
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.cjs +17861 -4115
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.mts +323 -55
  11. package/dist/index.d.ts +323 -55
  12. package/dist/index.js +17982 -4227
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.types-B9j4OQft.d.mts +222 -0
  15. package/dist/index.types-B9j4OQft.d.ts +222 -0
  16. package/dist/react/cache.cjs +107 -32
  17. package/dist/react/cache.cjs.map +1 -1
  18. package/dist/react/cache.d.mts +29 -21
  19. package/dist/react/cache.d.ts +29 -21
  20. package/dist/react/cache.js +107 -32
  21. package/dist/react/cache.js.map +1 -1
  22. package/dist/react/components.cjs +11 -12
  23. package/dist/react/components.cjs.map +1 -1
  24. package/dist/react/components.js +11 -12
  25. package/dist/react/components.js.map +1 -1
  26. package/dist/react/hooks.cjs +124 -74
  27. package/dist/react/hooks.cjs.map +1 -1
  28. package/dist/react/hooks.d.mts +6 -24
  29. package/dist/react/hooks.d.ts +6 -24
  30. package/dist/react/hooks.js +122 -71
  31. package/dist/react/hooks.js.map +1 -1
  32. package/dist/react/sockets.cjs +5 -6
  33. package/dist/react/sockets.cjs.map +1 -1
  34. package/dist/react/sockets.js +5 -6
  35. package/dist/react/sockets.js.map +1 -1
  36. package/dist/react/themes.cjs +61 -18
  37. package/dist/react/themes.cjs.map +1 -1
  38. package/dist/react/themes.js +63 -20
  39. package/dist/react/themes.js.map +1 -1
  40. package/dist/runtime.cjs +531 -104
  41. package/dist/runtime.cjs.map +1 -1
  42. package/dist/runtime.d.mts +2 -2
  43. package/dist/runtime.d.ts +2 -2
  44. package/dist/runtime.js +531 -104
  45. package/dist/runtime.js.map +1 -1
  46. package/package.json +56 -14
@@ -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\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\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 return (\r\n <img\r\n src={src}\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\";\r\n\r\n/**\r\n * Response data structure from server for route data requests\r\n */\r\nexport type RouteDataResponse = {\r\n /** Combined props (layout + page) - kept for backward compatibility */\r\n props?: Record<string, unknown>;\r\n /** Layout props (from layout.server.hook.ts) - only present when layout hooks were executed */\r\n layoutProps?: Record<string, unknown>;\r\n /** Page props (from page.server.hook.ts) - always present in data requests */\r\n pageProps?: Record<string, unknown>;\r\n metadata?: PageMetadata | null;\r\n theme?: string;\r\n redirect?: { destination: string; permanent?: boolean };\r\n notFound?: boolean;\r\n error?: boolean;\r\n message?: string;\r\n params?: Record<string, string>;\r\n /** Pathname after rewrite (for client-side route matching) */\r\n pathname?: string;\r\n};\r\n\r\ntype RouteData = {\r\n ok: boolean;\r\n status: number;\r\n json: RouteDataResponse;\r\n};\r\n\r\ntype CacheEntry =\r\n | { status: \"pending\"; promise: Promise<RouteData> }\r\n | { status: \"fulfilled\"; value: RouteData }\r\n | { status: \"rejected\"; error: any };\r\n\r\n// Use window to guarantee a single shared cache instance\r\n// across all bundles/modules\r\nconst CACHE_KEY = \"__FW_DATA_CACHE__\";\r\n\r\n// Maximum number of entries in the cache (LRU)\r\nconst MAX_CACHE_SIZE = 100;\r\n\r\ntype CacheStore = {\r\n data: Map<string, CacheEntry>;\r\n index: Map<string, Set<string>>; // pathBase -> Set of keys\r\n lru: string[]; // Ordered list: most recent at end, oldest at start\r\n};\r\n\r\nfunction getCacheStore(): CacheStore {\r\n if (typeof window !== \"undefined\") {\r\n if (!(window as any)[CACHE_KEY]) {\r\n (window as any)[CACHE_KEY] = {\r\n data: new Map<string, CacheEntry>(),\r\n index: new Map<string, Set<string>>(),\r\n lru: [],\r\n };\r\n }\r\n return (window as any)[CACHE_KEY];\r\n }\r\n // Fallback for SSR (though this shouldn't be used on the client)\r\n return {\r\n data: new Map<string, CacheEntry>(),\r\n index: new Map<string, Set<string>>(),\r\n lru: [],\r\n };\r\n}\r\n\r\nconst cacheStore = getCacheStore();\r\nconst dataCache = cacheStore.data;\r\nconst pathIndex = cacheStore.index;\r\nconst lru = cacheStore.lru;\r\n\r\n// Helper functions for cache management\r\n\r\n/**\r\n * Extract base path from a cache key (removes query params)\r\n */\r\nfunction extractPathBase(key: string): string {\r\n return key.split(\"?\")[0];\r\n}\r\n\r\n/**\r\n * Add key to path index\r\n */\r\nfunction addToIndex(key: string): void {\r\n const pathBase = extractPathBase(key);\r\n if (!pathIndex.has(pathBase)) {\r\n pathIndex.set(pathBase, new Set());\r\n }\r\n pathIndex.get(pathBase)!.add(key);\r\n}\r\n\r\n/**\r\n * Remove key from path index\r\n */\r\nfunction removeFromIndex(key: string): void {\r\n const pathBase = extractPathBase(key);\r\n const keys = pathIndex.get(pathBase);\r\n if (keys) {\r\n keys.delete(key);\r\n if (keys.size === 0) {\r\n pathIndex.delete(pathBase);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Update LRU order - move key to end (most recent)\r\n */\r\nfunction updateLRU(key: string): void {\r\n const index = lru.indexOf(key);\r\n if (index !== -1) {\r\n lru.splice(index, 1);\r\n }\r\n lru.push(key);\r\n}\r\n\r\n/**\r\n * Remove oldest entries if cache exceeds MAX_CACHE_SIZE\r\n */\r\nfunction evictOldest(): void {\r\n while (lru.length >= MAX_CACHE_SIZE && lru.length > 0) {\r\n const oldestKey = lru.shift()!;\r\n dataCache.delete(oldestKey);\r\n removeFromIndex(oldestKey);\r\n }\r\n}\r\n\r\n/**\r\n * Set cache entry and maintain indexes\r\n */\r\nfunction setCacheEntry(key: string, entry: CacheEntry): void {\r\n const existingEntry = dataCache.get(key);\r\n const wasFulfilled = existingEntry?.status === \"fulfilled\";\r\n \r\n dataCache.set(key, entry);\r\n \r\n // Only track fulfilled entries in LRU and index (not pending/rejected)\r\n if (entry.status === \"fulfilled\") {\r\n // Add to index if it wasn't already fulfilled (new entry or transition from pending/rejected)\r\n if (!wasFulfilled) {\r\n addToIndex(key);\r\n }\r\n updateLRU(key);\r\n evictOldest();\r\n } else if (wasFulfilled) {\r\n // If entry was fulfilled and now isn't (transitioning to pending/rejected), remove from index\r\n removeFromIndex(key);\r\n }\r\n}\r\n\r\n/**\r\n * Delete cache entry and clean up indexes\r\n */\r\nfunction deleteCacheEntry(key: string): void {\r\n if (dataCache.has(key)) {\r\n dataCache.delete(key);\r\n removeFromIndex(key);\r\n const lruIndex = lru.indexOf(key);\r\n if (lruIndex !== -1) {\r\n lru.splice(lruIndex, 1);\r\n }\r\n }\r\n}\r\n\r\nfunction buildDataUrl(url: string): string {\r\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + \"__fw_data=1\";\r\n}\r\n\r\nasync function fetchRouteDataOnce(\r\n url: string,\r\n skipLayoutHooks: boolean = true\r\n): Promise<RouteData> {\r\n const dataUrl = buildDataUrl(url);\r\n\r\n const headers: Record<string, string> = {\r\n \"x-fw-data\": \"1\",\r\n Accept: \"application/json\",\r\n };\r\n\r\n // Send header to skip layout hooks execution in SPA navigation\r\n // Only skip if skipLayoutHooks is true (normal SPA navigation)\r\n // If false (revalidate), don't send header to force execution of all hooks\r\n if (skipLayoutHooks) {\r\n headers[\"x-skip-layout-hooks\"] = \"true\";\r\n }\r\n\r\n const res = await fetch(dataUrl, { headers });\r\n\r\n let json: any = {};\r\n\r\n try {\r\n const text = await res.text();\r\n if (text) {\r\n json = JSON.parse(text);\r\n }\r\n } catch (parseError) {\r\n console.error(\r\n \"[client][cache] Failed to parse response as JSON:\",\r\n parseError\r\n );\r\n }\r\n\r\n const result: RouteData = {\r\n ok: res.ok,\r\n status: res.status,\r\n json,\r\n };\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Revalidates route data by removing it from the cache.\r\n * The next time you navigate to this route, fresh data will be fetched from the server.\r\n * This is a client-side function and does not require a server-side revalidation.\r\n *\r\n * @param path - The route path to revalidate (e.g., '/posts/1' or '/posts/1?page=2')\r\n * If query params are not included, revalidates all variants of that route.\r\n *\r\n * @example\r\n * ```ts\r\n * // After saving something to the DB, revalidate the route\r\n * await saveToDatabase(data);\r\n * revalidatePath('/posts');\r\n * \r\n * // Revalidate a specific route with query params\r\n * revalidatePath('/posts?page=2');\r\n * ```\r\n */\r\nexport function revalidatePath(path: string, skipAutoRevalidate: boolean = false): void {\r\n // Normalize the base path (without query params)\r\n const normalizedPath = path.split(\"?\")[0];\r\n const hasQueryParams = path.includes(\"?\");\r\n \r\n // Get all keys for this path base from index (O(1) lookup)\r\n const keysForPath = pathIndex.get(normalizedPath);\r\n \r\n if (!keysForPath || keysForPath.size === 0) {\r\n return; // No entries to revalidate\r\n }\r\n \r\n // If the path includes specific query params, extract them\r\n let specificQueryParams: string | undefined;\r\n if (hasQueryParams) {\r\n const queryPart = path.split(\"?\")[1];\r\n // Sort query params for consistent comparison\r\n specificQueryParams = queryPart\r\n .split(\"&\")\r\n .filter((p) => !p.startsWith(\"__fw_data=\"))\r\n .sort()\r\n .join(\"&\");\r\n }\r\n \r\n // Iterate only over keys for this path (much smaller set)\r\n const keysToDelete: string[] = [];\r\n for (const key of keysForPath) {\r\n // If specific query params were specified, check if they match\r\n if (hasQueryParams && specificQueryParams) {\r\n const [, keyQuery = \"\"] = key.split(\"?\");\r\n const keyQueryParams = keyQuery\r\n .split(\"&\")\r\n .filter((p) => !p.startsWith(\"__fw_data=\"))\r\n .sort()\r\n .join(\"&\");\r\n \r\n if (keyQueryParams === specificQueryParams) {\r\n keysToDelete.push(key);\r\n }\r\n } else {\r\n // If no specific query params, revalidate all variants\r\n keysToDelete.push(key);\r\n }\r\n }\r\n \r\n // Delete matching entries\r\n keysToDelete.forEach((key) => {\r\n deleteCacheEntry(key);\r\n });\r\n \r\n // If the revalidated path matches the current route, automatically refresh data\r\n // UNLESS skipAutoRevalidate is true (to prevent recursive calls from revalidate())\r\n if (!skipAutoRevalidate && typeof window !== \"undefined\") {\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n const matchesCurrentPath = normalizedPath === currentPathname;\r\n \r\n if (matchesCurrentPath) {\r\n if (hasQueryParams && specificQueryParams) {\r\n const currentQueryParams = currentSearch\r\n .replace(\"?\", \"\")\r\n .split(\"&\")\r\n .filter((p) => !p.startsWith(\"__fw_data=\"))\r\n .sort()\r\n .join(\"&\");\r\n \r\n if (currentQueryParams === specificQueryParams) {\r\n revalidate().catch((err) => {\r\n console.error(\r\n \"[client][cache] Error revalidating current route:\",\r\n err\r\n );\r\n });\r\n }\r\n } else {\r\n revalidate().catch((err) => {\r\n console.error(\r\n \"[client][cache] Error revalidating current route:\",\r\n err\r\n );\r\n });\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Revalidates and refreshes the current page data.\r\n * Similar to Next.js's `router.refresh()`.\r\n * \r\n * This function:\r\n * 1. Removes the current route from cache\r\n * 2. Fetches fresh data from the server\r\n * 3. Updates window.__FW_DATA__ with the new data\r\n * 4. Dispatches a 'fw-data-refresh' event for components to listen to\r\n * \r\n * @returns Promise that resolves with the fresh route data\r\n * \r\n * @example\r\n * ```ts\r\n * // Refresh current page data after a mutation\r\n * await revalidate();\r\n * ```\r\n */\r\n// Flag to prevent recursive calls to revalidate()\r\nlet isRevalidating = false;\r\n\r\nexport async function revalidate(): Promise<RouteData> {\r\n if (typeof window === \"undefined\") {\r\n throw new Error(\"revalidate() can only be called on the client\");\r\n }\r\n\r\n // Prevent multiple simultaneous revalidations\r\n if (isRevalidating) {\r\n // Wait for the current revalidation to complete\r\n const key = buildDataUrl(window.location.pathname + window.location.search);\r\n const entry = dataCache.get(key);\r\n if (entry && entry.status === \"pending\") {\r\n return entry.promise;\r\n }\r\n // If no pending entry, something went wrong, allow the call\r\n }\r\n\r\n isRevalidating = true;\r\n try {\r\n const pathname = window.location.pathname + window.location.search;\r\n \r\n // Revalidate the path (remove from cache)\r\n // Pass a flag to prevent revalidatePath from calling revalidate() again (recursive call)\r\n revalidatePath(pathname, true); // true = skip auto-revalidate\r\n \r\n // Fetch fresh data\r\n const freshData = await getRouteData(pathname, { revalidate: true });\r\n \r\n // Update window.__FW_DATA__ if it exists\r\n if ((window as any).__FW_DATA__ && freshData.ok && freshData.json) {\r\n const currentData = (window as any).__FW_DATA__;\r\n \r\n // Update preserved layout props if new ones were returned\r\n if (freshData.json.layoutProps !== undefined && freshData.json.layoutProps !== null) {\r\n (window as any).__FW_LAYOUT_PROPS__ = freshData.json.layoutProps;\r\n }\r\n \r\n // Combine layout props (new or preserved) + page props\r\n let combinedProps = currentData.props || {};\r\n if (freshData.json.layoutProps !== undefined && freshData.json.layoutProps !== null) {\r\n // Use new layout props\r\n combinedProps = {\r\n ...freshData.json.layoutProps,\r\n ...(freshData.json.pageProps ?? freshData.json.props ?? {}),\r\n };\r\n } else if (freshData.json.pageProps !== undefined) {\r\n // Use preserved layout props + new page props\r\n const preservedLayoutProps = (window as any).__FW_LAYOUT_PROPS__ || {};\r\n combinedProps = {\r\n ...preservedLayoutProps,\r\n ...freshData.json.pageProps,\r\n };\r\n } else if (freshData.json.props) {\r\n // Fallback to combined props\r\n combinedProps = freshData.json.props;\r\n }\r\n \r\n (window as any).__FW_DATA__ = {\r\n ...currentData,\r\n pathname: pathname.split(\"?\")[0],\r\n params: freshData.json.params || currentData.params || {},\r\n props: combinedProps,\r\n metadata: freshData.json.metadata ?? currentData.metadata ?? null,\r\n notFound: freshData.json.notFound ?? false,\r\n error: freshData.json.error ?? false,\r\n };\r\n \r\n // Dispatch event for components to listen to\r\n window.dispatchEvent(new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data: freshData },\r\n }));\r\n }\r\n \r\n return freshData;\r\n } finally {\r\n isRevalidating = false;\r\n }\r\n}\r\n\r\n/**\r\n * @deprecated Use `revalidatePath()` instead. This function is kept for backwards compatibility.\r\n */\r\nexport function revalidateRouteData(url: string): void {\r\n revalidatePath(url);\r\n}\r\n\r\nexport function prefetchRouteData(url: string): void {\r\n const key = buildDataUrl(url);\r\n\r\n const cached = dataCache.get(key);\r\n\r\n if (cached && cached.status !== \"rejected\") {\r\n // Update LRU if it exists and is fulfilled\r\n if (cached.status === \"fulfilled\") {\r\n updateLRU(key);\r\n }\r\n return;\r\n }\r\n\r\n // Prefetch uses skipLayoutHooks: true (normal navigation behavior)\r\n const promise = fetchRouteDataOnce(url, true)\r\n .then((value) => {\r\n setCacheEntry(key, { status: \"fulfilled\", value });\r\n return value;\r\n })\r\n .catch((error) => {\r\n console.error(\"[client][cache] Error prefetching route data:\", error);\r\n dataCache.set(key, { status: \"rejected\", error });\r\n throw error;\r\n });\r\n\r\n dataCache.set(key, { status: \"pending\", promise });\r\n}\r\n\r\nexport type GetRouteDataOptions = {\r\n /**\r\n * If true, forces revalidation of route data,\r\n * ignoring the cache and fetching fresh data from the server.\r\n * Similar to Next.js's `router.refresh()` behavior.\r\n */\r\n revalidate?: boolean;\r\n};\r\n\r\nexport async function getRouteData(\r\n url: string,\r\n options?: GetRouteDataOptions\r\n): Promise<RouteData> {\r\n const key = buildDataUrl(url);\r\n\r\n // If revalidation is requested, remove the entry from cache\r\n // This ensures we don't reuse pending or fulfilled entries\r\n if (options?.revalidate) {\r\n deleteCacheEntry(key);\r\n }\r\n\r\n const entry = dataCache.get(key);\r\n\r\n if (entry && !options?.revalidate) {\r\n // Only use cached entry if not revalidating\r\n if (entry.status === \"fulfilled\") {\r\n // Update LRU: mark as recently used\r\n updateLRU(key);\r\n return entry.value;\r\n }\r\n if (entry.status === \"pending\") {\r\n // Return existing pending promise to avoid duplicate requests\r\n return entry.promise;\r\n }\r\n }\r\n\r\n // No entry in cache (or revalidating), fetch it\r\n // skipLayoutHooks: true for normal SPA navigation, false when revalidating\r\n const skipLayoutHooks = !options?.revalidate;\r\n \r\n // Check again if an entry was added while we were processing (race condition)\r\n const currentEntry = dataCache.get(key);\r\n if (currentEntry && !options?.revalidate) {\r\n if (currentEntry.status === \"fulfilled\") {\r\n updateLRU(key);\r\n return currentEntry.value;\r\n }\r\n if (currentEntry.status === \"pending\") {\r\n return currentEntry.promise;\r\n }\r\n }\r\n \r\n // Create a new promise for this fetch\r\n const promise = fetchRouteDataOnce(url, skipLayoutHooks)\r\n .then((value) => {\r\n // Only set cache entry if this is still the current fetch for this key\r\n // This prevents race conditions where multiple revalidations happen simultaneously\r\n const entryAfterFetch = dataCache.get(key);\r\n if (!entryAfterFetch || entryAfterFetch.status === \"pending\") {\r\n setCacheEntry(key, { status: \"fulfilled\", value });\r\n }\r\n return value;\r\n })\r\n .catch((error) => {\r\n console.error(\"[client][cache] Error fetching route data:\", error);\r\n const entryAfterFetch = dataCache.get(key);\r\n if (!entryAfterFetch || entryAfterFetch.status === \"pending\") {\r\n dataCache.set(key, { status: \"rejected\", error });\r\n }\r\n throw error;\r\n });\r\n\r\n // Set pending entry - if revalidating, we already deleted it, so this is safe\r\n dataCache.set(key, { status: \"pending\", promise });\r\n \r\n return promise;\r\n}\r\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,6 +1,5 @@
1
1
  // modules/react/components/Image/index.tsx
2
2
  import { jsx } from "react/jsx-runtime";
3
- var BASE_DIR = "/static/assets";
4
3
  function Image({
5
4
  src,
6
5
  alt,
@@ -12,12 +11,10 @@ function Image({
12
11
  const style = {};
13
12
  if (width) style.width = width;
14
13
  if (height) style.height = height;
15
- const isRelative = !src.startsWith("http");
16
- const FULL_ROUTE = isRelative ? `${BASE_DIR}/${src}` : src;
17
14
  return /* @__PURE__ */ jsx(
18
15
  "img",
19
16
  {
20
- src: FULL_ROUTE,
17
+ src,
21
18
  alt,
22
19
  width,
23
20
  height,
@@ -103,14 +100,16 @@ function setCacheEntry(key, entry) {
103
100
  function buildDataUrl(url) {
104
101
  return url + (url.includes("?") ? "&" : "?") + "__fw_data=1";
105
102
  }
106
- async function fetchRouteDataOnce(url) {
103
+ async function fetchRouteDataOnce(url, skipLayoutHooks = true) {
107
104
  const dataUrl = buildDataUrl(url);
108
- const res = await fetch(dataUrl, {
109
- headers: {
110
- "x-fw-data": "1",
111
- Accept: "application/json"
112
- }
113
- });
105
+ const headers = {
106
+ "x-fw-data": "1",
107
+ Accept: "application/json"
108
+ };
109
+ if (skipLayoutHooks) {
110
+ headers["x-skip-layout-hooks"] = "true";
111
+ }
112
+ const res = await fetch(dataUrl, { headers });
114
113
  let json = {};
115
114
  try {
116
115
  const text = await res.text();
@@ -139,7 +138,7 @@ function prefetchRouteData(url) {
139
138
  }
140
139
  return;
141
140
  }
142
- const promise = fetchRouteDataOnce(url).then((value) => {
141
+ const promise = fetchRouteDataOnce(url, true).then((value) => {
143
142
  setCacheEntry(key, { status: "fulfilled", value });
144
143
  return value;
145
144
  }).catch((error) => {
@@ -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\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\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 return (\r\n <img\r\n src={src}\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\";\r\n\r\n/**\r\n * Response data structure from server for route data requests\r\n */\r\nexport type RouteDataResponse = {\r\n /** Combined props (layout + page) - kept for backward compatibility */\r\n props?: Record<string, unknown>;\r\n /** Layout props (from layout.server.hook.ts) - only present when layout hooks were executed */\r\n layoutProps?: Record<string, unknown>;\r\n /** Page props (from page.server.hook.ts) - always present in data requests */\r\n pageProps?: Record<string, unknown>;\r\n metadata?: PageMetadata | null;\r\n theme?: string;\r\n redirect?: { destination: string; permanent?: boolean };\r\n notFound?: boolean;\r\n error?: boolean;\r\n message?: string;\r\n params?: Record<string, string>;\r\n /** Pathname after rewrite (for client-side route matching) */\r\n pathname?: string;\r\n};\r\n\r\ntype RouteData = {\r\n ok: boolean;\r\n status: number;\r\n json: RouteDataResponse;\r\n};\r\n\r\ntype CacheEntry =\r\n | { status: \"pending\"; promise: Promise<RouteData> }\r\n | { status: \"fulfilled\"; value: RouteData }\r\n | { status: \"rejected\"; error: any };\r\n\r\n// Use window to guarantee a single shared cache instance\r\n// across all bundles/modules\r\nconst CACHE_KEY = \"__FW_DATA_CACHE__\";\r\n\r\n// Maximum number of entries in the cache (LRU)\r\nconst MAX_CACHE_SIZE = 100;\r\n\r\ntype CacheStore = {\r\n data: Map<string, CacheEntry>;\r\n index: Map<string, Set<string>>; // pathBase -> Set of keys\r\n lru: string[]; // Ordered list: most recent at end, oldest at start\r\n};\r\n\r\nfunction getCacheStore(): CacheStore {\r\n if (typeof window !== \"undefined\") {\r\n if (!(window as any)[CACHE_KEY]) {\r\n (window as any)[CACHE_KEY] = {\r\n data: new Map<string, CacheEntry>(),\r\n index: new Map<string, Set<string>>(),\r\n lru: [],\r\n };\r\n }\r\n return (window as any)[CACHE_KEY];\r\n }\r\n // Fallback for SSR (though this shouldn't be used on the client)\r\n return {\r\n data: new Map<string, CacheEntry>(),\r\n index: new Map<string, Set<string>>(),\r\n lru: [],\r\n };\r\n}\r\n\r\nconst cacheStore = getCacheStore();\r\nconst dataCache = cacheStore.data;\r\nconst pathIndex = cacheStore.index;\r\nconst lru = cacheStore.lru;\r\n\r\n// Helper functions for cache management\r\n\r\n/**\r\n * Extract base path from a cache key (removes query params)\r\n */\r\nfunction extractPathBase(key: string): string {\r\n return key.split(\"?\")[0];\r\n}\r\n\r\n/**\r\n * Add key to path index\r\n */\r\nfunction addToIndex(key: string): void {\r\n const pathBase = extractPathBase(key);\r\n if (!pathIndex.has(pathBase)) {\r\n pathIndex.set(pathBase, new Set());\r\n }\r\n pathIndex.get(pathBase)!.add(key);\r\n}\r\n\r\n/**\r\n * Remove key from path index\r\n */\r\nfunction removeFromIndex(key: string): void {\r\n const pathBase = extractPathBase(key);\r\n const keys = pathIndex.get(pathBase);\r\n if (keys) {\r\n keys.delete(key);\r\n if (keys.size === 0) {\r\n pathIndex.delete(pathBase);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Update LRU order - move key to end (most recent)\r\n */\r\nfunction updateLRU(key: string): void {\r\n const index = lru.indexOf(key);\r\n if (index !== -1) {\r\n lru.splice(index, 1);\r\n }\r\n lru.push(key);\r\n}\r\n\r\n/**\r\n * Remove oldest entries if cache exceeds MAX_CACHE_SIZE\r\n */\r\nfunction evictOldest(): void {\r\n while (lru.length >= MAX_CACHE_SIZE && lru.length > 0) {\r\n const oldestKey = lru.shift()!;\r\n dataCache.delete(oldestKey);\r\n removeFromIndex(oldestKey);\r\n }\r\n}\r\n\r\n/**\r\n * Set cache entry and maintain indexes\r\n */\r\nfunction setCacheEntry(key: string, entry: CacheEntry): void {\r\n const existingEntry = dataCache.get(key);\r\n const wasFulfilled = existingEntry?.status === \"fulfilled\";\r\n \r\n dataCache.set(key, entry);\r\n \r\n // Only track fulfilled entries in LRU and index (not pending/rejected)\r\n if (entry.status === \"fulfilled\") {\r\n // Add to index if it wasn't already fulfilled (new entry or transition from pending/rejected)\r\n if (!wasFulfilled) {\r\n addToIndex(key);\r\n }\r\n updateLRU(key);\r\n evictOldest();\r\n } else if (wasFulfilled) {\r\n // If entry was fulfilled and now isn't (transitioning to pending/rejected), remove from index\r\n removeFromIndex(key);\r\n }\r\n}\r\n\r\n/**\r\n * Delete cache entry and clean up indexes\r\n */\r\nfunction deleteCacheEntry(key: string): void {\r\n if (dataCache.has(key)) {\r\n dataCache.delete(key);\r\n removeFromIndex(key);\r\n const lruIndex = lru.indexOf(key);\r\n if (lruIndex !== -1) {\r\n lru.splice(lruIndex, 1);\r\n }\r\n }\r\n}\r\n\r\nfunction buildDataUrl(url: string): string {\r\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + \"__fw_data=1\";\r\n}\r\n\r\nasync function fetchRouteDataOnce(\r\n url: string,\r\n skipLayoutHooks: boolean = true\r\n): Promise<RouteData> {\r\n const dataUrl = buildDataUrl(url);\r\n\r\n const headers: Record<string, string> = {\r\n \"x-fw-data\": \"1\",\r\n Accept: \"application/json\",\r\n };\r\n\r\n // Send header to skip layout hooks execution in SPA navigation\r\n // Only skip if skipLayoutHooks is true (normal SPA navigation)\r\n // If false (revalidate), don't send header to force execution of all hooks\r\n if (skipLayoutHooks) {\r\n headers[\"x-skip-layout-hooks\"] = \"true\";\r\n }\r\n\r\n const res = await fetch(dataUrl, { headers });\r\n\r\n let json: any = {};\r\n\r\n try {\r\n const text = await res.text();\r\n if (text) {\r\n json = JSON.parse(text);\r\n }\r\n } catch (parseError) {\r\n console.error(\r\n \"[client][cache] Failed to parse response as JSON:\",\r\n parseError\r\n );\r\n }\r\n\r\n const result: RouteData = {\r\n ok: res.ok,\r\n status: res.status,\r\n json,\r\n };\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Revalidates route data by removing it from the cache.\r\n * The next time you navigate to this route, fresh data will be fetched from the server.\r\n * This is a client-side function and does not require a server-side revalidation.\r\n *\r\n * @param path - The route path to revalidate (e.g., '/posts/1' or '/posts/1?page=2')\r\n * If query params are not included, revalidates all variants of that route.\r\n *\r\n * @example\r\n * ```ts\r\n * // After saving something to the DB, revalidate the route\r\n * await saveToDatabase(data);\r\n * revalidatePath('/posts');\r\n * \r\n * // Revalidate a specific route with query params\r\n * revalidatePath('/posts?page=2');\r\n * ```\r\n */\r\nexport function revalidatePath(path: string, skipAutoRevalidate: boolean = false): void {\r\n // Normalize the base path (without query params)\r\n const normalizedPath = path.split(\"?\")[0];\r\n const hasQueryParams = path.includes(\"?\");\r\n \r\n // Get all keys for this path base from index (O(1) lookup)\r\n const keysForPath = pathIndex.get(normalizedPath);\r\n \r\n if (!keysForPath || keysForPath.size === 0) {\r\n return; // No entries to revalidate\r\n }\r\n \r\n // If the path includes specific query params, extract them\r\n let specificQueryParams: string | undefined;\r\n if (hasQueryParams) {\r\n const queryPart = path.split(\"?\")[1];\r\n // Sort query params for consistent comparison\r\n specificQueryParams = queryPart\r\n .split(\"&\")\r\n .filter((p) => !p.startsWith(\"__fw_data=\"))\r\n .sort()\r\n .join(\"&\");\r\n }\r\n \r\n // Iterate only over keys for this path (much smaller set)\r\n const keysToDelete: string[] = [];\r\n for (const key of keysForPath) {\r\n // If specific query params were specified, check if they match\r\n if (hasQueryParams && specificQueryParams) {\r\n const [, keyQuery = \"\"] = key.split(\"?\");\r\n const keyQueryParams = keyQuery\r\n .split(\"&\")\r\n .filter((p) => !p.startsWith(\"__fw_data=\"))\r\n .sort()\r\n .join(\"&\");\r\n \r\n if (keyQueryParams === specificQueryParams) {\r\n keysToDelete.push(key);\r\n }\r\n } else {\r\n // If no specific query params, revalidate all variants\r\n keysToDelete.push(key);\r\n }\r\n }\r\n \r\n // Delete matching entries\r\n keysToDelete.forEach((key) => {\r\n deleteCacheEntry(key);\r\n });\r\n \r\n // If the revalidated path matches the current route, automatically refresh data\r\n // UNLESS skipAutoRevalidate is true (to prevent recursive calls from revalidate())\r\n if (!skipAutoRevalidate && typeof window !== \"undefined\") {\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n const matchesCurrentPath = normalizedPath === currentPathname;\r\n \r\n if (matchesCurrentPath) {\r\n if (hasQueryParams && specificQueryParams) {\r\n const currentQueryParams = currentSearch\r\n .replace(\"?\", \"\")\r\n .split(\"&\")\r\n .filter((p) => !p.startsWith(\"__fw_data=\"))\r\n .sort()\r\n .join(\"&\");\r\n \r\n if (currentQueryParams === specificQueryParams) {\r\n revalidate().catch((err) => {\r\n console.error(\r\n \"[client][cache] Error revalidating current route:\",\r\n err\r\n );\r\n });\r\n }\r\n } else {\r\n revalidate().catch((err) => {\r\n console.error(\r\n \"[client][cache] Error revalidating current route:\",\r\n err\r\n );\r\n });\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Revalidates and refreshes the current page data.\r\n * Similar to Next.js's `router.refresh()`.\r\n * \r\n * This function:\r\n * 1. Removes the current route from cache\r\n * 2. Fetches fresh data from the server\r\n * 3. Updates window.__FW_DATA__ with the new data\r\n * 4. Dispatches a 'fw-data-refresh' event for components to listen to\r\n * \r\n * @returns Promise that resolves with the fresh route data\r\n * \r\n * @example\r\n * ```ts\r\n * // Refresh current page data after a mutation\r\n * await revalidate();\r\n * ```\r\n */\r\n// Flag to prevent recursive calls to revalidate()\r\nlet isRevalidating = false;\r\n\r\nexport async function revalidate(): Promise<RouteData> {\r\n if (typeof window === \"undefined\") {\r\n throw new Error(\"revalidate() can only be called on the client\");\r\n }\r\n\r\n // Prevent multiple simultaneous revalidations\r\n if (isRevalidating) {\r\n // Wait for the current revalidation to complete\r\n const key = buildDataUrl(window.location.pathname + window.location.search);\r\n const entry = dataCache.get(key);\r\n if (entry && entry.status === \"pending\") {\r\n return entry.promise;\r\n }\r\n // If no pending entry, something went wrong, allow the call\r\n }\r\n\r\n isRevalidating = true;\r\n try {\r\n const pathname = window.location.pathname + window.location.search;\r\n \r\n // Revalidate the path (remove from cache)\r\n // Pass a flag to prevent revalidatePath from calling revalidate() again (recursive call)\r\n revalidatePath(pathname, true); // true = skip auto-revalidate\r\n \r\n // Fetch fresh data\r\n const freshData = await getRouteData(pathname, { revalidate: true });\r\n \r\n // Update window.__FW_DATA__ if it exists\r\n if ((window as any).__FW_DATA__ && freshData.ok && freshData.json) {\r\n const currentData = (window as any).__FW_DATA__;\r\n \r\n // Update preserved layout props if new ones were returned\r\n if (freshData.json.layoutProps !== undefined && freshData.json.layoutProps !== null) {\r\n (window as any).__FW_LAYOUT_PROPS__ = freshData.json.layoutProps;\r\n }\r\n \r\n // Combine layout props (new or preserved) + page props\r\n let combinedProps = currentData.props || {};\r\n if (freshData.json.layoutProps !== undefined && freshData.json.layoutProps !== null) {\r\n // Use new layout props\r\n combinedProps = {\r\n ...freshData.json.layoutProps,\r\n ...(freshData.json.pageProps ?? freshData.json.props ?? {}),\r\n };\r\n } else if (freshData.json.pageProps !== undefined) {\r\n // Use preserved layout props + new page props\r\n const preservedLayoutProps = (window as any).__FW_LAYOUT_PROPS__ || {};\r\n combinedProps = {\r\n ...preservedLayoutProps,\r\n ...freshData.json.pageProps,\r\n };\r\n } else if (freshData.json.props) {\r\n // Fallback to combined props\r\n combinedProps = freshData.json.props;\r\n }\r\n \r\n (window as any).__FW_DATA__ = {\r\n ...currentData,\r\n pathname: pathname.split(\"?\")[0],\r\n params: freshData.json.params || currentData.params || {},\r\n props: combinedProps,\r\n metadata: freshData.json.metadata ?? currentData.metadata ?? null,\r\n notFound: freshData.json.notFound ?? false,\r\n error: freshData.json.error ?? false,\r\n };\r\n \r\n // Dispatch event for components to listen to\r\n window.dispatchEvent(new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data: freshData },\r\n }));\r\n }\r\n \r\n return freshData;\r\n } finally {\r\n isRevalidating = false;\r\n }\r\n}\r\n\r\n/**\r\n * @deprecated Use `revalidatePath()` instead. This function is kept for backwards compatibility.\r\n */\r\nexport function revalidateRouteData(url: string): void {\r\n revalidatePath(url);\r\n}\r\n\r\nexport function prefetchRouteData(url: string): void {\r\n const key = buildDataUrl(url);\r\n\r\n const cached = dataCache.get(key);\r\n\r\n if (cached && cached.status !== \"rejected\") {\r\n // Update LRU if it exists and is fulfilled\r\n if (cached.status === \"fulfilled\") {\r\n updateLRU(key);\r\n }\r\n return;\r\n }\r\n\r\n // Prefetch uses skipLayoutHooks: true (normal navigation behavior)\r\n const promise = fetchRouteDataOnce(url, true)\r\n .then((value) => {\r\n setCacheEntry(key, { status: \"fulfilled\", value });\r\n return value;\r\n })\r\n .catch((error) => {\r\n console.error(\"[client][cache] Error prefetching route data:\", error);\r\n dataCache.set(key, { status: \"rejected\", error });\r\n throw error;\r\n });\r\n\r\n dataCache.set(key, { status: \"pending\", promise });\r\n}\r\n\r\nexport type GetRouteDataOptions = {\r\n /**\r\n * If true, forces revalidation of route data,\r\n * ignoring the cache and fetching fresh data from the server.\r\n * Similar to Next.js's `router.refresh()` behavior.\r\n */\r\n revalidate?: boolean;\r\n};\r\n\r\nexport async function getRouteData(\r\n url: string,\r\n options?: GetRouteDataOptions\r\n): Promise<RouteData> {\r\n const key = buildDataUrl(url);\r\n\r\n // If revalidation is requested, remove the entry from cache\r\n // This ensures we don't reuse pending or fulfilled entries\r\n if (options?.revalidate) {\r\n deleteCacheEntry(key);\r\n }\r\n\r\n const entry = dataCache.get(key);\r\n\r\n if (entry && !options?.revalidate) {\r\n // Only use cached entry if not revalidating\r\n if (entry.status === \"fulfilled\") {\r\n // Update LRU: mark as recently used\r\n updateLRU(key);\r\n return entry.value;\r\n }\r\n if (entry.status === \"pending\") {\r\n // Return existing pending promise to avoid duplicate requests\r\n return entry.promise;\r\n }\r\n }\r\n\r\n // No entry in cache (or revalidating), fetch it\r\n // skipLayoutHooks: true for normal SPA navigation, false when revalidating\r\n const skipLayoutHooks = !options?.revalidate;\r\n \r\n // Check again if an entry was added while we were processing (race condition)\r\n const currentEntry = dataCache.get(key);\r\n if (currentEntry && !options?.revalidate) {\r\n if (currentEntry.status === \"fulfilled\") {\r\n updateLRU(key);\r\n return currentEntry.value;\r\n }\r\n if (currentEntry.status === \"pending\") {\r\n return currentEntry.promise;\r\n }\r\n }\r\n \r\n // Create a new promise for this fetch\r\n const promise = fetchRouteDataOnce(url, skipLayoutHooks)\r\n .then((value) => {\r\n // Only set cache entry if this is still the current fetch for this key\r\n // This prevents race conditions where multiple revalidations happen simultaneously\r\n const entryAfterFetch = dataCache.get(key);\r\n if (!entryAfterFetch || entryAfterFetch.status === \"pending\") {\r\n setCacheEntry(key, { status: \"fulfilled\", value });\r\n }\r\n return value;\r\n })\r\n .catch((error) => {\r\n console.error(\"[client][cache] Error fetching route data:\", error);\r\n const entryAfterFetch = dataCache.get(key);\r\n if (!entryAfterFetch || entryAfterFetch.status === \"pending\") {\r\n dataCache.set(key, { status: \"rejected\", error });\r\n }\r\n throw error;\r\n });\r\n\r\n // Set pending entry - if revalidating, we already deleted it, so this is safe\r\n dataCache.set(key, { status: \"pending\", promise });\r\n \r\n return promise;\r\n}\r\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"]}
@@ -21,7 +21,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var hooks_exports = {};
22
22
  __export(hooks_exports, {
23
23
  useBroadcastChannel: () => useBroadcastChannel,
24
- usePageProps: () => usePageProps,
25
24
  useRouter: () => useRouter
26
25
  });
27
26
  module.exports = __toCommonJS(hooks_exports);
@@ -30,65 +29,43 @@ module.exports = __toCommonJS(hooks_exports);
30
29
  var import_react = require("react");
31
30
  var useBroadcastChannel = (channelName) => {
32
31
  const [message, setMessage] = (0, import_react.useState)(null);
33
- const channel = new BroadcastChannel(channelName);
32
+ const channelRef = (0, import_react.useRef)(null);
34
33
  (0, import_react.useEffect)(() => {
34
+ if (!channelRef.current && typeof window !== "undefined") {
35
+ channelRef.current = new BroadcastChannel(channelName);
36
+ }
37
+ const channel = channelRef.current;
38
+ if (!channel) return;
35
39
  const handleMessage = (event) => {
36
40
  setMessage(event.data);
37
41
  };
38
42
  channel.onmessage = handleMessage;
39
43
  return () => {
40
- channel.close();
41
- };
42
- }, [channel]);
43
- const sendMessage = (msg) => {
44
- channel.postMessage(msg);
45
- };
46
- return { message, sendMessage };
47
- };
48
-
49
- // modules/react/hooks/usePageProps/index.ts
50
- var import_react2 = require("react");
51
- function usePageProps() {
52
- const [state, setState] = (0, import_react2.useState)(() => {
53
- if (typeof window !== "undefined" && window?.__FW_DATA__) {
54
- const data = window.__FW_DATA__;
55
- return {
56
- params: data.params || {},
57
- props: data.props || {}
58
- };
59
- }
60
- return {
61
- params: {},
62
- props: {}
63
- };
64
- });
65
- (0, import_react2.useEffect)(() => {
66
- const handleDataRefresh = () => {
67
- if (window?.__FW_DATA__) {
68
- const data = window.__FW_DATA__;
69
- setState({
70
- params: data.params || {},
71
- props: data.props || {}
72
- });
44
+ if (channelRef.current) {
45
+ channelRef.current.close();
46
+ channelRef.current = null;
73
47
  }
74
48
  };
75
- window.addEventListener("fw-data-refresh", handleDataRefresh);
76
- return () => {
77
- window.removeEventListener("fw-data-refresh", handleDataRefresh);
78
- };
49
+ }, [channelName]);
50
+ const sendMessage = (0, import_react.useCallback)((msg) => {
51
+ if (channelRef.current) {
52
+ channelRef.current.postMessage(msg);
53
+ }
79
54
  }, []);
80
- return { params: state.params, props: state.props };
81
- }
55
+ return { message, sendMessage };
56
+ };
82
57
 
83
58
  // modules/react/hooks/useRouter/index.ts
84
- var import_react4 = require("react");
59
+ var import_react3 = require("react");
85
60
 
86
61
  // modules/runtime/client/RouterContext.tsx
87
- var import_react3 = require("react");
88
- var RouterContext = (0, import_react3.createContext)(null);
62
+ var import_react2 = require("react");
63
+ var RouterContext = (0, import_react2.createContext)(null);
89
64
 
90
65
  // modules/runtime/client/constants.ts
91
66
  var WINDOW_DATA_KEY = "__FW_DATA__";
67
+ var ROUTER_DATA_KEY = "__LOLY_ROUTER_DATA__";
68
+ var ROUTER_NAVIGATE_KEY = "__LOLY_ROUTER_NAVIGATE__";
92
69
 
93
70
  // modules/runtime/client/window-data.ts
94
71
  function getWindowData() {
@@ -97,94 +74,167 @@ function getWindowData() {
97
74
  }
98
75
  return window[WINDOW_DATA_KEY] ?? null;
99
76
  }
77
+ function getRouterData() {
78
+ if (typeof window === "undefined") {
79
+ return null;
80
+ }
81
+ return window[ROUTER_DATA_KEY] ?? null;
82
+ }
100
83
 
101
84
  // modules/react/hooks/useRouter/index.ts
102
85
  function useRouter() {
103
- const context = (0, import_react4.useContext)(RouterContext);
86
+ const context = (0, import_react3.useContext)(RouterContext);
104
87
  const navigate = context?.navigate;
105
- const [routeData, setRouteData] = (0, import_react4.useState)(() => {
88
+ const navigateRef = (0, import_react3.useRef)(navigate);
89
+ (0, import_react3.useEffect)(() => {
90
+ navigateRef.current = navigate;
91
+ }, [navigate]);
92
+ const [routeData, setRouteData] = (0, import_react3.useState)(() => {
106
93
  if (typeof window === "undefined") {
107
94
  return {
108
95
  pathname: "",
109
96
  query: {},
97
+ searchParams: {},
110
98
  params: {}
111
99
  };
112
100
  }
113
101
  const data = getWindowData();
102
+ const routerData = getRouterData();
103
+ const searchParams = routerData?.searchParams || parseQueryString(window.location.search);
114
104
  return {
115
- pathname: data?.pathname || window.location.pathname,
116
- query: parseQueryString(window.location.search),
117
- params: data?.params || {}
105
+ pathname: routerData?.pathname || data?.pathname || window.location.pathname,
106
+ query: searchParams,
107
+ // For backward compatibility
108
+ searchParams,
109
+ params: routerData?.params || data?.params || {}
118
110
  };
119
111
  });
120
- (0, import_react4.useEffect)(() => {
112
+ (0, import_react3.useEffect)(() => {
121
113
  if (typeof window === "undefined") return;
122
114
  const handleDataRefresh = () => {
123
115
  const data = getWindowData();
116
+ const routerData = getRouterData();
124
117
  const currentPathname = window.location.pathname;
125
118
  const currentSearch = window.location.search;
119
+ const searchParams = routerData?.searchParams || parseQueryString(currentSearch);
126
120
  setRouteData({
127
- pathname: data?.pathname || currentPathname,
128
- query: parseQueryString(currentSearch),
129
- params: data?.params || {}
121
+ pathname: routerData?.pathname || data?.pathname || currentPathname,
122
+ query: searchParams,
123
+ // For backward compatibility
124
+ searchParams,
125
+ params: routerData?.params || data?.params || {}
130
126
  });
131
127
  };
132
128
  window.addEventListener("fw-data-refresh", handleDataRefresh);
129
+ window.addEventListener("fw-router-data-refresh", handleDataRefresh);
133
130
  const handlePopState = () => {
134
131
  handleDataRefresh();
135
132
  };
136
133
  window.addEventListener("popstate", handlePopState);
137
134
  return () => {
138
135
  window.removeEventListener("fw-data-refresh", handleDataRefresh);
136
+ window.removeEventListener("fw-router-data-refresh", handleDataRefresh);
139
137
  window.removeEventListener("popstate", handlePopState);
140
138
  };
141
139
  }, []);
142
- const push = (0, import_react4.useCallback)(
140
+ const push = (0, import_react3.useCallback)(
143
141
  async (url, options) => {
144
142
  const fullUrl = url.startsWith("/") ? url : `/${url}`;
145
- if (!navigate) {
146
- if (typeof window !== "undefined") {
147
- window.location.href = fullUrl;
143
+ const getCurrentNavigate = () => {
144
+ if (navigateRef.current) return navigateRef.current;
145
+ if (navigate) return navigate;
146
+ if (typeof window !== "undefined" && window[ROUTER_NAVIGATE_KEY]) {
147
+ return window[ROUTER_NAVIGATE_KEY];
148
148
  }
149
+ return null;
150
+ };
151
+ let currentNavigate = getCurrentNavigate();
152
+ if (typeof window === "undefined") {
153
+ return;
154
+ }
155
+ if (!currentNavigate) {
156
+ for (let i = 0; i < 10; i++) {
157
+ await new Promise((resolve) => setTimeout(resolve, 10));
158
+ currentNavigate = getCurrentNavigate();
159
+ if (currentNavigate) break;
160
+ }
161
+ }
162
+ if (!currentNavigate) {
163
+ window.location.href = fullUrl;
149
164
  return;
150
165
  }
151
- if (typeof window !== "undefined") {
152
- window.history.pushState({}, "", fullUrl);
166
+ const currentUrl = window.location.pathname + window.location.search;
167
+ if (fullUrl === currentUrl) {
168
+ return;
153
169
  }
154
- await navigate(fullUrl, options);
170
+ window.history.pushState({}, "", fullUrl);
171
+ await currentNavigate(fullUrl, options);
155
172
  },
156
173
  [navigate]
174
+ // Include navigate in dependencies so it updates when context becomes available
157
175
  );
158
- const replace = (0, import_react4.useCallback)(
176
+ const replace = (0, import_react3.useCallback)(
159
177
  async (url, options) => {
160
178
  const fullUrl = url.startsWith("/") ? url : `/${url}`;
161
- if (!navigate) {
162
- if (typeof window !== "undefined") {
163
- window.location.replace(fullUrl);
179
+ const getCurrentNavigate = () => {
180
+ if (navigateRef.current) return navigateRef.current;
181
+ if (navigate) return navigate;
182
+ if (typeof window !== "undefined" && window[ROUTER_NAVIGATE_KEY]) {
183
+ return window[ROUTER_NAVIGATE_KEY];
164
184
  }
185
+ return null;
186
+ };
187
+ let currentNavigate = getCurrentNavigate();
188
+ if (typeof window === "undefined") {
165
189
  return;
166
190
  }
167
- if (typeof window !== "undefined") {
168
- window.history.replaceState({}, "", fullUrl);
191
+ if (!currentNavigate) {
192
+ for (let i = 0; i < 10; i++) {
193
+ await new Promise((resolve) => setTimeout(resolve, 10));
194
+ currentNavigate = getCurrentNavigate();
195
+ if (currentNavigate) break;
196
+ }
197
+ }
198
+ if (!currentNavigate) {
199
+ window.location.replace(fullUrl);
200
+ return;
169
201
  }
170
- await navigate(fullUrl, options);
202
+ window.history.replaceState({}, "", fullUrl);
203
+ await currentNavigate(fullUrl, options);
171
204
  },
172
205
  [navigate]
173
206
  );
174
- const back = (0, import_react4.useCallback)(() => {
207
+ const back = (0, import_react3.useCallback)(() => {
175
208
  if (typeof window !== "undefined") {
176
209
  window.history.back();
177
210
  }
178
211
  }, []);
179
- const refresh = (0, import_react4.useCallback)(async () => {
212
+ const refresh = (0, import_react3.useCallback)(async () => {
180
213
  const currentUrl = typeof window !== "undefined" ? window.location.pathname + window.location.search : routeData.pathname;
181
- if (!navigate) {
182
- if (typeof window !== "undefined") {
183
- window.location.reload();
214
+ const getCurrentNavigate = () => {
215
+ if (navigateRef.current) return navigateRef.current;
216
+ if (navigate) return navigate;
217
+ if (typeof window !== "undefined" && window[ROUTER_NAVIGATE_KEY]) {
218
+ return window[ROUTER_NAVIGATE_KEY];
184
219
  }
220
+ return null;
221
+ };
222
+ let currentNavigate = getCurrentNavigate();
223
+ if (typeof window === "undefined") {
224
+ return;
225
+ }
226
+ if (!currentNavigate) {
227
+ for (let i = 0; i < 10; i++) {
228
+ await new Promise((resolve) => setTimeout(resolve, 10));
229
+ currentNavigate = getCurrentNavigate();
230
+ if (currentNavigate) break;
231
+ }
232
+ }
233
+ if (!currentNavigate) {
234
+ window.location.reload();
185
235
  return;
186
236
  }
187
- await navigate(currentUrl, { revalidate: true });
237
+ await currentNavigate(currentUrl, { revalidate: true });
188
238
  }, [navigate, routeData.pathname]);
189
239
  return {
190
240
  push,
@@ -193,6 +243,7 @@ function useRouter() {
193
243
  refresh,
194
244
  pathname: routeData.pathname,
195
245
  query: routeData.query,
246
+ searchParams: routeData.searchParams,
196
247
  params: routeData.params
197
248
  };
198
249
  }
@@ -212,7 +263,6 @@ function parseQueryString(search) {
212
263
  // Annotate the CommonJS export names for ESM import in node:
213
264
  0 && (module.exports = {
214
265
  useBroadcastChannel,
215
- usePageProps,
216
266
  useRouter
217
267
  });
218
268
  //# sourceMappingURL=hooks.cjs.map