react-router
Version:
Declarative routing for React
1,167 lines • 122 kB
JavaScript
/**
* 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