alchemy 0.37.2 → 0.38.1

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 (262) hide show
  1. package/bin/alchemy.mjs +44306 -48055
  2. package/bin/alchemy.ts +80 -85
  3. package/bin/commands/create.ts +263 -0
  4. package/bin/constants.ts +13 -0
  5. package/bin/errors.ts +11 -0
  6. package/bin/services/dependencies.ts +47 -0
  7. package/bin/services/get-package-version.ts +11 -0
  8. package/bin/services/package-manager.ts +113 -0
  9. package/bin/services/template-manager.ts +137 -0
  10. package/bin/types.ts +83 -0
  11. package/lib/alchemy.d.ts.map +1 -1
  12. package/lib/alchemy.js +12 -3
  13. package/lib/alchemy.js.map +1 -1
  14. package/lib/apply.d.ts +2 -0
  15. package/lib/apply.d.ts.map +1 -1
  16. package/lib/apply.js +47 -12
  17. package/lib/apply.js.map +1 -1
  18. package/lib/build-date.d.ts +6 -0
  19. package/lib/build-date.d.ts.map +1 -0
  20. package/lib/build-date.js +8 -0
  21. package/lib/build-date.js.map +1 -0
  22. package/lib/cloudflare/api.d.ts.map +1 -1
  23. package/lib/cloudflare/api.js +2 -1
  24. package/lib/cloudflare/api.js.map +1 -1
  25. package/lib/cloudflare/do-state-store/internal.js +5 -3
  26. package/lib/cloudflare/do-state-store/internal.js.map +1 -1
  27. package/lib/cloudflare/do-state-store/worker.d.ts.map +1 -1
  28. package/lib/cloudflare/do-state-store/worker.js +6 -1
  29. package/lib/cloudflare/do-state-store/worker.js.map +1 -1
  30. package/lib/cloudflare/pipeline.d.ts +1 -1
  31. package/lib/cloudflare/pipeline.d.ts.map +1 -1
  32. package/lib/cloudflare/pipeline.js +6 -1
  33. package/lib/cloudflare/pipeline.js.map +1 -1
  34. package/lib/cloudflare/worker-stub.d.ts +23 -2
  35. package/lib/cloudflare/worker-stub.d.ts.map +1 -1
  36. package/lib/cloudflare/worker-stub.js +16 -2
  37. package/lib/cloudflare/worker-stub.js.map +1 -1
  38. package/lib/cloudflare/worker.d.ts +5 -4
  39. package/lib/cloudflare/worker.d.ts.map +1 -1
  40. package/lib/cloudflare/worker.js +4 -7
  41. package/lib/cloudflare/worker.js.map +1 -1
  42. package/lib/context.d.ts +2 -2
  43. package/lib/context.d.ts.map +1 -1
  44. package/lib/destroy.d.ts +8 -2
  45. package/lib/destroy.d.ts.map +1 -1
  46. package/lib/destroy.js +19 -5
  47. package/lib/destroy.js.map +1 -1
  48. package/lib/scope.d.ts +26 -4
  49. package/lib/scope.d.ts.map +1 -1
  50. package/lib/scope.js +104 -9
  51. package/lib/scope.js.map +1 -1
  52. package/lib/sentry/project.js +1 -1
  53. package/lib/sentry/project.js.map +1 -1
  54. package/lib/util/logger.js +1 -1
  55. package/lib/util/logger.js.map +1 -1
  56. package/lib/util/mutex.d.ts +9 -0
  57. package/lib/util/mutex.d.ts.map +1 -0
  58. package/lib/util/mutex.js +35 -0
  59. package/lib/util/mutex.js.map +1 -0
  60. package/package.json +16 -6
  61. package/src/alchemy.ts +17 -5
  62. package/src/apply.ts +67 -17
  63. package/src/build-date.ts +8 -0
  64. package/src/cloudflare/api.ts +2 -1
  65. package/src/cloudflare/do-state-store/internal.ts +5 -3
  66. package/src/cloudflare/do-state-store/worker.ts +13 -3
  67. package/src/cloudflare/pipeline.ts +6 -0
  68. package/src/cloudflare/worker-stub.ts +31 -2
  69. package/src/cloudflare/worker.ts +6 -15
  70. package/src/context.ts +2 -2
  71. package/src/destroy.ts +30 -7
  72. package/src/scope.ts +145 -10
  73. package/src/sentry/project.ts +1 -1
  74. package/src/util/logger.ts +1 -1
  75. package/src/util/mutex.ts +33 -0
  76. package/templates/astro/.vscode/extensions.json +4 -0
  77. package/templates/astro/.vscode/launch.json +11 -0
  78. package/templates/astro/README.md +48 -0
  79. package/templates/astro/_env.example +1 -0
  80. package/templates/astro/_gitignore +27 -0
  81. package/templates/astro/alchemy.run.ts +16 -0
  82. package/templates/astro/astro.config.mjs +8 -0
  83. package/templates/astro/package.json +23 -0
  84. package/templates/astro/public/favicon.svg +9 -0
  85. package/templates/astro/src/assets/astro.svg +1 -0
  86. package/templates/astro/src/assets/background.svg +1 -0
  87. package/templates/astro/src/components/Welcome.astro +210 -0
  88. package/templates/astro/src/layouts/Layout.astro +22 -0
  89. package/templates/astro/src/pages/api/hello.ts +19 -0
  90. package/templates/astro/src/pages/index.astro +11 -0
  91. package/templates/astro/tsconfig.json +18 -0
  92. package/templates/astro/types/env.d.ts +16 -0
  93. package/templates/astro/wrangler.jsonc +6 -0
  94. package/templates/nuxt/README.md +75 -0
  95. package/templates/nuxt/_env +1 -0
  96. package/templates/nuxt/_env.example +1 -0
  97. package/templates/nuxt/_gitignore +27 -0
  98. package/templates/nuxt/alchemy.run.ts +16 -0
  99. package/templates/nuxt/app.vue +6 -0
  100. package/templates/nuxt/nuxt.config.ts +13 -0
  101. package/templates/nuxt/package.json +26 -0
  102. package/templates/nuxt/public/favicon.ico +0 -0
  103. package/templates/nuxt/public/robots.txt +2 -0
  104. package/templates/nuxt/server/api/hello.ts +7 -0
  105. package/templates/nuxt/server/middleware/auth.ts +5 -0
  106. package/templates/nuxt/server/middleware/hello.ts +5 -0
  107. package/templates/nuxt/server/tsconfig.json +3 -0
  108. package/templates/nuxt/tsconfig.json +15 -0
  109. package/templates/nuxt/types/env.d.ts +16 -0
  110. package/templates/react-router/.vscode/settings.json +5 -0
  111. package/templates/react-router/README.md +79 -0
  112. package/templates/react-router/_env +1 -0
  113. package/templates/react-router/_env.example +1 -0
  114. package/templates/react-router/_gitignore +15 -0
  115. package/templates/react-router/alchemy.run.ts +17 -0
  116. package/templates/react-router/app/app.css +15 -0
  117. package/templates/react-router/app/entry.server.tsx +43 -0
  118. package/templates/react-router/app/root.tsx +75 -0
  119. package/templates/react-router/app/routes/home.tsx +17 -0
  120. package/templates/react-router/app/routes.ts +3 -0
  121. package/templates/react-router/app/welcome/logo-dark.svg +23 -0
  122. package/templates/react-router/app/welcome/logo-light.svg +23 -0
  123. package/templates/react-router/app/welcome/welcome.tsx +90 -0
  124. package/templates/react-router/package.json +34 -0
  125. package/templates/react-router/public/favicon.ico +0 -0
  126. package/templates/react-router/react-router.config.ts +8 -0
  127. package/templates/react-router/tsconfig.cloudflare.json +27 -0
  128. package/templates/react-router/tsconfig.json +17 -0
  129. package/templates/react-router/tsconfig.node.json +20 -0
  130. package/templates/react-router/types/env.d.ts +16 -0
  131. package/templates/react-router/vite.config.ts +14 -0
  132. package/templates/react-router/workers/app.ts +23 -0
  133. package/templates/react-router/wrangler.jsonc +6 -0
  134. package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-interruptors.mdc +247 -0
  135. package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-middleware.mdc +75 -0
  136. package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-react.mdc +110 -0
  137. package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-request-response.mdc +213 -0
  138. package/templates/rwsdk/.devcontainer/Dockerfile +8 -0
  139. package/templates/rwsdk/.devcontainer/devcontainer.json +21 -0
  140. package/templates/rwsdk/README.md +62 -0
  141. package/templates/rwsdk/_env +5 -0
  142. package/templates/rwsdk/_env.example +5 -0
  143. package/templates/rwsdk/_gitignore +71 -0
  144. package/templates/rwsdk/alchemy.run.ts +29 -0
  145. package/templates/rwsdk/package.json +61 -0
  146. package/templates/rwsdk/prisma/schema.prisma +43 -0
  147. package/templates/rwsdk/src/app/Document.tsx +16 -0
  148. package/templates/rwsdk/src/app/headers.ts +32 -0
  149. package/templates/rwsdk/src/app/pages/Home.tsx +13 -0
  150. package/templates/rwsdk/src/app/pages/user/Login.tsx +79 -0
  151. package/templates/rwsdk/src/app/pages/user/functions.ts +171 -0
  152. package/templates/rwsdk/src/app/pages/user/routes.ts +17 -0
  153. package/templates/rwsdk/src/app/shared/links.ts +3 -0
  154. package/templates/rwsdk/src/client.tsx +3 -0
  155. package/templates/rwsdk/src/db.ts +26 -0
  156. package/templates/rwsdk/src/scripts/seed.ts +21 -0
  157. package/templates/rwsdk/src/session/durableObject.ts +63 -0
  158. package/templates/rwsdk/src/session/store.ts +13 -0
  159. package/templates/rwsdk/src/worker.tsx +63 -0
  160. package/templates/rwsdk/tsconfig.json +47 -0
  161. package/templates/rwsdk/types/env.d.ts +16 -0
  162. package/templates/rwsdk/types/rw.d.ts +5 -0
  163. package/templates/rwsdk/types/vite.d.ts +4 -0
  164. package/templates/rwsdk/vite.config.mts +6 -0
  165. package/templates/rwsdk/wrangler.jsonc +28 -0
  166. package/templates/sveltekit/README.md +38 -0
  167. package/templates/sveltekit/_env +1 -0
  168. package/templates/sveltekit/_env.example +1 -0
  169. package/templates/sveltekit/_gitignore +26 -0
  170. package/templates/sveltekit/_npmrc +1 -0
  171. package/templates/sveltekit/_prettierrc +3 -0
  172. package/templates/sveltekit/alchemy.run.ts +16 -0
  173. package/templates/sveltekit/bun.lock +1099 -0
  174. package/templates/sveltekit/package.json +33 -0
  175. package/templates/sveltekit/src/app.css +1 -0
  176. package/templates/sveltekit/src/app.d.ts +13 -0
  177. package/templates/sveltekit/src/app.html +12 -0
  178. package/templates/sveltekit/src/routes/+layout.svelte +7 -0
  179. package/templates/sveltekit/src/routes/+page.svelte +2 -0
  180. package/templates/sveltekit/static/favicon.png +0 -0
  181. package/templates/sveltekit/svelte.config.js +12 -0
  182. package/templates/sveltekit/tsconfig.json +27 -0
  183. package/templates/sveltekit/types/env.d.ts +16 -0
  184. package/templates/sveltekit/vite.config.ts +6 -0
  185. package/templates/sveltekit/wrangler.jsonc +7 -0
  186. package/templates/tanstack-start/.vscode/settings.json +11 -0
  187. package/templates/tanstack-start/README.md +72 -0
  188. package/templates/tanstack-start/_env +1 -0
  189. package/templates/tanstack-start/_env.example +1 -0
  190. package/templates/tanstack-start/_gitignore +22 -0
  191. package/templates/tanstack-start/_prettierignore +4 -0
  192. package/templates/tanstack-start/alchemy.run.ts +16 -0
  193. package/templates/tanstack-start/package.json +37 -0
  194. package/templates/tanstack-start/public/android-chrome-192x192.png +0 -0
  195. package/templates/tanstack-start/public/android-chrome-512x512.png +0 -0
  196. package/templates/tanstack-start/public/apple-touch-icon.png +0 -0
  197. package/templates/tanstack-start/public/favicon-16x16.png +0 -0
  198. package/templates/tanstack-start/public/favicon-32x32.png +0 -0
  199. package/templates/tanstack-start/public/favicon.ico +0 -0
  200. package/templates/tanstack-start/public/favicon.png +0 -0
  201. package/templates/tanstack-start/public/site.webmanifest +19 -0
  202. package/templates/tanstack-start/src/components/DefaultCatchBoundary.tsx +53 -0
  203. package/templates/tanstack-start/src/components/NotFound.tsx +25 -0
  204. package/templates/tanstack-start/src/components/PostError.tsx +5 -0
  205. package/templates/tanstack-start/src/components/UserError.tsx +5 -0
  206. package/templates/tanstack-start/src/router.tsx +22 -0
  207. package/templates/tanstack-start/src/routes/__root.tsx +146 -0
  208. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-a.tsx +10 -0
  209. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-b.tsx +10 -0
  210. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout.tsx +34 -0
  211. package/templates/tanstack-start/src/routes/_pathlessLayout.tsx +16 -0
  212. package/templates/tanstack-start/src/routes/api/users.$userId.ts +28 -0
  213. package/templates/tanstack-start/src/routes/api/users.ts +59 -0
  214. package/templates/tanstack-start/src/routes/customScript[.]js.ts +10 -0
  215. package/templates/tanstack-start/src/routes/deferred.tsx +62 -0
  216. package/templates/tanstack-start/src/routes/index.tsx +12 -0
  217. package/templates/tanstack-start/src/routes/posts.$postId.tsx +34 -0
  218. package/templates/tanstack-start/src/routes/posts.index.tsx +8 -0
  219. package/templates/tanstack-start/src/routes/posts.tsx +38 -0
  220. package/templates/tanstack-start/src/routes/posts_.$postId.deep.tsx +29 -0
  221. package/templates/tanstack-start/src/routes/redirect.tsx +9 -0
  222. package/templates/tanstack-start/src/routes/users.$userId.tsx +44 -0
  223. package/templates/tanstack-start/src/routes/users.index.tsx +18 -0
  224. package/templates/tanstack-start/src/routes/users.tsx +49 -0
  225. package/templates/tanstack-start/src/styles/app.css +12 -0
  226. package/templates/tanstack-start/src/utils/loggingMiddleware.tsx +41 -0
  227. package/templates/tanstack-start/src/utils/posts.tsx +40 -0
  228. package/templates/tanstack-start/src/utils/seo.ts +33 -0
  229. package/templates/tanstack-start/src/utils/users.tsx +5 -0
  230. package/templates/tanstack-start/tsconfig.json +31 -0
  231. package/templates/tanstack-start/types/env.d.ts +16 -0
  232. package/templates/tanstack-start/vite.config.ts +33 -0
  233. package/templates/tanstack-start/wrangler.jsonc +7 -0
  234. package/templates/typescript/README.md +15 -0
  235. package/templates/typescript/_gitignore +37 -0
  236. package/templates/typescript/alchemy.run.ts +15 -0
  237. package/templates/typescript/index.ts +1 -0
  238. package/templates/typescript/package.json +18 -0
  239. package/templates/typescript/src/worker.js +5 -0
  240. package/templates/typescript/src/worker.ts +7 -0
  241. package/templates/typescript/tsconfig.json +21 -0
  242. package/templates/typescript/types/env.d.ts +16 -0
  243. package/templates/vite/README.md +54 -0
  244. package/templates/vite/_env.example +1 -0
  245. package/templates/vite/_gitignore +27 -0
  246. package/templates/vite/alchemy.run.ts +17 -0
  247. package/templates/vite/eslint.config.js +28 -0
  248. package/templates/vite/index.html +13 -0
  249. package/templates/vite/package.json +35 -0
  250. package/templates/vite/public/vite.svg +1 -0
  251. package/templates/vite/src/App.css +42 -0
  252. package/templates/vite/src/App.tsx +35 -0
  253. package/templates/vite/src/assets/react.svg +1 -0
  254. package/templates/vite/src/index.css +68 -0
  255. package/templates/vite/src/main.tsx +10 -0
  256. package/templates/vite/src/vite-env.d.ts +1 -0
  257. package/templates/vite/tsconfig.json +34 -0
  258. package/templates/vite/types/env.d.ts +16 -0
  259. package/templates/vite/vite.config.ts +9 -0
  260. package/templates/vite/worker/index.ts +12 -0
  261. package/templates/vite/wrangler.jsonc +6 -0
  262. package/bin/create-alchemy.ts +0 -1220
