| 1 | import * as Effect from "effect/Effect"; |
| 2 | import type { MemoOptions } from "../../Command/Memo.ts"; |
| 3 | import type { InputProps } from "../../Input.ts"; |
| 4 | import { effectClass } from "../../Util/effect.ts"; |
| 5 | import { Namespace } from "../KV/Namespace.ts"; |
| 6 | import type { Providers } from "../Providers.ts"; |
| 7 | import type { AssetsConfig } from "../Workers/Assets.ts"; |
| 8 | import { |
| 9 | Worker, |
| 10 | type NormalizedBindings, |
| 11 | type WorkerAssetsConfig, |
| 12 | type WorkerBindingProps, |
| 13 | type WorkerProps, |
| 14 | } from "../Workers/Worker.ts"; |
| 15 | |
| 16 | export interface AstroProps< |
| 17 | Bindings extends WorkerBindingProps = {}, |
| 18 | > extends Omit< |
| 19 | WorkerProps<Bindings>, |
| 20 | "vite" | "main" | "assets" | "source" | "script" | "bundle" |
| 21 | > { |
| 22 | /** |
| 23 | * Astro project root directory. |
| 24 | * Defaults to the current working directory (`process.cwd()`). |
| 25 | */ |
| 26 | rootDir?: string; |
| 27 | /** |
| 28 | * Controls which files are hashed to decide whether a rebuild is needed. |
| 29 | * By default every non-gitignored file under `rootDir` is hashed, plus the |
| 30 | * nearest package-manager lockfile. Provide explicit globs to narrow the |
| 31 | * scope. |
| 32 | */ |
| 33 | memo?: MemoOptions & { |
| 34 | /** |
| 35 | * Additional workspace directories to hash (relative to `rootDir`). |
| 36 | * By default (`"auto"`), workspaces are auto-detected from the build's |
| 37 | * module graph; an explicit array pins them. |
| 38 | * @default "auto" |
| 39 | */ |
| 40 | workspaces?: "auto" | Array<MemoOptions & { cwd: string }>; |
| 41 | }; |
| 42 | /** |
| 43 | * The name of the KV binding backing Astro's session API. |
| 44 | * |
| 45 | * A KV namespace is auto-provisioned and bound under this name on |
| 46 | * deploy, so `Astro.session` works with zero configuration. Bind your |
| 47 | * own KV namespace under this name in `env` to use it instead of the |
| 48 | * auto-provisioned one, or set this to `false` to disable session |
| 49 | * provisioning entirely. |
| 50 | * @default "SESSION" |
| 51 | */ |
| 52 | sessionKVBindingName?: string | false; |
| 53 | /** |
| 54 | * Runtime used to prerender static pages. `"workerd"` renders them in the |
| 55 | * production Worker runtime; `"node"` uses Astro's stock Node prerenderer. |
| 56 | * @default "workerd" |
| 57 | */ |
| 58 | prerenderEnvironment?: "workerd" | "node"; |
| 59 | /** |
| 60 | * Deploy-time Astro config overrides, merged OVER your natively-loaded |
| 61 | * `astro.config.*` (values here win). Use it for values that vary per |
| 62 | * stage or derive from other resources' Outputs — everything else |
| 63 | * belongs in the config file. |
| 64 | */ |
| 65 | astro?: { |
| 66 | /** Deployed URL origin (astro's `site`). */ |
| 67 | site?: string; |
| 68 | /** Base path the site is served from (astro's `base`). */ |
| 69 | base?: string; |
| 70 | /** |
| 71 | * Astro output target — a deploy-topology decision (whether Worker |
| 72 | * code runs at request time). `"server"` renders pages on demand in |
| 73 | * the Worker; individual pages opt into prerendering with |
| 74 | * `export const prerender = true`. `"static"` prerenders every page |
| 75 | * at build time and deploys assets-only. Supersedes a file-level |
| 76 | * `output`. |
| 77 | * @default "server" |
| 78 | */ |
| 79 | output?: "server" | "static"; |
| 80 | /** Source directory (astro's `srcDir`). */ |
| 81 | srcDir?: string; |
| 82 | /** Public assets directory (astro's `publicDir`). */ |
| 83 | publicDir?: string; |
| 84 | /** Build output directory (astro's `outDir`). */ |
| 85 | outDir?: string; |
| 86 | /** Trailing-slash handling (astro's `trailingSlash`). */ |
| 87 | trailingSlash?: "always" | "never" | "ignore"; |
| 88 | }; |
| 89 | /** |
| 90 | * Path to an alternate astro config file, relative to `rootDir`. |
| 91 | * Defaults to astro's own config discovery. |
| 92 | */ |
| 93 | config?: string; |
| 94 | /** |
| 95 | * Optional configuration for static asset routing behavior. |
| 96 | * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc. |
| 97 | */ |
| 98 | assets?: AssetsConfig; |
| 99 | } |
| 100 | |
| 101 | // These options are inspected while constructing the Worker. Resolve them in |
| 102 | // the outer props Effect; pass-through properties can remain deferred Inputs. |
| 103 | type AstroInput<Bindings extends WorkerBindingProps> = InputProps< |
| 104 | AstroProps<Bindings>, |
| 105 | "sessionKVBindingName" | "astro" | "env" |
| 106 | >; |
| 107 | |
| 108 | /** |
| 109 | * A Cloudflare Worker deployed from an [Astro](https://astro.build) |
| 110 | * project. |
| 111 | * |
| 112 | * `Astro` runs Astro's programmatic build with a wrangler-free |
| 113 | * Cloudflare adapter (`@alchemy.run/frontend-frameworks/astro`): server-rendered pages |
| 114 | * execute in the Worker, prerendered pages and client assets deploy as |
| 115 | * static assets. Your `astro.config.*` loads natively — no adapter |
| 116 | * setup or Wrangler configuration required. |
| 117 | * |
| 118 | * Input files are content-hashed (respecting `.gitignore` by default) |
| 119 | * so unchanged projects skip the build and deploy entirely. |
| 120 | * |
| 121 | * The `@alchemy.run/frontend-frameworks` package must be installed in your |
| 122 | * project; its `/astro` export is loaded dynamically at deploy time: |
| 123 | * |
| 124 | * ```sh |
| 125 | * bun add -d @alchemy.run/frontend-frameworks |
| 126 | * ``` |
| 127 | * |
| 128 | * |
| 129 | * ### Deploying an Astro Site |
| 130 | * A single call builds the project and deploys the server bundle plus |
| 131 | * static assets. Pages are server-rendered by default; pages that |
| 132 | * `export const prerender = true` are served as static assets. Astro's |
| 133 | * server runtime is built against Node APIs, so `nodejs_compat` is |
| 134 | * always included in the Worker's compatibility flags. |
| 135 | * |
| 136 | * **Example:** Astro site |
| 137 | * ```typescript |
| 138 | * const site = yield* Cloudflare.Website.Astro("Website"); |
| 139 | * ``` |
| 140 | * |
| 141 | * ### Static Sites |
| 142 | * With `astro: { output: "static" }` every page is prerendered at build |
| 143 | * time and the deploy is **assets-only**: no server bundle is uploaded — |
| 144 | * Cloudflare's asset layer answers every request (serve the built |
| 145 | * `404.html` via `assets: { notFoundHandling: "404-page" }`). Session |
| 146 | * provisioning is skipped for declared-static sites since no Worker |
| 147 | * code runs at request time. |
| 148 | * |
| 149 | * **Example:** Fully static Astro site |
| 150 | * ```typescript |
| 151 | * const site = yield* Cloudflare.Website.Astro("Docs", { |
| 152 | * astro: { output: "static" }, |
| 153 | * assets: { |
| 154 | * notFoundHandling: "404-page", |
| 155 | * }, |
| 156 | * }); |
| 157 | * ``` |
| 158 | * |
| 159 | * ### Bindings |
| 160 | * Bind resources through `env` like any other Worker. Astro code reads |
| 161 | * them via `import { env } from "cloudflare:workers"` (or |
| 162 | * `Astro.locals.runtime.env`). |
| 163 | * |
| 164 | * **Example:** Astro site with a KV namespace and an R2 bucket |
| 165 | * ```typescript |
| 166 | * const kv = yield* Cloudflare.KV.Namespace("Cache"); |
| 167 | * const bucket = yield* Cloudflare.R2.Bucket("Uploads"); |
| 168 | * |
| 169 | * const site = yield* Cloudflare.Website.Astro("Website", { |
| 170 | * env: { |
| 171 | * CACHE: kv, |
| 172 | * UPLOADS: bucket, |
| 173 | * }, |
| 174 | * }); |
| 175 | * ``` |
| 176 | * |
| 177 | * ### Sessions |
| 178 | * Astro's session API is backed by a KV namespace. One is provisioned |
| 179 | * and bound under the session binding name (`SESSION` by default) |
| 180 | * automatically, so `Astro.session` works with zero configuration. |
| 181 | * Bind your own namespace under that name to use it instead, or set |
| 182 | * `sessionKVBindingName: false` to opt out of session provisioning. |
| 183 | * |
| 184 | * **Example:** Bringing your own session namespace |
| 185 | * ```typescript |
| 186 | * const sessions = yield* Cloudflare.KV.Namespace("Sessions"); |
| 187 | * |
| 188 | * const site = yield* Cloudflare.Website.Astro("Website", { |
| 189 | * env: { |
| 190 | * SESSION: sessions, |
| 191 | * }, |
| 192 | * }); |
| 193 | * ``` |
| 194 | * |
| 195 | * **Example:** Opting out of session provisioning |
| 196 | * ```typescript |
| 197 | * const site = yield* Cloudflare.Website.Astro("Website", { |
| 198 | * sessionKVBindingName: false, |
| 199 | * }); |
| 200 | * ``` |
| 201 | * |
| 202 | * ### Custom Rebuild Scope |
| 203 | * By default, every non-gitignored file is hashed to decide whether a |
| 204 | * rebuild is needed. Use `memo` to narrow the scope when your project |
| 205 | * has large directories that don't affect the build output. |
| 206 | * |
| 207 | * **Example:** Narrowing the memo scope |
| 208 | * ```typescript |
| 209 | * const site = yield* Cloudflare.Website.Astro("Docs", { |
| 210 | * memo: { |
| 211 | * include: ["src/**", "public/**", "package.json"], |
| 212 | * }, |
| 213 | * }); |
| 214 | * ``` |
| 215 | * |
| 216 | * ### Astro Configuration |
| 217 | * Your `astro.config.*` is the home for Astro configuration |
| 218 | * (integrations, Vite plugins, `site`, `base`, ...) and loads natively. |
| 219 | * The Cloudflare adapter is injected for you — declaring an `adapter` |
| 220 | * in the config file fails the build. The `astro` prop is a |
| 221 | * deploy-time override bag merged OVER the file (values here win) for |
| 222 | * settings that vary per stage or derive from other resources' |
| 223 | * Outputs, which a config file cannot consume. `output` defaults to |
| 224 | * `"server"` — astro's zero-config `"static"` default would prerender |
| 225 | * every page inside workerd, where the Worker's bindings don't exist. |
| 226 | * Use `config` to point at an alternate config file (relative to |
| 227 | * `rootDir`). |
| 228 | * |
| 229 | * **Example:** Per-stage site URL override |
| 230 | * ```typescript |
| 231 | * const site = yield* Cloudflare.Website.Astro("Blog", { |
| 232 | * astro: { site: "https://blog.example.com" }, |
| 233 | * }); |
| 234 | * ``` |
| 235 | * |
| 236 | * ### Class Form |
| 237 | * Calling `Astro` with no arguments returns a constructor you can |
| 238 | * `extend` to declare the Worker as a named class. The class is both an |
| 239 | * `Effect` you can `yield*` to deploy and a type you can reference |
| 240 | * elsewhere — useful when other resources need to bind to this Worker. |
| 241 | * |
| 242 | * **Example:** Declaring a Worker class |
| 243 | * ```typescript |
| 244 | * class Website extends Cloudflare.Website.Astro<Website>()("Website") {} |
| 245 | * |
| 246 | * const site = yield* Website; |
| 247 | * ``` |
| 248 | * |
| 249 | * @resource |
| 250 | * @product Website |
| 251 | * @category Workers & Compute |
| 252 | */ |
| 253 | export const Astro: { |
| 254 | <Self>(): { |
| 255 | <const Bindings extends WorkerBindingProps = {}, Req = never>( |
| 256 | id: string, |
| 257 | propsEff?: |
| 258 | | AstroInput<Bindings> |
| 259 | | Effect.Effect<AstroInput<Bindings>, never, Req>, |
| 260 | ): Effect.Effect<Self, never, Req | Providers> & { |
| 261 | new (): Worker<{ |
| 262 | [ |
| 263 | binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig> |
| 264 | ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding]; |
| 265 | }>; |
| 266 | }; |
| 267 | }; |
| 268 | <const Bindings extends WorkerBindingProps = {}, Req = never>( |
| 269 | id: string, |
| 270 | propsEff?: |
| 271 | | AstroInput<Bindings> |
| 272 | | Effect.Effect<AstroInput<Bindings>, never, Req>, |
| 273 | ): Effect.Effect< |
| 274 | Worker<{ |
| 275 | [ |
| 276 | binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig> |
| 277 | ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding]; |
| 278 | }>, |
| 279 | never, |
| 280 | Req | Providers |
| 281 | >; |
| 282 | } = (<const Bindings extends WorkerBindingProps = {}, Req = never>( |
| 283 | id?: string, |
| 284 | propsEff?: |
| 285 | | AstroInput<Bindings> |
| 286 | | Effect.Effect<AstroInput<Bindings>, never, Req>, |
| 287 | ) => |
| 288 | id === undefined |
| 289 | ? <const Bindings extends WorkerBindingProps = {}, Req = never>( |
| 290 | id: string, |
| 291 | propsEff?: |
| 292 | | AstroInput<Bindings> |
| 293 | | Effect.Effect<AstroInput<Bindings>, never, Req>, |
| 294 | ) => effectClass(Astro(id, propsEff)) |
| 295 | : Worker( |
| 296 | id, |
| 297 | Effect.gen(function* () { |
| 298 | const props = |
| 299 | (Effect.isEffect(propsEff) ? yield* propsEff : propsEff) ?? {}; |
| 300 | const session = props.sessionKVBindingName; |
| 301 | const sessionBindingName = |
| 302 | typeof session === "string" ? session : "SESSION"; |
| 303 | let env: WorkerBindingProps | undefined = props.env; |
| 304 | // Auto-provision the KV namespace backing Astro's session API |
| 305 | // unless the user opted out (`sessionKVBindingName: false`) or |
| 306 | // already bound their own namespace under the session name. |
| 307 | // A declared `output: "static"` site is assets-only — no Worker |
| 308 | // script runs at request time, so a session namespace could never |
| 309 | // be read; skip provisioning it. Resource creation is deduped by |
| 310 | // logical id, so re-evaluating this props effect is safe. |
| 311 | if ( |
| 312 | session !== false && |
| 313 | props.astro?.output !== "static" && |
| 314 | env?.[sessionBindingName] === undefined |
| 315 | ) { |
| 316 | const sessions = yield* Namespace(`${id}Session`); |
| 317 | env = { ...env, [sessionBindingName]: sessions }; |
| 318 | } |
| 319 | return { |
| 320 | ...props, |
| 321 | env, |
| 322 | // Astro's vendored server runtime is built against Node APIs |
| 323 | // and needs `nodejs_compat`; `getCompatibility` already adds it |
| 324 | // to every non-python Worker (honoring an explicit |
| 325 | // `no_nodejs_compat` opt-out and the v2-mode date guard). |
| 326 | main: undefined!, |
| 327 | source: { |
| 328 | provider: "@alchemy.run/frontend-frameworks/astro/source", |
| 329 | devMode: "server", |
| 330 | rootDir: props.rootDir, |
| 331 | options: { |
| 332 | rootDir: props.rootDir, |
| 333 | memo: props.memo, |
| 334 | // Passed through verbatim (including `false`) so the |
| 335 | // source provider can skip its session-driver wiring on |
| 336 | // opt-out. |
| 337 | sessionKVBindingName: props.sessionKVBindingName, |
| 338 | prerenderEnvironment: props.prerenderEnvironment, |
| 339 | // The `astro` bag is the deploy-time overlay merged OVER |
| 340 | // the natively-loaded `astro.config.*`. Server output is |
| 341 | // the documented default: astro's own zero-config default |
| 342 | // is `"static"`, which would prerender every page at |
| 343 | // build time inside workerd — where the Worker's bindings |
| 344 | // don't exist. |
| 345 | astro: { |
| 346 | ...props.astro, |
| 347 | output: props.astro?.output ?? "server", |
| 348 | }, |
| 349 | config: props.config, |
| 350 | }, |
| 351 | }, |
| 352 | }; |
| 353 | }), |
| 354 | )) as any; |