devflare 1.0.0-next.1 → 1.0.0-next.11

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 +1424 -610
  2. package/R2.md +200 -0
  3. package/README.md +302 -505
  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-ezksv2dd.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 +2594 -1234
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/{deploy-nhceck39.js → deploy-jdpy21t6.js} +33 -15
  35. package/dist/{dev-qnxet3j9.js → dev-9mq7zhww.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-z4ffybce.js} +73 -50
  45. package/dist/{durable-object-t4kbb0yt.js → durable-object-yt8v1dyn.js} +1 -1
  46. package/dist/index-1p814k7s.js +227 -0
  47. package/dist/{index-tk6ej9dj.js → index-2q3pmzrx.js} +12 -16
  48. package/dist/{index-67qcae0f.js → index-51s1hkw4.js} +16 -1
  49. package/dist/{index-ep3445yc.js → index-53xcakh8.js} +414 -171
  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-8gtqgb3q.js +529 -0
  55. package/dist/{index-gz1gndna.js → index-9wt9x09k.js} +42 -62
  56. package/dist/index-dr6sbp8d.js +39 -0
  57. package/dist/index-fef08w43.js +231 -0
  58. package/dist/index-k7r18na8.js +0 -0
  59. package/dist/{index-m2q41jwa.js → index-n932ytmq.js} +9 -1
  60. package/dist/{index-07q6yxyc.js → index-v8vvsn9x.js} +1 -0
  61. package/dist/index-vky23txa.js +70 -0
  62. package/dist/{index-z14anrqp.js → index-wfbfz02q.js} +14 -15
  63. package/dist/index-ws68xvq2.js +311 -0
  64. package/dist/{index-hcex3rgh.js → index-wyf3s77s.js} +85 -8
  65. package/dist/index-xqfbd9fx.js +195 -0
  66. package/dist/index-xxwbb2nt.js +322 -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-nq5acrwh.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-xxwbb2nt.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-wyf3s77s.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
  }
@@ -1113,6 +1208,12 @@ var globalEnvProxy = null;
1113
1208
  var globalTransportDecode = null;
1114
1209
  var globalRemoteBindings = null;
1115
1210
  var globalMiniflareBindings = null;
