react-email-rails 0.4.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ada860a1bfdc970fd871cb2f0d4b89642e42b8641b8d11006ca66e9bf6f452a
4
- data.tar.gz: 742b838451251d7b97093077ff9909e3d60441e705a841fed522d1d64df83787
3
+ metadata.gz: 05b64507c82feabb25781eaec8487c5e7282f65060abbac88719bfb2bc79d499
4
+ data.tar.gz: fc1f7fe4d40195537b5218818aa0fb8b72be78834238223dc377c52f7843af15
5
5
  SHA512:
6
- metadata.gz: 2d7490d1c6bccdd3d7d7eeb5fa0f0a1d6a9888e6090bf560b2d63feab8db4edec448eb7ba6ddbc34134ed9cf1cd34e4a2cb229bf41794c44faefbafe0f98adc0
7
- data.tar.gz: c2a55b22219041afe9cc25f00fa5b2ef222f4d45656027c4e971b2666d90a68f02aae4894fc30f3771b3064e41b5c880febead6d79d4cecadb0cb5bd1736e3de
6
+ metadata.gz: 61afe5bf3fc51e550ca9608bd91deeeb9794cd4a90bac53a7bf0fb48f2f12f5984f50dadbbd245c05e34d8dc4ab7c478f2ff8420aa94e9f57c98e39fb04bab01
7
+ data.tar.gz: 992fd9253d2a2cf31f9ecd37f7bd960c52efddaf74541aefa5ed124ebef4941d9404c00d7f417a6cc2d36f998d3391d8f1bf3ec3d865ec232c43c5a2df2ed4c1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ - `ReactEmailRails.parse` now neutralizes unsafe link/button URI schemes: hrefs whose scheme is not `http`, `https`, `mailto`, or `tel` (e.g. `javascript:`/`data:`) are blanked before they reach the document `Hash`. Scheme detection ignores the whitespace and control characters browsers strip when resolving a scheme, so case- and whitespace-obfuscated payloads are caught too.
6
+
3
7
  ## 0.4.0
4
8
 
5
9
  - `ReactEmailRails.parse` now accepts `markdown:` as an alternative to `html:`. Markdown is converted to HTML and runs through the same extension-driven parse path, producing the same document `Hash` — handy for agent- or tool-generated content. Pass exactly one of `html:`/`markdown:`.
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- ![React Email + Rails](react-email-rails.png)
1
+ ![react-email-rails](react-email-rails.png)
2
2
 
3
- # React Email + Rails
3
+ # react-email-rails
4
4
 
