| 1 | import * as Effect from "effect/Effect"; |
| 2 | import type { InputProps } from "../../Input.ts"; |
| 3 | import { effectClass } from "../../Util/effect.ts"; |
| 4 | import type { Providers } from "../Providers.ts"; |
| 5 | import type { AssetsConfig } from "../Workers/Assets.ts"; |
| 6 | import { |
| 7 | Worker, |
| 8 | type NormalizedBindings, |
| 9 | type ViteOptions, |
| 10 | type WorkerAssetsConfig, |
| 11 | type WorkerBindingProps, |
| 12 | type WorkerProps, |
| 13 | } from "../Workers/Worker.ts"; |
| 14 | |
| 15 | export 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 | */ |
| 226 | export 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; |