1211
+ var DEFAULT_TRANSPORT_ENTRY_FILES = [
1212
+ "src/transport.ts",
1213
+ "src/transport.js",
1214
+ "src/transport.mts",
1215
+ "src/transport.mjs"
1216
+ ];
1116
1217
  function getCallerDirectory() {
1117
1218
  const bun = getBunRuntime2();
1118
1219
  if (bun?.main) {
@@ -1134,15 +1235,12 @@ function getCallerDirectory() {
1134
1235
  }
1135
1236
  return process.cwd();
1136
1237
  }
1137
- function findNearestConfig(startDir) {
1138
- const configNames = ["devflare.config.ts", "devflare.config.js"];
1238
+ async function findNearestConfig(startDir) {
1139
1239
  let currentDir = startDir;
1140
1240
  while (true) {
1141
- for (const name of configNames) {
1142
- const configPath = join6(currentDir, name);
1143
- if (existsSync2(configPath)) {
1144
- return configPath;
1145
- }
1241
+ const configPath = await resolveConfigPath(currentDir);
1242
+ if (configPath) {
1243
+ return configPath;
1146
1244
  }
1147
1245
  const parentDir = dirname2(currentDir);
1148
1246
  if (parentDir === currentDir) {
@@ -1151,22 +1249,37 @@ function findNearestConfig(startDir) {
1151
1249
  currentDir = parentDir;
1152
1250
  }
1153
1251
  }
1252
+ function resolveTransportFile(configDir6, configuredPath) {
1253
+ if (typeof configuredPath === "string") {
1254
+ return configuredPath;
1255
+ }
1256
+ if (configuredPath === null) {
1257
+ return null;
1258
+ }
1259
+ for (const defaultEntry of DEFAULT_TRANSPORT_ENTRY_FILES) {
1260
+ if (existsSync2(join7(configDir6, defaultEntry))) {
1261
+ return defaultEntry;
1262
+ }
1263
+ }
1264
+ return null;
1265
+ }
1154
1266
  async function createTestContext(configPath) {
1155
1267
  const callerDir = getCallerDirectory();
1156
1268
  let absolutePath;
1157
1269
  if (configPath) {
1158
1270
  absolutePath = resolve2(callerDir, configPath);
1159
1271
  } else {
1160
- const found = findNearestConfig(callerDir);
1272
+ const found = await findNearestConfig(callerDir);
1161
1273
  if (!found) {
1162
- throw new Error(`Could not find devflare.config.ts. Searched upward from: ${callerDir}
1274
+ throw new Error(`Could not find a devflare config file. Searched upward from: ${callerDir}
1275
+ ` + `Expected one of: devflare.config.ts, devflare.config.mts, devflare.config.js, devflare.config.mjs
1163
1276
  ` + `Either create a config file or provide an explicit path: createTestContext('./path/to/config.ts')`);
1164
1277
  }
1165
1278
  absolutePath = found;
1166
1279
  }
1167
- const configDir5 = dirname2(absolutePath);
1280
+ const configDir6 = dirname2(absolutePath);
1168
1281
  const config = await loadConfig({
1169
- cwd: configDir5,
1282
+ cwd: configDir6,
1170
1283
  configFile: absolutePath.split(/[/\\]/).pop()
1171
1284
  });
1172
1285
  globalRemoteBindings = {};
@@ -1186,6 +1299,11 @@ async function createTestContext(configPath) {
1186
1299
  globalRemoteBindings[key] = value;
1187
1300
  }
1188
1301
  }
1302
+ if (config.bindings?.sendEmail) {
1303
+ for (const [name, binding] of Object.entries(config.bindings.sendEmail)) {
1304
+ globalRemoteBindings[name] = createLocalSendEmailBinding(binding);
1305
+ }
1306
+ }
1189
1307
  const hints = {};
1190
1308
  if (config.bindings?.kv) {
1191
1309
  for (const name of Object.keys(config.bindings.kv))
@@ -1207,18 +1325,23 @@ async function createTestContext(configPath) {
1207
1325
  for (const name of Object.keys(config.bindings.services))
1208
1326
  hints[name] = "service";
1209
1327
  }
1328
+ if (config.bindings?.sendEmail) {
1329
+ for (const name of Object.keys(config.bindings.sendEmail))
1330
+ hints[name] = "sendEmail";
1331
+ }
1210
1332
  const needsMultiWorkerForServices = hasServiceBindings(config);
1211
1333
  const needsMultiWorkerForDOs = hasCrossWorkerDOs(config);
1212
1334
  const needsMultiWorker = needsMultiWorkerForServices || needsMultiWorkerForDOs;
1213
1335
  let serviceBindingResolution = null;
1214
1336
  let doBindingResolution = null;
1215
1337
  if (needsMultiWorkerForServices) {
1216
- serviceBindingResolution = await resolveServiceBindings(config, configDir5);
1338
+ serviceBindingResolution = await resolveServiceBindings(config, configDir6);
1217
1339
  }
1218
1340
  if (needsMultiWorkerForDOs) {
1219
- doBindingResolution = await resolveDOBindings(config, configDir5);
1341
+ doBindingResolution = await resolveDOBindings(config, configDir6);
1220
1342
  }
1221
1343
  const randomPort = 1e4 + Math.floor(Math.random() * 50000);
1344
+ const localWorkerBindings = config.vars ?? {};
1222
1345
  const mfConfig = {
1223
1346
  modules: true,
1224
1347
  port: randomPort
@@ -1236,9 +1359,28 @@ async function createTestContext(configPath) {
1236
1359
  }
1237
1360
  mfConfig.queueProducers = queueProducers;
1238
1361
  }
1239
- const transportFile = config.files?.transport;
1362
+ if (Object.keys(localWorkerBindings).length > 0) {
1363
+ mfConfig.bindings = localWorkerBindings;
1364
+ }
1365
+ if (config.bindings?.sendEmail) {
1366
+ mfConfig.email = {
1367
+ send_email: Object.entries(config.bindings.sendEmail).map(([name, binding]) => ({
1368
+ name,
1369
+ ...binding.destinationAddress && {
1370
+ destination_address: binding.destinationAddress
1371
+ },
1372
+ ...binding.allowedDestinationAddresses && {
1373
+ allowed_destination_addresses: binding.allowedDestinationAddresses
1374
+ },
1375
+ ...binding.allowedSenderAddresses && {
1376
+ allowed_sender_addresses: binding.allowedSenderAddresses
1377
+ }
1378
+ }))
1379
+ };
1380
+ }
1381
+ const transportFile = resolveTransportFile(configDir6, config.files?.transport);
1240
1382
  if (transportFile) {
1241
- const transportPath = join6(configDir5, transportFile);
1383
+ const transportPath = join7(configDir6, transportFile);
1242
1384
  const transportModule = await import(transportPath);
1243
1385
  if (!transportModule.transport) {
1244
1386
  console.warn(`[devflare] Warning: Transport file "${transportFile}" does not export a named "transport" object.
@@ -1260,7 +1402,7 @@ Transport encoding/decoding will be disabled.`);
1260
1402
  const doPattern = typeof doPatternConfig === "string" ? doPatternConfig : DEFAULT_DO_PATTERN;
1261
1403
  if (doPatternConfig !== false) {
1262
1404
  const fs = await import("fs/promises");
1263
- const doFiles = await findFiles(doPattern, { cwd: configDir5 });
1405
+ const doFiles = await findFiles(doPattern, { cwd: configDir6 });
1264
1406
  for (const filePath of doFiles) {
1265
1407
  try {
1266
1408
  const code = await fs.readFile(filePath, "utf-8");
@@ -1278,7 +1420,7 @@ Transport encoding/decoding will be disabled.`);
1278
1420
  }
1279
1421
  let scriptPath;
1280
1422
  if (doInfo.scriptName) {
1281
- scriptPath = join6(configDir5, "src", doInfo.scriptName);
1423
+ scriptPath = join7(configDir6, "src", doInfo.scriptName);
1282
1424
  } else {
1283
1425
  const discoveredPath = classToFilePath.get(doInfo.className);
1284
1426
  if (!discoveredPath) {
@@ -1295,7 +1437,7 @@ Either:
1295
1437
  const virtualImports = [];
1296
1438
  const virtualExports = [];
1297
1439
  if (transportFile) {
1298
- const transportPath = join6(configDir5, transportFile);
1440
+ const transportPath = join7(configDir6, transportFile);
1299
1441
  virtualImports.push(`import { transport } from '${transportPath.replace(/\\/g, "/")}'`);
1300
1442
  virtualExports.push("export { transport }");
1301
1443
  }
@@ -1307,7 +1449,7 @@ Either:
1307
1449
  if (virtualImports.length > 0) {
1308
1450
  const virtualEntry = [...virtualImports, "", ...virtualExports].join(`
1309
1451
  `);
1310
- const virtualPath = join6(configDir5, ".devflare", "__test_entry.ts");
1452
+ const virtualPath = join7(configDir6, ".devflare", "__test_entry.ts");
1311
1453
  const { writeFileSync, mkdirSync } = await import("fs");
1312
1454
  mkdirSync(dirname2(virtualPath), { recursive: true });
1313
1455
  writeFileSync(virtualPath, virtualEntry);
@@ -1348,6 +1490,7 @@ Either:
1348
1490
  ...mfConfig.kvNamespaces && { kvNamespaces: mfConfig.kvNamespaces },
1349
1491
  ...mfConfig.r2Buckets && { r2Buckets: mfConfig.r2Buckets },
1350
1492
  ...mfConfig.d1Databases && { d1Databases: mfConfig.d1Databases },
1493
+ ...mfConfig.email && { email: mfConfig.email },
1351
1494
  ...Object.keys(primaryDurableObjects).length > 0 && { durableObjects: primaryDurableObjects },
1352
1495
  ...serviceBindingResolution?.primaryServiceBindings && { serviceBindings: serviceBindingResolution.primaryServiceBindings }
1353
1496
  };
@@ -1381,7 +1524,7 @@ Either:
1381
1524
  const { Miniflare } = await import("miniflare");
1382
1525
  globalMiniflare = new Miniflare(mfConfig);
1383
1526
  await globalMiniflare.ready;
1384
- globalMiniflareBindings = await globalMiniflare.getBindings();
1527
+ globalMiniflareBindings = wrapEnvSendEmailBindings(await globalMiniflare.getBindings());
1385
1528
  const disposeContext = async () => {
1386
1529
  if (globalClient) {
1387
1530
  await globalClient.disconnect();
@@ -1403,26 +1546,42 @@ Either:
1403
1546
  __clearTestContext();
1404
1547
  };
1405
1548
  const getTestEnv = () => {
1406
- if (globalMiniflareBindings)
1407
- return globalMiniflareBindings;
1408
- if (globalRemoteBindings)
1409
- return globalRemoteBindings;
1410
- if (globalEnvProxy)
1411
- return globalEnvProxy;
1412
- return {};
1549
+ return new Proxy({}, {
1550
+ get(_, prop) {
1551
+ if (globalRemoteBindings && prop in globalRemoteBindings) {
1552
+ return globalRemoteBindings[prop];
1553
+ }
1554
+ if (hints[prop] === "sendEmail" && globalEnvProxy && prop in globalEnvProxy) {
1555
+ return globalEnvProxy[prop];
1556
+ }
1557
+ if (globalMiniflareBindings && prop in globalMiniflareBindings) {
1558
+ return globalMiniflareBindings[prop];
1559
+ }
1560
+ if (globalEnvProxy && prop in globalEnvProxy) {
1561
+ return globalEnvProxy[prop];
1562
+ }
1563
+ return;
1564
+ },
1565
+ has(_, prop) {
1566
+ return Boolean(globalRemoteBindings && prop in globalRemoteBindings || globalMiniflareBindings && prop in globalMiniflareBindings || globalEnvProxy && prop in globalEnvProxy);
1567
+ }
1568
+ });
1413
1569
  };
1414
1570
  const queuePath = config.files?.queue;
1415
1571
  const scheduledPath = config.files?.scheduled;
1416
1572
  const fetchPath = config.files?.fetch;
1573
+ const emailPath = config.files?.email;
1417
1574
  const DEFAULT_FETCH_PATH = "src/fetch.ts";
1418
1575
  const DEFAULT_QUEUE_PATH = "src/queue.ts";
1419
1576
  const DEFAULT_SCHEDULED_PATH = "src/scheduled.ts";
1577
+ const DEFAULT_EMAIL_PATH = "src/email.ts";
1578
+ const DEFAULT_TAIL_PATH = "src/tail.ts";
1420
1579
  const resolvePath = async (configValue, defaultPath) => {
1421
1580
  if (typeof configValue === "string")
1422
1581
  return configValue;
1423
1582
  if (configValue === false)
1424
1583
  return null;
1425
- const defaultAbsolute = join6(configDir5, defaultPath);
1584
+ const defaultAbsolute = join7(configDir6, defaultPath);
1426
1585
  try {
1427
1586
  const fs = await import("fs/promises");
1428
1587
  await fs.access(defaultAbsolute);
@@ -1431,32 +1590,45 @@ Either:
1431
1590
  return null;
1432
1591
  }
1433
1592
  };
1434
- const [resolvedFetchPath, resolvedQueuePath, resolvedScheduledPath] = await Promise.all([
1593
+ const [resolvedFetchPath, resolvedQueuePath, resolvedScheduledPath, resolvedEmailPath, resolvedTailPath, resolvedRoutes] = await Promise.all([
1435
1594
  resolvePath(fetchPath, DEFAULT_FETCH_PATH),
1436
1595
  resolvePath(queuePath, DEFAULT_QUEUE_PATH),
1437
- resolvePath(scheduledPath, DEFAULT_SCHEDULED_PATH)
1596
+ resolvePath(scheduledPath, DEFAULT_SCHEDULED_PATH),
1597
+ resolvePath(emailPath, DEFAULT_EMAIL_PATH),
1598
+ resolvePath(undefined, DEFAULT_TAIL_PATH),
1599
+ discoverRoutes(configDir6, config)
1438
1600
  ]);
1439
1601
  configureQueue({
1440
1602
  handlerPath: resolvedQueuePath,
1441
- configDir: configDir5,
1603
+ configDir: configDir6,
1442
1604
  getEnv: getTestEnv
1443
1605
  });
1444
1606
  configureScheduled({
1445
1607
  handlerPath: resolvedScheduledPath,
1446
- configDir: configDir5,
1608
+ configDir: configDir6,
1447
1609
  getEnv: getTestEnv
1448
1610
  });
1449
1611
  configureWorker({
1450
1612
  handlerPath: resolvedFetchPath,
1451
- configDir: configDir5,
1613
+ routes: resolvedRoutes?.routes.map((route) => ({
1614
+ filePath: route.filePath,
1615
+ routePath: route.routePath,
1616
+ segments: route.segments
1617
+ })) ?? [],
1618
+ configDir: configDir6,
1452
1619
  getEnv: getTestEnv
1453
1620
  });
1454
1621
  configureTail({
1455
- handlerPath: null,
1456
- configDir: configDir5,
1622
+ handlerPath: resolvedTailPath,
1623
+ configDir: configDir6,
1624
+ getEnv: getTestEnv
1625
+ });
1626
+ configureEmail({
1627
+ port: randomPort,
1628
+ handlerPath: resolvedEmailPath,
1629
+ configDir: configDir6,
1457
1630
  getEnv: getTestEnv
1458
1631
  });
1459
- configureEmail({ port: randomPort });
1460
1632
  if (hasMultiWorkerServices || hasMultiWorkerDOs) {
1461
1633
  setBindingHints(hints);
1462
1634
  const envAccessor2 = new Proxy({}, {
@@ -1490,6 +1662,9 @@ Either:
1490
1662
  if (globalRemoteBindings && prop in globalRemoteBindings) {
1491
1663
  return globalRemoteBindings[prop];
1492
1664
  }
1665
+ if (hints[prop] === "sendEmail" && globalEnvProxy && prop in globalEnvProxy) {
1666
+ return globalEnvProxy[prop];
1667
+ }
1493
1668
  if (globalMiniflareBindings && prop in globalMiniflareBindings) {
1494
1669
  return globalMiniflareBindings[prop];
1495
1670
  }
@@ -1588,6 +1763,7 @@ async function executeRpc(env, method, params) {
1588
1763
  const [bindingName, ...rest] = method.split('.')
1589
1764
  const op = rest.join('.')
1590
1765
  const binding = env[bindingName]
1766
+ const RAW_EMAIL = 'EmailMessage::raw'
1591
1767
  if (!binding) throw new Error('Binding not found: ' + bindingName)
1592
1768
 
1593
1769
  // KV operations
@@ -1619,6 +1795,11 @@ async function executeRpc(env, method, params) {
1619
1795
  if (op === 'prepare.first') return binding.prepare(params[0]).bind(...(params[1] || [])).first(params[2])
1620
1796
  if (op === 'prepare.raw') return binding.prepare(params[0]).bind(...(params[1] || [])).raw({ columnNames: params[2] })
1621
1797
 
1798
+ // Send email operations
1799
+ if (op === 'email.send') {
1800
+ return binding.send(__normalizeEmailMessage(params[0]))
1801
+ }
1802
+
1622
1803
  // DO operations
1623
1804
  if (op === 'idFromName') {
1624
1805
  return { __type: 'DOId', hex: binding.idFromName(params[0]).toString() }
@@ -1637,6 +1818,63 @@ async function executeRpc(env, method, params) {
1637
1818
 
1638
1819
  throw new Error('Unknown operation: ' + method)
1639
1820
  }
1821
+
1822
+ function __createEmailMessageRaw(raw) {
1823
+ if (typeof raw === 'string' || raw instanceof ReadableStream) {
1824
+ return raw
1825
+ }
1826
+ if (raw instanceof Uint8Array || raw instanceof ArrayBuffer) {
1827
+ return new Response(raw).body
1828
+ }
1829
+ throw new Error('Unsupported EmailMessage raw payload')
1830
+ }
1831
+
1832
+ function __buildRawEmail(message) {
1833
+ const lines = []
1834
+ const messageId = '<' + Date.now() + '-' + Math.random().toString(36).slice(2) + '@devflare.dev>'
1835
+
1836
+ lines.push('From: ' + message.from)
1837
+ lines.push('To: ' + (Array.isArray(message.to) ? message.to.join(', ') : message.to))
1838
+ lines.push('Date: ' + new Date().toUTCString())
1839
+ lines.push('Message-ID: ' + messageId)
1840
+
1841
+ if (message.subject) lines.push('Subject: ' + message.subject)
1842
+ if (message.replyTo) lines.push('Reply-To: ' + String(message.replyTo))
1843
+ if (message.cc) lines.push('Cc: ' + (Array.isArray(message.cc) ? message.cc.join(', ') : message.cc))
1844
+ if (message.bcc) lines.push('Bcc: ' + (Array.isArray(message.bcc) ? message.bcc.join(', ') : message.bcc))
1845
+
1846
+ for (const [key, value] of Object.entries(message.headers || {})) {
1847
+ lines.push(key + ': ' + value)
1848
+ }
1849
+
1850
+ lines.push('MIME-Version: 1.0')
1851
+ lines.push('Content-Type: ' + (message.html ? 'text/html' : 'text/plain') + '; charset=UTF-8')
1852
+ lines.push('')
1853
+ lines.push(String(message.html ?? message.text ?? '').replace(/\\r?\\n/g, '\\r\\n'))
1854
+
1855
+ return lines.join('\\r\\n')
1856
+ }
1857
+
1858
+ function __normalizeEmailMessage(message) {
1859
+ if (!message || typeof message !== 'object' || !('from' in message) || !('to' in message)) {
1860
+ return message
1861
+ }
1862
+ if ('EmailMessage::raw' in message) {
1863
+ return message
1864
+ }
1865
+ if ('raw' in message && message.raw !== undefined) {
1866
+ return {
1867
+ from: message.from,
1868
+ to: message.to,
1869
+ [RAW_EMAIL]: __createEmailMessageRaw(message.raw)
1870
+ }
1871
+ }
1872
+ return {
1873
+ from: message.from,
1874
+ to: message.to,
1875
+ [RAW_EMAIL]: __createEmailMessageRaw(__buildRawEmail(message))
1876
+ }
1877
+ }
1640
1878
  `;
1641
1879
  }
1642
1880
  // src/test/cf.ts
@@ -2146,7 +2384,7 @@ async function createBridgeTestContext(options = {}) {
2146
2384
  verbose: options.verbose ?? false
2147
2385
  });
2148
2386
  }
2149
- const bindings = await miniflare.getBindings();
2387
+ const bindings = wrapEnvSendEmailBindings(await miniflare.getBindings());
2150
2388
  if (config?.bindings) {
2151
2389
  const hints = {};
2152
2390
  if (config.bindings.kv) {
@@ -2176,6 +2414,11 @@ async function createBridgeTestContext(options = {}) {
2176
2414
  }
2177
2415
  if (config.bindings.ai)
2178
2416
  hints[config.bindings.ai.binding] = "ai";
2417
+ if (config.bindings.sendEmail) {
2418
+ Object.keys(config.bindings.sendEmail).forEach((name) => {
2419
+ hints[name] = "sendEmail";
2420
+ });
2421
+ }
2179
2422
  setBindingHints(hints);
2180
2423
  }
2181
2424
  const ctx = {
@@ -2222,4 +2465,4 @@ var testEnv = new Proxy({}, {
2222
2465
  function isKVNamespace(binding) {
2223
2466
  return typeof binding === "object" && binding !== null && "get" in binding && "put" in binding && "delete" in binding && "list" in binding;
2224
2467
  }
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 };
2468
+ 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 };