feat(next): Support has match and locale option on middleware config (#39257)
## Feature As the title, support `has` match, `local` that works the same with the `rewrites` and `redirects` of next.config.js on middleware config. With this PR, you can write the config like the following: ```js export const config = { matcher: [ "/foo", { source: "/bar" }, { source: "/baz", has: [ { type: 'header', key: 'x-my-header', value: 'my-value', } ] }, { source: "/en/asdf", locale: false, }, ] } ``` Also, fixes https://github.com/vercel/next.js/issues/39428 related https://github.com/vercel/edge-functions/issues/178, https://github.com/vercel/edge-functions/issues/179 - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` Co-authored-by: JJ Kasper <jj@jjsweb.site>
This commit is contained in:
parent
481950c34b
commit
b522b94cce
38 changed files with 965 additions and 263 deletions
|
@ -1,5 +1,6 @@
|
|||
import { isServerRuntime } from '../../server/config-shared'
|
||||
import type { NextConfig } from '../../server/config-shared'
|
||||
import type { Middleware, RouteHas } from '../../lib/load-custom-routes'
|
||||
import {
|
||||
extractExportedConstValue,
|
||||
UnsupportedValueError,
|
||||
|
@ -9,10 +10,17 @@ import { promises as fs } from 'fs'
|
|||
import { tryToParsePath } from '../../lib/try-to-parse-path'
|
||||
import * as Log from '../output/log'
|
||||
import { SERVER_RUNTIME } from '../../lib/constants'
|
||||
import { ServerRuntime } from '../../types'
|
||||
import { ServerRuntime } from 'next/types'
|
||||
import { checkCustomRoutes } from '../../lib/load-custom-routes'
|
||||
|
||||
interface MiddlewareConfig {
|
||||
pathMatcher: RegExp
|
||||
export interface MiddlewareConfig {
|
||||
matchers: MiddlewareMatcher[]
|
||||
}
|
||||
|
||||
export interface MiddlewareMatcher {
|
||||
regexp: string
|
||||
locale?: false
|
||||
has?: RouteHas[]
|
||||
}
|
||||
|
||||
export interface PageStaticInfo {
|
||||
|
@ -81,55 +89,63 @@ async function tryToReadFile(filePath: string, shouldThrow: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
function getMiddlewareRegExpStrings(
|
||||
function getMiddlewareMatchers(
|
||||
matcherOrMatchers: unknown,
|
||||
nextConfig: NextConfig
|
||||
): string[] {
|
||||
): MiddlewareMatcher[] {
|
||||
let matchers: unknown[] = []
|
||||
if (Array.isArray(matcherOrMatchers)) {
|
||||
return matcherOrMatchers.flatMap((matcher) =>
|
||||
getMiddlewareRegExpStrings(matcher, nextConfig)
|
||||
)
|
||||
matchers = matcherOrMatchers
|
||||
} else {
|
||||
matchers.push(matcherOrMatchers)
|
||||
}
|
||||
const { i18n } = nextConfig
|
||||
|
||||
if (typeof matcherOrMatchers !== 'string') {
|
||||
throw new Error(
|
||||
'`matcher` must be a path matcher or an array of path matchers'
|
||||
)
|
||||
}
|
||||
let routes = matchers.map(
|
||||
(m) => (typeof m === 'string' ? { source: m } : m) as Middleware
|
||||
)
|
||||
|
||||
let matcher: string = matcherOrMatchers
|
||||
// check before we process the routes and after to ensure
|
||||
// they are still valid
|
||||
checkCustomRoutes(routes, 'middleware')
|
||||
|
||||
if (!matcher.startsWith('/')) {
|
||||
throw new Error('`matcher`: path matcher must start with /')
|
||||
}
|
||||
const isRoot = matcher === '/'
|
||||
routes = routes.map((r) => {
|
||||
let { source } = r
|
||||
|
||||
if (i18n?.locales) {
|
||||
matcher = `/:nextInternalLocale([^/.]{1,})${isRoot ? '' : matcher}`
|
||||
}
|
||||
const isRoot = source === '/'
|
||||
|
||||
matcher = `/:nextData(_next/data/[^/]{1,})?${matcher}${
|
||||
isRoot
|
||||
? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?`
|
||||
: '(.json)?'
|
||||
}`
|
||||
if (i18n?.locales && r.locale !== false) {
|
||||
source = `/:nextInternalLocale([^/.]{1,})${isRoot ? '' : source}`
|
||||
}
|
||||
|
||||
if (nextConfig.basePath) {
|
||||
matcher = `${nextConfig.basePath}${matcher}`
|
||||
}
|
||||
const parsedPage = tryToParsePath(matcher)
|
||||
source = `/:nextData(_next/data/[^/]{1,})?${source}${
|
||||
isRoot
|
||||
? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?`
|
||||
: '(.json)?'
|
||||
}`
|
||||
|
||||
if (parsedPage.error) {
|
||||
throw new Error(`Invalid path matcher: ${matcher}`)
|
||||
}
|
||||
if (nextConfig.basePath) {
|
||||
source = `${nextConfig.basePath}${source}`
|
||||
}
|
||||
|
||||
const regexes = [parsedPage.regexStr].filter((x): x is string => !!x)
|
||||
if (regexes.length < 1) {
|
||||
throw new Error("Can't parse matcher")
|
||||
} else {
|
||||
return regexes
|
||||
}
|
||||
return { ...r, source }
|
||||
})
|
||||
|
||||
checkCustomRoutes(routes, 'middleware')
|
||||
|
||||
return routes.map((r) => {
|
||||
const { source, ...rest } = r
|
||||
const parsedPage = tryToParsePath(source)
|
||||
|
||||
if (parsedPage.error || !parsedPage.regexStr) {
|
||||
throw new Error(`Invalid source: ${source}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
regexp: parsedPage.regexStr,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getMiddlewareConfig(
|
||||
|
@ -139,15 +155,7 @@ function getMiddlewareConfig(
|
|||
const result: Partial<MiddlewareConfig> = {}
|
||||
|
||||
if (config.matcher) {
|
||||
result.pathMatcher = new RegExp(
|
||||
getMiddlewareRegExpStrings(config.matcher, nextConfig).join('|')
|
||||
)
|
||||
|
||||
if (result.pathMatcher.source.length > 4096) {
|
||||
throw new Error(
|
||||
`generated matcher config must be less than 4096 characters.`
|
||||
)
|
||||
}
|
||||
result.matchers = getMiddlewareMatchers(config.matcher, nextConfig)
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
|
@ -4,6 +4,10 @@ import type { EdgeSSRLoaderQuery } from './webpack/loaders/next-edge-ssr-loader'
|
|||
import type { NextConfigComplete } from '../server/config-shared'
|
||||
import type { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader'
|
||||
import type { webpack } from 'next/dist/compiled/webpack/webpack'
|
||||
import type {
|
||||
MiddlewareConfig,
|
||||
MiddlewareMatcher,
|
||||
} from './analysis/get-page-static-info'
|
||||
import type { LoadedEnvFiles } from '@next/env'
|
||||
import chalk from 'next/dist/compiled/chalk'
|
||||
import { posix, join } from 'path'
|
||||
|
@ -42,6 +46,7 @@ import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep'
|
|||
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
|
||||
import { serverComponentRegex } from './webpack/loaders/utils'
|
||||
import { ServerRuntime } from '../types'
|
||||
import { encodeMatchers } from './webpack/loaders/next-middleware-loader'
|
||||
|
||||
type ObjectValue<T> = T extends { [key: string]: infer V } ? V : never
|
||||
|
||||
|
@ -163,7 +168,7 @@ export function getEdgeServerEntry(opts: {
|
|||
isServerComponent: boolean
|
||||
page: string
|
||||
pages: { [page: string]: string }
|
||||
middleware?: { pathMatcher?: RegExp }
|
||||
middleware?: Partial<MiddlewareConfig>
|
||||
pagesType?: 'app' | 'pages' | 'root'
|
||||
appDirLoader?: string
|
||||
}) {
|
||||
|
@ -171,12 +176,9 @@ export function getEdgeServerEntry(opts: {
|
|||
const loaderParams: MiddlewareLoaderOptions = {
|
||||
absolutePagePath: opts.absolutePagePath,
|
||||
page: opts.page,
|
||||
// pathMatcher can have special characters that break the loader params
|
||||
// parsing so we base64 encode/decode the string
|
||||
matcherRegexp: Buffer.from(
|
||||
(opts.middleware?.pathMatcher && opts.middleware.pathMatcher.source) ||
|
||||
''
|
||||
).toString('base64'),
|
||||
matchers: opts.middleware?.matchers
|
||||
? encodeMatchers(opts.middleware.matchers)
|
||||
: '',
|
||||
}
|
||||
|
||||
return `next-middleware-loader?${stringify(loaderParams)}!`
|
||||
|
@ -347,7 +349,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
|
|||
const server: webpack.EntryObject = {}
|
||||
const client: webpack.EntryObject = {}
|
||||
const nestedMiddleware: string[] = []
|
||||
let middlewareRegex: string | undefined = undefined
|
||||
let middlewareMatchers: MiddlewareMatcher[] | undefined = undefined
|
||||
|
||||
const getEntryHandler =
|
||||
(mappings: Record<string, string>, pagesType: 'app' | 'pages' | 'root') =>
|
||||
|
@ -402,7 +404,9 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
|
|||
})
|
||||
|
||||
if (isMiddlewareFile(page)) {
|
||||
middlewareRegex = staticInfo.middleware?.pathMatcher?.source || '.*'
|
||||
middlewareMatchers = staticInfo.middleware?.matchers ?? [
|
||||
{ regexp: '.*' },
|
||||
]
|
||||
|
||||
if (target === 'serverless') {
|
||||
throw new MiddlewareInServerlessTargetError()
|
||||
|
@ -493,7 +497,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
|
|||
client,
|
||||
server,
|
||||
edgeServer,
|
||||
middlewareRegex,
|
||||
middlewareMatchers,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -849,7 +849,7 @@ export default async function build(
|
|||
runWebpackSpan,
|
||||
target,
|
||||
appDir,
|
||||
middlewareRegex: entrypoints.middlewareRegex,
|
||||
middlewareMatchers: entrypoints.middlewareMatchers,
|
||||
}
|
||||
|
||||
const configs = await runWebpackSpan
|
||||
|
|
|
@ -54,6 +54,7 @@ import type {
|
|||
SWC_TARGET_TRIPLE,
|
||||
} from './webpack/plugins/telemetry-plugin'
|
||||
import type { Span } from '../trace'
|
||||
import type { MiddlewareMatcher } from './analysis/get-page-static-info'
|
||||
import { withoutRSCExtensions } from './utils'
|
||||
import browserslist from 'next/dist/compiled/browserslist'
|
||||
import loadJsConfig from './load-jsconfig'
|
||||
|
@ -90,7 +91,7 @@ export function getDefineEnv({
|
|||
hasReactRoot,
|
||||
isNodeServer,
|
||||
isEdgeServer,
|
||||
middlewareRegex,
|
||||
middlewareMatchers,
|
||||
hasServerComponents,
|
||||
}: {
|
||||
dev?: boolean
|
||||
|
@ -100,7 +101,7 @@ export function getDefineEnv({
|
|||
hasReactRoot?: boolean
|
||||
isNodeServer?: boolean
|
||||
isEdgeServer?: boolean
|
||||
middlewareRegex?: string
|
||||
middlewareMatchers?: MiddlewareMatcher[]
|
||||
config: NextConfigComplete
|
||||
hasServerComponents?: boolean
|
||||
}) {
|
||||
|
@ -144,8 +145,8 @@ export function getDefineEnv({
|
|||
isEdgeServer ? 'edge' : 'nodejs'
|
||||
),
|
||||
}),
|
||||
'process.env.__NEXT_MIDDLEWARE_REGEX': JSON.stringify(
|
||||
middlewareRegex || ''
|
||||
'process.env.__NEXT_MIDDLEWARE_MATCHERS': JSON.stringify(
|
||||
middlewareMatchers || []
|
||||
),
|
||||
'process.env.__NEXT_MANUAL_CLIENT_BASE_PATH': JSON.stringify(
|
||||
config.experimental.manualClientBasePath
|
||||
|
@ -510,7 +511,7 @@ export default async function getBaseWebpackConfig(
|
|||
runWebpackSpan,
|
||||
target = COMPILER_NAMES.server,
|
||||
appDir,
|
||||
middlewareRegex,
|
||||
middlewareMatchers,
|
||||
}: {
|
||||
buildId: string
|
||||
config: NextConfigComplete
|
||||
|
@ -525,7 +526,7 @@ export default async function getBaseWebpackConfig(
|
|||
runWebpackSpan: Span
|
||||
target?: string
|
||||
appDir?: string
|
||||
middlewareRegex?: string
|
||||
middlewareMatchers?: MiddlewareMatcher[]
|
||||
}
|
||||
): Promise<webpack.Configuration> {
|
||||
const isClient = compilerType === COMPILER_NAMES.client
|
||||
|
@ -1673,7 +1674,7 @@ export default async function getBaseWebpackConfig(
|
|||
hasReactRoot,
|
||||
isNodeServer,
|
||||
isEdgeServer,
|
||||
middlewareRegex,
|
||||
middlewareMatchers,
|
||||
hasServerComponents,
|
||||
})
|
||||
),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { MiddlewareMatcher } from '../../analysis/get-page-static-info'
|
||||
import { webpack } from 'next/dist/compiled/webpack/webpack'
|
||||
|
||||
/**
|
||||
|
@ -25,7 +26,7 @@ export interface RouteMeta {
|
|||
|
||||
export interface EdgeMiddlewareMeta {
|
||||
page: string
|
||||
matcherRegexp?: string
|
||||
matchers?: MiddlewareMatcher[]
|
||||
}
|
||||
|
||||
export interface EdgeSSRMeta {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { MiddlewareMatcher } from '../../analysis/get-page-static-info'
|
||||
import { getModuleBuildInfo } from './get-module-build-info'
|
||||
import { stringifyRequest } from '../stringify-request'
|
||||
import { MIDDLEWARE_LOCATION_REGEXP } from '../../../lib/constants'
|
||||
|
@ -5,23 +6,32 @@ import { MIDDLEWARE_LOCATION_REGEXP } from '../../../lib/constants'
|
|||
export type MiddlewareLoaderOptions = {
|
||||
absolutePagePath: string
|
||||
page: string
|
||||
matcherRegexp?: string
|
||||
matchers?: string
|
||||
}
|
||||
|
||||
// matchers can have special characters that break the loader params
|
||||
// parsing so we base64 encode/decode the string
|
||||
export function encodeMatchers(matchers: MiddlewareMatcher[]) {
|
||||
return Buffer.from(JSON.stringify(matchers)).toString('base64')
|
||||
}
|
||||
|
||||
export function decodeMatchers(encodedMatchers: string) {
|
||||
return JSON.parse(
|
||||
Buffer.from(encodedMatchers, 'base64').toString()
|
||||
) as MiddlewareMatcher[]
|
||||
}
|
||||
|
||||
export default function middlewareLoader(this: any) {
|
||||
const {
|
||||
absolutePagePath,
|
||||
page,
|
||||
matcherRegexp: base64MatcherRegex,
|
||||
matchers: encodedMatchers,
|
||||
}: MiddlewareLoaderOptions = this.getOptions()
|
||||
const matcherRegexp = Buffer.from(
|
||||
base64MatcherRegex || '',
|
||||
'base64'
|
||||
).toString()
|
||||
const matchers = encodedMatchers ? decodeMatchers(encodedMatchers) : undefined
|
||||
const stringifiedPagePath = stringifyRequest(this, absolutePagePath)
|
||||
const buildInfo = getModuleBuildInfo(this._module)
|
||||
buildInfo.nextEdgeMiddleware = {
|
||||
matcherRegexp,
|
||||
matchers,
|
||||
page:
|
||||
page.replace(new RegExp(`/${MIDDLEWARE_LOCATION_REGEXP}$`), '') || '/',
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import type {
|
|||
EdgeMiddlewareMeta,
|
||||
} from '../loaders/get-module-build-info'
|
||||
import type { EdgeSSRMeta } from '../loaders/get-module-build-info'
|
||||
import type { MiddlewareMatcher } from '../../analysis/get-page-static-info'
|
||||
import { getNamedMiddlewareRegex } from '../../../shared/lib/router/utils/route-regex'
|
||||
import { getModuleBuildInfo } from '../loaders/get-module-build-info'
|
||||
import { getSortedRoutes } from '../../../shared/lib/router/utils'
|
||||
|
@ -23,13 +24,13 @@ export interface EdgeFunctionDefinition {
|
|||
files: string[]
|
||||
name: string
|
||||
page: string
|
||||
regexp: string
|
||||
matchers: MiddlewareMatcher[]
|
||||
wasm?: AssetBinding[]
|
||||
assets?: AssetBinding[]
|
||||
}
|
||||
|
||||
export interface MiddlewareManifest {
|
||||
version: 1
|
||||
version: 2
|
||||
sortedMiddleware: string[]
|
||||
middleware: { [page: string]: EdgeFunctionDefinition }
|
||||
functions: { [page: string]: EdgeFunctionDefinition }
|
||||
|
@ -49,7 +50,7 @@ const middlewareManifest: MiddlewareManifest = {
|
|||
sortedMiddleware: [],
|
||||
middleware: {},
|
||||
functions: {},
|
||||
version: 1,
|
||||
version: 2,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,14 +139,16 @@ function getCreateAssets(params: {
|
|||
const { namedRegex } = getNamedMiddlewareRegex(page, {
|
||||
catchAll: !metadata.edgeSSR && !metadata.edgeApiFunction,
|
||||
})
|
||||
const regexp = metadata?.edgeMiddleware?.matcherRegexp || namedRegex
|
||||
const matchers = metadata?.edgeMiddleware?.matchers ?? [
|
||||
{ regexp: namedRegex },
|
||||
]
|
||||
|
||||
const edgeFunctionDefinition: EdgeFunctionDefinition = {
|
||||
env: Array.from(metadata.env),
|
||||
files: getEntryFiles(entrypoint.getFiles(), metadata),
|
||||
name: entrypoint.name,
|
||||
page: page,
|
||||
regexp,
|
||||
matchers,
|
||||
wasm: Array.from(metadata.wasmBindings, ([name, filePath]) => ({
|
||||
name,
|
||||
filePath,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { ComponentType } from 'react'
|
||||
import type { RouteLoader } from './route-loader'
|
||||
import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
|
||||
import { addBasePath } from './add-base-path'
|
||||
import { interpolateAs } from '../shared/lib/router/router'
|
||||
import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route'
|
||||
|
@ -11,7 +12,7 @@ import { createRouteLoader, getClientBuildManifest } from './route-loader'
|
|||
|
||||
declare global {
|
||||
interface Window {
|
||||
__DEV_MIDDLEWARE_MANIFEST?: { location?: string }
|
||||
__DEV_MIDDLEWARE_MATCHERS?: MiddlewareMatcher[]
|
||||
__DEV_PAGES_MANIFEST?: { pages: string[] }
|
||||
__SSG_MANIFEST_CB?: () => void
|
||||
__SSG_MANIFEST?: Set<string>
|
||||
|
@ -30,7 +31,7 @@ export default class PageLoader {
|
|||
private assetPrefix: string
|
||||
private promisedSsgManifest: Promise<Set<string>>
|
||||
private promisedDevPagesManifest?: Promise<string[]>
|
||||
private promisedMiddlewareManifest?: Promise<{ location: string }>
|
||||
private promisedMiddlewareMatchers?: Promise<MiddlewareMatcher[]>
|
||||
|
||||
public routeLoader: RouteLoader
|
||||
|
||||
|
@ -80,32 +81,32 @@ export default class PageLoader {
|
|||
|
||||
getMiddleware() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const middlewareRegex = process.env.__NEXT_MIDDLEWARE_REGEX
|
||||
window.__MIDDLEWARE_MANIFEST = middlewareRegex
|
||||
? { location: middlewareRegex }
|
||||
const middlewareMatchers = process.env.__NEXT_MIDDLEWARE_MATCHERS
|
||||
window.__MIDDLEWARE_MATCHERS = middlewareMatchers
|
||||
? (middlewareMatchers as any as MiddlewareMatcher[])
|
||||
: undefined
|
||||
return window.__MIDDLEWARE_MANIFEST
|
||||
return window.__MIDDLEWARE_MATCHERS
|
||||
} else {
|
||||
if (window.__DEV_MIDDLEWARE_MANIFEST) {
|
||||
return window.__DEV_MIDDLEWARE_MANIFEST
|
||||
if (window.__DEV_MIDDLEWARE_MATCHERS) {
|
||||
return window.__DEV_MIDDLEWARE_MATCHERS
|
||||
} else {
|
||||
if (!this.promisedMiddlewareManifest) {
|
||||
if (!this.promisedMiddlewareMatchers) {
|
||||
// TODO: Decide what should happen when fetching fails instead of asserting
|
||||
// @ts-ignore
|
||||
this.promisedMiddlewareManifest = fetch(
|
||||
this.promisedMiddlewareMatchers = fetch(
|
||||
`${this.assetPrefix}/_next/static/${this.buildId}/_devMiddlewareManifest.json`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((manifest: { location?: string }) => {
|
||||
window.__DEV_MIDDLEWARE_MANIFEST = manifest
|
||||
return manifest
|
||||
.then((matchers: MiddlewareMatcher[]) => {
|
||||
window.__DEV_MIDDLEWARE_MATCHERS = matchers
|
||||
return matchers
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Failed to fetch _devMiddlewareManifest`, err)
|
||||
})
|
||||
}
|
||||
// TODO Remove this assertion as this could be undefined
|
||||
return this.promisedMiddlewareManifest!
|
||||
return this.promisedMiddlewareMatchers!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { ComponentType } from 'react'
|
||||
import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
|
||||
import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route'
|
||||
import { __unsafeCreateTrustedScriptURL } from './trusted-types'
|
||||
import { requestIdleCallback } from './request-idle-callback'
|
||||
|
@ -13,7 +14,7 @@ declare global {
|
|||
interface Window {
|
||||
__BUILD_MANIFEST?: Record<string, string[]>
|
||||
__BUILD_MANIFEST_CB?: Function
|
||||
__MIDDLEWARE_MANIFEST?: { location: string }
|
||||
__MIDDLEWARE_MATCHERS?: MiddlewareMatcher[]
|
||||
__MIDDLEWARE_MANIFEST_CB?: Function
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,12 @@ export type Redirect = {
|
|||
}
|
||||
)
|
||||
|
||||
export type Middleware = {
|
||||
source: string
|
||||
locale?: false
|
||||
has?: RouteHas[]
|
||||
}
|
||||
|
||||
const allowedHasTypes = new Set(['header', 'cookie', 'query', 'host'])
|
||||
const namedGroupsRegex = /\(\?<([a-zA-Z][a-zA-Z0-9]*)>/g
|
||||
|
||||
|
@ -111,9 +117,9 @@ function checkHeader(route: Header): string[] {
|
|||
|
||||
export type RouteType = 'rewrite' | 'redirect' | 'header'
|
||||
|
||||
function checkCustomRoutes(
|
||||
routes: Redirect[] | Header[] | Rewrite[],
|
||||
type: RouteType
|
||||
export function checkCustomRoutes(
|
||||
routes: Redirect[] | Header[] | Rewrite[] | Middleware[],
|
||||
type: RouteType | 'middleware'
|
||||
): void {
|
||||
if (!Array.isArray(routes)) {
|
||||
console.error(
|
||||
|
@ -127,17 +133,20 @@ function checkCustomRoutes(
|
|||
let hadInvalidStatus = false
|
||||
let hadInvalidHas = false
|
||||
|
||||
const allowedKeys = new Set<string>(['source', 'basePath', 'locale', 'has'])
|
||||
const allowedKeys = new Set<string>(['source', 'locale', 'has'])
|
||||
|
||||
if (type === 'rewrite') {
|
||||
allowedKeys.add('basePath')
|
||||
allowedKeys.add('destination')
|
||||
}
|
||||
if (type === 'redirect') {
|
||||
allowedKeys.add('basePath')
|
||||
allowedKeys.add('statusCode')
|
||||
allowedKeys.add('permanent')
|
||||
allowedKeys.add('destination')
|
||||
}
|
||||
if (type === 'header') {
|
||||
allowedKeys.add('basePath')
|
||||
allowedKeys.add('headers')
|
||||
}
|
||||
|
||||
|
@ -146,9 +155,11 @@ function checkCustomRoutes(
|
|||
console.error(
|
||||
`The route ${JSON.stringify(
|
||||
route
|
||||
)} is not a valid object with \`source\` and \`${
|
||||
type === 'header' ? 'headers' : 'destination'
|
||||
}\``
|
||||
)} is not a valid object with \`source\`${
|
||||
type !== 'middleware'
|
||||
? ` and \`${type === 'header' ? 'headers' : 'destination'}\``
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
numInvalidRoutes++
|
||||
continue
|
||||
|
@ -175,7 +186,11 @@ function checkCustomRoutes(
|
|||
const invalidKeys = keys.filter((key) => !allowedKeys.has(key))
|
||||
const invalidParts: string[] = []
|
||||
|
||||
if (typeof route.basePath !== 'undefined' && route.basePath !== false) {
|
||||
if (
|
||||
'basePath' in route &&
|
||||
typeof route.basePath !== 'undefined' &&
|
||||
route.basePath !== false
|
||||
) {
|
||||
invalidParts.push('`basePath` must be undefined or false')
|
||||
}
|
||||
|
||||
|
@ -237,7 +252,7 @@ function checkCustomRoutes(
|
|||
|
||||
if (type === 'header') {
|
||||
invalidParts.push(...checkHeader(route as Header))
|
||||
} else {
|
||||
} else if (type !== 'middleware') {
|
||||
let _route = route as Rewrite | Redirect
|
||||
if (!_route.destination) {
|
||||
invalidParts.push('`destination` is missing')
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { DynamicRoutes, PageChecker, Route } from './router'
|
|||
import type { FontManifest } from './font-utils'
|
||||
import type { LoadComponentsReturnType } from './load-components'
|
||||
import type { RouteMatch } from '../shared/lib/router/utils/route-matcher'
|
||||
import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-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'
|
||||
|
@ -68,6 +69,7 @@ import { getLocaleRedirect } from '../shared/lib/i18n/get-locale-redirect'
|
|||
import { getHostname } from '../shared/lib/get-hostname'
|
||||
import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url'
|
||||
import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info'
|
||||
import { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
|
||||
|
||||
export type FindComponentsResult = {
|
||||
components: LoadComponentsReturnType
|
||||
|
@ -80,6 +82,12 @@ export interface RoutingItem {
|
|||
re?: RegExp
|
||||
}
|
||||
|
||||
export interface MiddlewareRoutingItem {
|
||||
page: string
|
||||
match: MiddlewareRouteMatch
|
||||
matchers?: MiddlewareMatcher[]
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
* Object containing the configuration next.config.js
|
||||
|
|
|
@ -9,7 +9,8 @@ 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 type { MiddlewareRoutingItem, RoutingItem } from '../base-server'
|
||||
import type { MiddlewareMatcher } from '../../build/analysis/get-page-static-info'
|
||||
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
} from '../../shared/lib/constants'
|
||||
import Server, { WrappedBuildError } from '../next-server'
|
||||
import { getRouteMatcher } from '../../shared/lib/router/utils/route-matcher'
|
||||
import { getMiddlewareRouteMatcher } from '../../shared/lib/router/utils/middleware-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'
|
||||
|
@ -60,10 +62,7 @@ import {
|
|||
} from 'next/dist/compiled/@next/react-dev-overlay/dist/middleware'
|
||||
import * as Log from '../../build/output/log'
|
||||
import isError, { getProperError } from '../../lib/is-error'
|
||||
import {
|
||||
getMiddlewareRegex,
|
||||
getRouteRegex,
|
||||
} from '../../shared/lib/router/utils/route-regex'
|
||||
import { 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'
|
||||
|
@ -106,7 +105,7 @@ export default class DevServer extends Server {
|
|||
private pagesDir: string
|
||||
private appDir?: string
|
||||
private actualMiddlewareFile?: string
|
||||
private middleware?: RoutingItem
|
||||
private middleware?: MiddlewareRoutingItem
|
||||
private edgeFunctions?: RoutingItem[]
|
||||
private verifyingTypeScript?: boolean
|
||||
private usingTypeScript?: boolean
|
||||
|
@ -305,11 +304,12 @@ export default class DevServer extends Server {
|
|||
let enabledTypeScript = this.usingTypeScript
|
||||
|
||||
wp.on('aggregated', async () => {
|
||||
let middlewareMatcher: RegExp | undefined
|
||||
let middlewareMatchers: MiddlewareMatcher[] | undefined
|
||||
const routedPages: string[] = []
|
||||
const knownFiles = wp.getTimeInfoEntries()
|
||||
const appPaths: Record<string, string> = {}
|
||||
const edgeRoutesSet = new Set<string>()
|
||||
|
||||
let envChange = false
|
||||
let tsconfigChange = false
|
||||
|
||||
|
@ -369,9 +369,9 @@ export default class DevServer extends Server {
|
|||
|
||||
if (isMiddlewareFile(rootFile)) {
|
||||
this.actualMiddlewareFile = rootFile
|
||||
middlewareMatcher =
|
||||
staticInfo.middleware?.pathMatcher || new RegExp('.*')
|
||||
edgeRoutesSet.add('/')
|
||||
middlewareMatchers = staticInfo.middleware?.matchers || [
|
||||
{ regexp: '.*' },
|
||||
]
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -536,31 +536,29 @@ export default class DevServer extends Server {
|
|||
}
|
||||
|
||||
this.appPathRoutes = appPaths
|
||||
this.edgeFunctions = []
|
||||
const edgeRoutes = Array.from(edgeRoutesSet)
|
||||
getSortedRoutes(edgeRoutes).forEach((page) => {
|
||||
let appPath = this.getOriginalAppPath(page)
|
||||
this.edgeFunctions = getSortedRoutes(edgeRoutes).map((page) => {
|
||||
const appPath = this.getOriginalAppPath(page)
|
||||
|
||||
if (typeof appPath === 'string') {
|
||||
page = appPath
|
||||
}
|
||||
const isRootMiddleware = page === '/' && !!middlewareMatcher
|
||||
|
||||
const middlewareRegex = isRootMiddleware
|
||||
? { re: middlewareMatcher!, groups: {} }
|
||||
: getMiddlewareRegex(page, { catchAll: false })
|
||||
const routeItem = {
|
||||
match: getRouteMatcher(middlewareRegex),
|
||||
const edgeRegex = getRouteRegex(page)
|
||||
return {
|
||||
match: getRouteMatcher(edgeRegex),
|
||||
page,
|
||||
re: middlewareRegex.re,
|
||||
}
|
||||
if (isRootMiddleware) {
|
||||
this.middleware = routeItem
|
||||
} else {
|
||||
this.edgeFunctions!.push(routeItem)
|
||||
re: edgeRegex.re,
|
||||
}
|
||||
})
|
||||
|
||||
this.middleware = middlewareMatchers
|
||||
? {
|
||||
match: getMiddlewareRouteMatcher(middlewareMatchers),
|
||||
page: '/',
|
||||
matchers: middlewareMatchers,
|
||||
}
|
||||
: undefined
|
||||
|
||||
try {
|
||||
// we serve a separate manifest with all pages for the client in
|
||||
// dev mode so that we can match a page after a rewrite on the client
|
||||
|
@ -828,6 +826,7 @@ export default class DevServer extends Server {
|
|||
response: BaseNextResponse
|
||||
parsedUrl: ParsedUrl
|
||||
parsed: UrlWithParsedQuery
|
||||
middlewareList: MiddlewareRoutingItem[]
|
||||
}) {
|
||||
try {
|
||||
const result = await super.runMiddleware({
|
||||
|
@ -1158,17 +1157,7 @@ export default class DevServer extends Server {
|
|||
fn: async (_req, res) => {
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
res
|
||||
.body(
|
||||
JSON.stringify(
|
||||
this.middleware
|
||||
? {
|
||||
location: this.middleware.re!.source,
|
||||
}
|
||||
: {}
|
||||
)
|
||||
)
|
||||
.send()
|
||||
res.body(JSON.stringify(this.getMiddleware()?.matchers ?? [])).send()
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import type {
|
|||
Params,
|
||||
RouteMatch,
|
||||
} from '../shared/lib/router/utils/route-matcher'
|
||||
import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher'
|
||||
import type { NextConfig } from './config-shared'
|
||||
import type { DynamicRoutes, PageChecker } from './router'
|
||||
|
||||
|
@ -71,6 +72,7 @@ import BaseServer, {
|
|||
Options,
|
||||
FindComponentsResult,
|
||||
prepareServerlessUrl,
|
||||
MiddlewareRoutingItem,
|
||||
RoutingItem,
|
||||
NoFallbackError,
|
||||
RequestContext,
|
||||
|
@ -86,6 +88,7 @@ import { relativizeURL } from '../shared/lib/router/utils/relativize-url'
|
|||
import { prepareDestination } from '../shared/lib/router/utils/prepare-destination'
|
||||
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
|
||||
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
|
||||
import { getMiddlewareRouteMatcher } from '../shared/lib/router/utils/middleware-route-matcher'
|
||||
import { loadEnvConfig } from '@next/env'
|
||||
import { getCustomRoute, stringifyQuery } from './server-route-utils'
|
||||
import { urlQueryToSearchParams } from '../shared/lib/router/utils/querystring'
|
||||
|
@ -123,28 +126,55 @@ export interface NodeRequestHandler {
|
|||
|
||||
const MiddlewareMatcherCache = new WeakMap<
|
||||
MiddlewareManifest['middleware'][string],
|
||||
MiddlewareRouteMatch
|
||||
>()
|
||||
|
||||
const EdgeMatcherCache = new WeakMap<
|
||||
MiddlewareManifest['functions'][string],
|
||||
RouteMatch
|
||||
>()
|
||||
|
||||
function getMiddlewareMatcher(
|
||||
info: MiddlewareManifest['middleware'][string]
|
||||
): RouteMatch {
|
||||
): MiddlewareRouteMatch {
|
||||
const stored = MiddlewareMatcherCache.get(info)
|
||||
if (stored) {
|
||||
return stored
|
||||
}
|
||||
|
||||
if (typeof info.regexp !== 'string' || !info.regexp) {
|
||||
if (!Array.isArray(info.matchers)) {
|
||||
throw new Error(
|
||||
`Invariant: invalid regexp for middleware ${JSON.stringify(info)}`
|
||||
`Invariant: invalid matchers for middleware ${JSON.stringify(info)}`
|
||||
)
|
||||
}
|
||||
|
||||
const matcher = getRouteMatcher({ re: new RegExp(info.regexp), groups: {} })
|
||||
const matcher = getMiddlewareRouteMatcher(info.matchers)
|
||||
MiddlewareMatcherCache.set(info, matcher)
|
||||
return matcher
|
||||
}
|
||||
|
||||
function getEdgeMatcher(
|
||||
info: MiddlewareManifest['functions'][string]
|
||||
): RouteMatch {
|
||||
const stored = EdgeMatcherCache.get(info)
|
||||
if (stored) {
|
||||
return stored
|
||||
}
|
||||
|
||||
if (!Array.isArray(info.matchers) || info.matchers.length !== 1) {
|
||||
throw new Error(
|
||||
`Invariant: invalid matchers for middleware ${JSON.stringify(info)}`
|
||||
)
|
||||
}
|
||||
|
||||
const matcher = getRouteMatcher({
|
||||
re: new RegExp(info.matchers[0].regexp),
|
||||
groups: {},
|
||||
})
|
||||
EdgeMatcherCache.set(info, matcher)
|
||||
return matcher
|
||||
}
|
||||
|
||||
export default class NextNodeServer extends BaseServer {
|
||||
private imageResponseCache?: ResponseCache
|
||||
|
||||
|
@ -1491,7 +1521,7 @@ export default class NextNodeServer extends BaseServer {
|
|||
}
|
||||
|
||||
/** Returns the middleware routing item if there is one. */
|
||||
protected getMiddleware(): RoutingItem | undefined {
|
||||
protected getMiddleware(): MiddlewareRoutingItem | undefined {
|
||||
const manifest = this.getMiddlewareManifest()
|
||||
const middleware = manifest?.middleware?.['/']
|
||||
if (!middleware) {
|
||||
|
@ -1511,7 +1541,7 @@ export default class NextNodeServer extends BaseServer {
|
|||
}
|
||||
|
||||
return Object.keys(manifest.functions).map((page) => ({
|
||||
match: getMiddlewareMatcher(manifest.functions[page]),
|
||||
match: getEdgeMatcher(manifest.functions[page]),
|
||||
page,
|
||||
}))
|
||||
}
|
||||
|
@ -1637,10 +1667,6 @@ export default class NextNodeServer extends BaseServer {
|
|||
}
|
||||
}
|
||||
|
||||
const allHeaders = new Headers()
|
||||
let result: FetchEventResult | null = null
|
||||
const method = (params.request.method || 'GET').toUpperCase()
|
||||
|
||||
const middleware = this.getMiddleware()
|
||||
if (!middleware) {
|
||||
return { finished: false }
|
||||
|
@ -1649,50 +1675,52 @@ export default class NextNodeServer extends BaseServer {
|
|||
return { finished: false }
|
||||
}
|
||||
|
||||
if (middleware && middleware.match(normalizedPathname)) {
|
||||
await this.ensureMiddleware()
|
||||
const middlewareInfo = this.getEdgeFunctionInfo({
|
||||
page: middleware.page,
|
||||
middleware: true,
|
||||
})
|
||||
await this.ensureMiddleware()
|
||||
const middlewareInfo = this.getEdgeFunctionInfo({
|
||||
page: middleware.page,
|
||||
middleware: true,
|
||||
})
|
||||
|
||||
if (!middlewareInfo) {
|
||||
throw new MiddlewareNotFoundError()
|
||||
}
|
||||
if (!middlewareInfo) {
|
||||
throw new MiddlewareNotFoundError()
|
||||
}
|
||||
|
||||
result = await run({
|
||||
distDir: this.distDir,
|
||||
name: middlewareInfo.name,
|
||||
paths: middlewareInfo.paths,
|
||||
env: middlewareInfo.env,
|
||||
edgeFunctionEntry: middlewareInfo,
|
||||
request: {
|
||||
headers: params.request.headers,
|
||||
method,
|
||||
nextConfig: {
|
||||
basePath: this.nextConfig.basePath,
|
||||
i18n: this.nextConfig.i18n,
|
||||
trailingSlash: this.nextConfig.trailingSlash,
|
||||
},
|
||||
url: url,
|
||||
page: page,
|
||||
body: getRequestMeta(params.request, '__NEXT_CLONABLE_BODY'),
|
||||
const method = (params.request.method || 'GET').toUpperCase()
|
||||
|
||||
const result = await run({
|
||||
distDir: this.distDir,
|
||||
name: middlewareInfo.name,
|
||||
paths: middlewareInfo.paths,
|
||||
env: middlewareInfo.env,
|
||||
edgeFunctionEntry: middlewareInfo,
|
||||
request: {
|
||||
headers: params.request.headers,
|
||||
method,
|
||||
nextConfig: {
|
||||
basePath: this.nextConfig.basePath,
|
||||
i18n: this.nextConfig.i18n,
|
||||
trailingSlash: this.nextConfig.trailingSlash,
|
||||
},
|
||||
useCache: !this.nextConfig.experimental.runtime,
|
||||
onWarning: params.onWarning,
|
||||
url: url,
|
||||
page: page,
|
||||
body: getRequestMeta(params.request, '__NEXT_CLONABLE_BODY'),
|
||||
},
|
||||
useCache: !this.nextConfig.experimental.runtime,
|
||||
onWarning: params.onWarning,
|
||||
})
|
||||
|
||||
const allHeaders = new Headers()
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
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) {
|
||||
|
@ -1734,7 +1762,7 @@ export default class NextNodeServer extends BaseServer {
|
|||
const normalizedPathname = removeTrailingSlash(
|
||||
parsed.pathname || ''
|
||||
)
|
||||
if (!middleware.match(normalizedPathname)) {
|
||||
if (!middleware.match(normalizedPathname, req, parsedUrl.query)) {
|
||||
return { finished: false }
|
||||
}
|
||||
|
||||
|
|
|
@ -91,21 +91,27 @@ interface MiddlewareEffectParams<T extends FetchDataOutput> {
|
|||
router: Router
|
||||
}
|
||||
|
||||
function matchesMiddleware<T extends FetchDataOutput>(
|
||||
export async function matchesMiddleware<T extends FetchDataOutput>(
|
||||
options: MiddlewareEffectParams<T>
|
||||
): Promise<boolean> {
|
||||
return Promise.resolve(options.router.pageLoader.getMiddleware()).then(
|
||||
(middleware) => {
|
||||
const { pathname: asPathname } = parsePath(options.asPath)
|
||||
const cleanedAs = hasBasePath(asPathname)
|
||||
? removeBasePath(asPathname)
|
||||
: asPathname
|
||||
const matchers = await Promise.resolve(
|
||||
options.router.pageLoader.getMiddleware()
|
||||
)
|
||||
if (!matchers) return false
|
||||
|
||||
const regex = middleware?.location
|
||||
return (
|
||||
!!regex && new RegExp(regex).test(addLocale(cleanedAs, options.locale))
|
||||
)
|
||||
}
|
||||
const { pathname: asPathname } = parsePath(options.asPath)
|
||||
// remove basePath first since path prefix has to be in the order of `/${basePath}/${locale}`
|
||||
const cleanedAs = hasBasePath(asPathname)
|
||||
? removeBasePath(asPathname)
|
||||
: asPathname
|
||||
const asWithBasePathAndLocale = addBasePath(
|
||||
addLocale(cleanedAs, options.locale)
|
||||
)
|
||||
|
||||
// Check only path match on client. Matching "has" should be done on server
|
||||
// where we can access more info such as headers, HttpOnly cookie, etc.
|
||||
return matchers.some((m) =>
|
||||
new RegExp(m.regexp).test(asWithBasePathAndLocale)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import type { BaseNextRequest } from '../../../../server/base-http'
|
||||
import type { MiddlewareMatcher } from '../../../../build/analysis/get-page-static-info'
|
||||
import type { Params } from './route-matcher'
|
||||
import { matchHas } from './prepare-destination'
|
||||
|
||||
export interface MiddlewareRouteMatch {
|
||||
(
|
||||
pathname: string | null | undefined,
|
||||
request: BaseNextRequest,
|
||||
query: Params
|
||||
): boolean
|
||||
}
|
||||
|
||||
export function getMiddlewareRouteMatcher(
|
||||
matchers: MiddlewareMatcher[]
|
||||
): MiddlewareRouteMatch {
|
||||
return (
|
||||
pathname: string | null | undefined,
|
||||
req: BaseNextRequest,
|
||||
query: Params
|
||||
) => {
|
||||
for (const matcher of matchers) {
|
||||
const routeMatch = new RegExp(matcher.regexp).exec(pathname!)
|
||||
if (!routeMatch) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (matcher.has) {
|
||||
const hasParams = matchHas(req, matcher.has, query)
|
||||
if (!hasParams) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -144,36 +144,7 @@ export function getNamedRouteRegex(normalizedRoute: string) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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 ? '.*' : ''
|
||||
return {
|
||||
groups: {},
|
||||
re: new RegExp(`^/${catchAllRegex}$`),
|
||||
}
|
||||
}
|
||||
|
||||
let catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : ''
|
||||
return {
|
||||
groups: groups,
|
||||
re: new RegExp(`^${parameterizedRoute}${catchAllGroupedRegex}$`),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A server version for getMiddlewareRegex that generates a named regexp.
|
||||
* Generates a named regexp.
|
||||
* This is intended to be using for build time only.
|
||||
*/
|
||||
export function getNamedMiddlewareRegex(
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export default function middleware(request) {
|
||||
const nextUrl = request.nextUrl.clone()
|
||||
nextUrl.pathname = '/'
|
||||
const res = NextResponse.rewrite(nextUrl)
|
||||
res.headers.set('X-From-Middleware', 'true')
|
||||
return res
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
{
|
||||
source: '/hello',
|
||||
},
|
||||
],
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
basePath: '/docs',
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
export default (props) => (
|
||||
<>
|
||||
<h1>home</h1>
|
||||
<div id="from-middleware">{props.fromMiddleware}</div>
|
||||
</>
|
||||
)
|
||||
|
||||
export async function getServerSideProps({ res }) {
|
||||
return {
|
||||
props: {
|
||||
fromMiddleware: res.getHeader('x-from-middleware') || null,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default (props) => (
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/hello">
|
||||
<a id="hello">/hello</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
|
@ -0,0 +1,56 @@
|
|||
/* eslint-env jest */
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
|
||||
import { join } from 'path'
|
||||
import webdriver from 'next-webdriver'
|
||||
import { fetchViaHTTP } from 'next-test-utils'
|
||||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
|
||||
const itif = (condition: boolean) => (condition ? it : it.skip)
|
||||
|
||||
const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy'
|
||||
|
||||
describe('Middleware custom matchers basePath', () => {
|
||||
let next: NextInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: new FileRef(join(__dirname, '../app')),
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
// FIXME
|
||||
// See https://linear.app/vercel/issue/EC-170/middleware-rewrite-of-nextjs-with-basepath-does-not-work-on-vercel
|
||||
itif(!isModeDeploy)('should match', async () => {
|
||||
for (const path of [
|
||||
'/docs/hello',
|
||||
`/docs/_next/data/${next.buildId}/hello.json`,
|
||||
]) {
|
||||
const res = await fetchViaHTTP(next.url, path)
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-from-middleware')).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['/hello', '/invalid/docs/hello'])(
|
||||
'should not match',
|
||||
async (path) => {
|
||||
const res = await fetchViaHTTP(next.url, path)
|
||||
expect(res.status).toBe(404)
|
||||
}
|
||||
)
|
||||
|
||||
// FIXME:
|
||||
// See https://linear.app/vercel/issue/EC-160/header-value-set-on-middleware-is-not-propagated-on-client-request-of
|
||||
itif(!isModeDeploy)('should match has query on client routing', async () => {
|
||||
const browser = await webdriver(next.url, '/docs/routes')
|
||||
await browser.eval('window.__TEST_NO_RELOAD = true')
|
||||
await browser.elementById('hello').click()
|
||||
const fromMiddleware = await browser.elementById('from-middleware').text()
|
||||
expect(fromMiddleware).toBe('true')
|
||||
const noReload = await browser.eval('window.__TEST_NO_RELOAD')
|
||||
expect(noReload).toBe(true)
|
||||
})
|
||||
})
|
21
test/e2e/middleware-custom-matchers-i18n/app/middleware.js
Normal file
21
test/e2e/middleware-custom-matchers-i18n/app/middleware.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export default function middleware(request) {
|
||||
const nextUrl = request.nextUrl.clone()
|
||||
nextUrl.pathname = '/'
|
||||
const res = NextResponse.rewrite(nextUrl)
|
||||
res.headers.set('X-From-Middleware', 'true')
|
||||
return res
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
{
|
||||
source: '/hello',
|
||||
},
|
||||
{
|
||||
source: '/nl-NL/about',
|
||||
locale: false,
|
||||
},
|
||||
],
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
i18n: {
|
||||
locales: ['en', 'nl-NL'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
}
|
14
test/e2e/middleware-custom-matchers-i18n/app/pages/index.js
Normal file
14
test/e2e/middleware-custom-matchers-i18n/app/pages/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default (props) => (
|
||||
<>
|
||||
<h1>home</h1>
|
||||
<div id="from-middleware">{props.fromMiddleware}</div>
|
||||
</>
|
||||
)
|
||||
|
||||
export async function getServerSideProps({ res }) {
|
||||
return {
|
||||
props: {
|
||||
fromMiddleware: res.getHeader('x-from-middleware') || null,
|
||||
},
|
||||
}
|
||||
}
|
26
test/e2e/middleware-custom-matchers-i18n/app/pages/routes.js
Normal file
26
test/e2e/middleware-custom-matchers-i18n/app/pages/routes.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default (props) => (
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/hello">
|
||||
<a id="hello">/hello</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/en/hello">
|
||||
<a id="en_hello">/en/hello</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/nl-NL/hello">
|
||||
<a id="nl-NL_hello">/nl-NL/hello</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/nl-NL/about">
|
||||
<a id="nl-NL_about">/nl-NL/about</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
55
test/e2e/middleware-custom-matchers-i18n/test/index.test.ts
Normal file
55
test/e2e/middleware-custom-matchers-i18n/test/index.test.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/* eslint-env jest */
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
|
||||
import { join } from 'path'
|
||||
import webdriver from 'next-webdriver'
|
||||
import { fetchViaHTTP } from 'next-test-utils'
|
||||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
|
||||
const itif = (condition: boolean) => (condition ? it : it.skip)
|
||||
|
||||
const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy'
|
||||
|
||||
describe('Middleware custom matchers i18n', () => {
|
||||
let next: NextInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: new FileRef(join(__dirname, '../app')),
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it.each(['/hello', '/en/hello', '/nl-NL/hello', '/nl-NL/about'])(
|
||||
'should match',
|
||||
async (path) => {
|
||||
const res = await fetchViaHTTP(next.url, path)
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-from-middleware')).toBeDefined()
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['/invalid/hello', '/hello/invalid', '/about', '/en/about'])(
|
||||
'should not match',
|
||||
async (path) => {
|
||||
const res = await fetchViaHTTP(next.url, path)
|
||||
expect(res.status).toBe(404)
|
||||
}
|
||||
)
|
||||
|
||||
// FIXME:
|
||||
// See https://linear.app/vercel/issue/EC-160/header-value-set-on-middleware-is-not-propagated-on-client-request-of
|
||||
itif(!isModeDeploy).each(['hello', 'en_hello', 'nl-NL_hello', 'nl-NL_about'])(
|
||||
'should match has query on client routing',
|
||||
async (id) => {
|
||||
const browser = await webdriver(next.url, '/routes')
|
||||
await browser.eval('window.__TEST_NO_RELOAD = true')
|
||||
await browser.elementById(id).click()
|
||||
const fromMiddleware = await browser.elementById('from-middleware').text()
|
||||
expect(fromMiddleware).toBe('true')
|
||||
const noReload = await browser.eval('window.__TEST_NO_RELOAD')
|
||||
expect(noReload).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
61
test/e2e/middleware-custom-matchers/app/middleware.js
Normal file
61
test/e2e/middleware-custom-matchers/app/middleware.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export default function middleware(request) {
|
||||
const res = NextResponse.rewrite(new URL('/', request.url))
|
||||
res.headers.set('X-From-Middleware', 'true')
|
||||
return res
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
{ source: '/source-match' },
|
||||
{
|
||||
source: '/has-match-1',
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'x-my-header',
|
||||
value: '(?<myHeader>.*)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/has-match-2',
|
||||
has: [
|
||||
{
|
||||
type: 'query',
|
||||
key: 'my-query',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/has-match-3',
|
||||
has: [
|
||||
{
|
||||
type: 'cookie',
|
||||
key: 'loggedIn',
|
||||
value: '(?<loggedIn>true)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/has-match-4',
|
||||
has: [
|
||||
{
|
||||
type: 'host',
|
||||
value: 'example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/has-match-5',
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'hasParam',
|
||||
value: 'with-params',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
14
test/e2e/middleware-custom-matchers/app/pages/index.js
Normal file
14
test/e2e/middleware-custom-matchers/app/pages/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default (props) => (
|
||||
<>
|
||||
<h1>home</h1>
|
||||
<div id="from-middleware">{props.fromMiddleware}</div>
|
||||
</>
|
||||
)
|
||||
|
||||
export async function getServerSideProps({ res }) {
|
||||
return {
|
||||
props: {
|
||||
fromMiddleware: res.getHeader('x-from-middleware') || null,
|
||||
},
|
||||
}
|
||||
}
|
16
test/e2e/middleware-custom-matchers/app/pages/routes.js
Normal file
16
test/e2e/middleware-custom-matchers/app/pages/routes.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default (props) => (
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/has-match-2?my-query=hellooo">
|
||||
<a id="has-match-2">has-match-2</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/has-match-3">
|
||||
<a id="has-match-3">has-match-3</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
145
test/e2e/middleware-custom-matchers/test/index.test.ts
Normal file
145
test/e2e/middleware-custom-matchers/test/index.test.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
/* eslint-env jest */
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
import { join } from 'path'
|
||||
import webdriver from 'next-webdriver'
|
||||
import { fetchViaHTTP } from 'next-test-utils'
|
||||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
|
||||
const itif = (condition: boolean) => (condition ? it : it.skip)
|
||||
|
||||
const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy'
|
||||
|
||||
describe('Middleware custom matchers', () => {
|
||||
let next: NextInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: new FileRef(join(__dirname, '../app')),
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
const runTests = () => {
|
||||
it('should match source path', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/source-match')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-from-middleware')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should match has header', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/has-match-1', undefined, {
|
||||
headers: {
|
||||
'x-my-header': 'hello world!!',
|
||||
},
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-from-middleware')).toBeDefined()
|
||||
|
||||
const res2 = await fetchViaHTTP(next.url, '/has-match-1')
|
||||
expect(res2.status).toBe(404)
|
||||
})
|
||||
|
||||
it('should match has query', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/has-match-2', {
|
||||
'my-query': 'hellooo',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-from-middleware')).toBeDefined()
|
||||
|
||||
const res2 = await fetchViaHTTP(next.url, '/has-match-2')
|
||||
expect(res2.status).toBe(404)
|
||||
})
|
||||
|
||||
it('should match has cookie', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/has-match-3', undefined, {
|
||||
headers: {
|
||||
cookie: 'loggedIn=true',
|
||||
},
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-from-middleware')).toBeDefined()
|
||||
|
||||
const res2 = await fetchViaHTTP(next.url, '/has-match-3', undefined, {
|
||||
headers: {
|
||||
cookie: 'loggedIn=false',
|
||||
},
|
||||
})
|
||||
expect(res2.status).toBe(404)
|
||||
})
|
||||
|
||||
// Cannot modify host when testing with real deployment
|
||||
itif(!isModeDeploy)('should match has host', async () => {
|
||||
const res1 = await fetchViaHTTP(next.url, '/has-match-4')
|
||||
expect(res1.status).toBe(404)
|
||||
|
||||
const res = await fetchViaHTTP(next.url, '/has-match-4', undefined, {
|
||||
headers: {
|
||||
host: 'example.com',
|
||||
},
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-from-middleware')).toBeDefined()
|
||||
|
||||
const res2 = await fetchViaHTTP(next.url, '/has-match-4', undefined, {
|
||||
headers: {
|
||||
host: 'example.org',
|
||||
},
|
||||
})
|
||||
expect(res2.status).toBe(404)
|
||||
})
|
||||
|
||||
it('should match has header value', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/has-match-5', undefined, {
|
||||
headers: {
|
||||
hasParam: 'with-params',
|
||||
},
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-from-middleware')).toBeDefined()
|
||||
|
||||
const res2 = await fetchViaHTTP(next.url, '/has-match-5', undefined, {
|
||||
headers: {
|
||||
hasParam: 'without-params',
|
||||
},
|
||||
})
|
||||
expect(res2.status).toBe(404)
|
||||
})
|
||||
|
||||
// FIXME: Test fails on Vercel deployment for now.
|
||||
// See https://linear.app/vercel/issue/EC-160/header-value-set-on-middleware-is-not-propagated-on-client-request-of
|
||||
itif(!isModeDeploy)(
|
||||
'should match has query on client routing',
|
||||
async () => {
|
||||
const browser = await webdriver(next.url, '/routes')
|
||||
await browser.eval('window.__TEST_NO_RELOAD = true')
|
||||
await browser.elementById('has-match-2').click()
|
||||
const fromMiddleware = await browser
|
||||
.elementById('from-middleware')
|
||||
.text()
|
||||
expect(fromMiddleware).toBe('true')
|
||||
const noReload = await browser.eval('window.__TEST_NO_RELOAD')
|
||||
expect(noReload).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
itif(!isModeDeploy)(
|
||||
'should match has cookie on client routing',
|
||||
async () => {
|
||||
const browser = await webdriver(next.url, '/routes')
|
||||
await browser.addCookie({ name: 'loggedIn', value: 'true' })
|
||||
await browser.refresh()
|
||||
await browser.eval('window.__TEST_NO_RELOAD = true')
|
||||
await browser.elementById('has-match-3').click()
|
||||
const fromMiddleware = await browser
|
||||
.elementById('from-middleware')
|
||||
.text()
|
||||
expect(fromMiddleware).toBe('true')
|
||||
const noReload = await browser.eval('window.__TEST_NO_RELOAD')
|
||||
expect(noReload).toBe(true)
|
||||
}
|
||||
)
|
||||
}
|
||||
runTests()
|
||||
})
|
|
@ -99,8 +99,8 @@ describe('Middleware Runtime', () => {
|
|||
next.url,
|
||||
`/_next/static/${next.buildId}/_devMiddlewareManifest.json`
|
||||
)
|
||||
const { location } = await res.json()
|
||||
expect(location).toBe('.*')
|
||||
const matchers = await res.json()
|
||||
expect(matchers).toEqual([{ regexp: '.*' }])
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -119,7 +119,7 @@ describe('Middleware Runtime', () => {
|
|||
files: ['server/edge-runtime-webpack.js', 'server/middleware.js'],
|
||||
name: 'middleware',
|
||||
page: '/',
|
||||
regexp: '^/.*$',
|
||||
matchers: [{ regexp: '^/.*$' }],
|
||||
wasm: [],
|
||||
assets: [],
|
||||
},
|
||||
|
|
|
@ -94,20 +94,19 @@ describe('Middleware can set the matcher in its config', () => {
|
|||
expect(response.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
|
||||
it('should load matches in client manifest correctly', async () => {
|
||||
it('should load matches in client matchers correctly', async () => {
|
||||
const browser = await webdriver(next.url, '/')
|
||||
|
||||
await check(async () => {
|
||||
const manifest = await browser.eval(
|
||||
const matchers = await browser.eval(
|
||||
(global as any).isNextDev
|
||||
? 'window.__DEV_MIDDLEWARE_MANIFEST'
|
||||
: 'window.__MIDDLEWARE_MANIFEST'
|
||||
? 'window.__DEV_MIDDLEWARE_MATCHERS'
|
||||
: 'window.__MIDDLEWARE_MATCHERS'
|
||||
)
|
||||
|
||||
const { location } = manifest
|
||||
return location &&
|
||||
location.includes('with-middleware') &&
|
||||
location.includes('another-middleware')
|
||||
return matchers &&
|
||||
matchers.some((m) => m.regexp.includes('with-middleware')) &&
|
||||
matchers.some((m) => m.regexp.includes('another-middleware'))
|
||||
? 'success'
|
||||
: 'failed'
|
||||
}, 'success')
|
||||
|
|
|
@ -59,7 +59,7 @@ describe('Middleware Runtime trailing slash', () => {
|
|||
name: 'middleware',
|
||||
env: [],
|
||||
page: '/',
|
||||
regexp: '^/.*$',
|
||||
matchers: [{ regexp: '^/.*$' }],
|
||||
wasm: [],
|
||||
assets: [],
|
||||
},
|
||||
|
|
|
@ -67,7 +67,7 @@ describe('Switchable runtime', () => {
|
|||
`/_next/static/${next.buildId}/_devMiddlewareManifest.json`
|
||||
)
|
||||
const devMiddlewareManifest = await res.json()
|
||||
expect(devMiddlewareManifest).toEqual({})
|
||||
expect(devMiddlewareManifest).toEqual([])
|
||||
})
|
||||
|
||||
it('should sort edge SSR routes correctly', async () => {
|
||||
|
@ -184,7 +184,7 @@ describe('Switchable runtime', () => {
|
|||
],
|
||||
name: 'pages/api/hello',
|
||||
page: '/api/hello',
|
||||
regexp: '^/api/hello$',
|
||||
matchers: [{ regexp: '^/api/hello$' }],
|
||||
wasm: [],
|
||||
},
|
||||
'/api/edge': {
|
||||
|
@ -195,7 +195,7 @@ describe('Switchable runtime', () => {
|
|||
],
|
||||
name: 'pages/api/edge',
|
||||
page: '/api/edge',
|
||||
regexp: '^/api/edge$',
|
||||
matchers: [{ regexp: '^/api/edge$' }],
|
||||
wasm: [],
|
||||
},
|
||||
},
|
||||
|
@ -328,7 +328,7 @@ describe('Switchable runtime', () => {
|
|||
],
|
||||
name: 'pages/api/hello',
|
||||
page: '/api/hello',
|
||||
regexp: '^/api/hello$',
|
||||
matchers: [{ regexp: '^/api/hello$' }],
|
||||
wasm: [],
|
||||
},
|
||||
'/api/edge': {
|
||||
|
@ -339,7 +339,7 @@ describe('Switchable runtime', () => {
|
|||
],
|
||||
name: 'pages/api/edge',
|
||||
page: '/api/edge',
|
||||
regexp: '^/api/edge$',
|
||||
matchers: [{ regexp: '^/api/edge$' }],
|
||||
wasm: [],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -986,7 +986,7 @@ const runTests = (isDev = false) => {
|
|||
host: '1',
|
||||
})
|
||||
|
||||
const res2 = await fetchViaHTTP(appPort, '/has-rewrite-3')
|
||||
const res2 = await fetchViaHTTP(appPort, '/has-rewrite-4')
|
||||
expect(res2.status).toBe(404)
|
||||
})
|
||||
|
||||
|
|
|
@ -143,10 +143,6 @@ const runTests = () => {
|
|||
`\`destination\` is missing for route {"source":"/hello","permanent":false}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`\`destination\` is missing for route {"source":"/hello","permanent":false}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`\`source\` is not a string for route {"source":123,"destination":"/another","permanent":false}`
|
||||
)
|
||||
|
@ -163,14 +159,6 @@ const runTests = () => {
|
|||
`\`permanent\` is not set to \`true\` or \`false\` for route {"source":"/hello","destination":"/another","permanent":"yes"}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`\`permanent\` is not set to \`true\` or \`false\` for route {"source":"/hello","destination":"/another","permanent":"yes"}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`\`permanent\` is not set to \`true\` or \`false\` for route {"source":"/hello","destination":"/another","permanent":"yes"}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`\`destination\` has unnamed params :0 for route {"source":"/hello/world/(.*)","destination":"/:0","permanent":true}`
|
||||
)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export default () => 'hi'
|
169
test/integration/invalid-middleware-matchers/test/index.test.js
Normal file
169
test/integration/invalid-middleware-matchers/test/index.test.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
import fs from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { fetchViaHTTP, findPort, launchApp, nextBuild } from 'next-test-utils'
|
||||
|
||||
let appDir = join(__dirname, '..')
|
||||
const middlewarePath = join(appDir, 'middleware.js')
|
||||
|
||||
const writeMiddleware = async (matchers) => {
|
||||
await fs.writeFile(
|
||||
middlewarePath,
|
||||
`
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export default function middleware() {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ${JSON.stringify(matchers)},
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
let getStderr
|
||||
|
||||
const runTests = () => {
|
||||
it('should error when source length is exceeded', async () => {
|
||||
await writeMiddleware([{ source: `/${Array(4096).join('a')}` }])
|
||||
const stderr = await getStderr()
|
||||
expect(stderr).toContain(
|
||||
'`source` exceeds max built length of 4096 for route {"source":"/aaaaaaaaaaaaaaaaaa'
|
||||
)
|
||||
})
|
||||
|
||||
it('should error during next build for invalid matchers', async () => {
|
||||
await writeMiddleware([
|
||||
{
|
||||
// missing source
|
||||
},
|
||||
{
|
||||
// invalid source
|
||||
source: 123,
|
||||
},
|
||||
// missing forward slash in source
|
||||
'hello',
|
||||
{
|
||||
// extra field
|
||||
source: '/hello',
|
||||
destination: '/not-allowed',
|
||||
},
|
||||
|
||||
// invalid objects
|
||||
null,
|
||||
// invalid has items
|
||||
{
|
||||
source: '/hello',
|
||||
has: [
|
||||
{
|
||||
type: 'cookiee',
|
||||
key: 'loggedIn',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/hello',
|
||||
has: [
|
||||
{
|
||||
type: 'headerr',
|
||||
},
|
||||
{
|
||||
type: 'queryr',
|
||||
key: 'hello',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/hello',
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: '/hello',
|
||||
locale: true,
|
||||
},
|
||||
])
|
||||
const stderr = await getStderr()
|
||||
|
||||
expect(stderr).toContain(`\`source\` is missing for route {}`)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`\`source\` is not a string for route {"source":123}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`\`source\` does not start with / for route {"source":"hello"}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`invalid field: destination for route {"source":"/hello","destination":"/not-allowed"}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain(
|
||||
`The route null is not a valid object with \`source\``
|
||||
)
|
||||
|
||||
expect(stderr).toContain('Invalid `has` item:')
|
||||
expect(stderr).toContain(
|
||||
`invalid type "cookiee" for {"type":"cookiee","key":"loggedIn"}`
|
||||
)
|
||||
expect(stderr).toContain(
|
||||
`invalid \`has\` item found for route {"source":"/hello","has":[{"type":"cookiee","key":"loggedIn"}]}`
|
||||
)
|
||||
|
||||
expect(stderr).toContain('Invalid `has` items:')
|
||||
expect(stderr).toContain(
|
||||
`invalid type "headerr", invalid key "undefined" for {"type":"headerr"}`
|
||||
)
|
||||
expect(stderr).toContain(
|
||||
`invalid type "queryr" for {"type":"queryr","key":"hello"}`
|
||||
)
|
||||
expect(stderr).toContain(
|
||||
`invalid \`has\` items found for route {"source":"/hello","has":[{"type":"headerr"},{"type":"queryr","key":"hello"}]}`
|
||||
)
|
||||
expect(stderr).toContain(`Valid \`has\` object shape is {`)
|
||||
expect(stderr).toContain(
|
||||
`invalid field: basePath for route {"source":"/hello","basePath":false}`
|
||||
)
|
||||
expect(stderr).toContain(
|
||||
'`locale` must be undefined or false for route {"source":"/hello","locale":true}'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
describe('Errors on invalid custom middleware matchers', () => {
|
||||
afterAll(() => fs.remove(middlewarePath))
|
||||
|
||||
describe('dev mode', () => {
|
||||
beforeAll(() => {
|
||||
getStderr = async () => {
|
||||
let stderr = ''
|
||||
const port = await findPort()
|
||||
await launchApp(appDir, port, {
|
||||
onStderr(msg) {
|
||||
stderr += msg
|
||||
},
|
||||
})
|
||||
await fetchViaHTTP(port, '/')
|
||||
// suppress error
|
||||
.catch(() => {})
|
||||
return stderr
|
||||
}
|
||||
})
|
||||
|
||||
runTests()
|
||||
})
|
||||
|
||||
describe('production mode', () => {
|
||||
beforeAll(() => {
|
||||
getStderr = async () => {
|
||||
const { stderr } = await nextBuild(appDir, [], { stderr: true })
|
||||
return stderr
|
||||
}
|
||||
})
|
||||
|
||||
runTests()
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue