diff --git a/docs/advanced-features/middleware.md b/docs/advanced-features/middleware.md index 22dd0988b8163c5..3d2029b3ff93a12 100644 --- a/docs/advanced-features/middleware.md +++ b/docs/advanced-features/middleware.md @@ -31,7 +31,7 @@ To begin using Middleware, follow the steps below: npm install next@latest ``` -2. Create a `middleware.ts` (or `.js`) file at the same level as your `pages` directory +2. Create a `middleware.ts` (or `.js`) file at the root or in the `src` directory (same level as your `pages`) 3. Export a middleware function from the `middleware.ts` file: ```typescript diff --git a/docs/api-reference/next/future/image.md b/docs/api-reference/next/future/image.md index b0a8883262c6e43..bd97cbeced80d96 100644 --- a/docs/api-reference/next/future/image.md +++ b/docs/api-reference/next/future/image.md @@ -7,11 +7,11 @@ description: Try the latest Image Optimization with the new `next/future/image`
Version History -| Version | Changes | -| --------- | --------------------------------------------------------------------------------------------------------------------------- | -| `v12.3.0` | `next/future/image` component stable. `remotePatterns` config stable. `unoptimized` config stable. `alt` property required. | -| `v12.2.4` | `fill` property added. | -| `v12.2.0` | Experimental `next/future/image` component introduced. | +| Version | Changes | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `v12.3.0` | `next/future/image` component stable. `remotePatterns` config stable. `unoptimized` config stable. `alt` property required. `onLoadingComplete` receives `` | +| `v12.2.4` | `fill` property added. | +| `v12.2.0` | Experimental `next/future/image` component introduced. |
@@ -33,6 +33,7 @@ Compared to `next/image`, the new `next/future/image` component has the followin - Removes `lazyRoot` prop since there is no native equivalent - Removes `loader` config in favor of [`loader`](#loader) prop - Changed `alt` prop from optional to required +- Changed `onLoadingComplete` callback to receive reference to `` element ## Known Browser Bugs @@ -151,7 +152,7 @@ Must be one of the following: 2. A path string. This can be either an absolute external URL, or an internal path depending on the [loader](#loader) prop. -When using an external URL, you must add it to [domains](#domains) in `next.config.js`. +When using an external URL, you must add it to [remotePatterns](#remote-patterns) in `next.config.js`. ### width @@ -306,10 +307,7 @@ Also keep in mind that the required `width` and `height` props can interact with A callback function that is invoked once the image is completely loaded and the [placeholder](#placeholder) has been removed. -The callback function will be called with one argument, an object with the following properties: - -- [`naturalWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalWidth) -- [`naturalHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalHeight) +The callback function will be called with one argument, a reference to the underlying `` element. ### onLoad @@ -430,6 +428,8 @@ The `**` syntax does not work in the middle of the pattern. ### Domains +> Note: We recommend using [`remotePatterns`](#remote-patterns) instead so you can restrict protocol and pathname. + Similar to [`remotePatterns`](#remote-patterns), the `domains` configuration can be used to provide a list of allowed hostnames for external images. However, the `domains` configuration does not support wildcard pattern matching and it cannot restrict protocol, port, or pathname. diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 60ffe36f20dac17..5c0bc210e088cae 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -45,7 +45,7 @@ Must be one of the following: or an internal path depending on the [loader](#loader) prop or [loader configuration](#loader-configuration). When using an external URL, you must add it to -[domains](#domains) in +[remotePatterns](#remote-patterns) in `next.config.js`. ### width @@ -393,6 +393,8 @@ The `**` syntax does not work in the middle of the pattern. ### Domains +> Note: We recommend using [`remotePatterns`](#remote-patterns) instead so you can restrict protocol and pathname. + Similar to [`remotePatterns`](#remote-patterns), the `domains` configuration can be used to provide a list of allowed hostnames for external images. However, the `domains` configuration does not support wildcard pattern matching and it cannot restrict protocol, port, or pathname. diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 1b5ac21aa200c8a..e53e61f8b773250 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -72,7 +72,7 @@ function Home() { ### Remote Images -To use a remote image, the `src` property should be a URL string, which can be [relative](#loaders) or [absolute](/docs/api-reference/next/image.md#domains). Because Next.js does not have access to remote files during the build process, you'll need to provide the [`width`](/docs/api-reference/next/image.md#width), [`height`](/docs/api-reference/next/image.md#height) and optional [`blurDataURL`](/docs/api-reference/next/image.md#blurdataurl) props manually: +To use a remote image, the `src` property should be a URL string, which can be [relative](#loaders) or [absolute](/docs/api-reference/next/image.md#remote-patterns). Because Next.js does not have access to remote files during the build process, you'll need to provide the [`width`](/docs/api-reference/next/image.md#width), [`height`](/docs/api-reference/next/image.md#height) and optional [`blurDataURL`](/docs/api-reference/next/image.md#blurdataurl) props manually: ```jsx import Image from 'next/image' @@ -97,17 +97,17 @@ export default function Home() { ### Domains -Sometimes you may want to access a remote image, but still use the built-in Next.js Image Optimization API. To do this, leave the `loader` at its default setting and enter an absolute URL for the Image `src`. +Sometimes you may want to optimize a remote image, but still use the built-in Next.js Image Optimization API. To do this, leave the `loader` at its default setting and enter an absolute URL for the Image `src` prop. -To protect your application from malicious users, you must define a list of remote hostnames you intend to allow remote access. +To protect your application from malicious users, you must define a list of remote hostnames you intend to use with the `next/image` component. -> Learn more about [`domains`](/docs/api-reference/next/image.md#domains) configuration. +> Learn more about [`remotePatterns`](/docs/api-reference/next/image.md#remote-patterns) configuration. ### Loaders Note that in the [example earlier](#remote-images), a partial URL (`"/me.png"`) is provided for a remote image. This is possible because of the `next/image` [loader](/docs/api-reference/next/image.md#loader) architecture. -A loader is a function that generates the URLs for your image. It appends a root domain to your provided `src`, and generates multiple URLs to request the image at different sizes. These multiple URLs are used in the automatic [srcset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset) generation, so that visitors to your site will be served an image that is the right size for their viewport. +A loader is a function that generates the URLs for your image. It modifies the provided `src`, and generates multiple URLs to request the image at different sizes. These multiple URLs are used in the automatic [srcset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset) generation, so that visitors to your site will be served an image that is the right size for their viewport. The default loader for Next.js applications uses the built-in Image Optimization API, which optimizes images from anywhere on the web, and then serves them directly from the Next.js web server. If you would like to serve your images directly from a CDN or image server, you can use one of the [built-in loaders](/docs/api-reference/next/image.md#built-in-loaders) or write your own with a few lines of JavaScript. @@ -209,7 +209,7 @@ For examples of the Image component used with the various fill modes, see the [I ## Configuration -The `next/image` component and Next.js Image Optimization API can be configured in the [`next.config.js` file](/docs/api-reference/next.config.js/introduction.md). These configurations allow you to [enable remote images](/docs/api-reference/next/image.md#domains), [define custom image breakpoints](/docs/api-reference/next/image.md#device-sizes), [change caching behavior](/docs/api-reference/next/image.md#caching-behavior) and more. +The `next/image` component and Next.js Image Optimization API can be configured in the [`next.config.js` file](/docs/api-reference/next.config.js/introduction.md). These configurations allow you to [enable remote images](/docs/api-reference/next/image.md#remote-patterns), [define custom image breakpoints](/docs/api-reference/next/image.md#device-sizes), [change caching behavior](/docs/api-reference/next/image.md#caching-behavior) and more. [**Read the full image configuration documentation for more information.**](/docs/api-reference/next/image.md#configuration-options) diff --git a/docs/going-to-production.md b/docs/going-to-production.md index 78ad8d33fe8b9ab..53d3f39d4a6a1c7 100644 --- a/docs/going-to-production.md +++ b/docs/going-to-production.md @@ -72,7 +72,7 @@ export async function getServerSideProps({ req, res }) { By default, `Cache-Control` headers will be set differently depending on how your page fetches data. - If the page uses `getServerSideProps` or `getInitialProps`, it will use the default `Cache-Control` header set by `next start` in order to prevent accidental caching of responses that cannot be cached. If you want a different cache behavior while using `getServerSideProps`, use `res.setHeader('Cache-Control', 'value_you_prefer')` inside of the function as shown above. -- If the page is using `getStaticProps`, it will have a `Cache-Control` header of `s-maxage=REVALIDATE_SECONDS, stale-while-revalidate`, or if `revalidate` is _not_ used , `s-maxage=31536000, stale-while-revalidate` to cache for the maximum age possible. +- If the page is using `getStaticProps`, it will have a `Cache-Control` header of `s-maxage=REVALIDATE_SECONDS, stale-while-revalidate`, or if `revalidate` is _not_ used, `s-maxage=31536000, stale-while-revalidate` to cache for the maximum age possible. > **Note:** Your deployment provider must support caching for dynamic responses. If you are self-hosting, you will need to add this logic yourself using a key/value store like Redis. If you are using Vercel, [Edge Caching works without configuration](https://vercel.com/docs/edge-network/caching?utm_source=next-site&utm_medium=docs&utm_campaign=next-website). @@ -90,7 +90,7 @@ To reduce the amount of JavaScript sent to the browser, you can use the followin - [Import Cost](https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost) – Display the size of the imported package inside VSCode. - [Package Phobia](https://packagephobia.com/) – Find the cost of adding a new dev dependency to your project. - [Bundle Phobia](https://bundlephobia.com/) - Analyze how much a dependency can increase bundle sizes. -- [Webpack Bundle Analyzer](https://github.com/vercel/next.js/tree/canary/packages/next-bundle-analyzer) – Visualize size of webpack output files with an interactive, zoomable treemap. +- [Webpack Bundle Analyzer](https://github.com/vercel/next.js/tree/canary/packages/next-bundle-analyzer) – Visualize the size of webpack output files with an interactive, zoomable treemap. - [bundlejs](https://bundlejs.com/) - An online tool to quickly bundle & minify your projects, while viewing the compressed gzip/brotli bundle size, all running locally on your browser. Each file inside your `pages/` directory will automatically be code split into its own JavaScript bundle during `next build`. You can also use [Dynamic Imports](/docs/advanced-features/dynamic-import.md) to lazy-load components and libraries. For example, you might want to defer loading your modal code until a user clicks the open button. @@ -142,7 +142,7 @@ Once you are able to measure the loading performance, use the following strategi - Setting up your Code Editor to view import costs and sizes - Finding alternative smaller packages - Dynamically loading components and dependencies - - For more in depth information, review this [guide](https://papyrus.dev/@PapyrusBlog/how-we-reduced-next.js-page-size-by-3.5x-and-achieved-a-98-lighthouse-score) and this [performance checklist](https://dev.to/endymion1818/nextjs-performance-checklist-5gjb). + - For more in-depth information, review this [guide](https://papyrus.dev/@PapyrusBlog/how-we-reduced-next.js-page-size-by-3.5x-and-achieved-a-98-lighthouse-score) and this [performance checklist](https://dev.to/endymion1818/nextjs-performance-checklist-5gjb). ## Related diff --git a/docs/migrating/incremental-adoption.md b/docs/migrating/incremental-adoption.md index a52182e3766711f..4334ad640ec605f 100644 --- a/docs/migrating/incremental-adoption.md +++ b/docs/migrating/incremental-adoption.md @@ -92,4 +92,4 @@ Once your monorepo is set up, push changes to your Git repository as usual and y ## Conclusion -To learn more, read about [subpaths](/docs/api-reference/next.config.js/basepath.md) and [rewrites](/docs/api-reference/next.config.js/rewrites.md) or [deploy a Next.jsmonorepo](https://vercel.com/templates/next.js/monorepo). +To learn more, read about [subpaths](/docs/api-reference/next.config.js/basepath.md) and [rewrites](/docs/api-reference/next.config.js/rewrites.md) or [deploy a Next.js monorepo](https://vercel.com/templates/next.js/monorepo). diff --git a/docs/testing.md b/docs/testing.md index e3504d418de3ab5..c8ad384536eed82 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -145,7 +145,7 @@ Playwright is a testing framework that lets you automate Chromium, Firefox, and ### Quickstart -The fastest way to get started, is to use `create-next-app` with the [with-playwright example](https://github.com/vercel/next.js/tree/canary/examples/with-playwright). This will create a Next.js project complete with Playwright all set up. +The fastest way to get started is to use `create-next-app` with the [with-playwright example](https://github.com/vercel/next.js/tree/canary/examples/with-playwright). This will create a Next.js project complete with Playwright all set up. ```bash npx create-next-app@latest --example with-playwright with-playwright-app @@ -214,7 +214,7 @@ test('should navigate to the about page', async ({ page }) => { await page.goto('http://localhost:3000/') // Find an element with the text 'About Page' and click on it await page.click('text=About') - // The new url should be "/about" (baseURL is used there) + // The new URL should be "/about" (baseURL is used there) await expect(page).toHaveURL('http://localhost:3000/about') // The new page should contain an h1 with "About Page" await expect(page.locator('h1')).toContainText('About Page') @@ -307,7 +307,7 @@ Under the hood, `next/jest` is automatically configuring Jest for you, including ### Setting up Jest (with Babel) -If you opt-out of the [Rust Compiler](https://nextjs.org/docs/advanced-features/compiler), you will need to manually configure Jest and install `babel-jest` and `identity-obj-proxy` in addition to the packages above. +If you opt out of the [Rust Compiler](https://nextjs.org/docs/advanced-features/compiler), you will need to manually configure Jest and install `babel-jest` and `identity-obj-proxy` in addition to the packages above. Here are the recommended options to configure Jest for Next.js: @@ -440,7 +440,7 @@ Add the Jest executable in watch mode to the `package.json` scripts: **Create your first tests** -Your project is now ready to run tests. Follow Jests convention by adding tests to the `__tests__` folder in your project's root directory. +Your project is now ready to run tests. Follow Jest's convention by adding tests to the `__tests__` folder in your project's root directory. For example, we can add a test to check if the `` component successfully renders a heading: diff --git a/docs/upgrading.md b/docs/upgrading.md index 2ba95e79e9c6280..e8c4f5b6cb8cc5b 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -44,7 +44,7 @@ yarn add next@12 ### SWC replacing Babel -Next.js now uses Rust-based compiler [SWC](https://swc.rs/) to compile JavaScript/TypeScript. This new compiler is up to 17x faster than Babel when compiling individual files and up to 5x faster Fast Refresh. +Next.js now uses a Rust-based compiler, [SWC](https://swc.rs/), to compile JavaScript/TypeScript. This new compiler is up to 17x faster than Babel when compiling individual files and allows for up to 5x faster Fast Refresh. Next.js provides full backwards compatibility with applications that have [custom Babel configuration](https://nextjs.org/docs/advanced-features/customizing-babel-config). All transformations that Next.js handles by default like styled-jsx and tree-shaking of `getStaticProps` / `getStaticPaths` / `getServerSideProps` have been ported to Rust. @@ -72,15 +72,15 @@ Minification using SWC is an opt-in flag to ensure it can be tested against more ### Improvements to styled-jsx CSS parsing -On top of the Rust-based compiler we've implemented a new CSS parser based on the CSS parser that was used for the styled-jsx Babel transform. This new parser has improved handling of CSS and now errors when invalid CSS is used that would previously slip through and cause unexpected behavior. +On top of the Rust-based compiler, we've implemented a new CSS parser based on the CSS parser that was used for the styled-jsx Babel transform. This new parser has improved handling of CSS and now errors when invalid CSS is used that would previously slip through and cause unexpected behavior. -Because of this change invalid CSS will throw an error during development and `next build`. This change only affects styled-jsx usage. +Because of this change, invalid CSS will throw an error during development and `next build`. This change only affects styled-jsx usage. ### `next/image` changed wrapping element `next/image` now renders the `` inside a `` instead of `
`. -If your application has specific CSS targeting span, for example `.container span`, upgrading to Next.js 12 might incorrectly match the wrapping element inside the `` component. You can avoid this by restricting the selector to a specific class such as `.container span.item` and updating the relevant component with that className, such as ``. +If your application has specific CSS targeting span, for example, `.container span`, upgrading to Next.js 12 might incorrectly match the wrapping element inside the `` component. You can avoid this by restricting the selector to a specific class such as `.container span.item` and updating the relevant component with that className, such as ``. If your application has specific CSS targeting the `next/image` `
` tag, for example `.container div`, it may not match anymore. You can update the selector `.container span`, or preferably, add a new `
` wrapping the `` component and target that instead such as `.container .wrapper`. @@ -341,7 +341,7 @@ TypeScript Definitions are published with the `next` package, so you need to uni The following types are different: -> This list was created by the community to help you upgrade, if you find other differences please send a pull-request to this list to help other users. +> This list was created by the community to help you upgrade, if you find other differences please send a pull request to this list to help other users. From: @@ -359,7 +359,7 @@ import { AppContext, AppInitialProps } from 'next/app' import { DocumentContext, DocumentInitialProps } from 'next/document' ``` -#### The `config` key is now an export on a page +#### The `config` key is now a named export on a page You may no longer export a custom variable named `config` from a page (i.e. `export { config }` / `export const config ...`). This exported variable is now used to specify page-level Next.js configuration like Opt-in AMP and API Route features. @@ -385,7 +385,7 @@ const DynamicComponentWithCustomLoading = dynamic( Next.js now has the concept of page-level configuration, so the `withAmp` higher-order component has been removed for consistency. -This change can be **automatically migrated by running the following commands in the root of your Next.js project:** +This change can be **automatically migrated by running the following commands at the root of your Next.js project:** ```bash curl -L https://github.com/vercel/next-codemod/archive/master.tar.gz | tar -xz --strip=2 next-codemod-master/transforms/withamp-to-config.js npx jscodeshift -t ./withamp-to-config.js pages/**/*.js diff --git a/errors/manifest.json b/errors/manifest.json index 25a43afd1ba3fbb..872dcea5b8a68b2 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -733,6 +733,10 @@ { "title": "middleware-parse-user-agent", "path": "/errors/middleware-parse-user-agent.md" + }, + { + "title": "nonce-contained-invalid-characters", + "path": "/errors/nonce-contained-invalid-characters.md" } ] } diff --git a/errors/nonce-contained-invalid-characters.md b/errors/nonce-contained-invalid-characters.md new file mode 100644 index 000000000000000..3befd0651f9f02a --- /dev/null +++ b/errors/nonce-contained-invalid-characters.md @@ -0,0 +1,20 @@ +# nonce contained invalid characters + +#### Why This Error Occurred + +This happens when there is a request that contains a `Content-Security-Policy` +header that contains a `script-src` directive with a nonce value that contains +invalid characters (any one of `<>&` characters). For example: + +- `'nonce-` ) @@ -167,7 +172,7 @@ function useFlightResponse( writer.close() } else { const responsePartial = decodeText(value) - const scripts = `` @@ -205,7 +210,8 @@ function createServerComponentRenderer( serverContexts: Array< [ServerContextName: string, JSONValue: Object | number | string] > - } + }, + nonce?: string ) { // We need to expose the `__webpack_require__` API globally for // react-server-dom-webpack. This is a hack until we find a better way. @@ -240,7 +246,8 @@ function createServerComponentRenderer( writable, cachePrefix, reqStream, - serverComponentManifest + serverComponentManifest, + nonce ) return response.readRoot() } @@ -406,6 +413,56 @@ function getCssInlinedLinkTags( return [...chunks] } +function getScriptNonceFromHeader(cspHeaderValue: string): string | undefined { + const directives = cspHeaderValue + // Directives are split by ';'. + .split(';') + .map((directive) => directive.trim()) + + // First try to find the directive for the 'script-src', otherwise try to + // fallback to the 'default-src'. + const directive = + directives.find((dir) => dir.startsWith('script-src')) || + directives.find((dir) => dir.startsWith('default-src')) + + // If no directive could be found, then we're done. + if (!directive) { + return + } + + // Extract the nonce from the directive + const nonce = directive + .split(' ') + // Remove the 'strict-src'/'default-src' string, this can't be the nonce. + .slice(1) + .map((source) => source.trim()) + // Find the first source with the 'nonce-' prefix. + .find( + (source) => + source.startsWith("'nonce-") && + source.length > 8 && + source.endsWith("'") + ) + // Grab the nonce by trimming the 'nonce-' prefix. + ?.slice(7, -1) + + // If we could't find the nonce, then we're done. + if (!nonce) { + return + } + + // Don't accept the nonce value if it contains HTML escape characters. + // Technically, the spec requires a base64'd value, but this is just an + // extra layer. + if (ESCAPE_REGEX.test(nonce)) { + throw new Error( + 'Nonce value from Content-Security-Policy contained HTML escape characters.\nLearn more: https://nextjs.org/docs/messages/nonce-contained-invalid-characters' + ) + } + + return nonce +} + export async function renderToHTMLOrFlight( req: IncomingMessage, res: ServerResponse, @@ -426,6 +483,7 @@ export async function renderToHTMLOrFlight( const { buildManifest, + subresourceIntegrityManifest, serverComponentManifest, serverCSSManifest = {}, supportsDynamicHTML, @@ -999,6 +1057,13 @@ export async function renderToHTMLOrFlight( // TODO-APP: validate req.url as it gets passed to render. const initialCanonicalUrl = req.url! + // Get the nonce from the incomming request if it has one. + const csp = req.headers['content-security-policy'] + let nonce: string | undefined + if (csp && typeof csp === 'string') { + nonce = getScriptNonceFromHeader(csp) + } + /** * A new React Component that renders the provided React Component * using Flight which can then be rendered to HTML. @@ -1027,7 +1092,8 @@ export async function renderToHTMLOrFlight( transformStream: serverComponentsInlinedTransformStream, serverComponentManifest, serverContexts, - } + }, + nonce ) const flushEffectsCallbacks: Set<() => React.ReactNode> = new Set() @@ -1080,10 +1146,16 @@ export async function renderToHTMLOrFlight( ReactDOMServer, element: content, streamOptions: { + nonce, // Include hydration scripts in the HTML - bootstrapScripts: buildManifest.rootMainFiles.map( - (src) => `${renderOpts.assetPrefix || ''}/_next/` + src - ), + bootstrapScripts: subresourceIntegrityManifest + ? buildManifest.rootMainFiles.map((src) => ({ + src: `${renderOpts.assetPrefix || ''}/_next/` + src, + integrity: subresourceIntegrityManifest[src], + })) + : buildManifest.rootMainFiles.map( + (src) => `${renderOpts.assetPrefix || ''}/_next/` + src + ), }, }) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index a518b36cfcfb789..411983b3ed9ff6c 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -245,6 +245,7 @@ export default abstract class Server { params: Params isAppPath: boolean appPaths?: string[] | null + sriEnabled?: boolean }): Promise protected abstract getFontManifest(): FontManifest | undefined protected abstract getPrerenderManifest(): PrerenderManifest @@ -1546,8 +1547,8 @@ export default abstract class Server { params: ctx.renderOpts.params || {}, isAppPath: Array.isArray(appPaths), appPaths, + sriEnabled: !!this.nextConfig.experimental.sri?.algorithm, }) - if (result) { try { return await this.renderToResponseWithComponents(ctx, result) diff --git a/packages/next/server/config-schema.ts b/packages/next/server/config-schema.ts index 1ed39ed3c9e69ab..6c06e28a48b3980 100644 --- a/packages/next/server/config-schema.ts +++ b/packages/next/server/config-schema.ts @@ -253,7 +253,14 @@ const configSchema = { type: 'boolean', }, esmExternals: { - type: 'boolean', + oneOf: [ + { + type: 'boolean', + }, + { + const: 'loose', + }, + ] as any, }, externalDir: { type: 'boolean', @@ -338,6 +345,15 @@ const configSchema = { sharedPool: { type: 'boolean', }, + sri: { + properties: { + algorithm: { + enum: ['sha256', 'sha384', 'sha512'] as any, + type: 'string', + }, + }, + type: 'object', + }, swcFileReading: { type: 'boolean', }, diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index b0bf627bfb9d1fb..e0e0ddcfe639300 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -7,6 +7,7 @@ import { imageConfigDefault, } from '../shared/lib/image-config' import { ServerRuntime } from 'next/types' +import { SubresourceIntegrityAlgorithm } from '../build/webpack/plugins/subresource-integrity-plugin' export type NextConfigComplete = Required & { images: Required @@ -146,6 +147,9 @@ export interface ExperimentalConfig { * [webpack/webpack#ModuleNotoundError.js#L13-L42](https://github.com/webpack/webpack/blob/2a0536cf510768111a3a6dceeb14cb79b9f59273/lib/ModuleNotFoundError.js#L13-L42) */ fallbackNodePolyfills?: false + sri?: { + algorithm?: SubresourceIntegrityAlgorithm + } } export type ExportPathMap = { diff --git a/packages/next/server/config.ts b/packages/next/server/config.ts index e5c1260a8d10535..07406b6b32c0658 100644 --- a/packages/next/server/config.ts +++ b/packages/next/server/config.ts @@ -511,10 +511,6 @@ function assignDefaults(userConfig: { [key: string]: any }) { result.compiler.removeConsole = (result.experimental as any).removeConsole } - if (result.swcMinify) { - Log.info('SWC minify release candidate enabled. https://nextjs.link/swcmin') - } - if (result.experimental?.swcMinifyDebugOptions) { Log.warn( 'SWC minify debug option specified. This option is for debugging minifier issues and will be removed once SWC minifier is stable.' diff --git a/packages/next/server/dev/static-paths-worker.ts b/packages/next/server/dev/static-paths-worker.ts index 185948d7f2d146b..e56d018b1f26720 100644 --- a/packages/next/server/dev/static-paths-worker.ts +++ b/packages/next/server/dev/static-paths-worker.ts @@ -38,13 +38,13 @@ export async function loadStaticPaths( require('../../shared/lib/runtime-config').setConfig(config) setHttpAgentOptions(httpAgentOptions) - const components = await loadComponents( + const components = await loadComponents({ distDir, pathname, serverless, - false, - false - ) + hasServerComponents: false, + isAppPath: false, + }) if (!components.getStaticPaths) { // we shouldn't get to this point since the worker should diff --git a/packages/next/server/htmlescape.ts b/packages/next/server/htmlescape.ts index 7bcda3c3570b775..fa06e75df98ac09 100644 --- a/packages/next/server/htmlescape.ts +++ b/packages/next/server/htmlescape.ts @@ -9,7 +9,7 @@ const ESCAPE_LOOKUP: { [match: string]: string } = { '\u2029': '\\u2029', } -const ESCAPE_REGEX = /[&><\u2028\u2029]/g +export const ESCAPE_REGEX = /[&><\u2028\u2029]/g export function htmlEscapeJsonString(str: string): string { return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]) diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 5cbf543ccc40b78..8f9f5431ff9e894 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -30,6 +30,7 @@ export type LoadComponentsReturnType = { Component: NextComponentType pageConfig: PageConfig buildManifest: BuildManifest + subresourceIntegrityManifest?: Record reactLoadableManifest: ReactLoadableManifest serverComponentManifest?: any Document: DocumentType @@ -59,13 +60,19 @@ export async function loadDefaultErrorComponents(distDir: string) { } } -export async function loadComponents( - distDir: string, - pathname: string, - serverless: boolean, - hasServerComponents: boolean, +export async function loadComponents({ + distDir, + pathname, + serverless, + hasServerComponents, + isAppPath, +}: { + distDir: string + pathname: string + serverless: boolean + hasServerComponents: boolean isAppPath: boolean -): Promise { +}): Promise { if (serverless) { const ComponentMod = await requirePage(pathname, distDir, serverless) if (typeof ComponentMod === 'string') { diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 27316dd1fd3e035..be21b05c9e854b5 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -248,20 +248,20 @@ export default class NextNodeServer extends BaseServer { if (!options.dev) { // pre-warm _document and _app as these will be // needed for most requests - loadComponents( - this.distDir, - '/_document', - this._isLikeServerless, - false, - false - ).catch(() => {}) - loadComponents( - this.distDir, - '/_app', - this._isLikeServerless, - false, - false - ).catch(() => {}) + loadComponents({ + distDir: this.distDir, + pathname: '/_document', + serverless: this._isLikeServerless, + hasServerComponents: false, + isAppPath: false, + }).catch(() => {}) + loadComponents({ + distDir: this.distDir, + pathname: '/_app', + serverless: this._isLikeServerless, + hasServerComponents: false, + isAppPath: false, + }).catch(() => {}) } } @@ -763,7 +763,6 @@ export default class NextNodeServer extends BaseServer { params, page, appPaths: null, - isAppPath: false, }) if (handledAsEdgeFunction) { @@ -913,7 +912,6 @@ export default class NextNodeServer extends BaseServer { params: ctx.renderOpts.params, page, appPaths, - isAppPath, }) return null } @@ -934,39 +932,37 @@ export default class NextNodeServer extends BaseServer { params: Params | null isAppPath: boolean }): Promise { - let paths = [ + const paths: string[] = [pathname] + if (query.amp) { // try serving a static AMP version first - query.amp - ? (isAppPath - ? normalizeAppPath(pathname) - : normalizePagePath(pathname)) + '.amp' - : null, - pathname, - ].filter(Boolean) + paths.unshift( + (isAppPath ? normalizeAppPath(pathname) : normalizePagePath(pathname)) + + '.amp' + ) + } if (query.__nextLocale) { - paths = [ + paths.unshift( ...paths.map( (path) => `/${query.__nextLocale}${path === '/' ? '' : path}` - ), - ...paths, - ] + ) + ) } for (const pagePath of paths) { try { - const components = await loadComponents( - this.distDir, - pagePath!, - !this.renderOpts.dev && this._isLikeServerless, - !!this.renderOpts.serverComponents, - isAppPath - ) + const components = await loadComponents({ + distDir: this.distDir, + pathname: pagePath, + serverless: !this.renderOpts.dev && this._isLikeServerless, + hasServerComponents: !!this.renderOpts.serverComponents, + isAppPath, + }) if ( query.__nextLocale && typeof components.Component === 'string' && - !pagePath?.startsWith(`/${query.__nextLocale}`) + !pagePath.startsWith(`/${query.__nextLocale}`) ) { // if loading an static HTML file the locale is required // to be present since all HTML files are output under their locale @@ -2031,12 +2027,12 @@ export default class NextNodeServer extends BaseServer { params: Params | undefined page: string appPaths: string[] | null - isAppPath: boolean onWarning?: (warning: Error) => void }): Promise { let middlewareInfo: ReturnType | undefined - const page = params.page + const { query, page } = params + await this.ensureEdgeFunction({ page, appPaths: params.appPaths }) middlewareInfo = this.getEdgeFunctionInfo({ page, @@ -2048,21 +2044,20 @@ export default class NextNodeServer extends BaseServer { } // For middleware to "fetch" we must always provide an absolute URL - const isDataReq = !!params.query.__nextDataReq - const query = urlQueryToSearchParams(params.query).toString() - const locale = params.query.__nextLocale - // Use original pathname (without `/page`) instead of appPath for url - let normalizedPathname = params.page + const locale = query.__nextLocale + const isDataReq = !!query.__nextDataReq + const queryString = urlQueryToSearchParams(query).toString() if (isDataReq) { params.req.headers['x-nextjs-data'] = '1' } + let normalizedPathname = normalizeAppPath(page) if (isDynamicRoute(normalizedPathname)) { - const routeRegex = getNamedRouteRegex(params.page) + const routeRegex = getNamedRouteRegex(normalizedPathname) normalizedPathname = interpolateDynamicPath( - params.page, - Object.assign({}, params.params, params.query), + normalizedPathname, + Object.assign({}, params.params, query), routeRegex ) } @@ -2070,7 +2065,7 @@ export default class NextNodeServer extends BaseServer { const url = `${getRequestMeta(params.req, '_protocol')}://${ this.hostname }:${this.port}${locale ? `/${locale}` : ''}${normalizedPathname}${ - query ? `?${query}` : '' + queryString ? `?${queryString}` : '' }` if (!url.startsWith('http')) { diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index c314f8517b5b653..f17d05ba0f83924 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -26,6 +26,7 @@ export const APP_PATHS_MANIFEST = 'app-paths-manifest.json' export const APP_PATH_ROUTES_MANIFEST = 'app-path-routes-manifest.json' export const BUILD_MANIFEST = 'build-manifest.json' export const APP_BUILD_MANIFEST = 'app-build-manifest.json' +export const SUBRESOURCE_INTEGRITY_MANIFEST = 'subresource-integrity-manifest' export const EXPORT_MARKER = 'export-marker.json' export const EXPORT_DETAIL = 'export-detail.json' export const PRERENDER_MANIFEST = 'prerender-manifest.json' diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 11602ec757a0375..ecbf133e549a305 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "12.2.6-canary.12", + "version": "12.3.1-canary.0", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index eb8a3c1762febb8..d66abadd4b70189 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "12.2.6-canary.12", + "version": "12.3.1-canary.0", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d204490f545aca6..5cbe4a27ab64d7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,7 @@ importers: '@types/http-proxy': 1.17.3 '@types/jest': 24.0.13 '@types/node': 13.11.0 + '@types/node-fetch': 2.6.1 '@types/react': 16.9.17 '@types/react-dom': 16.9.4 '@types/relay-runtime': 13.0.0 @@ -139,8 +140,8 @@ importers: react-17: npm:react@17.0.2 react-dom: 18.2.0 react-dom-17: npm:react-dom@17.0.2 - react-dom-exp: npm:react-dom@0.0.0-experimental-0de3ddf56-20220825 - react-exp: npm:react@0.0.0-experimental-0de3ddf56-20220825 + react-dom-exp: npm:react-dom@0.0.0-experimental-7028ce745-20220907 + react-exp: npm:react@0.0.0-experimental-7028ce745-20220907 react-ssr-prepass: 1.0.8 react-virtualized: 9.22.3 relay-compiler: 13.0.2 @@ -194,6 +195,7 @@ importers: '@types/http-proxy': 1.17.3 '@types/jest': 24.0.13 '@types/node': 13.11.0 + '@types/node-fetch': 2.6.1 '@types/react': 16.9.17 '@types/react-dom': 16.9.4 '@types/relay-runtime': 13.0.0 @@ -203,7 +205,7 @@ importers: '@types/trusted-types': 2.0.2 '@typescript-eslint/eslint-plugin': 4.29.1_qxyn66xcaddhgaahwkbomftvi4 '@typescript-eslint/parser': 4.29.1_6x3mpmmsttbpxxsctsorxedanu - '@vercel/fetch': 6.1.1_wbqoqouw2iimn65bqgaw3lwmza + '@vercel/fetch': 6.1.1_fii5qhbaymjqmfm7e2spxc5z4m '@webassemblyjs/ast': 1.11.1 '@webassemblyjs/floating-point-hex-parser': 1.11.1 '@webassemblyjs/helper-api-error': 1.11.1 @@ -293,8 +295,8 @@ importers: react-17: /react/17.0.2 react-dom: 18.2.0_react@18.2.0 react-dom-17: /react-dom/17.0.2_react@18.2.0 - react-dom-exp: /react-dom/0.0.0-experimental-0de3ddf56-20220825_react@18.2.0 - react-exp: /react/0.0.0-experimental-0de3ddf56-20220825 + react-dom-exp: /react-dom/0.0.0-experimental-7028ce745-20220907_react@18.2.0 + react-exp: /react/0.0.0-experimental-7028ce745-20220907 react-ssr-prepass: 1.0.8_qncsgtzehe3fgiqp6tr7lwq6fm react-virtualized: 9.22.3_biqbaboplfbrettd7655fr4n2y relay-compiler: 13.0.2 @@ -363,7 +365,7 @@ importers: packages/eslint-config-next: specifiers: - '@next/eslint-plugin-next': 12.2.6-canary.12 + '@next/eslint-plugin-next': 12.3.1-canary.0 '@rushstack/eslint-patch': ^1.1.3 '@typescript-eslint/parser': ^5.21.0 eslint-import-resolver-node: ^0.3.6 @@ -419,12 +421,12 @@ importers: '@hapi/accept': 5.0.2 '@napi-rs/cli': 2.7.0 '@napi-rs/triples': 1.1.0 - '@next/env': 12.2.6-canary.12 - '@next/polyfill-module': 12.2.6-canary.12 - '@next/polyfill-nomodule': 12.2.6-canary.12 - '@next/react-dev-overlay': 12.2.6-canary.12 - '@next/react-refresh-utils': 12.2.6-canary.12 - '@next/swc': 12.2.6-canary.12 + '@next/env': 12.3.1-canary.0 + '@next/polyfill-module': 12.3.1-canary.0 + '@next/polyfill-nomodule': 12.3.1-canary.0 + '@next/react-dev-overlay': 12.3.1-canary.0 + '@next/react-refresh-utils': 12.3.1-canary.0 + '@next/swc': 12.3.1-canary.0 '@segment/ajv-human-errors': 2.1.2 '@swc/helpers': 0.4.11 '@taskr/clear': 1.1.0 @@ -546,7 +548,7 @@ importers: raw-body: 2.4.1 react-is: 17.0.2 react-refresh: 0.12.0 - react-server-dom-webpack: 0.0.0-experimental-0de3ddf56-20220825 + react-server-dom-webpack: 0.0.0-experimental-7028ce745-20220907 regenerator-runtime: 0.13.4 sass-loader: 12.4.0 schema-utils2: npm:schema-utils@2.7.1 @@ -735,7 +737,7 @@ importers: raw-body: 2.4.1 react-is: 17.0.2 react-refresh: 0.12.0 - react-server-dom-webpack: 0.0.0-experimental-0de3ddf56-20220825_webpack@5.74.0 + react-server-dom-webpack: 0.0.0-experimental-7028ce745-20220907_webpack@5.74.0 regenerator-runtime: 0.13.4 sass-loader: 12.4.0_webpack@5.74.0 schema-utils2: /schema-utils/2.7.1 @@ -7275,16 +7277,6 @@ packages: form-data: 3.0.1 dev: true - /@types/node-fetch/2.6.2: - resolution: - { - integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==, - } - dependencies: - '@types/node': 17.0.21 - form-data: 3.0.1 - dev: true - /@types/node/10.12.18: resolution: { @@ -7890,7 +7882,7 @@ packages: - supports-color dev: true - /@vercel/fetch/6.1.1_wbqoqouw2iimn65bqgaw3lwmza: + /@vercel/fetch/6.1.1_fii5qhbaymjqmfm7e2spxc5z4m: resolution: { integrity: sha512-nddCkgpA0aVIqOlzh+qVlzDNcQq0cSnqefM+x6SciGI4GCvVZeaZ7WEowgX8I/HwBAq8Uj5Bdnd+r0+sYsJsig==, @@ -7900,7 +7892,7 @@ packages: node-fetch: '2' dependencies: '@types/async-retry': 1.2.1 - '@types/node-fetch': 2.6.2 + '@types/node-fetch': 2.6.1 '@vercel/fetch-cached-dns': 2.0.2_node-fetch@2.6.7 '@vercel/fetch-retry': 5.0.3_node-fetch@2.6.7 agentkeepalive: 3.4.1 @@ -24206,17 +24198,17 @@ packages: strip-json-comments: 2.0.1 dev: true - /react-dom/0.0.0-experimental-0de3ddf56-20220825_react@18.2.0: + /react-dom/0.0.0-experimental-7028ce745-20220907_react@18.2.0: resolution: { - integrity: sha512-d2pS60dObYaVw4cybVNlQ7JL+Mkpz6MVZo35KgsUzU4IUUywxrc09JJivOGiOyJTwTShITAgOvn/c/LZjlTpcg==, + integrity: sha512-vNt0UKStPB47hPjpk6I1JEnxjl3vb0VItCtpZolzcNtYlMyfDmZFInh6IGiMtfBpm6Fp1LCztfopAnuiYoEb3Q==, } peerDependencies: - react: 0.0.0-experimental-0de3ddf56-20220825 + react: 0.0.0-experimental-7028ce745-20220907 dependencies: loose-envify: 1.4.0 react: 18.2.0 - scheduler: 0.0.0-experimental-0de3ddf56-20220825 + scheduler: 0.0.0-experimental-7028ce745-20220907 dev: true /react-dom/17.0.2_react@18.2.0: @@ -24281,14 +24273,14 @@ packages: engines: { node: '>=0.10.0' } dev: true - /react-server-dom-webpack/0.0.0-experimental-0de3ddf56-20220825_webpack@5.74.0: + /react-server-dom-webpack/0.0.0-experimental-7028ce745-20220907_webpack@5.74.0: resolution: { - integrity: sha512-mowgFHsHjlGnL2YcedTXQMXmJaP3tKPjXjY3tyZVRVH7AaLRa+SMPEjx5GFCzz1nn6W0x/NQlCvqxV+F35nytg==, + integrity: sha512-DqVpIa9DdgQNre2urGq5brXRrgtd6jqCUO4Ax5yBAHvmWW5L2XkU3jI+YCMMZdxVwcP+NIJoB9mkIsFyIo8NAQ==, } engines: { node: '>=0.10.0' } peerDependencies: - react: 0.0.0-experimental-0de3ddf56-20220825 + react: 0.0.0-experimental-7028ce745-20220907 webpack: ^5.59.0 dependencies: acorn: 6.4.2 @@ -24330,10 +24322,10 @@ packages: react-lifecycles-compat: 3.0.4 dev: true - /react/0.0.0-experimental-0de3ddf56-20220825: + /react/0.0.0-experimental-7028ce745-20220907: resolution: { - integrity: sha512-4KH+Ylv+P0SsKAjjpGALztqHBkqoh01GFh8hJkCAPJ7fAx6yLDY7i4XeoDutqo6okWnJncq7ixUoyru4hXFn4A==, + integrity: sha512-GJkc7NNWnUV7imij/n8YxJTQEY+OsE8qSmHB4esfFV/Gdi5zFOtzXr9LEB8CItAIoIpZ0eK1LXcOAO3cenDzLQ==, } engines: { node: '>=0.10.0' } dependencies: @@ -25663,10 +25655,10 @@ packages: xmlchars: 2.2.0 dev: true - /scheduler/0.0.0-experimental-0de3ddf56-20220825: + /scheduler/0.0.0-experimental-7028ce745-20220907: resolution: { - integrity: sha512-cMaG6fmnYZvH2C5LuMGw//oHBskpicjdJ+GbLs7nAQLiuYl6sXzmqQeGgDLJvd76hcBz2aK2tEKZMHrmwuWVcA==, + integrity: sha512-vmk4QI8sXQYFk7rtB9omwlsyNOhdsai4BKvud+XE+CfZuI5iTV+Nzj2EQq19PcfS7cmf1UOQ83wgv4yGgsefUQ==, } dependencies: loose-envify: 1.4.0 diff --git a/test/e2e/app-dir/app/next.config.js b/test/e2e/app-dir/app/next.config.js index 087742808cea756..0e04741a08bf144 100644 --- a/test/e2e/app-dir/app/next.config.js +++ b/test/e2e/app-dir/app/next.config.js @@ -4,6 +4,9 @@ module.exports = { serverComponents: true, legacyBrowsers: false, browsersListForSwc: true, + sri: { + algorithm: 'sha256', + }, }, // assetPrefix: '/assets', rewrites: async () => { diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 6ff89b27588809b..e92332aa0c8ca25 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1,4 +1,5 @@ import { createNext, FileRef } from 'e2e-utils' +import crypto from 'crypto' import { NextInstance } from 'test/lib/next-modes/base' import { check, fetchViaHTTP, renderViaHTTP, waitFor } from 'next-test-utils' import path from 'path' @@ -1194,6 +1195,128 @@ describe('app dir', () => { }) }) }) + ;(isDev ? describe.skip : describe)('Subresource Integrity', () => { + function fetchWithPolicy(policy: string | null) { + return fetchViaHTTP(next.url, '/dashboard', undefined, { + headers: policy + ? { + 'Content-Security-Policy': policy, + } + : {}, + }) + } + + async function renderWithPolicy(policy: string | null) { + const res = await fetchWithPolicy(policy) + + expect(res.ok).toBe(true) + + const html = await res.text() + + return cheerio.load(html) + } + + it('does not include nonce when not enabled', async () => { + const policies = [ + `script-src 'nonce-'`, // invalid nonce + 'style-src "nonce-cmFuZG9tCg=="', // no script or default src + '', // empty string + ] + + for (const policy of policies) { + const $ = await renderWithPolicy(policy) + + // Find all the script tags without src attributes and with nonce + // attributes. + const elements = $('script[nonce]:not([src])') + + // Expect there to be none. + expect(elements.length).toBe(0) + } + }) + + it('includes a nonce value with inline scripts when Content-Security-Policy header is defined', async () => { + // A random nonce value, base64 encoded. + const nonce = 'cmFuZG9tCg==' + + // Validate all the cases where we could parse the nonce. + const policies = [ + `script-src 'nonce-${nonce}'`, // base case + ` script-src 'nonce-${nonce}' `, // extra space added around sources and directive + `style-src 'self'; script-src 'nonce-${nonce}'`, // extra directives + `script-src 'self' 'nonce-${nonce}' 'nonce-othernonce'`, // extra nonces + `default-src 'nonce-othernonce'; script-src 'nonce-${nonce}';`, // script and then fallback case + `default-src 'nonce-${nonce}'`, // fallback case + ] + + for (const policy of policies) { + const $ = await renderWithPolicy(policy) + + // Find all the script tags without src attributes. + const elements = $('script:not([src])') + + // Expect there to be at least 1 script tag without a src attribute. + expect(elements.length).toBeGreaterThan(0) + + // Expect all inline scripts to have the nonce value. + elements.each((i, el) => { + expect(el.attribs['nonce']).toBe(nonce) + }) + } + }) + + it('includes an integrity attribute on scripts', async () => { + const html = await renderViaHTTP(next.url, '/dashboard') + + const $ = cheerio.load(html) + + // Find all the script tags with src attributes. + const elements = $('script[src]') + + // Expect there to be at least 1 script tag with a src attribute. + expect(elements.length).toBeGreaterThan(0) + + // Collect all the scripts with integrity hashes so we can verify them. + const files: [string, string][] = [] + + // For each of these attributes, ensure that there's an integrity + // attribute and starts with the correct integrity hash prefix. + elements.each((i, el) => { + const integrity = el.attribs['integrity'] + expect(integrity).toBeDefined() + expect(integrity).toStartWith('sha256-') + + const src = el.attribs['src'] + expect(src).toBeDefined() + + files.push([src, integrity]) + }) + + // For each script tag, ensure that the integrity attribute is the + // correct hash of the script tag. + for (const [src, integrity] of files) { + const res = await fetchViaHTTP(next.url, src) + expect(res.status).toBe(200) + const content = await res.text() + + const hash = crypto + .createHash('sha256') + .update(content) + .digest() + .toString('base64') + + expect(integrity).toEndWith(hash) + } + }) + + it('throws when escape characters are included in nonce', async () => { + const res = await fetchWithPolicy( + `script-src 'nonce-">"'` + ) + + expect(res.status).toBe(500) + }) + }) } describe('without assetPrefix', () => { diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index c9996fe5ec0d286..0debb6818399675 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -340,6 +340,20 @@ describe('app dir - react server components', () => { expect(head).toMatch(/{color:(\s*)blue;?}/) }) + it('should stick to the url without trailing /page suffix', async () => { + const browser = await webdriver(next.url, '/edge/dynamic') + const indexUrl = await browser.url() + + await browser.loadPage(`${next.url}/edge/dynamic/123`, { + disableCache: false, + beforePageLoad: null, + }) + + const dynamicRouteUrl = await browser.url() + expect(indexUrl).toBe(`${next.url}/edge/dynamic`) + expect(dynamicRouteUrl).toBe(`${next.url}/edge/dynamic/123`) + }) + it('should support streaming for flight response', async () => { await fetchViaHTTP(next.url, '/?__flight__=1').then(async (response) => { const result = await resolveStreamResponse(response) diff --git a/test/e2e/app-dir/rsc-basic/app/edge/dynamic/[id]/page.server.js b/test/e2e/app-dir/rsc-basic/app/edge/dynamic/[id]/page.server.js new file mode 100644 index 000000000000000..87bac2ff6be9ec8 --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/app/edge/dynamic/[id]/page.server.js @@ -0,0 +1,7 @@ +export default function page() { + return 'dynamic route [id] page' +} + +export const config = { + runtime: 'experimental-edge', +} diff --git a/test/e2e/app-dir/rsc-basic/app/edge/dynamic/page.server.js b/test/e2e/app-dir/rsc-basic/app/edge/dynamic/page.server.js new file mode 100644 index 000000000000000..1b83f0120a6ab43 --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/app/edge/dynamic/page.server.js @@ -0,0 +1,7 @@ +export default function page() { + return 'dynamic route index page' +} + +export const config = { + runtime: 'experimental-edge', +} diff --git a/test/e2e/app-dir/rsc-basic/next.config.js b/test/e2e/app-dir/rsc-basic/next.config.js index ca13fceaff38c09..eab44b6855ae317 100644 --- a/test/e2e/app-dir/rsc-basic/next.config.js +++ b/test/e2e/app-dir/rsc-basic/next.config.js @@ -7,4 +7,14 @@ module.exports = { appDir: true, serverComponents: true, }, + rewrites: async () => { + return { + afterFiles: [ + { + source: '/rewritten-to-edge-dynamic', + destination: '/edge/dynamic', + }, + ], + } + }, } diff --git a/test/integration/image-future/default/pages/on-loading-complete.js b/test/integration/image-future/default/pages/on-loading-complete.js index b89d655268e37c2..da60e069f31956e 100644 --- a/test/integration/image-future/default/pages/on-loading-complete.js +++ b/test/integration/image-future/default/pages/on-loading-complete.js @@ -105,13 +105,15 @@ function ImageWithMessage({ id, idToCount, setIdToCount, ...props }) {
{ + onLoadingComplete={(img) => { + const { naturalWidth, naturalHeight, nodeName } = img let count = idToCount[id] || 0 count++ idToCount[id] = count setIdToCount(idToCount) + const name = nodeName.toLocaleLowerCase() setMsg( - `loaded ${count} img${id} with dimensions ${naturalWidth}x${naturalHeight}` + `loaded ${count} ${name}${id} with dimensions ${naturalWidth}x${naturalHeight}` ) }} {...props} diff --git a/test/integration/image-optimizer/test/util.ts b/test/integration/image-optimizer/test/util.ts index aa37a5aef7c78d4..0937682c742c607 100644 --- a/test/integration/image-optimizer/test/util.ts +++ b/test/integration/image-optimizer/test/util.ts @@ -15,6 +15,7 @@ import { waitFor, } from 'next-test-utils' import isAnimated from 'next/dist/compiled/is-animated' +import type { RequestInit } from 'node-fetch' const largeSize = 1080 // defaults defined in server/config.ts const sharpMissingText = `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended` @@ -115,10 +116,15 @@ async function expectAvifSmallerThanWebp(w, q, appPort) { expect(avif).toBeLessThanOrEqual(webp) } -async function fetchWithDuration(...args) { - console.warn('Fetching', args[1], args[2]) +async function fetchWithDuration( + appPort: string | number, + pathname: string, + query?: Record | string, + opts?: RequestInit +) { + console.warn('Fetching', pathname, query) const start = Date.now() - const res = await fetchViaHTTP(...args) + const res = await fetchViaHTTP(appPort, pathname, query, opts) const buffer = await res.buffer() const duration = Date.now() - start return { duration, buffer, res } @@ -140,7 +146,10 @@ export function runTests(ctx) { slowImageServer.port }/slow.png?delay=${1}&status=308` const query = { url, w: ctx.w, q: 39 } - const opts = { headers: { accept: 'image/webp' }, redirect: 'manual' } + const opts: RequestInit = { + headers: { accept: 'image/webp' }, + redirect: 'manual', + } const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) expect(res.status).toBe(500) diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index d46441e156f38b4..6d07d22915e0827 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -83,6 +83,12 @@ export function initNextServerScript( }) } +/** + * @param {string | number} appPortOrUrl + * @param {string} [url] + * @param {string} [hostname] + * @returns + */ export function getFullUrl(appPortOrUrl, url, hostname) { let fullUrl = typeof appPortOrUrl === 'string' && appPortOrUrl.startsWith('http') @@ -110,11 +116,24 @@ export function renderViaAPI(app, pathname, query) { return app.renderToHTML({ url }, {}, pathname, query) } +/** + * @param {string | number} appPort + * @param {string} pathname + * @param {Record | string | undefined} [query] + * @param {import('node-fetch').RequestInit} [opts] + * @returns {Promise} + */ export function renderViaHTTP(appPort, pathname, query, opts) { return fetchViaHTTP(appPort, pathname, query, opts).then((res) => res.text()) } -/** @return {Promise} */ +/** + * @param {string | number} appPort + * @param {string} pathname + * @param {Record | string | undefined} [query] + * @param {import('node-fetch').RequestInit} [opts] + * @returns {Promise} + */ export function fetchViaHTTP(appPort, pathname, query, opts) { const url = `${pathname}${ typeof query === 'string' ? query : query ? `?${qs.stringify(query)}` : '' diff --git a/test/production/required-server-files-i18n.test.ts b/test/production/required-server-files-i18n.test.ts index 17c5df1c80dff2a..8d290d779df5390 100644 --- a/test/production/required-server-files-i18n.test.ts +++ b/test/production/required-server-files-i18n.test.ts @@ -171,7 +171,7 @@ describe('should set-up next', () => { await next.patchFile('standalone/data.txt', 'show') const res = await fetchViaHTTP(appPort, '/gsp', undefined, { - redirect: 'manual ', + redirect: 'manual', }) expect(res.status).toBe(200) expect(res.headers.get('cache-control')).toBe( @@ -182,7 +182,7 @@ describe('should set-up next', () => { await next.patchFile('standalone/data.txt', 'hide') const res2 = await fetchViaHTTP(appPort, '/gsp', undefined, { - redirect: 'manual ', + redirect: 'manual', }) expect(res2.status).toBe(404) expect(res2.headers.get('cache-control')).toBe( @@ -194,7 +194,7 @@ describe('should set-up next', () => { await next.patchFile('standalone/data.txt', 'show') const res = await fetchViaHTTP(appPort, '/gssp', undefined, { - redirect: 'manual ', + redirect: 'manual', }) expect(res.status).toBe(200) expect(res.headers.get('cache-control')).toBe( @@ -204,7 +204,7 @@ describe('should set-up next', () => { await next.patchFile('standalone/data.txt', 'hide') const res2 = await fetchViaHTTP(appPort, '/gssp', undefined, { - redirect: 'manual ', + redirect: 'manual', }) await next.patchFile('standalone/data.txt', 'show') diff --git a/test/production/required-server-files.test.ts b/test/production/required-server-files.test.ts index 1d97b5bc5be3c35..de7d1bcc26c32de 100644 --- a/test/production/required-server-files.test.ts +++ b/test/production/required-server-files.test.ts @@ -422,7 +422,7 @@ describe('should set-up next', () => { await next.patchFile('standalone/data.txt', 'show') const res = await fetchViaHTTP(appPort, '/gsp', undefined, { - redirect: 'manual ', + redirect: 'manual', }) expect(res.status).toBe(200) expect(res.headers.get('cache-control')).toBe( @@ -433,7 +433,7 @@ describe('should set-up next', () => { await next.patchFile('standalone/data.txt', 'hide') const res2 = await fetchViaHTTP(appPort, '/gsp', undefined, { - redirect: 'manual ', + redirect: 'manual', }) expect(res2.status).toBe(404) expect(res2.headers.get('cache-control')).toBe( @@ -445,7 +445,7 @@ describe('should set-up next', () => { await next.patchFile('standalone/data.txt', 'show') const res = await fetchViaHTTP(appPort, '/gssp', undefined, { - redirect: 'manual ', + redirect: 'manual', }) expect(res.status).toBe(200) expect(res.headers.get('cache-control')).toBe( @@ -455,7 +455,7 @@ describe('should set-up next', () => { await next.patchFile('standalone/data.txt', 'hide') const res2 = await fetchViaHTTP(appPort, '/gssp', undefined, { - redirect: 'manual ', + redirect: 'manual', }) await next.patchFile('standalone/data.txt', 'show')