| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 |
|
| 11 | import { createPath } from "../../router/history.js";
|
| 12 | import { joinPaths } from "../../router/utils.js";
|
| 13 | import { createClientRoutes } from "./routes.js";
|
| 14 | import * as React$1 from "react";
|
| 15 |
|
| 16 | const nextPaths = new Set();
|
| 17 | const discoveredPathsMaxSize = 1e3;
|
| 18 | const discoveredPaths = new Set();
|
| 19 | const URL_LIMIT = 7680;
|
| 20 | function getPathsWithAncestors(paths) {
|
| 21 | let result = new Set();
|
| 22 | paths.forEach((path) => {
|
| 23 | if (!path.startsWith("/")) path = `/${path}`;
|
| 24 | for (let i = 1; i < path.length; i++) if (path[i] === "/") result.add(path.slice(0, i));
|
| 25 | result.add(path);
|
| 26 | });
|
| 27 | return Array.from(result);
|
| 28 | }
|
| 29 | function isFogOfWarEnabled(routeDiscovery, ssr) {
|
| 30 | return routeDiscovery.mode === "lazy" && ssr;
|
| 31 | }
|
| 32 | const MANIFEST_MATCH_COST_LIMIT = 5e4;
|
| 33 | const MANIFEST_MATCH_COST_SEGMENT_THRESHOLD = 10;
|
| 34 | function shouldUseInitialRouteDiscovery(pathname, matchCost = 0) {
|
| 35 | let segmentCount = pathname.split("/").length - 1;
|
| 36 | return Math.max(segmentCount - 1, 0) * pathname.length + 1 > 7680 || segmentCount > MANIFEST_MATCH_COST_SEGMENT_THRESHOLD && segmentCount * matchCost > MANIFEST_MATCH_COST_LIMIT;
|
| 37 | }
|
| 38 | function getPartialManifest({ sri, ...manifest }, router) {
|
| 39 | let routeIds = new Set(router.state.matches.map((m) => m.route.id));
|
| 40 | let segments = router.state.location.pathname.split("/").filter(Boolean);
|
| 41 | let paths = ["/"];
|
| 42 | segments.pop();
|
| 43 | while (segments.length > 0) {
|
| 44 | paths.push(`/${segments.join("/")}`);
|
| 45 | segments.pop();
|
| 46 | }
|
| 47 | paths.forEach((path) => {
|
| 48 | let matches = router.match(path);
|
| 49 | if (matches) matches.forEach((m) => routeIds.add(m.route.id));
|
| 50 | });
|
| 51 | let initialRoutes = [...routeIds].reduce((acc, id) => Object.assign(acc, { [id]: manifest.routes[id] }), {});
|
| 52 | return {
|
| 53 | ...manifest,
|
| 54 | routes: initialRoutes,
|
| 55 | sri: sri ? true : void 0
|
| 56 | };
|
| 57 | }
|
| 58 | function getPatchRoutesOnNavigationFunction(getRouter, manifest, routeModules, ssr, routeDiscovery, isSpaMode, basename, fetchImplementation = (request) => fetch(request), serverOrigin, isApiOnly = false) {
|
| 59 | if (!isFogOfWarEnabled(routeDiscovery, ssr)) return;
|
| 60 | return async ({ path, patch, signal, fetcherKey }) => {
|
| 61 | if (discoveredPaths.has(path)) return;
|
| 62 | let { state } = getRouter();
|
| 63 | await fetchAndApplyManifestPatches([path], fetcherKey ? window.location.href : createPath(state.navigation.location || state.location), manifest, routeModules, ssr, isSpaMode, basename, routeDiscovery.manifestPath, patch, signal, fetchImplementation, serverOrigin, isApiOnly);
|
| 64 | };
|
| 65 | }
|
| 66 | function useFogOFWarDiscovery(router, manifest, routeModules, ssr, routeDiscovery, isSpaMode, fetchImplementation = (request) => fetch(request), serverOrigin, isApiOnly = false) {
|
| 67 | React$1.useEffect(() => {
|
| 68 | if (!isFogOfWarEnabled(routeDiscovery, ssr) || window.navigator?.connection?.saveData === true) return;
|
| 69 | function registerElement(el) {
|
| 70 | let path = el.tagName === "FORM" ? el.getAttribute("action") : el.getAttribute("href");
|
| 71 | if (!path) return;
|
| 72 | let pathname = el.tagName === "A" ? el.pathname : new URL(path, window.location.origin).pathname;
|
| 73 | if (!discoveredPaths.has(pathname)) nextPaths.add(pathname);
|
| 74 | }
|
| 75 | async function fetchPatches() {
|
| 76 | document.querySelectorAll("a[data-discover], form[data-discover]").forEach(registerElement);
|
| 77 | let lazyPaths = Array.from(nextPaths.keys()).filter((path) => {
|
| 78 | if (discoveredPaths.has(path)) {
|
| 79 | nextPaths.delete(path);
|
| 80 | return false;
|
| 81 | }
|
| 82 | return true;
|
| 83 | });
|
| 84 | if (lazyPaths.length === 0) return;
|
| 85 | try {
|
| 86 | await fetchAndApplyManifestPatches(lazyPaths, null, manifest, routeModules, ssr, isSpaMode, router.basename, routeDiscovery.manifestPath, router.patchRoutes, void 0, fetchImplementation, serverOrigin, isApiOnly);
|
| 87 | } catch (e) {
|
| 88 | console.error("Failed to fetch manifest patches", e);
|
| 89 | }
|
| 90 | }
|
| 91 | let debouncedFetchPatches = debounce(fetchPatches, 100);
|
| 92 | fetchPatches();
|
| 93 | let observer = new MutationObserver(() => debouncedFetchPatches());
|
| 94 | observer.observe(document.documentElement, {
|
| 95 | subtree: true,
|
| 96 | childList: true,
|
| 97 | attributes: true,
|
| 98 | attributeFilter: [
|
| 99 | "data-discover",
|
| 100 | "href",
|
| 101 | "action"
|
| 102 | ]
|
| 103 | });
|
| 104 | return () => observer.disconnect();
|
| 105 | }, [
|
| 106 | ssr,
|
| 107 | isSpaMode,
|
| 108 | manifest,
|
| 109 | routeModules,
|
| 110 | router,
|
| 111 | routeDiscovery,
|
| 112 | fetchImplementation,
|
| 113 | serverOrigin,
|
| 114 | isApiOnly
|
| 115 | ]);
|
| 116 | }
|
| 117 | function getManifestPathsParam(paths) {
|
| 118 | return paths.slice().sort().map((path) => path.replace(/,/g, "%2C")).join(",");
|
| 119 | }
|
| 120 | function getManifestPath(_manifestPath, basename) {
|
| 121 | let manifestPath = _manifestPath || "/__manifest";
|
| 122 | return basename == null ? manifestPath : joinPaths([basename, manifestPath]);
|
| 123 | }
|
| 124 | const MANIFEST_VERSION_STORAGE_KEY = "react-router-manifest-version";
|
| 125 | const MANIFEST_RELOAD_TIMEOUT = 5e3;
|
| 126 | let pendingManifestReload;
|
| 127 | async function handleClientVersionMismatch(needsReload, version, errorReloadPath) {
|
| 128 | if (pendingManifestReload) return pendingManifestReload;
|
| 129 | if (!needsReload) {
|
| 130 | try {
|
| 131 | sessionStorage.removeItem(MANIFEST_VERSION_STORAGE_KEY);
|
| 132 | } catch {}
|
| 133 | return false;
|
| 134 | }
|
| 135 | if (!errorReloadPath) {
|
| 136 | console.warn("Detected a manifest version mismatch during eager route discovery. The next navigation/fetch to an undiscovered route will result in a new document navigation to sync up with the latest manifest.");
|
| 137 | return true;
|
| 138 | }
|
| 139 | let alreadyReloaded = false;
|
| 140 | try {
|
| 141 | alreadyReloaded = sessionStorage.getItem(MANIFEST_VERSION_STORAGE_KEY) === version;
|
| 142 | if (!alreadyReloaded) sessionStorage.setItem(MANIFEST_VERSION_STORAGE_KEY, version);
|
| 143 | } catch {}
|
| 144 | if (alreadyReloaded) throw new Error("Unable to discover routes due to manifest version mismatch.");
|
| 145 | let rejectReload;
|
| 146 | let reloadPromise = new Promise((_, reject) => {
|
| 147 | rejectReload = reject;
|
| 148 | });
|
| 149 | pendingManifestReload = reloadPromise;
|
| 150 | let failReload = () => {
|
| 151 | clearTimeout(timeout);
|
| 152 | window.removeEventListener("pageshow", onPageShow);
|
| 153 | pendingManifestReload = void 0;
|
| 154 | rejectReload( new Error("Unable to discover routes due to manifest version mismatch."));
|
| 155 | };
|
| 156 | let timeout = setTimeout(failReload, MANIFEST_RELOAD_TIMEOUT);
|
| 157 | let onPageShow = (event) => {
|
| 158 | if (event.persisted) failReload();
|
| 159 | };
|
| 160 | window.addEventListener("pageshow", onPageShow);
|
| 161 | try {
|
| 162 | window.location.href = errorReloadPath;
|
| 163 | console.warn("Detected manifest version mismatch, reloading...");
|
| 164 | } catch {
|
| 165 | failReload();
|
| 166 | }
|
| 167 | return reloadPromise;
|
| 168 | }
|
| 169 | async function fetchAndApplyManifestPatches(paths, errorReloadPath, manifest, routeModules, ssr, isSpaMode, basename, manifestPath, patchRoutes, signal, fetchImplementation = (request) => fetch(request), serverOrigin, isApiOnly = false) {
|
| 170 | paths = getPathsWithAncestors(paths);
|
| 171 | const searchParams = new URLSearchParams();
|
| 172 | searchParams.set("paths", getManifestPathsParam(paths));
|
| 173 | searchParams.set("version", manifest.version);
|
| 174 | let url = new URL(getManifestPath(manifestPath, basename), serverOrigin ?? window.location.origin);
|
| 175 | url.search = searchParams.toString();
|
| 176 | if (url.toString().length > 7680) {
|
| 177 | nextPaths.clear();
|
| 178 | return;
|
| 179 | }
|
| 180 | let serverPatches;
|
| 181 | try {
|
| 182 | let res = await fetchImplementation(new Request(url, { signal }), { type: "manifest" });
|
| 183 | if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
| 184 | if (await handleClientVersionMismatch(res.status === 204 && res.headers.has("X-Remix-Reload-Document"), manifest.version, errorReloadPath)) return;
|
| 185 | serverPatches = await res.json();
|
| 186 | } catch (e) {
|
| 187 | if (signal?.aborted) return;
|
| 188 | throw e;
|
| 189 | }
|
| 190 | if (signal?.aborted) return;
|
| 191 | let knownRoutes = new Set(Object.keys(manifest.routes));
|
| 192 | let patches = Object.values(serverPatches).reduce((acc, route) => {
|
| 193 | if (route && !knownRoutes.has(route.id)) acc[route.id] = route;
|
| 194 | return acc;
|
| 195 | }, {});
|
| 196 | Object.assign(manifest.routes, patches);
|
| 197 | paths.forEach((p) => addToFifoQueue(p, discoveredPaths));
|
| 198 | let parentIds = new Set();
|
| 199 | Object.values(patches).forEach((patch) => {
|
| 200 | if (patch && (!patch.parentId || !patches[patch.parentId])) parentIds.add(patch.parentId);
|
| 201 | });
|
| 202 | parentIds.forEach((parentId) => patchRoutes(parentId || null, createClientRoutes(patches, routeModules, null, ssr, isSpaMode, parentId, void 0, void 0, isApiOnly)));
|
| 203 | }
|
| 204 | function addToFifoQueue(path, queue) {
|
| 205 | if (queue.size >= discoveredPathsMaxSize) {
|
| 206 | let first = queue.values().next().value;
|
| 207 | if (first !== void 0) queue.delete(first);
|
| 208 | }
|
| 209 | queue.add(path);
|
| 210 | }
|
| 211 | function debounce(callback, wait) {
|
| 212 | let timeoutId;
|
| 213 | return (...args) => {
|
| 214 | window.clearTimeout(timeoutId);
|
| 215 | timeoutId = window.setTimeout(() => callback(...args), wait);
|
| 216 | };
|
| 217 | }
|
| 218 |
|
| 219 | export { URL_LIMIT, getManifestPath, getManifestPathsParam, getPartialManifest, getPatchRoutesOnNavigationFunction, getPathsWithAncestors, handleClientVersionMismatch, isFogOfWarEnabled, shouldUseInitialRouteDiscovery, useFogOFWarDiscovery };
|