alchemy 0.74.1 → 0.75.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 (206) hide show
  1. package/README.md +1 -1
  2. package/bin/alchemy.js +1760 -1725
  3. package/bin/alchemy.ts +1 -1
  4. package/bin/commands/create.ts +5 -4
  5. package/bin/commands/init.ts +109 -65
  6. package/bin/constants.ts +2 -2
  7. package/bin/services/dependencies.ts +4 -4
  8. package/bin/services/execute-alchemy.ts +1 -1
  9. package/bin/services/find-workspace-root.ts +1 -1
  10. package/bin/services/get-package-version.ts +4 -4
  11. package/bin/services/github-workflow.ts +7 -7
  12. package/bin/services/template-manager.ts +20 -13
  13. package/bin/services/vibe-rules.ts +6 -5
  14. package/bin/trpc.ts +7 -0
  15. package/bin/types.ts +1 -0
  16. package/lib/alchemy.d.ts.map +1 -1
  17. package/lib/alchemy.js +29 -1
  18. package/lib/alchemy.js.map +1 -1
  19. package/lib/auth.js +1 -1
  20. package/lib/auth.js.map +1 -1
  21. package/lib/cloudflare/astro/astro.js +1 -1
  22. package/lib/cloudflare/astro/astro.js.map +1 -1
  23. package/lib/cloudflare/bun-spa/bun-spa.d.ts +1 -1
  24. package/lib/cloudflare/bun-spa/bun-spa.d.ts.map +1 -1
  25. package/lib/cloudflare/bun-spa/bun-spa.js +10 -11
  26. package/lib/cloudflare/bun-spa/bun-spa.js.map +1 -1
  27. package/lib/cloudflare/bun-spa/get-bun-spa-backend.d.ts +11 -0
  28. package/lib/cloudflare/bun-spa/get-bun-spa-backend.d.ts.map +1 -0
  29. package/lib/cloudflare/bun-spa/get-bun-spa-backend.js +26 -0
  30. package/lib/cloudflare/bun-spa/get-bun-spa-backend.js.map +1 -0
  31. package/lib/cloudflare/bun-spa/index.d.ts +2 -0
  32. package/lib/cloudflare/bun-spa/index.d.ts.map +1 -0
  33. package/lib/cloudflare/bun-spa/index.js +2 -0
  34. package/lib/cloudflare/bun-spa/index.js.map +1 -0
  35. package/lib/cloudflare/bundle/internal-worker-bundle.js +1 -1
  36. package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
  37. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js +1 -1
  38. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -1
  39. package/lib/cloudflare/bundle/plugin-node-compat.js +1 -1
  40. package/lib/cloudflare/bundle/plugin-node-compat.js.map +1 -1
  41. package/lib/cloudflare/bundle/plugin-wasm.js +1 -1
  42. package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
  43. package/lib/cloudflare/d1-migrations.js +1 -1
  44. package/lib/cloudflare/d1-migrations.js.map +1 -1
  45. package/lib/cloudflare/kv-namespace.d.ts +5 -0
  46. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  47. package/lib/cloudflare/kv-namespace.js +44 -19
  48. package/lib/cloudflare/kv-namespace.js.map +1 -1
  49. package/lib/cloudflare/miniflare/miniflare-controller.js +1 -1
  50. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  51. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  52. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +34 -4
  53. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  54. package/lib/cloudflare/miniflare/paths.d.ts.map +1 -1
  55. package/lib/cloudflare/miniflare/paths.js +2 -1
  56. package/lib/cloudflare/miniflare/paths.js.map +1 -1
  57. package/lib/cloudflare/miniflare/symlink-miniflare-state.js +1 -1
  58. package/lib/cloudflare/miniflare/symlink-miniflare-state.js.map +1 -1
  59. package/lib/cloudflare/next.js +1 -1
  60. package/lib/cloudflare/next.js.map +1 -1
  61. package/lib/cloudflare/nuxt/nuxt.js +1 -1
  62. package/lib/cloudflare/nuxt/nuxt.js.map +1 -1
  63. package/lib/cloudflare/react-router/plugin.d.ts.map +1 -1
  64. package/lib/cloudflare/react-router/plugin.js +2 -1
  65. package/lib/cloudflare/react-router/plugin.js.map +1 -1
  66. package/lib/cloudflare/react-router/react-router.js +1 -1
  67. package/lib/cloudflare/react-router/react-router.js.map +1 -1
  68. package/lib/cloudflare/redwood/redwood.js +1 -1
  69. package/lib/cloudflare/redwood/redwood.js.map +1 -1
  70. package/lib/cloudflare/sveltekit/sveltekit.js +1 -1
  71. package/lib/cloudflare/sveltekit/sveltekit.js.map +1 -1
  72. package/lib/cloudflare/tanstack-start/tanstack-start.js +1 -1
  73. package/lib/cloudflare/tanstack-start/tanstack-start.js.map +1 -1
  74. package/lib/cloudflare/tunnel.d.ts.map +1 -1
  75. package/lib/cloudflare/tunnel.js +25 -1
  76. package/lib/cloudflare/tunnel.js.map +1 -1
  77. package/lib/cloudflare/vite/plugin.js +1 -1
  78. package/lib/cloudflare/vite/plugin.js.map +1 -1
  79. package/lib/cloudflare/website.js +1 -1
  80. package/lib/cloudflare/website.js.map +1 -1
  81. package/lib/cloudflare/worker.d.ts.map +1 -1
  82. package/lib/cloudflare/worker.js +1 -1
  83. package/lib/cloudflare/worker.js.map +1 -1
  84. package/lib/cloudflare/wrangler.json.js +1 -1
  85. package/lib/cloudflare/wrangler.json.js.map +1 -1
  86. package/lib/coinbase/faucet.js +2 -2
  87. package/lib/coinbase/faucet.js.map +1 -1
  88. package/lib/docker/api.js +1 -1
  89. package/lib/docker/api.js.map +1 -1
  90. package/lib/docker/image.js +1 -1
  91. package/lib/docker/image.js.map +1 -1
  92. package/lib/docker/volume.d.ts.map +1 -1
  93. package/lib/docker/volume.js +3 -2
  94. package/lib/docker/volume.js.map +1 -1
  95. package/lib/esbuild/bundle.js +1 -1
  96. package/lib/esbuild/bundle.js.map +1 -1
  97. package/lib/fs/file.js +1 -1
  98. package/lib/fs/file.js.map +1 -1
  99. package/lib/llms.js +1 -1
  100. package/lib/llms.js.map +1 -1
  101. package/lib/os/exec.js +1 -1
  102. package/lib/os/exec.js.map +1 -1
  103. package/lib/scope.d.ts.map +1 -1
  104. package/lib/scope.js +1 -1
  105. package/lib/scope.js.map +1 -1
  106. package/lib/state/file-system-state-store.js +1 -1
  107. package/lib/state/file-system-state-store.js.map +1 -1
  108. package/lib/state/migrations.js +1 -1
  109. package/lib/state/migrations.js.map +1 -1
  110. package/lib/state/sqlite-state-store.js +1 -1
  111. package/lib/state/sqlite-state-store.js.map +1 -1
  112. package/lib/stripe/file.d.ts +3 -3
  113. package/lib/stripe/file.js +3 -3
  114. package/lib/test/bun.js +1 -1
  115. package/lib/test/bun.js.map +1 -1
  116. package/lib/test/prune.js +1 -1
  117. package/lib/test/prune.js.map +1 -1
  118. package/lib/test/vitest.js +1 -1
  119. package/lib/test/vitest.js.map +1 -1
  120. package/lib/util/content-type.js +1 -1
  121. package/lib/util/content-type.js.map +1 -1
  122. package/lib/util/idempotent-spawn.js +1 -1
  123. package/lib/util/idempotent-spawn.js.map +1 -1
  124. package/lib/util/lock.js +1 -1
  125. package/lib/util/lock.js.map +1 -1
  126. package/package.json +6 -1
  127. package/src/alchemy.ts +56 -1
  128. package/src/auth.ts +1 -1
  129. package/src/cloudflare/astro/astro.ts +1 -1
  130. package/src/cloudflare/bun-spa/bun-spa.ts +13 -13
  131. package/src/cloudflare/bun-spa/get-bun-spa-backend.ts +29 -0
  132. package/src/cloudflare/bun-spa/index.ts +1 -0
  133. package/src/cloudflare/bundle/internal-worker-bundle.ts +1 -1
  134. package/src/cloudflare/bundle/plugin-hybrid-node-compat.ts +1 -1
  135. package/src/cloudflare/bundle/plugin-node-compat.ts +1 -1
  136. package/src/cloudflare/bundle/plugin-wasm.ts +1 -1
  137. package/src/cloudflare/d1-migrations.ts +1 -1
  138. package/src/cloudflare/kv-namespace.ts +52 -20
  139. package/src/cloudflare/miniflare/miniflare-controller.ts +1 -1
  140. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +39 -4
  141. package/src/cloudflare/miniflare/paths.ts +2 -1
  142. package/src/cloudflare/miniflare/symlink-miniflare-state.ts +1 -1
  143. package/src/cloudflare/next.ts +1 -1
  144. package/src/cloudflare/nuxt/nuxt.ts +1 -1
  145. package/src/cloudflare/react-router/plugin.ts +2 -1
  146. package/src/cloudflare/react-router/react-router.ts +1 -1
  147. package/src/cloudflare/redwood/redwood.ts +1 -1
  148. package/src/cloudflare/sveltekit/sveltekit.ts +1 -1
  149. package/src/cloudflare/tanstack-start/tanstack-start.ts +1 -1
  150. package/src/cloudflare/tunnel.ts +36 -1
  151. package/src/cloudflare/vite/plugin.ts +1 -1
  152. package/src/cloudflare/website.ts +1 -1
  153. package/src/cloudflare/worker.ts +1 -3
  154. package/src/cloudflare/wrangler.json.ts +1 -1
  155. package/src/coinbase/faucet.ts +2 -2
  156. package/src/docker/api.ts +1 -1
  157. package/src/docker/image.ts +1 -1
  158. package/src/docker/volume.ts +3 -2
  159. package/src/esbuild/bundle.ts +1 -1
  160. package/src/fs/file.ts +1 -1
  161. package/src/llms.ts +1 -1
  162. package/src/os/exec.ts +1 -1
  163. package/src/scope.ts +1 -1
  164. package/src/state/file-system-state-store.ts +1 -1
  165. package/src/state/migrations.ts +1 -1
  166. package/src/state/sqlite-state-store.ts +1 -1
  167. package/src/stripe/file.ts +3 -3
  168. package/src/test/bun.ts +1 -1
  169. package/src/test/prune.ts +1 -1
  170. package/src/test/vitest.ts +1 -1
  171. package/src/util/content-type.ts +1 -1
  172. package/src/util/idempotent-spawn.ts +1 -1
  173. package/src/util/lock.ts +1 -1
  174. package/templates/astro/alchemy.run.ts +0 -2
  175. package/templates/bun-spa/README.md +94 -0
  176. package/templates/bun-spa/_env +1 -0
  177. package/templates/bun-spa/_env.example +1 -0
  178. package/templates/bun-spa/_gitignore +29 -0
  179. package/templates/bun-spa/alchemy.run.ts +21 -0
  180. package/templates/bun-spa/bun-env.d.ts +26 -0
  181. package/templates/bun-spa/eslint.config.js +27 -0
  182. package/templates/bun-spa/package.json +30 -0
  183. package/templates/bun-spa/src/App.css +46 -0
  184. package/templates/bun-spa/src/App.tsx +54 -0
  185. package/templates/bun-spa/src/assets/logo.svg +1 -0
  186. package/templates/bun-spa/src/assets/react.svg +1 -0
  187. package/templates/bun-spa/src/frontend.tsx +15 -0
  188. package/templates/bun-spa/src/index.css +69 -0
  189. package/templates/bun-spa/src/index.html +14 -0
  190. package/templates/bun-spa/src/worker.ts +51 -0
  191. package/templates/bun-spa/tsconfig.json +24 -0
  192. package/templates/hono/alchemy.run.ts +0 -2
  193. package/templates/nextjs/alchemy.run.ts +0 -2
  194. package/templates/nuxt/alchemy.run.ts +0 -2
  195. package/templates/react-router/alchemy.run.ts +0 -2
  196. package/templates/react-router/package.json +1 -1
  197. package/templates/rwsdk/alchemy.run.ts +0 -1
  198. package/templates/rwsdk/package.json +1 -1
  199. package/templates/sveltekit/alchemy.run.ts +0 -2
  200. package/templates/sveltekit/package.json +1 -1
  201. package/templates/tanstack-start/alchemy.run.ts +0 -2
  202. package/templates/tanstack-start/package.json +1 -1
  203. package/templates/typescript/alchemy.run.ts +0 -2
  204. package/templates/vite/alchemy.run.ts +0 -2
  205. package/templates/vite/package.json +1 -1
  206. package/workers/tunnel-proxy.js +1 -1
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
- import path from "node:path";
2
+ import path from "pathe";
3
3
  import { Scope } from "../../scope.ts";
