1// Alchemy modifications are licensed under Apache-2.0.
2// This file includes third-party code; see /THIRD_PARTY_LICENSES.md.
3/**
4 * `@alchemy.run/frontend-frameworks/sveltekit/cloudflare` — the Cloudflare Workers deploy
5 * target for `@alchemy.run/frontend-frameworks/sveltekit`.
6 *
7 * Everything Cloudflare-specific lives here (and in the modules this file
8 * imports): the wrangler-free in-memory kit adapter (worker shim, `_headers`
9 * / `_redirects` / `.assetsignore` generation, the proxy-backed dev
10 * platform) and the finishing pass that re-bundles kit's node-flavored
11 * `_worker.js` output for workerd with rolldown +
12 * `@alchemy.run/cloudflare-runtime/rolldown`.
13 *
14 * The default export is the target factory the framework package applies to
15 * the config it assembles from `SvelteKitOptions`
16 * (`resolveDeployTarget(root, target, config)` — a value, a factory, or this
17 * module's specifier).
18 */
19import cloudflare from "@alchemy.run/cloudflare-runtime/rolldown";
20import * as FrameworkCore from "../core/index.ts";
21import { DeployTargetError, makeDeployTarget } from "../core/index.ts";
22import * as Effect from "effect/Effect";
23import * as FileSystem from "effect/FileSystem";
24import * as NodePath from "node:path";
25import * as Path from "effect/Path";
26import { rolldown } from "rolldown";
27import { makeCloudflareAdapter } from "./Adapter.ts";
28import type { SvelteKitTarget, SvelteKitTargetConfig } from "./SvelteKit.ts";
29
30export {
31 appendHeaders,
32 generateAssetsIgnore,
33 generateHeaders,
34 generateRedirects,
35 makeCloudflareAdapter,
36 makeDevPlatformEmulator,
37 type CloudflareAdapter,
38 type CloudflareAdapterOptions,
39 type CloudflareAdapterResult,
40 type CloudflareDevPlatformOptions,
41 type CloudflareEmulator,
42 type DevPlatformProxy,
43 type OpenDevPlatformProxy,
44 type OpenDevPlatformProxyOptions,
45} from "./Adapter.ts";
46export { generateWorkerShim, type WorkerShimOptions } from "./WorkerShim.ts";
47
48const fail = (message: string, cause?: unknown) =>
49 new DeployTargetError({ platform: "cloudflare", message, cause });
50
51/**
52 * Create the Cloudflare Workers {@link SvelteKitTarget}:
53 *
54 * - `adapter(context)` — the in-memory, wrangler-free fork of
55 * `@sveltejs/adapter-cloudflare` (always Workers mode; generates the worker
56 * shim with real relative imports). In dev it supplies the proxy-backed
57 * `platform`: `context.dev.bindings` (cloudflare-runtime binding hooks)
58 * served through `getPlatformProxy`'s workerd instance, `context.dev.env`
59 * literals overriding same-named proxied values, a `caches` that actually
60 * round-trips, and wrangler-parity `cf`/`ctx` mocks.
61 * - `finish(output, context)` — re-bundles the adapter's unbundled
62 * `_worker.js` (and the whole `.svelte-kit/output/server` graph it imports)
63 * for workerd, replacing the bundling `wrangler deploy` performs for the
64 * upstream adapter. Produces entry-first `serverModules` plus the external
65 * workspace set.
66 * - `bundle` — workerd resolve conditions and the `cloudflare:` externals.
67 */
68export const makeCloudflareTarget = (
69 config: SvelteKitTargetConfig = {},
70): SvelteKitTarget =>
71 makeDeployTarget({
72 platform: "cloudflare",
73 config,
74 bundle: {
75 conditions: ["workerd", "worker", "module", "browser"],
76 external: ["cloudflare:"],
77 },
78 adapter: (context) =>
79 makeCloudflareAdapter({
80 ...config.adapter,
81 root: context.root,
82 ...(context.dev !== undefined
83 ? {
84 platform: {
85 env: context.dev.env,
86 bindings: context.dev.bindings,
87 services: context.dev.services,
88 compatibilityDate: config.compatibilityDate,
89 compatibilityFlags: config.compatibilityFlags,
90 },
91 }
92 : undefined),
93 }),
94 finish: (output, context) =>
95 Effect.gen(function* () {
96 const fs = yield* FileSystem.FileSystem;
97 const path = yield* Path.Path;
98 const entry = context.entry;
99 if (entry === undefined) {
100 return yield* Effect.fail(
101 fail(
102 "The SvelteKit build produced no on-disk worker entry for the finishing pass " +
103 "(context.entry is missing)",
104 ),
105 );
106 }
107 const root = context.root;
108 const distDirectory =
109 output.distDirectory ?? path.resolve(root, "dist");
110 const serverOutDir = path.join(distDirectory, "server");
111 yield* fs
112 .remove(serverOutDir, { recursive: true, force: true })
113 .pipe(
114 Effect.mapError((error) =>
115 fail("Failed to clean dist/server", error),
116 ),
117 );
118
119 const externalDirectories = yield* Effect.tryPromise({
120 try: async () => {
121 const bundle = await rolldown({
122 cwd: root,
123 input: entry,
124 plugins: cloudflare({
125 ...(config.compatibilityDate !== undefined
126 ? { compatibilityDate: config.compatibilityDate }
127 : undefined),
128 compatibilityFlags: config.compatibilityFlags ?? [
129 "nodejs_compat",
130 ],
131 exports: ["default"],
132 }),
133 });
134 try {
135 const { output: chunks } = await bundle.write({
136 dir: serverOutDir,
137 format: "esm",
138 entryFileNames: "index.js",
139 chunkFileNames: "chunks/[name].js",
140 sourcemap: false,
141 });
142 const directories = new Set<string>();
143 for (const chunk of chunks) {
144 if (chunk.type !== "chunk") continue;
145 for (const id of Object.keys(chunk.modules)) {
146 if (
147 !NodePath.isAbsolute(id) ||
148 id.includes("node_modules") ||
149 id.startsWith(root)
150 ) {
151 continue;
152 }
153 directories.add(NodePath.dirname(id));
154 }
155 }
156 return directories;
157 } finally {
158 await bundle.close();
159 }
160 },
161 catch: (error) =>
162 fail("Failed to bundle the worker for workerd", error),
163 });
164
165 const wrapCollectorError = (error: FrameworkCore.CollectorError) =>
166 fail(error.message, error.cause);
167 const modules = yield* FrameworkCore.readServerModulesFromDisk({
168 directory: serverOutDir,
169 prefix: "server",
170 }).pipe(Effect.mapError(wrapCollectorError));
171 const serverModules = FrameworkCore.sortServerModules(
172 modules,
173 NodePath.join("server", "index.js"),
174 );
175 const externalWorkspaces =
176 yield* FrameworkCore.collectExternalWorkspaces(
177 externalDirectories,
178 ).pipe(Effect.mapError(wrapCollectorError));
179
180 return {
181 ...output,
182 distDirectory,
183 serverModules,
184 externalWorkspaces,
185 } satisfies FrameworkCore.BuildOutput;
186 }),
187 });
188
189/**
190 * The deploy-target module contract (`resolveDeployTarget` accepts the
191 * default export — or the named `target` export — as a value or factory).
192 */
193export const target = makeCloudflareTarget;
194
195export default makeCloudflareTarget;