Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
4 changes: 4 additions & 0 deletions apps/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ Generates the plain HTML files of your emails into a `out` directory.
<ResponseField name="--dir" type="string" default="emails">
Change the directory of your email templates.
</ResponseField>
<ResponseField name="--extension" type="string">
Set a custom file extension for rendered templates (for example, `blade.php`). When omitted,
the extension defaults to `.html`, or `.txt` when `--plainText` is enabled.
</ResponseField>

## `email help <cmd>`

Expand Down
13 changes: 9 additions & 4 deletions packages/react-email/src/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const getEmailTemplatesFromDirectory = (emailDirectory: EmailsDirectory) => {
};

type ExportTemplatesOptions = Options & {
extension?: string;
silent?: boolean;
pretty?: boolean;
};
Expand Down Expand Up @@ -115,6 +116,13 @@ export const exportTemplates = async (
},
);

const extension =
options.extension && options.extension.length > 0
? `.${options.extension}`
: options.plainText
? '.txt'
: '.html';

for await (const template of allBuiltTemplates) {
try {
if (spinner) {
Expand All @@ -134,10 +142,7 @@ export const exportTemplates = async (
emailModule.reactEmailCreateReactElement(emailModule.default, {}),
options,
);
const htmlPath = template.replace(
'.cjs',
options.plainText ? '.txt' : '.html',
);
const htmlPath = template.replace('.cjs', extension);
writeFileSync(htmlPath, rendered);
unlinkSync(template);
} catch (exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,199 @@ exports[`email export 1`] = `
</html>
"
`;

exports[`email export with custom extension 1`] = `
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<link rel="preload" as="image" href="/static/vercel-logo.png" />
<link rel="preload" as="image" href="/static/vercel-arrow.png" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
</head>
<body
style='margin-left:auto;margin-right:auto;margin-top:auto;margin-bottom:auto;background-color:rgb(255,255,255);padding-left:8px;padding-right:8px;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'>
<table
border="0"
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
align="center">
<tbody>
<tr>
<td
style='margin-left:auto;margin-right:auto;margin-top:auto;margin-bottom:auto;background-color:rgb(255,255,255);padding-left:8px;padding-right:8px;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'>
<div
style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0"
data-skip-in-text="true">
Join undefined on Vercel
<div>
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
</div>
</div>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;max-width:465px;border-radius:0.25rem;border-width:1px;border-color:rgb(234,234,234);border-style:solid;padding:20px">
<tbody>
<tr style="width:100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top:32px">
<tbody>
<tr>
<td>
<img
alt="Vercel Logo"
height="37"
src="/static/vercel-logo.png"
style="margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;display:block;outline:none;border:none;text-decoration:none"
width="40" />
</td>
</tr>
</tbody>
</table>
<h1
style="margin-left:0;margin-right:0;margin-top:30px;margin-bottom:30px;padding:0;text-align:center;font-weight:400;font-size:24px;color:rgb(0,0,0)">
Join <strong></strong> on <strong>Vercel</strong>
</h1>
<p
style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px">
Hello
<!-- -->,
</p>
<p
style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px">
<strong></strong> (<a
href="mailto:undefined"
style="color:rgb(37,99,235);text-decoration-line:none"
target="_blank"></a
>) has invited you to the <strong></strong> team on<!-- -->
<strong>Vercel</strong>.
</p>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation">
<tbody>
<tr>
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation">
<tbody style="width:100%">
<tr style="width:100%">
<td
align="right"
data-id="__react-email-column">
<img
alt="undefined&#x27;s profile picture"
height="64"
style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none"
width="64" />
</td>
<td
align="center"
data-id="__react-email-column">
<img
alt="Arrow indicating invitation"
height="9"
src="/static/vercel-arrow.png"
style="display:block;outline:none;border:none;text-decoration:none"
width="12" />
</td>
<td
align="left"
data-id="__react-email-column">
<img
alt="undefined team logo"
height="64"
style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none"
width="64" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top:32px;margin-bottom:32px;text-align:center">
<tbody>
<tr>
<td>
<a
style="border-radius:0.25rem;background-color:rgb(0,0,0);padding-left:20px;padding-right:20px;padding-top:12px;padding-bottom:12px;text-align:center;font-weight:600;font-size:12px;color:rgb(255,255,255);text-decoration-line:none;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px"
target="_blank"
><span
><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span
><span
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px"
>Join the team</span
><span
><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span
></a
>
</td>
</tr>
</tbody>
</table>
<p
style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px">
or copy and paste this URL into your browser:<!-- -->
<a
style="color:rgb(37,99,235);text-decoration-line:none"
target="_blank"></a>
</p>
<hr
style="margin-left:0;margin-right:0;margin-top:26px;margin-bottom:26px;width:100%;border-width:1px;border-color:rgb(234,234,234);border-style:solid;border:none;border-top:1px solid #eaeaea" />
<p
style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-top:16px;margin-bottom:16px">
This invitation was intended for<!-- -->
<span style="color:rgb(0,0,0)"></span>. This invite was
sent from <span style="color:rgb(0,0,0)"></span>
<!-- -->located in<!-- -->
<span style="color:rgb(0,0,0)"></span>. If you were not
expecting this invitation, you can ignore this email. If
you are concerned about your account&#x27;s safety, please
reply to this email to get in touch with us.
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--/$-->
</body>
</html>
"
`;
19 changes: 19 additions & 0 deletions packages/react-email/src/commands/testing/export.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,22 @@ test('email export', { retry: 3 }, async () => {
),
).toMatchSnapshot();
});

test('email export with custom extension', { retry: 3 }, async () => {
const pathToEmailsDirectory = path.resolve(__dirname, './emails');
const pathToDumpMarkup = path.resolve(__dirname, './out');

await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, {
silent: true,
pretty: true,
extension: 'blade.php',
});

const outputFile = path.resolve(
pathToDumpMarkup,
'./vercel-invite-user.blade.php',
);

expect(fs.existsSync(outputFile)).toBe(true);
expect(await fs.promises.readFile(outputFile, 'utf8')).toMatchSnapshot();
});
8 changes: 6 additions & 2 deletions packages/react-email/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ program
.option('-p, --pretty', 'Pretty print the output', false)
.option('-t, --plainText', 'Set output format as plain text', false)
.option('-d, --dir <path>', 'Directory with your email templates', './emails')
.option(
'-e, --extension <extension>',
'Set a custom file extension for rendered emails (e.g. blade.php)',
)
.option(
'-s, --silent',
'To, or not to show a spinner with process information',
false,
)
.action(({ outDir, pretty, plainText, silent, dir: srcDir }) =>
exportTemplates(outDir, srcDir, { silent, plainText, pretty }),
.action(({ outDir, pretty, plainText, silent, dir: srcDir, extension }) =>
exportTemplates(outDir, srcDir, { silent, plainText, pretty, extension }),
);

program.parse();
Loading