Skip to content
Source
Sign inSign up

IsolatedProject.ts

110 lines5.0 KB

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