1# @alchemy.run/frontend-frameworks/nuxt
2
3Wrangler-free Nuxt integration: programmatic build and dev for Nuxt projects,
4with the deploy platform passed in as a **deploy target** value. Cloudflare
5Workers is the built-in target (`@alchemy.run/frontend-frameworks/nuxt/cloudflare`); the
6framework half of this package contains no Cloudflare code at all.
7
8## Architecture: framework half × target half
9
10The package follows the framework × deploy-target split defined in
11`@alchemy.run/frontend-frameworks/core` (see its README for the full `DeployTarget`
12contract):
13
14| Module | Role |
15| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16| `@alchemy.run/frontend-frameworks/nuxt` (`src/nuxt/Nuxt.ts`, `src/nuxt/index.ts`) | **Framework half.** Drives the PROJECT's `@nuxt/kit` programmatically (loaded via the universally-resolvable `nuxt/kit` subpath): `loadNuxt` for build and dev, hook registration, nitro output mapping onto framework-core's `BuildOutput`, and the `Framework` service implementation. Target-agnostic — zero Cloudflare imports. |
17| `@alchemy.run/frontend-frameworks/nuxt/cloudflare` (`src/nuxt/cloudflare.ts`) | **Target half.** The Cloudflare Workers `NuxtTarget`: nitro's `cloudflare_module` preset, the wrangler-free nitro config enforcement (`deployConfig: false`, `nodeCompat: true`), the user-entry seam (`main` → nitro's entry), and the proxy-backed dev platform (`src/nuxt/dev/host.ts`). |
18| `@alchemy.run/frontend-frameworks/nuxt/source` (`src/nuxt/source.ts`) | Alchemy Worker **source provider** (structural `WorkerSourceModule` contract): maps the Nuxt build onto alchemy's bundle/assets/hash slots, plus a rebuild-free memo hash and nitro-dev `dev()`. Cloudflare-specific by nature; it passes the cloudflare target factory to the framework directly. |
19| `src/nuxt/dev/{host,plugin,shared}.ts` | The **dev transport**: host half opens cloudflare-runtime's platform proxy and injects connect info; worker half is a dev-only nitro plugin that reconstructs `event.context.cloudflare` inside nitro's dev SSR worker thread. |
20
21Background on why the integration is shaped this way:
22
23- **The project's `nuxt.config.ts` loads natively.** `loadNuxt({ cwd, dev,
24ready: false, overrides })` resolves the user's config, layers, and modules
25 through c12 exactly as `nuxi` would. The integration's injection rides the
26 highest-priority `overrides` layer (`src/UserConfig.ts`); the user's file
27 stays authoritative for everything not named there.
28- **Hooks register before `nuxt.ready()`.** With `ready: true` the hooks fire
29 inside `loadNuxt` itself and a late registration misses them, so the
30 package always loads with `ready: false`, registers `nitro:config` /
31 `nitro:init`, then calls `ready()` + `buildNuxt` — the same flow `nuxi`
32 drives.
33- **The deploy target owns the nitro preset.** `nitro:config` gets the last
34 word on the keys the integration owns. Because the overrides layer would
35 silently shadow a user-set preset in the merged options, the user's raw
36 value is read from the project's own config layer
37 (`nuxt.options._layers[0].config`) and a foreign `nitro.preset` **fails the
38 build with an actionable error** (`findPresetConflict`) — the user's nitro
39 output would never be deployed.
40- **The `cloudflare_module` output needs no finishing pass.** Nitro emits
41 self-contained workerd ESM: `.output/server/index.mjs` + chunks and
42 `.output/public` (prerendered pages and `_headers` merged in). The
43 `BuildOutput` is read straight from disk — `serverModules` entry-first
44 from `.output/server`, `clientDirectory` = `.output/public`.
45
46### The `NuxtTarget` contract
47
48The framework half declares what any deploy target must provide
49(`src/Nuxt.ts`):
50
51```ts
52interface NuxtTarget extends DeployTarget<NuxtTargetConfig> {
53 /** The nitro deployment preset this target builds with. */
54 nitroPreset: string;
55 /** Last-word mutation of the resolved nitro config (`nitro:config`). */
56 configureNitro?(nitroConfig: NitroConfigSlice, context: NuxtNitroContext): void;
57 /** Acquire the dev platform (scoped) and return the injection dev needs. */
58 devPlatform?(context: NuxtDevPlatformContext): Effect<NuxtDevPlatform, DeployTargetError, Scope>;
59}
60```
61
62- `nitroPreset` — enforced on the resolved nitro config after every module
63 and layer has contributed.
64- `configureNitro(nitroConfig, context)` — runs from the `nitro:config` hook;
65 `context.entry` carries the resolved user worker entry when one is
66 configured.
67- `devPlatform(context)` — the target's dev story: acquire whatever serves
68 the platform environment in dev (scoped — the finalizer runs AFTER the dev
69 server closes, so the platform outlives the last in-flight request) and
70 return `{ nitroPlugins, runtimeConfig }` for the framework half to inject
71 through the `loadNuxt` overrides. When absent, `dev` runs the plain
72 framework dev server with no platform bridge.
73
74`Framework.build` runs: resolve target → (`target.build`? delegate wholesale)
75→ `loadNuxt` with the preset + user overrides → register hooks → `ready()` +
76`buildNuxt` → read `.output` → `applyDeployTargetFinish` (a no-op for
77cloudflare_module, but the contract is honored for targets that
78post-process). `Framework.dev` runs `loadNuxt({ dev: true })` → hooks →
79`ready()` → `nuxt.server.listen` → `buildNuxt` (the same flow `nuxi dev`
80drives), with a bounded readiness probe before returning the URL.
81
82### The Cloudflare target
83
84`makeCloudflareTarget(config)` (default export of
85`@alchemy.run/frontend-frameworks/nuxt/cloudflare`) implements the contract:
86
87- **`nitroPreset`** — `"cloudflare_module"`.
88- **`configureNitro`** — wrangler-free doctrine: `cloudflare.deployConfig:
89false` (nitro must never write a `wrangler.json` into user projects) and
90 `cloudflare.nodeCompat: true` by default (without a wrangler config on
91 disk nitro would otherwise skip its hybrid workerd node-compat; the
92 deployed worker enables the `nodejs_compat` flag to match).
93- **The user-entry seam** — a configured `main` becomes nitro's `entry`, so
94 the user module's exports are the worker's exports. The entry is set on
95 the nitro INSTANCE at `nitro:init`, not in the config: nitro's prerenderer
96 clones `options._config` for its Node-preset sub-build, and a
97 workerd-flavored user entry (importing `cloudflare:` modules) must not
98 leak into that clone. A user entry wraps nitro's runtime handler and adds
99 its own exports (Durable Object classes, ...):
100
101 ```ts
102 // worker-entry.mjs
103 import nitroHandler from "nitropack/presets/cloudflare/runtime/cloudflare-module";
104 export { MyDurableObject } from "./durable-object.mjs";
105 export default nitroHandler;
106 ```
107
108- **`devPlatform`** — the wrangler-free dev bridge (next section).
109
110## The dev transport
111
112Dev runs Nuxt's own dev server (nitro dev, SSR in a Node worker thread, full
113HMR) with the `cloudflare_module` runtime contract served wrangler-free. The
114transport has two halves sharing one contract (`src/dev/shared.ts`):
115
116- **Host half (`src/dev/host.ts`)** — runs in the alchemy / e2e-harness
117 process. It opens cloudflare-runtime's platform proxy (`getPlatformProxy`,
118 our wrangler-free reimplementation of wrangler's API: a local workerd
119 instance hosting the configured binding hooks) and produces the
120 `NuxtDevPlatform` injection: the dev-only nitro plugin path plus a
121 `runtimeConfig` entry carrying the `DevConnectInfo` — the proxy's `url` and
122 `token` (two plain strings), the absolute path of cloudflare-runtime's
123 runtime-free client module, and the literal env overrides.
124- **Worker half (`src/dev/plugin.ts`)** — a nitro plugin bundled into
125 nitro's dev SSR worker thread. It reads the connect info back out of
126 `useRuntimeConfig()` and reconstructs the platform over HTTP with
127 `platform-proxy/connect`. On every request it sets the same contract
128 `nitro-cloudflare-dev` provides and production serves natively:
129 `event.context.cf`, `event.context.waitUntil`, and
130 `event.context.cloudflare = { request, env, context }` — `env` is the live
131 proxied environment (binding calls round-trip to the host's workerd
132 instance, so state is SHARED with the host proxy).
133
134Because the client state is just `{ url, token }`, the worker-thread hop is
135trivial: `runtimeConfig` strings survive nitro's bundling and worker
136replacement, so the plugin reconnects on its own after every dev reload
137while binding state (KV, DO storage, ...) stays live in the host's workerd
138instance. The connection is established lazily on the first request and
139re-created after a failure; when the host proxy is gone, requests fail fast
140with a descriptive cause instead of hanging.
141
142Injected plugins live under `node_modules` (this package's dist), so their
143directories are forced into `nitro.externals.inline` — nitro's externals
144pass would otherwise keep the plugin external and node would resolve its
145`nitropack/runtime` import against the raw package, whose
146`#nitro-internal-virtual/*` imports only exist inside the dev bundle.
147
148## Usage
149
150### With the e2e harness (`e2e.config.ts`)
151
152```ts
153import * as Options from "@alchemy.run/cloudflare-test-tools/e2e/Options";
154import * as Nuxt from "@alchemy.run/frontend-frameworks/nuxt";
155
156export default Options.make({
157 // string form: framework: "@alchemy.run/frontend-frameworks/nuxt"
158 framework: (options) => Nuxt.layer(Nuxt.fromHarnessOptions(options as Nuxt.HarnessOptions)),
159 target: {
160 cloudflare: {
161 worker: {
162 compatibilityDate: "2026-03-10",
163 compatibilityFlags: ["nodejs_compat"],
164 main: "worker-entry.ts",
165 worker: {
166 bindings: [
167 /* ... */
168 ],
169 },
170 },
171 preview: {
172 /* miniflare options for `e2e preview` / Playwright "live" */
173 },
174 },
175 },
176});
177```
178
179`fromHarnessOptions` reads the target-scoped carriage
180(`target.cloudflare.worker`, falling back to the deprecated top-level `vite`
181alias): compatibility date/flags, the user worker entry, and the declared
182binding hooks, which pass through wholesale to the dev platform proxy —
183resource bindings (KV, D1, …) included, not just literal values.
184
185### Direct (typed) usage
186
187```ts
188import * as Nuxt from "@alchemy.run/frontend-frameworks/nuxt";
189
190const layer = Nuxt.layer({
191 compatibilityDate: "2026-03-10",
192 compatibilityFlags: ["nodejs_compat"],
193 main: "worker-entry.ts",
194 nuxt: { routeRules: { "/about": { prerender: true } } },
195 dev: {
196 port: 3104,
197 // cloudflare-runtime binding hooks, served on event.context.cloudflare.env
198 bindings: [KvNamespace.local("MY_KV")],
199 // literal overrides — win over same-named proxied values
200 env: { MY_SECRET: "..." },
201 },
202});
203```
204
205### Selecting a deploy target
206
207`NuxtOptions.target` accepts a `DeployTargetInput`:
208
209```ts
210// 1. default — omitted: resolves "@alchemy.run/frontend-frameworks/nuxt/cloudflare"
211// from the project's node_modules
212Nuxt.layer({});
213
214// 2. factory — applied to the config the framework assembles from options
215import cloudflareTarget from "@alchemy.run/frontend-frameworks/nuxt/cloudflare";
216Nuxt.layer({ target: cloudflareTarget });
217
218// 3. value — full control; options-level compat fields are ignored
219Nuxt.layer({ target: cloudflareTarget({ compatibilityDate: "2026-03-10" }) });
220
221// 4. specifier — loaded from the project's dependency tree
222Nuxt.layer({ target: "@my-scope/nuxt-aws" });
223```
224
225A future target (e.g. AWS) implements `NuxtTarget` in its own module — no
226change to this package's framework half.
227
228## Options
229
230### `NuxtOptions` (framework)
231
232| Option | Default | Purpose |
233| -------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
234| `root` | `process.cwd()` | Project root (the directory containing `nuxt.config.ts`). |
235| `target` | `"@alchemy.run/frontend-frameworks/nuxt/cloudflare"` | Deploy target: value, factory, or module specifier. |
236| `compatibilityDate` | — | Forwarded to the target config (carried for serve/deploy consumers). |
237| `compatibilityFlags` | `["nodejs_compat"]` (applied by the cloudflare target) | Forwarded to the target config. Nitro's workerd build uses hybrid Node compat, so `nodejs_compat` is effectively required. |
238| `main` | — | The USER's worker entry (nitro's entry/exports seam). Relative paths resolve against the root. |
239| `nuxt` | — | Nuxt config overrides merged over the project's `nuxt.config.ts` (integration wins). `nitro.preset` is always owned by the target. |
240| `dev.port` | random | Dev-server port (overridden by `FrameworkDevOptions.port`). |
241| `dev.bindings` | `[]` | Binding specs the target's dev platform serves (Cloudflare: cloudflare-runtime `BindingHooks`). |
242| `dev.env` | `{}` | Literal env overrides on the dev platform — a same-named literal wins over the proxied binding value. |
243
244### `NuxtSourceOptions` (`./source`, alchemy)
245
246`rootDir`, `memo` (`include`/`exclude` globs + `lockfile`, default
247gitignore-aware), `nuxt`, `main` — all JSON-serializable (they persist in
248alchemy state and participate in the Worker's metadata hash). `build()` maps
249the nitro output onto alchemy's source contract (entry-first bundle files,
250manifest-hashed assets honoring `.assetsignore`/`_headers`/`_redirects`,
251project-tree `input` hash); `hash()` recomputes the input hash without
252building; `dev()` starts nitro's dev server with the proxy-backed platform:
253the Worker's binding hooks (`ctx.worker.bindings`) serve on
254`event.context.cloudflare.env`, while literal `props.env` values (strings;
255`Redacted` values unwrapped) apply as overrides.
256
257## Limitations & known constraints
258
259- **Pinned upstream surfaces.** nuxt `4.5.x`, nitropack `2.13.x` (see
260 `packages/frontend-frameworks/fixtures/nuxt`); treat version bumps as deliberate migrations. Note the
261 repo's `minimumReleaseAge` install gate (3 days) when bumping.
262- **`isr` route rule ignored.** Nitro implements `isr` only in the Vercel
263 and Netlify presets — on `cloudflare_module` it is silently dropped at
264 build time and the route renders on demand like any other SSR route. Use
265 `prerender` for build-time static routes or `cache` rules for runtime
266 caching.
267- **Durable Objects are not servable in dev.** The platform proxy cannot
268 host user worker-entry classes — DOs declared via the entry/exports seam
269 only exist in the production build, so DO namespace bindings fail in dev.
270- **Foreign `nitro.preset` is a hard error.** The deploy target owns the
271 preset; a user-configured preset fails the build with an actionable
272 message instead of being silently replaced.
273- **Dev fidelity.** SSR runs in nitro's Node worker thread, not workerd —
274 forcing nitro's dev flow into workerd is a non-goal. The platform proxy's
275 documented limitations apply in dev:
276 - binding methods whose results are rich class instances (e.g. R2's
277 `R2Object`) are not supported over the proxy — results must be
278 JSON-compatible values, bytes, dates, streams, or `DurableObjectId`s;
279 - no synchronous materialisation of intermediate values —
280 `env.DO.idFromName("a").toString()` needs
281 `(await env.DO.idFromName("a")).toString()`;
282 - `connect()` on sockets is unsupported, and cross-binding stub arguments
283 are rejected.
284- **`event.context.waitUntil` is accepted and dropped in dev** (long-lived
285 dev process: background work simply runs). Production serves the real
286 `ExecutionContext`.
287- **`context.cloudflare.request` carries no body in dev.** The node request
288 stream belongs to h3's own body parsing — read bodies through h3
289 (`readBody`), not `context.cloudflare.request`.
290- **Telemetry/devtools are disabled in dev** — the dev server runs headless
291 under alchemy / the e2e harness.
292
293## Testing
294
295- `bun run test` — unit tests: build hook wiring and output mapping
296 (`test/Nuxt.test.ts`), the target contract (`test/Target.test.ts`),
297 config synthesis and preset-conflict policy (`test/UserConfig.test.ts`),
298 and the dev transport — host platform acquisition, connect-info shape,
299 and the plugin's request bridge against a fake proxy server
300 (`test/Dev.test.ts`).
301- `packages/frontend-frameworks/fixtures/nuxt` — the end-to-end fixture (Playwright against both the
302 miniflare-served production build and the nitro dev server); see its
303 layout for the covered surface (SSR routes, `server/` API routes,
304 prerendered pages, `event.context.cloudflare.env` with real bindings and
305 literal overrides, and the custom `worker-entry.ts` seam).
306
307## Upstream references
308
309- Nuxt: [`17119f8ee6f753faf2e61564bba3211871e16e2c`](https://github.com/nuxt/nuxt/tree/17119f8ee6f753faf2e61564bba3211871e16e2c)
310- Nitro: [`52abde8aa9d85215bc0e8dc3b007f29fc6112ac3`](https://github.com/nitrojs/nitro/tree/52abde8aa9d85215bc0e8dc3b007f29fc6112ac3)
311- Nitro Cloudflare dev: [`a22a83b7151b314d70a01089c352ca6007161051`](https://github.com/nitrojs/nitro-cloudflare-dev/tree/a22a83b7151b314d70a01089c352ca6007161051)