diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index d27ef9057b1b..af30649b7edc 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -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}`) } diff --git a/test/e2e/middleware-matcher/index.test.ts b/test/e2e/middleware-matcher/index.test.ts index d76a1b5d8ed0..d31d2e37aba9 100644 --- a/test/e2e/middleware-matcher/index.test.ts +++ b/test/e2e/middleware-matcher/index.test.ts @@ -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
Hi there!
+ } + `, + '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

{message}

@@ -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

catchall page

@@ -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()) - - 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 /` }, + 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') }) - 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

root page

@@ -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

catchall page

@@ -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()) + + 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') }) - }) - 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 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') }) - 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 /` }, + + 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') }) - 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(`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() + }) + } +)