@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.
@@ -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 { useRouterState } from './useRouterState'
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
+ }
@@ -0,0 +1,6 @@
1
+ // Development entry point - re-exports everything from index.tsx
2
+ // but overrides HeadContent with the dev version that handles
3
+ // dev styles cleanup after hydration
4
+
5
+ export * from './index'
6
+ export { HeadContent } from './HeadContent.dev'
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'