react-email-rails 0.11.1 → 0.12.1
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 +18 -0
- data/README.md +48 -41
- data/lib/react_email_rails/configuration.rb +23 -40
- data/lib/react_email_rails/render_protocol.rb +0 -4
- data/lib/react_email_rails/{render_modes/persistent/server.rb → renderer/child.rb} +61 -58
- data/lib/react_email_rails/{render_modes/subprocess.rb → renderer.rb} +64 -22
- data/lib/react_email_rails/version.rb +1 -1
- data/lib/react_email_rails.rb +9 -8
- metadata +3 -7
- data/lib/react_email_rails/render_modes/persistent/command_runner.rb +0 -40
- data/lib/react_email_rails/render_modes/persistent.rb +0 -26
- data/lib/react_email_rails/render_modes/subprocess/command_runner.rb +0 -56
- data/lib/react_email_rails/render_modes.rb +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 281158df961cccec48c34e03628715fda9a9c9cf16bbda4cae3097f380498a48
|
|
4
|
+
data.tar.gz: 68d882e14c9a5ed7281b5e1bb39a046e7ecb77314b25667cb27a289509aac319
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a679b532deae816d2337e38810077045b85a38d0e0e86089d516ba70d8e9060a06b4c9e9af5a8fa4a7aaa310f101c2817c0f246e559f97c1baaf829f19db91c
|
|
7
|
+
data.tar.gz: 7117f2c68aacaa3a4683d2272384a8ee5171d3684c3ad2a7b809bb833e7b85b4a19df738de66de3c45ce234d7e87a78537db3d1c8b24c733fcb25b26b8fa3d60
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
|
|
8
|
+
## 0.12.0
|
|
9
|
+
|
|
10
|
+
- **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.
|
|
11
|
+
- **Breaking:** Replace `config.transform_props` (`:camel`, `:lower_camel`, `:dash`, `:snake`, `:none`) with `config.prop_transformer`, matching [inertia-rails](https://inertia-rails.dev/guide/configuration#prop_transformer). The default is a no-op (`->(props:) { props }`), so keys stay as `as_json` produced them. Camelize in an initializer if you want the previous 0.11 behavior. The exported `Mailer` and `Message` TypeScript types now describe that default snake_case shape.
|
|
12
|
+
- Render HTML once and derive plain text with `toPlainText`, so `htmlToTextOptions` and `data-skip-in-text` apply to the same markup.
|
|
13
|
+
- Raise clearer errors when a component is missing (including a capped list of known names) or has no default export.
|
|
14
|
+
- Default `render_timeout` to 30 seconds in development so the first Vite-backed render can finish booting.
|
|
15
|
+
- Timeouts with no stdout mention matching gem and npm versions, so a gem 0.12 talking to an older package that waits for EOF is easier to diagnose.
|
|
16
|
+
- Recycle the Node child after invalid JSON so the next render can succeed.
|
|
17
|
+
- Include Node stderr when the child exits without a protocol line, instead of reporting only that it exited.
|
|
18
|
+
- Treat only `{ "health": true }` as a health check so a render payload that also sets `health` still renders.
|
|
19
|
+
- Apply `html.pretty` only to the HTML body so pretty-printing does not change plain text.
|
|
20
|
+
|
|
3
21
|
## 0.11.1
|
|
4
22
|
|
|
5
23
|
- Republish of 0.11.0 with no code changes. The 0.11.0 npm package was never published: npm provenance publishing requires GitHub-hosted runners, so the release workflow's publish job now runs on one. Do not use gem 0.11.0; it has no matching npm package.
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@ react-email-rails lets Rails render React Email components into HTML and plain t
|
|
|
8
8
|
|
|
9
9
|
## Why
|
|
10
10
|
|
|
11
|
-
HTML email is still awkward. React Email gives you a nicer component model, email-safe primitives, Tailwind support, and TypeScript. This gem connects that workflow to Rails without replacing Action Mailer: components render through Vite in development and a prebuilt renderer bundle in production,
|
|
11
|
+
HTML email is still awkward. React Email gives you a nicer component model, email-safe primitives, Tailwind support, and TypeScript. This gem connects that workflow to Rails without replacing Action Mailer: components render through Vite in development and a prebuilt renderer bundle in production, using a long-lived Node process per Ruby process.
|
|
12
12
|
|
|
13
13
|
## Status
|
|
14
14
|
|
|
@@ -159,11 +159,11 @@ React Email also provides primitives like [`<Button>`, `<Heading>`, `<Tailwind>`
|
|
|
159
159
|
|
|
160
160
|
## Rendering
|
|
161
161
|
|
|
162
|
-
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.
|
|
162
|
+
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. A long-lived Node child stays warm across previews and `deliver_now` calls so Vite is not cold-started for every email.
|
|
163
163
|
|
|
164
|
-
In production, `assets:precompile` builds a server-side renderer bundle from your Vite config. Rails
|
|
164
|
+
In production, `assets:precompile` builds a server-side renderer bundle from your Vite config. Rails keeps one Node child per Ruby process and renders through that bundle.
|
|
165
165
|
|
|
166
|
-
Every `react:` email renders HTML and plain text from
|
|
166
|
+
Every `react:` email renders HTML once and derives plain text from that markup. If rendering fails, the email is not sent and `ReactEmailRails::RenderError` is raised.
|
|
167
167
|
|
|
168
168
|
### Live-Reloading Previews
|
|
169
169
|
|
|
@@ -322,7 +322,7 @@ Set `config.deep_merge_shared_props = true` to make deep merging the default for
|
|
|
322
322
|
|
|
323
323
|
Every `react:` email receives `mailer` and `message` props, mirroring the [`mailer` and `message` view helpers](https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-view-helpers) available to Action Mailer ERB views.
|
|
324
324
|
|
|
325
|
-
`mailer` identifies the mailer action. `message` reflects the email after Action Mailer has assigned headers and defaults, including default `from` and `reply_to` values.
|
|
325
|
+
`mailer` identifies the mailer action. `message` reflects the email after Action Mailer has assigned headers and defaults, including default `from` and `reply_to` values. Import the `Mailer` and `Message` types to annotate them:
|
|
326
326
|
|
|
327
327
|
```tsx
|
|
328
328
|
import type { Mailer, Message } from "react-email-rails"
|
|
@@ -334,24 +334,24 @@ 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
|
-
| `mailer.
|
|
340
|
-
| `mailer.
|
|
341
|
+
| `mailer.mailer_name` | `"account_mailer"` |
|
|
342
|
+
| `mailer.action_name` | `"welcome"` |
|
|
341
343
|
| `message.subject` | `"Welcome"` |
|
|
342
344
|
| `message.to` | `["account@example.com"]` |
|
|
343
345
|
| `message.cc`, `message.bcc` | `["…"]` or `null` |
|
|
344
|
-
| `message.from`, `message.
|
|
345
|
-
|
|
346
|
-
Context is merged before prop serialization, so keys follow `config.transform_props` just like your own props. The exported TypeScript types describe the default `:lower_camel` shape.
|
|
346
|
+
| `message.from`, `message.reply_to` | `["app@example.com"]` |
|
|
347
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
|
|
|
352
352
|
Props are serialized with `as_json`, just like `render json:`. You can pass hashes, arrays, Active Model objects, and serializer output from libraries such as [Alba](https://github.com/okuramasafumi/alba) or [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
|
|
353
353
|
|
|
354
|
-
|
|
354
|
+
Keys are left as serialized by default, so `plan_name` arrives in React as `plan_name`. See [Prop Transformation](#prop-transformation) to camelize them.
|
|
355
355
|
|
|
356
356
|
### Component Files
|
|
357
357
|
|
|
@@ -409,7 +409,7 @@ export default function Welcome() {
|
|
|
409
409
|
|
|
410
410
|
Configuration lives in two places:
|
|
411
411
|
|
|
412
|
-
- Rails configuration controls mailer behavior, prop handling,
|
|
412
|
+
- Rails configuration controls mailer behavior, prop handling, timeouts, and error hooks.
|
|
413
413
|
- Vite configuration controls email component discovery and the renderer bundle.
|
|
414
414
|
|
|
415
415
|
### Rails Configuration
|
|
@@ -418,17 +418,16 @@ Override Rails-side defaults in `config/initializers/react_email_rails.rb`:
|
|
|
418
418
|
|
|
419
419
|
```ruby
|
|
420
420
|
ReactEmailRails.configure do |config|
|
|
421
|
-
# config.
|
|
421
|
+
# config.render_timeout = 10
|
|
422
422
|
end
|
|
423
423
|
```
|
|
424
424
|
|
|
425
425
|
| Option | Default |
|
|
426
426
|
|--------|---------|
|
|
427
427
|
| `component_path_resolver` | `->(mailer:, action:) { "#{mailer}/#{action}" }` |
|
|
428
|
-
| `
|
|
429
|
-
| `render_mode` | `:subprocess` |
|
|
428
|
+
| `prop_transformer` | `->(props:) { props }` |
|
|
430
429
|
| `render_options` | `{}` |
|
|
431
|
-
| `render_timeout` | `10` seconds |
|
|
430
|
+
| `render_timeout` | `30` seconds in development, `10` seconds otherwise |
|
|
432
431
|
| `render_process_max_requests` | `1_000` |
|
|
433
432
|
| `on_render_error` | `nil` |
|
|
434
433
|
| `deep_merge_shared_props` | `false` |
|
|
@@ -436,48 +435,56 @@ end
|
|
|
436
435
|
|
|
437
436
|
### Prop Transformation
|
|
438
437
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
| Value | Example |
|
|
442
|
-
|-------|---------|
|
|
443
|
-
| `:camel` | `AccountName` |
|
|
444
|
-
| `:lower_camel` | `accountName` |
|
|
445
|
-
| `:dash` | `account-name` |
|
|
446
|
-
| `:snake` | `account_name` |
|
|
447
|
-
| `:none` | preserves serialized keys |
|
|
438
|
+
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:
|
|
448
439
|
|
|
449
440
|
```ruby
|
|
450
441
|
ReactEmailRails.configure do |config|
|
|
451
|
-
config.
|
|
442
|
+
config.prop_transformer = lambda do |props:|
|
|
443
|
+
props.deep_transform_keys { |key| key.to_s.camelize(:lower) }
|
|
444
|
+
end
|
|
452
445
|
end
|
|
453
446
|
```
|
|
454
447
|
|
|
455
|
-
|
|
448
|
+
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.
|
|
449
|
+
|
|
450
|
+
If you camelize in Ruby, augment `Register` once so `import type { Mailer, Message }` matches runtime — the same "configure once" idea as the initializer:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
// e.g. react-email-rails.d.ts
|
|
454
|
+
import "react-email-rails"
|
|
455
|
+
|
|
456
|
+
declare module "react-email-rails" {
|
|
457
|
+
interface Register {
|
|
458
|
+
propKeys: "camel"
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
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.
|
|
456
464
|
|
|
457
|
-
|
|
465
|
+
If you do not want a `.d.ts`, alias the camel types at the import. `CamelMailer` and `CamelMessage` are always camel, regardless of `Register`:
|
|
458
466
|
|
|
459
|
-
|
|
467
|
+
```ts
|
|
468
|
+
import type { CamelMailer as Mailer, CamelMessage as Message } from "react-email-rails"
|
|
469
|
+
```
|
|
460
470
|
|
|
461
|
-
|
|
471
|
+
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.
|
|
462
472
|
|
|
463
|
-
|
|
473
|
+
### Render Process
|
|
474
|
+
|
|
475
|
+
Each Ruby process keeps one long-lived Node child. Renders are newline-delimited JSON and processed one at a time. Throughput scales with more worker processes. The child is fork-safe: clustered Puma and forking job runners spawn one Node child per worker.
|
|
476
|
+
|
|
477
|
+
The child recycles after `render_process_max_requests` renders so a leaked or stale renderer cannot live forever. Set that option to `nil` to disable recycling, or to `1` to start a fresh Node process for every email:
|
|
464
478
|
|
|
465
479
|
```ruby
|
|
466
480
|
ReactEmailRails.configure do |config|
|
|
467
|
-
config.
|
|
481
|
+
config.render_process_max_requests = 1
|
|
468
482
|
end
|
|
469
483
|
```
|
|
470
484
|
|
|
471
|
-
Persistent mode details:
|
|
472
|
-
|
|
473
|
-
- Renders are newline-delimited JSON and processed one at a time.
|
|
474
|
-
- Throughput scales with more worker processes.
|
|
475
|
-
- It is fork-safe; clustered Puma and forking job runners spawn one Node child per worker.
|
|
476
|
-
- The child recycles after `render_process_max_requests` renders. Set that option to `nil` to disable recycling.
|
|
477
|
-
|
|
478
485
|
### Render Options
|
|
479
486
|
|
|
480
|
-
`render_options` is passed to [@react-email/render](https://react.email/docs/utilities/render).
|
|
487
|
+
`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`.
|
|
481
488
|
|
|
482
489
|
```ruby
|
|
483
490
|
ReactEmailRails.configure do |config|
|
|
@@ -5,21 +5,11 @@ class ReactEmailRails::Configuration
|
|
|
5
5
|
DEV_RENDER_BIN = "node_modules/.bin/react-email-rails-dev"
|
|
6
6
|
|
|
7
7
|
DEFAULT_RENDER_TIMEOUT = 10
|
|
8
|
+
DEVELOPMENT_RENDER_TIMEOUT = 30
|
|
8
9
|
DEFAULT_RENDER_PROCESS_MAX_REQUESTS = 1_000
|
|
9
10
|
DEFAULT_LIVE_RELOAD_URL = "http://localhost:5173"
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
subprocess: ReactEmailRails::RenderModes::Subprocess,
|
|
13
|
-
persistent: ReactEmailRails::RenderModes::Persistent,
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
|
-
KEY_TRANSFORMS = {
|
|
17
|
-
camel: ->(key) { key.to_s.camelize },
|
|
18
|
-
lower_camel: ->(key) { key.to_s.camelize(:lower) },
|
|
19
|
-
dash: ->(key) { key.to_s.underscore.dasherize },
|
|
20
|
-
snake: ->(key) { key.to_s.underscore },
|
|
21
|
-
none: ->(key) { key },
|
|
22
|
-
}.freeze
|
|
12
|
+
DEFAULT_PROP_TRANSFORMER = ->(props:) { props }
|
|
23
13
|
|
|
24
14
|
DEFAULT_RENDER_COMMAND = lambda do
|
|
25
15
|
if Rails.env.development?
|
|
@@ -32,14 +22,13 @@ class ReactEmailRails::Configuration
|
|
|
32
22
|
attr_accessor(
|
|
33
23
|
:component_path_resolver,
|
|
34
24
|
:render_options,
|
|
35
|
-
:transform_props,
|
|
36
25
|
:on_render_error,
|
|
37
26
|
:deep_merge_shared_props,
|
|
38
27
|
:live_reload_url,
|
|
39
28
|
)
|
|
40
29
|
|
|
41
30
|
attr_reader(
|
|
42
|
-
:
|
|
31
|
+
:prop_transformer,
|
|
43
32
|
:render_timeout,
|
|
44
33
|
:render_process_max_requests,
|
|
45
34
|
)
|
|
@@ -48,11 +37,10 @@ class ReactEmailRails::Configuration
|
|
|
48
37
|
def default
|
|
49
38
|
new.tap do |config|
|
|
50
39
|
config.component_path_resolver = ->(mailer:, action:) { "#{mailer}/#{action}" }
|
|
51
|
-
config.render_mode = :subprocess
|
|
52
40
|
config.render_options = {}
|
|
53
|
-
config.render_timeout = DEFAULT_RENDER_TIMEOUT
|
|
41
|
+
config.render_timeout = Rails.env.development? ? DEVELOPMENT_RENDER_TIMEOUT : DEFAULT_RENDER_TIMEOUT
|
|
54
42
|
config.render_process_max_requests = DEFAULT_RENDER_PROCESS_MAX_REQUESTS
|
|
55
|
-
config.
|
|
43
|
+
config.prop_transformer = DEFAULT_PROP_TRANSFORMER
|
|
56
44
|
config.on_render_error = nil
|
|
57
45
|
config.deep_merge_shared_props = false
|
|
58
46
|
config.live_reload_url = DEFAULT_LIVE_RELOAD_URL
|
|
@@ -66,12 +54,12 @@ class ReactEmailRails::Configuration
|
|
|
66
54
|
live_reload_url.to_s.chomp("/")
|
|
67
55
|
end
|
|
68
56
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
raise(ArgumentError, "
|
|
57
|
+
def prop_transformer=(value)
|
|
58
|
+
unless value.respond_to?(:call)
|
|
59
|
+
raise(ArgumentError, "react-email-rails prop_transformer must be callable")
|
|
72
60
|
end
|
|
73
61
|
|
|
74
|
-
@
|
|
62
|
+
@prop_transformer = value
|
|
75
63
|
end
|
|
76
64
|
|
|
77
65
|
def render_timeout=(value)
|
|
@@ -88,14 +76,6 @@ class ReactEmailRails::Configuration
|
|
|
88
76
|
@render_process_max_requests = value
|
|
89
77
|
end
|
|
90
78
|
|
|
91
|
-
def resolved_render_mode
|
|
92
|
-
return render_mode unless render_mode.is_a?(Symbol) || render_mode.is_a?(String)
|
|
93
|
-
|
|
94
|
-
RENDER_MODES.fetch(render_mode.to_sym) do
|
|
95
|
-
raise(ArgumentError, "Unknown react-email-rails render mode: #{render_mode.inspect}")
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
79
|
def resolve_render_options(context = nil)
|
|
100
80
|
value =
|
|
101
81
|
if render_options.respond_to?(:call) && context
|
|
@@ -106,7 +86,7 @@ class ReactEmailRails::Configuration
|
|
|
106
86
|
render_options
|
|
107
87
|
end
|
|
108
88
|
|
|
109
|
-
|
|
89
|
+
camelize_render_option_keys(value.as_json)
|
|
110
90
|
end
|
|
111
91
|
|
|
112
92
|
private
|
|
@@ -116,23 +96,26 @@ class ReactEmailRails::Configuration
|
|
|
116
96
|
end
|
|
117
97
|
|
|
118
98
|
def serialize_props(props)
|
|
119
|
-
|
|
99
|
+
transform_serialized_props(props.as_json)
|
|
120
100
|
end
|
|
121
101
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
102
|
+
def transform_serialized_props(value)
|
|
103
|
+
case value
|
|
104
|
+
when Hash
|
|
105
|
+
prop_transformer.call(props: value)
|
|
106
|
+
when Array
|
|
107
|
+
value.map { |item| transform_serialized_props(item) }
|
|
108
|
+
else
|
|
109
|
+
value
|
|
127
110
|
end
|
|
128
111
|
end
|
|
129
112
|
|
|
130
|
-
def
|
|
113
|
+
def camelize_render_option_keys(value)
|
|
131
114
|
case value
|
|
132
|
-
when Array
|
|
133
|
-
value.map { |item| deep_transform_keys(item, transform) }
|
|
134
115
|
when Hash
|
|
135
|
-
value.
|
|
116
|
+
value.deep_transform_keys { |key| key.to_s.camelize(:lower) }
|
|
117
|
+
when Array
|
|
118
|
+
value.map { |item| camelize_render_option_keys(item) }
|
|
136
119
|
else
|
|
137
120
|
value
|
|
138
121
|
end
|
|
@@ -6,10 +6,6 @@ module ReactEmailRails
|
|
|
6
6
|
module RenderProtocol
|
|
7
7
|
extend(self)
|
|
8
8
|
|
|
9
|
-
def healthy_result?(result)
|
|
10
|
-
result.status.success? && compatible_response?(JSON.parse(result.stdout))
|
|
11
|
-
end
|
|
12
|
-
|
|
13
9
|
def compatible_response?(body)
|
|
14
10
|
body["ok"] == true && compatible_metadata?(body)
|
|
15
11
|
end
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
class ReactEmailRails::
|
|
1
|
+
class ReactEmailRails::Renderer::Child
|
|
2
2
|
STDERR_LIMIT = 8 * 1024
|
|
3
3
|
|
|
4
|
-
Status = Data.define(:success) do
|
|
5
|
-
def success? = success
|
|
6
|
-
end
|
|
7
|
-
|
|
8
4
|
def initialize(command)
|
|
9
5
|
@command = command
|
|
10
6
|
@mutex = Mutex.new
|
|
@@ -14,17 +10,35 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
14
10
|
@requests = 0
|
|
15
11
|
end
|
|
16
12
|
|
|
17
|
-
def
|
|
13
|
+
def exchange(input, timeout:, max_requests:)
|
|
18
14
|
with_retry_on_broken_pipe do
|
|
19
|
-
|
|
15
|
+
request(input, timeout:).tap { recycle_if_needed(max_requests) }
|
|
20
16
|
end
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
def health_check(timeout:)
|
|
24
|
-
with_retry_on_broken_pipe {
|
|
20
|
+
with_retry_on_broken_pipe { request(JSON.generate(health: true), timeout:) }
|
|
25
21
|
end
|
|
26
22
|
|
|
27
23
|
def stop
|
|
24
|
+
if @mutex.owned?
|
|
25
|
+
stop_unlocked
|
|
26
|
+
else
|
|
27
|
+
@mutex.synchronize { stop_unlocked }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def abandon
|
|
32
|
+
release_io
|
|
33
|
+
rescue IOError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader(:command)
|
|
40
|
+
|
|
41
|
+
def stop_unlocked
|
|
28
42
|
if @wait_thread&.alive?
|
|
29
43
|
terminate_process("TERM", @wait_thread.pid)
|
|
30
44
|
@wait_thread.join(1)
|
|
@@ -37,14 +51,6 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
37
51
|
release_io
|
|
38
52
|
end
|
|
39
53
|
|
|
40
|
-
def abandon
|
|
41
|
-
release_io
|
|
42
|
-
rescue IOError
|
|
43
|
-
nil
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
54
|
def release_io
|
|
49
55
|
[@stdin, @stdout, @stderr].compact.each { |io| io.close unless io.closed? }
|
|
50
56
|
@stdin = @stdout = @stderr = @wait_thread = @stderr_reader = nil
|
|
@@ -58,38 +64,13 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
58
64
|
begin
|
|
59
65
|
@mutex.synchronize(&block)
|
|
60
66
|
rescue Errno::EPIPE, IOError
|
|
61
|
-
|
|
67
|
+
failed("render process exited before responding")
|
|
62
68
|
end
|
|
63
69
|
end
|
|
64
70
|
|
|
65
|
-
attr_reader(:command)
|
|
66
|
-
|
|
67
|
-
def capture_once(input:, timeout:)
|
|
68
|
-
response = request(input, timeout:)
|
|
69
|
-
return failure(response["error"].to_s.presence || "render process failed") unless response["ok"]
|
|
70
|
-
|
|
71
|
-
success(JSON.generate(
|
|
72
|
-
{
|
|
73
|
-
protocolVersion: response["protocolVersion"],
|
|
74
|
-
packageVersion: response["packageVersion"],
|
|
75
|
-
}.tap do |body|
|
|
76
|
-
body[:html] = response["html"] if response.key?("html")
|
|
77
|
-
body[:text] = response["text"] if response.key?("text")
|
|
78
|
-
end,
|
|
79
|
-
))
|
|
80
|
-
rescue JSON::ParserError => e
|
|
81
|
-
failure("render process returned invalid JSON: #{e.message}")
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def health_check_once(timeout:)
|
|
85
|
-
response = request(JSON.generate(health: true), timeout:)
|
|
86
|
-
response["ok"] ? success(JSON.generate(response)) : failure(response["error"].to_s.presence || "render process failed")
|
|
87
|
-
rescue JSON::ParserError => e
|
|
88
|
-
failure("render process returned invalid JSON: #{e.message}")
|
|
89
|
-
end
|
|
90
|
-
|
|
91
71
|
def start
|
|
92
|
-
|
|
72
|
+
release_io
|
|
73
|
+
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(*command, pgroup: true)
|
|
93
74
|
@stderr_buffer = +""
|
|
94
75
|
@stdout_buffer = +""
|
|
95
76
|
@requests = 0
|
|
@@ -108,9 +89,12 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
108
89
|
@stdin.flush
|
|
109
90
|
|
|
110
91
|
line = read_response_line(timeout)
|
|
111
|
-
return
|
|
92
|
+
return failed("render process exited before responding") unless line
|
|
112
93
|
|
|
113
94
|
JSON.parse(line)
|
|
95
|
+
rescue JSON::ParserError => e
|
|
96
|
+
stop
|
|
97
|
+
failed("render process returned invalid JSON: #{e.message}")
|
|
114
98
|
end
|
|
115
99
|
|
|
116
100
|
def read_response_line(timeout)
|
|
@@ -128,7 +112,17 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
128
112
|
|
|
129
113
|
remaining = deadline - monotonic_time
|
|
130
114
|
if remaining <= 0 || IO.select([@stdout], nil, nil, remaining).nil?
|
|
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
|
|
131
123
|
stop
|
|
124
|
+
raise(Timeout::Error, unmatched_package_message) if line.empty?
|
|
125
|
+
|
|
132
126
|
raise(Timeout::Error)
|
|
133
127
|
end
|
|
134
128
|
|
|
@@ -139,9 +133,20 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
139
133
|
end
|
|
140
134
|
end
|
|
141
135
|
rescue EOFError
|
|
136
|
+
wait_for_stderr
|
|
142
137
|
line.presence
|
|
143
138
|
end
|
|
144
139
|
|
|
140
|
+
def unmatched_package_message
|
|
141
|
+
"no response received; gem and npm package react-email-rails must both be #{ReactEmailRails::VERSION}"
|
|
142
|
+
end
|
|
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
|
+
|
|
145
150
|
def consume_buffered_response_line
|
|
146
151
|
separator = @stdout_buffer.index("\n")
|
|
147
152
|
return unless separator
|
|
@@ -160,7 +165,8 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
160
165
|
end
|
|
161
166
|
|
|
162
167
|
def drain_stderr
|
|
163
|
-
|
|
168
|
+
loop do
|
|
169
|
+
chunk = @stderr.readpartial(4096)
|
|
164
170
|
@stderr_mutex.synchronize do
|
|
165
171
|
@stderr_buffer << chunk
|
|
166
172
|
@stderr_buffer = @stderr_buffer.byteslice(-STDERR_LIMIT, STDERR_LIMIT) if @stderr_buffer.bytesize > STDERR_LIMIT
|
|
@@ -170,20 +176,17 @@ class ReactEmailRails::RenderModes::Persistent::Server
|
|
|
170
176
|
nil
|
|
171
177
|
end
|
|
172
178
|
|
|
173
|
-
def
|
|
174
|
-
|
|
175
|
-
stdout:,
|
|
176
|
-
stderr: "",
|
|
177
|
-
status: Status.new(true),
|
|
178
|
-
)
|
|
179
|
+
def wait_for_stderr
|
|
180
|
+
@stderr_reader&.join(1)
|
|
179
181
|
end
|
|
180
182
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
def failed(message)
|
|
184
|
+
wait_for_stderr unless @wait_thread&.alive?
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
"ok" => false,
|
|
188
|
+
"error" => [message, stderr_buffer].reject(&:blank?).join("\n"),
|
|
189
|
+
}
|
|
187
190
|
end
|
|
188
191
|
|
|
189
192
|
def stderr_buffer
|
|
@@ -1,50 +1,86 @@
|
|
|
1
|
-
class ReactEmailRails::
|
|
1
|
+
class ReactEmailRails::Renderer
|
|
2
2
|
class << self
|
|
3
3
|
def healthy?(command:, timeout:)
|
|
4
|
-
|
|
5
|
-
ReactEmailRails::RenderProtocol.
|
|
4
|
+
body = child_for(command).health_check(timeout:)
|
|
5
|
+
ReactEmailRails::RenderProtocol.compatible_response?(body)
|
|
6
6
|
rescue StandardError
|
|
7
7
|
false
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
def stop_all
|
|
11
|
+
@mutex.synchronize do
|
|
12
|
+
@children&.each_value(&:stop)
|
|
13
|
+
@children&.clear
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def exchange(command, input, timeout:, max_requests:)
|
|
20
|
+
child_for(command).exchange(input, timeout:, max_requests:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def child_for(command)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
reset_after_fork
|
|
26
|
+
@children[command.map(&:to_s)] ||= Child.new(command)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset_after_fork
|
|
31
|
+
return if @owner_pid == Process.pid && @children
|
|
32
|
+
|
|
33
|
+
@children&.each_value(&:abandon)
|
|
34
|
+
@children = {}
|
|
35
|
+
@owner_pid = Process.pid
|
|
36
|
+
end
|
|
9
37
|
end
|
|
10
38
|
|
|
39
|
+
@mutex = Mutex.new
|
|
40
|
+
|
|
11
41
|
def initialize(payload:, label:)
|
|
12
42
|
@payload = payload
|
|
13
43
|
@label = label
|
|
14
44
|
end
|
|
15
45
|
|
|
16
46
|
def render
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
body = exchange
|
|
48
|
+
raise(render_error(error_message(body["error"]))) unless body["ok"]
|
|
19
49
|
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
attr_reader(:payload, :label)
|
|
23
|
-
|
|
24
|
-
def run
|
|
25
|
-
result = capture(payload_json)
|
|
26
|
-
raise(render_error(error_message(result.stderr, result.status))) unless result.status.success?
|
|
27
|
-
|
|
28
|
-
body = JSON.parse(result.stdout)
|
|
29
50
|
validate_metadata!(body)
|
|
30
51
|
build_rendered_email(body)
|
|
31
|
-
rescue JSON::ParserError => e
|
|
32
|
-
raise(render_error("render process returned invalid JSON: #{e.message}"))
|
|
33
52
|
rescue KeyError => e
|
|
34
53
|
raise(render_error("render process returned an invalid response: missing #{e.key.inspect}"))
|
|
35
54
|
end
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader(:payload, :label)
|
|
59
|
+
|
|
60
|
+
def exchange
|
|
38
61
|
with_capture_rescues do
|
|
39
62
|
validate_command!
|
|
40
|
-
|
|
63
|
+
self.class.send(
|
|
64
|
+
:exchange,
|
|
65
|
+
command,
|
|
66
|
+
payload_json,
|
|
67
|
+
timeout: render_timeout,
|
|
68
|
+
max_requests: render_process_max_requests,
|
|
69
|
+
)
|
|
41
70
|
end
|
|
42
71
|
end
|
|
43
72
|
|
|
44
73
|
def with_capture_rescues
|
|
45
74
|
yield
|
|
46
|
-
rescue Timeout::Error
|
|
47
|
-
|
|
75
|
+
rescue Timeout::Error => e
|
|
76
|
+
detail = e.message
|
|
77
|
+
suffix =
|
|
78
|
+
if detail.present? && detail != "Timeout::Error" && detail != "execution expired"
|
|
79
|
+
" (#{detail})"
|
|
80
|
+
else
|
|
81
|
+
""
|
|
82
|
+
end
|
|
83
|
+
raise(render_error("render process timed out after #{render_timeout}s#{suffix}"))
|
|
48
84
|
rescue Errno::ENOENT
|
|
49
85
|
raise(render_error("render command not found: #{command.inspect}"))
|
|
50
86
|
end
|
|
@@ -57,12 +93,16 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
57
93
|
ReactEmailRails.configuration.render_timeout
|
|
58
94
|
end
|
|
59
95
|
|
|
96
|
+
def render_process_max_requests
|
|
97
|
+
ReactEmailRails.configuration.render_process_max_requests
|
|
98
|
+
end
|
|
99
|
+
|
|
60
100
|
def payload_json
|
|
61
101
|
@payload_json ||= JSON.generate(payload)
|
|
62
102
|
end
|
|
63
103
|
|
|
64
|
-
def error_message(
|
|
65
|
-
|
|
104
|
+
def error_message(message)
|
|
105
|
+
message.to_s.strip.presence || "render process failed"
|
|
66
106
|
end
|
|
67
107
|
|
|
68
108
|
def validate_command!
|
|
@@ -97,3 +137,5 @@ class ReactEmailRails::RenderModes::Subprocess
|
|
|
97
137
|
ReactEmailRails::RenderError.new("React Email render failed for #{label}: #{message}")
|
|
98
138
|
end
|
|
99
139
|
end
|
|
140
|
+
|
|
141
|
+
at_exit { ReactEmailRails::Renderer.stop_all }
|
data/lib/react_email_rails.rb
CHANGED
|
@@ -6,6 +6,7 @@ require("active_support/notifications")
|
|
|
6
6
|
require("active_support/core_ext/object/blank")
|
|
7
7
|
require("active_support/core_ext/object/json")
|
|
8
8
|
require("active_support/core_ext/hash/deep_merge")
|
|
9
|
+
require("active_support/core_ext/hash/keys")
|
|
9
10
|
require("active_support/inflector")
|
|
10
11
|
require("rails/railtie")
|
|
11
12
|
|
|
@@ -16,12 +17,8 @@ require_relative("react_email_rails/render_protocol")
|
|
|
16
17
|
require_relative("react_email_rails/action_mailer")
|
|
17
18
|
require_relative("react_email_rails/render_error")
|
|
18
19
|
require_relative("react_email_rails/rendered_email")
|
|
19
|
-
require_relative("react_email_rails/
|
|
20
|
-
require_relative("react_email_rails/
|
|
21
|
-
require_relative("react_email_rails/render_modes/subprocess/command_runner")
|
|
22
|
-
require_relative("react_email_rails/render_modes/persistent")
|
|
23
|
-
require_relative("react_email_rails/render_modes/persistent/server")
|
|
24
|
-
require_relative("react_email_rails/render_modes/persistent/command_runner")
|
|
20
|
+
require_relative("react_email_rails/renderer")
|
|
21
|
+
require_relative("react_email_rails/renderer/child")
|
|
25
22
|
require_relative("react_email_rails/configuration")
|
|
26
23
|
require_relative("react_email_rails/tasks")
|
|
27
24
|
require_relative("react_email_rails/props_resolver")
|
|
@@ -45,7 +42,7 @@ module ReactEmailRails
|
|
|
45
42
|
payload[:renderOptions] = render_options if render_options.present?
|
|
46
43
|
|
|
47
44
|
instrument(component:) do
|
|
48
|
-
|
|
45
|
+
renderer_class.new(payload:, label: component).render
|
|
49
46
|
end
|
|
50
47
|
rescue ReactEmailRails::RenderError => e
|
|
51
48
|
configuration.on_render_error&.call(e, component:)
|
|
@@ -53,7 +50,7 @@ module ReactEmailRails
|
|
|
53
50
|
end
|
|
54
51
|
|
|
55
52
|
def healthy?
|
|
56
|
-
|
|
53
|
+
Renderer.healthy?(
|
|
57
54
|
command: configuration.send(:resolved_render_command),
|
|
58
55
|
timeout: configuration.render_timeout,
|
|
59
56
|
)
|
|
@@ -63,6 +60,10 @@ module ReactEmailRails
|
|
|
63
60
|
|
|
64
61
|
private
|
|
65
62
|
|
|
63
|
+
def renderer_class
|
|
64
|
+
Renderer
|
|
65
|
+
end
|
|
66
|
+
|
|
66
67
|
def serialized_props(value)
|
|
67
68
|
configuration.send(:serialize_props, value)
|
|
68
69
|
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.12.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Supertape
|
|
@@ -158,14 +158,10 @@ files:
|
|
|
158
158
|
- lib/react_email_rails/props_resolver.rb
|
|
159
159
|
- lib/react_email_rails/railtie.rb
|
|
160
160
|
- lib/react_email_rails/render_error.rb
|
|
161
|
-
- lib/react_email_rails/render_modes.rb
|
|
162
|
-
- lib/react_email_rails/render_modes/persistent.rb
|
|
163
|
-
- lib/react_email_rails/render_modes/persistent/command_runner.rb
|
|
164
|
-
- lib/react_email_rails/render_modes/persistent/server.rb
|
|
165
|
-
- lib/react_email_rails/render_modes/subprocess.rb
|
|
166
|
-
- lib/react_email_rails/render_modes/subprocess/command_runner.rb
|
|
167
161
|
- lib/react_email_rails/render_protocol.rb
|
|
168
162
|
- lib/react_email_rails/rendered_email.rb
|
|
163
|
+
- lib/react_email_rails/renderer.rb
|
|
164
|
+
- lib/react_email_rails/renderer/child.rb
|
|
169
165
|
- lib/react_email_rails/shared_props.rb
|
|
170
166
|
- lib/react_email_rails/tasks.rb
|
|
171
167
|
- lib/react_email_rails/version.rb
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
class ReactEmailRails::RenderModes::Persistent::CommandRunner
|
|
2
|
-
@mutex = Mutex.new
|
|
3
|
-
|
|
4
|
-
class << self
|
|
5
|
-
def capture(command, input:, timeout:, max_requests: nil)
|
|
6
|
-
server_for(command).capture(input:, timeout:, max_requests:)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def healthy?(command, timeout:)
|
|
10
|
-
result = server_for(command).health_check(timeout:)
|
|
11
|
-
ReactEmailRails::RenderProtocol.healthy_result?(result)
|
|
12
|
-
rescue StandardError
|
|
13
|
-
false
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def stop_all
|
|
17
|
-
@mutex.synchronize do
|
|
18
|
-
@servers&.each_value(&:stop)
|
|
19
|
-
@servers&.clear
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def server_for(command)
|
|
26
|
-
@mutex.synchronize do
|
|
27
|
-
reset_after_fork
|
|
28
|
-
@servers[command.map(&:to_s)] ||= ReactEmailRails::RenderModes::Persistent::Server.new(command)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def reset_after_fork
|
|
33
|
-
return if @owner_pid == Process.pid && @servers
|
|
34
|
-
|
|
35
|
-
@servers&.each_value(&:abandon)
|
|
36
|
-
@servers = {}
|
|
37
|
-
@owner_pid = Process.pid
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
class ReactEmailRails::RenderModes::Persistent < ReactEmailRails::RenderModes::Subprocess
|
|
2
|
-
class << self
|
|
3
|
-
def healthy?(command:, timeout:)
|
|
4
|
-
CommandRunner.healthy?(command, timeout:)
|
|
5
|
-
end
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
def capture(input)
|
|
11
|
-
with_capture_rescues do
|
|
12
|
-
CommandRunner.capture(
|
|
13
|
-
command,
|
|
14
|
-
input:,
|
|
15
|
-
timeout: render_timeout,
|
|
16
|
-
max_requests: render_process_max_requests,
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def render_process_max_requests
|
|
22
|
-
ReactEmailRails.configuration.render_process_max_requests
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
at_exit { ReactEmailRails::RenderModes::Persistent::CommandRunner.stop_all }
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
class ReactEmailRails::RenderModes::Subprocess::CommandRunner
|
|
2
|
-
Result = Data.define(:stdout, :stderr, :status)
|
|
3
|
-
|
|
4
|
-
class << self
|
|
5
|
-
def capture(command, input: nil, timeout:)
|
|
6
|
-
new(command:, input:, timeout:).capture
|
|
7
|
-
end
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def initialize(command:, input:, timeout:)
|
|
11
|
-
@command = command
|
|
12
|
-
@input = input
|
|
13
|
-
@timeout = timeout
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def capture
|
|
17
|
-
Open3.popen3(*command, pgroup: true) do |stdin, stdout, stderr, wait_thread|
|
|
18
|
-
out_reader = read_async(stdout)
|
|
19
|
-
err_reader = read_async(stderr)
|
|
20
|
-
|
|
21
|
-
write_input(stdin)
|
|
22
|
-
|
|
23
|
-
if wait_thread.join(timeout).nil?
|
|
24
|
-
terminate_process("KILL", wait_thread.pid)
|
|
25
|
-
wait_thread.join
|
|
26
|
-
out_reader.kill
|
|
27
|
-
err_reader.kill
|
|
28
|
-
raise(Timeout::Error)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
Result.new(stdout: out_reader.value, stderr: err_reader.value, status: wait_thread.value)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
attr_reader(:command, :input, :timeout)
|
|
38
|
-
|
|
39
|
-
def read_async(io)
|
|
40
|
-
Thread.new { io.read }.tap { |thread| thread.report_on_exception = false }
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def write_input(stdin)
|
|
44
|
-
stdin.write(input) if input
|
|
45
|
-
rescue Errno::EPIPE
|
|
46
|
-
nil
|
|
47
|
-
ensure
|
|
48
|
-
stdin.close
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def terminate_process(signal, pid)
|
|
52
|
-
Process.kill(signal, -pid)
|
|
53
|
-
rescue Errno::ESRCH, Errno::EPERM
|
|
54
|
-
nil
|
|
55
|
-
end
|
|
56
|
-
end
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module ReactEmailRails::RenderModes; end
|