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 */
18import type { CloudflareVitePluginOptions } from "@alchemy.run/cloudflare-runtime/vite";
19import { makeDeployTarget } from "../core/index.ts";
20import * as Effect from "effect/Effect";
21import * as NodePath from "node:path";
22import { distilledCloudflare } from "./integration.ts";
23import 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 */
31export 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
71export 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 */
83export 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
144export default target;
145
146export {
147 distilledCloudflare,
148 IMAGE_PASSTHROUGH_ENDPOINT,
149 makeIntegrationPluginOptions,
150 SERVER_ENTRYPOINT,
151 usesCloudflareKVSessionDriver,
152 withDevSessionKv,
153 withPrerenderSessionKv,
154 type DistilledCloudflareOptions,
155} from "./integration.ts";