import { clsx as cx } from 'clsx' import { interpolatePath, rootRouteId, trimPath } from '@tanstack/router-core' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, untrack, } from 'solid-js' import { useDevtoolsOnClose } from './context' import { useStyles } from './useStyles' import useLocalStorage from './useLocalStorage' import { Explorer } from './Explorer' import { getRouteStatusColor, getStatusColor, multiSortBy } from './utils' import { AgeTicker } from './AgeTicker' // import type { DevtoolsPanelOptions } from './TanStackRouterDevtoolsPanel' import { NavigateButton } from './NavigateButton' import type { AnyContext, AnyRoute, AnyRouteMatch, AnyRouter, FileRouteTypes, MakeRouteMatchUnion, Route, RouterState, } from '@tanstack/router-core' import type { Accessor, JSX } from 'solid-js' export interface BaseDevtoolsPanelOptions { /** * The standard React style object used to style a component with inline styles */ style?: Accessor /** * The standard React class property used to style a component with classes */ className?: Accessor /** * A boolean variable indicating whether the panel is open or closed */ isOpen?: boolean /** * A function that toggles the open and close state of the panel */ setIsOpen?: (isOpen: boolean) => void /** * Handles the opening and closing the devtools panel */ handleDragStart?: (e: any) => void /** * A boolean variable indicating if the "lite" version of the library is being used */ router: Accessor routerState: Accessor /** * Use this to attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot } const HISTORY_LIMIT = 15 function Logo(props: any) { const { className, ...rest } = props const styles = useStyles() return ( ) } function NavigateLink(props: { class?: string left?: JSX.Element children?: JSX.Element right?: JSX.Element }) { return (
{props.left}
{props.children}
{props.right}
) } function RouteComp({ routerState, pendingMatches, router, route, isRoot, activeId, setActiveId, }: { routerState: Accessor< RouterState< Route< any, any, any, '/', '/', string, '__root__', undefined, {}, {}, AnyContext, AnyContext, {}, undefined, any, FileRouteTypes, unknown, undefined >, MakeRouteMatchUnion > > pendingMatches: Accessor> router: Accessor route: AnyRoute isRoot?: boolean activeId: Accessor setActiveId: (id: string) => void }) { const styles = useStyles() const matches = createMemo(() => pendingMatches().length ? pendingMatches() : routerState().matches, ) const match = createMemo(() => routerState().matches.find((d) => d.routeId === route.id), ) const param = createMemo(() => { try { if (match()?.params) { const p = match()?.params const r: string = route.path || trimPath(route.id) if (r.startsWith('$')) { const trimmed = r.slice(1) if (p[trimmed]) { return `(${p[trimmed]})` } } } return '' } catch (error) { return '' } }) const navigationTarget = createMemo(() => { if (isRoot) return undefined // rootRouteId has no path if (!route.path) return undefined // no path to navigate to // flatten all params in the router state, into a single object const allParams = Object.assign({}, ...matches().map((m) => m.params)) // interpolatePath is used by router-core to generate the `to` // path for the navigate function in the router const interpolated = interpolatePath({ path: route.fullPath, params: allParams, decoder: router().pathParamsDecoder, }) // only if `interpolated` is not missing params, return the path since this // means that all the params are present for a successful navigation return !interpolated.isMissingParams ? interpolated.interpolatedPath : undefined }) return (
{ if (match()) { setActiveId(activeId() === route.id ? '' : route.id) } }} class={cx( styles().routesRowContainer(route.id === activeId(), !!match()), )} >
{(navigate) => } } right={} > {isRoot ? rootRouteId : route.path || trimPath(route.id)}{' '} {param()}
{route.children?.length ? (
{[...(route.children as Array)] .sort((a, b) => { return a.rank - b.rank }) .map((r) => ( ))}
) : null}
) } export const BaseTanStackRouterDevtoolsPanel = function BaseTanStackRouterDevtoolsPanel({ ...props }: BaseDevtoolsPanelOptions): JSX.Element { const { isOpen = true, setIsOpen, handleDragStart, router, routerState, shadowDOMTarget, ...panelProps } = props const { onCloseClick } = useDevtoolsOnClose() const styles = useStyles() const { className, style, ...otherPanelProps } = panelProps // useStore(router.stores.__store) const [currentTab, setCurrentTab] = useLocalStorage< 'routes' | 'matches' | 'history' >('tanstackRouterDevtoolsActiveTab', 'routes') const [activeId, setActiveId] = useLocalStorage( 'tanstackRouterDevtoolsActiveRouteId', '', ) const [history, setHistory] = createSignal>([]) const [hasHistoryOverflowed, setHasHistoryOverflowed] = createSignal(false) let pendingMatches: Accessor> let cachedMatches: Accessor> // subscribable implementation if ('subscribe' in router().stores.pendingMatches) { const [_pendingMatches, setPending] = createSignal>( [], ) pendingMatches = _pendingMatches const [_cachedMatches, setCached] = createSignal>([]) cachedMatches = _cachedMatches type Subscribe = (fn: () => void) => { unsubscribe: () => void } createEffect(() => { const pendingMatchesStore = router().stores.pendingMatches setPending(pendingMatchesStore.get()) const subscription = ( (pendingMatchesStore as any).subscribe as Subscribe )(() => { setPending(pendingMatchesStore.get()) }) onCleanup(() => subscription.unsubscribe()) }) createEffect(() => { const cachedMatchesStore = router().stores.cachedMatches setCached(cachedMatchesStore.get()) const subscription = ( (cachedMatchesStore as any).subscribe as Subscribe )(() => { setCached(cachedMatchesStore.get()) }) onCleanup(() => subscription.unsubscribe()) }) } // signal implementation else { pendingMatches = () => router().stores.pendingMatches.get() cachedMatches = () => router().stores.cachedMatches.get() } createEffect(() => { const matches = routerState().matches const currentMatch = matches[matches.length - 1] if (!currentMatch) { return } // Read history WITHOUT tracking it to avoid infinite loops const historyUntracked = untrack(() => history()) const lastMatch = historyUntracked[0] const sameLocation = lastMatch && lastMatch.pathname === currentMatch.pathname && JSON.stringify(lastMatch.search ?? {}) === JSON.stringify(currentMatch.search ?? {}) if (!lastMatch || !sameLocation) { if (historyUntracked.length >= HISTORY_LIMIT) { setHasHistoryOverflowed(true) } setHistory((prev) => { const newHistory = [currentMatch, ...prev] // truncate to ensure we don't overflow too much the ui newHistory.splice(HISTORY_LIMIT) return newHistory }) } }) const activeMatch = createMemo(() => { const matches = [ ...pendingMatches(), ...routerState().matches, ...cachedMatches(), ] return matches.find( (d) => d.routeId === activeId() || d.id === activeId(), ) }) const hasSearch = createMemo( () => Object.keys(routerState().location.search).length, ) const explorerState = createMemo(() => { return { ...router(), state: routerState(), } }) const routerExplorerValue = createMemo(() => Object.fromEntries( multiSortBy( Object.keys(explorerState()), ( [ 'state', 'routesById', 'routesByPath', 'options', 'manifest', ] as const ).map((d) => (dd) => dd !== d), ) .map((key) => [key, (explorerState() as any)[key]]) .filter( (d) => typeof d[1] !== 'function' && ![ 'stores', 'basepath', 'injectedHtml', 'subscribers', 'latestLoadPromise', 'navigateTimeout', 'resetNextScroll', 'tempLocationKey', 'latestLocation', 'routeTree', 'history', ].includes(d[0]), ), ), ) const activeMatchLoaderData = createMemo(() => activeMatch()?.loaderData) const activeMatchValue = createMemo(() => activeMatch()) const locationSearchValue = createMemo(() => routerState().location.search) return (
{handleDragStart ? (
) : null}
{ if (setIsOpen) { setIsOpen(false) } onCloseClick(e) }} />
{ return subEntries.filter( (d: any) => typeof d.value() !== 'function', ) }} />
Pathname {routerState().location.maskedLocation ? (
masked
) : null}
{routerState().location.pathname} {routerState().location.maskedLocation ? ( {routerState().location.maskedLocation?.pathname} ) : null}
age / staleTime / gcTime
{(pendingMatches().length ? pendingMatches() : routerState().matches ).map((match: any, _i: any) => { return (
setActiveId(activeId() === match.id ? '' : match.id) } class={cx(styles().matchRow(match === activeMatch()))} >
} right={} > {`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`}
) })}
    {(match, index) => (
  • } right={ } > {`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`}
  • )}
    {hasHistoryOverflowed() ? (
  • This panel displays the most recent {HISTORY_LIMIT}{' '} navigations.
  • ) : null}
{cachedMatches().length ? (
Cached Matches
age / staleTime / gcTime
{cachedMatches().map((match: any) => { return (
setActiveId(activeId() === match.id ? '' : match.id) } class={cx(styles().matchRow(match === activeMatch()))} >
} right={} > {`${match.id}`}
) })}
) : null}
{activeMatch() && activeMatch()?.status ? (
Match Details
{activeMatch()?.status === 'success' && activeMatch()?.isFetching ? 'fetching' : activeMatch()?.status}
ID:
{activeMatch()?.id}
State:
{pendingMatches().find((d) => d.id === activeMatch()?.id) ? 'Pending' : routerState().matches.find( (d: any) => d.id === activeMatch()?.id, ) ? 'Active' : 'Cached'}
Last Updated:
{activeMatch()?.updatedAt ? new Date(activeMatch()?.updatedAt).toLocaleTimeString() : 'N/A'}
{activeMatchLoaderData() ? ( <>
Loader Data
) : null}
Explorer
) : null} {hasSearch() ? (
Search Params {typeof navigator !== 'undefined' ? ( { const search = routerState().location.search return JSON.stringify(search) }} /> ) : null}
{ obj[next] = {} return obj }, {})} />
) : null}
) } function CopyButton({ getValue }: { getValue: () => string }) { const [copied, setCopied] = createSignal(false) let timeoutId: ReturnType | null = null const handleCopy = async () => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) { console.warn('TanStack Router Devtools: Clipboard API unavailable') return } try { const value = getValue() await navigator.clipboard.writeText(value) setCopied(true) if (timeoutId) clearTimeout(timeoutId) timeoutId = setTimeout(() => setCopied(false), 2500) } catch (e) { console.error('TanStack Router Devtools: Failed to copy', e) } } onCleanup(() => { if (timeoutId) clearTimeout(timeoutId) }) return ( ) } export default BaseTanStackRouterDevtoolsPanel