| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as FileSystem from "effect/FileSystem"; |
| 3 | |
| 4 | /** |
| 5 | * An "isolated project": a throwaway consumer project OUTSIDE the repository |
| 6 | * tree, laid out the way bun's isolated linker (or pnpm) lays out a real |
| 7 | * consumer — `alchemy` present in the project's `node_modules` (a symlink to |
| 8 | * this package), and NONE of alchemy's own dependencies hoisted beside it — |
| 9 | * whose `main.ts` re-exports a test fixture by absolute path. |
| 10 | * |
| 11 | * Every `main`-bundled platform (ECS, App Runner, Batch, EC2, Lambda, |
| 12 | * Cloudflare Containers, Docker, Fly, Hetzner, Prisma, MicroVM, …) wraps the |
| 13 | * user's program in a generated entry — a *virtual* rolldown module. A |
| 14 | * virtual module has no directory, so rolldown resolves its bare imports |
| 15 | * from the build `cwd`, i.e. the nearest `package.json` above `main`. Inside |
| 16 | * this monorepo that is `packages/alchemy`, from which everything resolves — |
| 17 | * so a fixture checked in next to its test can never reproduce the |
| 18 | * consumer-side failure: when the generated entry imported alchemy's own |
| 19 | * dependencies (`@distilled.cloud/*`, `@effect/platform-*`) directly, an |
| 20 | * isolated install left them `[UNRESOLVED_IMPORT]`, external, and the |
| 21 | * deployed process died at boot with `Cannot find module`. The entry is now |
| 22 | * a shim importing only `alchemy/Runtime/Bootstrap/<Platform>` (resolvable |
| 23 | * from any consumer) plus `main`; this harness pins that contract. |
| 24 | * |
| 25 | * Pointing a fixture's `main` at {@link IsolatedProject.main} makes the bundle |
| 26 | * `cwd` this project. The fixture itself (and the user code it represents) |
| 27 | * is reached through the absolute-path re-export, so its imports still |
| 28 | * resolve from the repo as usual — only the generated entry's own imports |
| 29 | * are exercised, exactly as for a real consumer. |
| 30 | * |
| 31 | * The location is deterministic (no nonce) and computed at module scope |
| 32 | * because `main` is a prop of the fixture's declaration: it has to be known |
| 33 | * when the fixture module is evaluated, at test collection, before any hook |
| 34 | * runs. A plain `/tmp` prefix (not `os.tmpdir()`) keeps fixture modules free |
| 35 | * of Node builtins — Worker/Durable Object bundles import the container |
| 36 | * fixture for its class and run on workerd. |
| 37 | */ |
| 38 | export interface IsolatedProject { |
| 39 | /** Unique project name; also the directory name under the shared root. */ |
| 40 | readonly name: string; |
| 41 | /** Project directory (outside the repository). */ |
| 42 | readonly dir: string; |
| 43 | /** The `main` to hand to the platform resource. */ |
| 44 | readonly main: string; |
| 45 | /** Absolute path of the fixture module `main.ts` re-exports. */ |
| 46 | readonly fixture: string; |
| 47 | } |
| 48 | |
| 49 | const ROOT = "/tmp/alchemy-test-isolated-projects"; |
| 50 | |
| 51 | /** `packages/alchemy` — this helper lives at `packages/alchemy/test/`. */ |
| 52 | const ALCHEMY_PACKAGE_DIR = new URL("..", import.meta.url).pathname.replace( |
| 53 | /\/$/, |
| 54 | "", |
| 55 | ); |
| 56 | |
| 57 | /** |
| 58 | * Declare an isolated project for `fixture` (pass `import.meta.filename`). |
| 59 | * Pure: nothing is written until {@link materializeIsolatedProject}. |
| 60 | */ |
| 61 | export const isolatedProject = ( |
| 62 | name: string, |
| 63 | fixture: string, |
| 64 | ): IsolatedProject => { |
| 65 | const dir = `${ROOT}/${name}`; |
| 66 | return { name, dir, main: `${dir}/main.ts`, fixture }; |
| 67 | }; |
| 68 | |
| 69 | /** |
| 70 | * Write the project to disk: a `package.json` (so the bundle `cwd` stops |
| 71 | * here), `node_modules/alchemy` linked to this package (the consumer's one |
| 72 | * direct dependency, as an isolated install lays it out — alchemy's own |
| 73 | * dependencies are deliberately NOT reachable from here), and a `main.ts` |
| 74 | * re-exporting the fixture's default export. Idempotent — safe to call at |
| 75 | * the top of every test that deploys it. |
| 76 | */ |
| 77 | export const materializeIsolatedProject = (project: IsolatedProject) => |
| 78 | Effect.gen(function* () { |
| 79 | const fs = yield* FileSystem.FileSystem; |
| 80 | yield* fs.makeDirectory(`${project.dir}/node_modules`, { |
| 81 | recursive: true, |
| 82 | }); |
| 83 | const alchemyLink = `${project.dir}/node_modules/alchemy`; |
| 84 | yield* fs.remove(alchemyLink, { recursive: true }).pipe(Effect.ignore); |
| 85 | yield* fs.symlink(ALCHEMY_PACKAGE_DIR, alchemyLink); |
| 86 | yield* fs.writeFileString( |
| 87 | `${project.dir}/package.json`, |
| 88 | JSON.stringify({ |
| 89 | name: `alchemy-test-isolated-${project.name}`, |
| 90 | private: true, |
| 91 | type: "module", |
| 92 | }), |
| 93 | ); |
| 94 | yield* fs.writeFileString( |
| 95 | project.main, |
| 96 | `export { default } from ${JSON.stringify(project.fixture)};\n`, |
| 97 | ); |
| 98 | }); |
| 99 | |
| 100 | /** Remove the project directory (ignores a missing directory). */ |
| 101 | export const removeIsolatedProject = (project: IsolatedProject) => |
| 102 | FileSystem.FileSystem.pipe( |
| 103 | Effect.flatMap((fs) => fs.remove(project.dir, { recursive: true })), |
| 104 | Effect.ignore, |
| 105 | ); |