react-email-rails 0.5.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bbfbb40c2044856c9061ba2a3c134d07dbd5aa9b58b6cfe3d576d25fe45b4d2
4
- data.tar.gz: 7c272a65e71044f813c813b248a48bc69e3a043494ad9f6133ae8c69ab006b24
3
+ metadata.gz: 58d96423ee9be99d650a3ad7d164cbb8aca228ccbcc2b36d1a9be2bac2e06172
4
+ data.tar.gz: 370303cb64b83fd747009f59291ddf5f4598bd5670651ed89ec23edcf4b1072e
5
5
  SHA512:
6
- metadata.gz: c5b434f0983a8c1280d622203869f535a0c98fb293bcfff3d51d39a8676d730d000b901cb9bcc6b3b478fe633904974798131482e44982140f40e12951ac1423
7
- data.tar.gz: 21652f85cc0ee448ec90ffd7b24c4eaf05b892dbe0e83d487777ae57bbcda7a34c46e1b9becad9174838edd0d33919b11aa9efb56cbd520433803c8a3fd82385
6
+ metadata.gz: ccee408e14a4f0b831ae58d19ce6891687b273a9f017e6b97184bdbeef7a79107a4e7ba32b4e5db551c4160e2a85b62509c9c85e1ee53bca394b77857af4e2c9
7
+ data.tar.gz: 262e70dd3acaf9b6bd0e8d1854649bb4a475634c4b0569c69dd2498f3a8bd9093bc33e89e35fe2c6785315247838122d081e4a26206c73d8d51da97698aa31a3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
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
+
3
7
  ## 0.5.0
4
8
 
5
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.
data/README.md CHANGED
@@ -2,15 +2,36 @@
2
2
 
3
3
  # react-email-rails
4
4
 
