Skip to content

feat(react-email): add --esbuild-plugins option for custom template bundling - #3750

Merged
dielduarte merged 10 commits into
resend:canaryfrom
melv-n:feat/esbuild-plugins-option
Sep 23, 2026
Merged

dielduarte merged 10 commits into
resend:canaryfrom
melv-n:feat/esbuild-plugins-option

Conversation

@melv-n

@melv-n melv-n commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Feature. Lets users apply their own esbuild plugins when react-email bundles their templates, so templates that depend on a compile-time transform can be previewed and exported. The motivating case is Lingui macros for i18n, requested in discussion #2334 ("Load custom babel/swc/esbuild plugins"): the macro transform runs as an esbuild plugin, but the preview server's esbuild call had a fixed plugin list, so such templates could only be authored with Lingui's more verbose runtime API.

Changes

  • New --esbuild-plugins <path> option on email dev, email build and email export. The path points to a module whose default export is an array of esbuild plugins, or a function returning one (sync or async) for plugins that need setup.
  • dev and build forward the resolved absolute path to the preview app through a new REACT_EMAIL_INTERNAL_ESBUILD_PLUGINS_PATH variable, following how --clients and the other internal variables are passed. The preview app loads the module once per process and passes the plugins to getEmailComponent. export loads the module directly in the CLI with jiti, like the other user-project code the CLI evaluates.
  • renderingUtilitiesExporter (both the CLI and the preview app copies) no longer reads the template's source itself. It now resolves each template entry point to a wrapper module, using an esbuild path suffix so output file names are unchanged, and the wrapper re-exports the template plus render and reactEmailCreateReactElement. Without this, whichever onLoad claimed the entry first won, so the exporter and a user plugin could never both apply to the template file. With the wrapper, the template goes through the regular load pipeline and user plugins can transform it.
  • A changeset (minor for react-email and @react-email/ui) and a short section in the react-email readme.

Not covered: no config-file form of the option (only the CLI flag), and the built-in inlineCssLoader and exporter still run before user plugins, so a user plugin cannot replace those.

Technical notes

The alternative of appending user plugins after the built-in ones would have kept the exporter as is, but the exporter's onLoad returned the entry's contents and esbuild stops at the first onLoad result, so user plugins never saw the template file, which is exactly the file a macro transform needs to see. Rewriting the exporter as an entry wrapper was the smallest change that makes the two compose. The wrapper uses export * from plus import * as and export default emailModule.default rather than export { default } from, so a template without a default export still reaches the existing "does not contain a default exported function" error instead of failing inside esbuild.

Testing

  • New spec for the plugins loader and a new getEmailComponent case that runs a fixture plugin rewriting the template source, asserting the rendered HTML changed. All 108 preview app specs pass, including the existing inline snapshots, which confirms the wrapper produces the same output as before.
  • pnpm --filter @react-email/ui build (Next 16, Turbopack) succeeds, which covers the bundler-ignored dynamic import of the user module.
  • Manually ran email dev --esbuild-plugins ./plugins.mjs against apps/demo with a plugin that stamps the <Preview> text and confirmed the stamp in the served preview, and email export --esbuild-plugins with the same plugin, confirming the stamp in all built templates.

Note

AI agent: Claude Code | Model: Claude Fable 5.1 | Subagents: None


Summary by cubic

Adds an --esbuild-plugins <path> option to email dev, email build, and email export so templates that depend on a compile-time transform, such as Lingui's i18n macros, can be previewed and exported.

  • The path points to a module whose default export is an array of esbuild plugins, or a function returning one (sync or async). Both the preview app and export load the module with jiti, so TypeScript plugin modules work on every supported Node version; dev and build forward the resolved path via REACT_EMAIL_INTERNAL_ESBUILD_PLUGINS_PATH.
  • The rendering utilities exporter now resolves each template entry to a wrapper module instead of reading the template source, so user plugins and the exporter can both apply to the template file.
  • A plugin module that fails to load surfaces as a normal error instead of crashing: the preview shows it in the render's error overlay, and export exits with "Failed to build emails". Failed loads aren't cached, so fixing the module takes effect on the next render without a restart.
  • Adds CLI documentation for the option on all three commands and a "Custom esbuild plugins" guide.
  • There is no config-file form of the option, and the built-in inlineCssLoader and exporter still run before user plugins, so a user plugin cannot replace them.

Written for commit 91c4926. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 91c4926

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
react-email Minor
@react-email/ui Minor
@react-email/editor Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the linear-synced PR has been synced to Linear label Sep 22, 2026
@pkg-pr-new

pkg-pr-new Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/react-email@3750
npm i https://pkg.pr.new/@react-email/ui@3750

commit: 91c4926

@vercel

vercel Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

@dielduarte is attempting to deploy a commit to the resend Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
react-email Ready Ready Preview Sep 23, 2026 7:38pm UTC
react-email-demo Ready Ready Preview Sep 23, 2026 7:38pm UTC

Request Review

melv-n and others added 10 commits September 23, 2026 16:35
The preview loaded the plugins module with Node's native `import()`, so a
TypeScript plugins module only loaded on Node versions that strip types
natively, and never with non-erasable syntax or extensionless imports, while
`email export` already loaded it with jiti. The module is now evaluated
through jiti in the preview too, with `jiti` kept external in both the
preview's Next config and the one `email build` generates.

A failed load was also cached for the lifetime of the process, so fixing the
module had no effect until the server restarted. Only a successful load is
kept now, and the source is evaluated directly because Node's own module
caches would hand a retry the broken `.mjs`/`.cjs` module back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Loading the plugins ran outside the render's error handling, so a broken
plugins module made the server action throw, leaving the spinner running and
the preview without its error overlay. The failure is now returned as the
render's error, like any other.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The plugins module was loaded before the build's error handling, so a
module that failed to load crashed `email export` with an unhandled
rejection instead of "Failed to build emails" and exit code 1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pshot

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dielduarte
dielduarte merged commit 0432f96 into resend:canary Sep 23, 2026
12 of 13 checks passed
@melv-n
melv-n deleted the feat/esbuild-plugins-option branch September 23, 2026 20:07
@melv-n

melv-n commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @dielduarte! 🙏

Copy link
Copy Markdown
Contributor

thank you for the contribution!!

This branch was successfully deployed

2 active deployments
Preview – react-email-demo — 91c49263 Deployed Sep 23, 2026 by vercel[bot]
Preview – react-email — 91c49263 Deployed Sep 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

linear-synced PR has been synced to Linear

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants