Skip to content

Commit

Permalink
fix Next.js static routes with server actions (#6664)
Browse files Browse the repository at this point in the history
  • Loading branch information
leoortizz committed Mar 4, 2024
1 parent 06220c3 commit 730aeae
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Fix demo projects + web frameworks with emulators (#6737)
- Fix Next.js static routes with server actions (#6664)
65 changes: 65 additions & 0 deletions src/frameworks/next/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import type {
PRERENDER_MANIFEST as PRERENDER_MANIFEST_TYPE,
ROUTES_MANIFEST as ROUTES_MANIFEST_TYPE,
APP_PATHS_MANIFEST as APP_PATHS_MANIFEST_TYPE,
SERVER_REFERENCE_MANIFEST as SERVER_REFERENCE_MANIFEST_TYPE,
} from "next/constants";
import type { WEBPACK_LAYERS as NEXTJS_WEBPACK_LAYERS } from "next/dist/lib/constants";

export const APP_PATH_ROUTES_MANIFEST: typeof APP_PATH_ROUTES_MANIFEST_TYPE =
"app-path-routes-manifest.json";
Expand All @@ -18,5 +20,68 @@ 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";
export const SERVER_REFERENCE_MANIFEST: `${typeof SERVER_REFERENCE_MANIFEST_TYPE}.json` =
"server-reference-manifest.json";

export const ESBUILD_VERSION = "0.19.2";

// This is copied from Next.js source code to keep WEBPACK_LAYERS in sync with the Next.js definition.
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;

// 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,
GROUP: {
server: [
WEBPACK_LAYERS_NAMES.reactServerComponents,
WEBPACK_LAYERS_NAMES.actionBrowser,
WEBPACK_LAYERS_NAMES.appMetadataRoute,
WEBPACK_LAYERS_NAMES.appRouteHandler,
],
nonClientServerTarget: [
// plus middleware and pages api
WEBPACK_LAYERS_NAMES.middleware,
WEBPACK_LAYERS_NAMES.api,
],
app: [
WEBPACK_LAYERS_NAMES.reactServerComponents,
WEBPACK_LAYERS_NAMES.actionBrowser,
WEBPACK_LAYERS_NAMES.appMetadataRoute,
WEBPACK_LAYERS_NAMES.appRouteHandler,
WEBPACK_LAYERS_NAMES.serverSideRendering,
WEBPACK_LAYERS_NAMES.appPagesBrowser,
WEBPACK_LAYERS_NAMES.shared,
],
},
};
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 @@ import {
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 @@ import type {
RoutesManifest,
NpmLsDepdendency,
MiddlewareManifest,
ActionManifest,
} from "./interfaces";
import {
MIDDLEWARE_MANIFEST,
Expand All @@ -72,6 +74,7 @@ import {
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 Down Expand Up @@ -220,13 +223,16 @@ export async function build(
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<ActionManifest>(join(dir, distDir, "server", SERVER_REFERENCE_MANIFEST)).catch(
() => undefined,
),
]);

if (appPathRoutesManifest) {
Expand Down Expand Up @@ -257,6 +263,17 @@ export async function build(
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 @@ export async function ɵcodegenPublicDirectory(
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 @@ export async function ɵcodegenPublicDirectory(
readJSON<AppPathRoutesManifest>(join(sourceDir, distDir, APP_PATH_ROUTES_MANIFEST)).catch(
() => ({}),
),
readJSON<ActionManifest>(join(sourceDir, distDir, "server", SERVER_REFERENCE_MANIFEST)).catch(
() => ({ node: {}, edge: {}, encryptionKey: "" }),
),
]);

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

const staticRoutesUsingServerActions = getRoutesWithServerAction(
serverReferenceManifest,
appPathRoutesManifest,
);

const pagesManifestLikePrerender: PrerenderManifest["routes"] = Object.fromEntries(
Object.entries(pagesManifest)
.filter(([, srcRoute]) => srcRoute.endsWith(".html"))
Expand Down Expand Up @@ -457,6 +483,12 @@ export async function ɵcodegenPublicDirectory(
);
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
26 changes: 22 additions & 4 deletions src/frameworks/next/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,29 @@ export interface AppPathsManifest {
[key: string]: string;
}

export interface AppPathRoutesManifest {
[key: string]: string;
}

export interface HostingHeadersWithSource {
source: string;
headers: HostingHeaders["headers"];
}

export type AppPathRoutesManifest = Record<string, string>;

/**
* Note: This is a copy of the type from `next/dist/build/webpack/plugins/flight-client-entry-plugin`.
* It's copied here due to type errors caused by internal dependencies of Next.js when importing that file.
*/
export type ActionManifest = {
encryptionKey: string;
node: Actions;
edge: Actions;
};
type Actions = {
[actionId: string]: {
workers: {
[name: string]: string | number;
};
layer: {
[name: string]: string;
};
};
};
32 changes: 31 additions & 1 deletion src/frameworks/next/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ import type {
MiddlewareManifestV1,
MiddlewareManifestV2,
AppPathsManifest,
AppPathRoutesManifest,
HostingHeadersWithSource,
AppPathRoutesManifest,
ActionManifest,
} from "./interfaces";
import {
APP_PATH_ROUTES_MANIFEST,
EXPORT_MARKER,
IMAGES_MANIFEST,
MIDDLEWARE_MANIFEST,
WEBPACK_LAYERS,
} 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: ActionManifest,
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.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
@@ -1,6 +1,7 @@
import { PrerenderManifest } from "next/dist/build";
import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin";
import type {
ActionManifest,
AppPathRoutesManifest,
AppPathsManifest,
} from "../../../../frameworks/next/interfaces";
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 @@ -69,3 +74,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: ActionManifest = {
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"]);
});
});
});

0 comments on commit 730aeae

Please sign in to comment.