1import * as Alchemy from "alchemy";
2import * as AWS from "alchemy/AWS";
3import * as Cloudflare from "alchemy/Cloudflare";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import ApiFunction from "./src/ApiFunction.ts";
7import EchoWorker from "./src/EchoWorker.ts";
8import MicrovmWorker from "./src/MicrovmWorker.ts";
9import { PORTS } from "./src/ports.ts";
10import ShellImageLive from "./src/ShellImage.ts";
11import Ec2Box from "./src/Ec2Box.ts";
12import { SandboxLive } from "./src/SandboxContainer.ts";
13import { STACK_NAME } from "./src/stack-config.ts";
14// <<EXTRA_IMPORTS>>
15// <</EXTRA_IMPORTS>>
16
17/**
18 * A deliberately cross-cloud `alchemy dev` stack, built to be REWRITTEN
19 * while the dev server is running. `test/dev-stress.test.ts` copies this
20 * project into a scratch directory, launches the real `alchemy dev` CLI
21 * against the copy, and then edits, breaks, moves and churns these files
22 * to prove the dev server neither dies nor goes stale.
23 *
24 * The regions marked `<<NAME>> … <</NAME>>` are patched by the suite; keep
25 * them intact. See ./README.md for the layout.
26 */
27export default Alchemy.Stack(
28 STACK_NAME,
29 {
30 providers: Layer.mergeAll(AWS.providers(), Cloudflare.providers()),
31 state: Alchemy.localState(),
32 },
33 Effect.gen(function* () {
34 // ── AWS: Lambda + S3 + DynamoDB + SQS, all inside the floci emulator.
35 // The whole subsystem lives in a region the stress suite deletes and
36 // restores wholesale.
37 // <<AWS_HALF>>
38 const api = yield* ApiFunction;
39 // <</AWS_HALF>>
40
41 // ── Cloudflare: Effect-native Worker (source imported by this stack) ──
42 const echo = yield* EchoWorker;
43
44 // ── Cloudflare: path-`main` Worker (source NOT imported by this stack).
45 // Its `AWS_LAMBDA_URL` var is the cross-cloud edge in the graph: a
46 // change to the Lambda propagates into the Worker's bindings.
47 const apiKv = yield* Cloudflare.KV.Namespace("ApiKV");
48 const apiWorker = yield* Cloudflare.Worker("ApiWorker", {
49 main: "./src/api-worker.ts",
50 dev: { port: PORTS.api, strictPort: true },
51 env: {
52 KV: apiKv,
53 // <<API_VARIABLE>>
54 API_VARIABLE: "api-variable-v1",
55 // <</API_VARIABLE>>
56 // <<AWS_LAMBDA_URL>>
57 AWS_LAMBDA_URL: api.functionUrl.as<string>(),
58 // <</AWS_LAMBDA_URL>>
59 // <<API_EXTRA_ENV>>
60 // <</API_EXTRA_ENV>>
61 },
62 });
63
64 // ── Cloudflare Website: build mode. Every re-apply re-runs build.sh
65 // and republishes the assets through the local Worker simulator.
66 const cfSite = yield* Cloudflare.Website.StaticSite("CfSite", {
67 command: "bash build.sh",
68 shell: true,
69 cwd: "site/cf",
70 outdir: "dist",
71 main: "./site/cf/worker.ts",
72 compatibility: { date: "2024-01-01" },
73 });
74
75 // ── AWS Website: dev-command mode. No AWS resources at all in dev —
76 // just a `Command.Dev` child in the sidecar, restarted whenever its
77 // resolved config changes.
78 const awsSite = yield* AWS.Website.StaticSite("AwsSite", {
79 path: "site/aws",
80 dev: {
81 command: "bun serve.mjs",
82 cwd: "site/aws",
83 env: {
84 PORT: String(PORTS.awsSite),
85 // <<SITE_MARKER>>
86 SITE_MARKER: "aws-site-env-v1",
87 // <</SITE_MARKER>>
88 },
89 },
90 });
91
92 // ── Cross-cloud headline: a Cloudflare Worker that mounts an AWS
93 // Lambda MicroVM. Alchemy mints the IAM User + AccessKey + assume-role
94 // Role for it; under `alchemy dev` all of it lands in floci.
95 const microvmWorker = yield* MicrovmWorker;
96
97 // ── AWS ECS: real docker containers under `alchemy dev` (floci runs
98 // the tasks on the host daemon). Bridge networking publishes the
99 // literal container port, so the suite probes fixed localhost ports.
100 // Two services, two reload surfaces:
101 // - `EcsService` builds `site/ecs/` — its content, its Dockerfile,
102 // and its `env` prop are all mutated live by the stress suite;
103 // - `EcsInlineService`'s Dockerfile is INLINE — editing it is a pure
104 // prop change, which must roll the running containers too.
105 const ecsCluster = yield* AWS.ECS.Cluster("StressEcsCluster");
106 const ecsService = yield* AWS.ECS.Service("EcsService", {
107 cluster: ecsCluster,
108 context: "site/ecs",
109 port: PORTS.ecs,
110 cpu: 256,
111 memory: 256,
112 networkMode: "bridge",
113 requiresCompatibilities: ["EC2"],
114 launchType: "EC2",
115 desiredCount: 1,
116 runtimePlatform: {
117 cpuArchitecture: process.arch === "arm64" ? "ARM64" : "X86_64",
118 operatingSystemFamily: "LINUX",
119 },
120 deploymentStabilizationTimeout: "3 minutes",
121 env: {
122 // <<ECS_ENV>>
123 STRESS_ENV: "ecs-env-v1",
124 // <</ECS_ENV>>
125 },
126 });
127 const ecsInlineService = yield* AWS.ECS.Service("EcsInlineService", {
128 cluster: ecsCluster,
129 dockerfile: {
130 content: [
131 "FROM busybox:stable",
132 // <<ECS_INLINE_MARKER>>
133 "RUN mkdir -p /www && echo -n ecs-inline-v1 > /www/index.html",
134 // <</ECS_INLINE_MARKER>>
135 `CMD ["httpd", "-f", "-p", "${PORTS.ecsInline}", "-h", "/www"]`,
136 ].join("\n"),
137 },
138 port: PORTS.ecsInline,
139 cpu: 256,
140 memory: 256,
141 networkMode: "bridge",
142 requiresCompatibilities: ["EC2"],
143 launchType: "EC2",
144 desiredCount: 1,
145 runtimePlatform: {
146 cpuArchitecture: process.arch === "arm64" ? "ARM64" : "X86_64",
147 operatingSystemFamily: "LINUX",
148 },
149 deploymentStabilizationTimeout: "3 minutes",
150 });
151
152 // ── AWS EC2: a hosted instance — the fourth floci compute unit. It
153 // runs as a real container in the emulator; edits to its program go
154 // through the ENGINE (re-plan → in-place bundle update → reboot), not
155 // a sidecar hot swap.
156 const ec2Box = yield* Ec2Box;
157
158 // <<EXTRA>>
159 // <</EXTRA>>
160
161 return {
162 // <<AWS_OUTPUTS>>
163 apiUrl: api.functionUrl,
164 // <</AWS_OUTPUTS>>
165 echoUrl: echo.url,
166 apiWorkerUrl: apiWorker.url,
167 cfSiteUrl: cfSite.url,
168 awsSiteUrl: awsSite.url,
169 microvmUrl: microvmWorker.url,
170 ecsServiceArn: ecsService.serviceArn,
171 ecsInlineServiceArn: ecsInlineService.serviceArn,
172 ec2Dns: ec2Box.publicDnsName,
173 // <<EXTRA_OUTPUTS>>
174 // <</EXTRA_OUTPUTS>>
175 };
176 }).pipe(
177 Effect.provide(
178 Layer.mergeAll(
179 SandboxLive,
180 ShellImageLive,
181 // <<EXTRA_LAYERS>>
182 // <</EXTRA_LAYERS>>
183 ),
184 ),
185 ),
186);