@tanstack/react-router 1.104.0 → 1.105.0

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 (44) hide show
  1. package/dist/cjs/Asset.cjs +41 -0
  2. package/dist/cjs/Asset.cjs.map +1 -0
  3. package/dist/cjs/Asset.d.cts +2 -0
  4. package/dist/cjs/HeadContent.cjs +138 -0
  5. package/dist/cjs/HeadContent.cjs.map +1 -0
  6. package/dist/cjs/HeadContent.d.cts +7 -0
  7. package/dist/cjs/Matches.cjs.map +1 -1
  8. package/dist/cjs/Matches.d.cts +1 -0
  9. package/dist/cjs/Scripts.cjs +50 -0
  10. package/dist/cjs/Scripts.cjs.map +1 -0
  11. package/dist/cjs/Scripts.d.cts +1 -0
  12. package/dist/cjs/index.cjs +6 -0
  13. package/dist/cjs/index.cjs.map +1 -1
  14. package/dist/cjs/index.d.cts +3 -0
  15. package/dist/cjs/route.cjs.map +1 -1
  16. package/dist/cjs/route.d.cts +10 -7
  17. package/dist/cjs/router.cjs +31 -25
  18. package/dist/cjs/router.cjs.map +1 -1
  19. package/dist/esm/Asset.d.ts +2 -0
  20. package/dist/esm/Asset.js +41 -0
  21. package/dist/esm/Asset.js.map +1 -0
  22. package/dist/esm/HeadContent.d.ts +7 -0
  23. package/dist/esm/HeadContent.js +122 -0
  24. package/dist/esm/HeadContent.js.map +1 -0
  25. package/dist/esm/Matches.d.ts +1 -0
  26. package/dist/esm/Matches.js.map +1 -1
  27. package/dist/esm/Scripts.d.ts +1 -0
  28. package/dist/esm/Scripts.js +50 -0
  29. package/dist/esm/Scripts.js.map +1 -0
  30. package/dist/esm/index.d.ts +3 -0
  31. package/dist/esm/index.js +6 -0
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/route.d.ts +10 -7
  34. package/dist/esm/route.js.map +1 -1
  35. package/dist/esm/router.js +31 -25
  36. package/dist/esm/router.js.map +1 -1
  37. package/package.json +3 -3
  38. package/src/Asset.tsx +40 -0
  39. package/src/HeadContent.tsx +151 -0
  40. package/src/Matches.tsx +1 -0
  41. package/src/Scripts.tsx +64 -0
  42. package/src/index.tsx +4 -0
  43. package/src/route.ts +71 -32
  44. package/src/router.ts +28 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
- "version": "1.104.0",
3
+ "version": "1.105.0",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -53,8 +53,8 @@
53
53
  "jsesc": "^3.1.0",
54
54
  "tiny-invariant": "^1.3.3",
55
55
  "tiny-warning": "^1.0.3",
