Skip to content

Commit cdff581

Browse files
authored
feat(preview-server): isolated development workflow (#2249)
1 parent 2e9126f commit cdff581

23 files changed

+688
-100
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"pkg-pr-new": "0.0.51",
2424
"tsconfig": "workspace:*",
2525
"tsup": "8.4.0",
26+
"tsx": "4.19.3",
2627
"turbo": "2.5.4",
2728
"vite": "6.3.4",
2829
"vitest": "3.2.3"

packages/preview-server/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ node_modules
33

44
# for testing
55
static
6+
emails
7+
.env*

packages/preview-server/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
"version": "4.1.0-canary.12",
44
"description": "A live preview of your emails right in your browser.",
55
"scripts": {
6-
"caniemail:fetch": "node ./scripts/fill-caniemail-data.mjs",
6+
"build": "tsx ./scripts/build-preview-server.mts",
7+
"caniemail:fetch": "tsx ./scripts/fill-caniemail-data.mts",
78
"clean": "rm -rf dist",
8-
"build": "node ./scripts/build-preview-server.mjs",
9+
"dev": "tsx ./scripts/dev.mts",
10+
"dev:seed": "tsx ./scripts/seed.mts",
911
"test": "vitest run",
1012
"test:watch": "vitest"
1113
},

packages/preview-server/readme.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<div align="center"><strong>@react-email/preview-server</strong></div>
2+
<div align="center">A live preview of your emails right in your browser.</div>
3+
<br />
4+
<div align="center">
5+
<a href="https://react.email">Website</a>
6+
<span> · </span>
7+
<a href="https://github.com/resend/react-email">GitHub</a>
8+
<span> · </span>
9+
<a href="https://react.email/discord">Discord</a>
10+
</div>
11+
12+
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.
13+
14+
## Development workflow
15+
16+
### 1. Seed email templates
17+
18+
```sh
19+
pnpm dev:seed
20+
```
21+
22+
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.
23+
24+
### 2. Run development server
25+
26+
```sh
27+
pnpm dev
28+
```
29+
30+
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.
31+
32+
### 3. Open in your browser
33+
34+
Go to http://localhost:3000
35+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import child_process from 'node:child_process';
2+
import { promises as fs } from 'node:fs';
3+
import path from 'node:path';
4+
import url from 'node:url';
5+
6+
const filename = url.fileURLToPath(import.meta.url);
7+
const dirname = path.dirname(filename);
8+
9+
const previewServerRoot = path.resolve(dirname, '..');
10+
const emailsDirectoryPath = path.join(previewServerRoot, 'emails');
11+
12+
const envPath = path.join(previewServerRoot, '.env.local');
13+
14+
await fs.writeFile(
15+
envPath,
16+
`EMAILS_DIR_RELATIVE_PATH=./emails
17+
EMAILS_DIR_ABSOLUTE_PATH=${emailsDirectoryPath}
18+
USER_PROJECT_LOCATION=${previewServerRoot}
19+
NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT=true`,
20+
'utf8',
21+
);
22+
23+
const webServerProcess = child_process.spawn('next', ['dev'], {
24+
cwd: previewServerRoot,
25+
shell: true,
26+
stdio: 'inherit',
27+
});
28+
29+
webServerProcess.on('exit', async () => {
30+
await fs.rm(envPath);
31+
});
32+
33+
process.on('SIGINT', () => {
34+
webServerProcess.kill('SIGINT');
35+
});
36+
process.on('SIGUSR1', () => {
37+
webServerProcess.kill('SIGUSR1');
38+
});
39+
process.on('SIGUSR2', () => {
40+
webServerProcess.kill('SIGUSR2');
41+
});
42+
process.on('uncaughtExceptionMonitor', () => {
43+
webServerProcess.kill();
44+
});
45+
process.on('exit', () => {
46+
webServerProcess.kill();
47+
});

packages/preview-server/scripts/fill-caniemail-data.mjs renamed to packages/preview-server/scripts/fill-caniemail-data.mts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ This could be happneing for the following reasons:
1414
- You don't have internet connectivity
1515
- Caniemail is down
1616
- 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.`,
17-
{
18-
cause: {
19-
responseFromCaniemail,
20-
},
21-
},
2217
);
2318
}
2419

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { existsSync, promises as fs } from 'node:fs';
2+
import path from 'node:path';
3+
import url from 'node:url';
4+
5+
const filename = url.fileURLToPath(import.meta.url);
6+
const dirname = path.dirname(filename);
7+
8+
const previewServerRoot = path.resolve(dirname, '..');
9+
const emailsDirectoryPath = path.join(previewServerRoot, 'emails');
10+
11+
const seedPath = path.join(dirname, './utils/default-seed/');
12+
13+
if (existsSync(emailsDirectoryPath)) {
14+
console.info('Deleting previous emails directory');
15+
await fs.rm(emailsDirectoryPath, { recursive: true, force: true });
16+
}
17+
18+
console.info('Copying over the defalt seed to the emails directory');
19+
await fs.cp(seedPath, emailsDirectoryPath, {
20+
recursive: true,
21+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
Body,
3+
Button,
4+
Column,
5+
Container,
6+
Head,
7+
Heading,
8+
Hr,
9+
Html,
10+
Preview,
11+
Row,
12+
Tailwind,
13+
Text,
14+
} from '@react-email/components';
15+
16+
interface AccountConfirmationProps {
17+
confirmLink: string;
18+
expiryTime: string;
19+
}
20+
21+
export default function AccountConfirmation({
22+
confirmLink,
23+
expiryTime,
24+
}: AccountConfirmationProps) {
25+
return (
26+
<Html>
27+
<Head />
28+
<Tailwind>
29+
<Body className="bg-black text-white">
30+
<Preview>Confirm your React Email account</Preview>
31+
<Container className="mx-auto">
32+
<Heading className="font-bold text-center my-[48px] text-[32px]">
33+
Welcome to React Email!
34+
</Heading>
35+
<Text>
36+
Thank you for signing up! To complete your registration and start
37+
using React Email, please confirm your email address.
38+
</Text>
39+
<Text className="mb-6">
40+
Click the button below to verify your email. This link will expire
41+
in {expiryTime}.
42+
</Text>
43+
<Row className="w-full">
44+
<Column className="w-full">
45+
<Button
46+
href={confirmLink}
47+
className="bg-cyan-300 text-[20px] font-bold text-[#404040] w-full text-center border border-solid border-cyan-900 py-[8px] rounded-[8px]"
48+
>
49+
Confirm Email
50+
</Button>
51+
</Column>
52+
</Row>
53+
<Text className="mt-6">- React Email team</Text>
54+
<Hr style={{ borderTopColor: '#404040' }} />
55+
<Text className="text-[#606060] font-bold">
56+
React Email, 999 React St, Email City, EC 12345
57+
</Text>
58+
</Container>
59+
</Body>
60+
</Tailwind>
61+
</Html>
62+
);
63+
}
64+
65+
AccountConfirmation.PreviewProps = {
66+
confirmLink: 'https://react.email/confirm/123',
67+
expiryTime: '24 hours',
68+
} satisfies AccountConfirmationProps;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
Body,
3+
Button,
4+
Column,
5+
Container,
6+
Head,
7+
Heading,
8+
Hr,
9+
Html,
10+
Preview,
11+
Row,
12+
Tailwind,
13+
Text,
14+
} from '@react-email/components';
15+
16+
interface ForgotPasswordProps {
17+
resetLink: string;
18+
expiryTime: string;
19+
}
20+
21+
export default function ForgotPassword({
22+
resetLink,
23+
expiryTime,
24+
}: ForgotPasswordProps) {
25+
return (
26+
<Html>
27+
<Head />
28+
<Tailwind>
29+
<Body className="bg-black text-white">
30+
<Preview>Reset your React Email password</Preview>
31+
<Container className="mx-auto">
32+
<Heading className="font-bold text-center my-[48px] text-[32px]">
33+
Reset Your Password
34+
</Heading>
35+
<Text>
36+
We received a request to reset your password for your React Email
37+
account.
38+
</Text>
39+
<Text>
40+
Click the button below to create a new password. This link will
41+
expire in {expiryTime}.
42+
</Text>
43+
<Text className="mb-6">
44+
If you didn't request this, you can safely ignore this email.
45+
</Text>
46+
<Row className="w-full">
47+
<Column className="w-full">
48+
<Button
49+
href={resetLink}
50+
className="bg-cyan-300 text-[20px] font-bold text-[#404040] w-full text-center border border-solid border-cyan-900 py-[8px] rounded-[8px]"
51+
>
52+
Reset Password
53+
</Button>
54+
</Column>
55+
</Row>
56+
<Text className="mt-6">- React Email team</Text>
57+
<Hr style={{ borderTopColor: '#404040' }} />
58+
<Text className="text-[#606060] font-bold">
59+
React Email, 999 React St, Email City, EC 12345
60+
</Text>
61+
</Container>
62+
</Body>
63+
</Tailwind>
64+
</Html>
65+
);
66+
}
67+
68+
ForgotPassword.PreviewProps = {
69+
resetLink: 'https://react.email/reset-password/123',
70+
expiryTime: '1 hour',
71+
} satisfies ForgotPasswordProps;

0 commit comments

Comments
 (0)