Skip to content

feat(react-email): Unify all components into react-email #1800

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 79 commits into
base: 4.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
9edd2a9
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
6a4f59c
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
d6bdc23
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
42d78e8
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
45be789
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
11e1998
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
68533fd
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
ea193c5
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
f9e7ade
small changes to favour not having `react-email` as a devDEpendency
gabrielmfern Dec 3, 2024
3ef1edf
export all components from react-email
gabrielmfern Dec 3, 2024
345ab44
update demo to use react-email instead of components
gabrielmfern Dec 3, 2024
13e1c45
update rendering utilities exporter plugin to import render from reac…
gabrielmfern Dec 3, 2024
35569d9
use react 18.3.1 due to prism-react-renderer not support React 19
gabrielmfern Dec 3, 2024
dfa0fbb
add achangeset
gabrielmfern Dec 3, 2024
d37c407
format
gabrielmfern Dec 3, 2024
944cf3d
import from specific postcss files for smaller bundle size
gabrielmfern Dec 11, 2024
77c534d
update integration locks
gabrielmfern Dec 11, 2024
c878ee5
initial attempt at using tsup to build tailwind
gabrielmfern Dec 12, 2024
0681f32
fix issue with postcss's patch
gabrielmfern Dec 16, 2024
a7f2e9a
move patches into the actual workspace for proper separation
gabrielmfern Jan 8, 2025
9367aa3
fix issue inside tailwind
gabrielmfern Jan 8, 2025
b1bcfd9
export from workspace
gabrielmfern Jan 8, 2025
83fc3bb
build one for each env
gabrielmfern Jan 8, 2025
3d31762
remove src from pached tailwind
gabrielmfern Jan 8, 2025
27fc0bf
fix big integers on patched tailwind
gabrielmfern Jan 8, 2025
b16d60b
get things to build in the react-email package
gabrielmfern Jan 8, 2025
f7a5847
remove scripts from patched dependencies
gabrielmfern Jan 8, 2025
626ca53
always import react-dom with the default to avoid vite errors
gabrielmfern Jan 8, 2025
2df5ef6
separate commands
gabrielmfern Jan 8, 2025
4f4ecae
refactor integration tests
gabrielmfern Jan 8, 2025
774c04f
fix test being duplicated
gabrielmfern Jan 8, 2025
5dbda89
remove now unused file
gabrielmfern Jan 8, 2025
7f1aef2
fix Root import
gabrielmfern Jan 8, 2025
e6f9f84
avoid tests from modules running
gabrielmfern Jan 8, 2025
271a768
fix import
gabrielmfern Jan 8, 2025
5949b97
save up contexts and use the rebuild for faster hot reloads
gabrielmfern Jan 8, 2025
027ecdd
optimize build for esbuild
gabrielmfern Jan 8, 2025
696337b
remove components from demo
gabrielmfern Jan 8, 2025
657361d
update lock
gabrielmfern Jan 8, 2025
bdf8cc4
fix max RAM usage error
gabrielmfern Jan 8, 2025
ecdfd5a
fix issues with workspace dev dependencies
gabrielmfern Jan 8, 2025
b1cdd9c
remove git modules
gabrielmfern Jan 8, 2025
af86189
fix build for tailwind package
gabrielmfern Jan 8, 2025
bb3c205
reset tailwind package
gabrielmfern Jan 8, 2025
49d8dd8
reset components to no changes
gabrielmfern Jan 8, 2025
fd72f59
import from react-email on vercel-invite-user demo email
gabrielmfern Jan 8, 2025
75eafb9
format
gabrielmfern Jan 8, 2025
bc8012f
remove unecessary filters
gabrielmfern Jan 8, 2025
f1d108e
remove responsive-email on build
gabrielmfern Jan 8, 2025
6089e85
add cache for react-email-starter
gabrielmfern Jan 8, 2025
cb19cdb
fix selector parser patch being ignroed
gabrielmfern Jan 8, 2025
d37185e
format
gabrielmfern Jan 8, 2025
21d4660
move install command from demo to CLI build
gabrielmfern Jan 8, 2025
fbf6db4
use private instead of changeset ignore
gabrielmfern Jan 8, 2025
85186e3
fix build error on demo
gabrielmfern Jan 8, 2025
d2a5496
fix integration test
gabrielmfern Jan 8, 2025
9cf09bb
increase timeout for export test
gabrielmfern Jan 8, 2025
e2fa5b6
fix side effecst
gabrielmfern Jan 8, 2025
c841ec2
update vitest
gabrielmfern Jan 9, 2025
305f1a7
remove lock and yalc lock
gabrielmfern Jan 9, 2025
c697743
increase timeout for export.spec
gabrielmfern Jan 9, 2025
cd5ae75
ignore package-lock
gabrielmfern Jan 9, 2025
33c3720
increase timeout time for export.spec
gabrielmfern Jan 9, 2025
cad3148
Revert "ignore package-lock"
gabrielmfern Jan 9, 2025
03539ed
remove unecessary duplicated builds from workflows
gabrielmfern Jan 9, 2025
62e0565
make linting depend on build
gabrielmfern Jan 9, 2025
e4f7818
fix dependsOn for test script
gabrielmfern Jan 9, 2025
9ac5fce
move the script into the onSuccess callback from build
gabrielmfern Jan 9, 2025
a47c7a3
add && back in
gabrielmfern Jan 9, 2025
937da70
fix linting
gabrielmfern Jan 9, 2025
6890fa2
update rendering utilites exporter so that it still works with @react…
gabrielmfern Jan 9, 2025
7893566
update changeset
gabrielmfern Jan 9, 2025
7dadd34
Merge branch '4.0' into feat/unify-packages
gabrielmfern Mar 24, 2025
731d25f
update components inside react-email/
gabrielmfern Mar 24, 2025
6ae530d
remove types prettier dep
gabrielmfern Mar 24, 2025
90b5aa9
fix type issues
gabrielmfern Mar 24, 2025
02a8e76
delete eslint configs
gabrielmfern Mar 24, 2025
7271506
lint
gabrielmfern Mar 24, 2025
5a7e753
drop patched version completely
gabrielmfern Mar 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/selfish-dogs-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-email": major
---

