Skip to content

fix: populate static API routes for our staticRouteMatcher #875

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/famous-kids-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/aws": patch
---

fix: populate static API routes for our staticRouteMatcher
59 changes: 59 additions & 0 deletions examples/app-pages-router/pages/[[...page]].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Home from "@/app/home";
import type {
GetStaticPathsResult,
GetStaticPropsContext,
InferGetStaticPropsType,
} from "next";

const validRootPages = ["conico974", "kheuzy", "sommeeer"];

export async function getStaticPaths(): Promise<GetStaticPathsResult> {
const rootPaths = validRootPages.map((page) => ({
params: { page: [page] },
}));

const paths = [{ params: { page: [] } }, ...rootPaths];

return {
paths,
fallback: false,
};
}

export async function getStaticProps(context: GetStaticPropsContext) {
const page = (context.params?.page as string[]) || [];

if (page.length === 0) {
return {
props: {
subpage: [],
pageType: "home",
},
};
}
if (page.length === 1 && validRootPages.includes(page[0])) {
return {
props: {
subpage: page,
pageType: "root",
},
};
}

return { notFound: true };
}

export default function Page({
subpage,
pageType,
}: InferGetStaticPropsType<typeof getStaticProps>) {
if (subpage.length === 0 && pageType === "home") {
return <Home />;
}
return (
<div>
<h1>{pageType === "home" ? "Homepage" : `Root page: ${subpage}`}</h1>
<p>Path: {subpage.join("/")}</p>
</div>
);
}
37 changes: 37 additions & 0 deletions packages/open-next/src/adapters/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from "node:path";

