Try reading this section if you feel like the amount of directories or files are cumbersome or if you can’t understand what is what.

Top-level directories

After cloning the React Email repository you will be able to see a few directories at the root:

DirectoryDescription

apps

Here you can find all of the apps related to our online presence, like:

benchmarks

These are meant to be benchmarks we make from version-to-version to demonstrate with actual data that performance gains are really true with metrics like p99 and p75.

One example of them is the one for the Improved Performance for Tailwind Emails

packages

This is probably where you are going to spend the majority of your time if you are going to contribute.

It contains all the NPM published packages that we currently have, each directory should be one of these, with the exception of a few of them that are meant for shared configuration but you shouldn’t have to worry about them in the vast majority of cases.

If you have an idea of a better way we could structure these packages so that they could be more manageable and approachable we would really appreciate you create a discussion with your thoughts on this.

Multiple packages

This repository is a pnpm monorepo which means it contains multiple packages that are maintained. We keep each component we make inside of a package for itself. This being a pnpm monorepo means you need to use pnpm to install the packages and we recommend you do so by using corepack as follows:

corepack enable
corepack prepare pnpm@latest --activate

Currently we have the following packages:

Don’t be scared by the amount of packages as most of them are quite small, with
the largest component package being only 900 lines of code. And the largest one being the CLI itself which is actually two things in one, more information on that later.

Turborepo

For managing the tasks that you might want to run on all of the packages we use turborepo it does so in parallel and won’t re-run already ran code which makes it really fast after initial the run.

If you want to use turborepo on the projects you can just use the root scripts we have defined that directly calls into turborepo and you can see them on the package.json file.

Even though you don’t need it, we do recommend you have it installed globally as it allows you to go into one of the packages and run something like turbo build which will build both the package you are on, as well as the packages it depends on. Very useful for testing to make sure things are working as expected.

Also, don’t worry about installing it globally and it version mismatching with the one we have in the repo, as turborepo also deals with that.

The package for our CLI (i.e. packages/react-email)

The CLI is one of the most important pieces for the best development we can provide to users, it can both cause the biggest amount of pain, during development, as well as increase the user’s speed by quite a lot. Besides that, it’s also one of our most complicated packages so let’s go over it a bit.

The CLI is both a Next app, for the email dev and email build commands, as well as a commander.js CLI. The way we are able to do this is by just having a common Next app file structure and then including a sneaky src/cli directory that is not published and is compiled into a root cli directory.

This allows for a really good developer experience as we can both share certain functions, as well as communicate between them without that much of an issue.

For triggering rebuilds of email templates after they have been saved we use the chokidar package alongside the socket.io package to detect file changes and send a message to the server that then triggers a rebuild respectively.

Testing

For testing we use vitest for its really awesome speed and simplicity, we prefer it with its globals defined and all the tests are ran under the happy-dom environment.

This of course, with the exception of the @react-email/render package’s renderAsync that does a fair bit of magic to simulate edge and other environments that are not supported by happy-dom. For this we override the environment on a per-file basis for its tests

We don’t have a very strict policy about testing coverage, but we do try to keep things tested to avoid both regressions and to make sure that the code is working as expected. A good rule of thumb is that if you need to simulate use cases to just test a specific portion of code, you should probably have that split into a function

Linting

We use eslint for linting, and we have a private unpublished package called
eslint-config-custom that we share across packages and apps. This package is not meant to be used outside the repository and is not published to npm.

For each type of project we have a different configuration to extend and use from the shared configuration, they are all based on the Vercel Engineering Style Guide. If you want to run linting check on a specific package they all have a lint script that you can run with
pnpm lint.

We also use prettier for formatting, and we have just one configuration for all the projects on the root of the repo, if you want to use it you can just run pnpm format at the root of the repository, and it will do so throughout the project.

Both the linting and formatting are ensured by our GitHub CI so make sure that you have your linting and formatting right before you get to opening a PR or asking for a review on it.

Building

What we currently use, for the most part, is the awesome tsup. The only exception for this is the @react-email/tailwind package which currently uses vite due to a few issues with tsup and tailwindcss’s bundling.

This will literally run tsup with a few settings we have for it. Generally they are just src/index.ts --format esm,cjs --dts --external react which generates both an ESM and CJS versions of the package and the type definitions that were exported from src/index.ts, which is the entry point. --external react here ensures that react is not bundled in which can cause many issues.

Why build before publishing?

Currently, we do building for most of the packages. This is important for a few key reasons that improve the usability of each package:

  1. All the exported types can be imported from the same place the JavaScript is imported
  2. We have proper CommonJS and ES Modules support
  3. Code that isn’t exported is not published nor downloaded