react-email-rails 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +347 -141
- data/lib/generators/react_email_rails/email_generator.rb +0 -1
- data/lib/react_email_rails/action_mailer.rb +44 -2
- data/lib/react_email_rails/configuration.rb +2 -0
- data/lib/react_email_rails/mailer_context.rb +40 -0
- data/lib/react_email_rails/shared_props.rb +44 -0
- data/lib/react_email_rails/version.rb +1 -1
- data/lib/react_email_rails.rb +4 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58d96423ee9be99d650a3ad7d164cbb8aca228ccbcc2b36d1a9be2bac2e06172
|
|
4
|
+
data.tar.gz: 370303cb64b83fd747009f59291ddf5f4598bd5670651ed89ec23edcf4b1072e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccee408e14a4f0b831ae58d19ce6891687b273a9f017e6b97184bdbeef7a79107a4e7ba32b4e5db551c4160e2a85b62509c9c85e1ee53bca394b77857af4e2c9
|
|
7
|
+
data.tar.gz: 262e70dd3acaf9b6bd0e8d1854649bb4a475634c4b0569c69dd2498f3a8bd9093bc33e89e35fe2c6785315247838122d081e4a26206c73d8d51da97698aa31a3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
- Every `react:` email now receives `mailer` and `message` props, mirroring Action Mailer's `mailer`/`message` ERB view helpers. Rendering now happens after Action Mailer assigns headers, so `message` includes subject, addressing, and default `from`/`reply_to` values. Per-mail and shared props win on conflict, serializers whose `as_json` returns a Hash receive the context, and collection props keep their original shape. The npm package exports matching `Mailer`/`Message` TypeScript types.
|
|
6
|
+
|
|
7
|
+
## 0.5.0
|
|
8
|
+
|
|
9
|
+
- Add `react_email_share` for sharing props across every `react:` email a mailer renders, mirroring inertia-rails' `inertia_share`. Supports static values, lazy lambdas and blocks (evaluated in the mailer instance), `only`/`except`/`if`/`unless` filters, and subclass inheritance. Per-mail props win over shared props.
|
|
10
|
+
- Shared props merge shallowly by default. Pass `deep_merge: true` to `mail` to merge nested hashes, or set `config.deep_merge_shared_props = true` to make it the default.
|
|
11
|
+
|
|
3
12
|
## 0.4.1
|
|
4
13
|
|
|
5
14
|
- `ReactEmailRails.parse` now neutralizes unsafe link/button URI schemes: hrefs whose scheme is not `http`, `https`, `mailto`, or `tel` (e.g. `javascript:`/`data:`) are blanked before they reach the document `Hash`. Scheme detection ignores the whitespace and control characters browsers strip when resolving a scheme, so case- and whitespace-obfuscated payloads are caught too.
|
data/README.md
CHANGED
|
@@ -2,15 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
# react-email-rails
|
|
4
4
|
|
|
5
|
-
Build and send
|
|
5
|
+
Build and send Action Mailer emails with [React Email](https://react.email), TypeScript, and Vite.
|
|
6
|
+
|
|
7
|
+
react-email-rails lets Rails render React Email components into HTML and plain text, then deliver them through the Action Mailer stack you already know: previews, headers, callbacks, queues, instrumentation, and delivery all keep working normally.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
HTML email is still awkward. React Email gives you a nicer component model, email-safe primitives, Tailwind support, and TypeScript. This gem connects that workflow to Rails without replacing Action Mailer.
|
|
12
|
+
|
|
13
|
+
You get:
|
|
14
|
+
|
|
15
|
+
- React Email components rendered from `mail(...)`
|
|
16
|
+
- HTML and plain-text output from the same component
|
|
17
|
+
- Rails mailer previews, tests, queues, callbacks, and delivery
|
|
18
|
+
- Vite-powered development rendering
|
|
19
|
+
- A production renderer bundle built during `assets:precompile`
|
|
20
|
+
- Optional persistent rendering for high-volume workers
|
|
21
|
+
- Optional server-side rendering for `@react-email/editor` documents
|
|
22
|
+
|
|
23
|
+
## Status
|
|
24
|
+
|
|
25
|
+
react-email-rails is pre-1.0. It was extracted from [XOXO](https://xoxo.email), which is pre-launch, so the API may still change and it has not yet been battle-tested in high-volume production.
|
|
26
|
+
|
|
27
|
+
The supported Ruby, Rails, Node, React, and Vite versions are tested in CI. Please [open an issue](https://github.com/heysupertape/react-email-rails/issues) for bugs, rough edges, or integration feedback.
|
|
6
28
|
|
|
7
29
|
## Contents
|
|
8
30
|
|
|
9
|
-
- [Why](#why)
|
|
10
|
-
- [How](#how)
|
|
11
|
-
- [Status](#status)
|
|
12
31
|
- [Requirements](#requirements)
|
|
13
|
-
- [
|
|
32
|
+
- [Installation](#installation)
|
|
33
|
+
- [Your First Email](#your-first-email)
|
|
34
|
+
- [How Rendering Works](#how-rendering-works)
|
|
14
35
|
- [Usage](#usage)
|
|
15
36
|
- [Configuration](#configuration)
|
|
16
37
|
- [Deployment](#deployment)
|
|
@@ -19,73 +40,58 @@ Build and send emails using React and Rails with [React Email](https://react.ema
|
|
|
19
40
|
- [Security](#security)
|
|
20
41
|
- [License](#license)
|
|
21
42
|
|
|
22
|
-
## Why
|
|
23
|
-
|
|
24
|
-
Building HTML emails is painfully archaic. [React Email](https://react.email) brings React, Tailwind, and TypeScript to email templates. This gem wires it into Action Mailer so React components deliver as generated HTML and text emails.
|
|
25
|
-
|
|
26
|
-
## How
|
|
27
|
-
|
|
28
|
-
**In development,** the gem renders components live through Vite's dev pipeline, so your emails get the same module resolution and transforms as the rest of your frontend.
|
|
29
|
-
|
|
30
|
-
**In production,** Rails builds a server-side renderer bundle during `assets:precompile` using `reactEmailRails()` in your Vite config for discovery and options.
|
|
31
|
-
|
|
32
|
-
react-email-rails automatically renders both HTML and plain-text versions from the same component. Delivery, headers, previews, queues, and callbacks all stay normal Action Mailer. If rendering fails, no email is sent and `ReactEmailRails::RenderError` is raised.
|
|
33
|
-
|
|
34
|
-
## Status
|
|
35
|
-
|
|
36
|
-
**react-email-rails is pre-1.0.** It's tested in CI across the supported Ruby, Rails, Node, and Vite versions, but it hasn't been battle-tested in high-volume production environments yet, and the API may still change before 1.0. Please [share feedback and report issues](https://github.com/heysupertape/react-email-rails/issues).
|
|
37
|
-
|
|
38
43
|
## Requirements
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
| Dependency | Version |
|
|
46
|
+
|------------|---------|
|
|
47
|
+
| Ruby | >= 3.3 |
|
|
48
|
+
| Rails | Action Mailer, Active Support, and Railties >= 7.1 and < 9.0 |
|
|
49
|
+
| Node | >= 20.19 |
|
|
50
|
+
| Vite | 7 or 8 |
|
|
51
|
+
| React | 18 or 19 |
|
|
52
|
+
| `@react-email/render` | 2.x |
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
We recommend [rails_vite](https://github.com/skryukov/rails_vite/) for Vite in Rails apps.
|
|
48
55
|
|
|
49
|
-
##
|
|
56
|
+
## Installation
|
|
50
57
|
|
|
51
|
-
Add the gem:
|
|
58
|
+
Add the Ruby gem:
|
|
52
59
|
|
|
53
60
|
```ruby
|
|
54
61
|
# Gemfile
|
|
55
|
-
|
|
56
62
|
gem "react-email-rails"
|
|
57
63
|
```
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Run the installer:
|
|
65
|
+
Then install it with Rails:
|
|
62
66
|
|
|
63
67
|
```sh
|
|
68
|
+
bundle install
|
|
64
69
|
bin/rails generate react_email_rails:install
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
The installer creates `config/initializers/react_email_rails.rb`, installs missing JavaScript dependencies when it can detect your package manager, adds `reactEmailRails()` to `vite.config.*`, and creates `app/javascript/emails`.
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
After installation, the normal Rails flow applies:
|
|
70
75
|
|
|
71
|
-
- `bin/rails generate react_email_rails:email
|
|
72
|
-
- `bin/dev`
|
|
73
|
-
- `bin/rails assets:precompile`
|
|
74
|
-
- `bin/rails react_email_rails:build`
|
|
76
|
+
- Generate mailers and components with `bin/rails generate react_email_rails:email ...`.
|
|
77
|
+
- Run `bin/dev` in development; email components render through Vite on demand.
|
|
78
|
+
- Run `bin/rails assets:precompile` for production; the renderer bundle builds automatically.
|
|
79
|
+
- Run `bin/rails react_email_rails:build` directly when CI or tests need the bundle without the full asset task.
|
|
75
80
|
|
|
76
|
-
### Manual
|
|
81
|
+
### Manual Setup
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
If you prefer to wire things up yourself, install the npm package and React Email dependencies:
|
|
79
84
|
|
|
80
85
|
```sh
|
|
81
86
|
npm i react-email-rails @react-email/render @react-email/components react react-dom
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
Use the equivalent command for pnpm, Yarn, or Bun if your app uses a different package manager.
|
|
90
|
+
|
|
91
|
+
Then add the Vite plugin:
|
|
85
92
|
|
|
86
93
|
```ts
|
|
87
94
|
// vite.config.ts
|
|
88
|
-
|
|
89
95
|
import { defineConfig } from "vite"
|
|
90
96
|
import { reactEmailRails } from "react-email-rails"
|
|
91
97
|
|
|
@@ -94,7 +100,7 @@ export default defineConfig({
|
|
|
94
100
|
})
|
|
95
101
|
```
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
## Your First Email
|
|
98
104
|
|
|
99
105
|
Generate a mailer and React Email component:
|
|
100
106
|
|
|
@@ -102,13 +108,15 @@ Generate a mailer and React Email component:
|
|
|
102
108
|
bin/rails generate react_email_rails:email Account welcome
|
|
103
109
|
```
|
|
104
110
|
|
|
105
|
-
The generator follows Rails' mailer generator shape: `NAME [method method]`. It creates
|
|
111
|
+
The generator follows Rails' mailer generator shape: `NAME [method method]`. It creates a mailer, React component, mailer preview, and test. It also reads `emails.path` and `emails.extension` from `reactEmailRails()` when available.
|
|
112
|
+
|
|
113
|
+
Pass flags when you need to override the detected component directory or extension:
|
|
106
114
|
|
|
107
115
|
```sh
|
|
108
116
|
bin/rails generate react_email_rails:email Account welcome --emails-path=app/emails --extension=jsx
|
|
109
117
|
```
|
|
110
118
|
|
|
111
|
-
|
|
119
|
+
Update the generated mailer to pass props into the React component:
|
|
112
120
|
|
|
113
121
|
```ruby
|
|
114
122
|
class AccountMailer < ApplicationMailer
|
|
@@ -128,11 +136,10 @@ class AccountMailer < ApplicationMailer
|
|
|
128
136
|
end
|
|
129
137
|
```
|
|
130
138
|
|
|
131
|
-
|
|
139
|
+
Then edit the generated component:
|
|
132
140
|
|
|
133
141
|
```tsx
|
|
134
142
|
// app/javascript/emails/account_mailer/welcome.tsx
|
|
135
|
-
|
|
136
143
|
import { Body, Container, Html, Text } from "@react-email/components"
|
|
137
144
|
|
|
138
145
|
type WelcomeProps = {
|
|
@@ -154,27 +161,70 @@ export default function Welcome({ account }: WelcomeProps) {
|
|
|
154
161
|
}
|
|
155
162
|
```
|
|
156
163
|
|
|
157
|
-
|
|
164
|
+
Deliver it like any other Action Mailer email:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
AccountMailer.with(account: current_account).welcome.deliver_later
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
React Email also provides primitives like [`<Button>`, `<Heading>`, `<Tailwind>`, and more](https://react.email/docs/components/html).
|
|
171
|
+
|
|
172
|
+
## How Rendering Works
|
|
173
|
+
|
|
174
|
+
In development, react-email-rails renders components through Vite's dev pipeline. Your email components get the same module resolution and transforms as the rest of your frontend.
|
|
175
|
+
|
|
176
|
+
In production, `assets:precompile` builds a server-side renderer bundle from your Vite config. Rails runs that bundle with Node whenever an email needs to render.
|
|
158
177
|
|
|
159
|
-
|
|
178
|
+
Every `react:` email renders HTML and plain text from the same component. If rendering fails, the email is not sent and `ReactEmailRails::RenderError` is raised.
|
|
160
179
|
|
|
161
180
|
## Usage
|
|
162
181
|
|
|
163
|
-
|
|
182
|
+
### Passing Props
|
|
183
|
+
|
|
184
|
+
Top-level keys passed to `react:` become props on the component's default export. The API is intentionally close to [inertia-rails](https://inertia-rails.dev), so apps using both libraries should feel consistent.
|
|
164
185
|
|
|
165
186
|
```ruby
|
|
166
|
-
mail
|
|
187
|
+
mail(
|
|
188
|
+
to: account.email,
|
|
189
|
+
subject: "Welcome",
|
|
190
|
+
react: {
|
|
191
|
+
account: {
|
|
192
|
+
name: account.name,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
)
|
|
167
196
|
```
|
|
168
197
|
|
|
169
198
|
```tsx
|
|
170
|
-
|
|
199
|
+
type WelcomeProps = {
|
|
200
|
+
account: {
|
|
201
|
+
name: string
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default function Welcome({ account }: WelcomeProps) {
|
|
171
206
|
// ...
|
|
172
207
|
}
|
|
173
208
|
```
|
|
174
209
|
|
|
175
|
-
|
|
210
|
+
### Component Inference
|
|
211
|
+
|
|
212
|
+
By default, react-email-rails infers the component from the mailer and action:
|
|
213
|
+
|
|
214
|
+
| Mailer action | Component |
|
|
215
|
+
|---------------|-----------|
|
|
216
|
+
| `AccountMailer#welcome` | `account_mailer/welcome` |
|
|
217
|
+
| `Users::InviteMailer#new_invite` | `users/invite_mailer/new_invite` |
|
|
176
218
|
|
|
177
|
-
|
|
219
|
+
`AccountMailer#welcome` resolves to `app/javascript/emails/account_mailer/welcome.tsx` or `.jsx` with the default Vite options.
|
|
220
|
+
|
|
221
|
+
Use `react: true` to render the inferred component. By default it renders with no props, which is handy for emails that take none:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
mail(to: account.email, subject: "Welcome", react: true)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
To have `react: true` build props automatically, enable `use_react_instance_props`. The component then receives the mailer's instance variables as props:
|
|
178
228
|
|
|
179
229
|
```ruby
|
|
180
230
|
class AccountMailer < ApplicationMailer
|
|
@@ -182,21 +232,24 @@ class AccountMailer < ApplicationMailer
|
|
|
182
232
|
|
|
183
233
|
def welcome
|
|
184
234
|
@account = params.fetch(:account)
|
|
185
|
-
|
|
235
|
+
|
|
236
|
+
mail(to: @account.email, subject: "Welcome", react: true)
|
|
186
237
|
end
|
|
187
238
|
end
|
|
188
239
|
```
|
|
189
240
|
|
|
190
|
-
Action Mailer's framework assigns
|
|
241
|
+
Action Mailer's framework assigns, including `params` and `rendered_format`, are excluded from instance props.
|
|
191
242
|
|
|
192
|
-
|
|
243
|
+
### Explicit Components
|
|
193
244
|
|
|
194
|
-
|
|
245
|
+
Pass a component name when the mailer action and component path do not line up:
|
|
195
246
|
|
|
196
247
|
```ruby
|
|
197
248
|
mail(
|
|
198
|
-
|
|
199
|
-
|
|
249
|
+
to: account.email,
|
|
250
|
+
subject: "Welcome",
|
|
251
|
+
react: "accounts/welcome",
|
|
252
|
+
props: {
|
|
200
253
|
account: {
|
|
201
254
|
name: account.name,
|
|
202
255
|
},
|
|
@@ -204,58 +257,168 @@ mail(
|
|
|
204
257
|
)
|
|
205
258
|
```
|
|
206
259
|
|
|
207
|
-
|
|
260
|
+
To change naming globally, override `component_path_resolver` in your [Rails configuration](#rails-configuration).
|
|
261
|
+
|
|
262
|
+
### Shared Props
|
|
263
|
+
|
|
264
|
+
Use `react_email_share` to merge props into every `react:` email for a mailer and its subclasses:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
class MarketingMailer < ApplicationMailer
|
|
268
|
+
react_email_share app_name: "Acme"
|
|
269
|
+
|
|
270
|
+
react_email_share unread_count: -> { params[:account]&.unread_count }
|
|
271
|
+
|
|
272
|
+
react_email_share do
|
|
273
|
+
{ brand: { name: "Acme", url: marketing_url } }
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Per-mail props win over shared props of the same name:
|
|
208
279
|
|
|
209
280
|
```ruby
|
|
210
281
|
mail(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
name: account.name,
|
|
216
|
-
},
|
|
282
|
+
to: account.email,
|
|
283
|
+
subject: "Welcome",
|
|
284
|
+
react: {
|
|
285
|
+
app_name: "Acme Pro",
|
|
217
286
|
},
|
|
218
287
|
)
|
|
219
288
|
```
|
|
220
289
|
|
|
221
|
-
|
|
290
|
+
Shared props work with `react:` hashes, `react: true`, and explicit `react: "component", props: ...` calls.
|
|
222
291
|
|
|
223
|
-
###
|
|
292
|
+
### Conditional Shared Props
|
|
224
293
|
|
|
225
|
-
|
|
294
|
+
`react_email_share` accepts the same filter options as `before_action`: `only`, `except`, `if`, and `unless`.
|
|
226
295
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
296
|
+
```ruby
|
|
297
|
+
react_email_share only: [:welcome, :reactivation] do
|
|
298
|
+
{ promotion: params.fetch(:promotion) }
|
|
299
|
+
end
|
|
231
300
|
|
|
232
|
-
|
|
301
|
+
react_email_share if: :account_active? do
|
|
302
|
+
{ account: { name: params.fetch(:account).name } }
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
You can also share props inside an action before calling `mail`:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
def welcome
|
|
310
|
+
account = params.fetch(:account)
|
|
311
|
+
|
|
312
|
+
react_email_share notice: "Thanks for joining!"
|
|
313
|
+
|
|
314
|
+
mail(
|
|
315
|
+
to: account.email,
|
|
316
|
+
subject: "Welcome",
|
|
317
|
+
react: { account: account.as_json(only: [:name]) },
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Deep Merging Shared Props
|
|
233
323
|
|
|
234
|
-
|
|
324
|
+
Shared props are merged shallowly by default. That means a per-mail prop replaces a shared prop with the same name.
|
|
325
|
+
|
|
326
|
+
Pass `deep_merge: true` to merge nested hashes instead:
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
react_email_share do
|
|
330
|
+
{ settings: { theme: "light", locale: "en" } }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
mail(
|
|
334
|
+
to: account.email,
|
|
335
|
+
subject: "Welcome",
|
|
336
|
+
react: {
|
|
337
|
+
settings: {
|
|
338
|
+
theme: "dark",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
deep_merge: true,
|
|
342
|
+
)
|
|
343
|
+
```
|
|
235
344
|
|
|
236
|
-
|
|
345
|
+
The component receives:
|
|
237
346
|
|
|
238
347
|
```ruby
|
|
239
|
-
|
|
348
|
+
{ settings: { theme: "dark", locale: "en" } }
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Set `config.deep_merge_shared_props = true` to make deep merging the default for every email.
|
|
352
|
+
|
|
353
|
+
### Mailer and Message Props
|
|
354
|
+
|
|
355
|
+
Every `react:` email receives `mailer` and `message` props, mirroring the [`mailer` and `message` view helpers](https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-view-helpers) available to Action Mailer ERB views.
|
|
356
|
+
|
|
357
|
+
`mailer` identifies the mailer action. `message` reflects the email after Action Mailer has assigned headers and defaults, including default `from` and `reply_to` values. With the default prop transform, import the `Mailer` and `Message` types to annotate them:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
import type { Mailer, Message } from "react-email-rails"
|
|
361
|
+
import { Body, Container, Html, Text } from "@react-email/components"
|
|
362
|
+
|
|
363
|
+
type WelcomeProps = {
|
|
364
|
+
account: { name: string }
|
|
365
|
+
mailer: Mailer
|
|
366
|
+
message: Message
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export default function Welcome({ account, mailer, message }: WelcomeProps) {
|
|
370
|
+
return (
|
|
371
|
+
<Html>
|
|
372
|
+
<Body>
|
|
373
|
+
<Container>
|
|
374
|
+
<Text>Welcome, {account.name}</Text>
|
|
375
|
+
<Text>Re: {message.subject}</Text>
|
|
376
|
+
</Container>
|
|
377
|
+
</Body>
|
|
378
|
+
</Html>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
240
381
|
```
|
|
241
382
|
|
|
242
|
-
|
|
383
|
+
| Prop | Example |
|
|
384
|
+
|------|---------|
|
|
385
|
+
| `mailer.mailerName` | `"account_mailer"` |
|
|
386
|
+
| `mailer.actionName` | `"welcome"` |
|
|
387
|
+
| `message.subject` | `"Welcome"` |
|
|
388
|
+
| `message.to` | `["account@example.com"]` |
|
|
389
|
+
| `message.cc`, `message.bcc` | `["…"]` or `null` |
|
|
390
|
+
| `message.from`, `message.replyTo` | `["app@example.com"]` |
|
|
391
|
+
|
|
392
|
+
Context is merged before prop serialization, so keys follow `config.transform_props` just like your own props. The exported TypeScript types describe the default `:lower_camel` shape.
|
|
393
|
+
|
|
394
|
+
Per-mail and shared props win on conflict, so a prop named `mailer` or `message` overrides the injected context. Serializer props receive the context when `as_json` returns a hash. Collections, arrays, and other non-object values pass through unchanged so their top-level shape is preserved.
|
|
243
395
|
|
|
244
396
|
### Prop Serialization
|
|
245
397
|
|
|
246
|
-
|
|
398
|
+
Props are serialized with `as_json`, just like `render json:`. You can pass hashes, arrays, Active Model objects, and serializer output from libraries such as [Alba](https://github.com/okuramasafumi/alba) or [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
|
|
247
399
|
|
|
248
|
-
|
|
400
|
+
Prop keys are camelized by default, so `plan_name` arrives in React as `planName`. See [Prop Transformation](#prop-transformation) to change that behavior.
|
|
401
|
+
|
|
402
|
+
### Component Files
|
|
249
403
|
|
|
250
|
-
|
|
404
|
+
Files and directories starting with `_` are ignored as renderable email entries by default. Use them for shared components, layouts, and helpers:
|
|
405
|
+
|
|
406
|
+
```text
|
|
407
|
+
app/javascript/emails/
|
|
408
|
+
account_mailer/
|
|
409
|
+
welcome.tsx
|
|
410
|
+
_components/
|
|
411
|
+
email_layout.tsx
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Ignored files can still be imported by email components.
|
|
251
415
|
|
|
252
416
|
### Layouts
|
|
253
417
|
|
|
254
|
-
Action Mailer layouts
|
|
418
|
+
Action Mailer layouts are not applied to `react:` emails. In React Email, layouts are normal React components:
|
|
255
419
|
|
|
256
420
|
```tsx
|
|
257
421
|
// app/javascript/emails/_components/email_layout.tsx
|
|
258
|
-
|
|
259
422
|
import { Body, Container, Html } from "@react-email/components"
|
|
260
423
|
import type { ReactNode } from "react"
|
|
261
424
|
|
|
@@ -276,7 +439,6 @@ export function EmailLayout({ children }: EmailLayoutProps) {
|
|
|
276
439
|
|
|
277
440
|
```tsx
|
|
278
441
|
// app/javascript/emails/account_mailer/welcome.tsx
|
|
279
|
-
|
|
280
442
|
import { Text } from "@react-email/components"
|
|
281
443
|
import { EmailLayout } from "../_components/email_layout"
|
|
282
444
|
|
|
@@ -289,21 +451,28 @@ export default function Welcome() {
|
|
|
289
451
|
}
|
|
290
452
|
```
|
|
291
453
|
|
|
292
|
-
|
|
454
|
+
### Editor Documents
|
|
293
455
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
If you're also using [@react-email/editor](https://react.email/docs/editor) to let users compose emails inside your app, `ReactEmailRails.compose` can render those stored documents on the server.
|
|
456
|
+
If your app uses [@react-email/editor](https://react.email/docs/editor) to let users compose emails visually, `ReactEmailRails.compose` can render stored editor documents on the server.
|
|
297
457
|
|
|
298
458
|
See [Editor rendering](docs/editor.md) for setup and usage.
|
|
299
459
|
|
|
300
460
|
## Configuration
|
|
301
461
|
|
|
302
|
-
Configuration
|
|
462
|
+
Configuration lives in two places:
|
|
463
|
+
|
|
464
|
+
- Rails configuration controls mailer behavior, prop handling, rendering mode, timeouts, and error hooks.
|
|
465
|
+
- Vite configuration controls email component discovery and the renderer bundle.
|
|
303
466
|
|
|
304
467
|
### Rails Configuration
|
|
305
468
|
|
|
306
|
-
|
|
469
|
+
Override Rails-side defaults in `config/initializers/react_email_rails.rb`:
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
ReactEmailRails.configure do |config|
|
|
473
|
+
# config.render_mode = :persistent
|
|
474
|
+
end
|
|
475
|
+
```
|
|
307
476
|
|
|
308
477
|
| Option | Default |
|
|
309
478
|
|--------|---------|
|
|
@@ -314,15 +483,16 @@ If the defaults don't fit, override them in `config/initializers/react_email_rai
|
|
|
314
483
|
| `render_timeout` | `10` seconds |
|
|
315
484
|
| `render_process_max_requests` | `1_000` |
|
|
316
485
|
| `on_render_error` | `nil` |
|
|
486
|
+
| `deep_merge_shared_props` | `false` |
|
|
317
487
|
|
|
318
|
-
|
|
488
|
+
### Prop Transformation
|
|
319
489
|
|
|
320
|
-
Set `transform_props` to
|
|
490
|
+
Set `transform_props` to choose how Ruby prop keys are exposed to React:
|
|
321
491
|
|
|
322
492
|
| Value | Example |
|
|
323
493
|
|-------|---------|
|
|
324
494
|
| `:camel` | `AccountName` |
|
|
325
|
-
| `:lower_camel`
|
|
495
|
+
| `:lower_camel` | `accountName` |
|
|
326
496
|
| `:dash` | `account-name` |
|
|
327
497
|
| `:snake` | `account_name` |
|
|
328
498
|
| `:none` | preserves serialized keys |
|
|
@@ -333,15 +503,15 @@ ReactEmailRails.configure do |config|
|
|
|
333
503
|
end
|
|
334
504
|
```
|
|
335
505
|
|
|
336
|
-
|
|
506
|
+
Only prop keys are transformed. Values are always serialized with `as_json`.
|
|
337
507
|
|
|
338
|
-
|
|
508
|
+
### Render Modes
|
|
339
509
|
|
|
340
|
-
`:subprocess` starts a fresh Node process for each render. It
|
|
510
|
+
The default `:subprocess` mode starts a fresh Node process for each render. It is simple, isolated, and always uses the latest bundle, but it pays Node startup and bundle load time for each email.
|
|
341
511
|
|
|
342
|
-
`:persistent`
|
|
512
|
+
`:persistent` mode keeps one long-lived Node child process per Ruby process. It is faster for render-heavy workers, but uses more memory and can serve a stale component until the child is recycled.
|
|
343
513
|
|
|
344
|
-
|
|
514
|
+
Switch to persistent mode when Node startup shows up in traces or when a worker renders many emails from the same bundle:
|
|
345
515
|
|
|
346
516
|
```ruby
|
|
347
517
|
ReactEmailRails.configure do |config|
|
|
@@ -349,21 +519,22 @@ ReactEmailRails.configure do |config|
|
|
|
349
519
|
end
|
|
350
520
|
```
|
|
351
521
|
|
|
352
|
-
Persistent mode
|
|
522
|
+
Persistent mode details:
|
|
353
523
|
|
|
354
|
-
- Renders are newline-delimited JSON and processed one at a time.
|
|
355
|
-
-
|
|
356
|
-
-
|
|
524
|
+
- Renders are newline-delimited JSON and processed one at a time.
|
|
525
|
+
- Throughput scales with more worker processes.
|
|
526
|
+
- It is fork-safe; clustered Puma and forking job runners spawn one Node child per worker.
|
|
527
|
+
- The child recycles after `render_process_max_requests` renders. Set that option to `nil` to disable recycling.
|
|
357
528
|
|
|
358
|
-
|
|
529
|
+
### Render Options
|
|
359
530
|
|
|
360
|
-
`render_options` is passed to [@react-email/render](https://react.email/docs/utilities/render). Use `html` and `text` keys
|
|
531
|
+
`render_options` is passed to [@react-email/render](https://react.email/docs/utilities/render). Use `html` and `text` keys to configure each output. Option keys are camelized before they cross into JavaScript.
|
|
361
532
|
|
|
362
533
|
```ruby
|
|
363
534
|
ReactEmailRails.configure do |config|
|
|
364
535
|
config.render_options = {
|
|
365
536
|
html: {
|
|
366
|
-
pretty: Rails.env.development
|
|
537
|
+
pretty: Rails.env.development?,
|
|
367
538
|
},
|
|
368
539
|
text: {
|
|
369
540
|
html_to_text_options: {
|
|
@@ -374,9 +545,11 @@ ReactEmailRails.configure do |config|
|
|
|
374
545
|
end
|
|
375
546
|
```
|
|
376
547
|
|
|
377
|
-
|
|
548
|
+
`render_options` can also be a callable. When rendering from a mailer, it is evaluated in the mailer instance so it can use helpers or per-email state.
|
|
378
549
|
|
|
379
|
-
|
|
550
|
+
### Error Reporting
|
|
551
|
+
|
|
552
|
+
Use `on_render_error` to report failures before the exception is re-raised:
|
|
380
553
|
|
|
381
554
|
```ruby
|
|
382
555
|
ReactEmailRails.configure do |config|
|
|
@@ -386,33 +559,50 @@ ReactEmailRails.configure do |config|
|
|
|
386
559
|
end
|
|
387
560
|
```
|
|
388
561
|
|
|
389
|
-
|
|
562
|
+
The callback receives the error plus render context such as `kind:` and `component:`.
|
|
563
|
+
|
|
564
|
+
### Instrumentation
|
|
390
565
|
|
|
391
|
-
Every render emits `render.react-email-rails` through [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html).
|
|
566
|
+
Every render emits `render.react-email-rails` through [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html). Email payloads include `kind`, `component`, and successful HTML size in `html_bytes`.
|
|
392
567
|
|
|
393
568
|
```ruby
|
|
394
569
|
ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
|
|
395
|
-
Rails.logger.info(
|
|
570
|
+
Rails.logger.info(
|
|
571
|
+
"[react-email-rails] Rendered #{event.payload[:component]} " \
|
|
572
|
+
"(#{event.duration.round}ms, #{event.payload[:html_bytes]} bytes)",
|
|
573
|
+
)
|
|
396
574
|
end
|
|
397
575
|
```
|
|
398
576
|
|
|
399
577
|
### Vite Configuration
|
|
400
578
|
|
|
401
|
-
Most apps only need the
|
|
579
|
+
Most apps only need the plugin from [Installation](#installation):
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
import { defineConfig } from "vite"
|
|
583
|
+
import { reactEmailRails } from "react-email-rails"
|
|
584
|
+
|
|
585
|
+
export default defineConfig({
|
|
586
|
+
plugins: [reactEmailRails()],
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
The isolated renderer loads `reactEmailRails()`, JSX support, and component-facing Vite config such as `resolve`, `define`, `css`, `json`, `assetsInclude`, `esbuild`, and `oxc`. It does not load your other app plugins unless you explicitly add them with `vite.plugins`.
|
|
402
591
|
|
|
403
|
-
|
|
592
|
+
Server, preview, dependency optimization, and build output settings stay owned by react-email-rails so Rails can always find the renderer bundle.
|
|
404
593
|
|
|
405
|
-
|
|
594
|
+
### Vite Plugin Options
|
|
406
595
|
|
|
407
596
|
| Option | Default | Description |
|
|
408
597
|
|--------|---------|-------------|
|
|
409
598
|
| `emails.path` | `"app/javascript/emails"` | Directory containing email components |
|
|
410
599
|
| `emails.extension` | `[".tsx", ".jsx"]` | Component extension, or an array of extensions |
|
|
411
600
|
| `emails.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `emails.path` |
|
|
601
|
+
| `documents` | `false` | Optional `@react-email/editor` document renderer discovery; see [Editor rendering](docs/editor.md) |
|
|
412
602
|
| `standalone` | `true` | Inline production renderer bundle dependencies |
|
|
413
603
|
| `vite` | `{}` | Extra email-only Vite config for compilation and resolution |
|
|
414
604
|
|
|
415
|
-
Use a custom directory:
|
|
605
|
+
Use a custom email directory:
|
|
416
606
|
|
|
417
607
|
```ts
|
|
418
608
|
reactEmailRails({
|
|
@@ -420,7 +610,7 @@ reactEmailRails({
|
|
|
420
610
|
})
|
|
421
611
|
```
|
|
422
612
|
|
|
423
|
-
Use
|
|
613
|
+
Use custom extensions:
|
|
424
614
|
|
|
425
615
|
```ts
|
|
426
616
|
reactEmailRails({
|
|
@@ -430,11 +620,11 @@ reactEmailRails({
|
|
|
430
620
|
})
|
|
431
621
|
```
|
|
432
622
|
|
|
433
|
-
Component names come from the Vite directory layout
|
|
623
|
+
Component names come from the Vite directory layout. To map mailer actions to a different layout, override `component_path_resolver` on the Ruby side so both halves stay in sync.
|
|
434
624
|
|
|
435
|
-
|
|
625
|
+
### Email-Only Vite Plugins
|
|
436
626
|
|
|
437
|
-
Most apps
|
|
627
|
+
Most apps do not need extra email plugins. If email components need a transform that is not part of Vite's default pipeline, add it to the isolated renderer:
|
|
438
628
|
|
|
439
629
|
```ts
|
|
440
630
|
import mdx from "@mdx-js/rollup"
|
|
@@ -452,13 +642,13 @@ export default defineConfig({
|
|
|
452
642
|
})
|
|
453
643
|
```
|
|
454
644
|
|
|
455
|
-
|
|
645
|
+
Only `assetsInclude`, `css`, `define`, `esbuild`, `json`, `oxc`, `plugins`, and `resolve` are accepted inside `vite`. Output settings such as `build.outDir` and `build.rollupOptions` are ignored.
|
|
456
646
|
|
|
457
|
-
|
|
647
|
+
### Standalone Builds
|
|
458
648
|
|
|
459
|
-
By default the production renderer bundle inlines React, `@react-email/render`, and other Node dependencies. This works well for Rails deploys that build assets in one stage and run without `node_modules` in the final image.
|
|
649
|
+
By default, the production renderer bundle inlines React, `@react-email/render`, and other Node dependencies. This works well for Rails deploys that build assets in one stage and run without `node_modules` in the final image.
|
|
460
650
|
|
|
461
|
-
Set `standalone: false` when your runtime
|
|
651
|
+
Set `standalone: false` when your runtime ships `node_modules` and you prefer a smaller SSR-style bundle:
|
|
462
652
|
|
|
463
653
|
```ts
|
|
464
654
|
reactEmailRails({
|
|
@@ -466,45 +656,43 @@ reactEmailRails({
|
|
|
466
656
|
})
|
|
467
657
|
```
|
|
468
658
|
|
|
469
|
-
Externalized bundles
|
|
659
|
+
Externalized bundles can be smaller and may build faster, but the externalized packages must be available at runtime.
|
|
470
660
|
|
|
471
661
|
## Deployment
|
|
472
662
|
|
|
473
|
-
|
|
663
|
+
Production deploys should run the normal Rails asset task:
|
|
474
664
|
|
|
475
665
|
```sh
|
|
476
666
|
bin/rails assets:precompile
|
|
477
667
|
```
|
|
478
668
|
|
|
479
|
-
|
|
669
|
+
react-email-rails hooks `react_email_rails:build` into `assets:precompile`. The build task loads `reactEmailRails()` options from your Vite config and writes `tmp/react-email-rails/emails.js` with the email component registry. If [Editor rendering](docs/editor.md) is enabled, the bundle also includes document renderers.
|
|
480
670
|
|
|
481
|
-
You can run
|
|
671
|
+
You can run the renderer build directly:
|
|
482
672
|
|
|
483
673
|
```sh
|
|
484
674
|
bin/rails react_email_rails:build
|
|
485
675
|
```
|
|
486
676
|
|
|
487
|
-
Production rendering
|
|
677
|
+
Production rendering requires that bundle. If it is missing, rendering raises `ReactEmailRails::RenderError` and Action Mailer does not send the email.
|
|
488
678
|
|
|
489
|
-
|
|
679
|
+
Set `SKIP_REACT_EMAIL_RAILS_BUILD=1` to skip the automatic asset hook. Directly running `bin/rails react_email_rails:build` always attempts the build.
|
|
490
680
|
|
|
491
|
-
The
|
|
681
|
+
The npm package, Vite, React, and `@react-email/render` must be available when Rails runs `assets:precompile`. If [Editor rendering](docs/editor.md) is enabled, its peer dependencies must be available too.
|
|
492
682
|
|
|
493
|
-
The Ruby gem and npm package must stay on the same version.
|
|
683
|
+
The Ruby gem and npm package must stay on the same version. A protocol/version handshake catches mismatched installs and raises an actionable `ReactEmailRails::RenderError`.
|
|
494
684
|
|
|
495
|
-
|
|
685
|
+
### Verify the Renderer
|
|
496
686
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
To confirm the renderer is ready before relying on it, run:
|
|
687
|
+
Run this before relying on the production renderer:
|
|
500
688
|
|
|
501
689
|
```sh
|
|
502
690
|
bin/rails react_email_rails:verify
|
|
503
691
|
```
|
|
504
692
|
|
|
505
|
-
It checks that the render command runs and that the npm package version matches the gem
|
|
693
|
+
It checks that the render command runs and that the npm package version matches the gem. It exits non-zero with an actionable message on failure, so it is safe to wire into CI or release steps.
|
|
506
694
|
|
|
507
|
-
For programmatic checks
|
|
695
|
+
For programmatic checks, `ReactEmailRails.healthy?` returns a boolean:
|
|
508
696
|
|
|
509
697
|
```ruby
|
|
510
698
|
Rails.application.config.after_initialize do
|
|
@@ -514,13 +702,31 @@ Rails.application.config.after_initialize do
|
|
|
514
702
|
end
|
|
515
703
|
```
|
|
516
704
|
|
|
705
|
+
If you check at boot, scope it to processes that send mail so the rest of the app does not pay the cost.
|
|
706
|
+
|
|
517
707
|
## Development
|
|
518
708
|
|
|
519
709
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, checks, formatting, and release verification.
|
|
520
710
|
|
|
711
|
+
The short version:
|
|
712
|
+
|
|
713
|
+
```sh
|
|
714
|
+
bundle install
|
|
715
|
+
cd vite && pnpm install
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
Run the core checks before opening a pull request:
|
|
719
|
+
|
|
720
|
+
```sh
|
|
721
|
+
ruby scripts/check_version_sync.rb
|
|
722
|
+
bin/test
|
|
723
|
+
bin/lint
|
|
724
|
+
cd vite && pnpm run build
|
|
725
|
+
```
|
|
726
|
+
|
|
521
727
|
## Contributing
|
|
522
728
|
|
|
523
|
-
Bug reports and pull requests are welcome
|
|
729
|
+
Bug reports and pull requests are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request so the local checks and release expectations are clear.
|
|
524
730
|
|
|
525
731
|
## Security
|
|
526
732
|
|
|
@@ -528,4 +734,4 @@ Please report vulnerabilities privately. See [SECURITY.md](SECURITY.md) for deta
|
|
|
528
734
|
|
|
529
735
|
## License
|
|
530
736
|
|
|
531
|
-
The gem and npm package are available as open source under the terms of the [MIT License](LICENSE.md).
|
|
737
|
+
The Ruby gem and npm package are available as open source under the terms of the [MIT License](LICENSE.md).
|
|
@@ -164,7 +164,6 @@ class ReactEmailRails::Generators::EmailGenerator < Rails::Generators::NamedBase
|
|
|
164
164
|
)
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
-
# First capture-group match across an ordered list of patterns, or nil.
|
|
168
167
|
def first_capture(source, *patterns)
|
|
169
168
|
patterns.each do |pattern|
|
|
170
169
|
match = source[pattern, 1]
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module ReactEmailRails::ActionMailer
|
|
2
2
|
extend(ActiveSupport::Concern)
|
|
3
3
|
|
|
4
|
+
# `react_email_share` kwargs reserved for `before_action` filtering, not prop data.
|
|
5
|
+
SHARED_FILTER_OPTIONS = [:if, :unless, :only, :except].freeze
|
|
6
|
+
|
|
4
7
|
prepended do
|
|
5
8
|
class_attribute(:react_email_use_instance_props, default: false)
|
|
6
9
|
end
|
|
@@ -9,6 +12,19 @@ module ReactEmailRails::ActionMailer
|
|
|
9
12
|
def use_react_instance_props
|
|
10
13
|
self.react_email_use_instance_props = true
|
|
11
14
|
end
|
|
15
|
+
|
|
16
|
+
def react_email_share(hash = nil, **props, &block)
|
|
17
|
+
options = props.slice(*SHARED_FILTER_OPTIONS)
|
|
18
|
+
data = hash || props.except(*SHARED_FILTER_OPTIONS)
|
|
19
|
+
|
|
20
|
+
before_action(**options) do
|
|
21
|
+
react_email_append_shared(data, block)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def react_email_share(hash = nil, **props, &block)
|
|
27
|
+
react_email_append_shared(hash || props, block)
|
|
12
28
|
end
|
|
13
29
|
|
|
14
30
|
def mail(headers = {}, &block)
|
|
@@ -17,15 +33,41 @@ module ReactEmailRails::ActionMailer
|
|
|
17
33
|
headers = headers.dup
|
|
18
34
|
react = headers.delete(:react)
|
|
19
35
|
props = headers.delete(:props) if headers.key?(:props)
|
|
36
|
+
deep_merge = headers.delete(:deep_merge) if headers.key?(:deep_merge)
|
|
20
37
|
|
|
21
38
|
component, resolved_props = ReactEmailRails::PropsResolver.new(self).resolve(react, props)
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
resolved_props = ReactEmailRails::SharedProps.new(self).merge_into(
|
|
40
|
+
resolved_props,
|
|
41
|
+
deep_merge: react_email_deep_merge?(deep_merge),
|
|
42
|
+
)
|
|
24
43
|
|
|
25
44
|
super(headers) do |format|
|
|
45
|
+
rendered = react_email_render(component, resolved_props)
|
|
46
|
+
|
|
26
47
|
format.html { rendered.html }
|
|
27
48
|
format.text { rendered.text } if rendered.text.present?
|
|
49
|
+
|
|
28
50
|
yield(format) if block
|
|
29
51
|
end
|
|
30
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def react_email_render(component, props)
|
|
57
|
+
props = ReactEmailRails::MailerContext.new(self).merge_into(props)
|
|
58
|
+
render_options = ReactEmailRails.configuration.resolve_render_options(self)
|
|
59
|
+
ReactEmailRails.render(component:, props:, render_options:)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def react_email_append_shared(data, block)
|
|
63
|
+
store = (@_react_email_shared ||= [])
|
|
64
|
+
store << data.dup.freeze if data.present?
|
|
65
|
+
store << block if block
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def react_email_deep_merge?(override)
|
|
69
|
+
return ReactEmailRails.configuration.deep_merge_shared_props if override.nil?
|
|
70
|
+
|
|
71
|
+
override
|
|
72
|
+
end
|
|
31
73
|
end
|
|
@@ -34,6 +34,7 @@ class ReactEmailRails::Configuration
|
|
|
34
34
|
:render_options,
|
|
35
35
|
:transform_props,
|
|
36
36
|
:on_render_error,
|
|
37
|
+
:deep_merge_shared_props,
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
attr_reader(
|
|
@@ -52,6 +53,7 @@ class ReactEmailRails::Configuration
|
|
|
52
53
|
config.render_process_max_requests = DEFAULT_RENDER_PROCESS_MAX_REQUESTS
|
|
53
54
|
config.transform_props = :lower_camel
|
|
54
55
|
config.on_render_error = nil
|
|
56
|
+
config.deep_merge_shared_props = false
|
|
55
57
|
end
|
|
56
58
|
end
|
|
57
59
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class ReactEmailRails::MailerContext
|
|
2
|
+
MESSAGE_FIELDS = [:subject, :to, :cc, :bcc, :from, :reply_to].freeze
|
|
3
|
+
|
|
4
|
+
def initialize(mailer)
|
|
5
|
+
@mailer = mailer
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def merge_into(props)
|
|
9
|
+
serialized_props = props.as_json
|
|
10
|
+
return props unless serialized_props.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
to_h.merge(serialized_props)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{
|
|
17
|
+
"mailer" => mailer_context,
|
|
18
|
+
"message" => message_context,
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader(:mailer)
|
|
25
|
+
|
|
26
|
+
def mailer_context
|
|
27
|
+
{
|
|
28
|
+
"mailer_name" => mailer.class.mailer_name,
|
|
29
|
+
"action_name" => mailer.action_name,
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def message_context
|
|
34
|
+
message = mailer.message
|
|
35
|
+
|
|
36
|
+
MESSAGE_FIELDS.each_with_object({}) do |field, context|
|
|
37
|
+
context[field.to_s] = message.public_send(field)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Collects props registered with `react_email_share` and merges them beneath the
|
|
2
|
+
# per-mail props, which win on conflict. Mirrors inertia-rails shared data.
|
|
3
|
+
class ReactEmailRails::SharedProps
|
|
4
|
+
IVAR = :@_react_email_shared
|
|
5
|
+
|
|
6
|
+
def initialize(mailer)
|
|
7
|
+
@mailer = mailer
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns `props` untouched when nothing is shared, so non-Hash inputs (e.g.
|
|
11
|
+
# serializers) still flow straight through to serialization.
|
|
12
|
+
def merge_into(props, deep_merge:)
|
|
13
|
+
shared = to_h
|
|
14
|
+
return props if shared.empty?
|
|
15
|
+
|
|
16
|
+
base = shared.as_json
|
|
17
|
+
incoming = props.as_json
|
|
18
|
+
deep_merge ? base.deep_merge(incoming) : base.merge(incoming)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
entries.each_with_object({}) do |entry, props|
|
|
23
|
+
props.merge!(resolve(entry))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader(:mailer)
|
|
30
|
+
|
|
31
|
+
def entries
|
|
32
|
+
mailer.instance_variable_get(IVAR) || []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# An entry is either a static Hash or a block evaluated at render time. Callable
|
|
36
|
+
# values inside a Hash are evaluated too, so `unread_count: -> { ... }` works.
|
|
37
|
+
def resolve(entry)
|
|
38
|
+
hash = entry.respond_to?(:call) ? mailer.instance_exec(&entry) : entry
|
|
39
|
+
|
|
40
|
+
(hash || {}).transform_values do |value|
|
|
41
|
+
value.respond_to?(:call) ? mailer.instance_exec(&value) : value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/react_email_rails.rb
CHANGED
|
@@ -5,6 +5,7 @@ require("active_support/concern")
|
|
|
5
5
|
require("active_support/notifications")
|
|
6
6
|
require("active_support/core_ext/object/blank")
|
|
7
7
|
require("active_support/core_ext/object/json")
|
|
8
|
+
require("active_support/core_ext/hash/deep_merge")
|
|
8
9
|
require("active_support/inflector")
|
|
9
10
|
require("rails/railtie")
|
|
10
11
|
|
|
@@ -24,6 +25,8 @@ require_relative("react_email_rails/render_modes/persistent/command_runner")
|
|
|
24
25
|
require_relative("react_email_rails/configuration")
|
|
25
26
|
require_relative("react_email_rails/tasks")
|
|
26
27
|
require_relative("react_email_rails/props_resolver")
|
|
28
|
+
require_relative("react_email_rails/shared_props")
|
|
29
|
+
require_relative("react_email_rails/mailer_context")
|
|
27
30
|
require_relative("react_email_rails/railtie")
|
|
28
31
|
|
|
29
32
|
module ReactEmailRails
|
|
@@ -56,8 +59,7 @@ module ReactEmailRails
|
|
|
56
59
|
perform(payload:, label: type, kind: "document", type:)
|
|
57
60
|
end
|
|
58
61
|
|
|
59
|
-
#
|
|
60
|
-
# extensions. Pass exactly one of `html:` or `markdown:`.
|
|
62
|
+
# Returns an editor document Hash, built via the renderer's extensions. Pass exactly one of `html:`/`markdown:`.
|
|
61
63
|
def parse(type:, html: nil, markdown: nil, context: {})
|
|
62
64
|
payload = {
|
|
63
65
|
kind: "parse",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: react-email-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Supertape
|
|
@@ -153,6 +153,7 @@ files:
|
|
|
153
153
|
- lib/react_email_rails.rb
|
|
154
154
|
- lib/react_email_rails/action_mailer.rb
|
|
155
155
|
- lib/react_email_rails/configuration.rb
|
|
156
|
+
- lib/react_email_rails/mailer_context.rb
|
|
156
157
|
- lib/react_email_rails/props_resolver.rb
|
|
157
158
|
- lib/react_email_rails/railtie.rb
|
|
158
159
|
- lib/react_email_rails/render_error.rb
|
|
@@ -164,6 +165,7 @@ files:
|
|
|
164
165
|
- lib/react_email_rails/render_modes/subprocess/command_runner.rb
|
|
165
166
|
- lib/react_email_rails/render_protocol.rb
|
|
166
167
|
- lib/react_email_rails/rendered_email.rb
|
|
168
|
+
- lib/react_email_rails/shared_props.rb
|
|
167
169
|
- lib/react_email_rails/tasks.rb
|
|
168
170
|
- lib/react_email_rails/version.rb
|
|
169
171
|
- lib/tasks/react_email_rails/build.rake
|