-
Notifications
You must be signed in to change notification settings - Fork 0
Email v5 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Renamed render() -> compose() since it's not actually rendering now. This also allows rendering to be async.
MJML libs now need to be declared by consumers - @faire/mjml-react - mjml
```ts
compose(<Template foo="bar" />).send()
send('me@me.com', <Template foo="bar" />)
```
```ts
send(
{ to: 'me@me.com', 'reply-to': 'bob@me.com' },
<Template foo="bar" />
)
```
Now messages are created with
```ts
const mailer: EmailService;
const msg = mailer.compose(..);
// or
const msg = EmailMessage.from(...);
```
and are sent with
```ts
const mailer: EmailService;
await mailer.send(msg);
// or if msg is created from mailer...
await mailer.compose(...).send();
```
Now headers are passed first, then the body.
The header can also just be one or more recipients.
The body can be an element (new) or a tuple of a component with its props.
```ts
compose('me@example.com', <Template foo="bar" />);
compose(
{ to: 'me@example.com' },
[Template, { foo: "bar" }]
);
```
…aultHeaders` option
…to account for cc/bcc
I believe this compilation implementation is more robust than `emailjs`, and the headers are better typed. Since it already supports SES v2, there is no reason to not use it for transport as well. Though the transporter is now injectable. I adapted its logger (bunyan) to one for NestJS too.
```ts
await compose({ to: "", text: "" }).send()
```
Now we only render once for React, meaning that components should only be rendered once. This is more important now that components can be async and do data loading work. The context/hooks for "in text only" have been removed. Components now need to declarativity state that with data attributes. This is the biggest breaking change here, but I think it is worth it. Declaring output for both formats at once is very similar to CSS styling, giving up control of when each of those outputs is used. Also, I don't think this is that big of a deal. In practice, we didn't have any JS logic based on this output format. We then post-process those data attributes from the HTML given to us by React to split the output for text & HTML. Helpers <InText> & <InHtml> can be used to ease this. Or `data-render-only="text/html"` can be declared anywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a major version upgrade (v5) of the email module that completely redesigns the API around React Server Components and Suspense support. The upgrade replaces MJML as a hard dependency with optional support, switches from SES direct sending to nodemailer-based transport, and introduces lazy rendering with improved composability patterns.
- Replaces
EmailServicewithMailerServiceusing a more functional composition pattern - Implements React Server Components and Suspense support for async data loading in templates
- Refactors template system to use
Headerscomponent instead ofTitlefor subject setting - Replaces text/HTML rendering system with DOM-based processing using
data-render-onlyattributes
Reviewed Changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/email/src/transporter.ts | Introduces abstract base class for email transport abstraction |
| packages/email/src/templates/title.tsx | Removes legacy Title component and subject collection system |
| packages/email/src/templates/text-rendering.tsx | Replaces context-based text rendering with DOM attribute system |
| packages/email/src/templates/mjml.tsx | Updates MJML exports to include Title and Doc aliases |
| packages/email/src/templates/index.ts | Updates template exports to focus on Headers and rendering components |
| packages/email/src/templates/headers.tsx | Introduces new Headers component for declarative email metadata |
| packages/email/src/templates/attachment.tsx | Removes legacy attachment handling system |
| packages/email/src/processRenderOnlyElements.ts | Implements DOM processing for conditional text/HTML rendering |
| packages/email/src/message.ts | Complete rewrite with EmailMessage and SendableEmailMessage classes |
| packages/email/src/mailer.service.ts | New service implementation with nodemailer integration and async rendering |
| packages/email/src/logger.ts | Adds nodemailer logger adapter for NestJS integration |
| packages/email/src/index.ts | Updates public API exports |
| packages/email/src/email.service.ts | Removes legacy EmailService implementation |
| packages/email/src/email.options.ts | Simplifies configuration options structure |
| packages/email/src/email.module.ts | Refactors module to use ConfigurableModuleBuilder pattern |
| packages/email/src/email.module.test.ts | Updates tests for new API surface |
| packages/email/package.json | Updates dependencies and peer dependencies for v5 changes |
| packages/email/UPGRADE.md | Comprehensive migration guide for v5 upgrade |
| packages/email/README.md | Complete documentation rewrite covering new features |
| package.json | Minor Node.js types version update |
Comments suppressed due to low confidence (1)
packages/email/src/mailer.service.ts:164
- [nitpick] Method name 'sendMessage' is ambiguous since the class already has a public 'send' method. Consider renaming to 'transportMessage' or 'deliverMessage' for clarity.
private async sendMessage(msg: EmailMessage<RenderedProps>) {
Upgrading to v5
v5 revamps everything from the public surface area to the underlying implementation.
It adds support for other React JSX based email components, like
react-email,and allows loading data in templates via Suspense or React Server Components.
It uses
nodemailerto do the message compilation & transporting.Data Loading / Suspense / RSC
This allows templates to do async work.
Suspense
Any
Suspensestyle loading works:React Server Components (RSC)
If using React 19, the components can be async:
MJML
MJML is now optional. To keep using it, you need to add the dependencies explicitly:
The MJML components are now re-exported from:
These component types currently don't compile with React 19, but this can be worked around with:
A fix for this is pending here: mjml-react#133
Module Registration
The registration methods were renamed to be more idiomatic:
And the configuration structure has changed:
EmailModule.register({ - from: 'no-reply@example.com', - replyTo: 'helpdesk@example.com', + defaultHeaders: { + from: 'no-reply@example.com', + replyTo: 'helpdesk@example.com', + }, })Service Rename
The main service has been renamed to better convey it is an actor:
Rendering and Sending
Rendering now happens lazily when
send()is called. For this reason,renderwas renamed tocompose.Additionally, its parameters have been updated.
send()no longer will compose messages, and only accepts aEmailMessageobject.Messages can be composed with the
EmailMessageclass or withMailerService.composeHeaders
Just like
render/sendbefore, a to address can be given first, but now any headers can also be given:All headers can also be omitted and given later:
Note that this
.withHeaders()was renamed from.with()in v4.JSX Headers
We now provide a
Headerscomponent that can declare any headers.Headers declared here take precedence over the
defaultHeadersdeclared in the module config.Headers declared on the
EmailMessagetake precedence over the headers in the JSX body.Body
"Body" is now what the given JSX is called.
JSX can now be given directly
In case you don't want to use JSX in your file, the component & its props can be passed, just like before, except now
they're in a tuple.
Of course, the headers can be declared before the body as well:
The body can also be swapped out before sending if needed:
Plain text only / Bodiless messages
HTML rendering can be skipped completely now as well:
Subject
Previously, the exported
<Title>component would automatically set the subject.Now you need to explicitly set the subject using the
<Headers>component:<Title>A Notification</Title> + <Headers subject="A Notification" />Or in the message composition:
Text Rendering
The components for controlling text rendering have changed:
The
inTexthook has been removed.Before we rendered React twice, once for HTML and once for text.
This allowed conditional JS logic based on the specific output.
Now that bodies can be async, we don't want to do that work twice.
Templates must declare both text & HTML outputs at the same time.
Internally, we split & prune after the React rendering is done.
This is controlled by a
data-render-only="text/html"attribute.This can appear on any element in the render. The attribute will be stripped out before sending,
and the element will be removed if it is the opposite output format.