4
4
  import { exists } from "../../util/exists.ts";
5
5
  import type { Bindings } from "../bindings.ts";
@@ -18,7 +18,7 @@ export interface BunSPAProps<B extends Bindings> extends WebsiteProps<B> {
18
18
  /**
19
19
  * The path to the frontend entrypoints that bun should bundle for deployment & serve in dev mode.
20
20
  * These are usually html files. Glob patterns are supported.
21
- * Typically set to index.html
21
+ * Typically set to src/index.html
22
22
  */
23
23
  frontend: string | string[];
24
24
  outDir?: string;
@@ -70,6 +70,8 @@ export async function BunSPA<B extends Bindings>(
70
70
  }
71
71
 
72
72
  const scope = Scope.current;
73
+ const nodeEnv =
74
+ (props.bindings?.NODE_ENV ?? scope.local) ? "development" : "production";
73
75
  console.log("creating website", outDir);
74
76
  const website = await Website(id, {
75
77
  spa: true,
@@ -77,17 +79,14 @@ export async function BunSPA<B extends Bindings>(
77
79
  bindings: {
78
80
  ...props.bindings,
79
81
  // set NODE_ENV in worker appropriately if not already set
80
- NODE_ENV:
81
- (props.bindings?.NODE_ENV ?? scope.local)
82
- ? "development"
83
- : "production",
82
+ NODE_ENV: nodeEnv,
84
83
  } as unknown as B,
85
84
  assets: {
86
85
  directory: path.resolve(outDir),
87
86
  },
88
87
  build: spreadBuildProps(
89
88
  props,
90
- `bun build '${frontendPaths.join("' '")}' --outdir ${outDir}`,
89
+ `bun build '${frontendPaths.join("' '")}' --target=browser --minify --define:process.env.NODE_ENV='"${nodeEnv}"' --env='BUN_PUBLIC_*' --outdir ${outDir}`,
91
90
  ),
92
91
  });
93
92
 
@@ -129,7 +128,7 @@ export async function BunSPA<B extends Bindings>(
129
128
  ...process.env,
130
129
  NODE_ENV: "development",
131
130
  ALCHEMY_ROOT: Scope.current.rootDir,
132
- PUBLIC_BACKEND_URL: apiUrl,
131
+ BUN_PUBLIC_BACKEND_URL: apiUrl,
133
132
  },
134
133
  });
