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', }, } )