feat(react-email): add --esbuild-plugins option for custom template bundling
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 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.- A changeset (
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 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
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.- Manually ran
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 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
exportload the module withjiti, so TypeScript plugin modules work on every supported Node version;devandbuildforward the resolved path viaREACT_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
exportexits 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
inlineCssLoaderand exporter still run before user plugins, so a user plugin cannot replace them.