react-email-rails 0.7.0 → 0.9.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 -0
- data/README.md +13 -21
- data/SECURITY.md +0 -4
- data/lib/react_email_rails/action_mailer.rb +2 -2
- data/lib/react_email_rails/render_modes/persistent/server.rb +0 -2
- data/lib/react_email_rails/render_modes/subprocess.rb +4 -25
- data/lib/react_email_rails/render_protocol.rb +1 -1
- data/lib/react_email_rails/rendered_email.rb +1 -5
- data/lib/react_email_rails/version.rb +1 -1
- data/lib/react_email_rails.rb +9 -51
- 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: 8a57e71ae6b09c032c117a6e3cb0e30c2913340b91bf3ed1c3a8480ca8b04355
|
|
4
|
+
data.tar.gz: 1880d340ee6dc94daeafe63386b90d775c99b8eaedcbad9704d55a70b3854456
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60ce6e92fe530cd05aede3d905985d36e4aa08c8167c0a6248908f7453930dd30f0e32e14f4a6921cb9ad1fbcade89054a4c95b627df07282900e6a753542b14
|
|
7
|
+
data.tar.gz: b8a0ca20d81079a24a1c7c62400bd42d60b4c559a60189cdc948be8c61236016c56251999231825152c2a5ca11e266f572a8549b8e9fbd0f15d8d3224487f34e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
- **Breaking:** Rename `react_email_share` to `react_share`, matching the `react:` key used in `mail`. Update any `react_email_share` calls in your mailers to `react_share`.
|
|
6
|
+
|
|
7
|
+
## 0.8.0
|
|
8
|
+
|
|
9
|
+
- **Breaking:** Remove `@react-email/editor` document rendering to focus the library on component-based Action Mailer emails. `ReactEmailRails.compose`, `ReactEmailRails.parse`, the `documents` Vite plugin option, and the `react-email-rails/document` module are gone, along with the `@react-email/editor`, `@tiptap/core`, `@tiptap/html`, `happy-dom`, and `marked` optional peer dependencies.
|
|
10
|
+
- **Breaking:** `on_render_error` callbacks and the `render.react-email-rails` instrumentation payload no longer include `kind:` (every render is a component email again); `RenderedEmail` no longer has `warnings`.
|
|
11
|
+
- Bump the render protocol to 4 (the renderer no longer accepts document or parse requests). The Ruby gem and npm package must be upgraded together, as before.
|
|
12
|
+
|
|
3
13
|
## 0.7.0
|
|
4
14
|
|
|
5
15
|
- Add development live-reloading for Action Mailer previews. A development-only preview interceptor injects Vite's `@vite/client` into previews, and the `reactEmailRails()` plugin triggers a full reload when an email component changes, so the open preview refreshes on save. Configure the dev-server URL with `live_reload_url`, or set it to a falsy value to disable.
|
data/README.md
CHANGED
|
@@ -18,7 +18,6 @@ You get:
|
|
|
18
18
|
- Vite-powered development rendering
|
|
19
19
|
- A production renderer bundle built during `assets:precompile`
|
|
20
20
|
- Optional persistent rendering for high-volume workers
|
|
21
|
-
- Optional server-side rendering for `@react-email/editor` documents
|
|
22
21
|
|
|
23
22
|
## Status
|
|
24
23
|
|
|
@@ -281,15 +280,15 @@ To change naming globally, override `component_path_resolver` in your [Rails con
|
|
|
281
280
|
|
|
282
281
|
### Shared Props
|
|
283
282
|
|
|
284
|
-
Use `
|
|
283
|
+
Use `react_share` to merge props into every `react:` email for a mailer and its subclasses:
|
|
285
284
|
|
|
286
285
|
```ruby
|
|
287
286
|
class MarketingMailer < ApplicationMailer
|
|
288
|
-
|
|
287
|
+
react_share app_name: "Acme"
|
|
289
288
|
|
|
290
|
-
|
|
289
|
+
react_share unread_count: -> { params[:account]&.unread_count }
|
|
291
290
|
|
|
292
|
-
|
|
291
|
+
react_share do
|
|
293
292
|
{ brand: { name: "Acme", url: marketing_url } }
|
|
294
293
|
end
|
|
295
294
|
end
|
|
@@ -311,14 +310,14 @@ Shared props work with `react:` hashes, `react: true`, and explicit `react: "com
|
|
|
311
310
|
|
|
312
311
|
### Conditional Shared Props
|
|
313
312
|
|
|
314
|
-
`
|
|
313
|
+
`react_share` accepts the same filter options as `before_action`: `only`, `except`, `if`, and `unless`.
|
|
315
314
|
|
|
316
315
|
```ruby
|
|
317
|
-
|
|
316
|
+
react_share only: [:welcome, :reactivation] do
|
|
318
317
|
{ promotion: params.fetch(:promotion) }
|
|
319
318
|
end
|
|
320
319
|
|
|
321
|
-
|
|
320
|
+
react_share if: :account_active? do
|
|
322
321
|
{ account: { name: params.fetch(:account).name } }
|
|
323
322
|
end
|
|
324
323
|
```
|
|
@@ -329,7 +328,7 @@ You can also share props inside an action before calling `mail`:
|
|
|
329
328
|
def welcome
|
|
330
329
|
account = params.fetch(:account)
|
|
331
330
|
|
|
332
|
-
|
|
331
|
+
react_share notice: "Thanks for joining!"
|
|
333
332
|
|
|
334
333
|
mail(
|
|
335
334
|
to: account.email,
|
|
@@ -346,7 +345,7 @@ Shared props are merged shallowly by default. That means a per-mail prop replace
|
|
|
346
345
|
Pass `deep_merge: true` to merge nested hashes instead:
|
|
347
346
|
|
|
348
347
|
```ruby
|
|
349
|
-
|
|
348
|
+
react_share do
|
|
350
349
|
{ settings: { theme: "light", locale: "en" } }
|
|
351
350
|
end
|
|
352
351
|
|
|
@@ -471,12 +470,6 @@ export default function Welcome() {
|
|
|
471
470
|
}
|
|
472
471
|
```
|
|
473
472
|
|
|
474
|
-
### Editor Documents
|
|
475
|
-
|
|
476
|
-
If your app uses [@react-email/editor](https://react.email/docs/editor) to let users compose emails visually, `ReactEmailRails.compose` can render stored editor documents on the server.
|
|
477
|
-
|
|
478
|
-
See [Editor rendering](docs/editor.md) for setup and usage.
|
|
479
|
-
|
|
480
473
|
## Configuration
|
|
481
474
|
|
|
482
475
|
Configuration lives in two places:
|
|
@@ -580,11 +573,11 @@ ReactEmailRails.configure do |config|
|
|
|
580
573
|
end
|
|
581
574
|
```
|
|
582
575
|
|
|
583
|
-
The callback receives the error plus render context such as `
|
|
576
|
+
The callback receives the error plus render context such as `component:`.
|
|
584
577
|
|
|
585
578
|
### Instrumentation
|
|
586
579
|
|
|
587
|
-
Every render emits `render.react-email-rails` through [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html).
|
|
580
|
+
Every render emits `render.react-email-rails` through [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html). Payloads include `component` and successful HTML size in `html_bytes`.
|
|
588
581
|
|
|
589
582
|
```ruby
|
|
590
583
|
ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
|
|
@@ -619,7 +612,6 @@ Server, preview, dependency optimization, and build output settings stay owned b
|
|
|
619
612
|
| `emails.path` | `"app/javascript/emails"` | Directory containing email components |
|
|
620
613
|
| `emails.extension` | `[".tsx", ".jsx"]` | Component extension, or an array of extensions |
|
|
621
614
|
| `emails.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `emails.path` |
|
|
622
|
-
| `documents` | `false` | Optional `@react-email/editor` document renderer discovery; see [Editor rendering](docs/editor.md) |
|
|
623
615
|
| `standalone` | `true` | Inline production renderer bundle dependencies |
|
|
624
616
|
| `vite` | `{}` | Extra email-only Vite config for compilation and resolution |
|
|
625
617
|
|
|
@@ -687,7 +679,7 @@ Production deploys should run the normal Rails asset task:
|
|
|
687
679
|
bin/rails assets:precompile
|
|
688
680
|
```
|
|
689
681
|
|
|
690
|
-
react-email-rails hooks `react_email_rails:build` into `assets:precompile`. The build task loads `reactEmailRails()` options from your Vite config and writes `tmp/react-email-rails/emails.js` with the email component registry.
|
|
682
|
+
react-email-rails hooks `react_email_rails:build` into `assets:precompile`. The build task loads `reactEmailRails()` options from your Vite config and writes `tmp/react-email-rails/emails.js` with the email component registry.
|
|
691
683
|
|
|
692
684
|
You can run the renderer build directly:
|
|
693
685
|
|
|
@@ -699,7 +691,7 @@ Production rendering requires that bundle. If it is missing, rendering raises `R
|
|
|
699
691
|
|
|
700
692
|
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.
|
|
701
693
|
|
|
702
|
-
The npm package, Vite, React, and `@react-email/render` must be available when Rails runs `assets:precompile`.
|
|
694
|
+
The npm package, Vite, React, and `@react-email/render` must be available when Rails runs `assets:precompile`.
|
|
703
695
|
|
|
704
696
|
The Ruby gem and npm package must stay on the same version. A protocol/version handshake catches mismatched installs and raises an actionable `ReactEmailRails::RenderError`.
|
|
705
697
|
|
data/SECURITY.md
CHANGED
|
@@ -3,7 +3,3 @@
|
|
|
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.
|
|
@@ -12,7 +12,7 @@ module ReactEmailRails::ActionMailer
|
|
|
12
12
|
self.react_email_use_instance_props = true
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def
|
|
15
|
+
def react_share(hash = nil, **props, &block)
|
|
16
16
|
options = props.slice(*SHARED_FILTER_OPTIONS)
|
|
17
17
|
data = hash || props.except(*SHARED_FILTER_OPTIONS)
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ module ReactEmailRails::ActionMailer
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def
|
|
25
|
+
def react_share(hash = nil, **props, &block)
|
|
26
26
|
react_email_append_shared(hash || props, block)
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -75,8 +75,6 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
75
75
|
}.tap do |body|
|
|
76
76
|
body[:html] = response["html"] if response.key?("html")
|
|
77
77
|
body[:text] = response["text"] if response.key?("text")
|
|
78
|
-
body[:warnings] = response["warnings"] if response.key?("warnings")
|
|
79
|
-
body[:document] = response["document"] if response.key?("document")
|
|
80
78
|
end,
|
|
81
79
|
))
|
|
82
80
|
rescue JSON::ParserError => e
|
|
@@ -8,10 +8,9 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def initialize(payload:, label
|
|
11
|
+
def initialize(payload:, label:)
|
|
12
12
|
@payload = payload
|
|
13
13
|
@label = label
|
|
14
|
-
@response = response
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def render
|
|
@@ -20,7 +19,7 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
20
19
|
|
|
21
20
|
private
|
|
22
21
|
|
|
23
|
-
attr_reader(:payload, :label
|
|
22
|
+
attr_reader(:payload, :label)
|
|
24
23
|
|
|
25
24
|
def run
|
|
26
25
|
result = capture(payload_json)
|
|
@@ -28,7 +27,7 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
28
27
|
|
|
29
28
|
body = JSON.parse(result.stdout)
|
|
30
29
|
validate_metadata!(body)
|
|
31
|
-
|
|
30
|
+
build_rendered_email(body)
|
|
32
31
|
rescue JSON::ParserError => e
|
|
33
32
|
raise(render_error("render process returned invalid JSON: #{e.message}"))
|
|
34
33
|
rescue KeyError => e
|
|
@@ -86,32 +85,12 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
86
85
|
raise(render_error(ReactEmailRails::RenderProtocol.mismatch_message(body)))
|
|
87
86
|
end
|
|
88
87
|
|
|
89
|
-
def build_result(body)
|
|
90
|
-
response == :document ? build_document(body) : build_rendered_email(body)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
88
|
def build_rendered_email(body)
|
|
94
89
|
raise(KeyError.new(key: "html")) unless body.key?("html")
|
|
95
90
|
raise(render_error("render process returned an invalid response: html must be a string")) unless body["html"].is_a?(String)
|
|
96
91
|
raise(render_error("render process returned an invalid response: text must be a string")) if body.key?("text") && !body["text"].is_a?(String)
|
|
97
92
|
|
|
98
|
-
ReactEmailRails::RenderedEmail.new(html: body.fetch("html"), text: body["text"].to_s
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def build_document(body)
|
|
102
|
-
raise(KeyError.new(key: "document")) unless body.key?("document")
|
|
103
|
-
|
|
104
|
-
document = body.fetch("document")
|
|
105
|
-
raise(render_error("parse process returned an invalid response: document must be an object")) unless document.is_a?(Hash)
|
|
106
|
-
|
|
107
|
-
document
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def warnings_from(body)
|
|
111
|
-
warnings = body["warnings"]
|
|
112
|
-
return [] unless warnings.is_a?(Array)
|
|
113
|
-
|
|
114
|
-
warnings.filter_map { |warning| warning.transform_keys(&:to_sym) if warning.is_a?(Hash) }
|
|
93
|
+
ReactEmailRails::RenderedEmail.new(html: body.fetch("html"), text: body["text"].to_s)
|
|
115
94
|
end
|
|
116
95
|
|
|
117
96
|
def render_error(message)
|
data/lib/react_email_rails.rb
CHANGED
|
@@ -44,29 +44,12 @@ module ReactEmailRails
|
|
|
44
44
|
payload = { component:, props: serialized_props(props) }
|
|
45
45
|
payload[:renderOptions] = render_options if render_options.present?
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
type:,
|
|
54
|
-
document: document.as_json,
|
|
55
|
-
context: serialized_props(context),
|
|
56
|
-
preview:,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
perform(payload:, label: type, kind: "document", type:)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def parse(type:, html: nil, markdown: nil, context: {})
|
|
63
|
-
payload = {
|
|
64
|
-
kind: "parse",
|
|
65
|
-
type:,
|
|
66
|
-
context: serialized_props(context),
|
|
67
|
-
}.merge(parse_source(html:, markdown:))
|
|
68
|
-
|
|
69
|
-
perform(payload:, label: type, response: :document, kind: "parse", type:)
|
|
47
|
+
instrument(component:) do
|
|
48
|
+
configuration.resolved_render_mode.new(payload:, label: component).render
|
|
49
|
+
end
|
|
50
|
+
rescue ReactEmailRails::RenderError => e
|
|
51
|
+
configuration.on_render_error&.call(e, component:)
|
|
52
|
+
raise
|
|
70
53
|
end
|
|
71
54
|
|
|
72
55
|
def healthy?
|
|
@@ -80,38 +63,13 @@ module ReactEmailRails
|
|
|
80
63
|
|
|
81
64
|
private
|
|
82
65
|
|
|
83
|
-
def perform(payload:, label:, response: :email, **metadata)
|
|
84
|
-
instrument(**metadata) do
|
|
85
|
-
configuration.resolved_render_mode.new(payload:, label:, response:).render
|
|
86
|
-
end
|
|
87
|
-
rescue ReactEmailRails::RenderError => e
|
|
88
|
-
configuration.on_render_error&.call(e, **metadata)
|
|
89
|
-
raise
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def parse_source(html:, markdown:)
|
|
93
|
-
if !html.nil? && !markdown.nil?
|
|
94
|
-
raise(ArgumentError, "ReactEmailRails.parse accepts only one of html: or markdown:")
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
return { html: html.to_s } unless html.nil?
|
|
98
|
-
return { markdown: markdown.to_s } unless markdown.nil?
|
|
99
|
-
|
|
100
|
-
raise(ArgumentError, "ReactEmailRails.parse requires html: or markdown:")
|
|
101
|
-
end
|
|
102
|
-
|
|
103
66
|
def serialized_props(value)
|
|
104
67
|
configuration.send(:serialize_props, value)
|
|
105
68
|
end
|
|
106
69
|
|
|
107
|
-
def instrument(
|
|
108
|
-
ActiveSupport::Notifications.instrument("render.react-email-rails",
|
|
109
|
-
yield.tap
|
|
110
|
-
next unless result.respond_to?(:html)
|
|
111
|
-
|
|
112
|
-
payload[:html_bytes] = result.html.bytesize
|
|
113
|
-
payload[:warnings] = result.warnings if result.warnings.present?
|
|
114
|
-
end
|
|
70
|
+
def instrument(component:)
|
|
71
|
+
ActiveSupport::Notifications.instrument("render.react-email-rails", component:) do |payload|
|
|
72
|
+
yield.tap { |result| payload[:html_bytes] = result.html.bytesize }
|
|
115
73
|
end
|
|
116
74
|
end
|
|
117
75
|
end
|