| 1 | import * as Effect from "effect/Effect"; |
| 2 | import type { MemoOptions } from "../../Command/Memo.ts"; |
| 3 | import type { InputProps } from "../../Input.ts"; |
| 4 | import { effectClass } from "../../Util/effect.ts"; |
| 5 | import type { Providers } from "../Providers.ts"; |
| 6 | import type { AssetsConfig } from "../Workers/Assets.ts"; |
| 7 | import { |
| 8 | Worker, |
| 9 | type NormalizedBindings, |
| 10 | type WorkerAssetsConfig, |
| 11 | type WorkerBindingProps, |
| 12 | type WorkerProps, |
| 13 | } from "../Workers/Worker.ts"; |
| 14 | |
| 15 | export 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. |
| 81 | type 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 | */ |
| 185 | export 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; |