alchemy 0.63.0 → 0.64.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 (181) hide show
  1. package/bin/alchemy.js +97 -87
  2. package/bin/commands/init.ts +1 -1
  3. package/bin/services/github-workflow.ts +2 -2
  4. package/lib/alchemy.d.ts +4 -0
  5. package/lib/alchemy.d.ts.map +1 -1
  6. package/lib/alchemy.js +10 -4
  7. package/lib/alchemy.js.map +1 -1
  8. package/lib/apply.d.ts +3 -0
  9. package/lib/apply.d.ts.map +1 -1
  10. package/lib/apply.js +11 -1
  11. package/lib/apply.js.map +1 -1
  12. package/lib/cloudflare/bindings.d.ts +3 -1
  13. package/lib/cloudflare/bindings.d.ts.map +1 -1
  14. package/lib/cloudflare/bucket.d.ts +2 -1
  15. package/lib/cloudflare/bucket.d.ts.map +1 -1
  16. package/lib/cloudflare/bucket.js.map +1 -1
  17. package/lib/cloudflare/bundle/cloudflare-internal.d.ts +6 -0
  18. package/lib/cloudflare/bundle/cloudflare-internal.d.ts.map +1 -0
  19. package/lib/cloudflare/bundle/cloudflare-internal.js +39 -0
  20. package/lib/cloudflare/bundle/cloudflare-internal.js.map +1 -0
  21. package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts +2 -0
  22. package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts.map +1 -0
  23. package/lib/cloudflare/bundle/nodejs-builtin-modules.js +9 -0
  24. package/lib/cloudflare/bundle/nodejs-builtin-modules.js.map +1 -0
  25. package/lib/cloudflare/bundle/plugin-als-external.d.ts +6 -0
  26. package/lib/cloudflare/bundle/plugin-als-external.d.ts.map +1 -0
  27. package/lib/cloudflare/bundle/plugin-als-external.js +12 -0
  28. package/lib/cloudflare/bundle/plugin-als-external.js.map +1 -0
  29. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts +4 -1
  30. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts.map +1 -1
  31. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js +23 -23
  32. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -1
  33. package/lib/cloudflare/bundle/plugin-node-compat.d.ts +11 -0
  34. package/lib/cloudflare/bundle/plugin-node-compat.d.ts.map +1 -0
  35. package/lib/cloudflare/bundle/plugin-node-compat.js +93 -0
  36. package/lib/cloudflare/bundle/plugin-node-compat.js.map +1 -0
  37. package/lib/cloudflare/cloudflare-env-proxy.js +1 -1
  38. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  39. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  40. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  41. package/lib/cloudflare/d1-local-migrations.js.map +1 -1
  42. package/lib/cloudflare/durable-object-namespace.d.ts +1 -0
  43. package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
  44. package/lib/cloudflare/hyperdrive.js +1 -1
  45. package/lib/cloudflare/hyperdrive.js.map +1 -1
  46. package/lib/cloudflare/website.d.ts.map +1 -1
  47. package/lib/cloudflare/website.js +80 -82
  48. package/lib/cloudflare/website.js.map +1 -1
  49. package/lib/cloudflare/worker-bundle.d.ts +6 -0
  50. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  51. package/lib/cloudflare/worker-bundle.js +34 -10
  52. package/lib/cloudflare/worker-bundle.js.map +1 -1
  53. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  54. package/lib/cloudflare/worker-metadata.js +2 -1
  55. package/lib/cloudflare/worker-metadata.js.map +1 -1
  56. package/lib/cloudflare/worker.d.ts.map +1 -1
  57. package/lib/cloudflare/worker.js +24 -6
  58. package/lib/cloudflare/worker.js.map +1 -1
  59. package/lib/cloudflare/wrangler.json.d.ts +2 -4
  60. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  61. package/lib/cloudflare/wrangler.json.js +16 -14
  62. package/lib/cloudflare/wrangler.json.js.map +1 -1
  63. package/lib/context.d.ts +3 -1
  64. package/lib/context.d.ts.map +1 -1
  65. package/lib/context.js +2 -2
  66. package/lib/context.js.map +1 -1
  67. package/lib/destroy.d.ts +8 -0
  68. package/lib/destroy.d.ts.map +1 -1
  69. package/lib/destroy.js +20 -4
  70. package/lib/destroy.js.map +1 -1
  71. package/lib/neon/project.d.ts.map +1 -1
  72. package/lib/neon/project.js +4 -5
  73. package/lib/neon/project.js.map +1 -1
  74. package/lib/planetscale/api/client.gen.d.ts +1750 -0
  75. package/lib/planetscale/api/client.gen.d.ts.map +1 -0
  76. package/lib/planetscale/api/client.gen.js +2401 -0
  77. package/lib/planetscale/api/client.gen.js.map +1 -0
  78. package/lib/planetscale/api/index.d.ts +3 -0
  79. package/lib/planetscale/api/index.d.ts.map +1 -0
  80. package/lib/planetscale/api/index.js +4 -0
  81. package/lib/planetscale/api/index.js.map +1 -0
  82. package/lib/planetscale/api/types.gen.d.ts +12156 -0
  83. package/lib/planetscale/api/types.gen.d.ts.map +1 -0
  84. package/lib/planetscale/api/types.gen.js +2 -0
  85. package/lib/planetscale/api/types.gen.js.map +1 -0
  86. package/lib/planetscale/branch.d.ts +5 -8
  87. package/lib/planetscale/branch.d.ts.map +1 -1
  88. package/lib/planetscale/branch.js +111 -97
  89. package/lib/planetscale/branch.js.map +1 -1
  90. package/lib/planetscale/database.d.ts +2 -6
  91. package/lib/planetscale/database.d.ts.map +1 -1
  92. package/lib/planetscale/database.js +157 -118
  93. package/lib/planetscale/database.js.map +1 -1
  94. package/lib/planetscale/index.d.ts +1 -1
  95. package/lib/planetscale/index.d.ts.map +1 -1
  96. package/lib/planetscale/index.js +1 -1
  97. package/lib/planetscale/index.js.map +1 -1
  98. package/lib/planetscale/password.d.ts +4 -7
  99. package/lib/planetscale/password.d.ts.map +1 -1
  100. package/lib/planetscale/password.js +71 -56
  101. package/lib/planetscale/password.js.map +1 -1
  102. package/lib/planetscale/utils.d.ts +8 -6
  103. package/lib/planetscale/utils.d.ts.map +1 -1
  104. package/lib/planetscale/utils.js +85 -70
  105. package/lib/planetscale/utils.js.map +1 -1
  106. package/lib/scope.d.ts +5 -1
  107. package/lib/scope.d.ts.map +1 -1
  108. package/lib/scope.js +11 -3
  109. package/lib/scope.js.map +1 -1
  110. package/lib/upstash/redis.js +1 -1
  111. package/lib/upstash/redis.js.map +1 -1
  112. package/lib/util/dedent.d.ts +3 -14
  113. package/lib/util/dedent.d.ts.map +1 -1
  114. package/lib/util/dedent.js +28 -52
  115. package/lib/util/dedent.js.map +1 -1
  116. package/lib/util/idempotent-spawn.d.ts.map +1 -1
  117. package/lib/util/idempotent-spawn.js +21 -2
  118. package/lib/util/idempotent-spawn.js.map +1 -1
  119. package/lib/util/xml.js +1 -1
  120. package/lib/util/xml.js.map +1 -1
  121. package/package.json +4 -3
  122. package/src/alchemy.ts +14 -6
  123. package/src/apply.ts +13 -1
  124. package/src/cloudflare/bindings.ts +3 -1
  125. package/src/cloudflare/bucket.ts +3 -1
  126. package/src/cloudflare/bundle/cloudflare-internal.ts +42 -0
  127. package/src/cloudflare/bundle/nodejs-builtin-modules.ts +14 -0
  128. package/src/cloudflare/bundle/plugin-als-external.ts +13 -0
  129. package/src/cloudflare/bundle/plugin-hybrid-node-compat.ts +29 -28
  130. package/src/cloudflare/bundle/plugin-node-compat.ts +114 -0
  131. package/src/cloudflare/cloudflare-env-proxy.ts +1 -1
  132. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  133. package/src/cloudflare/d1-local-migrations.ts +2 -2
  134. package/src/cloudflare/durable-object-namespace.ts +1 -1
  135. package/src/cloudflare/hyperdrive.ts +1 -1
  136. package/src/cloudflare/website.ts +88 -89
  137. package/src/cloudflare/worker-bundle.ts +46 -9
  138. package/src/cloudflare/worker-metadata.ts +3 -1
  139. package/src/cloudflare/worker.ts +35 -7
  140. package/src/cloudflare/wrangler.json.ts +108 -110
  141. package/src/context.ts +5 -3
  142. package/src/destroy.ts +26 -8
  143. package/src/neon/project.ts +4 -5
  144. package/src/planetscale/api/client.gen.ts +2882 -0
  145. package/src/planetscale/api/index.ts +4 -0
  146. package/src/planetscale/api/types.gen.ts +13041 -0
  147. package/src/planetscale/branch.ts +152 -169
  148. package/src/planetscale/database.ts +195 -204
  149. package/src/planetscale/index.ts +1 -1
  150. package/src/planetscale/password.ts +86 -85
  151. package/src/planetscale/utils.ts +141 -122
  152. package/src/scope.ts +12 -3
  153. package/src/upstash/redis.ts +1 -1
  154. package/src/util/dedent.ts +33 -61
  155. package/src/util/idempotent-spawn.ts +23 -2
  156. package/src/util/xml.ts +1 -1
  157. package/templates/astro/alchemy.run.ts +1 -3
  158. package/templates/hono/alchemy.run.ts +0 -1
  159. package/templates/nuxt/alchemy.run.ts +1 -3
  160. package/templates/react-router/alchemy.run.ts +1 -3
  161. package/templates/rwsdk/alchemy.run.ts +0 -2
  162. package/templates/sveltekit/alchemy.run.ts +1 -3
  163. package/templates/tanstack-start/alchemy.run.ts +1 -3
  164. package/templates/typescript/alchemy.run.ts +0 -1
  165. package/templates/vite/alchemy.run.ts +0 -1
  166. package/workers/cloudflare-state-store.js +58 -58
  167. package/lib/cloudflare/bundle/externals.d.ts +0 -3
  168. package/lib/cloudflare/bundle/externals.d.ts.map +0 -1
  169. package/lib/cloudflare/bundle/externals.js +0 -35
  170. package/lib/cloudflare/bundle/externals.js.map +0 -1
  171. package/lib/cloudflare/bundle/plugin-compat-warning.d.ts +0 -10
  172. package/lib/cloudflare/bundle/plugin-compat-warning.d.ts.map +0 -1
  173. package/lib/cloudflare/bundle/plugin-compat-warning.js +0 -67
  174. package/lib/cloudflare/bundle/plugin-compat-warning.js.map +0 -1
  175. package/lib/planetscale/api.d.ts +0 -53
  176. package/lib/planetscale/api.d.ts.map +0 -1
  177. package/lib/planetscale/api.js +0 -84
  178. package/lib/planetscale/api.js.map +0 -1
  179. package/src/cloudflare/bundle/externals.ts +0 -36
  180. package/src/cloudflare/bundle/plugin-compat-warning.ts +0 -94
  181. package/src/planetscale/api.ts +0 -115