56
- "@tanstack/router-core": "^1.102.5",
57
- "@tanstack/history": "1.99.13"
56
+ "@tanstack/history": "1.99.13",
57
+ "@tanstack/router-core": "^1.104.1"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@testing-library/jest-dom": "^6.6.3",
package/src/Asset.tsx ADDED
@@ -0,0 +1,40 @@
1
+ import type { RouterManagedTag } from '@tanstack/router-core'
2
+
3
+ export function Asset({ tag, attrs, children }: RouterManagedTag): any {
4
+ switch (tag) {
5
+ case 'title':
6
+ return (
7
+ <title {...attrs} suppressHydrationWarning>
8
+ {children}
9
+ </title>
10
+ )
11
+ case 'meta':
12
+ return <meta {...attrs} suppressHydrationWarning />
13
+ case 'link':
14
+ return <link {...attrs} suppressHydrationWarning />
15
+ case 'style':
16
+ return (
17
+ <style
18
+ {...attrs}
19
+ dangerouslySetInnerHTML={{ __html: children as any }}
20
+ />
21
+ )
22
+ case 'script':
23
+ if ((attrs as any) && (attrs as any).src) {
24
+ return <script {...attrs} suppressHydrationWarning />
25
+ }
26
+ if (typeof children === 'string')
27
+ return (
28
+ <script
29
+ {...attrs}
30
+ dangerouslySetInnerHTML={{
31
+ __html: children,
32
+ }}
33
+ suppressHydrationWarning
34
+ />
35
+ )
36
+ return null
37
+ default:
38
+ return null
39
+ }
40
+ }
@@ -0,0 +1,151 @@
1
+ import * as React from 'react'
2
+ import { Asset } from './Asset'
3
+ import { useRouter } from './useRouter'
4
+ import { useRouterState } from './useRouterState'
5
+ import type { RouterManagedTag } from '@tanstack/router-core'
6
+
7
+ export const useTags = () => {
8
+ const router = useRouter()
9
+
10
+ const routeMeta = useRouterState({
11
+ select: (state) => {
12
+ return state.matches.map((match) => match.meta!).filter(Boolean)
13
+ },
14
+ })
15
+
16
+ const meta: Array<RouterManagedTag> = React.useMemo(() => {
17
+ const resultMeta: Array<RouterManagedTag> = []
18
+ const metaByAttribute: Record<string, true> = {}
19
+ let title: RouterManagedTag | undefined
20
+ ;[...routeMeta].reverse().forEach((metas) => {
21
+ ;[...metas].reverse().forEach((m) => {
22
+ if (!m) return
23
+
24
+ if (m.title) {
25
+ if (!title) {
26
+ title = {
27
+ tag: 'title',
28
+ children: m.title,
29
+ }
30
+ }
31
+ } else {
32
+ const attribute = m.name ?? m.property
33
+ if (attribute) {
34
+ if (metaByAttribute[attribute]) {
35
+ return
36
+ } else {
37
+ metaByAttribute[attribute] = true
38
+ }
39
+ }
40
+
41
+ resultMeta.push({
42
+ tag: 'meta',
43
+ attrs: {
44
+ ...m,
45
+ },
46
+ })
47
+ }
48
+ })
49
+ })
50
+
51
+ if (title) {
52
+ resultMeta.push(title)
53
+ }
54
+
55
+ resultMeta.reverse()
56
+
57
+ return resultMeta
58
+ }, [routeMeta])
59
+
60
+ const links = useRouterState({
61
+ select: (state) =>
62
+ state.matches
63
+ .map((match) => match.links!)
64
+ .filter(Boolean)
65
+ .flat(1)
66
+ .map((link) => ({
67
+ tag: 'link',
68
+ attrs: {
69
+ ...link,
70
+ },
71
+ })) as Array<RouterManagedTag>,
72
+ structuralSharing: true as any,
73
+ })
74
+
75
+ const preloadMeta = useRouterState({
76
+ select: (state) => {
77
+ const preloadMeta: Array<RouterManagedTag> = []
78
+
79
+ state.matches
80
+ .map((match) => router.looseRoutesById[match.routeId]!)
81
+ .forEach((route) =>
82
+ router.ssr?.manifest?.routes[route.id]?.preloads
83
+ ?.filter(Boolean)
84
+ .forEach((preload) => {
85
+ preloadMeta.push({
86
+ tag: 'link',
87
+ attrs: {
88
+ rel: 'modulepreload',
89
+ href: preload,
90
+ },
91
+ })
92
+ }),
93
+ )
94
+
95
+ return preloadMeta
96
+ },
97
+ structuralSharing: true as any,
98
+ })
99
+
100
+ const headScripts = useRouterState({
101
+ select: (state) =>
102
+ (
103
+ state.matches
104
+ .map((match) => match.headScripts!)
105
+ .flat(1)
106
+ .filter(Boolean) as Array<RouterManagedTag>
107
+ ).map(({ children, ...script }) => ({
108
+ tag: 'script',
109
+ attrs: {
110
+ ...script,
111
+ },
112
+ children,
113
+ })),
114
+ structuralSharing: true as any,
115
+ })
116
+
117
+ return uniqBy(
118
+ [
119
+ ...meta,
120
+ ...preloadMeta,
121
+ ...links,
122
+ ...headScripts,
123
+ ] as Array<RouterManagedTag>,
124
+ (d) => {
125
+ return JSON.stringify(d)
126
+ },
127
+ )
128
+ }
129
+
130
+ /**
131
+ * @description The `HeadContent` component is used to render meta tags, links, and scripts for the current route.
132
+ * It should be rendered in the `<head>` of your document.
133
+ */
134
+ export function HeadContent() {
135
+ const tags = useTags()
136
+ return tags.map((tag) => (
137
+ <Asset {...tag} key={`tsr-meta-${JSON.stringify(tag)}`} />
138
+ ))
139
+ }
140
+
141
+ function uniqBy<T>(arr: Array<T>, fn: (item: T) => string) {
142
+ const seen = new Set<string>()
143
+ return arr.filter((item) => {
144
+ const key = fn(item)
145
+ if (seen.has(key)) {
146
+ return false
147
+ }
148
+ seen.add(key)
149
+ return true
150
+ })
151
+ }
package/src/Matches.tsx CHANGED
@@ -88,6 +88,7 @@ export interface RouteMatch<
88
88
  meta?: Array<React.JSX.IntrinsicElements['meta'] | undefined>
