diff --git a/src/build/content/server.ts b/src/build/content/server.ts index 941f8a8241..48be0382b6 100644 --- a/src/build/content/server.ts +++ b/src/build/content/server.ts @@ -16,6 +16,7 @@ import { join as posixJoin, sep as posixSep } from 'node:path/posix' import { trace } from '@opentelemetry/api' import { wrapTracer } from '@opentelemetry/api/experimental' import glob from 'fast-glob' +import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js' import { prerelease, satisfies, lt as semverLowerThan, lte as semverLowerThanOrEqual } from 'semver' import type { RunConfig } from '../../run/config.js' @@ -324,15 +325,17 @@ export const copyNextDependencies = async (ctx: PluginContext): Promise => } /** - * Generates a copy of the middleware manifest without any middleware in it. We + * Generates a copy of the middleware manifest that make all matchers never match on anything. We * do this because we'll run middleware in an edge function, and we don't want - * to run it again in the server handler. + * to run it again in the server handler. Additionally Next.js conditionally enable some handling + * depending if there is a middleware present, so we need to keep reference to middleware in server + * even if we don't actually want to ever run it there. */ const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) => { await mkdir(dirname(destPath), { recursive: true }) const data = await readFile(sourcePath, 'utf8') - const manifest = JSON.parse(data) + const manifest = JSON.parse(data) as MiddlewareManifest // TODO: Check for `manifest.version` and write an error to the system log // when we find a value that is not equal to 2. This will alert us in case @@ -340,7 +343,26 @@ const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) = // one with the old version. const newManifest = { ...manifest, - middleware: {}, + middleware: Object.fromEntries( + Object.entries(manifest.middleware).map(([key, edgeFunctionDefinition]) => { + return [ + key, + { + ...edgeFunctionDefinition, + matchers: edgeFunctionDefinition.matchers.map((matcher) => { + return { + ...matcher, + // matcher that won't match on anything + // this is meant to disable actually running middleware in the server handler, + // while still allowing next server to enable some middleware specific handling + // such as _next/data normalization ( https://github.com/vercel/next.js/blob/7bb72e508572237fe0d4aac5418546d4b4b3a363/packages/next/src/server/lib/router-utils/resolve-routes.ts#L395 ) + regexp: '(?!.*)', + } + }), + }, + ] + }), + ), } const newData = JSON.stringify(newManifest) diff --git a/tests/e2e/edge-middleware.test.ts b/tests/e2e/edge-middleware.test.ts index c40eabc931..e78333c81d 100644 --- a/tests/e2e/edge-middleware.test.ts +++ b/tests/e2e/edge-middleware.test.ts @@ -3,6 +3,10 @@ import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs' import { test } from '../utils/playwright-helpers.js' import { getImageSize } from 'next/dist/server/image-optimizer.js' +type ExtendedWindow = Window & { + didReload?: boolean +} + test('Runs edge middleware', async ({ page, middleware }) => { await page.goto(`${middleware.url}/test/redirect`) @@ -53,21 +57,144 @@ test('it should render OpenGraph image meta tag correctly', async ({ page, middl expect([size.width, size.height]).toEqual([1200, 630]) }) -test('json data rewrite works', async ({ middlewarePages }) => { - const response = await fetch(`${middlewarePages.url}/_next/data/build-id/sha.json`, { - headers: { - 'x-nextjs-data': '1', +test.describe('json data', () => { + const testConfigs = [ + { + describeLabel: 'NextResponse.next() -> getServerSideProps page', + selector: 'NextResponse.next()#getServerSideProps', + jsonPathMatcher: '/link/next-getserversideprops.json', }, - }) + { + describeLabel: 'NextResponse.next() -> getStaticProps page', + selector: 'NextResponse.next()#getStaticProps', + jsonPathMatcher: '/link/next-getstaticprops.json', + }, + { + describeLabel: 'NextResponse.next() -> fully static page', + selector: 'NextResponse.next()#fullyStatic', + jsonPathMatcher: '/link/next-fullystatic.json', + }, + { + describeLabel: 'NextResponse.rewrite() -> getServerSideProps page', + selector: 'NextResponse.rewrite()#getServerSideProps', + jsonPathMatcher: '/link/rewrite-me-getserversideprops.json', + }, + { + describeLabel: 'NextResponse.rewrite() -> getStaticProps page', + selector: 'NextResponse.rewrite()#getStaticProps', + jsonPathMatcher: '/link/rewrite-me-getstaticprops.json', + }, + ] + + // Linking to static pages reloads on rewrite for versions below 14 + if (nextVersionSatisfies('>=14.0.0')) { + testConfigs.push({ + describeLabel: 'NextResponse.rewrite() -> fully static page', + selector: 'NextResponse.rewrite()#fullyStatic', + jsonPathMatcher: '/link/rewrite-me-fullystatic.json', + }) + } + + test.describe('no 18n', () => { + for (const testConfig of testConfigs) { + test.describe(testConfig.describeLabel, () => { + test('json data fetch', async ({ middlewarePages, page }) => { + const dataFetchPromise = new Promise((resolve) => { + page.on('response', (response) => { + if (response.url().includes(testConfig.jsonPathMatcher)) { + resolve(response) + } + }) + }) + + await page.goto(`${middlewarePages.url}/link`) + + await page.hover(`[data-link="${testConfig.selector}"]`) - expect(response.ok).toBe(true) - const body = await response.text() + const dataResponse = await dataFetchPromise - expect(body).toMatch(/^{"pageProps":/) + expect(dataResponse.ok()).toBe(true) + }) - const data = JSON.parse(body) + test('navigation', async ({ middlewarePages, page }) => { + await page.goto(`${middlewarePages.url}/link`) - expect(data.pageProps.message).toBeDefined() + await page.evaluate(() => { + // set some value to window to check later if browser did reload and lost this state + ;(window as ExtendedWindow).didReload = false + }) + + await page.click(`[data-link="${testConfig.selector}"]`) + + // wait for page to be rendered + await page.waitForSelector(`[data-page="${testConfig.selector}"]`) + + // check if browser navigation worked by checking if state was preserved + const browserNavigationWorked = + (await page.evaluate(() => { + return (window as ExtendedWindow).didReload + })) === false + + // we expect client navigation to work without browser reload + expect(browserNavigationWorked).toBe(true) + }) + }) + } + }) + test.describe('with 18n', () => { + for (const testConfig of testConfigs) { + test.describe(testConfig.describeLabel, () => { + for (const { localeLabel, pageWithLinksPathname } of [ + { localeLabel: 'implicit default locale', pageWithLinksPathname: '/link' }, + { localeLabel: 'explicit default locale', pageWithLinksPathname: '/en/link' }, + { localeLabel: 'explicit non-default locale', pageWithLinksPathname: '/fr/link' }, + ]) { + test.describe(localeLabel, () => { + test('json data fetch', async ({ middlewareI18n, page }) => { + const dataFetchPromise = new Promise((resolve) => { + page.on('response', (response) => { + if (response.url().includes(testConfig.jsonPathMatcher)) { + resolve(response) + } + }) + }) + + await page.goto(`${middlewareI18n.url}${pageWithLinksPathname}`) + + await page.hover(`[data-link="${testConfig.selector}"]`) + + const dataResponse = await dataFetchPromise + + expect(dataResponse.ok()).toBe(true) + }) + + test('navigation', async ({ middlewareI18n, page }) => { + await page.goto(`${middlewareI18n.url}${pageWithLinksPathname}`) + + await page.evaluate(() => { + // set some value to window to check later if browser did reload and lost this state + ;(window as ExtendedWindow).didReload = false + }) + + await page.click(`[data-link="${testConfig.selector}"]`) + + // wait for page to be rendered + await page.waitForSelector(`[data-page="${testConfig.selector}"]`) + + // check if browser navigation worked by checking if state was preserved + const browserNavigationWorked = + (await page.evaluate(() => { + return (window as ExtendedWindow).didReload + })) === false + + // we expect client navigation to work without browser reload + expect(browserNavigationWorked).toBe(true) + }) + }) + } + }) + } + }) }) // those tests use `fetch` instead of `page.goto` intentionally to avoid potential client rendering diff --git a/tests/fixtures/middleware-i18n/middleware.js b/tests/fixtures/middleware-i18n/middleware.js index 0057764f47..3462214f1d 100644 --- a/tests/fixtures/middleware-i18n/middleware.js +++ b/tests/fixtures/middleware-i18n/middleware.js @@ -8,6 +8,26 @@ export async function middleware(request) { return NextResponse.next() } + if (url.pathname.startsWith('/link/next')) { + return NextResponse.next({ + headers: { + 'x-middleware-test': 'link-next', + }, + }) + } + + if (url.pathname.startsWith('/link/rewrite-me')) { + const rewriteUrl = new URL( + url.pathname.replace('/link/rewrite-me', '/link/rewrite-target'), + url, + ) + return NextResponse.rewrite(rewriteUrl, { + headers: { + 'x-middleware-test': 'link-rewrite', + }, + }) + } + if (url.pathname === '/old-home') { if (url.searchParams.get('override') === 'external') { return Response.redirect('https://example.vercel.sh') diff --git a/tests/fixtures/middleware-i18n/pages/link/index.js b/tests/fixtures/middleware-i18n/pages/link/index.js new file mode 100644 index 0000000000..73699d73a1 --- /dev/null +++ b/tests/fixtures/middleware-i18n/pages/link/index.js @@ -0,0 +1,67 @@ +import Link from 'next/link' + +export default function Page() { + return ( +
+

Page with Links

+
    +
  • + NextResponse.next() +
      +
    • + + getServerSideProps + +
    • + +
    • + + getStaticProps + +
    • + +
    • + + fullyStatic + +
    • +
    +
  • +
  • + NextResponse.rewrite() +
      +
    • + + getServerSideProps + +
    • + +
    • + + getStaticProps + +
    • + +
    • + + fullyStatic + +
    • +
    +
  • +
+
+ ) +} diff --git a/tests/fixtures/middleware-i18n/pages/link/next-fullystatic.js b/tests/fixtures/middleware-i18n/pages/link/next-fullystatic.js new file mode 100644 index 0000000000..f8461a24ef --- /dev/null +++ b/tests/fixtures/middleware-i18n/pages/link/next-fullystatic.js @@ -0,0 +1,7 @@ +export default function Page() { + return ( +
+

fully static page

+
+ ) +} diff --git a/tests/fixtures/middleware-i18n/pages/link/next-getserversideprops.js b/tests/fixtures/middleware-i18n/pages/link/next-getserversideprops.js new file mode 100644 index 0000000000..f177bf770b --- /dev/null +++ b/tests/fixtures/middleware-i18n/pages/link/next-getserversideprops.js @@ -0,0 +1,15 @@ +export default function Page() { + return ( +
+

+ getServerSideProps page +

+
+ ) +} + +export function getServerSideProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/middleware-i18n/pages/link/next-getstaticprops.js b/tests/fixtures/middleware-i18n/pages/link/next-getstaticprops.js new file mode 100644 index 0000000000..847a6f626b --- /dev/null +++ b/tests/fixtures/middleware-i18n/pages/link/next-getstaticprops.js @@ -0,0 +1,15 @@ +export default function Page() { + return ( +
+

+ getStaticProps page +

+
+ ) +} + +export function getStaticProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/middleware-i18n/pages/link/rewrite-target-fullystatic.js b/tests/fixtures/middleware-i18n/pages/link/rewrite-target-fullystatic.js new file mode 100644 index 0000000000..8de98fef67 --- /dev/null +++ b/tests/fixtures/middleware-i18n/pages/link/rewrite-target-fullystatic.js @@ -0,0 +1,7 @@ +export default function Page() { + return ( +
+

fully static page

+
+ ) +} diff --git a/tests/fixtures/middleware-i18n/pages/link/rewrite-target-getserversideprops.js b/tests/fixtures/middleware-i18n/pages/link/rewrite-target-getserversideprops.js new file mode 100644 index 0000000000..b621a86d93 --- /dev/null +++ b/tests/fixtures/middleware-i18n/pages/link/rewrite-target-getserversideprops.js @@ -0,0 +1,15 @@ +export default function Page() { + return ( +
+

+ getServerSideProps page +

+
+ ) +} + +export function getServerSideProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/middleware-i18n/pages/link/rewrite-target-getstaticprops.js b/tests/fixtures/middleware-i18n/pages/link/rewrite-target-getstaticprops.js new file mode 100644 index 0000000000..60ede7e031 --- /dev/null +++ b/tests/fixtures/middleware-i18n/pages/link/rewrite-target-getstaticprops.js @@ -0,0 +1,15 @@ +export default function Page() { + return ( +
+

+ getStaticProps page +

+
+ ) +} + +export function getStaticProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/middleware-pages/middleware.js b/tests/fixtures/middleware-pages/middleware.js index 1dbf1eaf26..a89a491a8c 100644 --- a/tests/fixtures/middleware-pages/middleware.js +++ b/tests/fixtures/middleware-pages/middleware.js @@ -8,6 +8,26 @@ export async function middleware(request) { return NextResponse.next() } + if (url.pathname.startsWith('/link/next')) { + return NextResponse.next({ + headers: { + 'x-middleware-test': 'link-next', + }, + }) + } + + if (url.pathname.startsWith('/link/rewrite-me')) { + const rewriteUrl = new URL( + url.pathname.replace('/link/rewrite-me', '/link/rewrite-target'), + url, + ) + return NextResponse.rewrite(rewriteUrl, { + headers: { + 'x-middleware-test': 'link-rewrite', + }, + }) + } + if (request.headers.get('x-prerender-revalidate')) { return NextResponse.next({ headers: { 'x-middleware': 'hi' }, diff --git a/tests/fixtures/middleware-pages/pages/link/index.js b/tests/fixtures/middleware-pages/pages/link/index.js new file mode 100644 index 0000000000..73699d73a1 --- /dev/null +++ b/tests/fixtures/middleware-pages/pages/link/index.js @@ -0,0 +1,67 @@ +import Link from 'next/link' + +export default function Page() { + return ( +
+

Page with Links

+
    +
  • + NextResponse.next() +
      +
    • + + getServerSideProps + +
    • + +
    • + + getStaticProps + +
    • + +
    • + + fullyStatic + +
    • +
    +
  • +
  • + NextResponse.rewrite() +
      +
    • + + getServerSideProps + +
    • + +
    • + + getStaticProps + +
    • + +
    • + + fullyStatic + +
    • +
    +
  • +
+
+ ) +} diff --git a/tests/fixtures/middleware-pages/pages/link/next-fullystatic.js b/tests/fixtures/middleware-pages/pages/link/next-fullystatic.js new file mode 100644 index 0000000000..f8461a24ef --- /dev/null +++ b/tests/fixtures/middleware-pages/pages/link/next-fullystatic.js @@ -0,0 +1,7 @@ +export default function Page() { + return ( +
+

fully static page

+
+ ) +} diff --git a/tests/fixtures/middleware-pages/pages/link/next-getserversideprops.js b/tests/fixtures/middleware-pages/pages/link/next-getserversideprops.js new file mode 100644 index 0000000000..f177bf770b --- /dev/null +++ b/tests/fixtures/middleware-pages/pages/link/next-getserversideprops.js @@ -0,0 +1,15 @@ +export default function Page() { + return ( +
+

+ getServerSideProps page +

+
+ ) +} + +export function getServerSideProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/middleware-pages/pages/link/next-getstaticprops.js b/tests/fixtures/middleware-pages/pages/link/next-getstaticprops.js new file mode 100644 index 0000000000..847a6f626b --- /dev/null +++ b/tests/fixtures/middleware-pages/pages/link/next-getstaticprops.js @@ -0,0 +1,15 @@ +export default function Page() { + return ( +
+

+ getStaticProps page +

+
+ ) +} + +export function getStaticProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/middleware-pages/pages/link/rewrite-target-fullystatic.js b/tests/fixtures/middleware-pages/pages/link/rewrite-target-fullystatic.js new file mode 100644 index 0000000000..8de98fef67 --- /dev/null +++ b/tests/fixtures/middleware-pages/pages/link/rewrite-target-fullystatic.js @@ -0,0 +1,7 @@ +export default function Page() { + return ( +
+

fully static page

+
+ ) +} diff --git a/tests/fixtures/middleware-pages/pages/link/rewrite-target-getserversideprops.js b/tests/fixtures/middleware-pages/pages/link/rewrite-target-getserversideprops.js new file mode 100644 index 0000000000..b621a86d93 --- /dev/null +++ b/tests/fixtures/middleware-pages/pages/link/rewrite-target-getserversideprops.js @@ -0,0 +1,15 @@ +export default function Page() { + return ( +
+

+ getServerSideProps page +

+
+ ) +} + +export function getServerSideProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/middleware-pages/pages/link/rewrite-target-getstaticprops.js b/tests/fixtures/middleware-pages/pages/link/rewrite-target-getstaticprops.js new file mode 100644 index 0000000000..60ede7e031 --- /dev/null +++ b/tests/fixtures/middleware-pages/pages/link/rewrite-target-getstaticprops.js @@ -0,0 +1,15 @@ +export default function Page() { + return ( +
+

+ getStaticProps page +

+
+ ) +} + +export function getStaticProps() { + return { + props: {}, + } +} diff --git a/tests/utils/create-e2e-fixture.ts b/tests/utils/create-e2e-fixture.ts index 5d17341700..d7af065d21 100644 --- a/tests/utils/create-e2e-fixture.ts +++ b/tests/utils/create-e2e-fixture.ts @@ -333,6 +333,7 @@ export const fixtureFactories = { pnpm: () => createE2EFixture('pnpm', { packageManger: 'pnpm' }), bun: () => createE2EFixture('simple', { packageManger: 'bun' }), middleware: () => createE2EFixture('middleware'), + middlewareI18n: () => createE2EFixture('middleware-i18n'), middlewareI18nExcludedPaths: () => createE2EFixture('middleware-i18n-excluded-paths'), middlewareOg: () => createE2EFixture('middleware-og'), middlewarePages: () => createE2EFixture('middleware-pages'),