@tanstack/router-devtools 1.112.0 → 1.112.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/cjs/AgeTicker.cjs +58 -0
  2. package/dist/cjs/AgeTicker.cjs.map +1 -0
  3. package/dist/cjs/AgeTicker.d.cts +5 -0
  4. package/dist/cjs/BaseTanStackRouterDevtoolsPanel.cjs +438 -0
  5. package/dist/cjs/BaseTanStackRouterDevtoolsPanel.cjs.map +1 -0
  6. package/dist/cjs/BaseTanStackRouterDevtoolsPanel.d.cts +5 -0
  7. package/dist/cjs/Explorer.cjs +1 -1
  8. package/dist/cjs/Explorer.cjs.map +1 -1
  9. package/dist/cjs/TanStackRouterDevtools.cjs +177 -0
  10. package/dist/cjs/TanStackRouterDevtools.cjs.map +1 -0
  11. package/dist/cjs/{devtools.d.cts → TanStackRouterDevtools.d.cts} +0 -31
  12. package/dist/cjs/TanStackRouterDevtoolsPanel.cjs +23 -0
  13. package/dist/cjs/TanStackRouterDevtoolsPanel.cjs.map +1 -0
  14. package/dist/cjs/TanStackRouterDevtoolsPanel.d.cts +35 -0
  15. package/dist/cjs/context.cjs +1 -1
  16. package/dist/cjs/context.cjs.map +1 -1
  17. package/dist/cjs/index.cjs +4 -3
  18. package/dist/cjs/index.cjs.map +1 -1
  19. package/dist/cjs/index.d.cts +2 -1
  20. package/dist/cjs/useStyles.cjs +570 -0
  21. package/dist/cjs/useStyles.cjs.map +1 -0
  22. package/dist/cjs/useStyles.d.cts +52 -0
  23. package/dist/cjs/utils.cjs.map +1 -1
  24. package/dist/cjs/utils.d.cts +3 -1
  25. package/dist/esm/AgeTicker.d.ts +5 -0
  26. package/dist/esm/AgeTicker.js +58 -0
  27. package/dist/esm/AgeTicker.js.map +1 -0
  28. package/dist/esm/BaseTanStackRouterDevtoolsPanel.d.ts +5 -0
  29. package/dist/esm/BaseTanStackRouterDevtoolsPanel.js +438 -0
  30. package/dist/esm/BaseTanStackRouterDevtoolsPanel.js.map +1 -0
  31. package/dist/esm/Explorer.js +1 -1
  32. package/dist/esm/Explorer.js.map +1 -1
  33. package/dist/esm/{devtools.d.ts → TanStackRouterDevtools.d.ts} +0 -31
  34. package/dist/esm/TanStackRouterDevtools.js +177 -0
  35. package/dist/esm/TanStackRouterDevtools.js.map +1 -0
  36. package/dist/esm/TanStackRouterDevtoolsPanel.d.ts +35 -0
  37. package/dist/esm/TanStackRouterDevtoolsPanel.js +23 -0
  38. package/dist/esm/TanStackRouterDevtoolsPanel.js.map +1 -0
  39. package/dist/esm/context.js +1 -1
  40. package/dist/esm/context.js.map +1 -1
  41. package/dist/esm/index.d.ts +2 -1
  42. package/dist/esm/index.js +2 -1
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/esm/useStyles.d.ts +52 -0
  45. package/dist/esm/useStyles.js +553 -0
  46. package/dist/esm/useStyles.js.map +1 -0
  47. package/dist/esm/utils.d.ts +3 -1
  48. package/dist/esm/utils.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/AgeTicker.tsx +73 -0
  51. package/src/BaseTanStackRouterDevtoolsPanel.tsx +499 -0
  52. package/src/Explorer.tsx +1 -1
  53. package/src/TanStackRouterDevtools.tsx +250 -0
  54. package/src/TanStackRouterDevtoolsPanel.tsx +55 -0
  55. package/src/context.ts +1 -1
  56. package/src/index.tsx +2 -1
  57. package/src/theme.tsx +2 -2
  58. package/src/useStyles.tsx +589 -0
  59. package/src/utils.ts +38 -31
  60. package/dist/cjs/devtools.cjs +0 -1212
  61. package/dist/cjs/devtools.cjs.map +0 -1
  62. package/dist/esm/devtools.js +0 -1195
  63. package/dist/esm/devtools.js.map +0 -1
  64. package/src/devtools.tsx +0 -1443
