1724 lines
65 KiB
Plaintext
1724 lines
65 KiB
Plaintext
import { Store, batch } from "@tanstack/store";
|
|
import { createMemoryHistory, createBrowserHistory, parseHref } from "@tanstack/history";
|
|
import invariant from "tiny-invariant";
|
|
import { pick, createControlledPromise, deepEqual, replaceEqualDeep, last, functionalUpdate } from "./utils.js";
|
|
import { trimPath, trimPathLeft, parsePathname, resolvePath, cleanPath, trimPathRight, matchPathname, interpolatePath, joinPaths } from "./path.js";
|
|
import { isNotFound } from "./not-found.js";
|
|
import { setupScrollRestoration } from "./scroll-restoration.js";
|
|
import { defaultParseSearch, defaultStringifySearch } from "./searchParams.js";
|
|
import { rootRouteId } from "./root.js";
|
|
import { isResolvedRedirect, isRedirect } from "./redirect.js";
|
|
function defaultSerializeError(err) {
|
|
if (err instanceof Error) {
|
|
const obj = {
|
|
name: err.name,
|
|
message: err.message
|
|
};
|
|
if (process.env.NODE_ENV === "development") {
|
|
obj.stack = err.stack;
|
|
}
|
|
return obj;
|
|
}
|
|
return {
|
|
data: err
|
|
};
|
|
}
|
|
function getLocationChangeInfo(routerState) {
|
|
const fromLocation = routerState.resolvedLocation;
|
|
const toLocation = routerState.location;
|
|
const pathChanged = (fromLocation == null ? void 0 : fromLocation.pathname) !== toLocation.pathname;
|
|
const hrefChanged = (fromLocation == null ? void 0 : fromLocation.href) !== toLocation.href;
|
|
const hashChanged = (fromLocation == null ? void 0 : fromLocation.hash) !== toLocation.hash;
|
|
return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged };
|
|
}
|
|
class RouterCore {
|
|
/**
|
|
* @deprecated Use the `createRouter` function instead
|
|
*/
|
|
constructor(options) {
|
|
this.tempLocationKey = `${Math.round(
|
|
Math.random() * 1e7
|
|
)}`;
|
|
this.resetNextScroll = true;
|
|
this.shouldViewTransition = void 0;
|
|
this.isViewTransitionTypesSupported = void 0;
|
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
this.isScrollRestoring = false;
|
|
this.isScrollRestorationSetup = false;
|
|
this.startTransition = (fn) => fn();
|
|
this.update = (newOptions) => {
|
|
var _a;
|
|
if (newOptions.notFoundRoute) {
|
|
console.warn(
|
|
"The notFoundRoute API is deprecated and will be removed in the next major version. See https://tanstack.com/router/v1/docs/framework/react/guide/not-found-errors#migrating-from-notfoundroute for more info."
|
|
);
|
|
}
|
|
const previousOptions = this.options;
|
|
this.options = {
|
|
...this.options,
|
|
...newOptions
|
|
};
|
|
this.isServer = this.options.isServer ?? typeof document === "undefined";
|
|
this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters ? new Map(
|
|
this.options.pathParamsAllowedCharacters.map((char) => [
|
|
encodeURIComponent(char),
|
|
char
|
|
])
|
|
) : void 0;
|
|
if (!this.basepath || newOptions.basepath && newOptions.basepath !== previousOptions.basepath) {
|
|
if (newOptions.basepath === void 0 || newOptions.basepath === "" || newOptions.basepath === "/") {
|
|
this.basepath = "/";
|
|
} else {
|
|
this.basepath = `/${trimPath(newOptions.basepath)}`;
|
|
}
|
|
}
|
|
if (
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
!this.history || this.options.history && this.options.history !== this.history
|
|
) {
|
|
this.history = this.options.history ?? (this.isServer ? createMemoryHistory({
|
|
initialEntries: [this.basepath || "/"]
|
|
}) : createBrowserHistory());
|
|
this.latestLocation = this.parseLocation();
|
|
}
|
|
if (this.options.routeTree !== this.routeTree) {
|
|
this.routeTree = this.options.routeTree;
|
|
this.buildRouteTree();
|
|
}
|
|
if (!this.__store) {
|
|
this.__store = new Store(getInitialRouterState(this.latestLocation), {
|
|
onUpdate: () => {
|
|
this.__store.state = {
|
|
...this.state,
|
|
cachedMatches: this.state.cachedMatches.filter(
|
|
(d) => !["redirected"].includes(d.status)
|
|
)
|
|
};
|
|
}
|
|
});
|
|
setupScrollRestoration(this);
|
|
}
|
|
if (typeof window !== "undefined" && "CSS" in window && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
typeof ((_a = window.CSS) == null ? void 0 : _a.supports) === "function") {
|
|
this.isViewTransitionTypesSupported = window.CSS.supports(
|
|
"selector(:active-view-transition-type(a)"
|
|
);
|
|
}
|
|
};
|
|
this.buildRouteTree = () => {
|
|
this.routesById = {};
|
|
this.routesByPath = {};
|
|
const notFoundRoute = this.options.notFoundRoute;
|
|
if (notFoundRoute) {
|
|
notFoundRoute.init({
|
|
originalIndex: 99999999999,
|
|
defaultSsr: this.options.defaultSsr
|
|
});
|
|
this.routesById[notFoundRoute.id] = notFoundRoute;
|
|
}
|
|
const recurseRoutes = (childRoutes) => {
|
|
childRoutes.forEach((childRoute, i) => {
|
|
childRoute.init({
|
|
originalIndex: i,
|
|
defaultSsr: this.options.defaultSsr
|
|
});
|
|
const existingRoute = this.routesById[childRoute.id];
|
|
invariant(
|
|
!existingRoute,
|
|
`Duplicate routes found with id: ${String(childRoute.id)}`
|
|
);
|
|
this.routesById[childRoute.id] = childRoute;
|
|
if (!childRoute.isRoot && childRoute.path) {
|
|
const trimmedFullPath = trimPathRight(childRoute.fullPath);
|
|
if (!this.routesByPath[trimmedFullPath] || childRoute.fullPath.endsWith("/")) {
|
|
this.routesByPath[trimmedFullPath] = childRoute;
|
|
}
|
|
}
|
|
const children = childRoute.children;
|
|
if (children == null ? void 0 : children.length) {
|
|
recurseRoutes(children);
|
|
}
|
|
});
|
|
};
|
|
recurseRoutes([this.routeTree]);
|
|
const scoredRoutes = [];
|
|
const routes = Object.values(this.routesById);
|
|
routes.forEach((d, i) => {
|
|
var _a;
|
|
if (d.isRoot || !d.path) {
|
|
return;
|
|
}
|
|
const trimmed = trimPathLeft(d.fullPath);
|
|
const parsed = parsePathname(trimmed);
|
|
while (parsed.length > 1 && ((_a = parsed[0]) == null ? void 0 : _a.value) === "/") {
|
|
parsed.shift();
|
|
}
|
|
const scores = parsed.map((segment) => {
|
|
if (segment.value === "/") {
|
|
return 0.75;
|
|
}
|
|
if (segment.type === "param") {
|
|
return 0.5;
|
|
}
|
|
if (segment.type === "wildcard") {
|
|
return 0.25;
|
|
}
|
|
return 1;
|
|
});
|
|
scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores });
|
|
});
|
|
this.flatRoutes = scoredRoutes.sort((a, b) => {
|
|
const minLength = Math.min(a.scores.length, b.scores.length);
|
|
for (let i = 0; i < minLength; i++) {
|
|
if (a.scores[i] !== b.scores[i]) {
|
|
return b.scores[i] - a.scores[i];
|
|
}
|
|
}
|
|
if (a.scores.length !== b.scores.length) {
|
|
return b.scores.length - a.scores.length;
|
|
}
|
|
for (let i = 0; i < minLength; i++) {
|
|
if (a.parsed[i].value !== b.parsed[i].value) {
|
|
return a.parsed[i].value > b.parsed[i].value ? 1 : -1;
|
|
}
|
|
}
|
|
return a.index - b.index;
|
|
}).map((d, i) => {
|
|
d.child.rank = i;
|
|
return d.child;
|
|
});
|
|
};
|
|
this.subscribe = (eventType, fn) => {
|
|
const listener = {
|
|
eventType,
|
|
fn
|
|
};
|
|
this.subscribers.add(listener);
|
|
return () => {
|
|
this.subscribers.delete(listener);
|
|
};
|
|
};
|
|
this.emit = (routerEvent) => {
|
|
this.subscribers.forEach((listener) => {
|
|
if (listener.eventType === routerEvent.type) {
|
|
listener.fn(routerEvent);
|
|
}
|
|
});
|
|
};
|
|
this.parseLocation = (previousLocation, locationToParse) => {
|
|
const parse = ({
|
|
pathname,
|
|
search,
|
|
hash,
|
|
state
|
|
}) => {
|
|
const parsedSearch = this.options.parseSearch(search);
|
|
const searchStr = this.options.stringifySearch(parsedSearch);
|
|
return {
|
|
pathname,
|
|
searchStr,
|
|
search: replaceEqualDeep(previousLocation == null ? void 0 : previousLocation.search, parsedSearch),
|
|
hash: hash.split("#").reverse()[0] ?? "",
|
|
href: `${pathname}${searchStr}${hash}`,
|
|
state: replaceEqualDeep(previousLocation == null ? void 0 : previousLocation.state, state)
|
|
};
|
|
};
|
|
const location = parse(locationToParse ?? this.history.location);
|
|
const { __tempLocation, __tempKey } = location.state;
|
|
if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
|
|
const parsedTempLocation = parse(__tempLocation);
|
|
parsedTempLocation.state.key = location.state.key;
|
|
delete parsedTempLocation.state.__tempLocation;
|
|
return {
|
|
...parsedTempLocation,
|
|
maskedLocation: location
|
|
};
|
|
}
|
|
return location;
|
|
};
|
|
this.resolvePathWithBase = (from, path) => {
|
|
const resolvedPath = resolvePath({
|
|
basepath: this.basepath,
|
|
base: from,
|
|
to: cleanPath(path),
|
|
trailingSlash: this.options.trailingSlash,
|
|
caseSensitive: this.options.caseSensitive
|
|
});
|
|
return resolvedPath;
|
|
};
|
|
this.matchRoutes = (pathnameOrNext, locationSearchOrOpts, opts) => {
|
|
if (typeof pathnameOrNext === "string") {
|
|
return this.matchRoutesInternal(
|
|
{
|
|
pathname: pathnameOrNext,
|
|
search: locationSearchOrOpts
|
|
},
|
|
opts
|
|
);
|
|
} else {
|
|
return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts);
|
|
}
|
|
};
|
|
this.getMatchedRoutes = (next, dest) => {
|
|
let routeParams = {};
|
|
const trimmedPath = trimPathRight(next.pathname);
|
|
const getMatchedParams = (route) => {
|
|
const result = matchPathname(this.basepath, trimmedPath, {
|
|
to: route.fullPath,
|
|
caseSensitive: route.options.caseSensitive ?? this.options.caseSensitive,
|
|
fuzzy: true
|
|
});
|
|
return result;
|
|
};
|
|
let foundRoute = (dest == null ? void 0 : dest.to) !== void 0 ? this.routesByPath[dest.to] : void 0;
|
|
if (foundRoute) {
|
|
routeParams = getMatchedParams(foundRoute);
|
|
} else {
|
|
foundRoute = this.flatRoutes.find((route) => {
|
|
const matchedParams = getMatchedParams(route);
|
|
if (matchedParams) {
|
|
routeParams = matchedParams;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
let routeCursor = foundRoute || this.routesById[rootRouteId];
|
|
const matchedRoutes = [routeCursor];
|
|
while (routeCursor.parentRoute) {
|
|
routeCursor = routeCursor.parentRoute;
|
|
matchedRoutes.unshift(routeCursor);
|
|
}
|
|
return { matchedRoutes, routeParams, foundRoute };
|
|
};
|
|
this.cancelMatch = (id) => {
|
|
const match = this.getMatch(id);
|
|
if (!match) return;
|
|
match.abortController.abort();
|
|
clearTimeout(match.pendingTimeout);
|
|
};
|
|
this.cancelMatches = () => {
|
|
var _a;
|
|
(_a = this.state.pendingMatches) == null ? void 0 : _a.forEach((match) => {
|
|
this.cancelMatch(match.id);
|
|
});
|
|
};
|
|
this.buildLocation = (opts) => {
|
|
const build = (dest = {}, matchedRoutesResult) => {
|
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
const fromMatches = dest._fromLocation ? this.matchRoutes(dest._fromLocation, { _buildLocation: true }) : this.state.matches;
|
|
const fromMatch = dest.from != null ? fromMatches.find(
|
|
(d) => matchPathname(this.basepath, trimPathRight(d.pathname), {
|
|
to: dest.from,
|
|
caseSensitive: false,
|
|
fuzzy: false
|
|
})
|
|
) : void 0;
|
|
const fromPath = (fromMatch == null ? void 0 : fromMatch.pathname) || this.latestLocation.pathname;
|
|
invariant(
|
|
dest.from == null || fromMatch != null,
|
|
"Could not find match for from: " + dest.from
|
|
);
|
|
const fromSearch = ((_a = this.state.pendingMatches) == null ? void 0 : _a.length) ? (_b = last(this.state.pendingMatches)) == null ? void 0 : _b.search : ((_c = last(fromMatches)) == null ? void 0 : _c.search) || this.latestLocation.search;
|
|
const stayingMatches = matchedRoutesResult == null ? void 0 : matchedRoutesResult.matchedRoutes.filter(
|
|
(d) => fromMatches.find((e) => e.routeId === d.id)
|
|
);
|
|
let pathname;
|
|
if (dest.to) {
|
|
const resolvePathTo = (fromMatch == null ? void 0 : fromMatch.fullPath) || ((_d = last(fromMatches)) == null ? void 0 : _d.fullPath) || this.latestLocation.pathname;
|
|
pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`);
|
|
} else {
|
|
const fromRouteByFromPathRouteId = this.routesById[(_e = stayingMatches == null ? void 0 : stayingMatches.find((route) => {
|
|
const interpolatedPath = interpolatePath({
|
|
path: route.fullPath,
|
|
params: (matchedRoutesResult == null ? void 0 : matchedRoutesResult.routeParams) ?? {},
|
|
decodeCharMap: this.pathParamsDecodeCharMap
|
|
}).interpolatedPath;
|
|
const pathname2 = joinPaths([this.basepath, interpolatedPath]);
|
|
return pathname2 === fromPath;
|
|
})) == null ? void 0 : _e.id];
|
|
pathname = this.resolvePathWithBase(
|
|
fromPath,
|
|
(fromRouteByFromPathRouteId == null ? void 0 : fromRouteByFromPathRouteId.to) ?? fromPath
|
|
);
|
|
}
|
|
const prevParams = { ...(_f = last(fromMatches)) == null ? void 0 : _f.params };
|
|
let nextParams = (dest.params ?? true) === true ? prevParams : {
|
|
...prevParams,
|
|
...functionalUpdate(dest.params, prevParams)
|
|
};
|
|
if (Object.keys(nextParams).length > 0) {
|
|
matchedRoutesResult == null ? void 0 : matchedRoutesResult.matchedRoutes.map((route) => {
|
|
var _a2;
|
|
return ((_a2 = route.options.params) == null ? void 0 : _a2.stringify) ?? route.options.stringifyParams;
|
|
}).filter(Boolean).forEach((fn) => {
|
|
nextParams = { ...nextParams, ...fn(nextParams) };
|
|
});
|
|
}
|
|
pathname = interpolatePath({
|
|
path: pathname,
|
|
params: nextParams ?? {},
|
|
leaveWildcards: false,
|
|
leaveParams: opts.leaveParams,
|
|
decodeCharMap: this.pathParamsDecodeCharMap
|
|
}).interpolatedPath;
|
|
let search = fromSearch;
|
|
if (opts._includeValidateSearch && ((_g = this.options.search) == null ? void 0 : _g.strict)) {
|
|
let validatedSearch = {};
|
|
matchedRoutesResult == null ? void 0 : matchedRoutesResult.matchedRoutes.forEach((route) => {
|
|
try {
|
|
if (route.options.validateSearch) {
|
|
validatedSearch = {
|
|
...validatedSearch,
|
|
...validateSearch(route.options.validateSearch, {
|
|
...validatedSearch,
|
|
...search
|
|
}) ?? {}
|
|
};
|
|
}
|
|
} catch {
|
|
}
|
|
});
|
|
search = validatedSearch;
|
|
}
|
|
const applyMiddlewares = (search2) => {
|
|
const allMiddlewares = (matchedRoutesResult == null ? void 0 : matchedRoutesResult.matchedRoutes.reduce(
|
|
(acc, route) => {
|
|
var _a2;
|
|
const middlewares = [];
|
|
if ("search" in route.options) {
|
|
if ((_a2 = route.options.search) == null ? void 0 : _a2.middlewares) {
|
|
middlewares.push(...route.options.search.middlewares);
|
|
}
|
|
} else if (route.options.preSearchFilters || route.options.postSearchFilters) {
|
|
const legacyMiddleware = ({
|
|
search: search3,
|
|
next
|
|
}) => {
|
|
let nextSearch = search3;
|
|
if ("preSearchFilters" in route.options && route.options.preSearchFilters) {
|
|
nextSearch = route.options.preSearchFilters.reduce(
|
|
(prev, next2) => next2(prev),
|
|
search3
|
|
);
|
|
}
|
|
const result = next(nextSearch);
|
|
if ("postSearchFilters" in route.options && route.options.postSearchFilters) {
|
|
return route.options.postSearchFilters.reduce(
|
|
(prev, next2) => next2(prev),
|
|
result
|
|
);
|
|
}
|
|
return result;
|
|
};
|
|
middlewares.push(legacyMiddleware);
|
|
}
|
|
if (opts._includeValidateSearch && route.options.validateSearch) {
|
|
const validate = ({ search: search3, next }) => {
|
|
const result = next(search3);
|
|
try {
|
|
const validatedSearch = {
|
|
...result,
|
|
...validateSearch(
|
|
route.options.validateSearch,
|
|
result
|
|
) ?? {}
|
|
};
|
|
return validatedSearch;
|
|
} catch {
|
|
return result;
|
|
}
|
|
};
|
|
middlewares.push(validate);
|
|
}
|
|
return acc.concat(middlewares);
|
|
},
|
|
[]
|
|
)) ?? [];
|
|
const final = ({ search: search3 }) => {
|
|
if (!dest.search) {
|
|
return {};
|
|
}
|
|
if (dest.search === true) {
|
|
return search3;
|
|
}
|
|
return functionalUpdate(dest.search, search3);
|
|
};
|
|
allMiddlewares.push(final);
|
|
const applyNext = (index, currentSearch) => {
|
|
if (index >= allMiddlewares.length) {
|
|
return currentSearch;
|
|
}
|
|
const middleware = allMiddlewares[index];
|
|
const next = (newSearch) => {
|
|
return applyNext(index + 1, newSearch);
|
|
};
|
|
return middleware({ search: currentSearch, next });
|
|
};
|
|
return applyNext(0, search2);
|
|
};
|
|
search = applyMiddlewares(search);
|
|
search = replaceEqualDeep(fromSearch, search);
|
|
const searchStr = this.options.stringifySearch(search);
|
|
const hash = dest.hash === true ? this.latestLocation.hash : dest.hash ? functionalUpdate(dest.hash, this.latestLocation.hash) : void 0;
|
|
const hashStr = hash ? `#${hash}` : "";
|
|
let nextState = dest.state === true ? this.latestLocation.state : dest.state ? functionalUpdate(dest.state, this.latestLocation.state) : {};
|
|
nextState = replaceEqualDeep(this.latestLocation.state, nextState);
|
|
return {
|
|
pathname,
|
|
search,
|
|
searchStr,
|
|
state: nextState,
|
|
hash: hash ?? "",
|
|
href: `${pathname}${searchStr}${hashStr}`,
|
|
unmaskOnReload: dest.unmaskOnReload
|
|
};
|
|
};
|
|
const buildWithMatches = (dest = {}, maskedDest) => {
|
|
var _a;
|
|
const next = build(dest);
|
|
let maskedNext = maskedDest ? build(maskedDest) : void 0;
|
|
if (!maskedNext) {
|
|
let params = {};
|
|
const foundMask = (_a = this.options.routeMasks) == null ? void 0 : _a.find((d) => {
|
|
const match = matchPathname(this.basepath, next.pathname, {
|
|
to: d.from,
|
|
caseSensitive: false,
|
|
fuzzy: false
|
|
});
|
|
if (match) {
|
|
params = match;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
if (foundMask) {
|
|
const { from: _from, ...maskProps } = foundMask;
|
|
maskedDest = {
|
|
...pick(opts, ["from"]),
|
|
...maskProps,
|
|
params
|
|
};
|
|
maskedNext = build(maskedDest);
|
|
}
|
|
}
|
|
const nextMatches = this.getMatchedRoutes(next, dest);
|
|
const final = build(dest, nextMatches);
|
|
if (maskedNext) {
|
|
const maskedMatches = this.getMatchedRoutes(maskedNext, maskedDest);
|
|
const maskedFinal = build(maskedDest, maskedMatches);
|
|
final.maskedLocation = maskedFinal;
|
|
}
|
|
return final;
|
|
};
|
|
if (opts.mask) {
|
|
return buildWithMatches(opts, {
|
|
...pick(opts, ["from"]),
|
|
...opts.mask
|
|
});
|
|
}
|
|
return buildWithMatches(opts);
|
|
};
|
|
this.commitLocation = ({
|
|
viewTransition,
|
|
ignoreBlocker,
|
|
...next
|
|
}) => {
|
|
const isSameState = () => {
|
|
const ignoredProps = [
|
|
"key",
|
|
"__TSR_index",
|
|
"__hashScrollIntoViewOptions"
|
|
];
|
|
ignoredProps.forEach((prop) => {
|
|
next.state[prop] = this.latestLocation.state[prop];
|
|
});
|
|
const isEqual = deepEqual(next.state, this.latestLocation.state);
|
|
ignoredProps.forEach((prop) => {
|
|
delete next.state[prop];
|
|
});
|
|
return isEqual;
|
|
};
|
|
const isSameUrl = this.latestLocation.href === next.href;
|
|
const previousCommitPromise = this.commitLocationPromise;
|
|
this.commitLocationPromise = createControlledPromise(() => {
|
|
previousCommitPromise == null ? void 0 : previousCommitPromise.resolve();
|
|
});
|
|
if (isSameUrl && isSameState()) {
|
|
this.load();
|
|
} else {
|
|
let { maskedLocation, hashScrollIntoView, ...nextHistory } = next;
|
|
if (maskedLocation) {
|
|
nextHistory = {
|
|
...maskedLocation,
|
|
state: {
|
|
...maskedLocation.state,
|
|
__tempKey: void 0,
|
|
__tempLocation: {
|
|
...nextHistory,
|
|
search: nextHistory.searchStr,
|
|
state: {
|
|
...nextHistory.state,
|
|
__tempKey: void 0,
|
|
__tempLocation: void 0,
|
|
key: void 0
|
|
}
|
|
}
|
|
}
|
|
};
|
|
if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
|
|
nextHistory.state.__tempKey = this.tempLocationKey;
|
|
}
|
|
}
|
|
nextHistory.state.__hashScrollIntoViewOptions = hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true;
|
|
this.shouldViewTransition = viewTransition;
|
|
this.history[next.replace ? "replace" : "push"](
|
|
nextHistory.href,
|
|
nextHistory.state,
|
|
{ ignoreBlocker }
|
|
);
|
|
}
|
|
this.resetNextScroll = next.resetScroll ?? true;
|
|
if (!this.history.subscribers.size) {
|
|
this.load();
|
|
}
|
|
return this.commitLocationPromise;
|
|
};
|
|
this.buildAndCommitLocation = ({
|
|
replace,
|
|
resetScroll,
|
|
hashScrollIntoView,
|
|
viewTransition,
|
|
ignoreBlocker,
|
|
href,
|
|
...rest
|
|
} = {}) => {
|
|
if (href) {
|
|
const currentIndex = this.history.location.state.__TSR_index;
|
|
const parsed = parseHref(href, {
|
|
__TSR_index: replace ? currentIndex : currentIndex + 1
|
|
});
|
|
rest.to = parsed.pathname;
|
|
rest.search = this.options.parseSearch(parsed.search);
|
|
rest.hash = parsed.hash.slice(1);
|
|
}
|
|
const location = this.buildLocation({
|
|
...rest,
|
|
_includeValidateSearch: true
|
|
});
|
|
return this.commitLocation({
|
|
...location,
|
|
viewTransition,
|
|
replace,
|
|
resetScroll,
|
|
hashScrollIntoView,
|
|
ignoreBlocker
|
|
});
|
|
};
|
|
this.navigate = ({ to, reloadDocument, href, ...rest }) => {
|
|
if (reloadDocument) {
|
|
if (!href) {
|
|
const location = this.buildLocation({ to, ...rest });
|
|
href = this.history.createHref(location.href);
|
|
}
|
|
if (rest.replace) {
|
|
window.location.replace(href);
|
|
} else {
|
|
window.location.href = href;
|
|
}
|
|
return;
|
|
}
|
|
return this.buildAndCommitLocation({
|
|
...rest,
|
|
href,
|
|
to
|
|
});
|
|
};
|
|
this.load = async (opts) => {
|
|
this.latestLocation = this.parseLocation(this.latestLocation);
|
|
let redirect;
|
|
let notFound;
|
|
let loadPromise;
|
|
loadPromise = new Promise((resolve) => {
|
|
this.startTransition(async () => {
|
|
var _a;
|
|
try {
|
|
const next = this.latestLocation;
|
|
const prevLocation = this.state.resolvedLocation;
|
|
this.cancelMatches();
|
|
let pendingMatches;
|
|
batch(() => {
|
|
pendingMatches = this.matchRoutes(next);
|
|
this.__store.setState((s) => ({
|
|
...s,
|
|
status: "pending",
|
|
isLoading: true,
|
|
location: next,
|
|
pendingMatches,
|
|
// If a cached moved to pendingMatches, remove it from cachedMatches
|
|
cachedMatches: s.cachedMatches.filter((d) => {
|
|
return !pendingMatches.find((e) => e.id === d.id);
|
|
})
|
|
}));
|
|
});
|
|
if (!this.state.redirect) {
|
|
this.emit({
|
|
type: "onBeforeNavigate",
|
|
...getLocationChangeInfo({
|
|
resolvedLocation: prevLocation,
|
|
location: next
|
|
})
|
|
});
|
|
}
|
|
this.emit({
|
|
type: "onBeforeLoad",
|
|
...getLocationChangeInfo({
|
|
resolvedLocation: prevLocation,
|
|
location: next
|
|
})
|
|
});
|
|
await this.loadMatches({
|
|
sync: opts == null ? void 0 : opts.sync,
|
|
matches: pendingMatches,
|
|
location: next,
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
onReady: async () => {
|
|
this.startViewTransition(async () => {
|
|
let exitingMatches;
|
|
let enteringMatches;
|
|
let stayingMatches;
|
|
batch(() => {
|
|
this.__store.setState((s) => {
|
|
const previousMatches = s.matches;
|
|
const newMatches = s.pendingMatches || s.matches;
|
|
exitingMatches = previousMatches.filter(
|
|
(match) => !newMatches.find((d) => d.id === match.id)
|
|
);
|
|
enteringMatches = newMatches.filter(
|
|
(match) => !previousMatches.find((d) => d.id === match.id)
|
|
);
|
|
stayingMatches = previousMatches.filter(
|
|
(match) => newMatches.find((d) => d.id === match.id)
|
|
);
|
|
return {
|
|
...s,
|
|
isLoading: false,
|
|
loadedAt: Date.now(),
|
|
matches: newMatches,
|
|
pendingMatches: void 0,
|
|
cachedMatches: [
|
|
...s.cachedMatches,
|
|
...exitingMatches.filter((d) => d.status !== "error")
|
|
]
|
|
};
|
|
});
|
|
this.clearExpiredCache();
|
|
});
|
|
[
|
|
[exitingMatches, "onLeave"],
|
|
[enteringMatches, "onEnter"],
|
|
[stayingMatches, "onStay"]
|
|
].forEach(([matches, hook]) => {
|
|
matches.forEach((match) => {
|
|
var _a2, _b;
|
|
(_b = (_a2 = this.looseRoutesById[match.routeId].options)[hook]) == null ? void 0 : _b.call(_a2, match);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
} catch (err) {
|
|
if (isResolvedRedirect(err)) {
|
|
redirect = err;
|
|
if (!this.isServer) {
|
|
this.navigate({
|
|
...redirect,
|
|
replace: true,
|
|
ignoreBlocker: true
|
|
});
|
|
}
|
|
} else if (isNotFound(err)) {
|
|
notFound = err;
|
|
}
|
|
this.__store.setState((s) => ({
|
|
...s,
|
|
statusCode: redirect ? redirect.statusCode : notFound ? 404 : s.matches.some((d) => d.status === "error") ? 500 : 200,
|
|
redirect
|
|
}));
|
|
}
|
|
if (this.latestLoadPromise === loadPromise) {
|
|
(_a = this.commitLocationPromise) == null ? void 0 : _a.resolve();
|
|
this.latestLoadPromise = void 0;
|
|
this.commitLocationPromise = void 0;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
this.latestLoadPromise = loadPromise;
|
|
await loadPromise;
|
|
while (this.latestLoadPromise && loadPromise !== this.latestLoadPromise) {
|
|
await this.latestLoadPromise;
|
|
}
|
|
if (this.hasNotFoundMatch()) {
|
|
this.__store.setState((s) => ({
|
|
...s,
|
|
statusCode: 404
|
|
}));
|
|
}
|
|
};
|
|
this.startViewTransition = (fn) => {
|
|
const shouldViewTransition = this.shouldViewTransition ?? this.options.defaultViewTransition;
|
|
delete this.shouldViewTransition;
|
|
if (shouldViewTransition && typeof document !== "undefined" && "startViewTransition" in document && typeof document.startViewTransition === "function") {
|
|
let startViewTransitionParams;
|
|
if (typeof shouldViewTransition === "object" && this.isViewTransitionTypesSupported) {
|
|
startViewTransitionParams = {
|
|
update: fn,
|
|
types: shouldViewTransition.types
|
|
};
|
|
} else {
|
|
startViewTransitionParams = fn;
|
|
}
|
|
document.startViewTransition(startViewTransitionParams);
|
|
} else {
|
|
fn();
|
|
}
|
|
};
|
|
this.updateMatch = (id, updater) => {
|
|
var _a;
|
|
let updated;
|
|
const isPending = (_a = this.state.pendingMatches) == null ? void 0 : _a.find((d) => d.id === id);
|
|
const isMatched = this.state.matches.find((d) => d.id === id);
|
|
const isCached = this.state.cachedMatches.find((d) => d.id === id);
|
|
const matchesKey = isPending ? "pendingMatches" : isMatched ? "matches" : isCached ? "cachedMatches" : "";
|
|
if (matchesKey) {
|
|
this.__store.setState((s) => {
|
|
var _a2;
|
|
return {
|
|
...s,
|
|
[matchesKey]: (_a2 = s[matchesKey]) == null ? void 0 : _a2.map(
|
|
(d) => d.id === id ? updated = updater(d) : d
|
|
)
|
|
};
|
|
});
|
|
}
|
|
return updated;
|
|
};
|
|
this.getMatch = (matchId) => {
|
|
return [
|
|
...this.state.cachedMatches,
|
|
...this.state.pendingMatches ?? [],
|
|
...this.state.matches
|
|
].find((d) => d.id === matchId);
|
|
};
|
|
this.loadMatches = async ({
|
|
location,
|
|
matches,
|
|
preload: allPreload,
|
|
onReady,
|
|
updateMatch = this.updateMatch,
|
|
sync
|
|
}) => {
|
|
let firstBadMatchIndex;
|
|
let rendered = false;
|
|
const triggerOnReady = async () => {
|
|
if (!rendered) {
|
|
rendered = true;
|
|
await (onReady == null ? void 0 : onReady());
|
|
}
|
|
};
|
|
const resolvePreload = (matchId) => {
|
|
return !!(allPreload && !this.state.matches.find((d) => d.id === matchId));
|
|
};
|
|
if (!this.isServer && !this.state.matches.length) {
|
|
triggerOnReady();
|
|
}
|
|
const handleRedirectAndNotFound = (match, err) => {
|
|
var _a, _b, _c, _d;
|
|
if (isResolvedRedirect(err)) {
|
|
if (!err.reloadDocument) {
|
|
throw err;
|
|
}
|
|
}
|
|
if (isRedirect(err) || isNotFound(err)) {
|
|
updateMatch(match.id, (prev) => ({
|
|
...prev,
|
|
status: isRedirect(err) ? "redirected" : isNotFound(err) ? "notFound" : "error",
|
|
isFetching: false,
|
|
error: err,
|
|
beforeLoadPromise: void 0,
|
|
loaderPromise: void 0
|
|
}));
|
|
if (!err.routeId) {
|
|
err.routeId = match.routeId;
|
|
}
|
|
(_a = match.beforeLoadPromise) == null ? void 0 : _a.resolve();
|
|
(_b = match.loaderPromise) == null ? void 0 : _b.resolve();
|
|
(_c = match.loadPromise) == null ? void 0 : _c.resolve();
|
|
if (isRedirect(err)) {
|
|
rendered = true;
|
|
err = this.resolveRedirect({ ...err, _fromLocation: location });
|
|
throw err;
|
|
} else if (isNotFound(err)) {
|
|
this._handleNotFound(matches, err, {
|
|
updateMatch
|
|
});
|
|
(_d = this.serverSsr) == null ? void 0 : _d.onMatchSettled({
|
|
router: this,
|
|
match: this.getMatch(match.id)
|
|
});
|
|
throw err;
|
|
}
|
|
}
|
|
};
|
|
try {
|
|
await new Promise((resolveAll, rejectAll) => {
|
|
;
|
|
(async () => {
|
|
var _a, _b, _c, _d;
|
|
try {
|
|
const handleSerialError = (index, err, routerCode) => {
|
|
var _a2, _b2;
|
|
const { id: matchId, routeId } = matches[index];
|
|
const route = this.looseRoutesById[routeId];
|
|
if (err instanceof Promise) {
|
|
throw err;
|
|
}
|
|
err.routerCode = routerCode;
|
|
firstBadMatchIndex = firstBadMatchIndex ?? index;
|
|
handleRedirectAndNotFound(this.getMatch(matchId), err);
|
|
try {
|
|
(_b2 = (_a2 = route.options).onError) == null ? void 0 : _b2.call(_a2, err);
|
|
} catch (errorHandlerErr) {
|
|
err = errorHandlerErr;
|
|
handleRedirectAndNotFound(this.getMatch(matchId), err);
|
|
}
|
|
updateMatch(matchId, (prev) => {
|
|
var _a3, _b3;
|
|
(_a3 = prev.beforeLoadPromise) == null ? void 0 : _a3.resolve();
|
|
(_b3 = prev.loadPromise) == null ? void 0 : _b3.resolve();
|
|
return {
|
|
...prev,
|
|
error: err,
|
|
status: "error",
|
|
isFetching: false,
|
|
updatedAt: Date.now(),
|
|
abortController: new AbortController(),
|
|
beforeLoadPromise: void 0
|
|
};
|
|
});
|
|
};
|
|
for (const [index, { id: matchId, routeId }] of matches.entries()) {
|
|
const existingMatch = this.getMatch(matchId);
|
|
const parentMatchId = (_a = matches[index - 1]) == null ? void 0 : _a.id;
|
|
const route = this.looseRoutesById[routeId];
|
|
const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
|
|
const shouldPending = !!(onReady && !this.isServer && !resolvePreload(matchId) && (route.options.loader || route.options.beforeLoad) && typeof pendingMs === "number" && pendingMs !== Infinity && (route.options.pendingComponent ?? ((_b = this.options) == null ? void 0 : _b.defaultPendingComponent)));
|
|
let executeBeforeLoad = true;
|
|
if (
|
|
// If we are in the middle of a load, either of these will be present
|
|
// (not to be confused with `loadPromise`, which is always defined)
|
|
existingMatch.beforeLoadPromise || existingMatch.loaderPromise
|
|
) {
|
|
if (shouldPending) {
|
|
setTimeout(() => {
|
|
try {
|
|
triggerOnReady();
|
|
} catch {
|
|
}
|
|
}, pendingMs);
|
|
}
|
|
await existingMatch.beforeLoadPromise;
|
|
executeBeforeLoad = this.getMatch(matchId).status !== "success";
|
|
}
|
|
if (executeBeforeLoad) {
|
|
try {
|
|
updateMatch(matchId, (prev) => {
|
|
const prevLoadPromise = prev.loadPromise;
|
|
return {
|
|
...prev,
|
|
loadPromise: createControlledPromise(() => {
|
|
prevLoadPromise == null ? void 0 : prevLoadPromise.resolve();
|
|
}),
|
|
beforeLoadPromise: createControlledPromise()
|
|
};
|
|
});
|
|
const abortController = new AbortController();
|
|
let pendingTimeout;
|
|
if (shouldPending) {
|
|
pendingTimeout = setTimeout(() => {
|
|
try {
|
|
triggerOnReady();
|
|
} catch {
|
|
}
|
|
}, pendingMs);
|
|
}
|
|
const { paramsError, searchError } = this.getMatch(matchId);
|
|
if (paramsError) {
|
|
handleSerialError(index, paramsError, "PARSE_PARAMS");
|
|
}
|
|
if (searchError) {
|
|
handleSerialError(index, searchError, "VALIDATE_SEARCH");
|
|
}
|
|
const getParentMatchContext = () => parentMatchId ? this.getMatch(parentMatchId).context : this.options.context ?? {};
|
|
updateMatch(matchId, (prev) => ({
|
|
...prev,
|
|
isFetching: "beforeLoad",
|
|
fetchCount: prev.fetchCount + 1,
|
|
abortController,
|
|
pendingTimeout,
|
|
context: {
|
|
...getParentMatchContext(),
|
|
...prev.__routeContext
|
|
}
|
|
}));
|
|
const { search, params, context, cause } = this.getMatch(matchId);
|
|
const preload = resolvePreload(matchId);
|
|
const beforeLoadFnContext = {
|
|
search,
|
|
abortController,
|
|
params,
|
|
preload,
|
|
context,
|
|
location,
|
|
navigate: (opts) => this.navigate({ ...opts, _fromLocation: location }),
|
|
buildLocation: this.buildLocation,
|
|
cause: preload ? "preload" : cause,
|
|
matches
|
|
};
|
|
const beforeLoadContext = await ((_d = (_c = route.options).beforeLoad) == null ? void 0 : _d.call(_c, beforeLoadFnContext)) ?? {};
|
|
if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
|
|
handleSerialError(index, beforeLoadContext, "BEFORE_LOAD");
|
|
}
|
|
updateMatch(matchId, (prev) => {
|
|
return {
|
|
...prev,
|
|
__beforeLoadContext: beforeLoadContext,
|
|
context: {
|
|
...getParentMatchContext(),
|
|
...prev.__routeContext,
|
|
...beforeLoadContext
|
|
},
|
|
abortController
|
|
};
|
|
});
|
|
} catch (err) {
|
|
handleSerialError(index, err, "BEFORE_LOAD");
|
|
}
|
|
updateMatch(matchId, (prev) => {
|
|
var _a2;
|
|
(_a2 = prev.beforeLoadPromise) == null ? void 0 : _a2.resolve();
|
|
return {
|
|
...prev,
|
|
beforeLoadPromise: void 0,
|
|
isFetching: false
|
|
};
|
|
});
|
|
}
|
|
}
|
|
const validResolvedMatches = matches.slice(0, firstBadMatchIndex);
|
|
const matchPromises = [];
|
|
validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
|
|
matchPromises.push(
|
|
(async () => {
|
|
const { loaderPromise: prevLoaderPromise } = this.getMatch(matchId);
|
|
let loaderShouldRunAsync = false;
|
|
let loaderIsRunningAsync = false;
|
|
if (prevLoaderPromise) {
|
|
await prevLoaderPromise;
|
|
const match = this.getMatch(matchId);
|
|
if (match.error) {
|
|
handleRedirectAndNotFound(match, match.error);
|
|
}
|
|
} else {
|
|
const parentMatchPromise = matchPromises[index - 1];
|
|
const route = this.looseRoutesById[routeId];
|
|
const getLoaderContext = () => {
|
|
const {
|
|
params,
|
|
loaderDeps,
|
|
abortController,
|
|
context,
|
|
cause
|
|
} = this.getMatch(matchId);
|
|
const preload2 = resolvePreload(matchId);
|
|
return {
|
|
params,
|
|
deps: loaderDeps,
|
|
preload: !!preload2,
|
|
parentMatchPromise,
|
|
abortController,
|
|
context,
|
|
location,
|
|
navigate: (opts) => this.navigate({ ...opts, _fromLocation: location }),
|
|
cause: preload2 ? "preload" : cause,
|
|
route
|
|
};
|
|
};
|
|
const age = Date.now() - this.getMatch(matchId).updatedAt;
|
|
const preload = resolvePreload(matchId);
|
|
const staleAge = preload ? route.options.preloadStaleTime ?? this.options.defaultPreloadStaleTime ?? 3e4 : route.options.staleTime ?? this.options.defaultStaleTime ?? 0;
|
|
const shouldReloadOption = route.options.shouldReload;
|
|
const shouldReload = typeof shouldReloadOption === "function" ? shouldReloadOption(getLoaderContext()) : shouldReloadOption;
|
|
updateMatch(matchId, (prev) => ({
|
|
...prev,
|
|
loaderPromise: createControlledPromise(),
|
|
preload: !!preload && !this.state.matches.find((d) => d.id === matchId)
|
|
}));
|
|
const runLoader = async () => {
|
|
var _a2, _b2, _c2, _d2, _e, _f, _g, _h, _i, _j, _k;
|
|
try {
|
|
const potentialPendingMinPromise = async () => {
|
|
const latestMatch = this.getMatch(matchId);
|
|
if (latestMatch.minPendingPromise) {
|
|
await latestMatch.minPendingPromise;
|
|
}
|
|
};
|
|
try {
|
|
this.loadRouteChunk(route);
|
|
updateMatch(matchId, (prev) => ({
|
|
...prev,
|
|
isFetching: "loader"
|
|
}));
|
|
const loaderData = await ((_b2 = (_a2 = route.options).loader) == null ? void 0 : _b2.call(_a2, getLoaderContext()));
|
|
handleRedirectAndNotFound(
|
|
this.getMatch(matchId),
|
|
loaderData
|
|
);
|
|
await route._lazyPromise;
|
|
await potentialPendingMinPromise();
|
|
const assetContext = {
|
|
matches,
|
|
match: this.getMatch(matchId),
|
|
params: this.getMatch(matchId).params,
|
|
loaderData
|
|
};
|
|
const headFnContent = (_d2 = (_c2 = route.options).head) == null ? void 0 : _d2.call(_c2, assetContext);
|
|
const meta = headFnContent == null ? void 0 : headFnContent.meta;
|
|
const links = headFnContent == null ? void 0 : headFnContent.links;
|
|
const headScripts = headFnContent == null ? void 0 : headFnContent.scripts;
|
|
const scripts = (_f = (_e = route.options).scripts) == null ? void 0 : _f.call(_e, assetContext);
|
|
const headers = (_h = (_g = route.options).headers) == null ? void 0 : _h.call(_g, {
|
|
loaderData
|
|
});
|
|
updateMatch(matchId, (prev) => ({
|
|
...prev,
|
|
error: void 0,
|
|
status: "success",
|
|
isFetching: false,
|
|
updatedAt: Date.now(),
|
|
loaderData,
|
|
meta,
|
|
links,
|
|
headScripts,
|
|
headers,
|
|
scripts
|
|
}));
|
|
} catch (e) {
|
|
let error = e;
|
|
await potentialPendingMinPromise();
|
|
handleRedirectAndNotFound(this.getMatch(matchId), e);
|
|
try {
|
|
(_j = (_i = route.options).onError) == null ? void 0 : _j.call(_i, e);
|
|
} catch (onErrorError) {
|
|
error = onErrorError;
|
|
handleRedirectAndNotFound(
|
|
this.getMatch(matchId),
|
|
onErrorError
|
|
);
|
|
}
|
|
updateMatch(matchId, (prev) => ({
|
|
...prev,
|
|
error,
|
|
status: "error",
|
|
isFetching: false
|
|
}));
|
|
}
|
|
(_k = this.serverSsr) == null ? void 0 : _k.onMatchSettled({
|
|
router: this,
|
|
match: this.getMatch(matchId)
|
|
});
|
|
await route._componentsPromise;
|
|
} catch (err) {
|
|
updateMatch(matchId, (prev) => ({
|
|
...prev,
|
|
loaderPromise: void 0
|
|
}));
|
|
handleRedirectAndNotFound(this.getMatch(matchId), err);
|
|
}
|
|
};
|
|
const { status, invalid } = this.getMatch(matchId);
|
|
loaderShouldRunAsync = status === "success" && (invalid || (shouldReload ?? age > staleAge));
|
|
if (preload && route.options.preload === false) {
|
|
} else if (loaderShouldRunAsync && !sync) {
|
|
loaderIsRunningAsync = true;
|
|
(async () => {
|
|
try {
|
|
await runLoader();
|
|
const { loaderPromise, loadPromise } = this.getMatch(matchId);
|
|
loaderPromise == null ? void 0 : loaderPromise.resolve();
|
|
loadPromise == null ? void 0 : loadPromise.resolve();
|
|
updateMatch(matchId, (prev) => ({
|
|
...prev,
|
|
loaderPromise: void 0
|
|
}));
|
|
} catch (err) {
|
|
if (isResolvedRedirect(err)) {
|
|
await this.navigate(err);
|
|
}
|
|
}
|
|
})();
|
|
} else if (status !== "success" || loaderShouldRunAsync && sync) {
|
|
await runLoader();
|
|
}
|
|
}
|
|
if (!loaderIsRunningAsync) {
|
|
const { loaderPromise, loadPromise } = this.getMatch(matchId);
|
|
loaderPromise == null ? void 0 : loaderPromise.resolve();
|
|
loadPromise == null ? void 0 : loadPromise.resolve();
|
|
}
|
|
updateMatch(matchId, (prev) => ({
|
|
...prev,
|
|
isFetching: loaderIsRunningAsync ? prev.isFetching : false,
|
|
loaderPromise: loaderIsRunningAsync ? prev.loaderPromise : void 0,
|
|
invalid: false
|
|
}));
|
|
return this.getMatch(matchId);
|
|
})()
|
|
);
|
|
});
|
|
await Promise.all(matchPromises);
|
|
resolveAll();
|
|
} catch (err) {
|
|
rejectAll(err);
|
|
}
|
|
})();
|
|
});
|
|
await triggerOnReady();
|
|
} catch (err) {
|
|
if (isRedirect(err) || isNotFound(err)) {
|
|
if (isNotFound(err) && !allPreload) {
|
|
await triggerOnReady();
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
return matches;
|
|
};
|
|
this.invalidate = (opts) => {
|
|
const invalidate = (d) => {
|
|
var _a;
|
|
if (((_a = opts == null ? void 0 : opts.filter) == null ? void 0 : _a.call(opts, d)) ?? true) {
|
|
return {
|
|
...d,
|
|
invalid: true,
|
|
...d.status === "error" ? { status: "pending", error: void 0 } : {}
|
|
};
|
|
}
|
|
return d;
|
|
};
|
|
this.__store.setState((s) => {
|
|
var _a;
|
|
return {
|
|
...s,
|
|
matches: s.matches.map(invalidate),
|
|
cachedMatches: s.cachedMatches.map(invalidate),
|
|
pendingMatches: (_a = s.pendingMatches) == null ? void 0 : _a.map(invalidate)
|
|
};
|
|
});
|
|
return this.load({ sync: opts == null ? void 0 : opts.sync });
|
|
};
|
|
this.resolveRedirect = (err) => {
|
|
const redirect = err;
|
|
if (!redirect.href) {
|
|
redirect.href = this.buildLocation(redirect).href;
|
|
}
|
|
return redirect;
|
|
};
|
|
this.clearCache = (opts) => {
|
|
const filter = opts == null ? void 0 : opts.filter;
|
|
if (filter !== void 0) {
|
|
this.__store.setState((s) => {
|
|
return {
|
|
...s,
|
|
cachedMatches: s.cachedMatches.filter(
|
|
(m) => !filter(m)
|
|
)
|
|
};
|
|
});
|
|
} else {
|
|
this.__store.setState((s) => {
|
|
return {
|
|
...s,
|
|
cachedMatches: []
|
|
};
|
|
});
|
|
}
|
|
};
|
|
this.clearExpiredCache = () => {
|
|
const filter = (d) => {
|
|
const route = this.looseRoutesById[d.routeId];
|
|
if (!route.options.loader) {
|
|
return true;
|
|
}
|
|
const gcTime = (d.preload ? route.options.preloadGcTime ?? this.options.defaultPreloadGcTime : route.options.gcTime ?? this.options.defaultGcTime) ?? 5 * 60 * 1e3;
|
|
return !(d.status !== "error" && Date.now() - d.updatedAt < gcTime);
|
|
};
|
|
this.clearCache({ filter });
|
|
};
|
|
this.loadRouteChunk = (route) => {
|
|
if (route._lazyPromise === void 0) {
|
|
if (route.lazyFn) {
|
|
route._lazyPromise = route.lazyFn().then((lazyRoute) => {
|
|
const { id: _id, ...options2 } = lazyRoute.options;
|
|
Object.assign(route.options, options2);
|
|
});
|
|
} else {
|
|
route._lazyPromise = Promise.resolve();
|
|
}
|
|
}
|
|
if (route._componentsPromise === void 0) {
|
|
route._componentsPromise = route._lazyPromise.then(
|
|
() => Promise.all(
|
|
componentTypes.map(async (type) => {
|
|
const component = route.options[type];
|
|
if (component == null ? void 0 : component.preload) {
|
|
await component.preload();
|
|
}
|
|
})
|
|
)
|
|
);
|
|
}
|
|
return route._componentsPromise;
|
|
};
|
|
this.preloadRoute = async (opts) => {
|
|
const next = this.buildLocation(opts);
|
|
let matches = this.matchRoutes(next, {
|
|
throwOnError: true,
|
|
preload: true,
|
|
dest: opts
|
|
});
|
|
const activeMatchIds = new Set(
|
|
[...this.state.matches, ...this.state.pendingMatches ?? []].map(
|
|
(d) => d.id
|
|
)
|
|
);
|
|
const loadedMatchIds = /* @__PURE__ */ new Set([
|
|
...activeMatchIds,
|
|
...this.state.cachedMatches.map((d) => d.id)
|
|
]);
|
|
batch(() => {
|
|
matches.forEach((match) => {
|
|
if (!loadedMatchIds.has(match.id)) {
|
|
this.__store.setState((s) => ({
|
|
...s,
|
|
cachedMatches: [...s.cachedMatches, match]
|
|
}));
|
|
}
|
|
});
|
|
});
|
|
try {
|
|
matches = await this.loadMatches({
|
|
matches,
|
|
location: next,
|
|
preload: true,
|
|
updateMatch: (id, updater) => {
|
|
if (activeMatchIds.has(id)) {
|
|
matches = matches.map((d) => d.id === id ? updater(d) : d);
|
|
} else {
|
|
this.updateMatch(id, updater);
|
|
}
|
|
}
|
|
});
|
|
return matches;
|
|
} catch (err) {
|
|
if (isRedirect(err)) {
|
|
if (err.reloadDocument) {
|
|
return void 0;
|
|
}
|
|
return await this.preloadRoute({
|
|
...err,
|
|
_fromLocation: next
|
|
});
|
|
}
|
|
if (!isNotFound(err)) {
|
|
console.error(err);
|
|
}
|
|
return void 0;
|
|
}
|
|
};
|
|
this.matchRoute = (location, opts) => {
|
|
const matchLocation = {
|
|
...location,
|
|
to: location.to ? this.resolvePathWithBase(
|
|
location.from || "",
|
|
location.to
|
|
) : void 0,
|
|
params: location.params || {},
|
|
leaveParams: true
|
|
};
|
|
const next = this.buildLocation(matchLocation);
|
|
if ((opts == null ? void 0 : opts.pending) && this.state.status !== "pending") {
|
|
return false;
|
|
}
|
|
const pending = (opts == null ? void 0 : opts.pending) === void 0 ? !this.state.isLoading : opts.pending;
|
|
const baseLocation = pending ? this.latestLocation : this.state.resolvedLocation || this.state.location;
|
|
const match = matchPathname(this.basepath, baseLocation.pathname, {
|
|
...opts,
|
|
to: next.pathname
|
|
});
|
|
if (!match) {
|
|
return false;
|
|
}
|
|
if (location.params) {
|
|
if (!deepEqual(match, location.params, { partial: true })) {
|
|
return false;
|
|
}
|
|
}
|
|
if (match && ((opts == null ? void 0 : opts.includeSearch) ?? true)) {
|
|
return deepEqual(baseLocation.search, next.search, { partial: true }) ? match : false;
|
|
}
|
|
return match;
|
|
};
|
|
this._handleNotFound = (matches, err, {
|
|
updateMatch = this.updateMatch
|
|
} = {}) => {
|
|
var _a;
|
|
const routeCursor = this.routesById[err.routeId ?? ""] ?? this.routeTree;
|
|
const matchesByRouteId = {};
|
|
for (const match of matches) {
|
|
matchesByRouteId[match.routeId] = match;
|
|
}
|
|
if (!routeCursor.options.notFoundComponent && ((_a = this.options) == null ? void 0 : _a.defaultNotFoundComponent)) {
|
|
routeCursor.options.notFoundComponent = this.options.defaultNotFoundComponent;
|
|
}
|
|
invariant(
|
|
routeCursor.options.notFoundComponent,
|
|
"No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router."
|
|
);
|
|
const matchForRoute = matchesByRouteId[routeCursor.id];
|
|
invariant(
|
|
matchForRoute,
|
|
"Could not find match for route: " + routeCursor.id
|
|
);
|
|
updateMatch(matchForRoute.id, (prev) => ({
|
|
...prev,
|
|
status: "notFound",
|
|
error: err,
|
|
isFetching: false
|
|
}));
|
|
if (err.routerCode === "BEFORE_LOAD" && routeCursor.parentRoute) {
|
|
err.routeId = routeCursor.parentRoute.id;
|
|
this._handleNotFound(matches, err, {
|
|
updateMatch
|
|
});
|
|
}
|
|
};
|
|
this.hasNotFoundMatch = () => {
|
|
return this.__store.state.matches.some(
|
|
(d) => d.status === "notFound" || d.globalNotFound
|
|
);
|
|
};
|
|
this.update({
|
|
defaultPreloadDelay: 50,
|
|
defaultPendingMs: 1e3,
|
|
defaultPendingMinMs: 500,
|
|
context: void 0,
|
|
...options,
|
|
caseSensitive: options.caseSensitive ?? false,
|
|
notFoundMode: options.notFoundMode ?? "fuzzy",
|
|
stringifySearch: options.stringifySearch ?? defaultStringifySearch,
|
|
parseSearch: options.parseSearch ?? defaultParseSearch
|
|
});
|
|
if (typeof document !== "undefined") {
|
|
window.__TSR_ROUTER__ = this;
|
|
}
|
|
}
|
|
get state() {
|
|
return this.__store.state;
|
|
}
|
|
get looseRoutesById() {
|
|
return this.routesById;
|
|
}
|
|
matchRoutesInternal(next, opts) {
|
|
const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
|
|
next,
|
|
opts == null ? void 0 : opts.dest
|
|
);
|
|
let isGlobalNotFound = false;
|
|
if (
|
|
// If we found a route, and it's not an index route and we have left over path
|
|
foundRoute ? foundRoute.path !== "/" && routeParams["**"] : (
|
|
// Or if we didn't find a route and we have left over path
|
|
trimPathRight(next.pathname)
|
|
)
|
|
) {
|
|
if (this.options.notFoundRoute) {
|
|
matchedRoutes.push(this.options.notFoundRoute);
|
|
} else {
|
|
isGlobalNotFound = true;
|
|
}
|
|
}
|
|
const globalNotFoundRouteId = (() => {
|
|
if (!isGlobalNotFound) {
|
|
return void 0;
|
|
}
|
|
if (this.options.notFoundMode !== "root") {
|
|
for (let i = matchedRoutes.length - 1; i >= 0; i--) {
|
|
const route = matchedRoutes[i];
|
|
if (route.children) {
|
|
return route.id;
|
|
}
|
|
}
|
|
}
|
|
return rootRouteId;
|
|
})();
|
|
const parseErrors = matchedRoutes.map((route) => {
|
|
var _a;
|
|
let parsedParamsError;
|
|
const parseParams = ((_a = route.options.params) == null ? void 0 : _a.parse) ?? route.options.parseParams;
|
|
if (parseParams) {
|
|
try {
|
|
const parsedParams = parseParams(routeParams);
|
|
Object.assign(routeParams, parsedParams);
|
|
} catch (err) {
|
|
parsedParamsError = new PathParamError(err.message, {
|
|
cause: err
|
|
});
|
|
if (opts == null ? void 0 : opts.throwOnError) {
|
|
throw parsedParamsError;
|
|
}
|
|
return parsedParamsError;
|
|
}
|
|
}
|
|
return;
|
|
});
|
|
const matches = [];
|
|
const getParentContext = (parentMatch) => {
|
|
const parentMatchId = parentMatch == null ? void 0 : parentMatch.id;
|
|
const parentContext = !parentMatchId ? this.options.context ?? {} : parentMatch.context ?? this.options.context ?? {};
|
|
return parentContext;
|
|
};
|
|
matchedRoutes.forEach((route, index) => {
|
|
var _a, _b;
|
|
const parentMatch = matches[index - 1];
|
|
const [preMatchSearch, strictMatchSearch, searchError] = (() => {
|
|
const parentSearch = (parentMatch == null ? void 0 : parentMatch.search) ?? next.search;
|
|
const parentStrictSearch = (parentMatch == null ? void 0 : parentMatch._strictSearch) ?? {};
|
|
try {
|
|
const strictSearch = validateSearch(route.options.validateSearch, { ...parentSearch }) ?? {};
|
|
return [
|
|
{
|
|
...parentSearch,
|
|
...strictSearch
|
|
},
|
|
{ ...parentStrictSearch, ...strictSearch },
|
|
void 0
|
|
];
|
|
} catch (err) {
|
|
let searchParamError = err;
|
|
if (!(err instanceof SearchParamError)) {
|
|
searchParamError = new SearchParamError(err.message, {
|
|
cause: err
|
|
});
|
|
}
|
|
if (opts == null ? void 0 : opts.throwOnError) {
|
|
throw searchParamError;
|
|
}
|
|
return [parentSearch, {}, searchParamError];
|
|
}
|
|
})();
|
|
const loaderDeps = ((_b = (_a = route.options).loaderDeps) == null ? void 0 : _b.call(_a, {
|
|
search: preMatchSearch
|
|
})) ?? "";
|
|
const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : "";
|
|
const { usedParams, interpolatedPath } = interpolatePath({
|
|
path: route.fullPath,
|
|
params: routeParams,
|
|
decodeCharMap: this.pathParamsDecodeCharMap
|
|
});
|
|
const matchId = interpolatePath({
|
|
path: route.id,
|
|
params: routeParams,
|
|
leaveWildcards: true,
|
|
decodeCharMap: this.pathParamsDecodeCharMap
|
|
}).interpolatedPath + loaderDepsHash;
|
|
const existingMatch = this.getMatch(matchId);
|
|
const previousMatch = this.state.matches.find(
|
|
(d) => d.routeId === route.id
|
|
);
|
|
const cause = previousMatch ? "stay" : "enter";
|
|
let match;
|
|
if (existingMatch) {
|
|
match = {
|
|
...existingMatch,
|
|
cause,
|
|
params: previousMatch ? replaceEqualDeep(previousMatch.params, routeParams) : routeParams,
|
|
_strictParams: usedParams,
|
|
search: previousMatch ? replaceEqualDeep(previousMatch.search, preMatchSearch) : replaceEqualDeep(existingMatch.search, preMatchSearch),
|
|
_strictSearch: strictMatchSearch
|
|
};
|
|
} else {
|
|
const status = route.options.loader || route.options.beforeLoad || route.lazyFn || routeNeedsPreload(route) ? "pending" : "success";
|
|
match = {
|
|
id: matchId,
|
|
index,
|
|
routeId: route.id,
|
|
params: previousMatch ? replaceEqualDeep(previousMatch.params, routeParams) : routeParams,
|
|
_strictParams: usedParams,
|
|
pathname: joinPaths([this.basepath, interpolatedPath]),
|
|
updatedAt: Date.now(),
|
|
search: previousMatch ? replaceEqualDeep(previousMatch.search, preMatchSearch) : preMatchSearch,
|
|
_strictSearch: strictMatchSearch,
|
|
searchError: void 0,
|
|
status,
|
|
isFetching: false,
|
|
error: void 0,
|
|
paramsError: parseErrors[index],
|
|
__routeContext: {},
|
|
__beforeLoadContext: {},
|
|
context: {},
|
|
abortController: new AbortController(),
|
|
fetchCount: 0,
|
|
cause,
|
|
loaderDeps: previousMatch ? replaceEqualDeep(previousMatch.loaderDeps, loaderDeps) : loaderDeps,
|
|
invalid: false,
|
|
preload: false,
|
|
links: void 0,
|
|
scripts: void 0,
|
|
headScripts: void 0,
|
|
meta: void 0,
|
|
staticData: route.options.staticData || {},
|
|
loadPromise: createControlledPromise(),
|
|
fullPath: route.fullPath
|
|
};
|
|
}
|
|
if (!(opts == null ? void 0 : opts.preload)) {
|
|
match.globalNotFound = globalNotFoundRouteId === route.id;
|
|
}
|
|
match.searchError = searchError;
|
|
const parentContext = getParentContext(parentMatch);
|
|
match.context = {
|
|
...parentContext,
|
|
...match.__routeContext,
|
|
...match.__beforeLoadContext
|
|
};
|
|
matches.push(match);
|
|
});
|
|
matches.forEach((match, index) => {
|
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
const route = this.looseRoutesById[match.routeId];
|
|
const existingMatch = this.getMatch(match.id);
|
|
if (!existingMatch && (opts == null ? void 0 : opts._buildLocation) !== true) {
|
|
const parentMatch = matches[index - 1];
|
|
const parentContext = getParentContext(parentMatch);
|
|
const contextFnContext = {
|
|
deps: match.loaderDeps,
|
|
params: match.params,
|
|
context: parentContext,
|
|
location: next,
|
|
navigate: (opts2) => this.navigate({ ...opts2, _fromLocation: next }),
|
|
buildLocation: this.buildLocation,
|
|
cause: match.cause,
|
|
abortController: match.abortController,
|
|
preload: !!match.preload,
|
|
matches
|
|
};
|
|
match.__routeContext = ((_b = (_a = route.options).context) == null ? void 0 : _b.call(_a, contextFnContext)) ?? {};
|
|
match.context = {
|
|
...parentContext,
|
|
...match.__routeContext,
|
|
...match.__beforeLoadContext
|
|
};
|
|
}
|
|
if (match.status === "success") {
|
|
match.headers = (_d = (_c = route.options).headers) == null ? void 0 : _d.call(_c, {
|
|
loaderData: match.loaderData
|
|
});
|
|
const assetContext = {
|
|
matches,
|
|
match,
|
|
params: match.params,
|
|
loaderData: match.loaderData
|
|
};
|
|
const headFnContent = (_f = (_e = route.options).head) == null ? void 0 : _f.call(_e, assetContext);
|
|
match.links = headFnContent == null ? void 0 : headFnContent.links;
|
|
match.headScripts = headFnContent == null ? void 0 : headFnContent.scripts;
|
|
match.meta = headFnContent == null ? void 0 : headFnContent.meta;
|
|
match.scripts = (_h = (_g = route.options).scripts) == null ? void 0 : _h.call(_g, assetContext);
|
|
}
|
|
});
|
|
return matches;
|
|
}
|
|
}
|
|
class SearchParamError extends Error {
|
|
}
|
|
class PathParamError extends Error {
|
|
}
|
|
function lazyFn(fn, key) {
|
|
return async (...args) => {
|
|
const imported = await fn();
|
|
return imported[key || "default"](...args);
|
|
};
|
|
}
|
|
function getInitialRouterState(location) {
|
|
return {
|
|
loadedAt: 0,
|
|
isLoading: false,
|
|
isTransitioning: false,
|
|
status: "idle",
|
|
resolvedLocation: void 0,
|
|
location,
|
|
matches: [],
|
|
pendingMatches: [],
|
|
cachedMatches: [],
|
|
statusCode: 200
|
|
};
|
|
}
|
|
function validateSearch(validateSearch2, input) {
|
|
if (validateSearch2 == null) return {};
|
|
if ("~standard" in validateSearch2) {
|
|
const result = validateSearch2["~standard"].validate(input);
|
|
if (result instanceof Promise)
|
|
throw new SearchParamError("Async validation not supported");
|
|
if (result.issues)
|
|
throw new SearchParamError(JSON.stringify(result.issues, void 0, 2), {
|
|
cause: result
|
|
});
|
|
return result.value;
|
|
}
|
|
if ("parse" in validateSearch2) {
|
|
return validateSearch2.parse(input);
|
|
}
|
|
if (typeof validateSearch2 === "function") {
|
|
return validateSearch2(input);
|
|
}
|
|
return {};
|
|
}
|
|
const componentTypes = [
|
|
"component",
|
|
"errorComponent",
|
|
"pendingComponent",
|
|
"notFoundComponent"
|
|
];
|
|
function routeNeedsPreload(route) {
|
|
var _a;
|
|
for (const componentType of componentTypes) {
|
|
if ((_a = route.options[componentType]) == null ? void 0 : _a.preload) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
export {
|
|
PathParamError,
|
|
RouterCore,
|
|
SearchParamError,
|
|
componentTypes,
|
|
defaultSerializeError,
|
|
getInitialRouterState,
|
|
getLocationChangeInfo,
|
|
lazyFn
|
|
};
|
|
//# sourceMappingURL=router.js.map
|