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:
parent
cc8ab99a92
commit
f354f46b3f
145 changed files with 2381 additions and 2499 deletions
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
26
errors/nested-middleware.md
Normal file
26
errors/nested-middleware.md
Normal 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.
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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`,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 `
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
|
|
26
packages/next/lib/flat-readdir.ts
Normal file
26
packages/next/lib/flat-readdir.ts
Normal 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)
|
||||
}
|
|
@ -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')
|
||||
|
|
65
packages/next/lib/try-to-parse-path.ts
Normal file
65
packages/next/lib/try-to-parse-path.ts
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]: '',
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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$/, '') || '/'
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}$`),
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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(/\/$/, '') || '/'
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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}$`,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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') {
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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,
|
298
test/integration/middleware-general/test/index.test.js
Normal file
298
test/integration/middleware-general/test/index.test.js
Normal 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),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
1
test/integration/middleware-module-errors/middleware.js
Normal file
1
test/integration/middleware-module-errors/middleware.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default () => {}
|
|
@ -0,0 +1 @@
|
|||
export function middleware() {}
|
|
@ -0,0 +1,7 @@
|
|||
export default function About() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title">About Page</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
3
test/integration/middleware-module-errors/pages/index.js
Normal file
3
test/integration/middleware-module-errors/pages/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return <div>ok</div>
|
||||
}
|
230
test/integration/middleware-module-errors/test/index.test.js
Normal file
230
test/integration/middleware-module-errors/test/index.test.js
Normal 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'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
16
test/integration/middleware-preflight/middleware.js
Normal file
16
test/integration/middleware-preflight/middleware.js
Normal 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')
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
i18n: {
|
||||
locales: ['ja', 'en', 'fr'],
|
||||
defaultLocale: 'ja',
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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()
|
||||
})
|
||||
}
|
67
test/integration/middleware-redirects/middleware.js
Normal file
67
test/integration/middleware-redirects/middleware.js
Normal 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)
|
||||
}
|
||||
}
|
6
test/integration/middleware-redirects/next.config.js
Normal file
6
test/integration/middleware-redirects/next.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
i18n: {
|
||||
locales: ['en', 'fr', 'nl'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
}
|
|
@ -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>
|
115
test/integration/middleware-redirects/test/index.test.js
Normal file
115
test/integration/middleware-redirects/test/index.test.js
Normal 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')
|
||||
})
|
||||
}
|
|
@ -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(
|
6
test/integration/middleware-responses/next.config.js
Normal file
6
test/integration/middleware-responses/next.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
i18n: {
|
||||
locales: ['en', 'fr', 'nl'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
}
|
|
@ -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 />
|
125
test/integration/middleware-responses/test/index.test.js
Normal file
125
test/integration/middleware-responses/test/index.test.js
Normal 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'])
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
)
|
400
test/integration/middleware-rewrites/test/index.test.js
Normal file
400
test/integration/middleware-rewrites/test/index.test.js
Normal 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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export default function Account() {
|
||||
return <p className="title">Welcome to a header page</p>
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue