alchemy 0.49.0 → 0.50.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 (82) hide show
  1. package/bin/alchemy.mjs +2 -2
  2. package/lib/alchemy.d.ts +7 -1
  3. package/lib/alchemy.d.ts.map +1 -1
  4. package/lib/alchemy.js +2 -1
  5. package/lib/alchemy.js.map +1 -1
  6. package/lib/apply.js +10 -7
  7. package/lib/apply.js.map +1 -1
  8. package/lib/aws/s3-state-store.d.ts +0 -1
  9. package/lib/aws/s3-state-store.d.ts.map +1 -1
  10. package/lib/aws/s3-state-store.js +1 -3
  11. package/lib/aws/s3-state-store.js.map +1 -1
  12. package/lib/cloudflare/api-response.d.ts +2 -2
  13. package/lib/cloudflare/api-response.d.ts.map +1 -1
  14. package/lib/cloudflare/api-response.js +2 -1
  15. package/lib/cloudflare/api-response.js.map +1 -1
  16. package/lib/cloudflare/bucket.d.ts +70 -105
  17. package/lib/cloudflare/bucket.d.ts.map +1 -1
  18. package/lib/cloudflare/bucket.js +127 -244
  19. package/lib/cloudflare/bucket.js.map +1 -1
  20. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  21. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  22. package/lib/cloudflare/nuxt.js +1 -1
  23. package/lib/cloudflare/nuxt.js.map +1 -1
  24. package/lib/cloudflare/orange.d.ts.map +1 -1
  25. package/lib/cloudflare/orange.js +4 -1
  26. package/lib/cloudflare/orange.js.map +1 -1
  27. package/lib/cloudflare/redwood.d.ts.map +1 -1
  28. package/lib/cloudflare/redwood.js +5 -1
  29. package/lib/cloudflare/redwood.js.map +1 -1
  30. package/lib/cloudflare/route.d.ts.map +1 -1
  31. package/lib/cloudflare/route.js +3 -0
  32. package/lib/cloudflare/route.js.map +1 -1
  33. package/lib/cloudflare/vite.d.ts.map +1 -1
  34. package/lib/cloudflare/vite.js +2 -10
  35. package/lib/cloudflare/vite.js.map +1 -1
  36. package/lib/cloudflare/website.d.ts.map +1 -1
  37. package/lib/cloudflare/website.js +1 -0
  38. package/lib/cloudflare/website.js.map +1 -1
  39. package/lib/cloudflare/worker.d.ts.map +1 -1
  40. package/lib/cloudflare/worker.js +1 -0
  41. package/lib/cloudflare/worker.js.map +1 -1
  42. package/lib/destroy.d.ts +3 -1
  43. package/lib/destroy.d.ts.map +1 -1
  44. package/lib/destroy.js +23 -4
  45. package/lib/destroy.js.map +1 -1
  46. package/lib/os/exec.d.ts +1 -1
  47. package/lib/os/exec.d.ts.map +1 -1
  48. package/lib/os/exec.js +1 -1
  49. package/lib/os/exec.js.map +1 -1
  50. package/lib/resource.d.ts +9 -0
  51. package/lib/resource.d.ts.map +1 -1
  52. package/lib/resource.js +2 -0
  53. package/lib/resource.js.map +1 -1
  54. package/lib/scope.d.ts +8 -0
  55. package/lib/scope.d.ts.map +1 -1
  56. package/lib/scope.js +6 -2
  57. package/lib/scope.js.map +1 -1
  58. package/lib/test/options.d.ts +7 -0
  59. package/lib/test/options.d.ts.map +1 -1
  60. package/lib/util/safe-fetch.d.ts.map +1 -1
  61. package/lib/util/safe-fetch.js +9 -5
  62. package/lib/util/safe-fetch.js.map +1 -1
  63. package/package.json +2 -2
  64. package/src/alchemy.ts +9 -2
  65. package/src/apply.ts +10 -9
  66. package/src/aws/s3-state-store.ts +2 -2
  67. package/src/cloudflare/api-response.ts +7 -3
  68. package/src/cloudflare/bucket.ts +276 -468
  69. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  70. package/src/cloudflare/nuxt.ts +1 -1
  71. package/src/cloudflare/orange.ts +4 -1
  72. package/src/cloudflare/redwood.ts +6 -1
  73. package/src/cloudflare/route.ts +4 -0
  74. package/src/cloudflare/vite.ts +2 -10
  75. package/src/cloudflare/website.ts +1 -0
  76. package/src/cloudflare/worker.ts +2 -1
  77. package/src/destroy.ts +27 -5
  78. package/src/os/exec.ts +2 -2
  79. package/src/resource.ts +11 -0
  80. package/src/scope.ts +12 -3
  81. package/src/test/options.ts +8 -0
  82. package/src/util/safe-fetch.ts +12 -5
