1import * as Alchemy from "alchemy";
2import * as GCP from "alchemy/GCP";
3import * as Output from "alchemy/Output";
4import * as Effect from "effect/Effect";
5import { fileURLToPath } from "node:url";
6
7export default Alchemy.Stack(
8 "GcpStaticSiteExample",
9 { providers: GCP.providers(), state: Alchemy.localState() },
10 Effect.gen(function* () {
11 const bucket = yield* GCP.Storage.Bucket("Site", {
12 uniformBucketLevelAccess: true,
13 // Applied when the bucket is served through a CNAME or an HTTPS
14 // load balancer; path-style storage.googleapis.com URLs ignore it.
15 website: { mainPageSuffix: "index.html", notFoundPage: "404.html" },
16 // Site content is fully owned by this stack.
17 forceDestroy: true,
18 });
19
20 // Anyone on the internet may read (and list) the site's objects.
21 yield* GCP.IAM.Member("PublicRead", {
22 kind: "storage.bucket",
23 name: bucket.bucketName,
24 role: "roles/storage.objectViewer",
25 member: "allUsers",
26 });
27
28 const files = yield* GCP.Storage.Files("SiteFiles", {
29 bucketName: bucket.bucketName,
30 path: fileURLToPath(new URL("./site", import.meta.url)),
31 // HTML revalidates on every load so deploys show up immediately.
32 cacheControl: (key) =>
33 key.endsWith(".html") ? "no-cache" : "public, max-age=300",
34 });
35
36 return {
37 bucketName: bucket.bucketName,
38 url: Output.interpolate`https://storage.googleapis.com/${bucket.bucketName}/index.html`,
39 fileCount: files.length,
40 };
41 }),
42);