react-email-rails 0.4.0 → 0.5.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 +119 -241
- data/lib/react_email_rails/action_mailer.rb +38 -0
- data/lib/react_email_rails/configuration.rb +2 -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 +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bbfbb40c2044856c9061ba2a3c134d07dbd5aa9b58b6cfe3d576d25fe45b4d2
|
|
4
|
+
data.tar.gz: 7c272a65e71044f813c813b248a48bc69e3a043494ad9f6133ae8c69ab006b24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5b434f0983a8c1280d622203869f535a0c98fb293bcfff3d51d39a8676d730d000b901cb9bcc6b3b478fe633904974798131482e44982140f40e12951ac1423
|
|
7
|
+
data.tar.gz: 21652f85cc0ee448ec90ffd7b24c4eaf05b892dbe0e83d487777ae57bbcda7a34c46e1b9becad9174838edd0d33919b11aa9efb56cbd520433803c8a3fd82385
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
|
|
8
|
+
## 0.4.1
|
|
9
|
+
|
|
10
|
+
- `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.
|
|
11
|
+
|
|
3
12
|
## 0.4.0
|
|
4
13
|
|
|
5
14
|
- `ReactEmailRails.parse` now accepts `markdown:` as an alternative to `html:`. Markdown is converted to HTML and runs through the same extension-driven parse path, producing the same document `Hash` — handy for agent- or tool-generated content. Pass exactly one of `html:`/`markdown:`.
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-

|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# react-email-rails
|
|
4
4
|
|
|
5
|
-
Build and send emails using React and Rails
|
|
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).
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
@@ -12,7 +12,6 @@ Build and send emails using React and Rails — a seamless integration between [
|
|
|
12
12
|
- [Requirements](#requirements)
|
|
13
13
|
- [Quick Start](#quick-start)
|
|
14
14
|
- [Usage](#usage)
|
|
15
|
-
- [Editor](#editor)
|
|
16
15
|
- [Configuration](#configuration)
|
|
17
16
|
- [Deployment](#deployment)
|
|
18
17
|
- [Development](#development)
|
|
@@ -22,19 +21,19 @@ Build and send emails using React and Rails — a seamless integration between [
|
|
|
22
21
|
|
|
23
22
|
## Why
|
|
24
23
|
|
|
25
|
-
Building HTML emails is painfully archaic. [React Email](https://react.email)
|
|
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.
|
|
26
25
|
|
|
27
26
|
## How
|
|
28
27
|
|
|
29
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.
|
|
30
29
|
|
|
31
|
-
**In production,** Rails builds a server-side
|
|
30
|
+
**In production,** Rails builds a server-side renderer bundle during `assets:precompile` using `reactEmailRails()` in your Vite config for discovery and options.
|
|
32
31
|
|
|
33
|
-
|
|
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.
|
|
34
33
|
|
|
35
34
|
## Status
|
|
36
35
|
|
|
37
|
-
**react-email-rails is pre-1.0.** It
|
|
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).
|
|
38
37
|
|
|
39
38
|
## Requirements
|
|
40
39
|
|
|
@@ -67,11 +66,11 @@ bin/rails generate react_email_rails:install
|
|
|
67
66
|
|
|
68
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`.
|
|
69
68
|
|
|
70
|
-
The installed setup follows the normal Rails lifecycle:
|
|
69
|
+
The installed setup then follows the normal Rails lifecycle:
|
|
71
70
|
|
|
72
71
|
- `bin/rails generate react_email_rails:email ...` creates matching mailers and React components.
|
|
73
|
-
-
|
|
74
|
-
- `bin/rails assets:precompile` builds the production
|
|
72
|
+
- `bin/dev` renders through Vite on demand.
|
|
73
|
+
- `bin/rails assets:precompile` builds the production renderer bundle automatically.
|
|
75
74
|
- `bin/rails react_email_rails:build` builds the bundle directly when CI or tests need it.
|
|
76
75
|
|
|
77
76
|
### Manual Install
|
|
@@ -103,9 +102,7 @@ Generate a mailer and React Email component:
|
|
|
103
102
|
bin/rails generate react_email_rails:email Account welcome
|
|
104
103
|
```
|
|
105
104
|
|
|
106
|
-
The generator follows Rails' mailer generator shape: `NAME [method method]`. It creates
|
|
107
|
-
|
|
108
|
-
The generator reads `emails.path` and `emails.extension` from `reactEmailRails()` when available. Pass flags to override them:
|
|
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:
|
|
109
106
|
|
|
110
107
|
```sh
|
|
111
108
|
bin/rails generate react_email_rails:email Account welcome --emails-path=app/emails --extension=jsx
|
|
@@ -159,11 +156,11 @@ export default function Welcome({ account }: WelcomeProps) {
|
|
|
159
156
|
|
|
160
157
|
> [@react-email/components](https://react.email/docs/components/html) provides primitives like `<Button>`, `<Heading>`, `<Tailwind>`, and more.
|
|
161
158
|
|
|
162
|
-
That's it
|
|
159
|
+
That's it. It now renders and delivers like any other Action Mailer email.
|
|
163
160
|
|
|
164
161
|
## Usage
|
|
165
162
|
|
|
166
|
-
Pass data from your
|
|
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.
|
|
167
164
|
|
|
168
165
|
```ruby
|
|
169
166
|
mail react: { foo: "bar" }, ...
|
|
@@ -175,9 +172,11 @@ export default function Email({ foo }: { foo: string }) {
|
|
|
175
172
|
}
|
|
176
173
|
```
|
|
177
174
|
|
|
178
|
-
|
|
175
|
+
### Props
|
|
176
|
+
|
|
177
|
+
Choose the level of inference you want for your props:
|
|
179
178
|
|
|
180
|
-
|
|
179
|
+
#### Implicit Component, Instance Props
|
|
181
180
|
|
|
182
181
|
```ruby
|
|
183
182
|
class AccountMailer < ApplicationMailer
|
|
@@ -194,7 +193,7 @@ Action Mailer's framework assigns (including `params` and `rendered_format`) are
|
|
|
194
193
|
|
|
195
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.
|
|
196
195
|
|
|
197
|
-
|
|
196
|
+
#### Implicit Component, Explicit Props
|
|
198
197
|
|
|
199
198
|
```ruby
|
|
200
199
|
mail(
|
|
@@ -207,7 +206,7 @@ mail(
|
|
|
207
206
|
)
|
|
208
207
|
```
|
|
209
208
|
|
|
210
|
-
|
|
209
|
+
#### Explicit Component, Explicit Props
|
|
211
210
|
|
|
212
211
|
```ruby
|
|
213
212
|
mail(
|
|
@@ -221,7 +220,81 @@ mail(
|
|
|
221
220
|
)
|
|
222
221
|
```
|
|
223
222
|
|
|
224
|
-
|
|
223
|
+
### Shared Props
|
|
224
|
+
|
|
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.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class MarketingMailer < ApplicationMailer
|
|
229
|
+
# Static value
|
|
230
|
+
react_email_share app_name: "Acme"
|
|
231
|
+
|
|
232
|
+
# Lambda value, evaluated lazily in the mailer instance
|
|
233
|
+
react_email_share unread_count: -> { current_user&.unread_count }
|
|
234
|
+
|
|
235
|
+
# Block, evaluated lazily in the mailer instance
|
|
236
|
+
react_email_share do
|
|
237
|
+
{ brand: { name: "Acme", url: marketing_url } }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Per-mail props win over shared props of the same name, so a mailer can always override what it inherits:
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
mail react: { app_name: "Acme Pro" }, ... # overrides the shared app_name
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Shared props apply to all three forms above (`react:` hash, `react: "component", props:`, and `react: true`).
|
|
249
|
+
|
|
250
|
+
#### Conditional Sharing
|
|
251
|
+
|
|
252
|
+
Pass any `before_action` filter (`only`, `except`, `if`, `unless`) to scope a share to certain actions:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
react_email_share only: [:welcome, :reactivation] do
|
|
256
|
+
{ promotion: current_promotion }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
react_email_share if: :user_signed_in? do
|
|
260
|
+
{ user: { name: current_user.name } }
|
|
261
|
+
end
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
You can also share from inside an action, before calling `mail`:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
def welcome
|
|
268
|
+
react_email_share notice: "Thanks for joining!"
|
|
269
|
+
mail react: { account: }, to: account.email, subject: "Welcome"
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Deep Merging
|
|
274
|
+
|
|
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:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
react_email_share do
|
|
279
|
+
{ settings: { theme: "light", locale: "en" } }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Shallow (default): settings => { theme: "dark" }
|
|
283
|
+
mail react: { settings: { theme: "dark" } }, ...
|
|
284
|
+
|
|
285
|
+
# Deep: settings => { theme: "dark", locale: "en" }
|
|
286
|
+
mail react: { settings: { theme: "dark" } }, deep_merge: true, ...
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Set `config.deep_merge_shared_props = true` to make deep merging the default for every email. (See [Configuration](#configuration))
|
|
290
|
+
|
|
291
|
+
### Prop Serialization
|
|
292
|
+
|
|
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).
|
|
294
|
+
|
|
295
|
+
### Prop Transformation
|
|
296
|
+
|
|
297
|
+
Prop keys are camelized by default, so `account.plan_name` arrives as `account.planName`. Override `transform_props` in your [configuration](#configuration).
|
|
225
298
|
|
|
226
299
|
### Component Names
|
|
227
300
|
|
|
@@ -232,9 +305,9 @@ Component names are inferred from the mailer and action:
|
|
|
232
305
|
| `AccountMailer#welcome` | `account_mailer/welcome` |
|
|
233
306
|
| `Users::InviteMailer#new_invite` | `users/invite_mailer/new_invite` |
|
|
234
307
|
|
|
235
|
-
Rails derives `account_mailer` from `AccountMailer` via
|
|
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`.
|
|
236
309
|
|
|
237
|
-
Files and directories starting with `_` are ignored as renderable email entries by default. Use them for shared components such as `_components/email_layout.tsx
|
|
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.
|
|
238
311
|
|
|
239
312
|
Override the inferred name per mail:
|
|
240
313
|
|
|
@@ -244,17 +317,9 @@ mail react: "users/welcome", props: { user: }, to:, subject:
|
|
|
244
317
|
|
|
245
318
|
Or override `component_path_resolver` globally in your [configuration](#configuration).
|
|
246
319
|
|
|
247
|
-
### Prop Serialization
|
|
248
|
-
|
|
249
|
-
Just like `render json:` in controllers, you can pass any object that responds to `as_json` to `mail react:`. Plain hashes, Active Model objects, and serialization libraries like [Alba](https://github.com/okuramasafumi/alba) or [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers) are supported.
|
|
250
|
-
|
|
251
|
-
### Prop Transformation
|
|
252
|
-
|
|
253
|
-
By default, prop keys are camelized on the way to React, so `account.plan_name` arrives as `account.planName` in your component. This makes them more idiomatic for the frontend, but you can override the `transform_props` behavior in your [configuration](#configuration).
|
|
254
|
-
|
|
255
320
|
### Layouts
|
|
256
321
|
|
|
257
|
-
Action Mailer layouts
|
|
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:
|
|
258
323
|
|
|
259
324
|
```tsx
|
|
260
325
|
// app/javascript/emails/_components/email_layout.tsx
|
|
@@ -294,191 +359,11 @@ export default function Welcome() {
|
|
|
294
359
|
|
|
295
360
|
See [Component Names](#component-names) for how shared `_` files are handled.
|
|
296
361
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
Alongside named components, the gem can render a [@react-email/editor](https://react.email/docs/editor) document — the Tiptap/ProseMirror JSON a visual editor produces — to HTML and text on the server.
|
|
300
|
-
|
|
301
|
-
React Email exposes [composeReactEmail](https://react.email/docs/editor/api-reference/compose-react-email) for this, but only from the browser, with a live editor instance from the export panel. `ReactEmailRails.compose` is the server analog: it rebuilds what `composeReactEmail` needs headlessly (no DOM, no live editor) from the stored document and the extensions you declare, then calls the same function. So `compose` is to the editor what `render` is to a component.
|
|
302
|
-
|
|
303
|
-
This is opt-in. The editor packages are optional peer dependencies and stay out of the email render path until you enable it.
|
|
304
|
-
|
|
305
|
-
### Setup
|
|
306
|
-
|
|
307
|
-
Install the editor packages:
|
|
308
|
-
|
|
309
|
-
```sh
|
|
310
|
-
npm i @react-email/editor @tiptap/core
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
To also parse HTML into documents with [`parse`](#parsing-html-or-markdown-into-a-document), add `@tiptap/html` and its server DOM, `happy-dom`:
|
|
314
|
-
|
|
315
|
-
```sh
|
|
316
|
-
npm i @tiptap/html happy-dom
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
To parse Markdown as well, add [`marked`](https://marked.js.org) — it converts Markdown to HTML, which then runs through the same parser:
|
|
320
|
-
|
|
321
|
-
```sh
|
|
322
|
-
npm i marked
|
|
323
|
-
```
|
|
362
|
+
### Editor
|
|
324
363
|
|
|
325
|
-
|
|
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.
|
|
326
365
|
|
|
327
|
-
|
|
328
|
-
// vite.config.ts
|
|
329
|
-
|
|
330
|
-
import { defineConfig } from "vite"
|
|
331
|
-
import { reactEmailRails } from "react-email-rails"
|
|
332
|
-
|
|
333
|
-
export default defineConfig({
|
|
334
|
-
plugins: [reactEmailRails({ documents: true })],
|
|
335
|
-
})
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
`documents: true` enables it with defaults (`app/javascript/documents`, `.ts`/`.tsx` extensions). Like `emails`, it also accepts a directory string or `{ path, extension, ignore }`. See [Plugin Options](#plugin-options).
|
|
339
|
-
|
|
340
|
-
### Document Renderers
|
|
341
|
-
|
|
342
|
-
A document doesn't carry the editor configuration it was authored with, so a headless renderer has to be told which extensions a given document needs. Each file under the documents directory is a **document renderer** — the editor-side analog of an email component — and its name is resolved from the directory layout just like [component names](#component-names) (so `broadcast` maps to `app/javascript/documents/broadcast.ts`).
|
|
343
|
-
|
|
344
|
-
```ts
|
|
345
|
-
// app/javascript/documents/broadcast.ts
|
|
346
|
-
|
|
347
|
-
import { StarterKit } from "@react-email/editor/extensions"
|
|
348
|
-
import { EmailTheming } from "@react-email/editor/plugins"
|
|
349
|
-
|
|
350
|
-
export function buildExtensions() {
|
|
351
|
-
return [StarterKit, EmailTheming]
|
|
352
|
-
}
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
A renderer can export two optional hooks:
|
|
356
|
-
|
|
357
|
-
| Export | Required | Description |
|
|
358
|
-
|--------|----------|-------------|
|
|
359
|
-
| `buildExtensions(context)` | Yes | Returns the Tiptap extension list for the document. |
|
|
360
|
-
| `transformDocument(document, context)` | No | Returns a rewritten document before rendering — for example, to inject header/footer nodes that aren't persisted in the stored document. |
|
|
361
|
-
| `getPreview(context)` | No | Returns inbox preview text when the `compose` call doesn't pass one. |
|
|
362
|
-
|
|
363
|
-
`context` is the optional data you pass to `compose` (see below). Use it to vary extensions, transforms, or preview text per render.
|
|
364
|
-
|
|
365
|
-
```ts
|
|
366
|
-
// app/javascript/documents/broadcast.ts
|
|
367
|
-
|
|
368
|
-
import { StarterKit } from "@react-email/editor/extensions"
|
|
369
|
-
import { EmailTheming } from "@react-email/editor/plugins"
|
|
370
|
-
|
|
371
|
-
export function buildExtensions(context) {
|
|
372
|
-
return [StarterKit, EmailTheming]
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function transformDocument(document, context) {
|
|
376
|
-
const header = {
|
|
377
|
-
type: "heading",
|
|
378
|
-
attrs: { level: 1 },
|
|
379
|
-
content: [{ type: "text", text: context.brandName }],
|
|
380
|
-
}
|
|
381
|
-
const themeIndex = document.content.findIndex((node) => node.type === "globalContent")
|
|
382
|
-
const at = themeIndex + 1
|
|
383
|
-
return {
|
|
384
|
-
...document,
|
|
385
|
-
content: [...document.content.slice(0, at), header, ...document.content.slice(at)],
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export function getPreview(context) {
|
|
390
|
-
return context.previewText
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
> **Match the extensions to the document.** `composeReactEmail` renders any node whose extension isn't registered as `null`, so a document that uses a node your `buildExtensions` omits will silently drop that content. Return the same extension list the document was authored with.
|
|
395
|
-
|
|
396
|
-
> **Keep the theme node.** The editor persists its theme in a `globalContent` node and `EmailTheming` reads it back when rendering. If you reshape the document in `transformDocument`, preserve that node.
|
|
397
|
-
|
|
398
|
-
### Composing a Document
|
|
399
|
-
|
|
400
|
-
Call `ReactEmailRails.compose` with the renderer `type`, the stored document, and optional `context`/`preview`:
|
|
401
|
-
|
|
402
|
-
```ruby
|
|
403
|
-
broadcast = Broadcast.find(params[:id])
|
|
404
|
-
|
|
405
|
-
rendered = ReactEmailRails.compose(
|
|
406
|
-
type: "broadcast",
|
|
407
|
-
document: broadcast.body,
|
|
408
|
-
context: { brand_name: "Acme", preview_text: broadcast.subject },
|
|
409
|
-
preview: broadcast.subject,
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
rendered.html # => "<!DOCTYPE html>..."
|
|
413
|
-
rendered.text # => "ACME\n\n..."
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
It returns the same `RenderedEmail` (`html`/`text`) as `render`, runs through the same [render modes](#render-modes), and raises `ReactEmailRails::RenderError` on failure. Documents don't go through Action Mailer — broadcasts and the like usually have their own delivery path — so deliver `rendered.html`/`rendered.text` however your app sends mail.
|
|
417
|
-
|
|
418
|
-
**The document is a `Hash`.** You pass and store it as a plain Ruby `Hash` with string keys — what a jsonb column hands back, and what [`parse`](#parsing-html-or-markdown-into-a-document) returns. "Tiptap JSON" names the document's *shape* (and the format it's serialized to over the wire to the renderer), not the Ruby type; `compose` accepts any object that responds to `as_json`, but a `Hash` is the norm.
|
|
419
|
-
|
|
420
|
-
**Keys:** the document's keys (`type`, `attrs`, `content`, `marks`, node names, `globalContent`) are structural and **passed through verbatim** — never transformed. Only `context` is key-transformed, camelized exactly like component props (so `brand_name` arrives as `brandName`, per [`transform_props`](#prop-transformation)).
|
|
421
|
-
|
|
422
|
-
`render_options` does not apply to documents; `composeReactEmail` controls its own rendering.
|
|
423
|
-
|
|
424
|
-
### Parsing HTML or Markdown into a Document
|
|
425
|
-
|
|
426
|
-
`ReactEmailRails.parse` converts semantic HTML into the same document `Hash` shape the editor stores, using the selected renderer's extensions. This needs the `@tiptap/html` and `happy-dom` packages (see [Setup](#setup)).
|
|
427
|
-
|
|
428
|
-
```ruby
|
|
429
|
-
document = ReactEmailRails.parse(
|
|
430
|
-
type: "broadcast",
|
|
431
|
-
html: params[:body_html],
|
|
432
|
-
context: { brand_name: "Acme" },
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
broadcast.update!(body: document)
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
Later, render the stored document like any other:
|
|
439
|
-
|
|
440
|
-
```ruby
|
|
441
|
-
rendered = ReactEmailRails.compose(type: "broadcast", document: broadcast.body)
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
`parse` returns a plain Ruby `Hash` with string keys, normalized through the renderer's schema. It uses the same [render modes](#render-modes) as `compose` and raises `ReactEmailRails::RenderError` on failure. `context` is key-transformed like component props; the HTML is sent verbatim.
|
|
445
|
-
|
|
446
|
-
#### Markdown
|
|
447
|
-
|
|
448
|
-
Pass `markdown:` in place of `html:` to parse Markdown — useful when the content comes from an LLM or another tool that emits Markdown more readily than HTML. It's converted to HTML with [`marked`](https://marked.js.org) and run through the same parse path, so it needs `marked` installed alongside the HTML peers (see [Setup](#setup)).
|
|
449
|
-
|
|
450
|
-
```ruby
|
|
451
|
-
document = ReactEmailRails.parse(
|
|
452
|
-
type: "broadcast",
|
|
453
|
-
markdown: "# Welcome\n\nThanks for signing up, **Ada**.",
|
|
454
|
-
context: { brand_name: "Acme" },
|
|
455
|
-
)
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
Pass exactly one of `html:` or `markdown:` — passing both, or neither, raises `ArgumentError`.
|
|
459
|
-
|
|
460
|
-
Markdown is a lower-friction *input*, not a wider one. It expresses less than HTML — headings, paragraphs, emphasis, links, lists, blockquotes, code, images, and rules — so it adds no new node types. Markdown that maps to nodes the renderer's extensions don't define (such as a GFM table without a table extension) is dropped or flattened, the same as the equivalent HTML.
|
|
461
|
-
|
|
462
|
-
What this means in practice:
|
|
463
|
-
|
|
464
|
-
- HTML maps to a node only when an extension defines how to parse it. Unknown elements, inline styles, and classes may be dropped or flattened.
|
|
465
|
-
- Editor-only constructs such as custom email nodes and the persisted `globalContent` theme node do not round-trip from plain HTML or Markdown.
|
|
466
|
-
- If you already have the document `Hash`, pass it to `compose` directly.
|
|
467
|
-
|
|
468
|
-
### Debugging Dropped Content
|
|
469
|
-
|
|
470
|
-
The most common integration bug is an extension/document mismatch. A node whose type is **missing from `buildExtensions` entirely** raises `ReactEmailRails::RenderError` (it can't be parsed) — loud and safe. The quiet case is a node that *is* in the schema but whose extension does not render to email (a plain Tiptap node rather than an email one): `composeReactEmail` renders it as nothing, with no error.
|
|
471
|
-
|
|
472
|
-
`compose` reports those dropped node types so the silent case isn't silent. They appear both on the result as `rendered.warnings` and on the [`render.react-email-rails`](#instrumentation) instrumentation event as `payload[:warnings]` — an array of `{ type:, count: }`. The editor's own non-rendering nodes (the `globalContent` theme node and similar) are excluded, so a non-empty `warnings` means real content was lost. Subscribe to the event to alert on it — or refuse to send:
|
|
473
|
-
|
|
474
|
-
```ruby
|
|
475
|
-
ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
|
|
476
|
-
warnings = event.payload[:warnings]
|
|
477
|
-
raise "dropped #{warnings.sum { _1[:count] }} node(s): #{warnings.map { _1[:type] }.join(", ")}" if warnings
|
|
478
|
-
end
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
If content is missing, confirm `buildExtensions` returns the **same** extensions the document was authored with — the editor's `StarterKit` plus every custom node and plugin in use — and, if you reshape the document in `transformDocument`, that you preserved the `globalContent` theme node. Treat a mismatch as data or version skew: pinning a document's renderer `type` to the extension set it was created with, and versioning that set, avoids drift.
|
|
366
|
+
See [Editor rendering](docs/editor.md) for setup and usage.
|
|
482
367
|
|
|
483
368
|
## Configuration
|
|
484
369
|
|
|
@@ -497,6 +382,7 @@ If the defaults don't fit, override them in `config/initializers/react_email_rai
|
|
|
497
382
|
| `render_timeout` | `10` seconds |
|
|
498
383
|
| `render_process_max_requests` | `1_000` |
|
|
499
384
|
| `on_render_error` | `nil` |
|
|
385
|
+
| `deep_merge_shared_props` | `false` |
|
|
500
386
|
|
|
501
387
|
#### Prop Transformation
|
|
502
388
|
|
|
@@ -516,17 +402,13 @@ ReactEmailRails.configure do |config|
|
|
|
516
402
|
end
|
|
517
403
|
```
|
|
518
404
|
|
|
519
|
-
`transform_props` only controls prop key names
|
|
405
|
+
`transform_props` only controls prop key names. Props are always serialized with `as_json`.
|
|
520
406
|
|
|
521
407
|
#### Render Modes
|
|
522
408
|
|
|
523
|
-
`:subprocess` starts a fresh Node process for each render. It's simple, always uses the latest bundle,
|
|
524
|
-
|
|
525
|
-
`:persistent` reuses one long-lived Node process per worker. It's faster because it avoids per-render startup, but uses more memory and can serve a stale component until recycled.
|
|
526
|
-
|
|
527
|
-
For background email delivery, the default `:subprocess` mode is usually enough. Switch to `:persistent` when Node startup appears in traces or batch jobs render many emails from the same bundle (see [Instrumentation](#instrumentation)).
|
|
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.
|
|
528
410
|
|
|
529
|
-
|
|
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.
|
|
530
412
|
|
|
531
413
|
Enable persistent mode for render-heavy worker processes:
|
|
532
414
|
|
|
@@ -538,13 +420,13 @@ end
|
|
|
538
420
|
|
|
539
421
|
Persistent mode keeps one Node child per process:
|
|
540
422
|
|
|
541
|
-
- Renders are
|
|
542
|
-
- It
|
|
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.
|
|
543
425
|
- The child is recycled after `render_process_max_requests` renders to bound memory growth. Set it to `nil` to disable recycling.
|
|
544
426
|
|
|
545
427
|
#### Render Options
|
|
546
428
|
|
|
547
|
-
`render_options` is passed to [@react-email/render](https://react.email/docs/utilities/render). `html`
|
|
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.
|
|
548
430
|
|
|
549
431
|
```ruby
|
|
550
432
|
ReactEmailRails.configure do |config|
|
|
@@ -563,7 +445,7 @@ end
|
|
|
563
445
|
|
|
564
446
|
#### Error Reporting
|
|
565
447
|
|
|
566
|
-
Use `on_render_error` to report failures before the exception is re-raised. The callback receives the error
|
|
448
|
+
Use `on_render_error` to report failures before the exception is re-raised. The callback receives the error plus `kind:` and `component:`:
|
|
567
449
|
|
|
568
450
|
```ruby
|
|
569
451
|
ReactEmailRails.configure do |config|
|
|
@@ -575,7 +457,7 @@ end
|
|
|
575
457
|
|
|
576
458
|
#### Instrumentation
|
|
577
459
|
|
|
578
|
-
Every render emits
|
|
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`:
|
|
579
461
|
|
|
580
462
|
```ruby
|
|
581
463
|
ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
|
|
@@ -585,9 +467,9 @@ end
|
|
|
585
467
|
|
|
586
468
|
### Vite Configuration
|
|
587
469
|
|
|
588
|
-
Most apps only need the `reactEmailRails()` plugin from [Quick Start](#quick-start). The options below change
|
|
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.
|
|
589
471
|
|
|
590
|
-
In development and production, the isolated renderer loads
|
|
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.
|
|
591
473
|
|
|
592
474
|
#### Plugin Options
|
|
593
475
|
|
|
@@ -596,11 +478,7 @@ In development and production, the isolated renderer loads the `reactEmailRails(
|
|
|
596
478
|
| `emails.path` | `"app/javascript/emails"` | Directory containing email components |
|
|
597
479
|
| `emails.extension` | `[".tsx", ".jsx"]` | Component extension, or an array of extensions |
|
|
598
480
|
| `emails.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `emails.path` |
|
|
599
|
-
| `
|
|
600
|
-
| `documents.path` | `"app/javascript/documents"` | Directory containing document renderers |
|
|
601
|
-
| `documents.extension` | `[".ts", ".tsx"]` | Renderer extension, or an array of extensions |
|
|
602
|
-
| `documents.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `documents.path` |
|
|
603
|
-
| `standalone` | `true` | Inline production email bundle dependencies |
|
|
481
|
+
| `standalone` | `true` | Inline production renderer bundle dependencies |
|
|
604
482
|
| `vite` | `{}` | Extra email-only Vite config for compilation and resolution |
|
|
605
483
|
|
|
606
484
|
Use a custom directory:
|
|
@@ -625,7 +503,7 @@ Component names come from the Vite directory layout (see [Component Names](#comp
|
|
|
625
503
|
|
|
626
504
|
#### Advanced: Email-Only Vite Plugins
|
|
627
505
|
|
|
628
|
-
Most apps
|
|
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:
|
|
629
507
|
|
|
630
508
|
```ts
|
|
631
509
|
import mdx from "@mdx-js/rollup"
|
|
@@ -643,11 +521,11 @@ export default defineConfig({
|
|
|
643
521
|
})
|
|
644
522
|
```
|
|
645
523
|
|
|
646
|
-
These `vite` options are used by `react-email-rails-dev` and `react-email-rails-build`.
|
|
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.
|
|
647
525
|
|
|
648
526
|
#### Standalone Builds
|
|
649
527
|
|
|
650
|
-
By default the production
|
|
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.
|
|
651
529
|
|
|
652
530
|
Set `standalone: false` when your runtime already ships `node_modules` and you prefer a smaller SSR-style bundle:
|
|
653
531
|
|
|
@@ -667,7 +545,7 @@ For production deploys, run the normal Rails asset task:
|
|
|
667
545
|
bin/rails assets:precompile
|
|
668
546
|
```
|
|
669
547
|
|
|
670
|
-
The `react_email_rails:build` task is hooked into `assets:precompile` automatically. It loads
|
|
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.
|
|
671
549
|
|
|
672
550
|
You can run it directly when needed:
|
|
673
551
|
|
|
@@ -677,9 +555,9 @@ bin/rails react_email_rails:build
|
|
|
677
555
|
|
|
678
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.
|
|
679
557
|
|
|
680
|
-
The npm package, Vite, React, and `@react-email/render` must be available when Rails runs `assets:precompile`.
|
|
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.
|
|
681
559
|
|
|
682
|
-
The bundle is required, not an optimization. If it's missing, renders raise `ReactEmailRails::RenderError
|
|
560
|
+
The bundle is required, not an optimization. If it's missing, renders raise `ReactEmailRails::RenderError`. Action Mailer deliveries aren't sent.
|
|
683
561
|
|
|
684
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.
|
|
685
563
|
|
|
@@ -693,7 +571,7 @@ To confirm the renderer is ready before relying on it, run:
|
|
|
693
571
|
bin/rails react_email_rails:verify
|
|
694
572
|
```
|
|
695
573
|
|
|
696
|
-
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
|
|
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.
|
|
697
575
|
|
|
698
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:
|
|
699
577
|
|
|
@@ -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,22 @@ module ReactEmailRails::ActionMailer
|
|
|
9
12
|
def use_react_instance_props
|
|
10
13
|
self.react_email_use_instance_props = true
|
|
11
14
|
end
|
|
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
|
+
def react_email_share(hash = nil, **props, &block)
|
|
19
|
+
options = props.slice(*SHARED_FILTER_OPTIONS)
|
|
20
|
+
data = hash || props.except(*SHARED_FILTER_OPTIONS)
|
|
21
|
+
|
|
22
|
+
before_action(**options) do
|
|
23
|
+
react_email_append_shared(data, block)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Share props from within an action, e.g. conditionally before calling `mail`.
|
|
29
|
+
def react_email_share(hash = nil, **props, &block)
|
|
30
|
+
react_email_append_shared(hash || props, block)
|
|
12
31
|
end
|
|
13
32
|
|
|
14
33
|
def mail(headers = {}, &block)
|
|
@@ -17,8 +36,13 @@ module ReactEmailRails::ActionMailer
|
|
|
17
36
|
headers = headers.dup
|
|
18
37
|
react = headers.delete(:react)
|
|
19
38
|
props = headers.delete(:props) if headers.key?(:props)
|
|
39
|
+
deep_merge = headers.delete(:deep_merge) if headers.key?(:deep_merge)
|
|
20
40
|
|
|
21
41
|
component, resolved_props = ReactEmailRails::PropsResolver.new(self).resolve(react, props)
|
|
42
|
+
resolved_props = ReactEmailRails::SharedProps.new(self).merge_into(
|
|
43
|
+
resolved_props,
|
|
44
|
+
deep_merge: react_email_deep_merge?(deep_merge),
|
|
45
|
+
)
|
|
22
46
|
render_options = ReactEmailRails.configuration.resolve_render_options(self)
|
|
23
47
|
rendered = ReactEmailRails.render(component:, props: resolved_props, render_options:)
|
|
24
48
|
|
|
@@ -28,4 +52,18 @@ module ReactEmailRails::ActionMailer
|
|
|
28
52
|
yield(format) if block
|
|
29
53
|
end
|
|
30
54
|
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def react_email_append_shared(data, block)
|
|
59
|
+
store = (@_react_email_shared ||= [])
|
|
60
|
+
store << data.dup.freeze if data.present?
|
|
61
|
+
store << block if block
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def react_email_deep_merge?(override)
|
|
65
|
+
return ReactEmailRails.configuration.deep_merge_shared_props if override.nil?
|
|
66
|
+
|
|
67
|
+
override
|
|
68
|
+
end
|
|
31
69
|
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,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,7 @@ 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")
|
|
27
29
|
require_relative("react_email_rails/railtie")
|
|
28
30
|
|
|
29
31
|
module ReactEmailRails
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Supertape
|
|
@@ -164,6 +164,7 @@ files:
|
|
|
164
164
|
- lib/react_email_rails/render_modes/subprocess/command_runner.rb
|
|
165
165
|
- lib/react_email_rails/render_protocol.rb
|
|
166
166
|
- lib/react_email_rails/rendered_email.rb
|
|
167
|
+
- lib/react_email_rails/shared_props.rb
|
|
167
168
|
- lib/react_email_rails/tasks.rb
|
|
168
169
|
- lib/react_email_rails/version.rb
|
|
169
170
|
- lib/tasks/react_email_rails/build.rake
|