Skip to content

Commit ba73ff3

Browse files
ijjkztanner
andauthored
Ensure manual nonce on Script works as expected (vercel#78939)
While investigating vercel#78936 we noticed that manually setting `nonce` prop on `next/script` tag it wasn't honored properly. This ensures we fully propagate it when manually set as a prop. --------- Co-authored-by: Zack Tanner <1939140+ztanner@users.noreply.github.com>
1 parent 617867f commit ba73ff3

File tree

6 files changed

+117
-3
lines changed

6 files changed

+117
-3
lines changed

packages/next/src/client/script.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,12 @@ function Script(props: ScriptProps): JSX.Element | null {
209209
} = props
210210

211211
// Context is available only during SSR
212-
const { updateScripts, scripts, getIsSsr, appDir, nonce } =
212+
let { updateScripts, scripts, getIsSsr, appDir, nonce } =
213213
useContext(HeadManagerContext)
214214

215+
// if a nonce is explicitly passed to the script tag, favor that over the automatic handling
216+
nonce = restProps.nonce || nonce
217+
215218
/**
216219
* - First mount:
217220
* 1. The useEffect for onReady executes
@@ -276,14 +279,18 @@ function Script(props: ScriptProps): JSX.Element | null {
276279
onReady,
277280
onError,
278281
...restProps,
282+
nonce,
279283
},
280284
])
281285
updateScripts(scripts)
282286
} else if (getIsSsr && getIsSsr()) {
283287
// Script has already loaded during SSR
284288
LoadCache.add(id || src)
285289
} else if (getIsSsr && !getIsSsr()) {
286-
loadScript(props)
290+
loadScript({
291+
...props,
292+
nonce,
293+
})
287294
}
288295
}
289296

packages/next/src/pages/_document.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ function getPreNextScripts(context: HtmlProps, props: OriginProps) {
330330
{...scriptProps}
331331
key={scriptProps.src || index}
332332
defer={scriptProps.defer ?? !disableOptimizedLoading}
333-
nonce={props.nonce}
333+
nonce={scriptProps.nonce || props.nonce}
334334
data-nscript="beforeInteractive"
335335
crossOrigin={props.crossOrigin || crossOrigin}
336336
/>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Script from 'next/script'
2+
import { ShowScriptOrder } from './show-order'
3+
4+
export default function Page() {
5+
return (
6+
<>
7+
<p>script-nonce</p>
8+
<Script strategy="afterInteractive" src="/test1.js" nonce="hello-world" />
9+
<Script
10+
strategy="beforeInteractive"
11+
src="/test2.js"
12+
nonce="hello-world"
13+
/>
14+
<Script strategy="beforeInteractive" id="3" nonce="hello-world" />
15+
<ShowScriptOrder />
16+
</>
17+
)
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client'
2+
import { useState } from 'react'
3+
4+
export function ShowScriptOrder() {
5+
const [order, setOrder] = useState(null)
6+
7+
return (
8+
<>
9+
<p id="order">{JSON.stringify(order)}</p>
10+
<button
11+
id="get-order"
12+
onClick={() => {
13+
setOrder(window._script_order)
14+
}}
15+
>
16+
get order
17+
</button>
18+
</>
19+
)
20+
}

test/e2e/app-dir/app/index.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,57 @@ describe('app dir - basic', () => {
17111711
}
17121712
})
17131713

1714+
it('should pass manual `nonce`', async () => {
1715+
const html = await next.render('/script-manual-nonce')
1716+
const $ = cheerio.load(html)
1717+
let scripts = $('script, link[rel="preload"][as="script"]')
1718+
1719+
scripts = scripts.filter((_, element) =>
1720+
(element.attribs.src || element.attribs.href)?.startsWith('/test')
1721+
)
1722+
1723+
expect(scripts.length).toBeGreaterThan(0)
1724+
1725+
scripts.each((_, element) => {
1726+
expect(element.attribs.nonce).toBeTruthy()
1727+
})
1728+
1729+
if (!isNextDev) {
1730+
const browser = await next.browser('/script-manual-nonce')
1731+
1732+
await retry(async () => {
1733+
await browser.elementByCss('#get-order').click()
1734+
const order = JSON.parse(await browser.elementByCss('#order').text())
1735+
expect(order?.length).toBe(2)
1736+
})
1737+
}
1738+
})
1739+
1740+
it('should pass manual `nonce` pages', async () => {
1741+
const html = await next.render('/pages-script-manual-nonce')
1742+
const $ = cheerio.load(html)
1743+
let scripts = $('script, link[rel="preload"][as="script"]')
1744+
1745+
scripts = scripts.filter((_, element) =>
1746+
(element.attribs.src || element.attribs.href)?.startsWith('/test')
1747+
)
1748+
1749+
expect(scripts.length).toBeGreaterThan(0)
1750+
1751+
scripts.each((_, element) => {
1752+
expect(element.attribs.nonce).toBeTruthy()
1753+
})
1754+
1755+
if (!isNextDev) {
1756+
await retry(async () => {
1757+
const browser = await next.browser('/pages-script-manual-nonce')
1758+
await browser.elementByCss('#get-order').click()
1759+
const order = JSON.parse(await browser.elementByCss('#order').text())
1760+
expect(order?.length).toBe(2)
1761+
})
1762+
}
1763+
})
1764+
17141765
it('should pass nonce when using next/font', async () => {
17151766
const html = await next.render('/script-nonce/with-next-font')
17161767
const $ = cheerio.load(html)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Script from 'next/script'
2+
import { ShowScriptOrder } from '../app/script-nonce/show-order'
3+
4+
export default function Page() {
5+
return (
6+
<>
7+
<p>script-nonce</p>
8+
<Script strategy="afterInteractive" src="/test1.js" nonce="hello-world" />
9+
<Script
10+
strategy="beforeInteractive"
11+
src="/test2.js"
12+
nonce="hello-world"
13+
/>
14+
<Script strategy="beforeInteractive" id="3" nonce="hello-world" />
15+
<ShowScriptOrder />
16+
</>
17+
)
18+
}

0 commit comments

Comments
 (0)