react-email-rails 0.6.1 → 0.8.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 +21 -16
- data/SECURITY.md +0 -4
- data/lib/generators/react_email_rails/email_generator.rb +0 -1
- data/lib/generators/react_email_rails/vite_config_files.rb +0 -1
- data/lib/react_email_rails/action_mailer.rb +0 -1
- data/lib/react_email_rails/configuration.rb +9 -3
- data/lib/react_email_rails/preview_live_reload.rb +33 -0
- data/lib/react_email_rails/props_resolver.rb +0 -2
- data/lib/react_email_rails/railtie.rb +1 -0
- data/lib/react_email_rails/render_modes/persistent/command_runner.rb +0 -3
- data/lib/react_email_rails/render_modes/persistent/server.rb +0 -8
- data/lib/react_email_rails/render_modes/subprocess.rb +4 -28
- data/lib/react_email_rails/render_protocol.rb +1 -2
- data/lib/react_email_rails/rendered_email.rb +1 -6
- data/lib/react_email_rails/shared_props.rb +0 -6
- data/lib/react_email_rails/version.rb +1 -1
- data/lib/react_email_rails.rb +10 -54
- 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: cf2e6b94aa19f4ccec30037d5eefb38b1b98ba2f070ede1a89c99818e47b2eec
|
|
4
|
+
data.tar.gz: 26710f44d94a84d1df7a73a0759973c37fc9b3ec031c143ae902aed890ebb2d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 911446dcf37e32e420bef4e0f02cdf8c30c6076854dba593737ae552138e139473f355d1ef14328bf50cf77943e5b376e82300185b8d23ea34fb41a1a3deaaaf
|
|
7
|
+
data.tar.gz: 708ac8a7339f5133be74caccce59a1f30dc890f6da95adeb5385a5117b894295fd8e3e5748a50e8a46d0cdd6bcc1abc6d3943abf9bf146dd2b57163c3d0c94a5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- **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.
|
|
6
|
+
- **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`.
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.7.0
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
3
13
|
## 0.6.1
|
|
4
14
|
|
|
5
15
|
- Fix `react:` rendering — and the `mailer`/`message` props — being skipped for actions that opt in through a class-level `default react: true` rather than a per-`mail` `react:` option. `mail` now resolves `react` (in any form: `true`, a component string, or a prop hash) from the mailer's `default`, a per-action `react: false` opts back out, and the internal `react`/`props`/`deep_merge` options never leak onto the message as email headers.
|
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
|
|
|
@@ -30,8 +29,8 @@ The supported Ruby, Rails, Node, React, and Vite versions are tested in CI. Plea
|
|
|
30
29
|
|
|
31
30
|
- [Requirements](#requirements)
|
|
32
31
|
- [Installation](#installation)
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
32
|
+
- [Quick Start](#quick-start)
|
|
33
|
+
- [Rendering](#rendering)
|
|
35
34
|
- [Usage](#usage)
|
|
36
35
|
- [Configuration](#configuration)
|
|
37
36
|
- [Deployment](#deployment)
|
|
@@ -100,7 +99,7 @@ export default defineConfig({
|
|
|
100
99
|
})
|
|
101
100
|
```
|
|
102
101
|
|
|
103
|
-
##
|
|
102
|
+
## Quick Start
|
|
104
103
|
|
|
105
104
|
Generate a mailer and React Email component:
|
|
106
105
|
|
|
@@ -169,7 +168,7 @@ AccountMailer.with(account: current_account).welcome.deliver_later
|
|
|
169
168
|
|
|
170
169
|
React Email also provides primitives like [`<Button>`, `<Heading>`, `<Tailwind>`, and more](https://react.email/docs/components/html).
|
|
171
170
|
|
|
172
|
-
##
|
|
171
|
+
## Rendering
|
|
173
172
|
|
|
174
173
|
In development, react-email-rails renders components through Vite's dev pipeline. Your email components get the same module resolution and transforms as the rest of your frontend.
|
|
175
174
|
|
|
@@ -177,6 +176,18 @@ In production, `assets:precompile` builds a server-side renderer bundle from you
|
|
|
177
176
|
|
|
178
177
|
Every `react:` email renders HTML and plain text from the same component. If rendering fails, the email is not sent and `ReactEmailRails::RenderError` is raised.
|
|
179
178
|
|
|
179
|
+
### Live-Reloading Previews
|
|
180
|
+
|
|
181
|
+
In development, Action Mailer previews automatically reload themselves when you edit an email component. react-email-rails registers a [preview interceptor](https://api.rubyonrails.org/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Previewing+emails) that injects `@vite/client` into the preview, and the `reactEmailRails()` plugin broadcasts a full reload over Vite's websocket whenever a file under your emails directory changes.
|
|
182
|
+
|
|
183
|
+
The `live_reload_url` defaults to Vite's `http://localhost:5173`, but you can point elsewhere if needed, or set it to a falsy value to disable live reload. (See [Configuration](#configuration))
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
ReactEmailRails.configure do |config|
|
|
187
|
+
config.live_reload_url = "http://localhost:3036" # or nil or false to disable
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
180
191
|
## Usage
|
|
181
192
|
|
|
182
193
|
### Passing Props
|
|
@@ -459,12 +470,6 @@ export default function Welcome() {
|
|
|
459
470
|
}
|
|
460
471
|
```
|
|
461
472
|
|
|
462
|
-
### Editor Documents
|
|
463
|
-
|
|
464
|
-
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.
|
|
465
|
-
|
|
466
|
-
See [Editor rendering](docs/editor.md) for setup and usage.
|
|
467
|
-
|
|
468
473
|
## Configuration
|
|
469
474
|
|
|
470
475
|
Configuration lives in two places:
|
|
@@ -492,6 +497,7 @@ end
|
|
|
492
497
|
| `render_process_max_requests` | `1_000` |
|
|
493
498
|
| `on_render_error` | `nil` |
|
|
494
499
|
| `deep_merge_shared_props` | `false` |
|
|
500
|
+
| `live_reload_url` | `http://localhost:5173` |
|
|
495
501
|
|
|
496
502
|
### Prop Transformation
|
|
497
503
|
|
|
@@ -567,11 +573,11 @@ ReactEmailRails.configure do |config|
|
|
|
567
573
|
end
|
|
568
574
|
```
|
|
569
575
|
|
|
570
|
-
The callback receives the error plus render context such as `
|
|
576
|
+
The callback receives the error plus render context such as `component:`.
|
|
571
577
|
|
|
572
578
|
### Instrumentation
|
|
573
579
|
|
|
574
|
-
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`.
|
|
575
581
|
|
|
576
582
|
```ruby
|
|
577
583
|
ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
|
|
@@ -606,7 +612,6 @@ Server, preview, dependency optimization, and build output settings stay owned b
|
|
|
606
612
|
| `emails.path` | `"app/javascript/emails"` | Directory containing email components |
|
|
607
613
|
| `emails.extension` | `[".tsx", ".jsx"]` | Component extension, or an array of extensions |
|
|
608
614
|
| `emails.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `emails.path` |
|
|
609
|
-
| `documents` | `false` | Optional `@react-email/editor` document renderer discovery; see [Editor rendering](docs/editor.md) |
|
|
610
615
|
| `standalone` | `true` | Inline production renderer bundle dependencies |
|
|
611
616
|
| `vite` | `{}` | Extra email-only Vite config for compilation and resolution |
|
|
612
617
|
|
|
@@ -674,7 +679,7 @@ Production deploys should run the normal Rails asset task:
|
|
|
674
679
|
bin/rails assets:precompile
|
|
675
680
|
```
|
|
676
681
|
|
|
677
|
-
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.
|
|
678
683
|
|
|
679
684
|
You can run the renderer build directly:
|
|
680
685
|
|
|
@@ -686,7 +691,7 @@ Production rendering requires that bundle. If it is missing, rendering raises `R
|
|
|
686
691
|
|
|
687
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.
|
|
688
693
|
|
|
689
|
-
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`.
|
|
690
695
|
|
|
691
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`.
|
|
692
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.
|
|
@@ -139,7 +139,6 @@ class ReactEmailRails::Generators::EmailGenerator < Rails::Generators::NamedBase
|
|
|
139
139
|
].find { |path| File.exist?(File.join(destination_root, path)) }
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
-
# The reactEmailRails({ ... }) plugin call, up to the option key being scanned for.
|
|
143
142
|
PLUGIN_OPENING = /reactEmailRails\s*\(\s*\{.*?/m
|
|
144
143
|
|
|
145
144
|
def emails_path_from_vite_config
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
class ReactEmailRails::Configuration
|
|
2
|
-
# Must match OUT_DIR/BUNDLE_FILE in vite/src/index.ts (check_version_sync.rb asserts it).
|
|
3
2
|
BUNDLE_PATH = "tmp/react-email-rails/emails.js"
|
|
4
3
|
BUILD_BIN = "node_modules/.bin/react-email-rails-build"
|
|
5
4
|
CONFIG_BIN = "node_modules/.bin/react-email-rails-config"
|
|
@@ -7,6 +6,7 @@ class ReactEmailRails::Configuration
|
|
|
7
6
|
|
|
8
7
|
DEFAULT_RENDER_TIMEOUT = 10
|
|
9
8
|
DEFAULT_RENDER_PROCESS_MAX_REQUESTS = 1_000
|
|
9
|
+
DEFAULT_LIVE_RELOAD_URL = "http://localhost:5173"
|
|
10
10
|
|
|
11
11
|
RENDER_MODES = {
|
|
12
12
|
subprocess: ReactEmailRails::RenderModes::Subprocess,
|
|
@@ -35,6 +35,7 @@ class ReactEmailRails::Configuration
|
|
|
35
35
|
:transform_props,
|
|
36
36
|
:on_render_error,
|
|
37
37
|
:deep_merge_shared_props,
|
|
38
|
+
:live_reload_url,
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
attr_reader(
|
|
@@ -54,10 +55,17 @@ class ReactEmailRails::Configuration
|
|
|
54
55
|
config.transform_props = :lower_camel
|
|
55
56
|
config.on_render_error = nil
|
|
56
57
|
config.deep_merge_shared_props = false
|
|
58
|
+
config.live_reload_url = DEFAULT_LIVE_RELOAD_URL
|
|
57
59
|
end
|
|
58
60
|
end
|
|
59
61
|
end
|
|
60
62
|
|
|
63
|
+
def resolve_live_reload_url
|
|
64
|
+
return if live_reload_url.blank?
|
|
65
|
+
|
|
66
|
+
live_reload_url.to_s.chomp("/")
|
|
67
|
+
end
|
|
68
|
+
|
|
61
69
|
def render_mode=(value)
|
|
62
70
|
if (value.is_a?(Symbol) || value.is_a?(String)) && !RENDER_MODES.key?(value.to_sym)
|
|
63
71
|
raise(ArgumentError, "Unknown react-email-rails render mode: #{value.inspect}")
|
|
@@ -88,8 +96,6 @@ class ReactEmailRails::Configuration
|
|
|
88
96
|
end
|
|
89
97
|
end
|
|
90
98
|
|
|
91
|
-
# A callable render_options is instance_exec'd against `context` (the mailer) when given,
|
|
92
|
-
# so it can use per-mail helpers; otherwise it's called or returned as-is.
|
|
93
99
|
def resolve_render_options(context = nil)
|
|
94
100
|
value =
|
|
95
101
|
if render_options.respond_to?(:call) && context
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class ReactEmailRails::PreviewLiveReload
|
|
2
|
+
SCRIPT_TEMPLATE = %(<script type="module" src="%s/@vite/client"></script>)
|
|
3
|
+
BODY_CLOSE = %r{</body>}i
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def previewing_email(message)
|
|
7
|
+
url = ReactEmailRails.configuration.resolve_live_reload_url
|
|
8
|
+
return unless url
|
|
9
|
+
|
|
10
|
+
part = html_part(message)
|
|
11
|
+
return unless part
|
|
12
|
+
|
|
13
|
+
part.body = inject(part.body.decoded, url)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def html_part(message)
|
|
19
|
+
return message.html_part if message.html_part
|
|
20
|
+
return message if message.mime_type == "text/html"
|
|
21
|
+
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inject(html, url)
|
|
26
|
+
snippet = format(SCRIPT_TEMPLATE, url)
|
|
27
|
+
return html if html.include?(snippet)
|
|
28
|
+
return "#{html}#{snippet}" unless BODY_CLOSE.match?(html)
|
|
29
|
+
|
|
30
|
+
html.sub(BODY_CLOSE) { |tag| "#{snippet}#{tag}" }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
class ReactEmailRails::PropsResolver
|
|
2
2
|
INTERNAL_ASSIGN_PREFIX = "_"
|
|
3
|
-
# Some Action Mailer framework assigns do not use the internal `_` prefix.
|
|
4
3
|
RESERVED_ASSIGNS = ["params", "rendered_format"].freeze
|
|
5
4
|
|
|
6
5
|
def initialize(mailer)
|
|
@@ -34,7 +33,6 @@ class ReactEmailRails::PropsResolver
|
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
def assign_props
|
|
37
|
-
# `react: true` infers the component; instance vars become props only when the mailer opts in.
|
|
38
36
|
return {} unless mailer.class.react_email_use_instance_props
|
|
39
37
|
|
|
40
38
|
mailer.instance_variables.each_with_object({}) do |ivar, props|
|
|
@@ -2,6 +2,7 @@ class ReactEmailRails::Railtie < Rails::Railtie
|
|
|
2
2
|
initializer("react-email-rails.action_mailer") do
|
|
3
3
|
ActiveSupport.on_load(:action_mailer) do
|
|
4
4
|
prepend(ReactEmailRails::ActionMailer)
|
|
5
|
+
register_preview_interceptor(ReactEmailRails::PreviewLiveReload) if Rails.env.development?
|
|
5
6
|
end
|
|
6
7
|
end
|
|
7
8
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
class ReactEmailRails::RenderModes::Persistent::CommandRunner
|
|
2
|
-
# Eager (not lazy) so concurrent first renders can't each create separate Mutexes.
|
|
3
2
|
@mutex = Mutex.new
|
|
4
3
|
|
|
5
4
|
class << self
|
|
@@ -30,8 +29,6 @@ class ReactEmailRails::RenderModes::Persistent::CommandRunner
|
|
|
30
29
|
end
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
# A forked child inherits the parent's Servers and their open pipes; sharing those pipes
|
|
34
|
-
# interleaves requests across processes, so drop them without killing the parent's process.
|
|
35
32
|
def reset_after_fork
|
|
36
33
|
return if @owner_pid == Process.pid && @servers
|
|
37
34
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
class ReactEmailRails::RenderModes::Persistent::Server
|
|
2
2
|
STDERR_LIMIT = 8 * 1024
|
|
3
3
|
|
|
4
|
-
# Minimal Process::Status stand-in; only #success? is ever read.
|
|
5
4
|
Status = Data.define(:success) do
|
|
6
5
|
def success? = success
|
|
7
6
|
end
|
|
@@ -38,7 +37,6 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
38
37
|
release_io
|
|
39
38
|
end
|
|
40
39
|
|
|
41
|
-
# Release this process's copy of an inherited child's pipes without signalling the parent-owned process.
|
|
42
40
|
def abandon
|
|
43
41
|
release_io
|
|
44
42
|
rescue IOError
|
|
@@ -47,14 +45,12 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
47
45
|
|
|
48
46
|
private
|
|
49
47
|
|
|
50
|
-
# Shared by stop (which also kills the process) and abandon (which must not).
|
|
51
48
|
def release_io
|
|
52
49
|
[@stdin, @stdout, @stderr].compact.each { |io| io.close unless io.closed? }
|
|
53
50
|
@stdin = @stdout = @stderr = @wait_thread = @stderr_reader = nil
|
|
54
51
|
@stdout_buffer.clear
|
|
55
52
|
end
|
|
56
53
|
|
|
57
|
-
# Run under the mutex; on a broken pipe, stop and retry once (the child respawns).
|
|
58
54
|
def with_retry_on_broken_pipe(&block)
|
|
59
55
|
@mutex.synchronize(&block)
|
|
60
56
|
rescue Errno::EPIPE, IOError
|
|
@@ -72,8 +68,6 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
72
68
|
response = request(input, timeout:)
|
|
73
69
|
return failure(response["error"].to_s.presence || "render process failed") unless response["ok"]
|
|
74
70
|
|
|
75
|
-
# Re-serialize into the same Result.stdout contract the subprocess produces, so
|
|
76
|
-
# Subprocess#run parses and validates every render uniformly.
|
|
77
71
|
success(JSON.generate(
|
|
78
72
|
{
|
|
79
73
|
protocolVersion: response["protocolVersion"],
|
|
@@ -81,8 +75,6 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
81
75
|
}.tap do |body|
|
|
82
76
|
body[:html] = response["html"] if response.key?("html")
|
|
83
77
|
body[:text] = response["text"] if response.key?("text")
|
|
84
|
-
body[:warnings] = response["warnings"] if response.key?("warnings")
|
|
85
|
-
body[:document] = response["document"] if response.key?("document")
|
|
86
78
|
end,
|
|
87
79
|
))
|
|
88
80
|
rescue JSON::ParserError => e
|
|
@@ -8,12 +8,9 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
# (RenderedEmail, for render/compose) or `:document` (parsed document, for parse).
|
|
13
|
-
def initialize(payload:, label:, response: :email)
|
|
11
|
+
def initialize(payload:, label:)
|
|
14
12
|
@payload = payload
|
|
15
13
|
@label = label
|
|
16
|
-
@response = response
|
|
17
14
|
end
|
|
18
15
|
|
|
19
16
|
def render
|
|
@@ -22,7 +19,7 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
22
19
|
|
|
23
20
|
private
|
|
24
21
|
|
|
25
|
-
attr_reader(:payload, :label
|
|
22
|
+
attr_reader(:payload, :label)
|
|
26
23
|
|
|
27
24
|
def run
|
|
28
25
|
result = capture(payload_json)
|
|
@@ -30,7 +27,7 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
30
27
|
|
|
31
28
|
body = JSON.parse(result.stdout)
|
|
32
29
|
validate_metadata!(body)
|
|
33
|
-
|
|
30
|
+
build_rendered_email(body)
|
|
34
31
|
rescue JSON::ParserError => e
|
|
35
32
|
raise(render_error("render process returned invalid JSON: #{e.message}"))
|
|
36
33
|
rescue KeyError => e
|
|
@@ -44,7 +41,6 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
44
41
|
end
|
|
45
42
|
end
|
|
46
43
|
|
|
47
|
-
# Shared by Persistent#capture so the transport-error messages live in one place.
|
|
48
44
|
def with_capture_rescues
|
|
49
45
|
yield
|
|
50
46
|
rescue Timeout::Error
|
|
@@ -89,32 +85,12 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
89
85
|
raise(render_error(ReactEmailRails::RenderProtocol.mismatch_message(body)))
|
|
90
86
|
end
|
|
91
87
|
|
|
92
|
-
def build_result(body)
|
|
93
|
-
response == :document ? build_document(body) : build_rendered_email(body)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
88
|
def build_rendered_email(body)
|
|
97
89
|
raise(KeyError.new(key: "html")) unless body.key?("html")
|
|
98
90
|
raise(render_error("render process returned an invalid response: html must be a string")) unless body["html"].is_a?(String)
|
|
99
91
|
raise(render_error("render process returned an invalid response: text must be a string")) if body.key?("text") && !body["text"].is_a?(String)
|
|
100
92
|
|
|
101
|
-
ReactEmailRails::RenderedEmail.new(html: body.fetch("html"), text: body["text"].to_s
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def build_document(body)
|
|
105
|
-
raise(KeyError.new(key: "document")) unless body.key?("document")
|
|
106
|
-
|
|
107
|
-
document = body.fetch("document")
|
|
108
|
-
raise(render_error("parse process returned an invalid response: document must be an object")) unless document.is_a?(Hash)
|
|
109
|
-
|
|
110
|
-
document
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def warnings_from(body)
|
|
114
|
-
warnings = body["warnings"]
|
|
115
|
-
return [] unless warnings.is_a?(Array)
|
|
116
|
-
|
|
117
|
-
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)
|
|
118
94
|
end
|
|
119
95
|
|
|
120
96
|
def render_error(message)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
require("json")
|
|
2
2
|
|
|
3
3
|
module ReactEmailRails
|
|
4
|
-
RENDER_PROTOCOL_VERSION =
|
|
4
|
+
RENDER_PROTOCOL_VERSION = 4
|
|
5
5
|
|
|
6
6
|
module RenderProtocol
|
|
7
7
|
extend(self)
|
|
8
8
|
|
|
9
|
-
# Callers keep their own rescue to also cover failures obtaining the result.
|
|
10
9
|
def healthy_result?(result)
|
|
11
10
|
result.status.success? && compatible_response?(JSON.parse(result.stdout))
|
|
12
11
|
end
|
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
module ReactEmailRails
|
|
2
|
-
|
|
3
|
-
RenderedEmail = Data.define(:html, :text, :warnings) do
|
|
4
|
-
def initialize(html:, text:, warnings: [])
|
|
5
|
-
super
|
|
6
|
-
end
|
|
7
|
-
end
|
|
2
|
+
RenderedEmail = Data.define(:html, :text)
|
|
8
3
|
end
|
|
@@ -1,5 +1,3 @@
|
|
|
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
1
|
class ReactEmailRails::SharedProps
|
|
4
2
|
IVAR = :@_react_email_shared
|
|
5
3
|
|
|
@@ -7,8 +5,6 @@ class ReactEmailRails::SharedProps
|
|
|
7
5
|
@mailer = mailer
|
|
8
6
|
end
|
|
9
7
|
|
|
10
|
-
# Returns `props` untouched when nothing is shared, so non-Hash inputs (e.g.
|
|
11
|
-
# serializers) still flow straight through to serialization.
|
|
12
8
|
def merge_into(props, deep_merge:)
|
|
13
9
|
shared = to_h
|
|
14
10
|
return props if shared.empty?
|
|
@@ -32,8 +28,6 @@ class ReactEmailRails::SharedProps
|
|
|
32
28
|
mailer.instance_variable_get(IVAR) || []
|
|
33
29
|
end
|
|
34
30
|
|
|
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
31
|
def resolve(entry)
|
|
38
32
|
hash = entry.respond_to?(:call) ? mailer.instance_exec(&entry) : entry
|
|
39
33
|
|
data/lib/react_email_rails.rb
CHANGED
|
@@ -27,6 +27,7 @@ require_relative("react_email_rails/tasks")
|
|
|
27
27
|
require_relative("react_email_rails/props_resolver")
|
|
28
28
|
require_relative("react_email_rails/shared_props")
|
|
29
29
|
require_relative("react_email_rails/mailer_context")
|
|
30
|
+
require_relative("react_email_rails/preview_live_reload")
|
|
30
31
|
require_relative("react_email_rails/railtie")
|
|
31
32
|
|
|
32
33
|
module ReactEmailRails
|
|
@@ -43,31 +44,12 @@ module ReactEmailRails
|
|
|
43
44
|
payload = { component:, props: serialized_props(props) }
|
|
44
45
|
payload[:renderOptions] = render_options if render_options.present?
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
kind: "document",
|
|
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
|
-
# Returns an editor document Hash, built via the renderer's extensions. Pass exactly one of `html:`/`markdown:`.
|
|
63
|
-
def parse(type:, html: nil, markdown: nil, context: {})
|
|
64
|
-
payload = {
|
|
65
|
-
kind: "parse",
|
|
66
|
-
type:,
|
|
67
|
-
context: serialized_props(context),
|
|
68
|
-
}.merge(parse_source(html:, markdown:))
|
|
69
|
-
|
|
70
|
-
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
|
|
71
53
|
end
|
|
72
54
|
|
|
73
55
|
def healthy?
|
|
@@ -81,39 +63,13 @@ module ReactEmailRails
|
|
|
81
63
|
|
|
82
64
|
private
|
|
83
65
|
|
|
84
|
-
def perform(payload:, label:, response: :email, **metadata)
|
|
85
|
-
instrument(**metadata) do
|
|
86
|
-
configuration.resolved_render_mode.new(payload:, label:, response:).render
|
|
87
|
-
end
|
|
88
|
-
rescue ReactEmailRails::RenderError => e
|
|
89
|
-
configuration.on_render_error&.call(e, **metadata)
|
|
90
|
-
raise
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Markdown is converted renderer-side, not here; both inputs are sent as-is.
|
|
94
|
-
def parse_source(html:, markdown:)
|
|
95
|
-
if !html.nil? && !markdown.nil?
|
|
96
|
-
raise(ArgumentError, "ReactEmailRails.parse accepts only one of html: or markdown:")
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
return { html: html.to_s } unless html.nil?
|
|
100
|
-
return { markdown: markdown.to_s } unless markdown.nil?
|
|
101
|
-
|
|
102
|
-
raise(ArgumentError, "ReactEmailRails.parse requires html: or markdown:")
|
|
103
|
-
end
|
|
104
|
-
|
|
105
66
|
def serialized_props(value)
|
|
106
67
|
configuration.send(:serialize_props, value)
|
|
107
68
|
end
|
|
108
69
|
|
|
109
|
-
def instrument(
|
|
110
|
-
ActiveSupport::Notifications.instrument("render.react-email-rails",
|
|
111
|
-
yield.tap
|
|
112
|
-
next unless result.respond_to?(:html)
|
|
113
|
-
|
|
114
|
-
payload[:html_bytes] = result.html.bytesize
|
|
115
|
-
payload[:warnings] = result.warnings if result.warnings.present?
|
|
116
|
-
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 }
|
|
117
73
|
end
|
|
118
74
|
end
|
|
119
75
|
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.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Supertape
|
|
@@ -154,6 +154,7 @@ files:
|
|
|
154
154
|
- lib/react_email_rails/action_mailer.rb
|
|
155
155
|
- lib/react_email_rails/configuration.rb
|
|
156
156
|
- lib/react_email_rails/mailer_context.rb
|
|
157
|
+
- lib/react_email_rails/preview_live_reload.rb
|
|
157
158
|
- lib/react_email_rails/props_resolver.rb
|
|
158
159
|
- lib/react_email_rails/railtie.rb
|
|
159
160
|
- lib/react_email_rails/render_error.rb
|