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 { Namespace } from "../KV/Namespace.ts";
6import type { Providers } from "../Providers.ts";
7import type { AssetsConfig } from "../Workers/Assets.ts";
8import { VersionMetadata } from "../Workers/VersionMetadata.ts";
9import {
10 Worker,
11 type NormalizedBindings,
12 type WorkerAssetsConfig,
13 type WorkerBindingProps,
14 type WorkerProps,
15} from "../Workers/Worker.ts";
16
17/**
18 * The default compatibility date when none is provided. Matches other
19 * Cloudflare Website resources so deploy and local dev agree.
20 */
21const DEFAULT_COMPATIBILITY_DATE = "2026-05-12";
22
23/**
24 * Default Worker entry for a vinext Cloudflare app. Official vinext
25 * examples wrap `vinext/server/fetch-handler` at this path.
26 */
27const DEFAULT_WORKER_ENTRY = "worker/index.ts";
28
29export interface VinextProps<
30 Bindings extends WorkerBindingProps = {},
31> extends Omit<
32 WorkerProps<Bindings>,
33 "vite" | "main" | "assets" | "script" | "bundle" | "source" | "rules"
34> {
35 /**
36 * Worker entry that delegates to vinext. Defaults to
37 * `worker/index.ts` — the official vinext Cloudflare layout:
38 *
39 * ```typescript
40 * import handler from "vinext/server/fetch-handler";
41 * export default {
42 * fetch(request: Request, env: Env, ctx: ExecutionContext) {
43 * return handler.fetch(request, env, ctx);
44 * },
45 * };
46 * ```
47 */
48 main?: string;
49 /**
50 * The vinext project root (the directory containing `app/` or `pages/`
51 * and `vite.config.ts`). Defaults to the process working directory.
52 */
53 rootDir?: string;
54 /**
55 * Controls which files are content-hashed to decide whether the Vite
56 * build needs to re-run. By default every project file outside build
57 * outputs and `node_modules` is hashed, plus the nearest lockfile.
58 */
59 memo?: MemoOptions;
60 /**
61 * Which Vite environments make up the deployed Worker.
62 *
63 * vinext's App Router build emits `rsc` + `ssr` + `client`. The
64 * default points the Worker entry at `rsc` and bundles `ssr`
65 * alongside it — the same shape vinext's official
66 * `@cloudflare/vite-plugin` config uses (`viteEnvironment: { name:
67 * "rsc", childEnvironments: ["ssr"] }`).
68 *
69 * @default { entry: "rsc", children: ["ssr"] }
70 */
71 viteEnvironments?: {
72 entry?: string;
73 children?: string[];
74 };
75 /**
76 * Optional configuration for static asset routing behavior.
77 * Defaults to assets-first (`runWorkerFirst` unset) with
78 * `htmlHandling` / `notFoundHandling` set to `"none"` — the same
79 * shape official vinext writes into `wrangler.jsonc`. Hashed
80 * `/_next/static/*` chunks are served by the asset layer; the
81 * Worker only `ASSETS.fetch`es files the RSC handler marks with
82 * its static-file signal (e.g. `public/`). `runWorkerFirst: true`
83 * would 404 those chunks: vinext does not fetch them itself.
84 */
85 assets?: AssetsConfig;
86}
87
88// These options are inspected while constructing the Worker. Resolve them in
89// the outer props Effect; pass-through properties can remain deferred Inputs.
90type VinextInput<Bindings extends WorkerBindingProps> = InputProps<
91 VinextProps<Bindings>,
92 "env" | "cache" | "compatibility" | "assets"
93>;
94
95/**
96 * A Cloudflare Worker deployed from a [vinext](https://vinext.dev) app.
97 *
98 * vinext reimplements the Next.js API surface on Vite. `Vinext` is the
99 * Alchemy-shaped deploy path: it runs the project's own `vite.config.ts`
100 * (which must register `vinext()`) through Alchemy's wrangler-free
101 * Cloudflare Vite plugin, then deploys the RSC Worker plus client
102 * assets. There is no `wrangler.jsonc` and no
103 * `@vinext/cloudflare deploy`.
104 *
105 * This is **not** `Cloudflare.Website.Nextjs`. That resource runs
106 * `next build` through OpenNext. vinext never consumes `next build`
107 * output — mixing the two stacks will fail.
108 *
109 * Install `vinext`, `@vitejs/plugin-rsc`,
110 * `react-server-dom-webpack`, and `@alchemy.run/frontend-frameworks`
111 * in the deploying project. Do **not** also register
112 * `@cloudflare/vite-plugin` in `vite.config.ts` — Alchemy injects its
113 * own Cloudflare plugin (`vite-plugin-cloudflare:alchemy`; vinext
114 * matches the `vite-plugin-cloudflare:` prefix) and no-ops an official
115 * plugin if one is still present. The Worker source is
116 * `@alchemy.run/frontend-frameworks/vinext/source` (loaded with a
117 * dynamic `import()`, like the other Website framework resources).
118 *
119 * Bindings are declared on this resource (`env`) and read from
120 * `import { env } from "cloudflare:workers"` in server components,
121 * route handlers, and server actions.
122 *
123 * The resource injects Alchemy's KV data-cache adapter into the Vite build
124 * and local server. No Alchemy plugin or cache configuration is needed in
125 * `vite.config.ts` (the same ISR codec is used by Redis / S3).
126 * `Website.Vinext` provisions `VINEXT_KV_CACHE` (do not bind it in
127 * `env`) and `alchemy deploy` seeds prerender pairs into it. Workers
128 * Cache is enabled (`cache.enabled`) and `CF_VERSION_METADATA` is
129 * bound. There is no `@vinext/cloudflare` data-cache adapter.
130 *
131 * ### Deploying a vinext App
132 * **Example:** Basic vinext site
133 * ```typescript
134 * const site = yield* Cloudflare.Website.Vinext("Site");
135 * ```
136 *
137 * **Example:** Bindings on the Worker
138 * ```typescript
139 * const site = yield* Cloudflare.Website.Vinext("Site", {
140 * env: {
141 * GREETING: "Hello from vinext on Cloudflare!",
142 * },
143 * });
144 * ```
145 *
146 * In a server component:
147 * ```typescript
148 * import { env } from "cloudflare:workers";
149 *
150 * export default function Page() {
151 * return <h1>{env.GREETING}</h1>;
152 * }
153 * ```
154 *
155 * **Example:** Custom Worker entry
156 * ```typescript
157 * const site = yield* Cloudflare.Website.Vinext("Site", {
158 * main: "worker/index.ts",
159 * });
160 * ```
161 *
162 * ### Class Form
163 * **Example:** Declaring a Worker class
164 * ```typescript
165 * class Site extends Cloudflare.Website.Vinext<Site>()("Site") {}
166 *
167 * const site = yield* Site;
168 * ```
169 *
170 * @resource
171 * @product Website
172 * @category Workers & Compute
173 */
174export const Vinext: {
175 <Self>(): {
176 <const Bindings extends WorkerBindingProps = {}, Req = never>(
177 id: string,
178 propsEff?:
179 | VinextInput<Bindings>
180 | Effect.Effect<VinextInput<Bindings>, never, Req>,
181 ): Effect.Effect<Self, never, Req | Providers> & {
182 new (): Worker<{
183 [
184 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
185 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
186 }>;
187 };
188 };
189 <const Bindings extends WorkerBindingProps = {}, Req = never>(
190 id: string,
191 propsEff?:
192 | VinextInput<Bindings>
193 | Effect.Effect<VinextInput<Bindings>, never, Req>,
194 ): Effect.Effect<
195 Worker<{
196 [
197 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
198 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
199 }>,
200 never,
201 Req | Providers
202 >;
203} = (<const Bindings extends WorkerBindingProps = {}, Req = never>(
204 id?: string,
205 propsEff?:
206 | VinextInput<Bindings>
207 | Effect.Effect<VinextInput<Bindings>, never, Req>,
208) =>
209 id === undefined
210 ? <const Bindings extends WorkerBindingProps = {}, Req = never>(
211 id: string,
212 propsEff?:
213 | VinextInput<Bindings>
214 | Effect.Effect<VinextInput<Bindings>, never, Req>,
215 ) => effectClass(Vinext(id, propsEff))
216 : Worker(
217 id,
218 Effect.gen(function* () {
219 const props =
220 (Effect.isEffect(propsEff) ? yield* propsEff : propsEff) ?? {};
221 // Auto-provision the ISR/TPR data-cache KV. Official vinext
222 // leaves a wrangler placeholder; Alchemy owns the namespace.
223 // Do not bind VINEXT_KV_CACHE in user `env`.
224 const cache = yield* Namespace(`${id}IsrCache`);
225 const env = {
226 ...props.env,
227 VINEXT_KV_CACHE: cache,
228 CF_VERSION_METADATA:
229 props.env?.CF_VERSION_METADATA ?? VersionMetadata(),
230 };
231 return {
232 ...props,
233 env,
234 cache: {
235 enabled: true,
236 ...props.cache,
237 },
238 compatibility: {
239 date: props?.compatibility?.date ?? DEFAULT_COMPATIBILITY_DATE,
240 flags: Array.from(
241 new Set([
242 "nodejs_compat",
243 ...(props?.compatibility?.flags ?? []),
244 ]),
245 ),
246 },
247 assets: {
248 htmlHandling: "none" as const,
249 notFoundHandling: "none" as const,
250 ...props?.assets,
251 },
252 source: {
253 provider: "@alchemy.run/frontend-frameworks/vinext/source",
254 devMode: "server",
255 runtime: "node",
256 rootDir: props?.rootDir,
257 options: {
258 main: props?.main ?? DEFAULT_WORKER_ENTRY,
259 rootDir: props?.rootDir,
260 memo: props?.memo,
261 viteEnvironments: props?.viteEnvironments ?? {
262 entry: "rsc",
263 children: ["ssr"],
264 },
265 },
266 },
267 };
268 }),
269 )) as any;