Include i18 and basePath on middleware with custom matchers (#37854)

When generating the RegExp for middleware using the `matcher` option we are not taking into consideration i18n and basePath. In this PR we include them always which should we the middleware default. In a followup PR we will provide an option to opt-out in the same way we do with rewrites and redirects defined from Next.js config.
This commit is contained in:
Javi Velasco 2022-06-21 00:34:03 +02:00 committed by GitHub
parent f3c1bcfc2f
commit ffa378901f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 206 additions and 5 deletions

View file

@ -1,6 +1,7 @@
import type { PageRuntime } from '../../server/config-shared'
import type { NextConfig } from '../../server/config-shared'
import { tryToExtractExportedConstValue } from './extract-const-value'
import { escapeStringRegexp } from '../../shared/lib/escape-regexp'
import { parseModule } from './parse-module'
import { promises as fs } from 'fs'
import { tryToParsePath } from '../../lib/try-to-parse-path'
@ -49,7 +50,7 @@ export async function getPageStaticInfo(params: {
runtime = 'edge'
}
const middlewareConfig = getMiddlewareConfig(config)
const middlewareConfig = getMiddlewareConfig(config, nextConfig)
return {
ssr,
@ -121,12 +122,15 @@ async function tryToReadFile(filePath: string, shouldThrow: boolean) {
}
}
function getMiddlewareConfig(config: any): Partial<MiddlewareConfig> {
function getMiddlewareConfig(
config: any,
nextConfig: NextConfig
): Partial<MiddlewareConfig> {
const result: Partial<MiddlewareConfig> = {}
if (config.matcher) {
result.pathMatcher = new RegExp(
getMiddlewareRegExpStrings(config.matcher).join('|')
getMiddlewareRegExpStrings(config.matcher, nextConfig).join('|')
)
if (result.pathMatcher.source.length > 4096) {
@ -139,9 +143,14 @@ function getMiddlewareConfig(config: any): Partial<MiddlewareConfig> {
return result
}
function getMiddlewareRegExpStrings(matcherOrMatchers: unknown): string[] {
function getMiddlewareRegExpStrings(
matcherOrMatchers: unknown,
nextConfig: NextConfig
): string[] {
if (Array.isArray(matcherOrMatchers)) {
return matcherOrMatchers.flatMap((x) => getMiddlewareRegExpStrings(x))
return matcherOrMatchers.flatMap((matcher) =>
getMiddlewareRegExpStrings(matcher, nextConfig)
)
}
if (typeof matcherOrMatchers !== 'string') {
@ -156,6 +165,18 @@ function getMiddlewareRegExpStrings(matcherOrMatchers: unknown): string[] {
throw new Error('`matcher`: path matcher must start with /')
}
if (nextConfig.i18n?.locales) {
matcher = `/:nextInternalLocale(${nextConfig.i18n.locales
.map((locale) => escapeStringRegexp(locale))
.join('|')})${
matcher === '/' && !nextConfig.trailingSlash ? '' : matcher
}`
}
if (nextConfig.basePath) {
matcher = `${nextConfig.basePath}${matcher === '/' ? '' : matcher}`
}
const parsedPage = tryToParsePath(matcher)
if (parsedPage.error) {
throw new Error(`Invalid path matcher: ${matcher}`)

View file

@ -200,3 +200,183 @@ describe('using a single matcher', () => {
expect(response.headers.get('X-From-Middleware')).toBeNull()
})
})
describe('using a single matcher with i18n', () => {
let next: NextInstance
beforeAll(async () => {
next = await createNext({
files: {
'pages/index.js': `
export default function Page({ message }) {
return <div>
<p>{message}</p>
</div>
}
export const getServerSideProps = ({ params, locale }) => ({
props: { message: \`(\${locale}) Hello from /\` }
})
`,
'pages/[...route].js': `
export default function Page({ message }) {
return <div>
<p>catchall page</p>
<p>{message}</p>
</div>
}
export const getServerSideProps = ({ params, locale }) => ({
props: { message: \`(\${locale}) Hello from /\` + params.route.join("/") }
})
`,
'middleware.js': `
import { NextResponse } from 'next/server'
export const config = { matcher: '/' };
export default (req) => {
const res = NextResponse.next();
res.headers.set('X-From-Middleware', 'true');
return res;
}
`,
'next.config.js': `
module.exports = {
i18n: {
localeDetection: false,
locales: ['es', 'en'],
defaultLocale: 'en',
}
}
`,
},
dependencies: {},
})
})
afterAll(() => next.destroy())
it(`adds the header for a matched path`, async () => {
const res1 = await fetchViaHTTP(next.url, `/`)
expect(await res1.text()).toContain(`(en) Hello from /`)
expect(res1.headers.get('X-From-Middleware')).toBe('true')
const res2 = await fetchViaHTTP(next.url, `/es`)
expect(await res2.text()).toContain(`(es) Hello from /`)
expect(res2.headers.get('X-From-Middleware')).toBe('true')
})
it(`adds the headers for a matched data path`, async () => {
const res1 = await fetchViaHTTP(
next.url,
`/_next/data/${next.buildId}/en.json`,
undefined,
{ headers: { 'x-nextjs-data': '1' } }
)
expect(await res1.json()).toMatchObject({
pageProps: { message: `(en) Hello from /` },
})
expect(res1.headers.get('X-From-Middleware')).toBe('true')
const res2 = await fetchViaHTTP(
next.url,
`/_next/data/${next.buildId}/es.json`,
undefined,
{ headers: { 'x-nextjs-data': '1' } }
)
expect(await res2.json()).toMatchObject({
pageProps: { message: `(es) Hello from /` },
})
expect(res2.headers.get('X-From-Middleware')).toBe('true')
})
it(`does not add the header for an unmatched path`, async () => {
const response = await fetchViaHTTP(next.url, `/about/me`)
expect(await response.text()).toContain('Hello from /about/me')
expect(response.headers.get('X-From-Middleware')).toBeNull()
})
})
describe('using a single matcher with i18n and basePath', () => {
let next: NextInstance
beforeAll(async () => {
next = await createNext({
files: {
'pages/index.js': `
export default function Page({ message }) {
return <div>
<p>root page</p>
<p>{message}</p>
</div>
}
export const getServerSideProps = ({ params, locale }) => ({
props: { message: \`(\${locale}) Hello from /\` }
})
`,
'pages/[...route].js': `
export default function Page({ message }) {
return <div>
<p>catchall page</p>
<p>{message}</p>
</div>
}
export const getServerSideProps = ({ params, locale }) => ({
props: { message: \`(\${locale}) Hello from /\` + params.route.join("/") }
})
`,
'middleware.js': `
import { NextResponse } from 'next/server'
export const config = { matcher: '/' };
export default (req) => {
const res = NextResponse.next();
res.headers.set('X-From-Middleware', 'true');
return res;
}
`,
'next.config.js': `
module.exports = {
basePath: '/root',
i18n: {
localeDetection: false,
locales: ['es', 'en'],
defaultLocale: 'en',
}
}
`,
},
dependencies: {},
})
})
afterAll(() => next.destroy())
it(`adds the header for a matched path`, async () => {
const res1 = await fetchViaHTTP(next.url, `/root`)
expect(await res1.text()).toContain(`(en) Hello from /`)
expect(res1.headers.get('X-From-Middleware')).toBe('true')
const res2 = await fetchViaHTTP(next.url, `/root/es`)
expect(await res2.text()).toContain(`(es) Hello from /`)
expect(res2.headers.get('X-From-Middleware')).toBe('true')
})
it(`adds the headers for a matched data path`, async () => {
const res1 = await fetchViaHTTP(
next.url,
`/root/_next/data/${next.buildId}/en.json`,
undefined,
{ headers: { 'x-nextjs-data': '1' } }
)
expect(await res1.json()).toMatchObject({
pageProps: { message: `(en) Hello from /` },
})
expect(res1.headers.get('X-From-Middleware')).toBe('true')
const res2 = await fetchViaHTTP(
next.url,
`/root/_next/data/${next.buildId}/es.json`,
undefined,
{ headers: { 'x-nextjs-data': '1' } }
)
expect(await res2.json()).toMatchObject({
pageProps: { message: `(es) Hello from /` },
})
expect(res2.headers.get('X-From-Middleware')).toBe('true')
})
it(`does not add the header for an unmatched path`, async () => {
const response = await fetchViaHTTP(next.url, `/root/about/me`)
expect(await response.text()).toContain('Hello from /about/me')
expect(response.headers.get('X-From-Middleware')).toBeNull()
})
})