Add the source for all components, build and export them all unified with `react-email`
3 changes: 0 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ jobs:
- name: Install packages
run: pnpm install --frozen-lockfile

- name: Run Build
run: pnpm build

- name: Run Lint
run: pnpm lint

Expand Down
2 changes: 1 addition & 1 deletion apps/demo/emails/notifications/vercel-invite-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Section,
Tailwind,
Text,
} from '@react-email/components';
} from 'react-email';

interface VercelInviteUserEmailProps {
username?: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"build": "pnpm install --frozen-lockfile && email build",
"build": "email build",
"dev": "email dev",
"start": "email start",
"export": "email export"
Expand Down
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"files": {
"ignore": [
"dist",
"patched",
"pnpm-lock.yaml",
".next",
"public",
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"private": true,
"scripts": {
"build": "turbo run build --filter=!react-email-starter",
"dev": "turbo run dev --filter=!react-email-starter --parallel --concurrency 25",
"release": "turbo run build --filter=./packages/* --filter=!react-email-starter && pnpm changeset publish",
"canary:enter": "changeset pre enter canary",
"canary:exit": "changeset pre exit",
"version": "changeset version && pnpm install --no-frozen-lockfile",
"dev": "turbo run dev --filter=!react-email-starter --parallel --concurrency 25",
"lint": "biome check",
"lint:fix": "biome check --write .",
"release": "turbo run build --filter=./packages/* --filter=!react-email-starter && pnpm changeset publish",
"test": "turbo run test",
"test:watch": "turbo run test:watch"
"test:watch": "turbo run test:watch",
"version": "changeset version && pnpm install --no-frozen-lockfile"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
Expand Down
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@react-email/components",
"version": "0.0.34",
"private": true,
"description": "A collection of all components React Email.",
"sideEffects": false,
"main": "./dist/index.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/react-email/.gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
node_modules
.next
cli
yalc.lock
!src/cli

# for testing
package-lock.json
.for-dependency-graph.ts
static
3 changes: 3 additions & 0 deletions packages/react-email/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ module.exports = {

return config;
},
eslint: {
ignoreDuringBuilds: true,
},
// Noticed an issue with typescript transpilation when going from Next 14.1.1 to 14.1.2
// and I narrowed that down into this PR https://github.com/vercel/next.js/pull/62005
//
Expand Down
100 changes: 88 additions & 12 deletions packages/react-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,75 @@
"bin": {
"email": "./dist/cli/index.js"
},
"sideEffects": ["./dist/code-block.*"],
"main": "./dist/index.browser.js",
"module": "./dist/index.browser.mjs",
"types": "./dist/index.browser.d.ts",
"exports": {
".": {
"node": {
"import": {
"types": "./dist/index.node.d.mts",
"default": "./dist/index.node.mjs"
},
"require": {
"types": "./dist/index.node.d.ts",
"default": "./dist/index.node.js"
}
},
"deno": {
"import": {
"types": "./dist/index.browser.d.mts",
"default": "./dist/index.browser.mjs"
},
"require": {
"types": "./dist/index.browser.d.ts",
"default": "./dist/index.browser.js"
}
},
"worker": {
"import": {
"types": "./dist/index.browser.d.mts",
"default": "./dist/index.browser.mjs"
},
"require": {
"types": "./dist/index.browser.d.ts",
"default": "./dist/index.browser.js"
}
},
"browser": {
"import": {
"types": "./dist/index.browser.d.mts",
"default": "./dist/index.browser.mjs"
},
"require": {
"types": "./dist/index.browser.d.ts",
"default": "./dist/index.browser.js"
}
},
"default": {
"import": {
"types": "./dist/index.node.d.mts",
"default": "./dist/index.node.mjs"
},
"require": {
"types": "./dist/index.node.d.ts",
"default": "./dist/index.node.js"
}
}
}
},
"scripts": {
"build": "tsup-node && node ./scripts/build-preview-server.mjs",
"build:preview": "node ./scripts/build-preview-server.mjs",
"build:tsup": "node --max-old-space-size=8192 ./node_modules/tsup/dist/cli-node.js",
"build:package": "pnpm build:tsup && pnpm install --frozen-lockfile",
"build": "pnpm build:package && pnpm build:preview",
"dev": "pnpm build:tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
"caniemail:fetch": "node ./scripts/fill-caniemail-data.mjs",
"clean": "rm -rf dist",
"dev": "tsup-node --watch",
"dev:preview": "cd ../../apps/demo && tsx ../../packages/react-email/src/cli/index.ts dev",
"test": "vitest run",
"test:watch": "vitest"
"dev:preview": "cd ../../apps/demo && tsx ../../packages/react-email/src/cli/index.ts dev"
},
"license": "MIT",
"repository": {
Expand All @@ -31,17 +92,28 @@
"chokidar": "4.0.3",
"commander": "11.1.0",
"debounce": "2.0.0",
"esbuild": "0.23.0",
"esbuild": "0.24.2",
"glob": "10.3.4",
"html-to-text": "9.0.5",
"log-symbols": "4.1.0",
"md-to-react-email": "5.0.5",
"mime-types": "2.1.35",
"next": "15.2.2",
"normalize-path": "3.0.0",
"ora": "5.4.1",
"prettier": "3.4.2",
"prismjs": "1.29.0",
"postcss": "8.4.40",
"postcss-selector-parser": "6.0.16",
"tailwindcss": "3.4.10",
"socket.io": "4.8.1"
},
"peerDependencies": {
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
},
"devDependencies": {
"@babel/core": "7.26.10",
"@edge-runtime/vm": "3.1.8",
"@lottiefiles/dotlottie-react": "0.12.3",
"@radix-ui/colors": "1.0.1",
"@radix-ui/react-collapsible": "1.1.0",
Expand All @@ -51,44 +123,48 @@
"@radix-ui/react-tabs": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.0",
"@radix-ui/react-tooltip": "1.1.2",
"@react-email/render": "workspace:*",
"@responsive-email/react-email": "0.0.3",
"@swc/core": "1.4.15",
"@types/babel__core": "7.20.5",
"@types/babel__traverse": "*",
"@types/fs-extra": "11.0.1",
"@types/html-to-text": "9.0.4",
"@types/mime-types": "2.1.4",
"@types/node": "22.10.2",
"@types/normalize-path": "3.0.2",
"@types/prismjs": "1.26.5",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/shelljs": "0.8.15",
"@types/webpack": "5.28.5",
"@vercel/style-guide": "5.1.0",
"autoprefixer": "10.4.20",
"clsx": "2.1.0",
"framer-motion": "12.0.0-alpha.2",
"jiti": "2.4.2",
"jsdom": "23.0.1",
"json5": "2.2.3",
"module-punycode": "npm:[email protected]",
"node-html-parser": "6.1.13",
"postcss": "8.4.40",
"prettier-plugin-tailwindcss": "0.6.6",
"pretty-bytes": "6.1.1",
"prism-react-renderer": "2.1.0",
"prism-react-renderer": "2.4.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"sharp": "0.33.3",
"shelljs": "0.8.5",
"socket.io-client": "4.8.0",
"sonner": "1.7.1",
"source-map-js": "1.0.2",
"spamc": "0.0.5",
"stacktrace-parser": "0.1.10",
"tailwind-merge": "2.2.0",
"tailwindcss": "3.4.0",
"tsconfig": "workspace:*",
"tsup": "7.2.0",
"tsx": "4.9.0",
"typescript": "5.8.2",
"use-debounce": "10.0.4",
"vitest": "1.1.3",
"vitest": "2.1.8",
"yalc": "1.0.0-pre.53",
"zod": "3.24.2"
}
}
36 changes: 36 additions & 0 deletions packages/react-email/scripts/adjust-package-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import fs from 'node:fs/promises';
import path from 'node:path';

export async function adjustPackageIndex() {
const distFiles = await fs.readdir(path.resolve(process.cwd(), 'dist'));
const indexFiles = distFiles.filter((file) => file.startsWith('index'));

for await (const file of indexFiles) {
const extension = path.extname(file);

if (extension === '.js' || extension === '.mjs') {
const pathToFile = path.resolve(process.cwd(), 'dist', file);
const originalContents = await fs.readFile(pathToFile, 'utf8');

await fs.writeFile(
pathToFile,
originalContents.replaceAll(
/"(?<componentPath>\.\/[^"]+)"/g,
(value, componentPath: string) => {
if (componentPath.startsWith('./render')) {
const newComponentPath = componentPath
.replace('/browser', '.browser')
.replace('/node', '.node');
return value.replace(
componentPath,
`${newComponentPath}${extension}`,
);
}
return value.replace(componentPath, `${componentPath}${extension}`);
},
),
'utf8',
);
}
}
}
23 changes: 16 additions & 7 deletions packages/react-email/src/cli/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,29 @@ const updatePackageJson = async (builtPreviewAppPath: string) => {
) as {
name: string;
scripts: Record<string, string>;
peerDependencies?: Record<string, string>;
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
};
packageJson.scripts.build = 'next build';
packageJson.scripts.start = 'next start';

packageJson.name = 'preview-server';
// We remove this one to avoid having resolve issues on our demo build process.
// This is only used in the `export` command so it's irrelevant to have it here.
//
// See `src/actions/render-email-by-path` for more info on how we render the
// email templates without `@react-email/render` being installed.
delete packageJson.devDependencies['@react-email/render'];
delete packageJson.devDependencies['@react-email/components'];
for (const [key, value] of Object.entries(packageJson.dependencies)) {
if (value.startsWith('workspace:')) {
delete packageJson.dependencies[key];
}
}
for (const [key, value] of Object.entries(packageJson.devDependencies)) {
if (value.startsWith('workspace:')) {
delete packageJson.devDependencies[key];
}
}

// This is meant to avoid issues with peer dependencies while the package doesn't upgrade
delete packageJson.devDependencies['@responsive-email/react-email'];

delete packageJson.peerDependencies;
await fs.promises.writeFile(
packageJsonPath,
JSON.stringify(packageJson),
Expand Down
6 changes: 3 additions & 3 deletions packages/react-email/src/cli/commands/export.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import fs, { unlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import type { Options } from '@react-email/render';
import { type BuildFailure, build } from 'esbuild';
import { glob } from 'glob';
import logSymbols from 'log-symbols';
import normalize from 'normalize-path';
import ora from 'ora';
import type React from 'react';
import type { createElement } from 'react';
import type { Options } from '../../package/render/node';
import { renderingUtilitiesExporter } from '../../utils/esbuild/renderring-utilities-exporter';
import {
type EmailsDirectory,
Expand Down Expand Up @@ -117,7 +117,7 @@ export const exportTemplates = async (
element: React.ReactElement,
options: Record<string, unknown>,
) => Promise<string>;
reactEmailCreateReactElement: typeof React.createElement;
reactEmailCreateReactElement: typeof createElement;
};
const rendered = await emailModule.render(
emailModule.reactEmailCreateReactElement(emailModule.default, {}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`<Body> component > renders correctly 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><body>Lorem ipsum<!--/$--></body>"`;
26 changes: 26 additions & 0 deletions packages/react-email/src/package/body/body.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Body } from '.';
import { render } from '../render/node';

describe('<Body> component', () => {
it('renders children correctly', async () => {
const testMessage = 'Test message';
const html = await render(<Body>{testMessage}</Body>);
expect(html).toContain(testMessage);
});

it('passes style and other props correctly', async () => {
const style = { backgroundColor: 'red' };
const html = await render(
<Body data-testid="body-test" style={style}>
Test
</Body>,
);
expect(html).toContain('style="background-color:red"');
expect(html).toContain('data-testid="body-test"');
});

it('renders correctly', async () => {
const actualOutput = await render(<Body>Lorem ipsum</Body>);
expect(actualOutput).toMatchSnapshot();
});
});
Loading
Loading