Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix Next.js static routes with server actions #6664

Merged
merged 19 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fix Next.js static routes with server actions (#6664)
50 changes: 50 additions & 0 deletions src/frameworks/next/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,55 @@ 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";
export const APP_PATHS_MANIFEST: typeof APP_PATHS_MANIFEST_TYPE = "app-paths-manifest.json";
// TODO: import from next/constants after bumping Next.js dependency
export const SERVER_REFERENCE_MANIFEST = "server-reference-manifest.json";

export const ESBUILD_VERSION = "0.19.2";

// TODO: import from next/constants after bumping Next.js dependency
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamesdaniels this depends on #6816

/**
* The names of the webpack layers. These layers are the primitives for the
* webpack chunks.
*/
export const WEBPACK_LAYERS_NAMES = {
/**
* The layer for the shared code between the client and server bundles.
*/
shared: "shared",
/**
* React Server Components layer (rsc).
*/
reactServerComponents: "rsc",
/**
* Server Side Rendering layer for app (ssr).
*/
serverSideRendering: "ssr",
/**
* The browser client bundle layer for actions.
*/
actionBrowser: "action-browser",
/**
* The layer for the API routes.
*/
api: "api",
/**
* The layer for the middleware code.
*/
middleware: "middleware",
/**
* The layer for assets on the edge.
*/
edgeAsset: "edge-asset",
/**
* The browser client bundle layer for App directory.
*/
appPagesBrowser: "app-pages-browser",
/**
* 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;
34 changes: 33 additions & 1 deletion src/frameworks/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
cleanI18n,
getNextVersion,
hasStaticAppNotFoundComponent,
getRoutesWithServerAction,
} from "./utils";
import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES, SHARP_VERSION, I18N_ROOT } from "../constants";
import type {
Expand All @@ -63,6 +64,7 @@
RoutesManifest,
NpmLsDepdendency,
MiddlewareManifest,
ServerReferenceManifest,
} from "./interfaces";
import {
MIDDLEWARE_MANIFEST,
Expand All @@ -72,6 +74,7 @@
APP_PATH_ROUTES_MANIFEST,
APP_PATHS_MANIFEST,
ESBUILD_VERSION,
SERVER_REFERENCE_MANIFEST,
} from "./constants";
import { getAllSiteDomains, getDeploymentDomain } from "../../hosting/api";
import { logger } from "../../logger";
Expand All @@ -90,13 +93,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 96 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe return of an `any` typed value

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .version on an `any` value
}

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

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

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
if (!(await pathExists(join(dir, "package.json")))) return;
const version = getNextVersion(dir);
if (!(await pathExists("next.config.js")) && !version) return;
Expand Down Expand Up @@ -141,10 +144,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 147 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Error`

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value
buildProcess.stderr?.on("data", (data) => logger.info(data.toString()));

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Error`

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value
buildProcess.on("error", (err) => {
reject(new FirebaseError(`Unable to build your Next.js app: ${err}`));

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

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "Error" of template literal expression
});
buildProcess.on("exit", (code) => {
resolve(code);
Expand Down Expand Up @@ -220,13 +223,16 @@
headers,
}));

const [appPathsManifest, appPathRoutesManifest] = await Promise.all([
const [appPathsManifest, appPathRoutesManifest, serverReferenceManifest] = await Promise.all([
readJSON<AppPathsManifest>(join(dir, distDir, "server", APP_PATHS_MANIFEST)).catch(
() => undefined,
),
readJSON<AppPathRoutesManifest>(join(dir, distDir, APP_PATH_ROUTES_MANIFEST)).catch(
() => undefined,
),
readJSON<ServerReferenceManifest>(
join(dir, distDir, "server", SERVER_REFERENCE_MANIFEST),
).catch(() => undefined),
]);

if (appPathRoutesManifest) {
Expand Down Expand Up @@ -257,6 +263,17 @@
reasonsForBackend.add(`non-static component ${key}`);
}
}

if (serverReferenceManifest) {
const routesWithServerAction = getRoutesWithServerAction(
serverReferenceManifest,
appPathRoutesManifest,
);

for (const key of routesWithServerAction) {
reasonsForBackend.add(`route with server action ${key}`);
}
}
}

const isEveryRedirectSupported = nextJsRedirects
Expand Down Expand Up @@ -385,6 +402,7 @@
routesManifest,
pagesManifest,
appPathRoutesManifest,
serverReferenceManifest,
] = await Promise.all([
readJSON<MiddlewareManifest>(join(sourceDir, distDir, "server", MIDDLEWARE_MANIFEST)),
readJSON<PrerenderManifest>(join(sourceDir, distDir, PRERENDER_MANIFEST)),
Expand All @@ -393,6 +411,9 @@
readJSON<AppPathRoutesManifest>(join(sourceDir, distDir, APP_PATH_ROUTES_MANIFEST)).catch(
() => ({}),
),
readJSON<ServerReferenceManifest>(
join(sourceDir, distDir, "server", SERVER_REFERENCE_MANIFEST),
).catch(() => ({ node: {}, edge: {}, encryptionKey: "" })),
]);

const appPathRoutesEntries = Object.entries(appPathRoutesManifest);
Expand Down Expand Up @@ -423,6 +444,11 @@
...headersRegexesNotSupportedByHosting,
];

const staticRoutesUsingServerActions = getRoutesWithServerAction(
serverReferenceManifest,
appPathRoutesManifest,
);

const pagesManifestLikePrerender: PrerenderManifest["routes"] = Object.fromEntries(
Object.entries(pagesManifest)
.filter(([, srcRoute]) => srcRoute.endsWith(".html"))
Expand All @@ -448,6 +474,12 @@
);
return;
}

if (staticRoutesUsingServerActions.some((it) => path === it)) {
logger.debug(`skipping ${path} due to server action`);
return;
}

const appPathRoute =
route.srcRoute && appPathRoutesEntries.find(([, it]) => it === route.srcRoute)?.[0];
const contentDist = join(sourceDir, distDir, "server", appPathRoute ? "app" : "pages");
Expand Down
16 changes: 16 additions & 0 deletions src/frameworks/next/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { RouteHas } 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";
import { WEBPACK_LAYERS_NAMES } from "./constants";

export interface RoutesManifestRewriteObject {
beforeFiles?: RoutesManifestRewrite[];
Expand Down Expand Up @@ -138,3 +139,18 @@ export interface HostingHeadersWithSource {
source: string;
headers: HostingHeaders["headers"];
}

interface Actions {
[actionId: string]: {
workers: {
[name: string]: string | number;
};
layer: Record<string, (typeof WEBPACK_LAYERS_NAMES)[keyof typeof WEBPACK_LAYERS_NAMES]>;
};
}

export interface ServerReferenceManifest {
node: Actions;
edge: Actions;
encryptionKey: string;
}
30 changes: 30 additions & 0 deletions src/frameworks/next/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import type {
AppPathsManifest,
AppPathRoutesManifest,
HostingHeadersWithSource,
ServerReferenceManifest,
} from "./interfaces";
import {
APP_PATH_ROUTES_MANIFEST,
EXPORT_MARKER,
IMAGES_MANIFEST,
MIDDLEWARE_MANIFEST,
WEBPACK_LAYERS_NAMES,
} from "./constants";
import { dirExistsSync, fileExistsSync } from "../../fsutils";

Expand Down Expand Up @@ -421,3 +423,31 @@ export async function hasStaticAppNotFoundComponent(
): Promise<boolean> {
return pathExists(join(sourceDir, distDir, "server", "app", "_not-found.html"));
}

/**
* Find routes using server actions by checking the server-reference-manifest.json
*/
export function getRoutesWithServerAction(
serverReferenceManifest: ServerReferenceManifest,
appPathRoutesManifest: AppPathRoutesManifest,
): string[] {
const routesWithServerAction = new Set<string>();

for (const key of Object.keys(serverReferenceManifest)) {
if (key !== "edge" && key !== "node") continue;

const edgeOrNode = serverReferenceManifest[key];

for (const actionId of Object.keys(edgeOrNode)) {
if (!edgeOrNode[actionId].layer) continue;

for (const [route, type] of Object.entries(edgeOrNode[actionId].layer)) {
if (type === WEBPACK_LAYERS_NAMES.actionBrowser) {
routesWithServerAction.add(appPathRoutesManifest[route.replace("app", "")]);
}
}
}
}

return Array.from(routesWithServerAction);
}
25 changes: 25 additions & 0 deletions src/test/frameworks/next/helpers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manife
import type {
AppPathRoutesManifest,
AppPathsManifest,
ServerReferenceManifest,
} from "../../../../frameworks/next/interfaces";

export const appPathsManifest: AppPathsManifest = {
Expand All @@ -15,6 +16,10 @@ export const appPathRoutesManifest: AppPathRoutesManifest = {
"/api/test/route": "/api/test",
"/api/static/route": "/api/static",
"/page": "/",
"/another-s-a/page": "/another-s-a",
"/server-action/page": "/server-action",
"/ssr/page": "/ssr",
"/server-action/edge/page": "/server-action/edge",
};

export const pagesManifest: PagesManifest = {
Expand Down Expand Up @@ -61,3 +66,23 @@ globalThis.__RSC_MANIFEST["/page"] =
export const clientReferenceManifestWithImage = `{"ssrModuleMapping":{"2306":{"*":{"id":"7833","name":"*","chunks":[],"async":false}},"2353":{"*":{"id":"8709","name":"*","chunks":[],"async":false}},"3029":{"*":{"id":"9556","name":"*","chunks":[],"async":false}},"7330":{"*":{"id":"7734","name":"*","chunks":[],"async":false}},"8531":{"*":{"id":"9150","name":"*","chunks":[],"async":false}},"9180":{"*":{"id":"2698","name":"*","chunks":[],"async":false}}},"edgeSSRModuleMapping":{},"clientModules":{"/app-path/node_modules/next/dist/client/components/app-router.js":{"id":2353,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/app-router.js":{"id":2353,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/layout-router.js":{"id":9180,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/layout-router.js":{"id":9180,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/render-from-template-context.js":{"id":2306,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/render-from-template-context.js":{"id":2306,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/static-generation-searchparams-bailout-provider.js":{"id":8531,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/static-generation-searchparams-bailout-provider.js":{"id":8531,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/error-boundary.js":{"id":7330,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/error-boundary.js":{"id":7330,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/image-component.js":{"id":3029,"name":"*","chunks":["931:static/chunks/app/page-8d47763b987bba19.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/image-component.js":{"id":3029,"name":"*","chunks":["931:static/chunks/app/page-8d47763b987bba19.js"],"async":false},"/app-path/node_modules/next/font/google/target.css?{\"path\":\"src/app/layout.tsx\",\"import\":\"Inter\",\"arguments\":[{\"subsets\":[\"latin\"]}],\"variableName\":\"inter\"}":{"id":670,"name":"*","chunks":["185:static/chunks/app/layout-09ef1f5c8b0e56d1.js"],"async":false},"/app-path/src/app/globals.css":{"id":8410,"name":"*","chunks":["185:static/chunks/app/layout-09ef1f5c8b0e56d1.js"],"async":false}},"entryCSSFiles":{"/app-path/src/app/page":[],"/app-path/src/app/layout":["static/css/110a35ea7c81b899.css"]}}`;

export const clientReferenceManifestWithoutImage = `{"ssrModuleMapping":{"2306":{"*":{"id":"7833","name":"*","chunks":[],"async":false}},"2353":{"*":{"id":"8709","name":"*","chunks":[],"async":false}},"3029":{"*":{"id":"9556","name":"*","chunks":[],"async":false}},"7330":{"*":{"id":"7734","name":"*","chunks":[],"async":false}},"8531":{"*":{"id":"9150","name":"*","chunks":[],"async":false}},"9180":{"*":{"id":"2698","name":"*","chunks":[],"async":false}}},"edgeSSRModuleMapping":{},"clientModules":{"/app-path/node_modules/next/dist/client/components/app-router.js":{"id":2353,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/app-router.js":{"id":2353,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/layout-router.js":{"id":9180,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/layout-router.js":{"id":9180,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/render-from-template-context.js":{"id":2306,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/render-from-template-context.js":{"id":2306,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/static-generation-searchparams-bailout-provider.js":{"id":8531,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/static-generation-searchparams-bailout-provider.js":{"id":8531,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/error-boundary.js":{"id":7330,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/error-boundary.js":{"id":7330,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/font/google/target.css?{\"path\":\"src/app/layout.tsx\",\"import\":\"Inter\",\"arguments\":[{\"subsets\":[\"latin\"]}],\"variableName\":\"inter\"}":{"id":670,"name":"*","chunks":["185:static/chunks/app/layout-09ef1f5c8b0e56d1.js"],"async":false},"/app-path/src/app/globals.css":{"id":8410,"name":"*","chunks":["185:static/chunks/app/layout-09ef1f5c8b0e56d1.js"],"async":false}},"entryCSSFiles":{"/app-path/src/app/page":[],"/app-path/src/app/layout":["static/css/110a35ea7c81b899.css"]}}`;

export const serverReferenceManifest: ServerReferenceManifest = {
node: {
"123": {
workers: { "app/another-s-a/page": 123, "app/server-action/page": 123 },
layer: {
"app/another-s-a/page": "action-browser",
"app/server-action/page": "action-browser",
"app/ssr/page": "rsc",
},
},
},
edge: {
"123": {
workers: { "app/server-action/edge/page": 123 },
layer: { "app/server-action/edge/page": "action-browser" },
},
},
encryptionKey: "456",
};
10 changes: 10 additions & 0 deletions src/test/frameworks/next/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
getHeadersFromMetaFiles,
isUsingNextImageInAppDirectory,
getNextVersion,
getRoutesWithServerAction,
} from "../../../frameworks/next/utils";

import * as frameworksUtils from "../../../frameworks/utils";
Expand Down Expand Up @@ -63,6 +64,7 @@ import {
pageClientReferenceManifestWithoutImage,
clientReferenceManifestWithImage,
clientReferenceManifestWithoutImage,
serverReferenceManifest,
} from "./helpers";
import { pathsWithCustomRoutesInternalPrefix } from "./helpers/i18n";

Expand Down Expand Up @@ -523,4 +525,12 @@ describe("Next.js utils", () => {
expect(getNextVersion("")).to.be.undefined;
});
});

describe("getRoutesWithServerAction", () => {
it("should get routes with server action", () => {
expect(
getRoutesWithServerAction(serverReferenceManifest, appPathRoutesManifest),
).to.deep.equal(["/another-s-a", "/server-action", "/server-action/edge"]);
});
});
});