alchemy 2.0.0-beta.51 → 2.0.0-beta.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/bin/exec.js +16 -22
  2. package/bin/exec.js.map +1 -1
  3. package/lib/Apply.js +15 -3
  4. package/lib/Apply.js.map +1 -1
  5. package/lib/Auth/AuthProvider.js +1 -1
  6. package/lib/Auth/AuthProvider.js.map +1 -1
  7. package/lib/Binding.d.ts.map +1 -1
  8. package/lib/Binding.js +12 -8
  9. package/lib/Binding.js.map +1 -1
  10. package/lib/Build/DevServer.d.ts +113 -0
  11. package/lib/Build/DevServer.d.ts.map +1 -0
  12. package/lib/Build/DevServer.js +212 -0
  13. package/lib/Build/DevServer.js.map +1 -0
  14. package/lib/Build/Local.d.ts +2 -0
  15. package/lib/Build/Local.d.ts.map +1 -0
  16. package/lib/Build/Local.js +4 -0
  17. package/lib/Build/Local.js.map +1 -0
  18. package/lib/Bundle/Bundle.js +1 -1
  19. package/lib/Bundle/Bundle.js.map +1 -1
  20. package/lib/Cloudflare/Access.js +1 -1
  21. package/lib/Cloudflare/Access.js.map +1 -1
  22. package/lib/Cloudflare/AiGateway/AiGateway.d.ts +1 -1
  23. package/lib/Cloudflare/AiGateway/AiGateway.js +1 -1
  24. package/lib/Cloudflare/Browser/Browser.d.ts +84 -12
  25. package/lib/Cloudflare/Browser/Browser.d.ts.map +1 -1
  26. package/lib/Cloudflare/Browser/Browser.js +80 -9
  27. package/lib/Cloudflare/Browser/Browser.js.map +1 -1
  28. package/lib/Cloudflare/Browser/BrowserBinding.d.ts +90 -16
  29. package/lib/Cloudflare/Browser/BrowserBinding.d.ts.map +1 -1
  30. package/lib/Cloudflare/Browser/BrowserBinding.js +41 -6
  31. package/lib/Cloudflare/Browser/BrowserBinding.js.map +1 -1
  32. package/lib/Cloudflare/Browser/index.d.ts +1 -1
  33. package/lib/Cloudflare/Browser/index.d.ts.map +1 -1
  34. package/lib/Cloudflare/Browser/index.js.map +1 -1
  35. package/lib/Cloudflare/Container/ContainerBinding.d.ts.map +1 -1
  36. package/lib/Cloudflare/Container/ContainerBinding.js +2 -4
  37. package/lib/Cloudflare/Container/ContainerBinding.js.map +1 -1
  38. package/lib/Cloudflare/Images/Images.d.ts +4 -3
  39. package/lib/Cloudflare/Images/Images.d.ts.map +1 -1
  40. package/lib/Cloudflare/Images/Images.js.map +1 -1
  41. package/lib/Cloudflare/Providers.d.ts +1 -1
  42. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  43. package/lib/Cloudflare/Providers.js +4 -1
  44. package/lib/Cloudflare/Providers.js.map +1 -1
  45. package/lib/Cloudflare/RateLimit/RateLimit.d.ts +4 -3
  46. package/lib/Cloudflare/RateLimit/RateLimit.d.ts.map +1 -1
  47. package/lib/Cloudflare/RateLimit/RateLimit.js.map +1 -1
  48. package/lib/Cloudflare/Website/StaticSite.d.ts +64 -12
  49. package/lib/Cloudflare/Website/StaticSite.d.ts.map +1 -1
  50. package/lib/Cloudflare/Website/StaticSite.js +80 -30
  51. package/lib/Cloudflare/Website/StaticSite.js.map +1 -1
  52. package/lib/Cloudflare/Website/Vite.d.ts +36 -7
  53. package/lib/Cloudflare/Website/Vite.d.ts.map +1 -1
  54. package/lib/Cloudflare/Website/Vite.js +26 -8
  55. package/lib/Cloudflare/Website/Vite.js.map +1 -1
  56. package/lib/Cloudflare/Workers/Assets.d.ts +10 -4
  57. package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
  58. package/lib/Cloudflare/Workers/Assets.js +6 -6
  59. package/lib/Cloudflare/Workers/Assets.js.map +1 -1
  60. package/lib/Cloudflare/Workers/DynamicWorkerLoader.d.ts +4 -4
  61. package/lib/Cloudflare/Workers/InferEnv.d.ts +1 -1
  62. package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
  63. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  64. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +45 -11
  65. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  66. package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.d.ts +5 -4
  67. package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.d.ts.map +1 -1
  68. package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.js +4 -2
  69. package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.js.map +1 -1
  70. package/lib/Cloudflare/Workers/RpcWorker.d.ts +3 -2
  71. package/lib/Cloudflare/Workers/RpcWorker.d.ts.map +1 -1
  72. package/lib/Cloudflare/Workers/RpcWorker.js +5 -3
  73. package/lib/Cloudflare/Workers/RpcWorker.js.map +1 -1
  74. package/lib/Cloudflare/Workers/VersionMetadata.d.ts +99 -0
  75. package/lib/Cloudflare/Workers/VersionMetadata.d.ts.map +1 -0
  76. package/lib/Cloudflare/Workers/VersionMetadata.js +65 -0
  77. package/lib/Cloudflare/Workers/VersionMetadata.js.map +1 -0
  78. package/lib/Cloudflare/Workers/VersionMetadataBinding.d.ts +35 -0
  79. package/lib/Cloudflare/Workers/VersionMetadataBinding.d.ts.map +1 -0
  80. package/lib/Cloudflare/Workers/VersionMetadataBinding.js +31 -0
  81. package/lib/Cloudflare/Workers/VersionMetadataBinding.js.map +1 -0
  82. package/lib/Cloudflare/Workers/Worker.d.ts +18 -18
  83. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  84. package/lib/Cloudflare/Workers/Worker.js +23 -37
  85. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  86. package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
  87. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +7 -0
  88. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
  89. package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -1
  90. package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
  91. package/lib/Cloudflare/Workers/WorkerBinding.js.map +1 -1
  92. package/lib/Cloudflare/Workers/Workflow.d.ts +8 -1
  93. package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
  94. package/lib/Cloudflare/Workers/Workflow.js +1 -0
  95. package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
  96. package/lib/Cloudflare/Workers/WorkflowBridge.d.ts.map +1 -1
  97. package/lib/Cloudflare/Workers/WorkflowBridge.js +31 -2
  98. package/lib/Cloudflare/Workers/WorkflowBridge.js.map +1 -1
  99. package/lib/Cloudflare/Workers/index.d.ts +2 -0
  100. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  101. package/lib/Cloudflare/Workers/index.js +2 -0
  102. package/lib/Cloudflare/Workers/index.js.map +1 -1
  103. package/lib/Cloudflare/Zaraz/ZarazConfig.d.ts.map +1 -1
  104. package/lib/Diff.d.ts.map +1 -1
  105. package/lib/Diff.js +7 -0
  106. package/lib/Diff.js.map +1 -1
  107. package/lib/Drizzle/Postgres.d.ts +13 -12
  108. package/lib/Drizzle/Postgres.d.ts.map +1 -1
  109. package/lib/Drizzle/Postgres.js +13 -12
  110. package/lib/Drizzle/Postgres.js.map +1 -1
  111. package/lib/Drizzle/Schema.d.ts +1 -1
  112. package/lib/Drizzle/Schema.d.ts.map +1 -1
  113. package/lib/Drizzle/Schema.js +14 -6
  114. package/lib/Drizzle/Schema.js.map +1 -1
  115. package/lib/Local/RpcSerialization.js +5 -2
  116. package/lib/Local/RpcSerialization.js.map +1 -1
  117. package/lib/Platform.d.ts.map +1 -1
  118. package/lib/Platform.js +27 -25
  119. package/lib/Platform.js.map +1 -1
  120. package/lib/Resource.d.ts +1 -1
  121. package/lib/Resource.d.ts.map +1 -1
  122. package/lib/Resource.js +9 -12
  123. package/lib/Resource.js.map +1 -1
  124. package/lib/Runtime.d.ts +1 -1
  125. package/lib/Runtime.d.ts.map +1 -1
  126. package/lib/Runtime.js +3 -4
  127. package/lib/Runtime.js.map +1 -1
  128. package/lib/Util/effect.d.ts.map +1 -1
  129. package/lib/Util/effect.js +13 -15
  130. package/lib/Util/effect.js.map +1 -1
  131. package/lib/Util/poll.d.ts +0 -1
  132. package/lib/Util/poll.d.ts.map +1 -1
  133. package/lib/Util/poll.js +2 -2
  134. package/lib/Util/poll.js.map +1 -1
  135. package/lib/tsconfig.test.tsbuildinfo +1 -1
  136. package/package.json +20 -20
  137. package/src/Apply.ts +22 -3
  138. package/src/Auth/AuthProvider.ts +1 -1
  139. package/src/Binding.ts +45 -38
  140. package/src/Build/DevServer.ts +330 -0
  141. package/src/Build/Local.ts +4 -0
  142. package/src/Bundle/Bundle.ts +1 -1
  143. package/src/Cloudflare/Access.ts +1 -1
  144. package/src/Cloudflare/AiGateway/AiGateway.ts +1 -1
  145. package/src/Cloudflare/Browser/Browser.ts +84 -12
  146. package/src/Cloudflare/Browser/BrowserBinding.ts +205 -33
  147. package/src/Cloudflare/Browser/index.ts +15 -2
  148. package/src/Cloudflare/Container/ContainerBinding.ts +5 -10
  149. package/src/Cloudflare/Images/Images.ts +4 -3
  150. package/src/Cloudflare/Providers.ts +5 -0
  151. package/src/Cloudflare/RateLimit/RateLimit.ts +4 -3
  152. package/src/Cloudflare/Website/StaticSite.ts +154 -41
  153. package/src/Cloudflare/Website/Vite.ts +73 -25
  154. package/src/Cloudflare/Workers/Assets.ts +10 -8
  155. package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +4 -4
  156. package/src/Cloudflare/Workers/InferEnv.ts +14 -12
  157. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +54 -13
  158. package/src/Cloudflare/Workers/RpcDurableObjectNamespace.ts +11 -8
  159. package/src/Cloudflare/Workers/RpcWorker.ts +8 -7
  160. package/src/Cloudflare/Workers/VersionMetadata.ts +129 -0
  161. package/src/Cloudflare/Workers/VersionMetadataBinding.ts +82 -0
  162. package/src/Cloudflare/Workers/Worker.ts +60 -81
  163. package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +6 -0
  164. package/src/Cloudflare/Workers/WorkerBinding.ts +2 -0
  165. package/src/Cloudflare/Workers/Workflow.ts +12 -1
  166. package/src/Cloudflare/Workers/WorkflowBridge.ts +47 -6
  167. package/src/Cloudflare/Workers/index.ts +2 -0
  168. package/src/Diff.ts +7 -0
  169. package/src/Drizzle/Postgres.ts +13 -12
  170. package/src/Drizzle/Schema.ts +16 -6
  171. package/src/Local/RpcSerialization.ts +5 -2
  172. package/src/Platform.ts +36 -32
  173. package/src/Resource.ts +12 -13
  174. package/src/Runtime.ts +3 -7
  175. package/src/Util/effect.ts +13 -17
  176. package/src/Util/poll.ts +3 -3
