1import { isDeployTarget } from "../../core/index.ts";
2import * as Effect from "effect/Effect";
3import * as NodePath from "node:path";
4import type * as ViteModule from "vite";
5import { describe, expect, it } from "vitest";
6import cloudflareTargetFactory, {
7 makeWakuCloudflareTarget,
8 makeWakuPluginOptions,
9} from "../cloudflare.ts";
10import { WAKU_SERVER_ENTRY_PATH } from "../index.ts";
11
12const WAKU_DIR = "/project/node_modules/waku";
13
14const context = {
15 root: "/project",
16 wakuDirectory: WAKU_DIR,
17 phase: "build",
18} as const;
19
20const flatten = (
21 plugins: ReadonlyArray<ViteModule.PluginOption>,
22): Array<ViteModule.Plugin> =>
23 (plugins as Array<unknown>)
24 .flat(8)
25 .filter(
26 (plugin): plugin is ViteModule.Plugin =>
27 typeof plugin === "object" && plugin !== null && "name" in plugin,
28 );
29
30describe("makeWakuPluginOptions", () => {
31 it("defaults main to waku's rsc worker entry and pins the rsc/ssr topology", () => {
32 const options = makeWakuPluginOptions({
33 root: "/project",
34 wakuDirectory: WAKU_DIR,
35 });
36 expect(options.main).toBe(NodePath.join(WAKU_DIR, WAKU_SERVER_ENTRY_PATH));
37 expect(options.viteEnvironments).toEqual({
38 entry: "rsc",
39 children: ["ssr"],
40 });
41 });
42
43 it("preserves user cloudflare options but overrides viteEnvironments", () => {
44 const options = makeWakuPluginOptions({
45 root: "/project",
46 wakuDirectory: WAKU_DIR,
47 pluginOptions: {
48 compatibilityDate: "2026-03-10",
49 compatibilityFlags: ["nodejs_als"],
50 worker: { name: "fixtures-waku", bindings: [] },
51 viteEnvironments: { entry: "ssr" },
52 },
53 });
54 expect(options.compatibilityDate).toBe("2026-03-10");
55 expect(options.compatibilityFlags).toEqual(["nodejs_als"]);
56 expect(options.worker?.name).toBe("fixtures-waku");
57 // The environment topology always wins: the worker must run in the rsc
58 // environment.
59 expect(options.main).toBe(NodePath.join(WAKU_DIR, WAKU_SERVER_ENTRY_PATH));
60 expect(options.viteEnvironments).toEqual({
61 entry: "rsc",
62 children: ["ssr"],
63 });
64 });
65
66 it("lets a user main (the user-entry seam) take precedence over waku's entry", () => {
67 const absolute = makeWakuPluginOptions({
68 root: "/project",
69 wakuDirectory: WAKU_DIR,
70 pluginOptions: { main: "/project/src/worker-entry.ts" },
71 });
72 expect(absolute.main).toBe("/project/src/worker-entry.ts");
73
74 // A relative main resolves against the project root, not process.cwd().
75 const relative = makeWakuPluginOptions({
76 root: "/project",
77 wakuDirectory: WAKU_DIR,
78 pluginOptions: { main: "./src/worker-entry.ts" },
79 });
80 expect(relative.main).toBe(
81 NodePath.resolve("/project", "src/worker-entry.ts"),
82 );
83 // The topology stays pinned even with a user main.
84 expect(relative.viteEnvironments).toEqual({
85 entry: "rsc",
86 children: ["ssr"],
87 });
88 });
89
90 it("defaults compatibilityFlags to nodejs_als (waku needs AsyncLocalStorage)", () => {
91 expect(
92 makeWakuPluginOptions({ root: "/project", wakuDirectory: WAKU_DIR })
93 .compatibilityFlags,
94 ).toEqual(["nodejs_als"]);
95 expect(
96 makeWakuPluginOptions({
97 root: "/project",
98 wakuDirectory: WAKU_DIR,
99 pluginOptions: { compatibilityFlags: [] },
100 }).compatibilityFlags,
101 ).toEqual(["nodejs_als"]);
102 expect(
103 makeWakuPluginOptions({
104 root: "/project",
105 wakuDirectory: WAKU_DIR,
106 pluginOptions: { compatibilityFlags: ["global_fetch_strictly_public"] },
107 }).compatibilityFlags,
108 ).toEqual(["global_fetch_strictly_public", "nodejs_als"]);
109 });
110
111 it("keeps user compatibilityFlags that already provide AsyncLocalStorage", () => {
112 expect(
113 makeWakuPluginOptions({
114 root: "/project",
115 wakuDirectory: WAKU_DIR,
116 pluginOptions: { compatibilityFlags: ["nodejs_als"] },
117 }).compatibilityFlags,
118 ).toEqual(["nodejs_als"]);
119 expect(
120 makeWakuPluginOptions({
121 root: "/project",
122 wakuDirectory: WAKU_DIR,
123 pluginOptions: { compatibilityFlags: ["nodejs_compat"] },
124 }).compatibilityFlags,
125 ).toEqual(["nodejs_compat"]);
126 expect(
127 makeWakuPluginOptions({
128 root: "/project",
129 wakuDirectory: WAKU_DIR,
130 pluginOptions: { compatibilityFlags: ["nodejs_compat_v2"] },
131 }).compatibilityFlags,
132 ).toEqual(["nodejs_compat_v2"]);
133 });
134});
135
136describe("makeWakuCloudflareTarget", () => {
137 it("default-exports the target factory", () => {
138 expect(cloudflareTargetFactory).toBe(makeWakuCloudflareTarget);
139 expect(isDeployTarget(cloudflareTargetFactory({}))).toBe(true);
140 });
141
142 it("is a cloudflare DeployTarget carrying the config opaquely", () => {
143 const config = { compatibilityDate: "2026-03-10" };
144 const target = makeWakuCloudflareTarget(config);
145 expect(target.platform).toBe("cloudflare");
146 expect(target.config).toBe(config);
147 expect(isDeployTarget(target)).toBe(true);
148 // `config` must be an own property even when undefined — `isDeployTarget`
149 // checks for its presence.
150 expect(isDeployTarget(makeWakuCloudflareTarget())).toBe(true);
151 });
152
153 it("declares the workerd bundle conditions and cloudflare: externals", () => {
154 const target = makeWakuCloudflareTarget();
155 expect(target.bundle?.conditions).toEqual([
156 "workerd",
157 "worker",
158 "module",
159 "browser",
160 ]);
161 expect(target.bundle?.external).toEqual(["cloudflare:"]);
162 });
163
164 it("does not take over the framework build (no wholesale build hook)", () => {
165 const target = makeWakuCloudflareTarget();
166 expect(target.build).toBeUndefined();
167 expect(target.finish).toBeUndefined();
168 });
169
170 it("resolves the adapter hook to this package's wrangler-free fork", () => {
171 const target = makeWakuCloudflareTarget();
172 const adapterPath = Effect.runSync(target.adapter(context));
173 expect(NodePath.isAbsolute(adapterPath)).toBe(true);
174 expect(adapterPath).toMatch(/adapter\.(ts|js)$/);
175 });
176
177 it("produces the cloudflare vite plugin with the pinned waku options", () => {
178 const target = makeWakuCloudflareTarget({
179 compatibilityDate: "2026-03-10",
180 });
181 const plugins = flatten(Effect.runSync(target.vitePlugins(context)));
182 expect(plugins.length).toBeGreaterThan(0);
183 expect(
184 plugins.some((plugin) => plugin.name.startsWith("distilled-cloudflare")),
185 ).toBe(true);
186 });
187
188 it("surfaces the config's main as the generic user-entry carriage", () => {
189 expect(makeWakuCloudflareTarget().entry).toBeUndefined();
190 expect(
191 makeWakuCloudflareTarget({ compatibilityDate: "2026-03-10" }).entry,
192 ).toBeUndefined();
193 expect(
194 makeWakuCloudflareTarget({ main: "./src/worker-entry.ts" }).entry,
195 ).toEqual({
196 main: "./src/worker-entry.ts",
197 });
198 });
199});