Skip to content

Commit

Permalink
feat(next): Support has match and locale option on middleware config (#…
Browse files Browse the repository at this point in the history
…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 #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>
  • Loading branch information
nkzawa and ijjk committed Aug 31, 2022
1 parent 481950c commit b522b94
Show file tree
Hide file tree
Showing 38 changed files with 964 additions and 262 deletions.
104 changes: 56 additions & 48 deletions packages/next/build/analysis/get-page-static-info.ts
@@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
24 changes: 14 additions & 10 deletions packages/next/build/entries.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -163,20 +168,17 @@ export function getEdgeServerEntry(opts: {
isServerComponent: boolean
page: string
pages: { [page: string]: string }
middleware?: { pathMatcher?: RegExp }
middleware?: Partial<MiddlewareConfig>
pagesType?: 'app' | 'pages' | 'root'
appDirLoader?: string
}) {
if (isMiddlewareFile(opts.page)) {
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)}!`
Expand Down Expand Up @@ -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') =>
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -493,7 +497,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
client,
server,
edgeServer,
middlewareRegex,
middlewareMatchers,
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/index.ts
Expand Up @@ -849,7 +849,7 @@ export default async function build(
runWebpackSpan,
target,
appDir,
middlewareRegex: entrypoints.middlewareRegex,
middlewareMatchers: entrypoints.middlewareMatchers,
}

const configs = await runWebpackSpan
Expand Down
15 changes: 8 additions & 7 deletions packages/next/build/webpack-config.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -90,7 +91,7 @@ export function getDefineEnv({
hasReactRoot,
isNodeServer,
isEdgeServer,
middlewareRegex,
middlewareMatchers,
hasServerComponents,
}: {
dev?: boolean
Expand All @@ -100,7 +101,7 @@ export function getDefineEnv({
hasReactRoot?: boolean
isNodeServer?: boolean
isEdgeServer?: boolean
middlewareRegex?: string
middlewareMatchers?: MiddlewareMatcher[]
config: NextConfigComplete
hasServerComponents?: boolean
}) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -510,7 +511,7 @@ export default async function getBaseWebpackConfig(
runWebpackSpan,
target = COMPILER_NAMES.server,
appDir,
middlewareRegex,
middlewareMatchers,
}: {
buildId: string
config: NextConfigComplete
Expand All @@ -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
Expand Down Expand Up @@ -1673,7 +1674,7 @@ export default async function getBaseWebpackConfig(
hasReactRoot,
isNodeServer,
isEdgeServer,
middlewareRegex,
middlewareMatchers,
hasServerComponents,
})
),
Expand Down
3 changes: 2 additions & 1 deletion packages/next/build/webpack/loaders/get-module-build-info.ts
@@ -1,3 +1,4 @@
import type { MiddlewareMatcher } from '../../analysis/get-page-static-info'
import { webpack } from 'next/dist/compiled/webpack/webpack'

/**
Expand Down Expand Up @@ -25,7 +26,7 @@ export interface RouteMeta {

export interface EdgeMiddlewareMeta {
page: string
matcherRegexp?: string
matchers?: MiddlewareMatcher[]
}

export interface EdgeSSRMeta {
Expand Down
24 changes: 17 additions & 7 deletions packages/next/build/webpack/loaders/next-middleware-loader.ts
@@ -1,27 +1,37 @@
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'

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}$`), '') || '/',
}
Expand Down

0 comments on commit b522b94

Please sign in to comment.