diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index e846b283192d2..8241a25789448 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -467,6 +467,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
href={fullHref}
// @ts-ignore
precedence={precedence}
+ crossOrigin={renderOpts.crossOrigin}
key={index}
/>
)
@@ -511,7 +512,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFilename)![1]
const type = `font/${ext}`
const href = `${assetPrefix}/_next/${fontFilename}`
- ComponentMod.preloadFont(href, type)
+ ComponentMod.preloadFont(href, type, renderOpts.crossOrigin)
}
} else {
try {
@@ -546,7 +547,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
const precedence =
process.env.NODE_ENV === 'development' ? 'next_' + href : 'next'
- ComponentMod.preloadStyle(fullHref)
+ ComponentMod.preloadStyle(fullHref, renderOpts.crossOrigin)
return (
)
@@ -1449,21 +1451,26 @@ export const renderToHTMLOrFlight: AppPageRender = (
tree: LoaderTree
formState: any
}) => {
- const polyfills = buildManifest.polyfillFiles
- .filter(
- (polyfill) =>
- polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')
- )
- .map((polyfill) => ({
- src: `${assetPrefix}/_next/${polyfill}${getAssetQueryString(
- false
- )}`,
- integrity: subresourceIntegrityManifest?.[polyfill],
- }))
+ const polyfills: JSX.IntrinsicElements['script'][] =
+ buildManifest.polyfillFiles
+ .filter(
+ (polyfill) =>
+ polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')
+ )
+ .map((polyfill) => ({
+ src: `${assetPrefix}/_next/${polyfill}${getAssetQueryString(
+ false
+ )}`,
+ integrity: subresourceIntegrityManifest?.[polyfill],
+ crossOrigin: renderOpts.crossOrigin,
+ noModule: true,
+ nonce,
+ }))
const [preinitScripts, bootstrapScript] = getRequiredScripts(
buildManifest,
assetPrefix,
+ renderOpts.crossOrigin,
subresourceIntegrityManifest,
getAssetQueryString(true),
nonce
@@ -1533,15 +1540,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
{polyfillsFlushed
? null
: polyfills?.map((polyfill) => {
- return (
-
- )
+ return
})}
{renderServerInsertedHTML()}
{errorMetaTags}
@@ -1651,6 +1650,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
getRequiredScripts(
buildManifest,
assetPrefix,
+ renderOpts.crossOrigin,
subresourceIntegrityManifest,
getAssetQueryString(false),
nonce
diff --git a/packages/next/src/server/app-render/required-scripts.tsx b/packages/next/src/server/app-render/required-scripts.tsx
index d8da09d9c016c..e686ba326c3a3 100644
--- a/packages/next/src/server/app-render/required-scripts.tsx
+++ b/packages/next/src/server/app-render/required-scripts.tsx
@@ -5,13 +5,25 @@ import ReactDOM from 'react-dom'
export function getRequiredScripts(
buildManifest: BuildManifest,
assetPrefix: string,
+ crossOrigin: string | undefined,
SRIManifest: undefined | Record,
qs: string,
nonce: string | undefined
-): [() => void, string | { src: string; integrity: string }] {
+): [
+ () => void,
+ { src: string; integrity?: string; crossOrigin?: string | undefined }
+] {
let preinitScripts: () => void
let preinitScriptCommands: string[] = []
- let bootstrapScript: string | { src: string; integrity: string } = ''
+ const bootstrapScript: {
+ src: string
+ integrity?: string
+ crossOrigin?: string | undefined
+ } = {
+ src: '',
+ crossOrigin,
+ }
+
const files = buildManifest.rootMainFiles
if (files.length === 0) {
throw new Error(
@@ -19,10 +31,9 @@ export function getRequiredScripts(
)
}
if (SRIManifest) {
- bootstrapScript = {
- src: `${assetPrefix}/_next/` + files[0] + qs,
- integrity: SRIManifest[files[0]],
- }
+ bootstrapScript.src = `${assetPrefix}/_next/` + files[0] + qs
+ bootstrapScript.integrity = SRIManifest[files[0]]
+
for (let i = 1; i < files.length; i++) {
const src = `${assetPrefix}/_next/` + files[i] + qs
const integrity = SRIManifest[files[i]]
@@ -34,12 +45,14 @@ export function getRequiredScripts(
ReactDOM.preinit(preinitScriptCommands[i], {
as: 'script',
integrity: preinitScriptCommands[i + 1],
+ crossOrigin,
nonce,
})
}
}
} else {
- bootstrapScript = `${assetPrefix}/_next/` + files[0] + qs
+ bootstrapScript.src = `${assetPrefix}/_next/` + files[0] + qs
+
for (let i = 1; i < files.length; i++) {
const src = `${assetPrefix}/_next/` + files[i] + qs
preinitScriptCommands.push(src)
@@ -50,6 +63,7 @@ export function getRequiredScripts(
ReactDOM.preinit(preinitScriptCommands[i], {
as: 'script',
nonce,
+ crossOrigin,
})
}
}
diff --git a/packages/next/src/server/app-render/rsc/preloads.ts b/packages/next/src/server/app-render/rsc/preloads.ts
index 6aae78ac668a4..fdb2bc39bdcc0 100644
--- a/packages/next/src/server/app-render/rsc/preloads.ts
+++ b/packages/next/src/server/app-render/rsc/preloads.ts
@@ -6,18 +6,29 @@ Files in the rsc directory are meant to be packaged as part of the RSC graph usi
import ReactDOM from 'react-dom'
-export function preloadStyle(href: string) {
- ReactDOM.preload(href, { as: 'style' })
-}
-
-export function preloadFont(href: string, type: string) {
- ;(ReactDOM as any).preload(href, { as: 'font', type })
+export function preloadStyle(href: string, crossOrigin?: string | undefined) {
+ const opts: any = { as: 'style' }
+ if (typeof crossOrigin === 'string') {
+ opts.crossOrigin = crossOrigin
+ }
+ ReactDOM.preload(href, opts)
}
-export function preconnect(href: string, crossOrigin?: string) {
+export function preloadFont(
+ href: string,
+ type: string,
+ crossOrigin?: string | undefined
+) {
+ const opts: any = { as: 'font', type }
if (typeof crossOrigin === 'string') {
- ;(ReactDOM as any).preconnect(href, { crossOrigin })
- } else {
- ;(ReactDOM as any).preconnect(href)
+ opts.crossOrigin = crossOrigin
}
+ ReactDOM.preload(href, opts)
+}
+
+export function preconnect(href: string, crossOrigin?: string | undefined) {
+ ;(ReactDOM as any).preconnect(
+ href,
+ typeof crossOrigin === 'string' ? { crossOrigin } : undefined
+ )
}
diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts
index 1d83b5f417a77..085b63629b4ca 100644
--- a/packages/next/src/server/app-render/types.ts
+++ b/packages/next/src/server/app-render/types.ts
@@ -101,7 +101,7 @@ export type ChildProp = {
segment: Segment
}
-export type RenderOptsPartial = {
+export interface RenderOptsPartial {
err?: Error | null
dev?: boolean
buildId: string
@@ -111,6 +111,7 @@ export type RenderOptsPartial = {
runtime?: ServerRuntime
serverComponents?: boolean
assetPrefix?: string
+ crossOrigin?: '' | 'anonymous' | 'use-credentials' | undefined
nextFontManifest?: NextFontManifest
isBot?: boolean
incrementalCache?: import('../lib/incremental-cache').IncrementalCache
diff --git a/test/e2e/app-dir/app-config-crossorigin/app/layout.js b/test/e2e/app-dir/app-config-crossorigin/app/layout.js
new file mode 100644
index 0000000000000..803f17d863c8a
--- /dev/null
+++ b/test/e2e/app-dir/app-config-crossorigin/app/layout.js
@@ -0,0 +1,7 @@
+export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/test/e2e/app-dir/app-config-crossorigin/app/page.js b/test/e2e/app-dir/app-config-crossorigin/app/page.js
new file mode 100644
index 0000000000000..84e7f049d5539
--- /dev/null
+++ b/test/e2e/app-dir/app-config-crossorigin/app/page.js
@@ -0,0 +1,3 @@
+export default function Index(props) {
+ return IndexPage
+}
diff --git a/test/e2e/app-dir/app-config-crossorigin/index.test.ts b/test/e2e/app-dir/app-config-crossorigin/index.test.ts
new file mode 100644
index 0000000000000..a0d08fc24e12c
--- /dev/null
+++ b/test/e2e/app-dir/app-config-crossorigin/index.test.ts
@@ -0,0 +1,37 @@
+import { createNextDescribe } from 'e2e-utils'
+
+createNextDescribe(
+ 'app dir - crossOrigin config',
+ {
+ files: __dirname,
+ skipDeployment: true,
+ },
+ ({ next, isNextStart }) => {
+ if (isNextStart) {
+ it('skip in start mode', () => {})
+ return
+ }
+ it('should render correctly with assetPrefix: "/"', async () => {
+ const $ = await next.render$('/')
+ // Only potential external (assetPrefix) and should have crossorigin attribute
+ $(
+ 'script[src*="https://example.vercel.sh"], link[href*="https://example.vercel.sh"]'
+ ).each((_, el) => {
+ const crossOrigin = $(el).attr('crossorigin')
+ expect(crossOrigin).toBe('use-credentials')
+ })
+
+ // Inline (including RSC payload) and should not have crossorigin attribute
+ $('script:not([src]), link:not([href])').each((_, el) => {
+ const crossOrigin = $(el).attr('crossorigin')
+ expect(crossOrigin).toBeUndefined()
+ })
+
+ // Same origin and should not have crossorigin attribute either
+ $('script[src^="/"], link[href^="/"]').each((_, el) => {
+ const crossOrigin = $(el).attr('crossorigin')
+ expect(crossOrigin).toBeUndefined()
+ })
+ })
+ }
+)
diff --git a/test/e2e/app-dir/app-config-crossorigin/next.config.js b/test/e2e/app-dir/app-config-crossorigin/next.config.js
new file mode 100644
index 0000000000000..6e9edd22337ce
--- /dev/null
+++ b/test/e2e/app-dir/app-config-crossorigin/next.config.js
@@ -0,0 +1,16 @@
+module.exports = {
+ /**
+ * The "assetPrefix" here doesn't needs to be real as we doesn't load the page in the browser in this test,
+ * we only care about if all assets prefixed with the "assetPrefix" are having correct "crossOrigin".
+ */
+ assetPrefix: 'https://example.vercel.sh',
+
+ /**
+ * According to HTML5 Spec (https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes),
+ * crossorigin="" and crossorigin="anonymous" has the same effect. And ReactDOM's preload methods (preload, preconnect, etc.)
+ * will prefer crossorigin="" to save bytes.
+ *
+ * So we use "use-credentials" here for easier testing.
+ */
+ crossOrigin: 'use-credentials',
+}