| 1 | import { DEFAULT_COMPATIBILITY_DATE } from "@alchemy.run/cloudflare-runtime/core/internal/constants"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import type { MemoOptions } from "../../Command/Memo.ts"; |
| 4 | import type { InputProps } from "../../Input.ts"; |
| 5 | import { effectClass } from "../../Util/effect.ts"; |
| 6 | import type { Providers } from "../Providers.ts"; |
| 7 | import type { Namespace } from "../KV/Namespace.ts"; |
| 8 | import { DurableObject } from "../Workers/DurableObject.ts"; |
| 9 | import type { AssetsConfig } from "../Workers/Assets.ts"; |
| 10 | import { |
| 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 | */ |
| 25 | const NEXTJS_SOURCE_PROVIDER = "@alchemy.run/frontend-frameworks/nextjs/source"; |
| 26 | |
| 27 | export 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. |
| 104 | type 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 | */ |
| 271 | export 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; |