alchemy 0.43.5 → 0.44.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 (144) hide show
  1. package/bin/alchemy.mjs +430 -343
  2. package/bin/alchemy.ts +7 -21
  3. package/bin/commands/create.ts +79 -23
  4. package/bin/services/template-manager.ts +9 -4
  5. package/bin/services/vibe-rules.ts +47 -0
  6. package/bin/types.ts +25 -5
  7. package/lib/alchemy.d.ts.map +1 -1
  8. package/lib/alchemy.js +8 -11
  9. package/lib/alchemy.js.map +1 -1
  10. package/lib/apply.js +2 -0
  11. package/lib/apply.js.map +1 -1
  12. package/lib/build-date.d.ts +1 -1
  13. package/lib/build-date.js +1 -1
  14. package/lib/cloudflare/assets.d.ts +1 -1
  15. package/lib/cloudflare/assets.d.ts.map +1 -1
  16. package/lib/cloudflare/bucket.d.ts.map +1 -1
  17. package/lib/cloudflare/bucket.js +9 -6
  18. package/lib/cloudflare/bucket.js.map +1 -1
  19. package/lib/cloudflare/bundle/bundle-worker-dev.d.ts.map +1 -1
  20. package/lib/cloudflare/bundle/bundle-worker-dev.js +4 -1
  21. package/lib/cloudflare/bundle/bundle-worker-dev.js.map +1 -1
  22. package/lib/cloudflare/bundle/bundle-worker.d.ts.map +1 -1
  23. package/lib/cloudflare/bundle/bundle-worker.js +4 -1
  24. package/lib/cloudflare/bundle/bundle-worker.js.map +1 -1
  25. package/lib/cloudflare/bundle/external.d.ts +1 -0
  26. package/lib/cloudflare/bundle/external.d.ts.map +1 -1
  27. package/lib/cloudflare/bundle/external.js +1 -1
  28. package/lib/cloudflare/bundle/external.js.map +1 -1
  29. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts +10 -0
  30. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts.map +1 -0
  31. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js +67 -0
  32. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js.map +1 -0
  33. package/lib/cloudflare/container.d.ts +27 -2
  34. package/lib/cloudflare/container.d.ts.map +1 -1
  35. package/lib/cloudflare/container.js +82 -24
  36. package/lib/cloudflare/container.js.map +1 -1
  37. package/lib/cloudflare/do-state-store/store.d.ts.map +1 -1
  38. package/lib/cloudflare/do-state-store/store.js +5 -1
  39. package/lib/cloudflare/do-state-store/store.js.map +1 -1
  40. package/lib/cloudflare/redirect-rule.js +1 -1
  41. package/lib/cloudflare/redirect-rule.js.map +1 -1
  42. package/lib/cloudflare/website.d.ts +17 -0
  43. package/lib/cloudflare/website.d.ts.map +1 -1
  44. package/lib/cloudflare/website.js +1 -0
  45. package/lib/cloudflare/website.js.map +1 -1
  46. package/lib/cloudflare/worker/miniflare.d.ts +4 -1
  47. package/lib/cloudflare/worker/miniflare.d.ts.map +1 -1
  48. package/lib/cloudflare/worker/miniflare.js +22 -2
  49. package/lib/cloudflare/worker/miniflare.js.map +1 -1
  50. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  51. package/lib/cloudflare/worker-metadata.js +12 -0
  52. package/lib/cloudflare/worker-metadata.js.map +1 -1
  53. package/lib/cloudflare/worker.d.ts +1 -1
  54. package/lib/cloudflare/wrangler.json.d.ts +24 -0
  55. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  56. package/lib/cloudflare/wrangler.json.js +20 -2
  57. package/lib/cloudflare/wrangler.json.js.map +1 -1
  58. package/lib/context.d.ts +6 -1
  59. package/lib/context.d.ts.map +1 -1
  60. package/lib/context.js +2 -1
  61. package/lib/context.js.map +1 -1
  62. package/lib/destroy.d.ts.map +1 -1
  63. package/lib/destroy.js +1 -0
  64. package/lib/destroy.js.map +1 -1
  65. package/lib/docker/api.d.ts +11 -0
  66. package/lib/docker/api.d.ts.map +1 -1
  67. package/lib/docker/api.js +41 -1
  68. package/lib/docker/api.js.map +1 -1
  69. package/lib/docker/image.d.ts +1 -1
  70. package/lib/docker/image.d.ts.map +1 -1
  71. package/lib/docker/image.js +11 -5
  72. package/lib/docker/image.js.map +1 -1
  73. package/lib/esbuild/bundle.d.ts +2 -2
  74. package/lib/index.d.ts.map +1 -1
  75. package/lib/index.js.map +1 -1
  76. package/lib/llms.cloudflare.txt +379 -0
  77. package/lib/llms.d.ts +10 -0
  78. package/lib/llms.d.ts.map +1 -0
  79. package/lib/llms.js +12 -0
  80. package/lib/llms.js.map +1 -0
  81. package/lib/planetscale/index.d.ts +3 -2
  82. package/lib/planetscale/index.d.ts.map +1 -1
  83. package/lib/planetscale/index.js +3 -2
  84. package/lib/planetscale/index.js.map +1 -1
  85. package/lib/planetscale/password.d.ts +233 -0
  86. package/lib/planetscale/password.d.ts.map +1 -0
  87. package/lib/planetscale/password.js +238 -0
  88. package/lib/planetscale/password.js.map +1 -0
  89. package/lib/scope.d.ts +6 -5
  90. package/lib/scope.d.ts.map +1 -1
  91. package/lib/scope.js +18 -7
  92. package/lib/scope.js.map +1 -1
  93. package/lib/test/bun.d.ts.map +1 -1
  94. package/lib/test/bun.js +1 -0
  95. package/lib/test/bun.js.map +1 -1
  96. package/lib/test/vitest.d.ts.map +1 -1
  97. package/lib/test/vitest.js +1 -0
  98. package/lib/test/vitest.js.map +1 -1
  99. package/lib/util/nanoid.d.ts +3 -0
  100. package/lib/util/nanoid.d.ts.map +1 -0
  101. package/lib/util/nanoid.js +13 -0
  102. package/lib/util/nanoid.js.map +1 -0
  103. package/lib/util/safe-fetch.d.ts.map +1 -1
  104. package/lib/util/safe-fetch.js +0 -1
  105. package/lib/util/safe-fetch.js.map +1 -1
  106. package/lib/util/telemetry/client.d.ts.map +1 -1
  107. package/lib/util/telemetry/client.js +13 -3
  108. package/lib/util/telemetry/client.js.map +1 -1
  109. package/lib/util/telemetry/constants.d.ts +2 -1
  110. package/lib/util/telemetry/constants.d.ts.map +1 -1
  111. package/lib/util/telemetry/constants.js +3 -2
  112. package/lib/util/telemetry/constants.js.map +1 -1
  113. package/package.json +10 -2
  114. package/src/alchemy.ts +8 -13
  115. package/src/apply.ts +2 -0
  116. package/src/build-date.ts +1 -1
  117. package/src/cloudflare/bucket.ts +22 -14
  118. package/src/cloudflare/bundle/bundle-worker-dev.ts +4 -1
  119. package/src/cloudflare/bundle/bundle-worker.ts +4 -1
  120. package/src/cloudflare/bundle/external.ts +1 -1
  121. package/src/cloudflare/bundle/nodejs-import-warning-plugin.ts +94 -0
  122. package/src/cloudflare/container.ts +113 -28
  123. package/src/cloudflare/do-state-store/store.ts +5 -1
  124. package/src/cloudflare/redirect-rule.ts +1 -1
  125. package/src/cloudflare/website.ts +21 -1
  126. package/src/cloudflare/worker/miniflare.ts +33 -8
  127. package/src/cloudflare/worker-metadata.ts +15 -0
  128. package/src/cloudflare/wrangler.json.ts +61 -5
  129. package/src/context.ts +7 -0
  130. package/src/destroy.ts +1 -0
  131. package/src/docker/api.ts +57 -1
  132. package/src/docker/image.ts +15 -6
  133. package/src/index.ts +1 -0
  134. package/src/llms.cloudflare.txt +379 -0
  135. package/src/llms.ts +25 -0
  136. package/src/planetscale/index.ts +3 -2
  137. package/src/planetscale/password.ts +366 -0
  138. package/src/scope.ts +25 -11
  139. package/src/test/bun.ts +1 -0
  140. package/src/test/vitest.ts +1 -0
  141. package/src/util/nanoid.ts +13 -0
  142. package/src/util/safe-fetch.ts +0 -1
  143. package/src/util/telemetry/client.ts +17 -3
  144. package/src/util/telemetry/constants.ts +5 -3
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/util/telemetry/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,GAAG,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAEzC,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;AAEvC,MAAM,CAAC,MAAM,kBAAkB,GAC7B,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAEzE,wCAAwC;AACxC,MAAM,CAAC,MAAM,UAAU,GACrB,OAAO,CAAC,GAAG,CAAC,qBAAqB;IACjC,mEAAmE,CAAC"}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/util/telemetry/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,GAAG,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAEzC,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;AAEvC,MAAM,CAAC,MAAM,kBAAkB,GAC7B,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAEzE,wCAAwC;AACxC,MAAM,CAAC,MAAM,uBAAuB,GAClC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,wBAAwB,CAAC;AAClE,MAAM,CAAC,MAAM,kBAAkB,GAC7B,OAAO,CAAC,GAAG,CAAC,kBAAkB;IAC9B,iDAAiD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.43.5",
3
+ "version": "0.44.1",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "license": "Apache-2.0",
@@ -16,7 +16,7 @@
16
16
  "build:workers": "bun ./scripts/build-workers.ts",