135
134
  }
@@ -144,8 +143,8 @@ async function validateBunfigToml(cwd: string): Promise<void> {
144
143
  "bunfig.toml is required for BunSPA to work correctly.\n\n" +
145
144
  `Create ${bunfigPath} with the following content:\n\n` +
146
145
  "[serve.static]\n" +
147
- `env='PUBLIC_*'\n\n` +
148
- "This allows Bun to expose PUBLIC_* environment variables to the frontend during development.",
146
+ `env='BUN_PUBLIC_*'\n\n` +
147
+ "This allows Bun to expose BUN_PUBLIC_* environment variables to the frontend during development.",
149
148
  );
150
149
  }
151
150
 
@@ -153,15 +152,16 @@ async function validateBunfigToml(cwd: string): Promise<void> {
153
152
  const config = Bun.TOML.parse(content) as Record<string, any>;
154
153
 
155
154
  const hasServeStatic = config.serve?.static;
156
- const hasEnvConfig = hasServeStatic && config.serve.static.env === "PUBLIC_*";
155
+ const hasEnvConfig =
156
+ hasServeStatic && config.serve.static.env === "BUN_PUBLIC_*";
157
157
 
158
158
  if (!hasServeStatic || !hasEnvConfig) {
159
159
  throw new Error(
160
160
  "bunfig.toml is missing required configuration for BunSPA.\n\n" +
161
161
  `Add the following section to ${bunfigPath}:\n\n` +
162
162
  "[serve.static]\n" +
163
- `env='PUBLIC_*'\n\n` +
164
- "This allows Bun to expose PUBLIC_* environment variables to the frontend during development.",
163
+ `env='BUN_PUBLIC_*'\n\n` +
164
+ "This allows Bun to expose BUN_PUBLIC_* environment variables to the frontend during development.",
165
165
  );
166
166
  }
167
167
  }
