alchemy 0.55.2 → 0.56.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 (87) hide show
  1. package/bin/alchemy.js +13920 -6724
  2. package/lib/alchemy.d.ts.map +1 -1
  3. package/lib/alchemy.js +7 -0
  4. package/lib/alchemy.js.map +1 -1
  5. package/lib/cloudflare/bundle/plugin-alias.d.ts +3 -0
  6. package/lib/cloudflare/bundle/plugin-alias.d.ts.map +1 -0
  7. package/lib/cloudflare/bundle/plugin-alias.js +40 -0
  8. package/lib/cloudflare/bundle/plugin-alias.js.map +1 -0
  9. package/lib/cloudflare/bundle/plugin-hot-reload.d.ts +1 -3
  10. package/lib/cloudflare/bundle/plugin-hot-reload.d.ts.map +1 -1
  11. package/lib/cloudflare/bundle/plugin-hot-reload.js +4 -1
  12. package/lib/cloudflare/bundle/plugin-hot-reload.js.map +1 -1
  13. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  14. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  15. package/lib/cloudflare/compatibility-presets.d.ts +1 -1
  16. package/lib/cloudflare/compatibility-presets.js +1 -1
  17. package/lib/cloudflare/compatibility-presets.js.map +1 -1
  18. package/lib/cloudflare/local-dev-cloudflare-shim.d.ts +3 -1
  19. package/lib/cloudflare/local-dev-cloudflare-shim.d.ts.map +1 -1
  20. package/lib/cloudflare/local-dev-cloudflare-shim.js +10 -5
  21. package/lib/cloudflare/local-dev-cloudflare-shim.js.map +1 -1
  22. package/lib/cloudflare/miniflare/build-worker-options.d.ts +22 -0
  23. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -0
  24. package/lib/cloudflare/miniflare/build-worker-options.js +394 -0
  25. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -0
  26. package/lib/cloudflare/miniflare/miniflare-controller.d.ts +23 -0
  27. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -0
  28. package/lib/cloudflare/miniflare/miniflare-controller.js +110 -0
  29. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -0
  30. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  31. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +5 -15
  32. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  33. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts +1 -5
  34. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  35. package/lib/cloudflare/miniflare/remote-binding-proxy.js +17 -22
  36. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  37. package/lib/cloudflare/website.js +4 -4
  38. package/lib/cloudflare/website.js.map +1 -1
  39. package/lib/cloudflare/worker-bundle.d.ts +2 -0
  40. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  41. package/lib/cloudflare/worker-bundle.js +21 -4
  42. package/lib/cloudflare/worker-bundle.js.map +1 -1
  43. package/lib/cloudflare/worker.d.ts.map +1 -1
  44. package/lib/cloudflare/worker.js +76 -97
  45. package/lib/cloudflare/worker.js.map +1 -1
  46. package/lib/context.d.ts +11 -0
  47. package/lib/context.d.ts.map +1 -1
  48. package/lib/context.js +8 -0
  49. package/lib/context.js.map +1 -1
  50. package/lib/scope.d.ts +12 -0
  51. package/lib/scope.d.ts.map +1 -1
  52. package/lib/scope.js +25 -0
  53. package/lib/scope.js.map +1 -1
  54. package/lib/util/telemetry/client.d.ts +3 -4
  55. package/lib/util/telemetry/client.d.ts.map +1 -1
  56. package/lib/util/telemetry/client.js +19 -25
  57. package/lib/util/telemetry/client.js.map +1 -1
  58. package/lib/util/telemetry/types.d.ts +1 -1
  59. package/lib/util/telemetry/types.d.ts.map +1 -1
  60. package/package.json +3 -2
  61. package/src/alchemy.ts +7 -0
  62. package/src/cloudflare/bundle/plugin-alias.ts +49 -0
  63. package/src/cloudflare/bundle/plugin-hot-reload.ts +4 -3
  64. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  65. package/src/cloudflare/compatibility-presets.ts +1 -1
  66. package/src/cloudflare/local-dev-cloudflare-shim.ts +11 -5
  67. package/src/cloudflare/miniflare/build-worker-options.ts +454 -0
  68. package/src/cloudflare/miniflare/miniflare-controller.ts +128 -0
  69. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +6 -15
  70. package/src/cloudflare/miniflare/remote-binding-proxy.ts +18 -23
  71. package/src/cloudflare/website.ts +4 -4
  72. package/src/cloudflare/worker-bundle.ts +30 -4
  73. package/src/cloudflare/worker.ts +90 -125
  74. package/src/context.ts +19 -0
  75. package/src/scope.ts +27 -0
  76. package/src/util/telemetry/client.ts +23 -33
  77. package/src/util/telemetry/types.ts +3 -3
  78. package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts +0 -14
  79. package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts.map +0 -1
  80. package/lib/cloudflare/miniflare/miniflare-worker-options.js +0 -468
  81. package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +0 -1
  82. package/lib/cloudflare/miniflare/miniflare.d.ts +0 -40
  83. package/lib/cloudflare/miniflare/miniflare.d.ts.map +0 -1
  84. package/lib/cloudflare/miniflare/miniflare.js +0 -142
  85. package/lib/cloudflare/miniflare/miniflare.js.map +0 -1
  86. package/src/cloudflare/miniflare/miniflare-worker-options.ts +0 -599
  87. package/src/cloudflare/miniflare/miniflare.ts +0 -183
