Skip to content
Draft
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
302 changes: 162 additions & 140 deletions npm-shrinkwrap.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firebase-tools",
"version": "15.32.1",
"version": "15.32.0",
"description": "Command-Line Interface for Firebase",
"main": "./lib/index.js",
"mcpName": "io.github.firebase/firebase-mcp",
Expand Down Expand Up @@ -241,7 +241,7 @@
"googleapis": "^105.0.0",
"mocha": "^11.7.1",
"mock-fs": "5.2.0",
"next": "15.5.24",
"next": "^16.3.7",
"nock": "^13.0.5",
"node-mocks-http": "^1.11.0",
"nyc": "^15.1.0",
Expand Down
37 changes: 23 additions & 14 deletions src/frameworks/next/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,50 +49,59 @@ const WEBPACK_LAYERS_NAMES = {
/**
* The browser client bundle layer for actions.
*/ actionBrowser: "action-browser",
apiNode: "api-node",
apiEdge: "api-edge",
/**
* The layer for the API routes.
*/ api: "api",
/**
* The layer for the middleware code.
*/ middleware: "middleware",
instrument: "instrument",
/**
* The layer for assets on the edge.
*/ edgeAsset: "edge-asset",
/**
* The browser client bundle layer for App directory.
*/ appPagesBrowser: "app-pages-browser",
pagesDirBrowser: "pages-dir-browser",
pagesDirEdge: "pages-dir-edge",
pagesDirNode: "pages-dir-node",
/**
* The server bundle layer for metadata routes.
*/ appMetadataRoute: "app-metadata-route",
/**
* The layer for the server bundle for App Route handlers.
*/ appRouteHandler: "app-route-handler",
} as const;

// This is copied from Next.js source code to keep WEBPACK_LAYERS in sync with the Next.js definition.
export const WEBPACK_LAYERS: typeof NEXTJS_WEBPACK_LAYERS = {
...WEBPACK_LAYERS_NAMES,
pagesDirBrowser: "pages-dir-browser",
pagesDirEdge: "pages-dir-edge",
pagesDirNode: "pages-dir-node",
apiNode: "api-node",
apiEdge: "api-edge",
instrument: "instrument",
GROUP: {
builtinReact: [WEBPACK_LAYERS_NAMES.reactServerComponents, WEBPACK_LAYERS_NAMES.actionBrowser],
serverOnly: [
WEBPACK_LAYERS_NAMES.middleware,
WEBPACK_LAYERS_NAMES.reactServerComponents,
WEBPACK_LAYERS_NAMES.actionBrowser,
WEBPACK_LAYERS_NAMES.instrument,
WEBPACK_LAYERS_NAMES.middleware,
"instrument",
],
neutralTarget: [WEBPACK_LAYERS_NAMES.apiNode, WEBPACK_LAYERS_NAMES.apiEdge],
neutralTarget: ["api-node", "api-edge"],
clientOnly: [WEBPACK_LAYERS_NAMES.serverSideRendering, WEBPACK_LAYERS_NAMES.appPagesBrowser],
bundled: [
WEBPACK_LAYERS_NAMES.shared,
WEBPACK_LAYERS_NAMES.middleware,
WEBPACK_LAYERS_NAMES.reactServerComponents,
WEBPACK_LAYERS_NAMES.actionBrowser,
WEBPACK_LAYERS_NAMES.serverSideRendering,
WEBPACK_LAYERS_NAMES.actionBrowser,
"instrument",
WEBPACK_LAYERS_NAMES.appPagesBrowser,
WEBPACK_LAYERS_NAMES.shared,
WEBPACK_LAYERS_NAMES.instrument,
WEBPACK_LAYERS_NAMES.middleware,
],
appPages: [
WEBPACK_LAYERS_NAMES.reactServerComponents,
WEBPACK_LAYERS_NAMES.serverSideRendering,
WEBPACK_LAYERS_NAMES.appPagesBrowser,
WEBPACK_LAYERS_NAMES.actionBrowser,
WEBPACK_LAYERS_NAMES.appPagesBrowser,
],
},
};
33 changes: 28 additions & 5 deletions src/frameworks/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { IncomingMessage, ServerResponse } from "http";
import * as clc from "colorette";
import { fileExistsSync } from "../../fsutils";
import { loadStreamJson } from "../../streamJson";

