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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4755984b895a6aa5f220079065da541f8b8da23dc905095fb5cae48471fb2096
4
- data.tar.gz: 66568a9bd36727d453904f765b2eb73b3aff4b8335b59de57d34933f1e4a4b8a
3
+ metadata.gz: cf2e6b94aa19f4ccec30037d5eefb38b1b98ba2f070ede1a89c99818e47b2eec
4
+ data.tar.gz: 26710f44d94a84d1df7a73a0759973c37fc9b3ec031c143ae902aed890ebb2d2
5
5
  SHA512:
6
- metadata.gz: d5b407fd84acd1a1f162fe412f5d1ee611a0fdd095a2923b2a38ef2c63e366023a513fa26c09453fbbc1c9821c3681bb2838713dfdcf463dd8c767cdc5375b5c
7
- data.tar.gz: 81f890f4900c9dae7631073bb68b66941642456fc895a81a3a78620f7e719df506ead6dcf5fddc38ab746182f544054f243f111968839e99e512e1a89eb0fe4b
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
- - [Your First Email](#your-first-email)
34
- - [How Rendering Works](#how-rendering-works)
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
- ## Your First Email
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
- ## How Rendering Works
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 `kind:` and `component:`.
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). Email payloads include `kind`, `component`, and successful HTML size in `html_bytes`.
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. If [Editor rendering](docs/editor.md) is enabled, the bundle also includes document renderers.
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`. If [Editor rendering](docs/editor.md) is enabled, its peer dependencies must be available too.
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
@@ -2,7 +2,6 @@ module ReactEmailRails; end
2
2
  module ReactEmailRails::Generators; end
3
3
 
4
4
  module ReactEmailRails::Generators
5
- # Candidate Vite config filenames in precedence order; shared by both generators.
6
5
  VITE_CONFIG_FILES = [
7
6
  "vite.config.ts",
8
7
  "vite.config.mts",
@@ -1,7 +1,6 @@
1
1
  module ReactEmailRails::ActionMailer
2
2
  extend(ActiveSupport::Concern)
3
3
 
4
- # `react_email_share` kwargs reserved for `before_action` filtering, not prop data.
5
4
  SHARED_FILTER_OPTIONS = [:if, :unless, :only, :except].freeze
6
5
 
7
6
  prepended do
@@ -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
- # `label` names the render in error messages. `response` reads the reply as `:email`
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, :response)
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
- build_result(body)
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, warnings: warnings_from(body))
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 = 3
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
- # `warnings` are non-fatal (document nodes nothing rendered); empty for component renders.
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
 
@@ -1,3 +1,3 @@
1
1
  module ReactEmailRails
2
- VERSION = "0.6.1"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -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
- perform(payload:, label: component, kind: "email", component:)
47
- end
48
-
49
- # The document is sent verbatim (keys are structural); only context is key-transformed, like props.
50
- def compose(type:, document:, context: {}, preview: nil)
51
- payload = {
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(**metadata)
110
- ActiveSupport::Notifications.instrument("render.react-email-rails", **metadata) do |payload|
111
- yield.tap do |result|
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.6.1
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