import type { RouteDefinition } from "types/next-types";
import { debug } from "../logger";
import {
loadAppPathRoutesManifest,
Expand All @@ -11,6 +12,7 @@ import {
loadFunctionsConfigManifest,
loadHtmlPages,
loadMiddlewareManifest,
loadPagesManifest,
loadPrerenderManifest,
loadRoutesManifest,
} from "./util.js";
Expand Down Expand Up @@ -39,3 +41,38 @@ export const AppPathRoutesManifest =

export const FunctionsConfigManifest =
/* @__PURE__ */ loadFunctionsConfigManifest(NEXT_DIR);

/**
* Returns static API routes for both app and pages router cause Next will filter them out in staticRoutes in `routes-manifest.json`.
* We also need to filter out page files that are under `app/api/*` as those would not be present in the routes manifest either.
* This line from Next.js skips it:
* https://github.com/vercel/next.js/blob/ded56f952154a40dcfe53bdb38c73174e9eca9e5/packages/next/src/build/index.ts#L1299
*
* Without it handleFallbackFalse will 404 on static API routes if there is a catch-all route on root level.
*/
export function getStaticAPIRoutes(): RouteDefinition[] {
const createRouteDefinition = (route: string) => ({
page: route,
regex: `^${route}(?:/)?$`,
});
const dynamicRoutePages = new Set(
RoutesManifest.routes.dynamic.map(({ page }) => page),
);
const PagesManifest = loadPagesManifest(NEXT_DIR);
const pagesStaticAPIRoutes = Object.keys(PagesManifest)
.filter(
(route) => route.startsWith("/api/") && !dynamicRoutePages.has(route),
)
.map(createRouteDefinition);

// We filter out both static API and page routes from the app paths manifest
const appPathsStaticAPIRoutes = Object.values(AppPathRoutesManifest)
.filter(
(route) =>
route.startsWith("/api/") ||
(route === "/api" && !dynamicRoutePages.has(route)),
)
.map(createRouteDefinition);

return [...pagesStaticAPIRoutes, ...appPathsStaticAPIRoutes];
}
2 changes: 1 addition & 1 deletion packages/open-next/src/adapters/config/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function loadBuildId(nextDir: string) {
export function loadPagesManifest(nextDir: string) {
const filePath = path.join(nextDir, "server/pages-manifest.json");
const json = fs.readFileSync(filePath, "utf-8");
return JSON.parse(json);
return JSON.parse(json) as Record<string, string>;
}

export function loadHtmlPages(nextDir: string) {
Expand Down
8 changes: 7 additions & 1 deletion packages/open-next/src/core/routing/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,12 @@ export function handleFallbackFalse(
? rawPath
: `/${NextConfig.i18n?.defaultLocale}${rawPath}`;
// We need to remove the trailing slash if it exists
if (NextConfig.trailingSlash && localizedPath.endsWith("/")) {
// Not if localizedPath is "/" tho, because that would not make it find `isPregenerated` below since it would be try to match an empty string.
if (
localizedPath !== "/" &&
NextConfig.trailingSlash &&
localizedPath.endsWith("/")
) {
localizedPath = localizedPath.slice(0, -1);
}
const matchedStaticRoute = staticRouteMatcher(localizedPath);
Expand All @@ -447,6 +452,7 @@ export function handleFallbackFalse(
const matchedDynamicRoute = dynamicRouteMatcher(localizedPath).filter(
({ route }) => !prerenderedFallbackRoutesName.includes(route),
);

const isPregenerated = Object.keys(routes).includes(localizedPath);
if (
routeFallback &&
Expand Down
11 changes: 9 additions & 2 deletions packages/open-next/src/core/routing/routeMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { AppPathRoutesManifest, RoutesManifest } from "config/index";
import {
AppPathRoutesManifest,
RoutesManifest,
getStaticAPIRoutes,
} from "config/index";
import type { RouteDefinition } from "types/next-types";
import type { ResolvedRoute, RouteType } from "types/open-next";

Expand Down Expand Up @@ -53,5 +57,8 @@ function routeMatcher(routeDefinitions: RouteDefinition[]) {
};
}

export const staticRouteMatcher = routeMatcher(RoutesManifest.routes.static);
export const staticRouteMatcher = routeMatcher([
...RoutesManifest.routes.static,
...getStaticAPIRoutes(),
]);
export const dynamicRouteMatcher = routeMatcher(RoutesManifest.routes.dynamic);
9 changes: 0 additions & 9 deletions packages/open-next/src/core/routingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
} from "./routing/matcher";
import { handleMiddleware } from "./routing/middleware";
import {
apiPrefix,
dynamicRouteMatcher,
staticRouteMatcher,
} from "./routing/routeMatcher";
Expand Down Expand Up @@ -158,13 +157,6 @@ export default async function routingHandler(
isExternalRewrite = fallbackRewrites.isExternalRewrite;
}

// Api routes are not present in the routes manifest except if they're not behind /api
// /api even if it's a page route doesn't get generated in the manifest
// Ideally we would need to properly check api routes here
const isApiRoute =
internalEvent.rawPath === apiPrefix ||
internalEvent.rawPath.startsWith(`${apiPrefix}/`);

const isNextImageRoute = internalEvent.rawPath.startsWith("/_next/image");

const isRouteFoundBeforeAllRewrites =
Expand All @@ -175,7 +167,6 @@ export default async function routingHandler(
if (
!(
isRouteFoundBeforeAllRewrites ||
isApiRoute ||
isNextImageRoute ||
// We need to check again once all rewrites have been applied
staticRouteMatcher(internalEvent.rawPath).length > 0 ||
Expand Down
12 changes: 12 additions & 0 deletions packages/tests-e2e/tests/appPagesRouter/catch-all.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { test } from "@playwright/test";

// Going to `/`, `/conico974`, `/kheuzy` and `/sommeeer` should be catched by our `[[...page]]` route.
test("Optional Catch all route in root should work", async ({ page }) => {
await page.goto("/");
await page.locator("h1").getByText("App Router").isVisible();
await page.locator("h1").getByText("Pages Router").isVisible();

await page.goto("/conico974");
const pElement = page.getByText("Path: conico974", { exact: true });
await pElement.isVisible();
});
12 changes: 6 additions & 6 deletions packages/tests-unit/tests/core/routing/matcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({
routeKeys: {},
namedRegex: "^/app(?:/)?$",
},
{
page: "/api/app",
regex: "^/api/app(?:/)?$",
routeKeys: {},
namedRegex: "^/api/app(?:/)?$",
},
{
page: "/page",
regex: "^/page(?:/)?$",
Expand All @@ -71,6 +65,12 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({
],
},
},
getStaticAPIRoutes: () => [
{
page: "/api/app",
regex: "^/api/app(?:/)?$",
},
],
}));
vi.mock("@opennextjs/aws/core/routing/i18n/index.js", () => ({
localizePath: (event: InternalEvent) => event.rawPath,
Expand Down
12 changes: 6 additions & 6 deletions packages/tests-unit/tests/core/routing/routeMatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,6 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({
routeKeys: {},
namedRegex: "^/app(?:/)?$",
},
{
page: "/api/app",
regex: "^/api/app(?:/)?$",
routeKeys: {},
namedRegex: "^/api/app(?:/)?$",
},
{
page: "/page",
regex: "^/page(?:/)?$",
Expand All @@ -66,6 +60,12 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({
],
},
},
getStaticAPIRoutes: () => [
{
page: "/api/app",
regex: "^/api/app(?:/)?$",
},
],
}));

describe("routeMatcher", () => {
Expand Down
Loading