diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b601a1f32f53d36..eacdc887f86c582 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,6 +4,7 @@ * @timneutkens @ijjk @shuding @huozhi /.github/ @timneutkens @ijjk @shuding @styfle @huozhi @padmaia @balazsorban44 /docs/ @timneutkens @ijjk @shuding @styfle @huozhi @padmaia @leerob @balazsorban44 +/errors/ @balazsorban44 /examples/ @timneutkens @ijjk @shuding @leerob @steven-tey @balazsorban44 # SWC Build & Telemetry (@padmaia) diff --git a/.github/workflows/validate_issue.yml b/.github/workflows/validate_issue.yml index 3c6e3a4a2e43a32..abd6c0784d5f156 100644 --- a/.github/workflows/validate_issue.yml +++ b/.github/workflows/validate_issue.yml @@ -15,4 +15,3 @@ jobs: run: node ./.github/actions/issue-validator/index.mjs env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEBUG: 1 diff --git a/contributing.md b/contributing.md index b3ec843cf2e71a2..e5403bdef11bae9 100644 --- a/contributing.md +++ b/contributing.md @@ -147,7 +147,7 @@ There are two options to develop with your local version of the codebase: with: ```json - "next": "file:/path/to/next.js/packages/next", + "next": "link:/path/to/next.js/packages/next", ``` 2. In your app's root directory, make sure to remove `next` from `node_modules` with: diff --git a/docs/advanced-features/compiler.md b/docs/advanced-features/compiler.md index 3f2bb69033ef778..6708b07d693d26a 100644 --- a/docs/advanced-features/compiler.md +++ b/docs/advanced-features/compiler.md @@ -9,6 +9,7 @@ description: Learn about the Next.js Compiler, written in Rust, which transforms | Version | Changes | | --------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `v12.3.0` | SWC Minifier [stable](https://nextjs.org/blog/next-12-3#swc-minifier-stable). | | `v12.2.0` | [SWC Plugins](#swc-plugins-Experimental) experimental support added. | | `v12.1.0` | Added support for Styled Components, Jest, Relay, Remove React Properties, Legacy Decorators, Remove Console, and jsxImportSource. | | `v12.0.0` | Next.js Compiler [introduced](https://nextjs.org/blog/next-12). | @@ -238,8 +239,6 @@ module.exports = { Only `importMap` in `@emotion/babel-plugin` is not supported for now. -## Experimental Features - ### Minification You can opt-in to using the Next.js compiler for minification. This is 7x faster than Terser. @@ -254,6 +253,8 @@ module.exports = { If you have feedback about `swcMinify`, please share it on the [feedback discussion](https://github.com/vercel/next.js/discussions/30237). +## Experimental Features + ### Minifier debug options While the minifier is experimental, we are making the following options available for debugging purposes. They will not be available once the minifier is made stable. diff --git a/docs/advanced-features/dynamic-import.md b/docs/advanced-features/dynamic-import.md index f80aa5b831e783f..118165d399cc5f4 100644 --- a/docs/advanced-features/dynamic-import.md +++ b/docs/advanced-features/dynamic-import.md @@ -42,7 +42,7 @@ If you are not using React 18, you can use the `loading` attribute in place of t ```jsx const DynamicHeader = dynamic(() => import('../components/header'), { - loading: () =>
, + loading: () =>
Loading...
, }) ``` diff --git a/docs/advanced-features/middleware.md b/docs/advanced-features/middleware.md index 22dd0988b8163c5..7812da86b3aaf13 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 @@ -88,6 +88,22 @@ export const config = { } ``` +The `matcher` config allows full regex so matching like negative lookaheads or character matching is supported. An example of a negative lookahead to match all except specific paths can be seen here: + +```js +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - static (static files) + * - favicon.ico (favicon file) + */ + '/((?!api|static|favicon.ico).*)', + ], +} +``` + > **Note:** The `matcher` values need to be constants so they can be statically analyzed at build-time. Dynamic values such as variables will be ignored. Configured matchers: @@ -101,8 +117,6 @@ Read more details on [path-to-regexp](https://github.com/pillarjs/path-to-regexp > **Note:** For backward compatibility, Next.js always considers `/public` as `/public/index`. Therefore, a matcher of `/public/:path` will match. -> **Note:** It is not possible to exclude middleware from matching static path starting with `_next/`. This allow enforcing security with middleware. - ### Conditional Statements ```typescript diff --git a/docs/advanced-features/react-18/server-components.md b/docs/advanced-features/react-18/server-components.md index 906dab699bff2da..027a7d6690b1d75 100644 --- a/docs/advanced-features/react-18/server-components.md +++ b/docs/advanced-features/react-18/server-components.md @@ -1,116 +1,27 @@ # React Server Components (RFC) -Server Components allow us to render React components on the server. This is fundamentally different from server-side rendering (SSR) where you're pre-generating HTML on the server. With Server Components, there's **zero client-side JavaScript needed,** making page rendering faster. This improves the user experience of your application, pairing the best parts of server-rendering with client-side interactivity. +React Server Components allow developers to build applications that span the server and client, combining the rich interactivity of client-side apps with the improved performance of traditional server rendering. -### Next Router and Layouts RFC +In an upcoming Next.js release, React and Next.js developers will be able to use Server Components inside the `app` directory as part of the changes outlined by the [Layouts RFC](https://nextjs.org/blog/layouts-rfc). -We are currently implementing the [Next.js Router and Layouts RFC](https://nextjs.org/blog/layouts-rfc). +## What are React Server Components? -The new Next.js router will be built on top of React 18 features, including React Server Components. +React Server Components improve the user experience of your application by pairing the best parts of server-rendering with client-side interactivity. -One of the biggest proposed changes is that, by default, files inside a new `app` directory will be rendered on the server as React Server Components. +With traditional React applications that are client-side only, developers often had to make tradeoffs between SEO and performance. Server Components enable developers to better leverage their server infrastructure and achieve great performance by default. -This will allow you to automatically adopt React Server Components when migrating from `pages` to `app`. +For example, large dependencies that previously would impact the JavaScript bundle size on the client can instead stay entirely on the server. By sending less JavaScript to the browser, the time to interactive for the page is decreased, leading to improved [Core Web Vitals](https://vercel.com/blog/core-web-vitals). -You can find more information on the [RFC](https://nextjs.org/blog/layouts-rfc) and we welcome your feedback on [Github Discussions](https://github.com/vercel/next.js/discussions/37136). +## React Server Components vs Server-Side Rendering -### Server Components Conventions +[Server-side Rendering](/docs/basic-features/pages.md#server-side-rendering) (SSR) dynamically builds your application into HTML on the server. This creates faster load times for users by offloading work from the user's device to the server, especially those with slower internet connections or older devices. However, developers still pay the cost to download, parse, and hydrate those components after the initial HTML loads. -To run a component on the server, append `.server.js` to the end of the filename. For example, `./pages/home.server.js` will be treated as a Server Component. +React Server Components, combined with Next.js server-side rendering, help eliminate the tradeoff of all-or-nothing data fetching. You can progressively show updates as your data comes in. -For client components, append `.client.js` to the filename. For example, `./components/avatar.client.js`. +## Using React Server Components with Next.js -Server components can import server components and client components. +The Next.js team at Vercel released the [Layouts RFC](https://nextjs.org/blog/layouts-rfc) a few months ago outlining the vision for the future of routing, layouts, and data fetching in the framework. These changes **aren't available yet**, but we can start learning about how they will be used. -Client components **cannot** import server components. +Pages and Layouts in `app` will be rendered as React Server Components by default. This improves performance by reducing the amount of JavaScript sent to the client for components that are not interactive. Client components will be able to be defined through either a file name extension or through a string literal in the file. -Components without a `server` or `client` extension will be treated as shared components and can be imported by server components and client components. For example: - -```jsx -// pages/home.server.js - -import { Suspense } from 'react' - -import Profile from '../components/profile.server' -import Content from '../components/content.client' - -export default function Home() { - return ( -
-

Welcome to React Server Components

- - - - -
- ) -} -``` - -The `` and `` components will always be server-side rendered and streamed to the client, and will not be included by the client-side JavaScript. However, `` will still be hydrated on the client-side, like normal React components. - -> Make sure you're using default imports and exports for server components (`.server.js`). The support of named exports are a work in progress! - -To see a full example, check out the [vercel/next-react-server-components demo](https://github.com/vercel/next-react-server-components). - -## Supported Next.js APIs - -### `next/link` and `next/image` - -You can use `next/link` and `next/image` like before and they will be treated as client components to keep the interaction on client side. - -### `next/document` - -If you have a custom `_document`, you have to change your `_document` to a functional component like below to use server components. If you don't have one, Next.js will use the default `_document` component for you. - -```jsx -// pages/_document.js -import { Html, Head, Main, NextScript } from 'next/document' - -export default function Document() { - return ( - - - -
- - - - ) -} -``` - -### `next/app` - -The usage of `_app.js` is the same as [Custom App](/docs/advanced-features/custom-app). Using custom app as server component such as `_app.server.js` is not recommended, to keep align with non server components apps for client specific things like global CSS imports. - -### Routing - -Both basic routes with path and queries and dynamic routes are supported. If you need to access the router in server components(`.server.js`), they will receive `router` instance as a prop so that you can directly access them without using the `useRouter()` hook. - -```jsx -// pages/index.server.js - -export default function Index({ router }) { - // You can access routing information by `router.pathname`, etc. - return 'hello' -} -``` - -### Unsupported Next.js APIs - -While RSC and SSR streaming are still in the alpha stage, not all Next.js APIs are supported. The following Next.js APIs have limited functionality within Server Components. React 18 use without SSR streaming is not affected. - -#### React internals - -Most React hooks, such as `useContext`, `useState`, `useReducer`, `useEffect` and `useLayoutEffect`, are not supported as of today since server components are executed per request and aren't stateful. - -#### Data Fetching & Styling - -Like streaming SSR, styling and data fetching within `Suspense` on the server side are not well supported. We're still working on them. - -Page level exported methods like `getInitialProps`, `getStaticProps` and `getStaticPaths` are not supported. - -#### `next/head` and I18n - -We are still working on support for these features. +We will be providing more updates about Server Components usage in Next.js soon. diff --git a/docs/advanced-features/using-mdx.md b/docs/advanced-features/using-mdx.md index 6ed9b3f5756f0c0..859db74188aaf9c 100644 --- a/docs/advanced-features/using-mdx.md +++ b/docs/advanced-features/using-mdx.md @@ -33,7 +33,7 @@ The following steps outline how to setup `@next/mdx` in your Next.js project: 1. Install the required packages: ```bash - npm install @next/mdx @mdx-js/loader + npm install @next/mdx @mdx-js/loader @mdx-js/react ``` 2. Require the package and configure to support top level `.mdx` pages. The following adds the `options` object key allowing you to pass in any plugins: diff --git a/docs/api-reference/edge-runtime.md b/docs/api-reference/edge-runtime.md index c62a84973bf4fd3..3a6bccc0ae73520 100644 --- a/docs/api-reference/edge-runtime.md +++ b/docs/api-reference/edge-runtime.md @@ -136,6 +136,25 @@ The following JavaScript language features are disabled, and **will not work:** - `eval`: Evaluates JavaScript code represented as a string - `new Function(evalString)`: Creates a new function with the code provided as an argument +- `WebAssembly.compile` +- `WebAssembly.instantiate` with [a buffer parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code) + +In rare cases, your code could contain (or import) some dynamic code evaluation statements which _can not be reached at runtime_ and which can not be removed by treeshaking. +You can relax the check to allow specific files with your Middleware or Edge API Route exported configuration: + +```javascript +export const config = { + runtime: 'experimental-edge', // for Edge API Routes only + unstable_allowDynamic: [ + '/lib/utilities.js', // allows a single file + '/node_modules/function-bind/**', // use a glob to allow anything in the function-bind 3rd party module + ], +} +``` + +`unstable_allowDynamic` is a [glob](https://github.com/micromatch/micromatch#matching-features), or an array of globs, ignoring dynamic code evaluation for specific files. The globs are relative to your application root folder. + +Be warned that if these statements are executed on the Edge, _they will throw and cause a runtime error_. ## Related 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/api-reference/next/server.md b/docs/api-reference/next/server.md index e2f5c8e6fca000a..5e83b10835e2150 100644 --- a/docs/api-reference/next/server.md +++ b/docs/api-reference/next/server.md @@ -10,7 +10,7 @@ description: Learn about the server-only helpers for Middleware and Edge API Rou The `NextRequest` object is an extension of the native [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) interface, with the following added methods and properties: -- `cookies` - A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with cookies from the `Request`. See [Using cookies in Edge Middleware](#using-cookies-in-edge-middleware) +- `cookies` - A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with cookies from the `Request`. See [Using cookies in Middleware](/docs/advanced-features/middleware#using-cookies) - `nextUrl`: Includes an extended, parsed, URL object that gives you access to Next.js specific properties such as `pathname`, `basePath`, `trailingSlash` and `i18n`. Includes the following properties: - `basePath` (`string`) - `buildId` (`string || undefined`) 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/basic-features/script.md b/docs/basic-features/script.md index 33df31800f437d1..0f92fad1c3d2097 100644 --- a/docs/basic-features/script.md +++ b/docs/basic-features/script.md @@ -19,10 +19,11 @@ description: Next.js helps you optimize loading third-party scripts with the bui
Version History -| Version | Changes | -| --------- | ------------------------- | -| `v12.2.4` | `onReady` prop added. | -| `v11.0.0` | `next/script` introduced. | +| Version | Changes | +| --------- | ------------------------------------------------------------------------- | +| `v12.2.4` | `onReady` prop added. | +| `v12.2.2` | Allow `next/script` with `beforeInteractive` to be placed in `_document`. | +| `v11.0.0` | `next/script` introduced. |
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/edge-dynamic-code-evaluation.md b/errors/edge-dynamic-code-evaluation.md new file mode 100644 index 000000000000000..29a5e74b6329fb5 --- /dev/null +++ b/errors/edge-dynamic-code-evaluation.md @@ -0,0 +1,34 @@ +# Dynamic code evaluation is not available in Middlewares or Edge API Routes + +#### Why This Error Occurred + +`eval()`, `new Function()` or compiling WASM binaries dynamically is not allowed in Middlewares or Edge API Routes. +Specifically, the following APIs are not supported: + +- `eval()` +- `new Function()` +- `WebAssembly.compile` +- `WebAssembly.instantiate` with [a buffer parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code) + +#### Possible Ways to Fix It + +You can bundle your WASM binaries using `import`: + +```typescript +import { NextResponse } from 'next/server' +import squareWasm from './square.wasm?module' + +export default async function middleware() { + const m = await WebAssembly.instantiate(squareWasm) + const answer = m.exports.square(9) + + const response = NextResponse.next() + response.headers.set('x-square', answer.toString()) + return response +} +``` + +In rare cases, your code could contain (or import) some dynamic code evaluation statements which _can not be reached at runtime_ and which can not be removed by treeshaking. +You can relax the check to allow specific files with your Middleware or Edge API Route exported [configuration](https://nextjs.org/docs/api-reference/edge-runtime#unsupported-apis). + +Be warned that if these statements are executed on the Edge, _they will throw and cause a runtime error_. diff --git a/errors/invalid-page-config.md b/errors/invalid-page-config.md index c9feeb38dbaa4f2..a535a29056c7dd0 100644 --- a/errors/invalid-page-config.md +++ b/errors/invalid-page-config.md @@ -7,7 +7,7 @@ In one of your pages or API Routes you did `export const config` with an invalid #### Possible Ways to Fix It The page's config must be an object initialized directly when being exported and not modified dynamically. -The config object must only contains static constant literals without expressions. +The config object must only contain static constant literals without expressions. diff --git a/errors/large-page-data.md b/errors/large-page-data.md index 9969b380299e8a3..6a195072f43fbbd 100644 --- a/errors/large-page-data.md +++ b/errors/large-page-data.md @@ -8,6 +8,12 @@ One of your pages includes a large amount of page data (>= 128kB). This can nega Reduce the amount of data returned from `getStaticProps`, `getServerSideProps`, or `getInitialProps` to only the essential data to render the page. The default threshold of 128kB can be configured in `largePageDataBytes` if absolutely necessary and the performance implications are understood. +To inspect the props passed to your page, you can inspect the below element's content in your browser devtools: + +```sh +document.getElementById("__NEXT_DATA__").text +``` + ### Useful Links - [Data Fetching Documentation](https://nextjs.org/docs/basic-features/data-fetching/overview) diff --git a/errors/manifest.json b/errors/manifest.json index 3f013867de559b8..41bb35c7d6004b5 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -714,6 +714,10 @@ "title": "middleware-dynamic-wasm-compilation", "path": "/errors/middleware-dynamic-wasm-compilation.md" }, + { + "title": "edge-dynamic-code-evaluation", + "path": "/errors/edge-dynamic-code-evaluation.md" + }, { "title": "node-module-in-edge-runtime", "path": "/errors/node-module-in-edge-runtime.md" @@ -729,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/middleware-dynamic-wasm-compilation.md b/errors/middleware-dynamic-wasm-compilation.md index 7b5272a41d505bd..ffe3b85546f4600 100644 --- a/errors/middleware-dynamic-wasm-compilation.md +++ b/errors/middleware-dynamic-wasm-compilation.md @@ -19,8 +19,8 @@ import squareWasm from './square.wasm?module' export default async function middleware() { const m = await WebAssembly.instantiate(squareWasm) const answer = m.exports.square(9) - const response = NextResponse.next() + response.headers.set('x-square', answer.toString()) return response } 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-` - ) + if (flightResponseRef.current) { + return flightResponseRef.current + } + + const [renderStream, forwardStream] = readableStreamTee(req) + flightResponseRef.current = createFromReadableStream(renderStream, { + moduleMap: serverComponentManifest.__ssr_module_mapping__, + }) + + let bootstrapped = false + // We only attach CSS chunks to the inlined data. + const forwardReader = forwardStream.getReader() + const writer = writable.getWriter() + const startScriptTag = nonce + ? `` ) - } - if (done) { - rscCache.delete(id) - writer.close() - } else { - const responsePartial = decodeText(value) - const scripts = `` - - writer.write(encodeText(scripts)) - process() - } - }) - } - process() + ) + } + if (done) { + flightResponseRef.current = null + writer.close() + } else { + const responsePartial = decodeText(value) + const scripts = `${startScriptTag}(self.__next_s=self.__next_s||[]).push(${htmlEscapeJsonString( + JSON.stringify([1, responsePartial]) + )})` + + writer.write(encodeText(scripts)) + process() + } + }) } - return entry + process() + + return flightResponseRef.current } /** @@ -194,18 +200,17 @@ function createServerComponentRenderer( } }, { - cachePrefix, transformStream, serverComponentManifest, serverContexts, }: { - cachePrefix: string transformStream: TransformStream serverComponentManifest: NonNullable 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. @@ -233,14 +238,17 @@ function createServerComponentRenderer( return RSCStream } + const flightResponseRef = { current: null } + const writable = transformStream.writable return function ServerComponentWrapper() { const reqStream = createRSCStream() const response = useFlightResponse( writable, - cachePrefix, reqStream, - serverComponentManifest + serverComponentManifest, + flightResponseRef, + nonce ) return response.readRoot() } @@ -283,12 +291,7 @@ export type Segment = type LoaderTree = [ segment: string, parallelRoutes: { [parallelRouterKey: string]: LoaderTree }, - components: { - filePath: string - layout?: () => any - loading?: () => any - page?: () => any - } + components: ComponentsType ] /** @@ -406,6 +409,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 +479,7 @@ export async function renderToHTMLOrFlight( const { buildManifest, + subresourceIntegrityManifest, serverComponentManifest, serverCSSManifest = {}, supportsDynamicHTML, @@ -466,6 +520,8 @@ export async function renderToHTMLOrFlight( const pageIsDynamic = isDynamicRoute(pathname) const LayoutRouter = ComponentMod.LayoutRouter as typeof import('../client/components/layout-router.client').default + const RenderFromTemplateContext = + ComponentMod.RenderFromTemplateContext as typeof import('../client/components/render-from-template-context.client').default const HotReloader = ComponentMod.HotReloader as | typeof import('../client/components/hot-reloader.client').default | null @@ -596,7 +652,11 @@ export async function renderToHTMLOrFlight( */ const createComponentTree = async ({ createSegmentPath, - loaderTree: [segment, parallelRoutes, { filePath, layout, loading, page }], + loaderTree: [ + segment, + parallelRoutes, + { layoutOrPagePath, layout, template, error, loading, page }, + ], parentParams, firstItem, rootLayoutIncluded, @@ -608,11 +668,17 @@ export async function renderToHTMLOrFlight( firstItem?: boolean }): Promise<{ Component: React.ComponentType }> => { // TODO-APP: enable stylesheet per layout/page - const stylesheets = getCssInlinedLinkTags( - serverComponentManifest, - serverCSSManifest!, - filePath - ) + const stylesheets: string[] = layoutOrPagePath + ? getCssInlinedLinkTags( + serverComponentManifest, + serverCSSManifest!, + layoutOrPagePath + ) + : [] + const Template = template + ? await interopDefault(template()) + : React.Fragment + const ErrorComponent = error ? await interopDefault(error()) : undefined const Loading = loading ? await interopDefault(loading()) : undefined const isLayout = typeof layout !== 'undefined' const isPage = typeof page !== 'undefined' @@ -688,6 +754,12 @@ export async function renderToHTMLOrFlight( parallelRouterKey={parallelRouteKey} segmentPath={createSegmentPath(currentSegmentPath)} loading={Loading ? : undefined} + error={ErrorComponent} + template={ + + } childProp={childProp} rootLayoutIncluded={rootLayoutIncludedAtThisLevelOrAbove} />, @@ -711,13 +783,21 @@ export async function renderToHTMLOrFlight( : childSegment, } + const segmentPath = createSegmentPath(currentSegmentPath) + // This is turned back into an object below. return [ parallelRouteKey, : undefined} + template={ + + } childProp={childProp} rootLayoutIncluded={rootLayoutIncludedAtThisLevelOrAbove} />, @@ -999,6 +1079,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. @@ -1023,11 +1110,11 @@ export async function renderToHTMLOrFlight( }, ComponentMod, { - cachePrefix: initialCanonicalUrl, transformStream: serverComponentsInlinedTransformStream, serverComponentManifest, serverContexts, - } + }, + nonce ) const flushEffectsCallbacks: Set<() => React.ReactNode> = new Set() @@ -1076,23 +1163,61 @@ export async function renderToHTMLOrFlight( return flushed } - const renderStream = await renderToInitialStream({ - ReactDOMServer, - element: content, - streamOptions: { - // Include hydration scripts in the HTML - bootstrapScripts: buildManifest.rootMainFiles.map( - (src) => `${renderOpts.assetPrefix || ''}/_next/` + src + try { + const renderStream = await renderToInitialStream({ + ReactDOMServer, + element: content, + streamOptions: { + nonce, + // Include hydration scripts in the HTML + bootstrapScripts: subresourceIntegrityManifest + ? buildManifest.rootMainFiles.map((src) => ({ + src: `${renderOpts.assetPrefix || ''}/_next/` + src, + integrity: subresourceIntegrityManifest[src], + })) + : buildManifest.rootMainFiles.map( + (src) => `${renderOpts.assetPrefix || ''}/_next/` + src + ), + }, + }) + + return await continueFromInitialStream(renderStream, { + dataStream: serverComponentsInlinedTransformStream?.readable, + generateStaticHTML: generateStaticHTML, + flushEffectHandler, + flushEffectsToHead: true, + }) + } catch (err) { + // TODO-APP: show error overlay in development. `element` should probably be wrapped in AppRouter for this case. + const renderStream = await renderToInitialStream({ + ReactDOMServer, + element: ( + + + + ), - }, - }) + streamOptions: { + nonce, + // Include hydration scripts in the HTML + bootstrapScripts: subresourceIntegrityManifest + ? buildManifest.rootMainFiles.map((src) => ({ + src: `${renderOpts.assetPrefix || ''}/_next/` + src, + integrity: subresourceIntegrityManifest[src], + })) + : buildManifest.rootMainFiles.map( + (src) => `${renderOpts.assetPrefix || ''}/_next/` + src + ), + }, + }) - return await continueFromInitialStream(renderStream, { - dataStream: serverComponentsInlinedTransformStream?.readable, - generateStaticHTML: generateStaticHTML, - flushEffectHandler, - flushEffectsToHead: true, - }) + return await continueFromInitialStream(renderStream, { + dataStream: serverComponentsInlinedTransformStream?.readable, + generateStaticHTML: generateStaticHTML, + flushEffectHandler, + flushEffectsToHead: true, + }) + } } return new RenderResult(await bodyResult()) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index d2e5db5bf0515e1..4bee04739af328a 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -48,7 +48,8 @@ import Router from './router' import { setRevalidateHeaders } from './send-payload/revalidate-headers' import { execOnce } from '../shared/lib/utils' -import { isBlockedPage, isBot } from './utils' +import { isBlockedPage } from './utils' +import { isBot } from '../shared/lib/router/utils/is-bot' import RenderResult from './render-result' import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash' import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' @@ -245,6 +246,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 @@ -959,7 +961,11 @@ export default abstract class Server { // Toggle whether or not this is a Data request const isDataReq = - !!(query.__nextDataReq || req.headers['x-nextjs-data']) && + !!( + query.__nextDataReq || + (req.headers['x-nextjs-data'] && + (this.serverOptions as any).webServerConfig) + ) && (isSSG || hasServerProps || isServerComponent) delete query.__nextDataReq @@ -1542,8 +1548,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/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 8e1d195b7c7bc9c..c0b2ea637e37a4e 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -361,6 +361,10 @@ export default class HotReloader { break } case 'client-full-reload': { + traceChild = { + name: payload.event, + attrs: { stackTrace: payload.stackTrace ?? '' }, + } Log.warn( 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' ) @@ -602,6 +606,7 @@ export default class HotReloader { ? await getPageStaticInfo({ pageFilePath: entryData.absolutePagePath, nextConfig: this.config, + isDev: true, }) : {} @@ -635,6 +640,7 @@ export default class HotReloader { name: bundlePath, value: getEdgeServerEntry({ absolutePagePath: entryData.absolutePagePath, + rootDir: this.dir, buildId: this.buildId, bundlePath, config: this.config, diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 920a6264845d0c2..d3176c140dd6565 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -273,12 +273,10 @@ export default class DevServer extends Server { }) } - const wp = (this.webpackWatcher = new Watchpack({ - ignored: /([/\\]node_modules[/\\]|[/\\]\.next[/\\]|[/\\]\.git[/\\])/, - })) const pages = this.pagesDir ? [this.pagesDir] : [] const app = this.appDir ? [this.appDir] : [] const directories = [...pages, ...app] + const files = this.pagesDir ? getPossibleMiddlewareFilenames( pathJoin(this.pagesDir, '..'), @@ -303,6 +301,15 @@ export default class DevServer extends Server { ] files.push(...tsconfigPaths) + const wp = (this.webpackWatcher = new Watchpack({ + ignored: (pathname: string) => { + return ( + !files.some((file) => file.startsWith(pathname)) && + !directories.some((dir) => pathname.startsWith(dir)) + ) + }, + })) + wp.watch({ directories: [this.dir], startTime: 0 }) const fileWatchTimes = new Map() let enabledTypeScript = this.usingTypeScript @@ -369,6 +376,7 @@ export default class DevServer extends Server { pageFilePath: fileName, nextConfig: this.nextConfig, page: rootFile, + isDev: true, }) if (isMiddlewareFile(rootFile)) { @@ -518,7 +526,6 @@ export default class DevServer extends Server { hasReactRoot: this.hotReloader?.hasReactRoot, isNodeServer, isEdgeServer, - hasServerComponents: this.hotReloader?.hasServerComponents, }) Object.keys(plugin.definitions).forEach((key) => { diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index a0a2fe8bf17182a..4784fcf4489f2b1 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -632,6 +632,7 @@ export function onDemandEntryHandler({ const staticInfo = await getPageStaticInfo({ pageFilePath: pagePathData.absolutePagePath, nextConfig, + isDev: true, }) const added = new Map>() @@ -648,12 +649,22 @@ export function onDemandEntryHandler({ }, onServer: () => { added.set(COMPILER_NAMES.server, addEntry(COMPILER_NAMES.server)) + const edgeServerEntry = `${COMPILER_NAMES.edgeServer}${pagePathData.page}` + if (entries[edgeServerEntry]) { + // Runtime switched from edge to server + delete entries[edgeServerEntry] + } }, onEdgeServer: () => { added.set( COMPILER_NAMES.edgeServer, addEntry(COMPILER_NAMES.edgeServer) ) + const serverEntry = `${COMPILER_NAMES.server}${pagePathData.page}` + if (entries[serverEntry]) { + // Runtime switched from server to edge + delete entries[serverEntry] + } }, }) 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 7084d982f18ad30..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,23 +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( - Object.assign({}, getRequestMeta(params.req, '__NEXT_INIT_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 ) } @@ -2072,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/server/render.tsx b/packages/next/server/render.tsx index c352429e7bf6ddd..2ec63d476a1b39b 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -375,7 +375,6 @@ export async function renderToHTML( getStaticProps, getStaticPaths, getServerSideProps, - serverComponentManifest, isDataReq, params, previewProps, @@ -384,18 +383,11 @@ export async function renderToHTML( supportsDynamicHTML, images, runtime: globalRuntime, - ComponentMod, App, } = renderOpts let Document = renderOpts.Document - // We don't need to opt-into the flight inlining logic if the page isn't a RSC. - const isServerComponent = - !!process.env.__NEXT_REACT_ROOT && - !!serverComponentManifest && - !!ComponentMod.__next_rsc__?.server - // Component will be wrapped by ServerComponentWrapper for RSC let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = renderOpts.Component @@ -412,12 +404,6 @@ export async function renderToHTML( // next internal queries should be stripped out stripInternalQueries(query) - if (isServerComponent) { - throw new Error( - 'Server Components are not supported from the pages/ directory.' - ) - } - const callMiddleware = async (method: string, args: any[], props = false) => { let results: any = props ? {} : [] @@ -1417,7 +1403,6 @@ export async function renderToHTML( err: renderOpts.err ? serializeError(dev, renderOpts.err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML gsp: !!getStaticProps ? true : undefined, // whether the page is getStaticProps gssp: !!getServerSideProps ? true : undefined, // whether the page is getServerSideProps - rsc: isServerComponent ? true : undefined, // whether the page is a server components page customServer, // whether the user is using a custom server gip: hasPageGetInitialProps ? true : undefined, // whether the page has getInitialProps appGip: !defaultAppGetInitialProps ? true : undefined, // whether the _app has getInitialProps diff --git a/packages/next/server/utils.ts b/packages/next/server/utils.ts index 856f5f7032629f7..9e4ca33e9abb246 100644 --- a/packages/next/server/utils.ts +++ b/packages/next/server/utils.ts @@ -17,12 +17,6 @@ export function cleanAmpPath(pathname: string): string { return pathname } -export function isBot(userAgent: string): boolean { - return /Googlebot|Mediapartners-Google|AdsBot-Google|googleweblight|Storebot-Google|Google-PageRenderer|Bingbot|BingPreview|Slurp|DuckDuckBot|baiduspider|yandex|sogou|LinkedInBot|bitlybot|tumblr|vkShare|quora link preview|facebookexternalhit|facebookcatalog|Twitterbot|applebot|redditbot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|ia_archiver/i.test( - userAgent - ) -} - export function isTargetLikeServerless(target: string) { const isServerless = target === 'serverless' const isServerlessTrace = target === 'experimental-serverless-trace' diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index 65b076daa3ae35d..d905ddc7625fcfa 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -148,7 +148,8 @@ async function createModuleContext(options: ModuleContextOptions) { if (!warnedEvals.has(key)) { const warning = getServerError( new Error( - `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime +Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation` ), COMPILER_NAMES.edgeServer ) @@ -166,7 +167,7 @@ async function createModuleContext(options: ModuleContextOptions) { if (!warnedWasmCodegens.has(key)) { const warning = getServerError( new Error(`Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Edge Runtime. -Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation`), +Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`), COMPILER_NAMES.edgeServer ) warning.name = 'DynamicWasmCodeGenerationWarning' @@ -193,7 +194,7 @@ Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) { const warning = getServerError( new Error(`Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Edge Runtime. -Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation`), +Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`), COMPILER_NAMES.edgeServer ) warning.name = 'DynamicWasmCodeGenerationWarning' diff --git a/packages/next/shared/lib/app-router-context.ts b/packages/next/shared/lib/app-router-context.ts index 74f73d1916e8fad..f9a36be37dc3f05 100644 --- a/packages/next/shared/lib/app-router-context.ts +++ b/packages/next/shared/lib/app-router-context.ts @@ -66,8 +66,11 @@ export const GlobalLayoutRouterContext = React.createContext<{ focusAndScrollRef: FocusAndScrollRef }>(null as any) +export const TemplateContext = React.createContext(null as any) + if (process.env.NODE_ENV !== 'production') { AppRouterContext.displayName = 'AppRouterContext' LayoutRouterContext.displayName = 'LayoutRouterContext' GlobalLayoutRouterContext.displayName = 'GlobalLayoutRouterContext' + TemplateContext.displayName = 'TemplateContext' } diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index c314f8517b5b653..c334cf38fd34b8e 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -1,4 +1,4 @@ -type ValueOf = Required[keyof T] +export type ValueOf = Required[keyof T] export const COMPILER_NAMES = { client: 'client', @@ -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' @@ -85,7 +86,6 @@ export const TEMPORARY_REDIRECT_STATUS = 307 export const PERMANENT_REDIRECT_STATUS = 308 export const STATIC_PROPS_ID = '__N_SSG' export const SERVER_PROPS_ID = '__N_SSP' -export const FLIGHT_PROPS_ID = '__N_RSC' export const GOOGLE_FONT_PROVIDER = 'https://fonts.googleapis.com/' export const OPTIMIZED_FONT_PROVIDERS = [ { url: GOOGLE_FONT_PROVIDER, preconnect: 'https://fonts.gstatic.com' }, diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx index 6e7dc8fa2290cb3..b79caf271dc4fca 100644 --- a/packages/next/shared/lib/dynamic.tsx +++ b/packages/next/shared/lib/dynamic.tsx @@ -65,29 +65,33 @@ export default function dynamic

