alchemy 2.0.0-beta.50 → 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 (254) hide show
  1. package/bin/exec.js +133 -135
  2. package/bin/exec.js.map +1 -1
  3. package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
  4. package/lib/AWS/Lambda/BucketEventSource.js +13 -2
  5. package/lib/AWS/Lambda/BucketEventSource.js.map +1 -1
  6. package/lib/AWS/S3/Bucket.d.ts.map +1 -1
  7. package/lib/AWS/S3/Bucket.js +49 -0
  8. package/lib/AWS/S3/Bucket.js.map +1 -1
  9. package/lib/AWS/S3/BucketNotifications.d.ts +10 -0
  10. package/lib/AWS/S3/BucketNotifications.d.ts.map +1 -1
  11. package/lib/AWS/S3/BucketNotifications.js.map +1 -1
  12. package/lib/Apply.js +15 -3
  13. package/lib/Apply.js.map +1 -1
  14. package/lib/Auth/AuthProvider.js +1 -1
  15. package/lib/Auth/AuthProvider.js.map +1 -1
  16. package/lib/Binding.d.ts.map +1 -1
  17. package/lib/Binding.js +12 -8
  18. package/lib/Binding.js.map +1 -1
  19. package/lib/Build/DevServer.d.ts +113 -0
  20. package/lib/Build/DevServer.d.ts.map +1 -0
  21. package/lib/Build/DevServer.js +212 -0
  22. package/lib/Build/DevServer.js.map +1 -0
  23. package/lib/Build/Local.d.ts +2 -0
  24. package/lib/Build/Local.d.ts.map +1 -0
  25. package/lib/Build/Local.js +4 -0
  26. package/lib/Build/Local.js.map +1 -0
  27. package/lib/Bundle/Bundle.js +1 -1
  28. package/lib/Bundle/Bundle.js.map +1 -1
  29. package/lib/Cli/commands/cloudflare.d.ts +1 -1
  30. package/lib/Cloudflare/Access.js +1 -1
  31. package/lib/Cloudflare/Access.js.map +1 -1
  32. package/lib/Cloudflare/AiGateway/AiGateway.d.ts +1 -1
  33. package/lib/Cloudflare/AiGateway/AiGateway.js +1 -1
  34. package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts +1 -1
  35. package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts.map +1 -1
  36. package/lib/Cloudflare/AiGateway/AiGatewayBinding.js +6 -2
  37. package/lib/Cloudflare/AiGateway/AiGatewayBinding.js.map +1 -1
  38. package/lib/Cloudflare/Browser/Browser.d.ts +84 -12
  39. package/lib/Cloudflare/Browser/Browser.d.ts.map +1 -1
  40. package/lib/Cloudflare/Browser/Browser.js +80 -9
  41. package/lib/Cloudflare/Browser/Browser.js.map +1 -1
  42. package/lib/Cloudflare/Browser/BrowserBinding.d.ts +90 -16
  43. package/lib/Cloudflare/Browser/BrowserBinding.d.ts.map +1 -1
  44. package/lib/Cloudflare/Browser/BrowserBinding.js +41 -6
  45. package/lib/Cloudflare/Browser/BrowserBinding.js.map +1 -1
  46. package/lib/Cloudflare/Browser/index.d.ts +1 -1
  47. package/lib/Cloudflare/Browser/index.d.ts.map +1 -1
  48. package/lib/Cloudflare/Browser/index.js.map +1 -1
  49. package/lib/Cloudflare/Container/ContainerBinding.d.ts.map +1 -1
  50. package/lib/Cloudflare/Container/ContainerBinding.js +2 -4
  51. package/lib/Cloudflare/Container/ContainerBinding.js.map +1 -1
  52. package/lib/Cloudflare/Images/Images.d.ts +4 -3
  53. package/lib/Cloudflare/Images/Images.d.ts.map +1 -1
  54. package/lib/Cloudflare/Images/Images.js.map +1 -1
  55. package/lib/Cloudflare/Providers.d.ts +1 -1
  56. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  57. package/lib/Cloudflare/Providers.js +6 -1
  58. package/lib/Cloudflare/Providers.js.map +1 -1
  59. package/lib/Cloudflare/RateLimit/RateLimit.d.ts +4 -3
  60. package/lib/Cloudflare/RateLimit/RateLimit.d.ts.map +1 -1
  61. package/lib/Cloudflare/RateLimit/RateLimit.js.map +1 -1
  62. package/lib/Cloudflare/Ruleset/Ruleset.d.ts +82 -0
  63. package/lib/Cloudflare/Ruleset/Ruleset.d.ts.map +1 -0
  64. package/lib/Cloudflare/Ruleset/Ruleset.js +126 -0
  65. package/lib/Cloudflare/Ruleset/Ruleset.js.map +1 -0
  66. package/lib/Cloudflare/Ruleset/index.d.ts +2 -0
  67. package/lib/Cloudflare/Ruleset/index.d.ts.map +1 -0
  68. package/lib/Cloudflare/Ruleset/index.js +2 -0
  69. package/lib/Cloudflare/Ruleset/index.js.map +1 -0
  70. package/lib/Cloudflare/SecretsStore/Secret.d.ts +1 -0
  71. package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
  72. package/lib/Cloudflare/SecretsStore/Secret.js +2 -0
  73. package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
  74. package/lib/Cloudflare/StateStore/State.d.ts +1 -1
  75. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  76. package/lib/Cloudflare/Website/StaticSite.d.ts +64 -12
  77. package/lib/Cloudflare/Website/StaticSite.d.ts.map +1 -1
  78. package/lib/Cloudflare/Website/StaticSite.js +80 -30
  79. package/lib/Cloudflare/Website/StaticSite.js.map +1 -1
  80. package/lib/Cloudflare/Website/Vite.d.ts +36 -7
  81. package/lib/Cloudflare/Website/Vite.d.ts.map +1 -1
  82. package/lib/Cloudflare/Website/Vite.js +26 -8
  83. package/lib/Cloudflare/Website/Vite.js.map +1 -1
  84. package/lib/Cloudflare/Workers/Assets.d.ts +10 -4
  85. package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
  86. package/lib/Cloudflare/Workers/Assets.js +6 -6
  87. package/lib/Cloudflare/Workers/Assets.js.map +1 -1
  88. package/lib/Cloudflare/Workers/DurableObjectChatPersistence.d.ts +69 -0
  89. package/lib/Cloudflare/Workers/DurableObjectChatPersistence.d.ts.map +1 -0
  90. package/lib/Cloudflare/Workers/DurableObjectChatPersistence.js +104 -0
  91. package/lib/Cloudflare/Workers/DurableObjectChatPersistence.js.map +1 -0
  92. package/lib/Cloudflare/Workers/DynamicWorkerLoader.d.ts +4 -4
  93. package/lib/Cloudflare/Workers/InferEnv.d.ts +1 -1
  94. package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
  95. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  96. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +45 -11
  97. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  98. package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.d.ts +5 -4
  99. package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.d.ts.map +1 -1
  100. package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.js +4 -2
  101. package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.js.map +1 -1
  102. package/lib/Cloudflare/Workers/RpcWorker.d.ts +3 -2
  103. package/lib/Cloudflare/Workers/RpcWorker.d.ts.map +1 -1
  104. package/lib/Cloudflare/Workers/RpcWorker.js +5 -3
  105. package/lib/Cloudflare/Workers/RpcWorker.js.map +1 -1
  106. package/lib/Cloudflare/Workers/VersionMetadata.d.ts +99 -0
  107. package/lib/Cloudflare/Workers/VersionMetadata.d.ts.map +1 -0
  108. package/lib/Cloudflare/Workers/VersionMetadata.js +65 -0
  109. package/lib/Cloudflare/Workers/VersionMetadata.js.map +1 -0
  110. package/lib/Cloudflare/Workers/VersionMetadataBinding.d.ts +35 -0
  111. package/lib/Cloudflare/Workers/VersionMetadataBinding.d.ts.map +1 -0
  112. package/lib/Cloudflare/Workers/VersionMetadataBinding.js +31 -0
  113. package/lib/Cloudflare/Workers/VersionMetadataBinding.js.map +1 -0
  114. package/lib/Cloudflare/Workers/Worker.d.ts +20 -19
  115. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  116. package/lib/Cloudflare/Workers/Worker.js +23 -37
  117. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  118. package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
  119. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +16 -0
  120. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
  121. package/lib/Cloudflare/Workers/WorkerBinding.d.ts +3 -1
  122. package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
  123. package/lib/Cloudflare/Workers/WorkerBinding.js.map +1 -1
  124. package/lib/Cloudflare/Workers/Workflow.d.ts +8 -1
  125. package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
  126. package/lib/Cloudflare/Workers/Workflow.js +1 -0
  127. package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
  128. package/lib/Cloudflare/Workers/WorkflowBridge.d.ts.map +1 -1
  129. package/lib/Cloudflare/Workers/WorkflowBridge.js +31 -2
  130. package/lib/Cloudflare/Workers/WorkflowBridge.js.map +1 -1
  131. package/lib/Cloudflare/Workers/index.d.ts +3 -0
  132. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  133. package/lib/Cloudflare/Workers/index.js +3 -0
  134. package/lib/Cloudflare/Workers/index.js.map +1 -1
  135. package/lib/Cloudflare/Zaraz/ZarazConfig.d.ts.map +1 -1
  136. package/lib/Cloudflare/index.d.ts +1 -0
  137. package/lib/Cloudflare/index.d.ts.map +1 -1
  138. package/lib/Cloudflare/index.js +1 -0
  139. package/lib/Cloudflare/index.js.map +1 -1
  140. package/lib/Deploy.d.ts +3 -2
  141. package/lib/Deploy.d.ts.map +1 -1
  142. package/lib/Deploy.js.map +1 -1
  143. package/lib/Destroy.d.ts +3 -2
  144. package/lib/Destroy.d.ts.map +1 -1
  145. package/lib/Destroy.js.map +1 -1
  146. package/lib/Diff.d.ts.map +1 -1
  147. package/lib/Diff.js +7 -0
  148. package/lib/Diff.js.map +1 -1
  149. package/lib/Drizzle/Postgres.d.ts +13 -12
  150. package/lib/Drizzle/Postgres.d.ts.map +1 -1
  151. package/lib/Drizzle/Postgres.js +13 -12
  152. package/lib/Drizzle/Postgres.js.map +1 -1
  153. package/lib/Drizzle/Schema.d.ts +1 -1
  154. package/lib/Drizzle/Schema.d.ts.map +1 -1
  155. package/lib/Drizzle/Schema.js +20 -9
  156. package/lib/Drizzle/Schema.js.map +1 -1
  157. package/lib/Local/RpcSerialization.js +5 -2
  158. package/lib/Local/RpcSerialization.js.map +1 -1
  159. package/lib/Output.d.ts.map +1 -1
  160. package/lib/Output.js +11 -10
  161. package/lib/Output.js.map +1 -1
  162. package/lib/Plan.d.ts.map +1 -1
  163. package/lib/Plan.js +5 -1
  164. package/lib/Plan.js.map +1 -1
  165. package/lib/Planetscale/MySQL/MySQLDatabase.d.ts +0 -2
  166. package/lib/Planetscale/MySQL/MySQLDatabase.d.ts.map +1 -1
  167. package/lib/Planetscale/MySQL/MySQLDatabase.js +1 -3
  168. package/lib/Planetscale/MySQL/MySQLDatabase.js.map +1 -1
  169. package/lib/Planetscale/Postgres/PostgresDatabase.d.ts +0 -2
  170. package/lib/Planetscale/Postgres/PostgresDatabase.d.ts.map +1 -1
  171. package/lib/Planetscale/Postgres/PostgresDatabase.js +1 -3
  172. package/lib/Planetscale/Postgres/PostgresDatabase.js.map +1 -1
  173. package/lib/Platform.d.ts +4 -4
  174. package/lib/Platform.d.ts.map +1 -1
  175. package/lib/Platform.js +27 -25
  176. package/lib/Platform.js.map +1 -1
  177. package/lib/Resource.d.ts +7 -1
  178. package/lib/Resource.d.ts.map +1 -1
  179. package/lib/Resource.js +11 -12
  180. package/lib/Resource.js.map +1 -1
  181. package/lib/Runtime.d.ts +1 -1
  182. package/lib/Runtime.d.ts.map +1 -1
  183. package/lib/Runtime.js +3 -4
  184. package/lib/Runtime.js.map +1 -1
  185. package/lib/Stack.d.ts +5 -4
  186. package/lib/Stack.d.ts.map +1 -1
  187. package/lib/Stack.js.map +1 -1
  188. package/lib/Test/Core.d.ts +2 -2
  189. package/lib/Test/Core.d.ts.map +1 -1
  190. package/lib/Util/effect.d.ts.map +1 -1
  191. package/lib/Util/effect.js +13 -15
  192. package/lib/Util/effect.js.map +1 -1
  193. package/lib/Util/poll.d.ts +0 -1
  194. package/lib/Util/poll.d.ts.map +1 -1
  195. package/lib/Util/poll.js +2 -2
  196. package/lib/Util/poll.js.map +1 -1
  197. package/lib/tsconfig.test.tsbuildinfo +1 -1
  198. package/package.json +20 -20
  199. package/src/AWS/Lambda/BucketEventSource.ts +16 -1
  200. package/src/AWS/S3/Bucket.ts +78 -0
  201. package/src/AWS/S3/BucketNotifications.ts +10 -0
  202. package/src/Apply.ts +22 -3
  203. package/src/Auth/AuthProvider.ts +1 -1
  204. package/src/Binding.ts +45 -38
  205. package/src/Build/DevServer.ts +330 -0
  206. package/src/Build/Local.ts +4 -0
  207. package/src/Bundle/Bundle.ts +1 -1
  208. package/src/Cloudflare/Access.ts +1 -1
  209. package/src/Cloudflare/AiGateway/AiGateway.ts +1 -1
  210. package/src/Cloudflare/AiGateway/AiGatewayBinding.ts +8 -3
  211. package/src/Cloudflare/Browser/Browser.ts +84 -12
  212. package/src/Cloudflare/Browser/BrowserBinding.ts +205 -33
  213. package/src/Cloudflare/Browser/index.ts +15 -2
  214. package/src/Cloudflare/Container/ContainerBinding.ts +5 -10
  215. package/src/Cloudflare/Images/Images.ts +4 -3
  216. package/src/Cloudflare/Providers.ts +8 -0
  217. package/src/Cloudflare/RateLimit/RateLimit.ts +4 -3
  218. package/src/Cloudflare/Ruleset/Ruleset.ts +223 -0
  219. package/src/Cloudflare/Ruleset/index.ts +1 -0
  220. package/src/Cloudflare/SecretsStore/Secret.ts +4 -0
  221. package/src/Cloudflare/Website/StaticSite.ts +154 -41
  222. package/src/Cloudflare/Website/Vite.ts +73 -25
  223. package/src/Cloudflare/Workers/Assets.ts +10 -8
  224. package/src/Cloudflare/Workers/DurableObjectChatPersistence.ts +131 -0
  225. package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +4 -4
  226. package/src/Cloudflare/Workers/InferEnv.ts +14 -12
  227. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +54 -13
  228. package/src/Cloudflare/Workers/RpcDurableObjectNamespace.ts +11 -8
  229. package/src/Cloudflare/Workers/RpcWorker.ts +8 -7
  230. package/src/Cloudflare/Workers/VersionMetadata.ts +129 -0
  231. package/src/Cloudflare/Workers/VersionMetadataBinding.ts +82 -0
  232. package/src/Cloudflare/Workers/Worker.ts +66 -82
  233. package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +14 -0
  234. package/src/Cloudflare/Workers/WorkerBinding.ts +4 -0
  235. package/src/Cloudflare/Workers/Workflow.ts +12 -1
  236. package/src/Cloudflare/Workers/WorkflowBridge.ts +47 -6
  237. package/src/Cloudflare/Workers/index.ts +3 -0
  238. package/src/Cloudflare/index.ts +1 -0
  239. package/src/Deploy.ts +2 -1
  240. package/src/Destroy.ts +2 -1
  241. package/src/Diff.ts +7 -0
  242. package/src/Drizzle/Postgres.ts +13 -12
  243. package/src/Drizzle/Schema.ts +27 -9
  244. package/src/Local/RpcSerialization.ts +5 -2
  245. package/src/Output.ts +12 -10
  246. package/src/Plan.ts +5 -1
  247. package/src/Planetscale/MySQL/MySQLDatabase.ts +1 -6
  248. package/src/Planetscale/Postgres/PostgresDatabase.ts +1 -5
  249. package/src/Platform.ts +48 -36
  250. package/src/Resource.ts +22 -13
  251. package/src/Runtime.ts +3 -7
  252. package/src/Stack.ts +11 -10
  253. package/src/Util/effect.ts +13 -17
  254. package/src/Util/poll.ts +3 -3