89
89
  links?: Array<React.JSX.IntrinsicElements['link'] | undefined>
90
90
  scripts?: Array<React.JSX.IntrinsicElements['script'] | undefined>
91
+ headScripts?: Array<React.JSX.IntrinsicElements['script'] | undefined>
91
92
  headers?: Record<string, string>
92
93
  globalNotFound?: boolean
93
94
  staticData: StaticDataRouteOption
@@ -0,0 +1,64 @@
1
+ import { Asset } from './Asset'
2
+ import { useRouterState } from './useRouterState'
3
+ import { useRouter } from './useRouter'
4
+ import type { RouterManagedTag } from '@tanstack/router-core'
5
+
6
+ export const Scripts = () => {
7
+ const router = useRouter()
8
+
9
+ const assetScripts = useRouterState({
10
+ select: (state) => {
11
+ const assetScripts: Array<RouterManagedTag> = []
12
+ const manifest = router.ssr?.manifest
13
+
14
+ if (!manifest) {
15
+ return []
16
+ }
17
+
18
+ state.matches
19
+ .map((match) => router.looseRoutesById[match.routeId]!)
20
+ .forEach((route) =>
21
+ manifest.routes[route.id]?.assets
22
+ ?.filter((d) => d.tag === 'script')
23
+ .forEach((asset) => {
24
+ assetScripts.push({
25
+ tag: 'script',
26
+ attrs: asset.attrs,
27
+ children: asset.children,
28
+ } as any)
29
+ }),
30
+ )
31
+
32
+ return assetScripts
33
+ },
34
+ structuralSharing: true as any,
35
+ })
36
+
37
+ const { scripts } = useRouterState({
38
+ select: (state) => ({
39
+ scripts: (
40
+ state.matches
41
+ .map((match) => match.scripts!)
42
+ .flat(1)
43
+ .filter(Boolean) as Array<RouterManagedTag>
44
+ ).map(({ children, ...script }) => ({
45
+ tag: 'script',
46
+ attrs: {
47
+ ...script,
48
+ suppressHydrationWarning: true,
49
+ },
50
+ children,
51
+ })),
52
+ }),
53
+ })
54
+
55
+ const allScripts = [...scripts, ...assetScripts] as Array<RouterManagedTag>
56
+
57
+ return (
58
+ <>
59
+ {allScripts.map((asset, i) => (
60
+ <Asset {...asset} key={`tsr-scripts-${asset.tag}-${i}`} />
61
+ ))}
62
+ </>
63
+ )
64
+ }
package/src/index.tsx CHANGED
@@ -356,3 +356,7 @@ export type { NotFoundError } from './not-found'
356
356
  export * from './typePrimitives'
357
357
 
358
358
  export { ScriptOnce } from './ScriptOnce'