( options?: DynamicOptions

): React.ComponentType

{ let loadableFn: LoadableFn

= Loadable - let loadableOptions: LoadableOptions

= { - // A loading component is not required, so we default it - loading: ({ error, isLoading, pastDelay }) => { - if (!pastDelay) return null - if (process.env.NODE_ENV === 'development') { - if (isLoading) { + + let loadableOptions: LoadableOptions

= options?.suspense + ? {} + : // only provide a default loading component when suspense is disabled + { + // A loading component is not required, so we default it + loading: ({ error, isLoading, pastDelay }) => { + if (!pastDelay) return null + if (process.env.NODE_ENV === 'development') { + if (isLoading) { + return null + } + if (error) { + return ( +

+ {error.message} +
+ {error.stack} +

+ ) + } + } + return null - } - if (error) { - return ( -

- {error.message} -
- {error.stack} -

- ) - } + }, } - return null - }, - } - // Support for direct import(), eg: dynamic(import('../hello-world')) // Note that this is only kept for the edge case where someone is passing in a promise as first argument // The react-loadable babel plugin will turn dynamic(import('../hello-world')) into dynamic(() => import('../hello-world')) @@ -112,8 +116,8 @@ export default function dynamic

( ) } - if (process.env.NODE_ENV !== 'production') { - if (loadableOptions.suspense) { + if (loadableOptions.suspense) { + if (process.env.NODE_ENV !== 'production') { /** * TODO: Currently, next/dynamic will opt-in to React.lazy if { suspense: true } is used * React 18 will always resolve the Suspense boundary on the server-side, effectively ignoring the ssr option @@ -122,19 +126,20 @@ export default function dynamic

( * React.lazy that can suspense on the server-side while only loading the component on the client-side */ if (loadableOptions.ssr === false) { - loadableOptions.ssr = true console.warn( `"ssr: false" is ignored by next/dynamic because you can not enable "suspense" while disabling "ssr" at the same time. Read more: https://nextjs.org/docs/messages/invalid-dynamic-suspense` ) } if (loadableOptions.loading != null) { - loadableOptions.loading = undefined console.warn( `"loading" is ignored by next/dynamic because you have enabled "suspense". Place your loading element in your suspense boundary's "fallback" prop instead. Read more: https://nextjs.org/docs/messages/invalid-dynamic-suspense` ) } } + + delete loadableOptions.ssr + delete loadableOptions.loading } // coming from build/babel/plugins/react-loadable-plugin.js diff --git a/packages/next/shared/lib/router/router.ts b/packages/next/shared/lib/router/router.ts index 5fab0820e1b67f2..cf1180fd6766ee0 100644 --- a/packages/next/shared/lib/router/router.ts +++ b/packages/next/shared/lib/router/router.ts @@ -47,6 +47,7 @@ import { hasBasePath } from '../../../client/has-base-path' import { getNextPathnameInfo } from './utils/get-next-pathname-info' import { formatNextPathnameInfo } from './utils/format-next-pathname-info' import { compareRouterStates } from './utils/compare-states' +import { isBot } from './utils/is-bot' declare global { interface Window { @@ -564,7 +565,6 @@ export type CompletePrivateRouteInfo = { styleSheets: StyleSheetTuple[] __N_SSG?: boolean __N_SSP?: boolean - __N_RSC?: boolean props?: Record err?: Error error?: any @@ -881,7 +881,6 @@ export default class Router implements BaseRouter { defaultLocale, domainLocales, isPreview, - isRsc, }: { subscription: Subscription initialProps: any @@ -896,7 +895,6 @@ export default class Router implements BaseRouter { defaultLocale?: string domainLocales?: DomainLocale[] isPreview?: boolean - isRsc?: boolean } ) { // represents the current component key @@ -915,7 +913,6 @@ export default class Router implements BaseRouter { err, __N_SSG: initialProps && initialProps.__N_SSG, __N_SSP: initialProps && initialProps.__N_SSP, - __N_RSC: !!isRsc, } } @@ -1993,7 +1990,6 @@ export default class Router implements BaseRouter { styleSheets: res.styleSheets, __N_SSG: res.mod.__N_SSG, __N_SSP: res.mod.__N_SSP, - __N_RSC: !!res.mod.__next_rsc__, }) )) @@ -2006,20 +2002,10 @@ export default class Router implements BaseRouter { } } - /** - * For server components, non-SSR pages will have statically optimized - * flight data in a production build. So only development and SSR pages - * will always have the real-time generated and streamed flight data. - */ - const useStreamedFlightData = - routeInfo.__N_RSC && - (process.env.NODE_ENV !== 'production' || routeInfo.__N_SSP) - - const shouldFetchData = - routeInfo.__N_SSG || routeInfo.__N_SSP || routeInfo.__N_RSC + const shouldFetchData = routeInfo.__N_SSG || routeInfo.__N_SSP const { props, cacheKey } = await this._getData(async () => { - if (shouldFetchData && !useStreamedFlightData) { + if (shouldFetchData) { const { json, cacheKey: _cacheKey } = data?.json ? data : await fetchNextData({ @@ -2083,31 +2069,7 @@ export default class Router implements BaseRouter { ).catch(() => {}) } - let flightInfo - if (routeInfo.__N_RSC) { - flightInfo = { - __flight__: useStreamedFlightData - ? ( - await this._getData(() => - this._getFlightData( - formatWithValidation({ - query: { ...query, __flight__: '1' }, - pathname: isDynamicRoute(route) - ? interpolateAs( - pathname, - parseRelativeUrl(resolvedAs).pathname, - query - ).result - : pathname, - }) - ) - ) - ).data - : props.__flight__, - } - } - - props.pageProps = Object.assign({}, props.pageProps, flightInfo) + props.pageProps = Object.assign({}, props.pageProps) routeInfo.props = props routeInfo.route = route routeInfo.query = query @@ -2210,6 +2172,12 @@ export default class Router implements BaseRouter { asPath: string = url, options: PrefetchOptions = {} ): Promise { + if (typeof window !== 'undefined' && isBot(window.navigator.userAgent)) { + // No prefetches for bots that render the link since they are typically navigating + // links via the equivalent of a hard navigation and hence never utilize these + // prefetches. + return + } let parsed = parseRelativeUrl(url) let { pathname, query } = parsed diff --git a/packages/next/shared/lib/router/utils/is-bot.ts b/packages/next/shared/lib/router/utils/is-bot.ts new file mode 100644 index 000000000000000..c512679b4381b3e --- /dev/null +++ b/packages/next/shared/lib/router/utils/is-bot.ts @@ -0,0 +1,5 @@ +export function isBot(userAgent: string): boolean { + return /Googlebot|Mediapartners-Google|AdsBot-Google|googleweblight|Storebot-Google|Google-PageRenderer|Bingbot|BingPreview|Slurp|DuckDuckBot|baiduspider|yandex|sogou|LinkedInBot|bitlybot|tumblr|vkShare|quora link preview|facebookexternalhit|facebookcatalog|Twitterbot|applebot|redditbot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|ia_archiver/i.test( + userAgent + ) +} diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index ec5197e0c3b9a80..67c486a83d1e447 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -27,10 +27,10 @@ export type DocumentType = NextComponentType< DocumentProps > -export type AppType = NextComponentType< +export type AppType

= NextComponentType< AppContextType, - AppInitialProps, - AppPropsType + P, + AppPropsType > export type AppTreeType = ComponentType< @@ -109,7 +109,6 @@ export type NEXT_DATA = { scriptLoader?: any[] isPreview?: boolean notFoundSrcPage?: string - rsc?: boolean } /** @@ -177,7 +176,6 @@ export type AppPropsType< router: R __N_SSG?: boolean __N_SSP?: boolean - __N_RSC?: boolean } export type DocumentContext = NextPageContext & { diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 9008d34e3e7b700..80899a2748f5690 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.10", + "version": "12.3.1-canary.1", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/internal/helpers.ts b/packages/react-refresh-utils/internal/helpers.ts index 7428b73196d1760..9ff15e9808c4989 100644 --- a/packages/react-refresh-utils/internal/helpers.ts +++ b/packages/react-refresh-utils/internal/helpers.ts @@ -48,7 +48,6 @@ function isSafeExport(key: string): boolean { key === '__esModule' || key === '__N_SSG' || key === '__N_SSP' || - key === '__N_RSC' || // TODO: remove this key from page config instead of allow listing it key === 'config' ) diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 13f37d3be78c13c..cf50b43de1a3481 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.10", + "version": "12.3.1-canary.1", "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 5c2a8952141be50..8bb382bf1ae0faa 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-c739cef2f-20220912 + react-exp: npm:react@0.0.0-experimental-c739cef2f-20220912 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-c739cef2f-20220912_react@18.2.0 + react-exp: /react/0.0.0-experimental-c739cef2f-20220912 react-ssr-prepass: 1.0.8_qncsgtzehe3fgiqp6tr7lwq6fm react-virtualized: 9.22.3_biqbaboplfbrettd7655fr4n2y relay-compiler: 13.0.2 @@ -363,14 +365,14 @@ importers: packages/eslint-config-next: specifiers: - '@next/eslint-plugin-next': 12.2.6-canary.10 + '@next/eslint-plugin-next': 12.3.1-canary.1 '@rushstack/eslint-patch': ^1.1.3 '@typescript-eslint/parser': ^5.21.0 eslint-import-resolver-node: ^0.3.6 eslint-import-resolver-typescript: ^2.7.1 eslint-plugin-import: ^2.26.0 eslint-plugin-jsx-a11y: ^6.5.1 - eslint-plugin-react: ^7.29.4 + eslint-plugin-react: ^7.31.7 eslint-plugin-react-hooks: ^4.5.0 dependencies: '@next/eslint-plugin-next': link:../eslint-plugin-next @@ -380,7 +382,7 @@ importers: eslint-import-resolver-typescript: 2.7.1_hpmu7kn6tcn2vnxpfzvv33bxmy eslint-plugin-import: 2.26.0_asoxhzjlkaozogjqriaz4fv5ly eslint-plugin-jsx-a11y: 6.5.1_eslint@7.32.0 - eslint-plugin-react: 7.29.4_eslint@7.32.0 + eslint-plugin-react: 7.31.8_eslint@7.32.0 eslint-plugin-react-hooks: 4.5.0_eslint@7.32.0 packages/eslint-plugin-next: @@ -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.10 - '@next/polyfill-module': 12.2.6-canary.10 - '@next/polyfill-nomodule': 12.2.6-canary.10 - '@next/react-dev-overlay': 12.2.6-canary.10 - '@next/react-refresh-utils': 12.2.6-canary.10 - '@next/swc': 12.2.6-canary.10 + '@next/env': 12.3.1-canary.1 + '@next/polyfill-module': 12.3.1-canary.1 + '@next/polyfill-nomodule': 12.3.1-canary.1 + '@next/react-dev-overlay': 12.3.1-canary.1 + '@next/react-refresh-utils': 12.3.1-canary.1 + '@next/swc': 12.3.1-canary.1 '@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 @@ -560,7 +562,7 @@ importers: string_decoder: 1.3.0 string-hash: 1.1.3 strip-ansi: 6.0.0 - styled-jsx: 5.0.6 + styled-jsx: 5.0.7 tar: 6.1.11 taskr: 1.1.0 terser: 5.14.1 @@ -584,7 +586,7 @@ importers: '@swc/helpers': 0.4.11 caniuse-lite: 1.0.30001332 postcss: 8.4.14 - styled-jsx: 5.0.6_@babel+core@7.18.0 + styled-jsx: 5.0.7_@babel+core@7.18.0 use-sync-external-store: 1.2.0 devDependencies: '@ampproject/toolbox-optimizer': 2.8.3 @@ -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 @@ -8749,8 +8741,21 @@ packages: engines: { node: '>= 0.4' } dependencies: call-bind: 1.0.2 - define-properties: 1.1.3 - es-abstract: 1.19.1 + define-properties: 1.1.4 + es-abstract: 1.20.2 + get-intrinsic: 1.1.1 + is-string: 1.0.7 + + /array-includes/3.1.5: + resolution: + { + integrity: sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==, + } + engines: { node: '>= 0.4' } + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.2 get-intrinsic: 1.1.1 is-string: 1.0.7 @@ -8823,8 +8828,8 @@ packages: engines: { node: '>= 0.4' } dependencies: call-bind: 1.0.2 - define-properties: 1.1.3 - es-abstract: 1.19.1 + define-properties: 1.1.4 + es-abstract: 1.20.2 /array.prototype.flatmap/1.2.5: resolution: @@ -8836,6 +8841,20 @@ packages: call-bind: 1.0.2 define-properties: 1.1.3 es-abstract: 1.19.1 + dev: true + + /array.prototype.flatmap/1.3.0: + resolution: + { + integrity: sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==, + } + engines: { node: '>= 0.4' } + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.2 + es-shim-unscopables: 1.0.0 + dev: false /arrify/1.0.1: resolution: @@ -10964,7 +10983,10 @@ packages: dev: true /concat-map/0.0.1: - resolution: { integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= } + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } /concat-stream/1.6.2: resolution: @@ -12364,6 +12386,16 @@ packages: dependencies: object-keys: 1.1.1 + /define-properties/1.1.4: + resolution: + { + integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==, + } + engines: { node: '>= 0.4' } + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + /define-property/0.2.5: resolution: { @@ -13140,7 +13172,7 @@ packages: get-intrinsic: 1.1.1 get-symbol-description: 1.0.0 has: 1.0.3 - has-symbols: 1.0.2 + has-symbols: 1.0.3 internal-slot: 1.0.3 is-callable: 1.2.4 is-negative-zero: 2.0.1 @@ -13155,6 +13187,37 @@ packages: string.prototype.trimstart: 1.0.4 unbox-primitive: 1.0.1 + /es-abstract/1.20.2: + resolution: + { + integrity: sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==, + } + engines: { node: '>= 0.4' } + dependencies: + call-bind: 1.0.2 + es-to-primitive: 1.2.1 + function-bind: 1.1.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.1.2 + get-symbol-description: 1.0.0 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + is-callable: 1.2.4 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-weakref: 1.0.2 + object-inspect: 1.12.2 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + string.prototype.trimend: 1.0.5 + string.prototype.trimstart: 1.0.5 + unbox-primitive: 1.0.2 + /es-module-lexer/0.9.0: resolution: { @@ -13162,6 +13225,15 @@ packages: } dev: true + /es-shim-unscopables/1.0.0: + resolution: + { + integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==, + } + dependencies: + has: 1.0.3 + dev: false + /es-to-primitive/1.2.1: resolution: { @@ -13553,17 +13625,17 @@ packages: string.prototype.matchall: 4.0.6 dev: true - /eslint-plugin-react/7.29.4_eslint@7.32.0: + /eslint-plugin-react/7.31.8_eslint@7.32.0: resolution: { - integrity: sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==, + integrity: sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==, } engines: { node: '>=4' } peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - array-includes: 3.1.4 - array.prototype.flatmap: 1.2.5 + array-includes: 3.1.5 + array.prototype.flatmap: 1.3.0 doctrine: 2.1.0 eslint: 7.32.0 estraverse: 5.3.0 @@ -13571,12 +13643,12 @@ packages: minimatch: 3.1.2 object.entries: 1.1.5 object.fromentries: 2.0.5 - object.hasown: 1.1.0 + object.hasown: 1.1.1 object.values: 1.1.5 prop-types: 15.8.1 resolve: 2.0.0-next.3 semver: 6.3.0 - string.prototype.matchall: 4.0.6 + string.prototype.matchall: 4.0.7 dev: false /eslint-scope/5.1.1: @@ -14889,12 +14961,30 @@ packages: integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==, } + /function.prototype.name/1.1.5: + resolution: + { + integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==, + } + engines: { node: '>= 0.4' } + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.2 + functions-have-names: 1.2.3 + /functional-red-black-tree/1.0.1: resolution: { integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==, } + /functions-have-names/1.2.3: + resolution: + { + integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==, + } + /gauge/2.7.4: resolution: { @@ -14943,7 +15033,17 @@ packages: dependencies: function-bind: 1.1.1 has: 1.0.3 - has-symbols: 1.0.2 + has-symbols: 1.0.3 + + /get-intrinsic/1.1.2: + resolution: + { + integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==, + } + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 /get-orientation/1.1.2: resolution: @@ -15033,7 +15133,7 @@ packages: engines: { node: '>= 0.4' } dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.1 + get-intrinsic: 1.1.2 /get-value/2.0.6: resolution: @@ -15649,6 +15749,12 @@ packages: integrity: sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==, } + /has-bigints/1.0.2: + resolution: + { + integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==, + } + /has-flag/1.0.0: resolution: { @@ -15679,6 +15785,14 @@ packages: engines: { node: '>=8' } dev: true + /has-property-descriptors/1.0.0: + resolution: + { + integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==, + } + dependencies: + get-intrinsic: 1.1.1 + /has-symbol-support-x/1.4.2: resolution: { @@ -15692,6 +15806,14 @@ packages: integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==, } engines: { node: '>= 0.4' } + dev: true + + /has-symbols/1.0.3: + resolution: + { + integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==, + } + engines: { node: '>= 0.4' } /has-to-string-tag-x/1.4.1: resolution: @@ -15709,7 +15831,7 @@ packages: } engines: { node: '>= 0.4' } dependencies: - has-symbols: 1.0.2 + has-symbols: 1.0.3 /has-unicode/2.0.1: resolution: @@ -17135,6 +17257,13 @@ packages: } engines: { node: '>= 0.4' } + /is-negative-zero/2.0.2: + resolution: + { + integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==, + } + engines: { node: '>= 0.4' } + /is-npm/4.0.0: resolution: { @@ -17341,6 +17470,14 @@ packages: integrity: sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==, } + /is-shared-array-buffer/1.0.2: + resolution: + { + integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==, + } + dependencies: + call-bind: 1.0.2 + /is-ssh/1.3.1: resolution: { @@ -17387,7 +17524,7 @@ packages: } engines: { node: '>= 0.4' } dependencies: - has-symbols: 1.0.2 + has-symbols: 1.0.3 /is-text-path/1.0.1: resolution: { integrity: sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= } @@ -17464,6 +17601,14 @@ packages: dependencies: call-bind: 1.0.2 + /is-weakref/1.0.2: + resolution: + { + integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==, + } + dependencies: + call-bind: 1.0.2 + /is-whitespace-character/1.0.3: resolution: { @@ -18701,7 +18846,7 @@ packages: } engines: { node: '>=4.0' } dependencies: - array-includes: 3.1.4 + array-includes: 3.1.5 object.assign: 4.1.2 /jszip/3.7.1: @@ -21290,6 +21435,12 @@ packages: integrity: sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==, } + /object-inspect/1.12.2: + resolution: + { + integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==, + } + /object-is/1.0.2: resolution: { @@ -21330,8 +21481,20 @@ packages: engines: { node: '>= 0.4' } dependencies: call-bind: 1.0.2 - define-properties: 1.1.3 - has-symbols: 1.0.2 + define-properties: 1.1.4 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + /object.assign/4.1.4: + resolution: + { + integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==, + } + engines: { node: '>= 0.4' } + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + has-symbols: 1.0.3 object-keys: 1.1.1 /object.defaults/1.1.0: @@ -21352,8 +21515,8 @@ packages: engines: { node: '>= 0.4' } dependencies: call-bind: 1.0.2 - define-properties: 1.1.3 - es-abstract: 1.19.1 + define-properties: 1.1.4 + es-abstract: 1.20.2 /object.fromentries/2.0.5: resolution: @@ -21377,14 +21540,14 @@ packages: es-abstract: 1.19.1 dev: true - /object.hasown/1.1.0: + /object.hasown/1.1.1: resolution: { - integrity: sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==, + integrity: sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==, } dependencies: - define-properties: 1.1.3 - es-abstract: 1.19.1 + define-properties: 1.1.4 + es-abstract: 1.20.2 dev: false /object.map/1.0.1: @@ -24206,17 +24369,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-c739cef2f-20220912_react@18.2.0: resolution: { - integrity: sha512-d2pS60dObYaVw4cybVNlQ7JL+Mkpz6MVZo35KgsUzU4IUUywxrc09JJivOGiOyJTwTShITAgOvn/c/LZjlTpcg==, + integrity: sha512-RbKxCHjX+H/n9yRvB2LFVhHUsFxWOiOlnhSGQ/JO45odUVreNVw0W7hez3biBF1r32f+0dp8RDVfvvKcF1sQyg==, } peerDependencies: - react: 0.0.0-experimental-0de3ddf56-20220825 + react: 0.0.0-experimental-c739cef2f-20220912 dependencies: loose-envify: 1.4.0 react: 18.2.0 - scheduler: 0.0.0-experimental-0de3ddf56-20220825 + scheduler: 0.0.0-experimental-c739cef2f-20220912 dev: true /react-dom/17.0.2_react@18.2.0: @@ -24281,14 +24444,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 +24493,10 @@ packages: react-lifecycles-compat: 3.0.4 dev: true - /react/0.0.0-experimental-0de3ddf56-20220825: + /react/0.0.0-experimental-c739cef2f-20220912: resolution: { - integrity: sha512-4KH+Ylv+P0SsKAjjpGALztqHBkqoh01GFh8hJkCAPJ7fAx6yLDY7i4XeoDutqo6okWnJncq7ixUoyru4hXFn4A==, + integrity: sha512-K3T+R0lw7LzA3HSPHtI4CIYXw6tXKV5ewvuvuY7vfom/0rDvgoUkWHpdkykFUiEaShMKrQyeK6+faq41LeucKA==, } engines: { node: '>=0.10.0' } dependencies: @@ -24746,6 +24909,18 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.3 + dev: true + + /regexp.prototype.flags/1.4.3: + resolution: + { + integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==, + } + engines: { node: '>= 0.4' } + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + functions-have-names: 1.2.3 /regexpp/3.1.0: resolution: @@ -25663,10 +25838,10 @@ packages: xmlchars: 2.2.0 dev: true - /scheduler/0.0.0-experimental-0de3ddf56-20220825: + /scheduler/0.0.0-experimental-c739cef2f-20220912: resolution: { - integrity: sha512-cMaG6fmnYZvH2C5LuMGw//oHBskpicjdJ+GbLs7nAQLiuYl6sXzmqQeGgDLJvd76hcBz2aK2tEKZMHrmwuWVcA==, + integrity: sha512-6HcdQoIp5PENl8IWJfCkIHcMx9Z+PVYxiTzFURS58bdkYhF8vsl65w/z6dpq6Ii/05miYi16VzrLc9T61EmexQ==, } dependencies: loose-envify: 1.4.0 @@ -26625,6 +26800,23 @@ packages: internal-slot: 1.0.3 regexp.prototype.flags: 1.3.1 side-channel: 1.0.4 + dev: true + + /string.prototype.matchall/4.0.7: + resolution: + { + integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==, + } + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.3 + es-abstract: 1.19.1 + get-intrinsic: 1.1.1 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + dev: false /string.prototype.padend/3.1.0: resolution: @@ -26644,7 +26836,17 @@ packages: } dependencies: call-bind: 1.0.2 - define-properties: 1.1.3 + define-properties: 1.1.4 + + /string.prototype.trimend/1.0.5: + resolution: + { + integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==, + } + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.2 /string.prototype.trimstart/1.0.4: resolution: @@ -26653,7 +26855,17 @@ packages: } dependencies: call-bind: 1.0.2 - define-properties: 1.1.3 + define-properties: 1.1.4 + + /string.prototype.trimstart/1.0.5: + resolution: + { + integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==, + } + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.2 /string_decoder/0.10.31: resolution: @@ -26881,10 +27093,10 @@ packages: postcss-load-plugins: 2.3.0 dev: true - /styled-jsx/5.0.6_@babel+core@7.18.0: + /styled-jsx/5.0.7_@babel+core@7.18.0: resolution: { - integrity: sha512-xOeROtkK5MGMDimBQ3J6iPId8q0t/BDoG5XN6oKkZClVz9ISF/hihN8OCn2LggMU6N32aXnrXBdn3auSqNS9fA==, + integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==, } engines: { node: '>= 12.0.0' } peerDependencies: @@ -28146,7 +28358,18 @@ packages: dependencies: function-bind: 1.1.1 has-bigints: 1.0.1 - has-symbols: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + /unbox-primitive/1.0.2: + resolution: + { + integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==, + } + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 /unc-path-regex/0.1.2: diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts index 48c49a704e69163..1b852b8df2ab352 100644 --- a/test/development/basic/hmr.test.ts +++ b/test/development/basic/hmr.test.ts @@ -776,11 +776,94 @@ describe('basic HMR', () => { }) }) + describe('Full reload', () => { + it('should warn about full reload in cli output - anonymous page function', async () => { + const start = next.cliOutput.length + const browser = await webdriver( + next.appPort, + `/hmr/anonymous-page-function` + ) + expect(await browser.elementByCss('p').text()).toBe('hello world') + expect(next.cliOutput.slice(start)).not.toContain( + 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' + ) + + const currentFileContent = await next.readFile( + './pages/hmr/anonymous-page-function.js' + ) + const newFileContent = currentFileContent.replace( + '

hello world

', + '

hello world!!!

' + ) + await next.patchFile( + './pages/hmr/anonymous-page-function.js', + newFileContent + ) + await check(() => browser.elementByCss('p').text(), 'hello world!!!') + + // CLI warning and stacktrace + expect(next.cliOutput.slice(start)).toContain( + 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' + ) + expect(next.cliOutput.slice(start)).toContain( + 'Error: Aborted because ./pages/hmr/anonymous-page-function.js is not accepted' + ) + + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree." + ) + ) + ).toBeTruthy() + }) + + it('should warn about full reload in cli output - runtime-error', async () => { + const start = next.cliOutput.length + const browser = await webdriver(next.appPort, `/hmr/runtime-error`) + await check( + () => getRedboxHeader(browser), + /ReferenceError: whoops is not defined/ + ) + expect(next.cliOutput.slice(start)).not.toContain( + 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' + ) + + const currentFileContent = await next.readFile( + './pages/hmr/runtime-error.js' + ) + const newFileContent = currentFileContent.replace('whoops', '"whoops"') + await next.patchFile('./pages/hmr/runtime-error.js', newFileContent) + await check(() => browser.elementByCss('body').text(), 'whoops') + + // CLI warning and stacktrace + expect(next.cliOutput.slice(start)).toContain( + 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' + ) + expect(next.cliOutput.slice(start)).not.toContain( + 'Error: Aborted because ./pages/runtime-error.js is not accepted' + ) + + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + '[Fast Refresh] performing full reload because your application had an unrecoverable error' + ) + ) + ).toBeTruthy() + }) + }) + it('should have client HMR events in trace file', async () => { const traceData = await next.readFile('.next/trace') expect(traceData).toContain('client-hmr-latency') expect(traceData).toContain('client-error') expect(traceData).toContain('client-success') + expect(traceData).toContain('client-full-reload') }) it('should have correct compile timing after fixing error', async () => { diff --git a/test/development/basic/hmr/pages/hmr/anonymous-page-function.js b/test/development/basic/hmr/pages/hmr/anonymous-page-function.js new file mode 100644 index 000000000000000..10e018391ed83cc --- /dev/null +++ b/test/development/basic/hmr/pages/hmr/anonymous-page-function.js @@ -0,0 +1,3 @@ +export default function () { + return

hello world

+} diff --git a/test/development/basic/hmr/pages/hmr/runtime-error.js b/test/development/basic/hmr/pages/hmr/runtime-error.js new file mode 100644 index 000000000000000..8ee8aeb6ea45c13 --- /dev/null +++ b/test/development/basic/hmr/pages/hmr/runtime-error.js @@ -0,0 +1,4 @@ +export default function () { + // eslint-disable-next-line no-undef + return whoops +} diff --git a/test/development/full-reload-warning/index.test.ts b/test/development/full-reload-warning/index.test.ts deleted file mode 100644 index 1ff815fa566493f..000000000000000 --- a/test/development/full-reload-warning/index.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { check, getRedboxHeader } from 'next-test-utils' -import webdriver from 'next-webdriver' - -describe('show a warning in CLI and browser when doing a full reload', () => { - let next: NextInstance - - beforeEach(async () => { - next = await createNext({ - files: { - 'pages/anonymous-page-function.js': ` - export default function() { - return

hello world

- } - `, - 'pages/runtime-error.js': ` - export default function() { - return whoops - } - `, - }, - dependencies: {}, - }) - }) - afterEach(() => next.destroy()) - - test('error', async () => { - const browser = await webdriver(next.url, `/anonymous-page-function`) - expect(await browser.elementByCss('p').text()).toBe('hello world') - expect(next.cliOutput).not.toContain( - 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' - ) - - const currentFileContent = await next.readFile( - './pages/anonymous-page-function.js' - ) - const newFileContent = currentFileContent.replace( - '

hello world

', - '

hello world!!!

' - ) - await next.patchFile('./pages/anonymous-page-function.js', newFileContent) - await check(() => browser.elementByCss('p').text(), 'hello world!!!') - - // CLI warning and stacktrace - expect(next.cliOutput).toContain( - 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' - ) - expect(next.cliOutput).toContain( - 'Error: Aborted because ./pages/anonymous-page-function.js is not accepted' - ) - - // Browser warning - const browserLogs = await browser.log() - expect( - browserLogs.some(({ message }) => - message.includes( - "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree." - ) - ) - ).toBeTruthy() - }) - - test('runtime-error', async () => { - const browser = await webdriver(next.url, `/runtime-error`) - await check( - () => getRedboxHeader(browser), - /ReferenceError: whoops is not defined/ - ) - expect(next.cliOutput).not.toContain( - 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' - ) - - const currentFileContent = await next.readFile('./pages/runtime-error.js') - const newFileContent = currentFileContent.replace('whoops', '"whoops"') - await next.patchFile('./pages/runtime-error.js', newFileContent) - await check(() => browser.elementByCss('body').text(), 'whoops') - - // CLI warning and stacktrace - expect(next.cliOutput).toContain( - 'Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works' - ) - expect(next.cliOutput).not.toContain( - 'Error: Aborted because ./pages/runtime-error.js is not accepted' - ) - - // Browser warning - const browserLogs = await browser.log() - expect( - browserLogs.some(({ message }) => - message.includes( - '[Fast Refresh] performing full reload because your application had an unrecoverable error' - ) - ) - ).toBeTruthy() - }) -}) diff --git a/test/e2e/app-dir/app-prefetch/app/dashboard/layout.server.js b/test/e2e/app-dir/app-prefetch/app/dashboard/layout.server.js index 84ebbb490d40615..8c83d8e7a21677f 100644 --- a/test/e2e/app-dir/app-prefetch/app/dashboard/layout.server.js +++ b/test/e2e/app-dir/app-prefetch/app/dashboard/layout.server.js @@ -1,5 +1,5 @@ export async function getServerSideProps() { - await new Promise((resolve) => setTimeout(resolve, 2000)) + await new Promise((resolve) => setTimeout(resolve, 400)) return { props: { message: 'Hello World', @@ -9,7 +9,7 @@ export async function getServerSideProps() { export default function DashboardLayout({ children, message }) { return ( <> -

Dashboard {message}

+

Dashboard {message}

{children} ) diff --git a/test/e2e/app-dir/app-prefetch/app/dashboard/page.server.js b/test/e2e/app-dir/app-prefetch/app/dashboard/page.server.js index d22dfdf51e84754..5171e25e542d4ab 100644 --- a/test/e2e/app-dir/app-prefetch/app/dashboard/page.server.js +++ b/test/e2e/app-dir/app-prefetch/app/dashboard/page.server.js @@ -9,7 +9,7 @@ export async function getServerSideProps() { export default function DashboardPage({ message }) { return ( <> -

{message}

+

{message}

) } diff --git a/test/e2e/app-dir/app-prefetch/app/page.server.js b/test/e2e/app-dir/app-prefetch/app/page.server.js index 5cb37da2ad8a970..be3690c6321bebd 100644 --- a/test/e2e/app-dir/app-prefetch/app/page.server.js +++ b/test/e2e/app-dir/app-prefetch/app/page.server.js @@ -2,7 +2,9 @@ import Link from 'next/link' export default function HomePage() { return ( <> - To Dashboard + + To Dashboard + ) } diff --git a/test/e2e/app-dir/app/app/error/clientcomponent/error.client.js b/test/e2e/app-dir/app/app/error/clientcomponent/error.client.js new file mode 100644 index 000000000000000..cc0c3b620bfd0ca --- /dev/null +++ b/test/e2e/app-dir/app/app/error/clientcomponent/error.client.js @@ -0,0 +1,10 @@ +export default function ErrorBoundary({ error, reset }) { + return ( + <> +

An error occurred: {error.message}

+ + + ) +} diff --git a/test/e2e/app-dir/app/app/error/clientcomponent/page.client.js b/test/e2e/app-dir/app/app/error/clientcomponent/page.client.js new file mode 100644 index 000000000000000..8c3fe1a72901c1d --- /dev/null +++ b/test/e2e/app-dir/app/app/error/clientcomponent/page.client.js @@ -0,0 +1,18 @@ +import { useState } from 'react' + +export default function Page() { + const [clicked, setClicked] = useState(false) + if (clicked) { + throw new Error('this is a test') + } + return ( + + ) +} diff --git a/test/e2e/app-dir/app/app/error/ssr-error-client-component/page.client.js b/test/e2e/app-dir/app/app/error/ssr-error-client-component/page.client.js new file mode 100644 index 000000000000000..1cca8f6810c8a0b --- /dev/null +++ b/test/e2e/app-dir/app/app/error/ssr-error-client-component/page.client.js @@ -0,0 +1,3 @@ +export default function Page() { + throw new Error('Error during SSR') +} diff --git a/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/loading.js b/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/loading.js new file mode 100644 index 000000000000000..f1ca6af341511b2 --- /dev/null +++ b/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/loading.js @@ -0,0 +1,3 @@ +export default function Loading() { + return

Loading...

+} diff --git a/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/page.server.js b/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/page.server.js new file mode 100644 index 000000000000000..9352955a50433cc --- /dev/null +++ b/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/page.server.js @@ -0,0 +1,15 @@ +// @ts-ignore +import { experimental_use as use } from 'react' + +const fetchCategory = async (categorySlug) => { + // artificial delay + await new Promise((resolve) => setTimeout(resolve, 3000)) + + return categorySlug + 'abc' +} + +export default function Page({ params }) { + const category = use(fetchCategory(params.categorySlug)) + + return
{category}
+} diff --git a/test/e2e/app-dir/app/app/template/clientcomponent/other/page.server.js b/test/e2e/app-dir/app/app/template/clientcomponent/other/page.server.js new file mode 100644 index 000000000000000..24c81bb71930dfe --- /dev/null +++ b/test/e2e/app-dir/app/app/template/clientcomponent/other/page.server.js @@ -0,0 +1,11 @@ +import Link from 'next/link' +export default function Page() { + return ( + <> +

Other Page

+ + To Page + + + ) +} diff --git a/test/e2e/app-dir/app/app/template/clientcomponent/page.server.js b/test/e2e/app-dir/app/app/template/clientcomponent/page.server.js new file mode 100644 index 000000000000000..89378c7e29f176b --- /dev/null +++ b/test/e2e/app-dir/app/app/template/clientcomponent/page.server.js @@ -0,0 +1,12 @@ +import Link from 'next/link' + +export default function Page() { + return ( + <> +

Page

+ + To Other + + + ) +} diff --git a/test/e2e/app-dir/app/app/template/clientcomponent/template.client.js b/test/e2e/app-dir/app/app/template/clientcomponent/template.client.js new file mode 100644 index 000000000000000..3f9960433a4e2aa --- /dev/null +++ b/test/e2e/app-dir/app/app/template/clientcomponent/template.client.js @@ -0,0 +1,12 @@ +import { useState } from 'react' + +export default function Template({ children }) { + const [count, setCount] = useState(0) + return ( + <> +

Template {count}

+ + {children} + + ) +} diff --git a/test/e2e/app-dir/app/app/template/servercomponent/other/page.server.js b/test/e2e/app-dir/app/app/template/servercomponent/other/page.server.js new file mode 100644 index 000000000000000..2ed3a0fa15cef91 --- /dev/null +++ b/test/e2e/app-dir/app/app/template/servercomponent/other/page.server.js @@ -0,0 +1,11 @@ +import Link from 'next/link' +export default function Page() { + return ( + <> +

Other Page

+ + To Page + + + ) +} diff --git a/test/e2e/app-dir/app/app/template/servercomponent/page.server.js b/test/e2e/app-dir/app/app/template/servercomponent/page.server.js new file mode 100644 index 000000000000000..27a7097ccd650ca --- /dev/null +++ b/test/e2e/app-dir/app/app/template/servercomponent/page.server.js @@ -0,0 +1,12 @@ +import Link from 'next/link' + +export default function Page() { + return ( + <> +

Page

+ + To Other + + + ) +} diff --git a/test/e2e/app-dir/app/app/template/servercomponent/template.server.js b/test/e2e/app-dir/app/app/template/servercomponent/template.server.js new file mode 100644 index 000000000000000..c3dc5dadb9255fb --- /dev/null +++ b/test/e2e/app-dir/app/app/template/servercomponent/template.server.js @@ -0,0 +1,10 @@ +export default function Template({ children }) { + return ( + <> +

+ Template {performance.now()} +

+ {children} + + ) +} 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..0bf456d2dbdc8ea 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' @@ -22,15 +23,7 @@ describe('app dir', () => { function runTests({ assetPrefix }: { assetPrefix?: boolean }) { beforeAll(async () => { next = await createNext({ - files: { - public: new FileRef(path.join(__dirname, 'app/public')), - styles: new FileRef(path.join(__dirname, 'app/styles')), - pages: new FileRef(path.join(__dirname, 'app/pages')), - app: new FileRef(path.join(__dirname, 'app/app')), - 'next.config.js': new FileRef( - path.join(__dirname, 'app/next.config.js') - ), - }, + files: new FileRef(path.join(__dirname, 'app')), dependencies: { react: 'experimental', 'react-dom': 'experimental', @@ -378,7 +371,8 @@ describe('app dir', () => { } }) - it('should soft replace', async () => { + // TODO-APP: investigate this test + it.skip('should soft replace', async () => { const browser = await webdriver(next.url, '/link-soft-replace') try { @@ -1194,6 +1188,244 @@ 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('template component', () => { + it('should render the template that holds state in a client component and reset on navigation', async () => { + const browser = await webdriver(next.url, '/template/clientcomponent') + expect(await browser.elementByCss('h1').text()).toBe('Template 0') + await browser.elementByCss('button').click() + expect(await browser.elementByCss('h1').text()).toBe('Template 1') + + await browser.elementByCss('#link').click() + await browser.waitForElementByCss('#other-page') + + expect(await browser.elementByCss('h1').text()).toBe('Template 0') + await browser.elementByCss('button').click() + expect(await browser.elementByCss('h1').text()).toBe('Template 1') + + await browser.elementByCss('#link').click() + await browser.waitForElementByCss('#page') + + expect(await browser.elementByCss('h1').text()).toBe('Template 0') + }) + + // TODO-APP: disable failing test and investigate later + it.skip('should render the template that is a server component and rerender on navigation', async () => { + const browser = await webdriver(next.url, '/template/servercomponent') + expect(await browser.elementByCss('h1').text()).toStartWith('Template') + + const currentTime = await browser + .elementByCss('#performance-now') + .text() + + await browser.elementByCss('#link').click() + await browser.waitForElementByCss('#other-page') + + expect(await browser.elementByCss('h1').text()).toStartWith('Template') + + // template should rerender on navigation even when it's a server component + expect(await browser.elementByCss('#performance-now').text()).toBe( + currentTime + ) + + await browser.elementByCss('#link').click() + await browser.waitForElementByCss('#page') + + expect(await browser.elementByCss('#performance-now').text()).toBe( + currentTime + ) + }) + }) + + // TODO-APP: This is disabled for development as the error overlay needs to be reworked. + ;(isDev ? describe.skip : describe)('error component', () => { + it('should trigger error component when an error happens during rendering', async () => { + const browser = await webdriver(next.url, '/error/clientcomponent') + await browser + .elementByCss('#error-trigger-button') + .click() + .waitForElementByCss('#error-boundary-message') + + expect( + await browser.elementByCss('#error-boundary-message').text() + ).toBe('An error occurred: this is a test') + }) + + it('should allow resetting error boundary', async () => { + const browser = await webdriver(next.url, '/error/clientcomponent') + + // Try triggering and resetting a few times in a row + for (let i = 0; i < 5; i++) { + await browser + .elementByCss('#error-trigger-button') + .click() + .waitForElementByCss('#error-boundary-message') + + expect( + await browser.elementByCss('#error-boundary-message').text() + ).toBe('An error occurred: this is a test') + + await browser + .elementByCss('#reset') + .click() + .waitForElementByCss('#error-trigger-button') + + expect( + await browser.elementByCss('#error-trigger-button').text() + ).toBe('Trigger Error!') + } + }) + + it('should hydrate empty shell to handle server-side rendering errors', async () => { + const browser = await webdriver( + next.url, + '/error/ssr-error-client-component' + ) + const logs = await browser.log() + const errors = logs + .filter((x) => x.source === 'error') + .map((x) => x.message) + .join('\n') + expect(errors).toInclude('Error during SSR') + }) + }) + + describe('known bugs', () => { + it('should not share flight data between requests', async () => { + const fetches = await Promise.all( + [...new Array(5)].map(() => + renderViaHTTP(next.url, '/loading-bug/electronics') + ) + ) + + for (const text of fetches) { + const $ = cheerio.load(text) + expect($('#category-id').text()).toBe('electronicsabc') + } + }) + }) } describe('without assetPrefix', () => { diff --git a/test/e2e/app-dir/prefetching.test.ts b/test/e2e/app-dir/prefetching.test.ts new file mode 100644 index 000000000000000..5f9f6bdb547471c --- /dev/null +++ b/test/e2e/app-dir/prefetching.test.ts @@ -0,0 +1,57 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { waitFor } from 'next-test-utils' +import path from 'path' +import webdriver from 'next-webdriver' + +describe('app dir prefetching', () => { + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + if (process.env.NEXT_TEST_REACT_VERSION === '^17') { + it('should skip for react v17', () => {}) + return + } + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(path.join(__dirname, 'app-prefetch')), + dependencies: { + react: 'experimental', + 'react-dom': 'experimental', + }, + skipStart: true, + }) + await next.start() + }) + afterAll(() => next.destroy()) + + it('should show layout eagerly when prefetched with loading one level down', async () => { + const browser = await webdriver(next.url, '/') + // Ensure the page is prefetched + await waitFor(1000) + + const before = Date.now() + await browser + .elementByCss('#to-dashboard') + .click() + .waitForElementByCss('#dashboard-layout') + const after = Date.now() + const timeToComplete = after - before + + expect(timeToComplete < 1000).toBe(true) + + expect(await browser.elementByCss('#dashboard-layout').text()).toBe( + 'Dashboard Hello World' + ) + + await browser.waitForElementByCss('#dashboard-page') + + expect(await browser.waitForElementByCss('#dashboard-page').text()).toBe( + 'Welcome to the dashboard' + ) + }) +}) diff --git a/test/e2e/app-dir/rendering.test.ts b/test/e2e/app-dir/rendering.test.ts index 69f1f0dd3859c94..d7168bcd275d50c 100644 --- a/test/e2e/app-dir/rendering.test.ts +++ b/test/e2e/app-dir/rendering.test.ts @@ -20,12 +20,7 @@ describe('app dir rendering', () => { beforeAll(async () => { next = await createNext({ - files: { - app: new FileRef(path.join(__dirname, 'app-rendering/app')), - 'next.config.js': new FileRef( - path.join(__dirname, 'app-rendering/next.config.js') - ), - }, + files: new FileRef(path.join(__dirname, 'app-rendering')), dependencies: { react: 'experimental', 'react-dom': 'experimental', diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index c9996fe5ec0d286..e548c458445f90d 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -35,15 +35,8 @@ describe('app dir - react server components', () => { } beforeAll(async () => { - const appDir = path.join(__dirname, './rsc-basic') next = await createNext({ - files: { - node_modules_bak: new FileRef(path.join(appDir, 'node_modules_bak')), - public: new FileRef(path.join(appDir, 'public')), - components: new FileRef(path.join(appDir, 'components')), - app: new FileRef(path.join(appDir, 'app')), - 'next.config.js': new FileRef(path.join(appDir, 'next.config.js')), - }, + files: new FileRef(path.join(__dirname, './rsc-basic')), dependencies: { 'styled-components': '6.0.0-alpha.5', react: 'experimental', @@ -340,6 +333,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/e2e/app-dir/trailingslash.test.ts b/test/e2e/app-dir/trailingslash.test.ts new file mode 100644 index 000000000000000..0b8ca82f7553265 --- /dev/null +++ b/test/e2e/app-dir/trailingslash.test.ts @@ -0,0 +1,66 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils' +import path from 'path' +import cheerio from 'cheerio' +import webdriver from 'next-webdriver' + +describe('app-dir trailingSlash handling', () => { + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + if (process.env.NEXT_TEST_REACT_VERSION === '^17') { + it('should skip for react v17', () => {}) + return + } + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(path.join(__dirname, 'trailingslash')), + dependencies: { + react: 'experimental', + 'react-dom': 'experimental', + }, + skipStart: true, + }) + + await next.start() + }) + afterAll(() => next.destroy()) + + it('should redirect route when requesting it directly', async () => { + const res = await fetchViaHTTP( + next.url, + '/a', + {}, + { + redirect: 'manual', + } + ) + expect(res.status).toBe(308) + expect(res.headers.get('location')).toBe(next.url + '/a/') + }) + + it('should render link with trailing slash', async () => { + const html = await renderViaHTTP(next.url, '/') + const $ = cheerio.load(html) + expect($('#to-a-trailing-slash').attr('href')).toBe('/a/') + }) + + it('should redirect route when requesting it directly by browser', async () => { + const browser = await webdriver(next.url, '/a') + expect(await browser.waitForElementByCss('#a-page').text()).toBe('A page') + }) + + it('should redirect route when clicking link', async () => { + const browser = await webdriver(next.url, '/') + await browser + .elementByCss('#to-a-trailing-slash') + .click() + .waitForElementByCss('#a-page') + expect(await browser.waitForElementByCss('#a-page').text()).toBe('A page') + }) +}) diff --git a/test/e2e/app-dir/trailingslash/app/a/page.server.js b/test/e2e/app-dir/trailingslash/app/a/page.server.js new file mode 100644 index 000000000000000..d61256ee9f3d400 --- /dev/null +++ b/test/e2e/app-dir/trailingslash/app/a/page.server.js @@ -0,0 +1,9 @@ +import Link from 'next/link' +export default function HomePage() { + return ( + <> +

A page

+ To home + + ) +} diff --git a/test/e2e/app-dir/trailingslash/app/layout.server.js b/test/e2e/app-dir/trailingslash/app/layout.server.js new file mode 100644 index 000000000000000..05b841b280b3fc7 --- /dev/null +++ b/test/e2e/app-dir/trailingslash/app/layout.server.js @@ -0,0 +1,10 @@ +export default function Root({ children }) { + return ( + + + Hello + + {children} + + ) +} diff --git a/test/e2e/app-dir/trailingslash/app/page.server.js b/test/e2e/app-dir/trailingslash/app/page.server.js new file mode 100644 index 000000000000000..f02fd1b341b1c44 --- /dev/null +++ b/test/e2e/app-dir/trailingslash/app/page.server.js @@ -0,0 +1,12 @@ +import Link from 'next/link' +export default function HomePage() { + return ( + <> +

+ + To a with trailing slash + +

+ + ) +} diff --git a/test/e2e/app-dir/trailingslash/next.config.js b/test/e2e/app-dir/trailingslash/next.config.js new file mode 100644 index 000000000000000..a4194e110b51791 --- /dev/null +++ b/test/e2e/app-dir/trailingslash/next.config.js @@ -0,0 +1,9 @@ +module.exports = { + experimental: { + appDir: true, + serverComponents: true, + legacyBrowsers: false, + browsersListForSwc: true, + }, + trailingSlash: true, +} diff --git a/test/e2e/middleware-general/app/middleware.js b/test/e2e/middleware-general/app/middleware.js index 83ba6722ff61e8c..5a5f2f903d01931 100644 --- a/test/e2e/middleware-general/app/middleware.js +++ b/test/e2e/middleware-general/app/middleware.js @@ -47,6 +47,12 @@ export async function middleware(request) { return NextResponse.next() } + if (url.pathname === '/api/edge-search-params') { + const newUrl = url.clone() + newUrl.searchParams.set('foo', 'bar') + return NextResponse.rewrite(newUrl) + } + if (url.pathname === '/') { url.pathname = '/ssg/first' return NextResponse.rewrite(url) diff --git a/test/e2e/middleware-general/app/pages/api/edge-search-params.js b/test/e2e/middleware-general/app/pages/api/edge-search-params.js new file mode 100644 index 000000000000000..a67cecdf18ec0e1 --- /dev/null +++ b/test/e2e/middleware-general/app/pages/api/edge-search-params.js @@ -0,0 +1,10 @@ +import { NextResponse } from 'next/server' + +export const config = { runtime: 'experimental-edge' } + +/** + * @param {import('next/server').NextRequest} + */ +export default (req) => { + return NextResponse.json(Object.fromEntries(req.nextUrl.searchParams)) +} diff --git a/test/e2e/middleware-general/test/index.test.ts b/test/e2e/middleware-general/test/index.test.ts index a99d002b1201001..e5e19c6bc1e95b1 100644 --- a/test/e2e/middleware-general/test/index.test.ts +++ b/test/e2e/middleware-general/test/index.test.ts @@ -116,7 +116,10 @@ describe('Middleware Runtime', () => { 'ANOTHER_MIDDLEWARE_TEST', 'STRING_ENV_VAR', ], - files: ['server/edge-runtime-webpack.js', 'server/middleware.js'], + files: expect.arrayContaining([ + 'server/edge-runtime-webpack.js', + 'server/middleware.js', + ]), name: 'middleware', page: '/', matchers: [{ regexp: '^/.*$' }], @@ -157,6 +160,17 @@ describe('Middleware Runtime', () => { }) } + it('passes search params with rewrites', async () => { + const response = await fetchViaHTTP(next.url, `/api/edge-search-params`, { + a: 'b', + }) + await expect(response.json()).resolves.toMatchObject({ + a: 'b', + // included from middleware + foo: 'bar', + }) + }) + it('should have init header for NextResponse.redirect', async () => { const res = await fetchViaHTTP( next.url, diff --git a/test/e2e/switchable-runtime/index.test.ts b/test/e2e/switchable-runtime/index.test.ts index f21f94dba24bf5d..5485116ebc5c10f 100644 --- a/test/e2e/switchable-runtime/index.test.ts +++ b/test/e2e/switchable-runtime/index.test.ts @@ -202,6 +202,258 @@ describe('Switchable runtime', () => { }) } }) + + it('should be possible to switch between runtimes in API routes', async () => { + await check( + () => renderViaHTTP(next.url, '/api/switch-in-dev'), + 'server response' + ) + + // Edge + await next.patchFile( + 'pages/api/switch-in-dev.js', + ` + export const config = { + runtime: 'experimental-edge', + } + + export default () => new Response('edge response') + ` + ) + await check( + () => renderViaHTTP(next.url, '/api/switch-in-dev'), + 'edge response' + ) + + // Server + await next.patchFile( + 'pages/api/switch-in-dev.js', + ` + export default function (req, res) { + res.send('server response again') + } + ` + ) + await check( + () => renderViaHTTP(next.url, '/api/switch-in-dev'), + 'server response again' + ) + + // Edge + await next.patchFile( + 'pages/api/switch-in-dev.js', + ` + export const config = { + runtime: 'experimental-edge', + } + + export default () => new Response('edge response again') + ` + ) + await check( + () => renderViaHTTP(next.url, '/api/switch-in-dev'), + 'edge response again' + ) + }) + + it('should be possible to switch between runtimes in pages', async () => { + await check( + () => renderViaHTTP(next.url, '/switch-in-dev'), + /Hello from edge page/ + ) + + // Server + await next.patchFile( + 'pages/switch-in-dev.js', + ` + export default function Page() { + return

Hello from server page

+ } + ` + ) + await check( + () => renderViaHTTP(next.url, '/switch-in-dev'), + /Hello from server page/ + ) + + // Edge + await next.patchFile( + 'pages/switch-in-dev.js', + ` + export default function Page() { + return

Hello from edge page again

+ } + + export const config = { + runtime: 'experimental-edge', + } + ` + ) + await check( + () => renderViaHTTP(next.url, '/switch-in-dev'), + /Hello from edge page again/ + ) + + // Server + await next.patchFile( + 'pages/switch-in-dev.js', + ` + export default function Page() { + return

Hello from server page again

+ } + ` + ) + await check( + () => renderViaHTTP(next.url, '/switch-in-dev'), + /Hello from server page again/ + ) + }) + + // Doesn't work, see https://github.com/vercel/next.js/pull/39327 + it.skip('should be possible to switch between runtimes with same content', async () => { + const fileContent = await next.readFile( + 'pages/api/switch-in-dev-same-content.js' + ) + console.log({ fileContent }) + await check( + () => renderViaHTTP(next.url, '/api/switch-in-dev-same-content'), + 'server response' + ) + + // Edge + await next.patchFile( + 'pages/api/switch-in-dev-same-content.js', + ` + export const config = { + runtime: 'experimental-edge', + } + + export default () => new Response('edge response') + ` + ) + await check( + () => renderViaHTTP(next.url, '/api/switch-in-dev-same-content'), + 'edge response' + ) + + // Server - same content as first compilation of the server runtime version + await next.patchFile( + 'pages/api/switch-in-dev-same-content.js', + fileContent + ) + await check( + () => renderViaHTTP(next.url, '/api/switch-in-dev-same-content'), + 'server response' + ) + }) + + it('should recover from syntax error when using edge runtime', async () => { + await check( + () => renderViaHTTP(next.url, '/api/syntax-error-in-dev'), + 'edge response' + ) + + // Syntax error + await next.patchFile( + 'pages/api/syntax-error-in-dev.js', + ` + export const config = { + runtime: 'experimental-edge', + } + + export default => new Response('edge response') + ` + ) + await check( + () => renderViaHTTP(next.url, '/api/syntax-error-in-dev'), + /Unexpected token/ + ) + + // Fix syntax error + await next.patchFile( + 'pages/api/syntax-error-in-dev.js', + ` + export default () => new Response('edge response again') + + export const config = { + runtime: 'experimental-edge', + } + + ` + ) + await check( + () => renderViaHTTP(next.url, '/api/syntax-error-in-dev'), + 'edge response again' + ) + }) + + it('should not crash the dev server when invalid runtime is configured', async () => { + await check( + () => renderViaHTTP(next.url, '/invalid-runtime'), + /Hello from page without errors/ + ) + + // Invalid runtime type + await next.patchFile( + 'pages/invalid-runtime.js', + ` + export default function Page() { + return

Hello from page with invalid type

+ } + + export const config = { + runtime: 10, + } + ` + ) + await check( + () => renderViaHTTP(next.url, '/invalid-runtime'), + /Hello from page with invalid type/ + ) + expect(next.cliOutput).toInclude( + 'error - The `runtime` config must be a string. Please leave it empty or choose one of:' + ) + + // Invalid runtime + await next.patchFile( + 'pages/invalid-runtime.js', + ` + export default function Page() { + return

Hello from page with invalid runtime

+ } + + export const config = { + runtime: "asd" + } + ` + ) + await check( + () => renderViaHTTP(next.url, '/invalid-runtime'), + /Hello from page with invalid runtime/ + ) + expect(next.cliOutput).toInclude( + 'error - Provided runtime "asd" is not supported. Please leave it empty or choose one of:' + ) + + // Fix the runtime + await next.patchFile( + 'pages/invalid-runtime.js', + ` + export default function Page() { + return

Hello from page without errors

+ } + + export const config = { + runtime: 'experimental-edge', + } + + ` + ) + await check( + () => renderViaHTTP(next.url, '/invalid-runtime'), + /Hello from page without errors/ + ) + }) }) } else { describe('Switchable runtime (prod)', () => { diff --git a/test/e2e/switchable-runtime/pages/api/switch-in-dev-same-content.js b/test/e2e/switchable-runtime/pages/api/switch-in-dev-same-content.js new file mode 100644 index 000000000000000..a587c8cb1a71b08 --- /dev/null +++ b/test/e2e/switchable-runtime/pages/api/switch-in-dev-same-content.js @@ -0,0 +1,3 @@ +export default (req, res) => { + res.send('server response') +} diff --git a/test/e2e/switchable-runtime/pages/api/switch-in-dev.js b/test/e2e/switchable-runtime/pages/api/switch-in-dev.js new file mode 100644 index 000000000000000..a587c8cb1a71b08 --- /dev/null +++ b/test/e2e/switchable-runtime/pages/api/switch-in-dev.js @@ -0,0 +1,3 @@ +export default (req, res) => { + res.send('server response') +} diff --git a/test/e2e/switchable-runtime/pages/api/syntax-error-in-dev.js b/test/e2e/switchable-runtime/pages/api/syntax-error-in-dev.js new file mode 100644 index 000000000000000..3b7243a1205ef17 --- /dev/null +++ b/test/e2e/switchable-runtime/pages/api/syntax-error-in-dev.js @@ -0,0 +1,5 @@ +export default () => new Response('edge response') + +export const config = { + runtime: `experimental-edge`, +} diff --git a/test/e2e/switchable-runtime/pages/invalid-runtime.js b/test/e2e/switchable-runtime/pages/invalid-runtime.js new file mode 100644 index 000000000000000..17235157127be1a --- /dev/null +++ b/test/e2e/switchable-runtime/pages/invalid-runtime.js @@ -0,0 +1,3 @@ +export default function Page() { + return

Hello from page without errors

+} diff --git a/test/e2e/switchable-runtime/pages/switch-in-dev.js b/test/e2e/switchable-runtime/pages/switch-in-dev.js new file mode 100644 index 000000000000000..b67d4cabd53c4f0 --- /dev/null +++ b/test/e2e/switchable-runtime/pages/switch-in-dev.js @@ -0,0 +1,7 @@ +export default function Page() { + return

Hello from edge page

+} + +export const config = { + runtime: 'experimental-edge', +} diff --git a/test/integration/edge-runtime-configurable-guards/lib/index.js b/test/integration/edge-runtime-configurable-guards/lib/index.js new file mode 100644 index 000000000000000..8fa47bde2f62fd8 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/lib/index.js @@ -0,0 +1 @@ +// populated by tests diff --git a/test/integration/edge-runtime-configurable-guards/middleware.js b/test/integration/edge-runtime-configurable-guards/middleware.js new file mode 100644 index 000000000000000..361c04d84d89f85 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/middleware.js @@ -0,0 +1,10 @@ +import { NextResponse } from 'next/server' + +// populated with tests +export default () => { + return NextResponse.next() +} + +export const config = { + matcher: '/', +} diff --git a/test/integration/edge-runtime-configurable-guards/pages/api/route.js b/test/integration/edge-runtime-configurable-guards/pages/api/route.js new file mode 100644 index 000000000000000..9d808b1a2bb6800 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/pages/api/route.js @@ -0,0 +1,8 @@ +// populated by tests +export default () => { + return Response.json({ ok: true }) +} + +export const config = { + runtime: 'experimental-edge', +} diff --git a/test/integration/edge-runtime-configurable-guards/pages/index.js b/test/integration/edge-runtime-configurable-guards/pages/index.js new file mode 100644 index 000000000000000..c5cc676685b679a --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/pages/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return
ok
+} diff --git a/test/integration/edge-runtime-configurable-guards/test/index.test.js b/test/integration/edge-runtime-configurable-guards/test/index.test.js new file mode 100644 index 000000000000000..bc218a0f03265b8 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/test/index.test.js @@ -0,0 +1,390 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { + fetchViaHTTP, + File, + findPort, + killApp, + launchApp, + nextBuild, + nextStart, + waitFor, +} from 'next-test-utils' +import { remove } from 'fs-extra' + +jest.setTimeout(1000 * 60 * 2) + +const context = { + appDir: join(__dirname, '../'), + logs: { output: '', stdout: '', stderr: '' }, + api: new File(join(__dirname, '../pages/api/route.js')), + middleware: new File(join(__dirname, '../middleware.js')), + lib: new File(join(__dirname, '../lib/index.js')), +} +const appOption = { + env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, + onStdout(msg) { + context.logs.output += msg + context.logs.stdout += msg + }, + onStderr(msg) { + context.logs.output += msg + context.logs.stderr += msg + }, +} +const routeUrl = '/api/route' +const middlewareUrl = '/' +const TELEMETRY_EVENT_NAME = 'NEXT_EDGE_ALLOW_DYNAMIC_USED' + +describe('Edge runtime configurable guards', () => { + beforeEach(async () => { + await remove(join(__dirname, '../.next')) + context.appPort = await findPort() + context.logs = { output: '', stdout: '', stderr: '' } + }) + + afterEach(() => { + if (context.app) { + killApp(context.app) + } + context.api.restore() + context.middleware.restore() + context.lib.restore() + }) + + describe('Multiple functions with different configurations', () => { + beforeEach(() => { + context.middleware.write(` + import { NextResponse } from 'next/server' + + export default () => { + eval('100') + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '/middleware.js' + } + `) + context.api.write(` + export default async function handler(request) { + eval('100') + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + unstable_allowDynamic: '/lib/**' + } + `) + }) + + it('warns in dev for allowed code', async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, middlewareUrl) + await waitFor(500) + expect(res.status).toBe(200) + expect(context.logs.output).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + + it('warns in dev for unallowed code', async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, routeUrl) + await waitFor(500) + expect(res.status).toBe(200) + expect(context.logs.output).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + + it('fails to build because of unallowed code', async () => { + const output = await nextBuild(context.appDir, undefined, { + stdout: true, + stderr: true, + env: { NEXT_TELEMETRY_DEBUG: 1 }, + }) + expect(output.stderr).toContain(`Build failed`) + expect(output.stderr).toContain(`./pages/api/route.js`) + expect(output.stderr).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime` + ) + expect(output.stderr).toContain(`Used by default`) + expect(output.stderr).toContain(TELEMETRY_EVENT_NAME) + }) + }) + + describe.each([ + { + title: 'Edge API', + url: routeUrl, + init() { + context.api.write(` + export default async function handler(request) { + eval('100') + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + unstable_allowDynamic: '**' + } + `) + }, + }, + { + title: 'Middleware', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + + export default () => { + eval('100') + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '**' + } + `) + }, + }, + { + title: 'Edge API using lib', + url: routeUrl, + init() { + context.api.write(` + import { hasDynamic } from '../../lib' + export default async function handler(request) { + await hasDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + unstable_allowDynamic: '/lib/**' + } + `) + context.lib.write(` + export async function hasDynamic() { + eval('100') + } + `) + }, + }, + { + title: 'Middleware using lib', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + import { hasDynamic } from './lib' + + // populated with tests + export default async function () { + await hasDynamic() + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '/lib/**' + } + `) + context.lib.write(` + export async function hasDynamic() { + eval('100') + } + `) + }, + }, + ])('$title with allowed, used dynamic code', ({ init, url }) => { + beforeEach(() => init()) + + it('still warns in dev at runtime', async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, url) + await waitFor(500) + expect(res.status).toBe(200) + expect(context.logs.output).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) + + describe.each([ + { + title: 'Edge API', + url: routeUrl, + init() { + context.api.write(` + export default async function handler(request) { + if ((() => false)()) { + eval('100') + } + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + unstable_allowDynamic: '**' + } + `) + }, + }, + { + title: 'Middleware', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + // populated with tests + export default () => { + if ((() => false)()) { + eval('100') + } + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '**' + } + `) + }, + }, + { + title: 'Edge API using lib', + url: routeUrl, + init() { + context.api.write(` + import { hasUnusedDynamic } from '../../lib' + export default async function handler(request) { + await hasUnusedDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + unstable_allowDynamic: '/lib/**' + } + `) + context.lib.write(` + export async function hasUnusedDynamic() { + if ((() => false)()) { + eval('100') + } + } + `) + }, + }, + { + title: 'Middleware using lib', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + import { hasUnusedDynamic } from './lib' + // populated with tests + export default async function () { + await hasUnusedDynamic() + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '/lib/**' + } + `) + context.lib.write(` + export async function hasUnusedDynamic() { + if ((() => false)()) { + eval('100') + } + } + `) + }, + }, + ])('$title with allowed, unused dynamic code', ({ init, url }) => { + beforeEach(() => init()) + + it('build and does not warn at runtime', async () => { + const output = await nextBuild(context.appDir, undefined, { + stdout: true, + stderr: true, + env: { NEXT_TELEMETRY_DEBUG: 1 }, + }) + expect(output.stderr).not.toContain(`Build failed`) + expect(output.stderr).toContain(TELEMETRY_EVENT_NAME) + context.app = await nextStart(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, url) + expect(res.status).toBe(200) + expect(context.logs.output).not.toContain(`warn`) + expect(context.logs.output).not.toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) + + describe.each([ + { + title: 'Edge API using lib', + url: routeUrl, + init() { + context.api.write(` + import { hasDynamic } from '../../lib' + export default async function handler(request) { + await hasDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + unstable_allowDynamic: '/pages/**' + } + `) + context.lib.write(` + export async function hasDynamic() { + eval('100') + } + `) + }, + }, + { + title: 'Middleware using lib', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + import { hasDynamic } from './lib' + export default async function () { + await hasDynamic() + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '/pages/**' + } + `) + context.lib.write(` + export async function hasDynamic() { + eval('100') + } + `) + }, + }, + ])('$title with unallowed, used dynamic code', ({ init, url }) => { + beforeEach(() => init()) + + it('warns in dev at runtime', async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, url) + await waitFor(500) + expect(res.status).toBe(200) + expect(context.logs.output).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + + it('fails to build because of dynamic code evaluation', async () => { + const output = await nextBuild(context.appDir, undefined, { + stdout: true, + stderr: true, + env: { NEXT_TELEMETRY_DEBUG: 1 }, + }) + expect(output.stderr).toContain(`Build failed`) + expect(output.stderr).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime` + ) + expect(output.stderr).toContain(TELEMETRY_EVENT_NAME) + }) + }) +}) 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/integration/next-dynamic/test/index.test.js b/test/integration/next-dynamic/test/index.test.js index e0c8f9be9ae73cd..b926fdfca98aa1b 100644 --- a/test/integration/next-dynamic/test/index.test.js +++ b/test/integration/next-dynamic/test/index.test.js @@ -31,6 +31,12 @@ function runTests() { // Failure case is 'Index3' expect(text).toBe('Index12344') expect(await browser.eval('window.caughtErrors')).toBe('') + + // should not print "invalid-dynamic-suspense" warning in browser's console + const logs = (await browser.log()).map((log) => log.message).join('\n') + expect(logs).not.toContain( + 'https://nextjs.org/docs/messages/invalid-dynamic-suspense' + ) }) } diff --git a/test/integration/preload-viewport/pages/bot-user-agent.js b/test/integration/preload-viewport/pages/bot-user-agent.js new file mode 100644 index 000000000000000..aa524d95585096c --- /dev/null +++ b/test/integration/preload-viewport/pages/bot-user-agent.js @@ -0,0 +1,22 @@ +import Head from 'next/head' +import Link from 'next/link' + +export default () => { + return ( +
+ + + +
+ + to /another + +
+ ) +} diff --git a/test/integration/preload-viewport/test/index.test.js b/test/integration/preload-viewport/test/index.test.js index b8aad8d5a45fb77..9f0e023ea54c6f8 100644 --- a/test/integration/preload-viewport/test/index.test.js +++ b/test/integration/preload-viewport/test/index.test.js @@ -110,6 +110,38 @@ describe('Prefetching Links in viewport', () => { } }) + it('should prefetch with non-bot UA', async () => { + let browser + try { + browser = await webdriver( + appPort, + `/bot-user-agent?useragent=${encodeURIComponent( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36' + )}` + ) + const links = await browser.elementsByCss('link[rel=prefetch]') + expect(links).toHaveLength(1) + } finally { + if (browser) await browser.close() + } + }) + + it('should not prefetch with bot UA', async () => { + let browser + try { + browser = await webdriver( + appPort, + `/bot-user-agent?useragent=${encodeURIComponent( + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/W.X.Y.Z Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' + )}` + ) + const links = await browser.elementsByCss('link[rel=prefetch]') + expect(links).toHaveLength(0) + } finally { + if (browser) await browser.close() + } + }) + it('should prefetch rewritten href with link in viewport onload', async () => { let browser try { diff --git a/test/integration/typescript/pages/_app.tsx b/test/integration/typescript/pages/_app.tsx index bc26412e55dae24..adc878fa93ecd44 100644 --- a/test/integration/typescript/pages/_app.tsx +++ b/test/integration/typescript/pages/_app.tsx @@ -1,20 +1,9 @@ -// import App from "next/app"; -import type { AppProps /*, AppContext */ } from 'next/app' +import type { AppType } from 'next/app' -function MyApp({ Component, pageProps }: AppProps) { +const MyApp: AppType<{ foo: string }> = ({ Component, pageProps }) => { return } -// Only uncomment this method if you have blocking data requirements for -// every single page in your application. This disables the ability to -// perform automatic static optimization, causing every page in your app to -// be server-side rendered. -// -// MyApp.getInitialProps = async (appContext: AppContext) => { -// // calls page's `getInitialProps` and fills `appProps.pageProps` -// const appProps = await App.getInitialProps(appContext); - -// return { ...appProps } -// } +MyApp.getInitialProps = () => ({ foo: 'bar' }) export default MyApp 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/edge-config-validations/index.test.ts b/test/production/edge-config-validations/index.test.ts new file mode 100644 index 000000000000000..d531172c34c2171 --- /dev/null +++ b/test/production/edge-config-validations/index.test.ts @@ -0,0 +1,35 @@ +import { createNext } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' + +describe('Edge config validations', () => { + let next: NextInstance + + afterAll(() => next.destroy()) + + it('fails to build when unstable_allowDynamic is not a string', async () => { + next = await createNext({ + skipStart: true, + files: { + 'pages/index.js': ` + export default function Page() { + return

hello world

+ } + `, + 'middleware.js': ` + import { NextResponse } from 'next/server' + export default async function middleware(request) { + return NextResponse.next() + } + + eval('toto') + + export const config = { unstable_allowDynamic: true } + `, + }, + }) + await expect(next.start()).rejects.toThrow('next build failed') + expect(next.cliOutput).toMatch( + `/middleware exported 'config.unstable_allowDynamic' contains invalid pattern 'true': Expected pattern to be a non-empty string` + ) + }) +}) 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 9f2446f93c99236..de7d1bcc26c32de 100644 --- a/test/production/required-server-files.test.ts +++ b/test/production/required-server-files.test.ts @@ -324,6 +324,12 @@ describe('should set-up next', () => { it('should de-dupe HTML/data requests', async () => { const res = await fetchViaHTTP(appPort, '/gsp', undefined, { redirect: 'manual', + headers: { + // ensure the nextjs-data header being present + // doesn't incorrectly return JSON for HTML path + // during prerendering + 'x-nextjs-data': '1', + }, }) expect(res.status).toBe(200) expect(res.headers.get('x-nextjs-cache')).toBeFalsy() @@ -416,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( @@ -427,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( @@ -439,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( @@ -449,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') diff --git a/test/readme.md b/test/readme.md index 574c5fda5124563..b9c28639e55f513 100644 --- a/test/readme.md +++ b/test/readme.md @@ -2,7 +2,7 @@ ## Getting Started -You can set-up a new test using `yarn new-test` which will start from a template related to the test type. +You can set-up a new test using `pnpnm new-test` which will start from a template related to the test type. ## Test Types in Next.js diff --git a/test/unit/eslint-plugin-next/no-script-component-in-head.test.ts b/test/unit/eslint-plugin-next/no-script-component-in-head.test.ts index a6882b12b961cc0..c4ae01685660b6c 100644 --- a/test/unit/eslint-plugin-next/no-script-component-in-head.test.ts +++ b/test/unit/eslint-plugin-next/no-script-component-in-head.test.ts @@ -44,7 +44,7 @@ ruleTester.run('no-script-in-head', rule, { errors: [ { message: - '`next/script` should not be used in `next/head` component. Move `