@@ -0,0 +1,330 @@
1
+ import * as Deferred from "effect/Deferred";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Exit from "effect/Exit";
4
+ import * as Hash from "effect/Hash";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Option from "effect/Option";
7
+ import * as Redacted from "effect/Redacted";
8
+ import * as Scope from "effect/Scope";
9
+ import * as Stream from "effect/Stream";
10
+ import { ChildProcess } from "effect/unstable/process";
11
+ import {
12
+ ChildProcessSpawner,
13
+ type ChildProcessHandle,
14
+ } from "effect/unstable/process/ChildProcessSpawner";
15
+ import { AlchemyContext } from "../AlchemyContext.ts";
16
+ import { isResolved } from "../Diff.ts";
17
+ import * as RpcProvider from "../Local/RpcProvider.ts";
18
+ import * as Provider from "../Provider.ts";
19
+ import { Resource } from "../Resource.ts";
20
+
21
+ export interface DevServerProps {
22
+ /**
23
+ * Shell command to run as a long-lived dev process (e.g. `npm run dev`).
24
+ */
25
+ command: string;
26
+ /**
27
+ * Working directory for the command. Defaults to `process.cwd()`.
28
+ */
29
+ cwd?: string;
30
+ /**
31
+ * Extra environment variables passed to the command on top of `process.env`.
32
+ */
33
+ env?: Record<string, string | Redacted.Redacted<string>>;
34
+ }
35
+
36
+ /**
37
+ * A long-lived shell process scoped to a stack instance, started during
38
+ * `alchemy dev`, restarted when inputs are changes.
39
+ *
40
+ * The provider runs inside the dev sidecar (see `Build/Local.ts`) so the
41
+ * child process survives user-code HMR — Alchemy's user process can restart
42
+ * without killing your `npm run dev` server. During deploy this performs a no-op
43
+ *
44
+ * The child's stdout/stderr are mirrored to the sidecar's terminal so the
45
+ * user still sees colored dev-server output, and each line is scanned for
46
+ * the first `http(s)://…` URL. The first match (with ANSI escapes stripped)
47
+ * is exposed as `url` — useful for surfacing a dev server's local URL back
48
+ * out to whatever resource declared this `DevServer`.
49
+ */
50
+ export interface DevServer extends Resource<
51
+ "Build.DevServer",
52
+ DevServerProps,
53
+ {
54
+ /**
55
+ * URL extracted from the first matching stdout/stderr line. Best-effort:
56
+ * `undefined` if no URL appeared within {@link URL_EXTRACT_TIMEOUT}.
57
+ */
58
+ url?: string;
59
+ }
60
+ > {}
61
+
62
+ /**
63
+ * A long-lived shell process scoped to a stack instance, started during
64
+ * `alchemy dev` and restarted when inputs change. During deploy this is
65
+ * a no-op — `DevServer` resources only run in dev mode.
66
+ *
67
+ * The child process runs inside the dev sidecar so it survives
68
+ * user-code HMR restarts. Its stdout/stderr are mirrored to the
69
+ * terminal and scanned for the first `http(s)://…` URL, which is
70
+ * exposed as the `url` output attribute.
71
+ *
72
+ * @resource
73
+ *
74
+ * @section Basic Usage
75
+ * Pass a shell command that starts a long-lived dev server. Alchemy
76
+ * runs it in the background and extracts the first URL it prints.
77
+ *
78
+ * @example Start a Vite dev server
79
+ * ```typescript
80
+ * const dev = yield* DevServer("Frontend", {
81
+ * command: "npm run dev",
82
+ * });
83
+ * console.log(dev.url); // e.g. "http://localhost:5173"
84
+ * ```
85
+ *
86
+ * @section Working Directory
87
+ * Use `cwd` to run the command in a subdirectory — useful in
88
+ * monorepos where each package has its own dev server.
89
+ *
90
+ * @example Monorepo package
91
+ * ```typescript
92
+ * const dev = yield* DevServer("Web", {
93
+ * command: "npm run dev",
94
+ * cwd: "apps/web",
95
+ * });
96
+ * ```
97
+ *
98
+ * @section Environment Variables
99
+ * Extra environment variables are merged on top of `process.env`.
100
+ * Sensitive values can be wrapped in `Redacted` to keep them out
101
+ * of logs and state files.
102
+ *
103
+ * @example Custom port and env
104
+ * ```typescript
105
+ * const dev = yield* DevServer("Api", {
106
+ * command: "npm run dev",
107
+ * env: {
108
+ * PORT: "4000",
109
+ * DATABASE_URL: Redacted.make("postgres://..."),
110
+ * },
111
+ * });
112
+ * ```
113
+ */
114
+ export const DevServer = Resource<DevServer>("Build.DevServer");
115
+
116
+ /**
117
+ * How long reconcile waits for a URL to appear in the child's output
118
+ * before giving up and returning `{ url: undefined }`. The child keeps
119
+ * running either way — this only bounds how long the deploy plan waits.
120
+ *
121
+ * Kept small because dev servers (vite, next, zola, …) print their URL
122
+ * within ~1s, and a long wait here just stalls `alchemy dev` startup
123
+ * for commands that never produce a URL.
124
+ */
125
+ const URL_EXTRACT_TIMEOUT = "5 seconds";
126
+
127
+ // Matches the first plain http(s) URL. Stops at whitespace and at a small
128
+ // set of punctuation typically used to wrap URLs in log output.
129
+ const URL_REGEX = /https?:\/\/[^\s)\],"'`]+/;
130
+
131
+ // ECMA-262 ANSI/VT100 escape sequences — `Vite`, `Next`, etc. surround the
132
+ // URL with color codes that would otherwise be eaten by the URL regex.
133
+ // eslint-disable-next-line no-control-regex
134
+ const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
135
+
136
+ /**
137
+ * Live-mode no-op. `DevServer` resources should only be created in dev mode;
138
+ * if one slips into a deploy, this is a noisy no-op rather than a crash.
139
+ */
140
+ export const LiveDevServerProvider = () =>
141
+ Provider.effect(
142
+ DevServer,
143
+ Effect.succeed(
144
+ DevServer.Provider.of({
145
+ diff: () => Effect.succeed({ action: "noop" }),
146
+ reconcile: () => Effect.succeed({}),
147
+ delete: () => Effect.void,
148
+ }),
149
+ ),
150
+ );
151
+
152
+ /**
153
+ * Dev-mode provider. Runs inside the RPC sidecar so the child process
154
+ * outlives user-code restarts. Tracks instances in a module-level closure
155
+ * keyed by resource id; on `reconcile`, it diffs the props hash and either
156
+ * reuses the existing process or interrupts + respawns.
157
+ */
158
+ export const LocalDevServerProvider = () =>
159
+ RpcProvider.effect(
160
+ DevServer,
161
+ import.meta.resolve(
162
+ // See LocalWorkerProvider — must match the on-disk extension of the
163
+ // sidecar entry file.
164
+ import.meta.url.endsWith(".ts") ? "./Local.ts" : "./Local.js",
165
+ import.meta.url,
166
+ ),
167
+ Effect.gen(function* () {
168
+ const spawner = yield* ChildProcessSpawner;
169
+ // The provider's outer scope lives for the lifetime of the sidecar,
170
+ // which is exactly what we want: child processes get forked into
171
+ // sub-scopes off of this one, so they survive any single user-code
172
+ // restart but die when the sidecar shuts down.
173
+ const rootScope = yield* Effect.scope;
174
+
175
+ const instances = new Map<
176
+ string,
177
+ {
178
+ hash: number;
179
+ handle: ChildProcessHandle;
180
+ scope: Scope.Closeable;
181
+ /**
182
+ * Resolves with the first URL found in the child's output. Stored
183
+ * on the instance so that no-op reconciles (same hash, process
184
+ * still running) can await it too — even if the URL hadn't yet
185
+ * appeared at the previous reconcile.
186
+ */
187
+ urlDeferred: Deferred.Deferred<string>;
188
+ }
189
+ >();
190
+
191
+ const spawn = Effect.fn(function* (
192
+ props: DevServerProps,
193
+ urlDeferred: Deferred.Deferred<string>,
194
+ ) {
195
+ const [command, ...args] = props.command.split(" ");
196
+
197
+ const handle = yield* spawner.spawn(
198
+ ChildProcess.make(command, args, {
199
+ cwd: props.cwd ?? process.cwd(),
200
+ env: {
201
+ ...process.env,
202
+ ...Object.fromEntries(
203
+ Object.entries(props.env ?? {}).map(([k, v]) => [
204
+ k,
205
+ Redacted.isRedacted(v) ? Redacted.value(v) : v,
206
+ ]),
207
+ ),
208
+ },
209
+ stdin: "inherit",
210
+ // Leave stdout/stderr piped so we can both mirror to the parent
211
+ // terminal AND parse lines for the first URL.
212
+ }),
213
+ );
214
+
215
+ // Mirror + extract on both streams, forked into the current scope so
216
+ // they're interrupted when the instance scope closes.
217
+ yield* mirrorAndExtract(handle.stdout, "stdout", urlDeferred).pipe(
218
+ Effect.forkScoped,
219
+ );
220
+ yield* mirrorAndExtract(handle.stderr, "stderr", urlDeferred).pipe(
221
+ Effect.forkScoped,
222
+ );
223
+
224
+ return handle;
225
+ });
226
+
227
+ const stop = Effect.fn(function* (id: string) {
228
+ const existing = instances.get(id);
229
+ if (existing) {
230
+ yield* existing.handle.kill();
231
+ yield* Scope.close(existing.scope, Exit.void);
232
+ instances.delete(id);
233
+ }
234
+ });
235
+
236
+ const awaitUrl = (deferred: Deferred.Deferred<string>) =>
237
+ Deferred.await(deferred).pipe(
238
+ Effect.timeoutOption(URL_EXTRACT_TIMEOUT),
239
+ Effect.map(Option.getOrUndefined),
240
+ );
241
+
242
+ return DevServer.Provider.of({
243
+ diff: Effect.fn(function* ({ id, news }) {
244
+ if (!isResolved(news)) return undefined;
245
+ const hash = Hash.structure(news);
246
+ if (instances.get(id)?.hash === hash) {
247
+ return { action: "noop" };
248
+ }
249
+ return { action: "update" };
250
+ }),
251
+ reconcile: Effect.fn(function* ({ id, news }) {
252
+ const hash = Hash.structure(news);
253
+ const existing = instances.get(id);
254
+
255
+ if (existing) {
256
+ if (existing.hash === hash && (yield* existing.handle.isRunning)) {
257
+ const url = yield* awaitUrl(existing.urlDeferred);
258
+ return { url };
259
+ }
260
+
261
+ yield* stop(id);
262
+ }
263
+
264
+ const scope = yield* Scope.fork(rootScope);
265
+ const urlDeferred = yield* Deferred.make<string>();
266
+ const handle = yield* spawn(news, urlDeferred).pipe(
267
+ Scope.provide(scope),
268
+ );
269
+ instances.set(id, { hash, handle, scope, urlDeferred });
270
+ const url = yield* awaitUrl(urlDeferred);
271
+ return { url };
272
+ }),
273
+ delete: Effect.fn(function* ({ id }) {
274
+ yield* stop(id);
275
+ }),
276
+ });
277
+ }),
278
+ );
279
+
280
+ /**
281
+ * Drains a child stdout/stderr stream into the matching parent stream while
282
+ * scanning each completed line for the first URL. Resolves `urlDeferred`
283
+ * the first time a URL is found (subsequent calls are no-ops).
284
+ *
285
+ * Modeled on `idempotent-spawn.ts`'s `extract` hook — single-pass over each
286
+ * line, ANSI escapes stripped before matching, with a buffer for incomplete
287
+ * trailing lines across chunk boundaries.
288
+ */
289
+ const mirrorAndExtract = (
290
+ source: Stream.Stream<Uint8Array, any>,
291
+ sink: "stdout" | "stderr",
292
+ urlDeferred: Deferred.Deferred<string>,
293
+ ) =>
294
+ Effect.gen(function* () {
295
+ let lineBuffer = "";
296
+ const decoder = new TextDecoder("utf-8");
297
+ yield* Stream.runForEach(source, (chunk) =>
298
+ Effect.sync(() => {
299
+ // Pass the raw bytes through so terminal colors, cursor moves,
300
+ // progress bars etc. keep working.
301
+ process[sink].write(chunk);
302
+
303
+ lineBuffer += decoder.decode(chunk, { stream: true });
304
+ let newlineIdx = lineBuffer.indexOf("\n");
305
+ while (newlineIdx !== -1) {
306
+ const line = lineBuffer.slice(0, newlineIdx);
307
+ lineBuffer = lineBuffer.slice(newlineIdx + 1);
308
+ newlineIdx = lineBuffer.indexOf("\n");
309
+ const match = line.replace(ANSI_REGEX, "").match(URL_REGEX);
310
+ if (match) {
311
+ // Deferred.doneUnsafe is a synchronous fire-and-forget — first
312
+ // caller wins, subsequent calls are dropped, which is exactly
313
+ // what we want for "record the FIRST URL".
314
+ Deferred.doneUnsafe(urlDeferred, Effect.succeed(match[0]));
315
+ return;
316
+ }
317
+ }
318
+ }),
319
+ );
320
+ });
321
+
322
+ /**
323
+ * Selects the live or dev DevServer provider based on `AlchemyContext.dev`.
324
+ */
325
+ export const DevServerProvider = () =>
326
+ Layer.unwrap(
327
+ AlchemyContext.useSync((context) =>
328
+ context.dev ? LocalDevServerProvider() : LiveDevServerProvider(),
329
+ ),
330
+ );
@@ -0,0 +1,4 @@
1
+ import { LocalDevServerProvider } from "../Build/DevServer.ts";
2
+ import * as RpcServer from "../Local/RpcServer.ts";
3
+
4
+ LocalDevServerProvider().pipe(RpcServer.launch);
@@ -63,7 +63,7 @@ export class BundleError extends Schema.TaggedErrorClass<BundleError>()(
63
63
  "BundleError",
64
64
  {
65
65
  message: Schema.String,
66
- cause: Schema.optional(Schema.DefectWithStack),
66
+ cause: Schema.optional(Schema.Defect({ includeStack: true })),
67
67
  },
68
68
  ) {}
