alchemy 0.58.0 → 0.59.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 (106) hide show
  1. package/bin/alchemy.js +2 -2
  2. package/lib/cloudflare/api.d.ts.map +1 -1
  3. package/lib/cloudflare/api.js +10 -2
  4. package/lib/cloudflare/api.js.map +1 -1
  5. package/lib/cloudflare/assets.d.ts +1 -1
  6. package/lib/cloudflare/assets.d.ts.map +1 -1
  7. package/lib/cloudflare/bucket.d.ts +21 -3
  8. package/lib/cloudflare/bucket.d.ts.map +1 -1
  9. package/lib/cloudflare/bucket.js +83 -53
  10. package/lib/cloudflare/bucket.js.map +1 -1
  11. package/lib/cloudflare/container.d.ts +8 -0
  12. package/lib/cloudflare/container.d.ts.map +1 -1
  13. package/lib/cloudflare/container.js +7 -1
  14. package/lib/cloudflare/container.js.map +1 -1
  15. package/lib/cloudflare/custom-domain.d.ts +8 -0
  16. package/lib/cloudflare/custom-domain.d.ts.map +1 -1
  17. package/lib/cloudflare/custom-domain.js +11 -0
  18. package/lib/cloudflare/custom-domain.js.map +1 -1
  19. package/lib/cloudflare/d1-database.d.ts +15 -1
  20. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  21. package/lib/cloudflare/d1-database.js +24 -17
  22. package/lib/cloudflare/d1-database.js.map +1 -1
  23. package/lib/cloudflare/hyperdrive.d.ts +37 -9
  24. package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
  25. package/lib/cloudflare/hyperdrive.js +91 -5
  26. package/lib/cloudflare/hyperdrive.js.map +1 -1
  27. package/lib/cloudflare/kv-namespace.d.ts +23 -4
  28. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  29. package/lib/cloudflare/kv-namespace.js +70 -58
  30. package/lib/cloudflare/kv-namespace.js.map +1 -1
  31. package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -0
  32. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  33. package/lib/cloudflare/miniflare/build-worker-options.js +9 -7
  34. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  35. package/lib/cloudflare/miniflare/delete.d.ts +21 -0
  36. package/lib/cloudflare/miniflare/delete.d.ts.map +1 -0
  37. package/lib/cloudflare/miniflare/delete.js +91 -0
  38. package/lib/cloudflare/miniflare/delete.js.map +1 -0
  39. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  40. package/lib/cloudflare/miniflare/miniflare-controller.js +26 -9
  41. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  42. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts +2 -0
  43. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  44. package/lib/cloudflare/miniflare/remote-binding-proxy.js +3 -4
  45. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  46. package/lib/cloudflare/pipeline.d.ts +3 -0
  47. package/lib/cloudflare/pipeline.d.ts.map +1 -1
  48. package/lib/cloudflare/pipeline.js +14 -1
  49. package/lib/cloudflare/pipeline.js.map +1 -1
  50. package/lib/cloudflare/queue-consumer.d.ts +8 -0
  51. package/lib/cloudflare/queue-consumer.d.ts.map +1 -1
  52. package/lib/cloudflare/queue-consumer.js +15 -4
  53. package/lib/cloudflare/queue-consumer.js.map +1 -1
  54. package/lib/cloudflare/queue.d.ts +22 -4
  55. package/lib/cloudflare/queue.d.ts.map +1 -1
  56. package/lib/cloudflare/queue.js +32 -9
  57. package/lib/cloudflare/queue.js.map +1 -1
  58. package/lib/cloudflare/route.d.ts +8 -0
  59. package/lib/cloudflare/route.d.ts.map +1 -1
  60. package/lib/cloudflare/route.js +13 -0
  61. package/lib/cloudflare/route.js.map +1 -1
  62. package/lib/cloudflare/worker-subdomain.d.ts +8 -0
  63. package/lib/cloudflare/worker-subdomain.d.ts.map +1 -1
  64. package/lib/cloudflare/worker-subdomain.js +9 -1
  65. package/lib/cloudflare/worker-subdomain.js.map +1 -1
  66. package/lib/cloudflare/worker.d.ts +7 -1
  67. package/lib/cloudflare/worker.d.ts.map +1 -1
  68. package/lib/cloudflare/worker.js +405 -375
  69. package/lib/cloudflare/worker.js.map +1 -1
  70. package/lib/cloudflare/wrangler.json.d.ts +1 -1
  71. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  72. package/lib/cloudflare/wrangler.json.js +24 -12
  73. package/lib/cloudflare/wrangler.json.js.map +1 -1
  74. package/lib/esbuild/bundle.d.ts +1 -1
  75. package/lib/secret.d.ts +9 -1
  76. package/lib/secret.d.ts.map +1 -1
  77. package/lib/secret.js +12 -0
  78. package/lib/secret.js.map +1 -1
  79. package/package.json +3 -3
  80. package/src/cloudflare/api.ts +13 -2
  81. package/src/cloudflare/bucket.ts +126 -68
  82. package/src/cloudflare/container.ts +18 -1
  83. package/src/cloudflare/custom-domain.ts +22 -0
  84. package/src/cloudflare/d1-database.ts +44 -19
  85. package/src/cloudflare/hyperdrive.ts +170 -14
  86. package/src/cloudflare/kv-namespace.ts +117 -73
  87. package/src/cloudflare/miniflare/build-worker-options.ts +12 -8
  88. package/src/cloudflare/miniflare/delete.ts +136 -0
  89. package/src/cloudflare/miniflare/miniflare-controller.ts +26 -9
  90. package/src/cloudflare/miniflare/remote-binding-proxy.ts +5 -4
  91. package/src/cloudflare/pipeline.ts +17 -1
  92. package/src/cloudflare/queue-consumer.ts +27 -5
  93. package/src/cloudflare/queue.ts +62 -12
  94. package/src/cloudflare/route.ts +24 -0
  95. package/src/cloudflare/worker-subdomain.ts +29 -6
  96. package/src/cloudflare/worker.ts +484 -465
  97. package/src/cloudflare/wrangler.json.ts +34 -13
  98. package/src/secret.ts +15 -1
  99. package/templates/astro/README.md +2 -0
  100. package/templates/nuxt/README.md +2 -0
  101. package/templates/react-router/README.md +2 -0
  102. package/templates/rwsdk/README.md +2 -0
  103. package/templates/sveltekit/README.md +2 -0
  104. package/templates/tanstack-start/README.md +2 -0
  105. package/templates/typescript/README.md +2 -0
  106. package/templates/vite/README.md +2 -0
