react-email-rails 0.1.3 → 0.2.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 +144 -4
- data/SECURITY.md +4 -0
- data/lib/react_email_rails/render_modes/persistent/server.rb +1 -0
- data/lib/react_email_rails/render_modes/subprocess.rb +15 -18
- data/lib/react_email_rails/render_protocol.rb +1 -1
- data/lib/react_email_rails/rendered_email.rb +7 -1
- data/lib/react_email_rails/version.rb +1 -1
- data/lib/react_email_rails.rb +40 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8de13aced1b8b88a622cdc40577ec17c80ad457cee75ea57266dc248e5355416
|
|
4
|
+
data.tar.gz: d42cacd94f77a32ccf75d72b5e8c19506549902c64617a723c6617e0bc613a58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1fbbbb75215e8b4b77de4d4c15e533348e2b7195196d5b9401541596e24507b3c046ca8f74998f20b76f9fbf6d9b67d8c0ec29a86af3aaa64e0b1b70bece4c85
|
|
7
|
+
data.tar.gz: 5e7025b1552256c5d1b2fd01a911c7d941b2fd41aa6cd94ddd646fbeb79713888dd6dc13785ff71c66cadb9362877e2abe4715855630813314e7073f509ccccc
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Add `ReactEmailRails.compose` for server-side rendering of `@react-email/editor` documents (Tiptap/ProseMirror JSON) to HTML and text, the server analog of the editor's client-side `composeReactEmail` export.
|
|
6
|
+
- Add the `documents` Vite plugin option for discovering document renderers, parallel to `emails`. It is off by default; the editor packages stay out of the email render path and build graph unless it is enabled.
|
|
7
|
+
- Report document nodes that render to nothing (a node whose extension is not an email renderer) as non-fatal warnings — on the result (`rendered.warnings`) and the `render.react-email-rails` instrumentation payload (`payload[:warnings]`) — so silently dropped content is detectable.
|
|
8
|
+
- Add `@react-email/editor` and `@tiptap/core` as optional peer dependencies (only required when rendering documents).
|
|
9
|
+
- Bump the render protocol to 2 (the renderer now accepts document requests). The Ruby gem and npm package must be upgraded together, as before.
|
|
10
|
+
- **Breaking:** `on_render_error` callbacks now receive a uniform `(error, **context)` shape, where `context` carries `kind:` (`"email"`/`"document"`) and either `component:` (emails) or `type:` (documents). Update `->(error, component:) { ... }` callbacks to `->(error, **context) { ... }`.
|
|
11
|
+
|
|
3
12
|
## 0.1.3
|
|
4
13
|
|
|
5
14
|
- Remove the `verify_render_on_boot` configuration option, which only logged on failure and duplicated the render-time `ReactEmailRails::RenderError`.
|
data/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# React Email + Rails
|
|
2
4
|
|
|
3
5
|
Build and send emails using React and Rails — a seamless integration between [React Email](https://react.email) and [Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html).
|
|
@@ -10,6 +12,7 @@ Build and send emails using React and Rails — a seamless integration between [
|
|
|
10
12
|
- [Requirements](#requirements)
|
|
11
13
|
- [Quick Start](#quick-start)
|
|
12
14
|
- [Usage](#usage)
|
|
15
|
+
- [Editor](#editor)
|
|
13
16
|
- [Configuration](#configuration)
|
|
14
17
|
- [Deployment](#deployment)
|
|
15
18
|
- [Development](#development)
|
|
@@ -41,6 +44,7 @@ React Email Rails automatically renders both HTML and plain-text versions from t
|
|
|
41
44
|
- Vite 7 or 8
|
|
42
45
|
- React 18 or 19
|
|
43
46
|
- `@react-email/render` 2.x
|
|
47
|
+
- For [rendering editor documents](#editor) (optional): `@react-email/editor` 1.5+ and `@tiptap/core` 3.x
|
|
44
48
|
|
|
45
49
|
> We recommend [rails_vite](https://github.com/skryukov/rails_vite/) for Vite with Rails.
|
|
46
50
|
|
|
@@ -291,6 +295,138 @@ export default function Welcome() {
|
|
|
291
295
|
|
|
292
296
|
See [Component Names](#component-names) for how shared `_` files are handled.
|
|
293
297
|
|
|
298
|
+
## Editor
|
|
299
|
+
|
|
300
|
+
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.
|
|
301
|
+
|
|
302
|
+
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.
|
|
303
|
+
|
|
304
|
+
This is opt-in. The editor packages are optional peer dependencies and stay out of the email render path until you enable it.
|
|
305
|
+
|
|
306
|
+
### Setup
|
|
307
|
+
|
|
308
|
+
Install the editor packages:
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
npm i @react-email/editor @tiptap/core
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Enable the `documents` option in your Vite config:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
// vite.config.ts
|
|
318
|
+
|
|
319
|
+
import { defineConfig } from "vite"
|
|
320
|
+
import { reactEmailRails } from "react-email-rails"
|
|
321
|
+
|
|
322
|
+
export default defineConfig({
|
|
323
|
+
plugins: [reactEmailRails({ documents: true })],
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`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).
|
|
328
|
+
|
|
329
|
+
### Document Renderers
|
|
330
|
+
|
|
331
|
+
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`).
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
// app/javascript/documents/broadcast.ts
|
|
335
|
+
|
|
336
|
+
import { StarterKit } from "@react-email/editor/extensions"
|
|
337
|
+
import { EmailTheming } from "@react-email/editor/plugins"
|
|
338
|
+
|
|
339
|
+
// Required: the Tiptap extensions the document was authored with.
|
|
340
|
+
export function buildExtensions() {
|
|
341
|
+
return [StarterKit, EmailTheming]
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
A renderer can export two optional hooks:
|
|
346
|
+
|
|
347
|
+
| Export | Required | Description |
|
|
348
|
+
|--------|----------|-------------|
|
|
349
|
+
| `buildExtensions(context)` | Yes | Returns the Tiptap extension list for the document. |
|
|
350
|
+
| `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. |
|
|
351
|
+
| `getPreview(context)` | No | Returns inbox preview text when the `compose` call doesn't pass one. |
|
|
352
|
+
|
|
353
|
+
`context` is the optional data you pass to `compose` (see below). Use it to vary extensions, transforms, or preview text per render.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// app/javascript/documents/broadcast.ts
|
|
357
|
+
|
|
358
|
+
import { StarterKit } from "@react-email/editor/extensions"
|
|
359
|
+
import { EmailTheming } from "@react-email/editor/plugins"
|
|
360
|
+
|
|
361
|
+
export function buildExtensions(context) {
|
|
362
|
+
return [StarterKit, EmailTheming]
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Inject a branded header after the persisted theme node, wherever it sits.
|
|
366
|
+
export function transformDocument(document, context) {
|
|
367
|
+
const header = {
|
|
368
|
+
type: "heading",
|
|
369
|
+
attrs: { level: 1 },
|
|
370
|
+
content: [{ type: "text", text: context.brandName }],
|
|
371
|
+
}
|
|
372
|
+
// Find globalContent and insert after it, rather than assuming a position, so
|
|
373
|
+
// the theme node is preserved.
|
|
374
|
+
const themeIndex = document.content.findIndex((node) => node.type === "globalContent")
|
|
375
|
+
const at = themeIndex + 1
|
|
376
|
+
return {
|
|
377
|
+
...document,
|
|
378
|
+
content: [...document.content.slice(0, at), header, ...document.content.slice(at)],
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function getPreview(context) {
|
|
383
|
+
return context.previewText
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
> **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.
|
|
388
|
+
|
|
389
|
+
> **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.
|
|
390
|
+
|
|
391
|
+
### Composing a Document
|
|
392
|
+
|
|
393
|
+
Call `ReactEmailRails.compose` with the renderer `type`, the stored document, and optional `context`/`preview`:
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
broadcast = Broadcast.find(params[:id])
|
|
397
|
+
|
|
398
|
+
rendered = ReactEmailRails.compose(
|
|
399
|
+
type: "broadcast",
|
|
400
|
+
document: broadcast.body, # Tiptap JSON, e.g. a jsonb column
|
|
401
|
+
context: { brand_name: "Acme", preview_text: broadcast.subject },
|
|
402
|
+
preview: broadcast.subject, # optional; falls back to getPreview(context)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
rendered.html # => "<!DOCTYPE html>..."
|
|
406
|
+
rendered.text # => "ACME\n\n..."
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
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.
|
|
410
|
+
|
|
411
|
+
**Keys:** `context` is key-transformed exactly like component props (so `brand_name` arrives as `brandName`, per [`transform_props`](#prop-transformation)). The **`document` is passed through verbatim** — its keys (`type`, `attrs`, `content`, `marks`, node names, `globalContent`) are structural and are never transformed.
|
|
412
|
+
|
|
413
|
+
`render_options` does not apply to documents; `composeReactEmail` controls its own rendering.
|
|
414
|
+
|
|
415
|
+
### Debugging Dropped Content
|
|
416
|
+
|
|
417
|
+
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.
|
|
418
|
+
|
|
419
|
+
`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:
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
|
|
423
|
+
warnings = event.payload[:warnings]
|
|
424
|
+
raise "dropped #{warnings.sum { _1[:count] }} node(s): #{warnings.map { _1[:type] }.join(", ")}" if warnings
|
|
425
|
+
end
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
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.
|
|
429
|
+
|
|
294
430
|
## Configuration
|
|
295
431
|
|
|
296
432
|
Configuration is handled primarily on the Rails side, though there are some Vite options to be aware of.
|
|
@@ -374,19 +510,19 @@ end
|
|
|
374
510
|
|
|
375
511
|
#### Error Reporting
|
|
376
512
|
|
|
377
|
-
Use `on_render_error` to report failures before the exception is re-raised:
|
|
513
|
+
Use `on_render_error` to report failures before the exception is re-raised. The callback receives the error and a `context` of `kind:` (`"email"` or `"document"`) plus the identifier — `component:` for emails, `type:` for documents. Accept `**context` so one handler covers both render kinds:
|
|
378
514
|
|
|
379
515
|
```ruby
|
|
380
516
|
ReactEmailRails.configure do |config|
|
|
381
|
-
config.on_render_error = ->(error,
|
|
382
|
-
Rails.error.report(error, context:
|
|
517
|
+
config.on_render_error = ->(error, **context) {
|
|
518
|
+
Rails.error.report(error, context:)
|
|
383
519
|
}
|
|
384
520
|
end
|
|
385
521
|
```
|
|
386
522
|
|
|
387
523
|
#### Instrumentation
|
|
388
524
|
|
|
389
|
-
Every render emits an [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html) event named `render.react-email-rails`, so you can log render timing or forward it to your APM. The payload carries the `component` name and, on success, the rendered HTML size in `html_bytes
|
|
525
|
+
Every render emits an [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html) event named `render.react-email-rails`, so you can log render timing or forward it to your APM. The payload carries a `kind` (`"email"` or `"document"`), the `component` name (email) or `type` (document), and, on success, the rendered HTML size in `html_bytes`. Document renders that drop content also include `warnings` (see [Debugging Dropped Content](#debugging-dropped-content)):
|
|
390
526
|
|
|
391
527
|
```ruby
|
|
392
528
|
ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
|
|
@@ -407,6 +543,10 @@ In development and production, the isolated renderer loads the `reactEmailRails(
|
|
|
407
543
|
| `emails.path` | `"app/javascript/emails"` | Directory containing email components |
|
|
408
544
|
| `emails.extension` | `[".tsx", ".jsx"]` | Component extension, or an array of extensions |
|
|
409
545
|
| `emails.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `emails.path` |
|
|
546
|
+
| `documents` | `false` (off) | Enable [editor document rendering](#editor). `true`, a path string, or `{ path, extension, ignore }` |
|
|
547
|
+
| `documents.path` | `"app/javascript/documents"` | Directory containing document renderers |
|
|
548
|
+
| `documents.extension` | `[".ts", ".tsx"]` | Renderer extension, or an array of extensions |
|
|
549
|
+
| `documents.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `documents.path` |
|
|
410
550
|
| `standalone` | `true` | Inline production email bundle dependencies |
|
|
411
551
|
| `vite` | `{}` | Extra email-only Vite config for compilation and resolution |
|
|
412
552
|
|
data/SECURITY.md
CHANGED
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
Please report security issues privately by emailing hi@supertape.com.
|
|
4
4
|
|
|
5
5
|
Do not open a public GitHub issue for suspected vulnerabilities. Include the affected version, a minimal reproduction when possible, and any relevant deployment details.
|
|
6
|
+
|
|
7
|
+
## Rendering editor documents
|
|
8
|
+
|
|
9
|
+
`ReactEmailRails.compose` renders an `@react-email/editor` document (Tiptap/ProseMirror JSON) server-side. Treat that document as untrusted input: it is typically authored in a visual editor and stored, then rendered later. The renderer only dispatches to the extensions you register for that document type, and React Email escapes rendered output, but URL and asset sanitization inside your custom extensions (links, image sources, button hrefs) remains your application's responsibility. Validate or sanitize those values where you build extensions or transform the document.
|
|
@@ -80,6 +80,7 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
80
80
|
}.tap do |body|
|
|
81
81
|
body[:html] = response["html"] if response.key?("html")
|
|
82
82
|
body[:text] = response["text"] if response.key?("text")
|
|
83
|
+
body[:warnings] = response["warnings"] if response.key?("warnings")
|
|
83
84
|
end,
|
|
84
85
|
))
|
|
85
86
|
rescue JSON::ParserError => e
|
|
@@ -8,10 +8,11 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@
|
|
11
|
+
# Payload-agnostic transport: the caller builds and serializes the payload.
|
|
12
|
+
# `label` identifies the render in error messages (component name or document type).
|
|
13
|
+
def initialize(payload:, label:)
|
|
14
|
+
@payload = payload
|
|
15
|
+
@label = label
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def render
|
|
@@ -20,7 +21,7 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
20
21
|
|
|
21
22
|
private
|
|
22
23
|
|
|
23
|
-
attr_reader(:
|
|
24
|
+
attr_reader(:payload, :label)
|
|
24
25
|
|
|
25
26
|
def run
|
|
26
27
|
result = capture(payload_json)
|
|
@@ -28,7 +29,7 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
28
29
|
|
|
29
30
|
body = JSON.parse(result.stdout)
|
|
30
31
|
validate_response!(body)
|
|
31
|
-
ReactEmailRails::RenderedEmail.new(html: body.fetch("html"), text: body["text"].to_s)
|
|
32
|
+
ReactEmailRails::RenderedEmail.new(html: body.fetch("html"), text: body["text"].to_s, warnings: warnings_from(body))
|
|
32
33
|
rescue JSON::ParserError => e
|
|
33
34
|
raise(render_error("render process returned invalid JSON: #{e.message}"))
|
|
34
35
|
rescue KeyError => e
|
|
@@ -52,17 +53,6 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
52
53
|
ReactEmailRails.configuration.render_timeout
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
def payload
|
|
56
|
-
@payload ||= begin
|
|
57
|
-
payload = {
|
|
58
|
-
component:,
|
|
59
|
-
props: ReactEmailRails.configuration.send(:serialize_props, props),
|
|
60
|
-
}
|
|
61
|
-
payload[:renderOptions] = render_options if render_options.present?
|
|
62
|
-
payload
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
56
|
def payload_json
|
|
67
57
|
@payload_json ||= JSON.generate(payload)
|
|
68
58
|
end
|
|
@@ -93,7 +83,14 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
93
83
|
raise(render_error("render process returned an invalid response: text must be a string")) if body.key?("text") && !body["text"].is_a?(String)
|
|
94
84
|
end
|
|
95
85
|
|
|
86
|
+
def warnings_from(body)
|
|
87
|
+
warnings = body["warnings"]
|
|
88
|
+
return [] unless warnings.is_a?(Array)
|
|
89
|
+
|
|
90
|
+
warnings.filter_map { |warning| warning.transform_keys(&:to_sym) if warning.is_a?(Hash) }
|
|
91
|
+
end
|
|
92
|
+
|
|
96
93
|
def render_error(message)
|
|
97
|
-
ReactEmailRails::RenderError.new("React Email render failed for #{
|
|
94
|
+
ReactEmailRails::RenderError.new("React Email render failed for #{label}: #{message}")
|
|
98
95
|
end
|
|
99
96
|
end
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
module ReactEmailRails
|
|
2
|
-
|
|
2
|
+
# `warnings` carries non-fatal renderer warnings (e.g. document nodes dropped
|
|
3
|
+
# because no extension rendered them); empty for component renders.
|
|
4
|
+
RenderedEmail = Data.define(:html, :text, :warnings) do
|
|
5
|
+
def initialize(html:, text:, warnings: [])
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
end
|
|
3
9
|
end
|
data/lib/react_email_rails.rb
CHANGED
|
@@ -37,13 +37,33 @@ module ReactEmailRails
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def render(component:, props:, render_options: configuration.resolve_render_options)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
payload = { component:, props: serialized_props(props) }
|
|
41
|
+
payload[:renderOptions] = render_options if render_options.present?
|
|
42
|
+
|
|
43
|
+
instrument(kind: "email", component:) do
|
|
44
|
+
configuration.resolved_render_mode.new(payload:, label: component).render
|
|
45
|
+
end
|
|
46
|
+
rescue ReactEmailRails::RenderError => e
|
|
47
|
+
configuration.on_render_error&.call(e, kind: "email", component:)
|
|
48
|
+
raise
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Render an @react-email/editor document (Tiptap JSON) to HTML+text. The document
|
|
52
|
+
# is sent verbatim (its keys are structural); only context is key-transformed, like props.
|
|
53
|
+
def compose(type:, document:, context: {}, preview: nil)
|
|
54
|
+
payload = {
|
|
55
|
+
kind: "document",
|
|
56
|
+
type:,
|
|
57
|
+
document: document.as_json,
|
|
58
|
+
context: serialized_props(context),
|
|
59
|
+
preview:,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
instrument(kind: "document", type:) do
|
|
63
|
+
configuration.resolved_render_mode.new(payload:, label: type).render
|
|
44
64
|
end
|
|
45
65
|
rescue ReactEmailRails::RenderError => e
|
|
46
|
-
configuration.on_render_error&.call(e,
|
|
66
|
+
configuration.on_render_error&.call(e, kind: "document", type:)
|
|
47
67
|
raise
|
|
48
68
|
end
|
|
49
69
|
|
|
@@ -55,5 +75,20 @@ module ReactEmailRails
|
|
|
55
75
|
rescue StandardError
|
|
56
76
|
false
|
|
57
77
|
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def serialized_props(value)
|
|
82
|
+
configuration.send(:serialize_props, value)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def instrument(**metadata)
|
|
86
|
+
ActiveSupport::Notifications.instrument("render.react-email-rails", **metadata) do |payload|
|
|
87
|
+
yield.tap do |rendered|
|
|
88
|
+
payload[:html_bytes] = rendered.html.bytesize
|
|
89
|
+
payload[:warnings] = rendered.warnings if rendered.warnings.present?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
58
93
|
end
|
|
59
94
|
end
|