alchemy 0.43.4 → 0.44.0

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 (141) hide show
  1. package/bin/alchemy.mjs +430 -346
  2. package/bin/alchemy.ts +7 -21
  3. package/bin/commands/create.ts +79 -23
  4. package/bin/services/package-manager.ts +0 -3
  5. package/bin/services/template-manager.ts +9 -4
  6. package/bin/services/vibe-rules.ts +47 -0
  7. package/bin/types.ts +25 -5
  8. package/lib/alchemy.d.ts.map +1 -1
  9. package/lib/alchemy.js +8 -11
  10. package/lib/alchemy.js.map +1 -1
  11. package/lib/apply.js +2 -0
  12. package/lib/apply.js.map +1 -1
  13. package/lib/build-date.d.ts +1 -1
  14. package/lib/build-date.js +1 -1
  15. package/lib/cloudflare/assets.d.ts +1 -1
  16. package/lib/cloudflare/assets.d.ts.map +1 -1
  17. package/lib/cloudflare/bucket.d.ts.map +1 -1
  18. package/lib/cloudflare/bucket.js +9 -6
  19. package/lib/cloudflare/bucket.js.map +1 -1
  20. package/lib/cloudflare/bundle/bundle-worker-dev.d.ts.map +1 -1
  21. package/lib/cloudflare/bundle/bundle-worker-dev.js +4 -1
  22. package/lib/cloudflare/bundle/bundle-worker-dev.js.map +1 -1
  23. package/lib/cloudflare/bundle/bundle-worker.d.ts.map +1 -1
  24. package/lib/cloudflare/bundle/bundle-worker.js +4 -1
  25. package/lib/cloudflare/bundle/bundle-worker.js.map +1 -1
  26. package/lib/cloudflare/bundle/external.d.ts +1 -0
  27. package/lib/cloudflare/bundle/external.d.ts.map +1 -1
  28. package/lib/cloudflare/bundle/external.js +1 -1
  29. package/lib/cloudflare/bundle/external.js.map +1 -1
  30. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts +10 -0
  31. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts.map +1 -0
  32. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js +67 -0
  33. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js.map +1 -0
  34. package/lib/cloudflare/container.d.ts +27 -2
  35. package/lib/cloudflare/container.d.ts.map +1 -1
  36. package/lib/cloudflare/container.js +82 -24
  37. package/lib/cloudflare/container.js.map +1 -1
  38. package/lib/cloudflare/redirect-rule.js +1 -1
  39. package/lib/cloudflare/redirect-rule.js.map +1 -1
  40. package/lib/cloudflare/website.d.ts +17 -0
  41. package/lib/cloudflare/website.d.ts.map +1 -1
  42. package/lib/cloudflare/website.js +1 -0
  43. package/lib/cloudflare/website.js.map +1 -1
  44. package/lib/cloudflare/worker/miniflare.d.ts +4 -1
  45. package/lib/cloudflare/worker/miniflare.d.ts.map +1 -1
  46. package/lib/cloudflare/worker/miniflare.js +22 -2
  47. package/lib/cloudflare/worker/miniflare.js.map +1 -1
  48. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  49. package/lib/cloudflare/worker-metadata.js +12 -0
  50. package/lib/cloudflare/worker-metadata.js.map +1 -1
  51. package/lib/cloudflare/worker.d.ts +1 -1
  52. package/lib/cloudflare/wrangler.json.d.ts +24 -0
  53. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  54. package/lib/cloudflare/wrangler.json.js +20 -2
  55. package/lib/cloudflare/wrangler.json.js.map +1 -1
  56. package/lib/context.d.ts +6 -1
  57. package/lib/context.d.ts.map +1 -1
  58. package/lib/context.js +2 -1
  59. package/lib/context.js.map +1 -1
  60. package/lib/destroy.d.ts.map +1 -1
  61. package/lib/destroy.js +1 -0
  62. package/lib/destroy.js.map +1 -1
  63. package/lib/docker/api.d.ts +11 -0
  64. package/lib/docker/api.d.ts.map +1 -1
  65. package/lib/docker/api.js +41 -1
  66. package/lib/docker/api.js.map +1 -1
  67. package/lib/docker/image.d.ts +1 -1
  68. package/lib/docker/image.d.ts.map +1 -1
  69. package/lib/docker/image.js +11 -5
  70. package/lib/docker/image.js.map +1 -1
  71. package/lib/esbuild/bundle.d.ts +2 -2
  72. package/lib/index.d.ts.map +1 -1
  73. package/lib/index.js.map +1 -1
  74. package/lib/llms.cloudflare.txt +379 -0
  75. package/lib/llms.d.ts +10 -0
  76. package/lib/llms.d.ts.map +1 -0
  77. package/lib/llms.js +12 -0
  78. package/lib/llms.js.map +1 -0
  79. package/lib/planetscale/index.d.ts +3 -2
  80. package/lib/planetscale/index.d.ts.map +1 -1
  81. package/lib/planetscale/index.js +3 -2
  82. package/lib/planetscale/index.js.map +1 -1
  83. package/lib/planetscale/password.d.ts +233 -0
  84. package/lib/planetscale/password.d.ts.map +1 -0
  85. package/lib/planetscale/password.js +238 -0
  86. package/lib/planetscale/password.js.map +1 -0
  87. package/lib/scope.d.ts +6 -5
  88. package/lib/scope.d.ts.map +1 -1
  89. package/lib/scope.js +18 -7
  90. package/lib/scope.js.map +1 -1
  91. package/lib/test/bun.d.ts.map +1 -1
  92. package/lib/test/bun.js +1 -0
  93. package/lib/test/bun.js.map +1 -1
  94. package/lib/test/vitest.d.ts.map +1 -1
  95. package/lib/test/vitest.js +1 -0
  96. package/lib/test/vitest.js.map +1 -1
  97. package/lib/util/nanoid.d.ts +3 -0
  98. package/lib/util/nanoid.d.ts.map +1 -0
  99. package/lib/util/nanoid.js +13 -0
  100. package/lib/util/nanoid.js.map +1 -0
  101. package/lib/util/safe-fetch.d.ts.map +1 -1
  102. package/lib/util/safe-fetch.js +0 -1
  103. package/lib/util/safe-fetch.js.map +1 -1
  104. package/lib/util/telemetry/client.d.ts.map +1 -1
  105. package/lib/util/telemetry/client.js +13 -3
  106. package/lib/util/telemetry/client.js.map +1 -1
  107. package/lib/util/telemetry/constants.d.ts +2 -1
  108. package/lib/util/telemetry/constants.d.ts.map +1 -1
  109. package/lib/util/telemetry/constants.js +3 -2
  110. package/lib/util/telemetry/constants.js.map +1 -1
  111. package/package.json +10 -2
  112. package/src/alchemy.ts +8 -13
  113. package/src/apply.ts +2 -0
  114. package/src/build-date.ts +1 -1
  115. package/src/cloudflare/bucket.ts +22 -14
  116. package/src/cloudflare/bundle/bundle-worker-dev.ts +4 -1
  117. package/src/cloudflare/bundle/bundle-worker.ts +4 -1
  118. package/src/cloudflare/bundle/external.ts +1 -1
  119. package/src/cloudflare/bundle/nodejs-import-warning-plugin.ts +94 -0
  120. package/src/cloudflare/container.ts +113 -28
  121. package/src/cloudflare/redirect-rule.ts +1 -1
  122. package/src/cloudflare/website.ts +21 -1
  123. package/src/cloudflare/worker/miniflare.ts +33 -8
  124. package/src/cloudflare/worker-metadata.ts +15 -0
  125. package/src/cloudflare/wrangler.json.ts +61 -5
  126. package/src/context.ts +7 -0
  127. package/src/destroy.ts +1 -0
  128. package/src/docker/api.ts +57 -1
  129. package/src/docker/image.ts +15 -6
  130. package/src/index.ts +1 -0
  131. package/src/llms.cloudflare.txt +379 -0
  132. package/src/llms.ts +25 -0
  133. package/src/planetscale/index.ts +3 -2
  134. package/src/planetscale/password.ts +366 -0
  135. package/src/scope.ts +25 -11
  136. package/src/test/bun.ts +1 -0
  137. package/src/test/vitest.ts +1 -0
  138. package/src/util/nanoid.ts +13 -0
  139. package/src/util/safe-fetch.ts +0 -1
  140. package/src/util/telemetry/client.ts +17 -3
  141. package/src/util/telemetry/constants.ts +5 -3
