1import * as Bundle from "@/Bundle/Bundle";
2import type { FunctionZipProps } from "@/AWS/Lambda/Function";
3import { makeFunctionBundler } from "@/AWS/Lambda/FunctionBundle";
4import { exec } from "@/Util/exec.ts";
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { expect, layer } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Path from "effect/Path";
10import { 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 */
17layer(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});