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.
-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:
-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`.
+
+
+
+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}
+
+ Page Views
+ Visitors
+
+ {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}
+
+ Page Views
+ Visitors
+
+ {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:
+
+
+
+To implement this pattern, start by creating a `/login` route that renders your **main** login page.
+
+
+
+```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
-
+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 (
+ <>
+
+ Open modal
+
+ {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 (
+ <>
+
+ Open modal
+
+ {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.
-
+```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 (
+ <>
+ {
+ router.back()
+ }}
+ >
+ Close modal
+
+ {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 (
+ <>
+ {
+ router.back()
+ }}
+ >
+ Close modal
+
+ {children}
+ >
+ )
}
```
-
+> **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:
-> 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
+export function Avatar({ id, alt }) {
+ return
+}
+
+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 () => {