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:
parent
f3c1bcfc2f
commit
ffa378901f
2 changed files with 206 additions and 5 deletions
|
@ -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}`)
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue