Skip to content

Commit 86274e6

Browse files
SukkaWhuozhi
andauthoredOct 2, 2023
fix(#53190): add missing crossOrigin to assetsPrefix resources (#56311)
Fixes #53190. Next.js App Router comprises two categories of resources, same-origin ones (RSC payload, in the form of inline `<script />`) and possibly third-party ones (chunks that respect the `assetPrefix`). The latter should also respect the `crossOrigin` option from `next.config.js`. Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
1 parent e970e05 commit 86274e6

File tree

8 files changed

+129
-40
lines changed

8 files changed

+129
-40
lines changed
 

‎packages/next/src/server/app-render/app-render.tsx

+22-22
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
467467
href={fullHref}
468468
// @ts-ignore
469469
precedence={precedence}
470+
crossOrigin={renderOpts.crossOrigin}
470471
key={index}
471472
/>
472473
)
@@ -511,7 +512,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
511512
const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFilename)![1]
512513
const type = `font/${ext}`
513514
const href = `${assetPrefix}/_next/${fontFilename}`
514-
ComponentMod.preloadFont(href, type)
515+
ComponentMod.preloadFont(href, type, renderOpts.crossOrigin)
515516
}
516517
} else {
517518
try {
@@ -546,14 +547,15 @@ export const renderToHTMLOrFlight: AppPageRender = (
546547
const precedence =
547548
process.env.NODE_ENV === 'development' ? 'next_' + href : 'next'
548549

549-
ComponentMod.preloadStyle(fullHref)
550+
ComponentMod.preloadStyle(fullHref, renderOpts.crossOrigin)
550551

551552
return (
552553
<link
553554
rel="stylesheet"
554555
href={fullHref}
555556
// @ts-ignore
556557
precedence={precedence}
558+
crossOrigin={renderOpts.crossOrigin}
557559
key={index}
558560
/>
559561
)
@@ -1449,21 +1451,26 @@ export const renderToHTMLOrFlight: AppPageRender = (
14491451
tree: LoaderTree
14501452
formState: any
14511453
}) => {
1452-
const polyfills = buildManifest.polyfillFiles
1453-
.filter(
1454-
(polyfill) =>
1455-
polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')
1456-
)
1457-
.map((polyfill) => ({
1458-
src: `${assetPrefix}/_next/${polyfill}${getAssetQueryString(
1459-
false
1460-
)}`,
1461-
integrity: subresourceIntegrityManifest?.[polyfill],
1462-
}))
1454+
const polyfills: JSX.IntrinsicElements['script'][] =
1455+
buildManifest.polyfillFiles
1456+
.filter(
1457+
(polyfill) =>
1458+
polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')
1459+
)
1460+
.map((polyfill) => ({
1461+
src: `${assetPrefix}/_next/${polyfill}${getAssetQueryString(
1462+
false
1463+
)}`,
1464+
integrity: subresourceIntegrityManifest?.[polyfill],
1465+
crossOrigin: renderOpts.crossOrigin,
1466+
noModule: true,
1467+
nonce,
1468+
}))
14631469

14641470
const [preinitScripts, bootstrapScript] = getRequiredScripts(
14651471
buildManifest,
14661472
assetPrefix,
1473+
renderOpts.crossOrigin,
14671474
subresourceIntegrityManifest,
14681475
getAssetQueryString(true),
14691476
nonce
@@ -1533,15 +1540,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
15331540
{polyfillsFlushed
15341541
? null
15351542
: polyfills?.map((polyfill) => {
1536-
return (
1537-
<script
1538-
key={polyfill.src}
1539-
src={polyfill.src}
1540-
integrity={polyfill.integrity}
1541-
noModule={true}
1542-
nonce={nonce}
1543-
/>
1544-
)
1543+
return <script key={polyfill.src} {...polyfill} />
15451544
})}
15461545
{renderServerInsertedHTML()}
15471546
{errorMetaTags}
@@ -1651,6 +1650,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
16511650
getRequiredScripts(
16521651
buildManifest,
16531652
assetPrefix,
1653+
renderOpts.crossOrigin,
16541654
subresourceIntegrityManifest,
16551655
getAssetQueryString(false),
16561656
nonce

‎packages/next/src/server/app-render/required-scripts.tsx

+21-7
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,35 @@ import ReactDOM from 'react-dom'
55
export function getRequiredScripts(
66
buildManifest: BuildManifest,
77
assetPrefix: string,
8+
crossOrigin: string | undefined,
89
SRIManifest: undefined | Record<string, string>,
910
qs: string,
1011
nonce: string | undefined
11-
): [() => void, string | { src: string; integrity: string }] {
12+
): [
13+
() => void,
14+
{ src: string; integrity?: string; crossOrigin?: string | undefined }
15+
] {
1216
let preinitScripts: () => void
1317
let preinitScriptCommands: string[] = []
14-
let bootstrapScript: string | { src: string; integrity: string } = ''
18+
const bootstrapScript: {
19+
src: string
20+
integrity?: string
21+
crossOrigin?: string | undefined
22+
} = {
23+
src: '',
24+
crossOrigin,
25+
}
26+
1527
const files = buildManifest.rootMainFiles
1628
if (files.length === 0) {
1729
throw new Error(
1830
'Invariant: missing bootstrap script. This is a bug in Next.js'
1931
)
2032
}
2133
if (SRIManifest) {
22-
bootstrapScript = {
23-
src: `${assetPrefix}/_next/` + files[0] + qs,
24-
integrity: SRIManifest[files[0]],
25-
}
34+
bootstrapScript.src = `${assetPrefix}/_next/` + files[0] + qs
35+
bootstrapScript.integrity = SRIManifest[files[0]]
36+
2637
for (let i = 1; i < files.length; i++) {
2738
const src = `${assetPrefix}/_next/` + files[i] + qs
2839
const integrity = SRIManifest[files[i]]
@@ -34,12 +45,14 @@ export function getRequiredScripts(
3445
ReactDOM.preinit(preinitScriptCommands[i], {
3546
as: 'script',
3647
integrity: preinitScriptCommands[i + 1],
48+
crossOrigin,
3749
nonce,
3850
})
3951
}
4052
}
4153
} else {
42-
bootstrapScript = `${assetPrefix}/_next/` + files[0] + qs
54+
bootstrapScript.src = `${assetPrefix}/_next/` + files[0] + qs
55+
4356
for (let i = 1; i < files.length; i++) {
4457
const src = `${assetPrefix}/_next/` + files[i] + qs
4558
preinitScriptCommands.push(src)
@@ -50,6 +63,7 @@ export function getRequiredScripts(
5063
ReactDOM.preinit(preinitScriptCommands[i], {
5164
as: 'script',
5265
nonce,
66+
crossOrigin,
5367
})
5468
}
5569
}

‎packages/next/src/server/app-render/rsc/preloads.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,29 @@ Files in the rsc directory are meant to be packaged as part of the RSC graph usi
66

77
import ReactDOM from 'react-dom'
88

9-
export function preloadStyle(href: string) {
10-
ReactDOM.preload(href, { as: 'style' })
11-
}
12-
13-
export function preloadFont(href: string, type: string) {
14-
;(ReactDOM as any).preload(href, { as: 'font', type })
9+
export function preloadStyle(href: string, crossOrigin?: string | undefined) {
10+
const opts: any = { as: 'style' }
11+
if (typeof crossOrigin === 'string') {
12+
opts.crossOrigin = crossOrigin
13+
}
14+
ReactDOM.preload(href, opts)
1515
}
1616

17-
export function preconnect(href: string, crossOrigin?: string) {
17+
export function preloadFont(
18+
href: string,
19+
type: string,
20+
crossOrigin?: string | undefined
21+
) {
22+
const opts: any = { as: 'font', type }
1823
if (typeof crossOrigin === 'string') {
19-
;(ReactDOM as any).preconnect(href, { crossOrigin })
20-
} else {
21-
;(ReactDOM as any).preconnect(href)
24+
opts.crossOrigin = crossOrigin
2225
}
26+
ReactDOM.preload(href, opts)
27+
}
28+
29+
export function preconnect(href: string, crossOrigin?: string | undefined) {
30+
;(ReactDOM as any).preconnect(
31+
href,
32+
typeof crossOrigin === 'string' ? { crossOrigin } : undefined
33+
)
2334
}

‎packages/next/src/server/app-render/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export type ChildProp = {
101101
segment: Segment
102102
}
103103

104-
export type RenderOptsPartial = {
104+
export interface RenderOptsPartial {
105105
err?: Error | null
106106
dev?: boolean
107107
buildId: string
@@ -111,6 +111,7 @@ export type RenderOptsPartial = {
111111
runtime?: ServerRuntime
112112
serverComponents?: boolean
113113
assetPrefix?: string
114+
crossOrigin?: '' | 'anonymous' | 'use-credentials' | undefined
114115
nextFontManifest?: NextFontManifest
115116
isBot?: boolean
116117
incrementalCache?: import('../lib/incremental-cache').IncrementalCache
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function RootLayout({ children }) {
2+
return (
3+
<html>
4+
<body>{children}</body>
5+
</html>
6+
)
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Index(props) {
2+
return <p id="title">IndexPage</p>
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { createNextDescribe } from 'e2e-utils'
2+
3+
createNextDescribe(
4+
'app dir - crossOrigin config',
5+
{
6+
files: __dirname,
7+
skipDeployment: true,
8+
},
9+
({ next, isNextStart }) => {
10+
if (isNextStart) {
11+
it('skip in start mode', () => {})
12+
return
13+
}
14+
it('should render correctly with assetPrefix: "/"', async () => {
15+
const $ = await next.render$('/')
16+
// Only potential external (assetPrefix) <script /> and <link /> should have crossorigin attribute
17+
$(
18+
'script[src*="https://example.vercel.sh"], link[href*="https://example.vercel.sh"]'
19+
).each((_, el) => {
20+
const crossOrigin = $(el).attr('crossorigin')
21+
expect(crossOrigin).toBe('use-credentials')
22+
})
23+
24+
// Inline <script /> (including RSC payload) and <link /> should not have crossorigin attribute
25+
$('script:not([src]), link:not([href])').each((_, el) => {
26+
const crossOrigin = $(el).attr('crossorigin')
27+
expect(crossOrigin).toBeUndefined()
28+
})
29+
30+
// Same origin <script /> and <link /> should not have crossorigin attribute either
31+
$('script[src^="/"], link[href^="/"]').each((_, el) => {
32+
const crossOrigin = $(el).attr('crossorigin')
33+
expect(crossOrigin).toBeUndefined()
34+
})
35+
})
36+
}
37+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module.exports = {
2+
/**
3+
* The "assetPrefix" here doesn't needs to be real as we doesn't load the page in the browser in this test,
4+
* we only care about if all assets prefixed with the "assetPrefix" are having correct "crossOrigin".
5+
*/
6+
assetPrefix: 'https://example.vercel.sh',
7+
8+
/**
9+
* According to HTML5 Spec (https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes),
10+
* crossorigin="" and crossorigin="anonymous" has the same effect. And ReactDOM's preload methods (preload, preconnect, etc.)
11+
* will prefer crossorigin="" to save bytes.
12+
*
13+
* So we use "use-credentials" here for easier testing.
14+
*/
15+
crossOrigin: 'use-credentials',
16+
}

0 commit comments

Comments
 (0)
Please sign in to comment.