1176 lines
46 KiB
Plaintext
1176 lines
46 KiB
Plaintext
import { DEFAULT_PROTOCOL_ALLOWLIST, createControlledPromise, decodePath, deepEqual, encodePathLikeUrl, findLast, functionalUpdate, hasKeys, isDangerousProtocol, last, nullReplaceEqualDeep, replaceEqualDeep } from "./utils.js";
|
|
import { createLRUCache } from "./lru-cache.js";
|
|
import { buildRouteBranch, findFlatMatch, findRouteMatch, findSingleMatch, processRouteMasks, processRouteTree } from "./new-process-route-tree.js";
|
|
import { cleanPath, compileDecodeCharMap, interpolatePath, resolvePath, trimPath, trimPathRight } 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 { isRedirect, redirect } from "./redirect.js";
|
|
import { loadMatches, loadRouteChunk, routeNeedsPreload } from "./load-matches.js";
|
|
import { composeRewrites, executeRewriteInput, executeRewriteOutput, rewriteBasepath } from "./rewrite.js";
|
|
import { createRouterStores } from "./stores.js";
|
|
import { createBrowserHistory, parseHref } from "@tanstack/history";
|
|
import { isServer } from "@tanstack/router-core/isServer";
|
|
//#region src/router.ts
|
|
/**
|
|
* Convert an unknown error into a minimal, serializable object.
|
|
* Includes name and message (and stack in development).
|
|
*/
|
|
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 };
|
|
}
|
|
/** Options for configuring trailing-slash behavior. */
|
|
const trailingSlashOptions = {
|
|
always: "always",
|
|
never: "never",
|
|
preserve: "preserve"
|
|
};
|
|
/**
|
|
* Compute whether path, href or hash changed between previous and current
|
|
* resolved locations.
|
|
*/
|
|
function getLocationChangeInfo(location, resolvedLocation) {
|
|
const fromLocation = resolvedLocation;
|
|
const toLocation = location;
|
|
return {
|
|
fromLocation,
|
|
toLocation,
|
|
pathChanged: fromLocation?.pathname !== toLocation.pathname,
|
|
hrefChanged: fromLocation?.href !== toLocation.href,
|
|
hashChanged: fromLocation?.hash !== toLocation.hash
|
|
};
|
|
}
|
|
/**
|
|
* Core, framework-agnostic router engine that powers TanStack Router.
|
|
*
|
|
* Provides navigation, matching, loading, preloading, caching and event APIs
|
|
* used by framework adapters (React/Solid). Prefer framework helpers like
|
|
* `createRouter` in app code.
|
|
*
|
|
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouterType
|
|
*/
|
|
var RouterCore = class {
|
|
/**
|
|
* @deprecated Use the `createRouter` function instead
|
|
*/
|
|
constructor(options, getStoreConfig) {
|
|
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.routeBranchCache = /* @__PURE__ */ new WeakMap();
|
|
this.startTransition = (fn) => fn();
|
|
this.update = (newOptions) => {
|
|
if (process.env.NODE_ENV !== "production") {
|
|
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 prevOptions = this.options;
|
|
const prevBasepath = this.basepath ?? prevOptions?.basepath ?? "/";
|
|
const basepathWasUnset = this.basepath === void 0;
|
|
const prevRewriteOption = prevOptions?.rewrite;
|
|
this.options = {
|
|
...prevOptions,
|
|
...newOptions
|
|
};
|
|
this.isServer = this.options.isServer ?? typeof document === "undefined";
|
|
this.protocolAllowlist = new Set(this.options.protocolAllowlist);
|
|
if (this.options.pathParamsAllowedCharacters) this.pathParamsDecoder = compileDecodeCharMap(this.options.pathParamsAllowedCharacters);
|
|
if (!this.history || this.options.history && this.options.history !== this.history) if (!this.options.history) {
|
|
if (!(isServer ?? this.isServer)) this.history = createBrowserHistory();
|
|
} else this.history = this.options.history;
|
|
this.origin = this.options.origin;
|
|
if (!this.origin) if (!(isServer ?? this.isServer) && window?.origin && window.origin !== "null") this.origin = window.origin;
|
|
else this.origin = "http://localhost";
|
|
if (this.history) this.updateLatestLocation();
|
|
if (this.options.routeTree !== this.routeTree) {
|
|
this.routeTree = this.options.routeTree;
|
|
let processRouteTreeResult;
|
|
if ((isServer ?? this.isServer) && process.env.NODE_ENV !== "development" && globalThis.__TSR_CACHE__ && globalThis.__TSR_CACHE__.routeTree === this.routeTree) {
|
|
const cached = globalThis.__TSR_CACHE__;
|
|
this.resolvePathCache = cached.resolvePathCache;
|
|
processRouteTreeResult = cached.processRouteTreeResult;
|
|
} else {
|
|
this.resolvePathCache = createLRUCache(1e3);
|
|
processRouteTreeResult = this.buildRouteTree();
|
|
if ((isServer ?? this.isServer) && process.env.NODE_ENV !== "development" && globalThis.__TSR_CACHE__ === void 0) globalThis.__TSR_CACHE__ = {
|
|
routeTree: this.routeTree,
|
|
processRouteTreeResult,
|
|
resolvePathCache: this.resolvePathCache
|
|
};
|
|
}
|
|
this.setRoutes(processRouteTreeResult);
|
|
}
|
|
if (!this.stores && this.latestLocation) {
|
|
const config = this.getStoreConfig(this);
|
|
this.batch = config.batch;
|
|
this.stores = createRouterStores(getInitialRouterState(this.latestLocation), config);
|
|
if (!(isServer ?? this.isServer)) setupScrollRestoration(this);
|
|
}
|
|
let needsLocationUpdate = false;
|
|
const nextBasepath = this.options.basepath ?? "/";
|
|
const nextRewriteOption = this.options.rewrite;
|
|
if (basepathWasUnset || prevBasepath !== nextBasepath || prevRewriteOption !== nextRewriteOption) {
|
|
this.basepath = nextBasepath;
|
|
const rewrites = [];
|
|
const trimmed = trimPath(nextBasepath);
|
|
if (trimmed && trimmed !== "/") rewrites.push(rewriteBasepath({ basepath: nextBasepath }));
|
|
if (nextRewriteOption) rewrites.push(nextRewriteOption);
|
|
this.rewrite = rewrites.length === 0 ? void 0 : rewrites.length === 1 ? rewrites[0] : composeRewrites(rewrites);
|
|
if (this.history) this.updateLatestLocation();
|
|
needsLocationUpdate = true;
|
|
}
|
|
if (needsLocationUpdate && this.stores) this.stores.location.set(this.latestLocation);
|
|
if (typeof window !== "undefined" && "CSS" in window && typeof window.CSS?.supports === "function") this.isViewTransitionTypesSupported = window.CSS.supports("selector(:active-view-transition-type(a)");
|
|
};
|
|
this.updateLatestLocation = () => {
|
|
this.latestLocation = this.parseLocation(this.history.location, this.latestLocation);
|
|
};
|
|
this.buildRouteTree = () => {
|
|
const result = processRouteTree(this.routeTree, this.options.caseSensitive, (route, i) => {
|
|
route.init({ originalIndex: i });
|
|
});
|
|
if (this.options.routeMasks) processRouteMasks(this.options.routeMasks, result.processedTree);
|
|
return result;
|
|
};
|
|
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 = (locationToParse, previousLocation) => {
|
|
const parse = ({ pathname, search, hash, href, state }) => {
|
|
if (!this.rewrite && !/[ \x00-\x1f\x7f\u0080-\uffff]/.test(pathname)) {
|
|
const parsedSearch = this.options.parseSearch(search);
|
|
const searchStr = this.options.stringifySearch(parsedSearch);
|
|
return {
|
|
href: pathname + searchStr + hash,
|
|
publicHref: pathname + searchStr + hash,
|
|
pathname: decodePath(pathname).path,
|
|
external: false,
|
|
searchStr,
|
|
search: nullReplaceEqualDeep(previousLocation?.search, parsedSearch),
|
|
hash: decodePath(hash.slice(1)).path,
|
|
state: replaceEqualDeep(previousLocation?.state, state)
|
|
};
|
|
}
|
|
const fullUrl = new URL(href, this.origin);
|
|
const url = executeRewriteInput(this.rewrite, fullUrl);
|
|
const parsedSearch = this.options.parseSearch(url.search);
|
|
const searchStr = this.options.stringifySearch(parsedSearch);
|
|
url.search = searchStr;
|
|
return {
|
|
href: url.href.replace(url.origin, ""),
|
|
publicHref: href,
|
|
pathname: decodePath(url.pathname).path,
|
|
external: !!this.rewrite && url.origin !== this.origin,
|
|
searchStr,
|
|
search: nullReplaceEqualDeep(previousLocation?.search, parsedSearch),
|
|
hash: decodePath(url.hash.slice(1)).path,
|
|
state: replaceEqualDeep(previousLocation?.state, state)
|
|
};
|
|
};
|
|
const location = parse(locationToParse);
|
|
const { __tempLocation, __tempKey } = location.state;
|
|
if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
|
|
const parsedTempLocation = parse(__tempLocation);
|
|
parsedTempLocation.state.key = location.state.key;
|
|
parsedTempLocation.state.__TSR_key = location.state.__TSR_key;
|
|
delete parsedTempLocation.state.__tempLocation;
|
|
return {
|
|
...parsedTempLocation,
|
|
maskedLocation: location
|
|
};
|
|
}
|
|
return location;
|
|
};
|
|
this.resolvePathWithBase = (from, path) => {
|
|
return resolvePath({
|
|
base: from,
|
|
to: path.includes("//") ? cleanPath(path) : path,
|
|
trailingSlash: this.options.trailingSlash,
|
|
cache: this.resolvePathCache
|
|
});
|
|
};
|
|
this.matchRoutes = (pathnameOrNext, locationSearchOrOpts, opts) => {
|
|
if (typeof pathnameOrNext === "string") return this.matchRoutesInternal({
|
|
pathname: pathnameOrNext,
|
|
search: locationSearchOrOpts
|
|
}, opts);
|
|
return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts);
|
|
};
|
|
this.getMatchedRoutes = (pathname) => {
|
|
return getMatchedRoutes({
|
|
pathname,
|
|
routesById: this.routesById,
|
|
processedTree: this.processedTree
|
|
});
|
|
};
|
|
this.cancelMatch = (id) => {
|
|
const match = this.getMatch(id);
|
|
if (!match) return;
|
|
match.abortController.abort();
|
|
clearTimeout(match._nonReactive.pendingTimeout);
|
|
match._nonReactive.pendingTimeout = void 0;
|
|
};
|
|
this.cancelMatches = () => {
|
|
this.stores.pendingIds.get().forEach((matchId) => {
|
|
this.cancelMatch(matchId);
|
|
});
|
|
this.stores.matchesId.get().forEach((matchId) => {
|
|
if (this.stores.pendingMatchStores.has(matchId)) return;
|
|
const match = this.stores.matchStores.get(matchId)?.get();
|
|
if (!match) return;
|
|
if (match.status === "pending" || match.isFetching === "loader") this.cancelMatch(matchId);
|
|
});
|
|
};
|
|
this.buildLocation = (opts) => {
|
|
const build = (dest = {}) => {
|
|
const currentLocation = dest._fromLocation || this.pendingBuiltLocation || this.latestLocation;
|
|
const lightweightResult = this.matchRoutesLightweight(currentLocation);
|
|
if (dest.from && process.env.NODE_ENV !== "production" && dest._isNavigate) {
|
|
const allFromMatches = this.getMatchedRoutes(dest.from).matchedRoutes;
|
|
const matchedFrom = findLast(lightweightResult.matchedRoutes, (d) => {
|
|
return comparePaths(d.fullPath, dest.from);
|
|
});
|
|
const matchedCurrent = findLast(allFromMatches, (d) => {
|
|
return comparePaths(d.fullPath, lightweightResult.fullPath);
|
|
});
|
|
if (!matchedFrom && !matchedCurrent) console.warn(`Could not find match for from: ${dest.from}`);
|
|
}
|
|
const defaultedFromPath = dest.unsafeRelative === "path" ? currentLocation.pathname : dest.from ?? lightweightResult.fullPath;
|
|
const destTo = dest.to ? `${dest.to}` : void 0;
|
|
const fromSearch = lightweightResult.search;
|
|
const fromParams = Object.assign(Object.create(null), lightweightResult.params);
|
|
const sourcePath = destTo?.charCodeAt(0) === 47 ? "/" : this.resolvePathWithBase(defaultedFromPath, ".");
|
|
const nextTo = destTo ? this.resolvePathWithBase(sourcePath, destTo) : sourcePath;
|
|
const nextParams = dest.params === false || dest.params === null ? Object.create(null) : (dest.params ?? true) === true ? fromParams : Object.assign(fromParams, functionalUpdate(dest.params, fromParams));
|
|
const destRoute = this.routesByPath[trimPathRight(nextTo)];
|
|
let destRoutes;
|
|
if (destRoute) destRoutes = this.getRouteBranch(destRoute);
|
|
else if (nextTo.includes("$")) destRoutes = [];
|
|
else {
|
|
const destMatchResult = this.getMatchedRoutes(nextTo);
|
|
destRoutes = destMatchResult.matchedRoutes;
|
|
if (this.options.notFoundRoute && (!destMatchResult.foundRoute || destMatchResult.foundRoute.path !== "/" && destMatchResult.routeParams["**"])) destRoutes = [...destRoutes, this.options.notFoundRoute];
|
|
}
|
|
if (destRoutes.length && hasKeys(nextParams)) for (const route of destRoutes) {
|
|
const fn = route.options.params?.stringify ?? route.options.stringifyParams;
|
|
if (fn) try {
|
|
Object.assign(nextParams, fn(nextParams));
|
|
} catch {}
|
|
}
|
|
const nextPathname = opts.leaveParams ? nextTo : decodePath(interpolatePath({
|
|
path: nextTo,
|
|
params: nextParams,
|
|
decoder: this.pathParamsDecoder,
|
|
server: this.isServer
|
|
}).interpolatedPath).path;
|
|
if (process.env.NODE_ENV !== "production" && destRoute && !opts.leaveParams) try {
|
|
if (this.getMatchedRoutes(nextPathname).foundRoute?.id !== destRoute.id) console.warn(`Generated path "${nextPathname}" for route "${destRoute.id}" did not match the same route after params.stringify.`);
|
|
} catch {}
|
|
let nextSearch = fromSearch;
|
|
if (opts._includeValidateSearch && this.options.search?.strict) {
|
|
const validatedSearch = {};
|
|
destRoutes.forEach((route) => {
|
|
if (route.options.validateSearch) try {
|
|
Object.assign(validatedSearch, validateSearch(route.options.validateSearch, {
|
|
...validatedSearch,
|
|
...nextSearch
|
|
}));
|
|
} catch {}
|
|
});
|
|
nextSearch = validatedSearch;
|
|
}
|
|
nextSearch = applySearchMiddleware({
|
|
search: nextSearch,
|
|
dest,
|
|
destRoutes,
|
|
_includeValidateSearch: opts._includeValidateSearch
|
|
});
|
|
nextSearch = nullReplaceEqualDeep(fromSearch, nextSearch);
|
|
const searchStr = this.options.stringifySearch(nextSearch);
|
|
const hash = dest.hash === true ? currentLocation.hash : dest.hash ? functionalUpdate(dest.hash, currentLocation.hash) : void 0;
|
|
const hashStr = hash ? `#${hash}` : "";
|
|
let nextState = dest.state === true ? currentLocation.state : dest.state ? functionalUpdate(dest.state, currentLocation.state) : {};
|
|
nextState = replaceEqualDeep(currentLocation.state, nextState);
|
|
const fullPath = `${nextPathname}${searchStr}${hashStr}`;
|
|
let href;
|
|
let publicHref;
|
|
let external = false;
|
|
if (this.rewrite) {
|
|
const url = new URL(fullPath, this.origin);
|
|
const rewrittenUrl = executeRewriteOutput(this.rewrite, url);
|
|
href = url.href.replace(url.origin, "");
|
|
if (rewrittenUrl.origin !== this.origin) {
|
|
publicHref = rewrittenUrl.href;
|
|
external = true;
|
|
} else publicHref = rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash;
|
|
} else {
|
|
href = encodePathLikeUrl(fullPath);
|
|
publicHref = href;
|
|
}
|
|
return {
|
|
publicHref,
|
|
href,
|
|
pathname: nextPathname,
|
|
search: nextSearch,
|
|
searchStr,
|
|
state: nextState,
|
|
hash: hash ?? "",
|
|
external,
|
|
unmaskOnReload: dest.unmaskOnReload
|
|
};
|
|
};
|
|
const buildWithMatches = (dest = {}, maskedDest) => {
|
|
const next = build(dest);
|
|
let maskedNext = maskedDest ? build(maskedDest) : void 0;
|
|
if (!maskedNext) {
|
|
const params = Object.create(null);
|
|
if (this.options.routeMasks) {
|
|
const match = findFlatMatch(next.pathname, this.processedTree);
|
|
if (match) {
|
|
Object.assign(params, match.rawParams);
|
|
const { from: _from, params: maskParams, ...maskProps } = match.route;
|
|
const nextParams = maskParams === false || maskParams === null ? Object.create(null) : (maskParams ?? true) === true ? params : Object.assign(params, functionalUpdate(maskParams, params));
|
|
maskedDest = {
|
|
from: opts.from,
|
|
...maskProps,
|
|
params: nextParams
|
|
};
|
|
maskedNext = build(maskedDest);
|
|
}
|
|
}
|
|
}
|
|
if (maskedNext) next.maskedLocation = maskedNext;
|
|
return next;
|
|
};
|
|
if (opts.mask) return buildWithMatches(opts, {
|
|
from: opts.from,
|
|
...opts.mask
|
|
});
|
|
return buildWithMatches(opts);
|
|
};
|
|
this.commitLocation = async ({ viewTransition, ignoreBlocker, ...next }) => {
|
|
const isSameState = () => {
|
|
const ignoredProps = [
|
|
"key",
|
|
"__TSR_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 = trimPathRight(this.latestLocation.href) === trimPathRight(next.href);
|
|
let previousCommitPromise = this.commitLocationPromise;
|
|
this.commitLocationPromise = createControlledPromise(() => {
|
|
previousCommitPromise?.resolve();
|
|
previousCommitPromise = void 0;
|
|
});
|
|
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,
|
|
__TSR_key: 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.publicHref, 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 });
|
|
const hrefUrl = new URL(parsed.pathname, this.origin);
|
|
rest.to = executeRewriteInput(this.rewrite, hrefUrl).pathname;
|
|
rest.search = this.options.parseSearch(parsed.search);
|
|
rest.hash = parsed.hash.slice(1);
|
|
}
|
|
const location = this.buildLocation({
|
|
...rest,
|
|
_includeValidateSearch: true
|
|
});
|
|
this.pendingBuiltLocation = location;
|
|
const commitPromise = this.commitLocation({
|
|
...location,
|
|
viewTransition,
|
|
replace,
|
|
resetScroll,
|
|
hashScrollIntoView,
|
|
ignoreBlocker
|
|
});
|
|
Promise.resolve().then(() => {
|
|
if (this.pendingBuiltLocation === location) this.pendingBuiltLocation = void 0;
|
|
});
|
|
return commitPromise;
|
|
};
|
|
this.navigate = async ({ to, reloadDocument, href, publicHref, ...rest }) => {
|
|
let hrefIsUrl = false;
|
|
if (href) try {
|
|
new URL(`${href}`);
|
|
hrefIsUrl = true;
|
|
} catch {}
|
|
if (hrefIsUrl && !reloadDocument) reloadDocument = true;
|
|
if (reloadDocument) {
|
|
if (to !== void 0 || !href) {
|
|
const location = this.buildLocation({
|
|
to,
|
|
...rest
|
|
});
|
|
href = href ?? location.publicHref;
|
|
publicHref = publicHref ?? location.publicHref;
|
|
}
|
|
const reloadHref = !hrefIsUrl && publicHref ? publicHref : href;
|
|
if (isDangerousProtocol(reloadHref, this.protocolAllowlist)) {
|
|
if (process.env.NODE_ENV !== "production") console.warn(`Blocked navigation to dangerous protocol: ${reloadHref}`);
|
|
return Promise.resolve();
|
|
}
|
|
if (!rest.ignoreBlocker) {
|
|
const blockers = this.history.getBlockers?.() ?? [];
|
|
for (const blocker of blockers) if (blocker?.blockerFn) {
|
|
if (await blocker.blockerFn({
|
|
currentLocation: this.latestLocation,
|
|
nextLocation: this.latestLocation,
|
|
action: "PUSH"
|
|
})) return Promise.resolve();
|
|
}
|
|
}
|
|
if (rest.replace) window.location.replace(reloadHref);
|
|
else window.location.href = reloadHref;
|
|
return Promise.resolve();
|
|
}
|
|
return this.buildAndCommitLocation({
|
|
...rest,
|
|
href,
|
|
to,
|
|
_isNavigate: true
|
|
});
|
|
};
|
|
this.beforeLoad = () => {
|
|
this.cancelMatches();
|
|
this.updateLatestLocation();
|
|
if (isServer ?? this.isServer) {
|
|
const nextLocation = this.buildLocation({
|
|
to: this.latestLocation.pathname,
|
|
search: true,
|
|
params: true,
|
|
hash: true,
|
|
state: true,
|
|
_includeValidateSearch: true
|
|
});
|
|
if (this.latestLocation.publicHref !== nextLocation.publicHref) {
|
|
const href = this.getParsedLocationHref(nextLocation);
|
|
if (nextLocation.external) throw redirect({ href });
|
|
else throw redirect({
|
|
href,
|
|
_builtLocation: nextLocation
|
|
});
|
|
}
|
|
}
|
|
const pendingMatches = this.matchRoutes(this.latestLocation);
|
|
const nextCachedMatches = this.stores.cachedMatches.get().filter((d) => !pendingMatches.some((e) => e.id === d.id));
|
|
this.batch(() => {
|
|
this.stores.status.set("pending");
|
|
this.stores.statusCode.set(200);
|
|
this.stores.isLoading.set(true);
|
|
this.stores.location.set(this.latestLocation);
|
|
this.stores.setPending(pendingMatches);
|
|
this.stores.setCached(nextCachedMatches);
|
|
});
|
|
};
|
|
this.load = async (opts) => {
|
|
let redirect;
|
|
let notFound;
|
|
let loadPromise;
|
|
const previousLocation = this.stores.resolvedLocation.get() ?? this.stores.location.get();
|
|
loadPromise = new Promise((resolve) => {
|
|
this.startTransition(async () => {
|
|
try {
|
|
this.beforeLoad();
|
|
const next = this.latestLocation;
|
|
const locationChangeInfo = getLocationChangeInfo(next, this.stores.resolvedLocation.get());
|
|
if (!this.stores.redirect.get()) this.emit({
|
|
type: "onBeforeNavigate",
|
|
...locationChangeInfo
|
|
});
|
|
this.emit({
|
|
type: "onBeforeLoad",
|
|
...locationChangeInfo
|
|
});
|
|
await loadMatches({
|
|
router: this,
|
|
sync: opts?.sync,
|
|
forceStaleReload: previousLocation.href === next.href,
|
|
matches: this.stores.pendingMatches.get(),
|
|
location: next,
|
|
updateMatch: this.updateMatch,
|
|
onReady: async () => {
|
|
this.startTransition(() => {
|
|
this.startViewTransition(async () => {
|
|
let exitingMatches = null;
|
|
let hookExitingMatches = null;
|
|
let hookEnteringMatches = null;
|
|
let hookStayingMatches = null;
|
|
this.batch(() => {
|
|
const pendingMatches = this.stores.pendingMatches.get();
|
|
const mountPending = pendingMatches.length;
|
|
const currentMatches = this.stores.matches.get();
|
|
exitingMatches = mountPending ? currentMatches.filter((match) => !this.stores.pendingMatchStores.has(match.id)) : null;
|
|
const pendingRouteIds = /* @__PURE__ */ new Set();
|
|
for (const s of this.stores.pendingMatchStores.values()) if (s.routeId) pendingRouteIds.add(s.routeId);
|
|
const activeRouteIds = /* @__PURE__ */ new Set();
|
|
for (const s of this.stores.matchStores.values()) if (s.routeId) activeRouteIds.add(s.routeId);
|
|
hookExitingMatches = mountPending ? currentMatches.filter((match) => !pendingRouteIds.has(match.routeId)) : null;
|
|
hookEnteringMatches = mountPending ? pendingMatches.filter((match) => !activeRouteIds.has(match.routeId)) : null;
|
|
hookStayingMatches = mountPending ? pendingMatches.filter((match) => activeRouteIds.has(match.routeId)) : currentMatches;
|
|
this.stores.isLoading.set(false);
|
|
this.stores.loadedAt.set(Date.now());
|
|
/**
|
|
* When committing new matches, cache any exiting matches that are still usable.
|
|
* Routes that resolved with `status: 'error'` or `status: 'notFound'` are
|
|
* deliberately excluded from `cachedMatches` so that subsequent invalidations
|
|
* or reloads re-run their loaders instead of reusing the failed/not-found data.
|
|
*/
|
|
if (mountPending) {
|
|
this.stores.setMatches(pendingMatches);
|
|
this.stores.setPending([]);
|
|
this.stores.setCached([...this.stores.cachedMatches.get(), ...exitingMatches.filter((d) => d.status !== "error" && d.status !== "notFound" && d.status !== "redirected")]);
|
|
this.clearExpiredCache();
|
|
}
|
|
});
|
|
for (const [matches, hook] of [
|
|
[hookExitingMatches, "onLeave"],
|
|
[hookEnteringMatches, "onEnter"],
|
|
[hookStayingMatches, "onStay"]
|
|
]) {
|
|
if (!matches) continue;
|
|
for (const match of matches) this.looseRoutesById[match.routeId].options[hook]?.(match);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
} catch (err) {
|
|
if (isRedirect(err)) {
|
|
redirect = err;
|
|
if (!(isServer ?? this.isServer)) this.navigate({
|
|
...redirect.options,
|
|
replace: true,
|
|
ignoreBlocker: true
|
|
});
|
|
} else if (isNotFound(err)) notFound = err;
|
|
const nextStatusCode = redirect ? redirect.status : notFound ? 404 : this.stores.matches.get().some((d) => d.status === "error") ? 500 : 200;
|
|
this.batch(() => {
|
|
this.stores.statusCode.set(nextStatusCode);
|
|
this.stores.redirect.set(redirect);
|
|
});
|
|
}
|
|
if (this.latestLoadPromise === loadPromise) {
|
|
this.commitLocationPromise?.resolve();
|
|
this.latestLoadPromise = void 0;
|
|
this.commitLocationPromise = void 0;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
this.latestLoadPromise = loadPromise;
|
|
await loadPromise;
|
|
while (this.latestLoadPromise && loadPromise !== this.latestLoadPromise) await this.latestLoadPromise;
|
|
let newStatusCode = void 0;
|
|
if (this.hasNotFoundMatch()) newStatusCode = 404;
|
|
else if (this.stores.matches.get().some((d) => d.status === "error")) newStatusCode = 500;
|
|
if (newStatusCode !== void 0) this.stores.statusCode.set(newStatusCode);
|
|
};
|
|
this.startViewTransition = (fn) => {
|
|
const shouldViewTransition = this.shouldViewTransition ?? this.options.defaultViewTransition;
|
|
this.shouldViewTransition = void 0;
|
|
if (shouldViewTransition && typeof document !== "undefined" && "startViewTransition" in document && typeof document.startViewTransition === "function") {
|
|
let startViewTransitionParams;
|
|
if (typeof shouldViewTransition === "object" && this.isViewTransitionTypesSupported) {
|
|
const next = this.latestLocation;
|
|
const prevLocation = this.stores.resolvedLocation.get();
|
|
const resolvedViewTransitionTypes = typeof shouldViewTransition.types === "function" ? shouldViewTransition.types(getLocationChangeInfo(next, prevLocation)) : shouldViewTransition.types;
|
|
if (resolvedViewTransitionTypes === false) {
|
|
fn();
|
|
return;
|
|
}
|
|
startViewTransitionParams = {
|
|
update: fn,
|
|
types: resolvedViewTransitionTypes
|
|
};
|
|
} else startViewTransitionParams = fn;
|
|
document.startViewTransition(startViewTransitionParams);
|
|
} else fn();
|
|
};
|
|
this.updateMatch = (id, updater) => {
|
|
this.startTransition(() => {
|
|
const pendingMatch = this.stores.pendingMatchStores.get(id);
|
|
if (pendingMatch) {
|
|
pendingMatch.set(updater);
|
|
return;
|
|
}
|
|
const activeMatch = this.stores.matchStores.get(id);
|
|
if (activeMatch) {
|
|
activeMatch.set(updater);
|
|
return;
|
|
}
|
|
const cachedMatch = this.stores.cachedMatchStores.get(id);
|
|
if (cachedMatch) {
|
|
const next = updater(cachedMatch.get());
|
|
if (next.status === "redirected") {
|
|
if (this.stores.cachedMatchStores.delete(id)) this.stores.cachedIds.set((prev) => prev.filter((matchId) => matchId !== id));
|
|
} else cachedMatch.set(next);
|
|
}
|
|
});
|
|
};
|
|
this.getMatch = (matchId) => {
|
|
return this.stores.cachedMatchStores.get(matchId)?.get() ?? this.stores.pendingMatchStores.get(matchId)?.get() ?? this.stores.matchStores.get(matchId)?.get();
|
|
};
|
|
this.invalidate = (opts) => {
|
|
const invalidate = (d) => {
|
|
if (opts?.filter?.(d) ?? true) return {
|
|
...d,
|
|
invalid: true,
|
|
...opts?.forcePending || d.status === "error" || d.status === "notFound" ? {
|
|
status: "pending",
|
|
error: void 0
|
|
} : void 0
|
|
};
|
|
return d;
|
|
};
|
|
this.batch(() => {
|
|
this.stores.setMatches(this.stores.matches.get().map(invalidate));
|
|
this.stores.setCached(this.stores.cachedMatches.get().map(invalidate));
|
|
this.stores.setPending(this.stores.pendingMatches.get().map(invalidate));
|
|
});
|
|
this.shouldViewTransition = false;
|
|
return this.load({ sync: opts?.sync });
|
|
};
|
|
this.getParsedLocationHref = (location) => {
|
|
return location.publicHref || "/";
|
|
};
|
|
this.resolveRedirect = (redirect) => {
|
|
const locationHeader = redirect.headers.get("Location");
|
|
if (!redirect.options.href || redirect.options._builtLocation) {
|
|
const location = redirect.options._builtLocation ?? this.buildLocation(redirect.options);
|
|
const href = this.getParsedLocationHref(location);
|
|
redirect.options.href = href;
|
|
redirect.headers.set("Location", href);
|
|
} else if (locationHeader) try {
|
|
const url = new URL(locationHeader);
|
|
if (this.origin && url.origin === this.origin) {
|
|
const href = url.pathname + url.search + url.hash;
|
|
redirect.options.href = href;
|
|
redirect.headers.set("Location", href);
|
|
}
|
|
} catch {}
|
|
if (redirect.options.href && !redirect.options._builtLocation && isDangerousProtocol(redirect.options.href, this.protocolAllowlist)) throw new Error(process.env.NODE_ENV !== "production" ? `Redirect blocked: unsafe protocol in href "${redirect.options.href}". Allowed protocols: ${Array.from(this.protocolAllowlist).join(", ")}.` : "Redirect blocked: unsafe protocol");
|
|
if (!redirect.headers.get("Location")) redirect.headers.set("Location", redirect.options.href);
|
|
return redirect;
|
|
};
|
|
this.clearCache = (opts) => {
|
|
const filter = opts?.filter;
|
|
if (filter !== void 0) this.stores.setCached(this.stores.cachedMatches.get().filter((m) => !filter(m)));
|
|
else this.stores.setCached([]);
|
|
};
|
|
this.clearExpiredCache = () => {
|
|
const now = Date.now();
|
|
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) ?? 300 * 1e3;
|
|
if (d.status === "error") return true;
|
|
return now - d.updatedAt >= gcTime;
|
|
};
|
|
this.clearCache({ filter });
|
|
};
|
|
this.loadRouteChunk = loadRouteChunk;
|
|
this.preloadRoute = async (opts) => {
|
|
const next = opts._builtLocation ?? this.buildLocation(opts);
|
|
let matches = this.matchRoutes(next, {
|
|
throwOnError: true,
|
|
preload: true,
|
|
dest: opts
|
|
});
|
|
const activeMatchIds = new Set([...this.stores.matchesId.get(), ...this.stores.pendingIds.get()]);
|
|
const loadedMatchIds = new Set([...activeMatchIds, ...this.stores.cachedIds.get()]);
|
|
const matchesToCache = matches.filter((match) => !loadedMatchIds.has(match.id));
|
|
if (matchesToCache.length) {
|
|
const cachedMatches = this.stores.cachedMatches.get();
|
|
this.stores.setCached([...cachedMatches, ...matchesToCache]);
|
|
}
|
|
try {
|
|
matches = await loadMatches({
|
|
router: this,
|
|
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.options.reloadDocument) return;
|
|
return await this.preloadRoute({
|
|
...err.options,
|
|
_fromLocation: next
|
|
});
|
|
}
|
|
if (!isNotFound(err)) console.error(err);
|
|
return;
|
|
}
|
|
};
|
|
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?.pending && this.stores.status.get() !== "pending") return false;
|
|
const baseLocation = (opts?.pending === void 0 ? !this.stores.isLoading.get() : opts.pending) ? this.latestLocation : this.stores.resolvedLocation.get() || this.stores.location.get();
|
|
const match = findSingleMatch(next.pathname, opts?.caseSensitive ?? false, opts?.fuzzy ?? false, baseLocation.pathname, this.processedTree);
|
|
if (!match) return false;
|
|
if (location.params) {
|
|
if (!deepEqual(match.rawParams, location.params, { partial: true })) return false;
|
|
}
|
|
if (opts?.includeSearch ?? true) return deepEqual(baseLocation.search, next.search, { partial: true }) ? match.rawParams : false;
|
|
return match.rawParams;
|
|
};
|
|
this.hasNotFoundMatch = () => {
|
|
return this.stores.matches.get().some((d) => d.status === "notFound" || d.globalNotFound);
|
|
};
|
|
this.getStoreConfig = getStoreConfig;
|
|
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,
|
|
protocolAllowlist: options.protocolAllowlist ?? DEFAULT_PROTOCOL_ALLOWLIST
|
|
});
|
|
if (typeof document !== "undefined") self.__TSR_ROUTER__ = this;
|
|
}
|
|
isShell() {
|
|
return !!this.options.isShell;
|
|
}
|
|
isPrerendering() {
|
|
return !!this.options.isPrerendering;
|
|
}
|
|
get state() {
|
|
return this.stores.__store.get();
|
|
}
|
|
setRoutes({ routesById, routesByPath, processedTree }) {
|
|
this.routesById = routesById;
|
|
this.routesByPath = routesByPath;
|
|
this.processedTree = processedTree;
|
|
const notFoundRoute = this.options.notFoundRoute;
|
|
if (notFoundRoute) {
|
|
notFoundRoute.init({ originalIndex: 99999999999 });
|
|
this.routesById[notFoundRoute.id] = notFoundRoute;
|
|
}
|
|
}
|
|
getRouteBranch(route) {
|
|
let branch = this.routeBranchCache.get(route);
|
|
if (!branch) {
|
|
branch = buildRouteBranch(route);
|
|
this.routeBranchCache.set(route, branch);
|
|
}
|
|
return branch;
|
|
}
|
|
get looseRoutesById() {
|
|
return this.routesById;
|
|
}
|
|
getParentContext(parentMatch) {
|
|
return !parentMatch?.id ? this.options.context ?? void 0 : parentMatch.context ?? this.options.context ?? void 0;
|
|
}
|
|
matchRoutesInternal(next, opts) {
|
|
const matchedRoutesResult = this.getMatchedRoutes(next.pathname);
|
|
const { foundRoute, routeParams } = matchedRoutesResult;
|
|
let { matchedRoutes } = matchedRoutesResult;
|
|
let isGlobalNotFound = false;
|
|
if (foundRoute ? foundRoute.path !== "/" && routeParams["**"] : trimPathRight(next.pathname)) if (this.options.notFoundRoute) matchedRoutes = [...matchedRoutes, this.options.notFoundRoute];
|
|
else isGlobalNotFound = true;
|
|
const globalNotFoundRouteId = isGlobalNotFound ? findGlobalNotFoundRouteId(this.options.notFoundMode, matchedRoutes) : void 0;
|
|
const matches = new Array(matchedRoutes.length);
|
|
const previousActiveMatchesByRouteId = /* @__PURE__ */ new Map();
|
|
for (const store of this.stores.matchStores.values()) if (store.routeId) previousActiveMatchesByRouteId.set(store.routeId, store.get());
|
|
for (let index = 0; index < matchedRoutes.length; index++) {
|
|
const route = matchedRoutes[index];
|
|
const parentMatch = matches[index - 1];
|
|
let preMatchSearch;
|
|
let strictMatchSearch;
|
|
let searchError;
|
|
{
|
|
const parentSearch = parentMatch?.search ?? next.search;
|
|
const parentStrictSearch = parentMatch?._strictSearch ?? void 0;
|
|
try {
|
|
const strictSearch = validateSearch(route.options.validateSearch, { ...parentSearch }) ?? void 0;
|
|
preMatchSearch = {
|
|
...parentSearch,
|
|
...strictSearch
|
|
};
|
|
strictMatchSearch = {
|
|
...parentStrictSearch,
|
|
...strictSearch
|
|
};
|
|
searchError = void 0;
|
|
} catch (err) {
|
|
let searchParamError = err;
|
|
if (!(err instanceof SearchParamError)) searchParamError = new SearchParamError(err.message, { cause: err });
|
|
if (opts?.throwOnError) throw searchParamError;
|
|
preMatchSearch = parentSearch;
|
|
strictMatchSearch = {};
|
|
searchError = searchParamError;
|
|
}
|
|
}
|
|
const loaderDeps = route.options.loaderDeps?.({ search: preMatchSearch }) ?? "";
|
|
const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : "";
|
|
const { interpolatedPath, usedParams } = interpolatePath({
|
|
path: route.fullPath,
|
|
params: routeParams,
|
|
decoder: this.pathParamsDecoder,
|
|
server: this.isServer
|
|
});
|
|
const matchId = route.id + interpolatedPath + loaderDepsHash;
|
|
const existingMatch = this.getMatch(matchId);
|
|
const previousMatch = previousActiveMatchesByRouteId.get(route.id);
|
|
const strictParams = existingMatch?._strictParams ?? usedParams;
|
|
let paramsError = void 0;
|
|
if (!existingMatch) try {
|
|
extractStrictParams(route, strictParams);
|
|
} catch (err) {
|
|
if (isNotFound(err) || isRedirect(err)) paramsError = err;
|
|
else paramsError = new PathParamError(err.message, { cause: err });
|
|
if (opts?.throwOnError) throw paramsError;
|
|
}
|
|
Object.assign(routeParams, strictParams);
|
|
const cause = previousMatch ? "stay" : "enter";
|
|
let match;
|
|
if (existingMatch) match = {
|
|
...existingMatch,
|
|
cause,
|
|
params: previousMatch?.params ?? routeParams,
|
|
_strictParams: strictParams,
|
|
search: previousMatch ? nullReplaceEqualDeep(previousMatch.search, preMatchSearch) : nullReplaceEqualDeep(existingMatch.search, preMatchSearch),
|
|
_strictSearch: strictMatchSearch
|
|
};
|
|
else {
|
|
const status = route.options.loader || route.options.beforeLoad || route.lazyFn || routeNeedsPreload(route) ? "pending" : "success";
|
|
match = {
|
|
id: matchId,
|
|
ssr: isServer ?? this.isServer ? void 0 : route.options.ssr,
|
|
index,
|
|
routeId: route.id,
|
|
params: previousMatch?.params ?? routeParams,
|
|
_strictParams: strictParams,
|
|
pathname: interpolatedPath,
|
|
updatedAt: Date.now(),
|
|
search: previousMatch ? nullReplaceEqualDeep(previousMatch.search, preMatchSearch) : preMatchSearch,
|
|
_strictSearch: strictMatchSearch,
|
|
searchError: void 0,
|
|
status,
|
|
isFetching: false,
|
|
error: void 0,
|
|
paramsError,
|
|
__routeContext: void 0,
|
|
_nonReactive: { loadPromise: createControlledPromise() },
|
|
__beforeLoadContext: void 0,
|
|
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 || {},
|
|
fullPath: route.fullPath
|
|
};
|
|
}
|
|
if (!opts?.preload) match.globalNotFound = globalNotFoundRouteId === route.id;
|
|
match.searchError = searchError;
|
|
const parentContext = this.getParentContext(parentMatch);
|
|
match.context = {
|
|
...parentContext,
|
|
...match.__routeContext,
|
|
...match.__beforeLoadContext
|
|
};
|
|
matches[index] = match;
|
|
}
|
|
for (let index = 0; index < matches.length; index++) {
|
|
const match = matches[index];
|
|
const route = this.looseRoutesById[match.routeId];
|
|
const existingMatch = this.getMatch(match.id);
|
|
const previousMatch = previousActiveMatchesByRouteId.get(match.routeId);
|
|
match.params = previousMatch ? nullReplaceEqualDeep(previousMatch.params, routeParams) : routeParams;
|
|
if (!existingMatch) {
|
|
const parentMatch = matches[index - 1];
|
|
const parentContext = this.getParentContext(parentMatch);
|
|
if (route.options.context) {
|
|
const contextFnContext = {
|
|
deps: match.loaderDeps,
|
|
params: match.params,
|
|
context: parentContext ?? {},
|
|
location: next,
|
|
navigate: (opts) => this.navigate({
|
|
...opts,
|
|
_fromLocation: next
|
|
}),
|
|
buildLocation: this.buildLocation,
|
|
cause: match.cause,
|
|
abortController: match.abortController,
|
|
preload: !!match.preload,
|
|
matches,
|
|
routeId: route.id
|
|
};
|
|
match.__routeContext = route.options.context(contextFnContext) ?? void 0;
|
|
}
|
|
match.context = {
|
|
...parentContext,
|
|
...match.__routeContext,
|
|
...match.__beforeLoadContext
|
|
};
|
|
}
|
|
}
|
|
return matches;
|
|
}
|
|
/**
|
|
* Lightweight route matching for buildLocation.
|
|
* Only computes fullPath, accumulated search, and params - skipping expensive
|
|
* operations like AbortController, ControlledPromise, loaderDeps, and full match objects.
|
|
*/
|
|
matchRoutesLightweight(location) {
|
|
const { matchedRoutes, routeParams } = this.getMatchedRoutes(location.pathname);
|
|
const lastRoute = last(matchedRoutes);
|
|
const accumulatedSearch = { ...location.search };
|
|
for (const route of matchedRoutes) try {
|
|
Object.assign(accumulatedSearch, validateSearch(route.options.validateSearch, accumulatedSearch));
|
|
} catch {}
|
|
const lastStateMatchId = last(this.stores.matchesId.get());
|
|
const lastStateMatch = lastStateMatchId && this.stores.matchStores.get(lastStateMatchId)?.get();
|
|
const canReuseParams = lastStateMatch && lastStateMatch.routeId === lastRoute.id && lastStateMatch.pathname === location.pathname;
|
|
let params;
|
|
if (canReuseParams) params = lastStateMatch.params;
|
|
else {
|
|
const strictParams = Object.assign(Object.create(null), routeParams);
|
|
for (const route of matchedRoutes) try {
|
|
extractStrictParams(route, strictParams);
|
|
} catch {}
|
|
params = strictParams;
|
|
}
|
|
return {
|
|
matchedRoutes,
|
|
fullPath: lastRoute.fullPath,
|
|
search: accumulatedSearch,
|
|
params
|
|
};
|
|
}
|
|
};
|
|
/** Error thrown when search parameter validation fails. */
|
|
var SearchParamError = class extends Error {};
|
|
/** Error thrown when path parameter parsing/validation fails. */
|
|
var PathParamError = class extends Error {};
|
|
const normalize = (str) => str.endsWith("/") && str.length > 1 ? str.slice(0, -1) : str;
|
|
function comparePaths(a, b) {
|
|
return normalize(a) === normalize(b);
|
|
}
|
|
/**
|
|
* Lazily import a module function and forward arguments to it, retaining
|
|
* parameter and return types for the selected export key.
|
|
*/
|
|
function lazyFn(fn, key) {
|
|
return async (...args) => {
|
|
return (await fn())[key || "default"](...args);
|
|
};
|
|
}
|
|
/** Create an initial RouterState from a parsed location. */
|
|
function getInitialRouterState(location) {
|
|
return {
|
|
loadedAt: 0,
|
|
isLoading: false,
|
|
isTransitioning: false,
|
|
status: "idle",
|
|
resolvedLocation: void 0,
|
|
location,
|
|
matches: [],
|
|
statusCode: 200
|
|
};
|
|
}
|
|
function validateSearch(validateSearch, input) {
|
|
if (validateSearch == null) return {};
|
|
if ("~standard" in validateSearch) {
|
|
const result = validateSearch["~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 validateSearch) return validateSearch.parse(input);
|
|
if (typeof validateSearch === "function") return validateSearch(input);
|
|
return {};
|
|
}
|
|
/**
|
|
* Build the matched route chain and extract params for a pathname.
|
|
* Falls back to the root route if no specific route is found.
|
|
*/
|
|
function getMatchedRoutes({ pathname, routesById, processedTree }) {
|
|
const routeParams = Object.create(null);
|
|
const trimmedPath = trimPathRight(pathname);
|
|
let foundRoute = void 0;
|
|
const match = findRouteMatch(trimmedPath, processedTree, true);
|
|
if (match) {
|
|
foundRoute = match.route;
|
|
Object.assign(routeParams, match.rawParams);
|
|
}
|
|
return {
|
|
matchedRoutes: match?.branch || [routesById["__root__"]],
|
|
routeParams,
|
|
foundRoute
|
|
};
|
|
}
|
|
/**
|
|
* TODO: once caches are persisted across requests on the server,
|
|
* we can cache the built middleware chain using `last(destRoutes)` as the key
|
|
*/
|
|
function applySearchMiddleware({ search, dest, destRoutes, _includeValidateSearch }) {
|
|
return buildMiddlewareChain(destRoutes)(search, dest, _includeValidateSearch ?? false);
|
|
}
|
|
function buildMiddlewareChain(destRoutes) {
|
|
const context = {
|
|
dest: null,
|
|
_includeValidateSearch: false,
|
|
middlewares: []
|
|
};
|
|
for (const route of destRoutes) {
|
|
if ("search" in route.options) {
|
|
if (route.options.search?.middlewares) context.middlewares.push(...route.options.search.middlewares);
|
|
} else if (route.options.preSearchFilters || route.options.postSearchFilters) {
|
|
const legacyMiddleware = ({ search, next }) => {
|
|
let nextSearch = search;
|
|
if ("preSearchFilters" in route.options && route.options.preSearchFilters) nextSearch = route.options.preSearchFilters.reduce((prev, next) => next(prev), search);
|
|
const result = next(nextSearch);
|
|
if ("postSearchFilters" in route.options && route.options.postSearchFilters) return route.options.postSearchFilters.reduce((prev, next) => next(prev), result);
|
|
return result;
|
|
};
|
|
context.middlewares.push(legacyMiddleware);
|
|
}
|
|
if (route.options.validateSearch) {
|
|
const validate = ({ search, next }) => {
|
|
const result = next(search);
|
|
if (!context._includeValidateSearch) return result;
|
|
try {
|
|
return {
|
|
...result,
|
|
...validateSearch(route.options.validateSearch, result) ?? void 0
|
|
};
|
|
} catch {
|
|
return result;
|
|
}
|
|
};
|
|
context.middlewares.push(validate);
|
|
}
|
|
}
|
|
const final = ({ search }) => {
|
|
const dest = context.dest;
|
|
if (!dest.search) return {};
|
|
if (dest.search === true) return search;
|
|
return functionalUpdate(dest.search, search);
|
|
};
|
|
context.middlewares.push(final);
|
|
const applyNext = (index, currentSearch, middlewares) => {
|
|
if (index >= middlewares.length) return currentSearch;
|
|
const middleware = middlewares[index];
|
|
const next = (newSearch) => {
|
|
return applyNext(index + 1, newSearch, middlewares);
|
|
};
|
|
return middleware({
|
|
search: currentSearch,
|
|
next
|
|
});
|
|
};
|
|
return function middleware(search, dest, _includeValidateSearch) {
|
|
context.dest = dest;
|
|
context._includeValidateSearch = _includeValidateSearch;
|
|
return applyNext(0, search, context.middlewares);
|
|
};
|
|
}
|
|
function findGlobalNotFoundRouteId(notFoundMode, routes) {
|
|
if (notFoundMode !== "root") for (let i = routes.length - 1; i >= 0; i--) {
|
|
const route = routes[i];
|
|
if (route.children) return route.id;
|
|
}
|
|
return rootRouteId;
|
|
}
|
|
function extractStrictParams(route, accumulatedParams) {
|
|
const parseParams = route.options.params?.parse ?? route.options.parseParams;
|
|
if (parseParams) {
|
|
const result = parseParams(accumulatedParams);
|
|
if (result === false) throw new Error("Route params.parse returned false for a matched route");
|
|
Object.assign(accumulatedParams, result);
|
|
}
|
|
}
|
|
//#endregion
|
|
export { PathParamError, RouterCore, SearchParamError, defaultSerializeError, getInitialRouterState, getLocationChangeInfo, getMatchedRoutes, lazyFn, trailingSlashOptions };
|
|
|
|
//# sourceMappingURL=router.js.map |