Skip to content

Commit b522b94

Browse files
nkzawaijjk
andauthored
feat(next): Support has match and locale option on middleware config (vercel#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 vercel#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 <[email protected]>
1 parent 481950c commit b522b94

File tree

38 files changed

+964
-262
lines changed

38 files changed

+964
-262
lines changed

packages/next/build/analysis/get-page-static-info.ts

+56-48
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isServerRuntime } from '../../server/config-shared'
22
import type { NextConfig } from '../../server/config-shared'
3+
import type { Middleware, RouteHas } from '../../lib/load-custom-routes'
34
import {
45
extractExportedConstValue,
56
UnsupportedValueError,
@@ -9,10 +10,17 @@ import { promises as fs } from 'fs'
910
import { tryToParsePath } from '../../lib/try-to-parse-path'
1011
import * as Log from '../output/log'
1112
import { SERVER_RUNTIME } from '../../lib/constants'
12-
import { ServerRuntime } from '../../types'
13+
import { ServerRuntime } from 'next/types'
14+
import { checkCustomRoutes } from '../../lib/load-custom-routes'
1315

14-
interface MiddlewareConfig {
15-
pathMatcher: RegExp
16+
export interface MiddlewareConfig {
17+
matchers: MiddlewareMatcher[]
18+
}
19+
20+
export interface MiddlewareMatcher {
21+
regexp: string
22+
locale?: false
23+
has?: RouteHas[]
1624
}
1725

1826
export interface PageStaticInfo {
@@ -81,55 +89,63 @@ async function tryToReadFile(filePath: string, shouldThrow: boolean) {
8189
}
8290
}
8391

84-
function getMiddlewareRegExpStrings(
92+
function getMiddlewareMatchers(
8593
matcherOrMatchers: unknown,
8694
nextConfig: NextConfig
87-
): string[] {
95+
): MiddlewareMatcher[] {
96+
let matchers: unknown[] = []
8897
if (Array.isArray(matcherOrMatchers)) {
89-
return matcherOrMatchers.flatMap((matcher) =>
90-
getMiddlewareRegExpStrings(matcher, nextConfig)
91-
)
98+
matchers = matcherOrMatchers
99+
} else {
100+
matchers.push(matcherOrMatchers)
92101
}
93102
const { i18n } = nextConfig
94103

95-
if (typeof matcherOrMatchers !== 'string') {
96-
throw new Error(
97-
'`matcher` must be a path matcher or an array of path matchers'
98-
)
99-
}
104+
let routes = matchers.map(
105+
(m) => (typeof m === 'string' ? { source: m } : m) as Middleware
106+
)
100107

101-
let matcher: string = matcherOrMatchers
108+
// check before we process the routes and after to ensure
109+
// they are still valid
110+
checkCustomRoutes(routes, 'middleware')
102111

103-
if (!matcher.startsWith('/')) {
104-
throw new Error('`matcher`: path matcher must start with /')
105-
}
106-
const isRoot = matcher === '/'
112+
routes = routes.map((r) => {
113+
let { source } = r
107114

108-
if (i18n?.locales) {
109-
matcher = `/:nextInternalLocale([^/.]{1,})${isRoot ? '' : matcher}`
110-
}
115+
const isRoot = source === '/'
111116

112-
matcher = `/:nextData(_next/data/[^/]{1,})?${matcher}${
113-
isRoot
114-
? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?`
115-
: '(.json)?'
116-
}`
117+
if (i18n?.locales && r.locale !== false) {
118+
source = `/:nextInternalLocale([^/.]{1,})${isRoot ? '' : source}`
119+
}
117120

118-
if (nextConfig.basePath) {
119-
matcher = `${nextConfig.basePath}${matcher}`
120-
}
121-
const parsedPage = tryToParsePath(matcher)
121+
source = `/:nextData(_next/data/[^/]{1,})?${source}${
122+
isRoot
123+
? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?`
124+
: '(.json)?'
125+
}`
122126

123-
if (parsedPage.error) {
124-
throw new Error(`Invalid path matcher: ${matcher}`)
125-
}
127+
if (nextConfig.basePath) {
128+
source = `${nextConfig.basePath}${source}`
129+
}
126130

127-
const regexes = [parsedPage.regexStr].filter((x): x is string => !!x)
128-
if (regexes.length < 1) {
129-
throw new Error("Can't parse matcher")
130-
} else {
131-
return regexes
132-
}
131+
return { ...r, source }
132+
})
133+
134+
checkCustomRoutes(routes, 'middleware')
135+
136+
return routes.map((r) => {
137+
const { source, ...rest } = r
138+
const parsedPage = tryToParsePath(source)
139+
140+
if (parsedPage.error || !parsedPage.regexStr) {
141+
throw new Error(`Invalid source: ${source}`)
142+
}
143+
144+
return {
145+
...rest,
146+
regexp: parsedPage.regexStr,
147+
}
148+
})
133149
}
134150

135151
function getMiddlewareConfig(
@@ -139,15 +155,7 @@ function getMiddlewareConfig(
139155
const result: Partial<MiddlewareConfig> = {}
140156

141157
if (config.matcher) {
142-
result.pathMatcher = new RegExp(
143-
getMiddlewareRegExpStrings(config.matcher, nextConfig).join('|')
144-
)
145-
146-
if (result.pathMatcher.source.length > 4096) {
147-
throw new Error(
148-
`generated matcher config must be less than 4096 characters.`
149-
)
150-
}
158+
result.matchers = getMiddlewareMatchers(config.matcher, nextConfig)
151159
}
152160

153161
return result

packages/next/build/entries.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import type { EdgeSSRLoaderQuery } from './webpack/loaders/next-edge-ssr-loader'
44
import type { NextConfigComplete } from '../server/config-shared'
55
import type { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader'
66
import type { webpack } from 'next/dist/compiled/webpack/webpack'
7+
import type {
8+
MiddlewareConfig,
9+
MiddlewareMatcher,
10+
} from './analysis/get-page-static-info'
711
import type { LoadedEnvFiles } from '@next/env'
812
import chalk from 'next/dist/compiled/chalk'
913
import { posix, join } from 'path'
@@ -42,6 +46,7 @@ import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep'
4246
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
4347
import { serverComponentRegex } from './webpack/loaders/utils'
4448
import { ServerRuntime } from '../types'
49+
import { encodeMatchers } from './webpack/loaders/next-middleware-loader'
4550

4651
type ObjectValue<T> = T extends { [key: string]: infer V } ? V : never
4752

@@ -163,20 +168,17 @@ export function getEdgeServerEntry(opts: {
163168
isServerComponent: boolean
164169
page: string
165170
pages: { [page: string]: string }
166-
middleware?: { pathMatcher?: RegExp }
171+
middleware?: Partial<MiddlewareConfig>
167172
pagesType?: 'app' | 'pages' | 'root'
168173
appDirLoader?: string
169174
}) {
170175
if (isMiddlewareFile(opts.page)) {
171176
const loaderParams: MiddlewareLoaderOptions = {
172177
absolutePagePath: opts.absolutePagePath,
173178
page: opts.page,
174-
// pathMatcher can have special characters that break the loader params
175-
// parsing so we base64 encode/decode the string
176-
matcherRegexp: Buffer.from(
177-
(opts.middleware?.pathMatcher && opts.middleware.pathMatcher.source) ||
178-
''
179-
).toString('base64'),
179+
matchers: opts.middleware?.matchers
180+
? encodeMatchers(opts.middleware.matchers)
181+
: '',
180182
}
181183

182184
return `next-middleware-loader?${stringify(loaderParams)}!`
@@ -347,7 +349,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
347349
const server: webpack.EntryObject = {}
348350
const client: webpack.EntryObject = {}
349351
const nestedMiddleware: string[] = []
350-
let middlewareRegex: string | undefined = undefined
352+
let middlewareMatchers: MiddlewareMatcher[] | undefined = undefined
351353

352354
const getEntryHandler =
353355
(mappings: Record<string, string>, pagesType: 'app' | 'pages' | 'root') =>
@@ -402,7 +404,9 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
402404
})
403405

404406
if (isMiddlewareFile(page)) {
405-
middlewareRegex = staticInfo.middleware?.pathMatcher?.source || '.*'
407+
middlewareMatchers = staticInfo.middleware?.matchers ?? [
408+
{ regexp: '.*' },
409+
]
406410

407411
if (target === 'serverless') {
408412
throw new MiddlewareInServerlessTargetError()
@@ -493,7 +497,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
493497
client,
494498
server,
495499
edgeServer,
496-
middlewareRegex,
500+
middlewareMatchers,
497501
}
498502
}
499503

packages/next/build/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,7 @@ export default async function build(
849849
runWebpackSpan,
850850
target,
851851
appDir,
852-
middlewareRegex: entrypoints.middlewareRegex,
852+
middlewareMatchers: entrypoints.middlewareMatchers,
853853
}
854854

855855
const configs = await runWebpackSpan

packages/next/build/webpack-config.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import type {
5454
SWC_TARGET_TRIPLE,
5555
} from './webpack/plugins/telemetry-plugin'
5656
import type { Span } from '../trace'
57+
import type { MiddlewareMatcher } from './analysis/get-page-static-info'
5758
import { withoutRSCExtensions } from './utils'
5859
import browserslist from 'next/dist/compiled/browserslist'
5960
import loadJsConfig from './load-jsconfig'
@@ -90,7 +91,7 @@ export function getDefineEnv({
9091
hasReactRoot,
9192
isNodeServer,
9293
isEdgeServer,
93-
middlewareRegex,
94+
middlewareMatchers,
9495
hasServerComponents,
9596
}: {
9697
dev?: boolean
@@ -100,7 +101,7 @@ export function getDefineEnv({
100101
hasReactRoot?: boolean
101102
isNodeServer?: boolean
102103
isEdgeServer?: boolean
103-
middlewareRegex?: string
104+
middlewareMatchers?: MiddlewareMatcher[]
104105
config: NextConfigComplete
105106
hasServerComponents?: boolean
106107
}) {
@@ -144,8 +145,8 @@ export function getDefineEnv({
144145
isEdgeServer ? 'edge' : 'nodejs'
145146
),
146147
}),
147-
'process.env.__NEXT_MIDDLEWARE_REGEX': JSON.stringify(
148-
middlewareRegex || ''
148+
'process.env.__NEXT_MIDDLEWARE_MATCHERS': JSON.stringify(
149+
middlewareMatchers || []
149150
),
150151
'process.env.__NEXT_MANUAL_CLIENT_BASE_PATH': JSON.stringify(
151152
config.experimental.manualClientBasePath
@@ -510,7 +511,7 @@ export default async function getBaseWebpackConfig(
510511
runWebpackSpan,
511512
target = COMPILER_NAMES.server,
512513
appDir,
513-
middlewareRegex,
514+
middlewareMatchers,
514515
}: {
515516
buildId: string
516517
config: NextConfigComplete
@@ -525,7 +526,7 @@ export default async function getBaseWebpackConfig(
525526
runWebpackSpan: Span
526527
target?: string
527528
appDir?: string
528-
middlewareRegex?: string
529+
middlewareMatchers?: MiddlewareMatcher[]
529530
}
530531
): Promise<webpack.Configuration> {
531532
const isClient = compilerType === COMPILER_NAMES.client
@@ -1673,7 +1674,7 @@ export default async function getBaseWebpackConfig(
16731674
hasReactRoot,
16741675
isNodeServer,
16751676
isEdgeServer,
1676-
middlewareRegex,
1677+
middlewareMatchers,
16771678
hasServerComponents,
16781679
})
16791680
),

packages/next/build/webpack/loaders/get-module-build-info.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { MiddlewareMatcher } from '../../analysis/get-page-static-info'
12
import { webpack } from 'next/dist/compiled/webpack/webpack'
23

34
/**
@@ -25,7 +26,7 @@ export interface RouteMeta {
2526

2627
export interface EdgeMiddlewareMeta {
2728
page: string
28-
matcherRegexp?: string
29+
matchers?: MiddlewareMatcher[]
2930
}
3031

3132
export interface EdgeSSRMeta {

packages/next/build/webpack/loaders/next-middleware-loader.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
1+
import type { MiddlewareMatcher } from '../../analysis/get-page-static-info'
12
import { getModuleBuildInfo } from './get-module-build-info'
23
import { stringifyRequest } from '../stringify-request'
34
import { MIDDLEWARE_LOCATION_REGEXP } from '../../../lib/constants'
45

56
export type MiddlewareLoaderOptions = {
67
absolutePagePath: string
78
page: string
8-
matcherRegexp?: string
9+
matchers?: string
10+
}
11+
12+
// matchers can have special characters that break the loader params
13+
// parsing so we base64 encode/decode the string
14+
export function encodeMatchers(matchers: MiddlewareMatcher[]) {
15+
return Buffer.from(JSON.stringify(matchers)).toString('base64')
16+
}
17+
18+
export function decodeMatchers(encodedMatchers: string) {
19+
return JSON.parse(
20+
Buffer.from(encodedMatchers, 'base64').toString()
21+
) as MiddlewareMatcher[]
922
}
1023

1124
export default function middlewareLoader(this: any) {
1225
const {
1326
absolutePagePath,
1427
page,
15-
matcherRegexp: base64MatcherRegex,
28+
matchers: encodedMatchers,
1629
}: MiddlewareLoaderOptions = this.getOptions()
17-
const matcherRegexp = Buffer.from(
18-
base64MatcherRegex || '',
19-
'base64'
20-
).toString()
30+
const matchers = encodedMatchers ? decodeMatchers(encodedMatchers) : undefined
2131
const stringifiedPagePath = stringifyRequest(this, absolutePagePath)
2232
const buildInfo = getModuleBuildInfo(this._module)
2333
buildInfo.nextEdgeMiddleware = {
24-
matcherRegexp,
34+
matchers,
2535
page:
2636
page.replace(new RegExp(`/${MIDDLEWARE_LOCATION_REGEXP}$`), '') || '/',
2737
}

0 commit comments

Comments
 (0)