@@ -3,11 +3,9 @@ import assert from "node:assert";
3
3
 
4
4
  export interface HotReloadPluginProps {
5
5
  onBuildStart?: () => void | Promise<void>;
6
- onBuildEnd?: (result: esbuild.BuildResult) => void | Promise<void>;
7
- onBuildError?: (errors: esbuild.Message[]) => void | Promise<void>;
8
6
  }
9
7
 
10
- export function createHotReloadPlugin(): {
8
+ export function createHotReloadPlugin(props?: HotReloadPluginProps): {
11
9
  plugin: esbuild.Plugin;
12
10
  iterator: AsyncIterable<esbuild.BuildResult>;
13
11
  } {
@@ -22,6 +20,9 @@ export function createHotReloadPlugin(): {
22
20
  const plugin: esbuild.Plugin = {
23
21
  name: "alchemy-hot-reload",
24
22
  setup(build) {
23
+ if (props?.onBuildStart) {
24
+ build.onStart(props.onBuildStart);
25
+ }
25
26
  build.onEnd(async (result) => {
26
27
  assert(_controller);
27
28
  _controller.enqueue(result);
@@ -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-26";
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-08-02";
@@ -13,7 +13,7 @@ export const COMPATIBILITY_PRESETS = {
13
13
  * Node.js compatibility preset
14
14
  * Enables Node.js APIs and runtime compatibility
15
15
  */
16
- node: ["nodejs_compat"],
16
+ node: ["nodejs_compat", "nodejs_compat_populate_process_env"],
17
17
  } as const;
18
18
 
19
19
  export type CompatibilityPreset = keyof typeof COMPATIBILITY_PRESETS;
@@ -3,7 +3,9 @@ import { dedent } from "../util/dedent.ts";
3
3
  /**
4
4
  * TanStackStart server functions and middleware run in Node.js intead of Miniflare when using `vite dev`.
5
5
  *
6
- * This plugin polyfills the cloudflare:workers module during the dev server phase.
6
+ * This plugin polyfills the cloudflare:workers module & includes `process.env` during the dev server phase.
7
+ *
8
+ * @see https://developers.cloudflare.com/workers/framework-guides/web-apps/tanstack/#using-cloudflare-bindings
7
9
  */
8
10
  export function cloudflareWorkersDevEnvironmentShim() {
9
11
  return {
@@ -14,12 +16,16 @@ export function cloudflareWorkersDevEnvironmentShim() {
14
16
  if (id === "cloudflare:workers") return id; // tell Vite we handled it
15
17
  },
16
18
  load(id: string) {
17
- if (id === "cloudflare:workers")
19
+ if (id === "cloudflare:workers") {
20
+ const processEnv = JSON.stringify(process.env);
21
+
22
+ // @see https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy
18
23
  return dedent`
19
24
  import { getPlatformProxy } from "wrangler";
20
- // TODO: should we export default cloudflare; ??
21
- const cloudflare = await getPlatformProxy();
22
- export const env = cloudflare.env;`;
25
+ const cloudflare = await getPlatformProxy({ experimental: { remoteBindings: true } });
26
+ export const env = Object.assign(${processEnv}, cloudflare.env);
27
+ `.trim();
28
+ }
23
29
  },
24
30
  } as const;
25
31
  }
@@ -0,0 +1,454 @@
1
+ import * as miniflare from "miniflare";
2
+ import { assertNever } from "../../util/assert-never.ts";
3
+ import type { HTTPServer } from "../../util/http.ts";
4
+ import {
5
+ Self,
6
+ type Binding,
7
+ type Bindings,
8
+ type WorkerBindingService,
9
+ type WorkerBindingSpec,
10
+ } from "../bindings.ts";
11
+ import { isQueueEventSource, type EventSource } from "../event-source.ts";
12
+ import type { WorkerBundle, WorkerBundleSource } from "../worker-bundle.ts";
13
+ import type { AssetsConfig } from "../worker.ts";
14
+ import { createRemoteProxyWorker } from "./remote-binding-proxy.ts";
15
+
16
+ export interface MiniflareWorkerInput {
17
+ id: string;
18
+ name: string;
19
+ compatibilityDate: string | undefined;
20
+ compatibilityFlags: string[] | undefined;
21
+ bindings: Bindings | undefined;
22
+ eventSources: EventSource[] | undefined;
23
+ assets: AssetsConfig | undefined;
24
+ bundle: WorkerBundleSource;
25
+ port: number | undefined;
26
+ }
27
+
28
+ type RemoteOnlyBindingType =
29
+ | "ai"
30
+ | "ai_gateway"
31
+ | "browser"
32
+ | "dispatch_namespace"
33
+ | "mtls_certificate"
34
+ | "vectorize";
35
+ type RemoteOptionalBindingType =
36
+ | "d1"
37
+ | "images"
38
+ | "kv_namespace"
39
+ | "queue"
40
+ | "r2_bucket";
41
+
42
+ type RemoteBinding =
43
+ | (Extract<
44
+ WorkerBindingSpec,
45
+ {
46
+ type: RemoteOnlyBindingType | RemoteOptionalBindingType;
47
+ }
48
+ > & {
49
+ raw: true;
50
+ })
51
+ | WorkerBindingService;
52
+
53
+ type BaseWorkerOptions = {
54
+ [K in keyof miniflare.WorkerOptions]: K extends "compatibilityFlags"
55
+ ? miniflare.WorkerOptions[K]
56
+ : Exclude<miniflare.WorkerOptions[K], string[]>;
57
+ };
58
+
59
+ export const buildWorkerOptions = async (
60
+ input: MiniflareWorkerInput,
61
+ ): Promise<{
62
+ watch: (signal: AbortSignal) => AsyncGenerator<miniflare.WorkerOptions>;
63
+ remoteProxy: HTTPServer | undefined;
64
+ }> => {
65
+ const remoteBindings: RemoteBinding[] = [];
66
+ const options: Partial<BaseWorkerOptions> = {
67
+ name: input.name,
68
+ compatibilityDate: input.compatibilityDate,
69
+ compatibilityFlags: input.compatibilityFlags,
70
+ // TODO: Setting `proxy: true` here causes the following error when connecting via a websocket:
71
+ // workerd/io/worker.c++:2164: info: uncaught exception; source = Uncaught (in promise); stack = TypeError: Invalid URL string.
72
+ unsafeDirectSockets: [{ proxy: false }],
73
+ containerEngine: {
74
+ localDocker: {
75
+ socketPath:
76
+ process.platform === "win32"
77
+ ? "//./pipe/docker_engine"
78
+ : "unix:///var/run/docker.sock",
79
+ },
80
+ },
81
+ };
82
+ for (const [key, binding] of Object.entries(input.bindings ?? {})) {
83
+ if (typeof binding === "string") {
84
+ (options.bindings ??= {})[key] = binding;
85
+ continue;
86
+ }
87
+ if (binding === Self) {
88
+ (options.serviceBindings ??= {})[key] = miniflare.kCurrentWorker;
89
+ continue;
90
+ }
91
+ switch (binding.type) {
92
+ case "ai":
93
+ case "ai_gateway": {
94
+ remoteBindings.push({
95
+ type: "ai",
96
+ name: key,
97
+ raw: true,
98
+ });
99
+ break;
100
+ }
101
+ case "analytics_engine": {
102
+ (options.analyticsEngineDatasets ??= {})[key] = {
103
+ dataset: binding.dataset,
104
+ };
105
+ break;
106
+ }
107
+ case "assets": {
108
+ options.assets = {
109
+ binding: key,
110
+ directory: binding.path,
111
+ assetConfig: {
112
+ html_handling: input.assets?.html_handling,
113
+ not_found_handling: input.assets?.not_found_handling,
114
+ },
115
+ };
116
+ break;
117
+ }
118
+ case "browser": {
119
+ remoteBindings.push({
120
+ type: "browser",
121
+ name: key,
122
+ raw: true,
123
+ });
124
+ break;
125
+ }
126
+ case "container": {
127
+ (options.durableObjects ??= {})[key] = {
128
+ className: binding.className,
129
+ scriptName: binding.scriptName,
130
+ useSQLite: binding.sqlite,
131
+ container: {
132
+ imageName: binding.image.imageRef,
133
+ },
134
+ };
135
+ break;
136
+ }
137
+ case "d1": {
138
+ if (isRemoteBinding(binding)) {
139
+ remoteBindings.push({
140
+ type: "d1",
141
+ name: key,
142
+ id: binding.id,
143
+ raw: true,
144
+ });
145
+ } else {
146
+ (options.d1Databases ??= {})[key] = binding.id;
147
+ }
148
+ break;
149
+ }
150
+ case "dispatch_namespace": {
151
+ remoteBindings.push({
152
+ type: "dispatch_namespace",
153
+ name: key,
154
+ namespace: binding.namespace,
155
+ raw: true,
156
+ });
157
+ break;
158
+ }
159
+ case "durable_object_namespace": {
160
+ (options.durableObjects ??= {})[key] = {
161
+ className: binding.className,
162
+ scriptName: binding.scriptName,
163
+ useSQLite: binding.sqlite,
164
+ };
165
+ break;
166
+ }
167
+ case "hyperdrive": {
168
+ throw new Error("Hyperdrive bindings are not supported in local mode");
169
+ }
170
+ case "images": {
171
+ if (isRemoteBinding(binding)) {
172
+ remoteBindings.push({
173
+ type: "images",
174
+ name: key,
175
+ raw: true,
176
+ });
177
+ } else {
178
+ options.images = {
179
+ binding: key,
180
+ };
181
+ }
182
+ break;
183
+ }
184
+ case "kv_namespace": {
185
+ const namespaceId =
186
+ "namespaceId" in binding ? binding.namespaceId : binding.id;
187
+ if (isRemoteBinding(binding)) {
188
+ remoteBindings.push({
189
+ type: "kv_namespace",
190
+ name: key,
191
+ namespace_id: namespaceId,
192
+ raw: true,
193
+ });
194
+ } else {
195
+ (options.kvNamespaces ??= {})[key] = namespaceId;
196
+ }
197
+ break;
198
+ }
199
+ case "json": {
200
+ (options.bindings ??= {})[key] = binding.json;
201
+ break;
202
+ }
203
+ case "queue": {
204
+ if (isRemoteBinding(binding)) {
205
+ remoteBindings.push({
206
+ type: "queue",
207
+ name: key,
208
+ queue_name: binding.name,
209
+ raw: true,
210
+ });
211
+ } else {
212
+ (options.queueProducers ??= {})[key] = {
213
+ queueName: binding.name,
214
+ deliveryDelay: binding.settings?.deliveryDelay,
215
+ };
216
+ }
217
+ break;
218
+ }
219
+ case "pipeline": {
220
+ (options.pipelines ??= {})[key] = binding.id;
221
+ break;
222
+ }
223
+ case "ratelimit": {
224
+ (options.ratelimits ??= {})[key] = {
225
+ simple: binding.simple,
226
+ };
227
+ break;
228
+ }
229
+ case "secret": {
230
+ (options.bindings ??= {})[key] = binding.unencrypted;
231
+ break;
232
+ }
233
+ case "secret_key": {
234
+ throw new Error("Secret keys are not supported in local mode");
235
+ }
236
+ case "secrets_store_secret": {
237
+ (options.secretsStoreSecrets ??= {})[key] = {
238
+ store_id: binding.storeId,
239
+ secret_name: binding.name,
240
+ };
241
+ break;
242
+ }
243
+ case "r2_bucket": {
244
+ if (isRemoteBinding(binding)) {
245
+ remoteBindings.push({
246
+ type: "r2_bucket",
247
+ name: key,
248
+ bucket_name: binding.name,
249
+ raw: true,
250
+ });
251
+ } else {
252
+ (options.r2Buckets ??= {})[key] = binding.name;
253
+ }
254
+ break;
255
+ }
256
+ case "service": {
257
+ const service = "service" in binding ? binding.service : binding.name;
258
+ const normalized: WorkerBindingService = {
259
+ type: "service",
260
+ name: key,
261
+ service,
262
+ environment:
263
+ "environment" in binding ? binding.environment : undefined,
264
+ };
265
+ if (isRemoteBinding(binding)) {
266
+ remoteBindings.push(normalized);
267
+ } else {
268
+ (options.serviceBindings ??= {})[key] = service;
269
+ }
270
+ break;
271
+ }
272
+ case "vectorize": {
273
+ remoteBindings.push({
274
+ type: "vectorize",
275
+ name: key,
276
+ index_name: binding.name,
277
+ raw: true,
278
+ });
279
+ break;
280
+ }
281
+ case "version_metadata": {
282
+ (options.bindings ??= {})[key] = {
283
+ id: crypto.randomUUID(),
284
+ tag: "",
285
+ timestamp: "0",
286
+ };
287
+ break;
288
+ }
289
+ case "workflow": {
290
+ (options.workflows ??= {})[key] = {
291
+ name: binding.workflowName,
292
+ className: binding.className,
293
+ scriptName: binding.scriptName,
294
+ };
295
+ break;
296
+ }
297
+ default: {
298
+ assertNever(binding);
299
+ }
300
+ }
301
+ }
302
+ for (const eventSource of input.eventSources ?? []) {
303
+ const queue = "queue" in eventSource ? eventSource.queue : eventSource;
304
+ if (queue.dev?.remote) {
305
+ throw new Error(
306
+ `Locally emulated workers cannot consume remote queues. Worker "${input.name}" is locally emulated but is consuming remote queue "${queue.name}".`,
307
+ );
308
+ }
309
+ if (isQueueEventSource(eventSource)) {
310
+ (options.queueConsumers ??= {})[queue.name] = {
311
+ maxBatchSize: eventSource.settings?.batchSize,
312
+ maxBatchTimeout: eventSource.settings?.maxWaitTimeMs,
313
+ maxRetries: eventSource.settings?.maxRetries,
314
+ retryDelay: eventSource.settings?.retryDelay,
315
+ };
316
+ } else {
317
+ (options.queueConsumers ??= {})[eventSource.name] = {};
318
+ }
319
+ }
320
+ async function* watch(signal: AbortSignal) {
321
+ for await (const bundle of input.bundle.watch(signal)) {
322
+ const { modules, rootPath } = normalizeBundle(bundle);
323
+ yield {
324
+ ...options,
325
+ modules,
326
+ rootPath,
327
+ };
328
+ }
329
+ }
330
+ if (remoteBindings.length > 0) {
331
+ const remoteProxy = await createRemoteProxyWorker({
332
+ name: input.name,
333
+ bindings: remoteBindings,
334
+ });
335
+ for (const binding of remoteBindings) {
336
+ switch (binding.type) {
337
+ case "ai":
338
+ options.ai = {
339
+ binding: binding.name,
340
+ remoteProxyConnectionString: remoteProxy.connectionString,
341
+ };
342
+ break;
343
+ case "browser":
344
+ options.browserRendering = {
345
+ binding: binding.name,
346
+ remoteProxyConnectionString: remoteProxy.connectionString,
347
+ };
348
+ break;
349
+ case "d1":
350
+ (options.d1Databases ??= {})[binding.name] = {
351
+ id: binding.id,
352
+ remoteProxyConnectionString: remoteProxy.connectionString,
353
+ };
354
+ break;
355
+ case "dispatch_namespace":
356
+ (options.dispatchNamespaces ??= {})[binding.name] = {
357
+ namespace: binding.namespace,
358
+ remoteProxyConnectionString: remoteProxy.connectionString,
359
+ };
360
+ break;
361
+ case "kv_namespace":
362
+ (options.kvNamespaces ??= {})[binding.name] = {
363
+ id: binding.namespace_id,
364
+ remoteProxyConnectionString: remoteProxy.connectionString,
365
+ };
366
+ break;
367
+ case "images":
368
+ options.images = {
369
+ binding: binding.name,
370
+ remoteProxyConnectionString: remoteProxy.connectionString,
371
+ };
372
+ break;
373
+ case "mtls_certificate":
374
+ (options.mtlsCertificates ??= {})[binding.name] = {
375
+ certificate_id: binding.certificate_id,
376
+ remoteProxyConnectionString: remoteProxy.connectionString,
377
+ };
378
+ break;
379
+ case "queue":
380
+ (options.queueProducers ??= {})[binding.name] = {
381
+ queueName: binding.queue_name,
382
+ remoteProxyConnectionString: remoteProxy.connectionString,
383
+ };
384
+ break;
385
+ case "r2_bucket":
386
+ (options.r2Buckets ??= {})[binding.name] = {
387
+ id: binding.bucket_name,
388
+ remoteProxyConnectionString: remoteProxy.connectionString,
389
+ };
390
+ break;
391
+ case "service":
392
+ (options.serviceBindings ??= {})[binding.name] = {
393
+ name: binding.name,
394
+ remoteProxyConnectionString: remoteProxy.connectionString,
395
+ };
396
+ break;
397
+ case "vectorize":
398
+ (options.vectorize ??= {})[binding.name] = {
399
+ index_name: binding.index_name,
400
+ remoteProxyConnectionString: remoteProxy.connectionString,
401
+ };
402
+ break;
403
+ default: {
404
+ assertNever(binding);
405
+ }
406
+ }
407
+ }
408
+ return {
409
+ watch,
410
+ remoteProxy: remoteProxy.server,
411
+ };
412
+ }
413
+ return {
414
+ watch,
415
+ remoteProxy: undefined,
416
+ };
417
+ };
418
+
419
+ const moduleTypes = {
420
+ esm: "ESModule",
421
+ cjs: "CommonJS",
422
+ text: "Text",
423
+ data: "Data",
424
+ wasm: "CompiledWasm",
425
+ sourcemap: "Text",
426
+ } as const;
427
+
428
+ const normalizeBundle = (bundle: WorkerBundle) => {
429
+ const modules = bundle.modules.map((module) => ({
430
+ type: moduleTypes[module.type],
431
+ path: module.path,
432
+ contents: module.content,
433
+ }));
434
+ const entry = modules.find((module) => module.path === bundle.entrypoint);
435
+ if (!entry) {
436
+ throw new Error(`Entrypoint "${bundle.entrypoint}" not found in bundle.`);
437
+ }
438
+ return {
439
+ modules: [entry, ...modules.filter((module) => module.path !== entry.path)],
440
+ rootPath: bundle.root,
441
+ };
442
+ };
443
+
444
+ const isRemoteBinding = (binding: Binding) => {
445
+ if (typeof binding === "string" || binding === Self) {
446
+ return false;
447
+ }
448
+ return (
449
+ "dev" in binding &&
450
+ typeof binding.dev === "object" &&
451
+ "remote" in binding.dev &&
452
+ !!binding.dev.remote
453
+ );
454
+ };
@@ -0,0 +1,128 @@
1
+ import * as miniflare from "miniflare";
2
+ import assert from "node:assert";
3
+ import path from "node:path";
4
+ import { findOpenPort } from "../../util/find-open-port.ts";
5
+ import type { HTTPServer } from "../../util/http.ts";
6
+ import { logger } from "../../util/logger.ts";
7
+ import { AsyncMutex } from "../../util/mutex.ts";
8
+ import {
9
+ buildWorkerOptions,
10
+ type MiniflareWorkerInput,
11
+ } from "./build-worker-options.ts";
12
+ import { MiniflareWorkerProxy } from "./miniflare-worker-proxy.ts";
13
+
14
+ declare global {
15
+ var ALCHEMY_MINIFLARE_CONTROLLER: MiniflareController | undefined;
16
+ }
17
+
18
+ export class MiniflareController {
19
+ abort = new AbortController();
20
+ miniflare: miniflare.Miniflare | undefined;
21
+ options = new Map<string, miniflare.WorkerOptions>();
22
+ localProxies = new Map<string, MiniflareWorkerProxy>();
23
+ remoteProxies = new Map<string, HTTPServer>();
24
+ mutex = new AsyncMutex();
25
+
26
+ static get singleton() {
27
+ globalThis.ALCHEMY_MINIFLARE_CONTROLLER ??= new MiniflareController();
28
+ return globalThis.ALCHEMY_MINIFLARE_CONTROLLER;
29
+ }
30
+
31
+ async add(input: MiniflareWorkerInput) {
32
+ const { watch, remoteProxy } = await buildWorkerOptions(input);
33
+ if (remoteProxy) {
34
+ this.remoteProxies.set(input.name, remoteProxy);
35
+ }
36
+ const watcher = watch(this.abort.signal);
37
+ const first = await watcher.next();
38
+ assert(first.value, "First value is undefined");
39
+ this.options.set(input.name, first.value);
40
+ const miniflare = await this.update();
41
+ const proxy = new MiniflareWorkerProxy({
42
+ name: input.name,
43
+ port: input.port ?? (await findOpenPort()),
44
+ miniflare,
45
+ });
46
+ this.localProxies.set(input.name, proxy);
47
+ void this.watch(input.id, watcher);
48
+ logger.task(input.id, {
49
+ message: `Ready at ${proxy.url}`,
50
+ status: "success",
51
+ resource: input.id,
52
+ prefix: "dev",
53
+ prefixColor: "cyanBright",
54
+ });
55
+ return proxy.url;
56
+ }
57
+
58
+ private async watch(
59
+ id: string,
60
+ watcher: AsyncGenerator<miniflare.WorkerOptions>,
61
+ ) {
62
+ for await (const options of watcher) {
63
+ this.options.set(options.name!, options);
64
+ await this.update();
65
+ logger.task(id, {
66
+ message: "Updated",
67
+ status: "success",
68
+ resource: id,
69
+ prefix: "dev",
70
+ prefixColor: "cyanBright",
71
+ });
72
+ }
73
+ }
74
+
75
+ private async update() {
76
+ return await this.mutex.lock(async () => {
77
+ const options: miniflare.MiniflareOptions = {
78
+ workers: Array.from(this.options.values()),
79
+ defaultPersistRoot: path.join(process.cwd(), ".alchemy/miniflare"),
80
+ unsafeDevRegistryPath: miniflare.getDefaultDevRegistryPath(),
81
+ analyticsEngineDatasetsPersist: true,
82
+ cachePersist: true,
83
+ d1Persist: true,
84
+ durableObjectsPersist: true,
85
+ kvPersist: true,
86
+ r2Persist: true,
87
+ secretsStorePersist: true,
88
+ workflowsPersist: true,
89
+ log: process.env.DEBUG
90
+ ? new miniflare.Log(miniflare.LogLevel.DEBUG)
91
+ : undefined,
92
+ };
93
+ return await this.setMiniflareOptions(options);
94
+ });
95
+ }
96
+
97
+ private async setMiniflareOptions(options: miniflare.MiniflareOptions) {
98
+ try {
99
+ if (this.miniflare) {
100
+ await this.miniflare.setOptions(options);
101
+ } else {
102
+ this.miniflare = new miniflare.Miniflare(options);
103
+ await this.miniflare.ready;
104
+ }
105
+ return this.miniflare;
106
+ } catch (error) {
107
+ if (
108
+ error instanceof miniflare.MiniflareCoreError &&
109
+ error.code === "ERR_MODULE_STRING_SCRIPT"
110
+ ) {
111
+ throw new Error(
112
+ 'Miniflare detected an external dependency that could not be resolved. This typically occurs when the "nodejs_compat" or "nodejs_als" compatibility flag is not enabled.',
113
+ );
114
+ } else {
115
+ throw error;
116
+ }
117
+ }
118
+ }
119
+
120
+ async dispose() {
121
+ this.abort.abort();
122
+ await Promise.all([
123
+ this.miniflare?.dispose(),
124
+ ...this.localProxies.values().map((proxy) => proxy.close()),
125
+ ...this.remoteProxies.values().map((proxy) => proxy.close()),
126
+ ]);
127
+ }
128
+ }
@@ -55,18 +55,10 @@ export class MiniflareWorkerProxy {
55
55
  return;
56
56
  }
57
57
  this.wss.handleUpgrade(req, socket, head, (client) => {
58
- client.on("message", (event, binary) => {
59
- server.send(event, { binary });
60
- });
61
- client.on("close", (code, reason) => {
62
- server.close(code, reason);
63
- });
64
- server.on("message", (event, binary) => {
65
- client.send(event, { binary });
66
- });
67
- server.on("close", (code, reason) => {
68
- client.close(code, reason);
69
- });
58
+ client.on("message", (event, binary) => server.send(event, { binary }));
59
+ client.on("close", (code, reason) => server.close(code, reason));
60
+ server.on("message", (event, binary) => client.send(event, { binary }));
61
+ server.on("close", (code, reason) => client.close(code, reason));
70
62
  this.wss.emit("connection", client, req);
71
63
  });
72
64
  }
@@ -157,9 +149,8 @@ const writeMiniflareResponseToNode = (
157
149
  out: http.ServerResponse,
158
150
  ) => {
159
151
  out.statusCode = res.status;
160
- res.headers.forEach((value, key) => {
161
- out.setHeader(key, value);
162
- });
152
+ res.headers.forEach((value, key) => out.setHeader(key, value));
153
+
163
154
  if (res.body) {
164
155
  Readable.fromWeb(res.body).pipe(out, { end: true });
165
156
  } else {