Repository navigation
feat(react-email): add --esbuild-plugins option for custom template bundling - #3750
Merged
Merged
Conversation
🦋 Changeset detectedLatest commit: 91c4926 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
commit: |
Contributor
|
@dielduarte is attempting to deploy a commit to the resend Team on Vercel. A member of the Team first needs to authorize it. |
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
dielduarte
force-pushed
the
feat/esbuild-plugins-option
branch
from
September 23, 2026 19:04
39d2cb8 to
bc0ab6d
Compare
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
force-pushed
the
feat/esbuild-plugins-option
branch
from
September 23, 2026 19:36
9a538a6 to
91c4926
Compare
dielduarte
approved these changes
Sep 23, 2026
Contributor
Author
|
Thanks @dielduarte! 🙏 |
Contributor
|
thank you for the contribution!! |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
--esbuild-plugins <path>option onemail dev,email buildandemail 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.devandbuildforward the resolved absolute path to the preview app through a newREACT_EMAIL_INTERNAL_ESBUILD_PLUGINS_PATHvariable, following how--clientsand the other internal variables are passed. The preview app loads the module once per process and passes the plugins togetEmailComponent.exportloads the module directly in the CLI withjiti, 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 plusrenderandreactEmailCreateReactElement. Without this, whicheveronLoadclaimed 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.minorforreact-emailand@react-email/ui) and a short section in thereact-emailreadme.Not covered: no config-file form of the option (only the CLI flag), and the built-in
inlineCssLoaderand 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
onLoadreturned the entry's contents and esbuild stops at the firstonLoadresult, 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 usesexport * fromplusimport * asandexport default emailModule.defaultrather thanexport { 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
getEmailComponentcase 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.email dev --esbuild-plugins ./plugins.mjsagainstapps/demowith a plugin that stamps the<Preview>text and confirmed the stamp in the served preview, andemail export --esbuild-pluginswith 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 toemail dev,email build, andemail exportso templates that depend on a compile-time transform, such as Lingui's i18n macros, can be previewed and exported.exportload the module withjiti, so TypeScript plugin modules work on every supported Node version;devandbuildforward the resolved path viaREACT_EMAIL_INTERNAL_ESBUILD_PLUGINS_PATH.exportexits with "Failed to build emails". Failed loads aren't cached, so fixing the module takes effect on the next render without a restart.inlineCssLoaderand exporter still run before user plugins, so a user plugin cannot replace them.Written for commit 91c4926. Summary will update on new commits.