11import * as Effect from "effect/Effect" ;
22import * as Layer from "effect/Layer" ;
3- import * as EmailWorker from "worker:./email.worker.ts" ;
3+ import * as EmailMessageWorker from "worker:./EmailMessage.worker.ts" ;
4+ import * as SendEmailBindingWorker from "worker:./SendEmailBinding.worker.ts" ;
45import { formatExtensionModule } from "../../internal/internal-modules.ts" ;
56import * as Plugin from "../../Plugin.ts" ;
67import type { BindingHook } from "../../PluginContext.ts" ;
@@ -9,26 +10,43 @@ import { makeRemoteBinding } from "../../remote-bindings/RemoteBindings.ts";
910
1011export class SendEmail extends Plugin . Service < SendEmail > ( ) ( "cloudflare-runtime/plugin/SendEmail" ) { }
1112
13+ /**
14+ * Wrapped-binding module for the local `send()` stub — see
15+ * `send-email.worker.ts`. Registered alongside the `cloudflare-internal:email`
16+ * shim so {@link local} can point its wrapped binding at it.
17+ */
18+ const LOCAL_SEND_EMAIL_MODULE = "cloudflare-runtime:send-email" ;
19+
1220export const SendEmailLive = Layer . succeed (
1321 SendEmail ,
1422 SendEmail . of (
15- Effect . map ( formatExtensionModule ( EmailWorker ) , ( esModule ) => ( {
16- extensions : [
17- {
18- modules : [
19- {
20- name : "cloudflare-internal:email" ,
21- internal : true ,
22- esModule,
23- } ,
24- ] ,
25- } ,
26- ] ,
27- } ) ) ,
23+ Effect . zipWith (
24+ formatExtensionModule ( EmailMessageWorker ) ,
25+ formatExtensionModule ( SendEmailBindingWorker ) ,
26+ ( emailMessage , sendEmailBinding ) => ( {
27+ extensions : [
28+ {
29+ modules : [
30+ {
31+ name : "cloudflare-internal:email" ,
32+ internal : true ,
33+ esModule : emailMessage ,
34+ } ,
35+ {
36+ name : LOCAL_SEND_EMAIL_MODULE ,
37+ internal : true ,
38+ esModule : sendEmailBinding ,
39+ } ,
40+ ] ,
41+ } ,
42+ ] ,
43+ } ) ,
44+ { concurrent : true } ,
45+ ) ,
2846 ) ,
2947) ;
3048
31- export interface RemoteSendEmailProps {
49+ export interface SendEmailProps {
3250 readonly binding : string ;
3351 readonly destinationAddress ?: string ;
3452 readonly allowedDestinationAddresses ?: Array < string > ;
@@ -42,7 +60,7 @@ export interface RemoteSendEmailProps {
4260 * the `EmailMessage` class to user code (workerd does not ship it
4361 * natively), matching the upstream Miniflare behavior.
4462 */
45- export const remote = ( props : RemoteSendEmailProps ) : BindingHook < RemoteBindings | SendEmail > =>
63+ export const remote = ( props : SendEmailProps ) : BindingHook < RemoteBindings | SendEmail > =>
4664 Plugin . use ( SendEmail , ( ) =>
4765 makeRemoteBinding (
4866 {
@@ -58,3 +76,22 @@ export const remote = (props: RemoteSendEmailProps): BindingHook<RemoteBindings
5876 } ) ,
5977 ) ,
6078 ) ;
79+
80+ /**
81+ * Bind a local `send_email` stub for `alchemy dev`. `send()` logs the message
82+ * and resolves instead of delivering real mail — matching how Miniflare and
83+ * Wrangler treat `send_email` unless you opt into the real, remote binding.
84+ *
85+ * Reuses the `cloudflare-internal:email` `EmailMessage` shim registered by
86+ * {@link SendEmailLive} (same as {@link remote}), so user code can still
87+ * construct `EmailMessage` instances in dev. The address props are accepted
88+ * to mirror {@link remote}'s signature; the stub does not enforce them.
89+ */
90+ export const local = ( props : SendEmailProps ) : BindingHook < SendEmail > =>
91+ Plugin . useSync ( SendEmail , ( ) => ( {
92+ name : props . binding ,
93+ wrapped : {
94+ moduleName : LOCAL_SEND_EMAIL_MODULE ,
95+ innerBindings : [ ] ,
96+ } ,
97+ } ) ) ;
0 commit comments