alchemy 0.47.0 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/bin/alchemy.mjs +63 -56
  2. package/lib/alchemy.d.ts +9 -3
  3. package/lib/alchemy.d.ts.map +1 -1
  4. package/lib/alchemy.js +19 -39
  5. package/lib/alchemy.js.map +1 -1
  6. package/lib/cloudflare/astro.d.ts.map +1 -1
  7. package/lib/cloudflare/astro.js +3 -0
  8. package/lib/cloudflare/astro.js.map +1 -1
  9. package/lib/cloudflare/compatibility-date.gen.d.ts +5 -0
  10. package/lib/cloudflare/compatibility-date.gen.d.ts.map +1 -0
  11. package/lib/cloudflare/compatibility-date.gen.js +7 -0
  12. package/lib/cloudflare/compatibility-date.gen.js.map +1 -0
  13. package/lib/cloudflare/container.js +1 -1
  14. package/lib/cloudflare/container.js.map +1 -1
  15. package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
  16. package/lib/cloudflare/d1-migrations.js +6 -11
  17. package/lib/cloudflare/d1-migrations.js.map +1 -1
  18. package/lib/cloudflare/index.d.ts +1 -0
  19. package/lib/cloudflare/index.d.ts.map +1 -1
  20. package/lib/cloudflare/index.js +1 -0
  21. package/lib/cloudflare/index.js.map +1 -1
  22. package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts +2 -2
  23. package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts.map +1 -1
  24. package/lib/cloudflare/miniflare/miniflare-worker-options.js +11 -2
  25. package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +1 -1
  26. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +20 -0
  27. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -0
  28. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +80 -0
  29. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -0
  30. package/lib/cloudflare/miniflare/miniflare.d.ts +6 -6
  31. package/lib/cloudflare/miniflare/miniflare.d.ts.map +1 -1
  32. package/lib/cloudflare/miniflare/miniflare.js +25 -16
  33. package/lib/cloudflare/miniflare/miniflare.js.map +1 -1
  34. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts +10 -10
  35. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  36. package/lib/cloudflare/miniflare/remote-binding-proxy.js +34 -44
  37. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  38. package/lib/cloudflare/nuxt.d.ts.map +1 -1
  39. package/lib/cloudflare/nuxt.js +3 -0
  40. package/lib/cloudflare/nuxt.js.map +1 -1
  41. package/lib/cloudflare/oauth.js +3 -3
  42. package/lib/cloudflare/oauth.js.map +1 -1
  43. package/lib/cloudflare/pipeline.d.ts.map +1 -1
  44. package/lib/cloudflare/pipeline.js +1 -3
  45. package/lib/cloudflare/pipeline.js.map +1 -1
  46. package/lib/cloudflare/react-router.d.ts.map +1 -1
  47. package/lib/cloudflare/react-router.js +3 -0
  48. package/lib/cloudflare/react-router.js.map +1 -1
  49. package/lib/cloudflare/vite.d.ts.map +1 -1
  50. package/lib/cloudflare/vite.js +0 -1
  51. package/lib/cloudflare/vite.js.map +1 -1
  52. package/lib/cloudflare/website.d.ts +0 -1
  53. package/lib/cloudflare/website.d.ts.map +1 -1
  54. package/lib/cloudflare/website.js +15 -8
  55. package/lib/cloudflare/website.js.map +1 -1
  56. package/lib/cloudflare/worker.d.ts +8 -4
  57. package/lib/cloudflare/worker.d.ts.map +1 -1
  58. package/lib/cloudflare/worker.js +82 -72
  59. package/lib/cloudflare/worker.js.map +1 -1
  60. package/lib/scope.d.ts +14 -2
  61. package/lib/scope.d.ts.map +1 -1
  62. package/lib/scope.js +8 -3
  63. package/lib/scope.js.map +1 -1
  64. package/lib/state/cloudflare-state-store.d.ts.map +1 -1
  65. package/lib/state/cloudflare-state-store.js +2 -2
  66. package/lib/state/cloudflare-state-store.js.map +1 -1
  67. package/lib/util/http.d.ts +13 -0
  68. package/lib/util/http.d.ts.map +1 -0
  69. package/lib/util/http.js +78 -0
  70. package/lib/util/http.js.map +1 -0
  71. package/lib/util/safe-fetch.d.ts.map +1 -1
  72. package/lib/util/safe-fetch.js +1 -0
  73. package/lib/util/safe-fetch.js.map +1 -1
  74. package/package.json +5 -4
  75. package/src/alchemy.ts +28 -49
  76. package/src/cloudflare/astro.ts +3 -0
  77. package/src/cloudflare/compatibility-date.gen.ts +7 -0
  78. package/src/cloudflare/container.ts +1 -1
  79. package/src/cloudflare/d1-migrations.ts +8 -21
  80. package/src/cloudflare/index.ts +1 -0
  81. package/src/cloudflare/miniflare/miniflare-worker-options.ts +13 -1
  82. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +97 -0
  83. package/src/cloudflare/miniflare/miniflare.ts +30 -21
  84. package/src/cloudflare/miniflare/remote-binding-proxy.ts +40 -52
  85. package/src/cloudflare/nuxt.ts +3 -0
  86. package/src/cloudflare/oauth.ts +3 -3
  87. package/src/cloudflare/pipeline.ts +1 -3
  88. package/src/cloudflare/react-router.ts +3 -0
  89. package/src/cloudflare/vite.ts +0 -1
  90. package/src/cloudflare/website.ts +16 -14
  91. package/src/cloudflare/worker.ts +104 -106
  92. package/src/scope.ts +21 -4
  93. package/src/state/cloudflare-state-store.ts +2 -2
  94. package/src/util/http.ts +94 -0
  95. package/src/util/safe-fetch.ts +1 -0
  96. package/templates/react-router/package.json +1 -1
  97. package/templates/react-router/tsconfig.cloudflare.json +5 -1
  98. package/templates/react-router/tsconfig.json +0 -3
  99. package/templates/react-router/tsconfig.node.json +0 -4
  100. package/lib/build-date.d.ts +0 -6
  101. package/lib/build-date.d.ts.map +0 -1
  102. package/lib/build-date.js +0 -8
  103. package/lib/build-date.js.map +0 -1
  104. package/lib/util/http-server.d.ts +0 -14
  105. package/lib/util/http-server.d.ts.map +0 -1
  106. package/lib/util/http-server.js +0 -71
  107. package/lib/util/http-server.js.map +0 -1
  108. package/src/build-date.ts +0 -8
  109. package/src/util/http-server.ts +0 -84
