1import * as NodeServices from "@effect/platform-node/NodeServices";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";
5import type * as Scope from "effect/Scope";
6import { describe, expect, it } from "vitest";
7import {
8 ADAPTER_NAME,
9 ADAPTER_PACKAGE,
10 makeCloudflareTarget,
11} from "../cloudflare.ts";
12import { fromHarnessOptions } from "../index.ts";
13import { readOctaneOutput } from "../Octane.ts";
14
15const runWithNode = <A, E>(
16 effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | Scope.Scope>,
17): Promise<A> =>
18 Effect.runPromise(
19 Effect.scoped(effect).pipe(
20 Effect.provide(NodeServices.layer),
21 ) as Effect.Effect<A, E>,
22 );
23
24describe("readOctaneOutput", () => {
25 it("maps an octane dist tree onto the BuildOutput contract, entry first", async () => {
26 const output = await runWithNode(
27 Effect.gen(function* () {
28 const fs = yield* FileSystem.FileSystem;
29 const path = yield* Path.Path;
30 const dir = yield* fs.makeTempDirectoryScoped({
31 prefix: "octane-output-",
32 });
33 const serverDir = path.join(dir, "server");
34 const clientDir = path.join(dir, "client");
35 yield* fs.makeDirectory(path.join(serverDir, "chunks"), {
36 recursive: true,
37 });
38 yield* fs.makeDirectory(clientDir, { recursive: true });
39 // alphabetically before worker.js, to prove entry-first sorting
40 yield* fs.writeFileString(
41 path.join(serverDir, "chunks", "a.js"),
42 "export const a = 1;",
43 );
44 yield* fs.writeFileString(
45 path.join(serverDir, "entry.js"),
46 "export const e = 1;",
47 );
48 yield* fs.writeFileString(
49 path.join(serverDir, "worker.js"),
50 "export default {};",
51 );
52 // adapt() inputs embedded into worker.js — must NOT surface as modules
53 yield* fs.writeFileString(
54 path.join(serverDir, "index.html"),
55 "<!doctype html>",
56 );
57 yield* fs.writeFileString(
58 path.join(serverDir, "octane-client-assets.json"),
59 "{}",
60 );
61 yield* fs.writeFileString(
62 path.join(clientDir, "robots.txt"),
63 "User-agent: *\n",
64 );
65 return yield* readOctaneOutput({ dir, serverDir, clientDir });
66 }),
67 );
68 expect(output.serverModules?.map((module) => module.name)).toEqual([
69 "server/worker.js",
70 "server/chunks/a.js",
71 "server/entry.js",
72 ]);
73 expect(output.clientDirectory?.endsWith("client")).toBe(true);
74 expect(output.externalWorkspaces.size).toBe(0);
75 });
76
77 it("fails actionably when the adapter entry is missing", async () => {
78 const error = await runWithNode(
79 Effect.gen(function* () {
80 const fs = yield* FileSystem.FileSystem;
81 const path = yield* Path.Path;
82 const dir = yield* fs.makeTempDirectoryScoped({
83 prefix: "octane-output-",
84 });
85 const serverDir = path.join(dir, "server");
86 const clientDir = path.join(dir, "client");
87 yield* fs.makeDirectory(serverDir, { recursive: true });
88 yield* fs.makeDirectory(clientDir, { recursive: true });
89 yield* fs.writeFileString(
90 path.join(serverDir, "entry.js"),
91 "export const e = 1;",
92 );
93 return yield* readOctaneOutput({ dir, serverDir, clientDir }).pipe(
94 Effect.flip,
95 );
96 }),
97 );
98 expect(error.message).toContain('no "server/worker.js" entry');
99 expect(error.message).toContain("adapt()");
100 });
101});
102
103describe("makeCloudflareTarget", () => {
104 it("owns the Cloudflare build and accepts the optional legacy adapter", () => {
105 const target = makeCloudflareTarget({ compatibilityDate: "2026-03-10" });
106 expect(target.platform).toBe("cloudflare");
107 expect(target.requiresAdapter).toBeUndefined();
108 expect(target.build).toBeTypeOf("function");
109 expect(target.adapterName).toBe(ADAPTER_NAME);
110 expect(target.adapterPackage).toBe(ADAPTER_PACKAGE);
111 expect(target.serverEntryFileName).toBe("worker.js");
112 expect(target.config.compatibilityDate).toBe("2026-03-10");
113 });
114});
115
116describe("fromHarnessOptions", () => {
117 it("prefers the target-scoped carriage over the deprecated vite alias", () => {
118 const options = fromHarnessOptions({
119 target: {
120 cloudflare: {
121 worker: {
122 compatibilityDate: "2026-03-10",
123 compatibilityFlags: ["nodejs_compat"],
124 },
125 },
126 },
127 vite: { compatibilityDate: "2020-01-01" },
128 });
129 expect(options.compatibilityDate).toBe("2026-03-10");
130 expect(options.compatibilityFlags).toEqual(["nodejs_compat"]);
131 });
132});