react-email-rails 0.2.0 → 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 +4 -4
- data/CHANGELOG.md +10 -4
- data/README.md +40 -4
- data/lib/react_email_rails/render_modes/persistent/server.rb +1 -0
- data/lib/react_email_rails/render_modes/subprocess.rb +28 -6
- data/lib/react_email_rails/render_protocol.rb +1 -1
- data/lib/react_email_rails/version.rb +1 -1
- data/lib/react_email_rails.rb +22 -3
- 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: 52da00b9618d5b217b66af2598edf2a25fa9d0850447bfc95d8637c778837583
|
|
4
|
+
data.tar.gz: 84bcdbd09672b293b85d65bbde9779c0d0d2c2c282dd83e72cc2412d38150c6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9ce2a8643e90776bee1f37c9758fd9d853465d37b73184e0ebdeb20a87e33d9d686354b1a8256cb3bb4887221d25b7c14c9b6733d905c04e86f3fa2d1e06cc7
|
|
7
|
+
data.tar.gz: 1257c29d6639ed5a1559d23d64316bea2c6ccf075b365e816dc0bfd41cb996003e466dbabe97bc50aa9b736e2a45e81e083cd908791f91d8d94f13b239e65f67
|
data/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.2.0
|
|
4
10
|
|
|
5
|
-
- Add `ReactEmailRails.compose` for server-side rendering of `@react-email/editor` documents
|
|
6
|
-
- Add the `documents` Vite plugin option for discovering document renderers, parallel to `emails`.
|
|
7
|
-
- Report document nodes that render to nothing
|
|
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.
|
|
8
14
|
- Add `@react-email/editor` and `@tiptap/core` as optional peer dependencies (only required when rendering documents).
|
|
9
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.
|
|
10
|
-
- **Breaking:** `on_render_error` callbacks now receive
|
|
16
|
+
- **Breaking:** `on_render_error` callbacks now receive `(error, **context)` with `kind:` plus `component:` for emails or `type:` for documents.
|
|
11
17
|
|
|
12
18
|
## 0.1.3
|
|
13
19
|
|
data/README.md
CHANGED
|
@@ -311,6 +311,12 @@ Install the editor packages:
|
|
|
311
311
|
npm i @react-email/editor @tiptap/core
|
|
312
312
|
```
|
|
313
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
|
+
|
|
314
320
|
Enable the `documents` option in your Vite config:
|
|
315
321
|
|
|
316
322
|
```ts
|
|
@@ -397,7 +403,7 @@ broadcast = Broadcast.find(params[:id])
|
|
|
397
403
|
|
|
398
404
|
rendered = ReactEmailRails.compose(
|
|
399
405
|
type: "broadcast",
|
|
400
|
-
document: broadcast.body, # Tiptap
|
|
406
|
+
document: broadcast.body, # a Hash in Tiptap's shape, e.g. from a jsonb column
|
|
401
407
|
context: { brand_name: "Acme", preview_text: broadcast.subject },
|
|
402
408
|
preview: broadcast.subject, # optional; falls back to getPreview(context)
|
|
403
409
|
)
|
|
@@ -408,10 +414,40 @@ rendered.text # => "ACME\n\n..."
|
|
|
408
414
|
|
|
409
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.
|
|
410
416
|
|
|
411
|
-
**
|
|
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)).
|
|
412
420
|
|
|
413
421
|
`render_options` does not apply to documents; `composeReactEmail` controls its own rendering.
|
|
414
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
|
+
|
|
415
451
|
### Debugging Dropped Content
|
|
416
452
|
|
|
417
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.
|
|
@@ -510,7 +546,7 @@ end
|
|
|
510
546
|
|
|
511
547
|
#### Error Reporting
|
|
512
548
|
|
|
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 `"
|
|
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:
|
|
514
550
|
|
|
515
551
|
```ruby
|
|
516
552
|
ReactEmailRails.configure do |config|
|
|
@@ -522,7 +558,7 @@ end
|
|
|
522
558
|
|
|
523
559
|
#### Instrumentation
|
|
524
560
|
|
|
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 `"
|
|
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)):
|
|
526
562
|
|
|
527
563
|
```ruby
|
|
528
564
|
ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
|
|
@@ -81,6 +81,7 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
81
81
|
body[:html] = response["html"] if response.key?("html")
|
|
82
82
|
body[:text] = response["text"] if response.key?("text")
|
|
83
83
|
body[:warnings] = response["warnings"] if response.key?("warnings")
|
|
84
|
+
body[:document] = response["document"] if response.key?("document")
|
|
84
85
|
end,
|
|
85
86
|
))
|
|
86
87
|
rescue JSON::ParserError => e
|
|
@@ -10,9 +10,12 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
10
10
|
|
|
11
11
|
# Payload-agnostic transport: the caller builds and serializes the payload.
|
|
12
12
|
# `label` identifies the render in error messages (component name or document type).
|
|
13
|
-
|
|
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)
|
|
14
16
|
@payload = payload
|
|
15
17
|
@label = label
|
|
18
|
+
@response = response
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def render
|
|
@@ -21,15 +24,15 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
21
24
|
|
|
22
25
|
private
|
|
23
26
|
|
|
24
|
-
attr_reader(:payload, :label)
|
|
27
|
+
attr_reader(:payload, :label, :response)
|
|
25
28
|
|
|
26
29
|
def run
|
|
27
30
|
result = capture(payload_json)
|
|
28
31
|
raise(render_error(error_message(result.stderr, result.status))) unless result.status.success?
|
|
29
32
|
|
|
30
33
|
body = JSON.parse(result.stdout)
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
validate_metadata!(body)
|
|
35
|
+
build_result(body)
|
|
33
36
|
rescue JSON::ParserError => e
|
|
34
37
|
raise(render_error("render process returned invalid JSON: #{e.message}"))
|
|
35
38
|
rescue KeyError => e
|
|
@@ -75,12 +78,31 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
75
78
|
raise(render_error("email bundle not found at #{bundle_path.inspect}; run react-email-rails-build before rendering React emails"))
|
|
76
79
|
end
|
|
77
80
|
|
|
78
|
-
def
|
|
79
|
-
|
|
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
|
|
80
90
|
|
|
91
|
+
def build_rendered_email(body)
|
|
81
92
|
raise(KeyError.new(key: "html")) unless body.key?("html")
|
|
82
93
|
raise(render_error("render process returned an invalid response: html must be a string")) unless body["html"].is_a?(String)
|
|
83
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
|
|
84
106
|
end
|
|
85
107
|
|
|
86
108
|
def warnings_from(body)
|
data/lib/react_email_rails.rb
CHANGED
|
@@ -67,6 +67,23 @@ module ReactEmailRails
|
|
|
67
67
|
raise
|
|
68
68
|
end
|
|
69
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
|
|
81
|
+
end
|
|
82
|
+
rescue ReactEmailRails::RenderError => e
|
|
83
|
+
configuration.on_render_error&.call(e, kind: "parse", type:)
|
|
84
|
+
raise
|
|
85
|
+
end
|
|
86
|
+
|
|
70
87
|
def healthy?
|
|
71
88
|
configuration.resolved_render_mode.healthy?(
|
|
72
89
|
command: configuration.send(:resolved_render_command),
|
|
@@ -84,9 +101,11 @@ module ReactEmailRails
|
|
|
84
101
|
|
|
85
102
|
def instrument(**metadata)
|
|
86
103
|
ActiveSupport::Notifications.instrument("render.react-email-rails", **metadata) do |payload|
|
|
87
|
-
yield.tap do |
|
|
88
|
-
|
|
89
|
-
|
|
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?
|
|
90
109
|
end
|
|
91
110
|
end
|
|
92
111
|
end
|