package/src/apply.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  type Resource,
13
13
  type ResourceProps,
14
14
  } from "./resource.ts";
15
- import { Scope } from "./scope.ts";
15
+ import { Scope, type PendingDeletions } from "./scope.ts";
16
16
  import { serialize } from "./serde.ts";
17
17
  import type { State } from "./state.ts";
18
18
  import { formatFQN } from "./util/cli.ts";
@@ -33,6 +33,8 @@ export function apply<Out extends Resource>(
33
33
  return _apply(resource, props, options);
34
34
  }
35
35
 
36
+ export class ReplacedSignal extends Error {}
37
+
36
38
  async function _apply<Out extends Resource>(
37
39
  resource: PendingResource<Out>,
38
40
  props: ResourceProps | undefined,
@@ -88,6 +90,7 @@ async function _apply<Out extends Resource>(
88
90
  };
89
91
  await scope.state.set(resource[ResourceID], state);
90
92
  }
93
+ const oldOutput = state.output;
91
94
 
92
95
  const alwaysUpdate =
93
96
  options?.alwaysUpdate ?? provider.options?.alwaysUpdate ?? false;
@@ -162,33 +165,83 @@ async function _apply<Out extends Resource>(
162
165
  props: state.oldProps,
163
166
  state,
164
167
  replace: () => {
168
+ if (phase === "create") {
169
+ throw new Error(
170
+ `Resource ${resource[ResourceKind]} ${resource[ResourceFQN]} cannot be replaced in create phase.`,
171
+ );
172
+ }
165
173
  if (isReplaced) {
166
174
  logger.warn(
167
175
  `Resource ${resource[ResourceKind]} ${resource[ResourceFQN]} is already marked as REPLACE`,
168
176
  );
169
- return;
170
177
  }
178
+
171
179
  isReplaced = true;
180
+ throw new ReplacedSignal();
172
181
  },
173
182
  });
