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
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 */
20const NUXT_SOURCE_PROVIDER = "@alchemy.run/frontend-frameworks/nuxt/source";
21
22export 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 */
276export 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;