react-email-rails 0.1.3 → 0.3.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: d5d324a8d629ad252aa11946478dd3b610c78920675bb027aedabf8c96c36239
4
- data.tar.gz: eaa690f2caecb96ef322bac6e587bb1dbf2bfe51e9c601a8b1b9dceb4d43d7a8
3
+ metadata.gz: 52da00b9618d5b217b66af2598edf2a25fa9d0850447bfc95d8637c778837583
4
+ data.tar.gz: 84bcdbd09672b293b85d65bbde9779c0d0d2c2c282dd83e72cc2412d38150c6b
5
5
  SHA512:
6
- metadata.gz: ca696714d6b95038f96fc579c8040fb9eaf6e98b213ddf42ba7ab92fdb49e5ab8d1dc3bb6b8a095d8fc91573314df6f4ae06136c4af41f3ec9cf66ea2bd22c85
7
- data.tar.gz: 77c513dfda5a19d5296396433d4adb79e198cffcda518311e4fe5f780efb19f532baba3743dd0b0d08928528eece7d74047280f0d3bde34a89a565ab06ddd975
6
+ metadata.gz: b9ce2a8643e90776bee1f37c9758fd9d853465d37b73184e0ebdeb20a87e33d9d686354b1a8256cb3bb4887221d25b7c14c9b6733d905c04e86f3fa2d1e06cc7
7
+ data.tar.gz: 1257c29d6639ed5a1559d23d64316bea2c6ccf075b365e816dc0bfd41cb996003e466dbabe97bc50aa9b736e2a45e81e083cd908791f91d8d94f13b239e65f67
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Add `ReactEmailRails.parse` to convert semantic HTML into a canonical `@react-email/editor` document using a renderer's extensions.
6
+ - Add `@tiptap/html` and `happy-dom` as optional peer dependencies, required only when calling `parse`; compose-only document rendering does not require them.
7
+ - Bump the render protocol to 3 (the renderer now accepts parse requests). The Ruby gem and npm package must be upgraded together, as before.
8
+
9
+ ## 0.2.0
10
+
11
+ - Add `ReactEmailRails.compose` for server-side rendering of `@react-email/editor` documents to HTML and text.
12
+ - Add the `documents` Vite plugin option for discovering document renderers, parallel to `emails`.
13
+ - Report document nodes that render to nothing as non-fatal warnings on the result and instrumentation payload.
14
+ - Add `@react-email/editor` and `@tiptap/core` as optional peer dependencies (only required when rendering documents).
15
+ - Bump the render protocol to 2 (the renderer now accepts document requests). The Ruby gem and npm package must be upgraded together, as before.
16
+ - **Breaking:** `on_render_error` callbacks now receive `(error, **context)` with `kind:` plus `component:` for emails or `type:` for documents.
17
+
3
18
  ## 0.1.3
4
19
 
5
20
  - 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
