Files
cloudflare__vinext/tests/client-assets-build.test.ts
T
James Anderson 89036154cc fix(pages-router): unify client asset bootstrap (#2378)
* fix(pages-router): hydrate worker-backed dev pages

* refactor(pages-router): unify client asset bootstrap

* fix(app): preinitialize bootstrap dependencies

* test(app): distinguish preinit scripts from bootstrap

* fix(app): root-anchor bootstrap preinit modules

* test(app): cover bootstrap preinitialization in browsers

* fix(app): share client assets with nested SSR modules

* test(app): verify bootstrap preinit CSP nonces

* test(app): serve CSP nonce in worker fixture

* fix(test): read CSP script nonce property

* fix(cloudflare): emit client assets beside final workers

* fix(build): resolve pages client assets from source

* fix(app): register client assets in ssr graph

* fix(pages-router): harden client bootstrap resolution

* fix(build): emit client assets for node outputs

* fix(build): package client assets in standalone output

* test(app): reference worker nonce fixture explicitly

* fix(build): preserve client assets across environments

* refactor(build): unify client asset outputs

* fix(build): stabilize custom client asset outputs

* fix(build): finalize client assets after app builds

* fix(build): reuse client assets in hybrid builds

* fix(build): scope hybrid client assets metadata

* test(e2e): colocate Cloudflare worker fixture
2026-06-26 19:48:26 +01:00

168 lines
6.6 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { build, createBuilder } from "vite";
import { describe, expect, it } from "vite-plus/test";
import vinext from "../packages/vinext/src/index.js";
import {
clearPagesClientAssetsBuildMetadata,
setPagesClientAssetsBuildMetadata,
takePagesClientAssetsBuildMetadata,
} from "../packages/vinext/src/build/pages-client-assets-module.js";
const ROOT_NODE_MODULES = path.resolve(import.meta.dirname, "../node_modules");
async function writeFile(file: string, source: string): Promise<void> {
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, source, "utf8");
}
async function createFixture(): Promise<string> {
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "vinext-client-assets-build-"));
await fs.symlink(ROOT_NODE_MODULES, path.join(fixtureRoot, "node_modules"), "junction");
await writeFile(
path.join(fixtureRoot, "package.json"),
`${JSON.stringify({ type: "module", dependencies: {} }, null, 2)}\n`,
);
await writeFile(
path.join(fixtureRoot, "app", "layout.tsx"),
`export default function Layout({ children }: { children: React.ReactNode }) {
return <html><body>{children}</body></html>;
}
`,
);
await writeFile(
path.join(fixtureRoot, "app", "page.tsx"),
`"use client";
import { useState } from "react";
export default function Page() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
`,
);
return fixtureRoot;
}
describe("client asset sidecar builds", () => {
it("clears unconsumed hybrid metadata at the end of a build session", () => {
const buildSession = "aborted-hybrid-build";
setPagesClientAssetsBuildMetadata(buildSession, 'export default {"clientEntry":"stale.js"};\n');
clearPagesClientAssetsBuildMetadata(buildSession);
expect(takePagesClientAssetsBuildMetadata(buildSession)).toBeNull();
});
it("keeps App Router metadata at the stable root of nested custom server outputs", async () => {
const fixtureRoot = await createFixture();
const outRoot = await fs.mkdtemp(path.join(os.tmpdir(), "vinext-client-assets-out-"));
try {
const serverRoot = path.join(outRoot, "custom", "server");
const rscOutDir = path.join(serverRoot, "rsc");
const ssrOutDir = serverRoot;
const clientOutDir = path.join(outRoot, "custom", "client");
const builder = await createBuilder({
root: fixtureRoot,
configFile: false,
logLevel: "silent",
plugins: [vinext({ appDir: fixtureRoot, rscOutDir, ssrOutDir, clientOutDir })],
});
await builder.buildApp();
const rscSidecar = await fs.readFile(path.join(rscOutDir, "vinext-client-assets.js"), "utf8");
const ssrSidecar = await fs.readFile(path.join(ssrOutDir, "vinext-client-assets.js"), "utf8");
expect(rscSidecar).toContain('"appBootstrapPreinitModules"');
expect(rscSidecar).not.toBe("export default {};\n");
expect(ssrSidecar).toBe(rscSidecar);
const rscEntry = await fs.readFile(path.join(rscOutDir, "index.js"), "utf8");
expect(rscEntry).toContain("./vinext-client-assets.js");
} finally {
await fs.rm(fixtureRoot, { recursive: true, force: true });
await fs.rm(outRoot, { recursive: true, force: true });
}
});
it("keeps disjoint App Router outputs self-contained", async () => {
const fixtureRoot = await createFixture();
const outRoot = await fs.mkdtemp(path.join(os.tmpdir(), "vinext-client-assets-disjoint-"));
try {
const rscOutDir = path.join(outRoot, "rsc-output");
const ssrOutDir = path.join(outRoot, "ssr-output");
const clientOutDir = path.join(outRoot, "client-output");
const builder = await createBuilder({
root: fixtureRoot,
configFile: false,
logLevel: "silent",
plugins: [vinext({ appDir: fixtureRoot, rscOutDir, ssrOutDir, clientOutDir })],
});
await builder.buildApp();
const rscSidecar = await fs.readFile(path.join(rscOutDir, "vinext-client-assets.js"), "utf8");
const ssrSidecar = await fs.readFile(path.join(ssrOutDir, "vinext-client-assets.js"), "utf8");
expect(rscSidecar).toContain('"appBootstrapPreinitModules"');
expect(ssrSidecar).toBe(rscSidecar);
await expect(fs.access(path.join(outRoot, "vinext-client-assets.js"))).rejects.toThrow();
const rscEntry = await fs.readFile(path.join(rscOutDir, "index.js"), "utf8");
expect(rscEntry).toContain("./vinext-client-assets.js");
} finally {
await fs.rm(fixtureRoot, { recursive: true, force: true });
await fs.rm(outRoot, { recursive: true, force: true });
}
});
it("reuses client metadata for the separate Pages server build in hybrid apps", async () => {
const fixtureRoot = await createFixture();
const clientOutDir = path.join(fixtureRoot, "custom-client-output");
const buildSession = `test-${path.basename(fixtureRoot)}`;
const previousBuildSession = process.env.__VINEXT_PAGES_CLIENT_ASSETS_BUILD_SESSION;
process.env.__VINEXT_PAGES_CLIENT_ASSETS_BUILD_SESSION = buildSession;
try {
await writeFile(
path.join(fixtureRoot, "pages", "legacy.tsx"),
`export default function Page() { return <p>legacy page</p>; }\n`,
);
const builder = await createBuilder({
root: fixtureRoot,
configFile: false,
logLevel: "silent",
plugins: [vinext({ appDir: fixtureRoot, clientOutDir })],
});
await builder.buildApp();
await build({
root: fixtureRoot,
configFile: false,
logLevel: "silent",
plugins: [vinext({ disableAppRouter: true })],
build: {
outDir: "dist/server",
emptyOutDir: false,
ssr: "virtual:vinext-server-entry",
rolldownOptions: { output: { entryFileNames: "entry.js" } },
},
});
const sidecar = await fs.readFile(
path.join(fixtureRoot, "dist", "vinext-client-assets.js"),
"utf8",
);
expect(sidecar).toContain('"clientEntry"');
expect(sidecar).not.toBe("export default {};\n");
const pagesEntry = await fs.readFile(
path.join(fixtureRoot, "dist", "server", "entry.js"),
"utf8",
);
expect(pagesEntry).toContain("../vinext-client-assets.js");
} finally {
if (previousBuildSession === undefined) {
delete process.env.__VINEXT_PAGES_CLIENT_ASSETS_BUILD_SESSION;
} else {
process.env.__VINEXT_PAGES_CLIENT_ASSETS_BUILD_SESSION = previousBuildSession;
}
await fs.rm(fixtureRoot, { recursive: true, force: true });
}
});
});