Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/custom-extension-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-email": minor
---

Enable custom export extensions via --extension/-e (e.g. .blade.php).
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
15 changes: 11 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,15 @@ export const exportTemplates = async (
},
);

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

for await (const template of allBuiltTemplates) {
try {
if (spinner) {
Expand All @@ -134,10 +144,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