1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
4import * as crypto from "node:crypto";
5import { fileURLToPath } from "node:url";
6import { Stack } from "../Stack.ts";
7import { Stage } from "../Stage.ts";
8
9/**
10 * Resolve a bundle entrypoint (`main`) to a real filesystem path.
11 *
12 * `main` is user-supplied and is very commonly `import.meta.url` — a
13 * `file://` URL — which `FileSystem.realPath` cannot `lstat` directly
14 * (it would try to stat a literal `file:` path). Convert any `file://`
15 * URL to a path first, then resolve to the canonical real path.
16 */
17export const resolveMainPath = Effect.fn(function* (main: string) {
18 const fs = yield* FileSystem.FileSystem;
19 const path = yield* Path.Path;
20 const asPath = yield* Effect.sync(() => {
21 try {
22 return fileURLToPath(main);
23 } catch {
24 return main;
25 }
26 });
27 return path.resolve(yield* fs.realPath(asPath));
28});
29
30/**
31 * Creates a unique bundle staging directory under the configured runtime directory's
32 * `tmp` root. Each invocation gets its own directory (via a random
33 * nonce) so concurrent bundle operations for the same resource never collide.
34 * Stale directories from previous crashed runs are cleaned up best-effort.
35 */
36export const createTempBundleDir = (
37 _entry: string,
38 dotAlchemy: string,
39 id: string,
40) =>
41 Effect.gen(function* () {
42 const fs = yield* FileSystem.FileSystem;
43 const path = yield* Path.Path;
44 const stack = yield* Stack;
45 const stage = yield* Stage;
46 const tempRoot = path.join(dotAlchemy, "tmp");
47 yield* fs.makeDirectory(tempRoot, { recursive: true });
48
49 const nonce = crypto.randomUUID().slice(0, 8);
50 const bundleId = `${stack.name}-${stage}-${id}-${nonce}`;
51 const tempDir = path.join(tempRoot, bundleId);
52 yield* fs.makeDirectory(tempDir, { recursive: true });
53
54 return tempDir;
55 });
56
57/**
58 * Returns a deterministic bundle staging directory without clearing it first.
59 * Useful for Docker build contexts where keeping the directory stable avoids
60 * unnecessary file churn between builds.
61 */
62export const getStableContextDir = (
63 _entry: string,
64 dotAlchemy: string,
65 id: string,
66) =>
67 Effect.gen(function* () {
68 const fs = yield* FileSystem.FileSystem;
69 const path = yield* Path.Path;
70 const stack = yield* Stack;
71 const stage = yield* Stage;
72 const tempRoot = path.join(dotAlchemy, "tmp");
73 const bundleId = `${stack.name}-${stage}-${id}`;
74 const tempDir = path.join(tempRoot, bundleId);
75
76 yield* fs.makeDirectory(tempDir, { recursive: true });
77
78 return tempDir;
79 });
80
81/**
82 * Cleans up a bundle's private temp directory.
83 */
84export const cleanupBundleTempDir = (tempDir: string) =>
85 Effect.gen(function* () {
86 const fs = yield* FileSystem.FileSystem;
87 yield* fs.remove(tempDir, { recursive: true }).pipe(Effect.ignore);
88 });
89
90export const findCwdForBundle = Effect.fn(function* (entry: string) {
91 const fs = yield* FileSystem.FileSystem;
92 const path = yield* Path.Path;
93 let current = path.dirname(entry);
94 while (true) {
95 if (yield* fs.exists(path.join(current, "package.json"))) {
96 return current;
97 }
98 const parent = path.dirname(current);
99 if (parent === current) {
100 return process.cwd();
101 }
102 current = parent;
103 }
104});