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 pathe from "pathe";
6import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
7import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Gated: CloudFront Distribution create blocks on Status === "Deployed"
12// (~5-15 min) and destroy requires disable -> wait -> delete (another
13// ~5-15 min). Skipped under --fast (FAST=1) (same gate
14// as the AWS.CloudFront and AWS.Website.Vite suites).
15const runLive = !process.env.FAST;
16
17const fixtureDir = pathe.resolve(
18 import.meta.dirname,
19 "fixtures",
20 "foldkit-app",
21);
22
23// Clone under the alchemy package so `vite`, `foldkit`, and
24// `@foldkit/vite-plugin` resolve from the workspace's hoisted node_modules
25// (the fixture has none of its own). Vite's `vite:build-html` plugin also
26// expresses emitted asset paths relative to the cwd, so the clone has to
27// live under the same workspace root.
28const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
29
30const fixtureEntries = [
31 ".gitignore",
32 "package.json",
33 "vite.config.ts",
34 "index.html",
35 "src",
36 "public",
37];
38
39// Skipped under the floci runner: in dev the composite deploys only the
40// framework dev server (no Lambda/S3/CloudFront), so this test's live
41// topology assertions are meaningless there. Dev behavior is covered by
42// the co-located Foldkit.local.test.ts suite.
43const runEmulated = process.env.ALCHEMY_TEST_DEV === "1";
44
45describe.skipIf(!runLive || runEmulated)(
46 "AWS.Website.Foldkit",
47 { tags: ["provider:aws", "provider:aws:website", "live"] },
48 () => {
49 // The resource's reason to exist: a Foldkit app routes on the client, so
50 // the deployment is assets-only and deep links fall back to the shell
51 // without the caller configuring anything.
52 test.provider(
53 "deploys the foldkit client build to S3 behind CloudFront with SPA fallback",
54 (stack) =>
55 Effect.gen(function* () {
56 yield* stack.destroy();
57
58 const rootDir = yield* cloneFixture(fixtureDir, {
59 prefix: "alchemy-foldkit-aws-live-",
60 tempRoot,
61 entries: fixtureEntries,
62 });
63
64 const deployed = yield* stack.deploy(
65 Effect.gen(function* () {
66 // Deliberately no `spa` — the default is what's under test.
67 const site = yield* AWS.Website.Foldkit("FoldkitSite", {
68 rootDir,
69 forceDestroy: true,
70 invalidation: { paths: "all", wait: true },
71 });
72 return { site };
73 }),
74 );
75
76 const url = deployed.site.url! as string;
77 expect(url).toMatch(/^https:\/\//);
78 // Assets-only: a Foldkit app is client-only, so the composite never
79 // creates a server function.
80 expect(deployed.site.server).toBeUndefined();
81 expect(deployed.site.serverUrl).toBeUndefined();
82
83 // The built index page serves from the edge.
84 yield* expectUrlContains(`${url}/`, "FOLDKIT_AWS_PAGE_MARKER", {
85 timeout: "180 seconds",
86 label: "index",
87 });
88 // publicDir passthrough landed in the bucket.
89 yield* expectUrlContains(`${url}/robots.txt`, "User-agent", {
90 label: "public asset",
91 });
92 // SPA fallback (the composite's default): a deep link boots the app
93 // instead of 404ing, and the Foldkit router resolves the route.
94 yield* expectUrlContains(
95 `${url}/counter/42`,
96 "FOLDKIT_AWS_PAGE_MARKER",
97 {
98 label: "spa fallback",
99 },
100 );
101
102 yield* stack.destroy();
103 }),
104 { timeout: 2_400_000 },
105 );
106 },
107);