1import * as Effect from "effect/Effect";
2import type { InputProps } from "../../Input.ts";
3import { effectClass } from "../../Util/effect.ts";
4import type { Providers } from "../Providers.ts";
5import type { AssetsConfig } from "../Workers/Assets.ts";
6import {
7 Worker,
8 type NormalizedBindings,
9 type ViteOptions,
10 type WorkerAssetsConfig,
11 type WorkerBindingProps,
12 type WorkerProps,
13} from "../Workers/Worker.ts";
14
15export interface ViteProps<Bindings extends WorkerBindingProps = {}>
16 extends Omit<WorkerProps<Bindings>, "vite" | "main" | "assets">, ViteOptions {
17 /**
18 * Optional configuration for static asset routing behavior.
19 * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc.
20 */
21 assets?: AssetsConfig;
22}
23
24/**
25 * A Cloudflare Worker deployed from a Vite project.
26 *
27 * `Vite` uses the Cloudflare Vite plugin to build both the server bundle
28 * and client assets in a single `vite build` invocation — no manual
29 * `main` entrypoint, build command, output directory, or Wrangler
30 * configuration required.
31 *
32 * Input files are content-hashed (respecting `.gitignore` by default) so
33 * unchanged projects skip the build and deploy entirely.
34 *
35 *
36 * ### Deploying a Static Site
37 * For a pure static site (no SSR), a single call is all you need.
38 * Vite builds the project and Alchemy deploys the output as a
39 * Cloudflare Worker with static assets.
40 *
41 * **Example:** Static Vite site
42 * ```typescript
43 * const site = yield* Cloudflare.Website.Vite("Website");
44 * ```
45 *
46 * ### SSR Frameworks
47 * SSR frameworks like TanStack Start or SolidStart work with a single
48 * call — the `nodejs_compat` compatibility flag is enabled by default
49 * so the server bundle can use Node.js APIs.
50 *
51 * **Example:** TanStack Start
52 * ```typescript
53 * const app = yield* Cloudflare.Website.Vite("TanStackStart");
54 * ```
55 *
56 * **Example:** SolidStart with worker-first routing
57 * ```typescript
58 * const app = yield* Cloudflare.Website.Vite("SolidStart", {
59 * assets: { runWorkerFirst: true },
60 * });
61 * ```
62 *
63 * **Example:** React Router
64 * React Router's server build (`virtual:react-router/server-build`) is a
65 * build manifest with no default export, so it cannot be deployed as the
66 * Worker entry directly. Point `main` at a module that wraps it with
67 * `createRequestHandler` (React Router's Cloudflare template ships this
68 * as `workers/app.ts`):
69 * ```typescript
70 * const app = yield* Cloudflare.Website.Vite("ReactRouter", {
71 * main: "workers/app.ts",
72 * });
73 * ```
74 *
75 * ### React Server Components
76 * Frameworks that emit more than one server environment (e.g. React
77 * Server Components, which split into an `rsc` environment and an `ssr`
78 * environment) need `viteEnvironments` to declare which environment
79 * produces the deployed Worker entry and which additional server
80 * environments to bundle alongside it. The `client` environment is
81 * always deployed as static assets.
82 *
83 * **Example:** React Router with RSC
84 * ```typescript
85 * const app = yield* Cloudflare.Website.Vite("ReactRouterRSC", {
86 * viteEnvironments: {
87 * entry: "rsc",
88 * children: ["ssr"],
89 * },
90 * });
91 * ```
92 *
93 * ### Custom Worker Entry
94 * By default the deployed Worker entry is the server bundle the
95 * framework produces. When the Worker must export more than the
96 * framework's fetch handler — Durable Object classes, additional
97 * handlers — point `main` at your own module that wraps the framework
98 * handler and re-exports the extras. `main` takes precedence over any
99 * entry configured in the Vite config.
100 *
101 * **Example:** Custom entry hosting Durable Objects
102 * ```typescript
103 * const app = yield* Cloudflare.Website.Vite("App", {
104 * main: "worker/index.ts",
105 * viteEnvironments: {
106 * entry: "rsc",
107 * children: ["ssr"],
108 * },
109 * });
110 * ```
111 *
112 * ### Single-Page Applications
113 * For SPAs (React, Vue, etc.), configure asset handling so unmatched
114 * routes fall back to `index.html` and the client router takes over.
115 *
116 * **Example:** Vue SPA
117 * ```typescript
118 * const app = yield* Cloudflare.Website.Vite("Vue", {
119 * assets: {
120 * notFoundHandling: "single-page-application",
121 * },
122 * });
123 * ```
124 *
125 * **Example:** Foldkit
126 * [Foldkit](https://foldkit.dev) apps are client-only Vite projects, so a
127 * single call deploys them — the Foldkit Vite plugin in the app's own
128 * `vite.config.ts` composes with the injected Cloudflare plugin. Enable
129 * `single-page-application` not-found handling so deep links boot the app:
130 * ```typescript
131 * const app = yield* Cloudflare.Website.Vite("Foldkit", {
132 * assets: {
133 * notFoundHandling: "single-page-application",
134 * },
135 * });
136 * ```
137 * {@link Foldkit | Cloudflare.Website.Foldkit} is the same thing with that
138 * default already applied.
139 *
140 * **Example:** Octane SPA
141 * A client-only [OctaneJS](https://octanejs.dev) app (no `octane.config.ts`
142 * routes) is a plain Vite SPA — the `octane()` compiler plugin in the app's
143 * own `vite.config.ts` composes with the injected Cloudflare plugin:
144 * ```typescript
145 * const app = yield* Cloudflare.Website.Vite("Octane", {
146 * assets: {
147 * notFoundHandling: "single-page-application",
148 * },
149 * });
150 * ```
151 * Fullstack Octane apps (routes + SSR in `octane.config.ts`) run their own
152 * two-pass build through Octane's Cloudflare adapter — deploy those with
153 * `Cloudflare.Website.Octane` instead.
154 *
155 * ### Serving on a Zone Route with a Path Prefix
156 * Cloudflare matches static assets against the full request pathname,
157 * so a site attached to a route like `example.com/docs*` only serves
158 * assets whose uploaded paths carry the `/docs` prefix. Set Vite's
159 * `base` in your `vite.config.ts` — the emitted HTML references its
160 * assets under the prefix, and Alchemy keys the uploaded asset manifest
161 * with the same resolved `base` so the two always agree.
162 *
163 * **Example:** vite.config.ts
164 * ```typescript
165 * import { defineConfig } from "vite";
166 *
167 * export default defineConfig({
168 * base: "/docs/",
169 * });
170 * ```
171 *
172 * **Example:** alchemy.run.ts
173 * ```typescript
174 * const docs = yield* Cloudflare.Website.Vite("Docs", {
175 * routes: [{ pattern: "example.com/docs*", zoneName: "example.com" }],
176 * });
177 * ```
178 *
179 * ### Custom Rebuild Scope
180 * By default, every non-gitignored file is hashed to decide whether
181 * a rebuild is needed. Use `memo` to narrow the scope when your
182 * project has large directories that don't affect the build output.
183 *
184 * **Example:** Narrowing the memo scope
185 * ```typescript
186 * const site = yield* Cloudflare.Website.Vite("Docs", {
187 * memo: {
188 * include: ["src/**", "content/**", "package.json"],
189 * },
190 * });
191 * ```
192 *
193 * **Example:** Rebuilding when a sibling workspace package changes
194 * The default scope only hashes files under the project root (plus the
195 * nearest lockfile), so edits to a sibling workspace package the app
196 * imports do not retrigger the build on their own. Add the sibling's
197 * sources with a `../` include glob — and keep `lockfile: true`, since
198 * providing `include` otherwise drops the lockfile from the hash:
199 * ```typescript
200 * const site = yield* Cloudflare.Website.Vite("Web", {
201 * rootDir: "apps/web",
202 * memo: {
203 * include: ["**\/*", "../../packages/env/src/**"],
204 * lockfile: true,
205 * },
206 * });
207 * ```
208 *
209 * ### Class Form
210 * Calling `Vite` with no arguments returns a constructor you can
211 * `extend` to declare the Worker as a named class. The class is both
212 * an `Effect` you can `yield*` to deploy and a type you can reference
213 * elsewhere — useful when other resources need to bind to this Worker.
214 *
215 * **Example:** Declaring a Worker class
216 * ```typescript
217 * class Website extends Cloudflare.Website.Vite<Website>()("Website") {}
218 *
219 * const site = yield* Website;
220 * ```
221 *
222 * @resource
223 * @product Website
224 * @category Workers & Compute
225 */
226export const Vite: {
227 <Self>(): {
228 <const Bindings extends WorkerBindingProps = {}, Req = never>(
229 id: string,
230 propsEff?:
231 | InputProps<ViteProps<Bindings>>
232 | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
233 ): Effect.Effect<Self, never, Req | Providers> & {
234 new (): Worker<{
235 [
236 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
237 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
238 }>;
239 };
240 };
241 <const Bindings extends WorkerBindingProps = {}, Req = never>(
242 id: string,
243 propsEff?:
244 | InputProps<ViteProps<Bindings>>
245 | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
246 ): Effect.Effect<
247 Worker<{
248 [
249 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
250 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
251 }>,
252 never,
253 Req | Providers
254 >;
255} = (<const Bindings extends WorkerBindingProps = {}, Req = never>(
256 id?: string,
257 propsEff?:
258 | InputProps<ViteProps<Bindings>>
259 | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
260) =>
261 id === undefined
262 ? <const Bindings extends WorkerBindingProps = {}, Req = never>(
263 id: string,
264 propsEff?:
265 | InputProps<ViteProps<Bindings>>
266 | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
267 ) => effectClass(Vite(id, propsEff))
268 : Worker(
269 id,
270 Effect.map(
271 Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
272 (props) => ({
273 ...props,
274 main: undefined!,
275 vite: {
276 main: props?.main,
277 rootDir: props?.rootDir,
278 memo: props?.memo,
279 viteEnvironments: props?.viteEnvironments,
280 },
281 }),
282 ),
283 )) as any;