Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions packages/react-router/src/Asset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@ export function Asset({
/>
)
case 'script':
return (
<Script attrs={attrs} nonce={nonce}>
{children}
</Script>
)
return <Script attrs={attrs}>{children}</Script>
default:
return null
}
Expand All @@ -47,11 +43,9 @@ export function Asset({
function Script({
attrs,
children,
nonce,
}: {
attrs?: ScriptAttrs
children?: string
nonce?: string
}) {
const router = useRouter()

Expand Down Expand Up @@ -154,7 +148,7 @@ function Script({
}

if (attrs?.src && typeof attrs.src === 'string') {
return <script {...attrs} suppressHydrationWarning nonce={nonce} />
return <script {...attrs} suppressHydrationWarning />
}

if (typeof children === 'string') {
Expand All @@ -163,7 +157,6 @@ function Script({
{...attrs}
dangerouslySetInnerHTML={{ __html: children }}
suppressHydrationWarning
nonce={nonce}
/>
)
}
Expand Down
17 changes: 16 additions & 1 deletion packages/react-router/src/HeadContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { RouterManagedTag } from '@tanstack/router-core'

export const useTags = () => {
const router = useRouter()

const nonce = router.options.ssr?.nonce
const routeMeta = useRouterState({
select: (state) => {
return state.matches.map((match) => match.meta!).filter(Boolean)
Expand Down Expand Up @@ -44,6 +44,7 @@ export const useTags = () => {
tag: 'meta',
attrs: {
...m,
nonce,
},
})
}
Expand All @@ -54,6 +55,15 @@ export const useTags = () => {
resultMeta.push(title)
}

if (router.options.ssr?.nonce) {
resultMeta.push({
tag: 'meta',
attrs: {
property: 'csp-nonce',
content: router.options.ssr.nonce,
},
})
}
resultMeta.reverse()

return resultMeta
Expand All @@ -69,6 +79,7 @@ export const useTags = () => {
tag: 'link',
attrs: {
...link,
nonce,
},
})) satisfies Array<RouterManagedTag>

Expand All @@ -88,6 +99,7 @@ export const useTags = () => {
attrs: {
...asset.attrs,
suppressHydrationWarning: true,
nonce,
},
}) satisfies RouterManagedTag,
)
Expand All @@ -112,6 +124,7 @@ export const useTags = () => {
attrs: {
rel: 'modulepreload',
href: preload,
nonce,
},
})
}),
Expand All @@ -133,6 +146,7 @@ export const useTags = () => {
tag: 'style',
attrs,
children,
nonce,
})),
structuralSharing: true as any,
})
Expand All @@ -148,6 +162,7 @@ export const useTags = () => {
tag: 'script',
attrs: {
...script,
nonce,
},
children,
})),
Expand Down
11 changes: 4 additions & 7 deletions packages/react-router/src/Scripts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { RouterManagedTag } from '@tanstack/router-core'

