| 1 | import * as Alchemy from "alchemy"; |
| 2 | import * as AWS from "alchemy/AWS"; |
| 3 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import ApiFunction from "./src/ApiFunction.ts"; |
| 7 | import EchoWorker from "./src/EchoWorker.ts"; |
| 8 | import MicrovmWorker from "./src/MicrovmWorker.ts"; |
| 9 | import { PORTS } from "./src/ports.ts"; |
| 10 | import ShellImageLive from "./src/ShellImage.ts"; |
| 11 | import Ec2Box from "./src/Ec2Box.ts"; |
| 12 | import { SandboxLive } from "./src/SandboxContainer.ts"; |
| 13 | import { 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 | */ |
| 27 | export 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 | ); |