alchemy 0.65.1 → 0.67.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 (185) hide show
  1. package/bin/alchemy.js +139 -79
  2. package/bin/commands/init.ts +6 -3
  3. package/bin/services/cdp-manager/server.ts +3 -2
  4. package/bin/services/execute-alchemy.ts +33 -3
  5. package/bin/services/find-workspace-root.ts +48 -0
  6. package/lib/alchemy.d.ts +20 -3
  7. package/lib/alchemy.d.ts.map +1 -1
  8. package/lib/alchemy.js +18 -5
  9. package/lib/alchemy.js.map +1 -1
  10. package/lib/apply.d.ts.map +1 -1
  11. package/lib/apply.js +51 -1
  12. package/lib/apply.js.map +1 -1
  13. package/lib/cloudflare/bindings.d.ts +17 -5
  14. package/lib/cloudflare/bindings.d.ts.map +1 -1
  15. package/lib/cloudflare/bindings.js +3 -1
  16. package/lib/cloudflare/bindings.js.map +1 -1
  17. package/lib/cloudflare/bound.d.ts +4 -3
  18. package/lib/cloudflare/bound.d.ts.map +1 -1
  19. package/lib/cloudflare/bucket.js +1 -1
  20. package/lib/cloudflare/bucket.js.map +1 -1
  21. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts +1 -1
  22. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts.map +1 -1
  23. package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
  24. package/lib/cloudflare/cloudflare-env-proxy.js +3 -3
  25. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  26. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  27. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  28. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  29. package/lib/cloudflare/d1-database.js +2 -1
  30. package/lib/cloudflare/d1-database.js.map +1 -1
  31. package/lib/cloudflare/d1-local-migrations.d.ts +1 -0
  32. package/lib/cloudflare/d1-local-migrations.d.ts.map +1 -1
  33. package/lib/cloudflare/d1-local-migrations.js +3 -2
  34. package/lib/cloudflare/d1-local-migrations.js.map +1 -1
  35. package/lib/cloudflare/hyperdrive-ref.d.ts +23 -0
  36. package/lib/cloudflare/hyperdrive-ref.d.ts.map +1 -0
  37. package/lib/cloudflare/hyperdrive-ref.js +52 -0
  38. package/lib/cloudflare/hyperdrive-ref.js.map +1 -0
  39. package/lib/cloudflare/index.d.ts +2 -0
  40. package/lib/cloudflare/index.d.ts.map +1 -1
  41. package/lib/cloudflare/index.js +2 -0
  42. package/lib/cloudflare/index.js.map +1 -1
  43. package/lib/cloudflare/kv-namespace.js +1 -1
  44. package/lib/cloudflare/kv-namespace.js.map +1 -1
  45. package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -1
  46. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  47. package/lib/cloudflare/miniflare/build-worker-options.js +11 -6
  48. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  49. package/lib/cloudflare/miniflare/delete.d.ts +3 -2
  50. package/lib/cloudflare/miniflare/delete.d.ts.map +1 -1
  51. package/lib/cloudflare/miniflare/delete.js +13 -12
  52. package/lib/cloudflare/miniflare/delete.js.map +1 -1
  53. package/lib/cloudflare/miniflare/miniflare-controller.d.ts +3 -1
  54. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  55. package/lib/cloudflare/miniflare/miniflare-controller.js +29 -13
  56. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  57. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +24 -13
  58. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  59. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +95 -79
  60. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  61. package/lib/cloudflare/miniflare/paths.d.ts +4 -4
  62. package/lib/cloudflare/miniflare/paths.d.ts.map +1 -1
  63. package/lib/cloudflare/miniflare/paths.js +10 -4
  64. package/lib/cloudflare/miniflare/paths.js.map +1 -1
  65. package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts +2 -0
  66. package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts.map +1 -0
  67. package/lib/cloudflare/miniflare/symlink-miniflare-state.js +19 -0
  68. package/lib/cloudflare/miniflare/symlink-miniflare-state.js.map +1 -0
  69. package/lib/cloudflare/miniflare/tunnel.d.ts +18 -0
  70. package/lib/cloudflare/miniflare/tunnel.d.ts.map +1 -0
  71. package/lib/cloudflare/miniflare/tunnel.js +79 -0
  72. package/lib/cloudflare/miniflare/tunnel.js.map +1 -0
  73. package/lib/cloudflare/next.d.ts.map +1 -1
  74. package/lib/cloudflare/next.js +3 -0
  75. package/lib/cloudflare/next.js.map +1 -1
  76. package/lib/cloudflare/nuxt/plugin.d.ts.map +1 -1
  77. package/lib/cloudflare/nuxt/plugin.js +3 -3
  78. package/lib/cloudflare/nuxt/plugin.js.map +1 -1
  79. package/lib/cloudflare/quick-tunnel.d.ts +1 -1
  80. package/lib/cloudflare/redwood/plugin.js +2 -2
  81. package/lib/cloudflare/redwood/plugin.js.map +1 -1
  82. package/lib/cloudflare/secret-ref.d.ts +38 -0
  83. package/lib/cloudflare/secret-ref.d.ts.map +1 -0
  84. package/lib/cloudflare/secret-ref.js +38 -0
  85. package/lib/cloudflare/secret-ref.js.map +1 -0
  86. package/lib/cloudflare/vite/plugin.d.ts.map +1 -1
  87. package/lib/cloudflare/vite/plugin.js +6 -3
  88. package/lib/cloudflare/vite/plugin.js.map +1 -1
  89. package/lib/cloudflare/website.d.ts.map +1 -1
  90. package/lib/cloudflare/website.js +5 -16
  91. package/lib/cloudflare/website.js.map +1 -1
  92. package/lib/cloudflare/worker-metadata.d.ts +1 -1
  93. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  94. package/lib/cloudflare/worker-metadata.js +7 -9
  95. package/lib/cloudflare/worker-metadata.js.map +1 -1
  96. package/lib/cloudflare/worker.d.ts +16 -1
  97. package/lib/cloudflare/worker.d.ts.map +1 -1
  98. package/lib/cloudflare/worker.js +16 -2
  99. package/lib/cloudflare/worker.js.map +1 -1
  100. package/lib/cloudflare/wrangler.json.d.ts +2 -1
  101. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  102. package/lib/cloudflare/wrangler.json.js +5 -6
  103. package/lib/cloudflare/wrangler.json.js.map +1 -1
  104. package/lib/llms.cloudflare.txt +1 -1
  105. package/lib/scope.d.ts +26 -1
  106. package/lib/scope.d.ts.map +1 -1
  107. package/lib/scope.js +12 -5
  108. package/lib/scope.js.map +1 -1
  109. package/lib/state/file-system-state-store.d.ts.map +1 -1
  110. package/lib/state/file-system-state-store.js +1 -2
  111. package/lib/state/file-system-state-store.js.map +1 -1
  112. package/lib/state/sqlite-state-store.d.ts +11 -0
  113. package/lib/state/sqlite-state-store.d.ts.map +1 -1
  114. package/lib/state/sqlite-state-store.js +22 -14
  115. package/lib/state/sqlite-state-store.js.map +1 -1
  116. package/lib/stripe/customer.d.ts +0 -8
  117. package/lib/stripe/customer.d.ts.map +1 -1
  118. package/lib/stripe/customer.js +0 -6
  119. package/lib/stripe/customer.js.map +1 -1
  120. package/lib/stripe/price.d.ts +23 -23
  121. package/lib/stripe/price.d.ts.map +1 -1
  122. package/lib/stripe/price.js +6 -22
  123. package/lib/stripe/price.js.map +1 -1
  124. package/lib/test/vitest.d.ts.map +1 -1
  125. package/lib/test/vitest.js +1 -1
  126. package/lib/test/vitest.js.map +1 -1
  127. package/lib/util/assert-never.js +1 -1
  128. package/lib/util/assert-never.js.map +1 -1
  129. package/lib/util/cli.d.ts.map +1 -1
  130. package/lib/util/cli.js +12 -13
  131. package/lib/util/cli.js.map +1 -1
  132. package/lib/util/idempotent-spawn.d.ts.map +1 -1
  133. package/lib/util/idempotent-spawn.js +5 -8
  134. package/lib/util/idempotent-spawn.js.map +1 -1
  135. package/lib/util/root-dir.d.ts +2 -0
  136. package/lib/util/root-dir.d.ts.map +1 -0
  137. package/lib/util/root-dir.js +2 -0
  138. package/lib/util/root-dir.js.map +1 -0
  139. package/package.json +4 -4
  140. package/src/alchemy.ts +43 -8
  141. package/src/apply.ts +58 -3
  142. package/src/cloudflare/bindings.ts +21 -4
  143. package/src/cloudflare/bound.ts +50 -52
  144. package/src/cloudflare/bucket.ts +1 -1
  145. package/src/cloudflare/bundle/internal-worker-bundle.ts +2 -1
  146. package/src/cloudflare/cloudflare-env-proxy.ts +5 -5
  147. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  148. package/src/cloudflare/d1-database.ts +2 -1
  149. package/src/cloudflare/d1-local-migrations.ts +10 -6
  150. package/src/cloudflare/hyperdrive-ref.ts +98 -0
  151. package/src/cloudflare/index.ts +2 -0
  152. package/src/cloudflare/kv-namespace.ts +1 -1
  153. package/src/cloudflare/miniflare/build-worker-options.ts +21 -12
  154. package/src/cloudflare/miniflare/delete.ts +19 -11
  155. package/src/cloudflare/miniflare/miniflare-controller.ts +33 -14
  156. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +122 -102
  157. package/src/cloudflare/miniflare/paths.ts +11 -7
  158. package/src/cloudflare/miniflare/symlink-miniflare-state.ts +19 -0
  159. package/src/cloudflare/miniflare/tunnel.ts +116 -0
  160. package/src/cloudflare/next.ts +3 -0
  161. package/src/cloudflare/nuxt/plugin.ts +12 -3
  162. package/src/cloudflare/redwood/plugin.ts +2 -2
  163. package/src/cloudflare/secret-ref.ts +56 -0
  164. package/src/cloudflare/vite/plugin.ts +8 -3
  165. package/src/cloudflare/website.ts +5 -19
  166. package/src/cloudflare/worker-metadata.ts +14 -12
  167. package/src/cloudflare/worker.ts +36 -3
  168. package/src/cloudflare/wrangler.json.ts +14 -11
  169. package/src/llms.cloudflare.txt +1 -1
  170. package/src/scope.ts +38 -6
  171. package/src/state/file-system-state-store.ts +1 -2
  172. package/src/state/sqlite-state-store.ts +41 -14
  173. package/src/stripe/customer.ts +0 -14
  174. package/src/stripe/price.ts +59 -56
  175. package/src/test/vitest.ts +4 -1
  176. package/src/util/assert-never.ts +1 -1
  177. package/src/util/cli.ts +15 -13
  178. package/src/util/idempotent-spawn.ts +5 -6
  179. package/src/util/root-dir.ts +1 -0
  180. package/templates/nextjs/alchemy.run.ts +2 -2
  181. package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +1 -1
  182. package/templates/rwsdk/package.json +3 -3
  183. package/workers/cloudflare-state-store.js +1 -1
  184. package/workers/tunnel-proxy.js +110 -0
  185. package/workers/tunnel-proxy.ts +121 -0