@@ -1,11 +1,17 @@
1
+ import * as Output from "alchemy/Output";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Redacted from "effect/Redacted";
4
+ import { AlchemyContext } from "../../AlchemyContext.ts";
3
5
  import { Command, type CommandProps } from "../../Build/Command.ts";
6
+ import { DevServer } from "../../Build/DevServer.ts";
4
7
  import type { InputProps } from "../../Input.ts";
5
8
  import * as Namespace from "../../Namespace.ts";
9
+ import { effectClass } from "../../Util/effect.ts";
10
+ import type { Providers } from "../Providers.ts";
6
11
  import type { AssetsConfig } from "../Workers/Assets.ts";
7
12
  import {
8
13
  Worker,
14
+ type NormalizedBindings,
9
15
  type WorkerAssetsConfig,
10
16
  type WorkerBindingProps,
11
17
  type WorkerProps,
@@ -19,13 +25,48 @@ export interface StaticSiteProps<Bindings extends WorkerBindingProps = {}>
19
25
  * Optional configuration for static asset routing behavior.
20
26
  * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc.
21
27
  */
22
- assetsConfig?: AssetsConfig;
28
+
29
+ assets?: AssetsConfig;
30
+ /**
31
+ * Local dev configuration. When `alchemy dev` runs, the build command is
32
+ * skipped and `command` is spawned as a long-lived child process tied to
33
+ * the stack's scope. Alchemy does not proxy or interpret the process —
34
+ * the dev server's own URL (e.g. `http://localhost:5173`) is what you
35
+ * open in the browser.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * Cloudflare.StaticSite("App", {
40
+ * command: "npm run build",
41
+ * outdir: "dist",
42
+ * main: "./src/worker.ts",
43
+ * dev: { command: "npm run dev" },
44
+ * });
45
+ * ```
46
+ */
23
47
  dev?: {
48
+ /**
49
+ * Shell command to run as the local dev server (e.g. `npm run dev`).
50
+ */
24
51
  command: string;
52
+ /**
53
+ * Working directory for {@link command}. Defaults to {@link CommandProps.cwd}
54
+ * (the build command's `cwd`), or `process.cwd()` if neither is set.
55
+ */
56
+ cwd?: string;
57
+ /**
58
+ * Override for the `url` output if alchemy fails to detect it from the stdout of the dev command
59
+ */
60
+ url?: string;
25
61
  };
26
62
  }
