Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
- Resolve timeouts when bundling Next.js applications for Cloud Functions (#5691)
- Fixes bug where the functions emulator would attempt to call to prod for 'demo-' projects (#5170)
- Address issues starting the Firebase Hosting emulator with some versions of Next.js (#5781)
- Fix regex page matcher for Next.js middlewares version 1 (#5496)
33 changes: 17 additions & 16 deletions src/frameworks/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { mkdir, copyFile } from "fs/promises";
import { basename, dirname, join } from "path";
import type { NextConfig } from "next";
import type { PrerenderManifest } from "next/dist/build";
import type { MiddlewareManifest } from "next/dist/build/webpack/plugins/middleware-plugin";
import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin";
import { copy, mkdirp, pathExists, pathExistsSync } from "fs-extra";
import { pathToFileURL, parse } from "url";
Expand All @@ -18,9 +17,19 @@ import { pick } from "stream-json/filters/Pick";
import { streamObject } from "stream-json/streamers/StreamObject";
import { fileExistsSync, dirExistsSync } from "../../fsutils";

import { BuildResult, FrameworkType, SupportLevel } from "../interfaces";
import { promptOnce } from "../../prompt";
import { FirebaseError } from "../../error";
import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES } from "../constants";
import type { EmulatorInfo } from "../../emulator/types";
import {
readJSON,
simpleProxy,
warnIfCustomBuildScript,
relativeRequire,
findDependency,
} from "../utils";
import { BuildResult, FrameworkType, SupportLevel } from "../interfaces";

import {
cleanEscapedChars,
getNextjsRewritesToUse,
Expand All @@ -30,27 +39,22 @@ import {
isUsingImageOptimization,
isUsingMiddleware,
allDependencyNames,
getMiddlewareMatcherRegexes,
getNonStaticRoutes,
getNonStaticServerComponents,
getHeadersFromMetaFiles,
usesAppDirRouter,
usesNextImage,
hasUnoptimizedImage,
} from "./utils";
import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES } from "../constants";
import type {
AppPathRoutesManifest,
AppPathsManifest,
HostingHeadersWithSource,
Manifest,
NpmLsDepdendency,
MiddlewareManifest,
} from "./interfaces";
import {
readJSON,
simpleProxy,
warnIfCustomBuildScript,
relativeRequire,
findDependency,
} from "../utils";
import type { EmulatorInfo } from "../../emulator/types";
import { usesAppDirRouter, usesNextImage, hasUnoptimizedImage } from "./utils";
import {
MIDDLEWARE_MANIFEST,
PAGES_MANIFEST,
Expand Down Expand Up @@ -325,10 +329,7 @@ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: strin

const appPathRoutesEntries = Object.entries(appPathRoutesManifest);

const middlewareMatcherRegexes = Object.values(middlewareManifest.middleware)
.map((it) => it.matchers)
.flat()
.map((it) => new RegExp(it.regexp));
const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareManifest);

const { redirects = [], rewrites = [], headers = [] } = routesManifest;

Expand Down
22 changes: 22 additions & 0 deletions src/frameworks/next/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Header, Rewrite, Redirect } from "next/dist/lib/load-custom-routes";
import type { ImageConfigComplete } from "next/dist/shared/lib/image-config";
import type { MiddlewareManifest as MiddlewareManifestV2FromNext } from "next/dist/build/webpack/plugins/middleware-plugin";
import type { HostingHeaders } from "../../firebaseConfig";

export interface RoutesManifestRewrite extends Rewrite {
Expand Down Expand Up @@ -39,6 +40,27 @@ export interface ExportMarker {
isNextImageImported: boolean;
}

export type MiddlewareManifest = MiddlewareManifestV1 | MiddlewareManifestV2FromNext;

export type MiddlewareManifestV2 = MiddlewareManifestV2FromNext;

// See: https://github.com/vercel/next.js/blob/b188fab3360855c28fd9407bd07c4ee9f5de16a6/packages/next/build/webpack/plugins/middleware-plugin.ts#L15-L29
export interface MiddlewareManifestV1 {
version: 1;
sortedMiddleware: string[];
clientInfo: [location: string, isSSR: boolean][];
middleware: {
[page: string]: {
env: string[];
files: string[];
name: string;
page: string;
regexp: string;
wasm?: any[]; // WasmBinding isn't exported from next
};
};
}

export interface ImagesManifest {
version: number;
images: ImageConfigComplete & {
Expand Down
25 changes: 24 additions & 1 deletion src/frameworks/next/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { existsSync } from "fs";
import { pathExists } from "fs-extra";
import { basename, extname, join } from "path";
import type { Header, Redirect, Rewrite } from "next/dist/lib/load-custom-routes";
import type { MiddlewareManifest } from "next/dist/build/webpack/plugins/middleware-plugin";
import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin";

import { isUrl, readJSON } from "../utils";
Expand All @@ -12,6 +11,9 @@ import type {
ExportMarker,
ImagesManifest,
NpmLsDepdendency,
MiddlewareManifest,
MiddlewareManifestV1,
MiddlewareManifestV2,
AppPathsManifest,
AppPathRoutesManifest,
HostingHeadersWithSource,
Expand Down Expand Up @@ -239,6 +241,27 @@ export function allDependencyNames(mod: NpmLsDepdendency): string[] {
return dependencyNames;
}

/**
* Get regexes from middleware matcher manifest
*/
export function getMiddlewareMatcherRegexes(middlewareManifest: MiddlewareManifest): RegExp[] {
const middlewareObjectValues = Object.values(middlewareManifest.middleware);

let middlewareMatchers: Record<"regexp", string>[];

if (middlewareManifest.version === 1) {
middlewareMatchers = middlewareObjectValues.map(
(page: MiddlewareManifestV1["middleware"]["page"]) => ({ regexp: page.regexp })
);
} else {
middlewareMatchers = middlewareObjectValues
.map((page: MiddlewareManifestV2["middleware"]["page"]) => page.matchers)
.flat();
}

return middlewareMatchers.map((matcher) => new RegExp(matcher.regexp));
}

/**
* Get non static routes based on pages-manifest, prerendered and dynamic routes
*/
Expand Down
32 changes: 29 additions & 3 deletions src/test/frameworks/next/helpers/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { MiddlewareManifest } from "next/dist/build/webpack/plugins/middleware-plugin";
import type {
MiddlewareManifestV1,
MiddlewareManifestV2,
} from "../../../../frameworks/next/interfaces";

export const middlewareManifestWhenUsed: MiddlewareManifest = {
export const middlewareV2ManifestWhenUsed: MiddlewareManifestV2 = {
sortedMiddleware: ["/"],
middleware: {
"/": {
Expand All @@ -22,9 +25,32 @@ export const middlewareManifestWhenUsed: MiddlewareManifest = {
version: 2,
};

export const middlewareManifestWhenNotUsed: MiddlewareManifest = {
export const middlewareV2ManifestWhenNotUsed: MiddlewareManifestV2 = {
sortedMiddleware: [],
middleware: {},
functions: {},
version: 2,
};

export const middlewareV1ManifestWhenUsed: MiddlewareManifestV1 = {
sortedMiddleware: ["/"],
clientInfo: [["/", false]],
middleware: {
"/": {
env: [],
files: ["server/edge-runtime-webpack.js", "server/pages/_middleware.js"],
name: "pages/_middleware",
page: "/",
regexp: "^/(?!_next).*$",
wasm: [],
},
},
version: 1,
};

export const middlewareV1ManifestWhenNotUsed: MiddlewareManifestV1 = {
sortedMiddleware: [],
clientInfo: [],
middleware: {},
version: 1,
};
41 changes: 37 additions & 4 deletions src/test/frameworks/next/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isUsingImageOptimization,
isUsingAppDirectory,
allDependencyNames,
getMiddlewareMatcherRegexes,
getNonStaticRoutes,
getNonStaticServerComponents,
getHeadersFromMetaFiles,
Expand All @@ -34,8 +35,8 @@ import {
exportMarkerWithoutImage,
imagesManifest,
imagesManifestUnoptimized,
middlewareManifestWhenNotUsed,
middlewareManifestWhenUsed,
middlewareV2ManifestWhenNotUsed,
middlewareV2ManifestWhenUsed,
pathsAsGlobs,
pathsWithEscapedChars,
pathsWithRegex,
Expand All @@ -48,6 +49,8 @@ import {
unsupportedRedirects,
unsupportedRewritesArray,
npmLsReturn,
middlewareV1ManifestWhenUsed,
middlewareV1ManifestWhenNotUsed,
pagesManifest,
prerenderManifest,
appPathsManifest,
Expand Down Expand Up @@ -266,12 +269,12 @@ describe("Next.js utils", () => {
});

it("should return true if using middleware in production", async () => {
sandbox.stub(fsExtra, "readJSON").resolves(middlewareManifestWhenUsed);
sandbox.stub(fsExtra, "readJSON").resolves(middlewareV2ManifestWhenUsed);
expect(await isUsingMiddleware("", false)).to.be.true;
});

it("should return false if not using middleware in production", async () => {
sandbox.stub(fsExtra, "readJSON").resolves(middlewareManifestWhenNotUsed);
sandbox.stub(fsExtra, "readJSON").resolves(middlewareV2ManifestWhenNotUsed);
expect(await isUsingMiddleware("", false)).to.be.false;
});
});
Expand Down Expand Up @@ -374,6 +377,36 @@ describe("Next.js utils", () => {
});
});

describe("getMiddlewareMatcherRegexes", () => {
it("should return regexes when using version 1", () => {
const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV1ManifestWhenUsed);

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);

expect(middlewareMatcherRegexes).to.eql([]);
});

it("should return regexes when using version 2", () => {
const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV2ManifestWhenUsed);

for (const regex of middlewareMatcherRegexes) {
expect(regex).to.be.an.instanceOf(RegExp);
}
});

it("should return empty array when using version 2 but not using middleware", () => {
const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV2ManifestWhenNotUsed);

expect(middlewareMatcherRegexes).to.eql([]);
});
});

describe("getNonStaticRoutes", () => {
it("should get non-static routes", () => {
expect(
Expand Down