Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(#53190): add missing crossOrigin to assetsPrefix resources #56311

Merged
merged 8 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
44 changes: 22 additions & 22 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
href={fullHref}
// @ts-ignore
precedence={precedence}
crossOrigin={renderOpts.crossOrigin}
key={index}
/>
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -546,14 +547,15 @@ export const renderToHTMLOrFlight: AppPageRender = (
const precedence =
process.env.NODE_ENV === 'development' ? 'next_' + href : 'next'

ComponentMod.preloadStyle(fullHref)
ComponentMod.preloadStyle(fullHref, renderOpts.crossOrigin)

return (
<link
rel="stylesheet"
href={fullHref}
// @ts-ignore
precedence={precedence}
crossOrigin={renderOpts.crossOrigin}
key={index}
/>
)
Expand Down Expand Up @@ -1446,21 +1448,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,
}))
Comment on lines +1454 to +1468
Copy link
Contributor Author

@SukkaW SukkaW Oct 2, 2023

Choose a reason for hiding this comment

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

Here I simplify the handling of polyfills by making its type as JSX.IntrinsicElements['script'][], so it can be spread ({...polyfill}) on the JSX later.


const [preinitScripts, bootstrapScript] = getRequiredScripts(
buildManifest,
assetPrefix,
renderOpts.crossOrigin,
subresourceIntegrityManifest,
getAssetQueryString(true),
nonce
Expand Down Expand Up @@ -1530,15 +1537,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}
Expand Down Expand Up @@ -1648,6 +1647,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
getRequiredScripts(
buildManifest,
assetPrefix,
renderOpts.crossOrigin,
subresourceIntegrityManifest,
getAssetQueryString(false),
nonce
Expand Down
28 changes: 21 additions & 7 deletions packages/next/src/server/app-render/required-scripts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ 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,
}
Comment on lines +18 to +25
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Although the bootstrapScripts option of ReactDOMServer.renderToReadableStream supports both string and object type, here I enforce the use of object type so we won't forget other attributes in the future.


const files = buildManifest.rootMainFiles
if (files.length === 0) {
throw new Error(
'Invariant: missing bootstrap script. This is a bug in Next.js'
)
}
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]]
Expand All @@ -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)
Expand All @@ -50,6 +63,7 @@ export function getRequiredScripts(
ReactDOM.preinit(preinitScriptCommands[i], {
as: 'script',
nonce,
crossOrigin,
})
}
}
Expand Down
14 changes: 9 additions & 5 deletions packages/next/src/server/app-render/rsc/preloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ 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 preloadStyle(href: string, crossOrigin?: string | undefined) {
ReactDOM.preload(href, { as: 'style', crossOrigin })
}

export function preloadFont(href: string, type: string) {
;(ReactDOM as any).preload(href, { as: 'font', type })
export function preloadFont(
href: string,
type: string,
crossOrigin?: string | undefined
) {
;(ReactDOM as any).preload(href, { as: 'font', type, crossOrigin })
}

export function preconnect(href: string, crossOrigin?: string) {
export function preconnect(href: string, crossOrigin?: string | undefined) {
if (typeof crossOrigin === 'string') {
;(ReactDOM as any).preconnect(href, { crossOrigin })
} else {
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export type ChildProp = {
segment: Segment
}

export type RenderOptsPartial = {
export interface RenderOptsPartial {
err?: Error | null
dev?: boolean
buildId: string
Expand All @@ -111,6 +111,7 @@ export type RenderOptsPartial = {
runtime?: ServerRuntime
serverComponents?: boolean
assetPrefix?: string
crossOrigin?: '' | 'anonymous' | 'use-credentials' | undefined
Copy link
Contributor Author

Choose a reason for hiding this comment

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

renderOpt.crossOrigin exists on PagesRenderOptsPartial but it is missing on AppRenderOptsPartial here.

nextFontManifest?: NextFontManifest
isBot?: boolean
incrementalCache?: import('../lib/incremental-cache').IncrementalCache
Expand Down
12 changes: 12 additions & 0 deletions test/integration/app-config-crossorigin/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
3 changes: 3 additions & 0 deletions test/integration/app-config-crossorigin/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Index(props) {
return <p id="title">IndexPage</p>
}
16 changes: 16 additions & 0 deletions test/integration/app-config-crossorigin/next.config.js
Original file line number Diff line number Diff line change
@@ -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',
}
50 changes: 50 additions & 0 deletions test/integration/app-config-crossorigin/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-env jest */
import { join } from 'path'
import {
killApp,
findPort,
launchApp,
renderViaHTTP,
File,
} from 'next-test-utils'
import cheerio from 'cheerio'

const appDir = join(__dirname, '../')

describe('App crossOrigin config', () => {
let appPort
let app

it('should render correctly with assetPrefix: "/"', async () => {
try {
appPort = await findPort()
app = await launchApp(appDir, appPort)

const html = await renderViaHTTP(appPort, '/')

const $ = cheerio.load(html)

// 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()
})
} finally {
killApp(app)
}
})
})
Loading