devflare 1.0.0-next.1 → 1.0.0-next.10

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 (123) hide show
  1. package/LLM.md +775 -637
  2. package/R2.md +200 -0
  3. package/README.md +285 -514
  4. package/bin/devflare.js +8 -8
  5. package/dist/{account-rvrj687w.js → account-8psavtg6.js} +27 -4
  6. package/dist/bridge/miniflare.d.ts +6 -0
  7. package/dist/bridge/miniflare.d.ts.map +1 -1
  8. package/dist/bridge/proxy.d.ts +5 -6
  9. package/dist/bridge/proxy.d.ts.map +1 -1
  10. package/dist/bridge/server.d.ts.map +1 -1
  11. package/dist/browser.d.ts +50 -0
  12. package/dist/browser.d.ts.map +1 -0
  13. package/dist/{build-mnf6v8gd.js → build-k36xrzvy.js} +26 -7
  14. package/dist/bundler/do-bundler.d.ts +7 -0
  15. package/dist/bundler/do-bundler.d.ts.map +1 -1
  16. package/dist/cli/commands/account.d.ts.map +1 -1
  17. package/dist/cli/commands/build.d.ts.map +1 -1
  18. package/dist/cli/commands/deploy.d.ts.map +1 -1
  19. package/dist/cli/commands/dev.d.ts.map +1 -1
  20. package/dist/cli/commands/doctor.d.ts.map +1 -1
  21. package/dist/cli/commands/init.d.ts.map +1 -1
  22. package/dist/cli/commands/types.d.ts.map +1 -1
  23. package/dist/cli/config-path.d.ts +5 -0
  24. package/dist/cli/config-path.d.ts.map +1 -0
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/package-metadata.d.ts +16 -0
  27. package/dist/cli/package-metadata.d.ts.map +1 -0
  28. package/dist/config/compiler.d.ts +7 -0
  29. package/dist/config/compiler.d.ts.map +1 -1
  30. package/dist/config/index.d.ts +1 -1
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/schema.d.ts +2575 -1221
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/{deploy-nhceck39.js → deploy-dbvfq8vq.js} +33 -15
  35. package/dist/{dev-qnxet3j9.js → dev-rk8p6pse.js} +900 -234
  36. package/dist/dev-server/miniflare-log.d.ts +12 -0
  37. package/dist/dev-server/miniflare-log.d.ts.map +1 -0
  38. package/dist/dev-server/runtime-stdio.d.ts +8 -0
  39. package/dist/dev-server/runtime-stdio.d.ts.map +1 -0
  40. package/dist/dev-server/server.d.ts +2 -0
  41. package/dist/dev-server/server.d.ts.map +1 -1
  42. package/dist/dev-server/vite-utils.d.ts +37 -0
  43. package/dist/dev-server/vite-utils.d.ts.map +1 -0
  44. package/dist/{doctor-e8fy6fj5.js → doctor-06y8nxd4.js} +73 -50
  45. package/dist/{durable-object-t4kbb0yt.js → durable-object-yt8v1dyn.js} +1 -1
  46. package/dist/index-05fyzwne.js +195 -0
  47. package/dist/index-1p814k7s.js +227 -0
  48. package/dist/{index-hcex3rgh.js → index-1phx14av.js} +84 -7
  49. package/dist/{index-tk6ej9dj.js → index-2q3pmzrx.js} +12 -16
  50. package/dist/{index-pf5s73n9.js → index-59df49vn.js} +11 -281
  51. package/dist/index-5yxg30va.js +304 -0
  52. package/dist/index-62b3gt2g.js +12 -0
  53. package/dist/index-6h8xbs75.js +44 -0
  54. package/dist/{index-67qcae0f.js → index-6v3wjg1r.js} +16 -1
  55. package/dist/index-8gtqgb3q.js +529 -0
  56. package/dist/{index-gz1gndna.js → index-9wt9x09k.js} +42 -62
  57. package/dist/index-fef08w43.js +231 -0
  58. package/dist/{index-ep3445yc.js → index-jht2j546.js} +393 -170
  59. package/dist/index-k7r18na8.js +0 -0
  60. package/dist/{index-m2q41jwa.js → index-n932ytmq.js} +9 -1
  61. package/dist/index-pwgyy2q9.js +39 -0
  62. package/dist/{index-07q6yxyc.js → index-v8vvsn9x.js} +1 -0
  63. package/dist/index-vky23txa.js +70 -0
  64. package/dist/index-vs49yxn4.js +322 -0
  65. package/dist/{index-z14anrqp.js → index-wfbfz02q.js} +14 -15
  66. package/dist/index-ws68xvq2.js +311 -0
  67. package/dist/index-y1d8za14.js +196 -0
  68. package/dist/{init-f9mgmew3.js → init-na2atvz2.js} +42 -55
  69. package/dist/router/types.d.ts +24 -0
  70. package/dist/router/types.d.ts.map +1 -0
  71. package/dist/runtime/context.d.ts +249 -8
  72. package/dist/runtime/context.d.ts.map +1 -1
  73. package/dist/runtime/exports.d.ts +50 -55
  74. package/dist/runtime/exports.d.ts.map +1 -1
  75. package/dist/runtime/index.d.ts +8 -1
  76. package/dist/runtime/index.d.ts.map +1 -1
  77. package/dist/runtime/middleware.d.ts +77 -60
  78. package/dist/runtime/middleware.d.ts.map +1 -1
  79. package/dist/runtime/router.d.ts +7 -0
  80. package/dist/runtime/router.d.ts.map +1 -0
  81. package/dist/runtime/validation.d.ts +1 -1
  82. package/dist/runtime/validation.d.ts.map +1 -1
  83. package/dist/src/browser.js +150 -0
  84. package/dist/src/cli/index.js +10 -0
  85. package/dist/{cloudflare → src/cloudflare}/index.js +3 -3
  86. package/dist/{decorators → src/decorators}/index.js +2 -2
  87. package/dist/src/index.js +132 -0
  88. package/dist/src/runtime/index.js +111 -0
  89. package/dist/{sveltekit → src/sveltekit}/index.js +14 -6
  90. package/dist/{test → src/test}/index.js +22 -13
  91. package/dist/{vite → src/vite}/index.js +128 -59
  92. package/dist/sveltekit/platform.d.ts.map +1 -1
  93. package/dist/test/bridge-context.d.ts +5 -2
  94. package/dist/test/bridge-context.d.ts.map +1 -1
  95. package/dist/test/cf.d.ts +25 -11
  96. package/dist/test/cf.d.ts.map +1 -1
  97. package/dist/test/email.d.ts +16 -7
  98. package/dist/test/email.d.ts.map +1 -1
  99. package/dist/test/queue.d.ts.map +1 -1
  100. package/dist/test/resolve-service-bindings.d.ts.map +1 -1
  101. package/dist/test/scheduled.d.ts.map +1 -1
  102. package/dist/test/simple-context.d.ts +1 -1
  103. package/dist/test/simple-context.d.ts.map +1 -1
  104. package/dist/test/tail.d.ts +2 -1
  105. package/dist/test/tail.d.ts.map +1 -1
  106. package/dist/test/worker.d.ts +6 -0
  107. package/dist/test/worker.d.ts.map +1 -1
  108. package/dist/transform/durable-object.d.ts.map +1 -1
  109. package/dist/transform/worker-entrypoint.d.ts.map +1 -1
  110. package/dist/{types-5nyrz1sz.js → types-x9q7t491.js} +30 -16
  111. package/dist/utils/entrypoint-discovery.d.ts +6 -3
  112. package/dist/utils/entrypoint-discovery.d.ts.map +1 -1
  113. package/dist/utils/send-email.d.ts +15 -0
  114. package/dist/utils/send-email.d.ts.map +1 -0
  115. package/dist/vite/plugin.d.ts.map +1 -1
  116. package/dist/worker-entry/composed-worker.d.ts +13 -0
  117. package/dist/worker-entry/composed-worker.d.ts.map +1 -0
  118. package/dist/worker-entry/routes.d.ts +22 -0
  119. package/dist/worker-entry/routes.d.ts.map +1 -0
  120. package/dist/{worker-entrypoint-m9th0rg0.js → worker-entrypoint-c259fmfs.js} +1 -1
  121. package/package.json +21 -19
  122. package/dist/index.js +0 -298
  123. package/dist/runtime/index.js +0 -111
