UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

1,533 lines (1,381 loc) 102 kB
import { createStore } from '@tanstack/store' import { createBrowserHistory, parseHref } from '@tanstack/history' import { isServer } from '@tanstack/router-core/isServer' import { batch } from './utils/batch' import { DEFAULT_PROTOCOL_ALLOWLIST, createControlledPromise, decodePath, deepEqual, encodePathLikeUrl, findLast, functionalUpdate, isDangerousProtocol, last, nullReplaceEqualDeep, replaceEqualDeep, } from './utils' import { findFlatMatch, findRouteMatch, findSingleMatch, processRouteMasks, processRouteTree, } from './new-process-route-tree' import { cleanPath, compileDecodeCharMap, interpolatePath, resolvePath, trimPath, trimPathRight, } from './path' import { createLRUCache } from './lru-cache' import { isNotFound } from './not-found' import { setupScrollRestoration } from './scroll-restoration' import { defaultParseSearch, defaultStringifySearch } from './searchParams' import { rootRouteId } from './root' import { isRedirect, redirect } from './redirect' import { loadMatches, loadRouteChunk, routeNeedsPreload } from './load-matches' import { composeRewrites, executeRewriteInput, executeRewriteOutput, rewriteBasepath, } from './rewrite' import type { Store } from '@tanstack/store' import type { LRUCache } from './lru-cache' import type { ProcessRouteTreeResult, ProcessedTree, } from './new-process-route-tree' import type { SearchParser, SearchSerializer } from './searchParams' import type { AnyRedirect, ResolvedRedirect } from './redirect' import type { HistoryLocation, HistoryState, ParsedHistoryState, RouterHistory, } from '@tanstack/history' import type { Awaitable, Constrain, ControlledPromise, NoInfer, NonNullableUpdater, PickAsRequired, Updater, } from './utils' import type { ParsedLocation } from './location' import type { AnyContext, AnyRoute, AnyRouteWithContext, LoaderStaleReloadMode, MakeRemountDepsOptionsUnion, RouteContextOptions, RouteLike, RouteMask, SearchMiddleware, } from './route' import type { FullSearchSchema, RouteById, RoutePaths, RoutesById, RoutesByPath, } from './routeInfo' import type { AnyRouteMatch, MakeRouteMatch, MakeRouteMatchUnion, MatchRouteOptions, } from './Matches' import type { BuildLocationFn, CommitLocationOptions, NavigateFn, } from './RouterProvider' import type { Manifest, RouterManagedTag } from './manifest' import type { AnySchema, AnyValidator } from './validators' import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link' import type { NotFoundError } from './not-found' import type { AnySerializationAdapter, ValidateSerializableInput, } from './ssr/serializer/transformer' // import type { AnyRouterConfig } from './config' export type ControllablePromise<T = any> = Promise<T> & { resolve: (value: T) => void reject: (value?: any) => void } export type InjectedHtmlEntry = Promise<string> export interface Register { // Lots of things on here like... // router // config // ssr } export type RegisteredSsr<TRegister = Register> = TRegister extends { ssr: infer TSSR } ? TSSR : false export type RegisteredRouter<TRegister = Register> = TRegister extends { router: infer TRouter } ? TRouter : AnyRouter export type RegisteredConfigType<TRegister, TKey> = TRegister extends { config: infer TConfig } ? TConfig extends { '~types': infer TTypes } ? TKey extends keyof TTypes ? TTypes[TKey] : unknown : unknown : unknown export type DefaultRemountDepsFn<TRouteTree extends AnyRoute> = ( opts: MakeRemountDepsOptionsUnion<TRouteTree>, ) => any export interface DefaultRouterOptionsExtensions {} export interface RouterOptionsExtensions extends DefaultRouterOptionsExtensions {} export type SSROption = boolean | 'data-only' export interface RouterOptions< TRouteTree extends AnyRoute, TTrailingSlashOption extends TrailingSlashOption, TDefaultStructuralSharingOption extends boolean = false, TRouterHistory extends RouterHistory = RouterHistory, TDehydrated = undefined, > extends RouterOptionsExtensions { /** * The history object that will be used to manage the browser history. * * If not provided, a new createBrowserHistory instance will be created and used. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#history-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/history-types) */ history?: TRouterHistory /** * A function that will be used to stringify search params when generating links. * * @default defaultStringifySearch * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#stringifysearch-method) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization) */ stringifySearch?: SearchSerializer /** * A function that will be used to parse search params when parsing the current location. * * @default defaultParseSearch * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#parsesearch-method) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization) */ parseSearch?: SearchParser /** * If `false`, routes will not be preloaded by default in any way. * * If `'intent'`, routes will be preloaded by default when the user hovers over a link or a `touchstart` event is detected on a `<Link>`. * * If `'viewport'`, routes will be preloaded by default when they are within the viewport. * * @default false * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreload-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading) */ defaultPreload?: false | 'intent' | 'viewport' | 'render' /** * The delay in milliseconds that a route must be hovered over or touched before it is preloaded. * * @default 50 * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloaddelay-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-delay) */ defaultPreloadDelay?: number /** * The default `preloadIntentProximity` a route should use if no preloadIntentProximity is provided. * * @default 0 * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadintentproximity-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-intent-proximity) */ defaultPreloadIntentProximity?: number /** * The default `pendingMs` a route should use if no pendingMs is provided. * * @default 1000 * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpendingms-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#avoiding-pending-component-flash) */ defaultPendingMs?: number /** * The default `pendingMinMs` a route should use if no pendingMinMs is provided. * * @default 500 * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpendingminms-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#avoiding-pending-component-flash) */ defaultPendingMinMs?: number /** * The default `staleTime` a route should use if no staleTime is provided. This is the time in milliseconds that a route will be considered fresh. * * @default 0 * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultstaletime-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#key-options) */ defaultStaleTime?: number /** * The default stale reload mode a route loader should use if no `loader.staleReloadMode` is provided. * * `'background'` preserves the current stale-while-revalidate behavior. * `'blocking'` waits for stale loader reloads to complete before resolving navigation. * * @default 'background' */ defaultStaleReloadMode?: LoaderStaleReloadMode /** * The default `preloadStaleTime` a route should use if no preloadStaleTime is provided. * * @default 30_000 `(30 seconds)` * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadstaletime-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading) */ defaultPreloadStaleTime?: number /** * The default `defaultPreloadGcTime` a route should use if no preloadGcTime is provided. * * @default 1_800_000 `(30 minutes)` * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadgctime-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading) */ defaultPreloadGcTime?: number /** * If `true`, route navigations will called using `document.startViewTransition()`. * * If the browser does not support this api, this option will be ignored. * * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for more information on how this function works. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultviewtransition-property) */ defaultViewTransition?: boolean | ViewTransitionOptions /** * The default `hashScrollIntoView` a route should use if no hashScrollIntoView is provided while navigating * * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) for more information on `ScrollIntoViewOptions`. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaulthashscrollintoview-property) */ defaultHashScrollIntoView?: boolean | ScrollIntoViewOptions /** * @default 'fuzzy' * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#notfoundmode-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/not-found-errors#the-notfoundmode-option) */ notFoundMode?: 'root' | 'fuzzy' /** * The default `gcTime` a route should use if no gcTime is provided. * * @default 1_800_000 `(30 minutes)` * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultgctime-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#key-options) */ defaultGcTime?: number /** * If `true`, all routes will be matched as case-sensitive. * * @default false * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#casesensitive-property) */ caseSensitive?: boolean /** * * The route tree that will be used to configure the router instance. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#routetree-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/routing/route-trees) */ routeTree?: TRouteTree /** * The basepath for then entire router. This is useful for mounting a router instance at a subpath. * ``` * @default '/' * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#basepath-property) */ basepath?: string /** * The root context that will be provided to all routes in the route tree. * * This can be used to provide a context to all routes in the tree without having to provide it to each route individually. * * Optional or required if the root route was created with [`createRootRouteWithContext()`](https://tanstack.com/router/latest/docs/framework/react/api/router/createRootRouteWithContextFunction). * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#context-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/router-context) */ context?: InferRouterContext<TRouteTree> additionalContext?: any /** * A function that will be called when the router is dehydrated. * * The return value of this function will be serialized and stored in the router's dehydrated state. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#dehydrate-method) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration) */ dehydrate?: () => Constrain< TDehydrated, ValidateSerializableInput<Register, TDehydrated> > /** * A function that will be called when the router is hydrated. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#hydrate-method) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration) */ hydrate?: (dehydrated: TDehydrated) => Awaitable<void> /** * An array of route masks that will be used to mask routes in the route tree. * * Route masking is when you display a route at a different path than the one it is configured to match, like a modal popup that when shared will unmask to the modal's content instead of the modal's context. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#routemasks-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/route-masking) */ routeMasks?: Array<RouteMask<TRouteTree>> /** * If `true`, route masks will, by default, be removed when the page is reloaded. * * This can be overridden on a per-mask basis by setting the `unmaskOnReload` option on the mask, or on a per-navigation basis by setting the `unmaskOnReload` option in the `Navigate` options. * * @default false * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#unmaskonreload-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/route-masking#unmasking-on-page-reload) */ unmaskOnReload?: boolean /** * Use `notFoundComponent` instead. * * @deprecated * See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info. * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#notfoundroute-property) */ notFoundRoute?: AnyRoute /** * Configures how trailing slashes are treated. * * - `'always'` will add a trailing slash if not present * - `'never'` will remove the trailing slash if present * - `'preserve'` will not modify the trailing slash. * * @default 'never' * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#trailingslash-property) */ trailingSlash?: TTrailingSlashOption /** * While usually automatic, sometimes it can be useful to force the router into a server-side state, e.g. when using the router in a non-browser environment that has access to a global.document object. * * @default typeof document !== 'undefined' * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#isserver-property) */ isServer?: boolean /** * @default false */ isShell?: boolean /** * @default false */ isPrerendering?: boolean /** * The default `ssr` a route should use if no `ssr` is provided. * * @default true */ defaultSsr?: SSROption search?: { /** * Configures how unknown search params (= not returned by any `validateSearch`) are treated. * * @default false * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#search.strict-property) */ strict?: boolean } /** * Configures whether structural sharing is enabled by default for fine-grained selectors. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultstructuralsharing-property) */ defaultStructuralSharing?: TDefaultStructuralSharingOption /** * Configures which URI characters are allowed in path params that would ordinarily be escaped by encodeURIComponent. * * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#pathparamsallowedcharacters-property) * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/path-params#allowed-characters) */ pathParamsAllowedCharacters?: Array< ';' | ':' | '@' | '&' | '=' | '+' | '$' | ',' > defaultRemountDeps?: DefaultRemountDepsFn<TRouteTree> /** * If `true`, scroll restoration will be enabled * * @default false */ scrollRestoration?: | boolean | ((opts: { location: ParsedLocation }) => boolean) /** * A function that will be called to get the key for the scroll restoration cache. * * @default (location) => location.href */ getScrollRestorationKey?: (location: ParsedLocation) => string /** * The default behavior for scroll restoration. * * @default 'auto' */ scrollRestorationBehavior?: ScrollBehavior /** * An array of selectors that will be used to scroll to the top of the page in addition to `window` * * @default ['window'] */ scrollToTopSelectors?: Array<string | (() => Element | null | undefined)> /** * When `true`, disables the global catch boundary that normally wraps all route matches. * This allows unhandled errors to bubble up to top-level error handlers in the browser. * * Useful for testing tools (like Storybook Test Runner), error reporting services, * and debugging scenarios where you want errors to reach the browser's global error handlers. * * @default false */ disableGlobalCatchBoundary?: boolean /** * An array of URL protocols to allow in links, redirects, and navigation. * Absolute URLs with protocols not in this list will be rejected. * * @default DEFAULT_PROTOCOL_ALLOWLIST (http:, https:, mailto:, tel:) * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#protocolallowlist-property) */ protocolAllowlist?: Array<string> serializationAdapters?: ReadonlyArray<AnySerializationAdapter> /** * Configures how the router will rewrite the location between the actual href and the internal href of the router. * * @default undefined * @description You can provide a custom rewrite pair (in/out). * This is useful for shifting data from the origin to the path (for things like subdomain routing), or other advanced use cases. */ rewrite?: LocationRewrite origin?: string ssr?: { nonce?: string } } export type LocationRewrite = { /** * A function that will be called to rewrite the URL before it is interpreted by the router from the history instance. * * @default undefined */ input?: LocationRewriteFunction /** * A function that will be called to rewrite the URL before it is committed to the actual history instance from the router. * * @default undefined */ output?: LocationRewriteFunction } /** * A function that will be called to rewrite the URL. * * @param url The URL to rewrite. * @returns The rewritten URL (as a URL instance or full href string) or undefined if no rewrite is needed. */ export type LocationRewriteFunction = ({ url, }: { url: URL }) => undefined | string | URL export interface RouterState< in out TRouteTree extends AnyRoute = AnyRoute, in out TRouteMatch = MakeRouteMatchUnion, > { status: 'pending' | 'idle' loadedAt: number isLoading: boolean isTransitioning: boolean matches: Array<TRouteMatch> pendingMatches?: Array<TRouteMatch> cachedMatches: Array<TRouteMatch> location: ParsedLocation<FullSearchSchema<TRouteTree>> resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>> statusCode: number redirect?: AnyRedirect } export interface BuildNextOptions { to?: string | number | null params?: true | Updater<unknown> search?: true | Updater<unknown> hash?: true | Updater<string> state?: true | NonNullableUpdater<ParsedHistoryState, HistoryState> mask?: { to?: string | number | null params?: true | Updater<unknown> search?: true | Updater<unknown> hash?: true | Updater<string> state?: true | NonNullableUpdater<ParsedHistoryState, HistoryState> unmaskOnReload?: boolean } from?: string href?: string _fromLocation?: ParsedLocation unsafeRelative?: 'path' _isNavigate?: boolean } type NavigationEventInfo = { fromLocation?: ParsedLocation toLocation: ParsedLocation pathChanged: boolean hrefChanged: boolean hashChanged: boolean } export interface RouterEvents { onBeforeNavigate: { type: 'onBeforeNavigate' } & NavigationEventInfo onBeforeLoad: { type: 'onBeforeLoad' } & NavigationEventInfo onLoad: { type: 'onLoad' } & NavigationEventInfo onResolved: { type: 'onResolved' } & NavigationEventInfo onBeforeRouteMount: { type: 'onBeforeRouteMount' } & NavigationEventInfo onRendered: { type: 'onRendered' } & NavigationEventInfo } export type RouterEvent = RouterEvents[keyof RouterEvents] export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void export type RouterListener<TRouterEvent extends RouterEvent> = { eventType: TRouterEvent['type'] fn: ListenerFn<TRouterEvent> } export type SubscribeFn = <TType extends keyof RouterEvents>( eventType: TType, fn: ListenerFn<RouterEvents[TType]>, ) => () => void export interface MatchRoutesOpts { preload?: boolean throwOnError?: boolean dest?: BuildNextOptions } export type InferRouterContext<TRouteTree extends AnyRoute> = TRouteTree['types']['routerContext'] export type RouterContextOptions<TRouteTree extends AnyRoute> = AnyContext extends InferRouterContext<TRouteTree> ? { context?: InferRouterContext<TRouteTree> } : { context: InferRouterContext<TRouteTree> } export type RouterConstructorOptions< TRouteTree extends AnyRoute, TTrailingSlashOption extends TrailingSlashOption, TDefaultStructuralSharingOption extends boolean, TRouterHistory extends RouterHistory, TDehydrated extends Record<string, any>, > = Omit< RouterOptions< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated >, 'context' | 'serializationAdapters' | 'defaultSsr' > & RouterContextOptions<TRouteTree> export type PreloadRouteFn< TRouteTree extends AnyRoute, TTrailingSlashOption extends TrailingSlashOption, TDefaultStructuralSharingOption extends boolean, TRouterHistory extends RouterHistory, > = < TFrom extends RoutePaths<TRouteTree> | string = string, TTo extends string | undefined = undefined, TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom, TMaskTo extends string = '', >( opts: NavigateOptions< RouterCore< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory >, TFrom, TTo, TMaskFrom, TMaskTo > & { /** * @internal * A **trusted** built location that can be used to redirect to. */ _builtLocation?: ParsedLocation }, ) => Promise<Array<AnyRouteMatch> | undefined> export type MatchRouteFn< TRouteTree extends AnyRoute, TTrailingSlashOption extends TrailingSlashOption, TDefaultStructuralSharingOption extends boolean, TRouterHistory extends RouterHistory, > = < TFrom extends RoutePaths<TRouteTree> = '/', TTo extends string | undefined = undefined, TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>, >( location: ToOptions< RouterCore< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory >, TFrom, TTo >, opts?: MatchRouteOptions, ) => false | RouteById<TRouteTree, TResolved>['types']['allParams'] export type UpdateFn< TRouteTree extends AnyRoute, TTrailingSlashOption extends TrailingSlashOption, TDefaultStructuralSharingOption extends boolean, TRouterHistory extends RouterHistory, TDehydrated extends Record<string, any>, > = ( newOptions: RouterConstructorOptions< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated >, ) => void export type InvalidateFn<TRouter extends AnyRouter> = (opts?: { filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean sync?: boolean forcePending?: boolean }) => Promise<void> export type ParseLocationFn<TRouteTree extends AnyRoute> = ( locationToParse: HistoryLocation, previousLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>, ) => ParsedLocation<FullSearchSchema<TRouteTree>> export type GetMatchRoutesFn = (pathname: string) => { matchedRoutes: ReadonlyArray<AnyRoute> /** exhaustive params, still in their string form */ routeParams: Record<string, string> /** partial params, parsed from routeParams during matching */ parsedParams: Record<string, unknown> | undefined foundRoute: AnyRoute | undefined parseError?: unknown } export type EmitFn = (routerEvent: RouterEvent) => void export type LoadFn = (opts?: { sync?: boolean }) => Promise<void> export type CommitLocationFn = ({ viewTransition, ignoreBlocker, ...next }: ParsedLocation & CommitLocationOptions) => Promise<void> export type StartTransitionFn = (fn: () => void) => void export interface MatchRoutesFn { ( pathname: string, locationSearch?: AnySchema, opts?: MatchRoutesOpts, ): Array<MakeRouteMatchUnion> /** * @deprecated use the following signature instead */ (next: ParsedLocation, opts?: MatchRoutesOpts): Array<AnyRouteMatch> ( pathnameOrNext: string | ParsedLocation, locationSearchOrOpts?: AnySchema | MatchRoutesOpts, opts?: MatchRoutesOpts, ): Array<AnyRouteMatch> } export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined export type UpdateMatchFn = ( id: string, updater: (match: AnyRouteMatch) => AnyRouteMatch, ) => void export type LoadRouteChunkFn = (route: AnyRoute) => Promise<Array<void>> export type ResolveRedirect = (err: AnyRedirect) => ResolvedRedirect export type ClearCacheFn<TRouter extends AnyRouter> = (opts?: { filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean }) => void export interface ServerSsr { /** * Injects HTML synchronously into the stream. * Emits an onInjectedHtml event that listeners can handle. * If no subscriber is listening, the HTML is buffered and can be retrieved via takeBufferedHtml(). */ injectHtml: (html: string) => void /** * Injects a script tag synchronously into the stream. */ injectScript: (script: string) => void isDehydrated: () => boolean isSerializationFinished: () => boolean onRenderFinished: (listener: () => void) => void setRenderFinished: () => void cleanup: () => void onSerializationFinished: (listener: () => void) => void dehydrate: () => Promise<void> takeBufferedScripts: () => RouterManagedTag | undefined /** * Takes any buffered HTML that was injected. * Returns the buffered HTML string (which may include multiple script tags) or undefined if empty. */ takeBufferedHtml: () => string | undefined liftScriptBarrier: () => void } export type AnyRouterWithContext<TContext> = RouterCore< AnyRouteWithContext<TContext>, any, any, any, any > export type AnyRouter = RouterCore<any, any, any, any, any> export interface ViewTransitionOptions { types: | Array<string> | ((locationChangeInfo: { fromLocation?: ParsedLocation toLocation: ParsedLocation pathChanged: boolean hrefChanged: boolean hashChanged: boolean }) => Array<string> | false) } // TODO where is this used? can we remove this? /** * Convert an unknown error into a minimal, serializable object. * Includes name and message (and stack in development). */ export function defaultSerializeError(err: unknown) { if (err instanceof Error) { const obj = { name: err.name, message: err.message, } if (process.env.NODE_ENV === 'development') { ;(obj as any).stack = err.stack } return obj } return { data: err, } } /** Options for configuring trailing-slash behavior. */ export const trailingSlashOptions = { always: 'always', never: 'never', preserve: 'preserve', } as const export type TrailingSlashOption = (typeof trailingSlashOptions)[keyof typeof trailingSlashOptions] /** * Compute whether path, href or hash changed between previous and current * resolved locations in router state. */ export function getLocationChangeInfo(routerState: { resolvedLocation?: ParsedLocation location: ParsedLocation }) { const fromLocation = routerState.resolvedLocation const toLocation = routerState.location const pathChanged = fromLocation?.pathname !== toLocation.pathname const hrefChanged = fromLocation?.href !== toLocation.href const hashChanged = fromLocation?.hash !== toLocation.hash return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged } } function filterRedirectedCachedMatches<T extends { status: string }>( matches: Array<T>, ): Array<T> { const filtered = matches.filter((d) => d.status !== 'redirected') return filtered.length === matches.length ? matches : filtered } export type CreateRouterFn = < TRouteTree extends AnyRoute, TTrailingSlashOption extends TrailingSlashOption = 'never', TDefaultStructuralSharingOption extends boolean = false, TRouterHistory extends RouterHistory = RouterHistory, TDehydrated extends Record<string, any> = Record<string, any>, >( options: undefined extends number ? 'strictNullChecks must be enabled in tsconfig.json' : RouterConstructorOptions< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated >, ) => RouterCore< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated > declare global { // eslint-disable-next-line no-var var __TSR_CACHE__: | { routeTree: AnyRoute processRouteTreeResult: ProcessRouteTreeResult<AnyRoute> resolvePathCache: LRUCache<string, string> } | undefined } /** * 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 */ type RouterStateStore<TState> = { state: TState setState: (updater: (prev: TState) => TState) => void } function createServerStore<TState>( initialState: TState, ): RouterStateStore<TState> { const store = { state: initialState, setState: (updater: (prev: TState) => TState) => { store.state = updater(store.state) }, } as RouterStateStore<TState> return store } export class RouterCore< in out TRouteTree extends AnyRoute, in out TTrailingSlashOption extends TrailingSlashOption, in out TDefaultStructuralSharingOption extends boolean, in out TRouterHistory extends RouterHistory = RouterHistory, in out TDehydrated extends Record<string, any> = Record<string, any>, > { // Option-independent properties tempLocationKey: string | undefined = `${Math.round( Math.random() * 10000000, )}` resetNextScroll = true shouldViewTransition?: boolean | ViewTransitionOptions = undefined isViewTransitionTypesSupported?: boolean = undefined subscribers = new Set<RouterListener<RouterEvent>>() viewTransitionPromise?: ControlledPromise<true> isScrollRestoring = false isScrollRestorationSetup = false // Must build in constructor __store!: Store<RouterState<TRouteTree>> options!: PickAsRequired< RouterOptions< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated >, 'stringifySearch' | 'parseSearch' | 'context' > history!: TRouterHistory rewrite?: LocationRewrite origin?: string latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>> pendingBuiltLocation?: ParsedLocation<FullSearchSchema<TRouteTree>> basepath!: string routeTree!: TRouteTree routesById!: RoutesById<TRouteTree> routesByPath!: RoutesByPath<TRouteTree> processedTree!: ProcessedTree<TRouteTree, any, any> resolvePathCache!: LRUCache<string, string> isServer!: boolean pathParamsDecoder?: (encoded: string) => string protocolAllowlist!: Set<string> /** * @deprecated Use the `createRouter` function instead */ constructor( options: RouterConstructorOptions< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated >, ) { this.update({ defaultPreloadDelay: 50, defaultPendingMs: 1000, defaultPendingMinMs: 500, context: undefined!, ...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 } } // This is a default implementation that can optionally be overridden // by the router provider once rendered. We provide this so that the // router can be used in a non-react environment if necessary startTransition: StartTransitionFn = (fn) => fn() isShell() { return !!this.options.isShell } isPrerendering() { return !!this.options.isPrerendering } update: UpdateFn< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated > = (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 === undefined 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() as TRouterHistory } } 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 { // fallback for the server, can be overridden by calling router.update({origin}) on the server this.origin = 'http://localhost' } } if (this.history) { this.updateLatestLocation() } if (this.options.routeTree !== this.routeTree) { this.routeTree = this.options.routeTree as TRouteTree let processRouteTreeResult: ProcessRouteTreeResult<TRouteTree> 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 as any } else { this.resolvePathCache = createLRUCache(1000) processRouteTreeResult = this.buildRouteTree() // only cache if nothing else is cached yet if ( (isServer ?? this.isServer) && process.env.NODE_ENV !== 'development' && globalThis.__TSR_CACHE__ === undefined ) { globalThis.__TSR_CACHE__ = { routeTree: this.routeTree, processRouteTreeResult: processRouteTreeResult as any, resolvePathCache: this.resolvePathCache, } } } this.setRoutes(processRouteTreeResult) } if (!this.__store && this.latestLocation) { if (isServer ?? this.isServer) { this.__store = createServerStore( getInitialRouterState(this.latestLocation), ) as unknown as Store<any> } else { this.__store = createStore(getInitialRouterState(this.latestLocation)) setupScrollRestoration(this) } } let needsLocationUpdate = false const nextBasepath = this.options.basepath ?? '/' const nextRewriteOption = this.options.rewrite const basepathChanged = basepathWasUnset || prevBasepath !== nextBasepath const rewriteChanged = prevRewriteOption !== nextRewriteOption if (basepathChanged || rewriteChanged) { this.basepath = nextBasepath const rewrites: Array<LocationRewrite> = [] const trimmed = trimPath(nextBasepath) if (trimmed && trimmed !== '/') { rewrites.push( rewriteBasepath({ basepath: nextBasepath, }), ) } if (nextRewriteOption) { rewrites.push(nextRewriteOption) } this.rewrite = rewrites.length === 0 ? undefined : rewrites.length === 1 ? rewrites[0] : composeRewrites(rewrites) if (this.history) { this.updateLatestLocation() } needsLocationUpdate = true } if (needsLocationUpdate && this.__store) { this.__store.setState((s) => ({ ...s, location: 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)', ) } } get state(): RouterState<TRouteTree> { return this.__store.state } updateLatestLocation = () => { this.latestLocation = this.parseLocation( this.history.location, this.latestLocation, ) } 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 } setRoutes({ routesById, routesByPath, processedTree, }: ProcessRouteTreeResult<TRouteTree>) { this.routesById = routesById as RoutesById<TRouteTree> this.routesByPath = routesByPath as RoutesByPath<TRouteTree> this.processedTree = processedTree const notFoundRoute = this.options.notFoundRoute if (notFoundRoute) { notFoundRoute.init({ originalIndex: 99999999999, }) this.routesById[notFoundRoute.id] = notFoundRoute } } /** * Subscribe to router lifecycle events like `onBeforeNavigate`, `onLoad`, * `onResolved`, etc. Returns an unsubscribe function. * * @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouterEventsType */ subscribe: SubscribeFn = (eventType, fn) => { const listener: RouterListener<any> = { eventType, fn, } this.subscribers.add(listener) return () => { this.subscribers.delete(listener) } } emit: EmitFn = (routerEvent) => { this.subscribers.forEach((listener) => { if (listener.eventType === routerEvent.type) { listener.fn(routerEvent) } }) } /** * Parse a HistoryLocation into a strongly-typed ParsedLocation using the * current router options, rewrite rules and search parser/stringifier. */ parseLocation: ParseLocationFn<TRouteTree> = ( locationToParse, previousLocation, ) => { const parse = ({ pathname, search, hash, href, state, }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => { // Fast path: no rewrite configured and pathname doesn't need encoding // Characters that need encoding: space, high unicode, control chars // eslint-disable-next-line no-control-regex 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: href, pathname: decodePath(pathname).path, external: false, searchStr, search: nullReplaceEqualDeep( previousLocation?.search, parsedSearch, ) as any, hash: decodePath(hash.slice(1)).path, state: replaceEqualDeep(previousLocation?.state, state), } } // Before we do any processing, we need to allow rewrites to modify the URL // build up the full URL by combining the href from history with the router's origin 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) // Make sure our final url uses the re-stringified pathname, search, and has for consistency // (We were already doing this, so just keeping it for now) url.search = searchStr const fullPath = url.href.replace(url.origin, '') return { href: fullPath, publicHref: href, pathname: decodePath(url.pathname).path, external: !!this.rewrite && url.origin !== this.origin, searchStr, search: nullReplaceEqualDeep( previousLocation?.search, parsedSearch, ) as any, 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)) { // Sync up the location keys const parsedTempLocation = parse(__tempLocation) as any parsedTempLocation.state.key = location.state.key // TODO: Remove in v2 - use __TSR_key instead parsedTempLocation.state.__TSR_key = location.state.__TSR_key delete parsedTempLocation.state.__tempLocation return { ...parsedTempLocation, maskedLocation: location, } } return location } /** Resolve a path against the router basepath and trailing-slash policy. */ resolvePathWithBase = (from: string, path: string) => { const resolvedPath = resolvePath({ base: from, to: cleanPath(path), trailingSlash: this.options.trailingSlash, cache: this.resolvePathCache, }) return resolvedPath } get looseRoutesById() { return this.routesById as Record<string, AnyRoute> } matchRoutes: MatchRoutesFn = ( pathnameOrNext: string | ParsedLocation, locationSearchOrOpts?: AnySchema | MatchRoutesOpts, opts?: MatchRoutesOpts, ) => { if (typeof pathnameOrNext === 'string') { return this.matchRoutesInternal( { pathname: pathnameOrNext, search: locationSearchOrOpts, } as ParsedLocation, opts, ) } return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts) } private getParentContext(parentMatch?: AnyRouteMatch) { const parentMatchId = parentMatch?.id const parentContext = !parentMatchId ? ((this.options.context as any) ?? undefined) : (parentMatch.context ?? this.options.context ?? undefined) return parentContext } private matchRoutesInternal( next: ParsedLocation, opts?: MatchRoutesOpts, ): Array<AnyRouteMatch> { const matchedRoutesResult = this.getMatchedRoutes(next.pathname) const { foundRoute, routeParams, parsedParams } = matchedRoutesResult let { matchedRoutes } = matchedRoutesResult let isGlobalNotFound = false // Check to see if the route needs a 404 entry 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 the user has defined an (old) 404 route, use it if (this.options.notFoundRoute) { matchedRoutes = [...matchedRoutes, this.options.notFoundRoute] } else { // If there is no routes found during path matching isGlobalNotFound = true } } const globalNotFoundRouteId = isGlobalNotFound ? findGlobalNotFoundRouteId(this.options.notFoundMode, matchedRoutes) : undefined const matches = new Array<AnyRouteMatch>(matchedRoutes.length) const previousMatchesByRouteId = new Map( this.state.matches.map((match) => [match.routeId, match]), ) for (let index = 0; index < matchedRoutes.length; index++) { const route = matchedRoutes[index]! // Take each matched route and resolve + validate its search params // This has to happen serially because each route's search params // can depend on the parent route's search params // It must also happen before we create the match so that we can // pass the search params to the route's potential key function // which is used to uniquely identify the route match in state const parentMatch = matches[index - 1] let preMatchSearch: Record<string, any> let strictMatchSearch: Record<string, any> let searchError: any { // Validate the search params and stabilize them const parentSearch = parentMatch?.search ?? next.search const parentStrictSearch = parentMatch?._strictSearch ?? undefined try { const strictSearch = validateSearch(route.options.validateSearch, { ...parentSearch }) ?? undefined preMatchSearch = { ...parentSearch, ...strictSearch, } strictMatchSearch = { ...parentStrictSearch, ...strictSearch } searchError = undefined } catch (err: any) { let searchParamError = err if (!(err instanceof SearchParamError)) { searchParamError = new SearchParamError(err.message, { cause: err, }) } if (opts?.throwOnError) { throw searchParamError } preMatchSearch = parentSearch strictMatchSearch = {} searchError = searchParamError } } // This is where we need to call route.options.loaderDeps() to get any additional // deps that the route's loader function might need to run. We need to do this // before we create the match so that we can pass the deps to the route's // potential key function which is used to uniquely identify the route match in state 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, }) // Waste not, want not. If we already have a match for this route, // reuse it. This is important for layout routes, which might stick // around between navigation actions that only change leaf routes. // Existing matches are matches that are already loaded along with // pending matches that are still loading const matchId = // route.id for disambiguation route.id + // interpolatedPath for param changes interpolatedPath + // explicit deps loaderDepsHash const existingMatch = this.getMatch(matchId) const previousMatch = previousMatchesByRouteId.get(route.id) const strict