package/src/alchemy.ts CHANGED
@@ -20,50 +20,6 @@ import type { LoggerApi } from "./util/cli.ts";
20
20
  import { logger } from "./util/logger.ts";
21
21
  import { TelemetryClient } from "./util/telemetry/client.ts";
22
22
 
23
- /**
24
- * Parses CLI arguments to extract alchemy options
25
- */
26
- function parseCliArgs(): Partial<AlchemyOptions> {
27
- const args = process.argv.slice(2);
28
- const options: Partial<AlchemyOptions> = {};
29
-
30
- // Parse phase from CLI arguments
31
- if (args.includes("--destroy")) {
32
- options.phase = "destroy";
33
- } else if (args.includes("--read")) {
34
- options.phase = "read";
35
- }
36
-
37
- if (args.includes("--local") || args.includes("--dev")) {
38
- options.dev = "prefer-local";
39
- } else if (
40
- args.includes("--remote") ||
41
- args.includes("--watch") ||
42
- process.execArgv.includes("--watch")
43
- ) {
44
- options.dev = "prefer-remote";
45
- }
46
-
47
- // Parse quiet flag
48
- if (args.includes("--quiet")) {
49
- options.quiet = true;
50
- }
51
-
52
- // Parse stage argument (--stage my-stage)
53
- const stageIndex = args.indexOf("--stage");
54
- if (stageIndex !== -1 && stageIndex + 1 < args.length) {
55
- options.stage = args[stageIndex + 1];
56
- }
57
- options.stage ??= process.env.STAGE;
58
-
59
- // Get password from environment variables
60
- if (process.env.ALCHEMY_PASSWORD) {
61
- options.password = process.env.ALCHEMY_PASSWORD;
62
- }
63
-
64
- return options;
65
- }
66
-
67
23
  /**
68
24
  * Type alias for semantic highlighting of `alchemy` as a type keyword
69
25
  */