17
17
  "build": "rm -rf ./*.tsbuildinfo && rm -rf ./lib && tsc -b && bun run build:cli && bun run build:workers",
18
18
  "docs:gen": "rm -rf ./docs && typedoc",
19
- "postbuild": "cpx 'src/**/*/types.d.ts' lib",
19
+ "postbuild": "cpx 'src/**/*/types.d.ts' lib && cpx 'src/**/*.txt' lib",
20
20
  "publish:npm": "cp ../README.md . && bun run build && bun run build:cli && npm publish && rm README.md"
21
21
  },
22
22
  "bin": {
@@ -36,6 +36,10 @@
36
36
  "bun": "./src/index.ts",
37
37
  "import": "./lib/index.js"
38
38
  },
39
+ "./llms": {
40
+ "bun": "./src/llms.ts",
41
+ "import": "./lib/llms.js"
42
+ },
39
43
  "./ai": {
40
44
  "bun": "./src/ai/index.ts",
41
45
  "import": "./lib/ai/index.js"
@@ -84,6 +88,10 @@
84
88
  "bun": "./src/os/index.ts",
85
89
  "import": "./lib/os/index.js"
86
90
  },
91
+ "./planetscale": {
92
+ "bun": "./src/planetscale/index.ts",
93
+ "import": "./lib/planetscale/index.js"
94
+ },
87
95
  "./sentry": {
88
96
  "bun": "./src/sentry/index.ts",
89
97
  "import": "./lib/sentry/index.js"
package/src/alchemy.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  ResourceSeq,
14
14
  } from "./resource.ts";