@@ -0,0 +1,29 @@
1
+ type GetBackendUrlProps = {
2
+ // If you're using routes with paths in your worker configuration, you'll need to pass the path you want to use here, e.g. "/api"
3
+ routePath?: string;
4
+ };
5
+
6
+ /**
7
+ * Use this function in your frontend code to automatically get the correct URL for your backend API in both local and deployed environments
8
+ * @param param0 { path: string }
9
+ * @returns
10
+ */
11
+ export function getBackendUrl({ routePath }: GetBackendUrlProps = {}): URL {
12
+ let pathToUse = !routePath
13
+ ? "/"
14
+ : routePath.startsWith("/")
15
+ ? routePath
16
+ : `/${routePath}`;
17
+ // This is ugly but it's necessary to support both local and prod environments
18
+ // Start with the current host
19
+ let backendUrl = `${window.location.protocol}//${window.location.host}${pathToUse}`;
20
+ try {
21
+ // Bun will only inline this if we use exactly `process.env.BUN_PUBLIC_BACKEND_URL` it we use other forms including process?.env for example bun will not inline it
22
+ // we can't check typeof process either because process may not be available but Bun may already have inlined process.env.BUN_PUBLIC_BACKEND_URL with the correct value
23
+ backendUrl = process.env.BUN_PUBLIC_BACKEND_URL ?? backendUrl;
24
+ } catch {
25
+ // Bun may not have had anything to inline and process.env may not exist to above can throw an error
26
+ // do nothing
27
+ }
28
+ return new URL(backendUrl);
29
+ }
@@ -0,0 +1 @@
1
+ export * from "./get-bun-spa-backend.ts";
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
- import path from "node:path";
3
+ import path from "pathe";
4
4
  import { memoize } from "../../util/memoize.ts";