27
63
 
28
- export type StaticSite = ReturnType<typeof StaticSite>;
64
+ type StaticSiteWorker<Bindings extends WorkerBindingProps> = Worker<{
65
+ [binding in keyof NormalizedBindings<
66
+ Bindings,
67
+ WorkerAssetsConfig
68
+ >]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
69
+ }>;
29
70
 
30
71
  /**
31
72
  * A Cloudflare Worker that serves static assets built by a shell command.
@@ -68,7 +109,7 @@ export type StaticSite = ReturnType<typeof StaticSite>;
68
109
  * ```
69
110
  *
70
111
  * @section Asset Configuration
71
- * Use `assetsConfig` to control how Cloudflare handles routing for
112
+ * Use `assets` to control how Cloudflare handles routing for
72
113
  * your static files — HTML handling, not-found behavior, etc.
73
114
  *
74
115
  * @example SPA-style routing
@@ -77,7 +118,7 @@ export type StaticSite = ReturnType<typeof StaticSite>;
77
118
  * command: "npm run build",
78
119
  * outdir: "dist",
79
120
  * main: "./src/worker.ts",
80
- * assetsConfig: {
121
+ * assets: {
81
122
  * htmlHandling: "auto-trailing-slash",
82
123
  * notFoundHandling: "single-page-application",
83
124
  * },
@@ -113,54 +154,126 @@ export type StaticSite = ReturnType<typeof StaticSite>;
113
154
  * },
114
155
  * });
115
156
  * ```
157
+ *
158
+ * @section Class Form
159
+ * Calling `StaticSite` with no arguments returns a constructor you can
160
+ * `extend` to declare the Worker as a named class. The class is both
161
+ * an `Effect` you can `yield*` to deploy and a type you can reference
162
+ * elsewhere — useful when other resources need to bind to this Worker.
163
+ *
164
+ * @example Declaring a Worker class
165
+ * ```typescript
166
+ * class Blog extends Cloudflare.StaticSite<Blog>()("Blog", {
167
+ * command: "hugo --minify",
168
+ * outdir: "public",
169
+ * main: "./src/worker.ts",
170
+ * }) {}
171
+ *
172
+ * const site = yield* Blog;
173
+ * ```
116
174
  */