5
- Build and send emails using React and Rails a seamless integration between [React Email](https://react.email) and [Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html).
5
+ Build and send emails using React and Rails with [React Email](https://react.email) and [Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html).
6
6
 
7
7
  ## Contents
8
8
 
@@ -12,7 +12,6 @@ Build and send emails using React and Rails — a seamless integration between [
12
12
  - [Requirements](#requirements)
13
13
  - [Quick Start](#quick-start)
14
14
  - [Usage](#usage)
15
- - [Editor](#editor)
16
15
  - [Configuration](#configuration)
17
16
  - [Deployment](#deployment)
18
17
  - [Development](#development)
@@ -22,15 +21,15 @@ Build and send emails using React and Rails — a seamless integration between [
22
21
 
23
22
  ## Why
24
23
 
25
- Building HTML emails is painfully archaic. [React Email](https://react.email) is a collection of unstyled components for building emails with React, Tailwind, and TypeScript. This gem brings that power directly into your Rails app. Write emails as React components, send them through Action Mailer, and recipients get automatically generated HTML and text emails.
24
+ Building HTML emails is painfully archaic. [React Email](https://react.email) brings React, Tailwind, and TypeScript to email templates. This gem wires it into Action Mailer so React components deliver as generated HTML and text emails.
26
25
 
27
26
  ## How
28
27
 
29
28
  **In development,** the gem renders components live through Vite's dev pipeline, so your emails get the same module resolution and transforms as the rest of your frontend.
30
29
 
31
- **In production,** Rails builds a server-side email bundle during `assets:precompile`. The bundled rake task runs an isolated email-only Vite build, using `reactEmailRails()` in your app's Vite config for discovery and options.
30
+ **In production,** Rails builds a server-side renderer bundle during `assets:precompile` using `reactEmailRails()` in your Vite config for discovery and options.
32
31
 
33
- React Email Rails automatically renders both HTML and plain-text versions from the same component. Delivery, headers, previews, queues, and callbacks all stay normal Action Mailer. If rendering fails, no email is sent and `ReactEmailRails::RenderError` is raised.
32
+ react-email-rails automatically renders both HTML and plain-text versions from the same component. Delivery, headers, previews, queues, and callbacks all stay normal Action Mailer. If rendering fails, no email is sent and `ReactEmailRails::RenderError` is raised.
34
33
 
35
34
  ## Status
36
35
 
@@ -67,11 +66,11 @@ bin/rails generate react_email_rails:install
67
66
 
68
67
  This creates `config/initializers/react_email_rails.rb`, installs missing JavaScript dependencies when it can detect your package manager, adds `reactEmailRails()` to `vite.config.*`, and creates `app/javascript/emails`.
69
68
 
70
- The installed setup follows the normal Rails lifecycle:
69
+ The installed setup then follows the normal Rails lifecycle:
71
70
 
72
71
  - `bin/rails generate react_email_rails:email ...` creates matching mailers and React components.
73
- - Development renders through Vite on demand.
74
- - `bin/rails assets:precompile` builds the production email bundle automatically.
72
+ - `bin/dev` renders through Vite on demand.
73
+ - `bin/rails assets:precompile` builds the production renderer bundle automatically.
75
74
  - `bin/rails react_email_rails:build` builds the bundle directly when CI or tests need it.
76
75
 
77
76
  ### Manual Install
@@ -103,9 +102,7 @@ Generate a mailer and React Email component:
103
102
  bin/rails generate react_email_rails:email Account welcome
104
103
  ```
105
104
 
106
- The generator follows Rails' mailer generator shape: `NAME [method method]`. It creates `app/mailers/account_mailer.rb`, matching components under your configured React Email directory, plus a mailer preview and test.
107
-
108
- The generator reads `emails.path` and `emails.extension` from `reactEmailRails()` when available. Pass flags to override them:
105
+ The generator follows Rails' mailer generator shape: `NAME [method method]`. It creates the mailer, matching React components, a mailer preview, and a test. It reads `emails.path` and `emails.extension` from `reactEmailRails()` when available. Pass flags to override them:
109
106
 
110
107
  ```sh
111
108
  bin/rails generate react_email_rails:email Account welcome --emails-path=app/emails --extension=jsx
@@ -159,11 +156,11 @@ export default function Welcome({ account }: WelcomeProps) {
159
156
 
160
157
  > [@react-email/components](https://react.email/docs/components/html) provides primitives like `<Button>`, `<Heading>`, `<Tailwind>`, and more.
161
158
 
162
- That's it it now renders and delivers like any other Action Mailer email.
159
+ That's it. It now renders and delivers like any other Action Mailer email.
163
160
 
164
161
  ## Usage
165
162
 
166
- Pass data from your mailers and each top-level key becomes a prop on the React component's default export.
163
+ Pass data from your mailer. Each top-level key becomes a prop on the component's default export.
167
164
 
168
165
  ```ruby
169
166
  mail react: { foo: "bar" }, ...
@@ -232,9 +229,9 @@ Component names are inferred from the mailer and action:
232
229
  | `AccountMailer#welcome` | `account_mailer/welcome` |
233
230
  | `Users::InviteMailer#new_invite` | `users/invite_mailer/new_invite` |
234
231
 
235
- Rails derives `account_mailer` from `AccountMailer` via its `mailer_name`. The default Vite plugin resolves those names under `app/javascript/emails`, so `account_mailer/welcome` maps to `app/javascript/emails/account_mailer/welcome.tsx` or `.jsx`.
232
+ Rails derives `account_mailer` from `AccountMailer` via `mailer_name`. By default, `account_mailer/welcome` resolves to `app/javascript/emails/account_mailer/welcome.tsx` or `.jsx`.
236
233
 
237
- Files and directories starting with `_` are ignored as renderable email entries by default. Use them for shared components such as `_components/email_layout.tsx`; they can still be imported by email components.
234
+ Files and directories starting with `_` are ignored as renderable email entries by default. Use them for shared components such as `_components/email_layout.tsx`. They can still be imported by email components.
238
235
 
239
236
  Override the inferred name per mail:
240
237
 
@@ -246,15 +243,15 @@ Or override `component_path_resolver` globally in your [configuration](#configur
246
243
 
247
244
  ### Prop Serialization
248
245
 
249
- Just like `render json:` in controllers, you can pass any object that responds to `as_json` to `mail react:`. Plain hashes, Active Model objects, and serialization libraries like [Alba](https://github.com/okuramasafumi/alba) or [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers) are supported.
246
+ Like `render json:`, `mail react:` accepts any object that responds to `as_json`, including hashes, Active Model objects, and serializers such as [Alba](https://github.com/okuramasafumi/alba) or [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
250
247
 
251
248
  ### Prop Transformation
252
249
 
253
- By default, prop keys are camelized on the way to React, so `account.plan_name` arrives as `account.planName` in your component. This makes them more idiomatic for the frontend, but you can override the `transform_props` behavior in your [configuration](#configuration).
250
+ Prop keys are camelized by default, so `account.plan_name` arrives as `account.planName`. Override `transform_props` in your [configuration](#configuration).
254
251
 
255
252
  ### Layouts
256
253
 
257
- Action Mailer layouts are not applied to `react:` emails. React Email treats layouts like any other component, so share structure with normal React composition instead:
254
+ Action Mailer layouts aren't applied to `react:` emails. React Email treats layouts like any other component, so share structure with normal React composition instead:
258
255
 
259
256
  ```tsx
260
257
  // app/javascript/emails/_components/email_layout.tsx
@@ -294,191 +291,11 @@ export default function Welcome() {
294
291
 
295
292
  See [Component Names](#component-names) for how shared `_` files are handled.
296
293
 
297
- ## Editor
298
-
299
- Alongside named components, the gem can render a [@react-email/editor](https://react.email/docs/editor) document — the Tiptap/ProseMirror JSON a visual editor produces — to HTML and text on the server.
300
-
301
- React Email exposes [composeReactEmail](https://react.email/docs/editor/api-reference/compose-react-email) for this, but only from the browser, with a live editor instance from the export panel. `ReactEmailRails.compose` is the server analog: it rebuilds what `composeReactEmail` needs headlessly (no DOM, no live editor) from the stored document and the extensions you declare, then calls the same function. So `compose` is to the editor what `render` is to a component.
302
-
303
- This is opt-in. The editor packages are optional peer dependencies and stay out of the email render path until you enable it.
294
+ ### Editor
304
295
 
305
- ### Setup
296
+ If you're also using [@react-email/editor](https://react.email/docs/editor) to let users compose emails inside your app, `ReactEmailRails.compose` can render those stored documents on the server.
306
297
 
307
- Install the editor packages:
308
-
309
- ```sh
310
- npm i @react-email/editor @tiptap/core
311
- ```
312
-
313
- To also parse HTML into documents with [`parse`](#parsing-html-or-markdown-into-a-document), add `@tiptap/html` and its server DOM, `happy-dom`:
314
-
315
- ```sh
316
- npm i @tiptap/html happy-dom
317
- ```
318
-
319
- To parse Markdown as well, add [`marked`](https://marked.js.org) — it converts Markdown to HTML, which then runs through the same parser:
320
-
321
- ```sh
322
- npm i marked
323
- ```
324
-
325
- Enable the `documents` option in your Vite config:
326
-
327
- ```ts
328
- // vite.config.ts
329
-
330
- import { defineConfig } from "vite"
331
- import { reactEmailRails } from "react-email-rails"
332
-
333
- export default defineConfig({
334
- plugins: [reactEmailRails({ documents: true })],
335
- })
336
- ```
337
-
338
- `documents: true` enables it with defaults (`app/javascript/documents`, `.ts`/`.tsx` extensions). Like `emails`, it also accepts a directory string or `{ path, extension, ignore }`. See [Plugin Options](#plugin-options).
339
-
340
- ### Document Renderers
341
-
342
- A document doesn't carry the editor configuration it was authored with, so a headless renderer has to be told which extensions a given document needs. Each file under the documents directory is a **document renderer** — the editor-side analog of an email component — and its name is resolved from the directory layout just like [component names](#component-names) (so `broadcast` maps to `app/javascript/documents/broadcast.ts`).
343
-
344
- ```ts
345
- // app/javascript/documents/broadcast.ts
346
-
347
- import { StarterKit } from "@react-email/editor/extensions"
348
- import { EmailTheming } from "@react-email/editor/plugins"
349
-
350
- export function buildExtensions() {
351
- return [StarterKit, EmailTheming]
352
- }
353
- ```
354
-
355
- A renderer can export two optional hooks:
356
-
357
- | Export | Required | Description |
358
- |--------|----------|-------------|
359
- | `buildExtensions(context)` | Yes | Returns the Tiptap extension list for the document. |
360
- | `transformDocument(document, context)` | No | Returns a rewritten document before rendering — for example, to inject header/footer nodes that aren't persisted in the stored document. |
361
- | `getPreview(context)` | No | Returns inbox preview text when the `compose` call doesn't pass one. |
362
-
363
- `context` is the optional data you pass to `compose` (see below). Use it to vary extensions, transforms, or preview text per render.
364
-
365
- ```ts
366
- // app/javascript/documents/broadcast.ts
367
-
368
- import { StarterKit } from "@react-email/editor/extensions"
369
- import { EmailTheming } from "@react-email/editor/plugins"
370
-
371
- export function buildExtensions(context) {
372
- return [StarterKit, EmailTheming]
373
- }
374
-
375
- export function transformDocument(document, context) {
376
- const header = {
377
- type: "heading",
378
- attrs: { level: 1 },
379
- content: [{ type: "text", text: context.brandName }],
380
- }
381
- const themeIndex = document.content.findIndex((node) => node.type === "globalContent")
382
- const at = themeIndex + 1
383
- return {
384
- ...document,
385
- content: [...document.content.slice(0, at), header, ...document.content.slice(at)],
386
- }
387
- }
388
-
389
- export function getPreview(context) {
390
- return context.previewText
391
- }
392
- ```
393
-
394
- > **Match the extensions to the document.** `composeReactEmail` renders any node whose extension isn't registered as `null`, so a document that uses a node your `buildExtensions` omits will silently drop that content. Return the same extension list the document was authored with.
395
-
396
- > **Keep the theme node.** The editor persists its theme in a `globalContent` node and `EmailTheming` reads it back when rendering. If you reshape the document in `transformDocument`, preserve that node.
397
-
398
- ### Composing a Document
399
-
400
- Call `ReactEmailRails.compose` with the renderer `type`, the stored document, and optional `context`/`preview`:
401
-
402
- ```ruby
403
- broadcast = Broadcast.find(params[:id])
404
-
405
- rendered = ReactEmailRails.compose(
406
- type: "broadcast",
407
- document: broadcast.body,
408
- context: { brand_name: "Acme", preview_text: broadcast.subject },
409
- preview: broadcast.subject,
410
- )
411
-
412
- rendered.html # => "<!DOCTYPE html>..."
413
- rendered.text # => "ACME\n\n..."
414
- ```
415
-
416
- It returns the same `RenderedEmail` (`html`/`text`) as `render`, runs through the same [render modes](#render-modes), and raises `ReactEmailRails::RenderError` on failure. Documents don't go through Action Mailer — broadcasts and the like usually have their own delivery path — so deliver `rendered.html`/`rendered.text` however your app sends mail.
417
-
418
- **The document is a `Hash`.** You pass and store it as a plain Ruby `Hash` with string keys — what a jsonb column hands back, and what [`parse`](#parsing-html-or-markdown-into-a-document) returns. "Tiptap JSON" names the document's *shape* (and the format it's serialized to over the wire to the renderer), not the Ruby type; `compose` accepts any object that responds to `as_json`, but a `Hash` is the norm.
419
-
420
- **Keys:** the document's keys (`type`, `attrs`, `content`, `marks`, node names, `globalContent`) are structural and **passed through verbatim** — never transformed. Only `context` is key-transformed, camelized exactly like component props (so `brand_name` arrives as `brandName`, per [`transform_props`](#prop-transformation)).
421
-
422
- `render_options` does not apply to documents; `composeReactEmail` controls its own rendering.
423
-
424
- ### Parsing HTML or Markdown into a Document
425
-
426
- `ReactEmailRails.parse` converts semantic HTML into the same document `Hash` shape the editor stores, using the selected renderer's extensions. This needs the `@tiptap/html` and `happy-dom` packages (see [Setup](#setup)).
427
-
428
- ```ruby
429
- document = ReactEmailRails.parse(
430
- type: "broadcast",
431
- html: params[:body_html],
432
- context: { brand_name: "Acme" },
433
- )
434
-
435
- broadcast.update!(body: document)
436
- ```
437
-
438
- Later, render the stored document like any other:
439
-
440
- ```ruby
441
- rendered = ReactEmailRails.compose(type: "broadcast", document: broadcast.body)
442
- ```
443
-
444
- `parse` returns a plain Ruby `Hash` with string keys, normalized through the renderer's schema. It uses the same [render modes](#render-modes) as `compose` and raises `ReactEmailRails::RenderError` on failure. `context` is key-transformed like component props; the HTML is sent verbatim.
445
-
446
- #### Markdown
447
-
448
- Pass `markdown:` in place of `html:` to parse Markdown — useful when the content comes from an LLM or another tool that emits Markdown more readily than HTML. It's converted to HTML with [`marked`](https://marked.js.org) and run through the same parse path, so it needs `marked` installed alongside the HTML peers (see [Setup](#setup)).
449
-
450
- ```ruby
451
- document = ReactEmailRails.parse(
452
- type: "broadcast",
453
- markdown: "# Welcome\n\nThanks for signing up, **Ada**.",
454
- context: { brand_name: "Acme" },
455
- )
456
- ```
457
-
458
- Pass exactly one of `html:` or `markdown:` — passing both, or neither, raises `ArgumentError`.
459
-
460
- Markdown is a lower-friction *input*, not a wider one. It expresses less than HTML — headings, paragraphs, emphasis, links, lists, blockquotes, code, images, and rules — so it adds no new node types. Markdown that maps to nodes the renderer's extensions don't define (such as a GFM table without a table extension) is dropped or flattened, the same as the equivalent HTML.
461
-
462
- What this means in practice:
463
-
464
- - HTML maps to a node only when an extension defines how to parse it. Unknown elements, inline styles, and classes may be dropped or flattened.
465
- - Editor-only constructs such as custom email nodes and the persisted `globalContent` theme node do not round-trip from plain HTML or Markdown.
466
- - If you already have the document `Hash`, pass it to `compose` directly.
467
-
468
- ### Debugging Dropped Content
469
-
470
- The most common integration bug is an extension/document mismatch. A node whose type is **missing from `buildExtensions` entirely** raises `ReactEmailRails::RenderError` (it can't be parsed) — loud and safe. The quiet case is a node that *is* in the schema but whose extension does not render to email (a plain Tiptap node rather than an email one): `composeReactEmail` renders it as nothing, with no error.
471
-
472
- `compose` reports those dropped node types so the silent case isn't silent. They appear both on the result as `rendered.warnings` and on the [`render.react-email-rails`](#instrumentation) instrumentation event as `payload[:warnings]` — an array of `{ type:, count: }`. The editor's own non-rendering nodes (the `globalContent` theme node and similar) are excluded, so a non-empty `warnings` means real content was lost. Subscribe to the event to alert on it — or refuse to send:
473
-
474
- ```ruby
475
- ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
476
- warnings = event.payload[:warnings]
477
- raise "dropped #{warnings.sum { _1[:count] }} node(s): #{warnings.map { _1[:type] }.join(", ")}" if warnings
478
- end
479
- ```
480
-
481
- If content is missing, confirm `buildExtensions` returns the **same** extensions the document was authored with — the editor's `StarterKit` plus every custom node and plugin in use — and, if you reshape the document in `transformDocument`, that you preserved the `globalContent` theme node. Treat a mismatch as data or version skew: pinning a document's renderer `type` to the extension set it was created with, and versioning that set, avoids drift.
298
+ See [Editor rendering](docs/editor.md) for setup and usage.
482
299
 
483
300
  ## Configuration
484
301
 
@@ -516,17 +333,13 @@ ReactEmailRails.configure do |config|
516
333
  end
517
334
  ```
518
335
 
519
- `transform_props` only controls prop key names; props are always serialized with `as_json`.
336
+ `transform_props` only controls prop key names. Props are always serialized with `as_json`.
520
337
 
521
338
  #### Render Modes
522
339
 
523
- `:subprocess` starts a fresh Node process for each render. It's simple, always uses the latest bundle, and keeps failures isolated, but pays Node startup and bundle load on every render.
524
-
525
- `:persistent` reuses one long-lived Node process per worker. It's faster because it avoids per-render startup, but uses more memory and can serve a stale component until recycled.
526
-
527
- For background email delivery, the default `:subprocess` mode is usually enough. Switch to `:persistent` when Node startup appears in traces or batch jobs render many emails from the same bundle (see [Instrumentation](#instrumentation)).
340
+ `:subprocess` starts a fresh Node process for each render. It's simple, isolated, and always uses the latest bundle, but pays Node startup and bundle load each time.
528
341
 
529
- The render mode also shapes the development experience: `:subprocess` boots a fresh Vite dev server per render and always reflects your latest edits, while `:persistent` reuses the server and may serve a stale component until the process is recycled.
342
+ `:persistent` reuses one long-lived Node process per worker. It's faster for render-heavy workers, but uses more memory and can serve a stale component until recycled. The default `:subprocess` mode is usually enough. Switch when Node startup shows up in traces or batch jobs render many emails from the same bundle.
530
343
 
531
344
  Enable persistent mode for render-heavy worker processes:
532
345
 
@@ -538,13 +351,13 @@ end
538
351
 
539
352
  Persistent mode keeps one Node child per process:
540
353
 
541
- - Renders are sent as newline-delimited JSON and processed one at a time, so a single child never renders concurrently. Scale throughput by adding worker processes.
542
- - It is fork-safe: under clustered Puma or forking job runners, each worker spawns its own child.
354
+ - Renders are newline-delimited JSON and processed one at a time. Scale throughput with more worker processes.
355
+ - It's fork-safe: under clustered Puma or forking job runners, each worker spawns its own child.
543
356
  - The child is recycled after `render_process_max_requests` renders to bound memory growth. Set it to `nil` to disable recycling.
544
357
 
545
358
  #### Render Options
546
359
 
547
- `render_options` is passed to [@react-email/render](https://react.email/docs/utilities/render). `html` options apply to HTML rendering and `text` options apply to plain-text rendering. Keys are camelized before they cross into JavaScript.
360
+ `render_options` is passed to [@react-email/render](https://react.email/docs/utilities/render). Use `html` and `text` keys for each output. Option keys are camelized before they cross into JavaScript.
548
361
 
549
362
  ```ruby
550
363
  ReactEmailRails.configure do |config|
@@ -563,7 +376,7 @@ end
563
376
 
564
377
  #### Error Reporting
565
378
 
566
- Use `on_render_error` to report failures before the exception is re-raised. The callback receives the error and a `context` of `kind:` (`"email"`, `"document"`, or `"parse"`) plus the identifier — `component:` for emails, `type:` for documents and parse requests. Accept `**context` so one handler covers every render kind:
379
+ Use `on_render_error` to report failures before the exception is re-raised. The callback receives the error plus `kind:` and `component:`:
567
380
 
568
381
  ```ruby
569
382
  ReactEmailRails.configure do |config|
@@ -575,7 +388,7 @@ end
575
388
 
576
389
  #### Instrumentation
577
390
 
578
- Every render emits an [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html) event named `render.react-email-rails`, so you can log render timing or forward it to your APM. The payload carries a `kind` (`"email"`, `"document"`, or `"parse"`), the `component` name (email) or `type` (document and parse), and, on a successful render, the rendered HTML size in `html_bytes` (omitted for `parse`, which returns a document rather than HTML). Document renders that drop content also include `warnings` (see [Debugging Dropped Content](#debugging-dropped-content)):
391
+ Every render emits `render.react-email-rails` through [ActiveSupport::Notifications](https://guides.rubyonrails.org/active_support_instrumentation.html). The payload includes `kind`, `component`, and successful HTML size in `html_bytes`:
579
392
 
580
393
  ```ruby
581
394
  ActiveSupport::Notifications.subscribe("render.react-email-rails") do |event|
@@ -585,9 +398,9 @@ end
585
398
 
586
399
  ### Vite Configuration
587
400
 
588
- Most apps only need the `reactEmailRails()` plugin from [Quick Start](#quick-start). The options below change where components are discovered, how the bundle handles dependencies, and which email-only Vite transforms run in the isolated renderer.
401
+ Most apps only need the `reactEmailRails()` plugin from [Quick Start](#quick-start). The options below change component discovery, bundle dependency handling, and isolated renderer transforms.
589
402
 
590
- In development and production, the isolated renderer loads the `reactEmailRails()` plugin, JSX support, and component-facing Vite config such as `resolve`, `define`, `css`, `json`, `assetsInclude`, `esbuild`, and `oxc` but none of your other app plugins. Forwarded config is only for compiling and resolving email components; server, preview, dependency optimization, and build output settings stay owned by React Email Rails.
403
+ In development and production, the isolated renderer loads `reactEmailRails()`, JSX support, and component-facing Vite config such as `resolve`, `define`, `css`, `json`, `assetsInclude`, `esbuild`, and `oxc`. It doesn't load your other app plugins. Server, preview, dependency optimization, and build output settings stay owned by react-email-rails.
591
404
 
592
405
  #### Plugin Options
593
406
 
@@ -596,11 +409,7 @@ In development and production, the isolated renderer loads the `reactEmailRails(
596
409
  | `emails.path` | `"app/javascript/emails"` | Directory containing email components |
597
410
  | `emails.extension` | `[".tsx", ".jsx"]` | Component extension, or an array of extensions |
598
411
  | `emails.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `emails.path` |
599
- | `documents` | `false` (off) | Enable [editor document rendering](#editor). `true`, a path string, or `{ path, extension, ignore }` |
600
- | `documents.path` | `"app/javascript/documents"` | Directory containing document renderers |
601
- | `documents.extension` | `[".ts", ".tsx"]` | Renderer extension, or an array of extensions |
602
- | `documents.ignore` | `["**/_*", "**/_*/**"]` | Glob patterns ignored under `documents.path` |
603
- | `standalone` | `true` | Inline production email bundle dependencies |
412
+ | `standalone` | `true` | Inline production renderer bundle dependencies |
604
413
  | `vite` | `{}` | Extra email-only Vite config for compilation and resolution |
605
414
 
606
415
  Use a custom directory:
@@ -625,7 +434,7 @@ Component names come from the Vite directory layout (see [Component Names](#comp
625
434
 
626
435
  #### Advanced: Email-Only Vite Plugins
627
436
 
628
- Most apps do not need extra email plugins. If email components need a transform that is not part of Vite's default pipeline, add that transform to the email renderer:
437
+ Most apps don't need extra email plugins. If email components need a transform that isn't part of Vite's default pipeline, add that transform to the isolated renderer:
629
438
 
630
439
  ```ts
631
440
  import mdx from "@mdx-js/rollup"
@@ -643,11 +452,11 @@ export default defineConfig({
643
452
  })
644
453
  ```
645
454
 
646
- These `vite` options are used by `react-email-rails-dev` and `react-email-rails-build`. They are intentionally scoped to React Email. Only `assetsInclude`, `css`, `define`, `esbuild`, `json`, `oxc`, `plugins`, and `resolve` are accepted here; output settings such as `build.outDir` and `build.rollupOptions` are ignored so the Ruby renderer can always find the generated bundle.
455
+ These `vite` options are used by `react-email-rails-dev` and `react-email-rails-build`. Only `assetsInclude`, `css`, `define`, `esbuild`, `json`, `oxc`, `plugins`, and `resolve` are accepted. Output settings such as `build.outDir` and `build.rollupOptions` are ignored so Ruby can always find the bundle.
647
456
 
648
457
  #### Standalone Builds
649
458
 
650
- By default the production email bundle inlines React, `@react-email/render`, and other Node dependencies. That makes the bundle larger, but it works well for Rails deploys that build assets in one stage and run without `node_modules` in the final runtime image. Development previews keep dependencies external for Vite's module runner, even when `standalone` is enabled.
459
+ By default the production renderer bundle inlines React, `@react-email/render`, and other Node dependencies. This works well for Rails deploys that build assets in one stage and run without `node_modules` in the final image. Development previews keep dependencies external, even when `standalone` is enabled.
651
460
 
652
461
  Set `standalone: false` when your runtime already ships `node_modules` and you prefer a smaller SSR-style bundle:
653
462
 
@@ -667,7 +476,7 @@ For production deploys, run the normal Rails asset task:
667
476
  bin/rails assets:precompile
668
477
  ```
669
478
 
670
- The `react_email_rails:build` task is hooked into `assets:precompile` automatically. It loads your Vite config to find `reactEmailRails()` and its options, then writes `tmp/react-email-rails/emails.js` with the isolated React Email pipeline.
479
+ The `react_email_rails:build` task is hooked into `assets:precompile` automatically. It loads `reactEmailRails()` options from your Vite config, then writes `tmp/react-email-rails/emails.js` with the email component registry. If [Editor document rendering](docs/editor.md) is enabled, the bundle also includes document renderers.
671
480
 
672
481
  You can run it directly when needed:
673
482
 
@@ -677,9 +486,9 @@ bin/rails react_email_rails:build
677
486
 
678
487
  Production rendering runs that bundle with Node. 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.
679
488
 
680
- The npm package, Vite, React, and `@react-email/render` must be available when Rails runs `assets:precompile`. This is the same stage where Rails apps normally install JavaScript dependencies and build frontend assets.
489
+ The npm package, Vite, React, and `@react-email/render` must be available when Rails runs `assets:precompile`. If you enable [Editor document rendering](docs/editor.md), its peer dependencies must be available too.
681
490
 
682
- The bundle is required, not an optimization. If it's missing, renders raise `ReactEmailRails::RenderError` and no mail is sent.
491
+ The bundle is required, not an optimization. If it's missing, renders raise `ReactEmailRails::RenderError`. Action Mailer deliveries aren't sent.
683
492
 
684
493
  The Ruby gem and npm package must stay on the same version. The renderer includes a small protocol/version handshake, so mismatched installs fail with an actionable `ReactEmailRails::RenderError` instead of silently returning malformed output.
685
494
 
@@ -693,7 +502,7 @@ To confirm the renderer is ready before relying on it, run:
693
502
  bin/rails react_email_rails:verify
694
503
  ```
695
504
 
696
- It checks that the render command runs and that the npm package version matches the gem, then exits non-zero with an actionable message on failure. Wire it into your CI or release step to catch a missing bundle or version drift before a deploy ships — a renderer failure won't otherwise surface until the first email is sent.
505
+ It checks that the render command runs and that the npm package version matches the gem, then exits non-zero with an actionable message on failure. Wire it into CI or release steps to catch missing bundles or version drift before the first render.
697
506
 
698
507
  For programmatic checks (for example, a health endpoint), `ReactEmailRails.healthy?` returns a boolean. If you specifically want a check at boot, call it from your own initializer and scope it to the processes that send mail so others don't pay the cost:
699
508
 
@@ -1,3 +1,3 @@
1
1
  module ReactEmailRails
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.1"
3
3
  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.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Supertape