Skip to content

Commit

Permalink
Allow middleware to set its matcher
Browse files Browse the repository at this point in the history
therefore allowing Middleware to be specific to the given routes by
setting the following config:

```ts
export const config = {
  matching: ['/will-use-middleware/:path*']
}
```
  • Loading branch information
Schniz committed May 31, 2022
1 parent 15354e9 commit c9f2431
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 46 deletions.
8 changes: 8 additions & 0 deletions packages/next/build/analysis/extract-const-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
NullLiteral,
NumericLiteral,
ObjectExpression,
RegExpLiteral,
StringLiteral,
VariableDeclaration,
} from '@swc/core'
Expand Down Expand Up @@ -119,6 +120,10 @@ function isKeyValueProperty(node: Node): node is KeyValueProperty {
return node.type === 'KeyValueProperty'
}

function isRegExpLiteral(node: Node): node is RegExpLiteral {
return node.type === 'RegExpLiteral'
}

class UnsupportedValueError extends Error {}
class NoSuchDeclarationError extends Error {}

Expand All @@ -134,6 +139,9 @@ function extractValue(node: Node): any {
} else if (isNumericLiteral(node)) {
// e.g. 123
return node.value
} else if (isRegExpLiteral(node)) {
// e.g. /abc/i
return new RegExp(node.pattern, node.flags)
} else if (isIdentifier(node)) {
switch (node.value) {
case 'undefined':
Expand Down
92 changes: 69 additions & 23 deletions packages/next/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ import type { NextConfig } from '../../server/config-shared'
import { tryToExtractExportedConstValue } from './extract-const-value'
import { parseModule } from './parse-module'
import { promises as fs } from 'fs'
import { tryToParsePath } from '../../lib/try-to-parse-path'
import { MIDDLEWARE_FILE } from '../../lib/constants'

interface MiddlewareConfig {
pathMatcher: RegExp
}

export interface PageStaticInfo {
runtime?: PageRuntime
ssg?: boolean
ssr?: boolean
middleware?: Partial<MiddlewareConfig>
}

/**
Expand All @@ -21,38 +28,36 @@ export async function getPageStaticInfo(params: {
nextConfig: Partial<NextConfig>
pageFilePath: string
isDev?: boolean
page?: string
}): Promise<PageStaticInfo> {
const { isDev, pageFilePath, nextConfig } = params

const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || ''
if (/runtime|getStaticProps|getServerSideProps/.test(fileContent)) {
if (/runtime|getStaticProps|getServerSideProps|matching/.test(fileContent)) {
const swcAST = await parseModule(pageFilePath, fileContent)
const { ssg, ssr } = checkExports(swcAST)
const config = tryToExtractExportedConstValue(swcAST, 'config') || {}
if (config?.runtime === 'edge') {
return {
runtime: config.runtime,
ssr: ssr,
ssg: ssg,
}
}
const runtime =
config?.runtime === 'edge'
? 'edge'
: // For Node.js runtime, we do static optimization.
config?.runtime === 'nodejs'
? ssr || ssg
? 'nodejs'
: undefined
: // When the runtime is required because there is ssr or ssg we fallback
ssr || ssg
? nextConfig.experimental?.runtime
: undefined

// For Node.js runtime, we do static optimization.
if (config?.runtime === 'nodejs') {
return {
runtime: ssr || ssg ? config.runtime : undefined,
ssr: ssr,
ssg: ssg,
}
}
const middlewareConfig =
params.page === MIDDLEWARE_FILE && getMiddlewareConfig(config)

// When the runtime is required because there is ssr or ssg we fallback
if (ssr || ssg) {
return {
runtime: nextConfig.experimental?.runtime,
ssr: ssr,
ssg: ssg,
}
return {
ssr,
ssg,
...(middlewareConfig && { middleware: middlewareConfig }),
...(runtime && { runtime }),
}
}

Expand Down Expand Up @@ -117,3 +122,44 @@ async function tryToReadFile(filePath: string, shouldThrow: boolean) {
}
}
}

function getMiddlewareConfig(config: any): Partial<MiddlewareConfig> {
const result: Partial<MiddlewareConfig> = {}

if (config.matching) {
result.pathMatcher = new RegExp(
getMiddlewareRegExpStrings(config.matching).join('|')
)
}

console.log(result)

return result
}

function getMiddlewareRegExpStrings(matching: string | string[]): string[] {
if (Array.isArray(matching)) {
return matching.flatMap((x) => getMiddlewareRegExpStrings(x))
}

if (typeof matching !== 'string') {
throw new Error(
'`matching` must be a path pattern or an array of path patterns'
)
}

let matcher = matching.startsWith('/') ? matching : `/${matching}`
const parsedPage = tryToParsePath(matcher)

matcher = `/_next/data/:__nextjsBuildId__${matcher}.json`
const parsedDataRoute = tryToParsePath(matcher)

const regexes = [parsedPage.regexStr, parsedDataRoute.regexStr].filter(
(x): x is string => !!x
)
if (regexes.length < 2) {
throw new Error("Can't parse matcher")
} else {
return regexes
}
}
5 changes: 5 additions & 0 deletions packages/next/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,14 @@ export function getEdgeServerEntry(opts: {
isServerComponent: boolean
page: string
pages: { [page: string]: string }
middleware?: { pathMatcher?: RegExp }
}) {
if (opts.page === MIDDLEWARE_FILE) {
const loaderParams: MiddlewareLoaderOptions = {
absolutePagePath: opts.absolutePagePath,
page: opts.page,
matcherRegexp:
opts.middleware?.pathMatcher && opts.middleware.pathMatcher.source,
}

return `next-middleware-loader?${stringify(loaderParams)}!`
Expand Down Expand Up @@ -327,6 +330,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
nextConfig: config,
pageFilePath,
isDev,
page,
})

runDependingOnPageType({
Expand Down Expand Up @@ -376,6 +380,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
isDev: false,
isServerComponent,
page,
middleware: staticInfo?.middleware,
})
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface RouteMeta {

export interface EdgeMiddlewareMeta {
page: string
matcherRegexp?: string
}

export interface EdgeSSRMeta {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { MIDDLEWARE_FILE } from '../../../lib/constants'
export type MiddlewareLoaderOptions = {
absolutePagePath: string
page: string
matcherRegexp?: string
}

export default function middlewareLoader(this: any) {
const { absolutePagePath, page }: MiddlewareLoaderOptions = this.getOptions()
const { absolutePagePath, page, matcherRegexp }: MiddlewareLoaderOptions =
this.getOptions()
const stringifiedPagePath = stringifyRequest(this, absolutePagePath)
const buildInfo = getModuleBuildInfo(this._module)
buildInfo.nextEdgeMiddleware = {
matcherRegexp,
page: page.replace(new RegExp(`${MIDDLEWARE_FILE}$`), '') || '/',
}

Expand Down
3 changes: 2 additions & 1 deletion packages/next/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,13 +394,14 @@ function getCreateAssets(params: {
const { namedRegex } = getNamedMiddlewareRegex(page, {
catchAll: !metadata.edgeSSR,
})
const regexp = metadata?.edgeMiddleware?.matcherRegexp ?? namedRegex

middlewareManifest.middleware[page] = {
env: Array.from(metadata.env),
files: getEntryFiles(entrypoint.getFiles(), metadata),
name: entrypoint.name,
page: page,
regexp: namedRegex,
regexp,
wasm: Array.from(metadata.wasmBindings),
}
}
Expand Down
35 changes: 22 additions & 13 deletions packages/next/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export default class DevServer extends Server {

wp.on('aggregated', async () => {
const routedMiddleware: string[] = []
let middlewareMatcher: RegExp | undefined
const routedPages: string[] = []
const knownFiles = wp.getTimeInfoEntries()
const appPaths: Record<string, string> = {}
Expand All @@ -307,8 +308,15 @@ export default class DevServer extends Server {
extensions: this.nextConfig.pageExtensions,
})

const staticInfo = await getPageStaticInfo({
pageFilePath: fileName,
nextConfig: this.nextConfig,
page: rootFile,
})

if (rootFile === MIDDLEWARE_FILE) {
routedMiddleware.push(`/`)
middlewareMatcher = staticInfo.middleware?.pathMatcher
routedMiddleware.push('/')
continue
}

Expand Down Expand Up @@ -343,11 +351,6 @@ export default class DevServer extends Server {
continue
}

const staticInfo = await getPageStaticInfo({
pageFilePath: fileName,
nextConfig: this.nextConfig,
})

runDependingOnPageType({
page: pageName,
pageRuntime: staticInfo.runtime,
Expand All @@ -362,13 +365,19 @@ export default class DevServer extends Server {
}

this.appPathRoutes = appPaths
this.middleware = getSortedRoutes(routedMiddleware).map((page) => ({
match: getRouteMatcher(
getMiddlewareRegex(page, { catchAll: !ssrMiddleware.has(page) })
),
page,
ssr: ssrMiddleware.has(page),
}))
this.middleware = getSortedRoutes(routedMiddleware).map((page) => {
return {
match: getRouteMatcher(
page === '/' && middlewareMatcher
? { re: middlewareMatcher, groups: {} }
: getMiddlewareRegex(page, {
catchAll: !ssrMiddleware.has(page),
})
),
page,
ssr: ssrMiddleware.has(page),
}
})

try {
// we serve a separate manifest with all pages for the client in
Expand Down
31 changes: 23 additions & 8 deletions packages/next/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ 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 type {
Params,
RouteMatch,
} from '../shared/lib/router/utils/route-matcher'

import fs from 'fs'
import { join, relative, resolve, sep } from 'path'
Expand Down Expand Up @@ -66,14 +69,12 @@ 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 { 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 { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
import { clonableBodyForRequest } from './body-streams'
import { getMiddlewareRegex } from '../shared/lib/router/utils/route-regex'
import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info'

export * from './base-server'
Expand Down Expand Up @@ -1021,11 +1022,7 @@ export default class NextNodeServer extends BaseServer {
))

return manifest.sortedMiddleware.map((page) => ({
match: getRouteMatcher(
getMiddlewareRegex(page, {
catchAll: manifest?.middleware?.[page].name === MIDDLEWARE_FILENAME,
})
),
match: getMiddlewareMatcher(manifest.middleware[page]),
page,
}))
}
Expand Down Expand Up @@ -1412,3 +1409,21 @@ export default class NextNodeServer extends BaseServer {
}
}
}

const MiddlewareMatcherCache = new WeakMap<
MiddlewareManifest['middleware'][string],
RouteMatch
>()

function getMiddlewareMatcher(
info: MiddlewareManifest['middleware'][string]
): RouteMatch {
const stored = MiddlewareMatcherCache.get(info)
if (stored) {
return stored
}

const matcher = getRouteMatcher({ re: new RegExp(info.regexp), groups: {} })
MiddlewareMatcherCache.set(info, matcher)
return matcher
}
Loading

0 comments on commit c9f2431

Please sign in to comment.