| 1 | import * as AWS from "@/AWS"; |
| 2 | import * as Test from "@/Test/Alchemy"; |
| 3 | import { describe, expect } from "alchemy-test"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as pathe from "pathe"; |
| 6 | import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts"; |
| 7 | import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts"; |
| 8 | |
| 9 | const { 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). |
| 15 | const runLive = !process.env.FAST; |
| 16 | |
| 17 | const 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. |
| 28 | const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp"); |
| 29 | |
| 30 | const 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. |
| 43 | const runEmulated = process.env.ALCHEMY_TEST_DEV === "1"; |
| 44 | |
| 45 | describe.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 | ); |