mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
a155fdf997
* fix(deploy): improve deployment id parity * test(router): isolate deployment prefetch globals * fix(deploy): restrict asset tokens to managed files
199 lines
7.5 KiB
TypeScript
199 lines
7.5 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { type ViteDevServer } from "vite";
|
|
import { beforeAll, afterAll, describe, expect, it } from "vitest";
|
|
import vinext from "../packages/vinext/src/index.js";
|
|
import { APP_FIXTURE_DIR, fetchHtml, RSC_ENTRIES } from "./helpers.js";
|
|
|
|
describe("RSC plugin auto-registration", () => {
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
// Create a server with ONLY vinext() — no explicit @vitejs/plugin-rsc.
|
|
// The plugin should auto-detect the app/ directory and inject RSC.
|
|
// Note: appDir is passed because process.cwd() differs from root in tests.
|
|
// In real projects, cwd === root so appDir is not needed.
|
|
const { createServer } = await import("vite");
|
|
server = await createServer({
|
|
root: APP_FIXTURE_DIR,
|
|
configFile: false,
|
|
plugins: [vinext({ appDir: APP_FIXTURE_DIR })],
|
|
optimizeDeps: { holdUntilCrawlEnd: true },
|
|
server: { port: 0, cors: false },
|
|
logLevel: "silent",
|
|
});
|
|
await server.listen();
|
|
const addr = server.httpServer?.address();
|
|
if (addr && typeof addr === "object") {
|
|
baseUrl = `http://localhost:${addr.port}`;
|
|
}
|
|
}, 30000);
|
|
|
|
afterAll(async () => {
|
|
await server?.close();
|
|
});
|
|
|
|
it("renders the home page without explicit RSC plugin", async () => {
|
|
const { html, res } = await fetchHtml(baseUrl, "/");
|
|
expect(res.status).toBe(200);
|
|
expect(html).toContain("Welcome to App Router");
|
|
});
|
|
|
|
it("renders dynamic routes without explicit RSC plugin", async () => {
|
|
const res = await fetch(`${baseUrl}/blog/auto-rsc-test`);
|
|
expect(res.status).toBe(200);
|
|
const html = await res.text();
|
|
expect(html).toContain("Blog Post");
|
|
expect(html).toContain("auto-rsc-test");
|
|
});
|
|
|
|
it("serves the browser bootstrap in dev when deploymentId is configured", async () => {
|
|
const { createServer } = await import("vite");
|
|
const deploymentServer = await createServer({
|
|
root: APP_FIXTURE_DIR,
|
|
configFile: false,
|
|
plugins: [
|
|
vinext({
|
|
appDir: APP_FIXTURE_DIR,
|
|
nextConfig: () => ({ deploymentId: "dev-deployment-id" }),
|
|
}),
|
|
],
|
|
optimizeDeps: { holdUntilCrawlEnd: true },
|
|
server: { port: 0, cors: false },
|
|
logLevel: "silent",
|
|
});
|
|
await deploymentServer.listen();
|
|
|
|
try {
|
|
const address = deploymentServer.httpServer?.address();
|
|
const url = address && typeof address === "object" ? `http://localhost:${address.port}` : "";
|
|
const response = await fetch(`${url}/`);
|
|
expect(response.status).toBe(200);
|
|
const html = await response.text();
|
|
const bootstrapUrl = html.match(/<script[^>]+type="module"[^>]+src="([^"]+)"/)?.[1];
|
|
expect(bootstrapUrl).not.toContain("dpl=");
|
|
|
|
const bootstrapResponse = await fetch(new URL(bootstrapUrl!, url));
|
|
expect(bootstrapResponse.status).toBe(200);
|
|
expect(bootstrapResponse.headers.get("content-type")).toContain("javascript");
|
|
} finally {
|
|
await deploymentServer.close();
|
|
}
|
|
}, 30_000);
|
|
|
|
it("does not double-register when RSC plugin is already present", async () => {
|
|
const { createServer } = await import("vite");
|
|
const rsc = (await import("@vitejs/plugin-rsc")).default;
|
|
|
|
// Create a server with BOTH vinext({ rsc: false }) and explicit rsc().
|
|
// Should work without errors (no duplicate registration).
|
|
const serverWithExplicitRsc = await createServer({
|
|
root: APP_FIXTURE_DIR,
|
|
configFile: false,
|
|
plugins: [vinext({ appDir: APP_FIXTURE_DIR, rsc: false }), rsc({ entries: RSC_ENTRIES })],
|
|
optimizeDeps: { holdUntilCrawlEnd: true },
|
|
server: { port: 0, cors: false },
|
|
logLevel: "silent",
|
|
});
|
|
await serverWithExplicitRsc.listen();
|
|
|
|
try {
|
|
const addr = serverWithExplicitRsc.httpServer?.address();
|
|
const url = addr && typeof addr === "object" ? `http://localhost:${addr.port}` : "";
|
|
const res = await fetch(`${url}/`);
|
|
expect(res.status).toBe(200);
|
|
const html = await res.text();
|
|
expect(html).toContain("Welcome to App Router");
|
|
} finally {
|
|
await serverWithExplicitRsc.close();
|
|
}
|
|
}, 30000);
|
|
|
|
it("throws an error when user double-registers rsc() alongside auto-registration", async () => {
|
|
const { createBuilder } = await import("vite");
|
|
const rsc = (await import("@vitejs/plugin-rsc")).default;
|
|
|
|
// vinext() auto-registers @vitejs/plugin-rsc when app/ is detected.
|
|
// Manually adding rsc() on top should throw a clear error telling
|
|
// the user to fix their config — not silently double the build time.
|
|
await expect(
|
|
createBuilder({
|
|
root: APP_FIXTURE_DIR,
|
|
configFile: false,
|
|
plugins: [vinext({ appDir: APP_FIXTURE_DIR }), rsc({ entries: RSC_ENTRIES })],
|
|
logLevel: "silent",
|
|
}),
|
|
).rejects.toThrow("Duplicate @vitejs/plugin-rsc detected");
|
|
}, 30000);
|
|
|
|
it("auto-injects RSC plugin when src/app exists but root-level app/ does not", async () => {
|
|
// Regression test: the early detection path (before config()) must check
|
|
// both {base}/app and {base}/src/app to match the full config() logic.
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-src-app-"));
|
|
try {
|
|
// Create only src/app/ — no root-level app/ directory.
|
|
fs.mkdirSync(path.join(tmpDir, "src", "app"), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, "src", "app", "page.tsx"),
|
|
"export default function Home() { return <h1>Home</h1>; }",
|
|
);
|
|
// Symlink node_modules so createRequire can find @vitejs/plugin-rsc
|
|
// from the temp directory (resolution is relative to appDir).
|
|
fs.symlinkSync(
|
|
path.resolve(__dirname, "..", "node_modules"),
|
|
path.join(tmpDir, "node_modules"),
|
|
"junction",
|
|
);
|
|
|
|
const plugins = vinext({ appDir: tmpDir, react: false });
|
|
|
|
const resolvedPlugins = (
|
|
await Promise.all(
|
|
plugins.map(async (plugin) => {
|
|
if (plugin && typeof (plugin as any).then === "function") {
|
|
return await (plugin as Promise<any>);
|
|
}
|
|
return plugin;
|
|
}),
|
|
)
|
|
).flat();
|
|
|
|
const hasRscPlugin = resolvedPlugins.some((p) => p && (p as any).name === "rsc");
|
|
expect(hasRscPlugin).toBe(true);
|
|
} finally {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("does NOT auto-inject RSC plugin when neither app/ nor src/app/ exists", async () => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-no-app-"));
|
|
try {
|
|
// Empty directory — no app/ or src/app/.
|
|
const plugins = vinext({ appDir: tmpDir, react: false });
|
|
|
|
const resolvedPlugins = (
|
|
await Promise.all(
|
|
plugins.map(async (plugin) => {
|
|
if (plugin && typeof (plugin as any).then === "function") {
|
|
return await (plugin as Promise<any>);
|
|
}
|
|
return plugin;
|
|
}),
|
|
)
|
|
).flat();
|
|
|
|
const hasRscPlugin = resolvedPlugins.some((p) => p && (p as any).name === "rsc");
|
|
expect(hasRscPlugin).toBe(false);
|
|
} finally {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── External rewrite proxy credential forwarding (App Router) ────────────────
|
|
// Regression test: the proxyExternalRequest (imported from config-matchers) in the generated RSC entry
|
|
// must forward credential headers like Next.js while still stripping
|
|
// x-middleware-* headers before forwarding to external rewrite destinations.
|