diff --git a/docs/02-app/01-building-your-application/01-routing/10-parallel-routes.mdx b/docs/02-app/01-building-your-application/01-routing/10-parallel-routes.mdx index 91261c114031c..0bffa7457777f 100644 --- a/docs/02-app/01-building-your-application/01-routing/10-parallel-routes.mdx +++ b/docs/02-app/01-building-your-application/01-routing/10-parallel-routes.mdx @@ -15,7 +15,7 @@ For example, considering a dashboard, you can use parallel routes to simultaneou srcLight="/docs/light/parallel-routes.png" srcDark="/docs/dark/parallel-routes.png" width="1600" - height="952" + height="942" /> ## Slots @@ -33,28 +33,32 @@ Parallel routes are created using named **slots**. Slots are defined with the `@ Slots are passed as props to the shared parent layout. For the example above, the component in `app/layout.js` now accepts the `@analytics` and `@team` slots props, and can render them in parallel alongside the `children` prop: ```tsx filename="app/layout.tsx" switcher -export default function Layout(props: { +export default function Layout({ + children, + team, + analytics, +}: { children: React.ReactNode analytics: React.ReactNode team: React.ReactNode }) { return ( <> - {props.children} - {props.team} - {props.analytics} + {children} + {team} + {analytics} ) } ``` ```jsx filename="app/layout.js" switcher -export default function Layout(props) { +export default function Layout({ children, team, analytics }) { return ( <> - {props.children} - {props.team} - {props.analytics} + {children} + {team} + {analytics} ) } @@ -71,17 +75,17 @@ However, slots are **not** [route segments](/docs/app/building-your-application/ By default, Next.js keeps track of the active _state_ (or subpage) for each slot. However, the content rendered within a slot will depend on the type of navigation: - [**Soft Navigation**](/docs/app/building-your-application/routing/linking-and-navigating#5-soft-navigation): During client-side navigation, Next.js will perform a [partial render](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering), changing the subpage within the slot, while maintaining the other slot's active subpages, even if they don't match the current URL. -- **Hard Navigation**: After a full-page load (browser refresh), Next.js cannot determine the active state of slots that don't match the current URL. Instead, it will render a [`default.js`](#defaultjs) file for the unmatched slots, or `404` if `default.js` doesn't exist. +- **Hard Navigation**: After a full-page load (browser refresh), Next.js cannot determine the active state for the slots that don't match the current URL. Instead, it will render a [`default.js`](#defaultjs) file for the unmatched slots, or `404` if `default.js` doesn't exist. > **Good to know**: > -> - The `404` for unmatched routes helps ensure that you don't accidentally render a route that shouldn't be parallel rendered. +> - The `404` for unmatched routes helps ensure that you don't accidentally render a parallel route on a page that it was not intended for. ### `default.js` You can define a `default.js` file to render as a fallback for unmatched slots during the initial load or full-page reload. -Consider the following folder structure. The `@team` slot has a `settings` page, but `@analytics` does not. +Consider the following folder structure. The `@team` slot has a `/settings` page, but `@analytics` does not. Parallel Routes unmatched routes -When navigating to `/dashboard/settings`, the `@team` slot will render the `settings` page while maintaining the currently active page for the `@analytics` slot. +When navigating to `/dashboard/settings`, the `@team` slot will render the `/settings` page while maintaining the currently active page for the `@analytics` slot. On refresh, Next.js will render a `default.js` for `@analytics`. If `default.js` doesn't exist, a `404` is rendered instead. @@ -106,10 +110,7 @@ Both [`useSelectedLayoutSegment`](/docs/app/api-reference/functions/use-selected import { useSelectedLayoutSegment } from 'next/navigation' -export default function Layout(props: { - //... - auth: React.ReactNode -}) { +export default function Layout({ auth }: { auth: React.ReactNode }) { const loginSegments = useSelectedLayoutSegment('auth') // ... } @@ -120,7 +121,7 @@ export default function Layout(props: { import { useSelectedLayoutSegment } from 'next/navigation' -export default function Layout(props) { +export default function Layout({ auth }) { const loginSegments = useSelectedLayoutSegment('auth') // ... } @@ -130,72 +131,136 @@ When a user navigates to `app/@auth/login` (or `/login` in the URL bar), `loginS ## Examples -### Modals +### Conditional Routes -Parallel Routing can be used to render modals. +You can use Parallel Routes to conditionally render routes based on certain conditions, such as user role. For example, to render a different dashboard page for the `/admin` or `/user` roles: Parallel Routes Diagram -The `@auth` slot renders a `` component that can be shown by navigating to a matching route, for example `/login`. +```tsx filename="app/dashboard/layout.tsx" switcher +import { checkUserRole } from '@/lib/auth' -```tsx filename="app/layout.tsx" switcher -export default async function Layout(props: { - // ... - auth: React.ReactNode +export default function Layout({ + user, + admin, +}: { + user: React.ReactNode + admin: React.ReactNode }) { + const role = checkUserRole() + return <>{role === 'admin' ? admin : user} +} +``` + +```jsx filename="app/dashboard/layout.js" switcher +import { checkUserRole } from '@/lib/auth' + +export default function Layout({ user, admin }) { + const role = checkUserRole() + return <>{role === 'admin' ? admin : user} +} +``` + +## Tab Groups + +You can add a `layout` inside a slot to allow users to navigate the slot independently. This is useful for creating tabs. + +For example, the `@analytics` slot has two subpages: `/page-views` and `/visitors`. + +Analytics slot with two subpages and a layout + +Within `@analytics`, create a [`layout`](/docs/app/building-your-application/routing/pages-and-layouts) file to share the tabs between the two pages: + +```tsx filename="app/dashboard/@analytics/layout.tsx" switcher +import Link from 'next/link' + +export default function Layout({ children }: { children: React.ReactNode }) { return ( <> - {/* ... */} - {props.auth} + +
{children}
) } ``` ```jsx filename="app/layout.js" switcher -export default async function Layout(props) { +import Link from 'next/link' + +export default function Layout({ children }: { children: React.ReactNode }) { return ( <> - {/* ... */} - {props.auth} + +
{children}
) } ``` -```tsx filename="app/@auth/login/page.tsx" switcher -import { Modal } from 'components/modal' +### Modals -export default function Login() { - return ( - -

Login

- {/* ... */} -
- ) +Parallel Routes can be used together with [Intercepting Routes](/docs/app/building-your-application/routing/intercepting-routes) to create modals. This allows you to solve common challenges when building modals, such as: + +- Making the modal content **shareable through a URL**. +- **Preserving context** when the page is refreshed, instead of closing the modal. +- **Closing the modal on backwards navigation** rather than going to the previous route. +- **Reopening the modal on forwards navigation**. + +Consider the following UI pattern, where a user can open a login modal from a layout using client-side navigation, or access a separate `/login` page: + +Parallel Routes Diagram + +To implement this pattern, start by creating a `/login` route that renders your **main** login page. + +Parallel Routes Diagram + +```tsx filename="app/login/page.tsx" switcher +import { Login } from '@/app/ui/login' + +export default function Page() { + return } ``` -```jsx filename="app/@auth/login/page.js" switcher -import { Modal } from 'components/modal' +```jsx filename="app/login/page.js" switcher +import { Login } from '@/app/ui/login' -export default function Login() { - return ( - -

Login

- {/* ... */} -
- ) +export default function Page() { + return } ``` -To ensure that the contents of the modal don't get rendered when it's not active, you can create a `default.js` file that returns `null`. +Then, inside the `@auth` slot, add [`default.js`](/docs/app/api-reference/file-conventions/default) file that returns `null`. This ensures that the modal is not rendered when it's not active. ```tsx filename="app/@auth/default.tsx" switcher export default function Default() { @@ -209,117 +274,142 @@ export default function Default() { } ``` -#### Dismissing a modal - -If a modal was initiated through client navigation, e.g. by using ``, you can dismiss the modal by calling `router.back()` or by using a `Link` component. +Inside your `@auth` slot, intercept the `/login` route by updating the `/(.)login` folder. Import the `` component and its children into the `/(.)login/page.tsx` file: -```tsx filename="app/@auth/login/page.tsx" highlight="5" switcher -'use client' -import { useRouter } from 'next/navigation' -import { Modal } from 'components/modal' +```tsx filename="app/@auth/(.)login/page.tsx" switcher +import { Modal } from '@/app/ui/modal' +import { Login } from '@/app/ui/login' -export default function Login() { - const router = useRouter() +export default function Page() { return ( - router.back()}>Close modal -

Login

- ... +
) } ``` -```jsx filename="app/@auth/login/page.js" highlight="5" switcher -'use client' -import { useRouter } from 'next/navigation' -import { Modal } from 'components/modal' +```jsx filename="app/@auth/(.)login/page.js" switcher +import { Modal } from '@/app/ui/modal' +import { Login } from '@/app/ui/login' -export default function Login() { - const router = useRouter() +export default function Page() { return ( - router.back()}>Close modal -

Login

- ... +
) } ``` -> More information on modals is covered in the [Intercepting Routes](/docs/app/building-your-application/routing/intercepting-routes) section. +> **Good to know:** +> +> - The convention used to intercept the route, e.g. `(.)`, depends on your file-system structure. See [Intercepting Routes convention](/docs/app/building-your-application/routing/intercepting-routes#convention). +> - By separating the `` functionality from the modal content (``), you can ensure any content inside the modal, e.g. [forms](/docs/app/building-your-application/data-fetching/server-actions-and-mutations#forms), are Server Components. See [Interleaving Client and Server Components](/docs/app/building-your-application/rendering/composition-patterns#supported-pattern-passing-server-components-to-client-components-as-props) for more information. -If you want to navigate elsewhere and dismiss a modal, you can also use a catch-all route. +### Opening the modal -Parallel Routes Diagram +Now, you can leverage the Next.js router to open and close the modal. This ensures the URL is correctly updated when the modal is open, and when navigating backwards and forwards. -```tsx filename="app/@auth/[...catchAll]/page.tsx" switcher -export default function CatchAll() { - return null +To open the modal, pass the `@auth` slot as a prop to the parent layout and render it alongside the `children` prop. + +```tsx filename="app/layout.tsx" switcher +import Link from 'next/link' + +export default function Layout({ + auth, + children, +}: { + auth: React.ReactNode + children: React.ReactNode +}) { + return ( + <> + +
{auth}
+
{children}
+ + ) } ``` -```jsx filename="app/@auth/[...catchAll]/page.js" switcher -export default function CatchAll() { - return null +```jsx filename="app/layout.js" switcher +import Link from 'next/link' + +export default function Layout({ auth, children }) { + return ( + <> + +
{auth}
+
{children}
+ + ) } ``` -> Catch-all routes take precedence over `default.js`. +When the user clicks the ``, the modal will open instead of navigating to the `/login` page. However, on refresh or initial load, navigating to `/login` will take the user to the main login page. -### Conditional Routes +### Closing the modal -Parallel Routes also allows you to conditionally render a slot based on certain conditions, such as authentication state. For example, you can render a `/dashboard` or `/login` page depending on whether the user is logged in: +You can close the modal by calling `router.back()` or by using the `Link` component. -Conditional routes diagram +```tsx filename="app/ui/modal.tsx switcher +'use client' -Parallel Routes can be used to implement conditional routing. For example, you can render a `@dashboard` or `@login` route depending on the authentication state. +import { useRouter } from 'next/navigation' -```tsx filename="app/layout.tsx" switcher -import { getUser } from '@/lib/auth' +export function Modal({ children }: { children: React.ReactNode }) { + const router = useRouter() -export default function Layout({ - dashboard, - login, -}: { - dashboard: React.ReactNode - login: React.ReactNode -}) { - const isLoggedIn = getUser() - return isLoggedIn ? dashboard : login + return ( + <> + +
{children}
+ + ) } ``` -```jsx filename="app/layout.js" switcher -import { getUser } from '@/lib/auth' +```tsx filename="app/ui/modal.tsx switcher +'use client' -export default function Layout({ dashboard, login }) { - const isLoggedIn = getUser() - return isLoggedIn ? dashboard : login +import { useRouter } from 'next/navigation' + +export function Modal({ children }) { + const router = useRouter() + + return ( + <> + +
{children}
+ + ) } ``` -Parallel routes authentication example +> **Good to know:** +> +> - Other examples could include opening a photo modal in a gallery while also having a dedicated `/photo/[id]` page, or opening a shopping cart in a side modal. +> - [View an example](https://github.com/vercel-labs/nextgram) of modals with Intercepted and Parallel Routes. -### Streaming +### Loading and Error UI Parallel Routes can be streamed independently, allowing you to define independent error and loading states for each route: @@ -330,3 +420,5 @@ Parallel Routes can be streamed independently, allowing you to define independen width="1600" height="1218" /> + +See the [Loading UI](/docs/app/building-your-application/routing/loading-ui-and-streaming) and [Error Handling](/docs/app/building-your-application/routing/error-handling) documentation for more information. diff --git a/docs/02-app/01-building-your-application/01-routing/11-intercepting-routes.mdx b/docs/02-app/01-building-your-application/01-routing/11-intercepting-routes.mdx index 1895e0105f93f..180e970c828be 100644 --- a/docs/02-app/01-building-your-application/01-routing/11-intercepting-routes.mdx +++ b/docs/02-app/01-building-your-application/01-routing/11-intercepting-routes.mdx @@ -57,14 +57,14 @@ For example, you can intercept the `photo` segment from within the `feed` segmen ### Modals -Intercepting Routes can be used together with [Parallel Routes](/docs/app/building-your-application/routing/parallel-routes) to create modals. +Intercepting Routes can be used together with [Parallel Routes](/docs/app/building-your-application/routing/parallel-routes) to create modals. This allows you to solve common challenges when building modals, such as: -Using this pattern to create modals overcomes some common challenges when working with modals, by allowing you to: +- Making the modal content **shareable through a URL**. +- **Preserving context** when the page is refreshed, instead of closing the modal. +- **Closing the modal on backwards navigation** rather than going to the previous route. +- **Reopening the modal on forwards navigation**. -- Make the modal content **shareable through a URL** -- **Preserve context** when the page is refreshed, instead of closing the modal -- **Close the modal on backwards navigation** rather than going to the previous route -- **Reopen the modal on forwards navigation** +Consider the following UI pattern, where a user can open a photo modal from a gallery using client-side navigation, or navigate to the photo page directly from a shareable URL: Intercepting routes modal example -> In the above example, the path to the `photo` segment can use the `(..)` matcher since `@modal` is a _slot_ and not a _segment_. This means that the `photo` route is only one _segment_ level higher, despite being two _file-system_ levels higher. +In the above example, the path to the `photo` segment can use the `(..)` matcher since `@modal` is a _slot_ and not a _segment_. This means that the `photo` route is only one _segment_ level higher, despite being two _file-system_ levels higher. -Other examples could include opening a login modal in a top navbar while also having a dedicated `/login` page, or opening a shopping cart in a side modal. +See the [Parallel Routes](/docs/app/building-your-application/routing/parallel-routes#modals) documentation for a step-by-step example, or see our [image gallery example](https://github.com/vercel-labs/nextgram). -[View an example](https://github.com/vercel-labs/nextgram) of modals with Intercepted and Parallel Routes. +> **Good to know:** +> +> - Other examples could include opening a login modal in a top navbar while also having a dedicated `/login` page, or opening a shopping cart in a side modal. diff --git a/docs/02-app/01-building-your-application/06-optimizing/11-static-assets.mdx b/docs/02-app/01-building-your-application/06-optimizing/11-static-assets.mdx index d6c3a2231ed41..7682b44463628 100644 --- a/docs/02-app/01-building-your-application/06-optimizing/11-static-assets.mdx +++ b/docs/02-app/01-building-your-application/06-optimizing/11-static-assets.mdx @@ -8,22 +8,26 @@ description: Next.js allows you to serve static files, like images, in the publi Next.js can serve static files, like images, under a folder called `public` in the root directory. Files inside `public` can then be referenced by your code starting from the base URL (`/`). -For example, if you add `me.png` inside `public`, the following code will access the image: +For example, the file `public/avatars/me.png` can be viewed by visiting the `/avatars/me.png` path. The code to display that image might look like: -```jsx filename="Avatar.js" +```jsx filename="avatar.js" import Image from 'next/image' -export function Avatar() { - return me +export function Avatar({ id, alt }) { + return {alt} +} + +export function AvatarOfMe() { + return } ``` ## Caching -Next.js automatically adds caching headers to immutable assets in the `public` folder. The default caching headers applied are: +Next.js cannot safely cache assets in the `public` folder because they may change. The default caching headers applied are: ```jsx -Cache-Control: public, max-age=31536000, immutable +Cache-Control: public, max-age=0 ``` ## Robots, Favicons, and others diff --git a/docs/02-app/02-api-reference/04-functions/revalidatePath.mdx b/docs/02-app/02-api-reference/04-functions/revalidatePath.mdx index 2433ef4f3253d..534e64383adf0 100644 --- a/docs/02-app/02-api-reference/04-functions/revalidatePath.mdx +++ b/docs/02-app/02-api-reference/04-functions/revalidatePath.mdx @@ -9,6 +9,8 @@ description: API Reference for the revalidatePath function. > > - `revalidatePath` is available in both [Node.js and Edge runtimes](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes). > - `revalidatePath` only invalidates the cache when the included path is next visited. This means calling `revalidatePath` with a dynamic route segment will not immediately trigger many revalidations at once. The invalidation only happens when the path is next visited. +> - Currently, `revalidatePath` invalidates all the routes in the [client-side Router Cache](/docs/app/building-your-application/caching#router-cache). This behavior is temporary and will be updated in the future to apply only to the specific path. +> - Using `revalidatePath` invalidates **only the specific path** in the [server-side Route Cache](/docs/app/building-your-application/caching#full-route-cache). ## Parameters diff --git a/lerna.json b/lerna.json index 43120e0a6a76a..2812fe090b3b9 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "14.1.1-canary.13" + "version": "14.1.1-canary.14" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 16a0746eba498..75e4d510f770c 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 13ae4dcfe9e15..d8b104bcfd166 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config", "dependencies": { - "@next/eslint-plugin-next": "14.1.1-canary.13", + "@next/eslint-plugin-next": "14.1.1-canary.14", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 4e63505cba9a9..eaf38dc2b86e6 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index 9da5cfa0a7686..5a49edc69f470 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 21d275036fb34..a13feb3045814 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index c1eef3162790d..8725f6fc67d43 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 8eca0bdb346a5..81b71b572138e 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 930b7df9fea6c..8aa6bbe498d23 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index bf722a10864fd..e43ff4cc621d6 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index ff695af7af61f..76f9cd5254535 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 3e71aed4fe5c9..6de8cf2d069c8 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index f962ab71eea92..fb66232de83fc 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 77bee39d556ab..20ccd3a7b800a 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -92,7 +92,7 @@ ] }, "dependencies": { - "@next/env": "14.1.1-canary.13", + "@next/env": "14.1.1-canary.14", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -146,11 +146,11 @@ "@mswjs/interceptors": "0.23.0", "@napi-rs/cli": "2.16.2", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "14.1.1-canary.13", - "@next/polyfill-nomodule": "14.1.1-canary.13", - "@next/react-dev-overlay": "14.1.1-canary.13", - "@next/react-refresh-utils": "14.1.1-canary.13", - "@next/swc": "14.1.1-canary.13", + "@next/polyfill-module": "14.1.1-canary.14", + "@next/polyfill-nomodule": "14.1.1-canary.14", + "@next/react-dev-overlay": "14.1.1-canary.14", + "@next/react-refresh-utils": "14.1.1-canary.14", + "@next/swc": "14.1.1-canary.14", "@opentelemetry/api": "1.6.0", "@playwright/test": "^1.35.1", "@taskr/clear": "1.1.0", diff --git a/packages/next/src/build/webpack/loaders/next-app-loader.ts b/packages/next/src/build/webpack/loaders/next-app-loader.ts index 5fdae7e86751f..d64fc97ad91ed 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader.ts @@ -272,6 +272,8 @@ async function createTreeCodeFromPath( } for (const [parallelKey, parallelSegment] of parallelSegments) { + // if parallelSegment is the page segment (ie, `page$` and not ['page$']), it gets loaded into the __PAGE__ slot + // as it's the page for the current route. if (parallelSegment === PAGE_SEGMENT) { const matchedPagePath = `${appDirPrefix}${segmentPath}${ parallelKey === 'children' ? '' : `/${parallelKey}` @@ -293,27 +295,37 @@ async function createTreeCodeFromPath( continue } + // if the parallelSegment was not matched to the __PAGE__ slot, then it's a parallel route at this level. + // the code below recursively traverses the parallel slots directory to match the corresponding __PAGE__ for each parallel slot + // while also filling in layout/default/etc files into the loader tree at each segment level. + const subSegmentPath = [...segments] if (parallelKey !== 'children') { + // A `children` parallel key should have already been processed in the above segment + // So we exclude it when constructing the subsegment path for the remaining segment levels subSegmentPath.push(parallelKey) } - const normalizedParallelSegments = Array.isArray(parallelSegment) - ? parallelSegment.slice(0, 1) - : [parallelSegment] + const normalizedParallelSegment = Array.isArray(parallelSegment) + ? parallelSegment[0] + : parallelSegment - subSegmentPath.push( - ...normalizedParallelSegments.filter( - (segment) => - segment !== PAGE_SEGMENT && segment !== PARALLEL_CHILDREN_SEGMENT - ) - ) + if ( + normalizedParallelSegment !== PAGE_SEGMENT && + normalizedParallelSegment !== PARALLEL_CHILDREN_SEGMENT + ) { + // If we don't have a page segment, nor a special $children marker, it means we need to traverse the next directory + // (ie, `normalizedParallelSegment` would correspond with the folder that contains the next level of pages/layout/etc) + // we push it to the subSegmentPath so that we can fill in the loader tree for that segment. + subSegmentPath.push(normalizedParallelSegment) + } const { treeCode: pageSubtreeCode } = await createSubtreePropsFromSegmentPath(subSegmentPath) const parallelSegmentPath = subSegmentPath.join('/') + // Fill in the loader tree for all of the special files types (layout, default, etc) at this level // `page` is not included here as it's added above. const filePaths = await Promise.all( Object.values(FILE_TYPES).map(async (file) => { @@ -534,14 +546,15 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { const isParallelRoute = rest[0].startsWith('@') if (isParallelRoute) { if (rest.length === 2 && rest[1] === 'page') { - // matched will be an empty object in case the parallel route is at a path with no existing page - // in which case, we need to mark it as a regular page segment - matched[rest[0]] = Object.keys(matched).length - ? [PAGE_SEGMENT] - : PAGE_SEGMENT + // We found a parallel route at this level. We don't want to mark it explicitly as the page segment, + // as that should be matched to the `children` slot. Instead, we use an array, to signal to `createSubtreePropsFromSegmentPath` + // that it needs to recursively fill in the loader tree code for the parallel route at the appropriate levels. + matched[rest[0]] = [PAGE_SEGMENT] continue } - // we insert a special marker in order to also process layout/etc files at the slot level + // If it was a parallel route but we weren't able to find the page segment (ie, maybe the page is nested further) + // we first insert a special marker to ensure that we still process layout/default/etc at the slot level prior to continuing + // on to the page segment. matched[rest[0]] = [PARALLEL_CHILDREN_SEGMENT, ...rest.slice(1)] continue } @@ -573,6 +586,7 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { matched.children = rest[0] } } + return Object.entries(matched) } diff --git a/packages/next/src/server/image-optimizer.ts b/packages/next/src/server/image-optimizer.ts index 6e69481e36930..7ad6f90be46e0 100644 --- a/packages/next/src/server/image-optimizer.ts +++ b/packages/next/src/server/image-optimizer.ts @@ -72,6 +72,12 @@ export interface ImageParamsResult { minimumCacheTTL: number } +interface ImageUpstream { + buffer: Buffer + contentType: string | null | undefined + cacheControl: string | null | undefined +} + function getSupportedMimeType(options: string[], accept = ''): string { const mimeType = mediaType(accept, options) return accept.includes(mimeType) ? mimeType : '' @@ -373,7 +379,9 @@ export class ImageError extends Error { } } -function parseCacheControl(str: string | null): Map { +function parseCacheControl( + str: string | null | undefined +): Map { const map = new Map() if (!str) { return map @@ -389,7 +397,7 @@ function parseCacheControl(str: string | null): Map { return map } -export function getMaxAge(str: string | null): number { +export function getMaxAge(str: string | null | undefined): number { const map = parseCacheControl(str) if (map) { let age = map.get('s-maxage') || map.get('max-age') || '' @@ -516,77 +524,89 @@ export async function optimizeImage({ return optimizedBuffer } -export async function imageOptimizer( +export async function fetchExternalImage(href: string): Promise { + const res = await fetch(href) + + if (!res.ok) { + Log.error('upstream image response failed for', href, res.status) + throw new ImageError( + res.status, + '"url" parameter is valid but upstream response is invalid' + ) + } + + const buffer = Buffer.from(await res.arrayBuffer()) + const contentType = res.headers.get('Content-Type') + const cacheControl = res.headers.get('Cache-Control') + + return { buffer, contentType, cacheControl } +} + +export async function fetchInternalImage( + href: string, _req: IncomingMessage, _res: ServerResponse, - paramsResult: ImageParamsResult, - nextConfig: NextConfigComplete, - isDev: boolean | undefined, handleRequest: ( newReq: IncomingMessage, newRes: ServerResponse, newParsedUrl?: NextUrlWithParsedQuery ) => Promise -): Promise<{ buffer: Buffer; contentType: string; maxAge: number }> { - let upstreamBuffer: Buffer - let upstreamType: string | null | undefined - let maxAge: number - const { isAbsolute, href, width, mimeType, quality } = paramsResult +): Promise { + try { + const mocked = createRequestResponseMocks({ + url: href, + method: _req.method || 'GET', + headers: _req.headers, + socket: _req.socket, + }) - if (isAbsolute) { - const upstreamRes = await fetch(href) + await handleRequest(mocked.req, mocked.res, nodeUrl.parse(href, true)) + await mocked.res.hasStreamed - if (!upstreamRes.ok) { - Log.error('upstream image response failed for', href, upstreamRes.status) + if (!mocked.res.statusCode) { + Log.error('image response failed for', href, mocked.res.statusCode) throw new ImageError( - upstreamRes.status, - '"url" parameter is valid but upstream response is invalid' + mocked.res.statusCode, + '"url" parameter is valid but internal response is invalid' ) } - upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer()) - upstreamType = - detectContentType(upstreamBuffer) || - upstreamRes.headers.get('Content-Type') - maxAge = getMaxAge(upstreamRes.headers.get('Cache-Control')) - } else { - try { - const mocked = createRequestResponseMocks({ - url: href, - method: _req.method || 'GET', - headers: _req.headers, - socket: _req.socket, - }) - - await handleRequest(mocked.req, mocked.res, nodeUrl.parse(href, true)) - await mocked.res.hasStreamed - - if (!mocked.res.statusCode) { - Log.error('image response failed for', href, mocked.res.statusCode) - throw new ImageError( - mocked.res.statusCode, - '"url" parameter is valid but internal response is invalid' - ) - } - - upstreamBuffer = Buffer.concat(mocked.res.buffers) - upstreamType = - detectContentType(upstreamBuffer) || - mocked.res.getHeader('Content-Type') - const cacheControl = mocked.res.getHeader('Cache-Control') - maxAge = cacheControl ? getMaxAge(cacheControl) : 0 - } catch (err) { - Log.error('upstream image response failed for', href, err) - throw new ImageError( - 500, - '"url" parameter is valid but upstream response is invalid' - ) - } + const buffer = Buffer.concat(mocked.res.buffers) + const contentType = mocked.res.getHeader('Content-Type') + const cacheControl = mocked.res.getHeader('Cache-Control') + return { buffer, contentType, cacheControl } + } catch (err) { + Log.error('upstream image response failed for', href, err) + throw new ImageError( + 500, + '"url" parameter is valid but upstream response is invalid' + ) } +} - if (upstreamType) { - upstreamType = upstreamType.toLowerCase().trim() +export async function imageOptimizer( + imageUpstream: ImageUpstream, + paramsResult: Pick< + ImageParamsResult, + 'href' | 'width' | 'quality' | 'mimeType' + >, + nextConfig: { + output: NextConfigComplete['output'] + images: Pick< + NextConfigComplete['images'], + 'dangerouslyAllowSVG' | 'minimumCacheTTL' + > + }, + isDev: boolean | undefined +): Promise<{ buffer: Buffer; contentType: string; maxAge: number }> { + const { href, quality, width, mimeType } = paramsResult + const upstreamBuffer = imageUpstream.buffer + const maxAge = getMaxAge(imageUpstream.cacheControl) + const upstreamType = + detectContentType(upstreamBuffer) || + imageUpstream.contentType?.toLowerCase().trim() + if (upstreamType) { if ( upstreamType.startsWith('image/svg') && !nextConfig.images.dangerouslyAllowSVG diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index b7da975e85a56..799f3701333c0 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -544,53 +544,65 @@ export default class NextNodeServer extends BaseServer { 'invariant: imageOptimizer should not be called in minimal mode' ) } else { - const { imageOptimizer } = + const { imageOptimizer, fetchExternalImage, fetchInternalImage } = require('./image-optimizer') as typeof import('./image-optimizer') - return imageOptimizer( - req.originalRequest, - res.originalResponse, - paramsResult, - this.nextConfig, - this.renderOpts.dev, - async (newReq, newRes) => { - if (newReq.url === req.url) { - throw new Error( - `Invariant attempted to optimize _next/image itself` - ) - } + const handleInternalReq = async ( + newReq: IncomingMessage, + newRes: ServerResponse + ) => { + if (newReq.url === req.url) { + throw new Error(`Invariant attempted to optimize _next/image itself`) + } - const protocol = this.serverOptions.experimentalHttpsServer - ? 'https' - : 'http' - - const invokeRes = await invokeRequest( - `${protocol}://${this.fetchHostname || 'localhost'}:${this.port}${ - newReq.url || '' - }`, - { - method: newReq.method || 'GET', - headers: newReq.headers, - signal: signalFromNodeResponse(res.originalResponse), - } - ) - const filteredResHeaders = filterReqHeaders( - toNodeOutgoingHttpHeaders(invokeRes.headers), - ipcForbiddenHeaders - ) + const protocol = this.serverOptions.experimentalHttpsServer + ? 'https' + : 'http' - for (const key of Object.keys(filteredResHeaders)) { - newRes.setHeader(key, filteredResHeaders[key] || '') + const invokeRes = await invokeRequest( + `${protocol}://${this.fetchHostname || 'localhost'}:${this.port}${ + newReq.url || '' + }`, + { + method: newReq.method || 'GET', + headers: newReq.headers, + signal: signalFromNodeResponse(res.originalResponse), } - newRes.statusCode = invokeRes.status || 200 + ) + const filteredResHeaders = filterReqHeaders( + toNodeOutgoingHttpHeaders(invokeRes.headers), + ipcForbiddenHeaders + ) - if (invokeRes.body) { - await pipeToNodeResponse(invokeRes.body, newRes) - } else { - res.send() - } - return + for (const key of Object.keys(filteredResHeaders)) { + newRes.setHeader(key, filteredResHeaders[key] || '') } + newRes.statusCode = invokeRes.status || 200 + + if (invokeRes.body) { + await pipeToNodeResponse(invokeRes.body, newRes) + } else { + res.send() + } + return + } + + const { isAbsolute, href } = paramsResult + + const imageUpstream = isAbsolute + ? await fetchExternalImage(href) + : await fetchInternalImage( + href, + req.originalRequest, + res.originalResponse, + handleInternalReq + ) + + return imageOptimizer( + imageUpstream, + paramsResult, + this.nextConfig, + this.renderOpts.dev ) } } diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index da12ec39d3a57..931630cb91ba0 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": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 811ff2d0482fa..31f1198747527 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": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 8027e36695c6f..62aee3976111f 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "14.1.1-canary.13", + "version": "14.1.1-canary.14", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "14.1.1-canary.13", + "next": "14.1.1-canary.14", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3db682ecd27e6..e92dc01f4c9df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -747,7 +747,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 14.1.1-canary.13 + specifier: 14.1.1-canary.14 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -809,7 +809,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 14.1.1-canary.13 + specifier: 14.1.1-canary.14 version: link:../next-env '@swc/helpers': specifier: 0.5.2 @@ -933,19 +933,19 @@ importers: specifier: 1.1.0 version: 1.1.0 '@next/polyfill-module': - specifier: 14.1.1-canary.13 + specifier: 14.1.1-canary.14 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 14.1.1-canary.13 + specifier: 14.1.1-canary.14 version: link:../next-polyfill-nomodule '@next/react-dev-overlay': - specifier: 14.1.1-canary.13 + specifier: 14.1.1-canary.14 version: link:../react-dev-overlay '@next/react-refresh-utils': - specifier: 14.1.1-canary.13 + specifier: 14.1.1-canary.14 version: link:../react-refresh-utils '@next/swc': - specifier: 14.1.1-canary.13 + specifier: 14.1.1-canary.14 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1569,7 +1569,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 14.1.1-canary.13 + specifier: 14.1.1-canary.14 version: link:../next outdent: specifier: 0.8.0 diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/layout.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/layout.tsx new file mode 100644 index 0000000000000..621aea648e1a0 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/layout.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link' +import React from 'react' + +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + +
+ to nested +
+
+ to nested subroute +
+

Root Layout

+
{children}
+ + + ) +} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/default.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/default.tsx new file mode 100644 index 0000000000000..ceaa4105ad9e0 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return '@bar default' +} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/layout.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/layout.tsx new file mode 100644 index 0000000000000..2e09ca74cc17d --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/layout.tsx @@ -0,0 +1,8 @@ +export default function Layout({ children }) { + return ( +
+

@bar Layout

+
{children}
+
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/page.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/page.tsx new file mode 100644 index 0000000000000..86d9ad310e688 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
Bar Slot
+} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/subroute/page.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/subroute/page.tsx new file mode 100644 index 0000000000000..83be0d5a350c7 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@bar/subroute/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
Subroute
+} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/default.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/default.tsx new file mode 100644 index 0000000000000..9b071d1c39e10 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return '@foo default' +} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/layout.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/layout.tsx new file mode 100644 index 0000000000000..ab0a56dffff47 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/layout.tsx @@ -0,0 +1,8 @@ +export default function Layout({ children }) { + return ( +
+

@foo Layout

+
{children}
+
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/page.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/page.tsx new file mode 100644 index 0000000000000..0b03547a745eb --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/@foo/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
Foo Slot
+} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/default.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/default.tsx new file mode 100644 index 0000000000000..0d8777ed565be --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/default.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'default page' +} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/layout.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/layout.tsx new file mode 100644 index 0000000000000..467f9ca246a53 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/layout.tsx @@ -0,0 +1,10 @@ +export default function Layout({ children, bar, foo }) { + return ( +
+

Nested Layout

+
{children}
+
{foo}
+
{bar}
+
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/nested/page.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/nested/page.tsx new file mode 100644 index 0000000000000..10ff51e2d6aea --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/nested/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
Hello from Nested
+} diff --git a/test/e2e/app-dir/parallel-routes-layouts/app/page.tsx b/test/e2e/app-dir/parallel-routes-layouts/app/page.tsx new file mode 100644 index 0000000000000..c9f9a60006193 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/app/page.tsx @@ -0,0 +1,3 @@ +export default async function Home() { + return
Hello World
+} diff --git a/test/e2e/app-dir/parallel-routes-layouts/next.config.js b/test/e2e/app-dir/parallel-routes-layouts/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/parallel-routes-layouts/parallel-routes-layouts.test.ts b/test/e2e/app-dir/parallel-routes-layouts/parallel-routes-layouts.test.ts new file mode 100644 index 0000000000000..431c5aaf9ce6f --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-layouts/parallel-routes-layouts.test.ts @@ -0,0 +1,78 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('parallel-routes-layouts', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should properly render layouts for multiple slots', async () => { + const browser = await next.browser('/nested') + + let layouts = await getLayoutHeadings(browser) + expect(layouts).toHaveLength(4) + expect(layouts).toEqual( + expect.arrayContaining([ + 'Root Layout', + 'Nested Layout', + '@foo Layout', + '@bar Layout', + ]) + ) + + // ensure nested/page is showing its contents + expect(await browser.elementById('nested-children').text()).toBe( + 'Hello from Nested' + ) + + // Ensure each slot is showing its contents + expect(await browser.elementById('foo-children').text()).toBe('Foo Slot') + expect(await browser.elementById('bar-children').text()).toBe('Bar Slot') + + // Navigate to a subroute that only has a match for the @foo slot + await browser.elementByCss('[href="/nested/subroute"]').click() + await retry(async () => { + // the bar slot has a match for the subroute, so we expect it to be rendered + expect(await browser.elementById('bar-children').text()).toBe('Subroute') + + // We still expect the previous active slots to be visible until reload even if they don't match + layouts = await getLayoutHeadings(browser) + expect(layouts).toEqual( + expect.arrayContaining([ + 'Root Layout', + 'Nested Layout', + '@foo Layout', + '@bar Layout', + ]) + ) + + expect(await browser.elementById('foo-children').text()).toBe('Foo Slot') + + expect(await browser.elementById('nested-children').text()).toBe( + 'Hello from Nested' + ) + }) + + // Trigger a reload, which will clear the previous active slots and show the ones that explicitly have matched + await browser.refresh() + + layouts = await getLayoutHeadings(browser) + + // the foo slot does not match on the subroute, so we don't expect the layout or page to be rendered + expect(layouts).toHaveLength(3) + expect(layouts).toEqual( + expect.arrayContaining(['Root Layout', 'Nested Layout', '@bar Layout']) + ) + + // we should now see defaults being rendered for both the page & foo slots + expect(await browser.elementById('nested-children').text()).toBe( + 'default page' + ) + expect(await browser.elementById('foo-slot').text()).toBe('@foo default') + }) +}) + +async function getLayoutHeadings(browser): Promise { + const elements = await browser.elementsByCss('h1') + return Promise.all(elements.map(async (el) => await el.innerText())) +} diff --git a/test/integration/next-image-new/asset-prefix/test/index.test.js b/test/integration/next-image-new/asset-prefix/test/index.test.js index 578e2227e2a07..8906ce83cc400 100644 --- a/test/integration/next-image-new/asset-prefix/test/index.test.js +++ b/test/integration/next-image-new/asset-prefix/test/index.test.js @@ -38,9 +38,13 @@ describe('Image Component assetPrefix Tests', () => { const bgImage = await browser.eval( `document.getElementById('${id}').style['background-image']` ) - expect(bgImage).toMatch( - /\/_next\/image\?url=https%3A%2F%2Fexample\.vercel\.sh%2Fpre%2F_next%2Fstatic%2Fmedia%2Ftest(.+).jpg&w=8&q=70/ - ) + if (process.env.TURBOPACK) { + expect(bgImage).toContain('data:image/svg+xml;') + } else { + expect(bgImage).toMatch( + /\/_next\/image\?url=https%3A%2F%2Fexample\.vercel\.sh%2Fpre%2F_next%2Fstatic%2Fmedia%2Ftest(.+).jpg&w=8&q=70/ + ) + } }) it('should not log a deprecation warning about using `images.domains`', async () => {