5
- Build and send emails using React and Rails with [React Email](https://react.email) and [Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html).
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
- - [Quick Start](#quick-start)
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 was extracted from [XOXO](https://xoxo.email) which is pre-launch, so it hasn't been battle-tested in high-volume production environments yet. It's tested in CI across the supported Ruby, Rails, Node, and Vite versions, but 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
- - Ruby >= 3.3
41
- - Action Mailer, Active Support, and Railties >= 7.1 and < 9.0
42
- - Node >= 20.19
43
- - Vite 7 or 8
44
- - React 18 or 19
45
- - `@react-email/render` 2.x
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
- > We recommend [rails_vite](https://github.com/skryukov/rails_vite/) for Vite with Rails.
54
+ We recommend [rails_vite](https://github.com/skryukov/rails_vite/) for Vite in Rails apps.
48
55
 
49
- ## Quick Start
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
- ### Automatic Install
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
- This 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`.
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
- The installed setup then follows the normal Rails lifecycle:
74
+ After installation, the normal Rails flow applies:
70
75
 
71
- - `bin/rails generate react_email_rails:email ...` creates matching mailers and React components.
72
- - `bin/dev` renders through Vite on demand.
73
- - `bin/rails assets:precompile` builds the production renderer bundle automatically.
74
- - `bin/rails react_email_rails:build` builds the bundle directly when CI or tests need it.
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 Install
81
+ ### Manual Setup
77
82
 
78
- Install the npm package and peer dependencies manually:
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
- Update your Vite config to add the plugin:
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
- ### Your First Email
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 the mailer, matching React components, a mailer preview, and a test. It reads `emails.path` and `emails.extension` from `reactEmailRails()` when available. Pass flags to override them:
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
- Edit the generated mailer to pass any necessary props:
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
- Edit the generated email component:
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,29 +161,70 @@ export default function Welcome({ account }: WelcomeProps) {
154
161
  }
155
162
  ```
156
163
 
157
- > [@react-email/components](https://react.email/docs/components/html) provides primitives like `<Button>`, `<Heading>`, `<Tailwind>`, and more.
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
- That's it. It now renders and delivers like any other Action Mailer email.
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
- Pass data from your mailer and each top-level key becomes a prop on the component's default export. Our API mirrors [inertia-rails](https://inertia-rails.dev), making the two libraries feel consistent when used together.
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 react: { foo: "bar" }, ...
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
- export default function Email({ foo }: { foo: string }) {
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
- ### Props
210
+ ### Component Inference
176
211
 
177
- Choose the level of inference you want for your props:
212
+ By default, react-email-rails infers the component from the mailer and action:
178
213
 
179
- #### Implicit Component, Instance Props
214
+ | Mailer action | Component |
215
+ |---------------|-----------|
216
+ | `AccountMailer#welcome` | `account_mailer/welcome` |
217
+ | `Users::InviteMailer#new_invite` | `users/invite_mailer/new_invite` |
218
+
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:
180
228
 
181
229
  ```ruby
182
230
  class AccountMailer < ApplicationMailer
@@ -184,33 +232,22 @@ class AccountMailer < ApplicationMailer
184
232
 
185
233
  def welcome
186
234
  @account = params.fetch(:account)
187
- mail react: true, to: @account.email, subject: "Welcome"
235
+
236
+ mail(to: @account.email, subject: "Welcome", react: true)
188
237
  end
189
238
  end
190
239
  ```
191
240
 
192
- Action Mailer's framework assigns (including `params` and `rendered_format`) are excluded from instance props.
193
-
194
- Without `use_react_instance_props`, `react: true` still infers the component and renders it with no props, which is handy for emails that take no props at all.
195
-
196
- #### Implicit Component, Explicit Props
241
+ Action Mailer's framework assigns, including `params` and `rendered_format`, are excluded from instance props.
197
242
 
198
- ```ruby
199
- mail(
200
- ...
201
- react: {
202
- account: {
203
- name: account.name,
204
- },
205
- },
206
- )
207
- ```
243
+ ### Explicit Components
208
244
 
209
- #### Explicit Component, Explicit Props
245
+ Pass a component name when the mailer action and component path do not line up:
210
246
 
211
247
  ```ruby
212
248
  mail(
213
- ...
249
+ to: account.email,
250
+ subject: "Welcome",
214
251
  react: "accounts/welcome",
215
252
  props: {
216
253
  account: {
@@ -220,110 +257,168 @@ mail(
220
257
  )
221
258
  ```
222
259
 
260
+ To change naming globally, override `component_path_resolver` in your [Rails configuration](#rails-configuration).
261
+
223
262
  ### Shared Props
224
263
 
225
- Use `react_email_share` to share data with all of a mailer's emails and subclasses, and it'll be automatically merged with any inline props.
264
+ Use `react_email_share` to merge props into every `react:` email for a mailer and its subclasses:
226
265
 
227
266
  ```ruby
228
267
  class MarketingMailer < ApplicationMailer
229
- # Static value
230
268
  react_email_share app_name: "Acme"
231
269
 
232
- # Lambda value, evaluated lazily in the mailer instance
233
- react_email_share unread_count: -> { current_user&.unread_count }
270
+ react_email_share unread_count: -> { params[:account]&.unread_count }
234
271
 
235
- # Block, evaluated lazily in the mailer instance
236
272
  react_email_share do
237
273
  { brand: { name: "Acme", url: marketing_url } }
238
274
  end
239
275
  end
240
276
  ```
241
277
 
242
- Per-mail props win over shared props of the same name, so a mailer can always override what it inherits:
278
+ Per-mail props win over shared props of the same name:
243
279
 
244
280
  ```ruby
245
- mail react: { app_name: "Acme Pro" }, ... # overrides the shared app_name
281
+ mail(
282
+ to: account.email,
283
+ subject: "Welcome",
284
+ react: {
285
+ app_name: "Acme Pro",
286
+ },
287
+ )
246
288
  ```
247
289
 
248
- Shared props apply to all three forms above (`react:` hash, `react: "component", props:`, and `react: true`).
290
+ Shared props work with `react:` hashes, `react: true`, and explicit `react: "component", props: ...` calls.
249
291
 
250
- #### Conditional Sharing
292
+ ### Conditional Shared Props
251
293
 
252
- Pass any `before_action` filter (`only`, `except`, `if`, `unless`) to scope a share to certain actions:
294
+ `react_email_share` accepts the same filter options as `before_action`: `only`, `except`, `if`, and `unless`.
253
295
 
254
296
  ```ruby
255
297
  react_email_share only: [:welcome, :reactivation] do
256
- { promotion: current_promotion }
298
+ { promotion: params.fetch(:promotion) }
257
299
  end
258
300
 
259
- react_email_share if: :user_signed_in? do
260
- { user: { name: current_user.name } }
301
+ react_email_share if: :account_active? do
302
+ { account: { name: params.fetch(:account).name } }
261
303
  end
262
304
  ```
263
305
 
264
- You can also share from inside an action, before calling `mail`:
306
+ You can also share props inside an action before calling `mail`:
265
307
 
266
308
  ```ruby
267
309
  def welcome
310
+ account = params.fetch(:account)
311
+
268
312
  react_email_share notice: "Thanks for joining!"
269
- mail react: { account: }, to: account.email, subject: "Welcome"
313
+
314
+ mail(
315
+ to: account.email,
316
+ subject: "Welcome",
317
+ react: { account: account.as_json(only: [:name]) },
318
+ )
270
319
  end
271
320
  ```
272
321
 
273
- #### Deep Merging
322
+ ### Deep Merging Shared Props
323
+
324
+ Shared props are merged shallowly by default. That means a per-mail prop replaces a shared prop with the same name.
274
325
 
275
- By default shared props are merged shallowly, so a per-mail prop replaces a shared one of the same name outright. Pass `deep_merge: true` to merge nested hashes instead:
326
+ Pass `deep_merge: true` to merge nested hashes instead:
276
327
 
277
328
  ```ruby
278
329
  react_email_share do
279
330
  { settings: { theme: "light", locale: "en" } }
280
331
  end
281
332
 
282
- # Shallow (default): settings => { theme: "dark" }
283
- mail react: { settings: { theme: "dark" } }, ...
333
+ mail(
334
+ to: account.email,
335
+ subject: "Welcome",
336
+ react: {
337
+ settings: {
338
+ theme: "dark",
339
+ },
340
+ },
341
+ deep_merge: true,
342
+ )
343
+ ```
344
+
345
+ The component receives:
284
346
 
285
- # Deep: settings => { theme: "dark", locale: "en" }
286
- mail react: { settings: { theme: "dark" } }, deep_merge: true, ...
347
+ ```ruby
348
+ { settings: { theme: "dark", locale: "en" } }
287
349
  ```
288
350
 
289
- Set `config.deep_merge_shared_props = true` to make deep merging the default for every email. (See [Configuration](#configuration))
351
+ Set `config.deep_merge_shared_props = true` to make deep merging the default for every email.
290
352
 
291
- ### Prop Serialization
353
+ ### Mailer and Message Props
292
354
 
293
- Like `render json:`, `mail react:` accepts any object that responds to `as_json`, including hashes, Active Model objects, and serializers such as [Alba](https://github.com/okuramasafumi/alba) or [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
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.
294
356
 
295
- ### Prop Transformation
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:
296
358
 
297
- Prop keys are camelized by default, so `account.plan_name` arrives as `account.planName`. Override `transform_props` in your [configuration](#configuration).
359
+ ```tsx
360
+ import type { Mailer, Message } from "react-email-rails"
361
+ import { Body, Container, Html, Text } from "@react-email/components"
298
362
 
299
- ### Component Names
363
+ type WelcomeProps = {
364
+ account: { name: string }
365
+ mailer: Mailer
366
+ message: Message
367
+ }
300
368
 
301
- Component names are inferred from the mailer and action:
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
+ }
381
+ ```
302
382
 
303
- | Mailer action | Component |
304
- |---------------|-----------|
305
- | `AccountMailer#welcome` | `account_mailer/welcome` |
306
- | `Users::InviteMailer#new_invite` | `users/invite_mailer/new_invite` |
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.
307
393
 
308
- Rails derives `account_mailer` from `AccountMailer` via `mailer_name`. By default, `account_mailer/welcome` resolves to `app/javascript/emails/account_mailer/welcome.tsx` or `.jsx`.
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.
309
395
 
310
- Files and directories starting with `_` are ignored as renderable email entries by default. Use them for shared components such as `_components/email_layout.tsx`. They can still be imported by email components.
396
+ ### Prop Serialization
311
397
 
312
- Override the inferred name per mail:
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).
313
399
 
314
- ```ruby
315
- mail react: "users/welcome", props: { user: }, to:, subject:
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
403
+
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
316
412
  ```
317
413
 
318
- Or override `component_path_resolver` globally in your [configuration](#configuration).
414
+ Ignored files can still be imported by email components.
319
415
 
320
416
  ### Layouts
321
417
 
322
- Action Mailer layouts aren't applied to `react:` emails. React Email treats layouts like any other component, so share structure with normal React composition instead:
418
+ Action Mailer layouts are not applied to `react:` emails. In React Email, layouts are normal React components:
323
419
 
324
420
  ```tsx
325
421
  // app/javascript/emails/_components/email_layout.tsx
326
-
327
422
  import { Body, Container, Html } from "@react-email/components"
328
423
  import type { ReactNode } from "react"
329
424
 
@@ -344,7 +439,6 @@ export function EmailLayout({ children }: EmailLayoutProps) {
344
439
 
345
440
  ```tsx
346
441
  // app/javascript/emails/account_mailer/welcome.tsx
347
-
348
442
  import { Text } from "@react-email/components"
349
443
  import { EmailLayout } from "../_components/email_layout"
350
444
 
@@ -357,21 +451,28 @@ export default function Welcome() {
357
451
  }
358
452
  ```
359
453
 
360
- See [Component Names](#component-names) for how shared `_` files are handled.
454
+ ### Editor Documents
361
455
 
362
- ### Editor
363
-
364
- 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.
365
457
 
366
458
  See [Editor rendering](docs/editor.md) for setup and usage.
367
459
 
368
460
  ## Configuration
369
461
 
370
- Configuration is handled primarily on the Rails side, though there are some Vite options to be aware of.
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.
371
466
 
372
467
  ### Rails Configuration
373
468
 
374
- If the defaults don't fit, override them in `config/initializers/react_email_rails.rb`:
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
+ ```
375
476
 
376
477
  | Option | Default |
377
478
  |--------|---------|
@@ -384,14 +485,14 @@ If the defaults don't fit, override them in `config/initializers/react_email_rai
384
485
  | `on_render_error` | `nil` |
385
486
  | `deep_merge_shared_props` | `false` |
386
487
 
387
- #### Prop Transformation
488
+ ### Prop Transformation
388
489
 
389
- Set `transform_props` to another supported value if you prefer a different prop key style:
490
+ Set `transform_props` to choose how Ruby prop keys are exposed to React:
390
491
 
391
492
  | Value | Example |
392
493
  |-------|---------|
393
494
  | `:camel` | `AccountName` |
394
- | `:lower_camel` (default) | `accountName` |
495
+ | `:lower_camel` | `accountName` |
395
496
  | `:dash` | `account-name` |
396
497
  | `:snake` | `account_name` |
397
498
  | `:none` | preserves serialized keys |
@@ -402,15 +503,15 @@ ReactEmailRails.configure do |config|
402
503
  end
403
504
  ```
404
505
 
405
- `transform_props` only controls prop key names. Props are always serialized with `as_json`.
506
+ Only prop keys are transformed. Values are always serialized with `as_json`.
406
507
 
407
- #### Render Modes
508
+ ### Render Modes
408
509
 
409
- `:subprocess` starts a fresh Node process for each render. It's simple, isolated, and always uses the latest bundle, but pays Node startup and bundle load each time.
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.
410
511
 
411
- `:persistent` reuses one long-lived Node process per worker. It's faster for render-heavy workers, but uses more memory and can serve a stale component until recycled. The default `:subprocess` mode is usually enough. Switch when Node startup shows up in traces or batch jobs render many emails from the same bundle.
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.
412
513
 
413
- Enable persistent mode for render-heavy worker processes:
514
+ Switch to persistent mode when Node startup shows up in traces or when a worker renders many emails from the same bundle:
414
515
 
415
516
  ```ruby
416
517
  ReactEmailRails.configure do |config|
@@ -418,21 +519,22 @@ ReactEmailRails.configure do |config|
418
519
  end
419
520
  ```
420
521
 
421
- Persistent mode keeps one Node child per process:
522
+ Persistent mode details:
422
523
 
423
- - Renders are newline-delimited JSON and processed one at a time. Scale throughput with more worker processes.
424
- - It's fork-safe: under clustered Puma or forking job runners, each worker spawns its own child.
425
- - The child is recycled after `render_process_max_requests` renders to bound memory growth. Set it to `nil` to disable recycling.
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.
426
528
 
427
- #### Render Options
529
+ ### Render Options
428
530
 
429
- `render_options` is passed to [@react-email/render](https://react.email/docs/utilities/render). Use `html` and `text` keys for each output. Option keys are camelized before they cross into JavaScript.
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.
430
532
 
431
533
  ```ruby
432
534
  ReactEmailRails.configure do |config|
433
535
  config.render_options = {
434
536
  html: {
435
- pretty: Rails.env.development?
537
+ pretty: Rails.env.development?,
436
538
  },
437
539
  text: {
438
540
  html_to_text_options: {
@@ -443,9 +545,11 @@ ReactEmailRails.configure do |config|
443
545
  end
444
546
  ```
445
547
 
446
- #### Error Reporting
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.
549
+
550
+ ### Error Reporting
447
551
 
448
- Use `on_render_error` to report failures before the exception is re-raised. The callback receives the error plus `kind:` and `component:`:
552
+ Use `on_render_error` to report failures before the exception is re-raised:
449
553
 
450
554
  ```ruby
451
555
  ReactEmailRails.configure do |config|
@@ -455,33 +559,50 @@ ReactEmailRails.configure do |config|
455
559
  end
456
560
  ```
457
561
 
458
- #### Instrumentation
562
+ The callback receives the error plus render context such as `kind:` and `component:`.
459
563
 
460
- Every render emits `render.react-email-rails` through [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html). The payload includes `kind`, `component`, and successful HTML size in `html_bytes`:
564
+ ### Instrumentation
565
+
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`.
461
567
 
462
568
  ```ruby
463
569
  ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
464
- Rails.logger.info("[react-email-rails] Rendered #{event.payload[:component]} (Duration: #{event.duration.round}ms | Size: #{event.payload[:html_bytes]} bytes)")
570
+ Rails.logger.info(
571
+ "[react-email-rails] Rendered #{event.payload[:component]} " \
572
+ "(#{event.duration.round}ms, #{event.payload[:html_bytes]} bytes)",
573
+ )
465
574
  end
466
575
  ```
467
576
 
468
577
  ### Vite Configuration
469
578
 
470
- Most apps only need the `reactEmailRails()` plugin from [Quick Start](#quick-start). The options below change component discovery, bundle dependency handling, and isolated renderer transforms.
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
+ ```
471
589
 
472
- In development and production, the isolated renderer loads `reactEmailRails()`, JSX support, and component-facing Vite config such as `resolve`, `define`, `css`, `json`, `assetsInclude`, `esbuild`, and `oxc`. It doesn't load your other app plugins. Server, preview, dependency optimization, and build output settings stay owned by react-email-rails.
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`.
473
591
 
474
- #### Plugin Options
592
+ Server, preview, dependency optimization, and build output settings stay owned by react-email-rails so Rails can always find the renderer bundle.
593
+
594
+ ### Vite Plugin Options
475
595
 
476
596
  | Option | Default | Description |
477
597
  |--------|---------|-------------|
478
598
  | `emails.path` | `"app/javascript/emails"` | Directory containing email components |
479
599
  | `emails.extension` | `[".tsx", ".jsx"]` | Component extension, or an array of extensions |
480
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) |
481
602
  | `standalone` | `true` | Inline production renderer bundle dependencies |
482
603
  | `vite` | `{}` | Extra email-only Vite config for compilation and resolution |
483
604
 
484
- Use a custom directory:
605
+ Use a custom email directory:
485
606
 
486
607
  ```ts
487
608
  reactEmailRails({
@@ -489,7 +610,7 @@ reactEmailRails({
489
610
  })
490
611
  ```
491
612
 
492
- Use multiple extensions:
613
+ Use custom extensions:
493
614
 
494
615
  ```ts
495
616
  reactEmailRails({
@@ -499,11 +620,11 @@ reactEmailRails({
499
620
  })
500
621
  ```
501
622
 
502
- Component names come from the Vite directory layout (see [Component Names](#component-names)). To map mailer actions to a different layout, override `component_path_resolver` on the Ruby side rather than renaming in the plugin, so both halves stay in sync.
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.
503
624
 
504
- #### Advanced: Email-Only Vite Plugins
625
+ ### Email-Only Vite Plugins
505
626
 
506
- Most apps don't need extra email plugins. If email components need a transform that isn't part of Vite's default pipeline, add that transform to the isolated renderer:
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:
507
628
 
508
629
  ```ts
509
630
  import mdx from "@mdx-js/rollup"
@@ -521,13 +642,13 @@ export default defineConfig({
521
642
  })
522
643
  ```
523
644
 
524
- These `vite` options are used by `react-email-rails-dev` and `react-email-rails-build`. Only `assetsInclude`, `css`, `define`, `esbuild`, `json`, `oxc`, `plugins`, and `resolve` are accepted. Output settings such as `build.outDir` and `build.rollupOptions` are ignored so Ruby can always find the bundle.
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.
525
646
 
526
- #### Standalone Builds
647
+ ### Standalone Builds
527
648
 
528
- 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. Development previews keep dependencies external, even when `standalone` is enabled.
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.
529
650
 
530
- Set `standalone: false` when your runtime already ships `node_modules` and you prefer a smaller SSR-style bundle:
651
+ Set `standalone: false` when your runtime ships `node_modules` and you prefer a smaller SSR-style bundle:
531
652
 
532
653
  ```ts
533
654
  reactEmailRails({
@@ -535,45 +656,43 @@ reactEmailRails({
535
656
  })
536
657
  ```
537
658
 
538
- Externalized bundles are smaller and may build faster, but the renderer needs the externalized packages available at runtime.
659
+ Externalized bundles can be smaller and may build faster, but the externalized packages must be available at runtime.
539
660
 
540
661
  ## Deployment
541
662
 
542
- For production deploys, run the normal Rails asset task:
663
+ Production deploys should run the normal Rails asset task:
543
664
 
544
665
  ```sh
545
666
  bin/rails assets:precompile
546
667
  ```
547
668
 
548
- The `react_email_rails:build` task is hooked into `assets:precompile` automatically. It loads `reactEmailRails()` options from your Vite config, then writes `tmp/react-email-rails/emails.js` with the email component registry. If [Editor document rendering](docs/editor.md) is enabled, the bundle also includes document renderers.
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.
549
670
 
550
- You can run it directly when needed:
671
+ You can run the renderer build directly:
551
672
 
552
673
  ```sh
553
674
  bin/rails react_email_rails:build
554
675
  ```
555
676
 
556
- Production rendering runs that bundle with Node. 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.
557
-
558
- The npm package, Vite, React, and `@react-email/render` must be available when Rails runs `assets:precompile`. If you enable [Editor document rendering](docs/editor.md), its peer dependencies must be available too.
677
+ Production rendering requires that bundle. If it is missing, rendering raises `ReactEmailRails::RenderError` and Action Mailer does not send the email.
559
678
 
560
- The bundle is required, not an optimization. If it's missing, renders raise `ReactEmailRails::RenderError`. Action Mailer deliveries aren't sent.
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.
561
680
 
562
- The Ruby gem and npm package must stay on the same version. The renderer includes a small protocol/version handshake, so mismatched installs fail with an actionable `ReactEmailRails::RenderError` instead of silently returning malformed output.
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.
563
682
 
564
- The build command preserves `emails.path`, `emails.extension`, `emails.ignore`, `standalone`, and email-only `vite` options.
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`.
565
684
 
566
- ### Renderer Verification
685
+ ### Verify the Renderer
567
686
 
568
- To confirm the renderer is ready before relying on it, run:
687
+ Run this before relying on the production renderer:
569
688
 
570
689
  ```sh
571
690
  bin/rails react_email_rails:verify
572
691
  ```
573
692
 
574
- It checks that the render command runs and that the npm package version matches the gem, then exits non-zero with an actionable message on failure. Wire it into CI or release steps to catch missing bundles or version drift before the first render.
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.
575
694
 
576
- For programmatic checks (for example, a health endpoint), `ReactEmailRails.healthy?` returns a boolean. If you specifically want a check at boot, call it from your own initializer and scope it to the processes that send mail so others don't pay the cost:
695
+ For programmatic checks, `ReactEmailRails.healthy?` returns a boolean:
577
696
 
578
697
  ```ruby
579
698
  Rails.application.config.after_initialize do
@@ -583,13 +702,31 @@ Rails.application.config.after_initialize do
583
702
  end
584
703
  ```
585
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
+
586
707
  ## Development
587
708
 
588
709
  See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, checks, formatting, and release verification.
589
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
+
590
727
  ## Contributing
591
728
 
592
- Bug reports and pull requests are welcome on GitHub. See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
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.
593
730
 
594
731
  ## Security
595
732
 
@@ -597,4 +734,4 @@ Please report vulnerabilities privately. See [SECURITY.md](SECURITY.md) for deta
597
734
 
598
735
  ## License
599
736
 
600
- 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]
@@ -13,8 +13,6 @@ module ReactEmailRails::ActionMailer
13
13
  self.react_email_use_instance_props = true
14
14
  end
15
15
 
16
- # Share props with every `react:` email this mailer and its subclasses render.
17
- # Mirrors inertia-rails `inertia_share`; `only`/`except`/`if`/`unless` go to `before_action`.
18
16
  def react_email_share(hash = nil, **props, &block)
19
17
  options = props.slice(*SHARED_FILTER_OPTIONS)
20
18
  data = hash || props.except(*SHARED_FILTER_OPTIONS)
@@ -25,7 +23,6 @@ module ReactEmailRails::ActionMailer
25
23
  end
26
24
  end
27
25
 
28
- # Share props from within an action, e.g. conditionally before calling `mail`.
29
26
  def react_email_share(hash = nil, **props, &block)
30
27
  react_email_append_shared(hash || props, block)
31
28
  end
@@ -43,18 +40,25 @@ module ReactEmailRails::ActionMailer
43
40
  resolved_props,
44
41
  deep_merge: react_email_deep_merge?(deep_merge),
45
42
  )
46
- render_options = ReactEmailRails.configuration.resolve_render_options(self)
47
- rendered = ReactEmailRails.render(component:, props: resolved_props, render_options:)
48
43
 
49
44
  super(headers) do |format|
45
+ rendered = react_email_render(component, resolved_props)
46
+
50
47
  format.html { rendered.html }
51
48
  format.text { rendered.text } if rendered.text.present?
49
+
52
50
  yield(format) if block
53
51
  end
54
52
  end
55
53
 
56
54
  private
57
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
+
58
62
  def react_email_append_shared(data, block)
59
63
  store = (@_react_email_shared ||= [])
60
64
  store << data.dup.freeze if data.present?
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ReactEmailRails
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -26,6 +26,7 @@ require_relative("react_email_rails/configuration")
26
26
  require_relative("react_email_rails/tasks")
27
27
  require_relative("react_email_rails/props_resolver")
28
28
  require_relative("react_email_rails/shared_props")
29
+ require_relative("react_email_rails/mailer_context")
29
30
  require_relative("react_email_rails/railtie")
30
31
 
31
32
  module ReactEmailRails
@@ -58,8 +59,7 @@ module ReactEmailRails
58
59
  perform(payload:, label: type, kind: "document", type:)
59
60
  end
60
61
 
61
- # Parse semantic HTML or Markdown into an editor document Hash using the renderer's
62
- # 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:`.
63
63
  def parse(type:, html: nil, markdown: nil, context: {})
64
64
  payload = {
65
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.5.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