1import {
2 Artifacts,
3 createArtifactStore,
4 makeScopedArtifacts,
5} from "@/Artifacts.ts";
6import {
7 makeSourceContext,
8 resolveSource,
9 SourceProviderError,
10 type SourceContext,
11} from "@/Cloudflare/Workers/Source.ts";
12import type { WorkerProps } from "@/Cloudflare/Workers/Worker.ts";
13import { NodeServices } from "@effect/platform-node";
14import { describe, expect, it } from "alchemy-test";
15import * as Effect from "effect/Effect";
16import * as crypto from "node:crypto";
17
18const providerModule = new URL(
19 "./fixtures/source-provider/provider.ts",
20 import.meta.url,
21).href;
22const invalidModule = new URL(
23 "./fixtures/source-provider/invalid.ts",
24 import.meta.url,
25).href;
26
27const ctx = (props: WorkerProps): SourceContext =>
28 makeSourceContext({
29 id: "TestWorker",
30 fqn: "TestWorker",
31 workerName: "stack-testworker-dev-abc123",
32 props,
33 compatibility: { date: "2024-01-01", flags: [] },
34 stack: { name: "stack", stage: "dev" },
35 });
36
37const provide = <A, E>(
38 effect: Effect.Effect<
39 A,
40 E,
41 Effect.Services<ReturnType<typeof resolveSource>> | Artifacts
42 >,
43) =>
44 effect.pipe(
45 Effect.provideService(
46 Artifacts,
47 makeScopedArtifacts(createArtifactStore(), "test"),
48 ),
49 Effect.provide(NodeServices.layer),
50 Effect.scoped,
51 );
52
53describe(
54 "resolveSource",
55 {
56 tags: [
57 "unit",
58 "provider:cloudflare",
59 "provider:cloudflare:worker",
60 "local",
61 ],
62 },
63 () => {
64 it.effect("maps props.script to the inline-script source", () =>
65 provide(
66 Effect.gen(function* () {
67 const script = "export default { fetch: () => new Response('hi') };";
68 const props: WorkerProps = { script };
69 const source = yield* resolveSource(props);
70 expect(source.ownsAssets).toBe(false);
71 const slots = yield* source.hash(ctx(props), undefined);
72 expect(slots.bundle).toBe(
73 crypto.createHash("sha256").update(script).digest("hex"),
74 );
75 const out = yield* source.build(ctx(props));
76 expect(out.bundle?.files[0].path).toBe("main.js");
77 expect(out.bundle?.hash).toBe(slots.bundle);
78 expect(out.assets).toBeUndefined();
79 }),
80 ),
81 );
82
83 it.effect("maps props.vite to the asset-owning vite source", () =>
84 provide(
85 Effect.gen(function* () {
86 const source = yield* resolveSource({ vite: { rootDir: "." } });
87 expect(source.ownsAssets).toBe(true);
88 }),
89 ),
90 );
91
92 it.effect("loads an external provider from a source descriptor", () =>
93 provide(
94 Effect.gen(function* () {
95 const props: WorkerProps = {
96 source: {
97 provider: providerModule,
98 devMode: "bundle",
99 options: { marker: "abc-123" },
100 },
101 };
102 const source = yield* resolveSource(props);
103 const out = yield* source.build(ctx(props));
104 expect(String(out.bundle?.files[0].content)).toContain("abc-123");
105 const slots = yield* source.hash(ctx(props), undefined);
106 expect(slots.bundle).toBe(out.bundle?.hash);
107 }),
108 ),
109 );
110
111 it.effect(
112 "fails with SourceProviderError naming the package when the module cannot be imported",
113 () =>
114 provide(
115 Effect.gen(function* () {
116 const result = yield* Effect.result(
117 resolveSource({
118 source: {
119 provider: "@alchemy.run/does-not-exist-fixture",
120 devMode: "bundle",
121 },
122 }),
123 );
124 expect(result._tag).toBe("Failure");
125 if (result._tag === "Failure") {
126 expect(result.failure).toBeInstanceOf(SourceProviderError);
127 expect((result.failure as SourceProviderError).message).toContain(
128 "@alchemy.run/does-not-exist-fixture",
129 );
130 }
131 }),
132 ),
133 );
134
135 it.effect(
136 "fails with SourceProviderError when the module's default export lacks make()",
137 () =>
138 provide(
139 Effect.gen(function* () {
140 const result = yield* Effect.result(
141 resolveSource({
142 source: { provider: invalidModule, devMode: "bundle" },
143 }),
144 );
145 expect(result._tag).toBe("Failure");
146 if (result._tag === "Failure") {
147 expect(result.failure).toBeInstanceOf(SourceProviderError);
148 expect((result.failure as SourceProviderError).message).toContain(
149 "WorkerSourceModule",
150 );
151 }
152 }),
153 ),
154 );
155
156 it.effect("rejects source combined with main/script/vite", () =>
157 provide(
158 Effect.gen(function* () {
159 const result = yield* Effect.result(
160 resolveSource({
161 source: { provider: providerModule, devMode: "bundle" },
162 main: "./worker.ts",
163 }),
164 );
165 expect(result._tag).toBe("Failure");
166 if (result._tag === "Failure") {
167 expect(result.failure).toBeInstanceOf(SourceProviderError);
168 expect((result.failure as SourceProviderError).message).toContain(
169 '"main"',
170 );
171 }
172 }),
173 ),
174 );
175 },
176);