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 ( <link @@ -554,6 +555,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( href={fullHref} // @ts-ignore precedence={precedence} + crossOrigin={renderOpts.crossOrigin} key={index} /> ) @@ -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 ( - <script - key={polyfill.src} - src={polyfill.src} - integrity={polyfill.integrity} - noModule={true} - nonce={nonce} - /> - ) + return <script key={polyfill.src} {...polyfill} /> })} {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<string, string>, 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 ( + <html> + <body>{children}</body> + </html> + ) +} 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 <p id="title">IndexPage</p> +} 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) <script /> and <link /> 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 <script /> (including RSC payload) and <link /> should not have crossorigin attribute + $('script:not([src]), link:not([href])').each((_, el) => { + const crossOrigin = $(el).attr('crossorigin') + expect(crossOrigin).toBeUndefined() + }) + + // Same origin <script /> and <link /> 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', +}