| 1 | /** |
| 2 | * The Cloudflare Workers deploy target for `@alchemy.run/frontend-frameworks/astro` |
| 3 | * (`@alchemy.run/frontend-frameworks/astro/cloudflare`). |
| 4 | * |
| 5 | * This module is the ONLY seam between the Astro framework module and |
| 6 | * Cloudflare: it owns the wrangler-free `@astrojs/cloudflare` fork (the |
| 7 | * integration over `@alchemy.run/cloudflare-runtime/vite`, its vendored |
| 8 | * server entrypoints, workerd dev via the vite module runner) and exposes it |
| 9 | * through the `AstroTarget` contract. The framework module |
| 10 | * (`Astro.ts`/`index.ts`) never imports anything Cloudflare-specific — it |
| 11 | * receives this target as a value (or resolves this module by specifier when |
| 12 | * no target is given). |
| 13 | * |
| 14 | * The default export is the target factory the framework's target resolution |
| 15 | * applies to the caller's config (`resolveDeployTarget`); the named exports |
| 16 | * re-expose the underlying integration surface for direct/advanced use. |
| 17 | */ |
| 18 | import type { CloudflareVitePluginOptions } from "@alchemy.run/cloudflare-runtime/vite"; |
| 19 | import { makeDeployTarget } from "../core/index.ts"; |
| 20 | import * as Effect from "effect/Effect"; |
| 21 | import * as NodePath from "node:path"; |
| 22 | import { distilledCloudflare } from "./integration.ts"; |
| 23 | import type { AstroTarget } from "./Target.ts"; |
| 24 | |
| 25 | /** |
| 26 | * Cloudflare target configuration. Matches the e2e harness's target-scoped |
| 27 | * carriage (`target.cloudflare.worker`): `worker` is the full |
| 28 | * `CloudflareVitePluginOptions` shape (compatibility date/flags, worker |
| 29 | * name/bindings/assets, runtime context). |
| 30 | */ |
| 31 | export interface AstroCloudflareConfig { |
| 32 | /** |
| 33 | * Options forwarded to `@alchemy.run/cloudflare-runtime/vite` |
| 34 | * (compatibility date/flags, worker name/bindings/assets, runtime |
| 35 | * context). `main`, `viteEnvironments`, and Astro's node-side |
| 36 | * environments in `skipEnvironments` are managed by the integration. |
| 37 | */ |
| 38 | readonly worker?: CloudflareVitePluginOptions | undefined; |
| 39 | /** |
| 40 | * Which runtime prerenders static pages at build time: `'workerd'` |
| 41 | * (default) renders them inside the production runtime via the built |
| 42 | * `prerender` environment worker; `'node'` falls back to Astro's stock |
| 43 | * node prerenderer. See `DistilledCloudflareOptions.prerenderEnvironment`. |
| 44 | * @default "workerd" |
| 45 | */ |
| 46 | readonly prerenderEnvironment?: "workerd" | "node" | undefined; |
| 47 | /** |
| 48 | * The name of the KV binding injected into Astro's session config when |
| 49 | * present on the Worker env. |
| 50 | * @default "SESSION" |
| 51 | */ |
| 52 | readonly sessionKVBindingName?: string | undefined; |
| 53 | /** |
| 54 | * Zero-config sessions: default Astro's session driver to Cloudflare KV |
| 55 | * (binding `sessionKVBindingName`) when none is configured. `false` leaves |
| 56 | * the session config untouched. |
| 57 | * @default true |
| 58 | */ |
| 59 | readonly sessions?: boolean | undefined; |
| 60 | /** |
| 61 | * Auto-inject an in-memory local KV namespace for the session binding: in |
| 62 | * dev on the dev worker, at build time on the workerd prerender worker |
| 63 | * (session-touching pages can prerender without the real namespace). |
| 64 | * Disable when the worker bindings already carry a KV binding with that |
| 65 | * name. |
| 66 | * @default true |
| 67 | */ |
| 68 | readonly sessionDevKV?: boolean | undefined; |
| 69 | } |
| 70 | |
| 71 | export interface AstroCloudflareTarget extends AstroTarget<AstroCloudflareConfig> {} |
| 72 | |
| 73 | /** |
| 74 | * Build the Cloudflare Workers deploy target for Astro. |
| 75 | * |
| 76 | * - `integration` — the wrangler-free `@astrojs/cloudflare` fork over |
| 77 | * `@alchemy.run/cloudflare-runtime/vite` (entry env `ssr`, node-side |
| 78 | * `astro` environment skipped, workerd prerendering by default with a |
| 79 | * `'node'` fallback, passthrough image service). |
| 80 | * - `bundle` — workerd resolve conditions + `cloudflare:` externals |
| 81 | * (informational for Astro: the integration configures the bundler itself). |
| 82 | */ |
| 83 | export const target = ( |
| 84 | config: AstroCloudflareConfig = {}, |
| 85 | ): AstroCloudflareTarget => { |
| 86 | // Written by the integration's `astro:config:done` (the client directory |
| 87 | // before the `base !== "/"` remap nests it); read by `finish` below. |
| 88 | let originalClientDir: string | undefined; |
| 89 | // The resolved build output mode from `astro:config:done`: `"static"` |
| 90 | // means every route is prerendered, so the deploy must be assets-only. |
| 91 | let buildOutput: "static" | "server" | undefined; |
| 92 | return makeDeployTarget({ |
| 93 | platform: "cloudflare", |
| 94 | config, |
| 95 | bundle: { |
| 96 | conditions: ["workerd", "worker", "module", "browser"], |
| 97 | external: ["cloudflare:"], |
| 98 | }, |
| 99 | integration: () => |
| 100 | distilledCloudflare({ |
| 101 | vite: config.worker, |
| 102 | prerenderEnvironment: config.prerenderEnvironment, |
| 103 | sessionKVBindingName: config.sessionKVBindingName, |
| 104 | sessions: config.sessions, |
| 105 | sessionDevKV: config.sessionDevKV, |
| 106 | onOriginalClientDir: (dir) => { |
| 107 | // Normalize (drop the directory-URL trailing slash) so the |
| 108 | // comparison with the collector's `resolve`d path is exact. |
| 109 | originalClientDir = NodePath.resolve(dir); |
| 110 | }, |
| 111 | onBuildOutput: (mode) => { |
| 112 | buildOutput = mode; |
| 113 | }, |
| 114 | }), |
| 115 | // Two in-memory equivalents of upstream's emitted-wrangler.json patches: |
| 116 | // |
| 117 | // - With `base !== "/"` the client build is nested under the base |
| 118 | // (`dist/client/<base>/`), so the collector captures the nested |
| 119 | // directory. The directory that must be served at the URL root is the |
| 120 | // original one (upstream: the wrangler.json `assets.directory` patch). |
| 121 | // - A fully-static build (`buildOutput === "static"` at |
| 122 | // `astro:config:done`: every route prerendered) deploys ASSETS-ONLY: |
| 123 | // the SSR entry astro bundled for prerendering is dropped from the |
| 124 | // output (`serverModules: undefined`) and the client directory carries |
| 125 | // all prerendered HTML, `404.html` included. |
| 126 | finish: (output) => { |
| 127 | const clientDirectory = |
| 128 | originalClientDir !== undefined && |
| 129 | output.clientDirectory !== undefined && |
| 130 | output.clientDirectory !== originalClientDir |
| 131 | ? originalClientDir |
| 132 | : output.clientDirectory; |
| 133 | return Effect.succeed( |
| 134 | buildOutput === "static" |
| 135 | ? { ...output, clientDirectory, serverModules: undefined } |
| 136 | : clientDirectory !== output.clientDirectory |
| 137 | ? { ...output, clientDirectory } |
| 138 | : output, |
| 139 | ); |
| 140 | }, |
| 141 | }); |
| 142 | }; |
| 143 | |
| 144 | export default target; |
| 145 | |
| 146 | export { |
| 147 | distilledCloudflare, |
| 148 | IMAGE_PASSTHROUGH_ENDPOINT, |
| 149 | makeIntegrationPluginOptions, |
| 150 | SERVER_ENTRYPOINT, |
| 151 | usesCloudflareKVSessionDriver, |
| 152 | withDevSessionKv, |
| 153 | withPrerenderSessionKv, |
| 154 | type DistilledCloudflareOptions, |
| 155 | } from "./integration.ts"; |