359
+
360
+ export { Asset } from './Asset'
361
+ export { HeadContent } from './HeadContent'
362
+ export { Scripts } from './Scripts'
package/src/route.ts CHANGED
@@ -325,6 +325,51 @@ export interface BeforeLoadContextOptions<
325
325
  >
326
326
  }
327
327
 
328
+ type AssetFnContextOptions<
329
+ in out TRouteId,
330
+ in out TFullPath,
331
+ in out TParentRoute extends AnyRoute,
332
+ in out TParams,
333
+ in out TSearchValidator,
334
+ in out TLoaderFn,
335
+ in out TRouterContext,
336
+ in out TRouteContextFn,
337
+ in out TBeforeLoadFn,
338
+ in out TLoaderDeps,
339
+ > = {
340
+ matches: Array<
341
+ RouteMatch<
342
+ TRouteId,
343
+ TFullPath,
344
+ ResolveAllParamsFromParent<TParentRoute, TParams>,
345
+ ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
346
+ ResolveLoaderData<TLoaderFn>,
347
+ ResolveAllContext<
348
+ TParentRoute,
349
+ TRouterContext,
350
+ TRouteContextFn,
351
+ TBeforeLoadFn
352
+ >,
353
+ TLoaderDeps
354
+ >
355
+ >
356
+ match: RouteMatch<
357
+ TRouteId,
358
+ TFullPath,
359
+ ResolveAllParamsFromParent<TParentRoute, TParams>,
360
+ ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
361
+ ResolveLoaderData<TLoaderFn>,
362
+ ResolveAllContext<
363
+ TParentRoute,
364
+ TRouterContext,
365
+ TRouteContextFn,
366
+ TBeforeLoadFn
367
+ >,
368
+ TLoaderDeps
369
+ >
370
+ params: ResolveAllParamsFromParent<TParentRoute, TParams>
371
+ loaderData: ResolveLoaderData<TLoaderFn>
372
+ }
328
373
  export interface UpdatableRouteOptions<
329
374
  in out TParentRoute extends AnyRoute,
330
375
  in out TRouteId,
@@ -427,44 +472,38 @@ export interface UpdatableRouteOptions<
427
472
  headers?: (ctx: {
428
473
  loaderData: ResolveLoaderData<TLoaderFn>
429
474
  }) => Record<string, string>
430
- head?: (ctx: {
431
- matches: Array<
432
- RouteMatch<
433
- TRouteId,
434
- TFullPath,
435
- ResolveAllParamsFromParent<TParentRoute, TParams>,
436
- ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
437
- ResolveLoaderData<TLoaderFn>,
438
- ResolveAllContext<
439
- TParentRoute,
440
- TRouterContext,
441
- TRouteContextFn,
442
- TBeforeLoadFn
443
- >,
444
- TLoaderDeps
445
- >
446
- >
447
- match: RouteMatch<
475
+ head?: (
476
+ ctx: AssetFnContextOptions<
448
477
  TRouteId,
449
478
  TFullPath,
450
- ResolveAllParamsFromParent<TParentRoute, TParams>,
451
- ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
452
- ResolveLoaderData<TLoaderFn>,
453
- ResolveAllContext<
454
- TParentRoute,
455
- TRouterContext,
456
- TRouteContextFn,
457
- TBeforeLoadFn
458
- >,
479
+ TParentRoute,
480
+ TParams,
481
+ TSearchValidator,
482
+ TLoaderFn,
483
+ TRouterContext,
484
+ TRouteContextFn,
485
+ TBeforeLoadFn,
459
486
  TLoaderDeps
460
- >
461
- params: ResolveAllParamsFromParent<TParentRoute, TParams>
462
- loaderData: ResolveLoaderData<TLoaderFn> | undefined
463
- }) => {
487
+ >,
488
+ ) => {
464
489
  links?: AnyRouteMatch['links']
465
- scripts?: AnyRouteMatch['scripts']
490
+ scripts?: AnyRouteMatch['headScripts']
466
491
  meta?: AnyRouteMatch['meta']
467
492
  }
493
+ scripts?: (
494
+ ctx: AssetFnContextOptions<
495
+ TRouteId,
496
+ TFullPath,
497
+ TParentRoute,
498
+ TParams,
499
+ TSearchValidator,
500
+ TLoaderFn,
501
+ TRouterContext,
502
+ TRouteContextFn,
503
+ TBeforeLoadFn,
504
+ TLoaderDeps
505
+ >,
506
+ ) => AnyRouteMatch['scripts']
468
507
  ssr?: boolean
469
508
  codeSplitGroupings?: Array<
470
509
  Array<
package/src/router.ts CHANGED
@@ -1311,6 +1311,7 @@ export class Router<
1311
1311
  preload: false,
1312
1312
  links: undefined,
1313
1313
  scripts: undefined,
1314
+ headScripts: undefined,
1314
1315
  meta: undefined,
1315
1316
  staticData: route.options.staticData || {},
1316
1317
  loadPromise: createControlledPromise(),
@@ -1318,15 +1319,6 @@ export class Router<
1318
1319
  }
