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:
Damien Simonin Feugas 2022-08-10 00:58:40 +02:00 committed by GitHub
parent e2119c068f
commit 15a216b856
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 239 additions and 103 deletions

View file

@ -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}`)
}

View file

@ -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()
})
}
)