@@ -709,14 +709,29 @@ const _Worker = Resource(
709
709
  id: string,
710
710
  props: WorkerProps<B>,
711
711
  ) {
712
- const adopt = props.adopt ?? this.scope.adopt;
712
+ let adopt = props.adopt ?? this.scope.adopt;
713
+ const workerName =
714
+ props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
715
+ if (this.phase === "create" && !props.adopt) {
716
+ // it is possible that this worker already exists and was created by the old Website wrapper with a nested scope
717
+ // we need to detect this and set adopt=true so that the previous version will be adopted seamlessly
718
+
719
+ // HEURISTIC: `this.scope` would previously be the nested outer scope alchemy.run
720
+ // so, if `this.scope` has a child of `wrangler.jsonc`, then it is likely that it was created by the old Website wrapper with a nested scope
721
+ if (await this.scope.has("wrangler.jsonc", "cloudflare::WranglerJson")) {
722
+ logger.warn(
723
+ `Migrating Worker '${workerName}' from the legacy Website wrapper.`,
724
+ );
725
+ props.adopt = true;
726
+ }
727
+ }
728
+
713
729
  const options = (() => {
714
730
  if (props.projectRoot) {
715
731
  logger.warn("projectRoot is deprecated, use cwd instead");
716
732
  props.cwd = props.projectRoot;
717
733
  }
718
- const name =
719
- props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
734
+
720
735
  const cwd = path.resolve(props.cwd ?? process.cwd());
721
736
  const compatibilityDate =
722
737
  props.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE;
@@ -739,7 +754,8 @@ const _Worker = Resource(
739
754
  cwd,
740
755
  compatibilityDate,
741
756
  compatibilityFlags,
742
- outdir: props.bundle?.outdir ?? path.join(cwd, ".alchemy", "out", name),
757
+ outdir:
758
+ props.bundle?.outdir ?? path.join(cwd, ".alchemy", "out", workerName),
743
759
  sourceMap: "sourceMap" in props ? props.sourceMap : undefined,
744
760
  });
745
761
 
@@ -756,19 +772,19 @@ const _Worker = Resource(
756
772
  containers.push(binding);
757
773
  } else if (
758
774
  binding.type === "workflow" &&
759
- (!binding.scriptName || binding.scriptName === name)
775
+ (!binding.scriptName || binding.scriptName === workerName)
760
776
  ) {
761
777
  workflows.push(binding);
762
778
  } else if (
763
779
  binding.type === "durable_object_namespace" &&
764
- (!binding.scriptName || binding.scriptName === name)
780
+ (!binding.scriptName || binding.scriptName === workerName)
765
781
  ) {
766
782
  durableObjects.push(binding);
767
783
  }
768
784
  }
769
785
 
770
786
  return {
771
- name,
787
+ name: workerName,
772
788
  cwd,
773
789
  compatibilityDate,
774
790
  compatibilityFlags,
@@ -780,7 +796,19 @@ const _Worker = Resource(
780
796
  durableObjects,
781
797
  };
782
798
  })();