+ ![React Email + Rails](react-email-rails.png)
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,174 @@ 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
+ To also parse HTML into documents with [`parse`](#parsing-html-into-a-document), add `@tiptap/html` and its server DOM, `happy-dom`:
315
+
316
+ ```sh
317
+ npm i @tiptap/html happy-dom
318
+ ```
319
+
320
+ Enable the `documents` option in your Vite config:
321
+
322
+ ```ts
323
+ // vite.config.ts
324
+
325
+ import { defineConfig } from "vite"
326
+ import { reactEmailRails } from "react-email-rails"
327
+
328
+ export default defineConfig({
329
+ plugins: [reactEmailRails({ documents: true })],
330
+ })
331
+ ```
332
+
333
+ `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).
334
+
335
+ ### Document Renderers
336
+
337
+ 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`).
338
+
339
+ ```ts
340
+ // app/javascript/documents/broadcast.ts
341
+
342
+ import { StarterKit } from "@react-email/editor/extensions"
343
+ import { EmailTheming } from "@react-email/editor/plugins"
344
+
345
+ // Required: the Tiptap extensions the document was authored with.
346
+ export function buildExtensions() {
347
+ return [StarterKit, EmailTheming]
348
+ }
349
+ ```
350
+
351
+ A renderer can export two optional hooks:
352
+
353
+ | Export | Required | Description |
354
+ |--------|----------|-------------|
355
+ | `buildExtensions(context)` | Yes | Returns the Tiptap extension list for the document. |
356
+ | `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. |
357
+ | `getPreview(context)` | No | Returns inbox preview text when the `compose` call doesn't pass one. |
358
+
359
+ `context` is the optional data you pass to `compose` (see below). Use it to vary extensions, transforms, or preview text per render.
360
+
361
+ ```ts
362
+ // app/javascript/documents/broadcast.ts
363
+
364
+ import { StarterKit } from "@react-email/editor/extensions"
365
+ import { EmailTheming } from "@react-email/editor/plugins"
366
+
367
+ export function buildExtensions(context) {
368
+ return [StarterKit, EmailTheming]
369
+ }
370
+
371
+ // Inject a branded header after the persisted theme node, wherever it sits.
372
+ export function transformDocument(document, context) {
373
+ const header = {
374
+ type: "heading",
375
+ attrs: { level: 1 },
376
+ content: [{ type: "text", text: context.brandName }],
377
+ }
378
+ // Find globalContent and insert after it, rather than assuming a position, so
379
+ // the theme node is preserved.
380
+ const themeIndex = document.content.findIndex((node) => node.type === "globalContent")
381
+ const at = themeIndex + 1
382
+ return {
383
+ ...document,
384
+ content: [...document.content.slice(0, at), header, ...document.content.slice(at)],
385
+ }
386
+ }
387
+
388
+ export function getPreview(context) {
389
+ return context.previewText
390
+ }
391
+ ```
392
+
393
+ > **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.
394
+
395
+ > **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.
396
+
397
+ ### Composing a Document
398
+
399
+ Call `ReactEmailRails.compose` with the renderer `type`, the stored document, and optional `context`/`preview`:
400
+
401
+ ```ruby
402
+ broadcast = Broadcast.find(params[:id])
403
+
404
+ rendered = ReactEmailRails.compose(
405
+ type: "broadcast",
406
+ document: broadcast.body, # a Hash in Tiptap's shape, e.g. from a jsonb column
407
+ context: { brand_name: "Acme", preview_text: broadcast.subject },
408
+ preview: broadcast.subject, # optional; falls back to getPreview(context)
409
+ )
410
+
411
+ rendered.html # => "<!DOCTYPE html>..."
412
+ rendered.text # => "ACME\n\n..."
413
+ ```
414
+
415
+ 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.
416
+
417
+ **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-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.
418
+
419
+ **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)).
420
+
421
+ `render_options` does not apply to documents; `composeReactEmail` controls its own rendering.
422
+
423
+ ### Parsing HTML into a Document
424
+
425
+ `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)).
426
+
427
+ ```ruby
428
+ document = ReactEmailRails.parse(
429
+ type: "broadcast", # the same renderer type compose uses
430
+ html: params[:body_html], # the HTML the caller sent
431
+ context: { brand_name: "Acme" }, # optional; reaches buildExtensions, like compose
432
+ )
433
+
434
+ broadcast.update!(body: document) # persist the Hash (e.g. to a jsonb column)
435
+ ```
436
+
437
+ Later, render the stored document like any other:
438
+
439
+ ```ruby
440
+ rendered = ReactEmailRails.compose(type: "broadcast", document: broadcast.body)
441
+ ```
442
+
443
+ `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.
444
+
445
+ What this means in practice:
446
+
447
+ - 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.
448
+ - Editor-only constructs such as custom email nodes and the persisted `globalContent` theme node do not round-trip from plain HTML.
449
+ - If you already have the document `Hash`, pass it to `compose` directly.
450
+
451
+ ### Debugging Dropped Content
452
+
453
+ 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.
454
+
455
+ `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:
456
+
457
+ ```ruby
458
+ ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
459
+ warnings = event.payload[:warnings]
460
+ raise "dropped #{warnings.sum { _1[:count] }} node(s): #{warnings.map { _1[:type] }.join(", ")}" if warnings
461
+ end
462
+ ```
463
+
464
+ 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.
465
+
294
466
  ## Configuration
295
467
 
296
468
  Configuration is handled primarily on the Rails side, though there are some Vite options to be aware of.
@@ -374,19 +546,19 @@ end
374
546
 
375
547
  #### Error Reporting
376
548
 
377
- Use `on_render_error` to report failures before the exception is re-raised:
549
+ Use `on_render_error` to report failures before the exception is re-raised. The callback receives the error and a `context` of `kind:` (`"email"`, `"document"`, or `"parse"`) plus the identifier — `component:` for emails, `type:` for documents and parse requests. Accept `**context` so one handler covers every render kind:
378
550
 
