diff --git a/package.json b/package.json
index 7fb0e6b9e..d32fbce2e 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"pkg-pr-new": "0.0.51",
"tsconfig": "workspace:*",
"tsup": "8.4.0",
+ "tsx": "4.19.3",
"turbo": "2.5.4",
"vite": "6.3.4",
"vitest": "3.2.3"
diff --git a/packages/preview-server/.gitignore b/packages/preview-server/.gitignore
index 659fa846e..49d1ca2df 100644
--- a/packages/preview-server/.gitignore
+++ b/packages/preview-server/.gitignore
@@ -3,3 +3,5 @@ node_modules
# for testing
static
+emails
+.env*
diff --git a/packages/preview-server/package.json b/packages/preview-server/package.json
index 08932b094..0025caf40 100644
--- a/packages/preview-server/package.json
+++ b/packages/preview-server/package.json
@@ -3,9 +3,11 @@
"version": "4.1.0-canary.12",
"description": "A live preview of your emails right in your browser.",
"scripts": {
- "caniemail:fetch": "node ./scripts/fill-caniemail-data.mjs",
+ "build": "tsx ./scripts/build-preview-server.mts",
+ "caniemail:fetch": "tsx ./scripts/fill-caniemail-data.mts",
"clean": "rm -rf dist",
- "build": "node ./scripts/build-preview-server.mjs",
+ "dev": "tsx ./scripts/dev.mts",
+ "dev:seed": "tsx ./scripts/seed.mts",
"test": "vitest run",
"test:watch": "vitest"
},
diff --git a/packages/preview-server/readme.md b/packages/preview-server/readme.md
new file mode 100644
index 000000000..8539cb849
--- /dev/null
+++ b/packages/preview-server/readme.md
@@ -0,0 +1,35 @@
+
@react-email/preview-server
+A live preview of your emails right in your browser.
+
+
+
+This package is used to store the preview server, it is also published and versioned so that it can be installed when the [CLI](../react-email) is being used.
+
+## Development workflow
+
+### 1. Seed email templates
+
+```sh
+pnpm dev:seed
+```
+
+This generates a boilerplate emails directory for you to work with. These files can also be modified as you see fit since they are not included in git.
+
+### 2. Run development server
+
+```sh
+pnpm dev
+```
+
+This is somewhat equivalent to `next dev` and does not support hot reloading for email templates like the CLI does. It lets you work on the UI for the preview server mainly.
+
+### 3. Open in your browser
+
+Go to http://localhost:3000
+
diff --git a/packages/preview-server/scripts/build-preview-server.mjs b/packages/preview-server/scripts/build-preview-server.mts
similarity index 100%
rename from packages/preview-server/scripts/build-preview-server.mjs
rename to packages/preview-server/scripts/build-preview-server.mts
diff --git a/packages/preview-server/scripts/dev.mts b/packages/preview-server/scripts/dev.mts
new file mode 100644
index 000000000..87e65ebdf
--- /dev/null
+++ b/packages/preview-server/scripts/dev.mts
@@ -0,0 +1,47 @@
+import child_process from 'node:child_process';
+import { promises as fs } from 'node:fs';
+import path from 'node:path';
+import url from 'node:url';
+
+const filename = url.fileURLToPath(import.meta.url);
+const dirname = path.dirname(filename);
+
+const previewServerRoot = path.resolve(dirname, '..');
+const emailsDirectoryPath = path.join(previewServerRoot, 'emails');
+
+const envPath = path.join(previewServerRoot, '.env.local');
+
+await fs.writeFile(
+ envPath,
+ `EMAILS_DIR_RELATIVE_PATH=./emails
+EMAILS_DIR_ABSOLUTE_PATH=${emailsDirectoryPath}
+USER_PROJECT_LOCATION=${previewServerRoot}
+NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT=true`,
+ 'utf8',
+);
+
+const webServerProcess = child_process.spawn('next', ['dev'], {
+ cwd: previewServerRoot,
+ shell: true,
+ stdio: 'inherit',
+});
+
+webServerProcess.on('exit', async () => {
+ await fs.rm(envPath);
+});
+
+process.on('SIGINT', () => {
+ webServerProcess.kill('SIGINT');
+});
+process.on('SIGUSR1', () => {
+ webServerProcess.kill('SIGUSR1');
+});
+process.on('SIGUSR2', () => {
+ webServerProcess.kill('SIGUSR2');
+});
+process.on('uncaughtExceptionMonitor', () => {
+ webServerProcess.kill();
+});
+process.on('exit', () => {
+ webServerProcess.kill();
+});
diff --git a/packages/preview-server/scripts/fill-caniemail-data.mjs b/packages/preview-server/scripts/fill-caniemail-data.mts
similarity index 94%
rename from packages/preview-server/scripts/fill-caniemail-data.mjs
rename to packages/preview-server/scripts/fill-caniemail-data.mts
index a5330775f..98eb5c9ec 100644
--- a/packages/preview-server/scripts/fill-caniemail-data.mjs
+++ b/packages/preview-server/scripts/fill-caniemail-data.mts
@@ -14,11 +14,6 @@ This could be happneing for the following reasons:
- You don't have internet connectivity
- Caniemail is down
- Caniemail changed from where to fetch their data from, which means we need to fix this. If this is the case, please open up an issue.`,
- {
- cause: {
- responseFromCaniemail,
- },
- },
);
}
diff --git a/packages/preview-server/scripts/seed.mts b/packages/preview-server/scripts/seed.mts
new file mode 100644
index 000000000..67cee2f82
--- /dev/null
+++ b/packages/preview-server/scripts/seed.mts
@@ -0,0 +1,21 @@
+import { existsSync, promises as fs } from 'node:fs';
+import path from 'node:path';
+import url from 'node:url';
+
+const filename = url.fileURLToPath(import.meta.url);
+const dirname = path.dirname(filename);
+
+const previewServerRoot = path.resolve(dirname, '..');
+const emailsDirectoryPath = path.join(previewServerRoot, 'emails');
+
+const seedPath = path.join(dirname, './utils/default-seed/');
+
+if (existsSync(emailsDirectoryPath)) {
+ console.info('Deleting previous emails directory');
+ await fs.rm(emailsDirectoryPath, { recursive: true, force: true });
+}
+
+console.info('Copying over the defalt seed to the emails directory');
+await fs.cp(seedPath, emailsDirectoryPath, {
+ recursive: true,
+});
diff --git a/packages/preview-server/scripts/utils/default-seed/auth/account-confirmation.tsx b/packages/preview-server/scripts/utils/default-seed/auth/account-confirmation.tsx
new file mode 100644
index 000000000..b610e8224
--- /dev/null
+++ b/packages/preview-server/scripts/utils/default-seed/auth/account-confirmation.tsx
@@ -0,0 +1,68 @@
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Hr,
+ Html,
+ Preview,
+ Row,
+ Tailwind,
+ Text,
+} from '@react-email/components';
+
+interface AccountConfirmationProps {
+ confirmLink: string;
+ expiryTime: string;
+}
+
+export default function AccountConfirmation({
+ confirmLink,
+ expiryTime,
+}: AccountConfirmationProps) {
+ return (
+
+