1import * as Effect from "effect/Effect";
2import type { MemoOptions } from "../../Command/Memo.ts";
3import type { InputProps } from "../../Input.ts";
4import { effectClass } from "../../Util/effect.ts";
5import type { Providers } from "../Providers.ts";
6import type { AssetsConfig } from "../Workers/Assets.ts";
7import {
8 Worker,
9 type NormalizedBindings,
10 type WorkerAssetsConfig,
11 type WorkerBindingProps,
12 type WorkerProps,
13} from "../Workers/Worker.ts";
14
15export interface SvelteKitProps<
16 Bindings extends WorkerBindingProps = {},
17> extends Omit<
18 WorkerProps<Bindings>,
19 "vite" | "main" | "assets" | "source" | "script" | "bundle"
20> {
21 /**
22 * SvelteKit project root (the directory containing `package.json` and
23 * `src/routes`). Relative paths resolve from the process working
24 * directory.
25 * @default process.cwd()
26 */
27 rootDir?: string;
28 /**
29 * Controls which files are content-hashed to decide whether a rebuild is
30 * needed. By default every non-gitignored file under `rootDir` (plus the
31 * nearest package-manager lockfile) is hashed; narrow the scope with
32 * `include`/`exclude` globs when the project sits in a large repository.
33 */
34 memo?: MemoOptions;
35 /**
36 * SvelteKit config overrides for the `sveltekit(...)` plugin call, merged
37 * over the options of the user's own call (these win). JSON-serializable
38 * only — no `preprocess`/`vitePlugin`/functions; construction-time options
39 * (`preprocess`, `extensions`, `compilerOptions`, `vitePlugin`) can only
40 * apply when no user `vite.config.*` exists. The `adapter` field is
41 * always owned by alchemy.
42 */
43 kit?: Record<string, unknown>;
44 /**
45 * Optional configuration for static asset routing behavior.
46 * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc.
47 */
48 assets?: AssetsConfig;
49}
50
51// These options are inspected while constructing the Worker. Resolve them in
52// the outer props Effect; pass-through properties can remain deferred Inputs.
53type SvelteKitInput<Bindings extends WorkerBindingProps> = InputProps<
54 SvelteKitProps<Bindings>,
55 "assets"
56>;
57
58/**
59 * A Cloudflare Worker deployed from a SvelteKit project.
60 *
61 * `SvelteKit` builds the app with SvelteKit's own Vite pipeline and a
62 * wrangler-free in-memory Cloudflare adapter, then re-bundles the
63 * Node-flavored server output for workerd. A project-owned
64 * `vite.config.*` loads natively (its `sveltekit(...)` options apply) —
65 * no `svelte.config.js` (kit v3 dropped it), no
66 * `@sveltejs/adapter-cloudflare`, no Wrangler configuration required.
67 * Client assets and prerendered pages are deployed as Worker static
68 * assets; dynamic routes are served by the generated Worker.
69 *
70 * The `@alchemy.run/frontend-frameworks` package must be installed in your
71 * project — its `/sveltekit` export is loaded dynamically at deploy time.
72 *
73 * Input files are content-hashed (respecting `.gitignore` by default) so
74 * unchanged projects skip the build and deploy entirely.
75 *
76 * SvelteKit's server code runs under `nodejs_compat` (the server graph is
77 * built for Node), so the flag is always included in the Worker's
78 * compatibility flags.
79 *
80 * Note on local dev: `alchemy dev` runs SvelteKit's own Vite dev server
81 * (Node SSR with full HMR). `platform.env` carries the Worker's real
82 * Cloudflare bindings (KV, R2, D1, ...) served by the cloudflare-runtime
83 * platform proxy, with literal `env` values (strings and secrets)
84 * overlaid.
85 *
86 *
87 * ### Deploying a SvelteKit App
88 * A single call builds and deploys the app — server-rendered routes,
89 * prerendered pages, and client assets included.
90 *
91 * **Example:** Basic SvelteKit site
92 * ```typescript
93 * const site = yield* Cloudflare.Website.SvelteKit("Website");
94 * ```
95 *
96 * ### Bindings
97 * Values passed via `env` are exposed to server routes through
98 * SvelteKit's `platform.env`.
99 *
100 * **Example:** Reading env from a server route
101 * ```typescript
102 * const site = yield* Cloudflare.Website.SvelteKit("Website", {
103 * env: {
104 * API_KEY: Config.Redacted("API_KEY"),
105 * },
106 * });
107 *
108 * // src/routes/+page.server.ts
109 * // export const load = ({ platform }) => ({
110 * // hasKey: platform?.env?.API_KEY !== undefined,
111 * // });
112 * ```
113 *
114 * ### Kit Options and 404 Handling
115 * Kit options live in the `sveltekit(...)` call in your
116 * `vite.config.ts`, which loads natively. Fallback-page behavior is
117 * driven by the platform-native `assets.notFoundHandling` knob — the
118 * build generates the matching fallback page (rendering the app shell,
119 * so kit's own error page shows).
120 *
121 * **Example:** App-shell 404 fallback
122 * ```typescript
123 * const site = yield* Cloudflare.Website.SvelteKit("Website", {
124 * assets: {
125 * notFoundHandling: "404-page",
126 * },
127 * });
128 * ```
129 *
130 * The `kit` prop is a deploy-time override bag merged over your own
131 * `sveltekit(...)` options (the prop wins) — useful for per-stage values
132 * the config file can't compute. JSON-serializable values only.
133 *
134 * **Example:** Deploy-time kit overrides
135 * ```typescript
136 * const site = yield* Cloudflare.Website.SvelteKit("Website", {
137 * kit: {
138 * paths: { base: "/docs" },
139 * },
140 * });
141 * ```
142 *
143 * ### Custom Rebuild Scope
144 * By default, every non-gitignored file is hashed to decide whether a
145 * rebuild is needed. Use `memo` to narrow the scope when the project
146 * lives in a large repository.
147 *
148 * **Example:** Narrowing the memo scope
149 * ```typescript
150 * const site = yield* Cloudflare.Website.SvelteKit("Website", {
151 * memo: {
152 * include: ["src/**", "static/**", "package.json"],
153 * },
154 * });
155 * ```
156 *
157 * ### Class Form
158 * Calling `SvelteKit` with no arguments returns a constructor you can
159 * `extend` to declare the Worker as a named class. The class is both an
160 * `Effect` you can `yield*` to deploy and a type you can reference
161 * elsewhere — useful when other resources need to bind to this Worker.
162 *
163 * **Example:** Declaring a Worker class
164 * ```typescript
165 * class Website extends Cloudflare.Website.SvelteKit<Website>()(
166 * "Website",
167 * ) {}
168 *
169 * const site = yield* Website;
170 * ```
171 *
172 * @resource
173 * @product Website
174 * @category Workers & Compute
175 */
176export const SvelteKit: {
177 <Self>(): {
178 <const Bindings extends WorkerBindingProps = {}, Req = never>(
179 id: string,
180 propsEff?:
181 | SvelteKitInput<Bindings>
182 | Effect.Effect<SvelteKitInput<Bindings>, never, Req>,
183 ): Effect.Effect<Self, never, Req | Providers> & {
184 new (): Worker<{
185 [
186 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
187 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
188 }>;
189 };
190 };
191 <const Bindings extends WorkerBindingProps = {}, Req = never>(
192 id: string,
193 propsEff?:
194 | SvelteKitInput<Bindings>
195 | Effect.Effect<SvelteKitInput<Bindings>, never, Req>,
196 ): Effect.Effect<
197 Worker<{
198 [
199 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
200 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
201 }>,
202 never,
203 Req | Providers
204 >;
205} = (<const Bindings extends WorkerBindingProps = {}, Req = never>(
206 id?: string,
207 propsEff?:
208 | SvelteKitInput<Bindings>
209 | Effect.Effect<SvelteKitInput<Bindings>, never, Req>,
210) =>
211 id === undefined
212 ? <const Bindings extends WorkerBindingProps = {}, Req = never>(
213 id: string,
214 propsEff?:
215 | SvelteKitInput<Bindings>
216 | Effect.Effect<SvelteKitInput<Bindings>, never, Req>,
217 ) => effectClass(SvelteKit(id, propsEff))
218 : Worker(
219 id,
220 Effect.map(
221 Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
222 (props) => ({
223 ...props,
224 // SvelteKit's server graph is built for Node and needs
225 // `nodejs_compat` — `getCompatibility` already adds it to every
226 // non-python Worker.
227 assets: props?.assets,
228 source: {
229 provider: "@alchemy.run/frontend-frameworks/sveltekit/source",
230 devMode: "server",
231 rootDir: props?.rootDir,
232 options: {
233 rootDir: props?.rootDir,
234 memo: props?.memo,
235 kit: props?.kit,
236 // The adapter's build-time page GENERATION (404.html /
237 // app-shell index.html) is derived from the one
238 // platform-native knob, `assets.notFoundHandling`, so a
239 // single prop configures generation AND serving — the two
240 // halves can never disagree. The generated 404-page
241 // renders the app shell so kit's own error page shows.
242 ...(props?.assets?.notFoundHandling !== undefined &&
243 props.assets.notFoundHandling !== "none"
244 ? {
245 adapter: {
246 notFoundHandling: props.assets.notFoundHandling,
247 fallback: "spa",
248 },
249 }
250 : {}),
251 },
252 },
253 }),
254 ),
255 )) as any;