799
+
783
800
  if (this.phase === "delete") {
801
+ // Heuristic: we must detect the case where this is the Worker wrapped in the old Website nested scope and not delete it
802
+ // we must not delete it because what we're actually doing is migrating to a flat worker
803
+ // we will achieve this by checking for a sibling resource with ID wrangler.jsonc and type cloudflare::WranglerJson
804
+ if (await this.scope.has("wrangler.jsonc", "cloudflare::WranglerJson")) {
805
+ // skip deletion
806
+ logger.warn(
807
+ `Migrating Worker '${options.name}' from the legacy Website wrapper.`,
808
+ );
809
+ return this.destroy(true);
810
+ }
811
+
784
812
  if (options.bundle.isOk()) {
785
813
  await options.bundle.value.delete?.();
786
814
  }
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { Context } from "../context.ts";
4
4
  import { Resource } from "../resource.ts";
5
+ import { Scope } from "../scope.ts";
5
6
  import { isSecret } from "../secret.ts";
6
7
  import { assertNever } from "../util/assert-never.ts";
7
8
  import {
@@ -9,6 +10,7 @@ import {
9
10
  type Bindings,
10
11
  type WorkerBindingRateLimit,
11
12
  } from "./bindings.ts";
13
+ import type { R2BucketJurisdiction } from "./bucket.ts";
12
14
  import type { DurableObjectNamespace } from "./durable-object-namespace.ts";
13
15
  import type { EventSource } from "./event-source.ts";
14
16
  import { isQueueEventSource } from "./event-source.ts";
@@ -86,9 +88,7 @@ export interface WranglerJsonProps {
86
88
  /**
87
89
  * Output returned after WranglerJson creation/update
88
90
  */
89
- export interface WranglerJson
90
- extends Resource<"cloudflare::WranglerJson">,
91
- WranglerJsonProps {
91
+ export interface WranglerJson extends WranglerJsonProps {
92
92
  /**
93
93
  * Time at which the file was created
94
94
  */
@@ -110,128 +110,123 @@ export interface WranglerJson
110
110
  spec: WranglerJsonSpec;
111
111
  }
112
112
 
113
+ // we are deprecating the WranglerJson resource (it is now just a funciton)
114
+ // but, a user may still have a resource that depends on it, so we register a no-op dummy resource so that it can be cleanly delted
115
+ Resource("cloudflare::WranglerJson", async function (this: Context<any>) {
116
+ if (this.phase === "delete") {
117
+ return this.destroy();
118
+ }
119
+
120
+ throw new Error("Not implemented");
121
+ });
122
+
113
123
  /**
114
124
  * Resource for managing wrangler.json configuration files
115
125
  */
116
- export const WranglerJson = Resource(
117
- "cloudflare::WranglerJson",
118
- {
119
- alwaysUpdate: true,
120
- },
121
- async function (
122
- this: Context<WranglerJson>,
123
- _id: string,
124
- props: WranglerJsonProps,
125
- ): Promise<WranglerJson> {
126
- if (this.phase === "delete") {
127
- return this.destroy();
128
- }
126
+ export async function WranglerJson(
127
+ props: WranglerJsonProps,
128
+ ): Promise<WranglerJson> {
129
+ const cwd = props.worker.cwd ? path.resolve(props.worker.cwd) : process.cwd();
129
130
 
130
- const cwd = props.worker.cwd
131
- ? path.resolve(props.worker.cwd)
132
- : process.cwd();
133
-
134
- const toAbsolute = <T extends string | undefined>(input: T): T => {
135
- return (input ? path.resolve(cwd, input) : undefined) as T;
136
- };
131
+ const toAbsolute = <T extends string | undefined>(input: T): T => {
132
+ return (input ? path.resolve(cwd, input) : undefined) as T;
133
+ };
137
134
 
138
- const main = toAbsolute(props.main ?? props.worker.entrypoint);
139
- let filePath = toAbsolute(props.path ?? cwd);
140
- if (!path.basename(filePath).match(".json")) {
141
- filePath = path.join(filePath, props.name ?? "wrangler.jsonc");
142
- }
135
+ const main = toAbsolute(props.main ?? props.worker.entrypoint);
136
+ let filePath = toAbsolute(props.path ?? cwd);
137
+ if (!path.basename(filePath).match(".json")) {
138
+ filePath = path.join(filePath, props.name ?? "wrangler.jsonc");
139
+ }
143
140
 
144
- const dirname = path.dirname(filePath);
141
+ const dirname = path.dirname(filePath);
145
142
 
146
- if (!main) {
147
- throw new Error(
148
- "Worker must have an entrypoint to generate a wrangler.json",
149
- );
150
- }
143
+ if (!main) {
144
+ throw new Error(
145
+ "Worker must have an entrypoint to generate a wrangler.json",
146
+ );
147
+ }
151
148
 
152
- const worker = props.worker;
153
-
154
- const spec: WranglerJsonSpec = {
155
- name: worker.name,
156
- // Use entrypoint as main if it exists
157
- main: path.relative(dirname, main),
158
- // see: https://developers.cloudflare.com/workers/configuration/compatibility-dates/
159
- compatibility_date: worker.compatibilityDate,
160
- compatibility_flags: props.worker.compatibilityFlags,
161
- assets: props.assets
162
- ? {
163
- directory: toAbsolute(props.assets.directory),
164
- binding: props.assets.binding,
165
- not_found_handling: props.worker.assets?.not_found_handling,
166
- html_handling: props.worker.assets?.html_handling,
167
- run_worker_first: props.worker.assets?.run_worker_first,
168
- }
169
- : undefined,
170
- placement: worker.placement,
171
- limits: worker.limits,
172
- };
149
+ const worker = props.worker;
150
+
151
+ const spec: WranglerJsonSpec = {
152
+ name: worker.name,
153
+ // Use entrypoint as main if it exists
154
+ main: path.relative(dirname, main),
155
+ // see: https://developers.cloudflare.com/workers/configuration/compatibility-dates/
156
+ compatibility_date: worker.compatibilityDate,
157
+ compatibility_flags: props.worker.compatibilityFlags,
158
+ assets: props.assets
159
+ ? {
160
+ directory: toAbsolute(props.assets.directory),
161
+ binding: props.assets.binding,
162
+ not_found_handling: props.worker.assets?.not_found_handling,
163
+ html_handling: props.worker.assets?.html_handling,
164
+ run_worker_first: props.worker.assets?.run_worker_first,
165
+ }
166
+ : undefined,
167
+ placement: worker.placement,
168
+ limits: worker.limits,
169
+ };
173
170
 
174
- // Process bindings if they exist
175
- if (worker.bindings) {
176
- processBindings(
177
- spec,
178
- worker.bindings,
179
- worker.eventSources,
180
- worker.name,
181
- cwd,
182
- props.secrets ?? false,
183
- this.scope.local && !props.worker.dev?.remote,
184
- );
185
- }
171
+ // Process bindings if they exist
172
+ if (worker.bindings) {
173
+ processBindings(
174
+ spec,
175
+ worker.bindings,
176
+ worker.eventSources,
177
+ worker.name,
178
+ cwd,
179
+ props.secrets ?? false,
180
+ Scope.current.local && !props.worker.dev?.remote,
181
+ );
182
+ }
186
183
 
187
- // Add environment variables as vars
188
- if (worker.env) {
189
- spec.vars = { ...worker.env };
190
- }
184
+ // Add environment variables as vars
185
+ if (worker.env) {
186
+ spec.vars = { ...worker.env };
187
+ }
191
188
 
192
- if (worker.crons && worker.crons.length > 0) {
193
- spec.triggers = { crons: worker.crons };
194
- }
189
+ if (worker.crons && worker.crons.length > 0) {
190
+ spec.triggers = { crons: worker.crons };
191
+ }
195
192
 
196
- if (spec.assets) {
197
- spec.assets.directory = path.relative(dirname, spec.assets.directory);
198
- }
193
+ if (spec.assets) {
194
+ spec.assets.directory = path.relative(dirname, spec.assets.directory);
195
+ }
199
196
 
200
- // Apply the wrangler configuration hook as the final transformation
201
- const finalSpec = props.transform?.wrangler
202
- ? await props.transform.wrangler(spec)
203
- : spec;
204
-
205
- await fs.mkdir(dirname, { recursive: true });
206
- if (props.secrets) {
207
- // If secrets are enabled, decrypt them in the wrangler.json file,
208
- // but do not modify `finalSpec` so that way secrets aren't written to state unencrypted.
209
- const withSecretsUnwrapped = {
210
- ...finalSpec,
211
- vars: {
212
- ...finalSpec.vars,
213
- ...Object.fromEntries(
214
- Object.entries(finalSpec.vars ?? {}).map(([key, value]) =>
215
- isSecret(value) ? [key, value.unencrypted] : [key, value],
216
- ),
197
+ // Apply the wrangler configuration hook as the final transformation
198
+ const finalSpec = props.transform?.wrangler
199
+ ? await props.transform.wrangler(spec)
200
+ : spec;
201
+
202
+ await fs.mkdir(dirname, { recursive: true });
203
+ if (props.secrets) {
204
+ // If secrets are enabled, decrypt them in the wrangler.json file,
205
+ // but do not modify `finalSpec` so that way secrets aren't written to state unencrypted.
206
+ const withSecretsUnwrapped = {
207
+ ...finalSpec,
208
+ vars: {
209
+ ...finalSpec.vars,
210
+ ...Object.fromEntries(
211
+ Object.entries(finalSpec.vars ?? {}).map(([key, value]) =>
212
+ isSecret(value) ? [key, value.unencrypted] : [key, value],
217
213
  ),
218
- },
219
- };
220
- await writeJSON(filePath, withSecretsUnwrapped);
221
- } else {
222
- await writeJSON(filePath, finalSpec);
223
- }
214
+ ),
215
+ },
216
+ };
217
+ await writeJSON(filePath, withSecretsUnwrapped);
218
+ } else {
219
+ await writeJSON(filePath, finalSpec);
220
+ }
224
221
 
225
- // Return the resource
226
- return this({
227
- ...props,
228
- path: path.relative(cwd, filePath),
229
- spec: finalSpec,
230
- createdAt: Date.now(),
231
- updatedAt: Date.now(),
232
- });
233
- },
234
- );
222
+ return {
223
+ ...props,
224
+ path: path.relative(cwd, filePath),
225
+ spec: finalSpec,
226
+ createdAt: Date.now(),
227
+ updatedAt: Date.now(),
228
+ };
229
+ }
235
230
 
236
231
  const writeJSON = async (filePath: string, content: any) => {
237
232
  await fs.writeFile(filePath, `${JSON.stringify(content, null, 2)}\n`);
@@ -537,6 +532,7 @@ function processBindings(
537
532
  binding: string;
538
533
  bucket_name: string;
539
534
  preview_bucket_name: string;
535
+ jurisdiction?: R2BucketJurisdiction;
540
536
  }[] = [];
541
537
  const services: { binding: string; service: string; environment?: string }[] =
542
538
  [];
@@ -683,6 +679,8 @@ function processBindings(
683
679
  binding: bindingName,
684
680
  bucket_name: name,
685
681
  preview_bucket_name: name,
682
+ jurisdiction:
683
+ binding.jurisdiction === "default" ? undefined : binding.jurisdiction,
686
684
  ...(binding.dev?.remote ? { experimental_remote: true } : {}),
687
685
  });
688
686
  } else if (binding.type === "secret") {
package/src/context.ts CHANGED
@@ -65,8 +65,10 @@ export interface BaseContext<Out extends Resource> {
65
65
  *
66
66
  * It is so that the resource lifecycle handler can "return never" instead of
67
67
  * "return undefined" so that `await MyResource()` always returns a value.
68
+ *
69
+ * @param retainChildren - Whether to retain the children of the resource.
68
70
  */
69
- destroy(): never;
71
+ destroy(retainChildren?: boolean): never;
70
72
  /**
71
73
  * Register a cleanup function that will be called when the process exits.
72
74
  *
@@ -161,8 +163,8 @@ export function context<
161
163
  return value;
162
164
  },
163
165
  quiet: scope.quiet,
164
- destroy: () => {
165
- throw new DestroyedSignal();
166
+ destroy: (retainChildren = false) => {
167
+ throw new DestroyedSignal(retainChildren);
166
168
  },
167
169
  onCleanup: (fn: () => void | Promise<void>) => {
168
170
  // make the function idempotent so repeated calls don't cause the process to hang
package/src/destroy.ts CHANGED
@@ -15,7 +15,16 @@ import type { State } from "./state.ts";
15
15
  import { formatFQN } from "./util/cli.ts";
16
16
  import { logger } from "./util/logger.ts";
17
17
 
18
- export class DestroyedSignal extends Error {}
18
+ export function isDestroyedSignal(error: any): error is DestroyedSignal {
19
+ return error instanceof Error && (error as any).kind === "DestroyedSignal";
20
+ }
21
+
22
+ export class DestroyedSignal extends Error {
23
+ readonly kind = "DestroyedSignal";
24
+ constructor(public readonly noop: boolean) {
25
+ super();
26
+ }
27
+ }
19
28
 
20
29
  export type DestroyStrategy = "sequential" | "parallel";
21
30
 
@@ -28,6 +37,10 @@ export interface DestroyOptions {
28
37
  props?: ResourceProps | undefined;
29
38
  output?: Resource<string>;
30
39
  };
40
+ /**
41
+ * If true, children of the resource will not be destroyed (but their state will be deleted).
42
+ */
43
+ noop?: boolean;
31
44
  }
32
45
 
33
46
  function isScopeArgs(a: any): a is [scope: Scope, options?: DestroyOptions] {
@@ -98,7 +111,7 @@ export async function destroy<Type extends string>(
98
111
  const quiet = options?.quiet ?? scope.quiet;
99
112
 
100
113
  try {
101
- if (!quiet) {
114
+ if (!quiet && !options?.noop) {
102
115
  logger.task(instance[ResourceFQN], {
103
116
  prefix: options?.replace ? "cleanup" : "deleting",
104
117
  prefixColor: options?.replace ? "magenta" : "redBright",
@@ -149,6 +162,8 @@ export async function destroy<Type extends string>(
149
162
  });
150
163
 
151
164
  let nestedScope: Scope | undefined;
165
+ let noop = options?.noop ?? false;
166
+
152
167
  try {
153
168
  // BUG: this does not restore persisted scope
154
169
  await alchemy.run(
@@ -158,17 +173,19 @@ export async function destroy<Type extends string>(
158
173
  isResource: instance[ResourceKind] !== "alchemy::Scope",
159
174
  parent: scope,
160
175
  destroyStrategy: instance[DestroyStrategy] ?? "sequential",
176
+ noop,
161
177
  },
162
178
  async (scope) => {
163
179
  nestedScope = options?.replace?.props == null ? scope : undefined;
164
- return await Provider.handler.bind(ctx)(
165
- instance[ResourceID],
166
- ctx.props,
167
- );
180
+ if (noop) {
181
+ return ctx.destroy(noop);
182
+ }
183
+ return Provider.handler.bind(ctx)(instance[ResourceID], ctx.props);
168
184
  },
169
185
  );
170
186
  } catch (err) {
171
- if (err instanceof DestroyedSignal) {
187
+ if (isDestroyedSignal(err)) {
188
+ noop = noop || err.noop;
172
189
  // TODO: should we fail if the DestroyedSignal is not thrown?
173
190
  } else {
174
191
  throw err;
@@ -178,6 +195,7 @@ export async function destroy<Type extends string>(
178
195
  if (nestedScope) {
179
196
  await destroy(nestedScope, {
180
197
  ...options,
198
+ noop,
181
199
  strategy: instance[DestroyStrategy] ?? "sequential",
182
200
  });
183
201
  }
@@ -195,7 +213,7 @@ export async function destroy<Type extends string>(
195
213
  await scope.set("pendingDeletions", pendingDeletions);
196
214
  }
197
215
 
198
- if (!quiet) {
216
+ if (!quiet && !options?.noop) {
199
217
  logger.task(instance[ResourceFQN], {
200
218
  prefix: options?.replace ? "cleaned" : "deleted",
201
219
  prefixColor: "greenBright",
@@ -630,15 +630,14 @@ export const NeonProject = Resource(
630
630
  default_branch_name: props.default_branch_name,
631
631
  baseUrl: props.baseUrl,
632
632
  // Add all available data
633
- // @ts-ignore - api ensures they're non-empty
633
+ // @ts-expect-error - api ensures they're non-empty
634
634
  connection_uris: response.connection_uris,
635
- // @ts-ignore
635
+ // @ts-expect-error
636
636
  roles: response.roles,
637
- // @ts-ignore
637
+ // @ts-expect-error
638
638
  databases: response.databases,
639
- // @ts-ignore
640
639
  branch: response.branch,
641
- // @ts-ignore
640
+ // @ts-expect-error
642
641
  endpoints: response.endpoints,
643
642
  });
644
643
  } catch (error) {