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 all 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
@@ -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 @@
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,
ActionManifest,
} 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<ActionManifest>(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<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 @@
...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 @@
);
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"]);
});
});
});