174
183
 
175
- const output = await alchemy.run(
176
- resource[ResourceID],
177
- {
178
- isResource: true,
179
- parent: scope,
180
- },
181
- async (scope) => {
182
- options?.resolveInnerScope?.(scope);
183
- return provider.handler.bind(ctx)(resource[ResourceID], props);
184
- },
185
- );
184
+ let output: Resource<string>;
185
+ try {
186
+ output = await alchemy.run(
187
+ resource[ResourceID],
188
+ {
189
+ isResource: true,
190
+ parent: scope,
191
+ },
192
+ async (scope) => {
193
+ options?.resolveInnerScope?.(scope);
194
+ return provider.handler.bind(ctx)(resource[ResourceID], props);
195
+ },
196
+ );
197
+ } catch (error) {
198
+ if (error instanceof ReplacedSignal) {
199
+ if (scope.children.get(resource[ResourceID])?.children.size! > 0) {
200
+ throw new Error(
201
+ `Resource ${resource[ResourceFQN]} has children and cannot be replaced.`,
202
+ );
203
+ }
204
+
205
+ output = await alchemy.run(
206
+ resource[ResourceID],
207
+ { isResource: true, parent: scope },
208
+ async () =>
209
+ provider.handler.bind(
210
+ context({
211
+ scope,
212
+ phase: "create",
213
+ kind: resource[ResourceKind],
214
+ id: resource[ResourceID],
215
+ fqn: resource[ResourceFQN],
216
+ seq: resource[ResourceSeq],
217
+ props: state.props,
218
+ state,
219
+ replace: () => {
220
+ throw new Error(
221
+ `Resource ${resource[ResourceKind]} ${resource[ResourceFQN]} cannot be replaced in create phase.`,
222
+ );
223
+ },
224
+ }),
225
+ )(resource[ResourceID], props),
226
+ );
227
+
228
+ const pendingDeletions =
229
+ (await scope.get<PendingDeletions>("pendingDeletions")) ?? [];
230
+ pendingDeletions.push({
231
+ resource: oldOutput,
232
+ oldProps: state.oldProps,
233
+ });
234
+ await scope.set("pendingDeletions", pendingDeletions);
235
+ } else {
236
+ throw error;
237
+ }
238
+ }
186
239
  if (!quiet) {
187
240
  logger.task(resource[ResourceFQN], {
188
241
  prefix: phase === "create" ? "created" : "updated",
189
242
  prefixColor: "greenBright",
190
243
  resource: formatFQN(resource[ResourceFQN]),
191
- message: `${phase === "create" ? "Created" : "Updated"} Resource`,
244
+ message: `${phase === "create" ? "Created" : isReplaced ? "Replaced" : "Updated"} Resource`,
192
245
  status: "success",
193
246
  });
194
247
  }
