Skip to content

Commit f6cc111

Browse files
committed
feat: add support for v2 middleware matchers
1 parent c7b3099 commit f6cc111

File tree

8 files changed

+267
-155
lines changed

8 files changed

+267
-155
lines changed

demos/canary/middleware.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextResponse } from 'next/server'
2+
import type { NextRequest } from 'next/server'
3+
4+
export async function middleware(req: NextRequest) {
5+
const res = NextResponse.rewrite(new URL('/', req.url))
6+
res.headers.set('x-response-header', 'set in middleware')
7+
res.headers.set('x-is-deno', 'Deno' in globalThis ? 'true' : 'false')
8+
return res
9+
}
10+
11+
export const config = {
12+
matcher: [
13+
'/foo',
14+
{ source: '/bar' },
15+
{
16+
source: '/baz',
17+
has: [
18+
{
19+
type: 'header',
20+
key: 'x-my-header',
21+
value: 'my-value',
22+
},
23+
],
24+
},
25+
{
26+
source: '/en/asdf',
27+
locale: false,
28+
},
29+
],
30+
}

demos/canary/package-lock.json

Lines changed: 135 additions & 132 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demos/canary/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12-
"next": "^12.1.7-canary.29",
12+
"next": "^12.2.6-canary.10",
1313
"react": "18.1.0",
1414
"react-dom": "18.1.0"
1515
},

packages/runtime/src/helpers/edge.ts

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,33 @@ import { resolve, join } from 'path'
55
import type { NetlifyConfig, NetlifyPluginConstants } from '@netlify/build'
66
import { copyFile, emptyDir, ensureDir, readJSON, readJson, writeJSON, writeJson } from 'fs-extra'
77
import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin'
8+
import type { RouteHas } from 'next/dist/lib/load-custom-routes'
89

9-
type EdgeFunctionDefinition = MiddlewareManifest['middleware']['name']
10+
// This is the format as of [email protected]
11+
interface EdgeFunctionDefinitionV1 {
12+
env: string[]
13+
files: string[]
14+
name: string
15+
page: string
16+
regexp: string
17+
}
18+
19+
export interface MiddlewareMatcher {
20+
regexp: string
21+
locale?: false
22+
has?: RouteHas[]
23+
}
24+
25+
// This is the format after [email protected]
26+
interface EdgeFunctionDefinitionV2 {
27+
env: string[]
28+
files: string[]
29+
name: string
30+
page: string
31+
matchers: MiddlewareMatcher[]
32+
}
33+
34+
type EdgeFunctionDefinition = EdgeFunctionDefinitionV1 | EdgeFunctionDefinitionV2
1035