@@ -175,8 +131,25 @@ async function _alchemy(
175
131
  if (typeof args[0] === "string") {
176
132
  const [appName, options] = args as [string, AlchemyOptions?];
177
133
 
178
- // Parse CLI arguments and merge with provided options (explicit options take precedence)
179
- const cliOptions = parseCliArgs();
134
+ const cliArgs = process.argv.slice(2);
135
+ const cliOptions = {
136
+ phase: cliArgs.includes("--destroy")
137
+ ? "destroy"
138
+ : cliArgs.includes("--read")
139
+ ? "read"
140
+ : "up",
141
+ local: cliArgs.includes("--local") || cliArgs.includes("--dev"),
142
+ watch: cliArgs.includes("--watch"),
143
+ quiet: cliArgs.includes("--quiet"),
144
+ // Parse stage argument (--stage my-stage) functionally and inline as a property declaration
145
+ stage: (function parseStage() {
146
+ const i = cliArgs.indexOf("--stage");
147
+ return i !== -1 && i + 1 < cliArgs.length
148
+ ? cliArgs[i + 1]
149
+ : process.env.STAGE;
150
+ })(),
151
+ password: process.env.ALCHEMY_PASSWORD,
152
+ } satisfies Partial<AlchemyOptions>;
180
153
  const mergedOptions = {
181
154
  ...cliOptions,
182
155
  ...options,
@@ -348,11 +321,17 @@ export interface AlchemyOptions {
348
321
  */
349
322
  phase?: Phase;
350
323
  /**
351
- * Determines how Alchemy will run in development mode.
324
+ * Determines if resources should be simulated locally (where possible)
325
+ *
326
+ * @default - `true` if ran with `alchemy dev` or `bun ./alchemy.run.ts --dev`
327
+ */
328
+ local?: boolean;
329
+ /**
330
+ * Determines if local changes to resources should be reactively pushed to the local or remote environment.
352
331
  *
353
- * @default - `"prefer-local"` if `--dev` or `--local` is passed as a CLI argument, `"prefer-remote"` if `--remote` or `--watch` is passed as a CLI argument, `undefined` otherwise
332
+ * @default - `true` if ran with `alchemy dev`, `alchemy watch`, `bun --watch ./alchemy.run.ts`
354
333
  */
355
- dev?: "prefer-local" | "prefer-remote";
334
+ watch?: boolean;
356
335
  /**
357
336
  * Name to scope the resource state under (e.g. `.alchemy/{stage}/..`).
358
337
  *
@@ -85,5 +85,8 @@ export async function Astro<B extends Bindings>(
85
85
  run_worker_first: false,
86
86
  },
87
87
  wrangler,
88
+ dev: props.dev ?? {
89
+ command: "astro dev",
90
+ },
88
91
  });
89
92
  }
@@ -0,0 +1,7 @@
1
+ // This file is auto-generated during build
2
+ // Do not edit manually
3
+
4
+ /**
5
+ * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
6
+ */
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-07-15";
@@ -191,7 +191,7 @@ export async function Container<T>(
191
191
  adopt: props.adopt,
192
192
  };
193
193
 
194
- const isDev = Scope.current.dev && !props.dev?.remote;
194
+ const isDev = Scope.current.local && !props.dev?.remote;
195
195
  if (isDev) {
196
196
  const image = await Image(id, {
197
197
  name: `cloudflare-dev/${name}`, // prefix used by Miniflare
@@ -319,9 +319,6 @@ export async function applyMigrations(
319
319
 
320
320
  if (applied.has(migrationName)) continue;
321
321
 
322
- // Run the migration SQL
323
- await executeD1SQL(options, migration.sql);
324
-
325
322
  // Generate a migration id: prefer sequential zero-padded numeric ids (e.g. 00014)
326
323
  // to keep consistency with legacy/imported data. If we cannot produce a numeric id
327
324
  // (e.g. existing IDs are not numeric), fall back to a unique timestamp-based ID.
@@ -334,26 +331,16 @@ export async function applyMigrations(
334
331
  Date.now().toString() + Math.random().toString(36).substr(2, 9);
335
332
  }
336
333
 
337
- const insertSQL = `INSERT INTO ${options.migrationsTable} (id, name, applied_at) VALUES (?, ?, datetime('now'));`;
338
-
339
- // Use parameterised query to record the migration
340
- const response = await options.api.post(
341
- `/accounts/${options.accountId}/d1/database/${options.databaseId}/query`,
342
- {
343
- sql: insertSQL,
344
- params: [migrationId, migrationName],
345
- },
334
+ // Run and record the migration in a single request.
335
+ // D1 over HTTP doesn't support transactions, so this is the next best thing.
336
+ await executeD1SQL(
337
+ options,
338
+ [
339
+ migration.sql,
340
+ `INSERT INTO ${options.migrationsTable} (id, name, applied_at) VALUES ('${migrationId}', '${migrationName}', datetime('now'));`,
341
+ ].join("\n"),
346
342
  );
347
343
 
348
- if (!response.ok) {
349
- await handleApiError(
350
- response,
351
- "inserting migration record",
352
- "D1 database",
353
- options.databaseId,
354
- );
355
- }
356
-
357
344
  if (!options.quiet) {
358
345
  logger.log(`Applied migration: ${migrationName}`);
359
346
  }
@@ -12,6 +12,7 @@ export * from "./bound.ts";
12
12
  export * from "./browser-rendering.ts";
13
13
  export * from "./bucket.ts";
14
14
  export * from "./certificate-pack.ts";
15
+ export * from "./compatibility-date.gen.ts";
15
16
  export * from "./container.ts";
16
17
  export * from "./custom-domain.ts";
17
18
  export * from "./d1-clone.ts";
@@ -17,6 +17,7 @@ export type MiniflareWorkerOptions = Pick<
17
17
  | "compatibilityDate"
18
18
  | "compatibilityFlags"
19
19
  | "format"
20
+ | "assets"
20
21
  > & {
21
22
  name: string;
22
23
  bundle: WorkerBundle;
@@ -209,6 +210,7 @@ function buildRemoteBinding(
209
210
 
210
211
  export async function buildMiniflareWorkerOptions({
211
212
  name: workerName,
213
+ assets,
212
214
  bundle,
213
215
  bindings,
214
216
  format,
@@ -232,7 +234,9 @@ export async function buildMiniflareWorkerOptions({
232
234
  ),
233
235
  compatibilityDate,
234
236
  compatibilityFlags,
235
- unsafeDirectSockets: [{ entrypoint: undefined, proxy: true }],
237
+ // This was true for compatibility with the dev registry, but that doesn't work anyway,
238
+ // and if we don't set it to false, websocket connections fail. I'm confused.
239
+ unsafeDirectSockets: [{ proxy: false }],
236
240
  containerEngine: {
237
241
  localDocker: {
238
242
  socketPath:
@@ -291,6 +295,14 @@ export async function buildMiniflareWorkerOptions({
291
295
  options.assets = {
292
296
  binding: name,
293
297
  directory: binding.path,
298
+ routerConfig: {
299
+ invoke_user_worker_ahead_of_assets:
300
+ assets?.run_worker_first === true,
301
+ },
302
+ assetConfig: {
303
+ html_handling: assets?.html_handling,
304
+ not_found_handling: assets?.not_found_handling,
305
+ },
294
306
  };
295
307
  break;
296
308
  }
@@ -0,0 +1,97 @@
1
+ import type http from "node:http";
2
+ import { WebSocket, WebSocketServer } from "ws";
3
+ import { HTTPServer } from "../../util/http.ts";
4
+
5
+ export interface MiniflareWorkerProxyOptions {
6
+ /**
7
+ * Used to proxy websocket connections to the worker.
8
+ * (This is a hack; used because trying to interact with `response.webSocket` doesn't work on Bun.)
9
+ */
10
+ getDirectURL: () => Promise<URL>;
11
+ /** Used to proxy HTTP requests to the worker. */
12
+ fetch: (request: Request) => Promise<Response>;
13
+ }
14
+
15
+ export class MiniflareWorkerProxy extends HTTPServer {
16
+ wsServer: WebSocketServer;
17
+ wsReconnectAttempts = new Map<string, number>();
18
+
19
+ constructor(private readonly options: MiniflareWorkerProxyOptions) {
20
+ super({
21
+ fetch: options.fetch,
22
+ });
23
+ this.wsServer = new WebSocketServer({ noServer: true });
24
+ this.httpServer.on("upgrade", async (req, socket, head) => {
25
+ this.wsServer.handleUpgrade(req, socket, head, async (client) => {
26
+ const id = crypto.randomUUID();
27
+ await this.handleUpgrade(id, client, req);
28
+ });
29
+ });
30
+ }
31
+
32
+ private async handleUpgrade(
33
+ id: string,
34
+ client: WebSocket,
35
+ request: http.IncomingMessage,
36
+ ) {
37
+ const attempt = this.wsReconnectAttempts.get(id) ?? 0;
38
+ this.wsReconnectAttempts.set(id, attempt + 1);
39
+ if (attempt > 3) {
40
+ client.close(1006, "Too many reconnect attempts");
41
+ return;
42
+ }
43
+ if (attempt > 0) {
44
+ const delay = attempt * 1000;
45
+ await new Promise((resolve) => setTimeout(resolve, delay));
46
+ }
47
+
48
+ const target = await this.options.getDirectURL();
49
+ const url = new URL(request.url ?? "/", target);
50
+ const headers = { ...request.headers };
51
+ // All of these headers are set by the WebSocket constructor,
52
+ // so if we don't delete them, the request will fail.
53
+ delete headers.host;
54
+ delete headers.connection;
55
+ delete headers.upgrade;
56
+ delete headers["sec-websocket-version"];
57
+ delete headers["sec-websocket-key"];
58
+ delete headers["sec-websocket-protocol"];
59
+ delete headers["sec-websocket-extensions"];
60
+ delete headers["sec-websocket-accept"];
61
+ const server = new WebSocket(url.toString(), {
62
+ protocol: request.headers["sec-websocket-protocol"],
63
+ key: request.headers["sec-websocket-key"],
64
+ headers,
65
+ });
66
+ server.on("open", () => {
67
+ // Reset the reconnect attempts when the connection is established successfully.
68
+ this.wsReconnectAttempts.set(id, 0);
69
+ });
70
+ server.on("message", (data, binary) => {
71
+ client.send(data, { binary });
72
+ });
73
+ server.on("close", async (code, reason) => {
74
+ if (code === 1006) {
75
+ // When the worker hot reloads, the websocket connection is closed with this code.
76
+ // Reconnecting allows the client to maintain the same connection.
77
+ await this.handleUpgrade(id, client, request);
78
+ return;
79
+ }
80
+ client.close(code, reason);
81
+ });
82
+ client.on("message", (data, binary) => {
83
+ server.send(data, { binary });
84
+ });
85
+ client.on("close", (code, reason) => {
86
+ this.wsReconnectAttempts.delete(id);
87
+ if (server.readyState === WebSocket.OPEN) {
88
+ server.close(code, reason);
89
+ }
90
+ });
91
+ }
92
+
93
+ async close() {
94
+ this.wsServer.close();
95
+ await super.close();
96
+ }
97
+ }
@@ -7,7 +7,6 @@ import {
7
7
  } from "miniflare";
8
8
  import path from "node:path";
9
9
  import { findOpenPort } from "../../util/find-open-port.ts";
10
- import { HTTPServer } from "../../util/http-server.ts";
11
10
  import { logger } from "../../util/logger.ts";
12
11
  import {
13
12
  promiseWithResolvers,
@@ -18,6 +17,7 @@ import {
18
17
  buildRemoteBindings,
19
18
  type MiniflareWorkerOptions,
20
19
  } from "./miniflare-worker-options.ts";
20
+ import { MiniflareWorkerProxy } from "./miniflare-worker-proxy.ts";
21
21
  import {
22
22
  createRemoteProxyWorker,
23
23
  type RemoteBindingProxy,
@@ -26,12 +26,12 @@ import {
26
26
  class MiniflareServer {
27
27
  miniflare?: Miniflare;
28
28
  workers = new Map<string, WorkerOptions>();
29
- servers = new Map<string, HTTPServer>();
29
+ workerProxies = new Map<string, MiniflareWorkerProxy>();
30
30
  remoteBindingProxies = new Map<string, RemoteBindingProxy>();
31
31
 
32
32
  stream = new WritableStream<{
33
33
  worker: MiniflareWorkerOptions;
34
- promise: PromiseWithResolvers<HTTPServer>;
34
+ promise: PromiseWithResolvers<MiniflareWorkerProxy>;
35
35
  }>({
36
36
  write: async ({ worker, promise }) => {
37
37
  try {
@@ -48,7 +48,7 @@ class MiniflareServer {
48
48
  writer = this.stream.getWriter();
49
49
 
50
50
  async push(worker: MiniflareWorkerOptions) {
51
- const promise = promiseWithResolvers<HTTPServer>();
51
+ const promise = promiseWithResolvers<MiniflareWorkerProxy>();
52
52
  const [, server] = await Promise.all([
53
53
  this.writer.write({ worker, promise }),
54
54
  promise.promise,
@@ -62,11 +62,10 @@ class MiniflareServer {
62
62
 
63
63
  private async set(worker: MiniflareWorkerOptions) {
64
64
  this.workers.set(
65
- worker.name as string,
65
+ worker.name,
66
66
  await buildMiniflareWorkerOptions({
67
67
  ...worker,
68
- remoteProxyConnectionString:
69
- await this.maybeCreateMixedModeProxy(worker),
68
+ remoteProxyConnectionString: await this.maybeCreateRemoteProxy(worker),
70
69
  }),
71
70
  );
72
71
  if (this.miniflare) {
@@ -90,33 +89,41 @@ class MiniflareServer {
90
89
  this.miniflare = new Miniflare(await this.miniflareOptions());
91
90
  await withErrorRewrite(this.miniflare.ready);
92
91
  }
93
- const existing = this.servers.get(worker.name);
92
+ const existing = this.workerProxies.get(worker.name);
94
93
  if (existing) {
95
94
  return existing;
96
95
  }
97
- const server = new HTTPServer({
98
- port: worker.port ?? (await findOpenPort()),
99
- fetch: this.createRequestHandler(worker.name as string),
96
+ const server = new MiniflareWorkerProxy({
97
+ getDirectURL: async () => {
98
+ const url = await this.miniflare?.unsafeGetDirectURL(worker.name);
99
+ if (!url) {
100
+ throw new Error(`Worker "${worker.name}" is not running`);
101
+ }
102
+ return url;
103
+ },
104
+ fetch: this.createRequestHandler(worker.name),
100
105
  });
101
- this.servers.set(worker.name, server);
102
- await server.ready;
106
+ this.workerProxies.set(worker.name, server);
107
+ await server.listen(worker.port ?? (await findOpenPort()));
103
108
  return server;
104
109
  }
105
110
 
106
111
  private async dispose() {
107
112
  await Promise.all([
108
113
  this.miniflare?.dispose(),
109
- ...Array.from(this.servers.values()).map((server) => server.stop()),
114
+ ...Array.from(this.workerProxies.values()).map((server) =>
115
+ server.close(),
116
+ ),
110
117
  ...Array.from(this.remoteBindingProxies.values()).map((proxy) =>
111
- proxy.server.stop(),
118
+ proxy.server.close(),
112
119
  ),
113
120
  ]);
114
121
  this.miniflare = undefined;
115
122
  this.workers.clear();
116
- this.servers.clear();
123
+ this.workerProxies.clear();
117
124
  }
118
125
 
119
- private async maybeCreateMixedModeProxy(
126
+ private async maybeCreateRemoteProxy(
120
127
  worker: MiniflareWorkerOptions,
121
128
  ): Promise<RemoteProxyConnectionString | undefined> {
122
129
  const bindings = buildRemoteBindings(worker);
@@ -130,6 +137,8 @@ class MiniflareServer {
130
137
  )
131
138
  ) {
132
139
  return existing.connectionString;
140
+ } else if (existing) {
141
+ await existing.server.close();
133
142
  }
134
143
  const proxy = await createRemoteProxyWorker({
135
144
  name: `mixed-mode-proxy-${crypto.randomUUID()}`,
@@ -150,8 +159,8 @@ class MiniflareServer {
150
159
  },
151
160
  );
152
161
  }
153
- const miniflare = await this.miniflare?.getWorker(name);
154
- if (!miniflare) {
162
+ const worker = await this.miniflare?.getWorker(name);
163
+ if (!worker) {
155
164
  return new Response(
156
165
  `[Alchemy] Cannot find worker "${name}". Please try again.`,
157
166
  {
@@ -159,12 +168,12 @@ class MiniflareServer {
159
168
  },
160
169
  );
161
170
  }
162
- const res = await miniflare.fetch(req.url, {
171
+ const res = await worker.fetch(req.url, {
163
172
  method: req.method,
164
173
  headers: req.headers as any,
165
174
  body: req.body as any,
166
- redirect: "manual",
167
175
  duplex: "half",
176
+ redirect: "manual",
168
177
  });
169
178
  return res as unknown as Response;
170
179
  } catch (error) {
@@ -1,5 +1,5 @@
1
1
  import type { RemoteProxyConnectionString } from "miniflare";
2
- import { HTTPServer } from "../../util/http-server.ts";
2
+ import { HTTPServer } from "../../util/http.ts";
3
3
  import { extractCloudflareResult } from "../api-response.ts";
4
4
  import { createCloudflareApi, type CloudflareApi } from "../api.ts";
5
5
  import type { WorkerBindingSpec } from "../bindings.ts";
@@ -22,6 +22,12 @@ interface WorkersPreviewSession {
22
22
  token: string;
23
23
  }
24
24
 
25
+ export interface RemoteBindingProxy {
26
+ server: HTTPServer;
27
+ bindings: WorkerBindingSpec[];
28
+ connectionString: RemoteProxyConnectionString;
29
+ }
30
+
25
31
  export async function createRemoteProxyWorker(input: {
26
32
  name: string;
27
33
  bindings: WorkerBindingSpec[];
@@ -47,62 +53,44 @@ export async function createRemoteProxyWorker(input: {
47
53
  }),
48
54
  import("../worker-subdomain.ts").then((m) => m.getAccountSubdomain(api)),
49
55
  ]);
50
- return new RemoteBindingProxy(
51
- `https://${input.name}.${subdomain}.workers.dev`,
52
- token,
53
- input.bindings,
54
- );
55
- }
56
56
 
57
- export class RemoteBindingProxy {
58
- server: HTTPServer;
57
+ const proxyURL = new URL(`https://${input.name}.${subdomain}.workers.dev`);
58
+ const server = new HTTPServer({
59
+ fetch: async (req) => {
60
+ const origin = new URL(req.url);
61
+ const url = new URL(origin.pathname, proxyURL.toString());
62
+ url.search = origin.search;
63
+ url.hash = origin.hash;
59
64
 
60
- constructor(
61
- readonly url: string,
62
- readonly token: string,
63
- readonly bindings: WorkerBindingSpec[],
64
- ) {
65
- this.server = new HTTPServer({
66
- fetch: this.fetch.bind(this),
67
- });
68
- }
65
+ const requestHeaders = new Headers(req.headers);
66
+ requestHeaders.set("cf-workers-preview-token", token);
67
+ requestHeaders.set("host", proxyURL.hostname);
68
+ requestHeaders.delete("cf-connecting-ip");
69
69
 
70
- get connectionString() {
71
- const hostname =
72
- this.server.hostname === "::" ? "localhost" : this.server.hostname;
73
- return new URL(
74
- `http://${hostname}:${this.server.port}`,
75
- ) as RemoteProxyConnectionString;
76
- }
77
-
78
- async fetch(req: Request) {
79
- const origin = new URL(req.url);
80
- const url = new URL(origin.pathname, this.url);
81
- url.search = origin.search;
82
- url.hash = origin.hash;
83
-
84
- const headers = new Headers(req.headers);
85
- headers.set("cf-workers-preview-token", this.token);
86
- headers.set("host", new URL(this.url).hostname);
87
- headers.delete("cf-connecting-ip");
88
-
89
- const res = await fetch(url, {
90
- method: req.method,
91
- headers,
92
- body: req.body,
93
- redirect: "manual",
94
- });
70
+ const res = await fetch(url, {
71
+ method: req.method,
72
+ headers: requestHeaders,
73
+ body: req.body,
74
+ redirect: "manual",
75
+ });
95
76
 
96
- // Remove headers that are not supported by miniflare
97
- const responseHeaders = new Headers(res.headers);
98
- responseHeaders.delete("transfer-encoding");
99
- responseHeaders.delete("content-encoding");
77
+ // Remove headers that are not supported by miniflare
78
+ const responseHeaders = new Headers(res.headers);
79
+ responseHeaders.delete("transfer-encoding");
80
+ responseHeaders.delete("content-encoding");
100
81
 
101
- return new Response(res.body, {
102
- status: res.status,
103
- headers: responseHeaders,
104
- });
105
- }
82
+ return new Response(res.body, {
83
+ status: res.status,
84
+ headers: responseHeaders,
85
+ });
86
+ },
87
+ });
88
+ await server.listen();
89
+ return {
90
+ server,
91
+ bindings: input.bindings,
92
+ connectionString: new URL(server.url) as RemoteProxyConnectionString,
93
+ };
106
94
  }
107
95
 
108
96
  async function createWorkersPreviewToken(
@@ -64,5 +64,8 @@ export async function Nuxt<B extends Bindings>(
64
64
  compatibilityFlags: ["nodejs_compat", ...(props?.compatibilityFlags ?? [])],
65
65
  // Enable wrangler by default, common for Nuxt/Cloudflare deployments
66
66
  wrangler: props?.wrangler ?? true,
67
+ dev: props?.dev ?? {
68
+ command: "nuxt dev",
69
+ },
67
70
  });
68
71
  }
@@ -7,7 +7,7 @@ import os from "node:os";
7
7
  import path from "node:path";
8
8
  import open from "open";
9
9
  import xdgAppPaths from "xdg-app-paths";
10
- import { HTTPServer } from "../util/http-server.ts";
10
+ import { HTTPServer } from "../util/http.ts";
11
11
  import { memoize } from "../util/memoize.ts";
12
12
  import {
13
13
  detached,
@@ -145,7 +145,6 @@ export const wranglerLogin = (
145
145
  return Response.redirect(`${ALCHEMY_BASE_URL}/auth/${type}`);
146
146
  };
147
147
  const server = new HTTPServer({
148
- port: 8976,
149
148
  fetch: async (request) => {
150
149
  const url = new URL(request.url);
151
150
  if (url.pathname !== "/oauth/callback") {
@@ -190,6 +189,7 @@ export const wranglerLogin = (
190
189
  return renderResponse("success");
191
190
  },
192
191
  });
192
+ server.listen(8976);
193
193
  const timeout = setTimeout(
194
194
  () => {
195
195
  err(
@@ -204,7 +204,7 @@ export const wranglerLogin = (
204
204
  );
205
205
  return ensure(result, () => {
206
206
  clearTimeout(timeout);
207
- void server.stop();
207
+ void server.close();
208
208
  });
209
209
  };
210
210
 
@@ -368,11 +368,9 @@ const PipelineResource = Resource("cloudflare::Pipeline", async function <
368
368
  let pipelineData: CloudflarePipelineResponse;
369
369
 
370
370
  if (this.phase === "create") {
371
- console.log(props);
372
371
  // Check if we should adopt an existing pipeline
373
372
  try {
374
373
  // Try to create pipeline first
375
- console.log("Creating new Cloudflare Pipeline:", pipelineName);
376
374
  pipelineData = await createPipeline(api, pipelineName, props);
377
375
  } catch (error) {
378
376
  // If creation fails with 409 (conflict), adopt existing pipeline
@@ -383,7 +381,7 @@ const PipelineResource = Resource("cloudflare::Pipeline", async function <
383
381
  error.message.includes("Pipeline with this name already exists")))
384
382
  ) {
385
383
  if (props.adopt) {
386
- console.log(
384
+ console.warn(
387
385
  "Pipeline already exists, adopting existing Cloudflare Pipeline:",
388
386
  pipelineName,
389
387
  );
@@ -41,5 +41,8 @@ export async function ReactRouter<B extends Bindings>(
41
41
  dist: props.assets.dist ?? defaultAssets,
42
42
  }
43
43
  : (props.assets ?? defaultAssets),
44
+ dev: props.dev ?? {
45
+ command: "react-router dev",
46
+ },
44
47
  });
45
48
  }
@@ -38,7 +38,6 @@ export async function Vite<B extends Bindings>(
38
38
  : (props.assets ?? defaultAssets),
39
39
  dev: props.dev ?? {
40
40
  command: devCommand,
41
- url: "http://localhost:5173",
42
41
  },
43
42
  });
44
43
  }