diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae0f507107..065a1de7870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ - Added the ability to deploy Angular apps using [the new application-builder](https://angular.dev/tools/cli/esbuild). (#6480) - Fixed an issue where `--non-interactive` flag is not respected in Firestore indexes deploys. (#6539) - Fixed an issue where `login:use` would not work outside of a Firebase project directory. (#6526) +- Prevent app router static `not-found` requiring a Cloud Function in Next.js deployments. (#6558) diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index 852e9329755..ccdcd36eced 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -47,6 +47,7 @@ import { getHeadersFromMetaFiles, cleanI18n, getNextVersion, + hasStaticAppNotFoundComponent, } from "./utils"; import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES, SHARP_VERSION, I18N_ROOT } from "../constants"; import type { @@ -215,6 +216,13 @@ export async function build(dir: string): Promise { dynamicRoutes ); + if ( + unrenderedServerComponents.has("/_not-found") && + (await hasStaticAppNotFoundComponent(dir, distDir)) + ) { + unrenderedServerComponents.delete("/_not-found"); + } + for (const key of unrenderedServerComponents) { reasonsForBackend.add(`non-static component ${key}`); } diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts index d625c105a84..94b23b40a25 100644 --- a/src/frameworks/next/utils.ts +++ b/src/frameworks/next/utils.ts @@ -338,7 +338,7 @@ export function getNonStaticServerComponents( appPathRoutesManifest: AppPathRoutesManifest, prerenderedRoutes: string[], dynamicRoutes: string[] -): string[] { +): Set { const nonStaticServerComponents = Object.entries(appPathsManifest) .filter(([it, src]) => { if (extname(src) !== ".js") return; @@ -347,7 +347,7 @@ export function getNonStaticServerComponents( }) .map(([it]) => it); - return nonStaticServerComponents; + return new Set(nonStaticServerComponents); } /** @@ -407,3 +407,17 @@ export function getNextVersion(cwd: string): string | undefined { return nextVersionSemver.toString(); } + +/** + * Whether the Next.js project has a static `not-found` page in the app directory. + * + * The Next.js build manifests are misleading regarding the existence of a static + * `not-found` component. Therefore, we check if a `_not-found.html` file exists + * in the generated app directory files to know whether `not-found` is static. + */ +export async function hasStaticAppNotFoundComponent( + sourceDir: string, + distDir: string +): Promise { + return pathExists(join(sourceDir, distDir, "server", "app", "_not-found.html")); +} diff --git a/src/test/frameworks/next/utils.spec.ts b/src/test/frameworks/next/utils.spec.ts index 0e5fa2163e2..6097523bb30 100644 --- a/src/test/frameworks/next/utils.spec.ts +++ b/src/test/frameworks/next/utils.spec.ts @@ -461,7 +461,7 @@ describe("Next.js utils", () => { Object.keys(prerenderManifest.routes), Object.keys(prerenderManifest.dynamicRoutes) ) - ).to.deep.equal(["/api/test/route"]); + ).to.deep.equal(new Set(["/api/test/route"])); }); });