mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
71dbb45ca8
* fix(pages): pin revalidate loopback origin * test(pages): cover production revalidation origin * fix(pages): preserve internal revalidation boundaries * fix(pages): preserve on-demand revalidation boundaries * fix(pages): dispatch Worker revalidation internally * fix(pages): authenticate revalidation request context * fix(pages): isolate revalidation transport headers * test(pages): account for generated route table size * fix(pages): align on-demand revalidation semantics * fix(pages): align revalidation response cache parity * fix(pages): preserve regenerated ISR representations * fix(pages): align ISR cache representations * fix(pages): preserve canonical ISR representations * fix(pages): align cached response parity * fix(pages): preserve custom App render props * fix(pages): preserve optional App page props * fix(pages): preserve optional App props on the client * fix(pages): normalize client App page props * fix(pages): preserve App data merge semantics * fix(pages): keep redirect status helper internal * fix(pages): match Next.js terminal ISR behavior * fix(pages): preserve custom app error envelopes * fix(pages): match Next.js dev revalidation semantics * test(pages): align dev revalidation parity coverage * chore(pages): remove stale cache helper exports * fix(cache): preserve explicit no-store context
195 lines
6.8 KiB
TypeScript
195 lines
6.8 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from "vite-plus/test";
|
|
import { build, type ViteDevServer } from "vite";
|
|
import path from "node:path";
|
|
import http from "node:http";
|
|
import vinext from "../packages/vinext/src/index.js";
|
|
import { createIsolatedFixture, PAGES_FIXTURE_DIR, startFixtureServer } from "./helpers.js";
|
|
|
|
const CONCURRENCY = 15;
|
|
|
|
async function fetchConcurrentPages(
|
|
baseUrl: string,
|
|
pagePath: string,
|
|
count: number,
|
|
): Promise<string[]> {
|
|
const requests = Array.from({ length: count }, (_, i) =>
|
|
fetch(`${baseUrl}${pagePath}?id=${i}`).then((r) => r.text()),
|
|
);
|
|
|
|
return Promise.all(requests);
|
|
}
|
|
|
|
function extractTestId(html: string, testId: string): string | null {
|
|
const pattern = new RegExp(`data-testid="${testId}"[^>]*>([^<]*)<`);
|
|
const match = html.match(pattern);
|
|
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
function extractTitle(html: string): string | null {
|
|
const match = html.match(/<title[^>]*>([^<]*)<\/title>/);
|
|
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
function extractMetaContent(html: string, name: string): string | null {
|
|
const pattern = new RegExp(`<meta(?=[^>]*name="${name}")[^>]+content="([^"]*)"`, "i");
|
|
const match = html.match(pattern);
|
|
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
describe("Pages Router dev concurrency isolation", () => {
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
({ server, baseUrl } = await startFixtureServer(PAGES_FIXTURE_DIR));
|
|
}, 30000);
|
|
|
|
afterAll(async () => {
|
|
await server?.close();
|
|
});
|
|
|
|
it("<Head> elements do not leak between concurrent requests", async () => {
|
|
const htmlResults = await fetchConcurrentPages(baseUrl, "/concurrent-head", CONCURRENCY);
|
|
|
|
for (let i = 0; i < CONCURRENCY; i++) {
|
|
const title = extractTitle(htmlResults[i]);
|
|
const metaReqId = extractMetaContent(htmlResults[i], "req-id");
|
|
const bodyReqId = extractTestId(htmlResults[i], "req-id");
|
|
|
|
expect(bodyReqId, `response ${i} should contain its own req-id`).toBe(String(i));
|
|
expect(title, `response ${i} should have its own <title>`).toBe(`req-${bodyReqId}`);
|
|
expect(metaReqId, `response ${i} meta req-id should match body`).toBe(bodyReqId);
|
|
}
|
|
});
|
|
|
|
it("router SSR context does not leak between concurrent requests", async () => {
|
|
const htmlResults = await fetchConcurrentPages(baseUrl, "/concurrent-router", CONCURRENCY);
|
|
|
|
for (let i = 0; i < CONCURRENCY; i++) {
|
|
const ssrPathname = extractTestId(htmlResults[i], "ssr-pathname");
|
|
const routerPathname = extractTestId(htmlResults[i], "router-pathname");
|
|
|
|
expect(ssrPathname, `response ${i} ssr-pathname`).toBe("/concurrent-router");
|
|
expect(routerPathname, `response ${i} router-pathname`).toBe("/concurrent-router");
|
|
|
|
const nextData = htmlResults[i].match(
|
|
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
|
|
);
|
|
if (!nextData) throw new Error(`no __NEXT_DATA__ found in response ${i}`);
|
|
const data = JSON.parse(nextData[1]);
|
|
expect(data.props.pageProps.ssrQuery.id, `response ${i} ssrQuery.id`).toBe(String(i));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Pages Router prod concurrency isolation", () => {
|
|
let prodServer: http.Server | undefined;
|
|
let prodPort: number;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await createIsolatedFixture(
|
|
PAGES_FIXTURE_DIR,
|
|
"vinext-pages-concurrency-",
|
|
(src) => !src.includes("node_modules") && !src.includes("alias-test"),
|
|
);
|
|
|
|
const outDir = path.join(tmpDir, "dist");
|
|
|
|
await build({
|
|
root: tmpDir,
|
|
configFile: false,
|
|
plugins: [vinext()],
|
|
logLevel: "silent",
|
|
// This isolated build deliberately skips the workspace Vite config;
|
|
// retain its source alias so fixture imports of exported vinext internals
|
|
// resolve without requiring a prebuilt package.
|
|
resolve: {
|
|
alias: {
|
|
"vinext/internal": path.resolve(import.meta.dirname, "../packages/vinext/src"),
|
|
},
|
|
},
|
|
build: {
|
|
outDir: path.join(outDir, "server"),
|
|
ssr: "virtual:vinext-server-entry",
|
|
rolldownOptions: { output: { entryFileNames: "entry.js" } },
|
|
},
|
|
});
|
|
|
|
await build({
|
|
root: tmpDir,
|
|
configFile: false,
|
|
plugins: [vinext()],
|
|
logLevel: "silent",
|
|
build: {
|
|
outDir: path.join(outDir, "client"),
|
|
manifest: true,
|
|
ssrManifest: true,
|
|
rolldownOptions: { input: "virtual:vinext-client-entry" },
|
|
},
|
|
});
|
|
|
|
const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
|
|
({ server: prodServer } = await startProdServer({
|
|
port: 0,
|
|
host: "127.0.0.1",
|
|
outDir,
|
|
}));
|
|
|
|
const addr = prodServer!.address();
|
|
if (!addr || typeof addr === "string") {
|
|
throw new Error("Failed to start production server");
|
|
}
|
|
prodPort = addr.port;
|
|
}, 60000);
|
|
|
|
afterAll(async () => {
|
|
if (prodServer) {
|
|
await new Promise<void>((resolve) => prodServer!.close(() => resolve()));
|
|
}
|
|
if (tmpDir) {
|
|
const fsp = await import("node:fs/promises");
|
|
await fsp.rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// prod does not inject <Head> children into the html <head> section, so we
|
|
// can only verify getServerSideProps data isolation here (not title/meta).
|
|
it("getServerSideProps data does not leak between concurrent requests", async () => {
|
|
const base = `http://127.0.0.1:${prodPort}`;
|
|
const htmlResults = await fetchConcurrentPages(base, "/concurrent-head", CONCURRENCY);
|
|
|
|
for (let i = 0; i < CONCURRENCY; i++) {
|
|
const bodyReqId = extractTestId(htmlResults[i], "req-id");
|
|
expect(bodyReqId, `response ${i} should contain its own req-id`).toBe(String(i));
|
|
|
|
const nextData = htmlResults[i].match(
|
|
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
|
|
);
|
|
if (!nextData) throw new Error(`no __NEXT_DATA__ found in response ${i}`);
|
|
const data = JSON.parse(nextData[1]);
|
|
expect(data.props.pageProps.reqId, `response ${i} pageProps.reqId`).toBe(String(i));
|
|
}
|
|
});
|
|
|
|
it("SSR props do not leak between concurrent requests", async () => {
|
|
const base = `http://127.0.0.1:${prodPort}`;
|
|
const htmlResults = await fetchConcurrentPages(base, "/concurrent-router", CONCURRENCY);
|
|
|
|
for (let i = 0; i < CONCURRENCY; i++) {
|
|
const ssrPathname = extractTestId(htmlResults[i], "ssr-pathname");
|
|
expect(ssrPathname, `response ${i} ssr-pathname`).toBe("/concurrent-router");
|
|
|
|
const nextData = htmlResults[i].match(
|
|
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
|
|
);
|
|
if (!nextData) throw new Error(`no __NEXT_DATA__ found in response ${i}`);
|
|
const data = JSON.parse(nextData[1]);
|
|
expect(data.props.pageProps.ssrQuery.id, `response ${i} ssrQuery.id`).toBe(String(i));
|
|
}
|
|
});
|
|
});
|