117
- export const StaticSite = <
175
+ export const StaticSite: {
176
+ <Self>(): {
177
+ <const Bindings extends WorkerBindingProps = {}, Req = never>(
178
+ id: string,
179
+ propsEff:
180
+ | InputProps<StaticSiteProps<Bindings>, "dev">
181
+ | Effect.Effect<
182
+ InputProps<StaticSiteProps<Bindings>, "dev">,
183
+ never,
184
+ Req
185
+ >,
186
+ ): Effect.Effect<Self, never, Req | Providers> & {
187
+ new (): StaticSiteWorker<Bindings>;
188
+ };
189
+ };
190
+ <const Bindings extends WorkerBindingProps = {}, Req = never>(
191
+ id: string,
192
+ propsEff:
193
+ | InputProps<StaticSiteProps<Bindings>, "dev">
194
+ | Effect.Effect<InputProps<StaticSiteProps<Bindings>, "dev">, never, Req>,
195
+ ): Effect.Effect<StaticSiteWorker<Bindings>, never, Req | Providers>;
196
+ } = ((id?: any, propsEff?: any) =>
197
+ id === undefined
198
+ ? (id: string, propsEff: any) => effectClass(makeStaticSite(id, propsEff))
199
+ : makeStaticSite(id, propsEff)) as any;
200
+
201
+ const makeStaticSite = <
118
202
  const Bindings extends WorkerBindingProps = {},
119
203
  Req = never,
120
204
  >(
121
205
  id: string,
122
206
  propsEff:
123
- | InputProps<StaticSiteProps<Bindings>>
124
- | Effect.Effect<InputProps<StaticSiteProps<Bindings>>, never, Req>,
207
+ | InputProps<StaticSiteProps<Bindings>, "dev">
208
+ | Effect.Effect<InputProps<StaticSiteProps<Bindings>, "dev">, never, Req>,
125
209
  ) =>
