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