1136
export interface FunctionManifest {
1237
version: 1
@@ -84,9 +109,6 @@ const copyEdgeSourceFile = ({
84109
target?: string
85110
}) => fs.copyFile(join(__dirname, '..', '..', 'src', 'templates', 'edge', file), join(edgeFunctionDir, target ?? file))
86111

87-
// Edge functions don't support lookahead expressions
88-
const stripLookahead = (regex: string) => regex.replace('^/(?!_next)', '^/')
89-
90112
const writeEdgeFunction = async ({
91113
edgeFunctionDefinition,
92114
edgeFunctionRoot,
@@ -95,10 +117,12 @@ const writeEdgeFunction = async ({
95117
edgeFunctionDefinition: EdgeFunctionDefinition
96118
edgeFunctionRoot: string
97119
netlifyConfig: NetlifyConfig
98-
}): Promise<{
99-
function: string
100-
pattern: string
101-
}> => {
120+
}): Promise<
121+
Array<{
122+
function: string
123+
pattern: string
124+
}>
125+
> => {
102126
const name = sanitizeName(edgeFunctionDefinition.name)
103127
const edgeFunctionDir = join(edgeFunctionRoot, name)
104128

@@ -117,10 +141,24 @@ const writeEdgeFunction = async ({
117141
})
118142

119143
await copyEdgeSourceFile({ edgeFunctionDir, file: 'utils.ts' })
120-
return {
121-
function: name,
122-
pattern: stripLookahead(edgeFunctionDefinition.regexp),
144+
await copyEdgeSourceFile({ edgeFunctionDir, file: 'next-utils.ts' })
145+
146+
const matchers: EdgeFunctionDefinitionV2['matchers'] = []
147+
148+
// The v1 middleware manifest has a single regexp, but the v2 has an array of matchers
149+
if ('regexp' in edgeFunctionDefinition) {
150+
matchers.push({ regexp: edgeFunctionDefinition.regexp })
151+
} else {
152+
matchers.push(...edgeFunctionDefinition.matchers)
123153
}
154+
155+
await writeJson(join(edgeFunctionDir, 'matchers.json'), matchers)
156+
157+
// We add a defintion for each matching path
158+
return matchers.map((matcher) => {
159+
const pattern = matcher.regexp
160+
return { function: name, pattern }
161+
})
124162
}
125163

126164
type NetlifyPluginConstantsWithEdgeFunctions = NetlifyPluginConstants & {
@@ -193,23 +231,23 @@ export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => {
193231

194232
for (const middleware of middlewareManifest.sortedMiddleware) {
195233
const edgeFunctionDefinition = middlewareManifest.middleware[middleware]
196-
const functionDefinition = await writeEdgeFunction({
234+
const functionDefinitions = await writeEdgeFunction({
197235
edgeFunctionDefinition,
198236
edgeFunctionRoot,
199237
netlifyConfig,
200238
})
201-
manifest.functions.push(functionDefinition)
239+
manifest.functions.push(...functionDefinitions)
202240
}
203241
// Older versions of the manifest format don't have the functions field
204242
// No, the version field was not incremented
205243
if (typeof middlewareManifest.functions === 'object') {
206244
for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
207-
const functionDefinition = await writeEdgeFunction({
245+
const functionDefinitions = await writeEdgeFunction({
208246
edgeFunctionDefinition,
209247
edgeFunctionRoot,
210248
netlifyConfig,
211249
})
212-
manifest.functions.push(functionDefinition)
250+
manifest.functions.push(...functionDefinitions)
213251
}
214252
}
215253
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

packages/runtime/src/templates/edge/next-utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,3 +359,37 @@ const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/
359359
export function isDynamicRoute(route: string): boolean {
360360
return TEST_ROUTE.test(route)
361361
}
362+
363+
// packages/next/shared/lib/router/utils/middleware-route-matcher.ts
364+
365+
export interface MiddlewareRouteMatch {
366+
(pathname: string | null | undefined, request: Pick<Request, 'headers' | 'url'>, query: Params): boolean
367+
}
368+
369+
export interface MiddlewareMatcher {
370+
regexp: string
371+
locale?: false
372+
has?: RouteHas[]
373+
}
374+
375+
export function getMiddlewareRouteMatcher(matchers: MiddlewareMatcher[]): MiddlewareRouteMatch {
376+
return (pathname: string | null | undefined, req: Pick<Request, 'headers' | 'url'>, query: Params) => {
377+
for (const matcher of matchers) {
378+
const routeMatch = new RegExp(matcher.regexp).exec(pathname!)
379+
if (!routeMatch) {
380+
continue
381+
}
382+
383+
if (matcher.has) {
384+
const hasParams = matchHas(req, matcher.has, query)
385+
if (!hasParams) {
386+
continue
387+
}
388+
}
389+
390+
return true
391+
}
392+
393+
return false
394+
}
395+
}

packages/runtime/src/templates/edge/router.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,7 @@ function matchHas(request: Request, has?: RouteHas[]): Params | false {
8484
if (!has?.length) {
8585
return {}
8686
}
87-
return nextMatchHas(
88-
request,
89-
// y u no narrow `has` type?
90-
has,
91-
searchParamsToUrlQuery(url.searchParams),
92-
)
87+
return nextMatchHas(request, has, searchParamsToUrlQuery(url.searchParams))
9388
}
9489

9590
/**

packages/runtime/src/templates/edge/runtime.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { Context } from 'https://edge.netlify.com'
2-
2+
// Available at build time
3+
import matchers from './matchers.json' assert { type: 'json' }
34
import edgeFunction from './bundle.js'
45
import { buildResponse } from './utils.ts'
6+
import { getMiddlewareRouteMatcher, MiddlewareRouteMatch, searchParamsToUrlQuery } from './next-utils.ts'
7+
8+
const matchesMiddleware: MiddlewareRouteMatch = getMiddlewareRouteMatcher(matchers || [])
59

610
export interface FetchEventResult {
711
response: Response
@@ -49,8 +53,15 @@ const handler = async (req: Request, context: Context) => {
4953
// Don't run in dev
5054
return
5155
}
56+
5257
const url = new URL(req.url)
5358

59+
// While we have already checked the path when mapping to the edge function,
60+
// Next.js supports extra rules that we need to check here too.
61+
if (!matchesMiddleware(url.pathname, req, searchParamsToUrlQuery(url.searchParams))) {
62+
return
63+
}
64+
5465
const geo = {
5566
country: context.geo.country?.code,
5667
region: context.geo.subdivision?.code,

0 commit comments

Comments
 (0)