mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
1ca8763fec
* fix(build): preserve require export conditions * fix(build): resolve require conditions in dev * test(build): assert visible conditional export output * fix(build): harden require condition resolution * perf(build): skip ineligible require condition scans * test(build): cover require conditions on Workers
223 lines
7.3 KiB
TypeScript
223 lines
7.3 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from "vite-plus/test";
|
|
import type { ViteDevServer } from "vite-plus";
|
|
import type { Server } from "node:http";
|
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import {
|
|
APP_FIXTURE_DIR,
|
|
PAGES_FIXTURE_DIR,
|
|
buildAppFixture,
|
|
startFixtureServer,
|
|
fetchHtml,
|
|
} from "./helpers.js";
|
|
|
|
async function writeFixtureFile(
|
|
root: string,
|
|
relativePath: string,
|
|
contents: string,
|
|
): Promise<void> {
|
|
const file = path.join(root, relativePath);
|
|
await mkdir(path.dirname(file), { recursive: true });
|
|
await writeFile(file, contents);
|
|
}
|
|
|
|
function visibleTextByTestId(html: string, testId: string): string {
|
|
const attribute = `data-testid="${testId}"`;
|
|
const attributeIndex = html.indexOf(attribute);
|
|
if (attributeIndex === -1) throw new Error(`Missing ${attribute}`);
|
|
const contentStart = html.indexOf(">", attributeIndex);
|
|
const contentEnd = html.indexOf("</", contentStart);
|
|
if (contentStart === -1 || contentEnd === -1) {
|
|
throw new Error(`Missing element content for ${attribute}`);
|
|
}
|
|
return html
|
|
.slice(contentStart + 1, contentEnd)
|
|
.replaceAll("<!-- -->", "")
|
|
.replaceAll(""", '"')
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll("&", "&");
|
|
}
|
|
|
|
describe("CJS interop (App Router)", () => {
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
({ server, baseUrl } = await startFixtureServer(APP_FIXTURE_DIR, { appRouter: true }));
|
|
}, 30000);
|
|
|
|
afterAll(async () => {
|
|
await server?.close();
|
|
});
|
|
|
|
it("renders page that uses CJS require() and module.exports", async () => {
|
|
const { res, html } = await fetchHtml(baseUrl, "/cjs/basic");
|
|
expect(res.status).toBe(200);
|
|
expect(html).toContain("cjs-basic");
|
|
// React SSR may insert comment nodes between text and expressions
|
|
// (e.g. "Random: <!-- -->4"), so use a regex.
|
|
expect(html).toMatch(/Random:.*4/);
|
|
});
|
|
|
|
it("renders page that uses CJS require('server-only')", async () => {
|
|
const { res, html } = await fetchHtml(baseUrl, "/cjs/server-only");
|
|
expect(res.status).toBe(200);
|
|
expect(html).toContain("cjs-server-only");
|
|
expect(html).toContain("This page uses CJS require");
|
|
});
|
|
});
|
|
|
|
describe("CJS interop (Pages Router)", () => {
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
({ server, baseUrl } = await startFixtureServer(PAGES_FIXTURE_DIR));
|
|
}, 30000);
|
|
|
|
afterAll(async () => {
|
|
await server?.close();
|
|
});
|
|
|
|
it("renders page that uses CJS require() and module.exports", async () => {
|
|
const { res, html } = await fetchHtml(baseUrl, "/cjs/basic");
|
|
expect(res.status).toBe(200);
|
|
expect(html).toContain("cjs-basic");
|
|
// Pages Router SSR inserts React comment nodes between text and
|
|
// expressions (e.g. "Random: <!-- -->4"), so use a regex.
|
|
expect(html).toMatch(/Random:.*4/);
|
|
});
|
|
});
|
|
|
|
// Ported from Next.js: test/e2e/app-dir/client-module-with-package-type/index.test.ts
|
|
// https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/client-module-with-package-type/index.test.ts
|
|
const CONDITIONAL_EXPORT_CASES = [
|
|
["/import-cjs", "lib-cjs", "esm"],
|
|
["/require-cjs", "lib-cjs", "cjs"],
|
|
["/import-esm", "lib-esm", "esm"],
|
|
["/require-esm", "lib-esm", "cjs"],
|
|
] as const;
|
|
|
|
async function expectConditionalExport(
|
|
baseUrl: string,
|
|
route: string,
|
|
label: string,
|
|
expected: string,
|
|
): Promise<void> {
|
|
const { res, html } = await fetchHtml(baseUrl, route);
|
|
expect(res.status).toBe(200);
|
|
expect(visibleTextByTestId(html, "conditional-result")).toBe(`${label}: ${expected}`);
|
|
}
|
|
|
|
describe("conditional package exports", () => {
|
|
let root: string;
|
|
let server: ViteDevServer;
|
|
let devBaseUrl: string;
|
|
let prodServer: Server;
|
|
let prodBaseUrl: string;
|
|
let buildOutDir: string;
|
|
|
|
beforeAll(async () => {
|
|
root = await mkdtemp(path.join(import.meta.dirname, ".require-condition-"));
|
|
await Promise.all([
|
|
writeFixtureFile(root, "package.json", JSON.stringify({ private: true, type: "module" })),
|
|
writeFixtureFile(
|
|
root,
|
|
"app/layout.tsx",
|
|
`export default function Layout({ children }: { children: React.ReactNode }) { return <html><body>{children}</body></html>; }`,
|
|
),
|
|
writeFixtureFile(
|
|
root,
|
|
"node_modules/lib-cjs/package.json",
|
|
JSON.stringify({
|
|
name: "lib-cjs",
|
|
type: "commonjs",
|
|
exports: { ".": { import: "./index.mjs", default: "./index.js" } },
|
|
}),
|
|
),
|
|
writeFixtureFile(
|
|
root,
|
|
"node_modules/lib-cjs/index.mjs",
|
|
`"use client"; export default () => "esm";`,
|
|
),
|
|
writeFixtureFile(
|
|
root,
|
|
"node_modules/lib-cjs/index.js",
|
|
`"use client"; module.exports = () => "cjs";`,
|
|
),
|
|
writeFixtureFile(
|
|
root,
|
|
"node_modules/lib-esm/package.json",
|
|
JSON.stringify({
|
|
name: "lib-esm",
|
|
type: "module",
|
|
exports: { ".": { require: "./index.cjs", default: "./index.js" } },
|
|
}),
|
|
),
|
|
writeFixtureFile(
|
|
root,
|
|
"node_modules/lib-esm/index.js",
|
|
`"use client"; export default () => "esm";`,
|
|
),
|
|
writeFixtureFile(
|
|
root,
|
|
"node_modules/lib-esm/index.cjs",
|
|
`"use client"; module.exports = () => "cjs";`,
|
|
),
|
|
...[
|
|
["import-cjs", `import Library from "lib-cjs";`, "lib-cjs"],
|
|
["require-cjs", `const Library = require("lib-cjs");`, "lib-cjs"],
|
|
["import-esm", `import Library from "lib-esm";`, "lib-esm"],
|
|
["require-esm", `const Library = require("lib-esm");`, "lib-esm"],
|
|
].map(([route, declaration, label]) =>
|
|
writeFixtureFile(
|
|
root,
|
|
`app/${route}/page.tsx`,
|
|
`${declaration}\nexport default function Page() { return <p data-testid="conditional-result">${label}: <Library /></p>; }`,
|
|
),
|
|
),
|
|
]);
|
|
({ server, baseUrl: devBaseUrl } = await startFixtureServer(root));
|
|
|
|
const rscBundlePath = await buildAppFixture(root);
|
|
buildOutDir = path.dirname(path.dirname(rscBundlePath));
|
|
const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
|
|
({ server: prodServer } = await startProdServer({
|
|
port: 0,
|
|
outDir: buildOutDir,
|
|
noCompression: true,
|
|
silent: true,
|
|
}));
|
|
const address = prodServer.address();
|
|
if (!address || typeof address === "string") {
|
|
throw new Error("Production server did not bind");
|
|
}
|
|
prodBaseUrl = `http://localhost:${address.port}`;
|
|
}, 120000);
|
|
|
|
afterAll(async () => {
|
|
await server?.close();
|
|
await new Promise<void>((resolve, reject) => {
|
|
if (!prodServer) return resolve();
|
|
prodServer.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
await rm(buildOutDir, { recursive: true, force: true });
|
|
await rm(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it.each(CONDITIONAL_EXPORT_CASES)(
|
|
"renders %s from the correct export condition in dev",
|
|
async (route, label, expected) => {
|
|
await expectConditionalExport(devBaseUrl, route, label, expected);
|
|
},
|
|
);
|
|
|
|
it.each(CONDITIONAL_EXPORT_CASES)(
|
|
"renders %s from the correct export condition in production",
|
|
async (route, label, expected) => {
|
|
await expectConditionalExport(prodBaseUrl, route, label, expected);
|
|
},
|
|
);
|
|
});
|