1import { WORKER_ENTRY_PREFIX } from "@alchemy.run/cloudflare-runtime/rolldown/plugins";
2import * as NodeFsPromises from "node:fs/promises";
3import * as NodeOs from "node:os";
4import * as NodePath from "node:path";
5import type * as ViteModule from "vite";
6import { afterEach, describe, expect, it } from "vitest";
7import {
8 makeIntegrationPluginOptions,
9 SERVER_ENTRYPOINT,
10} from "../cloudflare.ts";
11import { createWorkerdPrerenderEnvironmentPlugin } from "../prerender-environment.ts";
12import { collectOutputModules } from "../prerenderer.ts";
13
14describe("makeIntegrationPluginOptions (workerd prerendering)", () => {
15 it("promotes prerender to a child worker environment during a workerd build", () => {
16 const options = makeIntegrationPluginOptions(undefined, {
17 prerenderEnvironment: "workerd",
18 command: "build",
19 });
20 expect(options.viteEnvironments).toEqual({
21 entry: "ssr",
22 children: ["prerender"],
23 });
24 expect(options.skipEnvironments).toEqual(["astro"]);
25 });
26
27 it("filters a user-supplied prerender skip during a workerd build", () => {
28 const options = makeIntegrationPluginOptions(
29 { skipEnvironments: ["custom", "prerender"] },
30 { prerenderEnvironment: "workerd", command: "build" },
31 );
32 expect(options.viteEnvironments).toEqual({
33 entry: "ssr",
34 children: ["prerender"],
35 });
36 expect(options.skipEnvironments).toEqual(["astro", "custom"]);
37 });
38
39 it("keeps prerender skipped in dev and in node mode", () => {
40 for (const args of [
41 { prerenderEnvironment: "workerd", command: "dev" },
42 { prerenderEnvironment: "workerd", command: "sync" },
43 { prerenderEnvironment: "node", command: "build" },
44 ] as const) {
45 const options = makeIntegrationPluginOptions(undefined, args);
46 expect(options.viteEnvironments).toEqual({ entry: "ssr" });
47 expect(options.skipEnvironments).toEqual(["astro", "prerender"]);
48 }
49 });
50});
51
52describe("createWorkerdPrerenderEnvironmentPlugin", () => {
53 const configEnvironment = (
54 plugin: ViteModule.Plugin,
55 name: string,
56 ): unknown => {
57 const hook = plugin.configEnvironment;
58 const handler = typeof hook === "function" ? hook : hook?.handler;
59 if (!handler) throw new Error("configEnvironment hook missing");
60 return handler.call(undefined as never, name, {} as never, {} as never);
61 };
62
63 it("assigns the worker-wrapped server entrypoint as the prerender build input", () => {
64 const plugin = createWorkerdPrerenderEnvironmentPlugin(SERVER_ENTRYPOINT);
65 expect(plugin.apply).toBe("build");
66 expect(configEnvironment(plugin, "prerender")).toEqual({
67 build: {
68 rolldownOptions: {
69 input: `${WORKER_ENTRY_PREFIX}${SERVER_ENTRYPOINT}`,
70 },
71 },
72 });
73 });
74
75 it("leaves every other environment untouched", () => {
76 const plugin = createWorkerdPrerenderEnvironmentPlugin(SERVER_ENTRYPOINT);
77 for (const name of ["ssr", "client", "astro"]) {
78 expect(configEnvironment(plugin, name)).toBeUndefined();
79 }
80 });
81});
82
83describe("collectOutputModules", () => {
84 let dir: string | undefined;
85
86 const makeOutputDir = async (
87 files: Record<string, string | Uint8Array>,
88 ): Promise<string> => {
89 dir = await NodeFsPromises.mkdtemp(
90 NodePath.join(NodeOs.tmpdir(), "astro-prerender-test-"),
91 );
92 for (const [relative, content] of Object.entries(files)) {
93 const absolute = NodePath.join(dir, relative);
94 await NodeFsPromises.mkdir(NodePath.dirname(absolute), {
95 recursive: true,
96 });
97 await NodeFsPromises.writeFile(absolute, content);
98 }
99 return dir;
100 };
101
102 afterEach(async () => {
103 if (dir !== undefined) {
104 await NodeFsPromises.rm(dir, { recursive: true, force: true });
105 dir = undefined;
106 }
107 });
108
109 it("loads the output with the prerender entry chunk first", async () => {
110 const output = await makeOutputDir({
111 "prerender-entry.C3POR2D2.mjs": "export default {};",
112 "chunks/page_abc.mjs": "export const page = 1;",
113 "chunks/zzz_last.mjs": "export const z = 1;",
114 "chunks/page_abc.mjs.map": "{}",
115 "_astro/styles.css": "body {}",
116 "manifest.json": "{}",
117 "data.bin": new Uint8Array([1, 2, 3]),
118 });
119 const modules = await collectOutputModules(output);
120 expect(modules[0]).toMatchObject({
121 name: "prerender-entry.C3POR2D2.mjs",
122 type: "ESModule",
123 });
124 const byName = new Map(modules.map((module) => [module.name, module]));
125 expect(byName.get("chunks/page_abc.mjs")).toMatchObject({
126 type: "ESModule",
127 });
128 expect(byName.get("_astro/styles.css")).toMatchObject({ type: "Text" });
129 expect(byName.get("manifest.json")).toMatchObject({ type: "Json" });
130 expect(byName.get("data.bin")).toMatchObject({ type: "Data" });
131 // Sourcemaps are never shipped to workerd.
132 expect(byName.has("chunks/page_abc.mjs.map")).toBe(false);
133 // Deterministic ordering after the entry.
134 expect(modules.map((module) => module.name).slice(1)).toEqual(
135 [...byName.keys()]
136 .filter((name) => !name.startsWith("prerender-entry."))
137 .sort(),
138 );
139 });
140
141 it("fails with a clear error when the entry chunk is missing", async () => {
142 const output = await makeOutputDir({
143 "chunks/page_abc.mjs": "export const page = 1;",
144 });
145 await expect(collectOutputModules(output)).rejects.toThrow(
146 /prerender-entry/,
147 );
148 });
149
150 it("fails with a clear error when the output directory does not exist", async () => {
151 await expect(
152 collectOutputModules("/nonexistent/prerender-output"),
153 ).rejects.toThrow(/prerender build output/);
154 });
155});