mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
b4898289e7
* fix(app-router): match Next.js RSC Content-Type (no charset suffix)
Next.js sets the RSC (React Server Components / 'flight') response
Content-Type to exactly 'text/x-component' (RSC_CONTENT_TYPE_HEADER in
.nextjs-ref/packages/next/src/client/components/app-router-headers.ts:17,
used by FlightRenderResult in
.nextjs-ref/packages/next/src/server/app-render/flight-render-result.ts:15).
vinext was sending 'text/x-component; charset=utf-8'. The suffix breaks
Next.js's own segment-cache deduplication checks (action-handler.ts uses
.startsWith(RSC_CONTENT_TYPE_HEADER) on the unsuffixed string) and fails
Next.js e2e tests that assert strict equality, e.g.:
- test/e2e/app-dir/app/index.test.ts L362, L371
- test/e2e/app-dir/segment-cache/deployment-skew/deployment-skew.test.ts L80
Replace the duplicated literal at seven sites with the existing
VINEXT_RSC_CONTENT_TYPE constant in app-rsc-cache-busting.ts.
* fix(app-router): match Next.js 404 plain-text body 'This page could not be found'
Next.js's plain-text 404 response body is exactly
'This page could not be found' (no trailing period in the response body;
the React-rendered not-found component uses the same text with a period).
vinext was returning 'Not Found' / '404 Not Found' / '404 - Not found',
which fails Next.js e2e tests that assert .toContain('This page could
not be found'), e.g.:
- test/e2e/app-dir/app/index.test.ts (several cases)
- test/e2e/app-dir/parallel-route-not-found/parallel-route-not-found.test.ts L52
- test/e2e/app-dir/prefetching-not-found/prefetching-not-found.test.ts L16
Next.js source citations (in .nextjs-ref):
- packages/next/src/server/route-modules/pages/pages-handler.ts L121, L535
- packages/next/src/build/templates/app-route.ts L170, L349
- packages/next/src/build/templates/app-page.ts L701, L1043
- packages/next/src/pages/_error.tsx L7
- packages/next/src/client/components/builtin/not-found.tsx L7 (HTML variant)
Updates the body in notFoundResponse() and the open-redirect / pages-router
SSR-fallback / worker-entry inline 404 sites. The HTTP status reason phrase
('Not Found') is unchanged. Route-specific bodies like '404 - API route not
found' are intentionally retained.
208 lines
7.2 KiB
TypeScript
208 lines
7.2 KiB
TypeScript
import { describe, expect, it, vi } from "vite-plus/test";
|
|
import { handleAppPrerenderEndpoint } from "../packages/vinext/src/server/app-prerender-endpoints.js";
|
|
import { getRootParam } from "../packages/vinext/src/shims/root-params.js";
|
|
|
|
type TestPageRoute = {
|
|
pattern: string;
|
|
module?: {
|
|
getStaticPaths?: (opts: { locales: string[]; defaultLocale: string }) => unknown;
|
|
};
|
|
};
|
|
|
|
describe("App prerender endpoint helpers", () => {
|
|
it("falls through for non-prerender requests", async () => {
|
|
const response = await handleAppPrerenderEndpoint(new Request("http://localhost/blog/post"), {
|
|
isPrerenderEnabled: () => true,
|
|
pathname: "/blog/post",
|
|
staticParamsMap: {},
|
|
});
|
|
|
|
expect(response).toBeNull();
|
|
});
|
|
|
|
it("returns 404 for prerender endpoints outside prerender mode", async () => {
|
|
const response = await handleAppPrerenderEndpoint(
|
|
new Request("http://localhost/__vinext/prerender/static-params?pattern=/blog/:slug"),
|
|
{
|
|
isPrerenderEnabled: () => false,
|
|
pathname: "/__vinext/prerender/static-params",
|
|
staticParamsMap: {
|
|
"/blog/:slug": () => [{ slug: "hello" }],
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(response?.status).toBe(404);
|
|
await expect(response?.text()).resolves.toBe("This page could not be found");
|
|
});
|
|
|
|
it("calls generateStaticParams with object parent params and serializes the result", async () => {
|
|
const generateStaticParams = vi.fn(({ params }) => [
|
|
{ category: params.category, slug: "hello" },
|
|
]);
|
|
|
|
const response = await handleAppPrerenderEndpoint(
|
|
new Request(
|
|
"http://localhost/__vinext/prerender/static-params?pattern=/blog/:slug&parentParams=%7B%22category%22%3A%22docs%22%7D",
|
|
),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
pathname: "/__vinext/prerender/static-params",
|
|
staticParamsMap: {
|
|
"/blog/:slug": generateStaticParams,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(generateStaticParams).toHaveBeenCalledWith({ params: { category: "docs" } });
|
|
expect(response?.status).toBe(200);
|
|
await expect(response?.json()).resolves.toEqual([{ category: "docs", slug: "hello" }]);
|
|
});
|
|
|
|
it("seeds root params while calling generateStaticParams", async () => {
|
|
const generateStaticParams = vi.fn(async ({ params }) => [
|
|
{ locale: await getRootParam("locale"), slug: params.slug },
|
|
]);
|
|
|
|
const response = await handleAppPrerenderEndpoint(
|
|
new Request(
|
|
"http://localhost/__vinext/prerender/static-params?pattern=/:locale/blog/:slug&parentParams=%7B%22locale%22%3A%22en%22%2C%22slug%22%3A%22hello%22%7D",
|
|
),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
pathname: "/__vinext/prerender/static-params",
|
|
rootParamNamesByPattern: {
|
|
"/:locale/blog/:slug": ["locale"],
|
|
},
|
|
staticParamsMap: {
|
|
"/:locale/blog/:slug": generateStaticParams,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(response?.status).toBe(200);
|
|
await expect(response?.json()).resolves.toEqual([{ locale: "en", slug: "hello" }]);
|
|
await expect(getRootParam("locale")).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("passes empty parent params when static param input is not an object", async () => {
|
|
const generateStaticParams = vi.fn(() => []);
|
|
|
|
const response = await handleAppPrerenderEndpoint(
|
|
new Request(
|
|
"http://localhost/__vinext/prerender/static-params?pattern=/blog/:slug&parentParams=5",
|
|
),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
pathname: "/__vinext/prerender/static-params",
|
|
staticParamsMap: {
|
|
"/blog/:slug": generateStaticParams,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(generateStaticParams).toHaveBeenCalledWith({ params: {} });
|
|
expect(response?.status).toBe(200);
|
|
});
|
|
|
|
it("loads Pages Router routes lazily and calls getStaticPaths", async () => {
|
|
const getStaticPaths = vi.fn(() => ({
|
|
fallback: false,
|
|
paths: [{ params: { id: "first" } }],
|
|
}));
|
|
const pageRoutes: TestPageRoute[] = [
|
|
{
|
|
module: { getStaticPaths },
|
|
pattern: "/posts/:id",
|
|
},
|
|
];
|
|
|
|
const response = await handleAppPrerenderEndpoint(
|
|
new Request(
|
|
"http://localhost/__vinext/prerender/pages-static-paths?pattern=/posts/:id&locales=%5B%22en%22%5D&defaultLocale=en",
|
|
),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
loadPagesRoutes: async () => pageRoutes,
|
|
pathname: "/__vinext/prerender/pages-static-paths",
|
|
staticParamsMap: {},
|
|
},
|
|
);
|
|
|
|
expect(getStaticPaths).toHaveBeenCalledWith({ defaultLocale: "en", locales: ["en"] });
|
|
expect(response?.status).toBe(200);
|
|
await expect(response?.json()).resolves.toEqual({
|
|
fallback: false,
|
|
paths: [{ params: { id: "first" } }],
|
|
});
|
|
});
|
|
|
|
it("returns JSON null when the requested prerender function is absent", async () => {
|
|
const staticParamsResponse = await handleAppPrerenderEndpoint(
|
|
new Request("http://localhost/__vinext/prerender/static-params?pattern=/missing"),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
pathname: "/__vinext/prerender/static-params",
|
|
staticParamsMap: {},
|
|
},
|
|
);
|
|
const pagesResponse = await handleAppPrerenderEndpoint(
|
|
new Request("http://localhost/__vinext/prerender/pages-static-paths?pattern=/missing"),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
loadPagesRoutes: async () => [],
|
|
pathname: "/__vinext/prerender/pages-static-paths",
|
|
staticParamsMap: {},
|
|
},
|
|
);
|
|
|
|
expect(staticParamsResponse?.status).toBe(200);
|
|
await expect(staticParamsResponse?.text()).resolves.toBe("null");
|
|
expect(pagesResponse?.status).toBe(200);
|
|
await expect(pagesResponse?.text()).resolves.toBe("null");
|
|
});
|
|
|
|
it("returns JSON null when the Pages Router loader returns a non-route shape", async () => {
|
|
const response = await handleAppPrerenderEndpoint(
|
|
new Request("http://localhost/__vinext/prerender/pages-static-paths?pattern=/missing"),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
loadPagesRoutes: async () => ({ pageRoutes: [] }),
|
|
pathname: "/__vinext/prerender/pages-static-paths",
|
|
staticParamsMap: {},
|
|
},
|
|
);
|
|
|
|
expect(response?.status).toBe(200);
|
|
await expect(response?.text()).resolves.toBe("null");
|
|
});
|
|
|
|
it("returns explicit endpoint errors for missing query fields and thrown user functions", async () => {
|
|
const missingPatternResponse = await handleAppPrerenderEndpoint(
|
|
new Request("http://localhost/__vinext/prerender/static-params"),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
pathname: "/__vinext/prerender/static-params",
|
|
staticParamsMap: {},
|
|
},
|
|
);
|
|
const thrownResponse = await handleAppPrerenderEndpoint(
|
|
new Request("http://localhost/__vinext/prerender/static-params?pattern=/blog/:slug"),
|
|
{
|
|
isPrerenderEnabled: () => true,
|
|
pathname: "/__vinext/prerender/static-params",
|
|
staticParamsMap: {
|
|
"/blog/:slug": () => {
|
|
throw new Error("boom");
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(missingPatternResponse?.status).toBe(400);
|
|
await expect(missingPatternResponse?.text()).resolves.toBe("missing pattern");
|
|
expect(thrownResponse?.status).toBe(500);
|
|
await expect(thrownResponse?.json()).resolves.toEqual({ error: "Error: boom" });
|
|
});
|
|
});
|