69
69
 
@@ -12,7 +12,7 @@ export class AccessError extends Schema.TaggedErrorClass<AccessError>()(
12
12
  "AccessError",
13
13
  {
14
14
  message: Schema.String,
15
- cause: Schema.optional(Schema.Defect),
15
+ cause: Schema.optional(Schema.Defect()),
16
16
  },
17
17
  ) {}
18
18
 
@@ -277,7 +277,7 @@ export const isAiGateway = (value: unknown): value is AiGateway =>
277
277
  *
278
278
  * export default class Api extends Cloudflare.Worker<Api>()(
279
279
  * "Api",
280
- * { main: import.meta.path },
280
+ * { main: import.meta.filename },
281
281
  * Effect.gen(function* () {
282
282
  * const aiGateway = yield* Cloudflare.AiGateway.bind(Gateway);
283
283
  *
@@ -76,7 +76,7 @@ export interface AiGatewayClient {
76
76
  ): Effect.Effect<Response, AiGatewayError, RuntimeContext>;
77
77
 
78
78
  model(
79
- options: LanguageModelOptions,
79
+ options: Omit<LanguageModelOptions, "client">,
80
80
  ): Layer.Layer<LanguageModel, never, RuntimeContext>;
81
81
  }
82
82
 
@@ -166,7 +166,7 @@ export const AiGatewayBindingLive = Layer.effect(
166
166
  Effect.flatMap((gateway) => tryPromise(() => fn(gateway))),
167
167
  );
168
168
 
169
- return {
169
+ const self: AiGatewayClient = {
170
170
  raw: ai,
171
171
  gateway: runtimeGateway,
172
172
  id: gatewayIdAccessor,
@@ -175,8 +175,13 @@ export const AiGatewayBindingLive = Layer.effect(
175
175
  getLog: (logId) => use((gateway) => gateway.getLog(logId)),
176
176
  getUrl: (provider) => use((gateway) => gateway.getUrl(provider)),
177
177
  run: (data, options) => use((gateway) => gateway.run(data, options)),
178
- model: (options) => makeLanguageModelLayer(options),
178
+ model: (options) =>
179
+ makeLanguageModelLayer({
180
+ ...options,
181
+ client: self,
182
+ }),
179
183
  } satisfies AiGatewayClient;
184
+ return self;
180
185
  });
181
186
  }),
