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
15export interface FoldkitProps<
16 Bindings extends WorkerBindingProps = {},
17> extends Omit<
18 WorkerProps<Bindings>,
19 "vite" | "main" | "assets" | "source" | "script" | "bundle"
20> {
21 /**
22 * Overrides the module that becomes the deployed Worker entry. Relative
23 * paths resolve from {@link rootDir}.
24 *
25 * A Foldkit deployment is assets-only by default — no Worker code runs
26 * at request time. Point `main` at a custom module when the deployment
27 * needs code at the edge — API routes, error reporting, Durable Object
28 * classes. The entry serves the client build through its `ASSETS`
29 * binding:
30 *
31 * ```typescript
32 * // src/worker.ts
33 * export default {
34 * async fetch(request: Request, env: { ASSETS: Fetcher }) {
35 * const url = new URL(request.url);
36 * if (url.pathname === "/api/health") {
37 * return Response.json({ ok: true });
38 * }
39 * return env.ASSETS.fetch(request);
40 * },
41 * };
42 * ```
43 */
44 main?: string;
45 /**
46 * Foldkit project root directory.
47 * Defaults to the current working directory (`process.cwd()`).
48 */
49 rootDir?: string;
50 /**
51 * Controls which files are hashed to decide whether a rebuild is needed.
52 * By default every non-gitignored file under `rootDir` is hashed, plus the
53 * nearest package-manager lockfile. Provide explicit globs to narrow the
54 * scope.
55 */
56 memo?: MemoOptions & {
57 /**
58 * Additional workspace directories to hash (relative to `rootDir`).
59 * By default (`"auto"`), workspaces are auto-detected from the build's
60 * module graph; an explicit array pins them.
61 * @default "auto"
62 */
63 workspaces?: "auto" | Array<MemoOptions & { cwd: string }>;
64 };
65 /**
66 * Optional configuration for static asset routing behavior.
67 * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc.
68 *
69 * Foldkit apps route on the client, so `notFoundHandling` defaults to
70 * `"single-page-application"` — unmatched paths serve `index.html` and
71 * the app's router takes over. Set `notFoundHandling` explicitly to
72 * override (e.g. `"404-page"` to serve the built `404.html`).
73 *
74 * @default { notFoundHandling: "single-page-application" }
75 */
76 assets?: AssetsConfig;
77}
78
79// These options are inspected while constructing the Worker. Resolve them in
80// the outer props Effect; pass-through properties can remain deferred Inputs.
81type FoldkitInput<Bindings extends WorkerBindingProps> = InputProps<
82 FoldkitProps<Bindings>,
83 "assets"
84>;
85
86/**
87 * A Cloudflare Worker deployed from a [Foldkit](https://foldkit.dev) app.
88 *
89 * Foldkit apps are client-only Vite projects, so `Foldkit` drives the
90 * project's own `vite build` — the Foldkit Vite plugin in the app's
91 * `vite.config.ts` composes with the injected Cloudflare plugin — and
92 * deploys the client output as static assets. No Wrangler configuration,
93 * build command, or output directory required.
94 *
95 * Input files are content-hashed (respecting `.gitignore` by default) so
96 * unchanged projects skip the build and deploy entirely.
97 *
98 * Foldkit apps route on the client, so `assets.notFoundHandling`
99 * defaults to `"single-page-application"` — deep links serve
100 * `index.html` and the Foldkit router takes over.
101 *
102 *
103 * ### Deploying a Foldkit App
104 * A single call builds the project and deploys the client output as
105 * static assets — no configuration required.
106 *
107 * **Example:** Foldkit app
108 * ```typescript
109 * const site = yield* Cloudflare.Website.Foldkit("Website");
110 * ```
111 *
112 * **Example:** Foldkit project in a subdirectory
113 * ```typescript
114 * const site = yield* Cloudflare.Website.Foldkit("Website", {
115 * rootDir: "applications/web",
116 * });
117 * ```
118 *
119 * ### Single-Page Application Routing
120 * Unmatched paths serve `index.html` by default so deep links boot the
121 * app and the Foldkit router resolves the route. A site that ships real
122 * 404 content overrides the default with `notFoundHandling: "404-page"`.
123 *
124 * **Example:** Serving a real 404 page
125 * ```typescript
126 * const site = yield* Cloudflare.Website.Foldkit("Website", {
127 * assets: {
128 * notFoundHandling: "404-page",
129 * },
130 * });
131 * ```
132 *
133 * ### Custom Worker Entry
134 * By default the deployment is assets-only. When code must run at the
135 * edge — API routes, error reporting, Durable Object classes — point
136 * `main` at your own module that serves the client build through the
137 * `ASSETS` binding (see {@link FoldkitProps.main}). Bindings passed in
138 * `env` are reachable from the entry (and from cron handlers), not from
139 * browser code — a Foldkit app runs on the client, so anything it needs
140 * must come from a route the Worker serves.
141 *
142 * **Example:** Custom entry serving an API route from a KV namespace
143 * ```typescript
144 * const ticker = yield* Cloudflare.KV.Namespace("Ticker");
145 *
146 * const site = yield* Cloudflare.Website.Foldkit("Platform", {
147 * main: "src/worker.ts",
148 * env: {
149 * TICKER: ticker,
150 * },
151 * });
152 * ```
153 *
154 * ### Custom Rebuild Scope
155 * By default, every non-gitignored file is hashed to decide whether a
156 * rebuild is needed. Use `memo` to narrow the scope when your project
157 * has large directories that don't affect the build output.
158 *
159 * **Example:** Narrowing the memo scope
160 * ```typescript
161 * const site = yield* Cloudflare.Website.Foldkit("Website", {
162 * memo: {
163 * include: ["src/**", "public/**", "package.json"],
164 * },
165 * });
166 * ```
167 *
168 * ### Class Form
169 * Calling `Foldkit` with no arguments returns a constructor you can
170 * `extend` to declare the Worker as a named class. The class is both an
171 * `Effect` you can `yield*` to deploy and a type you can reference
172 * elsewhere — useful when other resources need to bind to this Worker.
173 *
174 * **Example:** Declaring a Worker class
175 * ```typescript
176 * class Website extends Cloudflare.Website.Foldkit<Website>()("Website") {}
177 *
178 * const site = yield* Website;
179 * ```
180 *
181 * @resource
182 * @product Website
183 * @category Workers & Compute
184 */
185export const Foldkit: {
186 <Self>(): {
187 <const Bindings extends WorkerBindingProps = {}, Req = never>(
188 id: string,
189 propsEff?:
190 | FoldkitInput<Bindings>
191 | Effect.Effect<FoldkitInput<Bindings>, never, Req>,
192 ): Effect.Effect<Self, never, Req | Providers> & {
193 new (): Worker<{
194 [
195 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
196 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
197 }>;
198 };
199 };
200 <const Bindings extends WorkerBindingProps = {}, Req = never>(
201 id: string,
202 propsEff?:
203 | FoldkitInput<Bindings>
204 | Effect.Effect<FoldkitInput<Bindings>, never, Req>,
205 ): Effect.Effect<
206 Worker<{
207 [
208 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
209 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
210 }>,
211 never,
212 Req | Providers
213 >;
214} = (<const Bindings extends WorkerBindingProps = {}, Req = never>(
215 id?: string,
216 propsEff?:
217 | FoldkitInput<Bindings>
218 | Effect.Effect<FoldkitInput<Bindings>, never, Req>,
219) =>
220 id === undefined
221 ? <const Bindings extends WorkerBindingProps = {}, Req = never>(
222 id: string,
223 propsEff?:
224 | FoldkitInput<Bindings>
225 | Effect.Effect<FoldkitInput<Bindings>, never, Req>,
226 ) => effectClass(Foldkit(id, propsEff))
227 : Worker(
228 id,
229 Effect.map(
230 Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
231 (props) => ({
232 ...props,
233 // Foldkit routes on the client; serve index.html for unmatched
234 // paths so deep links boot the app instead of 404ing. An
235 // explicit `assets.notFoundHandling` wins over the default.
236 assets: {
237 notFoundHandling: "single-page-application" as const,
238 ...props?.assets,
239 },
240 main: undefined!,
241 vite: {
242 main: props?.main,
243 rootDir: props?.rootDir,
244 memo: props?.memo,
245 },
246 }),
247 ),
248 )) as any;