| 1 | import * as Effect from "effect/Effect"; |
| 2 | import type { MemoOptions } from "../../Command/Memo.ts"; |
| 3 | import type { InputProps } from "../../Input.ts"; |
| 4 | import { effectClass } from "../../Util/effect.ts"; |
| 5 | import type { Providers } from "../Providers.ts"; |
| 6 | import type { AssetsConfig } from "../Workers/Assets.ts"; |
| 7 | import { |
| 8 | Worker, |
| 9 | type NormalizedBindings, |
| 10 | type WorkerAssetsConfig, |
| 11 | type WorkerBindingProps, |
| 12 | type WorkerProps, |
| 13 | } from "../Workers/Worker.ts"; |
| 14 | |
| 15 | /** |
| 16 | * The specifier of the Nuxt source-provider module. The package must be |
| 17 | * installed in the user's project — `loadSource` fails with a |
| 18 | * `SourceProviderError` naming it otherwise. |
| 19 | */ |
| 20 | const NUXT_SOURCE_PROVIDER = "@alchemy.run/frontend-frameworks/nuxt/source"; |
| 21 | |
| 22 | export interface NuxtProps< |
| 23 | Bindings extends WorkerBindingProps = {}, |
| 24 | > extends Omit< |
| 25 | WorkerProps<Bindings>, |
| 26 | "vite" | "main" | "assets" | "source" | "script" | "bundle" |
| 27 | > { |
| 28 | /** |
| 29 | * Nuxt project root (the directory containing `nuxt.config.ts`). |
| 30 | * Relative paths resolve from the process working directory. |
| 31 | * @default process.cwd() |
| 32 | */ |
| 33 | rootDir?: string; |
| 34 | /** |
| 35 | * Overrides the module that becomes the deployed Worker entry (nitro's |
| 36 | * entry/exports seam). Relative paths resolve from {@link rootDir}. |
| 37 | * |
| 38 | * By default nitro's own `cloudflare_module` entry is deployed. Point |
| 39 | * `main` at a custom module when the deployed Worker must export more |
| 40 | * than the framework's fetch handler — e.g. Durable Object classes. The |
| 41 | * custom entry wraps nitro's handler by importing it from |
| 42 | * `nitropack/presets/cloudflare/runtime/cloudflare-module` and |
| 43 | * re-exports the extras: |
| 44 | * |
| 45 | * ```typescript |
| 46 | * // worker-entry.ts |
| 47 | * import nitroHandler from "nitropack/presets/cloudflare/runtime/cloudflare-module"; |
| 48 | * export class Counter extends DurableObject {} |
| 49 | * export default nitroHandler; |
| 50 | * ``` |
| 51 | */ |
| 52 | main?: string; |
| 53 | /** |
| 54 | * Controls which files are content-hashed to decide whether a rebuild is |
| 55 | * needed. By default every non-gitignored file under `rootDir` (plus the |
| 56 | * nearest package-manager lockfile) is hashed; narrow the scope with |
| 57 | * `include`/`exclude` globs when the project sits in a large repository. |
| 58 | */ |
| 59 | memo?: MemoOptions; |
| 60 | /** |
| 61 | * Optional configuration for static asset routing behavior. |
| 62 | * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc. |
| 63 | */ |
| 64 | assets?: AssetsConfig; |
| 65 | /** |
| 66 | * Nuxt config overrides merged over the project's own `nuxt.config.ts` |
| 67 | * (the highest-priority c12 layer — a value here wins over the file). |
| 68 | * Use it for deploy-time values the config file can't express, e.g. |
| 69 | * per-stage `runtimeConfig`; `nuxt.config.ts` remains the primary home |
| 70 | * for everything else. |
| 71 | * |
| 72 | * Must be JSON-serializable — no functions, plugins, or modules (the |
| 73 | * value persists in state and participates in the rebuild hash). |
| 74 | * `nitro.preset` is always owned by the deploy target and cannot be |
| 75 | * overridden here. |
| 76 | */ |
| 77 | nuxt?: Record<string, unknown>; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * A Cloudflare Worker deployed from a [Nuxt](https://nuxt.com) project. |
| 82 | * |
| 83 | * `Nuxt` builds the app programmatically through the project's own |
| 84 | * `@nuxt/kit` with nitro's `cloudflare_module` preset — the project's |
| 85 | * `nuxt.config.ts` loads natively, no `nitro.preset` edits, no Wrangler |
| 86 | * configuration, and no build command required. The nitro server bundle |
| 87 | * deploys as the Worker script; client assets and prerendered pages |
| 88 | * (`.output/public`) deploy as Worker static assets. |
| 89 | * |
| 90 | * Requires the `@alchemy.run/frontend-frameworks` package to be installed in |
| 91 | * your project; the integration is loaded from its `/nuxt` export. Input files |
| 92 | * are content-hashed |
| 93 | * (respecting `.gitignore` by default) so unchanged projects skip the |
| 94 | * build and deploy entirely. |
| 95 | * |
| 96 | * The server build uses nitro's hybrid workerd Node compatibility |
| 97 | * (`cloudflare.nodeCompat`), which relies on workerd's native `node:*` |
| 98 | * modules — the `nodejs_compat` compatibility flag is always included in |
| 99 | * the Worker's compatibility flags to match. |
| 100 | * |
| 101 | * On local dev: `alchemy dev` runs Nuxt's own dev server (nitro dev, SSR |
| 102 | * in a Node worker thread, full HMR) with the Worker's bindings served on |
| 103 | * `event.context.cloudflare` through cloudflare-runtime's platform proxy — |
| 104 | * wrangler-free. Literal `env` values overlay the proxied bindings; |
| 105 | * resource bindings (KV, R2, D1, ...) round-trip to the proxy's local |
| 106 | * workerd instance, so dev state is live and shared. Durable Objects |
| 107 | * declared via a custom `main` entry only exist in the production build |
| 108 | * and are not servable in dev yet. |
| 109 | * |
| 110 | * |
| 111 | * ### Deploying a Nuxt App |
| 112 | * A single call builds and deploys the app — server-rendered pages, API |
| 113 | * routes, prerendered pages, and client assets included. |
| 114 | * |
| 115 | * **Example:** Basic Nuxt site |
| 116 | * ```typescript |
| 117 | * const site = yield* Cloudflare.Website.Nuxt("Website"); |
| 118 | * ``` |
| 119 | * |
| 120 | * **Example:** Nuxt project in a subdirectory |
| 121 | * ```typescript |
| 122 | * const site = yield* Cloudflare.Website.Nuxt("Website", { |
| 123 | * rootDir: "apps/web", |
| 124 | * }); |
| 125 | * ``` |
| 126 | * |
| 127 | * ### Bindings |
| 128 | * Values passed via `env` are exposed to server routes and SSR through |
| 129 | * nitro's `cloudflare_module` runtime contract: |
| 130 | * `event.context.cloudflare.env` (plus `event.context.cf` and |
| 131 | * `event.context.cloudflare.context.waitUntil`). |
| 132 | * |
| 133 | * **Example:** Reading env from an API route |
| 134 | * ```typescript |
| 135 | * const site = yield* Cloudflare.Website.Nuxt("Website", { |
| 136 | * env: { |
| 137 | * API_KEY: Config.Redacted("API_KEY"), |
| 138 | * }, |
| 139 | * }); |
| 140 | * |
| 141 | * // server/api/hello.ts |
| 142 | * // export default defineEventHandler((event) => ({ |
| 143 | * // hasKey: event.context.cloudflare?.env?.API_KEY !== undefined, |
| 144 | * // })); |
| 145 | * ``` |
| 146 | * |
| 147 | * **Example:** Binding an R2 bucket |
| 148 | * ```typescript |
| 149 | * const bucket = yield* Cloudflare.R2.Bucket("Uploads"); |
| 150 | * |
| 151 | * const site = yield* Cloudflare.Website.Nuxt("Website", { |
| 152 | * env: { |
| 153 | * UPLOADS: bucket, |
| 154 | * }, |
| 155 | * }); |
| 156 | * ``` |
| 157 | * |
| 158 | * ### Prerendering |
| 159 | * Routes marked for prerendering in `routeRules` (or via |
| 160 | * `nitro.prerender`) render at build time into `.output/public` and are |
| 161 | * served as static assets — no Worker invocation. Configure them in |
| 162 | * your `nuxt.config.ts`, which loads natively: |
| 163 | * |
| 164 | * **Example:** Prerendering a route (nuxt.config.ts) |
| 165 | * ```typescript |
| 166 | * // nuxt.config.ts |
| 167 | * export default defineNuxtConfig({ |
| 168 | * routeRules: { |
| 169 | * "/about": { prerender: true }, |
| 170 | * }, |
| 171 | * }); |
| 172 | * ``` |
| 173 | * |
| 174 | * ### Config Overrides |
| 175 | * `nuxt.config.ts` is the primary home for Nuxt configuration — it loads |
| 176 | * natively. The `nuxt` prop layers deploy-time overrides on top (the |
| 177 | * highest-priority c12 layer) for values the file can't express, like |
| 178 | * per-stage settings. The bag must be JSON-serializable — no functions, |
| 179 | * plugins, or modules — and `nitro.preset` stays owned by the deploy |
| 180 | * target. |
| 181 | * |
| 182 | * **Example:** Deploy-time config overrides |
| 183 | * ```typescript |
| 184 | * const site = yield* Cloudflare.Website.Nuxt("Website", { |
| 185 | * nuxt: { |
| 186 | * app: { baseURL: "/docs/" }, |
| 187 | * runtimeConfig: { |
| 188 | * public: { apiBase: "https://api.example.com" }, |
| 189 | * }, |
| 190 | * }, |
| 191 | * }); |
| 192 | * ``` |
| 193 | * |
| 194 | * ### Custom Worker Exports (Durable Objects) |
| 195 | * Nitro's entry module is the Worker's exports seam. Point `main` at your |
| 196 | * own module that re-exports nitro's runtime handler (imported from |
| 197 | * `nitropack/presets/cloudflare/runtime/cloudflare-module`) and adds |
| 198 | * extra exports — Durable Object classes must live on the deployed |
| 199 | * Worker for their namespace bindings to resolve. Every framework route |
| 200 | * keeps working through the re-exported handler. |
| 201 | * |
| 202 | * **Example:** Custom entry hosting a Durable Object |
| 203 | * ```typescript |
| 204 | * // worker-entry.ts |
| 205 | * // import nitroHandler from "nitropack/presets/cloudflare/runtime/cloudflare-module"; |
| 206 | * // export class Counter extends DurableObject { ... } |
| 207 | * // export default nitroHandler; |
| 208 | * |
| 209 | * const site = yield* Cloudflare.Website.Nuxt("Website", { |
| 210 | * main: "worker-entry.ts", |
| 211 | * env: { |
| 212 | * COUNTER: Cloudflare.DurableObject("Counter", { |
| 213 | * className: "Counter", |
| 214 | * }), |
| 215 | * }, |
| 216 | * }); |
| 217 | * ``` |
| 218 | * |
| 219 | * ### Dev |
| 220 | * `alchemy dev` runs Nuxt's own dev server (nitro dev, full HMR) with |
| 221 | * `event.context.cloudflare` served wrangler-free through |
| 222 | * cloudflare-runtime's platform proxy: resource bindings resolve against |
| 223 | * a local workerd instance, and literal `env` values overlay them. |
| 224 | * |
| 225 | * **Example:** Reading bindings in dev and production alike |
| 226 | * ```typescript |
| 227 | * // server/api/greeting.ts — identical code in dev and deployed |
| 228 | * // export default defineEventHandler((event) => ({ |
| 229 | * // greeting: event.context.cloudflare?.env?.GREETING, |
| 230 | * // })); |
| 231 | * const site = yield* Cloudflare.Website.Nuxt("Website", { |
| 232 | * env: { GREETING: "hello" }, |
| 233 | * }); |
| 234 | * ``` |
| 235 | * |
| 236 | * ### Custom Rebuild Scope |
| 237 | * By default, every non-gitignored file is hashed to decide whether a |
| 238 | * rebuild is needed. Use `memo` to narrow the scope when the project |
| 239 | * lives in a large repository. |
| 240 | * |
| 241 | * **Example:** Narrowing the memo scope |
| 242 | * ```typescript |
| 243 | * const site = yield* Cloudflare.Website.Nuxt("Website", { |
| 244 | * memo: { |
| 245 | * include: ["app/**", "server/**", "public/**", "nuxt.config.ts", "package.json"], |
| 246 | * }, |
| 247 | * }); |
| 248 | * ``` |
| 249 | * |
| 250 | * ### Limitations |
| 251 | * Nitro's `isr` route rule (incremental static regeneration) is |
| 252 | * implemented only by the Vercel and Netlify presets — on Cloudflare it |
| 253 | * is silently ignored at build time, and the route renders on demand in |
| 254 | * the Worker like any other SSR route. Use `prerender` for build-time |
| 255 | * static routes, or `cache` route rules for runtime caching. |
| 256 | * |
| 257 | * ### Class Form |
| 258 | * Calling `Nuxt` with no arguments returns a constructor you can |
| 259 | * `extend` to declare the Worker as a named class. The class is both an |
| 260 | * `Effect` you can `yield*` to deploy and a type you can reference |
| 261 | * elsewhere — useful when other resources need to bind to this Worker. |
| 262 | * |
| 263 | * **Example:** Declaring a Worker class |
| 264 | * ```typescript |
| 265 | * class Website extends Cloudflare.Website.Nuxt<Website>()( |
| 266 | * "Website", |
| 267 | * ) {} |
| 268 | * |
| 269 | * const site = yield* Website; |
| 270 | * ``` |
| 271 | * |
| 272 | * @resource |
| 273 | * @product Website |
| 274 | * @category Workers & Compute |
| 275 | */ |
| 276 | export const Nuxt: { |
| 277 | <Self>(): { |
| 278 | <const Bindings extends WorkerBindingProps = {}, Req = never>( |
| 279 | id: string, |
| 280 | propsEff?: |
| 281 | | InputProps<NuxtProps<Bindings>> |
| 282 | | Effect.Effect<InputProps<NuxtProps<Bindings>>, never, Req>, |
| 283 | ): Effect.Effect<Self, never, Req | Providers> & { |
| 284 | new (): Worker<{ |
| 285 | [ |
| 286 | binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig> |
| 287 | ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding]; |
| 288 | }>; |
| 289 | }; |
| 290 | }; |
| 291 | <const Bindings extends WorkerBindingProps = {}, Req = never>( |
| 292 | id: string, |
| 293 | propsEff?: |
| 294 | | InputProps<NuxtProps<Bindings>> |
| 295 | | Effect.Effect<InputProps<NuxtProps<Bindings>>, never, Req>, |
| 296 | ): Effect.Effect< |
| 297 | Worker<{ |
| 298 | [ |
| 299 | binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig> |
| 300 | ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding]; |
| 301 | }>, |
| 302 | never, |
| 303 | Req | Providers |
| 304 | >; |
| 305 | } = (<const Bindings extends WorkerBindingProps = {}, Req = never>( |
| 306 | id?: string, |
| 307 | propsEff?: |
| 308 | | InputProps<NuxtProps<Bindings>> |
| 309 | | Effect.Effect<InputProps<NuxtProps<Bindings>>, never, Req>, |
| 310 | ) => |
| 311 | id === undefined |
| 312 | ? <const Bindings extends WorkerBindingProps = {}, Req = never>( |
| 313 | id: string, |
| 314 | propsEff?: |
| 315 | | InputProps<NuxtProps<Bindings>> |
| 316 | | Effect.Effect<InputProps<NuxtProps<Bindings>>, never, Req>, |
| 317 | ) => effectClass(Nuxt(id, propsEff)) |
| 318 | : Worker( |
| 319 | id, |
| 320 | Effect.map( |
| 321 | Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff), |
| 322 | (props) => ({ |
| 323 | ...props, |
| 324 | // The server build uses nitro's hybrid workerd node-compat |
| 325 | // (`cloudflare.nodeCompat: true`), which relies on workerd's |
| 326 | // native `node:*` modules — `getCompatibility` already adds |
| 327 | // `nodejs_compat` to every non-python Worker. |
| 328 | // `main` is the source provider's user-entry seam (nitro's |
| 329 | // entry), not the Worker's own bundling entry. |
| 330 | main: undefined!, |
| 331 | source: { |
| 332 | provider: NUXT_SOURCE_PROVIDER, |
| 333 | devMode: "server", |
| 334 | rootDir: props?.rootDir, |
| 335 | options: { |
| 336 | rootDir: props?.rootDir, |
| 337 | main: props?.main, |
| 338 | memo: props?.memo, |
| 339 | nuxt: props?.nuxt, |
| 340 | }, |
| 341 | }, |
| 342 | }), |
| 343 | ), |
| 344 | )) as any; |