@@ -0,0 +1,136 @@
1
+ import crypto from "node:crypto";
2
+ import type { RmDirOptions } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "pathe";
5
+ import type { DurableObjectNamespace } from "../durable-object-namespace.ts";
6
+ import type { Workflow } from "../workflow.ts";
7
+ import { DEFAULT_PERSIST_PATH } from "./paths.ts";
8
+
9
+ const PERSIST_ROOT = path.resolve(DEFAULT_PERSIST_PATH);
10
+ const BINDING_TYPE_KEY = {
11
+ d1: "miniflare-D1DatabaseObject",
12
+ kv: "miniflare-KVNamespaceObject",
13
+ r2: "miniflare-R2BucketObject",
14
+ } as const;
15
+
16
+ type BindingType = keyof typeof BINDING_TYPE_KEY;
17
+
18
+ /**
19
+ * Delete a binding from the miniflare directory.
20
+ */
21
+ export async function deleteMiniflareBinding(type: BindingType, id: string) {
22
+ const bindingPath = path.join(PERSIST_ROOT, type, BINDING_TYPE_KEY[type]);
23
+
24
+ // delete durable object/sqlite files
25
+ const namespaceId = durableObjectNamespaceIdFromName(
26
+ BINDING_TYPE_KEY[type],
27
+ id,
28
+ );
29
+ const files = await fs
30
+ .readdir(bindingPath)
31
+ .then((files) => files.filter((file) => file.startsWith(namespaceId)))
32
+ .catch(() => []);
33
+ await Promise.all(files.map((file) => fs.rm(path.join(bindingPath, file))));
34
+
35
+ // delete data directory, if present
36
+ await removeDirectory(path.join(PERSIST_ROOT, type, id), {
37
+ recursive: true,
38
+ });
39
+
40
+ // delete the miniflare directory if it is empty
41
+ await cleanMiniflareDirectory();
42
+ }
43
+
44
+ /**
45
+ * Delete local durable object and workflow data for a given Worker script.
46
+ */
47
+ export async function deleteMiniflareWorkerData(
48
+ scriptName: string,
49
+ input: {
50
+ durableObjects: DurableObjectNamespace[];
51
+ workflows: Workflow[];
52
+ },
53
+ ) {
54
+ await Promise.all(
55
+ input.durableObjects.map((durableObject) =>
56
+ removeDirectory(
57
+ path.join(
58
+ PERSIST_ROOT,
59
+ "do",
60
+ `${scriptName}-${durableObject.className}`,
61
+ ),
62
+ {
63
+ recursive: true,
64
+ },
65
+ ),
66
+ ),
67
+ );
68
+ await Promise.all(
69
+ input.workflows.map((workflow) =>
70
+ removeDirectory(
71
+ path.join(
72
+ PERSIST_ROOT,
73
+ "workflows",
74
+ `miniflare-workflows-${workflow.workflowName}`,
75
+ ),
76
+ {
77
+ recursive: true,
78
+ },
79
+ ),
80
+ ),
81
+ );
82
+ await cleanMiniflareDirectory();
83
+ }
84
+
85
+ /**
86
+ * Delete the miniflare directory iff it is empty.
87
+ */
88
+ async function cleanMiniflareDirectory() {
89
+ const directory = path.resolve(".alchemy/miniflare");
90
+ const files = await fs
91
+ .readdir(directory, {
92
+ recursive: true,
93
+ withFileTypes: true,
94
+ })
95
+ .catch((error) => {
96
+ if (!isENOENT(error)) {
97
+ throw error;
98
+ }
99
+ return [];
100
+ });
101
+ if (!files.some((file) => !file.isDirectory())) {
102
+ await removeDirectory(directory, { recursive: true });
103
+ }
104
+ }
105
+
106
+ const removeDirectory = async (path: string, options?: RmDirOptions) => {
107
+ try {
108
+ await fs.rmdir(path, options);
109
+ } catch (error) {
110
+ if (!isENOENT(error)) {
111
+ throw error;
112
+ }
113
+ }
114
+ };
115
+
116
+ const isENOENT = (error: unknown): error is { code: "ENOENT" } =>
117
+ !!error &&
118
+ typeof error === "object" &&
119
+ "code" in error &&
120
+ error.code === "ENOENT";
121
+
122
+ // copied from https://github.com/cloudflare/workers-sdk/blob/a5892915e173ca8666c04fb394e70711e87b46d8/packages/miniflare/src/plugins/shared/index.ts#L236
123
+ const durableObjectNamespaceIdFromName = (uniqueKey: string, name: string) => {
124
+ const key = crypto.createHash("sha256").update(uniqueKey).digest();
125
+ const nameHmac = crypto
126
+ .createHmac("sha256", key)
127
+ .update(name)
128
+ .digest()
129
+ .subarray(0, 16);
130
+ const hmac = crypto
131
+ .createHmac("sha256", key)
132
+ .update(nameHmac)
133
+ .digest()
134
+ .subarray(0, 16);
135
+ return Buffer.concat([nameHmac, hmac]).toString("hex");
136
+ };
@@ -76,21 +76,38 @@ export class MiniflareController {
76
76
  private async update() {
77
77
  return await this.mutex.lock(async () => {
78
78
  const options: miniflare.MiniflareOptions = {
79
- workers: Array.from(this.options.values()),
79
+ workers: [],
80
80
  defaultPersistRoot: path.resolve(DEFAULT_PERSIST_PATH),
81
81
  unsafeDevRegistryPath: miniflare.getDefaultDevRegistryPath(),
82
- analyticsEngineDatasetsPersist: true,
83
- cachePersist: true,
84
- d1Persist: true,
85
- durableObjectsPersist: true,
86
- kvPersist: true,
87
- r2Persist: true,
88
- secretsStorePersist: true,
89
- workflowsPersist: true,
90
82
  log: process.env.DEBUG
91
83
  ? new miniflare.Log(miniflare.LogLevel.DEBUG)
92
84
  : undefined,
93
85
  };
86
+ for (const worker of this.options.values()) {
87
+ options.workers.push(worker);
88
+ // avoid creating unnecessary directories
89
+ if (worker.analyticsEngineDatasets) {
90
+ options.analyticsEngineDatasetsPersist = true;
91
+ }
92
+ if (worker.d1Databases) {
93
+ options.d1Persist = true;
94
+ }
95
+ if (worker.durableObjects) {
96
+ options.durableObjectsPersist = true;
97
+ }
98
+ if (worker.kvNamespaces) {
99
+ options.kvPersist = true;
100
+ }
101
+ if (worker.r2Buckets) {
102
+ options.r2Persist = true;
103
+ }
104
+ if (worker.secretsStoreSecrets) {
105
+ options.secretsStorePersist = true;
106
+ }
107
+ if (worker.workflows) {
108
+ options.workflowsPersist = true;
109
+ }
110
+ }
94
111
  return await this.setMiniflareOptions(options);
95
112
  });
96
113
  }
@@ -1,11 +1,12 @@
1
1
  import type { RemoteProxyConnectionString } from "miniflare";
2
2
  import { HTTPServer } from "../../util/http.ts";
3
3
  import { extractCloudflareResult } from "../api-response.ts";
4
- import { createCloudflareApi, type CloudflareApi } from "../api.ts";
4
+ import type { CloudflareApi } from "../api.ts";
5
5
  import type { WorkerBindingSpec } from "../bindings.ts";
6
6
  import { getInternalWorkerBundle } from "../bundle/internal-worker-bundle.ts";
7
7
  import { WorkerBundle } from "../worker-bundle.ts";
8
8
  import type { WorkerMetadata } from "../worker-metadata.ts";
9
+ import { getAccountSubdomain } from "../worker-subdomain.ts";
9
10
 
10
11
  type WranglerSessionConfig =
11
12
  | {
@@ -30,13 +31,13 @@ export interface RemoteBindingProxy {
30
31
  }
31
32
 
32
33
  export async function createRemoteProxyWorker(input: {
34
+ api: CloudflareApi;
33
35
  name: string;
34
36
  bindings: WorkerBindingSpec[];
35
37
  }): Promise<RemoteBindingProxy> {
36
- const api = await createCloudflareApi();
37
38
  const script = await getInternalWorkerBundle("remote-binding-proxy");
38
39
  const [token, subdomain] = await Promise.all([
39
- createWorkersPreviewToken(api, {
40
+ createWorkersPreviewToken(input.api, {
40
41
  name: input.name,
41
42
  metadata: {
42
43
  main_module: script.bundle.entrypoint,
@@ -50,7 +51,7 @@ export async function createRemoteProxyWorker(input: {
50
51
  minimal_mode: true,
51
52
  },
52
53
  }),
53
- import("../worker-subdomain.ts").then((m) => m.getAccountSubdomain(api)),
54
+ getAccountSubdomain(input.api),
54
55
  ]);
55
56
 
56
57
  const proxyURL = `https://${input.name}.${subdomain}.workers.dev`;
@@ -218,6 +218,8 @@ export interface PipelineProps extends CloudflareApiOptions {
218
218
  * @default false
219
219
  */
220
220
  adopt?: boolean;
221
+
222
+ dev?: { remote?: boolean };
221
223
  }
222
224
 
223
225
  /**
@@ -337,6 +339,20 @@ export const Pipeline = Resource("cloudflare::Pipeline", async function <
337
339
  const api = await createCloudflareApi(props);
338
340
  const pipelineName = props.name ?? id;
339
341
 
342
+ if (this.scope.local && !props.dev?.remote) {
343
+ return this({
344
+ type: "pipeline",
345
+ id: this.output?.id ?? "",
346
+ name: this.output?.name ?? pipelineName,
347
+ endpoint: this.output?.endpoint ?? "",
348
+ version: this.output?.version ?? 0,
349
+ source: this.output?.source ?? [],
350
+ destination: props.destination,
351
+ compression: props.compression,
352
+ accountId: this.output?.accountId ?? "",
353
+ });
354
+ }
355
+
340
356
  if (this.phase === "delete") {
341
357
  if (props.delete !== false) {
342
358
  // Delete Pipeline
@@ -348,7 +364,7 @@ export const Pipeline = Resource("cloudflare::Pipeline", async function <
348
364
  }
349
365
  let pipelineData: CloudflarePipelineResponse;
350
366
 
351
- if (this.phase === "create") {
367
+ if (this.phase === "create" || !this.output?.id) {
352
368
  // Check if we should adopt an existing pipeline
353
369
  try {
354
370
  // Try to create pipeline first
@@ -82,6 +82,15 @@ export interface QueueConsumerProps extends CloudflareApiOptions {
82
82
  * @default false
83
83
  */
84
84
  adopt?: boolean;
85
+
86
+ /**
87
+ * If true, the queue consumer will not be created, but will be retained if it already exists.
88
+ * This is used for local development.
89
+ *
90
+ * @default `false`
91
+ * @internal
92
+ */
93
+ dev?: boolean;
85
94
  }
86
95
 
87
96
  /**
@@ -148,16 +157,24 @@ export const QueueConsumer = Resource(
148
157
  _id: string,
149
158
  props: QueueConsumerProps,
150
159
  ): Promise<QueueConsumer> {
151
- const api = await createCloudflareApi(props);
152
-
153
160
  // Get queueId from either props.queue or props.queueId
154
161
  const queueId =
155
162
  typeof props.queue === "string" ? props.queue : props.queue.id;
156
163
 
157
- if (!queueId) {
158
- throw new Error("Either queue or queueId must be provided");
164
+ if (this.scope.local && props.dev) {
165
+ return this({
166
+ id: this.output?.id ?? "",
167
+ queueId,
168
+ queue: props.queue,
169
+ type: "worker",
170
+ scriptName: props.scriptName,
171
+ settings: props.settings,
172
+ accountId: this.output?.accountId ?? "",
173
+ });
159
174
  }
160
175
 
176
+ const api = await createCloudflareApi(props);
177
+
161
178
  if (this.phase === "delete") {
162
179
  logger.log(`Deleting Queue Consumer for queue ${queueId}`);
163
180
  if (props.delete !== false && this.output?.id) {
@@ -168,9 +185,14 @@ export const QueueConsumer = Resource(
168
185
  // Return void (a deleted consumer has no content)
169
186
  return this.destroy();
170
187
  }
188
+
189
+ if (!queueId) {
190
+ throw new Error("Either queue or queueId must be provided");
191
+ }
192
+
171
193
  let consumerData: CloudflareQueueConsumerResponse;
172
194
 
173
- if (this.phase === "create" || this.output?.id === undefined) {
195
+ if (this.phase === "create" || !this.output?.id) {
174
196
  try {
175
197
  consumerData = await createQueueConsumer(api, queueId, props);
176
198
  } catch (err) {
@@ -1,5 +1,6 @@
1
1
  import type { Context } from "../context.ts";
2
2
  import { Resource, ResourceKind } from "../resource.ts";
3
+ import { Scope } from "../scope.ts";
3
4
  import { CloudflareApiError, handleApiError } from "./api-error.ts";
4
5
  import {
5
6
  createCloudflareApi,
@@ -80,6 +81,12 @@ export interface QueueProps extends CloudflareApiOptions {
80
81
  * @default false
81
82
  */
82
83
  remote?: boolean;
84
+
85
+ /**
86
+ * Set when `Scope.local` is true to force update to the queue even if it was already deployed live.
87
+ * @internal
88
+ */
89
+ force?: boolean;
83
90
  };
84
91
  }
85
92
 
@@ -95,7 +102,7 @@ export function isQueue(eventSource: any): eventSource is Queue {
95
102
  */
96
103
  export interface Queue<Body = unknown>
97
104
  extends Resource<"cloudflare::Queue">,
98
- QueueProps {
105
+ Omit<QueueProps, "dev"> {
99
106
  /**
100
107
  * Type identifier for Cloudflare Queue
101
108
  */
@@ -127,6 +134,22 @@ export interface Queue<Body = unknown>
127
134
  Body: Body;
128
135
 
129
136
  Batch: MessageBatch<Body>;
137
+
138
+ /**
139
+ * Development mode properties
140
+ * @internal
141
+ */
142
+ dev: {
143
+ /**
144
+ * The ID of the queue in development mode
145
+ */
146
+ id: string;
147
+
148
+ /**
149
+ * Whether the queue is running remotely
150
+ */
151
+ remote: boolean;
152
+ };
130
153
  }
131
154
 
132
155
  /**
@@ -208,26 +231,57 @@ export interface Queue<Body = unknown>
208
231
  *
209
232
  * @see https://developers.cloudflare.com/queues/
210
233
  */
211
- export const Queue = Resource("cloudflare::Queue", async function <
234
+ export async function Queue<T = unknown>(
235
+ id: string,
236
+ props: QueueProps = {},
237
+ ): Promise<Queue<T>> {
238
+ return await _Queue(id, {
239
+ ...props,
240
+ dev: {
241
+ ...(props.dev ?? {}),
242
+ force: Scope.current.local,
243
+ },
244
+ });
245
+ }
246
+
247
+ const _Queue = Resource("cloudflare::Queue", async function <
212
248
  T = unknown,
213
249
  >(this: Context<Queue<T>>, id: string, props: QueueProps = {}): Promise<
214
250
  Queue<T>
215
251
  > {
216
- const api = await createCloudflareApi(props);
217
252
  const queueName = props.name ?? id;
253
+ const dev = {
254
+ id: this.output?.dev?.id ?? this.output?.id ?? id,
255
+ remote: props.dev?.remote ?? false,
256
+ };
257
+ if (this.scope.local && !props.dev?.remote) {
258
+ return this({
259
+ type: "queue",
260
+ id: this.output?.id ?? "",
261
+ name: queueName,
262
+ dev,
263
+ createdOn: this.output?.createdOn ?? new Date().toISOString(),
264
+ modifiedOn: this.output?.modifiedOn ?? new Date().toISOString(),
265
+ Body: undefined as T,
266
+ Batch: undefined! as MessageBatch<T>,
267
+ });
268
+ }
269
+
270
+ const api = await createCloudflareApi(props);
218
271
 
219
272
  if (this.phase === "delete") {
220
- if (props.delete !== false) {
273
+ if (props.delete !== false && this.output?.id) {
221
274
  // Delete Queue
222
- await deleteQueue(api, this.output?.id);
275
+ await deleteQueue(api, this.output.id);
223
276
  }
224
277
 
225
278
  // Return void (a deleted queue has no content)
226
279
  return this.destroy();
227
280
  }
281
+
228
282
  let queueData: CloudflareQueueResponse;
229
283
 
230
- if (this.phase === "create") {
284
+ if (this.phase === "create" || !this.output?.id) {
231
285
  try {
232
286
  queueData = await createQueue(api, queueName, props);
233
287
  } catch (error) {
@@ -282,7 +336,7 @@ export const Queue = Resource("cloudflare::Queue", async function <
282
336
  createdOn: queueData.result.created_on || new Date().toISOString(),
283
337
  modifiedOn: queueData.result.modified_on || new Date().toISOString(),
284
338
  accountId: api.accountId,
285
- dev: props.dev,
339
+ dev,
286
340
  // phantom properties
287
341
  Body: undefined as T,
288
342
  Batch: undefined! as MessageBatch<T>,
@@ -372,12 +426,8 @@ export async function getQueue(
372
426
  */
373
427
  export async function deleteQueue(
374
428
  api: CloudflareApi,
375
- queueId?: string,
429
+ queueId: string,
376
430
  ): Promise<void> {
377
- if (!queueId) {
378
- return;
379
- }
380
-
381
431
  // Delete Queue
382
432
  const deleteResponse = await api.delete(
383
433
  `/accounts/${api.accountId}/queues/${queueId}`,
@@ -48,6 +48,15 @@ export interface RouteProps extends CloudflareApiOptions {
48
48
  * @default false
49
49
  */
50
50
  adopt?: boolean;
51
+
52
+ /**
53
+ * If true, the route will not be created, but will be retained if it already exists.
54
+ * This is used for local development.
55
+ *
56
+ * @default `false`
57
+ * @internal
58
+ */
59
+ dev?: boolean;
51
60
  }
52
61
 
53
62
  /**
@@ -150,6 +159,21 @@ export const Route = Resource(
150
159
  return this.destroy();
151
160
  }
152
161
 
162
+ if (this.scope.local && props.dev) {
163
+ // Get or infer zone ID for noop mode too
164
+ let zoneId = props.zoneId;
165
+ if (!zoneId) {
166
+ zoneId = this.output?.zoneId ?? "noop-zone";
167
+ }
168
+
169
+ return this({
170
+ id: this.output?.id ?? "noop-route",
171
+ pattern: props.pattern,
172
+ script: scriptName,
173
+ zoneId: zoneId,
174
+ });
175
+ }
176
+
153
177
  // Get or infer zone ID (only needed for create/update phases)
154
178
  let zoneId = props.zoneId;
155
179
  if (!zoneId) {
@@ -1,6 +1,8 @@
1
1
  import type { Context } from "../context.ts";
2
2
  import { Resource } from "../resource.ts";
3
3
  import { memoize } from "../util/memoize.ts";
4
+ import { withExponentialBackoff } from "../util/retry.ts";
5
+ import { CloudflareApiError } from "./api-error.ts";
4
6
  import { extractCloudflareResult } from "./api-response.ts";
5
7
  import {
6
8
  createCloudflareApi,
@@ -25,6 +27,14 @@ interface WorkerSubdomainProps extends CloudflareApiOptions {
25
27
  * @default false
26
28
  */
27
29
  retain?: boolean;
30
+ /**
31
+ * If true, the subdomain will not be created, but will be retained if it already exists.
32
+ * This is used for local development.
33
+ *
34
+ * @default `false`
35
+ * @internal
36
+ */
37
+ dev?: boolean;
28
38
  }
29
39
 
30
40
  export interface WorkerSubdomain
@@ -42,6 +52,12 @@ export const WorkerSubdomain = Resource(
42
52
  id: string,
43
53
  props: WorkerSubdomainProps,
44
54
  ) {
55
+ if (this.scope.local && props.dev) {
56
+ return this({
57
+ url: this.output?.url ?? "https://unavailable.alchemy.run",
58
+ });
59
+ }
60
+
45
61
  const api = await createCloudflareApi(props);
46
62
  if (this.phase === "delete") {
47
63
  if (!props.retain) {
@@ -86,12 +102,18 @@ export async function enableWorkerSubdomain(
86
102
  api: CloudflareApi,
87
103
  scriptName: string,
88
104
  ) {
89
- await extractCloudflareResult<SubdomainResponse>(
90
- `enable subdomain for "${scriptName}"`,
91
- api.post(
92
- `/accounts/${api.accountId}/workers/scripts/${scriptName}/subdomain`,
93
- { enabled: true, previews_enabled: true },
94
- ),
105
+ await withExponentialBackoff(
106
+ () =>
107
+ extractCloudflareResult<SubdomainResponse>(
108
+ `enable subdomain for "${scriptName}"`,
109
+ api.post(
110
+ `/accounts/${api.accountId}/workers/scripts/${scriptName}/subdomain`,
111
+ { enabled: true, previews_enabled: true },
112
+ ),
113
+ ),
114
+ (error) => error instanceof CloudflareApiError && error.status === 404,
115
+ 10,
116
+ 1000,
95
117
  );
96
118
  }
97
119
 
@@ -105,6 +127,7 @@ export async function getWorkerSubdomain(
105
127
  `/accounts/${api.accountId}/workers/scripts/${scriptName}/subdomain`,
106
128
  ),
107
129
  ).catch((error): SubdomainResponse => {
130
+ console.log("error", error);
108
131
  if (error.status === 404) {
109
132
  return { enabled: false, previews_enabled: false };
110
133
  }