Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3ec12c9

Browse files
authored
fix: populate static API routes for our staticRouteMatcher (#875)
* fix: populate static api routes for our staticRouteMatcher * add e2e * changeset * fix unit test * rm api route from RoutesManifest in mock * improve e2e * test file * refactor test title * lint * revert sst config * review * unit test * rm console log * rm key from config in unit test * rm comment * migrate to pages-router * migrate test * fix e2e * remove skip * fix value in api test * review
1 parent ff40f33 commit 3ec12c9

File tree

13 files changed

+227
-75
lines changed

13 files changed

+227
-75
lines changed

.changeset/famous-kids-hope.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
fix: populate static API routes for our staticRouteMatcher

examples/pages-router/src/pages/index.tsx renamed to examples/pages-router/src/components/home.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
import Nav from "@example/shared/components/Nav";
22
import Head from "next/head";
33

4-
// Not used, but necessary to get prefetching to work
5-
export function getStaticProps() {
6-
return {
7-
props: {
8-
hello: "world",
9-
},
10-
};
11-
}
12-
134
export default function Home() {
145
return (
156
<>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import Home from "@/components/home";
2+
import type {
3+
GetStaticPathsResult,
4+
GetStaticPropsContext,
5+
InferGetStaticPropsType,
6+
} from "next";
7+
8+
const validRootPages = ["conico974", "kheuzy", "sommeeer"];
9+
const validLongPaths = ["super/long/path/to/secret/page"];
10+
11+
export async function getStaticPaths(): Promise<GetStaticPathsResult> {
12+
const rootPaths = validRootPages.map((page) => ({
13+
params: { page: [page] },
14+
}));
15+
16+
const longPaths = validLongPaths.map((path) => ({
17+
params: { page: path.split("/") },
18+
}));
19+
20+
const paths = [{ params: { page: [] } }, ...rootPaths, ...longPaths];
21+
22+
return {
23+
paths,
24+
fallback: false,
25+
};
26+
}
27+
28+
export async function getStaticProps(context: GetStaticPropsContext) {
29+
const page = (context.params?.page as string[]) || [];
30+
31+
if (page.length === 0) {
32+
return {
33+
props: {
34+
subpage: [],
35+
pageType: "home",
36+
},
37+
};
38+
}
39+
if (page.length === 1 && validRootPages.includes(page[0])) {
40+
return {
41+
props: {
42+
subpage: page,
43+
pageType: "root",
44+
},
45+
};
46+
}
47+
48+
const pagePath = page.join("/");
49+
if (validLongPaths.includes(pagePath)) {
50+
return { props: { subpage: page, pageType: "long-path" } };
51+
}
52+
return { notFound: true };
53+
}
54+
55+
export default function Page({
56+
subpage,
57+
pageType,
58+
}: InferGetStaticPropsType<typeof getStaticProps>) {
59+
if (subpage.length === 0 && pageType === "home") {
60+
return <Home />;
61+
}
62+
return (
63+
<div>
64+
<h1 data-testid="page">{`Page: ${subpage}`}</h1>
65+
<p>Page type: {pageType}</p>
66+
<p>Path: {subpage.join("/")}</p>
67+
</div>
68+
);
69+
}

packages/open-next/src/adapters/config/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
loadFunctionsConfigManifest,
1212
loadHtmlPages,
1313
loadMiddlewareManifest,
14+
loadPagesManifest,
1415
loadPrerenderManifest,
1516
loadRoutesManifest,
1617
} from "./util.js";
@@ -20,7 +21,6 @@ export const OPEN_NEXT_DIR = path.join(__dirname, ".open-next");
2021

2122
debug({ NEXT_DIR, OPEN_NEXT_DIR });
2223

23-
//TODO: inject these values at build time
2424
export const NextConfig = /* @__PURE__ */ loadConfig(NEXT_DIR);
2525
export const BuildId = /* @__PURE__ */ loadBuildId(NEXT_DIR);
2626
export const HtmlPages = /* @__PURE__ */ loadHtmlPages(NEXT_DIR);
@@ -29,6 +29,7 @@ export const RoutesManifest = /* @__PURE__ */ loadRoutesManifest(NEXT_DIR);
2929
export const ConfigHeaders = /* @__PURE__ */ loadConfigHeaders(NEXT_DIR);
3030
export const PrerenderManifest =
3131
/* @__PURE__ */ loadPrerenderManifest(NEXT_DIR);
32+
export const PagesManifest = /* @__PURE__ */ loadPagesManifest(NEXT_DIR);
3233
export const AppPathsManifestKeys =
3334
/* @__PURE__ */ loadAppPathsManifestKeys(NEXT_DIR);
3435
export const MiddlewareManifest =

packages/open-next/src/adapters/config/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function loadBuildId(nextDir: string) {
2424
export function loadPagesManifest(nextDir: string) {
2525
const filePath = path.join(nextDir, "server/pages-manifest.json");
2626
const json = fs.readFileSync(filePath, "utf-8");
27-
return JSON.parse(json);
27+
return JSON.parse(json) as Record<string, string>;
2828
}
2929

3030
export function loadHtmlPages(nextDir: string) {

packages/open-next/src/core/routing/matcher.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,12 @@ export function handleFallbackFalse(
437437
? rawPath
438438
: `/${NextConfig.i18n?.defaultLocale}${rawPath}`;
439439
// We need to remove the trailing slash if it exists
440-
if (NextConfig.trailingSlash && localizedPath.endsWith("/")) {
440+
if (
441+
// Not if localizedPath is "/" tho, because that would not make it find `isPregenerated` below since it would be try to match an empty string.
442+
localizedPath !== "/" &&
443+
NextConfig.trailingSlash &&
444+
localizedPath.endsWith("/")
445+
) {
441446
localizedPath = localizedPath.slice(0, -1);
442447
}
443448
const matchedStaticRoute = staticRouteMatcher(localizedPath);
@@ -447,6 +452,7 @@ export function handleFallbackFalse(
447452
const matchedDynamicRoute = dynamicRouteMatcher(localizedPath).filter(
448453
({ route }) => !prerenderedFallbackRoutesName.includes(route),
449454
);
455+
450456
const isPregenerated = Object.keys(routes).includes(localizedPath);
451457
if (
452458
routeFallback &&

packages/open-next/src/core/routing/routeMatcher.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { AppPathRoutesManifest, RoutesManifest } from "config/index";
1+
import {
2+
AppPathRoutesManifest,
3+
PagesManifest,
4+
RoutesManifest,
5+
} from "config/index";
26
import type { RouteDefinition } from "types/next-types";
37
import type { ResolvedRoute, RouteType } from "types/open-next";
48

@@ -10,9 +14,6 @@ const optionalBasepathPrefixRegex = RoutesManifest.basePath
1014
? `^${RoutesManifest.basePath}/?`
1115
: "^/";
1216

13-
// Add the basePath prefix to the api routes
14-
export const apiPrefix = `${RoutesManifest.basePath ?? ""}/api`;
15-
1617
const optionalPrefix = optionalLocalePrefixRegex.replace(
1718
"^/",
1819
optionalBasepathPrefixRegex,
@@ -53,5 +54,42 @@ function routeMatcher(routeDefinitions: RouteDefinition[]) {
5354
};
5455
}
5556

56-
export const staticRouteMatcher = routeMatcher(RoutesManifest.routes.static);
57+
export const staticRouteMatcher = routeMatcher([
58+
...RoutesManifest.routes.static,
59+
...getStaticAPIRoutes(),
60+
]);
5761
export const dynamicRouteMatcher = routeMatcher(RoutesManifest.routes.dynamic);
62+
63+
/**
64+
* Returns static API routes for both app and pages router cause Next will filter them out in staticRoutes in `routes-manifest.json`.
65+
* We also need to filter out page files that are under `app/api/*` as those would not be present in the routes manifest either.
66+
* This line from Next.js skips it:
67+
* https://github.com/vercel/next.js/blob/ded56f952154a40dcfe53bdb38c73174e9eca9e5/packages/next/src/build/index.ts#L1299
68+
*
69+
* Without it handleFallbackFalse will 404 on static API routes if there is a catch-all route on root level.
70+
*/
71+
function getStaticAPIRoutes(): RouteDefinition[] {
72+
const createRouteDefinition = (route: string) => ({
73+
page: route,
74+
regex: `^${route}(?:/)?$`,
75+
});
76+
const dynamicRoutePages = new Set(
77+
RoutesManifest.routes.dynamic.map(({ page }) => page),
78+
);
79+
const pagesStaticAPIRoutes = Object.keys(PagesManifest)
80+
.filter(
81+
(route) => route.startsWith("/api/") && !dynamicRoutePages.has(route),
82+
)
83+
.map(createRouteDefinition);
84+
85+
// We filter out both static API and page routes from the app paths manifest
86+
const appPathsStaticAPIRoutes = Object.values(AppPathRoutesManifest)
87+
.filter(
88+
(route) =>
89+
route.startsWith("/api/") ||
90+
(route === "/api" && !dynamicRoutePages.has(route)),
91+
)
92+
.map(createRouteDefinition);
93+
94+
return [...pagesStaticAPIRoutes, ...appPathsStaticAPIRoutes];
95+
}

packages/open-next/src/core/routingHandler.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
} from "./routing/matcher";
2525
import { handleMiddleware } from "./routing/middleware";
2626
import {
27-
apiPrefix,
2827
dynamicRouteMatcher,
2928
staticRouteMatcher,
3029
} from "./routing/routeMatcher";
@@ -143,12 +142,17 @@ export default async function routingHandler(
143142
isExternalRewrite = afterRewrites.isExternalRewrite;
144143
}
145144

145+
let isISR = false;
146146
// We want to run this just before the dynamic route check
147-
const { event: fallbackEvent, isISR } = handleFallbackFalse(
148-
internalEvent,
149-
PrerenderManifest,
150-
);
151-
internalEvent = fallbackEvent;
147+
// We can skip it if its an external rewrite
148+
if (!isExternalRewrite) {
149+
const fallbackResult = handleFallbackFalse(
150+
internalEvent,
151+
PrerenderManifest,
152+
);
153+
internalEvent = fallbackResult.event;
154+
isISR = fallbackResult.isISR;
155+
}
152156

153157
const foundDynamicRoute = dynamicRouteMatcher(internalEvent.rawPath);
154158
const isDynamicRoute = !isExternalRewrite && foundDynamicRoute.length > 0;
@@ -163,13 +167,6 @@ export default async function routingHandler(
163167
isExternalRewrite = fallbackRewrites.isExternalRewrite;
164168
}
165169

166-
// Api routes are not present in the routes manifest except if they're not behind /api
167-
// /api even if it's a page route doesn't get generated in the manifest
168-
// Ideally we would need to properly check api routes here
169-
const isApiRoute =
170-
internalEvent.rawPath === apiPrefix ||
171-
internalEvent.rawPath.startsWith(`${apiPrefix}/`);
172-
173170
const isNextImageRoute = internalEvent.rawPath.startsWith("/_next/image");
174171

175172
const isRouteFoundBeforeAllRewrites =
@@ -180,7 +177,6 @@ export default async function routingHandler(
180177
if (
181178
!(
182179
isRouteFoundBeforeAllRewrites ||
183-
isApiRoute ||
184180
isNextImageRoute ||
185181
// We need to check again once all rewrites have been applied
186182
staticRouteMatcher(internalEvent.rawPath).length > 0 ||
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
// Going to `/`, `/conico974`, `/kheuzy` and `/sommeeer` should be catched by our `[[...page]]` route.
4+
// Also the /super/long/path/to/secret/page should be pregenerated by the `getStaticPaths` function.
5+
test.describe("Catch-all optional route in root should work", () => {
6+
test("should be possible to visit home and a pregenerated subpage", async ({
7+
page,
8+
}) => {
9+
await page.goto("/");
10+
await page.locator("h1").getByText("Pages Router").isVisible();
11+
12+
await page.goto("/conico974");
13+
const pElement = page.getByText("Path: conico974", { exact: true });
14+
await pElement.isVisible();
15+
});
16+
test("should be possible to visit a long pregenerated path", async ({
17+
page,
18+
}) => {
19+
await page.goto("/super/long/path/to/secret/page");
20+
const h1Text = await page.getByTestId("page").textContent();
21+
expect(h1Text).toBe("Page: super,long,path,to,secret,page");
22+
});
23+
test("should be possible to request an API route when you have a catch-all in root", async ({
24+
request,
25+
}) => {
26+
const response = await request.get("/api/hello");
27+
expect(response.status()).toBe(200);
28+
const body = await response.json();
29+
expect(body).toEqual({ hello: "OpenNext rocks!" });
30+
});
31+
});

packages/tests-e2e/tests/pagesRouter/data.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ test("fix _next/data", async ({ page }) => {
1717
expect(response2.request().url()).toMatch(/\/_next\/data\/.*\/en\.json$/);
1818
await page.waitForURL("/");
1919
const body = await response2.json();
20-
expect(body).toEqual({ pageProps: { hello: "world" }, __N_SSG: true });
20+
expect(body).toEqual({
21+
pageProps: { subpage: [], pageType: "home" },
22+
__N_SSG: true,
23+
});
2124
});

packages/tests-e2e/tests/pagesRouter/rewrite.test.ts

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,37 @@ import { validateMd5 } from "../utils";
33

44
const EXT_PNG_MD5 = "405f45cc3397b09717a13ebd6f1e027b";
55

6-
test("Single Rewrite", async ({ page }) => {
7-
await page.goto("/rewrite");
8-
9-
const el = page.getByText("Nextjs Pages Router");
10-
await expect(el).toBeVisible();
11-
});
12-
13-
test("Rewrite with query", async ({ page }) => {
14-
await page.goto("/rewriteUsingQuery?d=ssr");
15-
16-
const el = page.getByText("SSR");
17-
await expect(el).toBeVisible();
18-
});
19-
20-
test("Rewrite to external image", async ({ request }) => {
21-
const response = await request.get("/external-on-image");
22-
expect(response.status()).toBe(200);
23-
expect(response.headers()["content-type"]).toBe("image/png");
24-
expect(validateMd5(await response.body(), EXT_PNG_MD5)).toBe(true);
25-
});
26-
27-
test("Rewrite with query in destination", async ({ request }) => {
28-
const response = await request.get("/rewriteWithQuery");
29-
expect(response.status()).toBe(200);
30-
expect(await response.json()).toEqual({ query: { q: "1" } });
31-
});
32-
33-
test("Rewrite with query should merge query params", async ({ request }) => {
34-
const response = await request.get("/rewriteWithQuery?b=2");
35-
expect(response.status()).toBe(200);
36-
expect(await response.json()).toEqual({ query: { q: "1", b: "2" } });
6+
test.describe("Rewrite", () => {
7+
test("Single Rewrite", async ({ page }) => {
8+
await page.goto("/rewrite");
9+
10+
const el = page.getByText("Nextjs Pages Router");
11+
await expect(el).toBeVisible();
12+
});
13+
14+
test("Rewrite with query", async ({ page }) => {
15+
await page.goto("/rewriteUsingQuery?d=ssr");
16+
17+
const el = page.getByText("SSR");
18+
await expect(el).toBeVisible();
19+
});
20+
21+
test("Rewrite to external image", async ({ request }) => {
22+
const response = await request.get("/external-on-image");
23+
expect(response.status()).toBe(200);
24+
expect(response.headers()["content-type"]).toBe("image/png");
25+
expect(validateMd5(await response.body(), EXT_PNG_MD5)).toBe(true);
26+
});
27+
28+
test("Rewrite with query in destination", async ({ request }) => {
29+
const response = await request.get("/rewriteWithQuery");
30+
expect(response.status()).toBe(200);
31+
expect(await response.json()).toEqual({ query: { q: "1" } });
32+
});
33+
34+
test("Rewrite with query should merge query params", async ({ request }) => {
35+
const response = await request.get("/rewriteWithQuery?b=2");
36+
expect(response.status()).toBe(200);
37+
expect(await response.json()).toEqual({ query: { q: "1", b: "2" } });
38+
});
3739
});

0 commit comments

Comments
 (0)