126
210
  Effect.gen(function* () {
127
211
  const props = Effect.isEffect(propsEff)
128
212
  ? propsEff
129
213
  : Effect.succeed(propsEff);
214
+ const { dev: isDevPhase } = yield* AlchemyContext;
130
215
 
131
- // TODO(sam): local dev/hmr support?
132
- const build = yield* Command(
133
- "Build",
134
- Effect.map(props, (props) => ({
135
- command: props.command,
136
- cwd: props.cwd,
137
- memo: props.memo,
138
- outdir: props.outdir,
139
- env: props.env
140
- ? Object.fromEntries(
141
- Object.entries(props.env).flatMap(([k, v]) => {
142
- if (v === undefined) return [];
143
- if (typeof v === "string" || Redacted.isRedacted(v))
144
- return [[k, v]];
145
- return [[k, JSON.stringify(v)]];
146
- }),
147
- )
148
- : undefined,
149
- })),
150
- );
216
+ return yield* Effect.gen(function* () {
217
+ const resolved = yield* props;
218
+ const useDevServer = isDevPhase && resolved.dev !== undefined;
151
219
 
152
- return yield* Worker<Bindings, WorkerAssetsConfig, Req>(
153
- "Worker",
154
- Effect.map(props, (props) => ({
155
- ...props,
156
- assets: {
157
- path: build.outdir,
158
- hash: build.hash,
159
- config: props.assetsConfig,
160
- },
161
- // Omit the dev command from WorkerProps since it's different from WorkerProps["dev"].
162
- // TODO: we'll need to update this when we add local dev support for StaticSite.
163
- dev: undefined,
164
- })),
165
- );
220
+ // In dev mode with a dev.command, declare a DevCommand resource so
221
+ // the sidecar owns the process lifecycle (survives user-code HMR),
222
+ // skip the build, and tell Worker not to start a local instance.
223
+ let devUrl = yield* useDevServer
224
+ ? DevServer("Dev", {
225
+ command: resolved.dev!.command,
226
+ cwd:
227
+ resolved.dev!.cwd ??
228
+ (typeof resolved.cwd === "string" ? resolved.cwd : undefined),
229
+ }).pipe(
230
+ Effect.map((d) =>
231
+ Output.map(d.url, (url) => url ?? resolved.dev?.url ?? false),
232
+ ),
233
+ )
234
+ : Effect.succeed(false as const);
235
+
236
+ const build = useDevServer
237
+ ? undefined
238
+ : yield* Command("Build", {
239
+ command: resolved.command,
240
+ cwd: resolved.cwd,
241
+ memo: resolved.memo,
242
+ outdir: resolved.outdir,
243
+ env: resolved.env
244
+ ? Object.fromEntries(
245
+ Object.entries(resolved.env).flatMap(([k, v]) => {
246
+ if (v === undefined) return [];
247
+ if (typeof v === "string" || Redacted.isRedacted(v))
248
+ return [[k, v]];
249
+ return [[k, JSON.stringify(v)]];
250
+ }),
251
+ )
252
+ : undefined,
253
+ });
254
+
255
+ // Pure-static sites don't need a custom Worker entrypoint —
256
+ // delegate every request straight to the ASSETS binding. Only
257
+ // injected when the user provided neither `main` nor `script`.
258
+ const fallbackScript =
259
+ resolved.main == null && resolved.script == null
260
+ ? `export default { fetch: (request, env) => env.ASSETS.fetch(request) };`
261
+ : undefined;
262
+
263
+ return yield* Worker<Bindings, WorkerAssetsConfig, Req>("Worker", {
264
+ ...resolved,
265
+ assets: build
266
+ ? {
267
+ directory: build.outdir,
268
+ hash: build.hash,
269
+ ...resolved.assets,
270
+ }
271
+ : undefined,
272
+ // Opt out of the local Worker in dev when the external DevCommand
273
+ // is serving the content. The Worker resource still exists in
274
+ // state with a stub Attributes shape.
275
+ dev: useDevServer ? devUrl : undefined,
276
+ script: fallbackScript ?? resolved.script,
277
+ });
278
+ });
166
279
  }).pipe(Namespace.push(id));
@@ -1,16 +1,19 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import type { MemoOptions } from "../../Build/Memo.ts";
3
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";
4
7
  import {
5
8
  Worker,
9
+ type NormalizedBindings,
6
10
  type WorkerAssetsConfig,
7
11
  type WorkerBindingProps,
8
12
  type WorkerProps,
9
13
  } from "../Workers/Worker.ts";
10
-
11
14
  export interface ViteProps<
12
15
  Bindings extends WorkerBindingProps = {},