@@ -4,4 +4,4 @@
4
4
  /**
5
5
  * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
6
6
  */
7
- export const DEFAULT_COMPATIBILITY_DATE = "2025-07-18";
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-07-19";
@@ -55,7 +55,7 @@ export async function Nuxt<B extends Bindings>(
55
55
  ...props,
56
56
  noBundle: props?.noBundle ?? true,
57
57
  // Default build command, can be overridden by props.command
58
- command: props?.command ?? "bun run build",
58
+ command: props?.command ?? "nuxt build",
59
59
  // Default entry point for cloudflare-module preset
60
60
  main: props?.main ?? "./.output/server/index.mjs",
61
61
  // Default static assets directory for cloudflare-module preset
@@ -40,7 +40,10 @@ export async function Orange<B extends Bindings>(
40
40
  ): Promise<Orange<B>> {
41
41
  return Website(id, {
42
42
  ...props,
43
- command: props?.command ?? "bun run build",
43
+ command: props?.command ?? "vite build",
44
+ dev: props?.dev ?? {
45
+ command: "vite dev",
46
+ },
44
47
  // The entrypoint and the Wrangler main must differ for the Cloudflare vite plugin
45
48
  // to bundle the application correctly, and alchemy must run our bundler on the Vite
46
49
  // output since we don't resolve vite imports.
@@ -43,7 +43,12 @@ export async function Redwood<B extends Bindings>(
43
43
  ): Promise<Redwood<B>> {
44
44
  return Website(id, {
45
45
  ...props,
46
- command: props?.command ?? "bun run clean && RWSDK_DEPLOY=1 bun run build",
46
+ command:
47
+ props?.command ??
48
+ "rm -rf ./node_modules/.vite && RWSDK_DEPLOY=1 vite build",
49
+ dev: props?.dev ?? {
50
+ command: "vite dev",
51
+ },
47
52
  noBundle: props?.noBundle ?? true,
48
53
  wrangler:
49
54
  props?.wrangler === false
@@ -282,6 +282,10 @@ async function updateRoute(
282
282
  },
283
283
  );
284
284
 
285
+ if (updateResponse.status === 404) {
286
+ return await createRoute(api, zoneId, pattern, script);
287
+ }
288
+
285
289
  if (!updateResponse.ok) {
286
290
  return await handleApiError(updateResponse, "updating", "Route", pattern);
287
291
  }
@@ -1,5 +1,4 @@
1
1
  import path from "node:path";
2
- import { detectPackageManager } from "../util/detect-package-manager.ts";
3
2
  import type { Assets } from "./assets.ts";
4
3
  import type { Bindings } from "./bindings.ts";
5
4
  import { Website, type WebsiteProps } from "./website.ts";
@@ -18,14 +17,6 @@ export async function Vite<B extends Bindings>(
18
17
  props: ViteProps<B>,
19
18
  ): Promise<Vite<B>> {
20
19
  const defaultAssets = path.join("dist", "client");
21
- const packageManager = await detectPackageManager();
22
- const devCommand = {
23
- npm: "npx vite dev",
24
- bun: "bun vite dev",
25
- pnpm: "pnpm vite dev",
26
- yarn: "yarn vite dev",
27
- deno: "deno vite dev",
28
- }[packageManager];
29
20
  return Website(id, {
30
21
  ...props,
31
22
  spa: true,
@@ -36,8 +27,9 @@ export async function Vite<B extends Bindings>(
36
27
  dist: props.assets.dist ?? defaultAssets,
37
28
  }
38
29
  : (props.assets ?? defaultAssets),
30
+ command: props.command ?? "vite build",
39
31
  dev: props.dev ?? {
40
- command: devCommand,
32
+ command: "vite dev",
41
33
  },
42
34
  });
43
35
  }
@@ -231,6 +231,7 @@ export default {
231
231
  cwd,
232
232
  command: props.command,
233
233
  env: {
234
+ ...(process.env ?? {}),
234
235
  ...(props.env ?? {}),
235
236
  ...(props.commandEnv ?? {}),
236
237
  ...Object.fromEntries(
@@ -1088,6 +1088,7 @@ export const _Worker = Resource(
1088
1088
  command: props.dev.command,
1089
1089
  cwd: props.dev.cwd || props.cwd || process.cwd(),
1090
1090
  env: {
1091
+ ...(process.env ?? {}),
1091
1092
  ...props.env,
1092
1093
  ...Object.fromEntries(
1093
1094
  Object.entries(props.bindings ?? {}).flatMap(([key, value]) =>
@@ -1834,7 +1835,7 @@ async function createDevCommand(props: {
1834
1835
  id: string;
1835
1836
  command: string;
1836
1837
  cwd: string;
1837
- env: Record<string, string>;
1838
+ env: Record<string, string | undefined>;
1838
1839
  }): Promise<{ url: string }> {
1839
1840
  const persistFile = path.join(process.cwd(), ".alchemy", `${props.id}.pid`);
1840
1841
  if (await exists(persistFile)) {
package/src/destroy.ts CHANGED
@@ -16,9 +16,13 @@ import { logger } from "./util/logger.ts";
16
16
 
17
17
  export class DestroyedSignal extends Error {}
18
18
 
19
+ export type DestroyStrategy = "sequential" | "parallel";
20
+
21
+ export const DestroyStrategy = Symbol.for("alchemy::DestroyStrategy");
22
+
19
23
  export interface DestroyOptions {
20
24
  quiet?: boolean;
21
- strategy?: "sequential" | "parallel";
25
+ strategy?: DestroyStrategy;
22
26
  replace?: {
23
27
  props?: ResourceProps | undefined;
24
28
  output?: Resource<string>;
@@ -40,7 +44,7 @@ export async function destroy<Type extends string>(
40
44
  if (isScopeArgs(args)) {
41
45
  const [scope] = args;
42
46
  const options = {
43
- strategy: "sequential",
47
+ strategy: scope.destroyStrategy ?? "sequential",
44
48
  ...(args[1] ?? {}),
45
49
  } satisfies DestroyOptions;
46
50
 
@@ -118,7 +122,20 @@ export async function destroy<Type extends string>(
118
122
  fqn: instance[ResourceFQN],
119
123
  seq: instance[ResourceSeq],
120
124
  props: options?.replace?.props ?? state.props,
121
- state,
125
+ state: options?.replace?.output
126
+ ? {
127
+ output: options.replace.output,
128
+ status: "deleting",
129
+ oldProps: options.replace.props,
130
+ data: {},
131
+ kind: instance[ResourceKind],
132
+ id: instance[ResourceID],
133
+ fqn: instance[ResourceFQN],
134
+ seq: instance[ResourceSeq],
135
+ props: options.replace.props,
136
+ }
137
+ : state,
138
+ // TODO(sam|michael): should this always be false or !!options?.replace
122
139
  isReplacement: false,
123
140
  replace: () => {
124
141
  throw new Error("Cannot replace a resource that is being deleted");
@@ -134,6 +151,7 @@ export async function destroy<Type extends string>(
134
151
  // TODO(sam): this is an awful hack to differentiate between naked scopes and resources
135
152
  isResource: instance[ResourceKind] !== "alchemy::Scope",
136
153
  parent: scope,
154
+ destroyStrategy: instance[DestroyStrategy] ?? "sequential",
137
155
  },
138
156
  async (scope) => {
139
157
  nestedScope = options?.replace?.props == null ? scope : undefined;
@@ -151,13 +169,17 @@ export async function destroy<Type extends string>(
151
169
  }
152
170
  }
153
171
 
172
+ const destroyOptions = {
173
+ ...options,
174
+ strategy: instance[DestroyStrategy] ?? "sequential",
175
+ };
154
176
  if (nestedScope) {
155
- await destroy(nestedScope, options);
177
+ await destroy(nestedScope, destroyOptions);
156
178
  }
157
179
 
158
180
  if (options?.replace == null) {
159
181
  if (nestedScope) {
160
- await destroy(nestedScope, options);
182
+ await destroy(nestedScope, destroyOptions);
161
183
  }
162
184
  await scope.deleteResource(instance[ResourceID]);
163
185
  } else {
package/src/os/exec.ts CHANGED
@@ -48,7 +48,7 @@ export interface ExecProps {
48
48
  /**
49
49
  * Environment variables to set
50
50
  */
51
- env?: Record<string, string | Secret<string>>;
51
+ env?: Record<string, string | Secret<string> | undefined>;
52
52
 
53
53
  /**
54
54
  * Whether to inherit stdio from parent process
@@ -218,7 +218,7 @@ export const Exec = Resource(
218
218
  for (const [key, value] of Object.entries(props.env)) {
219
219
  if (typeof value === "string") {
220
220
  processedEnv[key] = value;
221
- } else {
221
+ } else if (value) {
222
222
  // Handle Secret objects
223
223
  processedEnv[key] = value.unencrypted;
224
224
  }
package/src/resource.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { apply } from "./apply.ts";
2
2
  import type { Context } from "./context.ts";
3
+ import { DestroyStrategy } from "./destroy.ts";
3
4
  import { Scope as _Scope, type Scope } from "./scope.ts";
4
5
 
5
6
  declare global {
@@ -59,6 +60,13 @@ export interface ProviderOptions {
59
60
  * If true, the resource will be updated even if the inputs have not changed.
60
61
  */
61
62
  alwaysUpdate: boolean;
63
+
64
+ /**
65
+ * The strategy to use when destroying the resource.
66
+ *
67
+ * @default "sequential"
68
+ */
69
+ destroyStrategy?: DestroyStrategy;
62
70
  }
63
71
 
64
72
  export type ResourceProps = {
@@ -81,6 +89,7 @@ export interface PendingResource<Out = unknown> extends Promise<Out> {
81
89
  [ResourceFQN]: ResourceFQN;
82
90
  [ResourceScope]: Scope;
83
91
  [ResourceSeq]: number;
92
+ [DestroyStrategy]: DestroyStrategy;
84
93
  [InnerResourceScope]: Promise<Scope>;
85
94
  }
86
95
 
@@ -90,6 +99,7 @@ export interface Resource<Kind extends ResourceKind = ResourceKind> {
90
99
  [ResourceFQN]: ResourceFQN;
91
100
  [ResourceScope]: Scope;
92
101
  [ResourceSeq]: number;
102
+ [DestroyStrategy]: DestroyStrategy;
93
103
  }
94
104
 
95
105
  // helper for semantic syntax highlighting (color as a type/class instead of function/value)
@@ -167,6 +177,7 @@ export function Resource<
167
177
  [ResourceFQN]: scope.fqn(resourceID),
168
178
  [ResourceSeq]: seq,
169
179
  [ResourceScope]: scope,
180
+ [DestroyStrategy]: options?.destroyStrategy ?? "sequential",
170
181
  [InnerResourceScope]: new Promise<Scope>((resolve) => {
171
182
  resolveInnerScope = resolve;
172
183
  }),
package/src/scope.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import util from "node:util";
3
3
  import type { Phase } from "./alchemy.ts";
4
- import { destroy, destroyAll } from "./destroy.ts";
4
+ import { destroy, destroyAll, DestroyStrategy } from "./destroy.ts";
5
5
  import {
6
6
  ResourceFQN,
7
7
  ResourceID,
@@ -56,6 +56,12 @@ export interface ScopeOptions {
56
56
  * @default false
57
57
  */
58
58
  force?: boolean;
59
+ /**
60
+ * The strategy to use when destroying resources.
61
+ *
62
+ * @default "sequential"
63
+ */
64
+ destroyStrategy?: DestroyStrategy;
59
65
  telemetryClient?: ITelemetryClient;
60
66
  logger?: LoggerApi;
61
67
  }
@@ -127,6 +133,7 @@ export class Scope {
127
133
  public readonly local: boolean;
128
134
  public readonly watch: boolean;
129
135
  public readonly force: boolean;
136
+ public readonly destroyStrategy: DestroyStrategy;
130
137
  public readonly logger: LoggerApi;
131
138
  public readonly telemetryClient: ITelemetryClient;
132
139
  public readonly dataMutex: AsyncMutex;
@@ -185,7 +192,8 @@ export class Scope {
185
192
  this.local = options.local ?? this.parent?.local ?? false;
186
193
  this.watch = options.watch ?? this.parent?.watch ?? false;
187
194
  this.force = options.force ?? this.parent?.force ?? false;
188
-
195
+ this.destroyStrategy =
196
+ options.destroyStrategy ?? this.parent?.destroyStrategy ?? "sequential";
189
197
  if (this.local) {
190
198
  this.logger.warnOnce(
191
199
  "Development mode is in beta. Please report any issues to https://github.com/sam-goodwin/alchemy/issues.",
@@ -322,6 +330,7 @@ export class Scope {
322
330
  [ResourceKind]: "alchemy::Scope",
323
331
  [ResourceScope]: this,
324
332
  [ResourceSeq]: this.seq(),
333
+ [DestroyStrategy]: this.destroyStrategy,
325
334
  },
326
335
  props: {},
327
336
  }
@@ -433,7 +442,7 @@ export class Scope {
433
442
  );
434
443
  await destroyAll(orphans, {
435
444
  quiet: this.quiet,
436
- strategy: "sequential",
445
+ strategy: this.destroyStrategy,
437
446
  force: shouldForce,
438
447
  });
439
448
  this.rootTelemetryClient?.record({
@@ -1,3 +1,4 @@
1
+ import type { DestroyStrategy } from "../destroy.ts";
1
2
  import type { StateStoreType } from "../state.ts";
2
3
 
3
4
  /**
@@ -31,4 +32,11 @@ export interface TestOptions {
31
32
  * Prefix to use for the scope to isolate tests and environments.
32
33
  */
33
34
  prefix?: string;
35
+
36
+ /**
37
+ * The strategy to use when destroying resources.
38
+ *
39
+ * @default "sequential"
40
+ */
41
+ destroyStrategy?: DestroyStrategy;
34
42
  }
@@ -15,13 +15,10 @@ export async function safeFetch(
15
15
  dispatcher: await dispatcher(),
16
16
  } as RequestInit);
17
17
  } catch (err: any) {
18
+ console.log("err", err);
18
19
  latestErr = err;
19
20
  const shouldRetry =
20
- err?.code === "UND_ERR_SOCKET" ||
21
- err?.code === "ECONNRESET" ||
22
- err?.code === "UND_ERR_CONNECT_TIMEOUT" ||
23
- err?.code === "EPIPE" ||
24
- err?.name === "FetchError";
21
+ isTransientNetworkError(err) || isTransientNetworkError(err.cause);
25
22
 
26
23
  if (!shouldRetry || attempt === retries) {
27
24
  throw err;
@@ -34,6 +31,16 @@ export async function safeFetch(
34
31
  throw latestErr;
35
32
  }
36
33
 
34
+ function isTransientNetworkError(err: any) {
35
+ return (
36
+ err?.code === "UND_ERR_SOCKET" ||
37
+ err?.code === "ECONNRESET" ||
38
+ err?.code === "UND_ERR_CONNECT_TIMEOUT" ||
39
+ err?.code === "EPIPE" ||
40
+ err?.name === "FetchError"
41
+ );
42
+ }
43
+
37
44
  let agent:
38
45
  | {
39
46
  value: Agent | undefined;