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
15const VOCS_SOURCE_PROVIDER = "@alchemy.run/frontend-frameworks/vocs/source";
16
17export interface VocsProps<
18 Bindings extends WorkerBindingProps = {},
19> extends Omit<
20 WorkerProps<Bindings>,
21 "vite" | "main" | "assets" | "source" | "script" | "bundle"
22> {
23 /**
24 * Root directory of the Vocs project.
25 * @default process.cwd()
26 */
27 rootDir?: string;
28 /**
29 * Vocs build output directory, relative to {@link rootDir}. Set this when
30 * `vocs.config.*` customizes `outDir` so generated output stays outside the
31 * rebuild hash.
32 * @default "dist"
33 */
34 outDir?: string;
35 /**
36 * Controls which files feed the content hash that decides whether Vocs
37 * rebuilds. By default every non-gitignored file under {@link rootDir} is
38 * hashed, plus the nearest package-manager lockfile.
39 */
40 memo?: MemoOptions;
41 /**
42 * Static asset routing configuration. Vocs links prerendered pages without
43 * trailing slashes, so extensionless routing is enabled by default.
44 * @default { htmlHandling: "drop-trailing-slash" }
45 */
46 assets?: AssetsConfig;
47}
48
49// These options are inspected while constructing the Worker. Resolve them in
50// the outer props Effect; pass-through properties can remain deferred Inputs.
51type VocsInput<Bindings extends WorkerBindingProps> = InputProps<
52 VocsProps<Bindings>,
53 "assets"
54>;
55
56/**
57 * A Cloudflare Worker deployed from a [Vocs](https://vocs.dev) documentation project.
58 *
59 * Vocs' `vocs.config.*` loads natively. Alchemy runs Vocs' Waku/RSC build,
60 * deploys its server environments as a Worker, and publishes the client and
61 * prerendered output as static assets. No Vite or Wrangler config is required.
62 *
63 * Requires `@alchemy.run/frontend-frameworks`, `vocs`, and Vocs' Waku peer
64 * dependencies in the project.
65 *
66 * Input files are content-hashed (respecting `.gitignore` by default), so an
67 * unchanged project skips its build and deployment. Vocs' server runtime uses
68 * Node APIs, enabled by the Worker's compatibility date
69 * flags automatically.
70 *
71 * ### Deploying a Vocs Site
72 * A single resource builds the documentation project and deploys its server
73 * runtime, prerendered pages, generated files, and public assets.
74 *
75 * **Example:** Vocs documentation site
76 * ```typescript
77 * const docs = yield* Cloudflare.Website.Vocs("Docs", {
78 * rootDir: "./docs",
79 * });
80 * ```
81 *
82 * ### Bindings
83 * Pass Cloudflare resources through `env` like any other Worker. Server-side
84 * Vocs and MDX code can access them from `cloudflare:workers`.
85 *
86 * **Example:** Vocs with a KV namespace
87 * ```typescript
88 * const searchCache = yield* Cloudflare.KV.Namespace("SearchCache");
89 *
90 * const docs = yield* Cloudflare.Website.Vocs("Docs", {
91 * rootDir: "./docs",
92 * env: {
93 * SEARCH_CACHE: searchCache,
94 * },
95 * });
96 * ```
97 *
98 * ### Custom Build Output
99 * Vocs configuration continues to own the output directory. When
100 * `vocs.config.*` changes `outDir`, mirror that value on the resource so the
101 * generated directory is excluded from the rebuild hash and read correctly.
102 *
103 * **Example:** Custom output directory
104 * ```typescript
105 * // vocs.config.ts: defineConfig({ outDir: "build" })
106 * const docs = yield* Cloudflare.Website.Vocs("Docs", {
107 * rootDir: "./docs",
108 * outDir: "build",
109 * });
110 * ```
111 *
112 * ### Custom Rebuild Scope
113 * Use `memo` to narrow the files that trigger a rebuild in large projects.
114 *
115 * **Example:** Narrowing the memo scope
116 * ```typescript
117 * const docs = yield* Cloudflare.Website.Vocs("Docs", {
118 * rootDir: "./docs",
119 * memo: {
120 * include: ["src/**", "public/**", "vocs.config.ts", "package.json"],
121 * },
122 * });
123 * ```
124 *
125 * ### Class Form
126 * Calling `Vocs` without arguments returns a constructor for declaring the
127 * deployed Worker as a named class.
128 *
129 * **Example:** Declaring a Vocs Worker class
130 * ```typescript
131 * class Docs extends Cloudflare.Website.Vocs<Docs>()("Docs", {
132 * rootDir: "./docs",
133 * }) {}
134 *
135 * const docs = yield* Docs;
136 * ```
137 *
138 * @resource
139 * @product Website
140 * @category Workers & Compute
141 */
142export const Vocs: {
143 <Self>(): {
144 <const Bindings extends WorkerBindingProps = {}, Req = never>(
145 id: string,
146 propsEff?:
147 | VocsInput<Bindings>
148 | Effect.Effect<VocsInput<Bindings>, never, Req>,
149 ): Effect.Effect<Self, never, Req | Providers> & {
150 new (): Worker<{
151 [
152 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
153 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
154 }>;
155 };
156 };
157 <const Bindings extends WorkerBindingProps = {}, Req = never>(
158 id: string,
159 propsEff?:
160 | VocsInput<Bindings>
161 | Effect.Effect<VocsInput<Bindings>, never, Req>,
162 ): Effect.Effect<
163 Worker<{
164 [
165 binding in keyof NormalizedBindings<Bindings, WorkerAssetsConfig>
166 ]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
167 }>,
168 never,
169 Req | Providers
170 >;
171} = (<const Bindings extends WorkerBindingProps = {}, Req = never>(
172 id?: string,
173 propsEff?:
174 | VocsInput<Bindings>
175 | Effect.Effect<VocsInput<Bindings>, never, Req>,
176) =>
177 id === undefined
178 ? <const Bindings extends WorkerBindingProps = {}, Req = never>(
179 id: string,
180 propsEff?:
181 | VocsInput<Bindings>
182 | Effect.Effect<VocsInput<Bindings>, never, Req>,
183 ) => effectClass(Vocs(id, propsEff))
184 : Worker(
185 id,
186 Effect.map(
187 Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
188 (props) => ({
189 ...props,
190 // The Worker compatibility resolver enables Node.js APIs from
191 // the date (or adds the flag when a caller pins an older date).
192 assets: {
193 htmlHandling: "drop-trailing-slash" as const,
194 ...props?.assets,
195 },
196 main: undefined!,
197 source: {
198 provider: VOCS_SOURCE_PROVIDER,
199 devMode: "server",
200 options: {
201 rootDir: props?.rootDir,
202 outDir: props?.outDir,
203 memo: props?.memo,
204 },
205 },
206 }),
207 ),
208 )) as any;