export const Scripts = () => {
const router = useRouter()

const nonce = router.options.ssr?.nonce
const assetScripts = useRouterState({
select: (state) => {
const assetScripts: Array<RouterManagedTag> = []
Expand All @@ -23,7 +23,7 @@ export const Scripts = () => {
.forEach((asset) => {
assetScripts.push({
tag: 'script',
attrs: asset.attrs,
attrs: { ...asset.attrs, nonce },
children: asset.children,
} as any)
}),
Expand All @@ -46,6 +46,7 @@ export const Scripts = () => {
attrs: {
...script,
suppressHydrationWarning: true,
nonce,
},
children,
})),
Expand All @@ -58,11 +59,7 @@ export const Scripts = () => {
return (
<>
{allScripts.map((asset, i) => (
<Asset
{...asset}
key={`tsr-scripts-${asset.tag}-${i}`}
nonce={router.options.ssr?.nonce}
/>
<Asset {...asset} key={`tsr-scripts-${asset.tag}-${i}`} />
))}
</>
)
Expand Down
7 changes: 7 additions & 0 deletions packages/router-core/src/ssr/ssr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export async function hydrate(router: AnyRouter): Promise<any> {
router.ssr = {
manifest,
}
const meta = document.querySelector('meta[property="csp-nonce"]') as
| HTMLMetaElement
| undefined
const nonce = meta?.content
router.options.ssr = {
nonce,
}
Comment on lines +89 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential data loss: entire router.options.ssr object is overwritten.

Assigning router.options.ssr = { nonce } discards any pre-existing properties on router.options.ssr. If other parts of the hydration flow or user configuration set additional properties on this object, they will be lost.

Apply this diff to preserve existing properties:

  const meta = document.querySelector('meta[property="csp-nonce"]') as
    | HTMLMetaElement
    | undefined
  const nonce = meta?.content
- router.options.ssr = {
-   nonce,
- }
+ router.options.ssr = {
+   ...router.options.ssr,
+   nonce,
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const meta = document.querySelector('meta[property="csp-nonce"]') as
| HTMLMetaElement
| undefined
const nonce = meta?.content
router.options.ssr = {
nonce,
}
const meta = document.querySelector('meta[property="csp-nonce"]') as
| HTMLMetaElement
| undefined
const nonce = meta?.content
router.options.ssr = {
...router.options.ssr,
nonce,
}
🤖 Prompt for AI Agents
In packages/router-core/src/ssr/ssr-client.ts around lines 89 to 95, the code
unconditionally assigns router.options.ssr = { nonce } which overwrites any
existing properties; instead merge the nonce into the existing object
(preserving existing properties) by shallow-merging into router.options.ssr (or
initializing it if undefined) so the final object contains prior keys plus the
nonce.


// Hydrate the router state
const matches = router.matchRoutes(router.state.location)
Expand Down
19 changes: 17 additions & 2 deletions packages/solid-router/src/HeadContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { RouterManagedTag } from '@tanstack/router-core'

export const useTags = () => {
const router = useRouter()

const nonce = router.options.ssr?.nonce
const routeMeta = useRouterState({
select: (state) => {
return state.matches.map((match) => match.meta!).filter(Boolean)
Expand Down Expand Up @@ -46,6 +46,7 @@ export const useTags = () => {
tag: 'meta',
attrs: {
...m,
nonce,
},
})
}
Expand All @@ -56,6 +57,15 @@ export const useTags = () => {
resultMeta.push(title)
}

if (router.options.ssr?.nonce) {
resultMeta.push({
tag: 'meta',
attrs: {
property: 'csp-nonce',
content: router.options.ssr.nonce,
},
})
}
resultMeta.reverse()

return resultMeta
Expand All @@ -71,6 +81,7 @@ export const useTags = () => {
tag: 'link',
attrs: {
...link,
nonce,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Nonce applied to constructed links, but missing on preload and asset links.

While constructed links receive the nonce attribute (line 84), both:

  • Preload links (modulepreload) at lines 119-125
  • Manifest assets (link tags) at lines 97-102

do not receive the nonce. This inconsistency may cause CSP violations if those links contain resources that require nonce validation.

Apply this diff to add nonce to preload links:

           router.ssr?.manifest?.routes[route.id]?.preloads
             ?.filter(Boolean)
             .forEach((preload) => {
               preloadMeta.push({
                 tag: 'link',
                 attrs: {
                   rel: 'modulepreload',
                   href: preload,
+                  nonce,
                 },
               })
             }),

And this diff to add nonce to manifest assets:

       .map(
         (asset) =>
           ({
             tag: 'link',
-            attrs: asset.attrs,
+            attrs: {
+              ...asset.attrs,
+              nonce,
+            },
           }) satisfies RouterManagedTag,
       )

},
})) satisfies Array<RouterManagedTag>

Expand All @@ -87,7 +98,7 @@ export const useTags = () => {
(asset) =>
({
tag: 'link',
attrs: asset.attrs,
attrs: { ...asset.attrs, nonce },
}) satisfies RouterManagedTag,
)

Expand All @@ -110,6 +121,7 @@ export const useTags = () => {
attrs: {
rel: 'modulepreload',
href: preload,
nonce,
},
})
}),
Expand All @@ -130,6 +142,7 @@ export const useTags = () => {
tag: 'style',
attrs: {
...style,
nonce,
},
children,
})),
Expand All @@ -146,6 +159,7 @@ export const useTags = () => {
tag: 'script',
attrs: {
...script,
nonce,
},
children,
})),
Expand All @@ -172,6 +186,7 @@ export const useTags = () => {
*/
export function HeadContent() {
const tags = useTags()

return (
<MetaProvider>
{tags().map((tag) => (
Expand Down
5 changes: 3 additions & 2 deletions packages/solid-router/src/Scripts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { RouterManagedTag } from '@tanstack/router-core'

export const Scripts = () => {
const router = useRouter()

const nonce = router.options.ssr?.nonce
const assetScripts = useRouterState({
select: (state) => {
const assetScripts: Array<RouterManagedTag> = []
Expand All @@ -23,7 +23,7 @@ export const Scripts = () => {
.forEach((asset) => {
assetScripts.push({
tag: 'script',
attrs: asset.attrs,
attrs: { ...asset.attrs, nonce },
children: asset.children,
} as any)
}),
Comment on lines 23 to 29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the type assertion and ensure type safety.

The as any cast at line 28 suggests a type mismatch between the constructed object and RouterManagedTag. This bypasses TypeScript's type checking and could hide issues.

Please verify:

  1. Does the RouterManagedTag type properly account for the nonce attribute in script tags?
  2. Can the type assertion be removed by updating the type definition?

Run the following script to check the RouterManagedTag type definition:


🏁 Script executed:

#!/bin/bash
# Description: Find the RouterManagedTag type definition to verify if nonce is properly typed

# Search for RouterManagedTag type definition
ast-grep --pattern 'type RouterManagedTag = $$$'

# Also search for interface definition
ast-grep --pattern 'interface RouterManagedTag {
  $$$
}'

# Search for nonce in type definitions
rg -n --type=ts 'RouterManagedTag' -A 10 -B 2

Length of output: 30501


Replace as any with type-safe pattern for consistency.

The RouterManagedTag type properly supports the nonce attribute through its generic attrs?: Record<string, any>. The as any assertion is unnecessary and bypasses type checking. The codebase already uses the satisfies RouterManagedTag pattern elsewhere (e.g., HeadContent.tsx lines 86, 102). For consistency and type safety per the coding guidelines, replace the as any with:

} satisfies RouterManagedTag)

Alternatively, remove the assertion entirely, since other scripts in react-router follow this approach without needing individual assertions.

🤖 Prompt for AI Agents
In packages/solid-router/src/Scripts.tsx around lines 23 to 29, the object
pushed into assetScripts is asserted with "as any", which bypasses type
checking; replace that assertion with a type-safe pattern by using "satisfies
RouterManagedTag" on the object literal (or remove the assertion entirely like
other script entries) so the nonce attribute is correctly type-checked and the
codebase remains consistent.

Expand All @@ -44,6 +44,7 @@ export const Scripts = () => {
tag: 'script',
attrs: {
...script,
nonce,
},
children,
})),
Expand Down
Loading