1319
1320
  }
1320
1321
 
1321
- // If it's already a success, update the headers
1322
- // These may get updated again if the match is refreshed
1323
- // due to being stale
1324
- if (match.status === 'success') {
1325
- match.headers = route.options.headers?.({
1326
- loaderData: match.loaderData,
1327
- })
1328
- }
1329
-
1330
1322
  if (!opts?.preload) {
1331
1323
  // If we have a global not found, mark the right match as global not found
1332
1324
  match.globalNotFound = globalNotFoundRouteId === route.id
@@ -1380,16 +1372,25 @@ export class Router<
1380
1372
  }
1381
1373
  }
1382
1374
 
1383
- const headFnContent = route.options.head?.({
1384
- matches,
1385
- match,
1386
- params: match.params,
1387
- loaderData: match.loaderData ?? undefined,
1388
- })
1389
-
1390
- match.links = headFnContent?.links
1391
- match.scripts = headFnContent?.scripts
1392
- match.meta = headFnContent?.meta
1375
+ // If it's already a success, update headers and head content
1376
+ // These may get updated again if the match is refreshed
1377
+ // due to being stale
1378
+ if (match.status === 'success') {
1379
+ match.headers = route.options.headers?.({
1380
+ loaderData: match.loaderData,
1381
+ })
1382
+ const assetContext = {
1383
+ matches,
1384
+ match,
1385
+ params: match.params,
1386
+ loaderData: match.loaderData,
1387
+ }
1388
+ const headFnContent = route.options.head?.(assetContext)
1389
+ match.links = headFnContent?.links
1390
+ match.headScripts = headFnContent?.scripts
1391
+ match.meta = headFnContent?.meta
1392
+ match.scripts = route.options.scripts?.(assetContext)
1393
+ }
1393
1394
  })
1394
1395
 
1395
1396
  return matches
@@ -2558,16 +2559,19 @@ export class Router<
2558
2559
 
2559
2560
  await potentialPendingMinPromise()
2560
2561
 
2561
- const headFnContent = route.options.head?.({
2562
+ const assetContext = {
2562
2563
  matches,
2563
2564
  match: this.getMatch(matchId)!,
2564
2565
  params: this.getMatch(matchId)!.params,
2565
2566
  loaderData,
2566
- })
2567
+ }
2568
+ const headFnContent =
2569
+ route.options.head?.(assetContext)
2567
2570
  const meta = headFnContent?.meta
2568
2571
  const links = headFnContent?.links
2569
- const scripts = headFnContent?.scripts
2572
+ const headScripts = headFnContent?.scripts
2570
2573
 
2574
+ const scripts = route.options.scripts?.(assetContext)
2571
2575
  const headers = route.options.headers?.({
2572
2576
  loaderData,
2573
2577
  })
@@ -2581,8 +2585,9 @@ export class Router<
2581
2585
  loaderData,
2582
2586
  meta,
2583
2587
  links,
2584
- scripts,
2588
+ headScripts,
2585
2589
  headers,
2590
+ scripts,
2586
2591
  }))
2587
2592
  } catch (e) {
2588
2593
  let error = e