import { select } from "../../prompt";
import { FirebaseError } from "../../error";
Expand Down Expand Up @@ -41,7 +42,7 @@
isRewriteSupportedByHosting,
isUsingImageOptimization,
isUsingMiddleware,
getProductionDependencyNames,
allDependencyNames,
getMiddlewareMatcherRegexes,
getNonStaticRoutes,
getNonStaticServerComponents,
Expand All @@ -58,12 +59,13 @@
getNextVersionRaw,
isNextJsVersionVulnerable,
} from "./utils";
import { NODE_VERSION, SHARP_VERSION, I18N_ROOT } from "../constants";
import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES, SHARP_VERSION, I18N_ROOT } from "../constants";
import type {
AppPathRoutesManifest,
AppPathsManifest,
HostingHeadersWithSource,
RoutesManifest,
NpmLsDepdendency,
MiddlewareManifest,
ActionManifest,
CustomBuildOptions,
Expand Down Expand Up @@ -97,13 +99,13 @@
const DEFAULT_NUMBER_OF_REASONS_TO_LIST = 5;

function getReactVersion(cwd: string): string | undefined {
return findDependency("react-dom", { cwd, omitDev: false })?.version;

Check warning on line 102 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .version on an `any` value

Check warning on line 102 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe return of an `any` typed value
}

/**
* Returns whether this codebase is a Next.js backend.
*/
export async function discover(dir: string) {

Check warning on line 108 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Missing return type on function
if (!(await pathExists(join(dir, "package.json")))) return;
const version = getNextVersion(dir);
if (!(await whichNextConfigFile(dir)) && !version) return;
Expand Down Expand Up @@ -160,10 +162,10 @@

const nextBuild = new Promise((resolve, reject) => {
const buildProcess = spawn(cli, ["build"], { cwd: dir, env });
buildProcess.stdout?.on("data", (data) => logger.info(data.toString()));

Check warning on line 165 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe call of an `any` typed value

Check warning on line 165 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .toString on an `any` value

Check warning on line 165 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `Error`
buildProcess.stderr?.on("data", (data) => logger.info(data.toString()));

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe call of an `any` typed value

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .toString on an `any` value

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `Error`
buildProcess.on("error", (err) => {
reject(new FirebaseError(`Unable to build your Next.js app: ${err}`));

Check warning on line 168 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Invalid type "Error" of template literal expression
});
buildProcess.on("exit", (code) => {
resolve(code);
Expand Down Expand Up @@ -513,11 +515,11 @@
{
srcRoute: null,
initialRevalidateSeconds: false,
initialExpireSeconds: undefined,
dataRoute: "",
experimentalPPR: false,
renderingMode: undefined,
prefetchDataRoute: "",
initialExpireSeconds: undefined,
renderingMode: undefined,
allowHeader: [],
},
];
Expand Down Expand Up @@ -658,7 +660,28 @@
throw new FirebaseError(`Failed to load esbuild from path: ${esbuildPath}`);
}

const productionDeps = await getProductionDependencyNames(sourceDir);
const { chain, parser, pick, streamObject } = await loadStreamJson();
const productionDeps = await new Promise<string[]>((resolve) => {
const dependencies: string[] = [];
const npmLs = spawn("npm", ["ls", "--omit=dev", "--all", "--json=true"], {
cwd: sourceDir,
timeout: NPM_COMMAND_TIMEOUT_MILLIES,
});
const pipeline = chain([
npmLs.stdout,
parser({ packValues: false, packKeys: true, streamValues: false }),
pick({ filter: "dependencies" }),
streamObject(),
({ key, value }: { key: string; value: NpmLsDepdendency }) => [
key,
...allDependencyNames(value),
],
]);
pipeline.on("data", (it: string) => dependencies.push(it));
pipeline.on("end", () => {
resolve([...new Set(dependencies)]);
});
});

// Mark all production deps as externals, so they aren't bundled
// DevDeps won't be included in the Cloud Function, so they should be bundled
Expand Down
14 changes: 8 additions & 6 deletions src/frameworks/next/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export type MiddlewareManifest = MiddlewareManifestV1 | MiddlewareManifestV2 | M
export type MiddlewareManifestV2 = {
version: 2;
sortedMiddleware: string[];
middleware: { [page: string]: Omit<EdgeFunctionDefinition, "env"> };
functions: { [page: string]: Omit<EdgeFunctionDefinition, "env"> };
middleware: { [page: string]: EdgeFunctionDefinition };
functions: { [page: string]: EdgeFunctionDefinition };
};

/**
Expand All @@ -109,16 +109,17 @@ export type MiddlewareManifestV3 = {
};

/**
* Type required for MiddlewareManifestV3
* Type required for MiddlewareManifestV2 and MiddlewareManifestV3
*
* @see https://github.com/vercel/next.js/blob/3352f9ee9342b40aaded91c340e7e11650aa4867/packages/next/src/build/webpack/plugins/middleware-plugin.ts#L44-L53
*/
interface EdgeFunctionDefinition {
export interface EdgeFunctionDefinition {
files: string[];
name: string;
page: string;
matchers: ProxyMatcherNext16[];
Comment thread
joehan marked this conversation as resolved.
env: Record<string, string>;
entrypoint?: string;
env?: Record<string, string>;
wasm?: AssetBinding[];
assets?: AssetBinding[];
regions?: string[] | string;
Expand Down Expand Up @@ -210,8 +211,9 @@ export interface MiddlewareManifestV1 {

export interface ImagesManifest {
version: number;
images: ImageConfigComplete & {
images: Partial<ImageConfigComplete> & {
sizes: number[];
unoptimized: boolean;
};
}

Expand Down
8 changes: 4 additions & 4 deletions src/frameworks/next/testing/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,22 @@ export const prerenderManifest: PrerenderManifest = {
routes: {
"/": {
initialRevalidateSeconds: false,
initialExpireSeconds: undefined,
srcRoute: "/",
dataRoute: "/index.rsc",
experimentalPPR: false,
renderingMode: undefined,
prefetchDataRoute: "",
initialExpireSeconds: undefined,
renderingMode: undefined,
allowHeader: [],
},
"/api/static": {
initialRevalidateSeconds: false,
initialExpireSeconds: undefined,
srcRoute: "/api/static",
dataRoute: "",
experimentalPPR: false,
renderingMode: undefined,
prefetchDataRoute: "",
initialExpireSeconds: undefined,
renderingMode: undefined,
allowHeader: [],
},
},
Expand Down
21 changes: 6 additions & 15 deletions src/frameworks/next/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@ import type {
RoutesManifestRedirect,
RoutesManifestHeader,
MiddlewareManifest,
MiddlewareManifestV1,
MiddlewareManifestV2,
AppPathsManifest,
HostingHeadersWithSource,
AppPathRoutesManifest,
ActionManifest,
NextConfigFileName,
FunctionsConfigManifest,
MiddlewareManifestV3,
} from "./interfaces";
import {
APP_PATH_ROUTES_MANIFEST,
Expand Down Expand Up @@ -387,21 +384,18 @@ export function getMiddlewareMatcherRegexes(
middlewareManifest: MiddlewareManifest,
functionsConfigManifest: FunctionsConfigManifest,
): RegExp[] {
const middlewareObjectValues = Object.values(middlewareManifest.middleware);
const middlewareMatchers: Record<"regexp", string>[] = [];

if (middlewareManifest.version === 1) {
const values = Object.values(middlewareManifest.middleware);
middlewareMatchers.push(
...middlewareObjectValues.map((page: MiddlewareManifestV1["middleware"][string]) => ({
...values.map((page) => ({
regexp: page.regexp,
})),
);
} else if (middlewareManifest.version === 2) {
middlewareMatchers.push(
...middlewareObjectValues
.map((page: MiddlewareManifestV2["middleware"][string]) => page.matchers)
.flat(),
);
const values = Object.values(middlewareManifest.middleware);
middlewareMatchers.push(...values.map((page) => page.matchers || []).flat());
} else if (middlewareManifest.version === 3) {
if (functionsConfigManifest?.functions?.["/_middleware"]) {
// matchers from proxy.js
Expand All @@ -410,11 +404,8 @@ export function getMiddlewareMatcherRegexes(
);
} else {
// matchers from middleware.js
middlewareMatchers.push(
...middlewareObjectValues
.map((page: MiddlewareManifestV3["middleware"][string]) => page.matchers)
.flat(),
);
const values = Object.values(middlewareManifest.middleware);
middlewareMatchers.push(...values.map((page) => page.matchers || []).flat());
}
}

Expand Down
Loading