react-email-rails 0.12.0 → 0.12.2
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 +37 -7
- data/lib/generators/react_email_rails/email_generator.rb +5 -6
- data/lib/react_email_rails/configuration.rb +14 -12
- data/lib/react_email_rails/renderer/child.rb +15 -2
- data/lib/react_email_rails/renderer.rb +6 -7
- data/lib/react_email_rails/tasks.rb +5 -7
- data/lib/react_email_rails/version.rb +1 -1
- 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: fd436260ad38b370f59dbd8cfad0b09697634624b7c56d0917eee90dc583dc07
|
|
4
|
+
data.tar.gz: 9e5f81391d9864d46903129e4f4fd2971bca156a83c933c79f8d5e520331b0d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a73846be5159e9787b96ea08813051293b75ae697d35c40a25b42cfad9fef66f5c4df2d36369e2c0ca1824b2abfe178a64d8fd39cef4a293d83355b0f5074a3e
|
|
7
|
+
data.tar.gz: af1228467e11038502e528619e3ca8de5d2b635b97e949fb617aeeb0ff7b1a9ae34ed49fc6024911244ca3ba210b45e90ee15087be0ff17d03694656c3cb8459
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.2
|
|
4
|
+
|
|
5
|
+
- The development renderer keeps its Vite cache in `node_modules/.vite/react-email-rails` and skips dependency discovery. It previously reset the app dev server's `node_modules/.vite/deps` on start, so lazily imported dependencies 504ed until that server restarted.
|
|
6
|
+
- Add `config.js_runtime` (default `"node"`) to choose the executable that runs the development renderer, the production bundle, the build task, and the generator's Vite config probe. The gem now runs the package's `bin/*.mjs` scripts directly instead of relying on `node_modules/.bin` shims and their shebangs.
|
|
7
|
+
|
|
8
|
+
## 0.12.1
|
|
9
|
+
|
|
10
|
+
- Export `CamelMailer` and `CamelMessage` TypeScript types, plus a `Register` module augmentation (`propKeys: "camel"`) so apps that camelize props can type the injected `mailer` / `message` shapes without a local mapped type. Default `Mailer` / `Message` stay snake_case.
|
|
11
|
+
- Timeouts that already received a partial response line no longer suggest a gem/npm version mismatch. The renderer drains stdout before deciding the process was silent.
|
|
12
|
+
|
|
3
13
|
## 0.12.0
|
|
4
14
|
|
|
5
15
|
- **Breaking:** Remove `config.render_mode`. There is one renderer: a long-lived Node child per Ruby process, speaking newline-delimited JSON. Delete `config.render_mode = :persistent` from initializers. Set `config.render_process_max_requests = 1` to recycle the child after every email.
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# react-email-rails
|
|
4
4
|
|
|
5
|
-
Build and send Action Mailer emails with [React Email](https://react.email), TypeScript, and Vite.
|
|
5
|
+
Build and send Action Mailer emails with [React Email](https://react.email), Tailwind, TypeScript, and Vite.
|
|
6
6
|
|
|
7
7
|
react-email-rails lets Rails render React Email components into HTML and plain text, then deliver them through the Action Mailer stack you already know: previews, headers, callbacks, queues, instrumentation, and delivery all keep working normally.
|
|
8
8
|
|
|
@@ -334,6 +334,8 @@ type WelcomeProps = {
|
|
|
334
334
|
}
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
+
The table shows the default snake_case keys. Keys follow `config.prop_transformer` just like your own props, so a camelize transformer yields `mailerName` / `replyTo` at runtime. Augment `Register` once so `Mailer` / `Message` match, or alias `CamelMailer` / `CamelMessage` — see [Prop Transformation](#prop-transformation).
|
|
338
|
+
|
|
337
339
|
| Prop | Example |
|
|
338
340
|
|------|---------|
|
|
339
341
|
| `mailer.mailer_name` | `"account_mailer"` |
|
|
@@ -343,9 +345,7 @@ type WelcomeProps = {
|
|
|
343
345
|
| `message.cc`, `message.bcc` | `["…"]` or `null` |
|
|
344
346
|
| `message.from`, `message.reply_to` | `["app@example.com"]` |
|
|
345
347
|
|
|
346
|
-
Context is merged before prop serialization, so
|
|
347
|
-
|
|
348
|
-
Per-mail and shared props win on conflict, so a prop named `mailer` or `message` overrides the injected context. When props come from a serializer, the context is merged in as long as `as_json` returns a hash; collections, arrays, and other non-object values pass through unchanged so their top-level shape is preserved.
|
|
348
|
+
Context is merged before prop serialization. Per-mail and shared props win on conflict, so a prop named `mailer` or `message` overrides the injected context. When props come from a serializer, the context is merged in as long as `as_json` returns a hash; collections, arrays, and other non-object values pass through unchanged so their top-level shape is preserved.
|
|
349
349
|
|
|
350
350
|
### Prop Serialization
|
|
351
351
|
|
|
@@ -427,6 +427,7 @@ end
|
|
|
427
427
|
| `component_path_resolver` | `->(mailer:, action:) { "#{mailer}/#{action}" }` |
|
|
428
428
|
| `prop_transformer` | `->(props:) { props }` |
|
|
429
429
|
| `render_options` | `{}` |
|
|
430
|
+
| `js_runtime` | `"node"` |
|
|
430
431
|
| `render_timeout` | `30` seconds in development, `10` seconds otherwise |
|
|
431
432
|
| `render_process_max_requests` | `1_000` |
|
|
432
433
|
| `on_render_error` | `nil` |
|
|
@@ -435,9 +436,7 @@ end
|
|
|
435
436
|
|
|
436
437
|
### Prop Transformation
|
|
437
438
|
|
|
438
|
-
Use `prop_transformer` to change props after `as_json` and before they reach React.
|
|
439
|
-
|
|
440
|
-
To work with `snake_case` in Ruby and `camelCase` in components:
|
|
439
|
+
Use `prop_transformer` to change props after `as_json` and before they reach React. To work with `snake_case` in Ruby and `camelCase` in components:
|
|
441
440
|
|
|
442
441
|
```ruby
|
|
443
442
|
ReactEmailRails.configure do |config|
|
|
@@ -447,6 +446,29 @@ ReactEmailRails.configure do |config|
|
|
|
447
446
|
end
|
|
448
447
|
```
|
|
449
448
|
|
|
449
|
+
The transformer is arbitrary, so TypeScript cannot infer it. The default `Mailer` and `Message` types stay snake_case (`mailer_name`, `reply_to`). App-authored props (`expiresMinutes`, and so on) stay hand-typed in the app.
|
|
450
|
+
|
|
451
|
+
If you camelize in Ruby, augment `Register` once so `import type { Mailer, Message }` matches runtime — the same "configure once" idea as the initializer:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
// e.g. react-email-rails.d.ts
|
|
455
|
+
import "react-email-rails"
|
|
456
|
+
|
|
457
|
+
declare module "react-email-rails" {
|
|
458
|
+
interface Register {
|
|
459
|
+
propKeys: "camel"
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
The `import "react-email-rails"` (or an `export {}`) makes the file a module so TypeScript *augments* the package. Without it, a script-style `.d.ts` replaces the module and you lose the rest of the package's types.
|
|
465
|
+
|
|
466
|
+
If you do not want a `.d.ts`, alias the camel types at the import. `CamelMailer` and `CamelMessage` are always camel, regardless of `Register`:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
import type { CamelMailer as Mailer, CamelMessage as Message } from "react-email-rails"
|
|
470
|
+
```
|
|
471
|
+
|
|
450
472
|
The transformer receives one hash at a time: the root props object, or each object in a top-level collection. Nested hashes inside a single object are left to the transformer — `deep_transform_keys` walks those.
|
|
451
473
|
|
|
452
474
|
### Render Process
|
|
@@ -461,6 +483,14 @@ ReactEmailRails.configure do |config|
|
|
|
461
483
|
end
|
|
462
484
|
```
|
|
463
485
|
|
|
486
|
+
`js_runtime` names the executable that runs the renderer, the build task, and the generator's Vite config probe. It defaults to `node` from `PATH`. Point it at an absolute path when Node lives behind a version manager the app process cannot see, or at another runtime such as `bun`. Only Node is tested in CI:
|
|
487
|
+
|
|
488
|
+
```ruby
|
|
489
|
+
ReactEmailRails.configure do |config|
|
|
490
|
+
config.js_runtime = "/usr/local/bin/node"
|
|
491
|
+
end
|
|
492
|
+
```
|
|
493
|
+
|
|
464
494
|
### Render Options
|
|
465
495
|
|
|
466
496
|
`render_options` is passed to [@react-email/render](https://react.email/docs/utilities/render). HTML is rendered once; plain text is derived from that markup with `toPlainText` (or `unstableToPlainText` when `unstable_text_conversion` is set). `html.pretty` is applied only to the HTML part, so it does not change the text body. `text.html_to_text_options` is forwarded to `toPlainText`. Option keys are camelized before they cross into JavaScript, so `html_to_text_options` becomes `htmlToTextOptions`.
|
|
@@ -8,7 +8,7 @@ module ReactEmailRails; end
|
|
|
8
8
|
module ReactEmailRails::Generators; end
|
|
9
9
|
|
|
10
10
|
class ReactEmailRails::Generators::EmailGenerator < Rails::Generators::NamedBase
|
|
11
|
-
|
|
11
|
+
CONFIG_SCRIPT = "node_modules/react-email-rails/bin/config.mjs"
|
|
12
12
|
|
|
13
13
|
source_root(File.expand_path("templates/email", __dir__))
|
|
14
14
|
|
|
@@ -120,7 +120,7 @@ class ReactEmailRails::Generators::EmailGenerator < Rails::Generators::NamedBase
|
|
|
120
120
|
command = vite_config_command
|
|
121
121
|
if command
|
|
122
122
|
stdout, _stderr, status = Timeout.timeout(10) do
|
|
123
|
-
Open3.capture3(command, chdir: destination_root)
|
|
123
|
+
Open3.capture3(*command, chdir: destination_root)
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
status.success? ? JSON.parse(stdout) : {}
|
|
@@ -133,10 +133,9 @@ class ReactEmailRails::Generators::EmailGenerator < Rails::Generators::NamedBase
|
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def vite_config_command
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
].find { |path| File.exist?(File.join(destination_root, path)) }
|
|
136
|
+
return unless File.exist?(File.join(destination_root, CONFIG_SCRIPT))
|
|
137
|
+
|
|
138
|
+
[ReactEmailRails.configuration.js_runtime, CONFIG_SCRIPT]
|
|
140
139
|
end
|
|
141
140
|
|
|
142
141
|
PLUGIN_OPENING = /reactEmailRails\s*\(\s*\{.*?/m
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
class ReactEmailRails::Configuration
|
|
2
2
|
BUNDLE_PATH = "tmp/react-email-rails/emails.js"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
BUILD_SCRIPT = "node_modules/react-email-rails/bin/build.mjs"
|
|
4
|
+
CONFIG_SCRIPT = "node_modules/react-email-rails/bin/config.mjs"
|
|
5
|
+
DEV_RENDER_SCRIPT = "node_modules/react-email-rails/bin/dev.mjs"
|
|
6
6
|
|
|
7
|
+
DEFAULT_JS_RUNTIME = "node"
|
|
7
8
|
DEFAULT_RENDER_TIMEOUT = 10
|
|
8
9
|
DEVELOPMENT_RENDER_TIMEOUT = 30
|
|
9
10
|
DEFAULT_RENDER_PROCESS_MAX_REQUESTS = 1_000
|
|
@@ -11,14 +12,6 @@ class ReactEmailRails::Configuration
|
|
|
11
12
|
|
|
12
13
|
DEFAULT_PROP_TRANSFORMER = ->(props:) { props }
|
|
13
14
|
|
|
14
|
-
DEFAULT_RENDER_COMMAND = lambda do
|
|
15
|
-
if Rails.env.development?
|
|
16
|
-
[Rails.root.join(DEV_RENDER_BIN).to_s]
|
|
17
|
-
else
|
|
18
|
-
["node", Rails.root.join(BUNDLE_PATH).to_s]
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
15
|
attr_accessor(
|
|
23
16
|
:component_path_resolver,
|
|
24
17
|
:render_options,
|
|
@@ -28,6 +21,7 @@ class ReactEmailRails::Configuration
|
|
|
28
21
|
)
|
|
29
22
|
|
|
30
23
|
attr_reader(
|
|
24
|
+
:js_runtime,
|
|
31
25
|
:prop_transformer,
|
|
32
26
|
:render_timeout,
|
|
33
27
|
:render_process_max_requests,
|
|
@@ -38,6 +32,7 @@ class ReactEmailRails::Configuration
|
|
|
38
32
|
new.tap do |config|
|
|
39
33
|
config.component_path_resolver = ->(mailer:, action:) { "#{mailer}/#{action}" }
|
|
40
34
|
config.render_options = {}
|
|
35
|
+
config.js_runtime = DEFAULT_JS_RUNTIME
|
|
41
36
|
config.render_timeout = Rails.env.development? ? DEVELOPMENT_RENDER_TIMEOUT : DEFAULT_RENDER_TIMEOUT
|
|
42
37
|
config.render_process_max_requests = DEFAULT_RENDER_PROCESS_MAX_REQUESTS
|
|
43
38
|
config.prop_transformer = DEFAULT_PROP_TRANSFORMER
|
|
@@ -62,6 +57,12 @@ class ReactEmailRails::Configuration
|
|
|
62
57
|
@prop_transformer = value
|
|
63
58
|
end
|
|
64
59
|
|
|
60
|
+
def js_runtime=(value)
|
|
61
|
+
raise(ArgumentError, "react-email-rails js_runtime must be a non-empty string") unless value.is_a?(String) && value.present?
|
|
62
|
+
|
|
63
|
+
@js_runtime = value
|
|
64
|
+
end
|
|
65
|
+
|
|
65
66
|
def render_timeout=(value)
|
|
66
67
|
raise(ArgumentError, "react-email-rails render_timeout must be positive") unless value.is_a?(Numeric) && value.positive?
|
|
67
68
|
|
|
@@ -92,7 +93,8 @@ class ReactEmailRails::Configuration
|
|
|
92
93
|
private
|
|
93
94
|
|
|
94
95
|
def resolved_render_command
|
|
95
|
-
|
|
96
|
+
script = Rails.env.development? ? DEV_RENDER_SCRIPT : BUNDLE_PATH
|
|
97
|
+
[js_runtime, Rails.root.join(script).to_s]
|
|
96
98
|
end
|
|
97
99
|
|
|
98
100
|
def serialize_props(props)
|
|
@@ -112,9 +112,16 @@ class ReactEmailRails::Renderer::Child
|
|
|
112
112
|
|
|
113
113
|
remaining = deadline - monotonic_time
|
|
114
114
|
if remaining <= 0 || IO.select([@stdout], nil, nil, remaining).nil?
|
|
115
|
-
|
|
115
|
+
drain_stdout_nonblock
|
|
116
|
+
if (buffered_line = consume_buffered_response_line)
|
|
117
|
+
line << buffered_line
|
|
118
|
+
return line
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
line << @stdout_buffer
|
|
122
|
+
@stdout_buffer.clear
|
|
116
123
|
stop
|
|
117
|
-
raise(Timeout::Error, unmatched_package_message) if
|
|
124
|
+
raise(Timeout::Error, unmatched_package_message) if line.empty?
|
|
118
125
|
|
|
119
126
|
raise(Timeout::Error)
|
|
120
127
|
end
|
|
@@ -134,6 +141,12 @@ class ReactEmailRails::Renderer::Child
|
|
|
134
141
|
"no response received; gem and npm package react-email-rails must both be #{ReactEmailRails::VERSION}"
|
|
135
142
|
end
|
|
136
143
|
|
|
144
|
+
def drain_stdout_nonblock
|
|
145
|
+
loop { @stdout_buffer << @stdout.read_nonblock(16 * 1024) }
|
|
146
|
+
rescue IO::WaitReadable, IOError, Errno::EAGAIN, Errno::EINTR
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
137
150
|
def consume_buffered_response_line
|
|
138
151
|
separator = @stdout_buffer.index("\n")
|
|
139
152
|
return unless separator
|
|
@@ -106,17 +106,16 @@ class ReactEmailRails::Renderer
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def validate_command!
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
script = command[1].to_s
|
|
110
|
+
return if File.file?(script)
|
|
111
111
|
|
|
112
|
-
if
|
|
113
|
-
raise(render_error("development renderer not found at #{
|
|
112
|
+
if script.end_with?(ReactEmailRails::Configuration::DEV_RENDER_SCRIPT)
|
|
113
|
+
raise(render_error("development renderer not found at #{script.inspect}; install JavaScript dependencies with npm, pnpm, yarn, or bun"))
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
return unless
|
|
117
|
-
return if File.file?(bundle_path)
|
|
116
|
+
return unless script.end_with?(ReactEmailRails::Configuration::BUNDLE_PATH)
|
|
118
117
|
|
|
119
|
-
raise(render_error("email bundle not found at #{
|
|
118
|
+
raise(render_error("email bundle not found at #{script.inspect}; run react-email-rails-build before rendering React emails"))
|
|
120
119
|
end
|
|
121
120
|
|
|
122
121
|
def validate_metadata!(body)
|
|
@@ -4,9 +4,9 @@ module ReactEmailRails::Tasks
|
|
|
4
4
|
class << self
|
|
5
5
|
def build
|
|
6
6
|
command = build_command
|
|
7
|
-
raise("react-email-rails build command not found at #{command.inspect}; run JavaScript package install first") unless File.exist?(command)
|
|
7
|
+
raise("react-email-rails build command not found at #{command.last.inspect}; run JavaScript package install first") unless File.exist?(command.last)
|
|
8
8
|
|
|
9
|
-
system(command, exception: true, chdir: Rails.root.to_s)
|
|
9
|
+
system(*command, exception: true, chdir: Rails.root.to_s)
|
|
10
10
|
raise("react-email-rails build completed, but the email bundle was not found at #{bundle_path.inspect}") unless File.file?(bundle_path)
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -24,12 +24,10 @@ module ReactEmailRails::Tasks
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
def build_command
|
|
27
|
-
|
|
28
|
-
ReactEmailRails
|
|
29
|
-
|
|
27
|
+
[
|
|
28
|
+
ReactEmailRails.configuration.js_runtime,
|
|
29
|
+
Rails.root.join(ReactEmailRails::Configuration::BUILD_SCRIPT).to_s,
|
|
30
30
|
]
|
|
31
|
-
candidates.map { |path| Rails.root.join(path).to_s }.find { |path| File.exist?(path) } ||
|
|
32
|
-
Rails.root.join(ReactEmailRails::Configuration::BUILD_BIN).to_s
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
def bundle_path
|