Skip to content

Commit ecb743a

Browse files
committed
support custom export extension
1 parent 88205c9 commit ecb743a

File tree

5 files changed

+234
-6
lines changed

5 files changed

+234
-6
lines changed

apps/docs/cli.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ Generates the plain HTML files of your emails into a `out` directory.
190190
<ResponseField name="--dir" type="string" default="emails">
191191
Change the directory of your email templates.
192192
</ResponseField>
193+
<ResponseField name="--extension" type="string">
194+
Set a custom file extension for rendered templates (for example, `blade.php`). When omitted,
195+
the extension defaults to `.txt` if `--plainText` is enabled, otherwise `.html`.
196+
</ResponseField>
193197

194198
## `email help <cmd>`
195199

packages/react-email/src/commands/export.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const getEmailTemplatesFromDirectory = (emailDirectory: EmailsDirectory) => {
3030
};
3131

3232
type ExportTemplatesOptions = Options & {
33+
extension?: string;
3334
silent?: boolean;
3435
pretty?: boolean;
3536
};
@@ -115,6 +116,13 @@ export const exportTemplates = async (
115116
},
116117
);
117118

119+
const extension =
120+
options.extension && options.extension.length > 0
121+
? `.${options.extension}`
122+
: options.plainText
123+
? '.txt'
124+
: '.html';
125+
118126
for await (const template of allBuiltTemplates) {
119127
try {
120128
if (spinner) {
@@ -134,10 +142,7 @@ export const exportTemplates = async (
134142
emailModule.reactEmailCreateReactElement(emailModule.default, {}),
135143
options,
136144
);
137-
const htmlPath = template.replace(
138-
'.cjs',
139-
options.plainText ? '.txt' : '.html',
140-
);
145+
const htmlPath = template.replace('.cjs', extension);
141146
writeFileSync(htmlPath, rendered);
142147
unlinkSync(template);
143148
} catch (exception) {

packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,199 @@ exports[`email export 1`] = `
195195
</html>
196196
"
197197
`;
198+
199+
exports[`email export with custom extension 1`] = `
200+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
201+
<html dir="ltr" lang="en">
202+
<head>
203+
<link rel="preload" as="image" href="/static/vercel-logo.png" />
204+
<link rel="preload" as="image" href="/static/vercel-arrow.png" />
205+
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
206+
<meta name="x-apple-disable-message-reformatting" />
207+
<!--$-->
208+
</head>
209+
<body
210+
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"'>
211+
<table
212+
border="0"
213+
width="100%"
214+
cellpadding="0"
215+
cellspacing="0"
216+
role="presentation"
217+
align="center">
218+
<tbody>
219+
<tr>
220+
<td
221+
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"'>
222+
<div
223+
style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0"
224+
data-skip-in-text="true">
225+
Join undefined on Vercel
226+
<div>
227+
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
228+
</div>
229+
</div>
230+
<table
231+
align="center"
232+
width="100%"
233+
border="0"
234+
cellpadding="0"
235+
cellspacing="0"
236+
role="presentation"
237+
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">
238+
<tbody>
239+
<tr style="width:100%">
240+
<td>
241+
<table
242+
align="center"
243+
width="100%"
244+
border="0"
245+
cellpadding="0"
246+
cellspacing="0"
247+
role="presentation"
248+
style="margin-top:32px">
249+
<tbody>
250+
<tr>
251+
<td>
252+
<img
253+
alt="Vercel Logo"
254+
height="37"
255+
src="/static/vercel-logo.png"
256+
style="margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;display:block;outline:none;border:none;text-decoration:none"
257+
width="40" />
258+
</td>
259+
</tr>
260+
</tbody>
261+
</table>
262+
<h1
263+
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)">
264+
Join <strong></strong> on <strong>Vercel</strong>
265+
</h1>
266+
<p
267+
style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px">
268+
Hello
269+
<!-- -->,
270+
</p>
271+
<p
272+
style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px">
273+
<strong></strong> (<a
274+
href="mailto:undefined"
275+
style="color:rgb(37,99,235);text-decoration-line:none"
276+
target="_blank"></a
277+
>) has invited you to the <strong></strong> team on<!-- -->
278+
<strong>Vercel</strong>.
279+
</p>
280+
<table
281+
align="center"
282+
width="100%"
283+
border="0"
284+
cellpadding="0"
285+
cellspacing="0"
286+
role="presentation">
287+
<tbody>
288+
<tr>
289+
<td>
290+
<table
291+
align="center"
292+
width="100%"
293+
border="0"
294+
cellpadding="0"
295+
cellspacing="0"
296+
role="presentation">
297+
<tbody style="width:100%">
298+
<tr style="width:100%">
299+
<td
300+
align="right"
301+
data-id="__react-email-column">
302+
<img
303+
alt="undefined&#x27;s profile picture"
304+
height="64"
305+
style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none"
306+
width="64" />
307+
</td>
308+
<td
309+
align="center"
310+
data-id="__react-email-column">
311+
<img
312+
alt="Arrow indicating invitation"
313+
height="9"
314+
src="/static/vercel-arrow.png"
315+
style="display:block;outline:none;border:none;text-decoration:none"
316+
width="12" />
317+
</td>
318+
<td
319+
align="left"
320+
data-id="__react-email-column">
321+
<img
322+
alt="undefined team logo"
323+
height="64"
324+
style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none"
325+
width="64" />
326+
</td>
327+
</tr>
328+
</tbody>
329+
</table>
330+
</td>
331+
</tr>
332+
</tbody>
333+
</table>
334+
<table
335+
align="center"
336+
width="100%"
337+
border="0"
338+
cellpadding="0"
339+
cellspacing="0"
340+
role="presentation"
341+
style="margin-top:32px;margin-bottom:32px;text-align:center">
342+
<tbody>
343+
<tr>
344+
<td>
345+
<a
346+
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"
347+
target="_blank"
348+
><span
349+
><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span
350+
><span
351+
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px"
352+
>Join the team</span
353+
><span
354+
><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span
355+
></a
356+
>
357+
</td>
358+
</tr>
359+
</tbody>
360+
</table>
361+
<p
362+
style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px">
363+
or copy and paste this URL into your browser:<!-- -->
364+
<a
365+
style="color:rgb(37,99,235);text-decoration-line:none"
366+
target="_blank"></a>
367+
</p>
368+
<hr
369+
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" />
370+
<p
371+
style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-top:16px;margin-bottom:16px">
372+
This invitation was intended for<!-- -->
373+
<span style="color:rgb(0,0,0)"></span>. This invite was
374+
sent from <span style="color:rgb(0,0,0)"></span>
375+
<!-- -->located in<!-- -->
376+
<span style="color:rgb(0,0,0)"></span>. If you were not
377+
expecting this invitation, you can ignore this email. If
378+
you are concerned about your account&#x27;s safety, please
379+
reply to this email to get in touch with us.
380+
</p>
381+
</td>
382+
</tr>
383+
</tbody>
384+
</table>
385+
</td>
386+
</tr>
387+
</tbody>
388+
</table>
389+
<!--/$-->
390+
</body>
391+
</html>
392+
"
393+
`;

packages/react-email/src/commands/testing/export.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,22 @@ test('email export', { retry: 3 }, async () => {
1818
),
1919
).toMatchSnapshot();
2020
});
21+
22+
test('email export with custom extension', { retry: 3 }, async () => {
23+
const pathToEmailsDirectory = path.resolve(__dirname, './emails');
24+
const pathToDumpMarkup = path.resolve(__dirname, './out');
25+
26+
await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, {
27+
silent: true,
28+
pretty: true,
29+
extension: 'blade.php',
30+
});
31+
32+
const outputFile = path.resolve(
33+
pathToDumpMarkup,
34+
'./vercel-invite-user.blade.php',
35+
);
36+
37+
expect(fs.existsSync(outputFile)).toBe(true);
38+
expect(await fs.promises.readFile(outputFile, 'utf8')).toMatchSnapshot();
39+
});

packages/react-email/src/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@ program
4343
.option('-p, --pretty', 'Pretty print the output', false)
4444
.option('-t, --plainText', 'Set output format as plain text', false)
4545
.option('-d, --dir <path>', 'Directory with your email templates', './emails')
46+
.option(
47+
'-e, --extension <extension>',
48+
'Set a custom file extension for rendered emails (e.g. blade.php)',
49+
)
4650
.option(
4751
'-s, --silent',
4852
'To, or not to show a spinner with process information',
4953
false,
5054
)
51-
.action(({ outDir, pretty, plainText, silent, dir: srcDir }) =>
52-
exportTemplates(outDir, srcDir, { silent, plainText, pretty }),
55+
.action(({ outDir, pretty, plainText, silent, dir: srcDir, extension }) =>
56+
exportTemplates(outDir, srcDir, { silent, plainText, pretty, extension }),
5357
);
5458

5559
program.parse();

0 commit comments

Comments
 (0)