From 5728b12ceefdaa82c0436452d13c3782639cbc3a Mon Sep 17 00:00:00 2001 From: Leonardo Ortiz Date: Wed, 17 Dec 2025 11:45:59 -0300 Subject: [PATCH 1/2] middleware/proxy fixes for next.js 15/16 --- src/frameworks/next/constants.ts | 3 + src/frameworks/next/index.ts | 11 +- src/frameworks/next/interfaces.ts | 96 ++++++++++++- src/frameworks/next/testing/index.ts | 1 + src/frameworks/next/testing/middleware.ts | 73 +++++++++- src/frameworks/next/utils.spec.ts | 161 +++++++++++++++++----- src/frameworks/next/utils.ts | 59 ++++++-- 7 files changed, 358 insertions(+), 46 deletions(-) diff --git a/src/frameworks/next/constants.ts b/src/frameworks/next/constants.ts index ef507a72189..35282551494 100644 --- a/src/frameworks/next/constants.ts +++ b/src/frameworks/next/constants.ts @@ -8,6 +8,7 @@ import type { ROUTES_MANIFEST as ROUTES_MANIFEST_TYPE, APP_PATHS_MANIFEST as APP_PATHS_MANIFEST_TYPE, SERVER_REFERENCE_MANIFEST as SERVER_REFERENCE_MANIFEST_TYPE, + FUNCTIONS_CONFIG_MANIFEST as FUNCTIONS_CONFIG_MANIFEST_TYPE, } from "next/constants"; import type { WEBPACK_LAYERS as NEXTJS_WEBPACK_LAYERS } from "next/dist/lib/constants"; @@ -16,6 +17,8 @@ export const APP_PATH_ROUTES_MANIFEST: typeof APP_PATH_ROUTES_MANIFEST_TYPE = export const EXPORT_MARKER: typeof EXPORT_MARKER_TYPE = "export-marker.json"; export const IMAGES_MANIFEST: typeof IMAGES_MANIFEST_TYPE = "images-manifest.json"; export const MIDDLEWARE_MANIFEST: typeof MIDDLEWARE_MANIFEST_TYPE = "middleware-manifest.json"; +export const FUNCTIONS_CONFIG_MANIFEST: typeof FUNCTIONS_CONFIG_MANIFEST_TYPE = + "functions-config-manifest.json"; export const PAGES_MANIFEST: typeof PAGES_MANIFEST_TYPE = "pages-manifest.json"; export const PRERENDER_MANIFEST: typeof PRERENDER_MANIFEST_TYPE = "prerender-manifest.json"; export const ROUTES_MANIFEST: typeof ROUTES_MANIFEST_TYPE = "routes-manifest.json"; diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index 2688bc5753a..4cc7e1dc4a9 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -72,6 +72,7 @@ import type { MiddlewareManifest, ActionManifest, CustomBuildOptions, + FunctionsConfigManifest, } from "./interfaces"; import { MIDDLEWARE_MANIFEST, @@ -82,6 +83,7 @@ import { APP_PATHS_MANIFEST, SERVER_REFERENCE_MANIFEST, ESBUILD_VERSION, + FUNCTIONS_CONFIG_MANIFEST, } from "./constants"; import { getAllSiteDomains, getDeploymentDomain } from "../../hosting/api"; import { logger } from "../../logger"; @@ -454,6 +456,7 @@ export async function ɵcodegenPublicDirectory( pagesManifest, appPathRoutesManifest, serverReferenceManifest, + functionsConfigManifest, ] = await Promise.all([ readJSON(join(sourceDir, distDir, "server", MIDDLEWARE_MANIFEST)), readJSON(join(sourceDir, distDir, PRERENDER_MANIFEST)), @@ -465,11 +468,17 @@ export async function ɵcodegenPublicDirectory( readJSON(join(sourceDir, distDir, "server", SERVER_REFERENCE_MANIFEST)).catch( () => ({ node: {}, edge: {}, encryptionKey: "" }), ), + readJSON( + join(sourceDir, distDir, "server", FUNCTIONS_CONFIG_MANIFEST), + ).catch(() => ({ version: 0, functions: {} })), ]); const appPathRoutesEntries = Object.entries(appPathRoutesManifest); - const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareManifest); + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes( + middlewareManifest, + functionsConfigManifest, + ); const { redirects = [], rewrites = [], headers = [] } = routesManifest; diff --git a/src/frameworks/next/interfaces.ts b/src/frameworks/next/interfaces.ts index bd9ae9fb7e4..6eb55b67f4f 100644 --- a/src/frameworks/next/interfaces.ts +++ b/src/frameworks/next/interfaces.ts @@ -83,10 +83,104 @@ export interface ExportMarker { isNextImageImported: boolean; } -export type MiddlewareManifest = MiddlewareManifestV1 | MiddlewareManifestV2FromNext; +export type MiddlewareManifest = + | MiddlewareManifestV1 + | MiddlewareManifestV2FromNext + | MiddlewareManifestV3; export type MiddlewareManifestV2 = MiddlewareManifestV2FromNext; +/** + * Middleware manifest types for Next.js 16 + * + * @see https://github.com/vercel/next.js/blob/3352f9ee9342b40aaded91c340e7e11650aa4867/packages/next/src/build/webpack/plugins/middleware-plugin.ts#L55 + */ +export type MiddlewareManifestV3 = { + version: 3; + sortedMiddleware: string[]; + middleware: { [page: string]: EdgeFunctionDefinition }; + functions: { [page: string]: EdgeFunctionDefinition }; +}; + +/** + * Type required for MiddlewareManifestV3 + * + * @see https://github.com/vercel/next.js/blob/3352f9ee9342b40aaded91c340e7e11650aa4867/packages/next/src/build/webpack/plugins/middleware-plugin.ts#L44-L53 + */ +interface EdgeFunctionDefinition { + files: string[]; + name: string; + page: string; + matchers: ProxyMatcherNext16[]; + env: Record; + wasm?: AssetBinding[]; + assets?: AssetBinding[]; + regions?: string[] | string; +} + +/** + * Type required for MiddlewareManifestV3 + * + * @see https://github.com/vercel/next.js/blob/3352f9ee9342b40aaded91c340e7e11650aa4867/packages/next/src/build/analysis/get-page-static-info.ts#L48-L54 + */ +type ProxyMatcherNext16 = { + regexp: string; + locale?: false; + has?: RouteHasNext16[]; + missing?: RouteHasNext16[]; + originalSource: string; +}; + +/** + * Type required for MiddlewareManifestV3 + * + * @see https://github.com/vercel/next.js/blob/3352f9ee9342b40aaded91c340e7e11650aa4867/packages/next/src/lib/load-custom-routes.ts#L10-L20 + */ +type RouteHasNext16 = + | { + type: "header" | "cookie" | "query"; + key: string; + value?: string; + } + | { + type: "host"; + key?: undefined; + value: string; + }; + +/** + * Type required for MiddlewareManifestV3 + * + * @see https://github.com/vercel/next.js/blob/3352f9ee9342b40aaded91c340e7e11650aa4867/packages/next/src/build/webpack/loaders/get-module-build-info.ts#L59 + */ +interface AssetBinding { + filePath: string; + name: string; +} + +/** + * Manifest used to detect proxy path matchers in Next.js 16+ + * + * @see https://github.com/vercel/next.js/blob/3352f9ee9342b40aaded91c340e7e11650aa4867/packages/next/src/build/index.ts#L576-L588 + */ +export interface FunctionsConfigManifest { + version: number; + functions: Record< + string, + { + maxDuration?: number; + runtime?: "nodejs"; + regions?: string[] | string; + matchers?: Array<{ + regexp: string; + originalSource: string; + has?: RouteHas[]; + missing?: RouteHas[]; + }>; + } + >; +} + // See: https://github.com/vercel/next.js/blob/b188fab3360855c28fd9407bd07c4ee9f5de16a6/packages/next/build/webpack/plugins/middleware-plugin.ts#L15-L29 export interface MiddlewareManifestV1 { version: 1; diff --git a/src/frameworks/next/testing/index.ts b/src/frameworks/next/testing/index.ts index a50d4fca72d..3838698fb18 100644 --- a/src/frameworks/next/testing/index.ts +++ b/src/frameworks/next/testing/index.ts @@ -6,3 +6,4 @@ export * from "./images"; export * from "./middleware"; export * from "./npm"; export * from "./app"; +export * from "./i18n"; diff --git a/src/frameworks/next/testing/middleware.ts b/src/frameworks/next/testing/middleware.ts index 69750060d24..7cc06971f12 100644 --- a/src/frameworks/next/testing/middleware.ts +++ b/src/frameworks/next/testing/middleware.ts @@ -1,4 +1,75 @@ -import type { MiddlewareManifestV1, MiddlewareManifestV2 } from "../interfaces"; +import type { + MiddlewareManifestV1, + MiddlewareManifestV2, + MiddlewareManifestV3, + FunctionsConfigManifest, +} from "../interfaces"; + +export const middlewareV3ManifestWhenUsed: MiddlewareManifestV3 = { + sortedMiddleware: [], + middleware: {}, + functions: {}, + version: 3, +}; + +export const functionsConfigManifestWhenUsed: FunctionsConfigManifest = { + version: 1, + functions: { + "/_middleware": { + runtime: "nodejs", + matchers: [ + { + regexp: "^(?:\\/(_next\\/data\\/[^/]{1,}))?\\/(\\.json)?[\\/#\\?]?$", + originalSource: "/", + }, + ], + }, + }, +}; + +export const middlewareV3ManifestWhenNotUsed: MiddlewareManifestV3 = { + version: 3, + middleware: {}, + sortedMiddleware: [], + functions: {}, +}; + +export const functionsConfigManifestWhenNotUsed: FunctionsConfigManifest = { + version: 1, + functions: {}, +}; + +export const middlewareV3ManifestWithDeprecatedMiddleware: MiddlewareManifestV3 = { + version: 3, + middleware: { + "/": { + files: [ + "server/edge/chunks/[root-of-the-server]__123._.js", + "server/edge/chunks/node_modules_next_dist_123._.js", + "server/edge/chunks/turbopack-edge-wrapper_123.js", + ], + name: "middleware", + page: "/", + matchers: [ + { + regexp: "^(?:\\/(_next\\/data\\/[^/]{1,}))?\\/(\\\\.json)?[\\/#\\?]?$", + originalSource: "/", + }, + ], + wasm: [], + assets: [], + env: { + __NEXT_BUILD_ID: "1", + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: "1", + __NEXT_PREVIEW_MODE_ID: "1", + __NEXT_PREVIEW_MODE_ENCRYPTION_KEY: "1", + __NEXT_PREVIEW_MODE_SIGNING_KEY: "1", + }, + }, + }, + sortedMiddleware: ["/"], + functions: {}, +}; export const middlewareV2ManifestWhenUsed: MiddlewareManifestV2 = { sortedMiddleware: ["/"], diff --git a/src/frameworks/next/utils.spec.ts b/src/frameworks/next/utils.spec.ts index 8eba23c13eb..a0bfe92e034 100644 --- a/src/frameworks/next/utils.spec.ts +++ b/src/frameworks/next/utils.spec.ts @@ -12,6 +12,8 @@ import { IMAGES_MANIFEST, APP_PATH_ROUTES_MANIFEST, ESBUILD_VERSION, + FUNCTIONS_CONFIG_MANIFEST, + MIDDLEWARE_MANIFEST, } from "./constants"; import { @@ -72,8 +74,13 @@ import { clientReferenceManifestWithImage, clientReferenceManifestWithoutImage, serverReferenceManifest, + middlewareV3ManifestWhenUsed, + functionsConfigManifestWhenUsed, + middlewareV3ManifestWhenNotUsed, + functionsConfigManifestWhenNotUsed, + middlewareV3ManifestWithDeprecatedMiddleware, + pathsWithCustomRoutesInternalPrefix, } from "./testing"; -import { pathsWithCustomRoutesInternalPrefix } from "./testing/i18n"; describe("Next.js utils", () => { describe("cleanEscapedChars", () => { @@ -233,24 +240,66 @@ describe("Next.js utils", () => { beforeEach(() => (sandbox = sinon.createSandbox())); afterEach(() => sandbox.restore()); - it("should return true if using middleware in development", async () => { - sandbox.stub(fsExtra, "pathExists").resolves(true); - expect(await isUsingMiddleware("", true)).to.be.true; - }); + describe("development", () => { + it("should return true if using middleware", async () => { + sandbox.stub(fsExtra, "pathExists").resolves(true); + expect(await isUsingMiddleware("", true)).to.be.true; + }); - it("should return false if not using middleware in development", async () => { - sandbox.stub(fsExtra, "pathExists").resolves(false); - expect(await isUsingMiddleware("", true)).to.be.false; + it("should return false if not using middleware", async () => { + sandbox.stub(fsExtra, "pathExists").resolves(false); + expect(await isUsingMiddleware("", true)).to.be.false; + }); }); - it("should return true if using middleware in production", async () => { - sandbox.stub(fsExtra, "readJSON").resolves(middlewareV2ManifestWhenUsed); - expect(await isUsingMiddleware("", false)).to.be.true; + describe("production (v2)", () => { + it("should return true if using middleware", async () => { + sandbox.stub(fsExtra, "readJSON").resolves(middlewareV2ManifestWhenUsed); + expect(await isUsingMiddleware("", false)).to.be.true; + }); + + it("should return false if not using middleware", async () => { + sandbox.stub(fsExtra, "readJSON").resolves(middlewareV2ManifestWhenNotUsed); + expect(await isUsingMiddleware("", false)).to.be.false; + }); }); - it("should return false if not using middleware in production", async () => { - sandbox.stub(fsExtra, "readJSON").resolves(middlewareV2ManifestWhenNotUsed); - expect(await isUsingMiddleware("", false)).to.be.false; + describe("production (v3)", () => { + it("should return true if using middleware", async () => { + const readJsonStub = sandbox.stub(frameworksUtils, "readJSON"); + readJsonStub + .withArgs(sinon.match(MIDDLEWARE_MANIFEST)) + .resolves(middlewareV3ManifestWhenUsed); + readJsonStub + .withArgs(sinon.match(FUNCTIONS_CONFIG_MANIFEST)) + .resolves(functionsConfigManifestWhenUsed); + + expect(await isUsingMiddleware("", false)).to.be.true; + }); + + it("should return true if using deprecated middleware", async () => { + const readJsonStub = sandbox.stub(frameworksUtils, "readJSON"); + readJsonStub + .withArgs(sinon.match(MIDDLEWARE_MANIFEST)) + .resolves(middlewareV3ManifestWithDeprecatedMiddleware); + readJsonStub + .withArgs(sinon.match(FUNCTIONS_CONFIG_MANIFEST)) + .resolves(functionsConfigManifestWhenNotUsed); + + expect(await isUsingMiddleware("", false)).to.be.true; + }); + + it("should return false if not using middleware", async () => { + const readJsonStub = sandbox.stub(frameworksUtils, "readJSON"); + readJsonStub + .withArgs(sinon.match(MIDDLEWARE_MANIFEST)) + .resolves(middlewareV3ManifestWhenNotUsed); + readJsonStub + .withArgs(sinon.match(FUNCTIONS_CONFIG_MANIFEST)) + .resolves(functionsConfigManifestWhenNotUsed); + + expect(await isUsingMiddleware("", false)).to.be.false; + }); }); }); @@ -420,32 +469,82 @@ describe("Next.js utils", () => { }); describe("getMiddlewareMatcherRegexes", () => { - it("should return regexes when using version 1", () => { - const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV1ManifestWhenUsed); + describe("middleware version 1", () => { + it("should return regexes", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes( + middlewareV1ManifestWhenUsed, + functionsConfigManifestWhenNotUsed, + ); - for (const regex of middlewareMatcherRegexes) { - expect(regex).to.be.an.instanceOf(RegExp); - } - }); + for (const regex of middlewareMatcherRegexes) { + expect(regex).to.be.an.instanceOf(RegExp); + } + }); - it("should return empty array when using version 1 but not using middleware", () => { - const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV1ManifestWhenNotUsed); + it("should return empty array when unused", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes( + middlewareV1ManifestWhenNotUsed, + functionsConfigManifestWhenNotUsed, + ); - expect(middlewareMatcherRegexes).to.eql([]); + expect(middlewareMatcherRegexes).to.eql([]); + }); }); - it("should return regexes when using version 2", () => { - const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV2ManifestWhenUsed); + describe("middleware version 2", () => { + it("should return regexes", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes( + middlewareV2ManifestWhenUsed, + functionsConfigManifestWhenNotUsed, + ); - for (const regex of middlewareMatcherRegexes) { - expect(regex).to.be.an.instanceOf(RegExp); - } + for (const regex of middlewareMatcherRegexes) { + expect(regex).to.be.an.instanceOf(RegExp); + } + }); + + it("should return empty array when unused", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes( + middlewareV2ManifestWhenNotUsed, + functionsConfigManifestWhenNotUsed, + ); + + expect(middlewareMatcherRegexes).to.eql([]); + }); }); - it("should return empty array when using version 2 but not using middleware", () => { - const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV2ManifestWhenNotUsed); + describe("middleware version 3", () => { + it("should return regexes", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes( + middlewareV3ManifestWhenUsed, + functionsConfigManifestWhenUsed, + ); - expect(middlewareMatcherRegexes).to.eql([]); + for (const regex of middlewareMatcherRegexes) { + expect(regex).to.be.an.instanceOf(RegExp); + } + }); + + it("should return empty array when unused", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes( + middlewareV3ManifestWhenNotUsed, + functionsConfigManifestWhenNotUsed, + ); + + expect(middlewareMatcherRegexes).to.eql([]); + }); + + it("should return regexes from deprecated manifest", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes( + middlewareV3ManifestWithDeprecatedMiddleware, + functionsConfigManifestWhenNotUsed, + ); + + for (const regex of middlewareMatcherRegexes) { + expect(regex).to.be.an.instanceOf(RegExp); + } + expect(middlewareMatcherRegexes).to.have.length(1); + }); }); }); diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts index 3bc9c245c64..69ec2f86584 100644 --- a/src/frameworks/next/utils.ts +++ b/src/frameworks/next/utils.ts @@ -23,6 +23,8 @@ import type { AppPathRoutesManifest, ActionManifest, NextConfigFileName, + FunctionsConfigManifest, + MiddlewareManifestV3, } from "./interfaces"; import { APP_PATH_ROUTES_MANIFEST, @@ -32,6 +34,7 @@ import { WEBPACK_LAYERS, CONFIG_FILES, ESBUILD_VERSION, + FUNCTIONS_CONFIG_MANIFEST, } from "./constants"; import { dirExistsSync, fileExistsSync } from "../../fsutils"; import { IS_WINDOWS } from "../../utils"; @@ -195,24 +198,36 @@ export async function hasUnoptimizedImage(sourceDir: string, distDir: string): P } /** - * Whether Next.js middleware is being used + * Whether Next.js proxy/middleware is being used * * @param dir in development must be the project root path, otherwise `distDir` * @param isDevMode whether the project is running on dev or production */ export async function isUsingMiddleware(dir: string, isDevMode: boolean): Promise { if (isDevMode) { - const [middlewareJs, middlewareTs] = await Promise.all([ + const [middlewareJs, middlewareTs, proxyJs, proxyTs] = await Promise.all([ pathExists(join(dir, "middleware.js")), pathExists(join(dir, "middleware.ts")), + pathExists(join(dir, "proxy.js")), + pathExists(join(dir, "proxy.ts")), ]); - return middlewareJs || middlewareTs; + return middlewareJs || middlewareTs || proxyJs || proxyTs; } else { const middlewareManifest: MiddlewareManifest = await readJSON( join(dir, "server", MIDDLEWARE_MANIFEST), ); + if (middlewareManifest.version === 3) { + const functionsConfigManifest = await readJSON( + join(dir, "server", FUNCTIONS_CONFIG_MANIFEST), + ).catch(() => undefined); + + if ((functionsConfigManifest?.functions?.["/_middleware"]?.matchers || [])?.length > 0) { + return true; + } + } + return Object.keys(middlewareManifest.middleware).length > 0; } } @@ -303,19 +318,39 @@ export function allDependencyNames(mod: NpmLsDepdendency): string[] { /** * Get regexes from middleware matcher manifest */ -export function getMiddlewareMatcherRegexes(middlewareManifest: MiddlewareManifest): RegExp[] { +export function getMiddlewareMatcherRegexes( + middlewareManifest: MiddlewareManifest, + functionsConfigManifest: FunctionsConfigManifest, +): RegExp[] { const middlewareObjectValues = Object.values(middlewareManifest.middleware); - - let middlewareMatchers: Record<"regexp", string>[]; + const middlewareMatchers: Record<"regexp", string>[] = []; if (middlewareManifest.version === 1) { - middlewareMatchers = middlewareObjectValues.map( - (page: MiddlewareManifestV1["middleware"]["page"]) => ({ regexp: page.regexp }), + middlewareMatchers.push( + ...middlewareObjectValues.map((page: MiddlewareManifestV1["middleware"][string]) => ({ + regexp: page.regexp, + })), ); - } else { - middlewareMatchers = middlewareObjectValues - .map((page: MiddlewareManifestV2["middleware"]["page"]) => page.matchers) - .flat(); + } else if (middlewareManifest.version === 2) { + middlewareMatchers.push( + ...middlewareObjectValues + .map((page: MiddlewareManifestV2["middleware"][string]) => page.matchers) + .flat(), + ); + } else if (middlewareManifest.version === 3) { + if (functionsConfigManifest?.functions?.["/_middleware"]) { + // matchers from proxy.js + middlewareMatchers.push( + ...(functionsConfigManifest.functions["/_middleware"].matchers || []), + ); + } else { + // matchers from middleware.js + middlewareMatchers.push( + ...middlewareObjectValues + .map((page: MiddlewareManifestV3["middleware"][string]) => page.matchers) + .flat(), + ); + } } return middlewareMatchers.map((matcher) => new RegExp(matcher.regexp)); From f66edfe5aadd8bf2e9df84bf3deb45059409481f Mon Sep 17 00:00:00 2001 From: Leonardo Ortiz Date: Wed, 17 Dec 2025 15:41:46 -0300 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a4999e810..041da0bfea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Update Data Connect Emulator to 3.0.1, which addresses some internal errors (#9627) +- Fix proxy.js/proxy.ts in Next.js 16 (#9631)