Open History to see the last commit
1import { gunzipSync } from "node:zlib";
2import { readPythonWorkerBundle } from "@alchemy.run/cloudflare/Workers/Sources/Python";
3import { WorkerBundle } from "@alchemy.run/cloudflare/Workers/Sources/Rolldown";
4import { watchBundleDirectory } from "@alchemy.run/cloudflare/Workers/Sources/shared";
5import nextjsSource from "@alchemy.run/frontend-frameworks/nextjs/source";
6import { createComputeArchive } from "@alchemy.run/prisma/ComputeArchive";
7import { expect, layer } from "@alchemy.run/test";
8import { AlchemyContext, dotAlchemyDirectory } from "alchemy/AlchemyContext";
9import { createTempBundleDir, getStableContextDir } from "alchemy/Bundle/TempRoot";
10import { hashDirectory } from "alchemy/Command/Memo";
11import { ResourceContext } from "alchemy/ResourceContext";
12import { Stack } from "alchemy/Stack";
13import { Stage } from "alchemy/Stage";
14import { localState } from "alchemy/State/LocalState";
15import { State } from "alchemy/State/State";
16import { copyTree, hashExtraFiles } from "alchemy/Util/extraFiles";
17import { isPathWithin } from "alchemy/Util/isPathWithin";
18import { PlatformServices } from "alchemy/Util/PlatformServices";
19import { sha256 } from "alchemy/Util/sha256";
20import * as Effect from "effect/Effect";
21import * as FileSystem from "effect/FileSystem";
22import * as Layer from "effect/Layer";
23import * as Path from "effect/Path";
24import * as Stream from "effect/Stream";
25import { resourceContext } from "./Util/ResourceContext.ts";
26
27/**
28 * Runs a Worker build the way the engine runs a provider lifecycle call: with
29 * the resource's context, its stack, and (when given) the configured
30 * `.alchemy` directory. Without `dotAlchemy`, the default relative `.alchemy`
31 * applies.
32 */
33const inWorkerLifecycle =
34 (logicalId: string, dotAlchemy?: string) =>
35 <A, E, R>(effect: Effect.Effect<A, E, R>) =>
36 effect.pipe(
37 Effect.provide([
38 Layer.succeed(ResourceContext, resourceContext("0".repeat(32), logicalId)),
39 Layer.succeed(Stack, {
40 name: "test",
41 stage: "test",
42 resources: {},
43 bindings: {},
44 actions: {},
45 }),
46 dotAlchemy === undefined
47 ? Layer.empty
48 : Layer.succeed(AlchemyContext, { dotAlchemy, dev: false, adopt: false }),
49 ]),
50 );
51
52layer(PlatformServices)("runtime directory", (it) => {
53 it.effect(
54 "compares paths against the supplied base without using the process cwd",
55 () =>
56 Effect.sync(() => {
57 expect(
58 isPathWithin(".alchemy", "/workspace/app/.alchemy/bundles/worker.js", "/workspace/app"),
59 ).toBe(true);
60 expect(
61 isPathWithin(".alchemy", "/workspace/app/.alchemy/bundles/worker.js", "/workspace/other"),
62 ).toBe(false);
63 expect(isPathWithin(".alchemy", ".alchemy", "/workspace/app")).toBe(true);
64 expect(isPathWithin(".alchemy", ".alchemy-backup/worker.js", "/workspace/app")).toBe(false);
65 expect(isPathWithin(".alchemy", ".alchemy/../source.js", "/workspace/app")).toBe(false);
66 expect(isPathWithin("/runtime", "/runtime/worker.js", "/workspace/app")).toBe(true);
67 }),
68 { tags: ["unit", "local"] },
69 );
70
71 it.effect(
72 "preserves the relative fallback and configured roots",
73 () =>
74 Effect.gen(function* () {
75 expect(yield* dotAlchemyDirectory).toBe(".alchemy");
76 expect(
77 yield* dotAlchemyDirectory.pipe(
78 Effect.provideService(AlchemyContext, {
79 dotAlchemy: "node_modules/.cache/runtime",
80 dev: false,
81 adopt: false,
82 }),
83 ),
84 ).toBe("node_modules/.cache/runtime");
85 expect(isPathWithin("/tmp/runtime", "/tmp/runtime-sibling/file", process.cwd())).toBe(
86 false,
87 );
88 expect(isPathWithin("/tmp/runtime", "/tmp/runtime/file", process.cwd())).toBe(true);
89 }),
90 { tags: ["unit", "local"] },
91 );
92
93 it.effect(
94 "ignores runtime watch events using the caller base rather than the watched directory",
95 () =>
96 Effect.gen(function* () {
97 const fs = yield* FileSystem.FileSystem;
98 const path = yield* Path.Path;
99 const root = yield* fs.makeTempDirectoryScoped();
100 const runtime = path.join(root, "runtime");
101 const main = path.join(root, "worker.py");
102 yield* fs.writeFileString(main, "class Default: pass");
103 for (const [eventPath, expected] of [
104 ["runtime/state.json", ["Success"]],
105 ["worker.py", ["Success", "Start", "Success"]],
106 ] as const) {
107 const events = yield* watchBundleDirectory({
108 main,
109 read: Effect.succeed({
110 files: [{ path: "worker.py", content: "class Default: pass", hash: "test" }],
111 hash: "test",
112 }),
113 }).pipe(
114 Stream.runCollect,
115 Effect.provideService(FileSystem.FileSystem, {
116 ...fs,
117 watch: () => Stream.make({ _tag: "Update" as const, path: eventPath }),
118 }),
119 Effect.provideService(AlchemyContext, {
120 dotAlchemy: path.relative(process.cwd(), runtime),
121 dev: false,
122 adopt: false,
123 }),
124 );
125 expect(events.map((event) => event._tag)).toEqual(expected);
126 }
127 }),
128 { tags: ["unit", "provider:cloudflare", "provider:cloudflare:worker", "local"] },
129 );
130
131 it.effect(
132 "writes Worker bundles under the context and honors an explicit output directory",
133 () =>
134 Effect.gen(function* () {
135 const fs = yield* FileSystem.FileSystem;
136 const path = yield* Path.Path;
137 const root = yield* fs.makeTempDirectoryScoped();
138 const runtime = path.join(root, "node_modules/.cache/runtime");
139 const main = path.join(root, "worker.mjs");
140 yield* fs.writeFileString(path.join(root, "package.json"), "{}");
141 yield* fs.writeFileString(main, 'export default { fetch: () => new Response("ok") };');
142 const bundler = yield* WorkerBundle;
143 for (const override of [undefined, path.join(root, "explicit")]) {
144 const bundle = yield* bundler
145 .build({
146 id: "test",
147 main,
148 compatibility: { date: "2026-03-17", flags: [] },
149 entry: { kind: "external" },
150 stack: { name: "test", stage: "test" },
151 extraOptions: override ? { output: { dir: override } } : undefined,
152 })
153 .pipe(inWorkerLifecycle("test", runtime));
154 const directory = override ?? path.join(runtime, "bundles/test-test-test");
155 expect(bundle.files.length).toBeGreaterThan(0);
156 expect((yield* fs.readDirectory(directory)).length).toBeGreaterThan(0);
157 }
158 expect(yield* fs.exists(path.join(root, ".alchemy"))).toBe(false);
159 }),
160 { tags: ["unit", "provider:cloudflare", "provider:cloudflare:worker", "local"] },
161 );
162
163 it.effect(
164 "keeps standalone and configured relative Worker output paths relative to the bundle cwd",
165 () =>
166 Effect.gen(function* () {
167 const fs = yield* FileSystem.FileSystem;
168 const path = yield* Path.Path;
169 const root = yield* fs.makeTempDirectoryScoped();
170 const main = path.join(root, "worker.mjs");
171 yield* fs.writeFileString(path.join(root, "package.json"), "{}");
172 yield* fs.writeFileString(main, 'export default { fetch: () => new Response("ok") };');
173 for (const configured of [undefined, "custom/runtime"]) {
174 const bundler = yield* WorkerBundle;
175 yield* bundler
176 .build({
177 id: "relative",
178 main,
179 compatibility: { date: "2026-03-17", flags: [] },
180 entry: { kind: "external" },
181 stack: { name: "test", stage: "test" },
182 extraOptions: undefined,
183 })
184 .pipe(inWorkerLifecycle("relative", configured));
185 expect(
186 (yield* fs.readDirectory(
187 path.join(root, configured ?? ".alchemy", "bundles/test-test-relative"),
188 )).length,
189 ).toBeGreaterThan(0);
190 }
191 }),
192 { tags: ["unit", "provider:cloudflare", "provider:cloudflare:worker", "local"] },
193 );
194
195 it.effect(
196 "keeps temporary and stable container contexts under the full configured path",
197 () =>
198 Effect.gen(function* () {
199 const fs = yield* FileSystem.FileSystem;
200 const path = yield* Path.Path;
201 const root = yield* fs.makeTempDirectoryScoped();
202 const runtime = path.join(root, "node_modules/.cache/runtime");
203 yield* Effect.gen(function* () {
204 const stable = yield* getStableContextDir(path.join(root, "entry.ts"), runtime, "image");
205 const temporary = yield* createTempBundleDir(
206 path.join(root, "entry.ts"),
207 runtime,
208 "image",
209 );
210 expect(stable).toBe(path.join(runtime, "tmp/test-dev-image"));
211 const relative = path.relative(process.cwd(), runtime);
212 expect(yield* getStableContextDir(path.join(root, "entry.ts"), relative, "image")).toBe(
213 path.join(relative, "tmp/test-dev-image"),
214 );
215 expect(isPathWithin(runtime, temporary, process.cwd())).toBe(true);
216 expect(yield* fs.exists(temporary)).toBe(true);
217 }).pipe(
218 Effect.provideService(Stack, {
219 name: "test",
220 stage: "dev",
221 resources: {},
222 bindings: {},
223 actions: {},
224 }),
225 Effect.provideService(Stage, "dev"),
226 );
227 }),
228 { tags: ["unit", "local"] },
229 );
230
231 it.effect(
232 "captures the configured root for a lazily initialized local state store",
233 () =>
234 Effect.gen(function* () {
235 const fs = yield* FileSystem.FileSystem;
236 const path = yield* Path.Path;
237 const root = yield* fs.makeTempDirectoryScoped();
238 const getStore = yield* State.pipe(
239 Effect.provide(localState()),
240 Effect.provideService(AlchemyContext, { dotAlchemy: root, dev: false, adopt: false }),
241 );
242 const store = yield* getStore;
243 yield* store.setOutput({ stack: "runtime-directory", stage: "test", value: { value: 42 } });
244 expect(yield* store.getOutput({ stack: "runtime-directory", stage: "test" })).toEqual({
245 value: 42,
246 });
247 expect(yield* fs.exists(path.join(root, "state"))).toBe(true);
248 }),
249 { tags: ["unit", "local"] },
250 );
251
252 it.effect(
253 "excludes relocated runtime data from copies, hashes, and compute archives",
254 () =>
255 Effect.gen(function* () {
256 const fs = yield* FileSystem.FileSystem;
257 const path = yield* Path.Path;
258 const root = yield* fs.makeTempDirectoryScoped();
259 const runtime = path.join(root, "cache[private]");
260 yield* fs.makeDirectory(runtime);
261 yield* fs.writeFileString(path.join(root, "index.js"), "export default 42;");
262 yield* fs.writeFileString(path.join(runtime, "secret.txt"), "PRIVATE_RUNTIME_DATA");
263 yield* Effect.gen(function* () {
264 const artifactBefore = yield* hashDirectory({
265 cwd: runtime,
266 memo: { exclude: [], lockfile: false },
267 });
268 const before = yield* hashExtraFiles([{ source: root, dest: "." }]);
269 yield* fs.writeFileString(
270 path.join(runtime, "secret.txt"),
271 "CHANGED_PRIVATE_RUNTIME_DATA",
272 );
273 expect(
274 yield* hashDirectory({ cwd: runtime, memo: { exclude: [], lockfile: false } }),
275 ).not.toBe(artifactBefore);
276 expect(yield* hashExtraFiles([{ source: root, dest: "." }])).toEqual(before);
277 const target = yield* fs.makeTempDirectoryScoped();
278 yield* copyTree(root, target);
279 expect(yield* fs.exists(path.join(target, "cache[private]"))).toBe(false);
280 expect(yield* fs.exists(path.join(target, "index.js"))).toBe(true);
281 const archive = yield* createComputeArchive({ directory: root, entrypoint: "index.js" });
282 expect(gunzipSync(archive).toString()).not.toContain("PRIVATE_RUNTIME_DATA");
283 }).pipe(
284 Effect.provideService(AlchemyContext, {
285 dotAlchemy: path.relative(process.cwd(), runtime),
286 dev: false,
287 adopt: false,
288 }),
289 );
290 }),
291 { tags: ["unit", "provider:prisma", "provider:prisma:compute", "local"] },
292 );
293 it.effect(
294 "reads Python dependency caches from the configured root without including them as source",
295 () =>
296 Effect.gen(function* () {
297 const fs = yield* FileSystem.FileSystem;
298 const path = yield* Path.Path;
299 const root = yield* fs.makeTempDirectoryScoped();
300 const runtime = path.join(root, "runtime");
301 const staging = path.join(runtime, "python/test");
302 yield* fs.makeDirectory(path.join(staging, "python_modules"), { recursive: true });
303 yield* fs.writeFileString(path.join(root, "worker.py"), "class Default: pass");
304 yield* fs.writeFileString(
305 path.join(staging, "python_modules/cached.py"),
306 "CACHED_DEPENDENCY = True",
307 );
308 yield* fs.writeFileString(path.join(runtime, "private.py"), "PRIVATE_RUNTIME_DATA = True");
309 yield* fs.writeFileString(
310 path.join(staging, ".synced"),
311 yield* sha256("3.13\0https://index.pyodide.org/0.28.3\0"),
312 );
313 const bundle = yield* readPythonWorkerBundle({
314 id: "test",
315 fqn: "test",
316 main: path.join(root, "worker.py"),
317 compatibility: { date: "2026-03-17", flags: ["python_workers"] },
318 }).pipe(
319 Effect.provideService(AlchemyContext, { dotAlchemy: runtime, dev: false, adopt: false }),
320 );
321 expect(bundle.files.some((file) => file.path.includes("cached.py"))).toBe(true);
322 expect(bundle.files.some((file) => file.path.includes("private.py"))).toBe(false);
323 expect(yield* fs.exists(path.join(root, ".alchemy"))).toBe(false);
324 }),
325 { tags: ["unit", "provider:cloudflare", "provider:cloudflare:worker", "local"] },
326 );
327
328 it.effect(
329 "excludes the supplied runtime directory from Next.js source hashes",
330 () =>
331 Effect.gen(function* () {
332 const fs = yield* FileSystem.FileSystem;
333 const path = yield* Path.Path;
334 const root = yield* fs.makeTempDirectoryScoped();
335 const runtime = path.join(root, "runtime");
336 yield* fs.makeDirectory(runtime);
337 yield* fs.writeFileString(path.join(root, "app.js"), "export default 42;");
338 const provider = yield* nextjsSource.make({ root });
339 const context = {
340 id: "test",
341 workerName: "test",
342 compatibility: { date: "2026-03-17", flags: [] },
343 dotAlchemy: runtime,
344 };
345 const before = yield* provider.hash(context, undefined);
346 yield* fs.writeFileString(path.join(runtime, "state.json"), "{}");
347 expect(yield* provider.hash(context, undefined)).toEqual(before);
348 yield* fs.writeFileString(path.join(root, "app.js"), "export default 43;");
349 expect(yield* provider.hash(context, undefined)).not.toEqual(before);
350 }),
351 { tags: ["unit", "local"] },
352 );
353});