From 89b2795b86e90746f0dc36fc807a2d296a5785fe Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 27 Jul 2025 23:19:39 +0200 Subject: [PATCH 1/5] refactor(router-core): minor cleanup of router --- packages/router-core/src/router.ts | 1069 +++++++++++++--------------- 1 file changed, 513 insertions(+), 556 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 4b2a48eff2..3fd7a44332 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -656,7 +656,7 @@ export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined export type UpdateMatchFn = ( id: string, updater: (match: AnyRouteMatch) => AnyRouteMatch, -) => AnyRouteMatch +) => void export type LoadRouteChunkFn = (route: AnyRoute) => Promise> @@ -1839,7 +1839,7 @@ export class RouterCore< pendingMatches, // If a cached moved to pendingMatches, remove it from cachedMatches cachedMatches: s.cachedMatches.filter( - (d) => !pendingMatches.find((e) => e.id === d.id), + (d) => !pendingMatches.some((e) => e.id === d.id), ), })) } @@ -1897,14 +1897,14 @@ export class RouterCore< const newMatches = s.pendingMatches || s.matches exitingMatches = previousMatches.filter( - (match) => !newMatches.find((d) => d.id === match.id), + (match) => !newMatches.some((d) => d.id === match.id), ) enteringMatches = newMatches.filter( (match) => - !previousMatches.find((d) => d.id === match.id), + !previousMatches.some((d) => d.id === match.id), ) stayingMatches = previousMatches.filter((match) => - newMatches.find((d) => d.id === match.id), + newMatches.some((d) => d.id === match.id), ) return { @@ -2043,37 +2043,29 @@ export class RouterCore< } updateMatch: UpdateMatchFn = (id, updater) => { - let updated!: AnyRouteMatch - const isPending = this.state.pendingMatches?.find((d) => d.id === id) - const isMatched = this.state.matches.find((d) => d.id === id) - const isCached = this.state.cachedMatches.find((d) => d.id === id) - - const matchesKey = isPending + const matchesKey = this.state.pendingMatches?.find((d) => d.id === id) ? 'pendingMatches' - : isMatched + : this.state.matches.find((d) => d.id === id) ? 'matches' - : isCached + : this.state.cachedMatches.find((d) => d.id === id) ? 'cachedMatches' : '' if (matchesKey) { this.__store.setState((s) => ({ ...s, - [matchesKey]: s[matchesKey]?.map((d) => - d.id === id ? (updated = updater(d)) : d, - ), + [matchesKey]: s[matchesKey]?.map((d) => (d.id === id ? updater(d) : d)), })) } - - return updated } getMatch: GetMatchFn = (matchId: string) => { - return [ - ...this.state.cachedMatches, - ...(this.state.pendingMatches ?? []), - ...this.state.matches, - ].find((d) => d.id === matchId) + const find = (d: { id: string }) => d.id === matchId + return ( + this.state.cachedMatches.find(find) ?? + this.state.pendingMatches?.find(find) ?? + this.state.matches.find(find) + ) } loadMatches = async ({ @@ -2152,13 +2144,13 @@ export class RouterCore< err.options._fromLocation = location err.redirectHandled = true err = this.resolveRedirect(err) - throw err } else if (isNotFound(err)) { this._handleNotFound(matches, err, { updateMatch, }) - throw err } + + throw err } } @@ -2177,589 +2169,554 @@ export class RouterCore< return false } - try { - await new Promise((resolveAll, rejectAll) => { - ;(async () => { - try { - const handleSerialError = ( - index: number, - err: any, - routerCode: string, - ) => { - const { id: matchId, routeId } = matches[index]! - const route = this.looseRoutesById[routeId]! - - // Much like suspense, we use a promise here to know if - // we've been outdated by a new loadMatches call and - // should abort the current async operation - if (err instanceof Promise) { - throw err - } + const handleSerialError = (index: number, err: any, routerCode: string) => { + const { id: matchId, routeId } = matches[index]! + const route = this.looseRoutesById[routeId]! - err.routerCode = routerCode - firstBadMatchIndex = firstBadMatchIndex ?? index - handleRedirectAndNotFound(this.getMatch(matchId)!, err) - - try { - route.options.onError?.(err) - } catch (errorHandlerErr) { - err = errorHandlerErr - handleRedirectAndNotFound(this.getMatch(matchId)!, err) - } - - updateMatch(matchId, (prev) => { - prev.beforeLoadPromise?.resolve() - prev.loadPromise?.resolve() - - return { - ...prev, - error: err, - status: 'error', - isFetching: false, - updatedAt: Date.now(), - abortController: new AbortController(), - beforeLoadPromise: undefined, - } - }) - } + // Much like suspense, we use a promise here to know if + // we've been outdated by a new loadMatches call and + // should abort the current async operation + if (err instanceof Promise) { + throw err + } - for (const [index, { id: matchId, routeId }] of matches.entries()) { - const existingMatch = this.getMatch(matchId)! - const parentMatchId = matches[index - 1]?.id - const parentMatch = parentMatchId - ? this.getMatch(parentMatchId)! - : undefined + err.routerCode = routerCode + firstBadMatchIndex = firstBadMatchIndex ?? index + handleRedirectAndNotFound(this.getMatch(matchId)!, err) - const route = this.looseRoutesById[routeId]! + try { + route.options.onError?.(err) + } catch (errorHandlerErr) { + err = errorHandlerErr + handleRedirectAndNotFound(this.getMatch(matchId)!, err) + } - const pendingMs = - route.options.pendingMs ?? this.options.defaultPendingMs + updateMatch(matchId, (prev) => { + prev.beforeLoadPromise?.resolve() + prev.loadPromise?.resolve() - // on the server, determine whether SSR the current match or not - if (this.isServer) { - let ssr: boolean | 'data-only' - // in SPA mode, only SSR the root route - if (this.isShell()) { - ssr = matchId === rootRouteId - } else { - const defaultSsr = this.options.defaultSsr ?? true - if (parentMatch?.ssr === false) { - ssr = false - } else { - let tempSsr: boolean | 'data-only' - if (route.options.ssr === undefined) { - tempSsr = defaultSsr - } else if (typeof route.options.ssr === 'function') { - const { search, params } = this.getMatch(matchId)! - - function makeMaybe(value: any, error: any) { - if (error) { - return { status: 'error' as const, error } - } - return { status: 'success' as const, value } - } - - const ssrFnContext: SsrContextOptions = { - search: makeMaybe(search, existingMatch.searchError), - params: makeMaybe(params, existingMatch.paramsError), - location, - matches: matches.map((match) => ({ - index: match.index, - pathname: match.pathname, - fullPath: match.fullPath, - staticData: match.staticData, - id: match.id, - routeId: match.routeId, - search: makeMaybe(match.search, match.searchError), - params: makeMaybe(match.params, match.paramsError), - ssr: match.ssr, - })), - } - tempSsr = - (await route.options.ssr(ssrFnContext)) ?? defaultSsr - } else { - tempSsr = route.options.ssr - } + return { + ...prev, + error: err, + status: 'error', + isFetching: false, + updatedAt: Date.now(), + abortController: new AbortController(), + beforeLoadPromise: undefined, + } + }) + } - if (tempSsr === true && parentMatch?.ssr === 'data-only') { - ssr = 'data-only' - } else { - ssr = tempSsr - } + try { + for (const [index, { id: matchId, routeId }] of matches.entries()) { + const existingMatch = this.getMatch(matchId)! + const parentMatchId = matches[index - 1]?.id + const parentMatch = parentMatchId + ? this.getMatch(parentMatchId)! + : undefined + + const route = this.looseRoutesById[routeId]! + + const pendingMs = + route.options.pendingMs ?? this.options.defaultPendingMs + + // on the server, determine whether SSR the current match or not + if (this.isServer) { + let ssr: boolean | 'data-only' + // in SPA mode, only SSR the root route + if (this.isShell()) { + ssr = matchId === rootRouteId + } else { + const defaultSsr = this.options.defaultSsr ?? true + if (parentMatch?.ssr === false) { + ssr = false + } else { + let tempSsr: boolean | 'data-only' + if (route.options.ssr === undefined) { + tempSsr = defaultSsr + } else if (typeof route.options.ssr === 'function') { + const { search, params } = this.getMatch(matchId)! + + function makeMaybe(value: any, error: any) { + if (error) { + return { status: 'error' as const, error } } + return { status: 'success' as const, value } } - updateMatch(matchId, (prev) => ({ - ...prev, - ssr, - })) + + const ssrFnContext: SsrContextOptions = { + search: makeMaybe(search, existingMatch.searchError), + params: makeMaybe(params, existingMatch.paramsError), + location, + matches: matches.map((match) => ({ + index: match.index, + pathname: match.pathname, + fullPath: match.fullPath, + staticData: match.staticData, + id: match.id, + routeId: match.routeId, + search: makeMaybe(match.search, match.searchError), + params: makeMaybe(match.params, match.paramsError), + ssr: match.ssr, + })), + } + tempSsr = (await route.options.ssr(ssrFnContext)) ?? defaultSsr + } else { + tempSsr = route.options.ssr } - if (shouldSkipLoader(matchId)) { - continue + if (tempSsr === true && parentMatch?.ssr === 'data-only') { + ssr = 'data-only' + } else { + ssr = tempSsr } + } + } + updateMatch(matchId, (prev) => ({ + ...prev, + ssr, + })) + } - const shouldPending = !!( - onReady && - !this.isServer && - !resolvePreload(matchId) && - (route.options.loader || - route.options.beforeLoad || - routeNeedsPreload(route)) && - typeof pendingMs === 'number' && - pendingMs !== Infinity && - (route.options.pendingComponent ?? - (this.options as any)?.defaultPendingComponent) - ) + if (shouldSkipLoader(matchId)) { + continue + } - let executeBeforeLoad = true - const setupPendingTimeout = () => { - if ( - shouldPending && - this.getMatch(matchId)!.pendingTimeout === undefined - ) { - const pendingTimeout = setTimeout(() => { - try { - // Update the match and prematurely resolve the loadMatches promise so that - // the pending component can start rendering - triggerOnReady() - } catch {} - }, pendingMs) - updateMatch(matchId, (prev) => ({ - ...prev, - pendingTimeout, - })) - } - } - if ( - // If we are in the middle of a load, either of these will be present - // (not to be confused with `loadPromise`, which is always defined) - existingMatch.beforeLoadPromise || - existingMatch.loaderPromise - ) { - setupPendingTimeout() - - // Wait for the beforeLoad to resolve before we continue - await existingMatch.beforeLoadPromise - const match = this.getMatch(matchId)! - if (match.status === 'error') { - executeBeforeLoad = true - } else if ( - match.preload && - (match.status === 'redirected' || match.status === 'notFound') - ) { - handleRedirectAndNotFound(match, match.error) - } - } - if (executeBeforeLoad) { - // If we are not in the middle of a load OR the previous load failed, start it - try { - updateMatch(matchId, (prev) => { - // explicitly capture the previous loadPromise - const prevLoadPromise = prev.loadPromise - return { - ...prev, - loadPromise: createControlledPromise(() => { - prevLoadPromise?.resolve() - }), - beforeLoadPromise: createControlledPromise(), - } - }) + const shouldPending = !!( + onReady && + !this.isServer && + !resolvePreload(matchId) && + (route.options.loader || + route.options.beforeLoad || + routeNeedsPreload(route)) && + typeof pendingMs === 'number' && + pendingMs !== Infinity && + (route.options.pendingComponent ?? + (this.options as any)?.defaultPendingComponent) + ) - const { paramsError, searchError } = this.getMatch(matchId)! + const setupPendingTimeout = () => { + if ( + shouldPending && + this.getMatch(matchId)!.pendingTimeout === undefined + ) { + const pendingTimeout = setTimeout(() => { + try { + // Update the match and prematurely resolve the loadMatches promise so that + // the pending component can start rendering + triggerOnReady() + } catch {} + }, pendingMs) + updateMatch(matchId, (prev) => ({ + ...prev, + pendingTimeout, + })) + } + } + if ( + // If we are in the middle of a load, either of these will be present + // (not to be confused with `loadPromise`, which is always defined) + existingMatch.beforeLoadPromise || + existingMatch.loaderPromise + ) { + setupPendingTimeout() - if (paramsError) { - handleSerialError(index, paramsError, 'PARSE_PARAMS') - } + // Wait for the beforeLoad to resolve before we continue + await existingMatch.beforeLoadPromise + const match = this.getMatch(matchId)! + if ( + match.preload && + (match.status === 'redirected' || match.status === 'notFound') + ) { + handleRedirectAndNotFound(match, match.error) + } + } - if (searchError) { - handleSerialError(index, searchError, 'VALIDATE_SEARCH') - } + // If we are not in the middle of a load OR the previous load failed, start it + try { + // TODO: how many `updateMatch` can we skip if we know there is no `route.options.beforeLoad`? + updateMatch(matchId, (prev) => { + // explicitly capture the previous loadPromise + const prevLoadPromise = prev.loadPromise + return { + ...prev, + loadPromise: createControlledPromise(() => { + prevLoadPromise?.resolve() + }), + beforeLoadPromise: createControlledPromise(), + } + }) - setupPendingTimeout() + const { paramsError, searchError } = this.getMatch(matchId)! - const abortController = new AbortController() + if (paramsError) { + handleSerialError(index, paramsError, 'PARSE_PARAMS') + } - const parentMatchContext = - parentMatch?.context ?? this.options.context ?? {} + if (searchError) { + handleSerialError(index, searchError, 'VALIDATE_SEARCH') + } - updateMatch(matchId, (prev) => ({ - ...prev, - isFetching: 'beforeLoad', - fetchCount: prev.fetchCount + 1, - abortController, - context: { - ...parentMatchContext, - ...prev.__routeContext, - }, - })) + setupPendingTimeout() - const { search, params, context, cause } = - this.getMatch(matchId)! - - const preload = resolvePreload(matchId) - - const beforeLoadFnContext: BeforeLoadContextOptions< - any, - any, - any, - any, - any - > = { - search, - abortController, - params, - preload, - context, - location, - navigate: (opts: any) => - this.navigate({ ...opts, _fromLocation: location }), - buildLocation: this.buildLocation, - cause: preload ? 'preload' : cause, - matches, - } + const abortController = new AbortController() - const beforeLoadContext = - await route.options.beforeLoad?.(beforeLoadFnContext) + const parentMatchContext = + parentMatch?.context ?? this.options.context - if ( - isRedirect(beforeLoadContext) || - isNotFound(beforeLoadContext) - ) { - handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD') - } + updateMatch(matchId, (prev) => ({ + ...prev, + isFetching: 'beforeLoad', + fetchCount: prev.fetchCount + 1, + abortController, + context: { + ...parentMatchContext, + ...prev.__routeContext, + }, + })) - updateMatch(matchId, (prev) => { - return { - ...prev, - __beforeLoadContext: beforeLoadContext, - context: { - ...parentMatchContext, - ...prev.__routeContext, - ...beforeLoadContext, - }, - abortController, - } - }) - } catch (err) { - handleSerialError(index, err, 'BEFORE_LOAD') - } + let beforeLoadContext: any + if (route.options.beforeLoad) { + const { search, params, context, cause } = this.getMatch(matchId)! + const preload = resolvePreload(matchId) + beforeLoadContext = await route.options.beforeLoad({ + search, + abortController, + params, + preload, + context, + location, + navigate: (opts: any) => + this.navigate({ ...opts, _fromLocation: location }), + buildLocation: this.buildLocation, + cause: preload ? 'preload' : cause, + matches, + } satisfies BeforeLoadContextOptions) + if ( + isRedirect(beforeLoadContext) || + isNotFound(beforeLoadContext) + ) { + handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD') + } + } - updateMatch(matchId, (prev) => { - prev.beforeLoadPromise?.resolve() + updateMatch(matchId, (prev) => { + return { + ...prev, + __beforeLoadContext: beforeLoadContext, + context: { + ...parentMatchContext, + ...prev.__routeContext, + ...beforeLoadContext, + }, + abortController, + } + }) + } catch (err) { + handleSerialError(index, err, 'BEFORE_LOAD') + } - return { - ...prev, - beforeLoadPromise: undefined, - isFetching: false, - } - }) + updateMatch(matchId, (prev) => { + prev.beforeLoadPromise?.resolve() + + return { + ...prev, + beforeLoadPromise: undefined, + isFetching: false, + } + }) + } + + const validResolvedMatches = matches.slice(0, firstBadMatchIndex) + const matchPromises: Array> = [] + + validResolvedMatches.forEach(({ id: matchId, routeId }, index) => { + matchPromises.push( + (async () => { + let loaderShouldRunAsync = false + let loaderIsRunningAsync = false + const route = this.looseRoutesById[routeId]! + + const executeHead = async () => { + const match = this.getMatch(matchId) + // in case of a redirecting match during preload, the match does not exist + if (!match) { + return + } + const assetContext = { + matches, + match, + params: match.params, + loaderData: match.loaderData, + } + const headFnContent = await route.options.head?.(assetContext) + const meta = headFnContent?.meta + const links = headFnContent?.links + const headScripts = headFnContent?.scripts + const styles = headFnContent?.styles + + const scripts = await route.options.scripts?.(assetContext) + const headers = await route.options.headers?.(assetContext) + return { + meta, + links, + headScripts, + headers, + scripts, + styles, } } - const validResolvedMatches = matches.slice(0, firstBadMatchIndex) - const matchPromises: Array> = [] - - validResolvedMatches.forEach(({ id: matchId, routeId }, index) => { - matchPromises.push( - (async () => { - let loaderShouldRunAsync = false - let loaderIsRunningAsync = false - const route = this.looseRoutesById[routeId]! - - const executeHead = async () => { - const match = this.getMatch(matchId) - // in case of a redirecting match during preload, the match does not exist - if (!match) { - return - } - const assetContext = { - matches, - match, - params: match.params, - loaderData: match.loaderData, - } - const headFnContent = - await route.options.head?.(assetContext) - const meta = headFnContent?.meta - const links = headFnContent?.links - const headScripts = headFnContent?.scripts - const styles = headFnContent?.styles - - const scripts = await route.options.scripts?.(assetContext) - const headers = await route.options.headers?.(assetContext) - return { - meta, - links, - headScripts, - headers, - scripts, - styles, - } - } + const potentialPendingMinPromise = async () => { + const latestMatch = this.getMatch(matchId)! + if (latestMatch.minPendingPromise) { + await latestMatch.minPendingPromise + } + } - const potentialPendingMinPromise = async () => { - const latestMatch = this.getMatch(matchId)! - if (latestMatch.minPendingPromise) { - await latestMatch.minPendingPromise - } - } + const prevMatch = this.getMatch(matchId)! + if (shouldSkipLoader(matchId)) { + if (this.isServer) { + const head = await executeHead() + updateMatch(matchId, (prev) => ({ + ...prev, + ...head, + })) + return this.getMatch(matchId)! + } + } + // there is a loaderPromise, so we are in the middle of a load + else if (prevMatch.loaderPromise) { + // do not block if we already have stale data we can show + // but only if the ongoing load is not a preload since error handling is different for preloads + // and we don't want to swallow errors + if ( + prevMatch.status === 'success' && + !sync && + !prevMatch.preload + ) { + return this.getMatch(matchId)! + } + await prevMatch.loaderPromise + const match = this.getMatch(matchId)! + if (match.error) { + handleRedirectAndNotFound(match, match.error) + } + } else { + const parentMatchPromise = matchPromises[index - 1] as any - const prevMatch = this.getMatch(matchId)! - if (shouldSkipLoader(matchId)) { - if (this.isServer) { - const head = await executeHead() - updateMatch(matchId, (prev) => ({ - ...prev, - ...head, - })) - return this.getMatch(matchId)! - } - } - // there is a loaderPromise, so we are in the middle of a load - else if (prevMatch.loaderPromise) { - // do not block if we already have stale data we can show - // but only if the ongoing load is not a preload since error handling is different for preloads - // and we don't want to swallow errors + const getLoaderContext = (): LoaderFnContext => { + const { params, loaderDeps, abortController, context, cause } = + this.getMatch(matchId)! + + const preload = resolvePreload(matchId) + + return { + params, + deps: loaderDeps, + preload: !!preload, + parentMatchPromise, + abortController: abortController, + context, + location, + navigate: (opts) => + this.navigate({ ...opts, _fromLocation: location }), + cause: preload ? 'preload' : cause, + route, + } + } + + // This is where all of the stale-while-revalidate magic happens + const age = Date.now() - this.getMatch(matchId)!.updatedAt + + const preload = resolvePreload(matchId) + + const staleAge = preload + ? (route.options.preloadStaleTime ?? + this.options.defaultPreloadStaleTime ?? + 30_000) // 30 seconds for preloads by default + : (route.options.staleTime ?? + this.options.defaultStaleTime ?? + 0) + + const shouldReloadOption = route.options.shouldReload + + // Default to reloading the route all the time + // Allow shouldReload to get the last say, + // if provided. + const shouldReload = + typeof shouldReloadOption === 'function' + ? shouldReloadOption(getLoaderContext()) + : shouldReloadOption + + updateMatch(matchId, (prev) => ({ + ...prev, + loaderPromise: createControlledPromise(), + preload: + !!preload && + !this.state.matches.find((d) => d.id === matchId), + })) + + const runLoader = async () => { + // TODO: how many `updateMatch` can we skip if we know there is no `route.options.{loader|head|scripts|headers}`? + try { + // If the Matches component rendered + // the pending component and needs to show it for + // a minimum duration, we''ll wait for it to resolve + // before committing to the match and resolving + // the loadPromise + + // Actually run the loader and handle the result + try { if ( - prevMatch.status === 'success' && - !sync && - !prevMatch.preload + !this.isServer || + (this.isServer && this.getMatch(matchId)!.ssr === true) ) { - return this.getMatch(matchId)! - } - await prevMatch.loaderPromise - const match = this.getMatch(matchId)! - if (match.error) { - handleRedirectAndNotFound(match, match.error) + this.loadRouteChunk(route) } - } else { - const parentMatchPromise = matchPromises[index - 1] as any - - const getLoaderContext = (): LoaderFnContext => { - const { - params, - loaderDeps, - abortController, - context, - cause, - } = this.getMatch(matchId)! - - const preload = resolvePreload(matchId) - - return { - params, - deps: loaderDeps, - preload: !!preload, - parentMatchPromise, - abortController: abortController, - context, - location, - navigate: (opts) => - this.navigate({ ...opts, _fromLocation: location }), - cause: preload ? 'preload' : cause, - route, - } - } - - // This is where all of the stale-while-revalidate magic happens - const age = Date.now() - this.getMatch(matchId)!.updatedAt - const preload = resolvePreload(matchId) + updateMatch(matchId, (prev) => ({ + ...prev, + isFetching: 'loader', + })) - const staleAge = preload - ? (route.options.preloadStaleTime ?? - this.options.defaultPreloadStaleTime ?? - 30_000) // 30 seconds for preloads by default - : (route.options.staleTime ?? - this.options.defaultStaleTime ?? - 0) + // Kick off the loader! + if (route.options.loader) { + const loaderData = + await route.options.loader(getLoaderContext()) - const shouldReloadOption = route.options.shouldReload + handleRedirectAndNotFound( + this.getMatch(matchId)!, + loaderData, + ) + updateMatch(matchId, (prev) => ({ + ...prev, + loaderData, + })) + } - // Default to reloading the route all the time - // Allow shouldReload to get the last say, - // if provided. - const shouldReload = - typeof shouldReloadOption === 'function' - ? shouldReloadOption(getLoaderContext()) - : shouldReloadOption + // Lazy option can modify the route options, + // so we need to wait for it to resolve before + // we can use the options + await route._lazyPromise + const head = await executeHead() + await potentialPendingMinPromise() + // Last but not least, wait for the the components + // to be preloaded before we resolve the match + await route._componentsPromise updateMatch(matchId, (prev) => ({ ...prev, - loaderPromise: createControlledPromise(), - preload: - !!preload && - !this.state.matches.find((d) => d.id === matchId), + error: undefined, + status: 'success', + isFetching: false, + updatedAt: Date.now(), + ...head, })) + } catch (e) { + let error = e - const runLoader = async () => { - try { - // If the Matches component rendered - // the pending component and needs to show it for - // a minimum duration, we''ll wait for it to resolve - // before committing to the match and resolving - // the loadPromise - - // Actually run the loader and handle the result - try { - if ( - !this.isServer || - (this.isServer && - this.getMatch(matchId)!.ssr === true) - ) { - this.loadRouteChunk(route) - } - - updateMatch(matchId, (prev) => ({ - ...prev, - isFetching: 'loader', - })) - - // Kick off the loader! - const loaderData = - await route.options.loader?.(getLoaderContext()) - - handleRedirectAndNotFound( - this.getMatch(matchId)!, - loaderData, - ) - updateMatch(matchId, (prev) => ({ - ...prev, - loaderData, - })) - - // Lazy option can modify the route options, - // so we need to wait for it to resolve before - // we can use the options - await route._lazyPromise - const head = await executeHead() - await potentialPendingMinPromise() - - // Last but not least, wait for the the components - // to be preloaded before we resolve the match - await route._componentsPromise - updateMatch(matchId, (prev) => ({ - ...prev, - error: undefined, - status: 'success', - isFetching: false, - updatedAt: Date.now(), - ...head, - })) - } catch (e) { - let error = e - - await potentialPendingMinPromise() - - handleRedirectAndNotFound(this.getMatch(matchId)!, e) - - try { - route.options.onError?.(e) - } catch (onErrorError) { - error = onErrorError - handleRedirectAndNotFound( - this.getMatch(matchId)!, - onErrorError, - ) - } - const head = await executeHead() - updateMatch(matchId, (prev) => ({ - ...prev, - error, - status: 'error', - isFetching: false, - ...head, - })) - } - } catch (err) { - const head = await executeHead() - - updateMatch(matchId, (prev) => ({ - ...prev, - loaderPromise: undefined, - ...head, - })) - handleRedirectAndNotFound(this.getMatch(matchId)!, err) - } - } + await potentialPendingMinPromise() - // If the route is successful and still fresh, just resolve - const { status, invalid } = this.getMatch(matchId)! - loaderShouldRunAsync = - status === 'success' && - (invalid || (shouldReload ?? age > staleAge)) - if (preload && route.options.preload === false) { - // Do nothing - } else if (loaderShouldRunAsync && !sync) { - loaderIsRunningAsync = true - ;(async () => { - try { - await runLoader() - const { loaderPromise, loadPromise } = - this.getMatch(matchId)! - loaderPromise?.resolve() - loadPromise?.resolve() - updateMatch(matchId, (prev) => ({ - ...prev, - loaderPromise: undefined, - })) - } catch (err) { - if (isRedirect(err)) { - await this.navigate(err.options) - } - } - })() - } else if ( - status !== 'success' || - (loaderShouldRunAsync && sync) - ) { - await runLoader() - } else { - // if the loader did not run, still update head. - // reason: parent's beforeLoad may have changed the route context - // and only now do we know the route context (and that the loader would not run) - const head = await executeHead() - updateMatch(matchId, (prev) => ({ - ...prev, - ...head, - })) + handleRedirectAndNotFound(this.getMatch(matchId)!, e) + + try { + route.options.onError?.(e) + } catch (onErrorError) { + error = onErrorError + handleRedirectAndNotFound( + this.getMatch(matchId)!, + onErrorError, + ) } + const head = await executeHead() + updateMatch(matchId, (prev) => ({ + ...prev, + error, + status: 'error', + isFetching: false, + ...head, + })) } - if (!loaderIsRunningAsync) { + } catch (err) { + const head = await executeHead() + + updateMatch(matchId, (prev) => ({ + ...prev, + loaderPromise: undefined, + ...head, + })) + handleRedirectAndNotFound(this.getMatch(matchId)!, err) + } + } + + // If the route is successful and still fresh, just resolve + const { status, invalid } = this.getMatch(matchId)! + loaderShouldRunAsync = + status === 'success' && + (invalid || (shouldReload ?? age > staleAge)) + if (preload && route.options.preload === false) { + // Do nothing + } else if (loaderShouldRunAsync && !sync) { + loaderIsRunningAsync = true + ;(async () => { + try { + await runLoader() const { loaderPromise, loadPromise } = this.getMatch(matchId)! loaderPromise?.resolve() loadPromise?.resolve() - } - - updateMatch(matchId, (prev) => { - clearTimeout(prev.pendingTimeout) - return { + updateMatch(matchId, (prev) => ({ ...prev, - isFetching: loaderIsRunningAsync - ? prev.isFetching - : false, - loaderPromise: loaderIsRunningAsync - ? prev.loaderPromise - : undefined, - invalid: false, - pendingTimeout: undefined, - _dehydrated: undefined, + loaderPromise: undefined, + })) + } catch (err) { + if (isRedirect(err)) { + await this.navigate(err.options) } - }) - return this.getMatch(matchId)! - })(), - ) - }) - - await Promise.all(matchPromises) + } + })() + } else if ( + status !== 'success' || + (loaderShouldRunAsync && sync) + ) { + await runLoader() + } else { + // if the loader did not run, still update head. + // reason: parent's beforeLoad may have changed the route context + // and only now do we know the route context (and that the loader would not run) + const head = await executeHead() + updateMatch(matchId, (prev) => ({ + ...prev, + ...head, + })) + } + } + if (!loaderIsRunningAsync) { + const { loaderPromise, loadPromise } = this.getMatch(matchId)! + loaderPromise?.resolve() + loadPromise?.resolve() + } - resolveAll() - } catch (err) { - rejectAll(err) - } - })() + updateMatch(matchId, (prev) => { + clearTimeout(prev.pendingTimeout) + return { + ...prev, + isFetching: loaderIsRunningAsync ? prev.isFetching : false, + loaderPromise: loaderIsRunningAsync + ? prev.loaderPromise + : undefined, + invalid: false, + pendingTimeout: undefined, + _dehydrated: undefined, + } + }) + return this.getMatch(matchId)! + })(), + ) }) + + await Promise.all(matchPromises) await triggerOnReady() } catch (err) { if (isRedirect(err) || isNotFound(err)) { @@ -2790,7 +2747,7 @@ export class RouterCore< invalid: true, ...(opts?.forcePending || d.status === 'error' ? ({ status: 'pending', error: undefined } as const) - : {}), + : null), } } return d @@ -2858,7 +2815,7 @@ export class RouterCore< : (route.options.gcTime ?? this.options.defaultGcTime)) ?? 5 * 60 * 1000 - return !(d.status !== 'error' && Date.now() - d.updatedAt < gcTime) + return d.status === 'error' && Date.now() - d.updatedAt >= gcTime } this.clearCache({ filter }) } From 65b41cc280b6a985b6a3fdbefb845da6f7aec490 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Mon, 28 Jul 2025 00:01:43 +0200 Subject: [PATCH 2/5] some > find --- packages/router-core/src/router.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 3fd7a44332..cfad354e91 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -2043,11 +2043,11 @@ export class RouterCore< } updateMatch: UpdateMatchFn = (id, updater) => { - const matchesKey = this.state.pendingMatches?.find((d) => d.id === id) + const matchesKey = this.state.pendingMatches?.some((d) => d.id === id) ? 'pendingMatches' - : this.state.matches.find((d) => d.id === id) + : this.state.matches.some((d) => d.id === id) ? 'matches' - : this.state.cachedMatches.find((d) => d.id === id) + : this.state.cachedMatches.some((d) => d.id === id) ? 'cachedMatches' : '' From 8e3e03d83fa5a8c41b02e82a4e9740efd62b8105 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Mon, 28 Jul 2025 00:01:51 +0200 Subject: [PATCH 3/5] bugfix? --- packages/router-core/src/router.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index cfad354e91..1965120916 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -2607,6 +2607,7 @@ export class RouterCore< await route._componentsPromise updateMatch(matchId, (prev) => ({ ...prev, + loaderData: prev.loaderData ?? undefined, // TODO: this is a test, we probably don't need this error: undefined, status: 'success', isFetching: false, From 1459cef88660828e7cdd00a13f5a9836ed790a3b Mon Sep 17 00:00:00 2001 From: Sheraff Date: Mon, 28 Jul 2025 08:35:52 +0200 Subject: [PATCH 4/5] how about this? --- packages/router-core/src/router.ts | 179 ++++++++++++++--------------- 1 file changed, 88 insertions(+), 91 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 1965120916..845a22b4dd 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -108,10 +108,10 @@ export type DefaultRemountDepsFn = ( opts: MakeRemountDepsOptionsUnion, ) => any -export interface DefaultRouterOptionsExtensions {} +export interface DefaultRouterOptionsExtensions { } export interface RouterOptionsExtensions - extends DefaultRouterOptionsExtensions {} + extends DefaultRouterOptionsExtensions { } export interface RouterOptions< TRouteTree extends AnyRoute, @@ -513,12 +513,12 @@ export type InferRouterContext = export type RouterContextOptions = AnyContext extends InferRouterContext - ? { - context?: InferRouterContext - } - : { - context: InferRouterContext - } + ? { + context?: InferRouterContext + } + : { + context: InferRouterContext + } export type RouterConstructorOptions< TRouteTree extends AnyRoute, @@ -690,14 +690,14 @@ export type AnyRouter = RouterCore export interface ViewTransitionOptions { types: - | Array - | ((locationChangeInfo: { - fromLocation?: ParsedLocation - toLocation: ParsedLocation - pathChanged: boolean - hrefChanged: boolean - hashChanged: boolean - }) => Array) + | Array + | ((locationChangeInfo: { + fromLocation?: ParsedLocation + toLocation: ParsedLocation + pathChanged: boolean + hrefChanged: boolean + hashChanged: boolean + }) => Array) } export function defaultSerializeError(err: unknown) { @@ -708,7 +708,7 @@ export function defaultSerializeError(err: unknown) { } if (process.env.NODE_ENV === 'development') { - ;(obj as any).stack = err.stack + ; (obj as any).stack = err.stack } return obj @@ -743,12 +743,12 @@ export type CreateRouterFn = < options: undefined extends number ? 'strictNullChecks must be enabled in tsconfig.json' : RouterConstructorOptions< - TRouteTree, - TTrailingSlashOption, - TDefaultStructuralSharingOption, - TRouterHistory, - TDehydrated - >, + TRouteTree, + TTrailingSlashOption, + TDefaultStructuralSharingOption, + TRouterHistory, + TDehydrated + >, ) => RouterCore< TRouteTree, TTrailingSlashOption, @@ -859,11 +859,11 @@ export class RouterCore< this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters ? new Map( - this.options.pathParamsAllowedCharacters.map((char) => [ - encodeURIComponent(char), - char, - ]), - ) + this.options.pathParamsAllowedCharacters.map((char) => [ + encodeURIComponent(char), + char, + ]), + ) : undefined if ( @@ -889,8 +889,8 @@ export class RouterCore< this.options.history ?? ((this.isServer ? createMemoryHistory({ - initialEntries: [this.basepath || '/'], - }) + initialEntries: [this.basepath || '/'], + }) : createBrowserHistory()) as TRouterHistory) this.latestLocation = this.parseLocation() } @@ -1069,7 +1069,7 @@ export class RouterCore< foundRoute ? foundRoute.path !== '/' && routeParams['**'] : // Or if we didn't find a route and we have left over path - trimPathRight(next.pathname) + trimPathRight(next.pathname) ) { // If the user has defined an (old) 404 route, use it if (this.options.notFoundRoute) { @@ -1243,9 +1243,9 @@ export class RouterCore< } else { const status = route.options.loader || - route.options.beforeLoad || - route.lazyFn || - routeNeedsPreload(route) + route.options.beforeLoad || + route.lazyFn || + routeNeedsPreload(route) ? 'pending' : 'success' @@ -1462,9 +1462,9 @@ export class RouterCore< : (dest.params ?? true) === true ? fromParams : { - ...fromParams, - ...functionalUpdate(dest.params as any, fromParams), - } + ...fromParams, + ...functionalUpdate(dest.params as any, fromParams), + } // Interpolate the path first to get the actual resolved path, then match against that const interpolatedNextTo = interpolatePath({ @@ -1653,7 +1653,7 @@ export class RouterCore< '__hashScrollIntoViewOptions', ] as const ignoredProps.forEach((prop) => { - ;(next.state as any)[prop] = this.latestLocation.state[prop] + ; (next.state as any)[prop] = this.latestLocation.state[prop] }) const isEqual = deepEqual(next.state, this.latestLocation.state) ignoredProps.forEach((prop) => { @@ -1767,7 +1767,7 @@ export class RouterCore< try { new URL(`${href}`) reloadDocument = true - } catch {} + } catch { } } if (reloadDocument) { @@ -1922,18 +1922,18 @@ export class RouterCore< this.clearExpiredCache() }) - // - ;( - [ - [exitingMatches, 'onLeave'], - [enteringMatches, 'onEnter'], - [stayingMatches, 'onStay'], - ] as const - ).forEach(([matches, hook]) => { - matches.forEach((match) => { - this.looseRoutesById[match.routeId]!.options[hook]?.(match) + // + ; ( + [ + [exitingMatches, 'onLeave'], + [enteringMatches, 'onEnter'], + [stayingMatches, 'onStay'], + ] as const + ).forEach(([matches, hook]) => { + matches.forEach((match) => { + this.looseRoutesById[match.routeId]!.options[hook]?.(match) + }) }) - }) }) }, }) @@ -2021,11 +2021,11 @@ export class RouterCore< const resolvedViewTransitionTypes = typeof shouldViewTransition.types === 'function' ? shouldViewTransition.types( - getLocationChangeInfo({ - resolvedLocation: prevLocation, - location: next, - }), - ) + getLocationChangeInfo({ + resolvedLocation: prevLocation, + location: next, + }), + ) : shouldViewTransition.types startViewTransitionParams = { @@ -2134,7 +2134,7 @@ export class RouterCore< })) if (!(err as any).routeId) { - ;(err as any).routeId = match.routeId + ; (err as any).routeId = match.routeId } match.loadPromise?.resolve() @@ -2305,7 +2305,7 @@ export class RouterCore< // Update the match and prematurely resolve the loadMatches promise so that // the pending component can start rendering triggerOnReady() - } catch {} + } catch { } }, pendingMs) updateMatch(matchId, (prev) => ({ ...prev, @@ -2581,19 +2581,17 @@ export class RouterCore< })) // Kick off the loader! - if (route.options.loader) { - const loaderData = - await route.options.loader(getLoaderContext()) + const loaderData = + await route.options.loader(getLoaderContext()) - handleRedirectAndNotFound( - this.getMatch(matchId)!, - loaderData, - ) - updateMatch(matchId, (prev) => ({ - ...prev, - loaderData, - })) - } + handleRedirectAndNotFound( + this.getMatch(matchId)!, + loaderData, + ) + updateMatch(matchId, (prev) => ({ + ...prev, + loaderData, + })) // Lazy option can modify the route options, // so we need to wait for it to resolve before @@ -2607,7 +2605,6 @@ export class RouterCore< await route._componentsPromise updateMatch(matchId, (prev) => ({ ...prev, - loaderData: prev.loaderData ?? undefined, // TODO: this is a test, we probably don't need this error: undefined, status: 'success', isFetching: false, @@ -2660,23 +2657,23 @@ export class RouterCore< // Do nothing } else if (loaderShouldRunAsync && !sync) { loaderIsRunningAsync = true - ;(async () => { - try { - await runLoader() - const { loaderPromise, loadPromise } = - this.getMatch(matchId)! - loaderPromise?.resolve() - loadPromise?.resolve() - updateMatch(matchId, (prev) => ({ - ...prev, - loaderPromise: undefined, - })) - } catch (err) { - if (isRedirect(err)) { - await this.navigate(err.options) + ; (async () => { + try { + await runLoader() + const { loaderPromise, loadPromise } = + this.getMatch(matchId)! + loaderPromise?.resolve() + loadPromise?.resolve() + updateMatch(matchId, (prev) => ({ + ...prev, + loaderPromise: undefined, + })) + } catch (err) { + if (isRedirect(err)) { + await this.navigate(err.options) + } } - } - })() + })() } else if ( status !== 'success' || (loaderShouldRunAsync && sync) @@ -2934,9 +2931,9 @@ export class RouterCore< ...location, to: location.to ? this.resolvePathWithBase( - (location.from || '') as string, - location.to as string, - ) + (location.from || '') as string, + location.to as string, + ) : undefined, params: location.params || {}, leaveParams: true, @@ -3057,9 +3054,9 @@ export class RouterCore< } } -export class SearchParamError extends Error {} +export class SearchParamError extends Error { } -export class PathParamError extends Error {} +export class PathParamError extends Error { } const normalize = (str: string) => str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str From 2bee3e930fb02c01e06b883e80558dae1f9ffbd5 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 06:36:46 +0000 Subject: [PATCH 5/5] ci: apply automated fixes --- packages/router-core/src/router.ts | 156 ++++++++++++++--------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 845a22b4dd..187b3c383f 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -108,10 +108,10 @@ export type DefaultRemountDepsFn = ( opts: MakeRemountDepsOptionsUnion, ) => any -export interface DefaultRouterOptionsExtensions { } +export interface DefaultRouterOptionsExtensions {} export interface RouterOptionsExtensions - extends DefaultRouterOptionsExtensions { } + extends DefaultRouterOptionsExtensions {} export interface RouterOptions< TRouteTree extends AnyRoute, @@ -513,12 +513,12 @@ export type InferRouterContext = export type RouterContextOptions = AnyContext extends InferRouterContext - ? { - context?: InferRouterContext - } - : { - context: InferRouterContext - } + ? { + context?: InferRouterContext + } + : { + context: InferRouterContext + } export type RouterConstructorOptions< TRouteTree extends AnyRoute, @@ -690,14 +690,14 @@ export type AnyRouter = RouterCore export interface ViewTransitionOptions { types: - | Array - | ((locationChangeInfo: { - fromLocation?: ParsedLocation - toLocation: ParsedLocation - pathChanged: boolean - hrefChanged: boolean - hashChanged: boolean - }) => Array) + | Array + | ((locationChangeInfo: { + fromLocation?: ParsedLocation + toLocation: ParsedLocation + pathChanged: boolean + hrefChanged: boolean + hashChanged: boolean + }) => Array) } export function defaultSerializeError(err: unknown) { @@ -708,7 +708,7 @@ export function defaultSerializeError(err: unknown) { } if (process.env.NODE_ENV === 'development') { - ; (obj as any).stack = err.stack + ;(obj as any).stack = err.stack } return obj @@ -743,12 +743,12 @@ export type CreateRouterFn = < options: undefined extends number ? 'strictNullChecks must be enabled in tsconfig.json' : RouterConstructorOptions< - TRouteTree, - TTrailingSlashOption, - TDefaultStructuralSharingOption, - TRouterHistory, - TDehydrated - >, + TRouteTree, + TTrailingSlashOption, + TDefaultStructuralSharingOption, + TRouterHistory, + TDehydrated + >, ) => RouterCore< TRouteTree, TTrailingSlashOption, @@ -859,11 +859,11 @@ export class RouterCore< this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters ? new Map( - this.options.pathParamsAllowedCharacters.map((char) => [ - encodeURIComponent(char), - char, - ]), - ) + this.options.pathParamsAllowedCharacters.map((char) => [ + encodeURIComponent(char), + char, + ]), + ) : undefined if ( @@ -889,8 +889,8 @@ export class RouterCore< this.options.history ?? ((this.isServer ? createMemoryHistory({ - initialEntries: [this.basepath || '/'], - }) + initialEntries: [this.basepath || '/'], + }) : createBrowserHistory()) as TRouterHistory) this.latestLocation = this.parseLocation() } @@ -1069,7 +1069,7 @@ export class RouterCore< foundRoute ? foundRoute.path !== '/' && routeParams['**'] : // Or if we didn't find a route and we have left over path - trimPathRight(next.pathname) + trimPathRight(next.pathname) ) { // If the user has defined an (old) 404 route, use it if (this.options.notFoundRoute) { @@ -1243,9 +1243,9 @@ export class RouterCore< } else { const status = route.options.loader || - route.options.beforeLoad || - route.lazyFn || - routeNeedsPreload(route) + route.options.beforeLoad || + route.lazyFn || + routeNeedsPreload(route) ? 'pending' : 'success' @@ -1462,9 +1462,9 @@ export class RouterCore< : (dest.params ?? true) === true ? fromParams : { - ...fromParams, - ...functionalUpdate(dest.params as any, fromParams), - } + ...fromParams, + ...functionalUpdate(dest.params as any, fromParams), + } // Interpolate the path first to get the actual resolved path, then match against that const interpolatedNextTo = interpolatePath({ @@ -1653,7 +1653,7 @@ export class RouterCore< '__hashScrollIntoViewOptions', ] as const ignoredProps.forEach((prop) => { - ; (next.state as any)[prop] = this.latestLocation.state[prop] + ;(next.state as any)[prop] = this.latestLocation.state[prop] }) const isEqual = deepEqual(next.state, this.latestLocation.state) ignoredProps.forEach((prop) => { @@ -1767,7 +1767,7 @@ export class RouterCore< try { new URL(`${href}`) reloadDocument = true - } catch { } + } catch {} } if (reloadDocument) { @@ -1922,18 +1922,18 @@ export class RouterCore< this.clearExpiredCache() }) - // - ; ( - [ - [exitingMatches, 'onLeave'], - [enteringMatches, 'onEnter'], - [stayingMatches, 'onStay'], - ] as const - ).forEach(([matches, hook]) => { - matches.forEach((match) => { - this.looseRoutesById[match.routeId]!.options[hook]?.(match) - }) + // + ;( + [ + [exitingMatches, 'onLeave'], + [enteringMatches, 'onEnter'], + [stayingMatches, 'onStay'], + ] as const + ).forEach(([matches, hook]) => { + matches.forEach((match) => { + this.looseRoutesById[match.routeId]!.options[hook]?.(match) }) + }) }) }, }) @@ -2021,11 +2021,11 @@ export class RouterCore< const resolvedViewTransitionTypes = typeof shouldViewTransition.types === 'function' ? shouldViewTransition.types( - getLocationChangeInfo({ - resolvedLocation: prevLocation, - location: next, - }), - ) + getLocationChangeInfo({ + resolvedLocation: prevLocation, + location: next, + }), + ) : shouldViewTransition.types startViewTransitionParams = { @@ -2134,7 +2134,7 @@ export class RouterCore< })) if (!(err as any).routeId) { - ; (err as any).routeId = match.routeId + ;(err as any).routeId = match.routeId } match.loadPromise?.resolve() @@ -2305,7 +2305,7 @@ export class RouterCore< // Update the match and prematurely resolve the loadMatches promise so that // the pending component can start rendering triggerOnReady() - } catch { } + } catch {} }, pendingMs) updateMatch(matchId, (prev) => ({ ...prev, @@ -2657,23 +2657,23 @@ export class RouterCore< // Do nothing } else if (loaderShouldRunAsync && !sync) { loaderIsRunningAsync = true - ; (async () => { - try { - await runLoader() - const { loaderPromise, loadPromise } = - this.getMatch(matchId)! - loaderPromise?.resolve() - loadPromise?.resolve() - updateMatch(matchId, (prev) => ({ - ...prev, - loaderPromise: undefined, - })) - } catch (err) { - if (isRedirect(err)) { - await this.navigate(err.options) - } + ;(async () => { + try { + await runLoader() + const { loaderPromise, loadPromise } = + this.getMatch(matchId)! + loaderPromise?.resolve() + loadPromise?.resolve() + updateMatch(matchId, (prev) => ({ + ...prev, + loaderPromise: undefined, + })) + } catch (err) { + if (isRedirect(err)) { + await this.navigate(err.options) } - })() + } + })() } else if ( status !== 'success' || (loaderShouldRunAsync && sync) @@ -2931,9 +2931,9 @@ export class RouterCore< ...location, to: location.to ? this.resolvePathWithBase( - (location.from || '') as string, - location.to as string, - ) + (location.from || '') as string, + location.to as string, + ) : undefined, params: location.params || {}, leaveParams: true, @@ -3054,9 +3054,9 @@ export class RouterCore< } } -export class SearchParamError extends Error { } +export class SearchParamError extends Error {} -export class PathParamError extends Error { } +export class PathParamError extends Error {} const normalize = (str: string) => str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str