mirror of
https://github.com/resend/react-email.git
synced 2026-09-14 14:18:02 +08:00
feat(preview-server): isolated development workflow (#2249)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -3,3 +3,5 @@ node_modules
|
||||
|
||||
# for testing
|
||||
static
|
||||
emails
|
||||
.env*
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<div align="center"><strong>@react-email/preview-server</strong></div>
|
||||
<div align="center">A live preview of your emails right in your browser.</div>
|
||||
<br />
|
||||
<div align="center">
|
||||
<a href="https://react.email">Website</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/resend/react-email">GitHub</a>
|
||||
<span> · </span>
|
||||
<a href="https://react.email/discord">Discord</a>
|
||||
</div>
|
||||
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
-5
@@ -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,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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 (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-black text-white">
|
||||
<Preview>Confirm your React Email account</Preview>
|
||||
<Container className="mx-auto">
|
||||
<Heading className="font-bold text-center my-[48px] text-[32px]">
|
||||
Welcome to React Email!
|
||||
</Heading>
|
||||
<Text>
|
||||
Thank you for signing up! To complete your registration and start
|
||||
using React Email, please confirm your email address.
|
||||
</Text>
|
||||
<Text className="mb-6">
|
||||
Click the button below to verify your email. This link will expire
|
||||
in {expiryTime}.
|
||||
</Text>
|
||||
<Row className="w-full">
|
||||
<Column className="w-full">
|
||||
<Button
|
||||
href={confirmLink}
|
||||
className="bg-cyan-300 text-[20px] font-bold text-[#404040] w-full text-center border border-solid border-cyan-900 py-[8px] rounded-[8px]"
|
||||
>
|
||||
Confirm Email
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
<Text className="mt-6">- React Email team</Text>
|
||||
<Hr style={{ borderTopColor: '#404040' }} />
|
||||
<Text className="text-[#606060] font-bold">
|
||||
React Email, 999 React St, Email City, EC 12345
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
AccountConfirmation.PreviewProps = {
|
||||
confirmLink: 'https://react.email/confirm/123',
|
||||
expiryTime: '24 hours',
|
||||
} satisfies AccountConfirmationProps;
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Row,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from '@react-email/components';
|
||||
|
||||
interface ForgotPasswordProps {
|
||||
resetLink: string;
|
||||
expiryTime: string;
|
||||
}
|
||||
|
||||
export default function ForgotPassword({
|
||||
resetLink,
|
||||
expiryTime,
|
||||
}: ForgotPasswordProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-black text-white">
|
||||
<Preview>Reset your React Email password</Preview>
|
||||
<Container className="mx-auto">
|
||||
<Heading className="font-bold text-center my-[48px] text-[32px]">
|
||||
Reset Your Password
|
||||
</Heading>
|
||||
<Text>
|
||||
We received a request to reset your password for your React Email
|
||||
account.
|
||||
</Text>
|
||||
<Text>
|
||||
Click the button below to create a new password. This link will
|
||||
expire in {expiryTime}.
|
||||
</Text>
|
||||
<Text className="mb-6">
|
||||
If you didn't request this, you can safely ignore this email.
|
||||
</Text>
|
||||
<Row className="w-full">
|
||||
<Column className="w-full">
|
||||
<Button
|
||||
href={resetLink}
|
||||
className="bg-cyan-300 text-[20px] font-bold text-[#404040] w-full text-center border border-solid border-cyan-900 py-[8px] rounded-[8px]"
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
<Text className="mt-6">- React Email team</Text>
|
||||
<Hr style={{ borderTopColor: '#404040' }} />
|
||||
<Text className="text-[#606060] font-bold">
|
||||
React Email, 999 React St, Email City, EC 12345
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
ForgotPassword.PreviewProps = {
|
||||
resetLink: 'https://react.email/reset-password/123',
|
||||
expiryTime: '1 hour',
|
||||
} satisfies ForgotPasswordProps;
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Row,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from '@react-email/components';
|
||||
|
||||
interface PaymentOverdueProps {
|
||||
customerName: string;
|
||||
amount: string;
|
||||
dueDate: string;
|
||||
invoiceLink: string;
|
||||
}
|
||||
|
||||
export default function PaymentOverdue({
|
||||
customerName,
|
||||
amount,
|
||||
dueDate,
|
||||
invoiceLink,
|
||||
}: PaymentOverdueProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-black text-white">
|
||||
<Preview>Payment of {amount} is overdue - Action required</Preview>
|
||||
<Container className="mx-auto">
|
||||
<Heading className="font-bold text-center my-[48px] text-[32px]">
|
||||
Payment Overdue
|
||||
</Heading>
|
||||
<Text>Dear {customerName},</Text>
|
||||
<Text>
|
||||
We noticed that your payment of {amount} was due on {dueDate} and
|
||||
has not been received yet.
|
||||
</Text>
|
||||
<Text>
|
||||
To avoid any service interruption, please process your payment as
|
||||
soon as possible.
|
||||
</Text>
|
||||
<Text className="mb-6">
|
||||
You can view and pay your invoice by clicking the button below:
|
||||
</Text>
|
||||
<Row className="w-full">
|
||||
<Column className="w-full">
|
||||
<Button
|
||||
href={invoiceLink}
|
||||
className="bg-cyan-300 text-[20px] font-bold text-[#404040] w-full text-center border border-solid border-cyan-900 py-[8px] rounded-[8px]"
|
||||
>
|
||||
Pay Now
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
<Text className="mt-6">
|
||||
If you have already made this payment, please disregard this
|
||||
message.
|
||||
</Text>
|
||||
<Text className="mt-6">- React Email team</Text>
|
||||
<Hr style={{ borderTopColor: '#404040' }} />
|
||||
<Text className="text-[#606060] font-bold">
|
||||
React Email, 999 React St, Email City, EC 12345
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
PaymentOverdue.PreviewProps = {
|
||||
customerName: 'Jane Smith',
|
||||
amount: '$99.00',
|
||||
dueDate: 'June 1st, 2024',
|
||||
invoiceLink: 'https://react.email/invoice/123',
|
||||
} satisfies PaymentOverdueProps;
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Row,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from '@react-email/components';
|
||||
|
||||
interface TeamInviteProps {
|
||||
inviterName: string;
|
||||
teamName: string;
|
||||
inviteLink: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export default function TeamInvite({
|
||||
inviterName,
|
||||
teamName,
|
||||
inviteLink,
|
||||
role,
|
||||
}: TeamInviteProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-black text-white">
|
||||
<Preview>
|
||||
{inviterName} has invited you to join {teamName} on React Email
|
||||
</Preview>
|
||||
<Container className="mx-auto">
|
||||
<Heading className="font-bold text-center my-[48px] text-[32px]">
|
||||
You've been invited!
|
||||
</Heading>
|
||||
<Text>
|
||||
{inviterName} has invited you to join {teamName} as a {role}.
|
||||
</Text>
|
||||
<Text>
|
||||
React Email helps teams collaborate on email templates and manage
|
||||
their email campaigns effectively.
|
||||
</Text>
|
||||
<Text className="mb-6">
|
||||
Click the button below to accept the invitation and get started:
|
||||
</Text>
|
||||
<Row className="w-full">
|
||||
<Column className="w-full">
|
||||
<Button
|
||||
href={inviteLink}
|
||||
className="bg-cyan-300 text-[20px] font-bold text-[#404040] w-full text-center border border-solid border-cyan-900 py-[8px] rounded-[8px]"
|
||||
>
|
||||
Accept Invitation
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
<Text className="mt-6">- React Email team</Text>
|
||||
<Hr style={{ borderTopColor: '#404040' }} />
|
||||
<Text className="text-[#606060] font-bold">
|
||||
React Email, 999 React St, Email City, EC 12345
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
TeamInvite.PreviewProps = {
|
||||
inviterName: 'John Doe',
|
||||
teamName: 'Marketing Team',
|
||||
inviteLink: 'https://react.email/join/team/123',
|
||||
role: 'Editor',
|
||||
} satisfies TeamInviteProps;
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
Body,
|
||||
CodeBlock,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
vesper,
|
||||
} from '@react-email/components';
|
||||
|
||||
interface WebhooksFailedProps {
|
||||
date: string;
|
||||
error: {
|
||||
error: string;
|
||||
timestamp: string;
|
||||
webhookId: string;
|
||||
details: {
|
||||
reason: string;
|
||||
retryCount: number;
|
||||
lastAttempt: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function WebhooksFailed({ date, error }: WebhooksFailedProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-black text-white">
|
||||
<Preview>Some webhooks failed to deliver at {date}</Preview>
|
||||
<Container className="mx-auto">
|
||||
<Heading className="font-bold text-center text-[32px]">
|
||||
Failed webhook attempt
|
||||
</Heading>
|
||||
<Text>
|
||||
We encountered an issue with some of our webhooks. Please check
|
||||
the logs for more details. Here is the data for the error:
|
||||
</Text>
|
||||
<CodeBlock
|
||||
code={JSON.stringify(error, null, 2)}
|
||||
language="json"
|
||||
theme={vesper}
|
||||
style={{
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
lineNumbers
|
||||
/>
|
||||
<Text className="mb-6">
|
||||
If you have any questions or need assistance, please reach out to
|
||||
us at{' '}
|
||||
<a
|
||||
href="mailto:support@react.email"
|
||||
className="text-cyan-300 underline"
|
||||
>
|
||||
support@react.email
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
<Text className="mt-6">- React Email team</Text>
|
||||
<Hr style={{ borderTopColor: '#404040' }} />
|
||||
<Text className="text-[#606060] font-bold">
|
||||
React Email, 999 React St, Email City, EC 12345
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
WebhooksFailed.PreviewProps = {
|
||||
date: 'June 4th, 202',
|
||||
error: {
|
||||
error: 'Webhook delivery failed',
|
||||
timestamp: '2023-10-01T12:00:00Z',
|
||||
webhookId: 'wh_1234567890',
|
||||
details: {
|
||||
reason: 'Network error',
|
||||
retryCount: 3,
|
||||
lastAttempt: '2023-10-01T12:05:00Z',
|
||||
},
|
||||
},
|
||||
} satisfies WebhooksFailedProps;
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Row,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from '@react-email/components';
|
||||
|
||||
interface FeedbackRequestProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function FeedbackRequest({ name }: FeedbackRequestProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-black text-white">
|
||||
<Preview>
|
||||
Thank you for using our service. We would love to hear your
|
||||
feedback.
|
||||
</Preview>
|
||||
<Container className="mx-auto">
|
||||
<Heading className="font-bold text-center my-[48px] text-[32px]">
|
||||
Hello {name},
|
||||
</Heading>
|
||||
<Text>
|
||||
Thank you for using our service. We would love to hear your
|
||||
thoughts and suggestions.
|
||||
</Text>
|
||||
<Text>
|
||||
We work hard to improve our service and your feedback is crucial
|
||||
for us to understand what we are doing well and where we can
|
||||
improve.
|
||||
</Text>
|
||||
<Text className="mb-6">
|
||||
If you have any questions about the service or need assistance,
|
||||
please feel free to reach out to us at{' '}
|
||||
<a
|
||||
href="mailto:support@react.email"
|
||||
className="text-cyan-300 underline"
|
||||
>
|
||||
support@react.email
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
<Row className="w-full">
|
||||
<Column className="w-full">
|
||||
<Button
|
||||
href="https://react.email"
|
||||
className="bg-cyan-300 text-[20px] font-bold text-[#404040] w-full text-center border border-solid border-cyan-900 py-[8px] rounded-[8px]"
|
||||
>
|
||||
Give Feedback
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
<Text className="mt-6">- React Email team</Text>
|
||||
<Hr style={{ borderTopColor: '#404040' }} />
|
||||
<Text className="text-[#606060] font-bold">
|
||||
React Email, 999 React St, Email City, EC 12345
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
FeedbackRequest.PreviewProps = {
|
||||
name: 'user',
|
||||
} satisfies FeedbackRequestProps;
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from '@react-email/components';
|
||||
|
||||
interface ChangelogProps {
|
||||
receiver: string;
|
||||
changes: Partial<Record<'features' | 'fixes' | 'improvements', string[]>>;
|
||||
date: string;
|
||||
}
|
||||
|
||||
interface ChangesProps {
|
||||
title: React.ReactNode;
|
||||
content: string[];
|
||||
}
|
||||
|
||||
function Changes({ title, content }: ChangesProps) {
|
||||
return (
|
||||
<Section>
|
||||
<Heading as="h2" className="my-[12px]">
|
||||
{title}
|
||||
</Heading>
|
||||
<ul className="list-disc pl-6">
|
||||
{content.map((feature, index) => (
|
||||
<li key={index} className="mb-2">
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Changelog({ receiver, date, changes }: ChangelogProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-black text-white">
|
||||
<Preview>
|
||||
The changes as of {date} particularly tailored to you
|
||||
</Preview>
|
||||
<Container className="mx-auto">
|
||||
<Heading className="font-bold text-center my-[48px] text-[32px]">
|
||||
Hello {receiver},
|
||||
</Heading>
|
||||
<Text>
|
||||
We've been hard at work making improvements to our service, and we
|
||||
wanted to share the latest changes as of{' '}
|
||||
<span className="font-bold">{date}</span> with you.
|
||||
</Text>
|
||||
{changes.features && changes.features.length > 0 ? (
|
||||
<Changes title="Features" content={changes.features} />
|
||||
) : null}
|
||||
{changes.improvements && changes.improvements.length > 0 ? (
|
||||
<Changes title="Improvements" content={changes.improvements} />
|
||||
) : null}
|
||||
{changes.fixes && changes.fixes.length > 0 ? (
|
||||
<Changes title="Fixes" content={changes.fixes} />
|
||||
) : null}
|
||||
<Text className="mt-6">- React Email team</Text>
|
||||
<Hr style={{ borderTopColor: '#404040' }} />
|
||||
<Text className="text-[#606060] font-bold">
|
||||
React Email, 999 React St, Email City, EC 12345
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
Changelog.PreviewProps = {
|
||||
receiver: 'user',
|
||||
changes: {
|
||||
features: [
|
||||
'Added a new feature to enhance user experience',
|
||||
'Introduced a dark mode option for better accessibility',
|
||||
],
|
||||
fixes: [
|
||||
'Fixed a bug that caused the app to crash on startup',
|
||||
'Resolved an issue with email notifications not being sent',
|
||||
],
|
||||
improvements: [
|
||||
'Improved performance of the application by optimizing database queries',
|
||||
'Enhanced security measures to protect user data',
|
||||
],
|
||||
},
|
||||
date: 'June 4th, 2025',
|
||||
} satisfies ChangelogProps;
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import { getEmailsDirectoryMetadataAction } from '../actions/get-emails-directory-metadata-action';
|
||||
import { isBuilding } from '../app/env';
|
||||
import { isBuilding, isPreviewDevelopment } from '../app/env';
|
||||
import { useHotreload } from '../hooks/use-hot-reload';
|
||||
import type { EmailsDirectory } from '../utils/get-emails-directory-metadata';
|
||||
|
||||
@@ -31,7 +31,7 @@ export const EmailsProvider = (props: {
|
||||
const [emailsDirectoryMetadata, setEmailsDirectoryMetadata] =
|
||||
useState<EmailsDirectory>(props.initialEmailsDirectoryMetadata);
|
||||
|
||||
if (!isBuilding) {
|
||||
if (!isBuilding && !isPreviewDevelopment) {
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: this will not change on runtime so it doesn't violate the rules of hooks
|
||||
useHotreload(async () => {
|
||||
const metadata = await getEmailsDirectoryMetadataAction(
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
EmailRenderingResult,
|
||||
RenderedEmailMetadata,
|
||||
} from '../actions/render-email-by-path';
|
||||
import { isBuilding } from '../app/env';
|
||||
import { isBuilding, isPreviewDevelopment } from '../app/env';
|
||||
import { useEmailRenderingResult } from '../hooks/use-email-rendering-result';
|
||||
import { useHotreload } from '../hooks/use-hot-reload';
|
||||
import { useRenderingMetadata } from '../hooks/use-rendering-metadata';
|
||||
@@ -49,7 +49,7 @@ export const PreviewProvider = ({
|
||||
serverRenderingResult,
|
||||
);
|
||||
|
||||
if (!isBuilding) {
|
||||
if (!isBuilding && !isPreviewDevelopment) {
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: this will not change on runtime so it doesn't violate the rules of hooks
|
||||
useHotreload((changes) => {
|
||||
const changeForThisEmail = changes.find((change) =>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type EmailRenderingResult,
|
||||
renderEmailByPath,
|
||||
} from '../actions/render-email-by-path';
|
||||
import { isBuilding } from '../app/env';
|
||||
import { isBuilding, isPreviewDevelopment } from '../app/env';
|
||||
import { useEmails } from '../contexts/emails';
|
||||
import { containsEmailTemplate } from '../utils/contains-email-template';
|
||||
import { useHotreload } from './use-hot-reload';
|
||||
@@ -19,7 +19,7 @@ export const useEmailRenderingResult = (
|
||||
|
||||
const { emailsDirectoryMetadata } = useEmails();
|
||||
|
||||
if (!isBuilding) {
|
||||
if (!isBuilding && !isPreviewDevelopment) {
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: This is fine since isBuilding does not change at runtime
|
||||
useHotreload(async (changes) => {
|
||||
for await (const change of changes) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import path from 'node:path';
|
||||
import { isDev } from './start-dev-server.js';
|
||||
|
||||
export const getEnvVariablesForPreviewApp = (
|
||||
relativePathToEmailsDirectory: string,
|
||||
@@ -9,6 +8,5 @@ export const getEnvVariablesForPreviewApp = (
|
||||
EMAILS_DIR_RELATIVE_PATH: relativePathToEmailsDirectory,
|
||||
EMAILS_DIR_ABSOLUTE_PATH: path.resolve(cwd, relativePathToEmailsDirectory),
|
||||
USER_PROJECT_LOCATION: cwd,
|
||||
NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT: isDev ? 'true' : 'false',
|
||||
} as const;
|
||||
};
|
||||
|
||||
+1
-73
@@ -44,52 +44,21 @@ test('createDependencyGraph()', async () => {
|
||||
};
|
||||
|
||||
const initialDependencyGraph = convertPathsToAbsolute({
|
||||
'../../../../package.json': {
|
||||
dependencyPaths: [],
|
||||
dependentPaths: ['../../packageJson.ts'],
|
||||
moduleDependencies: [],
|
||||
path: '../../../../package.json',
|
||||
},
|
||||
'create-dependency-graph.ts': {
|
||||
path: 'create-dependency-graph.ts',
|
||||
dependencyPaths: [
|
||||
'../start-dev-server.ts',
|
||||
'get-imported-modules.ts',
|
||||
'resolve-path-aliases.ts',
|
||||
],
|
||||
dependencyPaths: ['get-imported-modules.ts', 'resolve-path-aliases.ts'],
|
||||
dependentPaths: [
|
||||
'create-dependency-graph.spec.ts',
|
||||
'setup-hot-reloading.ts',
|
||||
],
|
||||
moduleDependencies: ['node:fs', 'node:path', 'chokidar/handler.js'],
|
||||
},
|
||||
'../../get-preview-server-location.ts': {
|
||||
dependencyPaths: ['../../packageJson.ts'],
|
||||
dependentPaths: ['../../preview/start-dev-server.ts'],
|
||||
moduleDependencies: ['node:path', 'node:url', 'jiti', 'nypm', 'prompts'],
|
||||
path: '../../get-preview-server-location.ts',
|
||||
},
|
||||
'../../packageJson.ts': {
|
||||
dependencyPaths: ['../../../../package.json'],
|
||||
dependentPaths: [
|
||||
'../../get-preview-server-location.ts',
|
||||
'../../preview/start-dev-server.ts',
|
||||
],
|
||||
moduleDependencies: [],
|
||||
path: '../../packageJson.ts',
|
||||
},
|
||||
'create-dependency-graph.spec.ts': {
|
||||
path: 'create-dependency-graph.spec.ts',
|
||||
dependencyPaths: ['create-dependency-graph.ts'],
|
||||
dependentPaths: [],
|
||||
moduleDependencies: ['node:fs', 'node:path'],
|
||||
},
|
||||
'../get-env-variables-for-preview-app.ts': {
|
||||
dependencyPaths: ['../../preview/start-dev-server.ts'],
|
||||
dependentPaths: ['../../preview/start-dev-server.ts'],
|
||||
moduleDependencies: ['node:path'],
|
||||
path: '../../preview/get-env-variables-for-preview-app.ts',
|
||||
},
|
||||
'./test/some-file.ts': {
|
||||
dependencyPaths: [],
|
||||
dependentPaths: [],
|
||||
@@ -141,47 +110,6 @@ test('createDependencyGraph()', async () => {
|
||||
'socket.io',
|
||||
],
|
||||
},
|
||||
'../start-dev-server.ts': {
|
||||
dependencyPaths: [
|
||||
'../../register-spinner-autostopping.ts',
|
||||
'../../get-preview-server-location.ts',
|
||||
'../../packageJson.ts',
|
||||
'../../preview/get-env-variables-for-preview-app.ts',
|
||||
'../../preview/serve-static-file.ts',
|
||||
],
|
||||
path: '../start-dev-server.ts',
|
||||
dependentPaths: [
|
||||
'../../preview/get-env-variables-for-preview-app.ts',
|
||||
'create-dependency-graph.ts',
|
||||
],
|
||||
moduleDependencies: [
|
||||
'node:http',
|
||||
'node:path',
|
||||
'node:url',
|
||||
'chalk',
|
||||
'jiti',
|
||||
'log-symbols',
|
||||
'ora',
|
||||
],
|
||||
},
|
||||
'../../preview/serve-static-file.ts': {
|
||||
dependencyPaths: [],
|
||||
dependentPaths: ['../../preview/start-dev-server.ts'],
|
||||
moduleDependencies: [
|
||||
'node:fs',
|
||||
'node:http',
|
||||
'node:path',
|
||||
'node:url',
|
||||
'mime-types',
|
||||
],
|
||||
path: '../../preview/serve-static-file.ts',
|
||||
},
|
||||
'../../register-spinner-autostopping.ts': {
|
||||
dependencyPaths: [],
|
||||
dependentPaths: ['../../preview/start-dev-server.ts'],
|
||||
moduleDependencies: ['log-symbols', 'ora'],
|
||||
path: '../../register-spinner-autostopping.ts',
|
||||
},
|
||||
'../../types/hot-reload-event.ts': {
|
||||
dependencyPaths: [],
|
||||
dependentPaths: ['../../types/hot-reload-change.ts'],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { existsSync, promises as fs, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { EventName } from 'chokidar/handler.js';
|
||||
import { isDev } from '../start-dev-server.js';
|
||||
import { getImportedModules } from './get-imported-modules.js';
|
||||
import { resolvePathAliases } from './resolve-path-aliases.js';
|
||||
|
||||
@@ -134,8 +133,7 @@ export const createDependencyGraph = async (directory: string) => {
|
||||
);
|
||||
if (pathWithExtension) {
|
||||
pathToDependencyFromDirectory = pathWithExtension;
|
||||
} else if (isDev) {
|
||||
// only warn about this on development as it is probably going to be irrelevant otherwise
|
||||
} else {
|
||||
console.warn(
|
||||
`Could not find index file for directory at ${pathToDependencyFromDirectory}. This is probably going to cause issues with both hot reloading and your code.`,
|
||||
);
|
||||
@@ -157,8 +155,7 @@ export const createDependencyGraph = async (directory: string) => {
|
||||
|
||||
if (pathWithEnsuredExtension) {
|
||||
pathToDependencyFromDirectory = pathWithEnsuredExtension;
|
||||
} else if (isDev) {
|
||||
// only warn about this on development as it is probably going to be irrelevant otherwise
|
||||
} else {
|
||||
console.warn(
|
||||
`Could not find file at ${pathToDependencyFromDirectory}`,
|
||||
);
|
||||
|
||||
@@ -27,11 +27,6 @@ const safeAsyncServerListen = (server: http.Server, port: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
const filename = url.fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
export const isDev = !dirname.includes('dist');
|
||||
|
||||
export const startDevServer = async (
|
||||
emailsDirRelativePath: string,
|
||||
staticBaseDirRelativePath: string,
|
||||
@@ -143,7 +138,7 @@ export const startDevServer = async (
|
||||
|
||||
const app = next({
|
||||
// passing in env here does not get the environment variables there
|
||||
dev: isDev,
|
||||
dev: false,
|
||||
conf: {
|
||||
images: {
|
||||
// This is to avoid the warning with sharp
|
||||
|
||||
Generated
+4
-1
@@ -41,6 +41,9 @@ importers:
|
||||
tsup:
|
||||
specifier: 8.4.0
|
||||
version: 8.4.0(@microsoft/api-extractor@7.52.4(@types/node@22.14.1))(@swc/core@1.11.21)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.8.3)(yaml@2.6.1)
|
||||
tsx:
|
||||
specifier: 4.19.3
|
||||
version: 4.19.3
|
||||
turbo:
|
||||
specifier: 2.5.4
|
||||
version: 2.5.4
|
||||
@@ -17006,7 +17009,7 @@ snapshots:
|
||||
picomatch: 4.0.2
|
||||
postcss: 8.5.3
|
||||
rollup: 4.39.0
|
||||
tinyglobby: 0.2.13
|
||||
tinyglobby: 0.2.14
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
Reference in New Issue
Block a user