5
5
  import type { WorkerBundle } from "../worker-bundle.ts";
6
6
 
@@ -5,7 +5,7 @@
5
5
  import type { Plugin, PluginBuild } from "esbuild";
6
6
  import assert from "node:assert";
7
7
  import { createRequire } from "node:module";
8
- import nodePath from "node:path";
8
+ import nodePath from "pathe";
9
9
  import { dedent } from "../../util/dedent.ts";
10
10
  import { NODEJS_MODULES_RE } from "./nodejs-builtin-modules.ts";
11
11
 
@@ -1,6 +1,6 @@
1
1
  import type { Plugin } from "esbuild";
2
2
  import type { NodeJSCompatMode } from "miniflare";
3
- import { relative } from "node:path";
3
+ import { relative } from "pathe";
4
4
  import pc from "picocolors";
5
5
  import { dedent } from "../../util/dedent.ts";
6
6
  import { logger } from "../../util/logger.ts";
@@ -1,7 +1,7 @@
1
1
  import type esbuild from "esbuild";
2
2
  import assert from "node:assert";
3
3
  import fs from "node:fs/promises";
4
- import path from "node:path";
4
+ import path from "pathe";
5
5
  import type { WorkerBundle } from "../worker-bundle.ts";
6
6
 
7
7
  export function createWasmPlugin() {
@@ -1,6 +1,6 @@
1
1
  import { glob } from "glob";
2
2
  import * as fs from "node:fs/promises";
3
- import * as path from "node:path";
3
+ import path from "pathe";
4
4
  import { logger } from "../util/logger.ts";
5
5
  import { handleApiError } from "./api-error.ts";
6
6
  import type { CloudflareApi } from "./api.ts";
@@ -13,6 +13,8 @@ import {
13
13
  type CloudflareApiOptions,
14
14
  } from "./api.ts";
15
15
  import { deleteMiniflareBinding } from "./miniflare/delete.ts";
16
+ import * as mf from "miniflare";
17
+ import { getDefaultPersistPath } from "./miniflare/paths.ts";
16
18
 
17
19
  /**
18
20
  * Properties for creating or updating a KV Namespace
@@ -220,6 +222,13 @@ const _KVNamespace = Resource(
220
222
  };
221
223
 
222
224
  if (local) {
225
+ if (props.values) {
226
+ await insertLocalKVRecords({
227
+ namespaceId: dev.id,
228
+ values: props.values,
229
+ rootDir: this.scope.rootDir,
230
+ });
231
+ }
223
232
  return {
224
233
  type: "kv_namespace",
225
234
  namespaceId: this.output?.namespaceId ?? "",
@@ -340,31 +349,18 @@ export async function insertKVRecords(
340
349
  const BATCH_SIZE = 10000;
341
350
 
342
351
  for (let i = 0; i < props.values.length; i += BATCH_SIZE) {
343
- const batch = props.values.slice(i, i + BATCH_SIZE);
344
-
345
- const bulkPayload = batch.map((entry) => {
346
- const item: any = {
352
+ const bulkPayload = props.values
353
+ .slice(i, i + BATCH_SIZE)
354
+ .map((entry) => ({
347
355
  key: entry.key,
348
356
  value:
349
357
  typeof entry.value === "string"
350
358
  ? entry.value
351
359
  : JSON.stringify(entry.value),
352
- };
353
-
354
- if (entry.expiration) {
355
- item.expiration = entry.expiration;
356
- }
357
-
358
- if (entry.expirationTtl) {
359
- item.expiration_ttl = entry.expirationTtl;
360
- }
361
-
362
- if (entry.metadata) {
363
- item.metadata = entry.metadata;
364
- }
365
-
366
- return item;
367
- });
360
+ expiration: entry.expiration,
361
+ expiration_ttl: entry.expirationTtl,
362
+ metadata: entry.metadata,
363
+ }));
368
364
 
369
365
  await withExponentialBackoff(
370
366
  async () => {
@@ -397,6 +393,42 @@ export async function insertKVRecords(
397
393
  }
398
394
  }
399
395
 
396
+ export async function insertLocalKVRecords(input: {
397
+ namespaceId: string;
398
+ values: KVPair[];
399
+ rootDir: string;
400
+ }) {
401
+ const miniflare = new mf.Miniflare({
402
+ script: "",
403
+ modules: true,
404
+ defaultPersistRoot: getDefaultPersistPath(input.rootDir),
405
+ kvNamespaces: { KV: input.namespaceId },
406
+ kvPersist: true,
407
+ log: process.env.DEBUG ? new mf.Log(mf.LogLevel.DEBUG) : undefined,
408
+ });
409
+ try {
410
+ await miniflare.ready;
411
+ const kv = await miniflare.getKVNamespace("KV");
412
+ await Promise.all(
413
+ input.values.map((record) =>
414
+ kv.put(
415
+ record.key,
416
+ typeof record.value === "string"
417
+ ? record.value
418
+ : JSON.stringify(record.value),
419
+ {
420
+ expiration: record.expiration,
421
+ expirationTtl: record.expirationTtl,
422
+ metadata: record.metadata,
423
+ },
424
+ ),
425
+ ),
426
+ );
427
+ } finally {
428
+ await miniflare.dispose();
429
+ }
430
+ }
431
+
400
432
  /**
401
433
  * Interface representing a KV namespace as returned by Cloudflare API
402
434
  */
@@ -1,6 +1,6 @@
1
1
  import * as miniflare from "miniflare";
2
2
  import assert from "node:assert";
3
- import path from "node:path";
3
+ import path from "pathe";
4
4
  import { Scope } from "../../scope.ts";
5
5
  import { findOpenPort } from "../../util/find-open-port.ts";
6
6
  import type { HTTPServer } from "../../util/http.ts";
@@ -1,9 +1,11 @@
1
1
  import * as miniflare from "miniflare";
2
+ import assert from "node:assert";
2
3
  import { once } from "node:events";
3
4
  import http from "node:http";
4
5
  import { Readable } from "node:stream";
5
6
  import { WebSocket, WebSocketServer } from "ws";
6
7
  import { createUpgradeHandler } from "../../util/http.ts";
8
+ import { logger } from "../../util/logger.ts";
7
9
 
8
10
  export interface MiniflareWorkerProxy {
9
11
  url: URL;
@@ -90,12 +92,10 @@ export async function createMiniflareWorkerProxy(options: {
90
92
  });
91
93
  };
92
94
 
93
- server.listen(options.port);
94
- await once(server, "listening");
95
- const url = new URL(`http://localhost:${options.port}`);
95
+ await listen(server, options.port);
96
96
 
97
97
  return {
98
- url,
98
+ url: getAddress(server),
99
99
  close: async () => {
100
100
  // If we await this while the `wss` server is open, the server will hang.
101
101
  server.close();
@@ -103,6 +103,41 @@ export async function createMiniflareWorkerProxy(options: {
103
103
  };
104
104
  }
105
105
 
106
+ /**
107
+ * Listens on the given port, retrying on the next port if the port is already in use.
108
+ */
109
+ async function listen(server: http.Server, port: number) {
110
+ try {
111
+ server.listen(port);
112
+ await once(server, "listening");
113
+ } catch (error) {
114
+ if (
115
+ typeof error === "object" &&
116
+ error !== null &&
117
+ "code" in error &&
118
+ error.code === "EADDRINUSE"
119
+ ) {
120
+ logger.warn(`Port ${port} is already in use, trying ${port + 1}...`);
121
+ await listen(server, port + 1);
122
+ } else {
123
+ throw error;
124
+ }
125
+ }
126
+ }
127
+
128
+ function getAddress(server: http.Server) {
129
+ const address = server.address();
130
+ assert(
131
+ address &&
132
+ typeof address === "object" &&
133
+ "address" in address &&
134
+ "port" in address,
135
+ "Server is not listening",
136
+ );
137
+ const hostname = address.address === "::" ? "localhost" : address.address;
138
+ return new URL(`http://${hostname}:${address.port}`);
139
+ }
140
+
106
141
  interface RequestInfo {
107
142
  method: string;
108
143
  url: URL;
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ // biome-ignore lint/style/noRestrictedImports: I/O is acceptable here
2
3
  import { existsSync, readlinkSync, statSync } from "node:fs";
3
- import path from "node:path";
4
+ import path from "pathe";
4
5
  import { ALCHEMY_ROOT } from "../../util/root-dir.ts";
5
6
 
6
7
  const dynamicImportContext = new AsyncLocalStorage<boolean>();
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
- import path from "node:path";
2
+ import path from "pathe";
3
3
  import { getDefaultPersistPath } from "./paths.ts";
4
4
 
5
5
  export async function writeMiniflareSymlink(rootDir: string, cwd: string) {
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
- import path from "node:path";
2
+ import path from "pathe";
3
3
  import pc from "picocolors";
4
4
  import { getPackageManagerRunner } from "../util/detect-package-manager.ts";
5
5
  import { exists } from "../util/exists.ts";
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import path from "pathe";
2
2
  import { getPackageManagerRunner } from "../../util/detect-package-manager.ts";
3
3
  import type { Assets } from "../assets.ts";
4
4
  import type { Bindings } from "../bindings.ts";
@@ -1,6 +1,7 @@
1
1
  import type { PluginConfig } from "@cloudflare/vite-plugin";
2
+ // biome-ignore lint/style/noRestrictedImports: I/O is acceptable here
2
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
- import { dirname } from "node:path";
4
+ import { dirname } from "pathe";
4
5
  import { getDefaultConfigPath } from "../miniflare/paths.ts";
5
6
  import alchemyVite from "../vite/plugin.ts";
6
7
 
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import path from "pathe";
2
2
  import { getPackageManagerRunner } from "../../util/detect-package-manager.ts";
3
3
  import type { Assets } from "../assets.ts";
4
4
  import type { Bindings } from "../bindings.ts";
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import path from "pathe";
2
2
  import type { Assets } from "../assets.ts";
3
3
  import type { Bindings } from "../bindings.ts";
4
4
  import { Vite, type ViteProps } from "../vite/vite.ts";
@@ -1,4 +1,4 @@
1
- import { join } from "node:path";
1
+ import { join } from "pathe";
2
2
  import type { Assets } from "../assets.ts";
3
3
  import type { Bindings } from "../bindings.ts";
4
4
  import { Vite, type ViteProps } from "../vite/vite.ts";
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import path from "pathe";
2
2
  import { exists } from "../../util/exists.ts";
3
3
  import type { Assets } from "../assets.ts";
4
4
  import type { Bindings } from "../bindings.ts";
@@ -494,6 +494,9 @@ export const Tunnel = Resource(
494
494
  const name =
495
495
  props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
496
496
 
497
+ // Track if we're replacing the tunnel (don't fetch token in this case)
498
+ let isReplacing = false;
499
+
497
500
  if (this.phase === "update" && this.output.name !== name) {
498
501
  console.log("replacing tunnel", this.output.name, name);
499
502
  this.replace(true);
@@ -563,7 +566,6 @@ export const Tunnel = Resource(
563
566
  (error.message.includes("already have a tunnel with this name") ||
564
567
  error.message.includes("already exists"))
565
568
  ) {
566
- console.log(error);
567
569
  logger.log(`Tunnel '${name}' already exists, adopting it`);
568
570
 
569
571
  // Find the existing tunnel by name
@@ -596,6 +598,12 @@ export const Tunnel = Resource(
596
598
  }
597
599
  }
598
600
 
601
+ // Ensure tunnel token is available (unless we're replacing)
602
+ // Sometimes Cloudflare doesn't return the token in the initial response
603
+ if (!isReplacing && !tunnelData.token) {
604
+ tunnelData.token = await getTunnelToken(api, tunnelData.id);
605
+ }
606
+
599
607
  // Handle DNS records for ingress hostnames
600
608
  let dnsRecords = this.output?.dnsRecords || {};
601
609
 
@@ -918,3 +926,30 @@ export async function findTunnelByName(
918
926
  // No matching tunnel found
919
927
  return null;
920
928
  }
929
+
930
+ /**
931
+ * Get tunnel token
932
+ */
933
+ async function getTunnelToken(
934
+ api: CloudflareApi,
935
+ tunnelId: string,
936
+ ): Promise<string> {
937
+ const response = await api.get(
938
+ `/accounts/${api.accountId}/cfd_tunnel/${tunnelId}/token`,
939
+ );
940
+
941
+ if (!response.ok) {
942
+ logger.error(
943
+ `Failed to fetch token for tunnel ${tunnelId}: ${response.status} ${response.statusText}`,
944
+ );
945
+ await handleApiError(response, "get token", "tunnel", tunnelId);
946
+ }
947
+
948
+ const data = (await response.json()) as CloudflareApiResponse<string>;
949
+ if (!data.result) {
950
+ logger.warn(`Could not retrieve token for tunnel ${tunnelId}`);
951
+ } else {
952
+ logger.log(`Successfully fetched token for tunnel ${tunnelId}`);
953
+ }
954
+ return data.result;
955
+ }
@@ -1,5 +1,5 @@
1
1
  import { cloudflare, type PluginConfig } from "@cloudflare/vite-plugin";
2
- import path from "node:path";
2
+ import path from "pathe";
3
3
  import type { PluginOption } from "vite";
4
4
  import {
5
5
  getDefaultConfigPath,
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import fs from "node:fs/promises";
3
- import path from "node:path";
3
+ import path from "pathe";
4
4
  import { Exec } from "../os/index.ts";
5
5
  import { Scope } from "../scope.ts";
6
6
  import { dedent } from "../util/dedent.ts";
@@ -34,6 +34,7 @@ import {
34
34
  } from "./durable-object-namespace.ts";
35
35
  import { type EventSource, isQueueEventSource } from "./event-source.ts";
36
36
  import { deleteMiniflareWorkerData } from "./miniflare/delete.ts";
37
+ import { MiniflareController } from "./miniflare/miniflare-controller.ts";
37
38
  import {
38
39
  QueueConsumer,
39
40
  deleteQueueConsumer,
@@ -966,9 +967,6 @@ const _Worker = Resource(
966
967
  if (props.dev?.url) {
967
968
  url = props.dev.url;
968
969
  } else {
969
- const { MiniflareController } = await import(
970
- "./miniflare/miniflare-controller.js"
971
- );
972
970
  const controller = MiniflareController.singleton;
973
971
  url = await controller.add({
974
972
  api,
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
- import path from "node:path";
2
+ import path from "pathe";
3
3
  import type { Context } from "../context.ts";
4
4
  import { Resource } from "../resource.ts";
5
5
  import { Scope } from "../scope.ts";
@@ -8,9 +8,9 @@
8
8
  * bun node_modules/alchemy/src/coinbase/faucet.ts backend/dev # Fund specific scope
9
9
  */
10
10
 
11
- import { readFile, access } from "node:fs/promises";
12
- import { join } from "node:path";
13
11
  import { glob } from "glob";
12
+ import { access, readFile } from "node:fs/promises";
13
+ import { join } from "pathe";
14
14
  import { createCdpClient } from "./client.ts";
15
15
  import type { EvmAccount } from "./evm-account.ts";
16
16
  import type { EvmSmartAccount } from "./evm-smart-account.ts";
package/src/docker/api.ts CHANGED
@@ -2,7 +2,7 @@ import { execa } from "execa";
2
2
  import { Buffer } from "node:buffer";
3
3
  import { spawn } from "node:child_process";
4
4
  import fs from "node:fs/promises";
5
- import path from "node:path";
5
+ import path from "pathe";
6
6
  import type { Duration, HealthcheckConfig } from "./container.ts";
7
7
 
8
8
  /**
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
- import path from "node:path";
3
+ import path from "pathe";
4
4
  import type { Context } from "../context.ts";
5
5
  import { Resource } from "../resource.ts";
6
6
  import type { Secret } from "../secret.ts";
@@ -135,13 +135,13 @@ export const Volume = Resource(
135
135
  return this.destroy();
136
136
  } else {
137
137
  // Set default driver if not provided
138
- props.driver = props.driver || "local";
138
+ const driver = props.driver || "local";
139
139
  const driverOpts = props.driverOpts || {};
140
140
 
141
141
  // Create the volume
142
142
  const volumeId = await api.createVolume(
143
143
  volumeName,
144
- props.driver,
144
+ driver,
145
145
  driverOpts,
146
146
  processedLabels,
147
147
  );
@@ -152,6 +152,7 @@ export const Volume = Resource(
152
152
 
153
153
  return {
154
154
  ...props,
155
+ driver: driver,
155
156
  id: volumeId,
156
157
  name: volumeName,
157
158
  mountpoint,
@@ -1,7 +1,7 @@
1
1
  import type esbuild from "esbuild";
2
2
  import crypto from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
- import path from "node:path";
4
+ import path from "pathe";
5
5
  import type { Context } from "../context.ts";
6
6
  import { Resource } from "../resource.ts";
7
7
  import { logger } from "../util/logger.ts";
package/src/fs/file.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
2
+ import path from "pathe";
3
3
  import type { Context } from "../context.ts";
4
4
  import { Resource } from "../resource.ts";
5
5
  import { ignore } from "../util/ignore.ts";
package/src/llms.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
- import path from "node:path";
2
+ import path from "pathe";
3
3
 
4
4
  export interface AlchemyRule {
5
5
  name: string;
package/src/os/exec.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn, type SpawnOptions } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { join } from "node:path";
3
+ import { join } from "pathe";
4
4
  import type { Context } from "../context.ts";
5
5
  import { Resource } from "../resource.ts";
6
6
  import type { Secret } from "../secret.ts";
package/src/scope.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import path from "node:path";
3
2
  import util from "node:util";
3
+ import path from "pathe";
4
4
  import pc from "picocolors";
5
5
  import type { Phase } from "./alchemy.ts";
6
6
  import { destroy, destroyAll, DestroyStrategy } from "./destroy.ts";
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
2
+ import path from "pathe";
3
3
  import { ResourceScope } from "../resource.ts";
4
4
  import type { Scope } from "../scope.ts";
5
5
  import { deserialize, serialize } from "../serde.ts";
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import path from "pathe";
2
2
 
3
3
  export const MIGRATIONS_DIRECTORY = path.join(
4
4
  import.meta.dirname,
@@ -1,6 +1,6 @@
1
1
  import type { Config as LibSQLConfig } from "@libsql/client";
2
2
  import fs from "node:fs/promises";
3
- import path from "node:path";
3
+ import path from "pathe";
4
4
  import type { Scope } from "../scope.ts";
5
5
  import { exists } from "../util/exists.ts";
6
6
  import { memoize } from "../util/memoize.ts";