@@ -388,7 +388,7 @@ const _R2Bucket = Resource(
388
388
  if (this.phase === "delete") {
389
389
  if (props.delete !== false) {
390
390
  if (this.output.dev?.id) {
391
- await deleteMiniflareBinding("r2", this.output.dev.id);
391
+ await deleteMiniflareBinding(this.scope, "r2", this.output.dev.id);
392
392
  }
393
393
  if (props.empty) {
394
394
  await emptyBucket(api, bucketName, props);
@@ -7,7 +7,8 @@ import type { WorkerBundle } from "../worker-bundle.ts";
7
7
  export type InternalWorker =
8
8
  | "cloudflare-state-store"
9
9
  | "dofs-state-store"
10
- | "remote-binding-proxy";
10
+ | "remote-binding-proxy"
11
+ | "tunnel-proxy";
11
12
 
12
13
  export const getInternalWorkerBundle = memoize(async (name: InternalWorker) => {
13
14
  const content = await fs.readFile(
@@ -1,7 +1,7 @@
1
1
  import type { GetPlatformProxyOptions } from "wrangler";
2
2
  import {
3
- DEFAULT_CONFIG_PATH,
4
- DEFAULT_PERSIST_PATH,
3
+ getDefaultConfigPath,
4
+ getDefaultPersistPath,
5
5
  validateConfigPath,
6
6
  validatePersistPath,
7
7
  } from "./miniflare/paths.ts";
@@ -26,7 +26,7 @@ export const getPlatformProxyOptions = (
26
26
  typeof input.persist === "object" &&
27
27
  typeof input.persist.path === "string"
28
28
  ? input.persist.path
29
- : DEFAULT_PERSIST_PATH,
29
+ : getDefaultPersistPath(),
30
30
  ),
31
31
  };
32
32
  if (!persist) {
@@ -39,12 +39,12 @@ export const getPlatformProxyOptions = (
39
39
  }
40
40
  return {
41
41
  ...input,
42
- configPath: validateConfigPath(input.configPath ?? DEFAULT_CONFIG_PATH),
42
+ configPath: validateConfigPath(input.configPath ?? getDefaultConfigPath()),
43
43
  persist,
44
44
  experimental: (input as any).experimental ?? {
45
45
  remoteBindings: true,
46
46
  },
47
- } as any;
47
+ };
48
48
  };
49
49
 
50
50
  const warned = new Set<string>();
@@ -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-09-06";
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-09-13";
@@ -283,6 +283,7 @@ const _D1Database = Resource(
283
283
  databaseId: dev.id,
284
284
  migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
285
285
  migrations: props.migrationsFiles,
286
+ rootDir: this.scope.rootDir,
286
287
  });
287
288
  }
288
289
  return this({
@@ -301,7 +302,7 @@ const _D1Database = Resource(
301
302
 
302
303
  if (this.phase === "delete") {
303
304
  if (this.output.dev?.id) {
304
- await deleteMiniflareBinding("d1", this.output.dev.id);
305
+ await deleteMiniflareBinding(this.scope, "d1", this.output.dev.id);
305
306
  }
306
307
  if (props.delete !== false && this.output?.id) {
307
308
  await deleteDatabase(api, this.output.id);
@@ -1,7 +1,8 @@
1
1
  import * as mf from "miniflare";
2
- import path from "node:path";
2
+ import { getDefaultPersistPath } from "./miniflare/paths.ts";
3
3
 
4
4
  export interface D1LocalMigrationOptions {
5
+ rootDir: string;
5
6
  databaseId: string;
6
7
  migrationsTable: string;
7
8
  migrations: { id: string; sql: string }[];
@@ -13,15 +14,16 @@ export const applyLocalD1Migrations = async (
13
14
  const miniflare = new mf.Miniflare({
14
15
  script: "",
15
16
  modules: true,
16
- defaultPersistRoot: path.join(process.cwd(), ".alchemy/miniflare/v3"), // vite plugin forces /v3 suffix
17
+ defaultPersistRoot: getDefaultPersistPath(options.rootDir),
17
18
  d1Persist: true,
18
19
  d1Databases: { DB: options.databaseId },
19
20
  log: process.env.DEBUG ? new mf.Log(mf.LogLevel.DEBUG) : undefined,
20
21
  });
21
22
  try {
22
23
  await miniflare.ready;
23
- const db: D1Database = await miniflare.getD1Database("DB");
24
- const session: D1DatabaseSession = db.withSession("first-primary");
24
+ // TODO(sam): don't use `any` once prisma is fixed upstream
25
+ const db: any = await miniflare.getD1Database("DB");
26
+ const session: any = db.withSession("first-primary");
25
27
  await session
26
28
  .prepare(
27
29
  `CREATE TABLE IF NOT EXISTS ${options.migrationsTable} (
@@ -31,11 +33,13 @@ export const applyLocalD1Migrations = async (
31
33
  )`,
32
34
  )
33
35
  .run();
34
- const appliedMigrations = await session
36
+ const appliedMigrations: {
37
+ results: { name: string }[];
38
+ } = await session
35
39
  .prepare(
36
40
  `SELECT name FROM ${options.migrationsTable} ORDER BY applied_at ASC`,
37
41
  )
38
- .all<{ name: string }>();
42
+ .all();
39
43
  const insertRecord = session.prepare(
40
44
  `INSERT INTO ${options.migrationsTable} (name) VALUES (?)`,
41
45
  );
@@ -0,0 +1,98 @@
1
+ import { alchemy } from "../alchemy.ts";
2
+ import type { Secret } from "../secret.ts";
3
+ import { handleApiError } from "./api-error.ts";
4
+ import {
5
+ createCloudflareApi,
6
+ type CloudflareApi,
7
+ type CloudflareApiOptions,
8
+ } from "./api.ts";
9
+
10
+ export interface HyperdriveRefProps extends CloudflareApiOptions {
11
+ name?: string;
12
+ id?: string;
13
+ dev?: {
14
+ origin: string | Secret;
15
+ };
16
+ }
17
+
18
+ export type HyperdriveRef = {
19
+ type: "hyperdrive";
20
+ name: string;
21
+ hyperdriveId: string;
22
+ dev?: {
23
+ origin: Secret;
24
+ };
25
+ };
26
+
27
+ export async function HyperdriveRef(
28
+ props: HyperdriveRefProps,
29
+ ): Promise<HyperdriveRef> {
30
+ const api = await createCloudflareApi(props);
31
+
32
+ let hyperdriveId = props.id;
33
+ let hyperdriveName = props.name;
34
+
35
+ if (!hyperdriveId && hyperdriveName) {
36
+ // Find hyperdrive by name
37
+ hyperdriveId = await findHyperdriveIdByName(api, hyperdriveName);
38
+ if (!hyperdriveId) {
39
+ throw new Error(
40
+ `Hyperdrive config with name "${hyperdriveName}" not found`,
41
+ );
42
+ }
43
+ } else if (!hyperdriveName && hyperdriveId) {
44
+ throw new Error("HyperdriveRef requires either name or both name and id");
45
+ } else if (!hyperdriveName && !hyperdriveId) {
46
+ throw new Error("HyperdriveRef requires at least a name");
47
+ }
48
+
49
+ return {
50
+ type: "hyperdrive",
51
+ name: hyperdriveName!,
52
+ hyperdriveId: hyperdriveId!,
53
+ dev: props.dev
54
+ ? {
55
+ origin:
56
+ typeof props.dev.origin === "string"
57
+ ? alchemy.secret(props.dev.origin)
58
+ : props.dev.origin,
59
+ }
60
+ : undefined,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Find a Hyperdrive configuration by name
66
+ */
67
+ export async function findHyperdriveIdByName(
68
+ api: CloudflareApi,
69
+ name: string,
70
+ page = 1,
71
+ ): Promise<string | undefined> {
72
+ const response = await api.get(
73
+ `/accounts/${api.accountId}/hyperdrive/configs?page=${page}`,
74
+ );
75
+
76
+ if (!response.ok) {
77
+ await handleApiError(response, "list", "hyperdrive", "all");
78
+ }
79
+
80
+ const data: {
81
+ result: Array<{
82
+ id: string;
83
+ name: string;
84
+ }>;
85
+ result_info?: { total_pages?: number };
86
+ } = await response.json();
87
+
88
+ const found = data.result.find((config) => config.name === name);
89
+ if (found) {
90
+ return found.id;
91
+ }
92
+
93
+ if (data.result_info?.total_pages && page < data.result_info.total_pages) {
94
+ return await findHyperdriveIdByName(api, name, page + 1);
95
+ }
96
+
97
+ return undefined;
98
+ }
@@ -30,6 +30,7 @@ export * from "./email-address.ts";
30
30
  export * from "./email-catch-all.ts";
31
31
  export * from "./email-routing.ts";
32
32
  export * from "./email-rule.ts";
33
+ export * from "./hyperdrive-ref.ts";
33
34
  export * from "./hyperdrive.ts";
34
35
  export * from "./images.ts";
35
36
  export * from "./kv-namespace.ts";
@@ -49,6 +50,7 @@ export * from "./route.ts";
49
50
  export * from "./rule.ts";
50
51
  export * from "./ruleset.ts";
51
52
  export * from "./secret-key.ts";
53
+ export * from "./secret-ref.ts";
52
54
  export * from "./secret.ts";
53
55
  export * from "./secrets-store.ts";
54
56
  export * from "./state.ts";
@@ -240,7 +240,7 @@ const _KVNamespace = Resource(
240
240
 
241
241
  if (this.phase === "delete") {
242
242
  if (this.output.dev?.id) {
243
- await deleteMiniflareBinding("kv", this.output.dev.id);
243
+ await deleteMiniflareBinding(this.scope, "kv", this.output.dev.id);
244
244
  }
245
245
  if (this.output.namespaceId && props.delete !== false) {
246
246
  await deleteKVNamespace(api, this.output.namespaceId);
@@ -2,12 +2,11 @@ import * as miniflare from "miniflare";
2
2
  import { assertNever } from "../../util/assert-never.ts";
3
3
  import type { HTTPServer } from "../../util/http.ts";
4
4
  import type { CloudflareApi } from "../api.ts";
5
- import {
6
- Self,
7
- type Binding,
8
- type Bindings,
9
- type WorkerBindingService,
10
- type WorkerBindingSpec,
5
+ import type {
6
+ Binding,
7
+ Bindings,
8
+ WorkerBindingService,
9
+ WorkerBindingSpec,
11
10
  } from "../bindings.ts";
12
11
  import { isQueueEventSource, type EventSource } from "../event-source.ts";
13
12
  import type { WorkerBundle, WorkerBundleSource } from "../worker-bundle.ts";
@@ -25,11 +24,11 @@ export interface MiniflareWorkerInput {
25
24
  assets: AssetsConfig | undefined;
26
25
  bundle: WorkerBundleSource;
27
26
  port: number | undefined;
27
+ tunnel: boolean | undefined;
28
28
  }
29
29
 
30
30
  type RemoteOnlyBindingType =
31
31
  | "ai"
32
- | "ai_gateway"
33
32
  | "browser"
34
33
  | "dispatch_namespace"
35
34
  | "mtls_certificate"
@@ -92,13 +91,18 @@ export const buildWorkerOptions = async (
92
91
  (options.bindings ??= {})[key] = binding;
93
92
  continue;
94
93
  }
95
- if (binding === Self) {
94
+ if (binding.type === "cloudflare::Worker::Self") {
96
95
  (options.serviceBindings ??= {})[key] = miniflare.kCurrentWorker;
97
96
  continue;
98
97
  }
99
98
  switch (binding.type) {
100
- case "ai":
101
- case "ai_gateway": {
99
+ case "ai": {
100
+ const existing = remoteBindings.find((b) => b.type === "ai");
101
+ if (existing) {
102
+ throw new Error(
103
+ `Workers cannot have multiple AI bindings. Binding "${key}" conflicts with "${existing.name}".`,
104
+ );
105
+ }
102
106
  remoteBindings.push({
103
107
  type: "ai",
104
108
  name: key,
@@ -180,7 +184,9 @@ export const buildWorkerOptions = async (
180
184
  break;
181
185
  }
182
186
  case "hyperdrive": {
183
- (options.hyperdrives ??= {})[key] = binding.dev.origin.unencrypted;
187
+ if (binding.dev) {
188
+ (options.hyperdrives ??= {})[key] = binding.dev.origin.unencrypted;
189
+ }
184
190
  break;
185
191
  }
186
192
  case "images": {
@@ -459,7 +465,10 @@ const normalizeBundle = (bundle: WorkerBundle) => {
459
465
  };
460
466
 
461
467
  const isRemoteBinding = (binding: Binding) => {
462
- if (typeof binding === "string" || binding === Self) {
468
+ if (
469
+ typeof binding === "string" ||
470
+ binding.type === "cloudflare::Worker::Self"
471
+ ) {
463
472
  return false;
464
473
  }
465
474
  return (
@@ -2,11 +2,11 @@ import crypto from "node:crypto";
2
2
  import type { RmDirOptions } from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "pathe";
5
+ import type { Scope } from "../../scope.ts";
5
6
  import type { DurableObjectNamespace } from "../durable-object-namespace.ts";
6
7
  import type { Workflow } from "../workflow.ts";
7
- import { DEFAULT_PERSIST_PATH } from "./paths.ts";
8
+ import { getDefaultPersistPath } from "./paths.ts";
8
9
 
9
- const PERSIST_ROOT = path.resolve(DEFAULT_PERSIST_PATH);
10
10
  const BINDING_TYPE_KEY = {
11
11
  d1: "miniflare-D1DatabaseObject",
12
12
  kv: "miniflare-KVNamespaceObject",
@@ -18,8 +18,14 @@ type BindingType = keyof typeof BINDING_TYPE_KEY;
18
18
  /**
19
19
  * Delete a binding from the miniflare directory.
20
20
  */
21
- export async function deleteMiniflareBinding(type: BindingType, id: string) {
22
- const bindingPath = path.join(PERSIST_ROOT, type, BINDING_TYPE_KEY[type]);
21
+ export async function deleteMiniflareBinding(
22
+ scope: Scope,
23
+ type: BindingType,
24
+ id: string,
25
+ ) {
26
+ const persistRoot = path.resolve(getDefaultPersistPath(scope.rootDir));
27
+
28
+ const bindingPath = path.join(persistRoot, type, BINDING_TYPE_KEY[type]);
23
29
 
24
30
  // delete durable object/sqlite files
25
31
  const namespaceId = durableObjectNamespaceIdFromName(
@@ -33,29 +39,31 @@ export async function deleteMiniflareBinding(type: BindingType, id: string) {
33
39
  await Promise.all(files.map((file) => fs.rm(path.join(bindingPath, file))));
34
40
 
35
41
  // delete data directory, if present
36
- await removeDirectory(path.join(PERSIST_ROOT, type, id), {
42
+ await removeDirectory(path.join(persistRoot, type, id), {
37
43
  recursive: true,
38
44
  });
39
45
 
40
46
  // delete the miniflare directory if it is empty
41
- await cleanMiniflareDirectory();
47
+ await cleanMiniflareDirectory(scope.dotAlchemy);
42
48
  }
43
49
 
44
50
  /**
45
51
  * Delete local durable object and workflow data for a given Worker script.
46
52
  */
47
53
  export async function deleteMiniflareWorkerData(
54
+ scope: Scope,
48
55
  scriptName: string,
49
56
  input: {
50
57
  durableObjects: DurableObjectNamespace[];
51
58
  workflows: Workflow[];
52
59
  },
53
60
  ) {
61
+ const persistRoot = path.resolve(getDefaultPersistPath(scope.rootDir));
54
62
  await Promise.all(
55
63
  input.durableObjects.map((durableObject) =>
56
64
  removeDirectory(
57
65
  path.join(
58
- PERSIST_ROOT,
66
+ persistRoot,
59
67
  "do",
60
68
  `${scriptName}-${durableObject.className}`,
61
69
  ),
@@ -69,7 +77,7 @@ export async function deleteMiniflareWorkerData(
69
77
  input.workflows.map((workflow) =>
70
78
  removeDirectory(
71
79
  path.join(
72
- PERSIST_ROOT,
80
+ persistRoot,
73
81
  "workflows",
74
82
  `miniflare-workflows-${workflow.workflowName}`,
75
83
  ),
@@ -79,14 +87,14 @@ export async function deleteMiniflareWorkerData(
79
87
  ),
80
88
  ),
81
89
  );
82
- await cleanMiniflareDirectory();
90
+ await cleanMiniflareDirectory(persistRoot);
83
91
  }
84
92
 
85
93
  /**
86
94
  * Delete the miniflare directory iff it is empty.
87
95
  */
88
- async function cleanMiniflareDirectory() {
89
- const directory = path.resolve(".alchemy/miniflare");
96
+ async function cleanMiniflareDirectory(dotAlchemy: string) {
97
+ const directory = path.resolve(dotAlchemy, "miniflare");
90
98
  const files = await fs
91
99
  .readdir(directory, {
92
100
  recursive: true,
@@ -1,6 +1,7 @@
1
1
  import * as miniflare from "miniflare";
2
2
  import assert from "node:assert";
3
3
  import path from "node:path";
4
+ import { Scope } from "../../scope.ts";
4
5
  import { findOpenPort } from "../../util/find-open-port.ts";
5
6
  import type { HTTPServer } from "../../util/http.ts";
6
7
  import { logger } from "../../util/logger.ts";
@@ -9,8 +10,12 @@ import {
9
10
  buildWorkerOptions,
10
11
  type MiniflareWorkerInput,
11
12
  } from "./build-worker-options.ts";
12
- import { MiniflareWorkerProxy } from "./miniflare-worker-proxy.ts";
13
- import { DEFAULT_PERSIST_PATH } from "./paths.ts";
13
+ import {
14
+ createMiniflareWorkerProxy,
15
+ type MiniflareWorkerProxy,
16
+ } from "./miniflare-worker-proxy.ts";
17
+ import { getDefaultPersistPath } from "./paths.ts";
18
+ import { createTunnel, type Tunnel } from "./tunnel.ts";
14
19
 
15
20
  declare global {
16
21
  var ALCHEMY_MINIFLARE_CONTROLLER: MiniflareController | undefined;
@@ -20,13 +25,14 @@ export class MiniflareController {
20
25
  abort = new AbortController();
21
26
  miniflare: miniflare.Miniflare | undefined;
22
27
  options = new Map<string, miniflare.WorkerOptions>();
28
+ tunnel: Tunnel | undefined;
23
29
  localProxies = new Map<string, MiniflareWorkerProxy>();
24
30
  remoteProxies = new Map<string, HTTPServer>();
25
31
  mutex = new AsyncMutex();
26
32
 
27
33
  static get singleton() {
28
- globalThis.ALCHEMY_MINIFLARE_CONTROLLER ??= new MiniflareController();
29
- return globalThis.ALCHEMY_MINIFLARE_CONTROLLER;
34
+ return (globalThis.ALCHEMY_MINIFLARE_CONTROLLER ??=
35
+ new MiniflareController());
30
36
  }
31
37
 
32
38
  async add(input: MiniflareWorkerInput) {
@@ -39,21 +45,32 @@ export class MiniflareController {
39
45
  assert(first.value, "First value is undefined");
40
46
  this.options.set(input.name, first.value);
41
47
  const miniflare = await this.update();
42
- const proxy = new MiniflareWorkerProxy({
43
- name: input.name,
44
- port: input.port ?? (await findOpenPort()),
45
- miniflare,
46
- });
47
- this.localProxies.set(input.name, proxy);
48
+ let url: URL;
49
+ if (input.tunnel) {
50
+ this.tunnel ??= await createTunnel(miniflare);
51
+ url = await this.tunnel.configureWorker({
52
+ api: input.api,
53
+ name: input.name,
54
+ });
55
+ } else {
56
+ const proxy = await createMiniflareWorkerProxy({
57
+ port: input.port ?? (await findOpenPort()),
58
+ getWorkerName: () => input.name,
59
+ miniflare,
60
+ mode: "local",
61
+ });
62
+ this.localProxies.set(input.name, proxy);
63
+ url = proxy.url;
64
+ }
48
65
  void this.watch(input.id, watcher);
49
66
  logger.task(input.id, {
50
- message: `Ready at ${proxy.url}`,
67
+ message: `Ready at ${url}`,
51
68
  status: "success",
52
69
  resource: input.id,
53
70
  prefix: "dev",
54
71
  prefixColor: "cyanBright",
55
72
  });
56
- return proxy.url;
73
+ return url.toString();
57
74
  }
58
75
 
59
76
  private async watch(
@@ -77,12 +94,13 @@ export class MiniflareController {
77
94
  return await this.mutex.lock(async () => {
78
95
  const options: miniflare.MiniflareOptions = {
79
96
  workers: [],
80
- defaultPersistRoot: path.resolve(DEFAULT_PERSIST_PATH),
97
+ defaultPersistRoot: path.resolve(
98
+ getDefaultPersistPath(Scope.current.rootDir),
99
+ ),
81
100
  unsafeDevRegistryPath: miniflare.getDefaultDevRegistryPath(),
82
101
  log: process.env.DEBUG
83
102
  ? new miniflare.Log(miniflare.LogLevel.DEBUG)
84
103
  : undefined,
85
-
86
104
  // This is required to allow websites and other separate processes
87
105
  // to detect Alchemy-managed Durable Objects via the Wrangler dev registry.
88
106
  unsafeDevRegistryDurableObjectProxy: true,
@@ -143,6 +161,7 @@ export class MiniflareController {
143
161
  this.abort.abort();
144
162
  await Promise.all([
145
163
  this.miniflare?.dispose(),
164
+ this.tunnel?.close(),
146
165
  ...this.localProxies.values().map((proxy) => proxy.close()),
147
166
  ...this.remoteProxies.values().map((proxy) => proxy.close()),
148
167
  ]);