| 1 | import * as Bundle from "@/Bundle/Bundle"; |
| 2 | import type { FunctionZipProps } from "@/AWS/Lambda/Function"; |
| 3 | import { makeFunctionBundler } from "@/AWS/Lambda/FunctionBundle"; |
| 4 | import { exec } from "@/Util/exec.ts"; |
| 5 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 6 | import { expect, layer } from "alchemy-test"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as FileSystem from "effect/FileSystem"; |
| 9 | import * as Path from "effect/Path"; |
| 10 | import { ChildProcess } from "effect/process"; |
| 11 | |
| 12 | /** |
| 13 | * The side-effect-free Cloudflare barrel exposes Flagship through a nested |
| 14 | * namespace. Its Providers graph imports Flagship's AppProvider while App |
| 15 | * initializes `App = Resource(TypeId)`. |
| 16 | */ |
| 17 | layer(NodeServices.layer)("Bundle namespace initialization", (it) => { |
| 18 | it.effect( |
| 19 | "initializes Flagship App before the Cloudflare namespace consumes it", |
| 20 | () => |
| 21 | Effect.gen(function* () { |
| 22 | const fs = yield* FileSystem.FileSystem; |
| 23 | const path = yield* Path.Path; |
| 24 | const cwd = process.cwd(); |
| 25 | const root = yield* fs.makeTempDirectory({ |
| 26 | directory: path.join(cwd, ".alchemy"), |
| 27 | prefix: "namespace-initialization-", |
| 28 | }); |
| 29 | |
| 30 | try { |
| 31 | const cloudflare = path.join(cwd, "src", "Cloudflare", "index.ts"); |
| 32 | const effect = path.join( |
| 33 | cwd, |
| 34 | "node_modules", |
| 35 | "effect", |
| 36 | "dist", |
| 37 | "Effect.js", |
| 38 | ); |
| 39 | const cause = path.join( |
| 40 | cwd, |
| 41 | "node_modules", |
| 42 | "effect", |
| 43 | "dist", |
| 44 | "Cause.js", |
| 45 | ); |
| 46 | const entry = path.join(root, "entry.ts"); |
| 47 | yield* fs.writeFileString( |
| 48 | entry, |
| 49 | [ |
| 50 | `import * as Cloudflare from ${JSON.stringify(cloudflare)};`, |
| 51 | `import * as Effect from ${JSON.stringify(effect)};`, |
| 52 | `import * as Cause from ${JSON.stringify(cause)};`, |
| 53 | `const program = Effect.gen(function* () {`, |
| 54 | ` return yield* Cloudflare.Flagship.App("Flags", {});`, |
| 55 | `}).pipe(Effect.catchCause((cause) => Effect.sync(() => {`, |
| 56 | ` const rendered = Cause.pretty(cause);`, |
| 57 | ` if (rendered.includes("is not iterable") || rendered.includes("not a function")) {`, |
| 58 | ` throw new Error(rendered);`, |
| 59 | ` }`, |
| 60 | `})));`, |
| 61 | `Effect.runSync(program);`, |
| 62 | `console.log("NAMESPACE_INITIALIZATION_MARKER");`, |
| 63 | ].join("\n"), |
| 64 | ); |
| 65 | |
| 66 | const bundler = yield* makeFunctionBundler; |
| 67 | const plan = yield* bundler.resolveBundlePlan({ |
| 68 | main: entry, |
| 69 | isExternal: true, |
| 70 | build: { external: ["cloudflare:workers"] }, |
| 71 | } as FunctionZipProps); |
| 72 | const bundle = yield* Bundle.build( |
| 73 | plan.inputOptions, |
| 74 | plan.outputOptions, |
| 75 | plan.extra, |
| 76 | ); |
| 77 | yield* Effect.forEach(bundle.files, (file) => { |
| 78 | const output = path.join(root, file.path); |
| 79 | return Effect.gen(function* () { |
| 80 | yield* fs.makeDirectory(path.dirname(output), { |
| 81 | recursive: true, |
| 82 | }); |
| 83 | if (typeof file.content === "string") { |
| 84 | yield* fs.writeFileString(output, file.content); |
| 85 | } else { |
| 86 | yield* fs.writeFile(output, file.content); |
| 87 | } |
| 88 | }); |
| 89 | }); |
| 90 | const output = path.join(root, bundle.files[0].path); |
| 91 | const result = yield* exec( |
| 92 | ChildProcess.make(process.execPath, [output], { shell: false }), |
| 93 | ).pipe(Effect.scoped); |
| 94 | |
| 95 | expect(result.stderr).toBe(""); |
| 96 | expect(result.exitCode).toBe(0); |
| 97 | expect(result.stdout).toContain("NAMESPACE_INITIALIZATION_MARKER"); |
| 98 | } finally { |
| 99 | yield* fs.remove(root, { recursive: true }).pipe(Effect.ignore); |
| 100 | } |
| 101 | }), |
| 102 | { tags: ["unit", "provider:aws", "provider:aws:lambda", "local"] }, |
| 103 | ); |
| 104 | }); |