15
15
  import { isRuntime } from "./runtime/global.ts";
16
- import { Scope } from "./scope.ts";
16
+ import { DEFAULT_STAGE, Scope } from "./scope.ts";
17
17
  import { secret } from "./secret.ts";
18
18
  import type { StateStoreType } from "./state.ts";
19
19
  import type { LoggerApi } from "./util/cli.ts";
@@ -52,6 +52,7 @@ function parseCliArgs(): Partial<AlchemyOptions> {
52
52
  if (stageIndex !== -1 && stageIndex + 1 < args.length) {
53
53
  options.stage = args[stageIndex + 1];
54
54
  }
55
+ options.stage ??= process.env.STAGE;
55
56
 
56
57
  // Get password from environment variables
57
58
  if (process.env.ALCHEMY_PASSWORD) {
@@ -189,25 +190,18 @@ async function _alchemy(
189
190
  });
190
191
  const root = new Scope({
191
192
  ...mergedOptions,
192
- appName,
193
+ parent: undefined,
194
+ scopeName: appName,
193
195
  phase,
194
196
  password: mergedOptions?.password ?? process.env.ALCHEMY_PASSWORD,
195
197
  telemetryClient,
196
198
  });
199
+ const stageName = mergedOptions?.stage ?? DEFAULT_STAGE;
197
200
  const stage = new Scope({
198
201
  ...mergedOptions,
199
- scopeName:
200
- mergedOptions?.stage ??
201
- process.env.ALCHEMY_STAGE ??
202
- process.env.USER ??
203
- process.env.USERNAME,
204
202
  parent: root,
205
- appName,
206
- stage:
207
- mergedOptions?.stage ??
208
- process.env.ALCHEMY_STAGE ??
209
- process.env.USER ??
210
- process.env.USERNAME,
203
+ scopeName: stageName,
204
+ stage: stageName,
211
205
  });
212
206
  try {
213
207
  Scope.storage.enterWith(root);
@@ -455,6 +449,7 @@ async function run<T>(
455
449
  });
456
450
  const _scope = new Scope({
457
451
  ...options,
452
+ parent: options?.parent,
458
453
  scopeName: id,
459
454
  telemetryClient,
460
455
  });
package/src/apply.ts CHANGED
@@ -164,6 +164,7 @@ async function _apply<Out extends Resource>(
164
164
  seq: resource[ResourceSeq],
165
165
  props: state.oldProps,
166
166
  state,
167
+ isReplacement: false,
167
168
  replace: () => {
168
169
  if (phase === "create") {
169
170
  throw new Error(
@@ -216,6 +217,7 @@ async function _apply<Out extends Resource>(
216
217
  seq: resource[ResourceSeq],
217
218
  props: state.props,
218
219
  state,
220
+ isReplacement: true,
219
221
  replace: () => {
220
222
  throw new Error(
221
223
  `Resource ${resource[ResourceKind]} ${resource[ResourceFQN]} cannot be replaced in create phase.`,
package/src/build-date.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  * The build date used as the default worker compatibility date.
6
6
  * This is set to the date when the package was built.
7
7
  */
8
- export const BUILD_DATE = "2025-07-05";
8
+ export const BUILD_DATE = "2025-07-07";
@@ -4,6 +4,7 @@ import { Resource, ResourceKind } from "../resource.ts";
4
4
  import { bind } from "../runtime/bind.ts";
5
5
  import type { Secret } from "../secret.ts";
6
6
  import { logger } from "../util/logger.ts";
7
+ import { withExponentialBackoff } from "../util/retry.ts";
7
8
  import { CloudflareApiError, handleApiError } from "./api-error.ts";
8
9
  import { type CloudflareApi, createCloudflareApi } from "./api.ts";
9
10
  import type { Bound } from "./bound.ts";
@@ -600,22 +601,29 @@ export async function updatePublicAccess(
600
601
  ): Promise<void> {
601
602
  const headers = withJurisdiction({}, jurisdiction);
602
603
 
603
- const response = await api.put(
604
- `/accounts/${api.accountId}/r2/buckets/${bucketName}/domains/managed`,
605
- {
606
- enabled: allowPublicAccess,
604
+ await withExponentialBackoff(
605
+ async () => {
606
+ const response = await api.put(
607
+ `/accounts/${api.accountId}/r2/buckets/${bucketName}/domains/managed`,
608
+ {
609
+ enabled: allowPublicAccess,
610
+ },
611
+ { headers },
612
+ );
613
+
614
+ if (!response.ok) {
615
+ await handleApiError(
616
+ response,
617
+ "updating public access for",
618
+ "R2 bucket",
619
+ bucketName,
620
+ );
621
+ }
607
622
  },
608
- { headers },
623
+ (err) => err.status === 404,
624
+ 10,
625
+ 1000,
609
626
  );
610
-
611
- if (!response.ok) {
612
- await handleApiError(
613
- response,
614
- "updating public access for",
615
- "R2 bucket",
616
- bucketName,
617
- );
618
- }
619
627
  }
620
628
 
621
629
  /**
@@ -5,6 +5,7 @@ import { createAliasPlugin } from "./alias-plugin.ts";
5
5
  import { external, external_als } from "./external.ts";
6
6
  import { getNodeJSCompatMode } from "./nodejs-compat-mode.ts";
7
7
  import { nodeJsCompatPlugin } from "./nodejs-compat.ts";
8
+ import { nodeJsImportWarningPlugin } from "./nodejs-import-warning-plugin.ts";
8
9
  import { wasmPlugin } from "./wasm-plugin.ts";
9
10
 
10
11
  interface DevWorkerContext {
@@ -71,7 +72,9 @@ export async function createWorkerDevContext<B extends Bindings>(
71
72
  plugins: [
72
73
  wasmPlugin,
73
74
  ...(props.bundle?.plugins ?? []),
74
- ...(nodeJsCompatMode === "v2" ? [await nodeJsCompatPlugin()] : []),
75
+ nodeJsCompatMode === "v2"
76
+ ? await nodeJsCompatPlugin()
77
+ : nodeJsImportWarningPlugin(nodeJsCompatMode),
75
78
  ...(props.bundle?.alias
76
79
  ? [
77
80
  createAliasPlugin({
@@ -13,6 +13,7 @@ import {
13
13
  import { external, external_als } from "./external.ts";
14
14
  import { getNodeJSCompatMode } from "./nodejs-compat-mode.ts";
15
15
  import { nodeJsCompatPlugin } from "./nodejs-compat.ts";
16
+ import { nodeJsImportWarningPlugin } from "./nodejs-import-warning-plugin.ts";
16
17
  import { wasmPlugin } from "./wasm-plugin.ts";
17
18
 
18
19
  export type NoBundleResult = {
@@ -108,7 +109,9 @@ export async function bundleWorkerScript<B extends Bindings>(
108
109
  plugins: [
109
110
  wasmPlugin,
110
111
  ...(props.bundle?.plugins ?? []),
111
- ...(nodeJsCompatMode === "v2" ? [await nodeJsCompatPlugin()] : []),
112
+ nodeJsCompatMode === "v2"
113
+ ? await nodeJsCompatPlugin()
114
+ : nodeJsImportWarningPlugin(nodeJsCompatMode),
112
115
  ...(props.bundle?.alias
113
116
  ? [
114
117
  createAliasPlugin({
@@ -1,5 +1,5 @@
1
1
  // https://developers.cloudflare.com/workers/runtime-apis/nodejs/#supported-nodejs-apis
2
- const nodejs_compat = [
2
+ export const nodejs_compat = [
3
3
  "node:async_hooks",
4
4
  "node:assert",
5
5
  "node:buffer",
@@ -0,0 +1,94 @@
1
+ import type { Plugin, PluginBuild } from "esbuild";
2
+ import kleur from "kleur";
3
+ import module from "node:module";
4
+ import path from "node:path";
5
+ import { logger } from "../../util/logger.ts";
6
+
7
+ const NODE_REGEX = new RegExp(
8
+ `^(node:)?(${module.builtinModules.filter((m) => m !== "async_hooks").join("|")})$`,
9
+ );
10
+
11
+ /**
12
+ * ESBuild plugin to detect node:* imports and warn about missing compatibility flags.
13
+ *
14
+ * This plugin warns when:
15
+ * - Any node:* import is found without nodejs_compat flag
16
+ * - node:async_hooks is imported without nodejs_compat or nodejs_als flags
17
+ */
18
+ export function nodeJsImportWarningPlugin(mode: "als" | null): Plugin {
19
+ return {
20
+ name: "nodejs-import-warning",
21
+ setup(build: PluginBuild) {
22
+ const packages = new Set<string>();
23
+ const nodeImporters = new Set<string>();
24
+ const alsImporters = new Set<string>();
25
+
26
+ build.onStart(() => {
27
+ packages.clear();
28
+ nodeImporters.clear();
29
+ alsImporters.clear();
30
+ });
31
+
32
+ build.onResolve(
33
+ {
34
+ filter: NODE_REGEX,
35
+ },
36
+ (args) => {
37
+ packages.add(args.path);
38
+ nodeImporters.add(args.importer);
39
+ return null;
40
+ },
41
+ );
42
+
43
+ build.onResolve(
44
+ {
45
+ filter: /^(node:)?async_hooks$/,
46
+ },
47
+ (args) => {
48
+ alsImporters.add(args.importer);
49
+ return null;
50
+ },
51
+ );
52
+
53
+ build.onEnd(() => {
54
+ if (packages.size > 0) {
55
+ const formattedImports = Array.from(packages)
56
+ .sort()
57
+ .map((imp) => kleur.yellow(imp))
58
+ .join(", ");
59
+
60
+ logger.warn(
61
+ [
62
+ `Detected Node.js imports (${formattedImports}) but ${kleur.red("nodejs_compat")} compatibility flag is not set. `,
63
+ `Add ${kleur.blue("nodejs_compat")} to your compatibility flags and ensure compatibilityDate >= 2024-09-23. Imported from:`,
64
+ formatImporters(
65
+ nodeImporters,
66
+ build.initialOptions.absWorkingDir ?? process.cwd(),
67
+ ),
68
+ ].join("\n"),
69
+ );
70
+ }
71
+
72
+ if (alsImporters.size > 0 && !mode) {
73
+ logger.warn(
74
+ [
75
+ `Detected Node.js imports but ${kleur.red("nodejs_als")} compatibility flag is not set. `,
76
+ `Add ${kleur.blue("nodejs_als")} to your compatibility flags. Imported from:`,
77
+ formatImporters(
78
+ alsImporters,
79
+ build.initialOptions.absWorkingDir ?? process.cwd(),
80
+ ),
81
+ ].join("\n"),
82
+ );
83
+ }
84
+ });
85
+ },
86
+ };
87
+ }
88
+
89
+ function formatImporters(importers: Set<string>, cwd: string) {
90
+ return Array.from(importers)
91
+ .sort()
92
+ .map((imp) => `- ${kleur.yellow(path.relative(cwd, imp))}`)
93
+ .join("\n");
94
+ }
@@ -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 {
@@ -1,6 +1,7 @@
1
1
  import { alchemy } from "../../alchemy.ts";
2
2
  import { ResourceScope } from "../../resource.ts";
3
3
  import type { Scope } from "../../scope.ts";
4
+ import { serialize } from "../../serde.ts";
4
5
  import type { State, StateStore } from "../../state.ts";
5
6
  import { deserializeState } from "../../state.ts";
6
7
  import { createCloudflareApi, type CloudflareApiOptions } from "../api.ts";
@@ -153,7 +154,10 @@ export class DOStateStore implements StateStore {
153
154
 
154
155
  async set(key: string, value: State): Promise<void> {
155
156
  const client = await this.getClient();
156
- await client.rpc("set", { key: this.serializeKey(key), value });
157
+ await client.rpc("set", {
158
+ key: this.serializeKey(key),
159
+ value: await serialize(this.scope, value),
160
+ });
157
161
  }
158
162
 
159
163
  async delete(key: string): Promise<void> {
@@ -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