feat(middleware): augments / matcher with /index (#39397)
* feat(middleware): augments / matcher with /index * fix(#39396): not invoked with i18n and trailingSlash * add test case * tweak matcher a bit and add tests Co-authored-by: JJ Kasper <jj@jjsweb.site>
This commit is contained in:
parent
e2119c068f
commit
15a216b856
2 changed files with 239 additions and 103 deletions
|
@ -4,7 +4,6 @@ import {
|
|||
extractExportedConstValue,
|
||||
UnsupportedValueError,
|
||||
} 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'
|
||||
|
@ -185,6 +184,7 @@ function getMiddlewareRegExpStrings(
|
|||
getMiddlewareRegExpStrings(matcher, nextConfig)
|
||||
)
|
||||
}
|
||||
const { i18n } = nextConfig
|
||||
|
||||
if (typeof matcherOrMatchers !== 'string') {
|
||||
throw new Error(
|
||||
|
@ -197,20 +197,23 @@ function getMiddlewareRegExpStrings(
|
|||
if (!matcher.startsWith('/')) {
|
||||
throw new Error('`matcher`: path matcher must start with /')
|
||||
}
|
||||
const isRoot = matcher === '/'
|
||||
|
||||
if (nextConfig.i18n?.locales) {
|
||||
matcher = `/:nextInternalLocale(${nextConfig.i18n.locales
|
||||
.map((locale) => escapeStringRegexp(locale))
|
||||
.join('|')})${
|
||||
matcher === '/' && !nextConfig.trailingSlash ? '' : matcher
|
||||
}`
|
||||
if (i18n?.locales) {
|
||||
matcher = `/:nextInternalLocale([^/.]{1,})${isRoot ? '' : matcher}`
|
||||
}
|
||||
|
||||
matcher = `/:nextData(_next/data/[^/]{1,})?${matcher}${
|
||||
isRoot
|
||||
? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?`
|
||||
: '(.json)?'
|
||||
}`
|
||||
|
||||
if (nextConfig.basePath) {
|
||||
matcher = `${nextConfig.basePath}${matcher === '/' ? '' : matcher}`
|
||||
matcher = `${nextConfig.basePath}${matcher}`
|
||||
}
|
||||
|
||||
const parsedPage = tryToParsePath(matcher)
|
||||
|
||||
if (parsedPage.error) {
|
||||
throw new Error(`Invalid path matcher: ${matcher}`)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable jest/no-identical-title */
|
||||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import { check, fetchViaHTTP } from 'next-test-utils'
|
||||
|
@ -27,7 +28,7 @@ describe('Middleware can set the matcher in its config', () => {
|
|||
expect(await response.text()).toContain('This should run the middleware')
|
||||
})
|
||||
|
||||
it('adds the header for a matched data path', async () => {
|
||||
it('adds the header for a matched data path (with header)', async () => {
|
||||
const response = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/with-middleware.json`,
|
||||
|
@ -42,6 +43,19 @@ describe('Middleware can set the matcher in its config', () => {
|
|||
expect(response.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
|
||||
it('adds the header for a matched data path (without header)', async () => {
|
||||
const response = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/with-middleware.json`
|
||||
)
|
||||
expect(await response.json()).toMatchObject({
|
||||
pageProps: {
|
||||
message: 'Hello, cruel world.',
|
||||
},
|
||||
})
|
||||
expect(response.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
|
||||
it('adds the header for another matched path', async () => {
|
||||
const response = await fetchViaHTTP(next.url, '/another-middleware')
|
||||
expect(response.headers.get('X-From-Middleware')).toBe('true')
|
||||
|
@ -195,7 +209,7 @@ describe('using a single matcher', () => {
|
|||
expect(response.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
|
||||
it('adds the headers for a matched data path', async () => {
|
||||
it('adds the headers for a matched data path (with header)', async () => {
|
||||
const response = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/middleware/works.json`,
|
||||
|
@ -210,6 +224,19 @@ describe('using a single matcher', () => {
|
|||
expect(response.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
|
||||
it('adds the header for a matched data path (without header)', async () => {
|
||||
const response = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/middleware/works.json`
|
||||
)
|
||||
expect(await response.json()).toMatchObject({
|
||||
pageProps: {
|
||||
message: 'Hello from /middleware/works',
|
||||
},
|
||||
})
|
||||
expect(response.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')
|
||||
|
@ -217,12 +244,92 @@ describe('using a single matcher', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('using a single matcher with i18n', () => {
|
||||
describe('using root matcher', () => {
|
||||
let next: NextInstance
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
'pages/index.js': `
|
||||
export function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
message: 'hello world'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home({ message }) {
|
||||
return <div>Hi there!</div>
|
||||
}
|
||||
`,
|
||||
'middleware.js': `
|
||||
import { NextResponse } from 'next/server'
|
||||
export default (req) => {
|
||||
const res = NextResponse.next();
|
||||
res.headers.set('X-From-Middleware', 'true');
|
||||
return res;
|
||||
}
|
||||
|
||||
export const config = { matcher: '/' };
|
||||
`,
|
||||
},
|
||||
dependencies: {},
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it('adds the header to the /', async () => {
|
||||
const response = await fetchViaHTTP(next.url, '/')
|
||||
expect(response.status).toBe(200)
|
||||
expect(Object.fromEntries(response.headers)).toMatchObject({
|
||||
'x-from-middleware': 'true',
|
||||
})
|
||||
})
|
||||
|
||||
it('adds the header to the /index', async () => {
|
||||
const response = await fetchViaHTTP(next.url, '/index')
|
||||
expect(Object.fromEntries(response.headers)).toMatchObject({
|
||||
'x-from-middleware': 'true',
|
||||
})
|
||||
})
|
||||
|
||||
it('adds the header for a matched data path (with header)', async () => {
|
||||
const response = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/index.json`,
|
||||
undefined,
|
||||
{ headers: { 'x-nextjs-data': '1' } }
|
||||
)
|
||||
expect(await response.json()).toMatchObject({
|
||||
pageProps: {
|
||||
message: 'hello world',
|
||||
},
|
||||
})
|
||||
expect(response.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
|
||||
it('adds the header for a matched data path (without header)', async () => {
|
||||
const response = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/index.json`
|
||||
)
|
||||
expect(await response.json()).toMatchObject({
|
||||
pageProps: {
|
||||
message: 'hello world',
|
||||
},
|
||||
})
|
||||
expect(response.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
describe.each([{ title: ' and trailingSlash', trailingSlash: true }])(
|
||||
'using a single matcher with i18n$title',
|
||||
({ trailingSlash }) => {
|
||||
let next: NextInstance
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
'pages/index.js': `
|
||||
export default function Page({ message }) {
|
||||
return <div>
|
||||
<p>{message}</p>
|
||||
|
@ -232,7 +339,7 @@ describe('using a single matcher with i18n', () => {
|
|||
props: { message: \`(\${locale}) Hello from /\` }
|
||||
})
|
||||
`,
|
||||
'pages/[...route].js': `
|
||||
'pages/[...route].js': `
|
||||
export default function Page({ message }) {
|
||||
return <div>
|
||||
<p>catchall page</p>
|
||||
|
@ -243,7 +350,7 @@ describe('using a single matcher with i18n', () => {
|
|||
props: { message: \`(\${locale}) Hello from /\` + params.route.join("/") }
|
||||
})
|
||||
`,
|
||||
'middleware.js': `
|
||||
'middleware.js': `
|
||||
import { NextResponse } from 'next/server'
|
||||
export const config = { matcher: '/' };
|
||||
export default (req) => {
|
||||
|
@ -252,8 +359,9 @@ describe('using a single matcher with i18n', () => {
|
|||
return res;
|
||||
}
|
||||
`,
|
||||
'next.config.js': `
|
||||
'next.config.js': `
|
||||
module.exports = {
|
||||
${trailingSlash ? 'trailingSlash: true,' : ''}
|
||||
i18n: {
|
||||
localeDetection: false,
|
||||
locales: ['es', 'en'],
|
||||
|
@ -261,57 +369,71 @@ describe('using a single matcher with i18n', () => {
|
|||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
dependencies: {},
|
||||
},
|
||||
dependencies: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
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 /` },
|
||||
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')
|
||||
})
|
||||
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 /` },
|
||||
|
||||
it('adds the header for a mathed root path with /index', async () => {
|
||||
const res1 = await fetchViaHTTP(next.url, `/index`)
|
||||
expect(await res1.text()).toContain(`(en) Hello from /`)
|
||||
expect(res1.headers.get('X-From-Middleware')).toBe('true')
|
||||
const res2 = await fetchViaHTTP(next.url, `/es/index`)
|
||||
expect(await res2.text()).toContain(`(es) Hello from /`)
|
||||
expect(res2.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
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()
|
||||
})
|
||||
})
|
||||
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
|
||||
)
|
||||
expect(await res2.json()).toMatchObject({
|
||||
pageProps: { message: `(es) Hello from /` },
|
||||
})
|
||||
expect(res2.headers.get('X-From-Middleware')).toBe('true')
|
||||
})
|
||||
|
||||
describe('using a single matcher with i18n and basePath', () => {
|
||||
let next: NextInstance
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
'pages/index.js': `
|
||||
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.each([
|
||||
{ title: '' },
|
||||
{ title: ' and trailingSlash', trailingSlash: true },
|
||||
])(
|
||||
'using a single matcher with i18n and basePath$title',
|
||||
({ trailingSlash }) => {
|
||||
let next: NextInstance
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
'pages/index.js': `
|
||||
export default function Page({ message }) {
|
||||
return <div>
|
||||
<p>root page</p>
|
||||
|
@ -322,7 +444,7 @@ describe('using a single matcher with i18n and basePath', () => {
|
|||
props: { message: \`(\${locale}) Hello from /\` }
|
||||
})
|
||||
`,
|
||||
'pages/[...route].js': `
|
||||
'pages/[...route].js': `
|
||||
export default function Page({ message }) {
|
||||
return <div>
|
||||
<p>catchall page</p>
|
||||
|
@ -333,7 +455,7 @@ describe('using a single matcher with i18n and basePath', () => {
|
|||
props: { message: \`(\${locale}) Hello from /\` + params.route.join("/") }
|
||||
})
|
||||
`,
|
||||
'middleware.js': `
|
||||
'middleware.js': `
|
||||
import { NextResponse } from 'next/server'
|
||||
export const config = { matcher: '/' };
|
||||
export default (req) => {
|
||||
|
@ -342,8 +464,9 @@ describe('using a single matcher with i18n and basePath', () => {
|
|||
return res;
|
||||
}
|
||||
`,
|
||||
'next.config.js': `
|
||||
'next.config.js': `
|
||||
module.exports = {
|
||||
${trailingSlash ? 'trailingSlash: true,' : ''}
|
||||
basePath: '/root',
|
||||
i18n: {
|
||||
localeDetection: false,
|
||||
|
@ -352,47 +475,57 @@ describe('using a single matcher with i18n and basePath', () => {
|
|||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
dependencies: {},
|
||||
},
|
||||
dependencies: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
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 /` },
|
||||
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')
|
||||
})
|
||||
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()
|
||||
})
|
||||
})
|
||||
it('adds the header for a mathed root path with /index', async () => {
|
||||
const res1 = await fetchViaHTTP(next.url, `/root/index`)
|
||||
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/index`)
|
||||
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