@@ -1,34 +1,62 @@
1
+ import {
2
+ getRemoteModeStatus,
3
+ isRemoteModeActive
4
+ } from "./index-d8bdkx2h.js";
1
5
  import {
2
6
  transformWorkerEntrypoint
3
- } from "./index-z14anrqp.js";
7
+ } from "./index-wfbfz02q.js";
4
8
  import {
5
9
  discoverEntrypointsSync,
6
10
  resolvePackageSpecifier
7
- } from "./index-tk6ej9dj.js";
11
+ } from "./index-2q3pmzrx.js";
8
12
  import {
9
- getRemoteModeStatus,
10
- isRemoteModeActive
11
- } from "./index-d8bdkx2h.js";
13
+ __clearTestContext,
14
+ __setTestContext
15
+ } from "./index-vky23txa.js";
16
+ import {
17
+ createRouteResolve,
18
+ invokeFetchModule,
19
+ matchFetchRoute,
20
+ resolveFetchHandler
21
+ } from "./index-8gtqgb3q.js";
22
+ import {
23
+ createEmailEvent,
24
+ createFetchEvent,
25
+ createQueueEvent,
26
+ createScheduledEvent,
27
+ createTailEvent,
28
+ runWithContext,
29
+ runWithEventContext
30
+ } from "./index-5yxg30va.js";
31
+ import {
32
+ discoverRoutes
33
+ } from "./index-1p814k7s.js";
34
+ import {
35
+ findDurableObjectClasses
36
+ } from "./index-9wt9x09k.js";
12
37
  import {
13
38
  DEFAULT_DO_PATTERN,
14
39
  findFiles,
15
40
  findFilesSync
16
41
  } from "./index-rbht7m9r.js";