@@ -0,0 +1,73 @@
1
+ import { clsx as cx } from 'clsx'
2
+ import React from 'react'
3
+ import { useStyles } from './useStyles'
4
+ import type { AnyRouteMatch, AnyRouter } from '@tanstack/react-router'
5
+
6
+ function formatTime(ms: number) {
7
+ const units = ['s', 'min', 'h', 'd']
8
+ const values = [ms / 1000, ms / 60000, ms / 3600000, ms / 86400000]
9
+
10
+ let chosenUnitIndex = 0
11
+ for (let i = 1; i < values.length; i++) {
12
+ if (values[i]! < 1) break
13
+ chosenUnitIndex = i
14
+ }
15
+
16
+ const formatter = new Intl.NumberFormat(navigator.language, {
17
+ compactDisplay: 'short',
18
+ notation: 'compact',
19
+ maximumFractionDigits: 0,
20
+ })
21
+
22
+ return formatter.format(values[chosenUnitIndex]!) + units[chosenUnitIndex]
23
+ }
24
+
25
+ export function AgeTicker({
26
+ match,
27
+ router,
28
+ }: {
29
+ match?: AnyRouteMatch
30
+ router: AnyRouter
31
+ }) {
32
+ const styles = useStyles()
33
+ const rerender = React.useReducer(
34
+ () => ({}),
35
+ () => ({}),
36
+ )[1]
37
+
38
+ React.useEffect(() => {
39
+ const interval = setInterval(() => {
40
+ rerender()
41
+ }, 1000)
42
+
43
+ return () => {
44
+ clearInterval(interval)
45
+ }
46
+ }, [rerender])
47
+
48
+ if (!match) {
49
+ return null
50
+ }
51
+
52
+ const route = router.looseRoutesById[match.routeId]!
53
+
54
+ if (!route.options.loader) {
55
+ return null
56
+ }
57
+
58
+ const age = Date.now() - match.updatedAt
59
+ const staleTime =
60
+ route.options.staleTime ?? router.options.defaultStaleTime ?? 0
61
+ const gcTime =
62
+ route.options.gcTime ?? router.options.defaultGcTime ?? 30 * 60 * 1000
63
+
64
+ return (
65
+ <div className={cx(styles.ageTicker(age > staleTime))}>
66
+ <div>{formatTime(age)}</div>
67
+ <div>/</div>
68
+ <div>{formatTime(staleTime)}</div>
69
+ <div>/</div>
70
+ <div>{formatTime(gcTime)}</div>
71
+ </div>
72
+ )
73
+ }
@@ -0,0 +1,499 @@
1
+ import React from 'react'
2
+ import { clsx as cx } from 'clsx'
3
+ import {
4
+ invariant,
5
+ rootRouteId,
6
+ trimPath,
7
+ useRouter,
8
+ useRouterState,
9
+ } from '@tanstack/react-router'
10
+ import { useDevtoolsOnClose } from './context'
11
+ import { useStyles } from './useStyles'
12
+ import useLocalStorage from './useLocalStorage'
13
+ import Explorer from './Explorer'
14
+ import { getRouteStatusColor, getStatusColor, multiSortBy } from './utils'
15
+ import { AgeTicker } from './AgeTicker'
16
+ import type { DevtoolsPanelOptions } from './TanStackRouterDevtoolsPanel'
17
+
18
+ import type {
19
+ AnyRootRoute,
20
+ AnyRoute,
21
+ AnyRouter,
22
+ Route,
23
+ } from '@tanstack/react-router'
24
+
25
+ function Logo(props: React.HTMLAttributes<HTMLButtonElement>) {
26
+ const { className, ...rest } = props
27
+ const styles = useStyles()
28
+ return (
29
+ <button {...rest} className={cx(styles.logo, className)}>
30
+ <div className={styles.tanstackLogo}>TANSTACK</div>
31
+ <div className={styles.routerLogo}>React Router v1</div>
32
+ </button>
33
+ )
34
+ }
35
+
36
+ function RouteComp({
37
+ router,
38
+ route,
39
+ isRoot,
40
+ activeId,
41
+ setActiveId,
42
+ }: {
43
+ router: AnyRouter
44
+ route: AnyRootRoute | AnyRoute
45
+ isRoot?: boolean
46
+ activeId: string | undefined
47
+ setActiveId: (id: string) => void
48
+ }) {
49
+ const routerState = useRouterState({
50
+ router,
51
+ } as any)
52
+ const styles = useStyles()
53
+ const matches = routerState.pendingMatches || routerState.matches
54
+ const match = routerState.matches.find((d) => d.routeId === route.id)
55
+
56
+ const param = React.useMemo(() => {
57
+ try {
58
+ if (match?.params) {
59
+ const p = match.params
60
+ const r: string = route.path || trimPath(route.id)
61
+ if (r.startsWith('$')) {
62
+ const trimmed = r.slice(1)
63
+ if (p[trimmed]) {
64
+ return `(${p[trimmed]})`
65
+ }
66
+ }
67
+ }
68
+ return ''
69
+ } catch (error) {
70
+ return ''
71
+ }
72
+ }, [match, route])
73
+
74
+ return (
75
+ <div>
76
+ <div
77
+ role="button"
78
+ aria-label={`Open match details for ${route.id}`}
79
+ onClick={() => {
80
+ if (match) {
81
+ setActiveId(activeId === route.id ? '' : route.id)
82
+ }
83
+ }}
84
+ className={cx(
85
+ styles.routesRowContainer(route.id === activeId, !!match),
86
+ )}
87
+ >
88
+ <div
89
+ className={cx(
90
+ styles.matchIndicator(getRouteStatusColor(matches, route)),
91
+ )}
92
+ />
93
+ <div className={cx(styles.routesRow(!!match))}>
94
+ <div>
95
+ <code className={styles.code}>
96
+ {isRoot ? rootRouteId : route.path || trimPath(route.id)}{' '}
97
+ </code>
98
+ <code className={styles.routeParamInfo}>{param}</code>
99
+ </div>
100
+ <AgeTicker match={match} router={router} />
101
+ </div>
102
+ </div>
103
+ {route.children?.length ? (
104
+ <div className={styles.nestedRouteRow(!!isRoot)}>
105
+ {[...(route.children as Array<Route>)]
106
+ .sort((a, b) => {
107
+ return a.rank - b.rank
108
+ })
109
+ .map((r) => (
110
+ <RouteComp
111
+ key={r.id}
112
+ router={router}
113
+ route={r}
114
+ activeId={activeId}
115
+ setActiveId={setActiveId}
116
+ />
117
+ ))}
118
+ </div>
119
+ ) : null}
120
+ </div>
121
+ )
122
+ }
123
+
124
+ export const BaseTanStackRouterDevtoolsPanel =
125
+ function BaseTanStackRouterDevtoolsPanel({
126
+ ref,
127
+ ...props
128
+ }: DevtoolsPanelOptions & {
129
+ ref?: React.RefObject<HTMLDivElement | null>
130
+ }): React.ReactElement {
131
+ const {
132
+ isOpen = true,
133
+ setIsOpen,
134
+ handleDragStart,
135
+ router: userRouter,
136
+ shadowDOMTarget,
137
+ ...panelProps
138
+ } = props
139
+
140
+ const { onCloseClick } = useDevtoolsOnClose()
141
+ const styles = useStyles()
142
+ const { className, ...otherPanelProps } = panelProps
143
+
144
+ const contextRouter = useRouter({ warn: false })
145
+ const router = userRouter ?? contextRouter
146
+ const routerState = useRouterState({
147
+ router,
148
+ } as any)
149
+
150
+ invariant(
151
+ router,
152
+ 'No router was found for the TanStack Router Devtools. Please place the devtools in the <RouterProvider> component tree or pass the router instance to the devtools manually.',
153
+ )
154
+
155
+ // useStore(router.__store)
156
+
157
+ const [showMatches, setShowMatches] = useLocalStorage(
158
+ 'tanstackRouterDevtoolsShowMatches',
159
+ true,
160
+ )
161
+
162
+ const [activeId, setActiveId] = useLocalStorage(
163
+ 'tanstackRouterDevtoolsActiveRouteId',
164
+ '',
165
+ )
166
+
167
+ const activeMatch = React.useMemo(() => {
168
+ const matches = [
169
+ ...(routerState.pendingMatches ?? []),
170
+ ...routerState.matches,
171
+ ...routerState.cachedMatches,
172
+ ]
173
+ return matches.find((d) => d.routeId === activeId || d.id === activeId)
174
+ }, [
175
+ activeId,
176
+ routerState.cachedMatches,
177
+ routerState.matches,
178
+ routerState.pendingMatches,
179
+ ])
180
+
181
+ const hasSearch = Object.keys(routerState.location.search).length
182
+
183
+ const explorerState = {
184
+ ...router,
185
+ state: router.state,
186
+ }
187
+
188
+ return (
189
+ <div
190
+ ref={ref}
191
+ className={cx(
192
+ styles.devtoolsPanel,
193
+ 'TanStackRouterDevtoolsPanel',
194
+ className,
195
+ )}
196
+ {...otherPanelProps}
197
+ >
198
+ {handleDragStart ? (
199
+ <div
200
+ className={styles.dragHandle}
201
+ onMouseDown={handleDragStart}
202
+ ></div>
203
+ ) : null}
204
+ <button
205
+ className={styles.panelCloseBtn}
206
+ onClick={(e) => {
207
+ setIsOpen(false)
208
+ onCloseClick(e)
209
+ }}
210
+ >
211
+ <svg
212
+ xmlns="http://www.w3.org/2000/svg"
213
+ width="10"
214
+ height="6"
215
+ fill="none"
216
+ viewBox="0 0 10 6"
217
+ className={styles.panelCloseBtnIcon}
218
+ >
219
+ <path
220
+ stroke="currentColor"
221
+ strokeLinecap="round"
222
+ strokeLinejoin="round"
223
+ strokeWidth="1.667"
224
+ d="M1 1l4 4 4-4"
225
+ ></path>
226
+ </svg>
227
+ </button>
228
+ <div className={styles.firstContainer}>
229
+ <div className={styles.row}>
230
+ <Logo
231
+ aria-hidden
232
+ onClick={(e) => {
233
+ setIsOpen(false)
234
+ onCloseClick(e)
235
+ }}
236
+ />
237
+ </div>
238
+ <div className={styles.routerExplorerContainer}>
239
+ <div className={styles.routerExplorer}>
240
+ <Explorer
241
+ label="Router"
242
+ value={Object.fromEntries(
243
+ multiSortBy(
244
+ Object.keys(explorerState),
245
+ (
246
+ [
247
+ 'state',
248
+ 'routesById',
249
+ 'routesByPath',
250
+ 'flatRoutes',
251
+ 'options',
252
+ 'manifest',
253
+ ] as const
254
+ ).map((d) => (dd) => dd !== d),
255
+ )
256
+ .map((key) => [key, (explorerState as any)[key]])
257
+ .filter(
258
+ (d) =>
259
+ typeof d[1] !== 'function' &&
260
+ ![
261
+ '__store',
262
+ 'basepath',
263
+ 'injectedHtml',
264
+ 'subscribers',
265
+ 'latestLoadPromise',
266
+ 'navigateTimeout',
267
+ 'resetNextScroll',
268
+ 'tempLocationKey',
269
+ 'latestLocation',
270
+ 'routeTree',
271
+ 'history',
272
+ ].includes(d[0]),
273
+ ),
274
+ )}
275
+ defaultExpanded={{
276
+ state: {} as any,
277
+ context: {} as any,
278
+ options: {} as any,
279
+ }}
280
+ filterSubEntries={(subEntries) => {
281
+ return subEntries.filter((d) => typeof d.value !== 'function')
282
+ }}
283
+ />
284
+ </div>
285
+ </div>
286
+ </div>
287
+ <div className={styles.secondContainer}>
288
+ <div className={styles.matchesContainer}>
289
+ <div className={styles.detailsHeader}>
290
+ <span>Pathname</span>
291
+ {routerState.location.maskedLocation ? (
292
+ <div className={styles.maskedBadgeContainer}>
293
+ <span className={styles.maskedBadge}>masked</span>
294
+ </div>
295
+ ) : null}
296
+ </div>
297
+ <div className={styles.detailsContent}>
298
+ <code>{routerState.location.pathname}</code>
299
+ {routerState.location.maskedLocation ? (
300
+ <code className={styles.maskedLocation}>
301
+ {routerState.location.maskedLocation.pathname}
302
+ </code>
303
+ ) : null}
304
+ </div>
305
+ <div className={styles.detailsHeader}>
306
+ <div className={styles.routeMatchesToggle}>
307
+ <button
308
+ type="button"
309
+ onClick={() => {
310
+ setShowMatches(false)
311
+ }}
312
+ disabled={!showMatches}
313
+ className={cx(
314
+ styles.routeMatchesToggleBtn(!showMatches, true),
315
+ )}
316
+ >
317
+ Routes
318
+ </button>
319
+ <button
320
+ type="button"
321
+ onClick={() => {
322
+ setShowMatches(true)
323
+ }}
324
+ disabled={showMatches}
325
+ className={cx(
326
+ styles.routeMatchesToggleBtn(!!showMatches, false),
327
+ )}
328
+ >
329
+ Matches
330
+ </button>
331
+ </div>
332
+ <div className={styles.detailsHeaderInfo}>
333
+ <div>age / staleTime / gcTime</div>
334
+ </div>
335
+ </div>
336
+ <div className={cx(styles.routesContainer)}>
337
+ {!showMatches ? (
338
+ <RouteComp
339
+ router={router}
340
+ route={router.routeTree}
341
+ isRoot
342
+ activeId={activeId}
343
+ setActiveId={setActiveId}
344
+ />
345
+ ) : (
346
+ <div>
347
+ {(routerState.pendingMatches?.length
348
+ ? routerState.pendingMatches
349
+ : routerState.matches
350
+ ).map((match, i) => {
351
+ return (
352
+ <div
353
+ key={match.id || i}
354
+ role="button"
355
+ aria-label={`Open match details for ${match.id}`}
356
+ onClick={() =>
357
+ setActiveId(activeId === match.id ? '' : match.id)
358
+ }
359
+ className={cx(styles.matchRow(match === activeMatch))}
360
+ >
361
+ <div
362
+ className={cx(
363
+ styles.matchIndicator(getStatusColor(match)),
364
+ )}
365
+ />
366
+
367
+ <code
368
+ className={styles.matchID}
369
+ >{`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`}</code>
370
+ <AgeTicker match={match} router={router} />
371
+ </div>
372
+ )
373
+ })}
374
+ </div>
375
+ )}
376
+ </div>
377
+ </div>
378
+ {routerState.cachedMatches.length ? (
379
+ <div className={styles.cachedMatchesContainer}>
380
+ <div className={styles.detailsHeader}>
381
+ <div>Cached Matches</div>
382
+ <div className={styles.detailsHeaderInfo}>
383
+ age / staleTime / gcTime
384
+ </div>
385
+ </div>
386
+ <div>
387
+ {routerState.cachedMatches.map((match) => {
388
+ return (
389
+ <div
390
+ key={match.id}
391
+ role="button"
392
+ aria-label={`Open match details for ${match.id}`}
393
+ onClick={() =>
394
+ setActiveId(activeId === match.id ? '' : match.id)
395
+ }
396
+ className={cx(styles.matchRow(match === activeMatch))}
397
+ >
398
+ <div
399
+ className={cx(
400
+ styles.matchIndicator(getStatusColor(match)),
401
+ )}
402
+ />
403
+
404
+ <code className={styles.matchID}>{`${match.id}`}</code>
405
+
406
+ <AgeTicker match={match} router={router} />
407
+ </div>
408
+ )
409
+ })}
410
+ </div>
411
+ </div>
412
+ ) : null}
413
+ </div>
414
+ {activeMatch ? (
415
+ <div className={styles.thirdContainer}>
416
+ <div className={styles.detailsHeader}>Match Details</div>
417
+ <div>
418
+ <div className={styles.matchDetails}>
419
+ <div
420
+ className={styles.matchStatus(
421
+ activeMatch.status,
422
+ activeMatch.isFetching,
423
+ )}
424
+ >
425
+ <div>
426
+ {activeMatch.status === 'success' && activeMatch.isFetching
427
+ ? 'fetching'
428
+ : activeMatch.status}
429
+ </div>
430
+ </div>
431
+ <div className={styles.matchDetailsInfoLabel}>
432
+ <div>ID:</div>
433
+ <div className={styles.matchDetailsInfo}>
434
+ <code>{activeMatch.id}</code>
435
+ </div>
436
+ </div>
437
+ <div className={styles.matchDetailsInfoLabel}>
438
+ <div>State:</div>
439
+ <div className={styles.matchDetailsInfo}>
440
+ {routerState.pendingMatches?.find(
441
+ (d) => d.id === activeMatch.id,
442
+ )
443
+ ? 'Pending'
444
+ : routerState.matches.find((d) => d.id === activeMatch.id)
445
+ ? 'Active'
446
+ : 'Cached'}
447
+ </div>
448
+ </div>
449
+ <div className={styles.matchDetailsInfoLabel}>
450
+ <div>Last Updated:</div>
451
+ <div className={styles.matchDetailsInfo}>
452
+ {activeMatch.updatedAt
453
+ ? new Date(activeMatch.updatedAt).toLocaleTimeString()
454
+ : 'N/A'}
455
+ </div>
456
+ </div>
457
+ </div>
458
+ </div>
459
+ {activeMatch.loaderData ? (
460
+ <>
461
+ <div className={styles.detailsHeader}>Loader Data</div>
462
+ <div className={styles.detailsContent}>
463
+ <Explorer
464
+ label="loaderData"
465
+ value={activeMatch.loaderData}
466
+ defaultExpanded={{}}
467
+ />
468
+ </div>
469
+ </>
470
+ ) : null}
471
+ <div className={styles.detailsHeader}>Explorer</div>
472
+ <div className={styles.detailsContent}>
473
+ <Explorer
474
+ label="Match"
475
+ value={activeMatch}
476
+ defaultExpanded={{}}
477
+ />
478
+ </div>
479
+ </div>
480
+ ) : null}
481
+ {hasSearch ? (
482
+ <div className={styles.fourthContainer}>
483
+ <div className={styles.detailsHeader}>Search Params</div>
484
+ <div className={styles.detailsContent}>
485
+ <Explorer
486
+ value={routerState.location.search}
487
+ defaultExpanded={Object.keys(
488
+ routerState.location.search,
489
+ ).reduce((obj: any, next) => {
490
+ obj[next] = {}
491
+ return obj
492
+ }, {})}
493
+ />
494
+ </div>
495
+ </div>
496
+ ) : null}
497
+ </div>
498
+ )
499
+ }
package/src/Explorer.tsx CHANGED
@@ -356,7 +356,7 @@ const stylesFactory = (shadowDOMTarget?: ShadowRoot) => {
356
356
  }
357
357
 
358
358
  function useStyles() {
359
- const shadowDomTarget = React.useContext(ShadowDomTargetContext)
359
+ const shadowDomTarget = React.use(ShadowDomTargetContext)
360
360
  const [_styles] = React.useState(() => stylesFactory(shadowDomTarget))
361
361
  return _styles
362
362
  }