| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as FileSystem from "effect/FileSystem"; |
| 3 | import * as Path from "effect/Path"; |
| 4 | import * as crypto from "node:crypto"; |
| 5 | import { fileURLToPath } from "node:url"; |
| 6 | import { Stack } from "../Stack.ts"; |
| 7 | import { 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 | */ |
| 17 | export 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 | */ |
| 36 | export 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 | */ |
| 62 | export 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 | */ |
| 84 | export 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 | |
| 90 | export 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 | }); |