17
42
  import {
18
- findDurableObjectClasses
19
- } from "./index-gz1gndna.js";
43
+ startMiniflare,
44
+ startMiniflareFromConfig
45
+ } from "./index-vs49yxn4.js";
20
46
  import {
21
47
  BridgeClient,
22
- bridgeEnv,
23
48
  createEnvProxy,
24
- setBindingHints,
25
- startMiniflare,
26
- startMiniflareFromConfig
27
- } from "./index-pf5s73n9.js";
49
+ setBindingHints
50
+ } from "./index-59df49vn.js";
51
+ import {
52
+ createLocalSendEmailBinding,
53
+ wrapEnvSendEmailBindings
54
+ } from "./index-fef08w43.js";
28
55
  import {
29
56
  loadConfig,
30
- normalizeDOBinding
31
- } from "./index-hcex3rgh.js";
57
+ normalizeDOBinding,
58
+ resolveConfigPath
59
+ } from "./index-1phx14av.js";
32
60
  import {
33
61
  canProceedWithTest,
34
62
  getApiToken,
@@ -41,7 +69,7 @@ import {
41
69
  } from "./index-37x76zdn.js";
42
70
 
43
71
  // src/test/simple-context.ts
44
- import { resolve as resolve2, dirname as dirname2, join as join6 } from "path";
72
+ import { resolve as resolve2, dirname as dirname2, join as join7 } from "path";
45
73
  import { existsSync as existsSync2 } from "fs";
46
74
 
47
75
  // src/test/remote-ai.ts
@@ -242,6 +270,15 @@ var bundleCache = new Map;
242
270
  function clearBundleCache() {
243
271
  bundleCache.clear();
244
272
  }
273
+ function findDefaultServiceWorkerEntrypoint(refConfigDir) {
274
+ for (const candidate of ["src/worker.ts", "src/worker.js"]) {
275
+ const absolutePath = resolve(refConfigDir, candidate);
276
+ if (existsSync(absolutePath)) {
277
+ return absolutePath;
278
+ }
279
+ }
280
+ return null;
281
+ }
245
282
  function hasServiceBindings(config) {
246
283
  const services = config.bindings?.services;
247
284
  if (!services)
@@ -297,24 +334,23 @@ async function resolveRefWorker(ref, _entrypoint, parentConfigDir) {
297
334
  }
298
335
  const refConfigDir = resolve(parentConfigDir, dirname(configPath));
299
336
  const entrypoints = [];
300
- const files = config.files;
301
- const mainPath = files?.fetch ?? "src/worker.ts";
302
- const workerTsPath = resolve(refConfigDir, mainPath);
303
- if (existsSync(workerTsPath)) {
304
- const isWorkerTs = mainPath.endsWith("worker.ts") || mainPath.endsWith("worker.js");
337
+ const workerEntrypointPath = findDefaultServiceWorkerEntrypoint(refConfigDir);
338
+ if (workerEntrypointPath) {
305
339
  entrypoints.push({
306
- path: workerTsPath,
340
+ path: workerEntrypointPath,
307
341
  className: "Worker",
308
- isWorkerTs
342
+ isWorkerTs: true
309
343
  });
310
344
  }
311
- const discoveredEntrypoints = discoverEntrypointsSync(refConfigDir);
312
- for (const ep of discoveredEntrypoints) {
313
- entrypoints.push({
314
- path: ep.filePath,
315
- className: ep.className,
316
- isWorkerTs: false
317
- });
345
+ if (config.files?.entrypoints !== false) {
346
+ const discoveredEntrypoints = discoverEntrypointsSync(refConfigDir, typeof config.files?.entrypoints === "string" ? config.files.entrypoints : undefined);
347
+ for (const ep of discoveredEntrypoints) {
348
+ entrypoints.push({
349
+ path: ep.filePath,
350
+ className: ep.className,
351
+ isWorkerTs: false
352
+ });
353
+ }
318
354
  }
319
355
  if (entrypoints.length === 0) {
320
356
  console.warn(`[devflare] Worker "${ref.name}" has no entry points`);
@@ -574,86 +610,6 @@ export default {
574
610
  }
575
611
  }
576
612
 
577
- // src/runtime/context.ts
578
- import { AsyncLocalStorage } from "node:async_hooks";
579
- var storage = new AsyncLocalStorage;
580
- function runWithContext(env, ctx, request, fn, type = "fetch") {
581
- const context = {
582
- env,
583
- ctx,
584
- request,
585
- locals: {},
586
- type
587
- };
588
- return storage.run(context, fn);
589
- }
590
- function getContextOrNull() {
591
- const context = storage.getStore();
592
- return context ?? null;
593
- }
594
-
595
- // src/env.ts
596
- var testContextEnv = null;
597
- var testContextDispose = null;
598
- function __setTestContext(envBindings, dispose) {
599
- testContextEnv = envBindings;
600
- testContextDispose = dispose;
601
- }
602
- function __clearTestContext() {
603
- testContextEnv = null;
604
- testContextDispose = null;
605
- }
606
- var env = new Proxy({}, {
607
- get(_target, prop) {
608
- if (prop === "dispose") {
609
- return async () => {
610
- if (testContextDispose) {
611
- await testContextDispose();
612
- __clearTestContext();
613
- }
614
- };
615
- }
616
- const ctx = getContextOrNull();
617
- if (ctx?.env) {
618
- return ctx.env[prop];
619
- }
620
- if (testContextEnv) {
621
- return testContextEnv[prop];
622
- }
623
- return bridgeEnv[prop];
624
- },
625
- has(_target, prop) {
626
- if (prop === "dispose")
627
- return true;
628
- const ctx = getContextOrNull();
629
- if (ctx?.env) {
630
- return prop in ctx.env;
631
- }
632
- if (testContextEnv) {
633
- return prop in testContextEnv;
634
- }
635
- return prop in bridgeEnv;
636
- },
637
- ownKeys(_target) {
638
- const ctx = getContextOrNull();
639
- if (ctx?.env) {
640
- return Reflect.ownKeys(ctx.env);
641
- }
642
- if (testContextEnv) {
643
- return Reflect.ownKeys(testContextEnv);
644
- }
645
- return Reflect.ownKeys(bridgeEnv);
646
- },
647
- getOwnPropertyDescriptor(_target, prop) {
648
- if (prop === "dispose") {
649
- return { configurable: true, enumerable: false, writable: false };
650
- }
651
- const ctx = getContextOrNull();
652
- const source = ctx?.env ?? testContextEnv ?? bridgeEnv;
653
- return Reflect.getOwnPropertyDescriptor(source, prop);
654
- }
655
- });
656
-
657
613
  // src/test/queue.ts
658
614
  import { join as join2 } from "path";
659
615
  var queueHandlerPath = null;
@@ -721,8 +677,8 @@ async function trigger(messages) {
721
677
  const queueHandler = handlerModule.default ?? handlerModule.queue;
722
678
  if (typeof queueHandler !== "function") {
723
679
  throw new Error(`Queue handler at "${queueHandlerPath}" must export a default function or named "queue" export.
724
- Expected: export default async function queue(batch, env, ctx) { ... }
725
- Or: export async function queue(batch, env, ctx) { ... }`);
680
+ Expected: export async function queue(event) { ... }
681
+ Legacy compatibility is still supported for queue(batch, env, ctx).`);
726
682
  }
727
683
  const normalizedMessages = messages.map((msg) => {
728
684
  if (typeof msg === "object" && msg !== null && "body" in msg) {
@@ -740,8 +696,9 @@ Or: export async function queue(batch, env, ctx) { ... }`);
740
696
  passThroughOnException() {},
741
697
  props: {}
742
698
  };
743
- const env2 = testEnvGetter();
744
- await queueHandler(batch, env2, ctx);
699
+ const env = testEnvGetter();
700
+ const queueEvent = createQueueEvent(batch, env, ctx);
701
+ await runWithEventContext(queueEvent, () => queueHandler(queueEvent, env, ctx));
745
702
  await Promise.all(waitUntilPromises);
746
703
  const acked = [];
747
704
  const retried = [];
@@ -804,8 +761,8 @@ async function trigger2(cronOrOptions) {
804
761
  const scheduledHandler = handlerModule.default ?? handlerModule.scheduled;
805
762
  if (typeof scheduledHandler !== "function") {
806
763
  throw new Error(`Scheduled handler at "${scheduledHandlerPath}" must export a default function or named "scheduled" export.
807
- Expected: export default async function scheduled(controller, env, ctx) { ... }
808
- Or: export async function scheduled(controller, env, ctx) { ... }`);
764
+ Expected: export async function scheduled(event) { ... }
765
+ Legacy compatibility is still supported for scheduled(controller, env, ctx).`);
809
766
  }
810
767
  const controller = {
811
768
  scheduledTime,
@@ -820,9 +777,10 @@ Or: export async function scheduled(controller, env, ctx) { ... }`);
820
777
  passThroughOnException() {},
821
778
  props: {}
822
779
  };
823
- const env2 = testEnvGetter2();
780
+ const env = testEnvGetter2();
781
+ const scheduledEvent = createScheduledEvent(controller, env, ctx);
824
782
  try {
825
- await scheduledHandler(controller, env2, ctx);
783
+ await runWithEventContext(scheduledEvent, () => scheduledHandler(scheduledEvent, env, ctx));
826
784
  await Promise.all(waitUntilPromises);
827
785
  return {
828
786
  success: true,
@@ -847,23 +805,28 @@ import { join as join4 } from "path";
847
805
  var fetchHandlerPath = null;
848
806
  var configDir3 = null;
849
807
  var testEnvGetter3 = null;
808
+ var fileRoutes = [];
850
809
  function configureWorker(options) {
851
810
  fetchHandlerPath = options.handlerPath;
811
+ fileRoutes = options.routes ?? [];
852
812
  configDir3 = options.configDir;
853
813
  testEnvGetter3 = options.getEnv;
854
814
  }
855
815
  function resetWorkerState() {
856
816
  fetchHandlerPath = null;
817
+ fileRoutes = [];
857
818
  configDir3 = null;
858
819
  testEnvGetter3 = null;
859
820
  }
860
821
  async function fetch2(request, options) {
861
- if (!fetchHandlerPath) {
862
- throw new Error("Fetch handler not configured. Make sure your devflare.config.ts has files.fetch set, " + "and the file exists at the specified path (default: src/fetch.ts)");
822
+ if (!fetchHandlerPath && fileRoutes.length === 0) {
823
+ throw new Error("Fetch handler not configured. Make sure your devflare.config.ts has files.fetch set or a routes directory is available, " + "and that the corresponding files exist (defaults: src/fetch.ts and src/routes/**).");
863
824
  }
864
825
  if (!configDir3 || !testEnvGetter3) {
865
826
  throw new Error("Worker helper not initialized. Call createTestContext() before using cf.worker.fetch()");
866
827
  }
828
+ const workerConfigDir = configDir3;
829
+ const getEnv = testEnvGetter3;
867
830
  let req;
868
831
  if (typeof request === "string") {
869
832
  const url = request.startsWith("http") ? request : `http://localhost${request.startsWith("/") ? "" : "/"}${request}`;
@@ -887,13 +850,24 @@ async function fetch2(request, options) {
887
850
  } else {
888
851
  req = request;
889
852
  }
890
- const absolutePath = join4(configDir3, fetchHandlerPath);
891
- const handlerModule = await import(absolutePath);
892
- const fetchHandler = handlerModule.default ?? handlerModule.fetch;
893
- if (typeof fetchHandler !== "function") {
894
- throw new Error(`Fetch handler at "${fetchHandlerPath}" must export a default function or named "fetch" export.
895
- Expected: export default async function fetch(request, env, ctx) { ... }
896
- Or: export async function fetch(request, env, ctx) { ... }`);
853
+ const handlerModule = fetchHandlerPath ? await import(join4(workerConfigDir, fetchHandlerPath)) : {};
854
+ const routeModules = await Promise.all(fileRoutes.map(async (route) => {
855
+ return {
856
+ ...route,
857
+ module: await import(join4(workerConfigDir, route.filePath))
858
+ };
859
+ }));
860
+ const methodExports = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "ALL"];
861
+ const hasMethodHandler = methodExports.some((method) => {
862
+ return typeof handlerModule[method] === "function" || typeof handlerModule.default?.[method] === "function";
863
+ });
864
+ if (!resolveFetchHandler(handlerModule) && !hasMethodHandler && routeModules.length === 0) {
865
+ throw new Error(`Fetch handler at "${fetchHandlerPath}" must export one of:
866
+ - request-wide "handle" middleware
867
+ - named "fetch"
868
+ - default fetch handler
869
+ - HTTP method exports such as "GET" or "POST"
870
+ Legacy compatibility is still supported for fetch(request, env, ctx).`);
897
871
  }
898
872
  const waitUntilPromises = [];
899
873
  const ctx = {
@@ -903,8 +877,12 @@ Or: export async function fetch(request, env, ctx) { ... }`);
903
877
  passThroughOnException() {},
904
878
  props: {}
905
879
  };
906
- const env2 = testEnvGetter3();
907
- const response = await fetchHandler(req, env2, ctx);
880
+ const env = getEnv();
881
+ const initialRouteMatch = routeModules.length > 0 ? matchFetchRoute(routeModules, req) : null;
882
+ const fetchEvent = createFetchEvent(req, env, ctx, {
883
+ params: initialRouteMatch?.params ?? {}
884
+ });
885
+ const response = await runWithEventContext(fetchEvent, () => invokeFetchModule(handlerModule, fetchEvent, routeModules.length > 0 ? createRouteResolve(routeModules, fetchEvent) : undefined));
908
886
  return response;
909
887
  }
910
888
  async function get(path, headers) {
@@ -967,7 +945,7 @@ function createTraceItem(options) {
967
945
  }
968
946
  async function trigger3(items) {
969
947
  if (!tailHandlerPath) {
970
- throw new Error("Tail handler not configured. Make sure your devflare.config.ts has files.tail set, " + "or the file exists at src/tail.ts");
948
+ throw new Error("Tail handler not configured. Add a src/tail.ts file exporting tail(), " + "or configure a tail handler before calling cf.tail.trigger().");
971
949
  }
972
950
  if (!configDir4 || !testEnvGetter4) {
973
951
  throw new Error("Tail helper not initialized. Call createTestContext() before using cf.tail.trigger()");
@@ -983,8 +961,8 @@ async function trigger3(items) {
983
961
  const tailHandler = handlerModule.default ?? handlerModule.tail;
984
962
  if (typeof tailHandler !== "function") {
985
963
  throw new Error(`Tail handler at "${tailHandlerPath}" must export a default function or named "tail" export.
986
- Expected: export default async function tail(events, env, ctx) { ... }
987
- Or: export async function tail(events, env, ctx) { ... }`);
964
+ Expected: export async function tail(event) { ... }
965
+ Legacy compatibility is still supported for tail(events, env, ctx).`);
988
966
  }
989
967
  const waitUntilPromises = [];
990
968
  const ctx = {
@@ -994,9 +972,10 @@ Or: export async function tail(events, env, ctx) { ... }`);
994
972
  passThroughOnException() {},
995
973
  props: {}
996
974
  };
997
- const env2 = testEnvGetter4();
975
+ const env = testEnvGetter4();
976
+ const tailEvent = createTailEvent(traceItems, env, ctx);
998
977
  try {
999
- await tailHandler(traceItems, env2, ctx);
978
+ await runWithEventContext(tailEvent, () => tailHandler(tailEvent, env, ctx));
1000
979
  await Promise.all(waitUntilPromises);
1001
980
  return {
1002
981
  success: true,
@@ -1019,13 +998,20 @@ var tail = {
1019
998
  };
1020
999
 
1021
1000
  // src/test/email.ts
1001
+ import { join as join6 } from "path";
1022
1002
  var miniflarePort = 8787;
1023
1003
  var emailListeners = [];
1024
1004
  var sentEmails = [];
1005
+ var emailHandlerPath = null;
1006
+ var configDir5 = null;
1007
+ var testEnvGetter5 = null;
1025
1008
  function configureEmail(options = {}) {
1026
1009
  if (options.port) {
1027
1010
  miniflarePort = options.port;
1028
1011
  }
1012
+ emailHandlerPath = options.handlerPath ?? emailHandlerPath;
1013
+ configDir5 = options.configDir ?? configDir5;
1014
+ testEnvGetter5 = options.getEnv ?? testEnvGetter5;
1029
1015
  }
1030
1016
  function buildRawEmail(options) {
1031
1017
  if (options.raw) {
@@ -1053,8 +1039,103 @@ function buildRawEmail(options) {
1053
1039
  return lines.join(`\r
1054
1040
  `);
1055
1041
  }
1042
+ function createEmailHeaders(rawEmail) {
1043
+ const headers = new Headers;
1044
+ const lines = rawEmail.split(/\r?\n/);
1045
+ for (const line of lines) {
1046
+ if (!line.trim()) {
1047
+ break;
1048
+ }
1049
+ const colonIndex = line.indexOf(":");
1050
+ if (colonIndex <= 0) {
1051
+ continue;
1052
+ }
1053
+ headers.append(line.slice(0, colonIndex).trim(), line.slice(colonIndex + 1).trim());
1054
+ }
1055
+ return headers;
1056
+ }
1057
+ function createRawEmailStream(rawEmail) {
1058
+ return new ReadableStream({
1059
+ start(controller) {
1060
+ controller.enqueue(new TextEncoder().encode(rawEmail));
1061
+ controller.close();
1062
+ }
1063
+ });
1064
+ }
1065
+ function resolveEmailHandler(module) {
1066
+ if (typeof module.default === "function") {
1067
+ return module.default;
1068
+ }
1069
+ if (module.default && typeof module.default.email === "function") {
1070
+ return module.default.email.bind(module.default);
1071
+ }
1072
+ if (typeof module.email === "function") {
1073
+ return module.email;
1074
+ }
1075
+ return null;
1076
+ }
1077
+ function getRecordedRawContent(raw) {
1078
+ if (typeof raw === "string") {
1079
+ return raw;
1080
+ }
1081
+ return;
1082
+ }
1056
1083
  async function send2(options) {
1057
1084
  const raw = buildRawEmail(options);
1085
+ if (emailHandlerPath && configDir5 && testEnvGetter5) {
1086
+ const absolutePath = join6(configDir5, emailHandlerPath);
1087
+ const handlerModule = await import(absolutePath);
1088
+ const emailHandler = resolveEmailHandler(handlerModule);
1089
+ if (!emailHandler) {
1090
+ throw new Error(`Email handler at "${emailHandlerPath}" must export a default function or named "email" export.
1091
+ Expected: export async function email(message) { ... }
1092
+ Legacy compatibility is still supported for email(message, env, ctx).`);
1093
+ }
1094
+ const waitUntilPromises = [];
1095
+ const ctx = {
1096
+ waitUntil(promise) {
1097
+ waitUntilPromises.push(promise);
1098
+ },
1099
+ passThroughOnException() {},
1100
+ props: {}
1101
+ };
1102
+ const runtimeEnv = testEnvGetter5();
1103
+ const timestamp = new Date;
1104
+ const message = {
1105
+ from: options.from,
1106
+ to: options.to,
1107
+ headers: createEmailHeaders(raw),
1108
+ raw: createRawEmailStream(raw),
1109
+ rawSize: raw.length,
1110
+ setReject(reason) {
1111
+ throw new Error(`Email rejected: ${reason}`);
1112
+ },
1113
+ async forward(rcptTo) {
1114
+ recordSentEmail({
1115
+ type: "forward",
1116
+ from: options.from,
1117
+ to: rcptTo,
1118
+ raw,
1119
+ timestamp
1120
+ });
1121
+ },
1122
+ async reply(message2) {
1123
+ recordSentEmail({
1124
+ type: "reply",
1125
+ from: message2.from ?? options.to,
1126
+ to: message2.to ?? options.from,
1127
+ raw: getRecordedRawContent(message2.raw),
1128
+ timestamp
1129
+ });
1130
+ }
1131
+ };
1132
+ const emailEvent = createEmailEvent(message, runtimeEnv, ctx);
1133
+ await runWithEventContext(emailEvent, () => emailHandler(emailEvent, runtimeEnv, ctx));
1134
+ await Promise.all(waitUntilPromises);
1135
+ return new Response(JSON.stringify({ ok: true, from: options.from, to: options.to }), {
1136
+ headers: { "Content-Type": "application/json" }
1137
+ });
1138
+ }
1058
1139
  const url = new URL(`http://localhost:${miniflarePort}/cdn-cgi/handler/email`);
1059
1140
  url.searchParams.set("from", options.from);
1060
1141
  url.searchParams.set("to", options.to);
@@ -1079,7 +1160,21 @@ function getSentEmails() {
1079
1160
  function clearSentEmails() {
1080
1161
  sentEmails = [];
1081
1162
  }
1163
+ function recordSentEmail(email) {
1164
+ sentEmails.push(email);
1165
+ for (const listener of emailListeners) {
1166
+ try {
1167
+ listener(email);
1168
+ } catch (error) {
1169
+ console.error("[devflare/test] Email listener error:", error);
1170
+ }
1171
+ }
1172
+ }
1082
1173
  function resetEmailState() {
1174
+ miniflarePort = 8787;
1175
+ emailHandlerPath = null;
1176
+ configDir5 = null;
1177
+ testEnvGetter5 = null;
1083
1178
  emailListeners = [];
1084
1179
  sentEmails = [];
1085
1180
  }
@@ -1134,15 +1229,12 @@ function getCallerDirectory() {
1134
1229
  }
1135
1230
  return process.cwd();
1136
1231
  }
1137
- function findNearestConfig(startDir) {
1138
- const configNames = ["devflare.config.ts", "devflare.config.js"];
1232
+ async function findNearestConfig(startDir) {
1139
1233
  let currentDir = startDir;
1140
1234
  while (true) {
1141
- for (const name of configNames) {
1142
- const configPath = join6(currentDir, name);
1143
- if (existsSync2(configPath)) {
1144
- return configPath;
1145
- }
1235
+ const configPath = await resolveConfigPath(currentDir);
1236
+ if (configPath) {
1237
+ return configPath;
1146
1238
  }
1147
1239
  const parentDir = dirname2(currentDir);
1148
1240
  if (parentDir === currentDir) {
@@ -1157,16 +1249,17 @@ async function createTestContext(configPath) {
1157
1249
  if (configPath) {
1158
1250
  absolutePath = resolve2(callerDir, configPath);
1159
1251
  } else {
1160
- const found = findNearestConfig(callerDir);
1252
+ const found = await findNearestConfig(callerDir);
1161
1253
  if (!found) {
1162
- throw new Error(`Could not find devflare.config.ts. Searched upward from: ${callerDir}
1254
+ throw new Error(`Could not find a devflare config file. Searched upward from: ${callerDir}
1255
+ ` + `Expected one of: devflare.config.ts, devflare.config.mts, devflare.config.js, devflare.config.mjs
1163
1256
  ` + `Either create a config file or provide an explicit path: createTestContext('./path/to/config.ts')`);
1164
1257
  }
1165
1258
  absolutePath = found;
1166
1259
  }
1167
- const configDir5 = dirname2(absolutePath);
1260
+ const configDir6 = dirname2(absolutePath);
1168
1261
  const config = await loadConfig({
1169
- cwd: configDir5,
1262
+ cwd: configDir6,
1170
1263
  configFile: absolutePath.split(/[/\\]/).pop()
1171
1264
  });
1172
1265
  globalRemoteBindings = {};
@@ -1186,6 +1279,11 @@ async function createTestContext(configPath) {
1186
1279
  globalRemoteBindings[key] = value;
1187
1280
  }
1188
1281
  }
1282
+ if (config.bindings?.sendEmail) {
1283
+ for (const [name, binding] of Object.entries(config.bindings.sendEmail)) {
1284
+ globalRemoteBindings[name] = createLocalSendEmailBinding(binding);
1285
+ }
1286
+ }
1189
1287
  const hints = {};
1190
1288
  if (config.bindings?.kv) {
1191
1289
  for (const name of Object.keys(config.bindings.kv))
@@ -1207,18 +1305,23 @@ async function createTestContext(configPath) {
1207
1305
  for (const name of Object.keys(config.bindings.services))
1208
1306
  hints[name] = "service";
1209
1307
  }
1308
+ if (config.bindings?.sendEmail) {
1309
+ for (const name of Object.keys(config.bindings.sendEmail))
1310
+ hints[name] = "sendEmail";
1311
+ }
1210
1312
  const needsMultiWorkerForServices = hasServiceBindings(config);
1211
1313
  const needsMultiWorkerForDOs = hasCrossWorkerDOs(config);
1212
1314
  const needsMultiWorker = needsMultiWorkerForServices || needsMultiWorkerForDOs;
1213
1315
  let serviceBindingResolution = null;
1214
1316
  let doBindingResolution = null;
1215
1317
  if (needsMultiWorkerForServices) {
1216
- serviceBindingResolution = await resolveServiceBindings(config, configDir5);
1318
+ serviceBindingResolution = await resolveServiceBindings(config, configDir6);
1217
1319
  }
1218
1320
  if (needsMultiWorkerForDOs) {
1219
- doBindingResolution = await resolveDOBindings(config, configDir5);
1321
+ doBindingResolution = await resolveDOBindings(config, configDir6);
1220
1322
  }
1221
1323
  const randomPort = 1e4 + Math.floor(Math.random() * 50000);
1324
+ const localWorkerBindings = config.vars ?? {};
1222
1325
  const mfConfig = {
1223
1326
  modules: true,
1224
1327
  port: randomPort
@@ -1236,9 +1339,28 @@ async function createTestContext(configPath) {
1236
1339
  }
1237
1340
  mfConfig.queueProducers = queueProducers;
1238
1341
  }
1342
+ if (Object.keys(localWorkerBindings).length > 0) {
1343
+ mfConfig.bindings = localWorkerBindings;
1344
+ }
1345
+ if (config.bindings?.sendEmail) {
1346
+ mfConfig.email = {
1347
+ send_email: Object.entries(config.bindings.sendEmail).map(([name, binding]) => ({
1348
+ name,
1349
+ ...binding.destinationAddress && {
1350
+ destination_address: binding.destinationAddress
1351
+ },
1352
+ ...binding.allowedDestinationAddresses && {
1353
+ allowed_destination_addresses: binding.allowedDestinationAddresses
1354
+ },
1355
+ ...binding.allowedSenderAddresses && {
1356
+ allowed_sender_addresses: binding.allowedSenderAddresses
1357
+ }
1358
+ }))
1359
+ };
1360
+ }
1239
1361
  const transportFile = config.files?.transport;
1240
1362
  if (transportFile) {
1241
- const transportPath = join6(configDir5, transportFile);
1363
+ const transportPath = join7(configDir6, transportFile);
1242
1364
  const transportModule = await import(transportPath);
1243
1365
  if (!transportModule.transport) {
1244
1366
  console.warn(`[devflare] Warning: Transport file "${transportFile}" does not export a named "transport" object.
@@ -1260,7 +1382,7 @@ Transport encoding/decoding will be disabled.`);
1260
1382
  const doPattern = typeof doPatternConfig === "string" ? doPatternConfig : DEFAULT_DO_PATTERN;
1261
1383
  if (doPatternConfig !== false) {
1262
1384
  const fs = await import("fs/promises");
1263
- const doFiles = await findFiles(doPattern, { cwd: configDir5 });
1385
+ const doFiles = await findFiles(doPattern, { cwd: configDir6 });
1264
1386
  for (const filePath of doFiles) {
1265
1387
  try {
1266
1388
  const code = await fs.readFile(filePath, "utf-8");
@@ -1278,7 +1400,7 @@ Transport encoding/decoding will be disabled.`);
1278
1400
  }
1279
1401
  let scriptPath;
1280
1402
  if (doInfo.scriptName) {
1281
- scriptPath = join6(configDir5, "src", doInfo.scriptName);
1403
+ scriptPath = join7(configDir6, "src", doInfo.scriptName);
1282
1404
  } else {
1283
1405
  const discoveredPath = classToFilePath.get(doInfo.className);
1284
1406
  if (!discoveredPath) {
@@ -1295,7 +1417,7 @@ Either:
1295
1417
  const virtualImports = [];
1296
1418
  const virtualExports = [];
1297
1419
  if (transportFile) {
1298
- const transportPath = join6(configDir5, transportFile);
1420
+ const transportPath = join7(configDir6, transportFile);
1299
1421
  virtualImports.push(`import { transport } from '${transportPath.replace(/\\/g, "/")}'`);
1300
1422
  virtualExports.push("export { transport }");
1301
1423
  }
@@ -1307,7 +1429,7 @@ Either:
1307
1429
  if (virtualImports.length > 0) {
1308
1430
  const virtualEntry = [...virtualImports, "", ...virtualExports].join(`
1309
1431
  `);
1310
- const virtualPath = join6(configDir5, ".devflare", "__test_entry.ts");
1432
+ const virtualPath = join7(configDir6, ".devflare", "__test_entry.ts");
1311
1433
  const { writeFileSync, mkdirSync } = await import("fs");
1312
1434
  mkdirSync(dirname2(virtualPath), { recursive: true });
1313
1435
  writeFileSync(virtualPath, virtualEntry);
@@ -1348,6 +1470,7 @@ Either:
1348
1470
  ...mfConfig.kvNamespaces && { kvNamespaces: mfConfig.kvNamespaces },
1349
1471
  ...mfConfig.r2Buckets && { r2Buckets: mfConfig.r2Buckets },
1350
1472
  ...mfConfig.d1Databases && { d1Databases: mfConfig.d1Databases },
1473
+ ...mfConfig.email && { email: mfConfig.email },
1351
1474
  ...Object.keys(primaryDurableObjects).length > 0 && { durableObjects: primaryDurableObjects },
1352
1475
  ...serviceBindingResolution?.primaryServiceBindings && { serviceBindings: serviceBindingResolution.primaryServiceBindings }
1353
1476
  };
@@ -1381,7 +1504,7 @@ Either:
1381
1504
  const { Miniflare } = await import("miniflare");
1382
1505
  globalMiniflare = new Miniflare(mfConfig);
1383
1506
  await globalMiniflare.ready;
1384
- globalMiniflareBindings = await globalMiniflare.getBindings();
1507
+ globalMiniflareBindings = wrapEnvSendEmailBindings(await globalMiniflare.getBindings());
1385
1508
  const disposeContext = async () => {
1386
1509
  if (globalClient) {
1387
1510
  await globalClient.disconnect();
@@ -1403,26 +1526,42 @@ Either:
1403
1526
  __clearTestContext();
1404
1527
  };
1405
1528
  const getTestEnv = () => {
1406
- if (globalMiniflareBindings)
1407
- return globalMiniflareBindings;
1408
- if (globalRemoteBindings)
1409
- return globalRemoteBindings;
1410
- if (globalEnvProxy)
1411
- return globalEnvProxy;
1412
- return {};
1529
+ return new Proxy({}, {
1530
+ get(_, prop) {
1531
+ if (globalRemoteBindings && prop in globalRemoteBindings) {
1532
+ return globalRemoteBindings[prop];
1533
+ }
1534
+ if (hints[prop] === "sendEmail" && globalEnvProxy && prop in globalEnvProxy) {
1535
+ return globalEnvProxy[prop];
1536
+ }
1537
+ if (globalMiniflareBindings && prop in globalMiniflareBindings) {
1538
+ return globalMiniflareBindings[prop];
1539
+ }
1540
+ if (globalEnvProxy && prop in globalEnvProxy) {
1541
+ return globalEnvProxy[prop];
1542
+ }
1543
+ return;
1544
+ },
1545
+ has(_, prop) {
1546
+ return Boolean(globalRemoteBindings && prop in globalRemoteBindings || globalMiniflareBindings && prop in globalMiniflareBindings || globalEnvProxy && prop in globalEnvProxy);
1547
+ }
1548
+ });
1413
1549
  };
1414
1550
  const queuePath = config.files?.queue;
1415
1551
  const scheduledPath = config.files?.scheduled;
1416
1552
  const fetchPath = config.files?.fetch;
1553
+ const emailPath = config.files?.email;
1417
1554
  const DEFAULT_FETCH_PATH = "src/fetch.ts";
1418
1555
  const DEFAULT_QUEUE_PATH = "src/queue.ts";
1419
1556
  const DEFAULT_SCHEDULED_PATH = "src/scheduled.ts";
1557
+ const DEFAULT_EMAIL_PATH = "src/email.ts";
1558
+ const DEFAULT_TAIL_PATH = "src/tail.ts";
1420
1559
  const resolvePath = async (configValue, defaultPath) => {
1421
1560
  if (typeof configValue === "string")
1422
1561
  return configValue;
1423
1562
  if (configValue === false)
1424
1563
  return null;
1425
- const defaultAbsolute = join6(configDir5, defaultPath);
1564
+ const defaultAbsolute = join7(configDir6, defaultPath);
1426
1565
  try {
1427
1566
  const fs = await import("fs/promises");
1428
1567
  await fs.access(defaultAbsolute);
@@ -1431,32 +1570,45 @@ Either:
1431
1570
  return null;
1432
1571
  }
1433
1572
  };
1434
- const [resolvedFetchPath, resolvedQueuePath, resolvedScheduledPath] = await Promise.all([
1573
+ const [resolvedFetchPath, resolvedQueuePath, resolvedScheduledPath, resolvedEmailPath, resolvedTailPath, resolvedRoutes] = await Promise.all([
1435
1574
  resolvePath(fetchPath, DEFAULT_FETCH_PATH),
1436
1575
  resolvePath(queuePath, DEFAULT_QUEUE_PATH),
1437
- resolvePath(scheduledPath, DEFAULT_SCHEDULED_PATH)
1576
+ resolvePath(scheduledPath, DEFAULT_SCHEDULED_PATH),
1577
+ resolvePath(emailPath, DEFAULT_EMAIL_PATH),
1578
+ resolvePath(undefined, DEFAULT_TAIL_PATH),
1579
+ discoverRoutes(configDir6, config)
1438
1580
  ]);
1439
1581
  configureQueue({
1440
1582
  handlerPath: resolvedQueuePath,
1441
- configDir: configDir5,
1583
+ configDir: configDir6,
1442
1584
  getEnv: getTestEnv
1443
1585
  });
1444
1586
  configureScheduled({
1445
1587
  handlerPath: resolvedScheduledPath,
1446
- configDir: configDir5,
1588
+ configDir: configDir6,
1447
1589
  getEnv: getTestEnv
1448
1590
  });
1449
1591
  configureWorker({
1450
1592
  handlerPath: resolvedFetchPath,
1451
- configDir: configDir5,
1593
+ routes: resolvedRoutes?.routes.map((route) => ({
1594
+ filePath: route.filePath,
1595
+ routePath: route.routePath,
1596
+ segments: route.segments
1597
+ })) ?? [],
1598
+ configDir: configDir6,
1452
1599
  getEnv: getTestEnv
1453
1600
  });
1454
1601
  configureTail({
1455
- handlerPath: null,
1456
- configDir: configDir5,
1602
+ handlerPath: resolvedTailPath,
1603
+ configDir: configDir6,
1604
+ getEnv: getTestEnv
1605
+ });
1606
+ configureEmail({
1607
+ port: randomPort,
1608
+ handlerPath: resolvedEmailPath,
1609
+ configDir: configDir6,
1457
1610
  getEnv: getTestEnv
1458
1611
  });
1459
- configureEmail({ port: randomPort });
1460
1612
  if (hasMultiWorkerServices || hasMultiWorkerDOs) {
1461
1613
  setBindingHints(hints);
1462
1614
  const envAccessor2 = new Proxy({}, {
@@ -1490,6 +1642,9 @@ Either:
1490
1642
  if (globalRemoteBindings && prop in globalRemoteBindings) {
1491
1643
  return globalRemoteBindings[prop];
1492
1644
  }
1645
+ if (hints[prop] === "sendEmail" && globalEnvProxy && prop in globalEnvProxy) {
1646
+ return globalEnvProxy[prop];
1647
+ }
1493
1648
  if (globalMiniflareBindings && prop in globalMiniflareBindings) {
1494
1649
  return globalMiniflareBindings[prop];
1495
1650
  }
@@ -1588,6 +1743,7 @@ async function executeRpc(env, method, params) {
1588
1743
  const [bindingName, ...rest] = method.split('.')
1589
1744
  const op = rest.join('.')
1590
1745
  const binding = env[bindingName]
1746
+ const RAW_EMAIL = 'EmailMessage::raw'
1591
1747
  if (!binding) throw new Error('Binding not found: ' + bindingName)
1592
1748
 
1593
1749
  // KV operations
@@ -1619,6 +1775,11 @@ async function executeRpc(env, method, params) {
1619
1775
  if (op === 'prepare.first') return binding.prepare(params[0]).bind(...(params[1] || [])).first(params[2])
1620
1776
  if (op === 'prepare.raw') return binding.prepare(params[0]).bind(...(params[1] || [])).raw({ columnNames: params[2] })
1621
1777
 
1778
+ // Send email operations
1779
+ if (op === 'email.send') {
1780
+ return binding.send(__normalizeEmailMessage(params[0]))
1781
+ }
1782
+
1622
1783
  // DO operations
1623
1784
  if (op === 'idFromName') {
1624
1785
  return { __type: 'DOId', hex: binding.idFromName(params[0]).toString() }
@@ -1637,6 +1798,63 @@ async function executeRpc(env, method, params) {
1637
1798
 
1638
1799
  throw new Error('Unknown operation: ' + method)
1639
1800
  }
1801
+
1802
+ function __createEmailMessageRaw(raw) {
1803
+ if (typeof raw === 'string' || raw instanceof ReadableStream) {
1804
+ return raw
1805
+ }
1806
+ if (raw instanceof Uint8Array || raw instanceof ArrayBuffer) {
1807
+ return new Response(raw).body
1808
+ }
1809
+ throw new Error('Unsupported EmailMessage raw payload')
1810
+ }
1811
+
1812
+ function __buildRawEmail(message) {
1813
+ const lines = []
1814
+ const messageId = '<' + Date.now() + '-' + Math.random().toString(36).slice(2) + '@devflare.dev>'
1815
+
1816
+ lines.push('From: ' + message.from)
1817
+ lines.push('To: ' + (Array.isArray(message.to) ? message.to.join(', ') : message.to))
1818
+ lines.push('Date: ' + new Date().toUTCString())
1819
+ lines.push('Message-ID: ' + messageId)
1820
+
1821
+ if (message.subject) lines.push('Subject: ' + message.subject)
1822
+ if (message.replyTo) lines.push('Reply-To: ' + String(message.replyTo))
1823
+ if (message.cc) lines.push('Cc: ' + (Array.isArray(message.cc) ? message.cc.join(', ') : message.cc))
1824
+ if (message.bcc) lines.push('Bcc: ' + (Array.isArray(message.bcc) ? message.bcc.join(', ') : message.bcc))
1825
+
1826
+ for (const [key, value] of Object.entries(message.headers || {})) {
1827
+ lines.push(key + ': ' + value)
1828
+ }
1829
+
1830
+ lines.push('MIME-Version: 1.0')
1831
+ lines.push('Content-Type: ' + (message.html ? 'text/html' : 'text/plain') + '; charset=UTF-8')
1832
+ lines.push('')
1833
+ lines.push(String(message.html ?? message.text ?? '').replace(/\\r?\\n/g, '\\r\\n'))
1834
+
1835
+ return lines.join('\\r\\n')
1836
+ }
1837
+
1838
+ function __normalizeEmailMessage(message) {
1839
+ if (!message || typeof message !== 'object' || !('from' in message) || !('to' in message)) {
1840
+ return message
1841
+ }
1842
+ if ('EmailMessage::raw' in message) {
1843
+ return message
1844
+ }
1845
+ if ('raw' in message && message.raw !== undefined) {
1846
+ return {
1847
+ from: message.from,
1848
+ to: message.to,
1849
+ [RAW_EMAIL]: __createEmailMessageRaw(message.raw)
1850
+ }
1851
+ }
1852
+ return {
1853
+ from: message.from,
1854
+ to: message.to,
1855
+ [RAW_EMAIL]: __createEmailMessageRaw(__buildRawEmail(message))
1856
+ }
1857
+ }
1640
1858
  `;
1641
1859
  }
1642
1860
  // src/test/cf.ts
@@ -2146,7 +2364,7 @@ async function createBridgeTestContext(options = {}) {
2146
2364
  verbose: options.verbose ?? false
2147
2365
  });
2148
2366
  }
2149
- const bindings = await miniflare.getBindings();
2367
+ const bindings = wrapEnvSendEmailBindings(await miniflare.getBindings());
2150
2368
  if (config?.bindings) {
2151
2369
  const hints = {};
2152
2370
  if (config.bindings.kv) {
@@ -2176,6 +2394,11 @@ async function createBridgeTestContext(options = {}) {
2176
2394
  }
2177
2395
  if (config.bindings.ai)
2178
2396
  hints[config.bindings.ai.binding] = "ai";
2397
+ if (config.bindings.sendEmail) {
2398
+ Object.keys(config.bindings.sendEmail).forEach((name) => {
2399
+ hints[name] = "sendEmail";
2400
+ });
2401
+ }
2179
2402
  setBindingHints(hints);
2180
2403
  }
2181
2404
  const ctx = {
@@ -2222,4 +2445,4 @@ var testEnv = new Proxy({}, {
2222
2445
  function isKVNamespace(binding) {
2223
2446
  return typeof binding === "object" && binding !== null && "get" in binding && "put" in binding && "delete" in binding && "list" in binding;
2224
2447
  }
2225
- export { env, clearBundleCache, hasServiceBindings, resolveServiceBindings, hasCrossWorkerDOs, resolveDOBindings, queue, scheduled, worker, tail, email, createTestContext, cf, createMultiWorkerContext, createEntrypointScript, isRemoteModeEnabled, shouldSkip, createMockTestContext, withTestContext, createMockKV, createMockD1, createMockR2, createMockQueue, createMockEnv, createBridgeTestContext, stopBridgeTestContext, getBridgeTestContext, testEnv };
2448
+ export { clearBundleCache, hasServiceBindings, resolveServiceBindings, hasCrossWorkerDOs, resolveDOBindings, queue, scheduled, worker, tail, email, createTestContext, cf, createMultiWorkerContext, createEntrypointScript, isRemoteModeEnabled, shouldSkip, createMockTestContext, withTestContext, createMockKV, createMockD1, createMockR2, createMockQueue, createMockEnv, createBridgeTestContext, stopBridgeTestContext, getBridgeTestContext, testEnv };