test: parallelize the integration suite (#3201)

This commit is contained in:
James Anderson
2026-09-09 18:17:57 +01:00
committed by GitHub
parent 609d2227f3
commit f103e80700
7 changed files with 132 additions and 42 deletions
+33 -16
View File
@@ -3,10 +3,10 @@ import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { createBuilder } from "vite";
import { afterAll, describe, expect, it, vi } from "vite-plus/test";
import { afterAll, beforeAll, describe, expect, it, vi } from "vite-plus/test";
import vinext from "../packages/vinext/src/index.js";
import { runPrerender } from "../packages/vinext/src/build/run-prerender.js";
import { APP_FIXTURE_DIR } from "./helpers.js";
import { APP_FIXTURE_DIR, createIsolatedFixture, testCacheDir } from "./helpers.js";
type BuiltAppHandler = (request: Request) => Promise<Response | string | null | undefined>;
@@ -33,17 +33,29 @@ function readAllJs(dir: string): string {
}
describe("App Router Production build", () => {
const outDir = path.resolve(APP_FIXTURE_DIR, "dist");
let fixtureDir: string;
let outDir: string;
beforeAll(async () => {
fixtureDir = await createIsolatedFixture(
APP_FIXTURE_DIR,
"vinext-app-production-build-",
undefined,
path.join(APP_FIXTURE_DIR, "node_modules"),
);
outDir = path.join(fixtureDir, "dist");
});
afterAll(() => {
fs.rmSync(outDir, { recursive: true, force: true });
fs.rmSync(fixtureDir, { recursive: true, force: true });
});
it("produces RSC/SSR/client bundles via vite build", async () => {
const builder = await createBuilder({
root: APP_FIXTURE_DIR,
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
plugins: [vinext({ appDir: fixtureDir })],
logLevel: "silent",
});
await builder.buildApp();
@@ -193,9 +205,10 @@ describe("App Router Production build", () => {
process.env.__VINEXT_SHARED_BUILD_ID = sharedBuildId;
try {
const builder = await createBuilder({
root: APP_FIXTURE_DIR,
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
plugins: [vinext({ appDir: fixtureDir })],
logLevel: "silent",
});
await builder.buildApp();
@@ -223,9 +236,10 @@ describe("App Router Production build", () => {
process.env.__VINEXT_SHARED_PRERENDER_SECRET = sharedSecret;
try {
const builder = await createBuilder({
root: APP_FIXTURE_DIR,
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
plugins: [vinext({ appDir: fixtureDir })],
logLevel: "silent",
});
await builder.buildApp();
@@ -254,11 +268,12 @@ describe("App Router Production build", () => {
process.env.__VINEXT_SHARED_BUILD_ID = sharedBuildId;
try {
const builder = await createBuilder({
root: APP_FIXTURE_DIR,
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
// generateBuildId returning null falls back to a random UUID per
// instance; the shared ID must still be adopted.
plugins: [vinext({ appDir: APP_FIXTURE_DIR, nextConfig: { generateBuildId: () => null } })],
plugins: [vinext({ appDir: fixtureDir, nextConfig: { generateBuildId: () => null } })],
logLevel: "silent",
});
await builder.buildApp();
@@ -287,9 +302,10 @@ describe("App Router Production build", () => {
process.env.__VINEXT_SHARED_RSC_COMPATIBILITY_ID = sharedCompatId;
try {
const builder = await createBuilder({
root: APP_FIXTURE_DIR,
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
plugins: [vinext({ appDir: fixtureDir })],
logLevel: "silent",
});
await builder.buildApp();
@@ -581,9 +597,10 @@ export default async function OpenGraphImage() {
const { preview } = await import("vite");
const previewServer = await preview({
root: APP_FIXTURE_DIR,
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
plugins: [vinext({ appDir: fixtureDir })],
preview: { port: 0 },
logLevel: "silent",
});
+2 -1
View File
@@ -9,7 +9,7 @@ import {
getPagesClientAssets,
setPagesClientAssets,
} from "../packages/vinext/src/server/pages-client-assets.js";
import { APP_FIXTURE_DIR, createIsolatedFixture } from "./helpers.js";
import { APP_FIXTURE_DIR, createIsolatedFixture, testCacheDir } from "./helpers.js";
const ROOT_LAYOUT_NOT_FOUND_REDIRECT_FIXTURE_DIR = path.resolve(
import.meta.dirname,
@@ -296,6 +296,7 @@ describe("App Router Production server (startProdServer)", () => {
// Build the app-basic fixture to the default dist/ directory
const builder = await createBuilder({
root: APP_FIXTURE_DIR,
cacheDir: testCacheDir(APP_FIXTURE_DIR),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
logLevel: "silent",
+6 -1
View File
@@ -4,7 +4,7 @@ import path from "node:path";
import { type ViteDevServer } from "vite";
import { beforeAll, afterAll, describe, expect, it } from "vite-plus/test";
import vinext from "../packages/vinext/src/index.js";
import { APP_FIXTURE_DIR, fetchHtml, RSC_ENTRIES } from "./helpers.js";
import { APP_FIXTURE_DIR, fetchHtml, RSC_ENTRIES, testCacheDir } from "./helpers.js";
describe("RSC plugin auto-registration", () => {
let server: ViteDevServer;
@@ -18,6 +18,7 @@ describe("RSC plugin auto-registration", () => {
const { createServer } = await import("vite");
server = await createServer({
root: APP_FIXTURE_DIR,
cacheDir: testCacheDir(APP_FIXTURE_DIR),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
optimizeDeps: { holdUntilCrawlEnd: true },
@@ -53,6 +54,7 @@ describe("RSC plugin auto-registration", () => {
const { createServer } = await import("vite");
const deploymentServer = await createServer({
root: APP_FIXTURE_DIR,
cacheDir: testCacheDir(APP_FIXTURE_DIR),
configFile: false,
plugins: [
vinext({
@@ -91,6 +93,7 @@ describe("RSC plugin auto-registration", () => {
// Should work without errors (no duplicate registration).
const serverWithExplicitRsc = await createServer({
root: APP_FIXTURE_DIR,
cacheDir: testCacheDir(APP_FIXTURE_DIR),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR, rsc: false }), rsc({ entries: RSC_ENTRIES })],
optimizeDeps: { holdUntilCrawlEnd: true },
@@ -117,6 +120,7 @@ describe("RSC plugin auto-registration", () => {
await expect(
createServer({
root: APP_FIXTURE_DIR,
cacheDir: testCacheDir(APP_FIXTURE_DIR),
configFile: false,
plugins: [rsc({ entries: RSC_ENTRIES }), vinext({ appDir: APP_FIXTURE_DIR, rsc: false })],
optimizeDeps: { holdUntilCrawlEnd: true },
@@ -136,6 +140,7 @@ describe("RSC plugin auto-registration", () => {
await expect(
createBuilder({
root: APP_FIXTURE_DIR,
cacheDir: testCacheDir(APP_FIXTURE_DIR),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR }), rsc({ entries: RSC_ENTRIES })],
logLevel: "silent",
+36
View File
@@ -8,6 +8,7 @@
*/
import http, { type IncomingHttpHeaders } from "node:http";
import fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import { pathToFileURL } from "node:url";
@@ -15,6 +16,7 @@ import { createServer, build, type ViteDevServer } from "vite";
import vinext from "../packages/vinext/src/index.js";
import path from "node:path";
import type { NextConfigInput } from "../packages/vinext/src/config/next-config.js";
import { afterAll } from "vite-plus/test";
// ── Fixture paths ─────────────────────────────────────────────
export const PAGES_FIXTURE_DIR = path.resolve(import.meta.dirname, "./fixtures/pages-basic");
@@ -35,6 +37,36 @@ export const RSC_ENTRIES = {
client: "virtual:vinext-app-browser-entry",
} as const;
const testCacheDirs = new Map<string, string>();
let testCacheRoot: string | undefined;
if (process.env.VINEXT_PARALLEL_INTEGRATION === "true") {
afterAll(() => {
if (testCacheRoot) fsSync.rmSync(testCacheRoot, { recursive: true, force: true });
testCacheDirs.clear();
testCacheRoot = undefined;
});
}
export function testCacheDir(fixtureDir: string): string | undefined {
if (process.env.VINEXT_PARALLEL_INTEGRATION !== "true") return undefined;
const fixture = path.resolve(fixtureDir);
let cacheDir = testCacheDirs.get(fixture);
if (!cacheDir) {
if (!testCacheRoot) {
testCacheRoot = path.resolve(
import.meta.dirname,
`../node_modules/.vite-vitest-${process.pid}`,
);
fsSync.mkdirSync(testCacheRoot);
}
cacheDir = path.join(testCacheRoot, String(testCacheDirs.size));
testCacheDirs.set(fixture, cacheDir);
}
return cacheDir;
}
// ── Server lifecycle helper ───────────────────────────────────
export type TestServerResult = {
@@ -86,6 +118,7 @@ export async function startFixtureServer(
const server = await createServer({
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins,
publicDir: opts?.publicDir,
@@ -263,6 +296,7 @@ export async function buildPagesFixture(
// (hybrid); we only want the Pages Router SSR bundle here.
await build({
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ disableAppRouter: true, nextConfig })],
logLevel: "silent",
@@ -299,6 +333,7 @@ export async function buildAppFixture(
const { createBuilder } = await import("vite");
const builder = await createBuilder({
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ appDir: fixtureDir, rscOutDir, ssrOutDir, clientOutDir, nextConfig })],
logLevel: "silent",
@@ -349,6 +384,7 @@ export async function buildCloudflareAppFixture(fixtureDir: string): Promise<{
const { createBuilder } = await import("vite");
const builder = await createBuilder({
root: tmpDir,
cacheDir: testCacheDir(tmpDir),
configFile: false,
plugins: [
vinext({ appDir: tmpDir }),
+21 -7
View File
@@ -31,7 +31,13 @@ import path from "node:path";
import { describe, it, expect, beforeAll, afterAll } from "vite-plus/test";
import { createBuilder, preview, type ViteDevServer } from "vite-plus";
import vinext from "../../packages/vinext/src/index.js";
import { APP_FIXTURE_DIR, startFixtureServer, fetchHtml } from "../helpers.js";
import {
APP_FIXTURE_DIR,
createIsolatedFixture,
startFixtureServer,
fetchHtml,
testCacheDir,
} from "../helpers.js";
describe("Next.js compat: global-error", () => {
let server: ViteDevServer;
@@ -273,23 +279,31 @@ describe("Next.js compat: global-error", () => {
});
describe("Next.js compat: global-error (production preview)", () => {
const outDir = path.resolve(APP_FIXTURE_DIR, "dist");
let fixtureDir: string;
let previewServer: Awaited<ReturnType<typeof preview>>;
let baseUrl: string;
beforeAll(async () => {
fixtureDir = await createIsolatedFixture(
APP_FIXTURE_DIR,
"vinext-global-error-",
undefined,
path.join(APP_FIXTURE_DIR, "node_modules"),
);
const builder = await createBuilder({
root: APP_FIXTURE_DIR,
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
plugins: [vinext({ appDir: fixtureDir })],
logLevel: "silent",
});
await builder.buildApp();
previewServer = await preview({
root: APP_FIXTURE_DIR,
root: fixtureDir,
cacheDir: testCacheDir(fixtureDir),
configFile: false,
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
plugins: [vinext({ appDir: fixtureDir })],
preview: { port: 0 },
logLevel: "silent",
});
@@ -303,7 +317,7 @@ describe("Next.js compat: global-error (production preview)", () => {
afterAll(() => {
previewServer?.httpServer.close();
fs.rmSync(outDir, { recursive: true, force: true });
fs.rmSync(fixtureDir, { recursive: true, force: true });
});
it("server component throw without local error.tsx renders global-error with 500", async () => {
+26 -11
View File
@@ -13,7 +13,12 @@ import fs from "node:fs";
import { createServer, type Server } from "node:http";
import os from "node:os";
import path from "node:path";
import { buildPagesFixture, buildAppFixture, buildCloudflareAppFixture } from "./helpers.js";
import {
buildPagesFixture,
buildAppFixture,
buildCloudflareAppFixture,
createIsolatedFixture,
} from "./helpers.js";
import {
extractRscPayloadFromPrerenderedHtml,
resolveParentParams,
@@ -2015,25 +2020,35 @@ describe("prerenderApp — cacheComponents PPR fallback-shell artifacts", () =>
// ─── runPrerender — output: 'export' wiring ───────────────────────────────────
describe("runPrerender — output: 'export' wiring", () => {
let fixtureDir: string;
let pagesBundlePath: string;
let exportNextConfig: Awaited<
ReturnType<typeof import("../packages/vinext/src/config/next-config.js").resolveNextConfig>
>;
beforeAll(async () => {
// Build pages-basic to a fresh tmpdir — no fixture copying needed.
fixtureDir = await createIsolatedFixture(
PAGES_FIXTURE,
"vinext-run-prerender-",
undefined,
path.join(PAGES_FIXTURE, "node_modules"),
);
// Pass the bundle path and resolved config to runPrerender so it
// exercises output: 'export' without touching the real next.config.mjs.
pagesBundlePath = await buildPagesFixture(PAGES_FIXTURE);
const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
exportNextConfig = await resolveNextConfig({ output: "export" }, PAGES_FIXTURE);
exportNextConfig = await resolveNextConfig({ output: "export" }, fixtureDir);
}, 120_000);
afterAll(() => {
fs.rmSync(fixtureDir, { recursive: true, force: true });
});
it("throws when next.config output: 'export' and SSR routes exist", async () => {
const { runPrerender } = await import("../packages/vinext/src/build/run-prerender.js");
await expect(
runPrerender({
root: PAGES_FIXTURE,
root: fixtureDir,
nextConfig: exportNextConfig,
pagesBundlePath,
}),
@@ -2041,7 +2056,7 @@ describe("runPrerender — output: 'export' wiring", () => {
});
it("does not reload disk config when the caller supplies resolved config", async () => {
const configPath = path.join(PAGES_FIXTURE, "next.config.mjs");
const configPath = path.join(fixtureDir, "next.config.mjs");
const originalConfig = fs.readFileSync(configPath, "utf-8");
try {
@@ -2050,11 +2065,11 @@ describe("runPrerender — output: 'export' wiring", () => {
import("../packages/vinext/src/build/run-prerender.js"),
import("../packages/vinext/src/config/next-config.js"),
]);
const nextConfig = await resolveNextConfig({ output: "export" }, PAGES_FIXTURE);
const nextConfig = await resolveNextConfig({ output: "export" }, fixtureDir);
await expect(
runPrerender({
root: PAGES_FIXTURE,
root: fixtureDir,
nextConfig,
pagesBundlePath,
}),
@@ -2065,7 +2080,7 @@ describe("runPrerender — output: 'export' wiring", () => {
});
it("does not rewrite the Worker entry when prerender validation fails", async () => {
const workerEntry = path.join(PAGES_FIXTURE, "dist", "server", "index.js");
const workerEntry = path.join(fixtureDir, "dist", "server", "index.js");
const source = 'export default { fetch() { return new Response("unchanged"); } };\n';
fs.mkdirSync(path.dirname(workerEntry), { recursive: true });
fs.writeFileSync(workerEntry, source, "utf-8");
@@ -2074,7 +2089,7 @@ describe("runPrerender — output: 'export' wiring", () => {
const { runPrerender } = await import("../packages/vinext/src/build/run-prerender.js");
await expect(
runPrerender({
root: PAGES_FIXTURE,
root: fixtureDir,
nextConfig: exportNextConfig,
pagesBundlePath,
}),
@@ -2082,7 +2097,7 @@ describe("runPrerender — output: 'export' wiring", () => {
expect(fs.readFileSync(workerEntry, "utf-8")).toBe(source);
} finally {
fs.rmSync(path.join(PAGES_FIXTURE, "dist"), { recursive: true, force: true });
fs.rmSync(path.join(fixtureDir, "dist"), { recursive: true, force: true });
}
});
@@ -2090,7 +2105,7 @@ describe("runPrerender — output: 'export' wiring", () => {
const { runPrerender } = await import("../packages/vinext/src/build/run-prerender.js");
await expect(
runPrerender({
root: PAGES_FIXTURE,
root: fixtureDir,
nextConfig: exportNextConfig,
pagesBundlePath,
}),
+8 -6
View File
@@ -177,9 +177,8 @@ export default defineConfig({
include: ["tests/**/*.test.ts", "scripts/**/*.test.ts"],
exclude: [
"tests/fixtures/**/node_modules/**",
// Integration tests: spin up Vite dev servers against shared fixture
// dirs. Must run serially to avoid Vite deps optimizer cache races
// (node_modules/.vite/*) that produce "outdated pre-bundle" 500s.
// Integration tests spin up Vite dev servers against shared fixture
// dirs and use per-worker optimizer caches in their own project.
// When adding a test that calls startFixtureServer() or createServer(),
// move it here.
"tests/app-router-client-preloading.test.ts",
@@ -241,6 +240,9 @@ export default defineConfig({
},
test: {
name: "integration",
env: {
VINEXT_PARALLEL_INTEGRATION: "true",
},
// MSW is intentionally NOT installed in the integration project.
// Integration tests spin up in-process HTTP servers and fixture
// dev servers and exercise them via `fetch("http://127.0.0.1:<port>/...")`.
@@ -305,9 +307,9 @@ export default defineConfig({
"tests/nextjs-compat/**/*.test.ts",
],
testTimeout: 30000,
// Serial execution prevents Vite deps optimizer cache races when
// multiple test files share the same fixture directory.
fileParallelism: false,
fileParallelism: true,
maxWorkers: 3,
sequence: { groupOrder: 1 },
},
},
],