1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as cloudfront from "@distilled.cloud/aws/cloudfront";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as pathe from "pathe";
8import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
9import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// Gated with the rest of the AWS.Website suites: the CloudFront lifecycle
14// dominates the runtime (create ~5-15 min, destroy ~5-15 min).
15const runLive = !process.env.FAST;
16
17const fixtureDir = pathe.resolve(
18 import.meta.dirname,
19 "fixtures",
20 "solidstart-app",
21);
22
23// Clone under the alchemy package so `@solidjs/start`, `vite`, and
24// `@solidjs/vite-plugin-nitro-2` resolve from the workspace's hoisted
25// node_modules (the fixture has no node_modules).
26const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
27
28const fixtureEntries = [
29 ".gitignore",
30 "package.json",
31 "vite.config.ts",
32 "src",
33 "public",
34];
35
36// Under the floci runner the standalone composite deploys only the framework
37// dev server (no Lambda/S3/CloudFront), so the standalone test below is
38// live-only; the shared-Router test runs in both modes because the Router
39// pipeline deploys fully against the emulator, which serves the router's
40// edge on a local plain-HTTP port.
41const runEmulated = process.env.ALCHEMY_TEST_DEV === "1";
42
43describe.skipIf(!runLive)(
44 "AWS.Website.SolidStart",
45 {
46 tags: [
47 "provider:aws",
48 "provider:aws:cloudfront",
49 "provider:aws:website",
50 "live",
51 ],
52 },
53 () => {
54 test.provider.skipIf(runEmulated)(
55 "deploys SSR on a streaming Lambda URL with S3 assets behind CloudFront",
56 (stack) =>
57 Effect.gen(function* () {
58 yield* stack.destroy();
59
60 const rootDir = yield* cloneFixture(fixtureDir, {
61 prefix: "alchemy-solidstart-aws-",
62 tempRoot,
63 entries: fixtureEntries,
64 });
65
66 const deployed = yield* stack.deploy(
67 Effect.gen(function* () {
68 const site = yield* AWS.Website.SolidStart("SolidStartSite", {
69 rootDir,
70 forceDestroy: true,
71 invalidation: { paths: "all", wait: true },
72 // Forwarded into the nitro plugin the integration appends —
73 // proves nitro options reach the build.
74 nitro: { prerender: { routes: ["/prerendered"] } },
75 });
76 return { site };
77 }),
78 );
79
80 const url = deployed.site.url! as string;
81 expect(url).toMatch(/^https:\/\//);
82 expect(deployed.site.serverUrl).toBeDefined();
83 yield* Effect.log(
84 `site url: ${url} | server url: ${deployed.site.serverUrl}`,
85 );
86
87 // The Lambda Function URL serves the SSR page directly — isolates
88 // server-function health from the CloudFront edge routing.
89 yield* expectUrlContains(
90 `${deployed.site.serverUrl!}`,
91 "SOLIDSTART_AWS_PAGE_MARKER",
92 {
93 timeout: "120 seconds",
94 label: "SSR direct from Lambda URL",
95 },
96 );
97
98 // SSR page rendered by the Lambda through CloudFront.
99 yield* expectUrlContains(`${url}/`, "SOLIDSTART_AWS_PAGE_MARKER", {
100 timeout: "180 seconds",
101 label: "SSR home page",
102 });
103 // The fixture's own vite.config.ts applied (its `define` marker).
104 yield* expectUrlContains(
105 `${url}/`,
106 "config:solidstart-aws-user-config-loaded",
107 { label: "user vite.config.ts applied" },
108 );
109 // API route through the streaming Function URL origin.
110 yield* expectUrlContains(
111 `${url}/api/hello?echo=roundtrip`,
112 "SOLIDSTART_AWS_API_MARKER",
113 { label: "API route" },
114 );
115 yield* expectUrlContains(
116 `${url}/api/hello?echo=roundtrip`,
117 "roundtrip",
118 { label: "API route query echo" },
119 );
120 // Public file served from S3 via the KV file manifest.
121 yield* expectUrlContains(
122 `${url}/robots.txt`,
123 "solidstart-aws-robots-marker",
124 { label: "public asset from S3" },
125 );
126 // Prerendered into `.output/public` at build time (via the `nitro`
127 // prop's `prerender.routes`) and served from S3 by exact match at
128 // the edge — no Lambda invocation.
129 yield* expectUrlContains(
130 `${url}/prerendered`,
131 "SOLIDSTART_AWS_PRERENDERED_MARKER",
132 { label: "prerendered page from S3" },
133 );
134
135 const distributionId = deployed.site.distribution!.distributionId;
136
137 if (!process.env.NO_DESTROY) {
138 yield* stack.destroy();
139 yield* assertDistributionDeleted(distributionId);
140 }
141 }),
142 { timeout: 2_400_000 },
143 );
144
145 test.provider(
146 "serves SSR through a shared Router distribution with Lambda env applied",
147 (stack) =>
148 Effect.gen(function* () {
149 yield* stack.destroy();
150
151 const rootDir = yield* cloneFixture(fixtureDir, {
152 prefix: "alchemy-solidstart-aws-router-",
153 tempRoot,
154 entries: fixtureEntries,
155 });
156
157 const deployed = yield* stack.deploy(
158 Effect.gen(function* () {
159 const router = yield* AWS.Website.Router("FrontDoor", {
160 invalidation: { paths: "all", wait: true },
161 });
162 const site = yield* AWS.Website.SolidStart("SolidStartSite", {
163 rootDir,
164 forceDestroy: true,
165 domain: { router },
166 env: {
167 SOLIDSTART_ENV_MARKER: "solidstart-aws-live-env-marker",
168 },
169 });
170 return { router, site };
171 }),
172 );
173
174 const url = deployed.router.url as string;
175 // `https://{id}.cloudfront.net` live; the emulator serves the
176 // router's edge on a local plain-HTTP port.
177 expect(url).toMatch(
178 runEmulated ? /^http:\/\/localhost:\d+/ : /^https:\/\//,
179 );
180
181 // SSR through the ROUTER's distribution (the site registered
182 // itself in the router's KV store — no site-owned distribution).
183 expect(deployed.site.distribution).toBeUndefined();
184 // Generous budget: a fresh router distribution's KVS association +
185 // function propagation can lag past 180s on first serve.
186 yield* expectUrlContains(`${url}/`, "SOLIDSTART_AWS_PAGE_MARKER", {
187 timeout: "300 seconds",
188 label: "SSR via router",
189 });
190 // Lambda env applied on deploy (parity with the dev-server
191 // injection asserted in SolidStart.local.test.ts).
192 yield* expectUrlContains(
193 `${url}/`,
194 "env:solidstart-aws-live-env-marker",
195 { label: "server.environment on the Lambda" },
196 );
197 // The router's defaultTTL-0 cache policy must not cache SSR
198 // responses: the API route round-trips with distinct query strings,
199 // which a day-long cached body would break.
200 yield* expectUrlContains(
201 `${url}/api/hello?echo=router-one`,
202 "router-one",
203 { label: "API via router (query one)" },
204 );
205 yield* expectUrlContains(
206 `${url}/api/hello?echo=router-two`,
207 "router-two",
208 { label: "API via router (query two)" },
209 );
210 // Static asset from S3 through the router's edge function.
211 yield* expectUrlContains(
212 `${url}/robots.txt`,
213 "solidstart-aws-robots-marker",
214 { label: "public asset via router" },
215 );
216
217 const distributionId = deployed.router.distributionId as string;
218
219 if (!process.env.NO_DESTROY) {
220 yield* stack.destroy();
221 yield* assertDistributionDeleted(distributionId);
222 }
223 }),
224 { timeout: 2_400_000 },
225 );
226 },
227);
228
229const assertDistributionDeleted = (distributionId: string) =>
230 cloudfront.getDistribution({ Id: distributionId }).pipe(
231 Effect.flatMap(() => Effect.fail(new Error("DistributionStillExists"))),
232 Effect.catchTag("NoSuchDistribution", () => Effect.void),
233 Effect.retry({
234 while: (error): boolean =>
235 error instanceof Error && error.message === "DistributionStillExists",
236 schedule: Schedule.max([
237 Schedule.fixed("10 seconds"),
238 Schedule.recurs(60),
239 ]),
240 }),
241 );