1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import { describe, expect } from "alchemy-test";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as pathe from "pathe";
8import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
9import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
10
11// `dev: true` runs local providers behind the RPC sidecar proxy by default,
12// matching the process topology of the real `alchemy dev` command.
13const { test } = Test.make({ providers: AWS.providers(), dev: true });
14
15const fixtureDir = pathe.resolve(
16 import.meta.dirname,
17 "fixtures",
18 "staticsite-dev",
19);
20const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
21
22const htmlPage = (marker: string) => `<!doctype html>
23<html>
24 <head><title>${marker}</title></head>
25 <body><h1>${marker}</h1></body>
26</html>
27`;
28
29describe(
30 "AWS.Website.StaticSite local",
31 { tags: ["provider:aws", "provider:aws:website", "local"] },
32 () => {
33 /**
34 * The `dev.command` path: `alchemy dev` skips the build/upload entirely
35 * and spawns the command as a long-lived child in the sidecar
36 * (`Command.Dev`). The URL the child prints to stdout becomes the site's
37 * `url`, and no cloud resources are declared at all.
38 */
39 test.provider(
40 "dev.command serves the site through the external dev server",
41 (stack) =>
42 Effect.gen(function* () {
43 const fs = yield* FileSystem.FileSystem;
44 const path = yield* Path.Path;
45
46 yield* stack.destroy();
47
48 const cwd = yield* cloneFixture(fixtureDir, {
49 prefix: "alchemy-aws-staticsite-dev-cmd-",
50 tempRoot,
51 entries: ["serve.mjs", "site"],
52 });
53
54 const marker = "aws-staticsite-dev-command-marker";
55 yield* fs.writeFileString(
56 path.join(cwd, "site", "index.html"),
57 htmlPage(marker),
58 );
59
60 const deployed = yield* stack.deploy(
61 Effect.gen(function* () {
62 const site = yield* AWS.Website.StaticSite("DevCmdSite", {
63 path: cwd,
64 dev: {
65 command: "bun serve.mjs",
66 env: { DEV_MARKER: "aws-staticsite-dev-env-marker" },
67 },
68 });
69 return { site };
70 }),
71 );
72
73 // The site's url is the dev server's own localhost address,
74 // extracted from the child's stdout — and no cloud rows exist
75 // (proof no AWS call ran).
76 const url = deployed.site.url! as string;
77 expect(url).toMatch(/^http:\/\/localhost:\d+/);
78 expect(deployed.site.bucket).toBeUndefined();
79 expect(deployed.site.distribution).toBeUndefined();
80 expect(deployed.site.files).toBeUndefined();
81
82 // Dev-mode urls contract: `urls` is exactly the dev server's URL
83 // and `url` is always `urls[0]`.
84 expect(deployed.site.urls).toEqual([url]);
85 expect(deployed.site.url).toBe(deployed.site.urls[0]);
86
87 // Content serves through the dev server straight from `site/`.
88 yield* expectUrlContains(`${url}/`, marker, {
89 timeout: "60 seconds",
90 label: "dev.command index",
91 });
92 yield* expectUrlContains(`${url}/index.html`, marker, {
93 timeout: "30 seconds",
94 label: "dev.command explicit path",
95 });
96
97 // `dev.env` reached the spawned child process.
98 yield* expectUrlContains(
99 `${url}/__dev-env`,
100 "aws-staticsite-dev-env-marker",
101 {
102 timeout: "30 seconds",
103 label: "dev.command env passthrough",
104 },
105 );
106
107 // ── Live edit: rewrite the content in place. The stack is NOT
108 // re-applied — the external dev server reads from disk per request,
109 // so the very next fetch must serve the new content ──────────────
110 const editedMarker = "aws-staticsite-dev-command-marker-v2";
111 yield* fs.writeFileString(
112 path.join(cwd, "site", "index.html"),
113 htmlPage(editedMarker),
114 );
115 yield* expectUrlContains(`${url}/`, editedMarker, {
116 timeout: "30 seconds",
117 label: "dev.command index after live edit",
118 });
119 // `dev.env` still reaches the (unchanged, long-lived) child.
120 yield* expectUrlContains(
121 `${url}/__dev-env`,
122 "aws-staticsite-dev-env-marker",
123 { label: "dev.command env passthrough after live edit" },
124 );
125
126 yield* stack.destroy();
127 }),
128 { timeout: 300_000 },
129 );
130 },
131);