diff --git a/examples/with-styled-components-rtl/.gitignore b/examples/with-styled-components-rtl/.gitignore
deleted file mode 100644
index 8777267507c0e..0000000000000
--- a/examples/with-styled-components-rtl/.gitignore
+++ /dev/null
@@ -1,40 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.*
-.yarn/*
-!.yarn/patches
-!.yarn/plugins
-!.yarn/releases
-!.yarn/versions
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# local env files
-.env*.local
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
diff --git a/examples/with-styled-components-rtl/README.md b/examples/with-styled-components-rtl/README.md
deleted file mode 100644
index a5370e29109fb..0000000000000
--- a/examples/with-styled-components-rtl/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Example with styled-components RTL
-
-This example shows how to use nextjs with right to left (RTL) styles using styled-components.
-
-## Deploy your own
-
-Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-styled-components-rtl)
-
-[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-styled-components-rtl&project-name=with-styled-components-rtl&repository-name=with-styled-components-rtl)
-
-## How to use
-
-Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
-
-```bash
-npx create-next-app --example with-styled-components-rtl with-styled-components-rtl-app
-```
-
-```bash
-yarn create next-app --example with-styled-components-rtl with-styled-components-rtl-app
-```
-
-```bash
-pnpm create next-app --example with-styled-components-rtl with-styled-components-rtl-app
-```
-
-Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
-
-## Notes
-
-Right to left allows to "flip" every element in your site to fit the needs of the cultures that are read from right to left (like arabic for example).
-
-This example shows how to enable right to left styles using `styled-components`.
-
-The good news, is there is no need of doing it manually anymore. `stylis-plugin-rtl` makes the transformation automatic.
-
-From `pages/index.js` you can see, styles are `text-align: left;`, but what is actually applied is `text-align: right;`.
diff --git a/examples/with-styled-components-rtl/next.config.js b/examples/with-styled-components-rtl/next.config.js
deleted file mode 100644
index 7b09dd40f78df..0000000000000
--- a/examples/with-styled-components-rtl/next.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/** @type {import('next').NextConfig} */
-const nextConfig = {
- compiler: {
- styledComponents: true,
- },
-};
-
-module.exports = nextConfig;
diff --git a/examples/with-styled-components-rtl/package.json b/examples/with-styled-components-rtl/package.json
deleted file mode 100644
index 95e5fbf7a4ef6..0000000000000
--- a/examples/with-styled-components-rtl/package.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start"
- },
- "dependencies": {
- "next": "latest",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "styled-components": "^6.1.0",
- "stylis-plugin-rtl": "^2.1.1"
- },
- "devDependencies": {
- "@types/node": "^18.18.3",
- "@types/react": "^18.2.33",
- "@types/react-dom": "^18.2.14",
- "typescript": "^5.2.2"
- }
-}
diff --git a/examples/with-styled-components-rtl/pages/_app.tsx b/examples/with-styled-components-rtl/pages/_app.tsx
deleted file mode 100644
index cc7491ae2c058..0000000000000
--- a/examples/with-styled-components-rtl/pages/_app.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { AppProps } from "next/app";
-import type { DefaultTheme } from "styled-components";
-import { ThemeProvider } from "styled-components";
-
-const theme: DefaultTheme = {
- colors: {
- primary: "#0070f3",
- },
-};
-
-export default function App({ Component, pageProps }: AppProps) {
- return (
-
-
-
- );
-}
diff --git a/examples/with-styled-components-rtl/pages/_document.tsx b/examples/with-styled-components-rtl/pages/_document.tsx
deleted file mode 100644
index 57c7e22fc739c..0000000000000
--- a/examples/with-styled-components-rtl/pages/_document.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { DocumentContext, DocumentInitialProps } from "next/document";
-import Document from "next/document";
-import { ServerStyleSheet, StyleSheetManager } from "styled-components";
-import stylisRTLPlugin from "stylis-plugin-rtl";
-
-export default class MyDocument extends Document {
- static async getInitialProps(
- ctx: DocumentContext,
- ): Promise {
- const sheet = new ServerStyleSheet();
- const originalRenderPage = ctx.renderPage;
-
- try {
- ctx.renderPage = () =>
- originalRenderPage({
- enhanceApp: (App) => (props) =>
- sheet.collectStyles(
-
-
- ,
- ),
- });
-
- const initialProps = await Document.getInitialProps(ctx);
- return {
- ...initialProps,
- styles: [initialProps.styles, sheet.getStyleElement()],
- };
- } finally {
- sheet.seal();
- }
- }
-}
diff --git a/examples/with-styled-components-rtl/pages/index.tsx b/examples/with-styled-components-rtl/pages/index.tsx
deleted file mode 100644
index 15de3917c9436..0000000000000
--- a/examples/with-styled-components-rtl/pages/index.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import styled from "styled-components";
-
-const Title = styled.h1`
- font-size: 50px;
- color: ${({ theme }) => theme.colors.primary};
- text-align: left;
-`;
-
-export default function Home() {
- return My page;
-}
diff --git a/examples/with-styled-components-rtl/styled.d.ts b/examples/with-styled-components-rtl/styled.d.ts
deleted file mode 100644
index 8ae51809abd61..0000000000000
--- a/examples/with-styled-components-rtl/styled.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import "styled-components";
-
-declare module "styled-components" {
- export interface DefaultTheme {
- colors: {
- primary: string;
- };
- }
-}
diff --git a/examples/with-styled-components-rtl/tsconfig.json b/examples/with-styled-components-rtl/tsconfig.json
deleted file mode 100644
index f25ef74523264..0000000000000
--- a/examples/with-styled-components-rtl/tsconfig.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "compilerOptions": {
- "target": "es5",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "paths": {
- "@/*": ["./*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "styled.d.ts"],
- "exclude": ["node_modules"]
-}
diff --git a/lerna.json b/lerna.json
index d17c196f94c8b..0b907dfc480d1 100644
--- a/lerna.json
+++ b/lerna.json
@@ -16,5 +16,5 @@
"registry": "https://registry.npmjs.org/"
}
},
- "version": "15.1.1-canary.27"
+ "version": "15.2.0-canary.1"
}
diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json
index 905cabef5d401..cd46c15f08590 100644
--- a/packages/create-next-app/package.json
+++ b/packages/create-next-app/package.json
@@ -1,6 +1,6 @@
{
"name": "create-next-app",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"keywords": [
"react",
"next",
diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json
index 8d6340a125203..306ffb4ee530c 100644
--- a/packages/eslint-config-next/package.json
+++ b/packages/eslint-config-next/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-config-next",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"description": "ESLint configuration used by Next.js.",
"main": "index.js",
"license": "MIT",
@@ -10,7 +10,7 @@
},
"homepage": "https://nextjs.org/docs/app/api-reference/config/eslint",
"dependencies": {
- "@next/eslint-plugin-next": "15.1.1-canary.27",
+ "@next/eslint-plugin-next": "15.2.0-canary.1",
"@rushstack/eslint-patch": "^1.10.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json
index b43321abcd514..9316c1595d66a 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": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"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 7a6bd361e3453..23e1e2bbfe0b8 100644
--- a/packages/font/package.json
+++ b/packages/font/package.json
@@ -1,7 +1,7 @@
{
"name": "@next/font",
"private": true,
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"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 4c5838a58ff20..5044c64b6f448 100644
--- a/packages/next-bundle-analyzer/package.json
+++ b/packages/next-bundle-analyzer/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/bundle-analyzer",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"main": "index.js",
"types": "index.d.ts",
"license": "MIT",
diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json
index d1eeb7650b627..3e6bbca588ecd 100644
--- a/packages/next-codemod/package.json
+++ b/packages/next-codemod/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/codemod",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/packages/next-env/package.json b/packages/next-env/package.json
index cad996d72af98..641f093e479ef 100644
--- a/packages/next-env/package.json
+++ b/packages/next-env/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/env",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"keywords": [
"react",
"next",
diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json
index bf007bd6e4c7b..5198bc159791f 100644
--- a/packages/next-mdx/package.json
+++ b/packages/next-mdx/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/mdx",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"main": "index.js",
"license": "MIT",
"repository": {
diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json
index e28e1f44aebd7..092689203d391 100644
--- a/packages/next-plugin-storybook/package.json
+++ b/packages/next-plugin-storybook/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/plugin-storybook",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"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 ef30205efb410..4ad64af682358 100644
--- a/packages/next-polyfill-module/package.json
+++ b/packages/next-polyfill-module/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/polyfill-module",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"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 ffaef76cc50da..478f6296dad2f 100644
--- a/packages/next-polyfill-nomodule/package.json
+++ b/packages/next-polyfill-nomodule/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/polyfill-nomodule",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"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 0d66e47693a25..6ce55b3ae3c12 100644
--- a/packages/next-swc/package.json
+++ b/packages/next-swc/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/swc",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"private": true,
"scripts": {
"clean": "node ../../scripts/rm.mjs native",
diff --git a/packages/next/package.json b/packages/next/package.json
index a2a15d0cba9da..ad6500e957788 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -1,6 +1,6 @@
{
"name": "next",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
@@ -99,7 +99,7 @@
]
},
"dependencies": {
- "@next/env": "15.1.1-canary.27",
+ "@next/env": "15.2.0-canary.1",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
@@ -164,11 +164,11 @@
"@jest/types": "29.5.0",
"@mswjs/interceptors": "0.23.0",
"@napi-rs/triples": "1.2.0",
- "@next/font": "15.1.1-canary.27",
- "@next/polyfill-module": "15.1.1-canary.27",
- "@next/polyfill-nomodule": "15.1.1-canary.27",
- "@next/react-refresh-utils": "15.1.1-canary.27",
- "@next/swc": "15.1.1-canary.27",
+ "@next/font": "15.2.0-canary.1",
+ "@next/polyfill-module": "15.2.0-canary.1",
+ "@next/polyfill-nomodule": "15.2.0-canary.1",
+ "@next/react-refresh-utils": "15.2.0-canary.1",
+ "@next/swc": "15.2.0-canary.1",
"@opentelemetry/api": "1.6.0",
"@playwright/test": "1.41.2",
"@storybook/addon-essentials": "^8.4.7",
diff --git a/packages/next/src/client/dev/dev-build-indicator/initialize-for-page-router.ts b/packages/next/src/client/dev/dev-build-indicator/initialize-for-page-router.ts
new file mode 100644
index 0000000000000..3a20e262d782e
--- /dev/null
+++ b/packages/next/src/client/dev/dev-build-indicator/initialize-for-page-router.ts
@@ -0,0 +1,33 @@
+import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../../server/dev/hot-reloader-types'
+import { addMessageListener } from '../../components/react-dev-overlay/pages/websocket'
+import { devBuildIndicator } from './internal/dev-build-indicator'
+
+/** Integrates the generic dev build indicator with the Pages Router. */
+export const initializeDevBuildIndicatorForPageRouter = () => {
+ if (!process.env.__NEXT_BUILD_INDICATOR) {
+ return
+ }
+
+ devBuildIndicator.initialize(process.env.__NEXT_BUILD_INDICATOR_POSITION)
+
+ // Add message listener specifically for Pages Router to handle lifecycle events
+ // related to dev builds (building, built, sync)
+ addMessageListener((obj) => {
+ try {
+ if (!('action' in obj)) {
+ return
+ }
+
+ // eslint-disable-next-line default-case
+ switch (obj.action) {
+ case HMR_ACTIONS_SENT_TO_BROWSER.BUILDING:
+ devBuildIndicator.show()
+ break
+ case HMR_ACTIONS_SENT_TO_BROWSER.BUILT:
+ case HMR_ACTIONS_SENT_TO_BROWSER.SYNC:
+ devBuildIndicator.hide()
+ break
+ }
+ } catch {}
+ })
+}
diff --git a/packages/next/src/client/dev/dev-build-watcher.ts b/packages/next/src/client/dev/dev-build-indicator/internal/dev-build-indicator.ts
similarity index 79%
rename from packages/next/src/client/dev/dev-build-watcher.ts
rename to packages/next/src/client/dev/dev-build-indicator/internal/dev-build-indicator.ts
index 78764ef37b0b4..f8d69a6442792 100644
--- a/packages/next/src/client/dev/dev-build-watcher.ts
+++ b/packages/next/src/client/dev/dev-build-indicator/internal/dev-build-indicator.ts
@@ -1,26 +1,24 @@
-/* eslint-disable @typescript-eslint/no-use-before-define */
-import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../server/dev/hot-reloader-types'
-import type { HMR_ACTION_TYPES } from '../../server/dev/hot-reloader-types'
-import { addMessageListener } from '../components/react-dev-overlay/pages/websocket'
-
type VerticalPosition = 'top' | 'bottom'
type HorizonalPosition = 'left' | 'right'
-export interface ShowHideHandler {
- show: () => void
- hide: () => void
+const NOOP = () => {}
+
+export const devBuildIndicator = {
+ /** Shows build indicator when Next.js is compiling. Requires initialize() first. */
+ show: NOOP,
+ /** Hides build indicator when Next.js finishes compiling. Requires initialize() first. */
+ hide: NOOP,
+ /** Sets up the build indicator UI component. Call this before using show/hide. */
+ initialize,
}
-export default function initializeBuildWatcher(
- toggleCallback: (handlers: ShowHideHandler) => void,
- position = 'bottom-right'
-) {
+function initialize(position = 'bottom-right') {
const shadowHost = document.createElement('div')
const [verticalProperty, horizontalProperty] = position.split('-', 2) as [
VerticalPosition,
HorizonalPosition,
]
- shadowHost.id = '__next-build-watcher'
+ shadowHost.id = '__next-build-indicator'
// Make sure container is fixed and on a high zIndex so it shows
shadowHost.style.position = 'fixed'
// Ensure container's position to be top or bottom (default)
@@ -42,7 +40,7 @@ export default function initializeBuildWatcher(
// the Shadow DOM, we need to prefix all the names so there
// will be no conflicts
shadowRoot = shadowHost
- prefix = '__next-build-watcher-'
+ prefix = '__next-build-indicator-'
}
// Container
@@ -58,22 +56,14 @@ export default function initializeBuildWatcher(
let isBuilding = false
let timeoutId: null | ReturnType = null
- // Handle events
-
- addMessageListener((obj) => {
- try {
- handleMessage(obj)
- } catch {}
- })
-
- function show() {
+ devBuildIndicator.show = () => {
timeoutId && clearTimeout(timeoutId)
isVisible = true
isBuilding = true
updateContainer()
}
- function hide() {
+ devBuildIndicator.hide = () => {
isBuilding = false
// Wait for the fade out transition to complete
timeoutId = setTimeout(() => {
@@ -83,28 +73,6 @@ export default function initializeBuildWatcher(
updateContainer()
}
- function handleMessage(obj: HMR_ACTION_TYPES) {
- if (!('action' in obj)) {
- return
- }
-
- // eslint-disable-next-line default-case
- switch (obj.action) {
- case HMR_ACTIONS_SENT_TO_BROWSER.BUILDING:
- show()
- break
- case HMR_ACTIONS_SENT_TO_BROWSER.BUILT:
- case HMR_ACTIONS_SENT_TO_BROWSER.SYNC:
- hide()
- break
- }
- }
-
- toggleCallback({
- show,
- hide,
- })
-
function updateContainer() {
if (isBuilding) {
container.classList.add(`${prefix}building`)
diff --git a/packages/next/src/client/page-bootstrap.ts b/packages/next/src/client/page-bootstrap.ts
index 81b66f41fc609..54c5f49405e12 100644
--- a/packages/next/src/client/page-bootstrap.ts
+++ b/packages/next/src/client/page-bootstrap.ts
@@ -1,7 +1,6 @@
import { hydrate, router } from './'
import initOnDemandEntries from './dev/on-demand-entries-client'
-import initializeBuildWatcher from './dev/dev-build-watcher'
-import type { ShowHideHandler } from './dev/dev-build-watcher'
+import { devBuildIndicator } from './dev/dev-build-indicator/internal/dev-build-indicator'
import { displayContent } from './dev/fouc'
import {
connectHMR,
@@ -15,6 +14,7 @@ import { HMR_ACTIONS_SENT_TO_BROWSER } from '../server/dev/hot-reloader-types'
import { RuntimeErrorHandler } from './components/react-dev-overlay/internal/helpers/runtime-error-handler'
import { REACT_REFRESH_FULL_RELOAD_FROM_ERROR } from './components/react-dev-overlay/shared'
import { performFullReload } from './components/react-dev-overlay/pages/hot-reloader-client'
+import { initializeDevBuildIndicatorForPageRouter } from './dev/dev-build-indicator/initialize-for-page-router'
export function pageBootstrap(assetPrefix: string) {
connectHMR({ assetPrefix, path: '/_next/webpack-hmr' })
@@ -22,13 +22,7 @@ export function pageBootstrap(assetPrefix: string) {
return hydrate({ beforeRender: displayContent }).then(() => {
initOnDemandEntries()
- let buildIndicatorHandler: ShowHideHandler | undefined
-
- if (process.env.__NEXT_BUILD_INDICATOR) {
- initializeBuildWatcher((handler) => {
- buildIndicatorHandler = handler
- }, process.env.__NEXT_BUILD_INDICATOR_POSITION)
- }
+ initializeDevBuildIndicatorForPageRouter()
let reloading = false
@@ -98,10 +92,8 @@ export function pageBootstrap(assetPrefix: string) {
if (!router.clc && pages.includes(router.pathname)) {
console.log('Refreshing page data due to server-side change')
-
- buildIndicatorHandler?.show()
-
- const clearIndicator = () => buildIndicatorHandler?.hide()
+ devBuildIndicator.show()
+ const clearIndicator = () => devBuildIndicator.hide()
router
.replace(
diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts
index 21429c87b8f0b..d31c636a603d3 100644
--- a/packages/next/src/server/base-server.ts
+++ b/packages/next/src/server/base-server.ts
@@ -1229,34 +1229,25 @@ export default abstract class Server<
params = paramsResult.params
}
+ const routeMatchesHeader = req.headers['x-now-route-matches']
if (
- req.headers['x-now-route-matches'] &&
+ typeof routeMatchesHeader === 'string' &&
+ routeMatchesHeader &&
isDynamicRoute(matchedPath) &&
!paramsResult.hasValidParams
) {
- const opts: Record = {}
- const routeParams = utils.getParamsFromRouteMatches(
- req,
- opts,
- getRequestMeta(req, 'locale')
- )
+ const routeMatches =
+ utils.getParamsFromRouteMatches(routeMatchesHeader)
- // If this returns a locale, it means that the locale was detected
- // from the pathname.
- if (opts.locale) {
- addRequestMeta(req, 'locale', opts.locale)
+ if (routeMatches) {
+ paramsResult = utils.normalizeDynamicRouteParams(
+ routeMatches,
+ true
+ )
- // As the locale was parsed from the pathname, we should mark
- // that the locale was not inferred as the default.
- removeRequestMeta(req, 'localeInferredFromDefault')
- }
- paramsResult = utils.normalizeDynamicRouteParams(
- routeParams,
- true
- )
-
- if (paramsResult.hasValidParams) {
- params = paramsResult.params
+ if (paramsResult.hasValidParams) {
+ params = paramsResult.params
+ }
}
}
diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts
index 7eb9eeeabb740..fcf0dcf84b69e 100644
--- a/packages/next/src/server/dev/hot-reloader-webpack.ts
+++ b/packages/next/src/server/dev/hot-reloader-webpack.ts
@@ -350,10 +350,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
parsedPageBundleUrl: UrlObject
): Promise<{ finished?: true }> => {
const { pathname } = parsedPageBundleUrl
+ if (!pathname) return {}
+
const params = matchNextPageBundleRequest(pathname)
- if (!params) {
- return {}
- }
+ if (!params) return {}
let decodedPagePath: string
diff --git a/packages/next/src/server/server-utils.test.ts b/packages/next/src/server/server-utils.test.ts
new file mode 100644
index 0000000000000..7bf1c1922dfa8
--- /dev/null
+++ b/packages/next/src/server/server-utils.test.ts
@@ -0,0 +1,73 @@
+import { getUtils } from './server-utils'
+
+describe('getParamsFromRouteMatches', () => {
+ it('should return nothing for a non-dynamic route', () => {
+ const { getParamsFromRouteMatches } = getUtils({
+ page: '/',
+ basePath: '',
+ rewrites: {},
+ i18n: undefined,
+ pageIsDynamic: false,
+ caseSensitive: false,
+ })
+
+ const params = getParamsFromRouteMatches('nxtPslug=hello-world')
+ expect(params).toEqual(null)
+ })
+
+ it('should return the params from the route matches', () => {
+ const { getParamsFromRouteMatches } = getUtils({
+ page: '/[slug]',
+ basePath: '',
+ rewrites: {},
+ i18n: undefined,
+ pageIsDynamic: true,
+ caseSensitive: false,
+ })
+
+ const params = getParamsFromRouteMatches('nxtPslug=hello-world')
+ expect(params).toEqual({ slug: 'hello-world' })
+ })
+
+ it('should handle optional params', () => {
+ const { getParamsFromRouteMatches } = getUtils({
+ page: '/[slug]/[[...optional]]',
+ basePath: '',
+ rewrites: {},
+ i18n: undefined,
+ pageIsDynamic: true,
+ caseSensitive: false,
+ })
+
+ // Missing optional param
+ let params = getParamsFromRouteMatches('nxtPslug=hello-world')
+ expect(params).toEqual({ slug: 'hello-world' })
+
+ // Providing optional param
+ params = getParamsFromRouteMatches(
+ 'nxtPslug=hello-world&nxtPoptional=im-optional'
+ )
+ expect(params).toEqual({ slug: 'hello-world', optional: ['im-optional'] })
+ })
+
+ it('should handle rest params', () => {
+ const { getParamsFromRouteMatches } = getUtils({
+ page: '/[slug]/[...rest]',
+ basePath: '',
+ rewrites: {},
+ i18n: undefined,
+ pageIsDynamic: true,
+ caseSensitive: false,
+ })
+
+ // Missing rest param
+ let params = getParamsFromRouteMatches('nxtPslug=hello-world')
+ expect(params).toEqual(null)
+
+ // Providing rest param
+ params = getParamsFromRouteMatches(
+ 'nxtPslug=hello-world&nxtPrest=im-the/rest'
+ )
+ expect(params).toEqual({ slug: 'hello-world', rest: ['im-the', 'rest'] })
+ })
+})
diff --git a/packages/next/src/server/server-utils.ts b/packages/next/src/server/server-utils.ts
index bf37153765c70..44f2175f83255 100644
--- a/packages/next/src/server/server-utils.ts
+++ b/packages/next/src/server/server-utils.ts
@@ -20,6 +20,7 @@ import {
NEXT_INTERCEPTION_MARKER_PREFIX,
NEXT_QUERY_PARAM_PREFIX,
} from '../lib/constants'
+import { normalizeNextQueryParam } from './web/utils'
export function normalizeVercelUrl(
req: BaseNextRequest,
@@ -221,6 +222,9 @@ export function getUtils({
sensitive: !!caseSensitive,
}
)
+
+ if (!parsedUrl.pathname) return false
+
let params = matcher(parsedUrl.pathname)
if ((rewrite.has || rewrite.missing) && params) {
@@ -258,20 +262,17 @@ export function getUtils({
Object.assign(parsedUrl, parsedDestination)
fsPathname = parsedUrl.pathname
+ if (!fsPathname) return false
if (basePath) {
- fsPathname =
- fsPathname!.replace(new RegExp(`^${basePath}`), '') || '/'
+ fsPathname = fsPathname.replace(new RegExp(`^${basePath}`), '') || '/'
}
if (i18n) {
- const destLocalePathResult = normalizeLocalePath(
- fsPathname!,
- i18n.locales
- )
- fsPathname = destLocalePathResult.pathname
+ const result = normalizeLocalePath(fsPathname, i18n.locales)
+ fsPathname = result.pathname
parsedUrl.query.nextInternalLocale =
- destLocalePathResult.detectedLocale || params.nextInternalLocale
+ result.detectedLocale || params.nextInternalLocale
}
if (fsPathname === page) {
@@ -314,102 +315,56 @@ export function getUtils({
return rewriteParams
}
- function getParamsFromRouteMatches(
- req: BaseNextRequest,
- renderOpts?: any,
- detectedLocale?: string
- ) {
- return getRouteMatcher(
- (function () {
- const { groups, routeKeys } = defaultRouteRegex!
-
- return {
- re: {
- // Simulate a RegExp match from the \`req.url\` input
- exec: (str: string) => {
- const obj = Object.fromEntries(new URLSearchParams(str))
- const matchesHasLocale =
- i18n && detectedLocale && obj['1'] === detectedLocale
-
- for (const key of Object.keys(obj)) {
- const value = obj[key]
-
- if (
- key !== NEXT_QUERY_PARAM_PREFIX &&
- key.startsWith(NEXT_QUERY_PARAM_PREFIX)
- ) {
- const normalizedKey = key.substring(
- NEXT_QUERY_PARAM_PREFIX.length
- )
- obj[normalizedKey] = value
- delete obj[key]
- }
- }
-
- // favor named matches if available
- const routeKeyNames = Object.keys(routeKeys || {})
- const filterLocaleItem = (val: string | string[] | undefined) => {
- if (i18n) {
- // locale items can be included in route-matches
- // for fallback SSG pages so ensure they are
- // filtered
- const isCatchAll = Array.isArray(val)
- const _val = isCatchAll ? val[0] : val
-
- if (
- typeof _val === 'string' &&
- i18n.locales.some((item) => {
- if (item.toLowerCase() === _val.toLowerCase()) {
- detectedLocale = item
- renderOpts.locale = detectedLocale
- return true
- }
- return false
- })
- ) {
- // remove the locale item from the match
- if (isCatchAll) {
- ;(val as string[]).splice(0, 1)
- }
-
- // the value is only a locale item and
- // shouldn't be added
- return isCatchAll ? val.length === 0 : true
- }
- }
- return false
- }
-
- if (routeKeyNames.every((name) => obj[name])) {
- return routeKeyNames.reduce((prev, keyName) => {
- const paramName = routeKeys?.[keyName]
-
- if (paramName && !filterLocaleItem(obj[keyName])) {
- prev[groups[paramName].pos] = obj[keyName]
- }
- return prev
- }, {} as any)
- }
-
- return Object.keys(obj).reduce((prev, key) => {
- if (!filterLocaleItem(obj[key])) {
- let normalizedKey = key
-
- if (matchesHasLocale) {
- normalizedKey = parseInt(key, 10) - 1 + ''
- }
- return Object.assign(prev, {
- [normalizedKey]: obj[key],
- })
- }
- return prev
- }, {})
- },
- },
- groups,
- }
- })() as any
- )(req.headers['x-now-route-matches'] as string) as ParsedUrlQuery
+ function getParamsFromRouteMatches(routeMatchesHeader: string) {
+ // If we don't have a default route regex, we can't get params from route
+ // matches
+ if (!defaultRouteRegex) return null
+
+ const { groups, routeKeys } = defaultRouteRegex
+
+ const matcher = getRouteMatcher({
+ re: {
+ // Simulate a RegExp match from the \`req.url\` input
+ exec: (str: string) => {
+ // Normalize all the prefixed query params.
+ const obj: Record = Object.fromEntries(
+ new URLSearchParams(str)
+ )
+ for (const [key, value] of Object.entries(obj)) {
+ const normalizedKey = normalizeNextQueryParam(key)
+ if (!normalizedKey) continue
+
+ obj[normalizedKey] = value
+ delete obj[key]
+ }
+
+ // Use all the named route keys.
+ const result = {} as RegExpExecArray
+ for (const keyName of Object.keys(routeKeys)) {
+ const paramName = routeKeys[keyName]
+
+ // If this param name is not a valid parameter name, then skip it.
+ if (!paramName) continue
+
+ const group = groups[paramName]
+ const value = obj[keyName]
+
+ // When we're missing a required param, we can't match the route.
+ if (!group.optional && !value) return null
+
+ result[group.pos] = value
+ }
+
+ return result
+ },
+ },
+ groups,
+ })
+
+ const routeMatches = matcher(routeMatchesHeader)
+ if (!routeMatches) return null
+
+ return routeMatches
}
return {
diff --git a/packages/next/src/server/web/utils.ts b/packages/next/src/server/web/utils.ts
index 26440fa212c5f..c7afe05a40bca 100644
--- a/packages/next/src/server/web/utils.ts
+++ b/packages/next/src/server/web/utils.ts
@@ -159,8 +159,7 @@ export function normalizeNextQueryParam(key: string): null | string {
const prefixes = [NEXT_QUERY_PARAM_PREFIX, NEXT_INTERCEPTION_MARKER_PREFIX]
for (const prefix of prefixes) {
if (key !== prefix && key.startsWith(prefix)) {
- const normalizedKey = key.substring(prefix.length)
- return normalizedKey
+ return key.substring(prefix.length)
}
}
return null
diff --git a/packages/next/src/shared/lib/router/utils/path-match.ts b/packages/next/src/shared/lib/router/utils/path-match.ts
index e011897d22f89..a7aac43651a21 100644
--- a/packages/next/src/shared/lib/router/utils/path-match.ts
+++ b/packages/next/src/shared/lib/router/utils/path-match.ts
@@ -26,7 +26,7 @@ interface Options {
}
export type PatchMatcher = (
- pathname?: string | null,
+ pathname: string,
params?: Record
) => Record | false
diff --git a/packages/next/src/shared/lib/router/utils/route-matcher.ts b/packages/next/src/shared/lib/router/utils/route-matcher.ts
index 2e8d162c9c3a5..7eed61a3f7c67 100644
--- a/packages/next/src/shared/lib/router/utils/route-matcher.ts
+++ b/packages/next/src/shared/lib/router/utils/route-matcher.ts
@@ -1,38 +1,46 @@
-import type { RouteRegex } from './route-regex'
+import type { Group } from './route-regex'
import { DecodeError } from '../../utils'
import type { Params } from '../../../../server/request/params'
export interface RouteMatchFn {
- (pathname: string | null | undefined): false | Params
+ (pathname: string): false | Params
}
-export function getRouteMatcher({ re, groups }: RouteRegex): RouteMatchFn {
- return (pathname: string | null | undefined) => {
- const routeMatch = re.exec(pathname!)
- if (!routeMatch) {
- return false
- }
+type RouteMatcherOptions = {
+ // We only use the exec method of the RegExp object. This helps us avoid using
+ // type assertions that the passed in properties are of the correct type.
+ re: Pick
+ groups: Record
+}
+
+export function getRouteMatcher({
+ re,
+ groups,
+}: RouteMatcherOptions): RouteMatchFn {
+ return (pathname: string) => {
+ const routeMatch = re.exec(pathname)
+ if (!routeMatch) return false
const decode = (param: string) => {
try {
return decodeURIComponent(param)
- } catch (_) {
+ } catch {
throw new DecodeError('failed to decode param')
}
}
- const params: { [paramName: string]: string | string[] } = {}
- Object.keys(groups).forEach((slugName: string) => {
- const g = groups[slugName]
- const m = routeMatch[g.pos]
- if (m !== undefined) {
- params[slugName] = ~m.indexOf('/')
- ? m.split('/').map((entry) => decode(entry))
- : g.repeat
- ? [decode(m)]
- : decode(m)
+ const params: Params = {}
+ for (const [key, group] of Object.entries(groups)) {
+ const match = routeMatch[group.pos]
+ if (match !== undefined) {
+ if (group.repeat) {
+ params[key] = match.split('/').map((entry) => decode(entry))
+ } else {
+ params[key] = decode(match)
+ }
}
- })
+ }
+
return params
}
}
diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json
index f4a929449dc72..b25026e3118b6 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": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"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 56d26c42f2ad5..00669b3771209 100644
--- a/packages/third-parties/package.json
+++ b/packages/third-parties/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/third-parties",
- "version": "15.1.1-canary.27",
+ "version": "15.2.0-canary.1",
"repository": {
"url": "vercel/next.js",
"directory": "packages/third-parties"
@@ -26,7 +26,7 @@
"third-party-capital": "1.0.20"
},
"devDependencies": {
- "next": "15.1.1-canary.27",
+ "next": "15.2.0-canary.1",
"outdent": "0.8.0",
"prettier": "2.5.1",
"typescript": "5.7.2"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9d1b56e0df7ce..d1433c2ecce71 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -787,7 +787,7 @@ importers:
packages/eslint-config-next:
dependencies:
'@next/eslint-plugin-next':
- specifier: 15.1.1-canary.27
+ specifier: 15.2.0-canary.1
version: link:../eslint-plugin-next
'@rushstack/eslint-patch':
specifier: ^1.10.3
@@ -851,7 +851,7 @@ importers:
packages/next:
dependencies:
'@next/env':
- specifier: 15.1.1-canary.27
+ specifier: 15.2.0-canary.1
version: link:../next-env
'@swc/counter':
specifier: 0.1.3
@@ -979,19 +979,19 @@ importers:
specifier: 1.2.0
version: 1.2.0
'@next/font':
- specifier: 15.1.1-canary.27
+ specifier: 15.2.0-canary.1
version: link:../font
'@next/polyfill-module':
- specifier: 15.1.1-canary.27
+ specifier: 15.2.0-canary.1
version: link:../next-polyfill-module
'@next/polyfill-nomodule':
- specifier: 15.1.1-canary.27
+ specifier: 15.2.0-canary.1
version: link:../next-polyfill-nomodule
'@next/react-refresh-utils':
- specifier: 15.1.1-canary.27
+ specifier: 15.2.0-canary.1
version: link:../react-refresh-utils
'@next/swc':
- specifier: 15.1.1-canary.27
+ specifier: 15.2.0-canary.1
version: link:../next-swc
'@opentelemetry/api':
specifier: 1.6.0
@@ -1655,7 +1655,7 @@ importers:
version: 1.0.20
devDependencies:
next:
- specifier: 15.1.1-canary.27
+ specifier: 15.2.0-canary.1
version: link:../next
outdent:
specifier: 0.8.0
diff --git a/test/development/basic/gssp-ssr-change-reloading/test/index.test.ts b/test/development/basic/gssp-ssr-change-reloading/test/index.test.ts
index 59fbac9c3094b..17fcf32eddba0 100644
--- a/test/development/basic/gssp-ssr-change-reloading/test/index.test.ts
+++ b/test/development/basic/gssp-ssr-change-reloading/test/index.test.ts
@@ -14,7 +14,7 @@ import { NextInstance } from 'e2e-utils'
const installCheckVisible = (browser) => {
return browser.eval(`(function() {
window.checkInterval = setInterval(function() {
- let watcherDiv = document.querySelector('#__next-build-watcher')
+ let watcherDiv = document.querySelector('#__next-build-indicator')
watcherDiv = watcherDiv.shadowRoot || watcherDiv
window.showedBuilder = window.showedBuilder || (
watcherDiv.querySelector('div').className.indexOf('visible') > -1
diff --git a/test/e2e/app-dir/app-catch-all-optional/app-catch-all-optional.test.ts b/test/e2e/app-dir/app-catch-all-optional/app-catch-all-optional.test.ts
new file mode 100644
index 0000000000000..c82a63a69795e
--- /dev/null
+++ b/test/e2e/app-dir/app-catch-all-optional/app-catch-all-optional.test.ts
@@ -0,0 +1,21 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('app-catch-all-optional', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ })
+
+ it('should handle optional catchall', async () => {
+ let $ = await next.render$('/en/flags/the/rest')
+ expect($('body [data-lang]').text()).toBe('en')
+ expect($('body [data-flags]').text()).toBe('flags')
+ expect($('body [data-rest]').text()).toBe('the/rest')
+ })
+
+ it('should handle optional catchall with no params', async () => {
+ let $ = await next.render$('/en/flags')
+ expect($('body [data-lang]').text()).toBe('en')
+ expect($('body [data-flags]').text()).toBe('flags')
+ expect($('body [data-rest]').text()).toBe('')
+ })
+})
diff --git a/test/e2e/app-dir/app-catch-all-optional/app/[lang]/[flags]/[[...rest]]/page.tsx b/test/e2e/app-dir/app-catch-all-optional/app/[lang]/[flags]/[[...rest]]/page.tsx
new file mode 100644
index 0000000000000..e072ec89fa849
--- /dev/null
+++ b/test/e2e/app-dir/app-catch-all-optional/app/[lang]/[flags]/[[...rest]]/page.tsx
@@ -0,0 +1,15 @@
+export async function generateStaticParams() {
+ return []
+}
+
+export default async function Page(props) {
+ const params = await props.params
+
+ return (
+
+
{params.lang}
+
{params.flags}
+
{params.rest?.join('/')}
+
+ )
+}
diff --git a/test/e2e/app-dir/app-catch-all-optional/app/layout.tsx b/test/e2e/app-dir/app-catch-all-optional/app/layout.tsx
new file mode 100644
index 0000000000000..888614deda3ba
--- /dev/null
+++ b/test/e2e/app-dir/app-catch-all-optional/app/layout.tsx
@@ -0,0 +1,8 @@
+import { ReactNode } from 'react'
+export default function Root({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/test/e2e/app-dir/app-catch-all-optional/app/page.tsx b/test/e2e/app-dir/app-catch-all-optional/app/page.tsx
new file mode 100644
index 0000000000000..ff7159d9149fe
--- /dev/null
+++ b/test/e2e/app-dir/app-catch-all-optional/app/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return hello world
+}
diff --git a/test/e2e/app-dir/app-catch-all-optional/next.config.js b/test/e2e/app-dir/app-catch-all-optional/next.config.js
new file mode 100644
index 0000000000000..807126e4cf0bf
--- /dev/null
+++ b/test/e2e/app-dir/app-catch-all-optional/next.config.js
@@ -0,0 +1,6 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/test/integration/build-indicator/test/index.test.js b/test/integration/build-indicator/test/index.test.js
index 30a6ac8624d5d..4879e08f9c9bc 100644
--- a/test/integration/build-indicator/test/index.test.js
+++ b/test/integration/build-indicator/test/index.test.js
@@ -14,7 +14,7 @@ let app
const installCheckVisible = (browser) => {
return browser.eval(`(function() {
window.checkInterval = setInterval(function() {
- let watcherDiv = document.querySelector('#__next-build-watcher')
+ let watcherDiv = document.querySelector('#__next-build-indicator')
watcherDiv = watcherDiv.shadowRoot || watcherDiv
window.showedBuilder = window.showedBuilder || (
watcherDiv.querySelector('div').className.indexOf('visible') > -1
@@ -70,7 +70,7 @@ describe('Build Activity Indicator', () => {
it('Adds the build indicator container', async () => {
const browser = await webdriver(appPort, '/')
const html = await browser.eval('document.body.innerHTML')
- expect(html).toMatch(/__next-build-watcher/)
+ expect(html).toMatch(/__next-build-indicator/)
await browser.close()
})
;(process.env.TURBOPACK ? describe.skip : describe)('webpack only', () => {
@@ -120,7 +120,7 @@ describe('Build Activity Indicator', () => {
it('Does not add the build indicator container', async () => {
const browser = await webdriver(appPort, '/')
const html = await browser.eval('document.body.innerHTML')
- expect(html).not.toMatch(/__next-build-watcher/)
+ expect(html).not.toMatch(/__next-build-indicator/)
await browser.close()
})
})
diff --git a/test/integration/required-server-files-ssr-404/test/index.test.js b/test/integration/required-server-files-ssr-404/test/index.test.js
index 9821c589ea4b9..e18274c2a424b 100644
--- a/test/integration/required-server-files-ssr-404/test/index.test.js
+++ b/test/integration/required-server-files-ssr-404/test/index.test.js
@@ -236,7 +236,7 @@ describe('Required Server Files', () => {
{
headers: {
'x-matched-path': '/fallback/first',
- 'x-now-route-matches': '1=first',
+ 'x-now-route-matches': 'nxtPslug=first',
},
}
)
@@ -254,7 +254,7 @@ describe('Required Server Files', () => {
{
headers: {
'x-matched-path': '/fallback/[slug]',
- 'x-now-route-matches': '1=second',
+ 'x-now-route-matches': 'nxtPslug=second',
},
}
)
@@ -291,7 +291,7 @@ describe('Required Server Files', () => {
{
headers: {
'x-matched-path': `/_next/data/${buildId}/fallback/[slug].json`,
- 'x-now-route-matches': '1=second',
+ 'x-now-route-matches': 'nxtPslug=second',
},
}
)
@@ -328,7 +328,7 @@ describe('Required Server Files', () => {
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
- 'x-now-route-matches': '1=hello&catchAll=hello',
+ 'x-now-route-matches': 'nxtPrest=hello&catchAll=hello',
},
}
)
@@ -347,7 +347,8 @@ describe('Required Server Files', () => {
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
- 'x-now-route-matches': '1=hello/world&catchAll=hello/world',
+ 'x-now-route-matches':
+ 'nxtPrest=hello/world&catchAll=hello/world',
},
}
)
@@ -384,7 +385,7 @@ describe('Required Server Files', () => {
{
headers: {
'x-matched-path': `/_next/data/${buildId}/catch-all/[[...rest]].json`,
- 'x-now-route-matches': '1=hello&rest=hello',
+ 'x-now-route-matches': 'nxtPrest=hello&rest=hello',
},
}
)
@@ -401,7 +402,7 @@ describe('Required Server Files', () => {
{
headers: {
'x-matched-path': `/_next/data/${buildId}/catch-all/[[...rest]].json`,
- 'x-now-route-matches': '1=hello/world&rest=hello/world',
+ 'x-now-route-matches': 'nxtPrest=hello/world&rest=hello/world',
},
}
)
diff --git a/test/lib/next-test-utils.ts b/test/lib/next-test-utils.ts
index 58ec131b9984b..f0800cb6a669d 100644
--- a/test/lib/next-test-utils.ts
+++ b/test/lib/next-test-utils.ts
@@ -1522,3 +1522,26 @@ export async function toggleCollapseCallStackFrames(browser: BrowserInterface) {
expect(currExpanded).not.toBe(lastExpanded)
})
}
+
+/**
+ * Encodes the params into a URLSearchParams object using the format that the
+ * now builder uses for route matches (adding the `nxtP` prefix to the keys).
+ *
+ * @param params - The params to encode.
+ * @param extraQueryParams - The extra query params to encode (without the `nxtP` prefix).
+ * @returns The encoded URLSearchParams object.
+ */
+export function createNowRouteMatches(
+ params: Record,
+ extraQueryParams: Record = {}
+): URLSearchParams {
+ const urlSearchParams = new URLSearchParams()
+ for (const [key, value] of Object.entries(params)) {
+ urlSearchParams.append(`nxtP${key}`, value)
+ }
+ for (const [key, value] of Object.entries(extraQueryParams)) {
+ urlSearchParams.append(key, value)
+ }
+
+ return urlSearchParams
+}
diff --git a/test/production/standalone-mode/required-server-files/app/optional-catchall/[lang]/[flags]/[[...slug]]/page.jsx b/test/production/standalone-mode/required-server-files/app/optional-catchall/[lang]/[flags]/[[...slug]]/page.jsx
new file mode 100644
index 0000000000000..839f5ad87dea9
--- /dev/null
+++ b/test/production/standalone-mode/required-server-files/app/optional-catchall/[lang]/[flags]/[[...slug]]/page.jsx
@@ -0,0 +1,15 @@
+export async function generateStaticParams() {
+ return []
+}
+
+export default async function Page(props) {
+ const params = await props.params
+
+ return (
+
+
{params.lang}
+
{params.flags}
+
{params.slug}
+
+ )
+}
diff --git a/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts
index 6deeab6fb632b..20b31d75b2865 100644
--- a/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts
+++ b/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts
@@ -5,6 +5,7 @@ import cheerio from 'cheerio'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import {
+ createNowRouteMatches,
fetchViaHTTP,
findPort,
initNextServerScript,
@@ -107,7 +108,9 @@ describe('required server files app router', () => {
const res = await fetchViaHTTP(appPort, '/api/test/123', undefined, {
headers: {
'x-matched-path': '/api/test/[slug]',
- 'x-now-route-matches': '1=123&nxtPslug=123',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: '123',
+ }).toString(),
},
})
expect(res.status).toBe(200)
@@ -116,11 +119,59 @@ describe('required server files app router', () => {
)
})
+ it('should handle optional catchall', async () => {
+ let res = await fetchViaHTTP(
+ appPort,
+ '/optional-catchall/[lang]/[flags]/[[...slug]]',
+ undefined,
+ {
+ headers: {
+ 'x-matched-path': '/optional-catchall/[lang]/[flags]/[[...slug]]',
+ 'x-now-route-matches': createNowRouteMatches({
+ lang: 'en',
+ flags: 'flags',
+ slug: 'slug',
+ }).toString(),
+ },
+ }
+ )
+ expect(res.status).toBe(200)
+
+ let html = await res.text()
+ let $ = cheerio.load(html)
+ expect($('body [data-lang]').text()).toBe('en')
+ expect($('body [data-slug]').text()).toBe('slug')
+
+ res = await fetchViaHTTP(
+ appPort,
+ '/optional-catchall/[lang]/[flags]/[[...slug]]',
+ undefined,
+ {
+ headers: {
+ 'x-matched-path': '/optional-catchall/[lang]/[flags]/[[...slug]]',
+ 'x-now-route-matches': createNowRouteMatches({
+ lang: 'en',
+ flags: 'flags',
+ }).toString(),
+ },
+ }
+ )
+ expect(res.status).toBe(200)
+
+ html = await res.text()
+ $ = cheerio.load(html)
+ expect($('body [data-lang]').text()).toBe('en')
+ expect($('body [data-flags]').text()).toBe('flags')
+ expect($('body [data-slug]').text()).toBe('')
+ })
+
it('should send the right cache headers for an app page', async () => {
const res = await fetchViaHTTP(appPort, '/test/123', undefined, {
headers: {
'x-matched-path': '/test/[slug]',
- 'x-now-route-matches': '1=123&nxtPslug=123',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: '123',
+ }).toString(),
},
})
expect(res.status).toBe(200)
@@ -164,7 +215,9 @@ describe('required server files app router', () => {
headers: {
'user-agent':
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.179 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
- 'x-now-route-matches': '1=second&nxtPslug=new',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'new',
+ }).toString(),
'x-matched-path': '/isr/[slug]',
},
})
@@ -179,7 +232,9 @@ describe('required server files app router', () => {
headers: {
'user-agent':
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.179 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
- 'x-now-route-matches': '1=second&nxtPslug=new',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'new',
+ }).toString(),
'x-matched-path': '/isr/[slug]',
},
})
diff --git a/test/production/standalone-mode/required-server-files/required-server-files-i18n.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-i18n.test.ts
index c2561925b293f..12cd22fbb3fbb 100644
--- a/test/production/standalone-mode/required-server-files/required-server-files-i18n.test.ts
+++ b/test/production/standalone-mode/required-server-files/required-server-files-i18n.test.ts
@@ -6,6 +6,7 @@ import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import {
check,
+ createNowRouteMatches,
fetchViaHTTP,
findPort,
initNextServerScript,
@@ -373,7 +374,9 @@ describe('required server files i18n', () => {
const html = await renderViaHTTP(appPort, '/fallback/first', undefined, {
headers: {
'x-matched-path': '/fallback/first',
- 'x-now-route-matches': '1=first',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'first',
+ }).toString(),
},
})
const $ = cheerio.load(html)
@@ -386,7 +389,9 @@ describe('required server files i18n', () => {
const html2 = await renderViaHTTP(appPort, `/fallback/[slug]`, undefined, {
headers: {
'x-matched-path': '/fallback/[slug]',
- 'x-now-route-matches': '1=second',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'second',
+ }).toString(),
},
})
const $2 = cheerio.load(html2)
@@ -401,7 +406,9 @@ describe('required server files i18n', () => {
it('should return data correctly with x-matched-path', async () => {
const res = await fetchViaHTTP(
appPort,
- `/_next/data/${next.buildId}/en/dynamic/first.json?nxtPslug=first`,
+ `/_next/data/${next.buildId}/en/dynamic/first.json?${createNowRouteMatches(
+ { slug: 'first' }
+ ).toString()}`,
undefined,
{
headers: {
@@ -422,7 +429,9 @@ describe('required server files i18n', () => {
{
headers: {
'x-matched-path': `/_next/data/${next.buildId}/en/fallback/[slug].json`,
- 'x-now-route-matches': '1=second',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'second',
+ }).toString(),
},
}
)
@@ -459,7 +468,9 @@ describe('required server files i18n', () => {
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
- 'x-now-route-matches': '1=hello&nxtPcatchAll=hello',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: 'hello',
+ }).toString(),
},
}
)
@@ -478,7 +489,9 @@ describe('required server files i18n', () => {
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
- 'x-now-route-matches': '1=hello/world&nxtPcatchAll=hello/world',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: 'hello/world',
+ }).toString(),
},
}
)
@@ -515,7 +528,9 @@ describe('required server files i18n', () => {
{
headers: {
'x-matched-path': `/_next/data/${next.buildId}/en/catch-all/[[...rest]].json`,
- 'x-now-route-matches': '1=hello&nxtPrest=hello',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: 'hello',
+ }).toString(),
},
}
)
@@ -532,7 +547,9 @@ describe('required server files i18n', () => {
{
headers: {
'x-matched-path': `/_next/data/${next.buildId}/en/catch-all/[[...rest]].json`,
- 'x-now-route-matches': '1=hello/world&nxtPrest=hello/world',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: 'hello/world',
+ }).toString(),
},
}
)
@@ -585,7 +602,11 @@ describe('required server files i18n', () => {
const res = await fetchViaHTTP(
appPort,
'/to-dynamic/%c0.%c0.',
- '?path=%c0.%c0.',
+ Object.fromEntries(
+ createNowRouteMatches({
+ path: '%c0.%c0.',
+ })
+ ),
{
headers: {
'x-matched-path': '/dynamic/[slug]',
@@ -654,7 +675,16 @@ describe('required server files i18n', () => {
const res = await fetchViaHTTP(
appPort,
'/optional-ssp',
- { nxtPrest: '', another: 'value' },
+ Object.fromEntries(
+ createNowRouteMatches(
+ {
+ rest: '',
+ },
+ {
+ another: 'value',
+ }
+ )
+ ),
{
headers: {
'x-matched-path': '/optional-ssp/[[...rest]]',
@@ -677,7 +707,12 @@ describe('required server files i18n', () => {
{
headers: {
'x-matched-path': '/en/optional-ssg/[[...rest]]',
- 'x-now-route-matches': 'nextLocale=en&1=en',
+ 'x-now-route-matches': createNowRouteMatches(
+ {},
+ {
+ nextLocale: 'en',
+ }
+ ).toString(),
},
}
)
@@ -696,8 +731,10 @@ describe('required server files i18n', () => {
{
headers: {
'x-matched-path': '/en/[slug]/social/[[...rest]]',
- 'x-now-route-matches':
- 'nextLocale=en&1=en&2=user-123&nxtPslug=user-123',
+ 'x-now-route-matches': createNowRouteMatches(
+ { slug: 'user-123' },
+ { nextLocale: 'en' }
+ ).toString(),
},
}
)
@@ -719,8 +756,7 @@ describe('required server files i18n', () => {
{
headers: {
'x-matched-path': '/optional-ssg/[[...rest]]',
- 'x-now-route-matches':
- '1=en%2Fes%2Fhello%252Fworld&nxtPrest=en%2Fes%2Fhello%252Fworld',
+ 'x-now-route-matches': 'nxtPrest=en%2Fes%2Fhello%252Fworld',
},
}
)
@@ -737,7 +773,16 @@ describe('required server files i18n', () => {
const res = await fetchViaHTTP(
appPort,
'/api/optional',
- { nxtPrest: '', another: 'value' },
+ Object.fromEntries(
+ createNowRouteMatches(
+ {
+ rest: '',
+ },
+ {
+ another: 'value',
+ }
+ )
+ ),
{
headers: {
'x-matched-path': '/api/optional/[[...rest]]',
@@ -780,7 +825,14 @@ describe('required server files i18n', () => {
const res = await fetchViaHTTP(appPort, '/en/fallback/[slug]', undefined, {
headers: {
'x-matched-path': '/en/fallback/[slug]',
- 'x-now-route-matches': '2=another&nxtPslug=another&1=en&nextLocale=en',
+ 'x-now-route-matches': createNowRouteMatches(
+ {
+ slug: 'another',
+ },
+ {
+ nextLocale: 'en',
+ }
+ ).toString(),
},
redirect: 'manual',
})
@@ -798,7 +850,14 @@ describe('required server files i18n', () => {
const res = await fetchViaHTTP(appPort, '/fr/fallback/[slug]', undefined, {
headers: {
'x-matched-path': '/fr/fallback/[slug]',
- 'x-now-route-matches': '2=another&nxtPslug=another&1=fr&nextLocale=fr',
+ 'x-now-route-matches': createNowRouteMatches(
+ {
+ slug: 'another',
+ },
+ {
+ nextLocale: 'fr',
+ }
+ ).toString(),
},
redirect: 'manual',
})
diff --git a/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts
index 06a313047f826..0438ac7869356 100644
--- a/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts
+++ b/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts
@@ -5,6 +5,7 @@ import cheerio from 'cheerio'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import {
+ createNowRouteMatches,
fetchViaHTTP,
findPort,
initNextServerScript,
@@ -182,7 +183,9 @@ describe('required server files app router', () => {
headers: {
'user-agent':
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.179 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
- 'x-now-route-matches': '1=second&nxtPslug=new',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'new',
+ }).toString(),
'x-matched-path': '/isr/[slug]',
},
})
@@ -197,7 +200,9 @@ describe('required server files app router', () => {
headers: {
'user-agent':
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.179 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
- 'x-now-route-matches': '1=second&nxtPslug=new',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'new',
+ }).toString(),
'x-matched-path': '/isr/[slug]',
},
})
diff --git a/test/production/standalone-mode/required-server-files/required-server-files.test.ts b/test/production/standalone-mode/required-server-files/required-server-files.test.ts
index bb045fe42662c..06006c64d3adc 100644
--- a/test/production/standalone-mode/required-server-files/required-server-files.test.ts
+++ b/test/production/standalone-mode/required-server-files/required-server-files.test.ts
@@ -7,6 +7,7 @@ import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import {
check,
+ createNowRouteMatches,
fetchViaHTTP,
findPort,
initNextServerScript,
@@ -669,7 +670,9 @@ describe('required server files', () => {
const html = await renderViaHTTP(appPort, '/fallback/first', undefined, {
headers: {
'x-matched-path': '/fallback/first',
- 'x-now-route-matches': '1=first',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'first',
+ }).toString(),
},
})
const $ = cheerio.load(html)
@@ -682,7 +685,9 @@ describe('required server files', () => {
const html2 = await renderViaHTTP(appPort, `/fallback/[slug]`, undefined, {
headers: {
'x-matched-path': '/fallback/[slug]',
- 'x-now-route-matches': '1=second',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'second',
+ }).toString(),
},
})
const $2 = cheerio.load(html2)
@@ -698,7 +703,9 @@ describe('required server files', () => {
const html = await renderViaHTTP(appPort, '/fallback/first', undefined, {
headers: {
'x-matched-path': '/fallback/first',
- 'x-now-route-matches': '1=fallback%2ffirst',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'fallback/first',
+ }).toString(),
},
})
const $ = cheerio.load(html)
@@ -711,7 +718,9 @@ describe('required server files', () => {
const html2 = await renderViaHTTP(appPort, `/fallback/second`, undefined, {
headers: {
'x-matched-path': '/fallback/[slug]',
- 'x-now-route-matches': '1=fallback%2fsecond',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'fallback/second',
+ }).toString(),
},
})
const $2 = cheerio.load(html2)
@@ -727,7 +736,7 @@ describe('required server files', () => {
const html = await renderViaHTTP(appPort, '/optional-ssg', undefined, {
headers: {
'x-matched-path': '/optional-ssg',
- 'x-now-route-matches': '1=optional-ssg',
+ 'x-now-route-matches': '',
},
})
const $ = cheerio.load(html)
@@ -737,7 +746,9 @@ describe('required server files', () => {
const html2 = await renderViaHTTP(appPort, `/optional-ssg`, undefined, {
headers: {
'x-matched-path': '/optional-ssg',
- 'x-now-route-matches': '1=optional-ssg%2fanother',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'another',
+ }).toString(),
},
})
const $2 = cheerio.load(html2)
@@ -750,7 +761,9 @@ describe('required server files', () => {
it('should return data correctly with x-matched-path', async () => {
const res = await fetchViaHTTP(
appPort,
- `/_next/data/${next.buildId}/dynamic/first.json?nxtPslug=first`,
+ `/_next/data/${next.buildId}/dynamic/first.json?${createNowRouteMatches({
+ slug: 'first',
+ }).toString()}`,
undefined,
{
headers: {
@@ -771,7 +784,9 @@ describe('required server files', () => {
{
headers: {
'x-matched-path': `/_next/data/${next.buildId}/fallback/[slug].json`,
- 'x-now-route-matches': '1=second',
+ 'x-now-route-matches': createNowRouteMatches({
+ slug: 'second',
+ }).toString(),
},
}
)
@@ -808,7 +823,9 @@ describe('required server files', () => {
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
- 'x-now-route-matches': '1=hello&nxtPcatchAll=hello',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: 'hello',
+ }).toString(),
},
}
)
@@ -827,7 +844,9 @@ describe('required server files', () => {
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
- 'x-now-route-matches': '1=hello/world&nxtPcatchAll=hello/world',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: 'hello/world',
+ }).toString(),
},
}
)
@@ -865,7 +884,9 @@ describe('required server files', () => {
{
headers: {
'x-matched-path': `/_next/data/${next.buildId}/catch-all/[[...rest]].json`,
- 'x-now-route-matches': '1=hello&nxtPrest=hello',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: 'hello',
+ }).toString(),
},
}
)
@@ -882,7 +903,9 @@ describe('required server files', () => {
{
headers: {
'x-matched-path': `/_next/data/${next.buildId}/catch-all/[[...rest]].json`,
- 'x-now-route-matches': '1=hello/world&nxtPrest=hello/world',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: 'hello/world',
+ }).toString(),
},
}
)
@@ -934,7 +957,9 @@ describe('required server files', () => {
const res = await fetchViaHTTP(
appPort,
'/to-dynamic/%c0.%c0.',
- '?path=%c0.%c0.',
+ {
+ path: '%c0.%c0.',
+ },
{
headers: {
'x-matched-path': '/dynamic/[slug]',
@@ -1092,7 +1117,9 @@ describe('required server files', () => {
path: `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
headers: {
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
- 'x-now-route-matches': '1=',
+ 'x-now-route-matches': createNowRouteMatches({
+ rest: '',
+ }).toString(),
},
},
{
@@ -1149,8 +1176,7 @@ describe('required server files', () => {
{
headers: {
'x-matched-path': '/optional-ssg/[[...rest]]',
- 'x-now-route-matches':
- '1=en%2Fes%2Fhello%252Fworld&nxtPrest=en%2Fes%2Fhello%252Fworld',
+ 'x-now-route-matches': 'nxtPrest=en%2Fes%2Fhello%252Fworld',
},
}
)