182
187
  );
@@ -32,9 +32,10 @@ type BindEffect = Effect.Effect<BrowserClient, never, BrowserBinding>;
32
32
  * Worker and returns the runtime {@link BrowserClient} — no separate
33
33
  * `.bind(...)` step required.
34
34
  *
35
- * The divergence is achieved via `[Symbol.iterator]`: the object is not an
36
- * `Effect` (so `InferEnv` resolves it to the native `Fetcher` in the `env`
37
- * position), but it is iterable as one when `yield*`-ed.
35
+ * The divergence is achieved via `[Symbol.iterator]`: the object is
36
+ * deliberately not an `Effect` (so `InferEnv` and the Worker `env` resolver
37
+ * keep it as the native `BrowserRun` rather than `yield*`-ing it), but it is
38
+ * iterable as one when `yield*`-ed.
38
39
  */
39
40
  export interface Browser {
40
41
  kind: BrowserTypeId;
@@ -53,7 +54,73 @@ export const isBrowser = (value: unknown): value is Browser =>
53
54
  * @binding
54
55
  *
55
56
  * @section Effect-style Worker (recommended)
56
- * @example Render a page title with managed browser cleanup
57
+ * @example Bind the runtime client and convert a page to Markdown
58
+ * Yielding the marker attaches the binding to the surrounding Worker and
59
+ * returns the runtime {@link BrowserClient}. Every `cf.BrowserRun` method is
60
+ * mirrored as an Effect, so no `Effect.tryPromise` wrapping is needed.
61
+ * ```typescript
62
+ * import * as Effect from "effect/Effect";
63
+ *
64
+ * Cloudflare.Worker(
65
+ * "BrowserWorker",
66
+ * { main: import.meta.filename },
67
+ * Effect.gen(function* () {
68
+ * const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
69
+ *
70
+ * return {
71
+ * fetch: Effect.gen(function* () {
72
+ * return yield* browser.markdown({ url: "https://example.com" });
73
+ * }),
74
+ * };
75
+ * }).pipe(Effect.provide(Cloudflare.BrowserBindingLive)),
76
+ * );
77
+ * ```
78
+ *
79
+ * @section Quick Actions
80
+ * @example Render content, screenshot, PDF, and structured data
81
+ * JSON quick actions resolve to their parsed payload; binary actions
82
+ * (`screenshot`, `pdf`) resolve to a `Stream` of bytes. No `Promise` or
83
+ * `Response.json()` in sight.
84
+ * ```typescript
85
+ * import * as Effect from "effect/Effect";
86
+ * import * as Stream from "effect/Stream";
87
+ *
88
+ * const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
89
+ * const url = "https://example.com";
90
+ *
91
+ * // HTML content — parsed payload, title lives in `meta`.
92
+ * const content = yield* browser.content({ url });
93
+ * const title = content.meta.title;
94
+ *
95
+ * // Scrape elements by CSS selector.
96
+ * yield* browser.scrape({ url, elements: [{ selector: "h1" }] });
97
+ *
98
+ * // Extract all links.
99
+ * const { result: links } = yield* browser.links({ url });
100
+ *
101
+ * // Binary actions stream bytes — collect or pipe them.
102
+ * const png = yield* browser.screenshot({ url }).pipe(Stream.runCollect);
103
+ * const pdf = yield* browser.pdf({ url }).pipe(Stream.runCollect);
104
+ *
105
+ * // AI-extracted structured data.
106
+ * yield* browser.json({ url, prompt: "Extract the page heading" });
107
+ * ```
108
+ *
109
+ * @example Call the generic `quickAction` directly
110
+ * The named methods are thin wrappers over `quickAction`, which mirrors
111
+ * `cf.BrowserRun["quickAction"]` one-to-one.
112
+ * ```typescript
113
+ * const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
114
+ *
115
+ * const res = yield* browser.quickAction("snapshot", {
116
+ * url: "https://example.com",
117
+ * });
118
+ * ```
119
+ *
120
+ * @section Driving Puppeteer
121
+ * @example Use the raw `cf.BrowserRun` binding with `@cloudflare/puppeteer`
122
+ * `raw` hands you the underlying runtime binding. Puppeteer is promise-based,
123
+ * so this is the one place you reach for `Effect.tryPromise`.
57
124
  * ```typescript
58
125
  * import puppeteer from "@cloudflare/puppeteer";
59
126
  * import * as Effect from "effect/Effect";
@@ -62,25 +129,30 @@ export const isBrowser = (value: unknown): value is Browser =>
62
129
  * "BrowserWorker",
63
130
  * { main: import.meta.filename },
64
131
  * Effect.gen(function* () {
65
- * // Attaches the binding to this Worker AND returns the runtime client.
66
132
  * const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
67
133
  *
68
134
  * return {
69
- * fetch: browser.withBrowser(puppeteer, (browser) =>
70
- * Effect.gen(function* () {
71
- * const page = yield* Effect.tryPromise(() => browser.newPage());
135
+ * fetch: Effect.gen(function* () {
136
+ * const binding = yield* browser.raw;
137
+ * const session = yield* Effect.tryPromise(() =>
138
+ * puppeteer.launch(binding),
139
+ * );
140
+ * try {
141
+ * const page = yield* Effect.tryPromise(() => session.newPage());
72
142
  * yield* Effect.tryPromise(() => page.goto("https://example.com"));
73
143
  * const title = yield* Effect.tryPromise(() => page.title());
74
144
  * return Response.json({ title });
75
- * }),
76
- * ),
145
+ * } finally {
146
+ * yield* Effect.promise(() => session.close());
147
+ * }
148
+ * }),
77
149
  * };
78
150
  * }).pipe(Effect.provide(Cloudflare.BrowserBindingLive)),
79
151
  * );
80
152
  * ```
81
153
  *
82
154
  * @section Worker binding metadata
83
- * @example
155
+ * @example Declare the binding on `env`
84
156
  * ```typescript
85
157
  * export const Worker = Cloudflare.Worker("Worker", {
86
158
  * main: "./src/worker.ts",
@@ -90,7 +162,7 @@ export const isBrowser = (value: unknown): value is Browser =>
90
162
  * });
91
163
  *
92
164
  * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
93
- * // { BROWSER: Fetcher }
165
+ * // { BROWSER: BrowserRun }
94
166
  * ```
95
167
  *
96
168
  * @example Async-style worker with the raw runtime binding