379
551
  ```ruby
380
552
  ReactEmailRails.configure do |config|
381
- config.on_render_error = ->(error, component:) {
382
- Rails.error.report(error, context: { component: })
553
+ config.on_render_error = ->(error, **context) {
554
+ Rails.error.report(error, context:)
383
555
  }
384
556
  end
385
557
  ```
386
558
 
387
559
  #### Instrumentation
388
560
 
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`:
561
+ 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"`, `"document"`, or `"parse"`), the `component` name (email) or `type` (document and parse), and, on a successful render, the rendered HTML size in `html_bytes` (omitted for `parse`, which returns a document rather than HTML). Document renders that drop content also include `warnings` (see [Debugging Dropped Content](#debugging-dropped-content)):
390
562
 
391
563
  ```ruby
392
564
  ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
@@ -407,6 +579,10 @@ In development and production, the isolated renderer loads the `reactEmailRails(
407
579
  | `emails.path` | `"app/javascript/emails"` | Directory containing email components |
408
580
  | `emails.extension` | `[".tsx", ".jsx"]` | Component extension, or an array of extensions |
409
581
  | `emails.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `emails.path` |
582
+ | `documents` | `false` (off) | Enable [editor document rendering](#editor). `true`, a path string, or `{ path, extension, ignore }` |
583
+ | `documents.path` | `"app/javascript/documents"` | Directory containing document renderers |
584
+ | `documents.extension` | `[".ts", ".tsx"]` | Renderer extension, or an array of extensions |
585
+ | `documents.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `documents.path` |
410
586
  | `standalone` | `true` | Inline production email bundle dependencies |
411
587
  | `vite` | `{}` | Extra email-only Vite config for compilation and resolution |
412
588
 
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,8 @@ 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")
84
+ body[:document] = response["document"] if response.key?("document")
83
85
  end,
84
86
  ))
85
87
  rescue JSON::ParserError => e
@@ -8,10 +8,14 @@ class ReactEmailRails::RenderModes::Subprocess
8
8
  end
9
9
  end
10
10
 
11
- def initialize(component:, props:, render_options: {})
12
- @component = component
13
- @props = props
14
- @render_options = render_options
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
+ # `response` selects how the renderer's reply is interpreted: `:email` builds a
14
+ # RenderedEmail (render/compose), `:document` returns the parsed document (parse).
15
+ def initialize(payload:, label:, response: :email)
16
+ @payload = payload
17
+ @label = label
18
+ @response = response
15
19
  end
16
20
 
17
21
  def render
@@ -20,15 +24,15 @@ class ReactEmailRails::RenderModes::Subprocess
20
24
 
21
25
  private
22
26
 
23
- attr_reader(:component, :props, :render_options)
27
+ attr_reader(:payload, :label, :response)
24
28
 
25
29
  def run
26
30
  result = capture(payload_json)
27
31
  raise(render_error(error_message(result.stderr, result.status))) unless result.status.success?
28
32
 
29
33
  body = JSON.parse(result.stdout)
30
- validate_response!(body)
31
- ReactEmailRails::RenderedEmail.new(html: body.fetch("html"), text: body["text"].to_s)
34
+ validate_metadata!(body)
35
+ build_result(body)
32
36
  rescue JSON::ParserError => e
33
37
  raise(render_error("render process returned invalid JSON: #{e.message}"))
34
38
  rescue KeyError => e
@@ -52,17 +56,6 @@ class ReactEmailRails::RenderModes::Subprocess
52
56
  ReactEmailRails.configuration.render_timeout
53
57
  end
54
58
 
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
59
  def payload_json
67
60
  @payload_json ||= JSON.generate(payload)
68
61
  end
@@ -85,15 +78,41 @@ class ReactEmailRails::RenderModes::Subprocess
85
78
  raise(render_error("email bundle not found at #{bundle_path.inspect}; run react-email-rails-build before rendering React emails"))
86
79
  end
87
80
 
88
- def validate_response!(body)
89
- raise(render_error(ReactEmailRails::RenderProtocol.mismatch_message(body))) unless ReactEmailRails::RenderProtocol.compatible_metadata?(body)
81
+ def validate_metadata!(body)
82
+ return if ReactEmailRails::RenderProtocol.compatible_metadata?(body)
83
+
84
+ raise(render_error(ReactEmailRails::RenderProtocol.mismatch_message(body)))
85
+ end
86
+
87
+ def build_result(body)
88
+ response == :document ? build_document(body) : build_rendered_email(body)
89
+ end
90
90
 
91
+ def build_rendered_email(body)
91
92
  raise(KeyError.new(key: "html")) unless body.key?("html")
92
93
  raise(render_error("render process returned an invalid response: html must be a string")) unless body["html"].is_a?(String)
93
94
  raise(render_error("render process returned an invalid response: text must be a string")) if body.key?("text") && !body["text"].is_a?(String)
95
+
96
+ ReactEmailRails::RenderedEmail.new(html: body.fetch("html"), text: body["text"].to_s, warnings: warnings_from(body))
97
+ end
98
+
99
+ def build_document(body)
100
+ raise(KeyError.new(key: "document")) unless body.key?("document")
101
+
102
+ document = body.fetch("document")
103
+ raise(render_error("parse process returned an invalid response: document must be an object")) unless document.is_a?(Hash)
104
+
105
+ document
106
+ end
107
+
108
+ def warnings_from(body)
109
+ warnings = body["warnings"]
110
+ return [] unless warnings.is_a?(Array)
111
+
112
+ warnings.filter_map { |warning| warning.transform_keys(&:to_sym) if warning.is_a?(Hash) }
94
113
  end
95
114
 
96
115
  def render_error(message)
97
- ReactEmailRails::RenderError.new("React Email render failed for #{component}: #{message}")
116
+ ReactEmailRails::RenderError.new("React Email render failed for #{label}: #{message}")
98
117
  end
99
118
  end
@@ -1,5 +1,5 @@
1
1
  module ReactEmailRails
2
- RENDER_PROTOCOL_VERSION = 1
2
+ RENDER_PROTOCOL_VERSION = 3
3
3
 
4
4
  module RenderProtocol
5
5
  extend(self)
@@ -1,3 +1,9 @@
1
1
  module ReactEmailRails
2
- RenderedEmail = Data.define(:html, :text)
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
@@ -1,3 +1,3 @@
1
1
  module ReactEmailRails
2
- VERSION = "0.1.3"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -37,13 +37,50 @@ module ReactEmailRails
37
37
  end
38
38
 
39
39
  def render(component:, props:, render_options: configuration.resolve_render_options)
40
- ActiveSupport::Notifications.instrument("render.react-email-rails", component:) do |payload|
41
- configuration.resolved_render_mode.new(component:, props:, render_options:).render.tap do |rendered|
42
- payload[:html_bytes] = rendered.html.bytesize
43
- end
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
64
+ end
65
+ rescue ReactEmailRails::RenderError => e
66
+ configuration.on_render_error&.call(e, kind: "document", type:)
67
+ raise
68
+ end
69
+
70
+ # Parse HTML into an editor document Hash using the renderer's extensions.
71
+ def parse(type:, html:, context: {})
72
+ payload = {
73
+ kind: "parse",
74
+ type:,
75
+ html: html.to_s,
76
+ context: serialized_props(context),
77
+ }
78
+
79
+ instrument(kind: "parse", type:) do
80
+ configuration.resolved_render_mode.new(payload:, label: type, response: :document).render
44
81
  end
45
82
  rescue ReactEmailRails::RenderError => e
46
- configuration.on_render_error&.call(e, component:)
83
+ configuration.on_render_error&.call(e, kind: "parse", type:)
47
84
  raise
48
85
  end
49
86
 
@@ -55,5 +92,22 @@ module ReactEmailRails
55
92
  rescue StandardError
56
93
  false
57
94
  end
95
+
96
+ private
97
+
98
+ def serialized_props(value)
99
+ configuration.send(:serialize_props, value)
100
+ end
101
+
102
+ def instrument(**metadata)
103
+ ActiveSupport::Notifications.instrument("render.react-email-rails", **metadata) do |payload|
104
+ yield.tap do |result|
105
+ next unless result.respond_to?(:html)
106
+
107
+ payload[:html_bytes] = result.html.bytesize
108
+ payload[:warnings] = result.warnings if result.warnings.present?
109
+ end
110
+ end
111
+ end
58
112
  end
59
113
  end
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.1.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Supertape