UNPKG

react-router

Version:
1,167 lines • 122 kB
/** * react-router v8.0.0 * * Copyright (c) Remix Software Inc. * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ import { PROTOCOL_RELATIVE_URL_REGEX, normalizeProtocolRelativeUrl } from "./url.js"; import { createBrowserURLImpl, createLocation, createPath, invariant, parsePath, warning } from "./history.js"; import { ErrorResponseImpl, RouterContextProvider, convertRouteMatchToUiMatch, convertRoutesToDataRoutes, flattenAndRankRoutes, getPathContributingMatches, getResolveToMatches, getRoutePattern, isAbsoluteUrl, isRouteErrorResponse, isUnsupportedLazyRouteFunctionKey, isUnsupportedLazyRouteObjectKey, matchRoutesImpl, prependBasename, removeDoubleSlashes, resolveTo, stripBasename } from "./utils.js"; import { getRouteInstrumentationUpdates, instrumentClientSideRouter } from "./instrumentation.js"; //#region lib/router/router.ts const validMutationMethodsArr = [ "POST", "PUT", "PATCH", "DELETE" ]; const validMutationMethods = new Set(validMutationMethodsArr); const validRequestMethodsArr = ["GET", ...validMutationMethodsArr]; const validRequestMethods = new Set(validRequestMethodsArr); const redirectStatusCodes = new Set([ 301, 302, 303, 307, 308 ]); const redirectPreserveMethodStatusCodes = new Set([307, 308]); const IDLE_NAVIGATION = { state: "idle", location: void 0, matches: void 0, historyAction: void 0, formMethod: void 0, formAction: void 0, formEncType: void 0, formData: void 0, json: void 0, text: void 0 }; const IDLE_FETCHER = { state: "idle", data: void 0, formMethod: void 0, formAction: void 0, formEncType: void 0, formData: void 0, json: void 0, text: void 0 }; const IDLE_BLOCKER = { state: "unblocked", proceed: void 0, reset: void 0, location: void 0 }; const TRANSITIONS_STORAGE_KEY = "remix-router-transitions"; const ResetLoaderDataSymbol = Symbol("ResetLoaderData"); /** * Encapsulates the stable and in-flight route trees together with their * pre-computed branch caches so the structures always stay in sync. */ var DataRoutes = class { #routes; #branches; #hmrRoutes; #hmrBranches; constructor(routes) { this.#routes = routes; this.#branches = flattenAndRankRoutes(routes); } /** The stable route tree */ get stableRoutes() { return this.#routes; } /** The in-flight route tree if one is active, otherwise the stable tree */ get activeRoutes() { return this.#hmrRoutes ?? this.#routes; } /** Pre-computed branches */ get branches() { return this.#hmrBranches ?? this.#branches; } get hasHMRRoutes() { return this.#hmrRoutes != null; } /** Replace the stable route tree and recompute its branches */ setRoutes(routes) { this.#routes = routes; this.#branches = flattenAndRankRoutes(routes); } /** Set a new in-flight route tree and recompute its branches */ setHmrRoutes(routes) { this.#hmrRoutes = routes; this.#hmrBranches = flattenAndRankRoutes(routes); } /** Commit in-flight routes/branches to the stable slot and clear in-flight */ commitHmrRoutes() { if (this.#hmrRoutes) { this.#routes = this.#hmrRoutes; this.#branches = this.#hmrBranches; this.#hmrRoutes = void 0; this.#hmrBranches = void 0; } } }; /** * Create a router and listen to history POP navigations */ function createRouter(init) { const routerWindow = init.window ? init.window : typeof window !== "undefined" ? window : void 0; const isBrowser = typeof routerWindow !== "undefined" && typeof routerWindow.document !== "undefined" && typeof routerWindow.document.createElement !== "undefined"; invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter"); let hydrationRouteProperties = init.hydrationRouteProperties || []; let _mapRouteProperties = init.mapRouteProperties; let mapRouteProperties = _mapRouteProperties ? _mapRouteProperties : () => ({}); if (init.instrumentations) { let instrumentations = init.instrumentations; mapRouteProperties = (route) => { return { ..._mapRouteProperties?.(route), ...getRouteInstrumentationUpdates(instrumentations.map((i) => i.route).filter(Boolean), route) }; }; } let manifest = {}; let dataRoutes = new DataRoutes(convertRoutesToDataRoutes(init.routes, mapRouteProperties, void 0, manifest)); let basename = init.basename || "/"; if (!basename.startsWith("/")) basename = `/${basename}`; let dataStrategyImpl = init.dataStrategy || defaultDataStrategyWithMiddleware; let future = { ...init.future }; let unlistenHistory = null; let subscribers = /* @__PURE__ */ new Set(); let bufferedInitialStateUpdate = null; let savedScrollPositions = null; let getScrollRestorationKey = null; let getScrollPosition = null; let initialScrollRestored = init.hydrationData != null; let initialMatches = matchRoutesImpl(dataRoutes.activeRoutes, init.history.location, basename, false, dataRoutes.branches); let initialMatchesIsFOW = false; let initialErrors = null; let initialized; let renderFallback; if (initialMatches == null && !init.patchRoutesOnNavigation) { let error = getInternalRouterError(404, { pathname: init.history.location.pathname }); let { matches, route } = getShortCircuitMatches(dataRoutes.activeRoutes); initialized = true; renderFallback = !initialized; initialMatches = matches; initialErrors = { [route.id]: error }; } else { if (initialMatches && !init.hydrationData) { if (checkFogOfWar(initialMatches, dataRoutes.activeRoutes, init.history.location.pathname).active) initialMatches = null; } if (!initialMatches) { initialized = false; renderFallback = !initialized; initialMatches = []; let fogOfWar = checkFogOfWar(null, dataRoutes.activeRoutes, init.history.location.pathname); if (fogOfWar.active && fogOfWar.matches) { initialMatchesIsFOW = true; initialMatches = fogOfWar.matches; } } else if (initialMatches.some((m) => m.route.lazy)) { initialized = false; renderFallback = !initialized; } else if (!initialMatches.some((m) => routeHasLoaderOrMiddleware(m.route))) { initialized = true; renderFallback = !initialized; } else { let loaderData = init.hydrationData ? init.hydrationData.loaderData : null; let errors = init.hydrationData ? init.hydrationData.errors : null; let relevantMatches = initialMatches; if (errors) { let idx = initialMatches.findIndex((m) => errors[m.route.id] !== void 0); relevantMatches = relevantMatches.slice(0, idx + 1); } renderFallback = false; initialized = true; relevantMatches.forEach((m) => { let status = getRouteHydrationStatus(m.route, loaderData, errors); renderFallback = renderFallback || status.renderFallback; initialized = initialized && !status.shouldLoad; }); } } let router; let state = { historyAction: init.history.action, location: init.history.location, matches: initialMatches, initialized, renderFallback, navigation: IDLE_NAVIGATION, restoreScrollPosition: init.hydrationData != null ? false : null, preventScrollReset: false, revalidation: "idle", loaderData: init.hydrationData && init.hydrationData.loaderData || {}, actionData: init.hydrationData && init.hydrationData.actionData || null, errors: init.hydrationData && init.hydrationData.errors || initialErrors, fetchers: /* @__PURE__ */ new Map(), blockers: /* @__PURE__ */ new Map() }; let pendingAction = "POP"; let pendingPopstateNavigationDfd = null; let pendingPreventScrollReset = false; let pendingNavigationController; let pendingViewTransitionEnabled = false; let appliedViewTransitions = /* @__PURE__ */ new Map(); let removePageHideEventListener = null; let isUninterruptedRevalidation = false; let isRevalidationRequired = false; let cancelledFetcherLoads = /* @__PURE__ */ new Set(); let fetchControllers = /* @__PURE__ */ new Map(); let incrementingLoadId = 0; let pendingNavigationLoadId = -1; let fetchReloadIds = /* @__PURE__ */ new Map(); let fetchRedirectIds = /* @__PURE__ */ new Set(); let fetchLoadMatches = /* @__PURE__ */ new Map(); let activeFetchers = /* @__PURE__ */ new Map(); let fetchersQueuedForDeletion = /* @__PURE__ */ new Set(); let blockerFunctions = /* @__PURE__ */ new Map(); let unblockBlockerHistoryUpdate = void 0; let pendingRevalidationDfd = null; function initialize() { unlistenHistory = init.history.listen(({ action: historyAction, location, delta }) => { if (unblockBlockerHistoryUpdate) { unblockBlockerHistoryUpdate(); unblockBlockerHistoryUpdate = void 0; return; } warning(blockerFunctions.size === 0 || delta != null, "You are trying to use a blocker on a POP navigation to a location that was not created by @remix-run/router. This will fail silently in production. This can happen if you are navigating outside the router via `window.history.pushState`/`window.location.hash` instead of using router navigation APIs. This can also happen if you are using createHashRouter and the user manually changes the URL."); let blockerKey = shouldBlockNavigation({ currentLocation: state.location, nextLocation: location, historyAction }); if (blockerKey && delta != null) { let nextHistoryUpdatePromise = new Promise((resolve) => { unblockBlockerHistoryUpdate = resolve; }); init.history.go(delta * -1); updateBlocker(blockerKey, { state: "blocked", location, proceed() { updateBlocker(blockerKey, { state: "proceeding", proceed: void 0, reset: void 0, location }); nextHistoryUpdatePromise.then(() => init.history.go(delta)); }, reset() { let blockers = new Map(state.blockers); blockers.set(blockerKey, IDLE_BLOCKER); updateState({ blockers }); } }); pendingPopstateNavigationDfd?.resolve(); pendingPopstateNavigationDfd = null; return; } return startNavigation(historyAction, location); }); if (isBrowser) { restoreAppliedTransitions(routerWindow, appliedViewTransitions); let _saveAppliedTransitions = () => persistAppliedTransitions(routerWindow, appliedViewTransitions); routerWindow.addEventListener("pagehide", _saveAppliedTransitions); removePageHideEventListener = () => routerWindow.removeEventListener("pagehide", _saveAppliedTransitions); } if (!state.initialized) startNavigation("POP", state.location, { initialHydration: true }); return router; } function dispose() { if (unlistenHistory) unlistenHistory(); if (removePageHideEventListener) removePageHideEventListener(); subscribers.clear(); pendingNavigationController && pendingNavigationController.abort(); state.fetchers.forEach((_, key) => deleteFetcher(state.fetchers, key)); state.blockers.forEach((_, key) => deleteBlocker(key)); } function subscribe(fn) { subscribers.add(fn); if (bufferedInitialStateUpdate) { let { newErrors } = bufferedInitialStateUpdate; bufferedInitialStateUpdate = null; fn(state, { deletedFetchers: [], newErrors, viewTransitionOpts: void 0, flushSync: false }); } return () => subscribers.delete(fn); } function updateState(newState, opts = {}) { if (newState.matches) newState.matches = newState.matches.map((m) => { let route = manifest[m.route.id]; let matchRoute = m.route; if (matchRoute.element !== route.element || matchRoute.errorElement !== route.errorElement || matchRoute.hydrateFallbackElement !== route.hydrateFallbackElement) return { ...m, route }; return m; }); state = { ...state, ...newState }; let unmountedFetchers = []; let mountedFetchers = []; state.fetchers.forEach((fetcher, key) => { if (fetcher.state === "idle") if (fetchersQueuedForDeletion.has(key)) unmountedFetchers.push(key); else mountedFetchers.push(key); }); fetchersQueuedForDeletion.forEach((key) => { if (!state.fetchers.has(key) && !fetchControllers.has(key)) unmountedFetchers.push(key); }); if (subscribers.size === 0) bufferedInitialStateUpdate = { newErrors: newState.errors ?? null }; [...subscribers].forEach((subscriber) => subscriber(state, { deletedFetchers: unmountedFetchers, newErrors: newState.errors ?? null, viewTransitionOpts: opts.viewTransitionOpts, flushSync: opts.flushSync === true })); unmountedFetchers.forEach((key) => deleteFetcher(state.fetchers, key)); mountedFetchers.forEach((key) => state.fetchers.delete(key)); } function completeNavigation(location, newState, { flushSync } = {}) { let isActionReload = state.actionData != null && state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && state.navigation.state === "loading" && location.state?._isRedirect !== true; let actionData; if (newState.actionData) if (Object.keys(newState.actionData).length > 0) actionData = newState.actionData; else actionData = null; else if (isActionReload) actionData = state.actionData; else actionData = null; let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData; let blockers = state.blockers; if (blockers.size > 0) { blockers = new Map(blockers); blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER)); } let restoreScrollPosition = isUninterruptedRevalidation ? false : getSavedScrollPosition(location, newState.matches || state.matches); let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && location.state?._isRedirect !== true; dataRoutes.commitHmrRoutes(); if (isUninterruptedRevalidation) {} else if (pendingAction === "POP") {} else if (pendingAction === "PUSH") init.history.push(location, location.state); else if (pendingAction === "REPLACE") init.history.replace(location, location.state); let viewTransitionOpts; if (pendingAction === "POP") { let priorPaths = appliedViewTransitions.get(state.location.pathname); if (priorPaths && priorPaths.has(location.pathname)) viewTransitionOpts = { currentLocation: state.location, nextLocation: location }; else if (appliedViewTransitions.has(location.pathname)) viewTransitionOpts = { currentLocation: location, nextLocation: state.location }; } else if (pendingViewTransitionEnabled) { let toPaths = appliedViewTransitions.get(state.location.pathname); if (toPaths) toPaths.add(location.pathname); else { toPaths = new Set([location.pathname]); appliedViewTransitions.set(state.location.pathname, toPaths); } viewTransitionOpts = { currentLocation: state.location, nextLocation: location }; } updateState({ ...newState, actionData, loaderData, historyAction: pendingAction, location, initialized: true, renderFallback: false, navigation: IDLE_NAVIGATION, revalidation: "idle", restoreScrollPosition, preventScrollReset, blockers }, { viewTransitionOpts, flushSync: flushSync === true }); pendingAction = "POP"; pendingPreventScrollReset = false; pendingViewTransitionEnabled = false; isUninterruptedRevalidation = false; isRevalidationRequired = false; pendingPopstateNavigationDfd?.resolve(); pendingPopstateNavigationDfd = null; pendingRevalidationDfd?.resolve(); pendingRevalidationDfd = null; } async function navigate(to, opts) { pendingPopstateNavigationDfd?.resolve(); pendingPopstateNavigationDfd = null; if (typeof to === "number") { if (!pendingPopstateNavigationDfd) pendingPopstateNavigationDfd = createDeferred(); let promise = pendingPopstateNavigationDfd.promise; init.history.go(to); return promise; } let { path, submission, error } = normalizeNavigateOptions(false, normalizeTo(state.location, state.matches, basename, to, opts?.fromRouteId, opts?.relative), opts); let maskPath; if (opts?.mask) maskPath = { pathname: "", search: "", hash: "", ...typeof opts.mask === "string" ? parsePath(opts.mask) : { ...state.location.mask, ...opts.mask } }; let currentLocation = state.location; let nextLocation = createLocation(currentLocation, path, opts && opts.state, void 0, maskPath); nextLocation = { ...nextLocation, ...init.history.encodeLocation(nextLocation) }; let userReplace = opts && opts.replace != null ? opts.replace : void 0; let historyAction = "PUSH"; if (userReplace === true) historyAction = "REPLACE"; else if (userReplace === false) {} else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) historyAction = "REPLACE"; let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : void 0; let flushSync = (opts && opts.flushSync) === true; let blockerKey = shouldBlockNavigation({ currentLocation, nextLocation, historyAction }); if (blockerKey) { updateBlocker(blockerKey, { state: "blocked", location: nextLocation, proceed() { updateBlocker(blockerKey, { state: "proceeding", proceed: void 0, reset: void 0, location: nextLocation }); navigate(to, opts); }, reset() { let blockers = new Map(state.blockers); blockers.set(blockerKey, IDLE_BLOCKER); updateState({ blockers }); } }); return; } await startNavigation(historyAction, nextLocation, { submission, pendingError: error, preventScrollReset, replace: opts && opts.replace, enableViewTransition: opts && opts.viewTransition, flushSync, callSiteDefaultShouldRevalidate: opts && opts.defaultShouldRevalidate }); } function revalidate() { if (!pendingRevalidationDfd) pendingRevalidationDfd = createDeferred(); interruptActiveLoads(); updateState({ revalidation: "loading" }); let promise = pendingRevalidationDfd.promise; if (state.navigation.state === "submitting") return promise; if (state.navigation.state === "idle") { startNavigation(state.historyAction, state.location, { startUninterruptedRevalidation: true }); return promise; } startNavigation(pendingAction || state.historyAction, state.navigation.location, { overrideNavigation: state.navigation, enableViewTransition: pendingViewTransitionEnabled === true }); return promise; } async function startNavigation(historyAction, location, opts) { pendingNavigationController && pendingNavigationController.abort(); pendingNavigationController = null; pendingAction = historyAction; isUninterruptedRevalidation = (opts && opts.startUninterruptedRevalidation) === true; saveScrollPosition(state.location, state.matches); pendingPreventScrollReset = (opts && opts.preventScrollReset) === true; pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true; let routesToUse = dataRoutes.activeRoutes; let matches = opts?.initialHydration && state.matches && state.matches.length > 0 && !initialMatchesIsFOW ? state.matches : matchRoutesImpl(routesToUse, location, basename, false, dataRoutes.branches); let flushSync = (opts && opts.flushSync) === true; if (matches && state.initialized && !isRevalidationRequired && isHashChangeOnly(state.location, location) && !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))) { completeNavigation(location, { matches }, { flushSync }); return; } let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname); if (fogOfWar.active && fogOfWar.matches) matches = fogOfWar.matches; if (!matches) { let { error, notFoundMatches, route } = handleNavigational404(location.pathname); completeNavigation(location, { matches: notFoundMatches, loaderData: {}, errors: { [route.id]: error } }, { flushSync }); return; } let loadingNavigation = opts && opts.overrideNavigation ? { ...opts.overrideNavigation, matches, historyAction } : void 0; pendingNavigationController = new AbortController(); let request = createClientSideRequest(init.history, location, pendingNavigationController.signal, opts && opts.submission); let scopedContext = init.getContext ? await init.getContext() : new RouterContextProvider(); let pendingActionResult; if (opts && opts.pendingError) pendingActionResult = [findNearestBoundary(matches).route.id, { type: "error", error: opts.pendingError }]; else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) { let actionResult = await handleAction(request, location, opts.submission, matches, historyAction, scopedContext, fogOfWar.active, opts && opts.initialHydration === true, { replace: opts.replace, flushSync }); if (actionResult.shortCircuited) return; if (actionResult.pendingActionResult) { let [routeId, result] = actionResult.pendingActionResult; if (isErrorResult(result) && isRouteErrorResponse(result.error) && result.error.status === 404) { pendingNavigationController = null; completeNavigation(location, { matches: actionResult.matches, loaderData: {}, errors: { [routeId]: result.error } }); return; } } matches = actionResult.matches || matches; pendingActionResult = actionResult.pendingActionResult; loadingNavigation = getLoadingNavigation(location, matches, historyAction, opts.submission); flushSync = false; fogOfWar.active = false; request = createClientSideRequest(init.history, request.url, request.signal); } let { shortCircuited, matches: updatedMatches, loaderData, errors, workingFetchers } = await handleLoaders(request, location, matches, historyAction, scopedContext, fogOfWar.active, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionResult, opts && opts.callSiteDefaultShouldRevalidate); if (shortCircuited) return; pendingNavigationController = null; completeNavigation(location, { matches: updatedMatches || matches, ...getActionDataForCommit(pendingActionResult), loaderData, errors, ...workingFetchers ? { fetchers: workingFetchers } : {} }); } async function handleAction(request, location, submission, matches, historyAction, scopedContext, isFogOfWar, initialHydration, opts = {}) { interruptActiveLoads(); updateState({ navigation: getSubmittingNavigation(location, matches, historyAction, submission) }, { flushSync: opts.flushSync === true }); if (isFogOfWar) { let discoverResult = await discoverRoutes(matches, location.pathname, request.signal); if (discoverResult.type === "aborted") return { shortCircuited: true }; else if (discoverResult.type === "error") { if (discoverResult.partialMatches.length === 0) { let { matches, route } = getShortCircuitMatches(dataRoutes.activeRoutes); return { matches, pendingActionResult: [route.id, { type: "error", error: discoverResult.error }] }; } let boundaryId = findNearestBoundary(discoverResult.partialMatches).route.id; return { matches: discoverResult.partialMatches, pendingActionResult: [boundaryId, { type: "error", error: discoverResult.error }] }; } else if (!discoverResult.matches) { let { notFoundMatches, error, route } = handleNavigational404(location.pathname); return { matches: notFoundMatches, pendingActionResult: [route.id, { type: "error", error }] }; } else matches = discoverResult.matches; } let result; let actionMatch = getTargetMatch(matches, location); if (!actionMatch.route.action && !actionMatch.route.lazy) result = { type: "error", error: getInternalRouterError(405, { method: request.method, pathname: location.pathname, routeId: actionMatch.route.id }) }; else { let results = await callDataStrategy(request, location, getTargetedDataStrategyMatches(mapRouteProperties, manifest, request, location, matches, actionMatch, initialHydration ? [] : hydrationRouteProperties, scopedContext), scopedContext, null); result = results[actionMatch.route.id]; if (!result) { for (let match of matches) if (results[match.route.id]) { result = results[match.route.id]; break; } } if (request.signal.aborted) return { shortCircuited: true }; } if (isRedirectResult(result)) { let replace; if (opts && opts.replace != null) replace = opts.replace; else replace = normalizeRedirectLocation(result.response.headers.get("Location"), new URL(request.url), basename, init.history) === state.location.pathname + state.location.search; await startRedirectNavigation(request, result, true, { submission, replace }); return { shortCircuited: true }; } if (isErrorResult(result)) { let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id); if ((opts && opts.replace) !== true) pendingAction = "PUSH"; return { matches, pendingActionResult: [ boundaryMatch.route.id, result, actionMatch.route.id ] }; } return { matches, pendingActionResult: [actionMatch.route.id, result] }; } async function handleLoaders(request, location, matches, historyAction, scopedContext, isFogOfWar, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionResult, callSiteDefaultShouldRevalidate) { let loadingNavigation = overrideNavigation || getLoadingNavigation(location, matches, historyAction, submission); let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation); let shouldUpdateNavigationState = !isUninterruptedRevalidation && !initialHydration; if (isFogOfWar) { if (shouldUpdateNavigationState) { let actionData = getUpdatedActionData(pendingActionResult); updateState({ navigation: loadingNavigation, ...actionData !== void 0 ? { actionData } : {} }, { flushSync }); } let discoverResult = await discoverRoutes(matches, location.pathname, request.signal); if (discoverResult.type === "aborted") return { shortCircuited: true }; else if (discoverResult.type === "error") { if (discoverResult.partialMatches.length === 0) { let { matches, route } = getShortCircuitMatches(dataRoutes.activeRoutes); return { matches, loaderData: {}, errors: { [route.id]: discoverResult.error } }; } let boundaryId = findNearestBoundary(discoverResult.partialMatches).route.id; return { matches: discoverResult.partialMatches, loaderData: {}, errors: { [boundaryId]: discoverResult.error } }; } else if (!discoverResult.matches) { let { error, notFoundMatches, route } = handleNavigational404(location.pathname); return { matches: notFoundMatches, loaderData: {}, errors: { [route.id]: error } }; } else matches = discoverResult.matches; } let routesToUse = dataRoutes.activeRoutes; let { dsMatches, revalidatingFetchers } = getMatchesToLoad(request, scopedContext, mapRouteProperties, manifest, init.history, state, matches, activeSubmission, location, initialHydration ? [] : hydrationRouteProperties, initialHydration === true, isRevalidationRequired, cancelledFetcherLoads, fetchersQueuedForDeletion, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, init.patchRoutesOnNavigation != null, dataRoutes.branches, pendingActionResult, callSiteDefaultShouldRevalidate); pendingNavigationLoadId = ++incrementingLoadId; if (!init.dataStrategy && !dsMatches.some((m) => m.shouldLoad) && !dsMatches.some((m) => m.route.middleware && m.route.middleware.length > 0) && revalidatingFetchers.length === 0) { let workingFetchers = new Map(state.fetchers); let didUpdateFetcherRedirects = markFetchRedirectsDone(workingFetchers); completeNavigation(location, { matches, loaderData: {}, errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? { [pendingActionResult[0]]: pendingActionResult[1].error } : null, ...getActionDataForCommit(pendingActionResult), ...didUpdateFetcherRedirects ? { fetchers: workingFetchers } : {} }, { flushSync }); return { shortCircuited: true }; } if (shouldUpdateNavigationState) { let updates = {}; if (!isFogOfWar) { updates.navigation = loadingNavigation; let actionData = getUpdatedActionData(pendingActionResult); if (actionData !== void 0) updates.actionData = actionData; } if (revalidatingFetchers.length > 0) updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers); updateState(updates, { flushSync }); } revalidatingFetchers.forEach((rf) => { abortFetcher(rf.key); if (rf.controller) fetchControllers.set(rf.key, rf.controller); }); let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach((f) => abortFetcher(f.key)); if (pendingNavigationController) pendingNavigationController.signal.addEventListener("abort", abortPendingFetchRevalidations); let { loaderResults, fetcherResults } = await callLoadersAndMaybeResolveData(dsMatches, revalidatingFetchers, request, location, scopedContext); if (request.signal.aborted) return { shortCircuited: true }; if (pendingNavigationController) pendingNavigationController.signal.removeEventListener("abort", abortPendingFetchRevalidations); revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key)); let redirect = findRedirect(loaderResults); if (redirect) { await startRedirectNavigation(request, redirect.result, true, { replace }); return { shortCircuited: true }; } redirect = findRedirect(fetcherResults); if (redirect) { fetchRedirectIds.add(redirect.key); await startRedirectNavigation(request, redirect.result, true, { replace }); return { shortCircuited: true }; } let workingFetchers = new Map(state.fetchers); let { loaderData, errors } = processLoaderData(state, matches, loaderResults, pendingActionResult, revalidatingFetchers, fetcherResults, workingFetchers); if (initialHydration && state.errors) errors = { ...state.errors, ...errors }; let didUpdateFetcherRedirects = markFetchRedirectsDone(workingFetchers); let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId, workingFetchers); let shouldUpdateFetchers = didUpdateFetcherRedirects || didAbortFetchLoads || revalidatingFetchers.length > 0; return { matches, loaderData, errors, ...shouldUpdateFetchers ? { workingFetchers } : {} }; } function getUpdatedActionData(pendingActionResult) { if (pendingActionResult && !isErrorResult(pendingActionResult[1])) return { [pendingActionResult[0]]: pendingActionResult[1].data }; else if (state.actionData) if (Object.keys(state.actionData).length === 0) return null; else return state.actionData; } function getUpdatedRevalidatingFetchers(revalidatingFetchers) { let workingFetchers = new Map(state.fetchers); revalidatingFetchers.forEach((rf) => { let fetcher = workingFetchers.get(rf.key); let revalidatingFetcher = getLoadingFetcher(void 0, fetcher ? fetcher.data : void 0); workingFetchers.set(rf.key, revalidatingFetcher); }); return workingFetchers; } async function fetch(key, routeId, href, opts) { abortFetcher(key); let flushSync = (opts && opts.flushSync) === true; let routesToUse = dataRoutes.activeRoutes; let normalizedPath = normalizeTo(state.location, state.matches, basename, href, routeId, opts?.relative); let matches = matchRoutesImpl(routesToUse, normalizedPath, basename, false, dataRoutes.branches); let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath); if (fogOfWar.active && fogOfWar.matches) matches = fogOfWar.matches; if (!matches) { setFetcherError(key, routeId, getInternalRouterError(404, { pathname: normalizedPath }), { flushSync }); return; } let { path, submission, error } = normalizeNavigateOptions(true, normalizedPath, opts); if (error) { setFetcherError(key, routeId, error, { flushSync }); return; } let scopedContext = init.getContext ? await init.getContext() : new RouterContextProvider(); let preventScrollReset = (opts && opts.preventScrollReset) === true; if (submission && isMutationMethod(submission.formMethod)) { await handleFetcherAction(key, routeId, path, matches, scopedContext, fogOfWar.active, flushSync, preventScrollReset, submission, opts && opts.defaultShouldRevalidate); return; } fetchLoadMatches.set(key, { routeId, path }); await handleFetcherLoader(key, routeId, path, matches, scopedContext, fogOfWar.active, flushSync, preventScrollReset, submission); } async function handleFetcherAction(key, routeId, path, requestMatches, scopedContext, isFogOfWar, flushSync, preventScrollReset, submission, callSiteDefaultShouldRevalidate) { interruptActiveLoads(); fetchLoadMatches.delete(key); updateFetcherState(key, getSubmittingFetcher(submission, state.fetchers.get(key)), { flushSync }); let abortController = new AbortController(); let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission); if (isFogOfWar) { let discoverResult = await discoverRoutes(requestMatches, new URL(fetchRequest.url).pathname, fetchRequest.signal, key); if (discoverResult.type === "aborted") return; else if (discoverResult.type === "error") { setFetcherError(key, routeId, discoverResult.error, { flushSync }); return; } else if (!discoverResult.matches) { setFetcherError(key, routeId, getInternalRouterError(404, { pathname: path }), { flushSync }); return; } else requestMatches = discoverResult.matches; } let match = getTargetMatch(requestMatches, path); if (!match.route.action && !match.route.lazy) { setFetcherError(key, routeId, getInternalRouterError(405, { method: submission.formMethod, pathname: path, routeId }), { flushSync }); return; } fetchControllers.set(key, abortController); let originatingLoadId = incrementingLoadId; let fetchMatches = getTargetedDataStrategyMatches(mapRouteProperties, manifest, fetchRequest, path, requestMatches, match, hydrationRouteProperties, scopedContext); let actionResults = await callDataStrategy(fetchRequest, path, fetchMatches, scopedContext, key); let actionResult = actionResults[match.route.id]; if (!actionResult) { for (let match of fetchMatches) if (actionResults[match.route.id]) { actionResult = actionResults[match.route.id]; break; } } if (fetchRequest.signal.aborted) { if (fetchControllers.get(key) === abortController) fetchControllers.delete(key); return; } if (fetchersQueuedForDeletion.has(key)) { if (isRedirectResult(actionResult) || isErrorResult(actionResult)) { updateFetcherState(key, getDoneFetcher(void 0)); return; } } else { if (isRedirectResult(actionResult)) { fetchControllers.delete(key); if (pendingNavigationLoadId > originatingLoadId) { updateFetcherState(key, getDoneFetcher(void 0)); return; } else { fetchRedirectIds.add(key); updateFetcherState(key, getLoadingFetcher(submission)); return startRedirectNavigation(fetchRequest, actionResult, false, { fetcherSubmission: submission, preventScrollReset }); } } if (isErrorResult(actionResult)) { setFetcherError(key, routeId, actionResult.error); return; } } let nextLocation = state.navigation.location || state.location; let revalidationRequest = createClientSideRequest(init.history, nextLocation, abortController.signal); let routesToUse = dataRoutes.activeRoutes; let matches = state.navigation.state !== "idle" ? matchRoutesImpl(routesToUse, state.navigation.location, basename, false, dataRoutes.branches) : state.matches; invariant(matches, "Didn't find any matches after fetcher action"); let loadId = ++incrementingLoadId; fetchReloadIds.set(key, loadId); let { dsMatches, revalidatingFetchers } = getMatchesToLoad(revalidationRequest, scopedContext, mapRouteProperties, manifest, init.history, state, matches, submission, nextLocation, hydrationRouteProperties, false, isRevalidationRequired, cancelledFetcherLoads, fetchersQueuedForDeletion, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, init.patchRoutesOnNavigation != null, dataRoutes.branches, [match.route.id, actionResult], callSiteDefaultShouldRevalidate); let loadFetcher = getLoadingFetcher(submission, actionResult.data); let workingFetchers = new Map(state.fetchers); workingFetchers.set(key, loadFetcher); revalidatingFetchers.filter((rf) => rf.key !== key).forEach((rf) => { let staleKey = rf.key; let existingFetcher = workingFetchers.get(staleKey); let revalidatingFetcher = getLoadingFetcher(void 0, existingFetcher ? existingFetcher.data : void 0); workingFetchers.set(staleKey, revalidatingFetcher); abortFetcher(staleKey); if (rf.controller) fetchControllers.set(staleKey, rf.controller); }); updateState({ fetchers: workingFetchers }); let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach((rf) => abortFetcher(rf.key)); abortController.signal.addEventListener("abort", abortPendingFetchRevalidations); let { loaderResults, fetcherResults } = await callLoadersAndMaybeResolveData(dsMatches, revalidatingFetchers, revalidationRequest, nextLocation, scopedContext); if (abortController.signal.aborted) return; abortController.signal.removeEventListener("abort", abortPendingFetchRevalidations); fetchReloadIds.delete(key); fetchControllers.delete(key); revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key)); let fetcherIsMounted = state.fetchers.has(key); let getRedirectStateWithDoneFetcher = (s) => { if (!fetcherIsMounted) return s; let workingFetchers = new Map(s.fetchers); workingFetchers.set(key, getDoneFetcher(actionResult.data)); return { ...s, fetchers: workingFetchers }; }; let redirect = findRedirect(loaderResults); if (redirect) { state = getRedirectStateWithDoneFetcher(state); return startRedirectNavigation(revalidationRequest, redirect.result, false, { preventScrollReset }); } redirect = findRedirect(fetcherResults); if (redirect) { fetchRedirectIds.add(redirect.key); state = getRedirectStateWithDoneFetcher(state); return startRedirectNavigation(revalidationRequest, redirect.result, false, { preventScrollReset }); } let finalFetchers = new Map(state.fetchers); if (fetcherIsMounted) finalFetchers.set(key, getDoneFetcher(actionResult.data)); let { loaderData, errors } = processLoaderData(state, matches, loaderResults, void 0, revalidatingFetchers, fetcherResults, finalFetchers); abortStaleFetchLoads(loadId, finalFetchers); if (state.navigation.state === "loading" && loadId > pendingNavigationLoadId) { invariant(pendingAction, "Expected pending action"); pendingNavigationController && pendingNavigationController.abort(); completeNavigation(state.navigation.location, { matches, loaderData, errors, fetchers: finalFetchers }); } else { updateState({ errors, loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors), fetchers: finalFetchers }); isRevalidationRequired = false; } } async function handleFetcherLoader(key, routeId, path, matches, scopedContext, isFogOfWar, flushSync, preventScrollReset, submission) { let existingFetcher = state.fetchers.get(key); updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : void 0), { flushSync }); let abortController = new AbortController(); let fetchRequest = createClientSideRequest(init.history, path, abortController.signal); if (isFogOfWar) { let discoverResult = await discoverRoutes(matches, new URL(fetchRequest.url).pathname, fetchRequest.signal, key); if (discoverResult.type === "aborted") return; else if (discoverResult.type === "error") { setFetcherError(key, routeId, discoverResult.error, { flushSync }); return; } else if (!discoverResult.matches) { setFetcherError(key, routeId, getInternalRouterError(404, { pathname: path }), { flushSync }); return; } else matches = discoverResult.matches; } let match = getTargetMatch(matches, path); fetchControllers.set(key, abortController); let originatingLoadId = incrementingLoadId; let results = await callDataStrategy(fetchRequest, path, getTargetedDataStrategyMatches(mapRouteProperties, manifest, fetchRequest, path, matches, match, hydrationRouteProperties, scopedContext), scopedContext, key); let result = results[match.route.id]; if (!result) { for (let match of matches) if (results[match.route.id]) { result = results[match.route.id]; break; } } if (fetchControllers.get(key) === abortController) fetchControllers.delete(key); if (fetchRequest.signal.aborted) return; if (fetchersQueuedForDeletion.has(key)) { updateFetcherState(key, getDoneFetcher(void 0)); return; } if (isRedirectResult(result)) if (pendingNavigationLoadId > originatingLoadId) { updateFetcherState(key, getDoneFetcher(void 0)); return; } else { fetchRedirectIds.add(key); await startRedirectNavigation(fetchRequest, result, false, { preventScrollReset }); return; } if (isErrorResult(result)) { setFetcherError(key, routeId, result.error); return; } updateFetcherState(key, getDoneFetcher(result.data)); } /** * Utility function to handle redirects returned from an action or loader. * Normally, a redirect "replaces" the navigation that triggered it. So, for * example: * * - user is on /a * - user clicks a link to /b * - loader for /b redirects to /c * * In a non-JS app the browser would track the in-flight navigation to /b and * then replace it with /c when it encountered the redirect response. In * the end it would only ever update the URL bar with /c. * * In client-side routing using pushState/replaceState, we aim to emulate * this behavior and we also do not update history until the end of the * navigation (including processed redirects). This means that we never * actually touch history until we've processed redirects, so we just use * the history action from the original navigation (PUSH or REPLACE). */ async function startRedirectNavigation(request, redirect, isNavigation, { submission, fetcherSubmission, preventScrollReset, replace } = {}) { if (!isNavigation) { pendingPopstateNavigationDfd?.resolve(); pendingPopstateNavigationDfd = null; } if (redirect.response.headers.has("X-Remix-Revalidate")) isRevalidationRequired = true; let location = redirect.response.headers.get("Location"); invariant(location, "Expected a Location header on the redirect Response"); location = normalizeRedirectLocation(location, new URL(request.url), basename, init.history); let redirectLocation = createLocation(state.location, location, { _isRedirect: true }); if (isBrowser) { let isDocumentReload = false; if (redirect.response.headers.has("X-Remix-Reload-Document")) isDocumentReload = true; else if (isAbsoluteUrl(location)) { const url = createBrowserURLImpl(routerWindow, location, true); isDocumentReload = url.origin !== routerWindow.location.origin || stripBasename(url.pathname, basename) == null; } if (isDocumentReload) { if (replace) routerWindow.location.replace(location); else routerWindow.location.assign(location); return; } } pendingNavigationController = null; let redirectNavigationType = replace === true || redirect.response.headers.has("X-Remix-Replace") ? "REPLACE" : "PUSH"; let { formMethod, formAction, formEncType } = state.navigation; if (!submission && !fetcherSubmission && formMethod && formAction && formEncType) submission = getSubmissionFromNavigation(state.navigation); let activeSubmission = submission || fetcherSubmission; if (redirectPreserveMethodStatusCodes.has(redirect.response.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) await startNavigation(redirectNavigationType, redirectLocation, { submission: { ...activeSubmission, formAction: location }, preventScrollReset: preventScrollReset || pendingPreventScrollReset, enableViewTransition: isNavigation ? pendingViewTransitionEnabled : void 0 }); else await startNavigation(redirectNavigationType, redirectLocation, { overrideNavigation: getLoadingNavigation(redirectLocation, [], redirectNavigationType, submission), fetcherSubmission, preventScrollReset: preventScrollReset || pendingPreventScrollReset, enableViewTransition: isNavigation ? pendingViewTransitionEnabled : void 0 }); } async function callDataStrategy(request, path, matches, scopedContext, fetcherKey) { let results; let dataResults = {}; try { results = await callDataStrategyImpl(dataStrategyImpl, request, path, matches, fetcherKey, scopedContext, false); } catch (e) { matches.filter((m) => m.shouldLoad).forEach((m) => { dataResults[m.route.id] = { type: "error", error: e }; }); return dataResults; } if (request.signal.aborted) return dataResults; if (!isMutationMethod(request.method)) for (let match of matches) { if (results[match.route.id]?.type === "error") break; if (!results.hasOwnProperty(match.route.id) && !state.loaderData.hasOwnProperty(match.route.id) && (!state.errors || !state.errors.hasOwnProperty(match.route.id)) && match.shouldCallHandler()) results[match.route.id] = { type: "error", result: /* @__PURE__ */ new Error(`No result returned from dataStrategy for route ${match.route.id}`) }; } for (let [routeId, result] of Object.entries(results)) if (isRedirectDataStrategyResult(result)) { let response = result.result; dataResults[routeId] = { type: "redirect", response: normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename) }; } else dataResults[routeId] = await convertDataStrategyResultToDataResult(result); return dataResults; } async function callLoadersAndMaybeResolveData(matches, fetchersToLoad, request, location, scopedContext) { let loaderResultsPromise = callDataStrategy(request, location, matches, scopedContext, null); let fetcherResultsPromise = Promise.all(fetchersToLoad.map(async (f) => { if (f.matches && f.match && f.request && f.controller) { let result = (await callDataStrategy(f.request, f.path, f.matches, scopedContext, f.key))[f.match.route.id]; return { [f.key]: result }; } else return Promise.resolve({ [f.key]: { type: "error", error: getInternalRouterError(404, { pathname: f.path }) } }); })); return { loaderResults: await loaderResultsPromise, fetcherResults: (await fetcherResultsPromise).reduce((acc, r) => Object.assign(acc, r), {}) }; } function interruptActiveLoads() { isRevalidationRequired = true; fetchLoadMatches.forEach((_, key) => { if (fetchControllers.has(key)) cancelledFetcherLoads.add(key); abortFetcher(key); }); } function updateFetcherState(key, fetcher, opts = {}) { let workingFetchers = new Map(state.fetchers); workingFetchers.set(key, fetcher); updateState({ fetchers: workingFetchers }, { flushSync: (opts && opts.flushSync) === true }); } function setFetcherError(key, routeId, error, opts = {}) { let boundaryMatch = findNearestBoundary(state.matches, routeId); let workingFetchers = new Map(state.fetchers); deleteFetcher(workingFetchers, key); updateState({ errors: { [boundaryMatch.route.id]: error }, fetchers: workingFetchers }, { flushSync: (opts && opts.flushSync) === true }); } function getFetcher(key) { activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1); if (fetchersQueuedForDeletion.has(key)) fetchersQueuedForDeletion.delete(key); return state.fetchers.get(key) || IDLE_FETCHER; } function resetFetcher(key, opts) { abortFetcher(key, opts?.reason); updateFetcherState(key, getDoneFetcher(null)); } function deleteFetcher(fetchers, key) { let fetcher = state.fetchers.get(key); if (fetchControllers.has(key) && !(fetcher && fetcher.state === "loading" && fetchReloadIds.has(key))) abortFetcher(key); fetchLoadMatches.delete(key); fetchReloadIds.delete(key); fetchRedirectIds.delete(key); fetchersQueuedForDeletion.delete(key); cancelledFetcherLoads.delete(key); fetchers.delete(key); } function queueFetcherForDeletion(key) { let count = (activeFetchers.get(key) || 0) - 1; if (count <= 0) { activeFetchers.delete(key); fetchersQueuedForDeletion.add(key); } else activeFetchers.set(key, count); updateState({ fetchers: new Map(state.fetchers) }); } function abortFetcher(key, reason) { let controller = fetchControllers.get(key); if (controller) { controller.abort(reason); fetchControllers.delete(key); } } function markFetchersDone(keys, fetchers) { for (let key of keys) { let fetcher = fetchers.get(key); invariant(fetcher, `Expected fetcher: ${key}`); let doneFetcher = getDoneFetcher(fetcher.data); fetchers.set(key, doneFetcher); } } function markFetchRedirectsDone(fetchers) { let doneKeys = []; let didUpdateFetchers = false; for (let key of fetchRedirectIds) { let fetcher = fetchers.get(key); invariant(fetcher, `Expected fetcher: ${key}`); if (fetcher.state === "loading") { fetchRedirectIds.delete(key); doneKeys.push(key); didUpdateFetchers = true; } } markFetchersDone(doneKeys, fetchers); return didUpdateFetchers; } function abortStaleFetchLoads(landedId, fetchers) { let yeetedKeys = []; for (let [key, id] of fetchReloadIds) if (id < landedId) { let fetcher = fetchers.get(key); invariant(fetcher, `Expected fetcher: ${key}`); if (fetcher.state === "loading") { abortFetcher(key); fetchReload