@@ -213,9 +266,6 @@ async function _apply<Out extends Resource>(
213
266
  props,
214
267
  // deps: [...deps],
215
268
  });
216
- // if (output !== undefined) {
217
- // resource[Provide](output as Out);
218
- // }
219
269
  return output as any;
220
270
  } catch (error) {
221
271
  scope.telemetryClient.record({
@@ -0,0 +1,8 @@
1
+ // This file is auto-generated during build
2
+ // Do not edit manually
3
+
4
+ /**
5
+ * The build date used as the default worker compatibility date.
6
+ * This is set to the date when the package was built.
7
+ */
8
+ export const BUILD_DATE = "2025-06-23";
@@ -196,7 +196,8 @@ export class CloudflareApi {
196
196
  (error) =>
197
197
  error instanceof InternalError ||
198
198
  error instanceof TooManyRequestsError ||
199
- error instanceof ForbiddenError,
199
+ error instanceof ForbiddenError ||
200
+ error.code === "ECONNRESET",
200
201
  10, // Maximum 10 attempts (1 initial + 9 retries)
201
202
  1000, // Start with 1s delay, will exponentially increase
202
203
  );
@@ -153,7 +153,7 @@ export async function upsertStateStoreWorker(
153
153
  );
154
154
 
155
155
  // Put the worker with migration tag v1
156
- const response = await api.post(
156
+ const response = await api.put(
157
157
  `/accounts/${api.accountId}/workers/scripts/${workerName}`,
158
158
  formData,
159
159
  );
@@ -209,7 +209,7 @@ export async function getAccountSubdomain(api: CloudflareApi) {
209
209
  const res = await api.get(`/accounts/${api.accountId}/workers/subdomain`);
210
210
  if (!res.ok) {
211
211
  throw new Error(
212
- `Failed to get account subdomain: ${res.status} ${res.statusText}`,
212
+ `Failed to get account subdomain: ${res.status} ${res.statusText}: ${await res.text().catch(() => "unknown error")}`,
213
213
  );
214
214
  }
215
215
  const json: { result: { subdomain: string } } = await res.json();
@@ -237,5 +237,7 @@ async function bundleWorkerScript() {
237
237
  if (!result.outputFiles?.[0]) {
238
238
  throw new Error("Failed to bundle worker.ts");
239
239
  }
240
- return result.outputFiles[0].text;
240
+ return new File([result.outputFiles[0].text], "worker.js", {
241
+ type: "application/javascript+module",
242
+ });
241
243
  }
@@ -63,10 +63,20 @@ export default class extends WorkerEntrypoint<Env> {
63
63
  if (!token) {
64
64
  return false;
65
65
  }
66
- return timingSafeEqual(
67
- new TextEncoder().encode(token),
68
- new TextEncoder().encode(this.env.DOFS_TOKEN),
66
+
67
+ // Convert both to fixed-length buffers
68
+ const expectedBuffer = Buffer.from(this.env.DOFS_TOKEN, "utf8");
69
+ const providedBuffer = Buffer.alloc(expectedBuffer.length);
70
+
71
+ // Copy provided data, truncating or padding as needed
72
+ Buffer.from(token, "utf8").copy(
73
+ providedBuffer,
74
+ 0,
75
+ 0,
76
+ expectedBuffer.length,
69
77
  );
78
+
79
+ return timingSafeEqual(expectedBuffer, providedBuffer);
70
80
  }
71
81
 
72
82
  private getStub(request: Request): DurableObjectStub<DOFSStateStore> {
@@ -522,6 +522,7 @@ export async function createPipeline(
522
522
  api: CloudflareApi,
523
523
  pipelineName: string,
524
524
  props: PipelineProps,
525
+ attempt = 0,
525
526
  ): Promise<CloudflarePipelineResponse> {
526
527
  // Prepare the create payload
527
528
  const createPayload = preparePipelinePayload(api, pipelineName, props);
@@ -532,6 +533,11 @@ export async function createPipeline(
532
533
  );
533
534
 
534
535
  if (!createResponse.ok) {
536
+ if (createResponse.status === 404 && attempt < 3) {
537
+ // bucket does not exist, this might be transient, let's retry
538
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (1 + attempt)));
539
+ return await createPipeline(api, pipelineName, props, attempt + 1);
540
+ }
535
541
  return await handleApiError(
536
542
  createResponse,
537
543
  "creating",
@@ -7,6 +7,7 @@ import {
7
7
  type CloudflareApi,
8
8
  type CloudflareApiOptions,
9
9
  } from "./api.ts";
10
+ import { configureURL } from "./worker.ts";
10
11
 
11
12
  /**
12
13
  * Properties for creating a Worker stub
@@ -19,6 +20,14 @@ export interface WorkerStubProps<
19
20
  */
20
21
  name: string;
21
22
 
23
+ /**
24
+ * Whether to enable a workers.dev URL for this worker
25
+ *
26
+ * If true, the worker will be available at {name}.{subdomain}.workers.dev
27
+ * @default true
28
+ */
29
+ url?: boolean;
30
+
22
31
  /**
23
32
  * The RPC class to use for the worker.
24
33
  *
@@ -39,6 +48,12 @@ export interface WorkerStub<
39
48
  */
40
49
  name: string;
41
50
 
51
+ /**
52
+ * The worker's URL if enabled
53
+ * Format: {name}.{subdomain}.workers.dev
54
+ */
55
+ url?: string;
56
+
42
57
  /**
43
58
  * Optional type branding for the worker's RPC entrypoint.
44
59
  *
@@ -59,12 +74,21 @@ export function isWorkerStub(resource: Resource): resource is WorkerStub {
59
74
  * exists and creates an empty one if needed.
60
75
  *
61
76
  * @example
62
- * // Reserve a worker name without deploying code
77
+ * // Reserve a worker name without deploying code, with URL enabled (default)
63
78
  * const workerStub = await WorkerStub("my-worker", {
64
79
  * name: "my-reserved-worker"
65
80
  * });
66
81
  *
67
- * console.log(`Worker ${workerStub.name} exists: ${!workerStub.created}`);
82
+ * console.log(`Worker ${workerStub.name} is available at: ${workerStub.url}`);
83
+ *
84
+ * @example
85
+ * // Reserve a worker name without enabling URL
86
+ * const workerStub = await WorkerStub("my-worker", {
87
+ * name: "my-reserved-worker",
88
+ * url: false
89
+ * });
90
+ *
91
+ * console.log(`Worker ${workerStub.name} created without URL`);
68
92
  */
69
93
  export const WorkerStub = Resource("cloudflare::WorkerStub", async function <
70
94
  RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
@@ -84,11 +108,16 @@ export const WorkerStub = Resource("cloudflare::WorkerStub", async function <
84
108
  await createEmptyWorker(api, props.name);
85
109
  }
86
110
 
111
+ // Configure URL if requested (defaults to true)
112
+ const enableUrl = props.url ?? true;
113
+ const workerUrl = await configureURL(this, api, props.name, enableUrl);
114
+
87
115
  // Return the worker stub info
88
116
  return this({
89
117
  type: "service",
90
118
  __rpc__: props.rpc as unknown as RPC,
91
119
  ...props,
120
+ url: workerUrl,
92
121
  }) as WorkerStub<RPC>;
93
122
  });
94
123
 
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { BUILD_DATE } from "../build-date.ts";
2
3
  import type { Context } from "../context.ts";
3
4
  import type { BundleProps } from "../esbuild/bundle.ts";
4
5
  import { InnerResourceScope, Resource, ResourceKind } from "../resource.ts";
@@ -12,7 +13,6 @@ import type { type } from "../type.ts";
12
13
  import { getContentType } from "../util/content-type.ts";
13
14
  import { logger } from "../util/logger.ts";
14
15
  import { withExponentialBackoff } from "../util/retry.ts";
15
- import { slugify } from "../util/slugify.ts";
16
16
  import { CloudflareApiError, handleApiError } from "./api-error.ts";
17
17
  import {
18
18
  type CloudflareApi,
@@ -178,7 +178,7 @@ export interface BaseWorkerProps<
178
178
 
179
179
  /**
180
180
  * The compatibility date for the worker
181
- * @default "2025-04-26"
181
+ * @default BUILD_DATE - automatically pinned to the package build date
182
182
  */
183
183
  compatibilityDate?: string;
184
184
 
@@ -861,7 +861,7 @@ export function Worker<const B extends Bindings>(
861
861
  });
862
862
  }
863
863
 
864
- export const DEFAULT_COMPATIBILITY_DATE = "2025-04-20";
864
+ export const DEFAULT_COMPATIBILITY_DATE = BUILD_DATE;
865
865
 
866
866
  export const _Worker = Resource(
867
867
  "cloudflare::Worker",
@@ -1063,7 +1063,7 @@ export const _Worker = Resource(
1063
1063
  }
1064
1064
  // We always "adopt" when publishing versions
1065
1065
  } else if (!props.adopt) {
1066
- await assertWorkerDoesNotExist(this, api, workerName);
1066
+ await assertWorkerDoesNotExist(api, workerName);
1067
1067
  }
1068
1068
  }
1069
1069
 
@@ -1429,8 +1429,7 @@ export async function checkWorkerExists(
1429
1429
  return response.status === 200;
1430
1430
  }
1431
1431
 
1432
- export async function assertWorkerDoesNotExist<B extends Bindings>(
1433
- ctx: Context<Worker<B>>,
1432
+ export async function assertWorkerDoesNotExist(
1434
1433
  api: CloudflareApi,
1435
1434
  workerName: string,
1436
1435
  ) {
@@ -1449,14 +1448,6 @@ export async function assertWorkerDoesNotExist<B extends Bindings>(
1449
1448
  );
1450
1449
  }
1451
1450
 
1452
- if (
1453
- metadata.default_environment?.script.tags.includes(
1454
- `alchemy:id:${slugify(ctx.fqn)}`,
1455
- )
1456
- ) {
1457
- return true;
1458
- }
1459
-
1460
1451
  throw new Error(
1461
1452
  `Worker with name '${workerName}' already exists. Please use a unique name.`,
1462
1453
  );
@@ -1467,7 +1458,7 @@ export async function assertWorkerDoesNotExist<B extends Bindings>(
1467
1458
  }
1468
1459
 
1469
1460
  export async function configureURL<B extends Bindings>(
1470
- ctx: Context<Worker<B>>,
1461
+ ctx: Context<Worker<B>> | Context<WorkerStub>,
1471
1462
  api: CloudflareApi,
1472
1463
  workerName: string,
1473
1464
  url: boolean,
package/src/context.ts CHANGED
@@ -53,7 +53,7 @@ export interface BaseContext<Out extends Resource> {
53
53
  * Indicate that this resource is being replaced.
54
54
  * This will cause the resource to be deleted at the end of the stack's CREATE phase.
55
55
  */
56
- replace(): void;
56
+ replace(): never;
57
57
  /**
58
58
  * Terminate the resource lifecycle handler and destroy the resource.
59
59
  *
@@ -97,7 +97,7 @@ export function context<
97
97
  seq: number;
98
98
  props: Props;
99
99
  state: State<Kind, Props, Out>;
100
- replace: () => void;
100
+ replace: () => never;
101
101
  }): Context<Out> {
102
102
  type InternalSymbols =
103
103
  | typeof ResourceID
package/src/destroy.ts CHANGED
@@ -6,10 +6,11 @@ import {
6
6
  ResourceFQN,
7
7
  ResourceID,
8
8
  ResourceKind,
9
+ type ResourceProps,
9
10
  ResourceScope,
10
11
  ResourceSeq,
11
12
  } from "./resource.ts";
12
- import { isScope, Scope } from "./scope.ts";
13
+ import { isScope, type PendingDeletions, Scope } from "./scope.ts";
13
14
  import { formatFQN } from "./util/cli.ts";
14
15
  import { logger } from "./util/logger.ts";
15
16
 
@@ -18,6 +19,10 @@ export class DestroyedSignal extends Error {}
18
19
  export interface DestroyOptions {
19
20
  quiet?: boolean;
20
21
  strategy?: "sequential" | "parallel";
22
+ replace?: {
23
+ props?: ResourceProps | undefined;
24
+ output?: Resource<string>;
25
+ };
21
26
  }
22
27
 
23
28
  function isScopeArgs(a: any): a is [scope: Scope, options?: DestroyOptions] {
@@ -40,7 +45,8 @@ export async function destroy<Type extends string>(
40
45
  } satisfies DestroyOptions;
41
46
 
42
47
  await scope.run(async () => {
43
- // destroy all active resources
48
+ // destroy all active and pending resources
49
+ await scope.destroyPendingDeletions();
44
50
  await destroyAll(Array.from(scope.resources.values()), options);
45
51
 
46
52
  // then detect orphans and destroy them
@@ -109,7 +115,7 @@ export async function destroy<Type extends string>(
109
115
  id: instance[ResourceID],
110
116
  fqn: instance[ResourceFQN],
111
117
  seq: instance[ResourceSeq],
112
- props: state.props,
118
+ props: options?.replace?.props ?? state.props,
113
119
  state,
114
120
  replace: () => {
115
121
  throw new Error("Cannot replace a resource that is being deleted");
@@ -127,10 +133,10 @@ export async function destroy<Type extends string>(
127
133
  parent: scope,
128
134
  },
129
135
  async (scope) => {
130
- nestedScope = scope;
136
+ nestedScope = options?.replace?.props == null ? scope : undefined;
131
137
  return await Provider.handler.bind(ctx)(
132
138
  instance[ResourceID],
133
- state.props!,
139
+ options?.replace?.props ?? state.props!,
134
140
  );
135
141
  },
136
142
  );
@@ -146,7 +152,21 @@ export async function destroy<Type extends string>(
146
152
  await destroy(nestedScope, options);
147
153
  }
148
154
 
149
- await scope.delete(instance[ResourceID]);
155
+ if (options?.replace == null) {
156
+ if (nestedScope) {
157
+ await destroy(nestedScope, options);
158
+ }
159
+ await scope.deleteResource(instance[ResourceID]);
160
+ } else {
161
+ let pendingDeletions =
162
+ await state.output[ResourceScope].get<PendingDeletions>(
163
+ "pendingDeletions",
164
+ );
165
+ pendingDeletions = pendingDeletions?.filter(
166
+ (deletion) => deletion.resource[ResourceID] !== instance[ResourceID],
167
+ );
168
+ await scope.set("pendingDeletions", pendingDeletions);
169
+ }
150
170
 
151
171
  if (!quiet) {
152
172
  logger.task(instance[ResourceFQN], {
@@ -165,11 +185,14 @@ export async function destroy<Type extends string>(
165
185
 
166
186
  export async function destroyAll(
167
187
  resources: Resource[],
168
- options?: DestroyOptions,
188
+ options?: DestroyOptions & { force?: boolean },
169
189
  ) {
170
190
  if (options?.strategy !== "parallel") {
171
191
  const sorted = resources.sort((a, b) => b[ResourceSeq] - a[ResourceSeq]);
172
192
  for (const resource of sorted) {
193
+ if (isScope(resource)) {
194
+ await resource.destroyPendingDeletions();
195
+ }
173
196
  await destroy(resource, options);
174
197
  }
175
198
  } else {