@@ -96,6 +96,7 @@ export async function Container<T>(
96
96
  const image = await Image(id, {
97
97
  name: `${api.accountId}/${name}`,
98
98
  tag,
99
+ build: props.build,
99
100
  registry: {
100
101
  server: "registry.cloudflare.com",
101
102
  username: credentials.username || credentials.user!,
@@ -148,6 +149,15 @@ export interface ContainerApplicationProps extends CloudflareApiOptions {
148
149
  namespaceId: string;
149
150
  };
150
151
  rollout?: ContainerApplicationRollout;
152
+ /**
153
+ * Whether to adopt an existing container application with the same name.
154
+ *
155
+ * If `true`, the resource will attempt to adopt an existing container application
156
+ * instead of failing when one already exists with the same name.
157
+ *
158
+ * @default false
159
+ */
160
+ adopt?: boolean;
151
161
  }
152
162
 
153
163
  export type SchedulingPolicy =
@@ -155,7 +165,8 @@ export type SchedulingPolicy =
155
165
  | "gpu"
156
166
  | "regional"
157
167
  | "fill_metals"
158
- | "default";
168
+ | "default"
169
+ | (string & {});
159
170
 
160
171
  export interface ContainerApplication
161
172
  extends Resource<"cloudflare::ContainerApplication"> {
@@ -232,6 +243,21 @@ export interface ContainerApplication
232
243
  * })
233
244
  * }
234
245
  * });
