Deprecate nested Middleware in favor of root middleware (#36772)

This PR deprecates declaring a middleware under `pages` in favour of the project root naming it after `middleware` instead of `_middleware`. This is in the context of having a simpler execution model for middleware and also ships some refactor work. There is a ton of a code to be simplified after this deprecation but I think it is best to do it progressively.

With this PR, when in development, we will **fail** whenever we find a nested middleware but we do **not** include it in the compiler so if the project is using it, it will no longer work. For production we will **fail** too so it will not be possible to build and deploy a deprecated middleware. The error points to a page that should also be reviewed as part of **documentation**.

Aside from the deprecation, this migrates all middleware tests to work with a single middleware. It also splits tests into multiple folders to make them easier to isolate and work with. Finally it ships some small code refactor and simplifications.
This commit is contained in:
Javi Velasco 2022-05-19 17:46:21 +02:00 committed by GitHub
parent cc8ab99a92
commit f354f46b3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
145 changed files with 2381 additions and 2499 deletions

View file

@ -164,7 +164,7 @@ module.exports = {
Next, we can use [Middleware](/docs/middleware.md) to add custom routing rules:
```js
// pages/_middleware.ts
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

View file

@ -24,12 +24,12 @@ Middleware enables you to use code over configuration. This gives you full flexi
npm install next@latest
```
2. Then, create a `_middleware.ts` file under your `/pages` directory.
2. Then, create a `middleware.ts` file under your project root directory.
3. Finally, export a middleware function from the `_middleware.ts` file.
3. Finally, export a middleware function from the `middleware.ts` file.
```jsx
// pages/_middleware.ts
// middleware.ts
import type { NextFetchEvent, NextRequest } from 'next/server'
@ -42,7 +42,7 @@ In this example, we use the standard Web API Response ([MDN](https://developer.m
## API
Middleware is created by using a `middleware` function that lives inside a `_middleware` file. Its API is based upon the native [`FetchEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent), [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), and [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) objects.
Middleware is created by using a `middleware` function that lives inside a `middleware` file. Its API is based upon the native [`FetchEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent), [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), and [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) objects.
These native Web API objects are extended to give you more control over how you manipulate and configure a response, based on the incoming requests.
@ -75,31 +75,6 @@ Middleware can be used for anything that shares logic for a set of pages, includ
## Execution Order
If your Middleware is created in `/pages/_middleware.ts`, it will run on all routes within the `/pages` directory. The below example assumes you have `about.tsx` and `teams.tsx` routes.
```bash
- package.json
- /pages
_middleware.ts # Will run on all routes under /pages
index.tsx
about.tsx
teams.tsx
```
If you _do_ have sub-directories with nested routes, Middleware will run from the top down. For example, if you have `/pages/about/_middleware.ts` and `/pages/about/team/_middleware.ts`, `/about` will run first and then `/about/team`. The below example shows how this works with a nested routing structure.
```bash
- package.json
- /pages
index.tsx
- /about
_middleware.ts # Will run first
about.tsx
- /teams
_middleware.ts # Will run second
teams.tsx
```
Middleware runs directly after `redirects` and `headers`, before the first filesystem lookup. This excludes `/_next` files.
## Deployment

View file

@ -16,7 +16,7 @@ module.exports = {
> **Note**: The default value of `pageExtensions` is [`['tsx', 'ts', 'jsx', 'js']`](https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161).
> **Note**: configuring `pageExtensions` also affects `_document.js`, `_app.js`, `_middleware.js` as well as files under `pages/api/`. For example, setting `pageExtensions: ['page.tsx', 'page.ts']` means the following files: `_document.tsx`, `_app.tsx`, `_middleware.ts`, `pages/users.tsx` and `pages/api/users.ts` will have to be renamed to `_document.page.tsx`, `_app.page.tsx`, `_middleware.page.ts`, `pages/users.page.tsx` and `pages/api/users.page.ts` respectively.
> **Note**: configuring `pageExtensions` also affects `_document.js`, `_app.js`, `middleware.js` as well as files under `pages/api/`. For example, setting `pageExtensions: ['page.tsx', 'page.ts']` means the following files: `_document.tsx`, `_app.tsx`, `middleware.ts`, `pages/users.tsx` and `pages/api/users.ts` will have to be renamed to `_document.page.tsx`, `_app.page.tsx`, `middleware.page.ts`, `pages/users.page.tsx` and `pages/api/users.page.ts` respectively.
## Including non-page files in the `pages` directory
@ -32,7 +32,7 @@ module.exports = {
Then rename your pages to have a file extension that includes `.page` (ex. rename `MyPage.tsx` to `MyPage.page.tsx`).
> **Note**: Make sure you also rename `_document.js`, `_app.js`, `_middleware.js`, as well as files under `pages/api/`.
> **Note**: Make sure you also rename `_document.js`, `_app.js`, `middleware.js`, as well as files under `pages/api/`.
Without this config, Next.js assumes every tsx/ts/jsx/js file in the `pages` directory is a page or API route, and may expose unintended routes vulnerable to denial of service attacks, or throw an error like the following when building the production bundle:

View file

@ -8,7 +8,7 @@ The `next/server` module provides several exports for server-only helpers, such
## NextMiddleware
Middleware is created by using a `middleware` function that lives inside a `_middleware` file. The Middleware API is based upon the native [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), [`FetchEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects.
Middleware is created by using a `middleware` function that lives inside a `middleware` file. The Middleware API is based upon the native [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), [`FetchEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects.
These native Web API objects are extended to give you more control over how you manipulate and configure a response, based on the incoming requests.

View file

@ -626,6 +626,10 @@
"title": "middleware-relative-urls",
"path": "/errors/middleware-relative-urls.md"
},
{
"title": "nested-middleware",
"path": "/errors/nested-middleware.md"
},
{
"title": "deleting-query-params-in-middlewares",
"path": "/errors/deleting-query-params-in-middlewares.md"

View file

@ -5,7 +5,7 @@
Your application is using a Middleware function that is using parameters from the deprecated API.
```typescript
// _middleware.js
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(event) {
@ -24,7 +24,7 @@ export function middleware(event) {
Update to use the new API for Middleware:
```typescript
// _middleware.js
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {

View file

@ -0,0 +1,26 @@
# Nested Middleware
#### Why This Error Occurred
You are defining a middleware file in a location different from `<root>/middleware` which is not allowed.
While in beta, a middleware file under specific pages implied that it would _only_ be executed when pages below its declaration were matched.
This execution model allowed the nesting of multiple middleware, which is hard to reason about and led to consequences such as dragging effects between different middleware executions.
The API has been removed in favor of a simpler model with a single root middleware.
#### Possible Ways to Fix It
To fix this error, declare your middleware in the root folder and use `NextRequest` parsed URL to define which path the middleware code should be executed for. For example, a middleware declared under `pages/about/_middleware.js` can be moved to `middleware`. A conditional can be used to ensure the middleware executes only when it matches the `about/*` path:
```typescript
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
// Execute pages/about/_middleware.js
}
}
```
If you have more than one middleware, you will need to combine them into a single file and model their execution depending on the request.

View file

@ -2,14 +2,14 @@
### Why This Error Occurred
`next/server` was imported outside of `pages/**/_middleware.{js,ts}`.
`next/server` was imported outside of `middleware.{js,ts}`.
### Possible Ways to Fix It
Only import and use `next/server` in a file located within the pages directory: `pages/**/_middleware.{js,ts}`.
Only import and use `next/server` in a file located within the project root directory: `middleware.{js,ts}`.
```ts
// pages/_middleware.ts
// middleware.ts
import type { NextFetchEvent, NextRequest } from 'next/server'

View file

@ -3,8 +3,7 @@ const path = require('path')
module.exports = {
meta: {
docs: {
description:
'Disallow importing next/server outside of pages/_middleware.js',
description: 'Disallow importing next/server outside of middleware.js',
recommended: true,
url: 'https://nextjs.org/docs/messages/no-server-import-in-page',
},
@ -16,20 +15,18 @@ module.exports = {
return
}
const paths = context.getFilename().split('pages')
const page = paths[paths.length - 1]
const filename = context.getFilename()
if (
!page ||
page.includes(`${path.sep}_middleware`) ||
page.includes(`${path.posix.sep}_middleware`)
filename.startsWith('middleware.') ||
filename.startsWith(`${path.sep}middleware.`) ||
filename.startsWith(`${path.posix.sep}middleware.`)
) {
return
}
context.report({
node,
message: `next/server should not be imported outside of pages/_middleware.js. See: https://nextjs.org/docs/messages/no-server-import-in-page`,
message: `next/server should not be imported outside of middleware.js. See: https://nextjs.org/docs/messages/no-server-import-in-page`,
})
},
}

View file

@ -13,7 +13,10 @@ import { stringify } from 'querystring'
import {
API_ROUTE,
DOT_NEXT_ALIAS,
MIDDLEWARE_FILE,
MIDDLEWARE_FILENAME,
PAGES_DIR_ALIAS,
ROOT_DIR_ALIAS,
VIEWS_DIR_ALIAS,
} from '../lib/constants'
import {
@ -23,7 +26,6 @@ import {
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
EDGE_RUNTIME_WEBPACK,
} from '../shared/lib/constants'
import { MIDDLEWARE_ROUTE } from '../lib/constants'
import { __ApiPreviewProps } from '../server/api-utils'
import { isTargetLikeServerless } from '../server/utils'
import { warn } from './output/log'
@ -57,18 +59,17 @@ export function getPageFromPath(pagePath: string, pageExtensions: string[]) {
export function createPagesMapping({
hasServerComponents,
isDev,
isViews,
pageExtensions,
pagePaths,
pagesType,
}: {
hasServerComponents: boolean
isDev: boolean
isViews?: boolean
pageExtensions: string[]
pagePaths: string[]
pagesType: 'pages' | 'root' | 'views'
}): { [page: string]: string } {
const previousPages: { [key: string]: string } = {}
const pathAlias = isViews ? VIEWS_DIR_ALIAS : PAGES_DIR_ALIAS
const pages = pagePaths.reduce<{ [key: string]: string }>(
(result, pagePath) => {
// Do not process .d.ts files inside the `pages` folder
@ -97,17 +98,22 @@ export function createPagesMapping({
previousPages[pageKey] = pagePath
}
result[pageKey] = normalizePathSep(join(pathAlias, pagePath))
result[pageKey] = normalizePathSep(
join(
pagesType === 'pages'
? PAGES_DIR_ALIAS
: pagesType === 'views'
? VIEWS_DIR_ALIAS
: ROOT_DIR_ALIAS,
pagePath
)
)
return result
},
{}
)
// In development we always alias these to allow Webpack to fallback to
// the correct source file so that HMR can work properly when a file is
// added or removed.
if (isViews) {
if (pagesType !== 'pages') {
return pages
}
@ -118,7 +124,11 @@ export function createPagesMapping({
delete pages['/_document']
}
// In development we always alias these to allow Webpack to fallback to
// the correct source file so that HMR can work properly when a file is
// added or removed.
const root = isDev ? PAGES_DIR_ALIAS : 'next/dist/pages'
return {
'/_app': `${root}/_app`,
'/_error': `${root}/_error`,
@ -259,6 +269,8 @@ interface CreateEntrypointsParams {
pages: { [page: string]: string }
pagesDir: string
previewMode: __ApiPreviewProps
rootDir: string
rootPaths?: Record<string, string>
target: 'server' | 'serverless' | 'experimental-serverless-trace'
viewsDir?: string
viewPaths?: Record<string, string>
@ -275,7 +287,7 @@ export function getEdgeServerEntry(opts: {
page: string
pages: { [page: string]: string }
}) {
if (opts.page.match(MIDDLEWARE_ROUTE)) {
if (opts.page === MIDDLEWARE_FILE) {
const loaderParams: MiddlewareLoaderOptions = {
absolutePagePath: opts.absolutePagePath,
page: opts.page,
@ -388,6 +400,8 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
pages,
pagesDir,
isDev,
rootDir,
rootPaths,
target,
viewsDir,
viewPaths,
@ -398,14 +412,16 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
const client: webpack5.EntryObject = {}
const getEntryHandler =
(mappings: Record<string, string>, isViews: boolean) =>
(mappings: Record<string, string>, pagesType: 'views' | 'pages' | 'root') =>
async (page: string) => {
const bundleFile = normalizePagePath(page)
const clientBundlePath = posix.join('pages', bundleFile)
const serverBundlePath = posix.join(
isViews ? 'views' : 'pages',
bundleFile
)
const serverBundlePath =
pagesType === 'pages'
? posix.join('pages', bundleFile)
: pagesType === 'views'
? posix.join('views', bundleFile)
: bundleFile.slice(1)
const absolutePagePath = mappings[page]
// Handle paths that have aliases
@ -418,9 +434,27 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
return absolutePagePath.replace(VIEWS_DIR_ALIAS, viewsDir)
}
if (absolutePagePath.startsWith(ROOT_DIR_ALIAS)) {
return absolutePagePath.replace(ROOT_DIR_ALIAS, rootDir)
}
return require.resolve(absolutePagePath)
})()
/**
* When we find a middleware file that is not in the ROOT_DIR we fail.
* There is no need to check on `dev` as this should only happen when
* building for production.
*/
if (
!absolutePagePath.startsWith(ROOT_DIR_ALIAS) &&
/[\\\\/]_middleware$/.test(page)
) {
throw new Error(
`nested Middleware is not allowed (found pages${page}) - https://nextjs.org/docs/messages/nested-middleware`
)
}
const isServerComponent = serverComponentRegex.test(absolutePagePath)
runDependingOnPageType({
@ -439,7 +473,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
}
},
onServer: () => {
if (isViews && viewsDir) {
if (pagesType === 'views' && viewsDir) {
server[serverBundlePath] = getViewsEntry({
name: serverBundlePath,
pagePath: mappings[page],
@ -477,10 +511,15 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
}
if (viewsDir && viewPaths) {
const entryHandler = getEntryHandler(viewPaths, true)
const entryHandler = getEntryHandler(viewPaths, 'views')
await Promise.all(Object.keys(viewPaths).map(entryHandler))
}
await Promise.all(Object.keys(pages).map(getEntryHandler(pages, false)))
if (rootPaths) {
await Promise.all(
Object.keys(rootPaths).map(getEntryHandler(rootPaths, 'root'))
)
}
await Promise.all(Object.keys(pages).map(getEntryHandler(pages, 'pages')))
return {
client,
@ -496,7 +535,7 @@ export function runDependingOnPageType<T>(params: {
page: string
pageRuntime: PageRuntime
}) {
if (params.page.match(MIDDLEWARE_ROUTE)) {
if (params.page === MIDDLEWARE_FILE) {
return [params.onEdgeServer()]
} else if (params.page.match(API_ROUTE)) {
return [params.onServer()]
@ -545,7 +584,7 @@ export function finalizeEntrypoint({
if (compilerType === 'edge-server') {
return {
layer: MIDDLEWARE_ROUTE.test(name) ? 'middleware' : undefined,
layer: name === MIDDLEWARE_FILENAME ? 'middleware' : undefined,
library: { name: ['_ENTRIES', `middleware_[name]`], type: 'assign' },
runtime: EDGE_RUNTIME_WEBPACK,
asyncChunks: false,

View file

@ -15,7 +15,8 @@ import formatWebpackMessages from '../client/dev/error-overlay/format-webpack-me
import {
STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR,
PUBLIC_DIR_MIDDLEWARE_CONFLICT,
MIDDLEWARE_ROUTE,
MIDDLEWARE_FILENAME,
MIDDLEWARE_FILE,
PAGES_DIR_ALIAS,
} from '../lib/constants'
import { fileExists } from '../lib/file-exists'
@ -54,11 +55,7 @@ import {
STATIC_STATUS_PAGES,
MIDDLEWARE_MANIFEST,
} from '../shared/lib/constants'
import {
getRouteRegex,
getSortedRoutes,
isDynamicRoute,
} from '../shared/lib/router/utils'
import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils'
import { __ApiPreviewProps } from '../server/api-utils'
import loadConfig from '../server/config'
import { isTargetLikeServerless } from '../server/utils'
@ -115,6 +112,8 @@ import { recursiveCopy } from '../lib/recursive-copy'
import { recursiveReadDir } from '../lib/recursive-readdir'
import { lockfilePatchPromise, teardownTraceSubscriber } from './swc'
import { injectedClientEntries } from './webpack/plugins/flight-manifest-plugin'
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
import { flatReaddir } from '../lib/flat-readdir'
export type SsgRoute = {
initialRevalidateSeconds: number | false
@ -326,6 +325,13 @@ export default async function build(
)
}
const rootPaths = await flatReaddir(
dir,
new RegExp(
`^${MIDDLEWARE_FILENAME}\\.(?:${config.pageExtensions.join('|')})$`
)
)
// needed for static exporting since we want to replace with HTML
// files
@ -345,11 +351,12 @@ export default async function build(
hasServerComponents,
isDev: false,
pageExtensions: config.pageExtensions,
pagePaths,
pagesType: 'pages',
pagePaths: pagePaths,
})
)
let mappedViewPaths: ReturnType<typeof createPagesMapping> | undefined
let mappedViewPaths: { [page: string]: string } | undefined
if (viewPaths && viewsDir) {
mappedViewPaths = nextBuildSpan
@ -359,12 +366,23 @@ export default async function build(
pagePaths: viewPaths!,
hasServerComponents,
isDev: false,
isViews: true,
pagesType: 'views',
pageExtensions: config.pageExtensions,
})
)
}
let mappedRootPaths: { [page: string]: string } = {}
if (rootPaths.length > 0) {
mappedRootPaths = createPagesMapping({
hasServerComponents,
isDev: false,
pageExtensions: config.pageExtensions,
pagePaths: rootPaths,
pagesType: 'root',
})
}
const entrypoints = await nextBuildSpan
.traceChild('create-entrypoints')
.traceAsyncFn(() =>
@ -377,6 +395,8 @@ export default async function build(
pagesDir,
previewMode: previewProps,
target,
rootDir: dir,
rootPaths: mappedRootPaths,
viewsDir,
viewPaths: mappedViewPaths,
pageExtensions: config.pageExtensions,
@ -389,7 +409,7 @@ export default async function build(
const hasCustomErrorPage =
mappedPages['/_error'].startsWith(PAGES_DIR_ALIAS)
if (pageKeys.some((page) => MIDDLEWARE_ROUTE.test(page))) {
if (mappedRootPaths?.[MIDDLEWARE_FILE]) {
Log.warn(
`using beta Middleware (not covered by semver) - https://nextjs.org/docs/messages/beta-middleware`
)
@ -538,17 +558,10 @@ export default async function build(
redirects: redirects.map((r: any) => buildCustomRoute(r, 'redirect')),
headers: headers.map((r: any) => buildCustomRoute(r, 'header')),
dynamicRoutes: getSortedRoutes(pageKeys)
.filter(
(page) => isDynamicRoute(page) && !page.match(MIDDLEWARE_ROUTE)
)
.filter(isDynamicRoute)
.map(pageToRoute),
staticRoutes: getSortedRoutes(pageKeys)
.filter(
(page) =>
!isDynamicRoute(page) &&
!page.match(MIDDLEWARE_ROUTE) &&
!isReservedPage(page)
)
.filter((page) => !isDynamicRoute(page) && !isReservedPage(page))
.map(pageToRoute),
dataRoutes: [],
i18n: config.i18n || undefined,
@ -1069,7 +1082,6 @@ export default async function build(
let isServerComponent = false
let isHybridAmp = false
let ssgPageRoutes: string[] | null = null
let isMiddlewareRoute = !!page.match(MIDDLEWARE_ROUTE)
const pagePath = pagePaths.find(
(p) =>
@ -1088,7 +1100,6 @@ export default async function build(
}
if (
!isMiddlewareRoute &&
!isReservedPage(page) &&
// We currently don't support static optimization in the Edge runtime.
pageRuntime !== 'edge'
@ -1483,7 +1494,9 @@ export default async function build(
let routeKeys: { [named: string]: string } | undefined
if (isDynamicRoute(page)) {
const routeRegex = getRouteRegex(dataRoute.replace(/\.json$/, ''))
const routeRegex = getNamedRouteRegex(
dataRoute.replace(/\.json$/, '')
)
dataRouteRegex = normalizeRouteRegex(
routeRegex.re.source.replace(/\(\?:\\\/\)\?\$$/, `\\.json$`)
@ -2091,9 +2104,7 @@ export default async function build(
rewritesWithHasCount: combinedRewrites.filter((r: any) => !!r.has)
.length,
redirectsWithHasCount: redirects.filter((r: any) => !!r.has).length,
middlewareCount: pageKeys.filter((page) =>
MIDDLEWARE_ROUTE.test(page)
).length,
middlewareCount: Object.keys(rootPaths).length > 0 ? 1 : 0,
})
)
@ -2114,7 +2125,9 @@ export default async function build(
)
finalDynamicRoutes[tbdRoute] = {
routeRegex: normalizeRouteRegex(getRouteRegex(tbdRoute).re.source),
routeRegex: normalizeRouteRegex(
getNamedRouteRegex(tbdRoute).re.source
),
dataRoute,
fallback: ssgBlockingFallbackPages.has(tbdRoute)
? null
@ -2122,10 +2135,9 @@ export default async function build(
? `${normalizedRoute}.html`
: false,
dataRouteRegex: normalizeRouteRegex(
getRouteRegex(dataRoute.replace(/\.json$/, '')).re.source.replace(
/\(\?:\\\/\)\?\$$/,
'\\.json$'
)
getNamedRouteRegex(
dataRoute.replace(/\.json$/, '')
).re.source.replace(/\(\?:\\\/\)\?\$$/, '\\.json$')
),
}
})
@ -2257,6 +2269,7 @@ export default async function build(
useStatic404,
pageExtensions: config.pageExtensions,
buildManifest,
middlewareManifest,
gzipSize: config.experimental.gzipSize,
}
)
@ -2334,7 +2347,7 @@ function isTelemetryPlugin(plugin: unknown): plugin is TelemetryPlugin {
}
function pageToRoute(page: string) {
const routeRegex = getRouteRegex(page)
const routeRegex = getNamedRouteRegex(page)
return {
page,
regex: normalizeRouteRegex(routeRegex.re.source),

View file

@ -23,11 +23,12 @@ import {
SSG_GET_INITIAL_PROPS_CONFLICT,
SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
SERVER_PROPS_SSG_CONFLICT,
MIDDLEWARE_ROUTE,
MIDDLEWARE_FILENAME,
} from '../lib/constants'
import { EDGE_RUNTIME_WEBPACK } from '../shared/lib/constants'
import prettyBytes from '../lib/pretty-bytes'
import { getRouteMatcher, getRouteRegex } from '../shared/lib/router/utils'
import { getRouteRegex } from '../shared/lib/router/utils/route-regex'
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters'
import { findPageFile } from '../server/lib/find-page-file'
@ -89,6 +90,7 @@ export async function printTreeView(
pagesDir,
pageExtensions,
buildManifest,
middlewareManifest,
useStatic404,
gzipSize = true,
}: {
@ -97,6 +99,7 @@ export async function printTreeView(
pagesDir: string
pageExtensions: string[]
buildManifest: BuildManifest
middlewareManifest: MiddlewareManifest
useStatic404: boolean
gzipSize?: boolean
}
@ -194,8 +197,6 @@ export async function printTreeView(
const symbol =
item === '/_app' || item === '/_app.server'
? ' '
: item.endsWith('/_middleware')
? 'ƒ'
: pageInfo?.static
? '○'
: pageInfo?.isSsg
@ -351,6 +352,18 @@ export async function printTreeView(
])
})
const middlewareInfo = middlewareManifest.middleware?.['/']
if (middlewareInfo?.files.length > 0) {
const sizes = await Promise.all(
middlewareInfo.files
.map((dep) => `${distPath}/${dep}`)
.map(gzipSize ? fsStatGzip : fsStat)
)
messages.push(['', '', ''])
messages.push(['ƒ Middleware', getPrettySize(sum(sizes)), ''])
}
console.log(
textTable(messages, {
align: ['l', 'l', 'r'],
@ -362,11 +375,6 @@ export async function printTreeView(
console.log(
textTable(
[
usedSymbols.has('ƒ') && [
'ƒ',
'(Middleware)',
`intercepts requests (uses ${chalk.cyan('_middleware')})`,
],
usedSymbols.has('ℇ') && [
'ℇ',
'(Streaming)',
@ -1182,12 +1190,9 @@ export async function copyTracedFiles(
)
}
for (const page of pageKeys) {
if (MIDDLEWARE_ROUTE.test(page)) {
const { files } =
middlewareManifest.middleware[page.replace(/\/_middleware$/, '') || '/']
for (const file of files) {
for (const middleware of Object.values(middlewareManifest.middleware) || []) {
if (middleware.name === MIDDLEWARE_FILENAME) {
for (const file of middleware.files) {
const originalPath = path.join(distDir, file)
const fileOutputPath = path.join(
outputPath,
@ -1197,9 +1202,10 @@ export async function copyTracedFiles(
await fs.mkdir(path.dirname(fileOutputPath), { recursive: true })
await fs.copyFile(originalPath, fileOutputPath)
}
continue
}
}
for (const page of pageKeys) {
const pageFile = path.join(
distDir,
'server',

View file

@ -10,6 +10,7 @@ import {
NEXT_PROJECT_ROOT,
NEXT_PROJECT_ROOT_DIST_CLIENT,
PAGES_DIR_ALIAS,
ROOT_DIR_ALIAS,
VIEWS_DIR_ALIAS,
} from '../lib/constants'
import { fileExists } from '../lib/file-exists'
@ -671,6 +672,7 @@ export default async function getBaseWebpackConfig(
[VIEWS_DIR_ALIAS]: viewsDir,
}
: {}),
[ROOT_DIR_ALIAS]: dir,
[DOT_NEXT_ALIAS]: distDir,
...(isClient || isEdgeServer ? getOptimizedAliases() : {}),
...getReactProfilingInProduction(),

View file

@ -1,5 +1,6 @@
import { getModuleBuildInfo } from './get-module-build-info'
import { stringifyRequest } from '../stringify-request'
import { MIDDLEWARE_FILE } from '../../../lib/constants'
export type MiddlewareLoaderOptions = {
absolutePagePath: string
@ -11,7 +12,7 @@ export default function middlewareLoader(this: any) {
const stringifiedPagePath = stringifyRequest(this, absolutePagePath)
const buildInfo = getModuleBuildInfo(this._module)
buildInfo.nextEdgeMiddleware = {
page: page.replace(/\/_middleware$/, '') || '/',
page: page.replace(new RegExp(`${MIDDLEWARE_FILE}$`), '') || '/',
}
return `

View file

@ -1,6 +1,7 @@
import type { IncomingMessage, ServerResponse } from 'http'
import type { Rewrite } from '../../../../lib/load-custom-routes'
import type { BuildManifest } from '../../../../server/get-page-files'
import type { RouteMatch } from '../../../../shared/lib/router/utils/route-matcher'
import type { NextConfig } from '../../../../server/config'
import type {
GetServerSideProps,
@ -13,7 +14,7 @@ import { format as formatUrl, UrlWithParsedQuery, parse as parseUrl } from 'url'
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
import { normalizeLocalePath } from '../../../../shared/lib/i18n/normalize-locale-path'
import { getPathMatch } from '../../../../shared/lib/router/utils/path-match'
import { getRouteRegex } from '../../../../shared/lib/router/utils/route-regex'
import { getNamedRouteRegex } from '../../../../shared/lib/router/utils/route-regex'
import { getRouteMatcher } from '../../../../shared/lib/router/utils/route-matcher'
import {
matchHas,
@ -79,12 +80,12 @@ export function getUtils({
rewrites: ServerlessHandlerCtx['rewrites']
pageIsDynamic: ServerlessHandlerCtx['pageIsDynamic']
}) {
let defaultRouteRegex: ReturnType<typeof getRouteRegex> | undefined
let dynamicRouteMatcher: ReturnType<typeof getRouteMatcher> | undefined
let defaultRouteRegex: ReturnType<typeof getNamedRouteRegex> | undefined
let dynamicRouteMatcher: RouteMatch | undefined
let defaultRouteMatches: ParsedUrlQuery | undefined
if (pageIsDynamic) {
defaultRouteRegex = getRouteRegex(page)
defaultRouteRegex = getNamedRouteRegex(page)
dynamicRouteMatcher = getRouteMatcher(defaultRouteRegex)
defaultRouteMatches = dynamicRouteMatcher(page) as ParsedUrlQuery
}

View file

@ -1,6 +1,6 @@
import type { EdgeMiddlewareMeta } from '../loaders/get-module-build-info'
import type { EdgeSSRMeta, WasmBinding } from '../loaders/get-module-build-info'
import { getMiddlewareRegex } from '../../../shared/lib/router/utils'
import { getNamedMiddlewareRegex } from '../../../shared/lib/router/utils/route-regex'
import { getModuleBuildInfo } from '../loaders/get-module-build-info'
import { getSortedRoutes } from '../../../shared/lib/router/utils'
import { webpack, sources, webpack5 } from 'next/dist/compiled/webpack/webpack'
@ -358,12 +358,16 @@ function getCreateAssets(params: {
continue
}
const { namedRegex } = getNamedMiddlewareRegex(page, {
catchAll: !metadata.edgeSSR,
})
middlewareManifest.middleware[page] = {
env: Array.from(metadata.env),
files: getEntryFiles(entrypoint.getFiles(), metadata),
name: entrypoint.name,
page: page,
regexp: getMiddlewareRegex(page, !metadata.edgeSSR).namedRegex!,
regexp: namedRegex,
wasm: Array.from(metadata.wasmBindings),
}
}

View file

@ -1,4 +1,5 @@
import { webpack } from 'next/dist/compiled/webpack/webpack'
import { MIDDLEWARE_FILENAME } from '../../../lib/constants'
import type { webpack5 } from 'next/dist/compiled/webpack/webpack'
/**
@ -9,12 +10,7 @@ export const getMiddlewareSourceMapPlugins = () => {
return [
new webpack.SourceMapDevToolPlugin({
filename: '[file].map',
include: [
// Middlewares are the only ones who have `server/pages/[name]` as their filename
/^pages\//,
// All middleware chunks
/^edge-chunks\//,
],
include: [new RegExp(`^${MIDDLEWARE_FILENAME}.`), /^edge-chunks\//],
}),
new MiddlewareSourceMapsPlugin(),
]

View file

@ -101,7 +101,7 @@ export class TerserPlugin {
// and doesn't provide too much of a benefit as it's server-side
if (
name.match(
/(edge-runtime-webpack\.js|edge-chunks|_middleware\.js$)/
/(edge-runtime-webpack\.js|edge-chunks|middleware\.js$)/
)
) {
return false

View file

@ -19,12 +19,15 @@ export const NEXT_PROJECT_ROOT_DIST_SERVER = join(
export const API_ROUTE = /^\/api(?:\/|$)/
// Regex for middleware
export const MIDDLEWARE_ROUTE = /_middleware$/
export const MIDDLEWARE_ROUTE = /middleware$/
export const MIDDLEWARE_FILENAME = 'middleware'
export const MIDDLEWARE_FILE = `/${MIDDLEWARE_FILENAME}`
// Because on Windows absolute paths in the generated code can break because of numbers, eg 1 in the path,
// we have to use a private alias
export const PAGES_DIR_ALIAS = 'private-next-pages'
export const DOT_NEXT_ALIAS = 'private-dot-next'
export const ROOT_DIR_ALIAS = 'private-next-root-dir'
export const VIEWS_DIR_ALIAS = 'private-next-views-dir'
export const PUBLIC_DIR_MIDDLEWARE_CONFLICT = `You can not have a '_next' folder inside of your public folder. This conflicts with the internal '/_next' route. https://nextjs.org/docs/messages/public-next-folder-conflict`

View file

@ -0,0 +1,26 @@
import { join } from 'path'
import { nonNullable } from './non-nullable'
import { promises } from 'fs'
export async function flatReaddir(dir: string, include: RegExp) {
const dirents = await promises.readdir(dir, { withFileTypes: true })
const result = await Promise.all(
dirents.map(async (part) => {
const absolutePath = join(dir, part.name)
if (part.isSymbolicLink()) {
const stats = await promises.stat(absolutePath)
if (stats.isDirectory()) {
return null
}
}
if (part.isDirectory() || !include.test(part.name)) {
return null
}
return absolutePath.replace(dir, '')
})
)
return result.filter(nonNullable)
}

View file

@ -1,14 +1,11 @@
import type { NextConfig } from '../server/config'
import type { Token } from 'next/dist/compiled/path-to-regexp'
import chalk from './chalk'
import { parse as parseUrl } from 'url'
import * as pathToRegexp from 'next/dist/compiled/path-to-regexp'
import { escapeStringRegexp } from '../shared/lib/escape-regexp'
import {
PERMANENT_REDIRECT_STATUS,
TEMPORARY_REDIRECT_STATUS,
} from '../shared/lib/constants'
import isError from './is-error'
import { PERMANENT_REDIRECT_STATUS } from '../shared/lib/constants'
import { TEMPORARY_REDIRECT_STATUS } from '../shared/lib/constants'
import { tryToParsePath } from './try-to-parse-path'
export type RouteHas =
| {
@ -133,54 +130,6 @@ function checkHeader(route: Header): string[] {
return invalidParts
}
type ParseAttemptResult = {
error?: boolean
tokens?: pathToRegexp.Token[]
regexStr?: string
}
function tryParsePath(route: string, handleUrl?: boolean): ParseAttemptResult {
const result: ParseAttemptResult = {}
let routePath = route
try {
if (handleUrl) {
const parsedDestination = parseUrl(route, true)
routePath = `${parsedDestination.pathname!}${
parsedDestination.hash || ''
}`
}
// Make sure we can parse the source properly
result.tokens = pathToRegexp.parse(routePath)
const regex = pathToRegexp.tokensToRegexp(result.tokens)
result.regexStr = regex.source
} catch (err) {
// If there is an error show our error link but still show original error or a formatted one if we can
let errMatches
if (isError(err) && (errMatches = err.message.match(/at (\d{0,})/))) {
const position = parseInt(errMatches[1], 10)
console.error(
`\nError parsing \`${route}\` ` +
`https://nextjs.org/docs/messages/invalid-route-source\n` +
`Reason: ${err.message}\n\n` +
` ${routePath}\n` +
` ${new Array(position).fill(' ').join('')}^\n`
)
} else {
console.error(
`\nError parsing ${route} https://nextjs.org/docs/messages/invalid-route-source`,
err
)
}
result.error = true
}
return result
}
export type RouteType = 'rewrite' | 'redirect' | 'header'
function checkCustomRoutes(
@ -331,12 +280,12 @@ function checkCustomRoutes(
invalidParts.push(...result.invalidParts)
}
let sourceTokens: pathToRegexp.Token[] | undefined
let sourceTokens: Token[] | undefined
if (typeof route.source === 'string' && route.source.startsWith('/')) {
// only show parse error if we didn't already show error
// for not being a string
const { tokens, error, regexStr } = tryParsePath(route.source)
const { tokens, error, regexStr } = tryToParsePath(route.source)
if (error) {
invalidParts.push('`source` parse failed')
@ -399,7 +348,9 @@ function checkCustomRoutes(
tokens: destTokens,
regexStr: destRegexStr,
error: destinationParseFailed,
} = tryParsePath((route as Rewrite).destination, true)
} = tryToParsePath((route as Rewrite).destination, {
handleUrl: true,
})
if (destRegexStr && destRegexStr.length > 4096) {
invalidParts.push('`destination` exceeds max built length of 4096')

View file

@ -0,0 +1,65 @@
import type { Token } from 'next/dist/compiled/path-to-regexp'
import { parse, tokensToRegexp } from 'next/dist/compiled/path-to-regexp'
import { parse as parseURL } from 'url'
import isError from './is-error'
interface ParseResult {
error?: any
parsedPath: string
regexStr?: string
route: string
tokens?: Token[]
}
/**
* Attempts to parse a given route with `path-to-regexp` and returns an object
* with the result. Whenever an error happens on parse, it will print an error
* attempting to find the error position and showing a link to the docs. When
* `handleUrl` is set to `true` it will also attempt to parse the route
* and use the resulting pathname to parse with `path-to-regexp`.
*/
export function tryToParsePath(
route: string,
options?: {
handleUrl?: boolean
}
): ParseResult {
const result: ParseResult = { route, parsedPath: route }
try {
if (options?.handleUrl) {
const parsed = parseURL(route, true)
result.parsedPath = `${parsed.pathname!}${parsed.hash || ''}`
}
result.tokens = parse(result.parsedPath)
result.regexStr = tokensToRegexp(result.tokens).source
} catch (err) {
reportError(result, err)
result.error = err
}
return result
}
/**
* If there is an error show our error link but still show original error or
* a formatted one if we can
*/
function reportError({ route, parsedPath }: ParseResult, err: any) {
let errMatches
if (isError(err) && (errMatches = err.message.match(/at (\d{0,})/))) {
const position = parseInt(errMatches[1], 10)
console.error(
`\nError parsing \`${route}\` ` +
`https://nextjs.org/docs/messages/invalid-route-source\n` +
`Reason: ${err.message}\n\n` +
` ${parsedPath}\n` +
` ${new Array(position).fill(' ').join('')}^\n`
)
} else {
console.error(
`\nError parsing ${route} https://nextjs.org/docs/messages/invalid-route-source`,
err
)
}
}

View file

@ -1,10 +1,11 @@
import { __ApiPreviewProps } from './api-utils'
import type { CustomRoutes } from '../lib/load-custom-routes'
import type { DomainLocale } from './config'
import type { DynamicRoutes, PageChecker, Params, Route } from './router'
import type { DynamicRoutes, PageChecker, Route } from './router'
import type { FontManifest } from './font-utils'
import type { LoadComponentsReturnType } from './load-components'
import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
import type { RouteMatch } from '../shared/lib/router/utils/route-matcher'
import type { Params } from '../shared/lib/router/utils/route-matcher'
import type { NextConfig, NextConfigComplete } from './config-shared'
import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta'
import type { ParsedUrlQuery } from 'querystring'
@ -33,12 +34,7 @@ import {
STATIC_STATUS_PAGES,
TEMPORARY_REDIRECT_STATUS,
} from '../shared/lib/constants'
import {
getRouteMatcher,
getRouteRegex,
getSortedRoutes,
isDynamicRoute,
} from '../shared/lib/router/utils'
import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils'
import {
setLazyProp,
getCookieParser,
@ -64,22 +60,23 @@ import { getUtils } from '../build/webpack/loaders/next-serverless-loader/utils'
import ResponseCache from './response-cache'
import { parseNextUrl } from '../shared/lib/router/utils/parse-next-url'
import isError, { getProperError } from '../lib/is-error'
import { MIDDLEWARE_ROUTE } from '../lib/constants'
import { addRequestMeta, getRequestMeta } from './request-meta'
import { createHeaderRoute, createRedirectRoute } from './server-route-utils'
import { PrerenderManifest } from '../build'
import { ImageConfigComplete } from '../shared/lib/image-config'
import { replaceBasePath } from './router-utils'
import { normalizeViewPath } from '../shared/lib/router/utils/view-paths'
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
import { getRouteRegex } from '../shared/lib/router/utils/route-regex'
export type FindComponentsResult = {
components: LoadComponentsReturnType
query: NextParsedUrlQuery
}
interface RoutingItem {
export interface RoutingItem {
page: string
match: ReturnType<typeof getRouteMatcher>
match: RouteMatch
ssr?: boolean
}
@ -184,8 +181,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
protected dynamicRoutes?: DynamicRoutes
protected viewPathRoutes?: Record<string, string>
protected customRoutes: CustomRoutes
protected middlewareManifest?: MiddlewareManifest
protected middleware?: RoutingItem[]
protected serverComponentManifest?: any
public readonly hostname?: string
public readonly port?: number
@ -210,26 +205,13 @@ export default abstract class Server<ServerOptions extends Options = Options> {
fallback: Route[]
}
protected abstract getFilesystemPaths(): Set<string>
protected abstract getMiddleware(): {
match: (pathname: string | null | undefined) =>
| false
| {
[paramName: string]: string | string[]
}
page: string
}[]
protected abstract findPageComponents(
pathname: string,
query?: NextParsedUrlQuery,
params?: Params | null
): Promise<FindComponentsResult | null>
protected abstract hasMiddleware(
pathname: string,
_isSSR?: boolean
): Promise<boolean>
protected abstract getPagePath(pathname: string, locales?: string[]): string
protected abstract getFontManifest(): FontManifest | undefined
protected abstract getMiddlewareManifest(): MiddlewareManifest | undefined
protected abstract getRoutesManifest(): CustomRoutes
protected abstract getPrerenderManifest(): PrerenderManifest
protected abstract getServerComponentManifest(): any
@ -357,7 +339,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
this.pagesManifest = this.getPagesManifest()
this.viewPathsManifest = this.getViewPathsManifest()
this.middlewareManifest = this.getMiddlewareManifest()
this.customRoutes = this.getCustomRoutes()
this.router = new Router(this.generateRoutes())
@ -677,8 +658,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
return this.getPrerenderManifest().preview
}
protected async ensureMiddleware(_pathname: string, _isSSR?: boolean) {}
protected generateRoutes(): {
basePath: string
headers: Route[]
@ -840,13 +819,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
}
const bubbleNoFallback = !!query._nextBubbleNoFallback
if (pathname.match(MIDDLEWARE_ROUTE)) {
await this.render404(req, res, parsedUrl)
return {
finished: true,
}
}
if (pathname === '/api' || pathname.startsWith('/api/')) {
delete query._nextBubbleNoFallback
@ -878,9 +850,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
if (useFileSystemPublicRoutes) {
this.viewPathRoutes = this.getViewPathRoutes()
this.dynamicRoutes = this.getDynamicRoutes()
if (!this.minimalMode) {
this.middleware = this.getMiddleware()
}
}
return {

View file

@ -18,7 +18,7 @@ import { watchCompilers } from '../../build/output'
import getBaseWebpackConfig from '../../build/webpack-config'
import {
API_ROUTE,
MIDDLEWARE_ROUTE,
MIDDLEWARE_FILENAME,
VIEWS_DIR_ALIAS,
} from '../../lib/constants'
import { recursiveDelete } from '../../lib/recursive-delete'
@ -405,6 +405,7 @@ export default class HotReloader {
hasServerComponents: this.hasServerComponents,
isDev: true,
pageExtensions: this.config.pageExtensions,
pagesType: 'pages',
pagePaths: pagePaths.filter(
(i): i is string => typeof i === 'string'
),
@ -422,6 +423,7 @@ export default class HotReloader {
pages: this.pagesMapping,
pagesDir: this.pagesDir,
previewMode: this.previewProps,
rootDir: this.dir,
target: 'server',
pageExtensions: this.config.pageExtensions,
})
@ -490,6 +492,7 @@ export default class HotReloader {
},
pagesDir: this.pagesDir,
previewMode: this.previewProps,
rootDir: this.dir,
target: 'server',
pageExtensions: this.config.pageExtensions,
})
@ -674,7 +677,7 @@ export default class HotReloader {
(stats: webpack5.Compilation) => {
try {
stats.entrypoints.forEach((entry, key) => {
if (key.startsWith('pages/')) {
if (key.startsWith('pages/') || key === MIDDLEWARE_FILENAME) {
// TODO this doesn't handle on demand loaded chunks
entry.chunks.forEach((chunk) => {
if (chunk.id === key) {
@ -793,7 +796,7 @@ export default class HotReloader {
changedClientPages
)
const middlewareChanges = Array.from(changedEdgeServerPages).filter(
(name) => name.match(MIDDLEWARE_ROUTE)
(name) => name === MIDDLEWARE_FILENAME
)
changedClientPages.clear()
changedServerPages.clear()
@ -884,6 +887,7 @@ export default class HotReloader {
watcher: this.watcher,
pagesDir: this.pagesDir,
viewsDir: this.viewsDir,
rootDir: this.dir,
nextConfig: this.config,
...(this.config.onDemandEntries as {
maxInactiveAge: number

View file

@ -4,12 +4,13 @@ import type { FetchEventResult } from '../web/types'
import type { FindComponentsResult } from '../next-server'
import type { LoadComponentsReturnType } from '../load-components'
import type { Options as ServerOptions } from '../next-server'
import type { Params } from '../router'
import type { Params } from '../../shared/lib/router/utils/route-matcher'
import type { ParsedNextUrl } from '../../shared/lib/router/utils/parse-next-url'
import type { ParsedUrlQuery } from 'querystring'
import type { Server as HTTPServer } from 'http'
import type { UrlWithParsedQuery } from 'url'
import type { BaseNextRequest, BaseNextResponse } from '../base-http'
import type { RoutingItem } from '../base-server'
import crypto from 'crypto'
import fs from 'fs'
@ -21,6 +22,7 @@ import { join as pathJoin, relative, resolve as pathResolve, sep } from 'path'
import React from 'react'
import Watchpack from 'next/dist/compiled/watchpack'
import { ampValidation } from '../../build/output'
import { MIDDLEWARE_FILENAME } from '../../lib/constants'
import { PUBLIC_DIR_MIDDLEWARE_CONFLICT } from '../../lib/constants'
import { fileExists } from '../../lib/file-exists'
import { findPagesDir } from '../../lib/find-pages-dir'
@ -33,13 +35,8 @@ import {
DEV_CLIENT_PAGES_MANIFEST,
DEV_MIDDLEWARE_MANIFEST,
} from '../../shared/lib/constants'
import {
getRouteMatcher,
getRouteRegex,
getSortedRoutes,
isDynamicRoute,
} from '../../shared/lib/router/utils'
import Server, { WrappedBuildError } from '../next-server'
import { getRouteMatcher } from '../../shared/lib/router/utils/route-matcher'
import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path'
import { absolutePathToPage } from '../../shared/lib/page-path/absolute-path-to-page'
import Router from '../router'
@ -61,8 +58,12 @@ import {
} from 'next/dist/compiled/@next/react-dev-overlay/middleware'
import * as Log from '../../build/output/log'
import isError, { getProperError } from '../../lib/is-error'
import { getMiddlewareRegex } from '../../shared/lib/router/utils/get-middleware-regex'
import { isCustomErrorPage, isReservedPage } from '../../build/utils'
import {
getMiddlewareRegex,
getRouteRegex,
} from '../../shared/lib/router/utils/route-regex'
import { getSortedRoutes, isDynamicRoute } from '../../shared/lib/router/utils'
import { runDependingOnPageType } from '../../build/entries'
import { NodeNextResponse, NodeNextRequest } from '../base-http/node'
import {
getPageStaticInfo,
@ -70,6 +71,7 @@ import {
} from '../../build/entries'
import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep'
import { normalizeViewPath } from '../../shared/lib/router/utils/view-paths'
import { MIDDLEWARE_FILE } from '../../lib/constants'
// Load ReactDevOverlay only when needed
let ReactDevOverlayImpl: React.FunctionComponent
@ -103,6 +105,13 @@ export default class DevServer extends Server {
private pagesDir: string
private viewsDir?: string
/**
* Since the dev server is stateful and middleware routes can be added and
* removed over time, we need to keep a list of all of the middleware
* routing items to be returned in `getMiddleware()`
*/
private middleware?: RoutingItem[]
protected staticPathsWorker?: { [key: string]: any } & {
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
}
@ -245,10 +254,6 @@ export default class DevServer extends Server {
return
}
const regexMiddleware = new RegExp(
`[\\\\/](_middleware.(?:${this.nextConfig.pageExtensions.join('|')}))$`
)
const regexPageExtension = new RegExp(
`\\.+(?:${this.nextConfig.pageExtensions.join('|')})$`
)
@ -268,48 +273,53 @@ export default class DevServer extends Server {
})
let wp = (this.webpackWatcher = new Watchpack())
const toWatch = [this.pagesDir!]
const pages = [this.pagesDir]
const views = this.viewsDir ? [this.viewsDir] : []
const directories = [...pages, ...views]
const files = this.nextConfig.pageExtensions.map((extension) =>
pathJoin(this.dir, `${MIDDLEWARE_FILENAME}.${extension}`)
)
if (this.viewsDir) {
toWatch.push(this.viewsDir)
}
wp.watch([], toWatch, 0)
wp.watch(files, directories, 0)
wp.on('aggregated', async () => {
const routedMiddleware = []
const routedMiddleware: string[] = []
const routedPages: string[] = []
const knownFiles = wp.getTimeInfoEntries()
const viewPaths: Record<string, string> = {}
const ssrMiddleware = new Set<string>()
for (const [fileName, { accuracy, safeTime }] of knownFiles) {
if (accuracy === undefined || !regexPageExtension.test(fileName)) {
for (const [fileName, meta] of knownFiles) {
if (
meta?.accuracy === undefined ||
!regexPageExtension.test(fileName)
) {
continue
}
let pageName: string = ''
let isViewPath = false
if (
const isViewPath = Boolean(
this.viewsDir &&
normalizePathSep(fileName).startsWith(
normalizePathSep(this.viewsDir)
)
) {
isViewPath = true
pageName = absolutePathToPage(
this.viewsDir,
fileName,
this.nextConfig.pageExtensions,
false
)
} else {
pageName = absolutePathToPage(
this.pagesDir,
fileName,
this.nextConfig.pageExtensions
)
normalizePathSep(fileName).startsWith(
normalizePathSep(this.viewsDir)
)
)
const rootFile = absolutePathToPage(fileName, {
pagesDir: this.dir,
extensions: this.nextConfig.pageExtensions,
})
if (rootFile === MIDDLEWARE_FILE) {
routedMiddleware.push(`/`)
continue
}
let pageName = absolutePathToPage(fileName, {
pagesDir: isViewPath ? this.viewsDir! : this.pagesDir,
extensions: this.nextConfig.pageExtensions,
keepIndex: isViewPath,
})
if (isViewPath) {
// TODO: should only routes ending in /index.js be route-able?
const originalPageName = pageName
@ -324,36 +334,36 @@ export default class DevServer extends Server {
pageName = pageName.replace(/\/index$/, '') || '/'
}
if (regexMiddleware.test(fileName)) {
routedMiddleware.push(
`/${relative(this.pagesDir, fileName).replace(/\\+/g, '/')}`
.replace(/^\/+/g, '/')
.replace(regexMiddleware, '/')
/**
* If there is a middleware that is not declared in the root we will
* warn without adding it so it doesn't make its way into the system.
*/
if (/[\\\\/]_middleware$/.test(pageName)) {
Log.error(
`nested Middleware is not allowed (found pages${pageName}) - https://nextjs.org/docs/messages/nested-middleware`
)
continue
}
invalidatePageRuntimeCache(fileName, safeTime)
const pageRuntimeConfig = (
await getPageStaticInfo(fileName, this.nextConfig)
).runtime
const isEdgeRuntime = pageRuntimeConfig === 'edge'
if (
isEdgeRuntime &&
!(isReservedPage(pageName) || isCustomErrorPage(pageName))
) {
routedMiddleware.push(pageName)
ssrMiddleware.add(pageName)
}
invalidatePageRuntimeCache(fileName, meta.safeTime)
runDependingOnPageType({
page: pageName,
pageRuntime: (await getPageStaticInfo(fileName, this.nextConfig))
.runtime,
onClient: () => {},
onServer: () => {},
onEdgeServer: () => {
routedMiddleware.push(pageName)
ssrMiddleware.add(pageName)
},
})
routedPages.push(pageName)
}
this.viewPathRoutes = viewPaths
this.middleware = getSortedRoutes(routedMiddleware).map((page) => ({
match: getRouteMatcher(
getMiddlewareRegex(page, !ssrMiddleware.has(page))
getMiddlewareRegex(page, { catchAll: !ssrMiddleware.has(page) })
),
page,
ssr: ssrMiddleware.has(page),
@ -497,6 +507,14 @@ export default class DevServer extends Server {
return false
}
if (normalizedPath === MIDDLEWARE_FILE) {
return findPageFile(
this.dir,
normalizedPath,
this.nextConfig.pageExtensions
).then(Boolean)
}
// check viewsDir first if enabled
if (this.viewsDir) {
const pageFile = await findPageFile(
@ -792,12 +810,8 @@ export default class DevServer extends Server {
return undefined
}
protected getMiddleware(): never[] {
return []
}
protected getMiddlewareManifest(): undefined {
return undefined
protected getMiddleware() {
return this.middleware ?? []
}
protected getServerComponentManifest() {
@ -808,13 +822,11 @@ export default class DevServer extends Server {
pathname: string,
isSSR?: boolean
): Promise<boolean> {
return this.hasPage(isSSR ? pathname : getMiddlewareFilepath(pathname))
return this.hasPage(isSSR ? pathname : MIDDLEWARE_FILE)
}
protected async ensureMiddleware(pathname: string, isSSR?: boolean) {
return this.hotReloader!.ensurePage(
isSSR ? pathname : getMiddlewareFilepath(pathname)
)
return this.hotReloader!.ensurePage(isSSR ? pathname : MIDDLEWARE_FILE)
}
generateRoutes() {
@ -869,10 +881,10 @@ export default class DevServer extends Server {
res
.body(
JSON.stringify(
this.middleware?.map((middleware) => [
this.getMiddleware().map((middleware) => [
middleware.page,
!!middleware.ssr,
]) || []
])
)
)
.send()
@ -1102,9 +1114,3 @@ export default class DevServer extends Server {
return false
}
}
function getMiddlewareFilepath(pathname: string) {
return pathname.endsWith('/')
? `${pathname}_middleware`
: `${pathname}/_middleware`
}

View file

@ -13,6 +13,7 @@ import { pageNotFoundError } from '../require'
import { reportTrigger } from '../../build/output'
import getRouteFromEntrypoint from '../get-route-from-entrypoint'
import { serverComponentRegex } from '../../build/webpack/loaders/utils'
import { MIDDLEWARE_FILE, MIDDLEWARE_FILENAME } from '../../lib/constants'
export const ADDED = Symbol('added')
export const BUILDING = Symbol('building')
@ -62,6 +63,7 @@ export function onDemandEntryHandler({
nextConfig,
pagesBufferLength,
pagesDir,
rootDir,
viewsDir,
watcher,
}: {
@ -70,6 +72,7 @@ export function onDemandEntryHandler({
nextConfig: NextConfigComplete
pagesBufferLength: number
pagesDir: string
rootDir: string
viewsDir?: string
watcher: any
}) {
@ -96,6 +99,8 @@ export function onDemandEntryHandler({
pagePaths.push(`${type}${page}`)
} else if (root && entrypoint.name === 'root') {
pagePaths.push(`${type}/${entrypoint.name}`)
} else if (entrypoint.name === MIDDLEWARE_FILENAME) {
pagePaths.push(`${type}/${entrypoint.name}`)
}
}
@ -185,6 +190,7 @@ export function onDemandEntryHandler({
return {
async ensurePage(page: string, clientOnly: boolean) {
const pagePathData = await findPagePathData(
rootDir,
pagesDir,
page,
nextConfig.pageExtensions,
@ -346,11 +352,13 @@ class Invalidator {
* a page and allowed extensions. If the page can't be found it will throw an
* error. It defaults the `/_error` page to Next.js internal error page.
*
* @param rootDir Absolute path to the project root.
* @param pagesDir Absolute path to the pages folder with trailing `/pages`.
* @param normalizedPagePath The page normalized (it will be denormalized).
* @param pageExtensions Array of page extensions.
*/
async function findPagePathData(
rootDir: string,
pagesDir: string,
page: string,
extensions: string[],
@ -358,14 +366,43 @@ async function findPagePathData(
) {
const normalizedPagePath = tryToNormalizePagePath(page)
let pagePath: string | null = null
let isView = false
// check viewsDir first
if (normalizedPagePath === MIDDLEWARE_FILE) {
pagePath = await findPageFile(rootDir, normalizedPagePath, extensions)
if (!pagePath) {
throw pageNotFoundError(normalizedPagePath)
}
const pageUrl = ensureLeadingSlash(
removePagePathTail(normalizePathSep(pagePath), {
extensions,
})
)
return {
absolutePagePath: join(rootDir, pagePath),
bundlePath: normalizedPagePath.slice(1),
page: posix.normalize(pageUrl),
}
}
// Check viewsDir first falling back to pagesDir
if (viewsDir) {
pagePath = await findPageFile(viewsDir, normalizedPagePath, extensions)
if (pagePath) {
isView = true
const pageUrl = ensureLeadingSlash(
removePagePathTail(normalizePathSep(pagePath), {
keepIndex: true,
extensions,
})
)
return {
absolutePagePath: join(viewsDir, pagePath),
bundlePath: posix.join('views', normalizePagePath(pageUrl)),
page: posix.normalize(pageUrl),
}
}
}
@ -375,15 +412,14 @@ async function findPagePathData(
if (pagePath !== null) {
const pageUrl = ensureLeadingSlash(
removePagePathTail(normalizePathSep(pagePath), extensions, !isView)
removePagePathTail(normalizePathSep(pagePath), {
extensions,
})
)
const bundleFile = normalizePagePath(pageUrl)
const bundlePath = posix.join(isView ? 'views' : 'pages', bundleFile)
const absolutePagePath = join(isView ? viewsDir! : pagesDir, pagePath)
return {
absolutePagePath,
bundlePath,
absolutePagePath: join(pagesDir, pagePath),
bundlePath: posix.join('pages', normalizePagePath(pageUrl)),
page: posix.normalize(pageUrl),
}
}

View file

@ -1,7 +1,7 @@
import './node-polyfill-fetch'
import './node-polyfill-web-streams'
import type { Params, Route } from './router'
import type { Route } from './router'
import type { CacheFs } from '../shared/lib/utils'
import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
import type RenderResult from './render-result'
@ -13,6 +13,7 @@ import type { BaseNextRequest, BaseNextResponse } from './base-http'
import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
import type { PayloadOptions } from './send-payload'
import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta'
import type { Params } from '../shared/lib/router/utils/route-matcher'
import fs from 'fs'
import { join, relative, resolve, sep } from 'path'
@ -55,8 +56,10 @@ import BaseServer, {
FindComponentsResult,
prepareServerlessUrl,
stringifyQuery,
RoutingItem,
} from './base-server'
import { getMiddlewareInfo, getPagePath, requireFontManifest } from './require'
import { getPagePath, requireFontManifest, pageNotFoundError } from './require'
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { loadComponents } from './load-components'
import isError, { getProperError } from '../lib/is-error'
@ -66,14 +69,15 @@ import { relativizeURL } from '../shared/lib/router/utils/relativize-url'
import { parseNextUrl } from '../shared/lib/router/utils/parse-next-url'
import { prepareDestination } from '../shared/lib/router/utils/prepare-destination'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { getMiddlewareRegex, getRouteMatcher } from '../shared/lib/router/utils'
import { MIDDLEWARE_ROUTE } from '../lib/constants'
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
import { MIDDLEWARE_FILENAME } from '../lib/constants'
import { loadEnvConfig } from '@next/env'
import { getCustomRoute } from './server-route-utils'
import { urlQueryToSearchParams } from '../shared/lib/router/utils/querystring'
import ResponseCache from '../server/response-cache'
import { removePathTrailingSlash } from '../client/normalize-trailing-slash'
import { clonableBodyForRequest } from './body-streams'
import { getMiddlewareRegex } from '../shared/lib/router/utils/route-regex'
export * from './base-server'
@ -91,6 +95,12 @@ export interface NodeRequestHandler {
): Promise<void>
}
const middlewareBetaWarning = execOnce(() => {
Log.warn(
`using beta Middleware (not covered by semver) - https://nextjs.org/docs/messages/beta-middleware`
)
})
export default class NextNodeServer extends BaseServer {
private imageResponseCache?: ResponseCache
@ -830,24 +840,6 @@ export default class NextNodeServer extends BaseServer {
)
}
protected async hasMiddleware(
pathname: string,
_isSSR?: boolean
): Promise<boolean> {
try {
return (
getMiddlewareInfo({
dev: this.renderOpts.dev,
distDir: this.distDir,
page: pathname,
serverless: this._isLikeServerless,
}).paths.length > 0
)
} catch (_) {}
return false
}
public async serveStatic(
req: BaseNextRequest | IncomingMessage,
res: BaseNextResponse | ServerResponse,
@ -946,21 +938,6 @@ export default class NextNodeServer extends BaseServer {
return filesystemUrls.has(resolved)
}
protected getMiddlewareInfo(page: string) {
return getMiddlewareInfo({
dev: this.renderOpts.dev,
page,
distDir: this.distDir,
serverless: this._isLikeServerless,
})
}
protected getMiddlewareManifest(): MiddlewareManifest | undefined {
return !this.minimalMode
? require(join(this.serverDistDir, MIDDLEWARE_MANIFEST))
: undefined
}
protected generateRewrites({
restrictedRedirectPaths,
}: {
@ -1034,6 +1011,203 @@ export default class NextNodeServer extends BaseServer {
}
}
/**
* Return a list of middleware routing items. This method exists to be later
* overridden by the development server in order to use a different source
* to get the list.
*/
protected getMiddleware(): RoutingItem[] {
if (this.minimalMode) {
return []
}
const manifest: MiddlewareManifest = require(join(
this.serverDistDir,
MIDDLEWARE_MANIFEST
))
return manifest.sortedMiddleware.map((page) => ({
match: getRouteMatcher(
getMiddlewareRegex(page, {
catchAll: manifest?.middleware?.[page].name === MIDDLEWARE_FILENAME,
})
),
page,
}))
}
/**
* Get information for the middleware located in the provided page
* folder. If the middleware info can't be found it will throw
* an error.
*/
protected getMiddlewareInfo(page: string) {
const manifest: MiddlewareManifest = require(join(
this.serverDistDir,
MIDDLEWARE_MANIFEST
))
let foundPage: string
try {
foundPage = denormalizePagePath(normalizePagePath(page))
} catch (err) {
throw pageNotFoundError(page)
}
let pageInfo = manifest.middleware[foundPage]
if (!pageInfo) {
throw pageNotFoundError(foundPage)
}
return {
name: pageInfo.name,
paths: pageInfo.files.map((file) => join(this.distDir, file)),
env: pageInfo.env ?? [],
wasm: (pageInfo.wasm ?? []).map((binding) => ({
...binding,
filePath: join(this.distDir, binding.filePath),
})),
}
}
/**
* Checks if a middleware exists. This method is useful for the development
* server where we need to check the filesystem. Here we just check the
* middleware manifest.
*/
protected async hasMiddleware(
pathname: string,
_isSSR?: boolean
): Promise<boolean> {
try {
return this.getMiddlewareInfo(pathname).paths.length > 0
} catch (_) {}
return false
}
/**
* A placeholder for a function to be defined in the development server.
* It will make sure that the middleware has been compiled so that we
* can run it.
*/
protected async ensureMiddleware(_pathname: string, _isSSR?: boolean) {}
/**
* This method gets all middleware matchers and execute them when the request
* matches. It will make sure that each middleware exists and is compiled and
* ready to be invoked. The development server will decorate it to add warns
* and errors with rich traces.
*/
protected async runMiddleware(params: {
request: BaseNextRequest
response: BaseNextResponse
parsedUrl: ParsedNextUrl
parsed: UrlWithParsedQuery
onWarning?: (warning: Error) => void
}): Promise<FetchEventResult | null> {
middlewareBetaWarning()
const normalizedPathname = removePathTrailingSlash(
params.parsedUrl.pathname
)
// For middleware to "fetch" we must always provide an absolute URL
const url = getRequestMeta(params.request, '__NEXT_INIT_URL')!
if (!url.startsWith('http')) {
throw new Error(
'To use middleware you must provide a `hostname` and `port` to the Next.js Server'
)
}
const page: { name?: string; params?: { [key: string]: string } } = {}
if (await this.hasPage(normalizedPathname)) {
page.name = params.parsedUrl.pathname
} else if (this.dynamicRoutes) {
for (const dynamicRoute of this.dynamicRoutes) {
const matchParams = dynamicRoute.match(normalizedPathname)
if (matchParams) {
page.name = dynamicRoute.page
page.params = matchParams
break
}
}
}
const allHeaders = new Headers()
let result: FetchEventResult | null = null
const method = (params.request.method || 'GET').toUpperCase()
let originalBody =
method !== 'GET' && method !== 'HEAD'
? clonableBodyForRequest(params.request.body)
: undefined
for (const middleware of this.getMiddleware()) {
if (middleware.match(normalizedPathname)) {
if (!(await this.hasMiddleware(middleware.page, middleware.ssr))) {
console.warn(`The Edge Function for ${middleware.page} was not found`)
continue
}
await this.ensureMiddleware(middleware.page, middleware.ssr)
const middlewareInfo = this.getMiddlewareInfo(middleware.page)
result = await run({
name: middlewareInfo.name,
paths: middlewareInfo.paths,
env: middlewareInfo.env,
wasm: middlewareInfo.wasm,
request: {
headers: params.request.headers,
method,
nextConfig: {
basePath: this.nextConfig.basePath,
i18n: this.nextConfig.i18n,
trailingSlash: this.nextConfig.trailingSlash,
},
url: url,
page: page,
body: originalBody?.cloneBodyStream(),
},
useCache: !this.nextConfig.experimental.runtime,
onWarning: (warning: Error) => {
if (params.onWarning) {
warning.message += ` "./${middlewareInfo.name}"`
params.onWarning(warning)
}
},
})
for (let [key, value] of result.response.headers) {
if (key !== 'x-middleware-next') {
allHeaders.append(key, value)
}
}
if (!this.renderOpts.dev) {
result.waitUntil.catch((error) => {
console.error(`Uncaught: middleware waitUntil errored`, error)
})
}
if (!result.response.headers.has('x-middleware-next')) {
break
}
}
}
if (!result) {
this.render404(params.request, params.response, params.parsed)
} else {
for (let [key, value] of allHeaders) {
result.response.headers.set(key, value)
}
}
await originalBody?.finalize()
return result
}
protected generateCatchAllMiddlewareRoute(): Route | undefined {
if (this.minimalMode) return undefined
@ -1042,7 +1216,8 @@ export default class NextNodeServer extends BaseServer {
type: 'route',
name: 'middleware catchall',
fn: async (req, res, _params, parsed) => {
if (!this.middleware?.length) {
const middleware = this.getMiddleware()
if (!middleware.length) {
return { finished: false }
}
@ -1058,7 +1233,7 @@ export default class NextNodeServer extends BaseServer {
})
const normalizedPathname = removePathTrailingSlash(parsedUrl.pathname)
if (!this.middleware?.some((m) => m.match(normalizedPathname))) {
if (!middleware.some((m) => m.match(normalizedPathname))) {
return { finished: false }
}
@ -1210,133 +1385,6 @@ export default class NextNodeServer extends BaseServer {
}
}
protected getMiddleware() {
const middleware = this.middlewareManifest?.middleware || {}
return (
this.middlewareManifest?.sortedMiddleware.map((page) => ({
match: getRouteMatcher(
getMiddlewareRegex(page, MIDDLEWARE_ROUTE.test(middleware[page].name))
),
page,
})) || []
)
}
private middlewareBetaWarning = execOnce(() => {
Log.warn(
`using beta Middleware (not covered by semver) - https://nextjs.org/docs/messages/beta-middleware`
)
})
protected async runMiddleware(params: {
request: BaseNextRequest
response: BaseNextResponse
parsedUrl: ParsedNextUrl
parsed: UrlWithParsedQuery
onWarning?: (warning: Error) => void
}): Promise<FetchEventResult | null> {
this.middlewareBetaWarning()
const normalizedPathname = removePathTrailingSlash(
params.parsedUrl.pathname
)
// For middleware to "fetch" we must always provide an absolute URL
const url = getRequestMeta(params.request, '__NEXT_INIT_URL')!
if (!url.startsWith('http')) {
throw new Error(
'To use middleware you must provide a `hostname` and `port` to the Next.js Server'
)
}
const page: { name?: string; params?: { [key: string]: string } } = {}
if (await this.hasPage(normalizedPathname)) {
page.name = params.parsedUrl.pathname
} else if (this.dynamicRoutes) {
for (const dynamicRoute of this.dynamicRoutes) {
const matchParams = dynamicRoute.match(normalizedPathname)
if (matchParams) {
page.name = dynamicRoute.page
page.params = matchParams
break
}
}
}
const allHeaders = new Headers()
let result: FetchEventResult | null = null
const method = (params.request.method || 'GET').toUpperCase()
let originalBody =
method !== 'GET' && method !== 'HEAD'
? clonableBodyForRequest(params.request.body)
: undefined
for (const middleware of this.middleware || []) {
if (middleware.match(normalizedPathname)) {
if (!(await this.hasMiddleware(middleware.page, middleware.ssr))) {
console.warn(`The Edge Function for ${middleware.page} was not found`)
continue
}
await this.ensureMiddleware(middleware.page, middleware.ssr)
const middlewareInfo = this.getMiddlewareInfo(middleware.page)
result = await run({
name: middlewareInfo.name,
paths: middlewareInfo.paths,
env: middlewareInfo.env,
wasm: middlewareInfo.wasm,
request: {
headers: params.request.headers,
method,
nextConfig: {
basePath: this.nextConfig.basePath,
i18n: this.nextConfig.i18n,
trailingSlash: this.nextConfig.trailingSlash,
},
url: url,
page: page,
body: originalBody?.cloneBodyStream(),
},
useCache: !this.nextConfig.experimental.runtime,
onWarning: (warning: Error) => {
if (params.onWarning) {
warning.message += ` "./${middlewareInfo.name}"`
params.onWarning(warning)
}
},
})
for (let [key, value] of result.response.headers) {
if (key !== 'x-middleware-next') {
allHeaders.append(key, value)
}
}
if (!this.renderOpts.dev) {
result.waitUntil.catch((error) => {
console.error(`Uncaught: middleware waitUntil errored`, error)
})
}
if (!result.response.headers.has('x-middleware-next')) {
break
}
}
}
if (!result) {
this.render404(params.request, params.response, params.parsed)
} else {
for (let [key, value] of allHeaders) {
result.response.headers.set(key, value)
}
}
await originalBody?.finalize()
return result
}
private _cachedPreviewManifest: PrerenderManifest | undefined
protected getPrerenderManifest(): PrerenderManifest {
if (this._cachedPreviewManifest) {

View file

@ -125,12 +125,11 @@ export class NextServer {
}
private async loadConfig() {
const phase = this.options.dev
? PHASE_DEVELOPMENT_SERVER
: PHASE_PRODUCTION_SERVER
const dir = resolve(this.options.dir || '.')
const conf = await loadConfig(phase, dir, this.options.conf)
return conf
return loadConfig(
this.options.dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER,
resolve(this.options.dir || '.'),
this.options.conf
)
}
private async getServer() {

View file

@ -2,7 +2,6 @@ import { promises } from 'fs'
import { join } from 'path'
import {
FONT_MANIFEST,
MIDDLEWARE_MANIFEST,
PAGES_MANIFEST,
SERVER_DIRECTORY,
SERVERLESS_DIRECTORY,
@ -12,8 +11,6 @@ import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
import type { WasmBinding } from '../build/webpack/loaders/get-module-build-info'
export function pageNotFoundError(page: string): Error {
const err: any = new Error(`Cannot find module for page: ${page}`)
@ -111,48 +108,3 @@ export function requireFontManifest(distDir: string, serverless: boolean) {
const fontManifest = require(join(serverBuildPath, FONT_MANIFEST))
return fontManifest
}
export function getMiddlewareInfo(params: {
dev?: boolean
distDir: string
page: string
serverless: boolean
}): {
name: string
paths: string[]
env: string[]
wasm: WasmBinding[]
} {
const serverBuildPath = join(
params.distDir,
params.serverless && !params.dev ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
)
const middlewareManifest: MiddlewareManifest = require(join(
serverBuildPath,
MIDDLEWARE_MANIFEST
))
let page: string
try {
page = denormalizePagePath(normalizePagePath(params.page))
} catch (err) {
throw pageNotFoundError(params.page)
}
let pageInfo = middlewareManifest.middleware[page]
if (!pageInfo) {
throw pageNotFoundError(page)
}
return {
name: pageInfo.name,
paths: pageInfo.files.map((file) => join(params.distDir, file)),
env: pageInfo.env ?? [],
wasm: (pageInfo.wasm ?? []).map((binding) => ({
...binding,
filePath: join(params.distDir, binding.filePath),
})),
}
}

View file

@ -1,5 +1,9 @@
import type { ParsedUrlQuery } from 'querystring'
import type { BaseNextRequest, BaseNextResponse } from './base-http'
import type {
RouteMatch,
Params,
} from '../shared/lib/router/utils/route-matcher'
import { getNextInternalQuery, NextUrlWithParsedQuery } from './request-meta'
import { getPathMatch } from '../shared/lib/router/utils/path-match'
@ -9,10 +13,6 @@ import { RouteHas } from '../lib/load-custom-routes'
import { matchHas } from '../shared/lib/router/utils/prepare-destination'
import { getRequestMeta } from './request-meta'
export type Params = { [param: string]: any }
export type RouteMatch = (pathname: string | null | undefined) => false | Params
type RouteResult = {
finished: boolean
pathname?: string

View file

@ -2,7 +2,7 @@ import type { WebNextRequest, WebNextResponse } from './base-http/web'
import type { RenderOpts } from './render'
import type RenderResult from './render-result'
import type { NextParsedUrlQuery } from './request-meta'
import type { Params } from './router'
import type { Params } from '../shared/lib/router/utils/route-matcher'
import type { PayloadOptions } from './send-payload'
import type { LoadComponentsReturnType } from './load-components'
import type { Options } from './base-server'
@ -71,9 +71,6 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {
protected getHasStaticDir() {
return false
}
protected async hasMiddleware() {
return false
}
protected generateImageRoutes() {
return []
}
@ -86,18 +83,12 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {
protected generatePublicRoutes() {
return []
}
protected getMiddleware() {
return []
}
protected generateCatchAllMiddlewareRoute() {
return undefined
}
protected getFontManifest() {
return undefined
}
protected getMiddlewareManifest() {
return undefined
}
protected getPagesManifest() {
return {
[this.serverOptions.webServerConfig.page]: '',

View file

@ -9,19 +9,24 @@ import { removePagePathTail } from './remove-page-path-tail'
* relative to the pages folder. It doesn't consider index tail. Example:
* - `/Users/rick/my-project/pages/foo/bar/baz.js` -> `/foo/bar/baz`
*
* @param pagesDir Absolute path to the pages folder.
* @param filepath Absolute path to the page.
* @param extensions Extensions allowed for the page.
* @param opts.pagesDir Absolute path to the pages folder.
* @param opts.extensions Extensions allowed for the page.
* @param opts.keepIndex When true the trailing `index` kept in the path.
*/
export function absolutePathToPage(
pagesDir: string,
pagePath: string,
extensions: string[],
stripIndex = true
options: {
extensions: string[]
keepIndex?: boolean
pagesDir: string
}
) {
return removePagePathTail(
normalizePathSep(ensureLeadingSlash(relative(pagesDir, pagePath))),
extensions,
stripIndex
normalizePathSep(ensureLeadingSlash(relative(options.pagesDir, pagePath))),
{
extensions: options.extensions,
keepIndex: options.keepIndex,
}
)
}

View file

@ -8,19 +8,22 @@ import { normalizePathSep } from './normalize-path-sep'
* - `/foo/bar/baz.js` -> `/foo/bar/baz`
*
* @param pagePath A page to a page file (absolute or relative)
* @param extensions Extensions allowed for the page.
* @param options.extensions Extensions allowed for the page.
* @param options.keepIndex When true the trailing `index` is _not_ removed.
*/
export function removePagePathTail(
pagePath: string,
extensions: string[],
stripIndex?: boolean
options: {
extensions: string[]
keepIndex?: boolean
}
) {
pagePath = normalizePathSep(pagePath).replace(
new RegExp(`\\.+(?:${extensions.join('|')})$`),
new RegExp(`\\.+(?:${options.extensions.join('|')})$`),
''
)
if (stripIndex) {
if (options.keepIndex !== true) {
pagePath = pagePath.replace(/\/index$/, '') || '/'
}

View file

@ -36,8 +36,7 @@ import { parseRelativeUrl } from './utils/parse-relative-url'
import { searchParamsToUrlQuery } from './utils/querystring'
import resolveRewrites from './utils/resolve-rewrites'
import { getRouteMatcher } from './utils/route-matcher'
import { getRouteRegex } from './utils/route-regex'
import { getMiddlewareRegex } from './utils/get-middleware-regex'
import { getRouteRegex, getMiddlewareRegex } from './utils/route-regex'
import { formatWithValidation } from './utils/format-url'
declare global {
@ -1920,7 +1919,11 @@ export default class Router implements BaseRouter {
const fns = await this.pageLoader.getMiddlewareList()
const requiresPreflight = fns.some(([middleware, isSSR]) => {
return getRouteMatcher(getMiddlewareRegex(middleware, !isSSR))(cleanedAs)
return getRouteMatcher(
getMiddlewareRegex(middleware, {
catchAll: !isSSR,
})
)(cleanedAs)
})
if (!requiresPreflight) {

View file

@ -1,41 +0,0 @@
import { getParametrizedRoute, RouteRegex } from './route-regex'
export function getMiddlewareRegex(
normalizedRoute: string,
catchAll: boolean = true
): RouteRegex {
const result = getParametrizedRoute(normalizedRoute)
let catchAllRegex = catchAll ? '(?!_next).*' : ''
let catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : ''
if ('routeKeys' in result) {
if (result.parameterizedRoute === '/') {
return {
groups: {},
namedRegex: `^/${catchAllRegex}$`,
re: new RegExp(`^/${catchAllRegex}$`),
routeKeys: {},
}
}
return {
groups: result.groups,
namedRegex: `^${result.namedParameterizedRoute}${catchAllGroupedRegex}$`,
re: new RegExp(`^${result.parameterizedRoute}${catchAllGroupedRegex}$`),
routeKeys: result.routeKeys,
}
}
if (result.parameterizedRoute === '/') {
return {
groups: {},
re: new RegExp(`^/${catchAllRegex}$`),
}
}
return {
groups: {},
re: new RegExp(`^${result.parameterizedRoute}${catchAllGroupedRegex}$`),
}
}

View file

@ -1,5 +1,2 @@
export { getMiddlewareRegex } from './get-middleware-regex'
export { getRouteMatcher } from './route-matcher'
export { getRouteRegex } from './route-regex'
export { getSortedRoutes } from './sorted-routes'
export { isDynamicRoute } from './is-dynamic'

View file

@ -1,7 +1,7 @@
import type { IncomingMessage } from 'http'
import type { Key } from 'next/dist/compiled/path-to-regexp'
import type { NextParsedUrlQuery } from '../../../../server/request-meta'
import type { Params } from '../../../../server/router'
import type { Params } from './route-matcher'
import type { RouteHas } from '../../../../lib/load-custom-routes'
import type { BaseNextRequest } from '../../../../server/base-http'

View file

@ -0,0 +1,10 @@
/**
* Removes the trailing slash for a given route or page path. Preserves the
* root page. Examples:
* - `/foo/bar/` -> `/foo/bar`
* - `/foo/bar` -> `/foo/bar`
* - `/` -> `/`
*/
export function removeTrailingSlash(route: string) {
return route.replace(/\/$/, '') || '/'
}

View file

@ -1,8 +1,15 @@
import type { RouteRegex } from './route-regex'
import { DecodeError } from '../../utils'
import { getRouteRegex } from './route-regex'
export function getRouteMatcher(routeRegex: ReturnType<typeof getRouteRegex>) {
const { re, groups } = routeRegex
export interface RouteMatch {
(pathname: string | null | undefined): false | Params
}
export interface Params {
[param: string]: any
}
export function getRouteMatcher({ re, groups }: RouteRegex): RouteMatch {
return (pathname: string | null | undefined) => {
const routeMatch = re.exec(pathname!)
if (!routeMatch) {

View file

@ -1,65 +1,70 @@
import { escapeStringRegexp } from '../../escape-regexp'
import { removeTrailingSlash } from './remove-trailing-slash'
interface Group {
export interface Group {
pos: number
repeat: boolean
optional: boolean
}
function parseParameter(param: string) {
const optional = param.startsWith('[') && param.endsWith(']')
if (optional) {
param = param.slice(1, -1)
}
const repeat = param.startsWith('...')
if (repeat) {
param = param.slice(3)
}
return { key: param, repeat, optional }
export interface RouteRegex {
groups: { [groupName: string]: Group }
re: RegExp
}
export function getParametrizedRoute(route: string) {
const segments = (route.replace(/\/$/, '') || '/').slice(1).split('/')
/**
* From a normalized route this function generates a regular expression and
* a corresponding groups object inteded to be used to store matching groups
* from the regular expression.
*/
export function getRouteRegex(normalizedRoute: string): RouteRegex {
const { parameterizedRoute, groups } = getParametrizedRoute(normalizedRoute)
return {
re: new RegExp(`^${parameterizedRoute}(?:/)?$`),
groups: groups,
}
}
/**
* This function extends `getRouteRegex` generating also a named regexp where
* each group is named along with a routeKeys object that indexes the assigned
* named group with its corresponding key.
*/
export function getNamedRouteRegex(normalizedRoute: string) {
const result = getNamedParametrizedRoute(normalizedRoute)
return {
...getRouteRegex(normalizedRoute),
namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`,
routeKeys: result.routeKeys,
}
}
function getParametrizedRoute(route: string) {
const segments = removeTrailingSlash(route).slice(1).split('/')
const groups: { [groupName: string]: Group } = {}
let groupIndex = 1
const parameterizedRoute = segments
.map((segment) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1))
groups[key] = { pos: groupIndex++, repeat, optional }
return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)'
} else {
return `/${escapeStringRegexp(segment)}`
}
})
.join('')
// dead code eliminate for browser since it's only needed
// while generating routes-manifest
if (typeof window === 'undefined') {
let routeKeyCharCode = 97
let routeKeyCharLength = 1
// builds a minimal routeKey using only a-z and minimal number of characters
const getSafeRouteKey = () => {
let routeKey = ''
for (let i = 0; i < routeKeyCharLength; i++) {
routeKey += String.fromCharCode(routeKeyCharCode)
routeKeyCharCode++
if (routeKeyCharCode > 122) {
routeKeyCharLength++
routeKeyCharCode = 97
return {
parameterizedRoute: segments
.map((segment) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1))
groups[key] = { pos: groupIndex++, repeat, optional }
return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)'
} else {
return `/${escapeStringRegexp(segment)}`
}
}
return routeKey
}
})
.join(''),
groups,
}
}
const routeKeys: { [named: string]: string } = {}
let namedParameterizedRoute = segments
function getNamedParametrizedRoute(route: string) {
const segments = removeTrailingSlash(route).slice(1).split('/')
const getSafeRouteKey = buildGetSafeRouteKey()
const routeKeys: { [named: string]: string } = {}
return {
namedParameterizedRoute: segments
.map((segment) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1))
@ -91,42 +96,104 @@ export function getParametrizedRoute(route: string) {
return `/${escapeStringRegexp(segment)}`
}
})
.join('')
.join(''),
routeKeys,
}
}
/**
* Parses a given parameter from a route to a data structure that can be used
* to generate the parametrized route. Examples:
* - `[...slug]` -> `{ name: 'slug', repeat: true, optional: true }`
* - `[foo]` -> `{ name: 'foo', repeat: false, optional: true }`
* - `bar` -> `{ name: 'bar', repeat: false, optional: false }`
*/
function parseParameter(param: string) {
const optional = param.startsWith('[') && param.endsWith(']')
if (optional) {
param = param.slice(1, -1)
}
const repeat = param.startsWith('...')
if (repeat) {
param = param.slice(3)
}
return { key: param, repeat, optional }
}
/**
* Builds a function to generate a minimal routeKey using only a-z and minimal
* number of characters.
*/
function buildGetSafeRouteKey() {
let routeKeyCharCode = 97
let routeKeyCharLength = 1
return () => {
let routeKey = ''
for (let i = 0; i < routeKeyCharLength; i++) {
routeKey += String.fromCharCode(routeKeyCharCode)
routeKeyCharCode++
if (routeKeyCharCode > 122) {
routeKeyCharLength++
routeKeyCharCode = 97
}
}
return routeKey
}
}
/**
* From a middleware normalized route this function generates a regular
* expression for it. Temporarly we are using this to generate Edge Function
* routes too. In such cases the route should not include a trailing catch-all.
* For these cases the option `catchAll` should be set to false.
*/
export function getMiddlewareRegex(
normalizedRoute: string,
options?: {
catchAll?: boolean
}
): RouteRegex {
const { parameterizedRoute, groups } = getParametrizedRoute(normalizedRoute)
const { catchAll = true } = options ?? {}
if (parameterizedRoute === '/') {
let catchAllRegex = catchAll ? '(?!_next).*' : ''
return {
parameterizedRoute,
namedParameterizedRoute,
groups,
routeKeys,
groups: {},
re: new RegExp(`^/${catchAllRegex}$`),
}
}
let catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : ''
return {
parameterizedRoute,
groups,
groups: groups,
re: new RegExp(`^${parameterizedRoute}${catchAllGroupedRegex}$`),
}
}
export interface RouteRegex {
groups: { [groupName: string]: Group }
namedRegex?: string
re: RegExp
routeKeys?: { [named: string]: string }
}
export function getRouteRegex(normalizedRoute: string): RouteRegex {
const result = getParametrizedRoute(normalizedRoute)
if ('routeKeys' in result) {
/**
* A server version for getMiddlewareRegex that generates a named regexp.
* This is intended to be using for build time only.
*/
export function getNamedMiddlewareRegex(
normalizedRoute: string,
options: {
catchAll?: boolean
}
) {
const { parameterizedRoute } = getParametrizedRoute(normalizedRoute)
const { catchAll = true } = options
if (parameterizedRoute === '/') {
let catchAllRegex = catchAll ? '(?!_next).*' : ''
return {
re: new RegExp(`^${result.parameterizedRoute}(?:/)?$`),
groups: result.groups,
routeKeys: result.routeKeys,
namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`,
namedRegex: `^/${catchAllRegex}$`,
}
}
const { namedParameterizedRoute } = getNamedParametrizedRoute(normalizedRoute)
let catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : ''
return {
re: new RegExp(`^${result.parameterizedRoute}(?:/)?$`),
groups: result.groups,
namedRegex: `^${namedParameterizedRoute}${catchAllGroupedRegex}$`,
}
}

View file

@ -17,8 +17,11 @@ function baseNextConfig(): Parameters<typeof createNext>[0] {
return exports.add_one(a);
}
`,
'pages/_middleware.js': `
import { increment } from '../src/add.js'
'pages/index.js': `
export default function () { return <div>Hello, world!</div> }
`,
'middleware.js': `
import { increment } from './src/add.js'
export default async function middleware(request) {
const input = Number(request.nextUrl.searchParams.get('input')) || 1;
const value = await increment(input);

View file

@ -1,4 +1,4 @@
import { notUsingEval, usingEval } from '../lib/utils'
import { notUsingEval, usingEval } from './lib/utils'
export async function middleware(request) {
if (request.nextUrl.pathname === '/using-eval') {

View file

@ -45,7 +45,7 @@ describe('Middleware usage of dynamic code evaluation', () => {
expect(json.value).toEqual(100)
expect(output).toContain(DYNAMIC_CODE_ERROR)
expect(output).toContain('DynamicCodeEvaluationWarning')
expect(output).toContain('pages/_middleware')
expect(output).toContain('./middleware')
// TODO check why that has a backslash on windows
expect(output).toMatch(/lib[\\/]utils\.js/)
expect(output).toContain('usingEval')
@ -81,7 +81,7 @@ describe('Middleware usage of dynamic code evaluation', () => {
it('should have middleware warning during build', () => {
expect(buildResult.stderr).toContain(`Failed to compile`)
expect(buildResult.stderr).toContain(`Used by usingEval`)
expect(buildResult.stderr).toContain(`./pages/_middleware.js`)
expect(buildResult.stderr).toContain(`./middleware.js`)
expect(buildResult.stderr).toContain(DYNAMIC_CODE_ERROR)
})
})

View file

@ -1,37 +1,11 @@
/* global globalThis */
import { NextResponse } from 'next/server'
import { NextRequest, NextResponse } from 'next/server'
import magicValue from 'shared-package'
export async function middleware(request) {
const url = request.nextUrl
if (url.pathname.endsWith('/globalthis')) {
return new NextResponse(JSON.stringify(Object.keys(globalThis)), {
headers: {
'content-type': 'application/json; charset=utf-8',
},
})
}
if (url.pathname.endsWith('/fetchURL')) {
const response = {}
try {
await fetch(new URL('http://localhost'))
} catch (err) {
response.error = {
name: err.name,
message: err.message,
}
} finally {
return new NextResponse(JSON.stringify(response), {
headers: {
'content-type': 'application/json; charset=utf-8',
},
})
}
}
if (url.pathname.includes('/fetchUserAgentDefault')) {
if (url.pathname.startsWith('/fetch-user-agent-default')) {
try {
const apiRoute = new URL(url)
apiRoute.pathname = '/api/headers'
@ -52,7 +26,7 @@ export async function middleware(request) {
}
}
if (url.pathname.includes('/fetchUserAgentCustom')) {
if (url.pathname.startsWith('/fetch-user-agent-crypto')) {
try {
const apiRoute = new URL(url)
apiRoute.pathname = '/api/headers'
@ -77,6 +51,25 @@ export async function middleware(request) {
}
}
if (url.pathname === '/global') {
// The next line is required to allow to find the env variable
// eslint-disable-next-line no-unused-expressions
process.env.MIDDLEWARE_TEST
return NextResponse.json({
process: {
env: process.env,
},
})
}
if (url.pathname.endsWith('/globalthis')) {
return new NextResponse(JSON.stringify(Object.keys(globalThis)), {
headers: {
'content-type': 'application/json; charset=utf-8',
},
})
}
if (url.pathname.endsWith('/webcrypto')) {
const response = {}
try {
@ -99,11 +92,25 @@ export async function middleware(request) {
}
}
if (url.pathname.endsWith('/root-subrequest')) {
return fetch(url)
if (url.pathname.endsWith('/fetch-url')) {
const response = {}
try {
await fetch(new URL('http://localhost'))
} catch (err) {
response.error = {
name: err.name,
message: err.message,
}
} finally {
return new NextResponse(JSON.stringify(response), {
headers: {
'content-type': 'application/json; charset=utf-8',
},
})
}
}
if (url.pathname.endsWith('/abort-controller')) {
if (url.pathname === '/abort-controller') {
const controller = new AbortController()
const signal = controller.signal
@ -126,11 +133,52 @@ export async function middleware(request) {
}
}
if (url.pathname.endsWith('/dynamic-replace')) {
url.pathname = '/_interface/dynamic-path'
return NextResponse.rewrite(url)
if (url.pathname.endsWith('/root-subrequest')) {
const res = await fetch(url)
res.headers.set('x-dynamic-path', 'true')
return res
}
if (url.pathname === '/about') {
if (magicValue !== 42) throw new Error('shared-package problem')
return NextResponse.rewrite(new URL('/about/a', request.url))
}
if (url.pathname.startsWith('/url')) {
try {
if (request.nextUrl.pathname === '/url/relative-url') {
return NextResponse.json({ message: String(new URL('/relative')) })
}
if (request.nextUrl.pathname === '/url/relative-request') {
return fetch(new Request('/urls-b'))
}
if (request.nextUrl.pathname === '/url/relative-redirect') {
return Response.redirect('/urls-b')
}
if (request.nextUrl.pathname === '/url/relative-next-redirect') {
return NextResponse.redirect('/urls-b')
}
if (request.nextUrl.pathname === '/url/relative-next-rewrite') {
return NextResponse.rewrite('/urls-b')
}
if (request.nextUrl.pathname === '/url/relative-next-request') {
return fetch(new NextRequest('/urls-b'))
}
} catch (error) {
return NextResponse.json({
error: {
message: error.message,
},
})
}
}
// Map metadata by default
return new Response(null, {
headers: {
'req-url-basepath': request.nextUrl.basePath,

View file

@ -0,0 +1,298 @@
/* eslint-env jest */
import { join } from 'path'
import fs from 'fs-extra'
import webdriver from 'next-webdriver'
import {
fetchViaHTTP,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
waitFor,
} from 'next-test-utils'
jest.setTimeout(1000 * 60 * 2)
const middlewareWarning = 'using beta Middleware (not covered by semver)'
const urlsError = 'Please use only absolute URLs'
const context = {
appDir: join(__dirname, '../'),
buildLogs: { output: '', stdout: '', stderr: '' },
logs: { output: '', stdout: '', stderr: '' },
}
describe('Middleware Runtime', () => {
describe('dev mode', () => {
afterAll(() => killApp(context.app))
beforeAll(async () => {
context.appPort = await findPort()
context.app = await launchApp(context.appDir, context.appPort, {
env: {
MIDDLEWARE_TEST: 'asdf',
NEXT_RUNTIME: 'edge',
},
onStdout(msg) {
context.logs.output += msg
context.logs.stdout += msg
},
onStderr(msg) {
context.logs.output += msg
context.logs.stderr += msg
},
})
})
tests(context)
// This test has to be after something has been executed with middleware
it('should have showed warning for middleware usage', () => {
expect(context.logs.output).toContain(middlewareWarning)
})
it('refreshes the page when middleware changes ', async () => {
const browser = await webdriver(context.appPort, `/about`)
await browser.eval('window.didrefresh = "hello"')
const text = await browser.elementByCss('h1').text()
expect(text).toEqual('AboutA')
const middlewarePath = join(context.appDir, '/middleware.js')
const originalContent = fs.readFileSync(middlewarePath, 'utf-8')
const editedContent = originalContent.replace('/about/a', '/about/b')
try {
fs.writeFileSync(middlewarePath, editedContent)
await waitFor(1000)
const textb = await browser.elementByCss('h1').text()
expect(await browser.eval('window.itdidnotrefresh')).not.toBe('hello')
expect(textb).toEqual('AboutB')
} finally {
fs.writeFileSync(middlewarePath, originalContent)
await browser.close()
}
})
})
describe('production mode', () => {
afterAll(() => killApp(context.app))
beforeAll(async () => {
const build = await nextBuild(context.appDir, undefined, {
stderr: true,
stdout: true,
})
context.buildLogs = {
output: build.stdout + build.stderr,
stderr: build.stderr,
stdout: build.stdout,
}
context.appPort = await findPort()
context.app = await nextStart(context.appDir, context.appPort, {
env: {
MIDDLEWARE_TEST: 'asdf',
NEXT_RUNTIME: 'edge',
},
onStdout(msg) {
context.logs.output += msg
context.logs.stdout += msg
},
onStderr(msg) {
context.logs.output += msg
context.logs.stderr += msg
},
})
})
it('should have middleware warning during build', () => {
expect(context.buildLogs.output).toContain(middlewareWarning)
})
it('should have middleware warning during start', () => {
expect(context.logs.output).toContain(middlewareWarning)
})
it('should have correct files in manifest', async () => {
const manifest = await fs.readJSON(
join(context.appDir, '.next/server/middleware-manifest.json')
)
for (const key of Object.keys(manifest.middleware)) {
const middleware = manifest.middleware[key]
expect(middleware.files).toContainEqual(
expect.stringContaining('server/edge-runtime-webpack')
)
expect(middleware.files).not.toContainEqual(
expect.stringContaining('static/chunks/')
)
}
})
tests(context)
})
})
function tests(context, locale = '') {
it('should set fetch user agent correctly', async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/fetch-user-agent-default`
)
expect((await res.json()).headers['user-agent']).toBe('Next.js Middleware')
const res2 = await fetchViaHTTP(
context.appPort,
`${locale}/fetch-user-agent-crypto`
)
expect((await res2.json()).headers['user-agent']).toBe('custom-agent')
})
it('should contain process polyfill', async () => {
const res = await fetchViaHTTP(context.appPort, `/global`)
const json = await res.json()
expect(json).toEqual({
process: {
env: {
MIDDLEWARE_TEST: 'asdf',
NEXT_RUNTIME: 'edge',
},
},
})
})
it(`should contain \`globalThis\``, async () => {
const res = await fetchViaHTTP(context.appPort, '/globalthis')
const globals = await res.json()
expect(globals.length > 0).toBe(true)
})
it(`should contain crypto APIs`, async () => {
const res = await fetchViaHTTP(context.appPort, '/webcrypto')
const response = await res.json()
expect('error' in response).toBe(false)
})
it(`should accept a URL instance for fetch`, async () => {
const res = await fetchViaHTTP(context.appPort, '/fetch-url')
const response = await res.json()
expect('error' in response).toBe(true)
expect(response.error.name).not.toBe('TypeError')
})
it(`should allow to abort a fetch request`, async () => {
const res = await fetchViaHTTP(context.appPort, '/abort-controller')
const response = await res.json()
expect('error' in response).toBe(true)
expect(response.error.name).toBe('AbortError')
expect(response.error.message).toBe('The user aborted a request.')
})
it(`should validate & parse request url from any route`, async () => {
const res = await fetchViaHTTP(context.appPort, `${locale}/static`)
expect(res.headers.get('req-url-basepath')).toBe('')
expect(res.headers.get('req-url-pathname')).toBe('/static')
expect(res.headers.get('req-url-params')).not.toBe('{}')
expect(res.headers.get('req-url-query')).not.toBe('bar')
if (locale !== '') {
expect(res.headers.get('req-url-locale')).toBe(locale.slice(1))
}
})
it(`should validate & parse request url from a dynamic route with params`, async () => {
const res = await fetchViaHTTP(context.appPort, `/fr/1`)
expect(res.headers.get('req-url-basepath')).toBe('')
expect(res.headers.get('req-url-pathname')).toBe('/1')
expect(res.headers.get('req-url-params')).toBe('{"id":"1"}')
expect(res.headers.get('req-url-page')).toBe('/[id]')
expect(res.headers.get('req-url-query')).not.toBe('bar')
expect(res.headers.get('req-url-locale')).toBe('fr')
})
it(`should validate & parse request url from a dynamic route with params and no query`, async () => {
const res = await fetchViaHTTP(context.appPort, `/fr/abc123`)
expect(res.headers.get('req-url-basepath')).toBe('')
expect(res.headers.get('req-url-pathname')).toBe('/abc123')
expect(res.headers.get('req-url-params')).toBe('{"id":"abc123"}')
expect(res.headers.get('req-url-page')).toBe('/[id]')
expect(res.headers.get('req-url-query')).not.toBe('bar')
expect(res.headers.get('req-url-locale')).toBe('fr')
})
it(`should validate & parse request url from a dynamic route with params and query`, async () => {
const res = await fetchViaHTTP(context.appPort, `/abc123?foo=bar`)
expect(res.headers.get('req-url-basepath')).toBe('')
expect(res.headers.get('req-url-pathname')).toBe('/abc123')
expect(res.headers.get('req-url-params')).toBe('{"id":"abc123"}')
expect(res.headers.get('req-url-page')).toBe('/[id]')
expect(res.headers.get('req-url-query')).toBe('bar')
expect(res.headers.get('req-url-locale')).toBe('en')
})
it(`should render correctly rewriting with a root subrequest`, async () => {
const browser = await webdriver(context.appPort, '/root-subrequest')
const element = await browser.elementByCss('.title')
expect(await element.text()).toEqual('Dynamic route')
})
it(`should allow subrequests without infinite loops`, async () => {
const res = await fetchViaHTTP(context.appPort, `/root-subrequest`)
expect(res.headers.get('x-dynamic-path')).toBe('true')
})
it('should throw when using URL with a relative URL', async () => {
const res = await fetchViaHTTP(context.appPort, `/url/relative-url`)
const json = await res.json()
expect(json.error.message).toContain('Invalid URL')
})
it('should throw when using Request with a relative URL', async () => {
const res = await fetchViaHTTP(context.appPort, `/url/relative-request`)
const json = await res.json()
expect(json.error.message).toContain('Invalid URL')
})
it('should throw when using NextRequest with a relative URL', async () => {
const res = await fetchViaHTTP(
context.appPort,
`/url/relative-next-request`
)
const json = await res.json()
expect(json.error.message).toContain('Invalid URL')
})
it('should warn when using Response.redirect with a relative URL', async () => {
const response = await fetchViaHTTP(
context.appPort,
`/url/relative-redirect`
)
expect(await response.json()).toEqual({
error: {
message: expect.stringContaining(urlsError),
},
})
})
it('should warn when using NextResponse.redirect with a relative URL', async () => {
const response = await fetchViaHTTP(
context.appPort,
`/url/relative-next-redirect`
)
expect(await response.json()).toEqual({
error: {
message: expect.stringContaining(urlsError),
},
})
})
it('should throw when using NextResponse.rewrite with a relative URL', async () => {
const response = await fetchViaHTTP(
context.appPort,
`/url/relative-next-rewrite`
)
expect(await response.json()).toEqual({
error: {
message: expect.stringContaining(urlsError),
},
})
})
}

View file

@ -0,0 +1 @@
export default () => {}

View file

@ -0,0 +1 @@
export function middleware() {}

View file

@ -0,0 +1,7 @@
export default function About() {
return (
<div>
<h1 className="title">About Page</h1>
</div>
)
}

View file

@ -0,0 +1,3 @@
export default function Page() {
return <div>ok</div>
}

View file

@ -0,0 +1,230 @@
/* eslint-env jest */
import stripAnsi from 'next/dist/compiled/strip-ansi'
import { getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage } from 'next/dist/build/utils'
import { join } from 'path'
import {
fetchViaHTTP,
File,
findPort,
killApp,
launchApp,
nextBuild,
waitFor,
} from 'next-test-utils'
jest.setTimeout(1000 * 60 * 2)
const WEBPACK_BREAKING_CHANGE = 'BREAKING CHANGE:'
const context = {
appDir: join(__dirname, '../'),
buildLogs: { output: '', stdout: '', stderr: '' },
logs: { output: '', stdout: '', stderr: '' },
middleware: new File(join(__dirname, '../middleware.js')),
page: new File(join(__dirname, '../pages/index.js')),
}
describe('Middleware importing Node.js modules', () => {
function getModuleNotFound(name) {
return `Module not found: Can't resolve '${name}'`
}
function escapeLF(s) {
return s.replace(/\n/g, '\\n')
}
afterEach(() => {
context.middleware.restore()
context.page.restore()
if (context.app) {
killApp(context.app)
}
})
describe('dev mode', () => {
// restart the app for every test since the latest error is not shown sometimes
// See https://github.com/vercel/next.js/issues/36575
beforeEach(async () => {
context.logs = { output: '', stdout: '', stderr: '' }
context.appPort = await findPort()
context.app = await launchApp(context.appDir, context.appPort, {
env: { __NEXT_TEST_WITH_DEVTOOL: 1 },
onStdout(msg) {
context.logs.output += msg
context.logs.stdout += msg
},
onStderr(msg) {
context.logs.output += msg
context.logs.stderr += msg
},
})
})
it('shows the right error when importing `path` on middleware', async () => {
context.middleware.write(`
import { NextResponse } from 'next/server'
import { basename } from 'path'
export async function middleware(request) {
console.log(basename('/foo/bar/baz/asdf/quux.html'))
return NextResponse.next()
}
`)
const res = await fetchViaHTTP(context.appPort, '/')
const text = await res.text()
await waitFor(500)
const msg = getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('path')
expect(res.status).toBe(500)
expect(context.logs.output).toContain(getModuleNotFound('path'))
expect(context.logs.output).toContain(msg)
expect(text).toContain(escapeLF(msg))
expect(stripAnsi(context.logs.output)).toContain(
"import { basename } from 'path'"
)
expect(context.logs.output).not.toContain(WEBPACK_BREAKING_CHANGE)
})
it('shows the right error when importing `child_process` on middleware', async () => {
context.middleware.write(`
import { NextResponse } from 'next/server'
import { spawn } from 'child_process'
export async function middleware(request) {
console.log(spawn('ls', ['-lh', '/usr']))
return NextResponse.next()
}
`)
const res = await fetchViaHTTP(context.appPort, '/')
const text = await res.text()
await waitFor(500)
const msg =
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('child_process')
expect(res.status).toBe(500)
expect(context.logs.output).toContain(getModuleNotFound('child_process'))
expect(context.logs.output).toContain(msg)
expect(text).toContain(escapeLF(msg))
expect(stripAnsi(context.logs.output)).toContain(
"import { spawn } from 'child_process'"
)
expect(context.logs.output).not.toContain(WEBPACK_BREAKING_CHANGE)
})
it('shows the right error when importing a non-node-builtin module on middleware', async () => {
context.middleware.write(`
import { NextResponse } from 'next/server'
import NotExist from 'not-exist'
export async function middleware(request) {
new NotExist()
return NextResponse.next()
}
`)
const res = await fetchViaHTTP(context.appPort, '/')
expect(res.status).toBe(500)
const text = await res.text()
await waitFor(500)
const msg =
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('not-exist')
expect(context.logs.output).toContain(getModuleNotFound('not-exist'))
expect(context.logs.output).not.toContain(msg)
expect(text).not.toContain(escapeLF(msg))
})
it('shows the right error when importing `child_process` on a page', async () => {
context.page.write(`
import { spawn } from 'child_process'
export default function Page() {
spawn('ls', ['-lh', '/usr'])
return <div>ok</div>
}
`)
await fetchViaHTTP(context.appPort, '/')
// Need to request twice
// See: https://github.com/vercel/next.js/issues/36387
const res = await fetchViaHTTP(context.appPort, '/')
expect(res.status).toBe(500)
const text = await res.text()
await waitFor(500)
const msg =
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('child_process')
expect(context.logs.output).toContain(getModuleNotFound('child_process'))
expect(context.logs.output).not.toContain(msg)
expect(text).not.toContain(escapeLF(msg))
})
it('warns about nested middleware being not allowed', async () => {
const file = new File(join(__dirname, '../pages/about/_middleware.js'))
file.write(`export function middleware() {}`)
try {
const res = await fetchViaHTTP(context.appPort, '/about')
expect(context.logs.stderr).toContain(
'nested Middleware is not allowed (found pages/about/_middleware) - https://nextjs.org/docs/messages/nested-middleware'
)
expect(res.status).toBe(200)
} finally {
file.delete()
}
})
})
describe('production mode', () => {
it('fails with the right middleware error during build', async () => {
context.middleware.write(`
import { NextResponse } from 'next/server'
import { spawn } from 'child_process'
export async function middleware(request) {
console.log(spawn('ls', ['-lh', '/usr']))
return NextResponse.next()
}
`)
const buildResult = await nextBuild(context.appDir, undefined, {
stderr: true,
stdout: true,
})
expect(buildResult.stderr).toContain(getModuleNotFound('child_process'))
expect(buildResult.stderr).toContain(
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('child_process')
)
expect(buildResult.stderr).not.toContain(WEBPACK_BREAKING_CHANGE)
})
it('fails with the right page error during build', async () => {
context.page.write(`
import { spawn } from 'child_process'
export default function Page() {
spawn('ls', ['-lh', '/usr'])
return <div>ok</div>
}
`)
const buildResult = await nextBuild(context.appDir, undefined, {
stderr: true,
stdout: true,
})
expect(buildResult.stderr).toContain(getModuleNotFound('child_process'))
expect(buildResult.stderr).not.toContain(
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('child_process')
)
})
it('fails when there is a not allowed middleware', async () => {
const file = new File(join(__dirname, '../pages/about/_middleware.js'))
file.write(`export function middleware() {}`)
const buildResult = await nextBuild(context.appDir, undefined, {
stderr: true,
stdout: true,
})
expect(buildResult.stderr).toContain(
'Error: nested Middleware is not allowed (found pages/about/_middleware) - https://nextjs.org/docs/messages/nested-middleware'
)
})
})
})

View file

@ -0,0 +1,16 @@
import { NextResponse } from 'next/server'
export function middleware(req) {
const url = req.nextUrl.clone()
if (url.pathname.startsWith('/i18n')) {
url.searchParams.set('locale', url.locale)
return NextResponse.rewrite(url)
}
if (
url.pathname === '/error-throw' &&
req.headers.has('x-middleware-preflight')
) {
throw new Error('test error')
}
}

View file

@ -1,6 +1,6 @@
module.exports = {
i18n: {
locales: ['ja', 'en', 'fr'],
defaultLocale: 'ja',
defaultLocale: 'en',
},
}

View file

@ -3,7 +3,7 @@ import Link from 'next/link'
export default function Errors() {
return (
<div>
<Link href="/errors/throw-on-preflight?message=refreshed">
<Link href="/error-throw?message=refreshed">
<a id="throw-on-preflight">Throw on preflight</a>
</Link>
</div>

View file

@ -6,27 +6,27 @@ export default function Home({ locale }) {
<div className={locale}>{locale}</div>
<ul>
<li>
<Link href="/" locale="en">
<Link href="/i18n" locale="en">
<a id="link-en">Go to en</a>
</Link>
</li>
<li>
<Link href="/en" locale={false}>
<Link href="/en/i18n" locale={false}>
<a id="link-en2">Go to en2</a>
</Link>
</li>
<li>
<Link href="/" locale="ja">
<Link href="/i18n" locale="ja">
<a id="link-ja">Go to ja</a>
</Link>
</li>
<li>
<Link href="/ja" locale={false}>
<Link href="/ja/i18n" locale={false}>
<a id="link-ja2">Go to ja2</a>
</Link>
</li>
<li>
<Link href="/" locale="fr">
<Link href="/i18n" locale="fr">
<a id="link-fr">Go to fr</a>
</Link>
</li>

View file

@ -50,9 +50,7 @@ function runTests() {
itif(!USE_SELENIUM)(
`should send preflight for specified locale`,
async () => {
const browser = await webdriver(context.appPort, '/', {
locale: 'en-US,en',
})
const browser = await webdriver(context.appPort, '/i18n')
await browser.waitForElementByCss('.en')
await browser.elementByCss('#link-ja').click()
await browser.waitForElementByCss('.ja')
@ -66,4 +64,12 @@ function runTests() {
await browser.waitForElementByCss('.en')
}
)
it(`hard-navigates when preflight request failed`, async () => {
const browser = await webdriver(context.appPort, `/error`)
await browser.eval('window.__SAME_PAGE = true')
await browser.elementByCss('#throw-on-preflight').click()
await browser.waitForElementByCss('.refreshed')
expect(await browser.eval('window.__SAME_PAGE')).toBeUndefined()
})
}

View file

@ -0,0 +1,67 @@
export async function middleware(request) {
const url = request.nextUrl
if (url.pathname === '/old-home') {
url.pathname = '/new-home'
return Response.redirect(url)
}
if (url.searchParams.get('foo') === 'bar') {
url.pathname = '/new-home'
url.searchParams.delete('foo')
return Response.redirect(url)
}
// Chained redirects
if (url.pathname === '/redirect-me-alot') {
url.pathname = '/redirect-me-alot-2'
return Response.redirect(url)
}
if (url.pathname === '/redirect-me-alot-2') {
url.pathname = '/redirect-me-alot-3'
return Response.redirect(url)
}
if (url.pathname === '/redirect-me-alot-3') {
url.pathname = '/redirect-me-alot-4'
return Response.redirect(url)
}
if (url.pathname === '/redirect-me-alot-4') {
url.pathname = '/redirect-me-alot-5'
return Response.redirect(url)
}
if (url.pathname === '/redirect-me-alot-5') {
url.pathname = '/redirect-me-alot-6'
return Response.redirect(url)
}
if (url.pathname === '/redirect-me-alot-6') {
url.pathname = '/redirect-me-alot-7'
return Response.redirect(url)
}
if (url.pathname === '/redirect-me-alot-7') {
url.pathname = '/new-home'
return Response.redirect(url)
}
// Infinite loop
if (url.pathname === '/infinite-loop') {
url.pathname = '/infinite-loop-1'
return Response.redirect(url)
}
if (url.pathname === '/infinite-loop-1') {
url.pathname = '/infinite-loop'
return Response.redirect(url)
}
if (url.pathname === '/to') {
url.pathname = url.searchParams.get('pathname')
url.searchParams.delete('pathname')
return Response.redirect(url)
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
i18n: {
locales: ['en', 'fr', 'nl'],
defaultLocale: 'en',
},
}

View file

@ -4,32 +4,32 @@ export default function Home() {
return (
<div>
<p className="title">Home Page</p>
<Link href="/redirects/old-home">
<Link href="/old-home">
<a>Redirect me to a new version of a page</a>
</Link>
<div />
<Link href="/redirects/blank-page?foo=bar">
<Link href="/blank-page?foo=bar">
<a>Redirect me with URL params intact</a>
</Link>
<div />
<Link href="/redirects/redirect-to-google">
<Link href="/redirect-to-google">
<a>Redirect me to Google (with no body response)</a>
</Link>
<div />
<Link href="/redirects/redirect-to-google">
<Link href="/redirect-to-google">
<a>Redirect me to Google (with no stream response)</a>
</Link>
<div />
<Link href="/redirects/redirect-me-alot">
<Link href="/redirect-me-alot">
<a>Redirect me alot (chained requests)</a>
</Link>
<div />
<Link href="/redirects/infinite-loop">
<Link href="/infinite-loop">
<a>Redirect me alot (infinite loop)</a>
</Link>
<div />
<Link href="/redirects/to?pathname=/api/ok" locale="nl">
<a id="link-to-api-with-locale">>Redirect me to api with locale</a>
<Link href="/to?pathname=/api/ok" locale="nl">
<a id="link-to-api-with-locale">Redirect me to api with locale</a>
</Link>
<div />
</div>

View file

@ -0,0 +1,115 @@
/* eslint-env jest */
import { join } from 'path'
import cheerio from 'cheerio'
import webdriver from 'next-webdriver'
import {
fetchViaHTTP,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
} from 'next-test-utils'
jest.setTimeout(1000 * 60 * 2)
const context = {
appDir: join(__dirname, '../'),
logs: { output: '', stdout: '', stderr: '' },
}
describe('Middleware Redirect', () => {
describe('dev mode', () => {
afterAll(() => killApp(context.app))
beforeAll(async () => {
context.appPort = await findPort()
context.app = await launchApp(context.appDir, context.appPort)
})
testsWithLocale(context)
testsWithLocale(context, '/fr')
})
describe('production mode', () => {
afterAll(() => killApp(context.app))
beforeAll(async () => {
await nextBuild(context.appDir)
context.appPort = await findPort()
context.app = await nextStart(context.appDir, context.appPort)
})
testsWithLocale(context)
testsWithLocale(context, '/fr')
})
})
function testsWithLocale(context, locale = '') {
const label = locale ? `${locale} ` : ``
it(`${label}should redirect`, async () => {
const res = await fetchViaHTTP(context.appPort, `${locale}/old-home`)
const html = await res.text()
const $ = cheerio.load(html)
const browser = await webdriver(context.appPort, `${locale}/old-home`)
try {
expect(await browser.eval(`window.location.pathname`)).toBe(
`${locale}/new-home`
)
} finally {
await browser.close()
}
expect($('.title').text()).toBe('Welcome to a new page')
})
it(`${label}should redirect cleanly with the original url param`, async () => {
const browser = await webdriver(
context.appPort,
`${locale}/blank-page?foo=bar`
)
try {
expect(
await browser.eval(
`window.location.href.replace(window.location.origin, '')`
)
).toBe(`${locale}/new-home`)
} finally {
await browser.close()
}
})
it(`${label}should redirect multiple times`, async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/redirect-me-alot`
)
const browser = await webdriver(
context.appPort,
`${locale}/redirect-me-alot`
)
try {
expect(await browser.eval(`window.location.pathname`)).toBe(
`${locale}/new-home`
)
} finally {
await browser.close()
}
const html = await res.text()
const $ = cheerio.load(html)
expect($('.title').text()).toBe('Welcome to a new page')
})
it(`${label}should redirect (infinite-loop)`, async () => {
await expect(
fetchViaHTTP(context.appPort, `${locale}/infinite-loop`)
).rejects.toThrow()
})
it(`${label}should redirect to api route with locale`, async () => {
const browser = await webdriver(context.appPort, `${locale}`)
await browser.elementByCss('#link-to-api-with-locale').click()
await browser.waitForCondition('window.location.pathname === "/api/ok"')
const body = await browser.elementByCss('body').text()
expect(body).toBe('ok')
})
}

View file

@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import { createElement } from 'react'
import { renderToString } from 'react-dom/server.browser'
import { NextResponse } from 'next/server'
import { getText } from '../../lib/utils'
import { getText } from './lib/utils'
export async function middleware(request, ev) {
// eslint-disable-next-line no-undef
@ -27,12 +27,12 @@ export async function middleware(request, ev) {
}
// Sends a header
if (url.pathname === '/responses/header') {
if (url.pathname === '/header') {
next.headers.set('x-first-header', 'valid')
return next
}
if (url.pathname === '/responses/two-cookies') {
if (url.pathname === '/two-cookies') {
const headers = new Headers()
headers.append('set-cookie', 'foo=chocochip')
headers.append('set-cookie', 'bar=chocochip')
@ -42,7 +42,7 @@ export async function middleware(request, ev) {
}
// Streams a basic response
if (url.pathname === '/responses/stream-a-response') {
if (url.pathname === '/stream-a-response') {
ev.waitUntil(
(async () => {
writer.write(encoder.encode('this is a streamed '))
@ -55,14 +55,14 @@ export async function middleware(request, ev) {
return new Response(readable)
}
if (url.pathname === '/responses/bad-status') {
if (url.pathname === '/bad-status') {
return new Response('Auth required', {
headers: { 'WWW-Authenticate': 'Basic realm="Secure Area"' },
status: 401,
})
}
if (url.pathname === '/responses/stream-long') {
if (url.pathname === '/stream-long') {
ev.waitUntil(
(async () => {
writer.write(encoder.encode('this is a streamed '.repeat(10)))
@ -79,12 +79,12 @@ export async function middleware(request, ev) {
}
// Sends response
if (url.pathname === '/responses/send-response') {
if (url.pathname === '/send-response') {
return new Response(JSON.stringify({ message: 'hi!' }))
}
// Render React component
if (url.pathname === '/responses/react') {
if (url.pathname === '/react') {
return new Response(
renderToString(
createElement(
@ -97,7 +97,7 @@ export async function middleware(request, ev) {
}
// Stream React component
if (url.pathname === '/responses/react-stream') {
if (url.pathname === '/react-stream') {
ev.waitUntil(
(async () => {
writer.write(

View file

@ -0,0 +1,6 @@
module.exports = {
i18n: {
locales: ['en', 'fr', 'nl'],
defaultLocale: 'en',
},
}

View file

@ -4,58 +4,58 @@ export default function Home({ message }) {
return (
<div>
<p className="title">Hello {message}</p>
<Link href="/responses/stream-a-response">
<Link href="/stream-a-response">
<a>Stream a response</a>
</Link>
<div />
<Link href="/responses/stream-long">
<Link href="/stream-long">
<a>Stream a long response</a>
</Link>
<Link href="/responses/stream-end-stream">
<Link href="/stream-end-stream">
<a>Test streaming after response ends</a>
</Link>
<div />
<Link href="/responses/stream-header-end">
<Link href="/stream-header-end">
<a>Attempt to add a header after stream ends</a>
</Link>
<div />
<Link href="/responses/redirect-stream">
<Link href="/redirect-stream">
<a>Redirect to Google and attempt to stream after</a>
</Link>
<div />
<Link href="/responses/header">
<Link href="/header">
<a>Respond with a header</a>
</Link>
<div />
<Link href="/responses/header?nested-header=true">
<Link href="/header?nested-header=true">
<a>Respond with 2 headers (nested middleware effect)</a>
</Link>
<div />
<Link href="/responses/body-end-header">
<Link href="/body-end-header">
<a>Respond with body, end, set a header</a>
</Link>
<div />
<Link href="/responses/body-end-body">
<Link href="/body-end-body">
<a>Respond with body, end, send another body</a>
</Link>
<div />
<Link href="/responses/send-response">
<Link href="/send-response">
<a>Respond with body</a>
</Link>
<div />
<Link href="/responses/redirect-body">
<Link href="/redirect-body">
<a>Redirect and then send a body</a>
</Link>
<div />
<Link href="/responses/react">
<Link href="/react">
<a>Send React component as a body</a>
</Link>
<div />
<Link href="/responses/react-stream">
<Link href="/react-stream">
<a>Stream React component</a>
</Link>
<div />
<Link href="/responses/bad-status">
<Link href="/bad-status">
<a>404</a>
</Link>
<div />

View file

@ -0,0 +1,125 @@
/* eslint-env jest */
import { join } from 'path'
import cheerio from 'cheerio'
import {
fetchViaHTTP,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
} from 'next-test-utils'
jest.setTimeout(1000 * 60 * 2)
const context = { appDir: join(__dirname, '../') }
describe('Middleware Responses', () => {
describe('dev mode', () => {
afterAll(() => killApp(context.app))
beforeAll(async () => {
context.appPort = await findPort()
context.app = await launchApp(context.appDir, context.appPort)
})
testsWithLocale(context)
testsWithLocale(context, '/fr')
})
describe('production mode', () => {
afterAll(() => killApp(context.app))
beforeAll(async () => {
await nextBuild(context.appDir)
context.appPort = await findPort()
context.app = await nextStart(context.appDir, context.appPort)
})
testsWithLocale(context)
testsWithLocale(context, '/fr')
})
})
function testsWithLocale(context, locale = '') {
const label = locale ? `${locale} ` : ``
it(`${label}responds with multiple cookies`, async () => {
const res = await fetchViaHTTP(context.appPort, `${locale}/two-cookies`)
expect(res.headers.raw()['set-cookie']).toEqual([
'foo=chocochip',
'bar=chocochip',
])
})
it(`${label}should stream a response`, async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/stream-a-response`
)
const html = await res.text()
expect(html).toBe('this is a streamed response with some text')
})
it(`${label}should respond with a body`, async () => {
const res = await fetchViaHTTP(context.appPort, `${locale}/send-response`)
const html = await res.text()
expect(html).toBe('{"message":"hi!"}')
})
it(`${label}should respond with a 401 status code`, async () => {
const res = await fetchViaHTTP(context.appPort, `${locale}/bad-status`)
const html = await res.text()
expect(res.status).toBe(401)
expect(html).toBe('Auth required')
})
it(`${label}should render a React component`, async () => {
const res = await fetchViaHTTP(context.appPort, `${locale}/react?name=jack`)
const html = await res.text()
expect(html).toBe('<h1>SSR with React! Hello, jack</h1>')
})
it(`${label}should stream a React component`, async () => {
const res = await fetchViaHTTP(context.appPort, `${locale}/react-stream`)
const html = await res.text()
expect(html).toBe('<h1>I am a stream</h1><p>I am another stream</p>')
})
it(`${label}should stream a long response`, async () => {
const res = await fetchViaHTTP(context.appPort, '/stream-long')
const html = await res.text()
expect(html).toBe(
'this is a streamed this is a streamed this is a streamed this is a streamed this is a streamed this is a streamed this is a streamed this is a streamed this is a streamed this is a streamed after 2 seconds after 2 seconds after 2 seconds after 2 seconds after 2 seconds after 2 seconds after 2 seconds after 2 seconds after 2 seconds after 2 seconds after 4 seconds after 4 seconds after 4 seconds after 4 seconds after 4 seconds after 4 seconds after 4 seconds after 4 seconds after 4 seconds after 4 seconds '
)
})
it(`${label}should render the right content via SSR`, async () => {
const res = await fetchViaHTTP(context.appPort, '/')
const html = await res.text()
const $ = cheerio.load(html)
expect($('.title').text()).toBe('Hello World')
})
it(`${label}should respond with one header`, async () => {
const res = await fetchViaHTTP(context.appPort, `${locale}/header`)
expect(res.headers.get('x-first-header')).toBe('valid')
})
it(`${label}should respond with two headers`, async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/header?nested-header=true`
)
expect(res.headers.get('x-first-header')).toBe('valid')
expect(res.headers.get('x-nested-header')).toBe('valid')
})
it(`${label}should respond appending headers headers`, async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/?nested-header=true&append-me=true&cookie-me=true`
)
expect(res.headers.get('x-nested-header')).toBe('valid')
expect(res.headers.get('x-append-me')).toBe('top')
expect(res.headers.raw()['set-cookie']).toEqual(['bar=chocochip'])
})
}

View file

@ -1,63 +1,62 @@
import { NextResponse } from 'next/server'
const PUBLIC_FILE = /\.(.*)$/
/**
* @param {import('next/server').NextRequest} request
*/
export async function middleware(request) {
const url = request.nextUrl
if (
url.pathname.startsWith('/rewrites/about') &&
url.searchParams.has('override')
) {
if (url.pathname.startsWith('/about') && url.searchParams.has('override')) {
const isExternal = url.searchParams.get('override') === 'external'
return NextResponse.rewrite(
isExternal
? 'https://example.vercel.sh'
: new URL('/rewrites/a', request.url)
: new URL('/ab-test/a', request.url)
)
}
if (url.pathname.startsWith('/rewrites/to-blog')) {
if (url.pathname.startsWith('/to-blog')) {
const slug = url.pathname.split('/').pop()
url.pathname = `/rewrites/fallback-true-blog/${slug}`
url.pathname = `/fallback-true-blog/${slug}`
return NextResponse.rewrite(url)
}
if (url.pathname === '/rewrites/rewrite-to-ab-test') {
if (url.pathname === '/rewrite-to-ab-test') {
let bucket = request.cookies.get('bucket')
if (!bucket) {
bucket = Math.random() >= 0.5 ? 'a' : 'b'
url.pathname = `/rewrites/${bucket}`
url.pathname = `/ab-test/${bucket}`
const response = NextResponse.rewrite(url)
response.cookies.set('bucket', bucket, { maxAge: 10 })
return response
}
url.pathname = `/rewrites/${bucket}`
url.pathname = `/${bucket}`
return NextResponse.rewrite(url)
}
if (url.pathname === '/rewrites/rewrite-me-to-about') {
url.pathname = '/rewrites/about'
if (url.pathname === '/rewrite-me-to-about') {
url.pathname = '/about'
return NextResponse.rewrite(url)
}
if (url.pathname === '/rewrites/rewrite-me-with-a-colon') {
url.pathname = '/rewrites/with:colon'
if (url.pathname === '/rewrite-me-with-a-colon') {
url.pathname = '/with:colon'
return NextResponse.rewrite(url)
}
if (url.pathname === '/rewrites/colon:here') {
url.pathname = '/rewrites/no-colon-here'
if (url.pathname === '/colon:here') {
url.pathname = '/no-colon-here'
return NextResponse.rewrite(url)
}
if (url.pathname === '/rewrites/rewrite-me-to-vercel') {
if (url.pathname === '/rewrite-me-to-vercel') {
return NextResponse.rewrite('https://example.vercel.sh')
}
if (url.pathname === '/rewrites/clear-query-params') {
if (url.pathname === '/clear-query-params') {
const allowedKeys = ['allowed']
for (const key of [...url.searchParams.keys()]) {
if (!allowedKeys.includes(key)) {
@ -68,16 +67,34 @@ export async function middleware(request) {
}
if (
url.pathname === '/rewrites/rewrite-me-without-hard-navigation' ||
url.pathname === '/rewrite-me-without-hard-navigation' ||
url.searchParams.get('path') === 'rewrite-me-without-hard-navigation'
) {
url.searchParams.set('middleware', 'foo')
url.pathname = request.cookies.has('about-bypass')
? '/rewrites/about-bypass'
: '/rewrites/about'
? '/about-bypass'
: '/about'
const response = NextResponse.rewrite(url)
response.headers.set('x-middleware-cache', 'no-cache')
return response
}
if (url.pathname.endsWith('/dynamic-replace')) {
url.pathname = '/dynamic-fallback/catch-all'
return NextResponse.rewrite(url)
}
if (url.pathname.startsWith('/country')) {
const locale = url.searchParams.get('my-locale')
if (locale) {
url.locale = locale
}
const country = url.searchParams.get('country') || 'us'
if (!PUBLIC_FILE.test(url.pathname) && !url.pathname.includes('/api/')) {
url.pathname = `/country/${country}`
return NextResponse.rewrite(url)
}
}
}

View file

@ -10,7 +10,7 @@ export default function Page(props) {
export function getStaticPaths() {
return {
paths: ['/rewrites/fallback-true-blog/first'],
paths: ['/fallback-true-blog/first'],
fallback: true,
}
}

View file

@ -7,31 +7,31 @@ export default function Home() {
<div>
<p className="title">Home Page</p>
<div />
<Link href="/rewrites/rewrite-to-ab-test">
<Link href="/rewrite-to-ab-test">
<a>A/B test homepage</a>
</Link>
<div />
<Link href="/rewrites/rewrite-me-to-about">
<Link href="/rewrite-me-to-about">
<a>Rewrite me to about</a>
</Link>
<div />
<Link href="/rewrites/rewrite-me-to-vercel">
<Link href="/rewrite-me-to-vercel">
<a>Rewrite me to Vercel</a>
</Link>
<div />
<Link href="/rewrites/rewrite-me-external-twice">
<Link href="/rewrite-me-external-twice">
<a>Redirect me to Vercel (but with double reroutes)</a>
</Link>
<div />
<Link href="/rewrites/rewrite-me-without-hard-navigation?message=refreshed">
<Link href="/rewrite-me-without-hard-navigation?message=refreshed">
<a id="link-with-rewritten-url">Rewrite me without a hard navigation</a>
</Link>
<div />
<Link href="/rewrites/about?override=external">
<Link href="/about?override=external">
<a id="override-with-external-rewrite">Rewrite me to external site</a>
</Link>
<div />
<Link href="/rewrites/about?override=internal">
<Link href="/about?override=internal">
<a id="override-with-internal-rewrite">Rewrite me to internal path</a>
</Link>
<div />
@ -41,7 +41,7 @@ export default function Home() {
onClick={(e) => {
e.preventDefault()
router.push(
'/rewrites?path=rewrite-me-without-hard-navigation&message=refreshed',
'/?path=rewrite-me-without-hard-navigation&message=refreshed',
undefined,
{ shallow: true }
)

View file

@ -0,0 +1,400 @@
/* eslint-env jest */
import { join } from 'path'
import cheerio from 'cheerio'
import webdriver from 'next-webdriver'
import {
check,
fetchViaHTTP,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
} from 'next-test-utils'
jest.setTimeout(1000 * 60 * 2)
const context = {
appDir: join(__dirname, '../'),
logs: { output: '', stdout: '', stderr: '' },
}
describe('Middleware Rewrite', () => {
describe('dev mode', () => {
afterAll(() => killApp(context.app))
beforeAll(async () => {
context.appPort = await findPort()
context.app = await launchApp(context.appDir, context.appPort, {
onStdout(msg) {
context.logs.output += msg
context.logs.stdout += msg
},
onStderr(msg) {
context.logs.output += msg
context.logs.stderr += msg
},
})
})
tests(context)
testsWithLocale(context)
testsWithLocale(context, '/fr')
})
describe('production mode', () => {
afterAll(() => killApp(context.app))
beforeAll(async () => {
await nextBuild(context.appDir, undefined)
context.appPort = await findPort()
context.app = await nextStart(context.appDir, context.appPort, {
onStdout(msg) {
context.logs.output += msg
context.logs.stdout += msg
},
onStderr(msg) {
context.logs.output += msg
context.logs.stderr += msg
},
})
})
tests(context)
testsWithLocale(context)
testsWithLocale(context, '/fr')
})
})
function tests(context, locale = '') {
it('should override with rewrite internally correctly', async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/about`,
{ override: 'internal' },
{ redirect: 'manual' }
)
expect(res.status).toBe(200)
expect(await res.text()).toContain('Welcome Page A')
const browser = await webdriver(context.appPort, `${locale}`)
await browser.elementByCss('#override-with-internal-rewrite').click()
await check(
() => browser.eval('document.documentElement.innerHTML'),
/Welcome Page A/
)
expect(await browser.eval('window.location.pathname')).toBe(
`${locale || ''}/about`
)
expect(await browser.eval('window.location.search')).toBe(
'?override=internal'
)
})
it('should override with rewrite externally correctly', async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/about`,
{ override: 'external' },
{ redirect: 'manual' }
)
expect(res.status).toBe(200)
expect(await res.text()).toContain('Example Domain')
const browser = await webdriver(context.appPort, `${locale}`)
await browser.elementByCss('#override-with-external-rewrite').click()
await check(
() => browser.eval('document.documentElement.innerHTML'),
/Example Domain/
)
await check(
() => browser.eval('window.location.pathname'),
`${locale || ''}/about`
)
await check(
() => browser.eval('window.location.search'),
'?override=external'
)
})
it('should rewrite to fallback: true page successfully', async () => {
const randomSlug = `another-${Date.now()}`
const res2 = await fetchViaHTTP(
context.appPort,
`${locale}/to-blog/${randomSlug}`
)
expect(res2.status).toBe(200)
expect(await res2.text()).toContain('Loading...')
const randomSlug2 = `another-${Date.now()}`
const browser = await webdriver(
context.appPort,
`${locale}/to-blog/${randomSlug2}`
)
await check(async () => {
const props = JSON.parse(await browser.elementByCss('#props').text())
return props.params.slug === randomSlug2
? 'success'
: JSON.stringify(props)
}, 'success')
})
it(`warns about a query param deleted`, async () => {
await fetchViaHTTP(context.appPort, `${locale}/clear-query-params`, {
a: '1',
allowed: 'kept',
})
expect(context.logs.output).toContain(
'Query params are no longer automatically merged for rewrites in middleware'
)
})
it('should allow to opt-out preflight caching', async () => {
const browser = await webdriver(context.appPort, '/')
await browser.addCookie({ name: 'about-bypass', value: '1' })
await browser.eval('window.__SAME_PAGE = true')
await browser.elementByCss('#link-with-rewritten-url').click()
await browser.waitForElementByCss('.refreshed')
await browser.deleteCookies()
expect(await browser.eval('window.__SAME_PAGE')).toBe(true)
const element = await browser.elementByCss('.title')
expect(await element.text()).toEqual('About Bypassed Page')
})
it(`should allow to rewrite keeping the locale in pathname`, async () => {
const res = await fetchViaHTTP(context.appPort, '/fr/country', {
country: 'spain',
})
const html = await res.text()
const $ = cheerio.load(html)
expect($('#locale').text()).toBe('fr')
expect($('#country').text()).toBe('spain')
})
it(`should allow to rewrite to a different locale`, async () => {
const res = await fetchViaHTTP(context.appPort, '/country', {
'my-locale': 'es',
})
const html = await res.text()
const $ = cheerio.load(html)
expect($('#locale').text()).toBe('es')
expect($('#country').text()).toBe('us')
})
}
function testsWithLocale(context, locale = '') {
const label = locale ? `${locale} ` : ``
it(`${label}should add a cookie and rewrite to a/b test`, async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/rewrite-to-ab-test`
)
const html = await res.text()
const $ = cheerio.load(html)
// Set-Cookie header with Expires should not be split into two
expect(res.headers.raw()['set-cookie']).toHaveLength(1)
const bucket = getCookieFromResponse(res, 'bucket')
const expectedText = bucket === 'a' ? 'Welcome Page A' : 'Welcome Page B'
const browser = await webdriver(
context.appPort,
`${locale}/rewrite-to-ab-test`
)
try {
expect(await browser.eval(`window.location.pathname`)).toBe(
`${locale}/rewrite-to-ab-test`
)
} finally {
await browser.close()
}
// -1 is returned if bucket was not found in func getCookieFromResponse
expect(bucket).not.toBe(-1)
expect($('.title').text()).toBe(expectedText)
})
it(`${label}should clear query parameters`, async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/clear-query-params`,
{
a: '1',
b: '2',
foo: 'bar',
allowed: 'kept',
}
)
const html = await res.text()
const $ = cheerio.load(html)
expect(JSON.parse($('#my-query-params').text())).toEqual({
allowed: 'kept',
})
})
it(`${label}should rewrite to about page`, async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/rewrite-me-to-about`
)
const html = await res.text()
const $ = cheerio.load(html)
const browser = await webdriver(
context.appPort,
`${locale}/rewrite-me-to-about`
)
try {
expect(await browser.eval(`window.location.pathname`)).toBe(
`${locale}/rewrite-me-to-about`
)
} finally {
await browser.close()
}
expect($('.title').text()).toBe('About Page')
})
it(`${label}support colons in path`, async () => {
const path = `${locale}/not:param`
const res = await fetchViaHTTP(context.appPort, path)
const html = await res.text()
const $ = cheerio.load(html)
expect($('#props').text()).toBe('not:param')
const browser = await webdriver(context.appPort, path)
try {
expect(await browser.eval(`window.location.pathname`)).toBe(path)
} finally {
await browser.close()
}
})
it(`${label}can rewrite to path with colon`, async () => {
const path = `${locale}/rewrite-me-with-a-colon`
const res = await fetchViaHTTP(context.appPort, path)
const html = await res.text()
const $ = cheerio.load(html)
expect($('#props').text()).toBe('with:colon')
const browser = await webdriver(context.appPort, path)
try {
expect(await browser.eval(`window.location.pathname`)).toBe(path)
} finally {
await browser.close()
}
})
it(`${label}can rewrite from path with colon`, async () => {
const path = `${locale}/colon:here`
const res = await fetchViaHTTP(context.appPort, path)
const html = await res.text()
const $ = cheerio.load(html)
expect($('#props').text()).toBe('no-colon-here')
const browser = await webdriver(context.appPort, path)
try {
expect(await browser.eval(`window.location.pathname`)).toBe(path)
} finally {
await browser.close()
}
})
it(`${label}can rewrite from path with colon and retain query parameter`, async () => {
const path = `${locale}/colon:here?qp=arg`
const res = await fetchViaHTTP(context.appPort, path)
const html = await res.text()
const $ = cheerio.load(html)
expect($('#props').text()).toBe('no-colon-here')
expect($('#qp').text()).toBe('arg')
const browser = await webdriver(context.appPort, path)
try {
expect(
await browser.eval(
`window.location.href.replace(window.location.origin, '')`
)
).toBe(path)
} finally {
await browser.close()
}
})
it(`${label}can rewrite to path with colon and retain query parameter`, async () => {
const path = `${locale}/rewrite-me-with-a-colon?qp=arg`
const res = await fetchViaHTTP(context.appPort, path)
const html = await res.text()
const $ = cheerio.load(html)
expect($('#props').text()).toBe('with:colon')
expect($('#qp').text()).toBe('arg')
const browser = await webdriver(context.appPort, path)
try {
expect(
await browser.eval(
`window.location.href.replace(window.location.origin, '')`
)
).toBe(path)
} finally {
await browser.close()
}
})
it(`${label}should rewrite when not using localhost`, async () => {
const res = await fetchViaHTTP(
`http://localtest.me:${context.appPort}`,
`${locale}/rewrite-me-without-hard-navigation`
)
const html = await res.text()
const $ = cheerio.load(html)
expect($('.title').text()).toBe('About Page')
})
it(`${label}should rewrite to Vercel`, async () => {
const res = await fetchViaHTTP(
context.appPort,
`${locale}/rewrite-me-to-vercel`
)
const html = await res.text()
// const browser = await webdriver(context.appPort, '/rewrite-me-to-vercel')
// TODO: running this to chech the window.location.pathname hangs for some reason;
expect(html).toContain('Example Domain')
})
it(`${label}should rewrite without hard navigation`, async () => {
const browser = await webdriver(context.appPort, '/')
await browser.eval('window.__SAME_PAGE = true')
await browser.elementByCss('#link-with-rewritten-url').click()
await browser.waitForElementByCss('.refreshed')
expect(await browser.eval('window.__SAME_PAGE')).toBe(true)
const element = await browser.elementByCss('.middleware')
expect(await element.text()).toEqual('foo')
})
it(`${label}should not call middleware with shallow push`, async () => {
const browser = await webdriver(context.appPort, '')
await browser.elementByCss('#link-to-shallow-push').click()
await browser.waitForCondition(
'new URL(window.location.href).searchParams.get("path") === "rewrite-me-without-hard-navigation"'
)
await expect(async () => {
await browser.waitForElementByCss('.refreshed', 500)
}).rejects.toThrow()
})
it(`${label}should correctly rewriting to a different dynamic path`, async () => {
const browser = await webdriver(context.appPort, '/dynamic-replace')
const element = await browser.elementByCss('.title')
expect(await element.text()).toEqual('Parts page')
const logs = await browser.log()
expect(
logs.every((log) => log.source === 'log' || log.source === 'info')
).toEqual(true)
})
}
function getCookieFromResponse(res, cookieName) {
// node-fetch bundles the cookies as string in the Response
const cookieArray = res.headers.raw()['set-cookie']
for (const cookie of cookieArray) {
let individualCookieParams = cookie.split(';')
let individualCookie = individualCookieParams[0].split('=')
if (individualCookie[0] === cookieName) {
return individualCookie[1]
}
}
return -1
}

View file

@ -1,14 +0,0 @@
import { NextResponse } from 'next/server'
export async function middleware(request) {
const url = request.nextUrl
if (
url.pathname === '/errors/throw-on-preflight' &&
request.headers.has('x-middleware-preflight')
) {
throw new Error('test error')
}
return NextResponse.next()
}

View file

@ -1,11 +0,0 @@
import { NextResponse } from 'next/server'
export async function middleware(request, ev) {
console.log(process.env.MIDDLEWARE_TEST)
return NextResponse.json({
process: {
env: process.env,
},
})
}

View file

@ -1,7 +0,0 @@
import { NextResponse } from 'next/server'
export function middleware() {
const response = NextResponse.next()
response.headers.set('x-dynamic-path', 'true')
return response
}

View file

@ -1,67 +0,0 @@
export async function middleware(request) {
const url = request.nextUrl
if (url.searchParams.get('foo') === 'bar') {
url.pathname = '/redirects/new-home'
url.searchParams.delete('foo')
return Response.redirect(url)
}
if (url.pathname === '/redirects/old-home') {
url.pathname = '/redirects/new-home'
return Response.redirect(url)
}
// Chained redirects
if (url.pathname === '/redirects/redirect-me-alot') {
url.pathname = '/redirects/redirect-me-alot-2'
return Response.redirect(url)
}
if (url.pathname === '/redirects/redirect-me-alot-2') {
url.pathname = '/redirects/redirect-me-alot-3'
return Response.redirect(url)
}
if (url.pathname === '/redirects/redirect-me-alot-3') {
url.pathname = '/redirects/redirect-me-alot-4'
return Response.redirect(url)
}
if (url.pathname === '/redirects/redirect-me-alot-4') {
url.pathname = '/redirects/redirect-me-alot-5'
return Response.redirect(url)
}
if (url.pathname === '/redirects/redirect-me-alot-5') {
url.pathname = '/redirects/redirect-me-alot-6'
return Response.redirect(url)
}
if (url.pathname === '/redirects/redirect-me-alot-6') {
url.pathname = '/redirects/redirect-me-alot-7'
return Response.redirect(url)
}
if (url.pathname === '/redirects/redirect-me-alot-7') {
url.pathname = '/redirects/new-home'
return Response.redirect(url)
}
// Infinite loop
if (url.pathname === '/redirects/infinite-loop') {
url.pathname = '/redirects/infinite-loop-1'
return Response.redirect(url)
}
if (url.pathname === '/redirects/infinite-loop-1') {
url.pathname = '/redirects/infinite-loop'
return Response.redirect(url)
}
if (url.pathname === '/redirects/to') {
url.pathname = url.searchParams.get('pathname')
url.searchParams.delete('pathname')
return Response.redirect(url)
}
}

View file

@ -1,3 +0,0 @@
export default function Account() {
return <p className="title">Welcome to a header page</p>
}

View file

@ -1,3 +0,0 @@
export default function Account() {
return <p className="title">Welcome to a old page</p>
}

Some files were not shown because too many files have changed in this diff Show more