@tanstack/react-router 1.147.2 → 1.147.3
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.
- package/dist/cjs/HeadContent.cjs +3 -201
- package/dist/cjs/HeadContent.cjs.map +1 -1
- package/dist/cjs/HeadContent.d.cts +0 -6
- package/dist/cjs/HeadContent.dev.cjs +41 -0
- package/dist/cjs/HeadContent.dev.cjs.map +1 -0
- package/dist/cjs/HeadContent.dev.d.cts +10 -0
- package/dist/cjs/headContentUtils.cjs +185 -0
- package/dist/cjs/headContentUtils.cjs.map +1 -0
- package/dist/cjs/headContentUtils.d.cts +7 -0
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/index.dev.cjs +264 -0
- package/dist/cjs/index.dev.cjs.map +1 -0
- package/dist/cjs/index.dev.d.cts +2 -0
- package/dist/esm/HeadContent.d.ts +0 -6
- package/dist/esm/HeadContent.dev.d.ts +10 -0
- package/dist/esm/HeadContent.dev.js +25 -0
- package/dist/esm/HeadContent.dev.js.map +1 -0
- package/dist/esm/HeadContent.js +4 -186
- package/dist/esm/HeadContent.js.map +1 -1
- package/dist/esm/headContentUtils.d.ts +7 -0
- package/dist/esm/headContentUtils.js +168 -0
- package/dist/esm/headContentUtils.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.dev.d.ts +2 -0
- package/dist/esm/index.dev.js +144 -0
- package/dist/esm/index.dev.js.map +1 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/package.json +5 -3
- package/src/HeadContent.dev.tsx +46 -0
- package/src/HeadContent.tsx +1 -245
- package/src/headContentUtils.tsx +217 -0
- package/src/index.dev.tsx +6 -0
- package/src/index.tsx +1 -0
package/src/HeadContent.tsx
CHANGED
|
@@ -1,238 +1,7 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import { buildDevStylesUrl, escapeHtml } from '@tanstack/router-core'
|
|
3
2
|
import { Asset } from './Asset'
|
|
4
3
|
import { useRouter } from './useRouter'
|
|
5
|
-
import {
|
|
6
|
-
import type { RouterManagedTag } from '@tanstack/router-core'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Build the list of head/link/meta/script tags to render for active matches.
|
|
10
|
-
* Used internally by `HeadContent`.
|
|
11
|
-
*/
|
|
12
|
-
export const useTags = () => {
|
|
13
|
-
const router = useRouter()
|
|
14
|
-
const nonce = router.options.ssr?.nonce
|
|
15
|
-
const routeMeta = useRouterState({
|
|
16
|
-
select: (state) => {
|
|
17
|
-
return state.matches.map((match) => match.meta!).filter(Boolean)
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const meta: Array<RouterManagedTag> = React.useMemo(() => {
|
|
22
|
-
const resultMeta: Array<RouterManagedTag> = []
|
|
23
|
-
const metaByAttribute: Record<string, true> = {}
|
|
24
|
-
let title: RouterManagedTag | undefined
|
|
25
|
-
for (let i = routeMeta.length - 1; i >= 0; i--) {
|
|
26
|
-
const metas = routeMeta[i]!
|
|
27
|
-
for (let j = metas.length - 1; j >= 0; j--) {
|
|
28
|
-
const m = metas[j]
|
|
29
|
-
if (!m) continue
|
|
30
|
-
|
|
31
|
-
if (m.title) {
|
|
32
|
-
if (!title) {
|
|
33
|
-
title = {
|
|
34
|
-
tag: 'title',
|
|
35
|
-
children: m.title,
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
} else if ('script:ld+json' in m) {
|
|
39
|
-
// Handle JSON-LD structured data
|
|
40
|
-
// Content is HTML-escaped to prevent XSS when injected via dangerouslySetInnerHTML
|
|
41
|
-
try {
|
|
42
|
-
const json = JSON.stringify(m['script:ld+json'])
|
|
43
|
-
resultMeta.push({
|
|
44
|
-
tag: 'script',
|
|
45
|
-
attrs: {
|
|
46
|
-
type: 'application/ld+json',
|
|
47
|
-
},
|
|
48
|
-
children: escapeHtml(json),
|
|
49
|
-
})
|
|
50
|
-
} catch {
|
|
51
|
-
// Skip invalid JSON-LD objects
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
const attribute = m.name ?? m.property
|
|
55
|
-
if (attribute) {
|
|
56
|
-
if (metaByAttribute[attribute]) {
|
|
57
|
-
continue
|
|
58
|
-
} else {
|
|
59
|
-
metaByAttribute[attribute] = true
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
resultMeta.push({
|
|
64
|
-
tag: 'meta',
|
|
65
|
-
attrs: {
|
|
66
|
-
...m,
|
|
67
|
-
nonce,
|
|
68
|
-
},
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (title) {
|
|
75
|
-
resultMeta.push(title)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (nonce) {
|
|
79
|
-
resultMeta.push({
|
|
80
|
-
tag: 'meta',
|
|
81
|
-
attrs: {
|
|
82
|
-
property: 'csp-nonce',
|
|
83
|
-
content: nonce,
|
|
84
|
-
},
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
resultMeta.reverse()
|
|
88
|
-
|
|
89
|
-
return resultMeta
|
|
90
|
-
}, [routeMeta, nonce])
|
|
91
|
-
|
|
92
|
-
const links = useRouterState({
|
|
93
|
-
select: (state) => {
|
|
94
|
-
const constructed = state.matches
|
|
95
|
-
.map((match) => match.links!)
|
|
96
|
-
.filter(Boolean)
|
|
97
|
-
.flat(1)
|
|
98
|
-
.map((link) => ({
|
|
99
|
-
tag: 'link',
|
|
100
|
-
attrs: {
|
|
101
|
-
...link,
|
|
102
|
-
nonce,
|
|
103
|
-
},
|
|
104
|
-
})) satisfies Array<RouterManagedTag>
|
|
105
|
-
|
|
106
|
-
const manifest = router.ssr?.manifest
|
|
107
|
-
|
|
108
|
-
// These are the assets extracted from the ViteManifest
|
|
109
|
-
// using the `startManifestPlugin`
|
|
110
|
-
const assets = state.matches
|
|
111
|
-
.map((match) => manifest?.routes[match.routeId]?.assets ?? [])
|
|
112
|
-
.filter(Boolean)
|
|
113
|
-
.flat(1)
|
|
114
|
-
.filter((asset) => asset.tag === 'link')
|
|
115
|
-
.map(
|
|
116
|
-
(asset) =>
|
|
117
|
-
({
|
|
118
|
-
tag: 'link',
|
|
119
|
-
attrs: {
|
|
120
|
-
...asset.attrs,
|
|
121
|
-
suppressHydrationWarning: true,
|
|
122
|
-
nonce,
|
|
123
|
-
},
|
|
124
|
-
}) satisfies RouterManagedTag,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
return [...constructed, ...assets]
|
|
128
|
-
},
|
|
129
|
-
structuralSharing: true as any,
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
const preloadLinks = useRouterState({
|
|
133
|
-
select: (state) => {
|
|
134
|
-
const preloadLinks: Array<RouterManagedTag> = []
|
|
135
|
-
|
|
136
|
-
state.matches
|
|
137
|
-
.map((match) => router.looseRoutesById[match.routeId]!)
|
|
138
|
-
.forEach((route) =>
|
|
139
|
-
router.ssr?.manifest?.routes[route.id]?.preloads
|
|
140
|
-
?.filter(Boolean)
|
|
141
|
-
.forEach((preload) => {
|
|
142
|
-
preloadLinks.push({
|
|
143
|
-
tag: 'link',
|
|
144
|
-
attrs: {
|
|
145
|
-
rel: 'modulepreload',
|
|
146
|
-
href: preload,
|
|
147
|
-
nonce,
|
|
148
|
-
},
|
|
149
|
-
})
|
|
150
|
-
}),
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
return preloadLinks
|
|
154
|
-
},
|
|
155
|
-
structuralSharing: true as any,
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
const styles = useRouterState({
|
|
159
|
-
select: (state) =>
|
|
160
|
-
(
|
|
161
|
-
state.matches
|
|
162
|
-
.map((match) => match.styles!)
|
|
163
|
-
.flat(1)
|
|
164
|
-
.filter(Boolean) as Array<RouterManagedTag>
|
|
165
|
-
).map(({ children, ...attrs }) => ({
|
|
166
|
-
tag: 'style',
|
|
167
|
-
attrs,
|
|
168
|
-
children,
|
|
169
|
-
nonce,
|
|
170
|
-
})),
|
|
171
|
-
structuralSharing: true as any,
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const headScripts: Array<RouterManagedTag> = useRouterState({
|
|
175
|
-
select: (state) =>
|
|
176
|
-
(
|
|
177
|
-
state.matches
|
|
178
|
-
.map((match) => match.headScripts!)
|
|
179
|
-
.flat(1)
|
|
180
|
-
.filter(Boolean) as Array<RouterManagedTag>
|
|
181
|
-
).map(({ children, ...script }) => ({
|
|
182
|
-
tag: 'script',
|
|
183
|
-
attrs: {
|
|
184
|
-
...script,
|
|
185
|
-
nonce,
|
|
186
|
-
},
|
|
187
|
-
children,
|
|
188
|
-
})),
|
|
189
|
-
structuralSharing: true as any,
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
return uniqBy(
|
|
193
|
-
[
|
|
194
|
-
...meta,
|
|
195
|
-
...preloadLinks,
|
|
196
|
-
...links,
|
|
197
|
-
...styles,
|
|
198
|
-
...headScripts,
|
|
199
|
-
] as Array<RouterManagedTag>,
|
|
200
|
-
(d) => {
|
|
201
|
-
return JSON.stringify(d)
|
|
202
|
-
},
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Renders a stylesheet link for dev mode CSS collection.
|
|
208
|
-
* On the server, renders the full link with route-scoped CSS URL.
|
|
209
|
-
* On the client, renders the same link to avoid hydration mismatch,
|
|
210
|
-
* then removes it after hydration since Vite's HMR handles CSS updates.
|
|
211
|
-
*/
|
|
212
|
-
function DevStylesLink() {
|
|
213
|
-
const router = useRouter()
|
|
214
|
-
const routeIds = useRouterState({
|
|
215
|
-
select: (state) => state.matches.map((match) => match.routeId),
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
React.useEffect(() => {
|
|
219
|
-
// After hydration, remove the SSR-rendered dev styles link
|
|
220
|
-
document
|
|
221
|
-
.querySelectorAll('[data-tanstack-start-dev-styles]')
|
|
222
|
-
.forEach((el) => el.remove())
|
|
223
|
-
}, [])
|
|
224
|
-
|
|
225
|
-
const href = buildDevStylesUrl(router.basepath, routeIds)
|
|
226
|
-
|
|
227
|
-
return (
|
|
228
|
-
<link
|
|
229
|
-
rel="stylesheet"
|
|
230
|
-
href={href}
|
|
231
|
-
data-tanstack-start-dev-styles
|
|
232
|
-
suppressHydrationWarning
|
|
233
|
-
/>
|
|
234
|
-
)
|
|
235
|
-
}
|
|
4
|
+
import { useTags } from './headContentUtils'
|
|
236
5
|
|
|
237
6
|
/**
|
|
238
7
|
* Render route-managed head tags (title, meta, links, styles, head scripts).
|
|
@@ -245,22 +14,9 @@ export function HeadContent() {
|
|
|
245
14
|
const nonce = router.options.ssr?.nonce
|
|
246
15
|
return (
|
|
247
16
|
<>
|
|
248
|
-
{process.env.NODE_ENV !== 'production' && <DevStylesLink />}
|
|
249
17
|
{tags.map((tag) => (
|
|
250
18
|
<Asset {...tag} key={`tsr-meta-${JSON.stringify(tag)}`} nonce={nonce} />
|
|
251
19
|
))}
|
|
252
20
|
</>
|
|
253
21
|
)
|
|
254
22
|
}
|
|
255
|
-
|
|
256
|
-
function uniqBy<T>(arr: Array<T>, fn: (item: T) => string) {
|
|
257
|
-
const seen = new Set<string>()
|
|
258
|
-
return arr.filter((item) => {
|
|
259
|
-
const key = fn(item)
|
|
260
|
-
if (seen.has(key)) {
|
|
261
|
-
return false
|
|
262
|
-
}
|
|
263
|
-
seen.add(key)
|
|
264
|
-
return true
|
|
265
|
-
})
|
|
266
|
-
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { escapeHtml } from '@tanstack/router-core'
|
|
3
|
+
import { useRouter } from './useRouter'
|
|
4
|
+
import { useRouterState } from './useRouterState'
|
|
5
|
+
import type { RouterManagedTag } from '@tanstack/router-core'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build the list of head/link/meta/script tags to render for active matches.
|
|
9
|
+
* Used internally by `HeadContent`.
|
|
10
|
+
*/
|
|
11
|
+
export const useTags = () => {
|
|
12
|
+
const router = useRouter()
|
|
13
|
+
const nonce = router.options.ssr?.nonce
|
|
14
|
+
const routeMeta = useRouterState({
|
|
15
|
+
select: (state) => {
|
|
16
|
+
return state.matches.map((match) => match.meta!).filter(Boolean)
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const meta: Array<RouterManagedTag> = React.useMemo(() => {
|
|
21
|
+
const resultMeta: Array<RouterManagedTag> = []
|
|
22
|
+
const metaByAttribute: Record<string, true> = {}
|
|
23
|
+
let title: RouterManagedTag | undefined
|
|
24
|
+
for (let i = routeMeta.length - 1; i >= 0; i--) {
|
|
25
|
+
const metas = routeMeta[i]!
|
|
26
|
+
for (let j = metas.length - 1; j >= 0; j--) {
|
|
27
|
+
const m = metas[j]
|
|
28
|
+
if (!m) continue
|
|
29
|
+
|
|
30
|
+
if (m.title) {
|
|
31
|
+
if (!title) {
|
|
32
|
+
title = {
|
|
33
|
+
tag: 'title',
|
|
34
|
+
children: m.title,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else if ('script:ld+json' in m) {
|
|
38
|
+
// Handle JSON-LD structured data
|
|
39
|
+
// Content is HTML-escaped to prevent XSS when injected via dangerouslySetInnerHTML
|
|
40
|
+
try {
|
|
41
|
+
const json = JSON.stringify(m['script:ld+json'])
|
|
42
|
+
resultMeta.push({
|
|
43
|
+
tag: 'script',
|
|
44
|
+
attrs: {
|
|
45
|
+
type: 'application/ld+json',
|
|
46
|
+
},
|
|
47
|
+
children: escapeHtml(json),
|
|
48
|
+
})
|
|
49
|
+
} catch {
|
|
50
|
+
// Skip invalid JSON-LD objects
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
const attribute = m.name ?? m.property
|
|
54
|
+
if (attribute) {
|
|
55
|
+
if (metaByAttribute[attribute]) {
|
|
56
|
+
continue
|
|
57
|
+
} else {
|
|
58
|
+
metaByAttribute[attribute] = true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
resultMeta.push({
|
|
63
|
+
tag: 'meta',
|
|
64
|
+
attrs: {
|
|
65
|
+
...m,
|
|
66
|
+
nonce,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (title) {
|
|
74
|
+
resultMeta.push(title)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (nonce) {
|
|
78
|
+
resultMeta.push({
|
|
79
|
+
tag: 'meta',
|
|
80
|
+
attrs: {
|
|
81
|
+
property: 'csp-nonce',
|
|
82
|
+
content: nonce,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
resultMeta.reverse()
|
|
87
|
+
|
|
88
|
+
return resultMeta
|
|
89
|
+
}, [routeMeta, nonce])
|
|
90
|
+
|
|
91
|
+
const links = useRouterState({
|
|
92
|
+
select: (state) => {
|
|
93
|
+
const constructed = state.matches
|
|
94
|
+
.map((match) => match.links!)
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.flat(1)
|
|
97
|
+
.map((link) => ({
|
|
98
|
+
tag: 'link',
|
|
99
|
+
attrs: {
|
|
100
|
+
...link,
|
|
101
|
+
nonce,
|
|
102
|
+
},
|
|
103
|
+
})) satisfies Array<RouterManagedTag>
|
|
104
|
+
|
|
105
|
+
const manifest = router.ssr?.manifest
|
|
106
|
+
|
|
107
|
+
// These are the assets extracted from the ViteManifest
|
|
108
|
+
// using the `startManifestPlugin`
|
|
109
|
+
const assets = state.matches
|
|
110
|
+
.map((match) => manifest?.routes[match.routeId]?.assets ?? [])
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.flat(1)
|
|
113
|
+
.filter((asset) => asset.tag === 'link')
|
|
114
|
+
.map(
|
|
115
|
+
(asset) =>
|
|
116
|
+
({
|
|
117
|
+
tag: 'link',
|
|
118
|
+
attrs: {
|
|
119
|
+
...asset.attrs,
|
|
120
|
+
suppressHydrationWarning: true,
|
|
121
|
+
nonce,
|
|
122
|
+
},
|
|
123
|
+
}) satisfies RouterManagedTag,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return [...constructed, ...assets]
|
|
127
|
+
},
|
|
128
|
+
structuralSharing: true as any,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const preloadLinks = useRouterState({
|
|
132
|
+
select: (state) => {
|
|
133
|
+
const preloadLinks: Array<RouterManagedTag> = []
|
|
134
|
+
|
|
135
|
+
state.matches
|
|
136
|
+
.map((match) => router.looseRoutesById[match.routeId]!)
|
|
137
|
+
.forEach((route) =>
|
|
138
|
+
router.ssr?.manifest?.routes[route.id]?.preloads
|
|
139
|
+
?.filter(Boolean)
|
|
140
|
+
.forEach((preload) => {
|
|
141
|
+
preloadLinks.push({
|
|
142
|
+
tag: 'link',
|
|
143
|
+
attrs: {
|
|
144
|
+
rel: 'modulepreload',
|
|
145
|
+
href: preload,
|
|
146
|
+
nonce,
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return preloadLinks
|
|
153
|
+
},
|
|
154
|
+
structuralSharing: true as any,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const styles = useRouterState({
|
|
158
|
+
select: (state) =>
|
|
159
|
+
(
|
|
160
|
+
state.matches
|
|
161
|
+
.map((match) => match.styles!)
|
|
162
|
+
.flat(1)
|
|
163
|
+
.filter(Boolean) as Array<RouterManagedTag>
|
|
164
|
+
).map(({ children, ...attrs }) => ({
|
|
165
|
+
tag: 'style',
|
|
166
|
+
attrs: {
|
|
167
|
+
...attrs,
|
|
168
|
+
nonce,
|
|
169
|
+
},
|
|
170
|
+
children,
|
|
171
|
+
})),
|
|
172
|
+
structuralSharing: true as any,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const headScripts: Array<RouterManagedTag> = useRouterState({
|
|
176
|
+
select: (state) =>
|
|
177
|
+
(
|
|
178
|
+
state.matches
|
|
179
|
+
.map((match) => match.headScripts!)
|
|
180
|
+
.flat(1)
|
|
181
|
+
.filter(Boolean) as Array<RouterManagedTag>
|
|
182
|
+
).map(({ children, ...script }) => ({
|
|
183
|
+
tag: 'script',
|
|
184
|
+
attrs: {
|
|
185
|
+
...script,
|
|
186
|
+
nonce,
|
|
187
|
+
},
|
|
188
|
+
children,
|
|
189
|
+
})),
|
|
190
|
+
structuralSharing: true as any,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return uniqBy(
|
|
194
|
+
[
|
|
195
|
+
...meta,
|
|
196
|
+
...preloadLinks,
|
|
197
|
+
...links,
|
|
198
|
+
...styles,
|
|
199
|
+
...headScripts,
|
|
200
|
+
] as Array<RouterManagedTag>,
|
|
201
|
+
(d) => {
|
|
202
|
+
return JSON.stringify(d)
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function uniqBy<T>(arr: Array<T>, fn: (item: T) => string) {
|
|
208
|
+
const seen = new Set<string>()
|
|
209
|
+
return arr.filter((item) => {
|
|
210
|
+
const key = fn(item)
|
|
211
|
+
if (seen.has(key)) {
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
seen.add(key)
|
|
215
|
+
return true
|
|
216
|
+
})
|
|
217
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -343,6 +343,7 @@ export type {
|
|
|
343
343
|
export { ScriptOnce } from './ScriptOnce'
|
|
344
344
|
export { Asset } from './Asset'
|
|
345
345
|
export { HeadContent } from './HeadContent'
|
|
346
|
+
export { useTags } from './headContentUtils'
|
|
346
347
|
export { Scripts } from './Scripts'
|
|
347
348
|
export type * from './ssr/serializer'
|
|
348
349
|
export { composeRewrites } from '@tanstack/router-core'
|