246
+ *
247
+ * @example
248
+ * // Adopt an existing container application
249
+ * const existingApp = await ContainerApplication("existing-app", {
250
+ * name: "existing-app",
251
+ * adopt: true, // Will adopt instead of failing if already exists
252
+ * image: await Image("updated-app", {
253
+ * name: "updated-app",
254
+ * build: {
255
+ * context: "./docker/updated"
256
+ * }
257
+ * }),
258
+ * instances: 2,
259
+ * maxInstances: 5
260
+ * });
235
261
  */
236
262
  export const ContainerApplication = Resource(
237
263
  "cloudflare::ContainerApplication",
@@ -287,29 +313,78 @@ export const ContainerApplication = Resource(
287
313
  name: application.name,
288
314
  });
289
315
  } else {
290
- const application = await createContainerApplication(api, {
291
- name: props.name,
292
- scheduling_policy: props.schedulingPolicy ?? "default",
293
- instances: props.instances ?? 1,
294
- max_instances: props.maxInstances ?? 1,
295
- durable_objects: props.durableObjects
296
- ? {
297
- namespace_id: props.durableObjects.namespaceId,
298
- }
299
- : undefined,
300
- constraints: {
301
- tier: 1,
302
- },
303
- configuration: {
304
- image: imageReference,
305
- instance_type: props.instanceType ?? "dev",
306
- observability: {
307
- logs: {
308
- enabled: true,
316
+ let application: ContainerApplicationData;
317
+
318
+ try {
319
+ application = await createContainerApplication(api, {
320
+ name: props.name,
321
+ scheduling_policy: props.schedulingPolicy ?? "default",
322
+ instances: props.instances ?? 1,
323
+ max_instances: props.maxInstances ?? 1,
324
+ durable_objects: props.durableObjects
325
+ ? {
326
+ namespace_id: props.durableObjects.namespaceId,
327
+ }
328
+ : undefined,
329
+ constraints: {
330
+ tier: 1,
331
+ },
332
+ configuration: {
333
+ image: imageReference,
334
+ instance_type: props.instanceType ?? "dev",
335
+ observability: {
336
+ logs: {
337
+ enabled: true,
338
+ },
309
339
  },
310
340
  },
311
- },
312
- });
341
+ });
342
+ } catch (error) {
343
+ // Check if this is an "already exists" error and adopt is enabled
344
+ if (
345
+ props.adopt &&
346
+ error instanceof Error &&
347
+ error.message.includes("already exists")
348
+ ) {
349
+ // Find the existing container application
350
+ const existingApplication = await findContainerApplicationByName(
351
+ api,
352
+ props.name,
353
+ );
354
+
355
+ if (!existingApplication) {
356
+ throw new Error(
357
+ `Failed to find existing container application '${props.name}' for adoption`,
358
+ );
359
+ }
360
+
361
+ // Update the existing application with new properties
362
+ application = await updateContainerApplication(
363
+ api,
364
+ existingApplication.id,
365
+ {
366
+ instances: props.instances ?? existingApplication.instances,
367
+ max_instances:
368
+ props.maxInstances ?? existingApplication.max_instances,
369
+ scheduling_policy:
370
+ props.schedulingPolicy ?? existingApplication.scheduling_policy,
371
+ configuration,
372
+ },
373
+ );
374
+
375
+ // Create a rollout for the updated configuration
376
+ await createContainerApplicationRollout(api, application.id, {
377
+ description: "Update configuration on adoption",
378
+ strategy: "rolling",
379
+ kind: props.rollout?.kind ?? "full_auto",
380
+ step_percentage: props.rollout?.stepPercentage ?? 25,
381
+ target_configuration: configuration,
382
+ });
383
+ } else {
384
+ // Re-throw the error if adopt is false or different error
385
+ throw error;
386
+ }
387
+ }
313
388
 
314
389
  return this({
315
390
  id: application.id,
@@ -321,7 +396,7 @@ export const ContainerApplication = Resource(
321
396
 
322
397
  export interface ContainerApplicationData {
323
398
  name: string;
324
- scheduling_policy: string;
399
+ scheduling_policy: SchedulingPolicy;
325
400
  instances: number;
326
401
  max_instances: number;
327
402
  constraints: {
@@ -377,6 +452,14 @@ export async function listContainerApplications(
377
452
  );
378
453
  }
379
454
 
455
+ export async function findContainerApplicationByName(
456
+ api: CloudflareApi,
457
+ name: string,
458
+ ): Promise<ContainerApplicationData | undefined> {
459
+ const applications = await listContainerApplications(api);
460
+ return applications.find((app) => app.name === name);
461
+ }
462
+
380
463
  export interface CreateContainerApplicationBody {
381
464
  name: string;
382
465
  max_instances: number;
@@ -492,12 +575,14 @@ export async function deleteContainerApplication(
492
575
  `/accounts/${api.accountId}/containers/applications/${applicationId}`,
493
576
  );
494
577
  const result = (await response.json()) as any;
495
- if (response.ok) {
496
- return result.result;
578
+ // Treat missing applications as already-deleted so that destroy() is idempotent
579
+ if (response.ok || response.status === 404) {
580
+ return result?.result;
497
581
  }
498
- throw Error(
499
- `Failed to delete container application: ${result.errors?.map((e: { message: string }) => e.message).join(", ") ?? "Unknown error"}`,
500
- );
582
+ const errorMessages = Array.isArray(result?.errors)
583
+ ? result.errors.map((e: { message: string }) => e.message).join(", ")
584
+ : (result?.error ?? "Unknown error");
585
+ throw Error(`Failed to delete container application: ${errorMessages}`);
501
586
  }
502
587
 
503
588
  interface CreateRolloutApplicationRequest {
@@ -408,7 +408,7 @@ async function createRedirectRule(
408
408
  description: ruleset.description,
409
409
  kind: ruleset.kind,
410
410
  phase: ruleset.phase,
411
- rules: [...ruleset.rules, newRule],
411
+ rules: [...(ruleset.rules ?? []), newRule],
412
412
  });
413
413
 
414
414
  if (!response.ok) {
@@ -10,7 +10,7 @@ import {
10
10
  type AssetsConfig,
11
11
  type WorkerProps,
12
12
  } from "./worker.ts";
13
- import { WranglerJson } from "./wrangler.json.ts";
13
+ import { WranglerJson, type WranglerJsonSpec } from "./wrangler.json.ts";
14
14
 
15
15
  export interface WebsiteProps<B extends Bindings>
16
16
  extends Omit<WorkerProps<B>, "name" | "assets" | "entrypoint"> {
@@ -102,6 +102,25 @@ export interface WebsiteProps<B extends Bindings>
102
102
  command: string;
103
103
  url: string;
104
104
  };
105
+
106
+ /**
107
+ * Transform hooks to modify generated configuration files
108
+ */
109
+ transform?: {
110
+ /**
111
+ * Hook to modify the wrangler.json object before it's written
112
+ *
113
+ * This function receives the generated wrangler.json spec and should return
114
+ * a modified version. It's applied as the final transformation before the
115
+ * file is written to disk.
116
+ *
117
+ * @param spec - The generated wrangler.json specification
118
+ * @returns The modified wrangler.json specification
119
+ */
120
+ wrangler?: (
121
+ spec: WranglerJsonSpec,
122
+ ) => WranglerJsonSpec | Promise<WranglerJsonSpec>;
123
+ };
105
124
  }
106
125
 
107
126
  export type Website<B extends Bindings> = B extends { ASSETS: any }
@@ -193,6 +212,7 @@ export default {
193
212
  // path must be relative to the wrangler.jsonc file
194
213
  directory: path.relative(wranglerDir, assetsDirPath),
195
214
  },
215
+ transform: props.transform,
196
216
  });
197
217
  }
198
218
 
@@ -1,8 +1,9 @@
1
- import type {
2
- Miniflare,
3
- MiniflareOptions,
4
- RemoteProxyConnectionString,
5
- WorkerOptions,
1
+ import {
2
+ MiniflareCoreError,
3
+ type Miniflare,
4
+ type MiniflareOptions,
5
+ type RemoteProxyConnectionString,
6
+ type WorkerOptions,
6
7
  } from "miniflare";
7
8
  import path from "node:path";
8
9
  import { findOpenPort } from "../../util/find-open-port.ts";
@@ -66,7 +67,9 @@ class MiniflareServer {
66
67
  }),
67
68
  );
68
69
  if (this.miniflare) {
69
- await this.miniflare.setOptions(await this.miniflareOptions());
70
+ await withErrorRewrite(
71
+ this.miniflare.setOptions(await this.miniflareOptions()),
72
+ );
70
73
  } else {
71
74
  const { Miniflare } = await import("miniflare").catch(() => {
72
75
  throw new Error(
@@ -81,9 +84,8 @@ class MiniflareServer {
81
84
  process.exit(0);
82
85
  }
83
86
  });
84
-
85
87
  this.miniflare = new Miniflare(await this.miniflareOptions());
86
- await this.miniflare.ready;
88
+ await withErrorRewrite(this.miniflare.ready);
87
89
  }
88
90
  const existing = this.servers.get(worker.name);
89
91
  if (existing) {
@@ -191,6 +193,29 @@ class MiniflareServer {
191
193
  }
192
194
  }
193
195
 
196
+ export class ExternalDependencyError extends Error {
197
+ constructor() {
198
+ super(
199
+ 'Miniflare detected an external dependency that could not be resolved. This typically occurs when the "nodejs_compat" or "nodejs_als" compatibility flag is not enabled.',
200
+ );
201
+ }
202
+ }
203
+
204
+ async function withErrorRewrite<T>(promise: Promise<T>) {
205
+ try {
206
+ return await promise;
207
+ } catch (error) {
208
+ if (
209
+ error instanceof MiniflareCoreError &&
210
+ error.code === "ERR_MODULE_STRING_SCRIPT"
211
+ ) {
212
+ throw new ExternalDependencyError();
213
+ } else {
214
+ throw error;
215
+ }
216
+ }
217
+ }
218
+
194
219
  declare global {
195
220
  var _ALCHEMY_MINIFLARE_SERVER: MiniflareServer | undefined;
196
221
  }
@@ -351,6 +351,21 @@ export async function prepareWorkerMetadata(
351
351
 
352
352
  const bindings = (props.bindings ?? {}) as Bindings;
353
353
 
354
+ // Validate that all Container and DurableObjectNamespace bindings have unique IDs
355
+ const bindingIds = new Map<string, string>();
356
+ for (const [bindingName, binding] of Object.entries(bindings)) {
357
+ if (typeof binding === "object" && binding !== null && "id" in binding) {
358
+ if (isDurableObjectNamespace(binding) || isContainer(binding)) {
359
+ if (bindingIds.has(binding.id)) {
360
+ throw new Error(
361
+ `Duplicate binding ID '${binding.id}' found for bindings '${bindingIds.get(binding.id)}' and '${bindingName}'. Container and DurableObjectNamespace bindings must have unique IDs.`,
362
+ );
363
+ }
364
+ bindingIds.set(binding.id, bindingName);
365
+ }
366
+ }
367
+ }
368
+
354
369
  // Convert bindings to the format expected by the API
355
370
  for (const [bindingName, binding] of Object.entries(bindings)) {
356
371
  // Create a copy of the binding to avoid modifying the original
@@ -47,6 +47,25 @@ export interface WranglerJsonProps {
47
47
  binding: string;
48
48
  directory: string;
49
49
  };
50
+
51
+ /**
52
+ * Transform hooks to modify generated configuration files
53
+ */
54
+ transform?: {
55
+ /**
56
+ * Hook to modify the wrangler.json object before it's written
57
+ *
58
+ * This function receives the generated wrangler.json spec and should return
59
+ * a modified version. It's applied as the final transformation before the
60
+ * file is written to disk.
61
+ *
62
+ * @param spec - The generated wrangler.json specification
63
+ * @returns The modified wrangler.json specification
64
+ */
65
+ wrangler?: (
66
+ spec: WranglerJsonSpec,
67
+ ) => WranglerJsonSpec | Promise<WranglerJsonSpec>;
68
+ };
50
69
  }
51
70
 
52
71
  /**
@@ -156,14 +175,19 @@ export const WranglerJson = Resource(
156
175
  spec.assets.directory = path.relative(dirname, spec.assets.directory);
157
176
  }
158
177
 
178
+ // Apply the wrangler configuration hook as the final transformation
179
+ const finalSpec = props.transform?.wrangler
180
+ ? await props.transform.wrangler(spec)
181
+ : spec;
182
+
159
183
  await fs.mkdir(dirname, { recursive: true });
160
- await fs.writeFile(filePath, await formatJson(spec));
184
+ await fs.writeFile(filePath, await formatJson(finalSpec));
161
185
 
162
186
  // Return the resource
163
187
  return this({
164
188
  ...props,
165
189
  path: path.relative(cwd, filePath),
166
- spec,
190
+ spec: finalSpec,
167
191
  createdAt: Date.now(),
168
192
  updatedAt: Date.now(),
169
193
  });
@@ -216,6 +240,7 @@ export interface WranglerJsonSpec {
216
240
  */
217
241
  ai?: {
218
242
  binding: string;
243
+ experimental_remote?: boolean;
219
244
  };
220
245
 
221
246
  /**
@@ -223,6 +248,7 @@ export interface WranglerJsonSpec {
223
248
  */
224
249
  browser?: {
225
250
  binding: string;
251
+ experimental_remote?: boolean;
226
252
  };
227
253
 
228
254
  /**
@@ -230,6 +256,7 @@ export interface WranglerJsonSpec {
230
256
  */
231
257
  images?: {
232
258
  binding: string;
259
+ experimental_remote?: boolean;
233
260
  };
234
261
 
235
262
  /**
@@ -242,6 +269,7 @@ export interface WranglerJsonSpec {
242
269
  * The ID of the KV namespace used during `wrangler dev`
243
270
  */
244
271
  preview_id?: string;
272
+ experimental_remote?: boolean;
245
273
  }[];
246
274
 
247
275
  /**
@@ -266,6 +294,7 @@ export interface WranglerJsonSpec {
266
294
  * The preview name of this R2 bucket at the edge.
267
295
  */
268
296
  preview_bucket_name?: string;
297
+ experimental_remote?: boolean;
269
298
  }[];
270
299
 
271
300
  /**
@@ -308,6 +337,7 @@ export interface WranglerJsonSpec {
308
337
  vectorize?: {
309
338
  binding: string;
310
339
  index_name: string;
340
+ experimental_remote?: boolean;
311
341
  }[];
312
342
 
313
343
  /**
@@ -327,6 +357,7 @@ export interface WranglerJsonSpec {
327
357
  * The ID of the D1 database used during `wrangler dev`
328
358
  */
329
359
  preview_database_id?: string;
360
+ experimental_remote?: boolean;
330
361
  }[];
331
362
 
332
363
  /**
@@ -398,6 +429,7 @@ export interface WranglerJsonSpec {
398
429
  dispatch_namespaces?: {
399
430
  binding: string;
400
431
  namespace: string;
432
+ experimental_remote?: boolean;
401
433
  }[];
402
434
  }
403
435
 
@@ -412,8 +444,12 @@ function processBindings(
412
444
  workerCwd: string,
413
445
  ): void {
414
446
  // Arrays to collect different binding types
415
- const kvNamespaces: { binding: string; id: string; preview_id: string }[] =
416
- [];
447
+ const kvNamespaces: {
448
+ binding: string;
449
+ id: string;
450
+ preview_id: string;
451
+ experimental_remote?: boolean;
452
+ }[] = [];
417
453
  const durableObjects: {
418
454
  name: string;
419
455
  class_name: string;
@@ -440,6 +476,7 @@ function processBindings(
440
476
  database_name: string;
441
477
  migrations_dir?: string;
442
478
  preview_database_id: string;
479
+ experimental_remote?: boolean;
443
480
  }[] = [];
444
481
  const queues: {
445
482
  producers: { queue: string; binding: string }[];
@@ -459,7 +496,11 @@ function processBindings(
459
496
  const new_sqlite_classes: string[] = [];
460
497
  const new_classes: string[] = [];
461
498
 
462
- const vectorizeIndexes: { binding: string; index_name: string }[] = [];
499
+ const vectorizeIndexes: {
500
+ binding: string;
501
+ index_name: string;
502
+ experimental_remote?: boolean;
503
+ }[] = [];
463
504
  const analyticsEngineDatasets: { binding: string; dataset: string }[] = [];
464
505
  const hyperdrive: {
465
506
  binding: string;
@@ -475,6 +516,7 @@ function processBindings(
475
516
  const dispatchNamespaces: {
476
517
  binding: string;
477
518
  namespace: string;
519
+ experimental_remote?: boolean;
478
520
  }[] = [];
479
521
  const containers: {
480
522
  class_name: string;
@@ -527,6 +569,9 @@ function processBindings(
527
569
  binding: bindingName,
528
570
  id: id,
529
571
  preview_id: id,
572
+ ...("dev" in binding && binding.dev?.remote
573
+ ? { experimental_remote: true }
574
+ : {}),
530
575
  });
531
576
  } else if (
532
577
  typeof binding === "object" &&
@@ -550,6 +595,7 @@ function processBindings(
550
595
  binding: bindingName,
551
596
  bucket_name: binding.name,
552
597
  preview_bucket_name: binding.name,
598
+ ...(binding.dev?.remote ? { experimental_remote: true } : {}),
553
599
  });
554
600
  } else if (binding.type === "secret") {
555
601
  // Secret binding
@@ -573,6 +619,7 @@ function processBindings(
573
619
  database_name: binding.name,
574
620
  migrations_dir: binding.migrationsDir,
575
621
  preview_database_id: binding.id,
622
+ ...(binding.dev?.remote ? { experimental_remote: true } : {}),
576
623
  });
577
624
  } else if (binding.type === "queue") {
578
625
  queues.producers.push({
@@ -583,6 +630,8 @@ function processBindings(
583
630
  vectorizeIndexes.push({
584
631
  binding: bindingName,
585
632
  index_name: binding.name,
633
+ // https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
634
+ experimental_remote: true,
586
635
  });
587
636
  } else if (binding.type === "browser") {
588
637
  if (spec.browser) {
@@ -590,6 +639,8 @@ function processBindings(
590
639
  }
591
640
  spec.browser = {
592
641
  binding: bindingName,
642
+ // https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
643
+ experimental_remote: true,
593
644
  };
594
645
  } else if (binding.type === "ai") {
595
646
  if (spec.ai) {
@@ -597,6 +648,8 @@ function processBindings(
597
648
  }
598
649
  spec.ai = {
599
650
  binding: bindingName,
651
+ // https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
652
+ experimental_remote: true,
600
653
  };
601
654
  } else if (binding.type === "images") {
602
655
  if (spec.images) {
@@ -604,6 +657,8 @@ function processBindings(
604
657
  }
605
658
  spec.images = {
606
659
  binding: bindingName,
660
+ // https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
661
+ experimental_remote: true,
607
662
  };
608
663
  } else if (binding.type === "analytics_engine") {
609
664
  analyticsEngineDatasets.push({
@@ -648,6 +703,7 @@ function processBindings(
648
703
  dispatchNamespaces.push({
649
704
  binding: bindingName,
650
705
  namespace: binding.namespaceName,
706
+ experimental_remote: true,
651
707
  });
652
708
  } else if (binding.type === "secret_key") {
653
709
  // no-op
package/src/context.ts CHANGED
@@ -46,6 +46,10 @@ export interface BaseContext<Out extends Resource> {
46
46
  id: ResourceID;
47
47
  fqn: ResourceFQN;
48
48
  scope: Scope;
49
+ /**
50
+ * Indicates whether this resource is being created as a replacement for another resource
51
+ */
52
+ isReplacement: boolean;
49
53
  get<T>(key: string): Promise<T | undefined>;
50
54
  set<T>(key: string, value: T): Promise<void>;
51
55
  delete<T>(key: string): Promise<T | undefined>;
@@ -88,6 +92,7 @@ export function context<
88
92
  state,
89
93
  replace,
90
94
  props,
95
+ isReplacement = false,
91
96
  }: {
92
97
  scope: Scope;
93
98
  phase: "create" | "update" | "delete";
@@ -98,6 +103,7 @@ export function context<
98
103
  props: Props;
99
104
  state: State<Kind, Props, Out>;
100
105
  replace: () => never;
106
+ isReplacement?: boolean;
101
107
  }): Context<Out> {
102
108
  type InternalSymbols =
103
109
  | typeof ResourceID
@@ -133,6 +139,7 @@ export function context<
133
139
  output: state.output,
134
140
  props,
135
141
  replace,
142
+ isReplacement,
136
143
  get: (key: string) => state.data[key],
137
144
  set: async (key: string, value: any) => {
138
145
  state.data[key] = value;
package/src/destroy.ts CHANGED
@@ -119,6 +119,7 @@ export async function destroy<Type extends string>(
119
119
  seq: instance[ResourceSeq],
120
120
  props: options?.replace?.props ?? state.props,
121
121
  state,
122
+ isReplacement: false,
122
123
  replace: () => {
123
124
  throw new Error("Cannot replace a resource that is being deleted");
124
125
  },