13
- > extends Omit<WorkerProps<Bindings>, "vite" | "main"> {
16
+ > extends Omit<WorkerProps<Bindings>, "vite" | "main" | "assets"> {
14
17
  /**
15
18
  * Root directory passed to Vite's `root` option.
16
19
  * Defaults to the current working directory (`process.cwd()`).
@@ -24,6 +27,11 @@ export interface ViteProps<
24
27
  * @see {@link MemoOptions}
25
28
  */
26
29
  memo?: MemoOptions;
30
+ /**
31
+ * Optional configuration for static asset routing behavior.
32
+ * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc.
33
+ */
34
+ assets?: AssetsConfig;
27
35
  }
28
36
 
29
37
  /**
@@ -106,27 +114,67 @@ export interface ViteProps<
106
114
  * },
107
115
  * });
108
116
  * ```
117
+ *
118
+ * @section Class Form
119
+ * Calling `Vite` with no arguments returns a constructor you can
120
+ * `extend` to declare the Worker as a named class. The class is both
121
+ * an `Effect` you can `yield*` to deploy and a type you can reference
122
+ * elsewhere — useful when other resources need to bind to this Worker.
123
+ *
124
+ * @example Declaring a Worker class
125
+ * ```typescript
126
+ * class Website extends Cloudflare.Vite<Website>()("Website", {
127
+ * compatibility: { flags: ["nodejs_compat"] },
128
+ * }) {}
129
+ *
130
+ * const site = yield* Website;
131
+ * ```
109
132
  */
110
- export const Vite = <
111
- const Bindings extends WorkerBindingProps = {},
112
- Req = never,
113
- >(
114
- id: string,
115
- propsEff?:
116
- | InputProps<ViteProps<Bindings>>
117
- | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
118
- ) =>
119
- Worker<Bindings, WorkerAssetsConfig, Req>(
120
- id,
121
- Effect.map(
122
- Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
123
- (props) => ({
124
- ...props,
125
- main: undefined!,
126
- vite: {
127
- rootDir: props?.rootDir,
128
- memo: props?.memo,
129
- },
130
- }),
131
- ),
132
- );
133
+ export const Vite: {
134
+ <Self>(): {
135
+ <const Bindings extends WorkerBindingProps = {}, Req = never>(
136
+ id: string,
137
+ propsEff?:
138
+ | InputProps<ViteProps<Bindings>>
139
+ | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
140
+ ): Effect.Effect<Self, never, Req | Providers> & {
141
+ new (): Worker<{
142
+ [binding in keyof NormalizedBindings<
143
+ Bindings,
144
+ WorkerAssetsConfig
145
+ >]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
146
+ }>;
147
+ };
148
+ };
149
+ <const Bindings extends WorkerBindingProps = {}, Req = never>(
150
+ id: string,
151
+ propsEff?:
152
+ | InputProps<ViteProps<Bindings>>
153
+ | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
154
+ ): Effect.Effect<
155
+ Worker<{
156
+ [binding in keyof NormalizedBindings<
157
+ Bindings,
158
+ WorkerAssetsConfig
159
+ >]: NormalizedBindings<Bindings, WorkerAssetsConfig>[binding];
160
+ }>,
161
+ never,
162
+ Req | Providers
163
+ >;
164
+ } = ((id?: any, propsEff?: any) =>
165
+ id === undefined
166
+ ? (id: string, propsEff: any) => effectClass(Vite(id, propsEff))
167
+ : Worker(
168
+ id,
169
+ Effect.map(
170
+ Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
171
+ (props) => ({
172
+ ...props,
173
+ main: undefined!,
174
+ vite: {
175
+ rootDir: props?.rootDir,
176
+ memo: props?.memo,
177
+ },
178
+ }),
179
+ ),
180
+ )) as any;
@@ -32,9 +32,8 @@ export interface AssetReadResult {
32
32
  hash: string;
33
33
  }
34
34
 
35
- export interface AssetsProps {
35
+ export interface AssetsProps extends AssetsConfig {
36
36
  directory: string;
37
- config?: AssetsConfig;
38
37
  }
39
38
 
40
39
  export type ValidationError =
@@ -103,10 +102,13 @@ const createIgnoreMatcher = (patterns: string[]) => {
103
102
  return (file: string) => matcher.ignores(file);
104
103
  };
105
104
 
106
- export const readAssets = Effect.fnUntraced(function* (props: AssetsProps) {
105
+ export const readAssets = Effect.fnUntraced(function* ({
106
+ directory,
107
+ ...config
108
+ }: AssetsProps) {
107
109
  const fs = yield* FileSystem.FileSystem;
108
110
  const path = yield* Path.Path;
109
- const resolvedDirectory = path.resolve(props.directory);
111
+ const resolvedDirectory = path.resolve(directory);
110
112
  const [files, ignore, _headers, _redirects] = yield* Effect.all([
111
113
  fs.readDirectory(resolvedDirectory, { recursive: true }),
112
114
  maybeReadString(path.join(resolvedDirectory, ".assetsignore")),
@@ -151,7 +153,7 @@ export const readAssets = Effect.fnUntraced(function* (props: AssetsProps) {
151
153
  if (count > MAX_ASSET_COUNT) {
152
154
  return yield* new TooManyAssetsError({
153
155
  message: `Too many assets (the maximum count is ${MAX_ASSET_COUNT}; this directory has ${count} assets)`,
154
- directory: props.directory,
156
+ directory,
155
157
  count,
156
158
  });
157
159
  }
@@ -176,14 +178,14 @@ export const readAssets = Effect.fnUntraced(function* (props: AssetsProps) {
176
178
  // causes both unnecessary re-uploads and `NotFound` failures when
177
179
  // the previously-recorded path is gone.
178
180
  const hash = yield* sha256Object({
179
- config: props.config,
181
+ config,
180
182
  manifest: sortedManifest,
181
183
  _headers,
182
184
  _redirects,
183
185
  });
184
186
  return {
185
- directory: props.directory,
186
- config: props.config,
187
+ directory,
188
+ config,
187
189
  manifest: sortedManifest,
188
190
  _headers,
189
191
  _redirects,
@@ -100,10 +100,10 @@ type BindEffect = ReturnType<typeof bindLoader>;
100
100
  * surrounding Worker and returns a {@link DynamicWorkerLoaderHandle} with
101
101
  * `.load()` — no separate `.bind(...)` step required.
102
102
  *
103
- * The divergence is achieved via `[Symbol.iterator]`: the object is not an
104
- * `Effect` (so `InferEnv` resolves it to the native
105
- * {@link DynamicWorkerLoaderBinding} in the `env` position), but it is iterable
106
- * as one when `yield*`-ed.
103
+ * The divergence is achieved via `[Symbol.iterator]`: the object is
104
+ * deliberately not an `Effect` (so `InferEnv` and the Worker `env` resolver
105
+ * keep it as the native {@link DynamicWorkerLoaderBinding} rather than
106
+ * `yield*`-ing it), but it is iterable as one when `yield*`-ed.
107
107
  */
108
108
  export interface DynamicWorkerLoader {
109
109
  kind: DynamicWorkerTypeId;
@@ -45,20 +45,22 @@ export type GetBindingType<T> =
45
45
  : T extends Cloudflare.Images
46
46
  ? ImagesBinding
47
47
  : T extends Cloudflare.Browser
48
- ? Fetcher
48
+ ? BrowserRun
49
49
  : T extends Cloudflare.Hyperdrive
50
50
  ? Hyperdrive
51
- : T extends Cloudflare.DynamicWorkerLoader
52
- ? Cloudflare.DynamicWorkerLoaderBinding
53
- : T extends Cloudflare.DurableObjectNamespaceLike
54
- ? DurableObjectNamespace<
55
- Exclude<T["Shape"], undefined>
56
- >
57
- : T extends Redacted<any>
58
- ? // redacteds are always stored as secret_text, so are always string
59
- // we JSON.stringify when not a Redacted<string>
60
- string
61
- : T;
51
+ : T extends Cloudflare.VersionMetadata
52
+ ? WorkerVersionMetadata
53
+ : T extends Cloudflare.DynamicWorkerLoader
54
+ ? Cloudflare.DynamicWorkerLoaderBinding
55
+ : T extends Cloudflare.DurableObjectNamespaceLike
56
+ ? DurableObjectNamespace<
57
+ Exclude<T["Shape"], undefined>
58
+ >
59
+ : T extends Redacted<any>
60
+ ? // redacteds are always stored as secret_text, so are always string
61
+ // we JSON.stringify when not a Redacted<string>
62
+ string
63
+ : T;
62
64
 
63
65
  /**
64
66
  * Cloudflare service-binding wire shape for an Effect-native Worker.
@@ -66,6 +66,10 @@ import type { WorkerBinding } from "./WorkerBinding.ts";
66
66
  import { WorkerBundle, type WorkerBundleOptions } from "./WorkerBundle.ts";
67
67
  import { createWorkerName } from "./WorkerName.ts";
68
68
 
69
+ type WorkerPropsWithDev = Omit<WorkerProps, "dev"> & {
70
+ dev: Exclude<WorkerProps["dev"], false | string>;
71
+ };
72
+
69
73
  export class WorkerValidationError extends Schema.TaggedErrorClass<WorkerValidationError>()(
70
74
  "WorkerValidationError",
71
75
  {
@@ -227,7 +231,7 @@ export const LocalWorkerProvider = () =>
227
231
  bindings,
228
232
  }: {
229
233
  id: string;
230
- props: WorkerProps;
234
+ props: WorkerPropsWithDev;
231
235
  bindings: ResourceBinding<Worker["Binding"]>[];
232
236
  }) {
233
237
  const name = yield* createWorkerName(id, props.name);
@@ -244,6 +248,7 @@ export const LocalWorkerProvider = () =>
244
248
  ? Text.local(key, unredacted)
245
249
  : Json.local(key, unredacted);
246
250
  }),
251
+ ...(props.assets || props.vite ? [Assets.local("ASSETS")] : []),
247
252
  ];
248
253
  const durableObjectNamespaces: Record<string, string> = {};
249
254
  const hyperdrives: Record<string, Required<HyperdriveOrigin>> = {};
@@ -257,11 +262,19 @@ export const LocalWorkerProvider = () =>
257
262
  ) {
258
263
  // Reuse the existing namespace id if it was provided, otherwise generate a new one.
259
264
  // `workerd` uses this for the object's storage path, so it must be safe to use as a file name.
260
- durableObjectNamespaces[binding.className] =
265
+ const namespaceId =
261
266
  binding.namespaceId ??
262
- encodeURIComponent(`${id}-${binding.className}`);
267
+ encodeURIComponent(`${name}-${binding.className}`);
268
+ durableObjectNamespaces[binding.className] = namespaceId;
269
+ workerBindings.push(
270
+ yield* toRuntimeBinding({
271
+ ...binding,
272
+ namespaceId,
273
+ }),
274
+ );
275
+ } else {
276
+ workerBindings.push(yield* toRuntimeBinding(binding));
263
277
  }
264
- workerBindings.push(yield* toRuntimeBinding(binding));
265
278
  }
266
279
  if (data.hyperdrives) {
267
280
  for (const [id, origin] of Object.entries(data.hyperdrives)) {
@@ -408,7 +421,7 @@ export const LocalWorkerProvider = () =>
408
421
 
409
422
  const runInstance = Effect.fn(function* (options: {
410
423
  id: string;
411
- props: WorkerProps;
424
+ props: WorkerPropsWithDev;
412
425
  bindings: ResourceBinding<Worker["Binding"]>[];
413
426
  }) {
414
427
  const { props, bindings } = options;
@@ -450,7 +463,32 @@ export const LocalWorkerProvider = () =>
450
463
  };
451
464
  }),
452
465
  reconcile: Effect.fn(function* ({ id, news, bindings }) {
453
- const options = { id, props: news, bindings };
466
+ // `dev: false` opts out of running a local Worker entirely —
467
+ // typically because an external dev process (DevCommand) is
468
+ // serving requests. Tear down any prior instance and return a
469
+ // stub Attributes; the resource exists in state but has no
470
+ // running workerd / proxy behind it.
471
+ if (news.dev === false || typeof news.dev === "string") {
472
+ const existing = instances.get(id);
473
+ if (existing) {
474
+ yield* Fiber.interrupt(existing.fiber);
475
+ yield* Scope.close(existing.scope, Exit.void);
476
+ instances.delete(id);
477
+ }
478
+ const name = yield* createWorkerName(id, news.name);
479
+ return {
480
+ workerId: name,
481
+ workerName: name,
482
+ logpush: undefined,
483
+ url: news.dev || undefined,
484
+ tags: [],
485
+ durableObjectNamespaces: {},
486
+ accountId,
487
+ domains: [],
488
+ crons: news.crons ?? [],
489
+ } satisfies Worker["Attributes"];
490
+ }
491
+ const options = { id, props: news as WorkerPropsWithDev, bindings };
454
492
  const hash = Hash.structure(options);
455
493
  const existing = instances.get(options.id);
456
494
  if (existing) {
@@ -526,6 +564,9 @@ const toRuntimeBinding = Effect.fnUntraced(function* (b: WorkerBinding) {
526
564
  binding: b.name,
527
565
  className: b.className,
528
566
  scriptName: b.scriptName,
567
+ uniqueKey:
568
+ b.namespaceId ??
569
+ encodeURIComponent(`${b.scriptName!}-${b.className}`),
529
570
  });
530
571
  case "hyperdrive":
531
572
  return Hyperdrive.local(b.name, b.id);
@@ -600,24 +641,24 @@ const toRuntimeAssets = (
600
641
  };
601
642
  }
602
643
  return {
603
- directory: "directory" in assets ? assets.directory : assets.path,
604
- headers: assets.config?.headers,
605
- redirects: assets.config?.redirects,
644
+ directory: assets.directory,
645
+ headers: assets.headers,
646
+ redirects: assets.redirects,
606
647
  // Distilled widened generated string enums to open unions (`string & {}`);
607
648
  // the API only ever returns the known variants here.
608
- htmlHandling: assets.config?.htmlHandling as
649
+ htmlHandling: assets.htmlHandling as
609
650
  | "none"
610
651
  | "auto-trailing-slash"
611
652
  | "force-trailing-slash"
612
653
  | "drop-trailing-slash"
613
654
  | undefined,
614
- notFoundHandling: assets.config?.notFoundHandling as
655
+ notFoundHandling: assets.notFoundHandling as
615
656
  | "none"
616
657
  | "404-page"
617
658
  | "single-page-application"
618
659
  | undefined,
619
- runWorkerFirst: assets.config?.runWorkerFirst,
620
- serveDirectly: assets.config?.serveDirectly,
660
+ runWorkerFirst: assets.runWorkerFirst,
661
+ serveDirectly: assets.serveDirectly,
621
662
  };
622
663
  };
623
664