1import { DEFAULT_COMPATIBILITY_DATE } from "@alchemy.run/cloudflare-runtime/core/internal/constants";
2import * as Effect from "effect/Effect";
3import type { MemoOptions } from "../../Command/Memo.ts";
4import type { InputProps } from "../../Input.ts";
5import { effectClass } from "../../Util/effect.ts";
6import type { Providers } from "../Providers.ts";
7import type { Namespace } from "../KV/Namespace.ts";
8import { DurableObject } from "../Workers/DurableObject.ts";
9import type { AssetsConfig } from "../Workers/Assets.ts";
10import {
11 Self,
12 Worker,
13 type NormalizedBindings,
14 type WorkerAssetsConfig,
15 type WorkerBindingProps,
16 type WorkerProps,
17} from "../Workers/Worker.ts";
18
19/**
20 * The module specifier of the Next.js source provider. Loaded with a
21 * dynamic `import()`, so `@alchemy.run/frontend-frameworks` must be installed
22 * in the deploying project. The provider is exposed through its `/nextjs`
23 * subpath.
24 */
25const NEXTJS_SOURCE_PROVIDER = "@alchemy.run/frontend-frameworks/nextjs/source";
26
27export interface NextjsProps<
28 Bindings extends WorkerBindingProps = {},
29> extends Omit<
30 WorkerProps<Bindings>,
31 "vite" | "main" | "assets" | "script" | "bundle" | "source" | "rules" | "dev"
32> {
33 /**
34 * The Next.js project root. Defaults to the process working directory.
35 */
36 rootDir?: string;
37 /**
38 * Controls which files are content-hashed to decide whether the OpenNext
39 * build needs to re-run. By default every project file outside build
40 * outputs (`.next`, `.open-next`, `dist`) and `node_modules` is hashed,
41 * plus the nearest package-manager lockfile. Narrow the scope with
42 * `include`/`exclude` globs when the default is too broad.
43 * `open-next.config.ts` itself is always hashed. When narrowing `include`,
44 * also include any helpers imported by that config.
45 */
46 memo?: MemoOptions;
47 /**
48 * Binds KV caches and the same-worker Durable Object revalidation queue.
49 * Without `open-next.config.ts`, Alchemy selects matching KV adapters;
50 * omitting `isr` instead selects the read-only static-assets cache.
51 * A native config controls adapter selection and must choose adapters
52 * matching these bindings. KV fills on demand; build-time cache entries
53 * are not uploaded to KV.
54 */
55 isr?: {
56 /** KV namespace storing rendered pages and fetch results. */
57 incrementalCache: Namespace;
58 /** KV namespace storing cache-tag invalidations. */
59 tagCache: Namespace;
60 };
61 /** Build controls, separate from the optional native `open-next.config.ts`. */
62 openNext?: {
63 /**
64 * Overrides the native config's `buildCommand` when set. If neither sets
65 * a command, defaults to `npx next build`.
66 */
67 buildCommand?: string;
68 /** Minify the generated Worker. Defaults to false. */
69 minify?: boolean;
70 /** Enable OpenNext build diagnostics. Defaults to false. */
71 debug?: boolean;
72 };
73 /**
74 * Local dev (`alchemy dev`) behavior.
75 */
76 dev?: {
77 /**
78 * - `"preview"` (default): build the OpenNext worker and serve it
79 * under workerd — production parity (workerd APIs, ISR/cache
80 * semantics), no HMR.
81 * - `"hmr"`: run the real `next dev` (Turbopack HMR) in Node with the
82 * Worker's bindings proxied onto OpenNext's `getCloudflareContext()`
83 * contract. App code runs in Node, not workerd — CF-specific
84 * runtime behavior still needs `"preview"`.
85 * @default "preview"
86 */
87 mode?: "preview" | "hmr";
88 /**
89 * Port for the local dev server. `0` picks an ephemeral port.
90 */
91 port?: number;
92 };
93 /**
94 * Optional configuration for static asset routing behavior.
95 * Defaults to `runWorkerFirst: true` with `htmlHandling`/`notFoundHandling`
96 * set to `"none"` — the OpenNext server owns routing and delegates to the
97 * `ASSETS` binding itself.
98 */
99 assets?: AssetsConfig;
100}
101
102// These options are inspected while constructing the Worker. Resolve them in
103// the outer props Effect; pass-through properties can remain deferred Inputs.
104type NextjsInput<Bindings extends WorkerBindingProps> = InputProps<
105 NextjsProps<Bindings>,
106 "dev" | "isr" | "env" | "compatibility" | "assets" | "openNext"
107>;
108
109/**
110 * A Cloudflare Worker deployed from a Next.js project.
111 *
112 * `Nextjs` builds the app with the wrangler-free OpenNext pipeline from
113 * [`@alchemy.run/frontend-frameworks/nextjs`](https://github.com/alchemy-run/alchemy/tree/main/packages/frontend-frameworks/src/nextjs):
114 * `next build` runs through `@opennextjs/cloudflare`, the resulting worker
115 * is bundled into a self-contained ES module set, and the static assets
116 * (including prerendered pages and the read-only incremental cache) deploy
117 * as Workers static assets. Input files are content-hashed so unchanged
118 * projects skip the build and deploy entirely.
119 *
120 * Both `@alchemy.run/frontend-frameworks` and its peer
121 * `@opennextjs/cloudflare` must be installed in the deploying project. The
122 * source provider is loaded from the package's `/nextjs` export with a dynamic
123 * `import()`.
124 *
125 * Local dev (`alchemy dev`) defaults to preview parity — the built worker
126 * served under workerd. Set `dev: { mode: "hmr" }` for the real `next dev`
127 * (Turbopack HMR) with the Worker's bindings proxied onto
128 * `getCloudflareContext()`.
129 *
130 * If `open-next.config.ts` exists in the project root, Alchemy loads it through
131 * OpenNext's native compiler without rewriting it. Otherwise, Alchemy generates
132 * temporary defaults: a read-only static-assets cache, or KV adapters when
133 * `isr` is set. Static-assets revalidation writes are a no-op.
134 * Native Next.js and Tailwind configuration files are left unchanged.
135 *
136 * Known limitations (upstream `@opennextjs/cloudflare`):
137 * - Edge-runtime routes/pages (`export const runtime = "edge"`) are not
138 * supported — the build fails with the offending route list; remove the
139 * directive (the node runtime runs on Workers). Middleware is fine.
140 * - `next/image` optimization requires a zone with Cloudflare Images;
141 * on `workers.dev`, use `unoptimized` (images serve as raw assets).
142 * - Partial Prerendering / `"use cache"` (`cacheComponents`) and
143 * Pages-Router `i18n` config are untested/out of scope for now. App
144 * Router i18n via middleware works (middleware is fully supported).
145 *
146 *
147 * ### Deploying a Next.js App
148 * A single call builds the app and deploys the Worker plus static assets.
149 * No `open-next.config.ts`, Wrangler config, or Alchemy plugin is required.
150 *
151 * **Example:** Basic Next.js site
152 * ```typescript
153 * const site = yield* Cloudflare.Website.Nextjs("Site");
154 * ```
155 *
156 * **Example:** Explicit project root
157 * ```typescript
158 * const site = yield* Cloudflare.Website.Nextjs("Site", {
159 * rootDir: "./apps/web",
160 * });
161 * ```
162 *
163 * ### Optional Native OpenNext Configuration
164 * The same `Cloudflare.Website.Nextjs("Site")` call works with or without a
165 * config file. To customize OpenNext, add `open-next.config.ts` under `rootDir`
166 * (the working directory by default). Imports and callbacks are preserved;
167 * the native `OpenNextConfig` is passed to OpenNext's compiler, subject to
168 * Cloudflare's adapter constraints rather than AWS runtime feature parity.
169 *
170 * **Example:** Native config with the read-only static-assets cache
171 * ```typescript
172 * // open-next.config.ts
173 * import { defineCloudflareConfig, type OpenNextConfig } from "@opennextjs/cloudflare";
174 * import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
175 *
176 * export default {
177 * ...defineCloudflareConfig({ incrementalCache: staticAssetsIncrementalCache }),
178 * buildCommand: "pnpm exec next build",
179 * } satisfies OpenNextConfig;
180 * ```
181 *
182 * ### Bindings
183 * Resources passed via `env` become Worker bindings, readable in route
184 * handlers and server components through OpenNext's
185 * `getCloudflareContext()`.
186 *
187 * **Example:** Binding an R2 bucket
188 * ```typescript
189 * const bucket = yield* Cloudflare.R2.Bucket("Uploads");
190 * const site = yield* Cloudflare.Website.Nextjs("Site", {
191 * env: {
192 * UPLOADS: bucket,
193 * },
194 * });
195 * ```
196 *
197 * ```typescript
198 * // app/api/upload/route.ts
199 * import { getCloudflareContext } from "@opennextjs/cloudflare";
200 *
201 * export async function PUT(request: Request) {
202 * const { env } = getCloudflareContext();
203 * await env.UPLOADS.put("key", await request.text());
204 * return Response.json({ ok: true });
205 * }
206 * ```
207 *
208 * ### Writable ISR
209 * Supply the cache namespaces on the resource. Alchemy binds them and adds
210 * the Durable Object queue for background regeneration. Without a native
211 * config, it also selects the matching adapters. With a native config, select
212 * `kv-incremental-cache`, `kv-next-tag-cache`, and `do-queue` there; `isr`
213 * does not override the file's adapter choices.
214 *
215 * **Example:** Binding the writable-ISR resources
216 * ```typescript
217 * const incCache = yield* Cloudflare.KV.Namespace("NextIncCache");
218 * const tagCache = yield* Cloudflare.KV.Namespace("NextTagCache");
219 *
220 * const site = yield* Cloudflare.Website.Nextjs("Site", {
221 * isr: { incrementalCache: incCache, tagCache },
222 * });
223 * ```
224 *
225 * ### Custom Rebuild Scope
226 * By default, every project file outside build outputs is hashed to decide
227 * whether a rebuild is needed. Use `memo` to narrow the scope when the
228 * project has large directories that don't affect the build output.
229 * `open-next.config.ts` itself is always hashed. Include any imported config
230 * helpers in a narrowed `memo.include` too (for example, `config/**`).
231 *
232 * **Example:** Narrowing the memo scope
233 * ```typescript
234 * const site = yield* Cloudflare.Website.Nextjs("Site", {
235 * memo: {
236 * include: ["app/**", "public/**", "package.json", "next.config.mjs", "config/**"],
237 * },
238 * });
239 * ```
240 *
241 * ### Build Configuration
242 * An explicit `openNext.buildCommand` overrides the native config's command.
243 * `openNext.minify` and `openNext.debug` remain resource-level build controls.
244 *
245 * **Example:** Customize the build
246 * ```typescript
247 * const site = yield* Cloudflare.Website.Nextjs("Site", {
248 * openNext: { buildCommand: "pnpm exec next build", minify: true },
249 * });
250 * ```
251 *
252 * ### Class Form
253 * Calling `Nextjs` with no arguments returns a constructor you can
254 * `extend` to declare the Worker as a named class. The class is both an
255 * `Effect` you can `yield*` to deploy and a type you can reference
256 * elsewhere — useful when other resources need to bind to this Worker.
257 *
258 * **Example:** Declaring a Worker class
259 * ```typescript
260 * class Site extends Cloudflare.Website.Nextjs<Site>()("Site", {
261 * rootDir: "./apps/web",
262 * }) {}
263 *
264 * const site = yield* Site;
265 * ```
266 *
267 * @resource
268 * @product Website
269 * @category Workers & Compute
270 */
271export const Nextjs: {
272 <Self>(): {
273 <const Bindings extends WorkerBindingProps = {}, Req = never>(
274 id: string,
275 propsEff?:
276 | NextjsInput<Bindings>
277 | Effect.Effect<NextjsInput<Bindings>, never, Req>,
278 ): Effect.Effect<Self, never, Req | Providers> & {
279 new (): Worker<{
280 [
281 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
282 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
283 }>;
284 };
285 };
286 <const Bindings extends WorkerBindingProps = {}, Req = never>(
287 id: string,
288 propsEff?:
289 | NextjsInput<Bindings>
290 | Effect.Effect<NextjsInput<Bindings>, never, Req>,
291 ): Effect.Effect<
292 Worker<{
293 [
294 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
295 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
296 }>,
297 never,
298 Req | Providers
299 >;
300} = (<const Bindings extends WorkerBindingProps = {}, Req = never>(
301 id?: string,
302 propsEff?:
303 | NextjsInput<Bindings>
304 | Effect.Effect<NextjsInput<Bindings>, never, Req>,
305) =>
306 id === undefined
307 ? <const Bindings extends WorkerBindingProps = {}, Req = never>(
308 id: string,
309 propsEff?:
310 | NextjsInput<Bindings>
311 | Effect.Effect<NextjsInput<Bindings>, never, Req>,
312 ) => effectClass(Nextjs(id, propsEff))
313 : Worker(
314 id,
315 Effect.map(
316 Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
317 (props) => ({
318 ...props,
319 // `dev.mode` is the integration's dev behavior (routed through
320 // the source options below); only `port` maps onto the Worker's
321 // own local-dev config.
322 dev:
323 props?.dev?.port !== undefined
324 ? { port: props.dev.port }
325 : undefined,
326 // OpenNext's revalidation queues (memory-queue, do-queue) fetch
327 // the worker back through `WORKER_SELF_REFERENCE`. Always wire
328 // the self service binding — it's inert when unused, and its
329 // absence turns ISR revalidation into a silent no-op. An
330 // explicit user-provided `env.WORKER_SELF_REFERENCE` wins.
331 env: {
332 WORKER_SELF_REFERENCE: Self,
333 ...props?.env,
334 ...(props?.isr
335 ? {
336 NEXT_INC_CACHE_KV: props.isr.incrementalCache,
337 NEXT_TAG_CACHE_KV: props.isr.tagCache,
338 NEXT_CACHE_DO_QUEUE: DurableObject("NEXT_CACHE_DO_QUEUE", {
339 className: "DOQueueHandler",
340 }),
341 }
342 : {}),
343 },
344 // OpenNext requires Node.js APIs. The 2026-08-31 default date
345 // enables both nodejs_compat modes, so no redundant flag is sent.
346 compatibility: {
347 date: props?.compatibility?.date ?? DEFAULT_COMPATIBILITY_DATE,
348 flags: props?.compatibility?.flags,
349 },
350 // The OpenNext server owns routing: run the worker first and
351 // leave asset-path rewriting off. Users can still override.
352 assets: {
353 runWorkerFirst: true,
354 htmlHandling: "none" as const,
355 notFoundHandling: "none" as const,
356 ...props?.assets,
357 },
358 source: {
359 provider: NEXTJS_SOURCE_PROVIDER,
360 devMode: "server",
361 rootDir: props?.rootDir,
362 // `next dev` (Turbopack) cold-starts broken under bun (every
363 // route 404s until `.next` is warm) — pin the dev child to node.
364 runtime: "node",
365 options: {
366 root: props?.rootDir,
367 memo: props?.memo,
368 cache: props?.isr ? "kv" : "static-assets",
369 ...props?.openNext,
370 ...(props?.dev?.mode !== undefined
371 ? { dev: { mode: props.dev.mode } }
372 : {}),
373 },
374 },
375 }),
376 ),
377 )) as any;