react-email 6.6.8 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/cli/index.mjs +10 -10
- package/dist/index.cjs +3 -1
- package/dist/index.mjs +3 -1
- package/package.json +3 -3
- package/src/components/tailwind/tailwind.spec.tsx +34 -0
- package/src/components/tailwind/utils/css/extract-rules-per-class.spec.ts +23 -0
- package/src/components/tailwind/utils/css/extract-rules-per-class.ts +17 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# react-email
|
|
2
2
|
|
|
3
|
+
## 6.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b448c3b: Enable custom export extensions via --extension/-e (e.g. .blade.php).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [ba96cfa]
|
|
12
|
+
- Updated dependencies [58d8c08]
|
|
13
|
+
- @react-email/render@2.1.0
|
|
14
|
+
|
|
15
|
+
## 6.6.9
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- bc2f7e3: Fix `group` utilities including bad rules that don't work
|
|
20
|
+
|
|
3
21
|
## 6.6.8
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/dist/cli/index.mjs
CHANGED
|
@@ -6533,7 +6533,7 @@ const getTracingRootDir = async (usersProjectLocation) => {
|
|
|
6533
6533
|
//#region package.json
|
|
6534
6534
|
var package_default = {
|
|
6535
6535
|
name: "react-email",
|
|
6536
|
-
version: "6.
|
|
6536
|
+
version: "6.7.0",
|
|
6537
6537
|
description: "A live preview of your emails right in your browser.",
|
|
6538
6538
|
bin: { "email": "./dist/cli/index.mjs" },
|
|
6539
6539
|
type: "module",
|
|
@@ -6572,7 +6572,7 @@ var package_default = {
|
|
|
6572
6572
|
dependencies: {
|
|
6573
6573
|
"@babel/parser": "catalog:",
|
|
6574
6574
|
"@babel/traverse": "catalog:",
|
|
6575
|
-
"@react-email/render": "workspace:>=2.0
|
|
6575
|
+
"@react-email/render": "workspace:>=2.1.0",
|
|
6576
6576
|
"chokidar": "^4.0.3",
|
|
6577
6577
|
"commander": "catalog:",
|
|
6578
6578
|
"conf": "^15.0.2",
|
|
@@ -7541,7 +7541,7 @@ const renderWorkerSource = `
|
|
|
7541
7541
|
const { unlinkSync, writeFileSync } = require('node:fs');
|
|
7542
7542
|
const { parentPort, workerData } = require('node:worker_threads');
|
|
7543
7543
|
|
|
7544
|
-
const { templates, options } = workerData;
|
|
7544
|
+
const { templates, options, extension } = workerData;
|
|
7545
7545
|
|
|
7546
7546
|
(async () => {
|
|
7547
7547
|
for (const template of templates) {
|
|
@@ -7551,10 +7551,7 @@ const { templates, options } = workerData;
|
|
|
7551
7551
|
emailModule.reactEmailCreateReactElement(emailModule.default, {}),
|
|
7552
7552
|
options,
|
|
7553
7553
|
);
|
|
7554
|
-
const htmlPath = template.replace(
|
|
7555
|
-
'.cjs',
|
|
7556
|
-
options.plainText ? '.txt' : '.html',
|
|
7557
|
-
);
|
|
7554
|
+
const htmlPath = template.replace('.cjs', extension);
|
|
7558
7555
|
writeFileSync(htmlPath, rendered);
|
|
7559
7556
|
unlinkSync(template);
|
|
7560
7557
|
parentPort.postMessage({ type: 'progress', template });
|
|
@@ -7616,6 +7613,7 @@ const exportTemplates = async (pathToWhereEmailMarkupShouldBeDumped, emailsDirec
|
|
|
7616
7613
|
}
|
|
7617
7614
|
if (spinner) spinner.succeed();
|
|
7618
7615
|
const allBuiltTemplates = glob.sync(normalize$1(`${pathToWhereEmailMarkupShouldBeDumped}/**/*.cjs`), { absolute: true });
|
|
7616
|
+
const extension = options.extension && options.extension.length > 0 ? options.extension.startsWith(".") ? options.extension : `.${options.extension}` : options.plainText ? ".txt" : ".html";
|
|
7619
7617
|
if (spinner && allBuiltTemplates.length > 0) {
|
|
7620
7618
|
spinner.setText(`rendering ${allBuiltTemplates[0]?.split("/").pop()}`);
|
|
7621
7619
|
spinner.start();
|
|
@@ -7630,7 +7628,8 @@ const exportTemplates = async (pathToWhereEmailMarkupShouldBeDumped, emailsDirec
|
|
|
7630
7628
|
eval: true,
|
|
7631
7629
|
workerData: {
|
|
7632
7630
|
templates: batch,
|
|
7633
|
-
options
|
|
7631
|
+
options,
|
|
7632
|
+
extension
|
|
7634
7633
|
}
|
|
7635
7634
|
});
|
|
7636
7635
|
worker.on("message", (msg) => {
|
|
@@ -7786,10 +7785,11 @@ else {
|
|
|
7786
7785
|
program.command("dev").description("Starts the preview email development app").option("-d, --dir <path>", "Directory with your email templates", "./emails").option("-p --port <port>", "Port to run dev server on", "3000").option("-c, --clients <clients>", "Comma-separated list of email clients to show compatibility warnings for (overrides COMPATIBILITY_EMAIL_CLIENTS)", parseClientsOption).action(dev);
|
|
7787
7786
|
program.command("build").description("Copies the preview app for onto .react-email and builds it").option("-d, --dir <path>", "Directory with your email templates", "./emails").addOption(new Option("-p, --packageManager <name>").hideHelp()).action(build$1);
|
|
7788
7787
|
program.command("start").description("Runs the built preview app that is inside of \".react-email\"").action(start);
|
|
7789
|
-
program.command("export").description("Build the templates to the `out` directory").option("--outDir <path>", "Output directory", "out").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("-s, --silent", "To, or not to show a spinner with process information", false).action(({ outDir, pretty, plainText, silent, dir: srcDir }) => exportTemplates(outDir, srcDir, {
|
|
7788
|
+
program.command("export").description("Build the templates to the `out` directory").option("--outDir <path>", "Output directory", "out").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, extension }) => exportTemplates(outDir, srcDir, {
|
|
7790
7789
|
silent,
|
|
7791
7790
|
plainText,
|
|
7792
|
-
pretty
|
|
7791
|
+
pretty,
|
|
7792
|
+
extension
|
|
7793
7793
|
}));
|
|
7794
7794
|
const resend = program.command("resend");
|
|
7795
7795
|
resend.command("setup").description("Sets up the integration between the React Email CLI, and your Resend account through an API Key").action(resendSetup);
|
package/dist/index.cjs
CHANGED
|
@@ -37710,8 +37710,10 @@ function extractRulesPerClass(root, classes) {
|
|
|
37710
37710
|
walk(root, {
|
|
37711
37711
|
visit: "Rule",
|
|
37712
37712
|
enter(rule) {
|
|
37713
|
+
const firstSelector = rule.prelude.type === "SelectorList" ? rule.prelude.children.first : null;
|
|
37714
|
+
if (firstSelector?.type === "Selector" && firstSelector.children.first?.type === "NestingSelector") return;
|
|
37713
37715
|
const selectorClasses = [];
|
|
37714
|
-
walk(rule, {
|
|
37716
|
+
walk(rule.prelude, {
|
|
37715
37717
|
visit: "ClassSelector",
|
|
37716
37718
|
enter(classSelector) {
|
|
37717
37719
|
selectorClasses.push(decode$1(classSelector.name));
|
package/dist/index.mjs
CHANGED
|
@@ -37689,8 +37689,10 @@ function extractRulesPerClass(root, classes) {
|
|
|
37689
37689
|
walk(root, {
|
|
37690
37690
|
visit: "Rule",
|
|
37691
37691
|
enter(rule) {
|
|
37692
|
+
const firstSelector = rule.prelude.type === "SelectorList" ? rule.prelude.children.first : null;
|
|
37693
|
+
if (firstSelector?.type === "Selector" && firstSelector.children.first?.type === "NestingSelector") return;
|
|
37692
37694
|
const selectorClasses = [];
|
|
37693
|
-
walk(rule, {
|
|
37695
|
+
walk(rule.prelude, {
|
|
37694
37696
|
visit: "ClassSelector",
|
|
37695
37697
|
enter(classSelector) {
|
|
37696
37698
|
selectorClasses.push(decode$1(classSelector.name));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-email",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.0",
|
|
4
4
|
"description": "A live preview of your emails right in your browser.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"email": "./dist/cli/index.mjs"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@babel/parser": "7.27.0",
|
|
39
39
|
"@babel/traverse": "7.27.0",
|
|
40
|
-
"@react-email/render": ">=2.0
|
|
40
|
+
"@react-email/render": ">=2.1.0",
|
|
41
41
|
"chokidar": "^4.0.3",
|
|
42
42
|
"commander": "^13.0.0",
|
|
43
43
|
"conf": "^15.0.2",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"tsx": "4.21.0",
|
|
75
75
|
"typescript": "5.9.3",
|
|
76
76
|
"yalc": "1.0.0-pre.53",
|
|
77
|
-
"@react-email/render": "2.0
|
|
77
|
+
"@react-email/render": "2.1.0"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"build": "tsdown",
|
|
@@ -440,6 +440,40 @@ describe('Tailwind component', () => {
|
|
|
440
440
|
);
|
|
441
441
|
});
|
|
442
442
|
|
|
443
|
+
// See https://github.com/resend/react-email/pull/3594
|
|
444
|
+
it('does not leak a bare nested group/peer rule into the <head> <style>', async () => {
|
|
445
|
+
const html = await render(
|
|
446
|
+
<Html>
|
|
447
|
+
<Tailwind>
|
|
448
|
+
<Head />
|
|
449
|
+
<div className="group">
|
|
450
|
+
<a className="group-hover:underline" href="https://react.email">
|
|
451
|
+
link
|
|
452
|
+
</a>
|
|
453
|
+
</div>
|
|
454
|
+
</Tailwind>
|
|
455
|
+
</Html>,
|
|
456
|
+
).then(pretty);
|
|
457
|
+
|
|
458
|
+
expect(html).toMatchInlineSnapshot(`
|
|
459
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
460
|
+
<html dir="ltr" lang="en">
|
|
461
|
+
<head>
|
|
462
|
+
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
|
463
|
+
<meta name="x-apple-disable-message-reformatting" />
|
|
464
|
+
<style>
|
|
465
|
+
.group-hover_underline{@media (hover:hover){&:is(:where(.group):hover *){text-decoration-line:underline!important}}}
|
|
466
|
+
</style></head
|
|
467
|
+
><!--$--><!--html--><!--head-->
|
|
468
|
+
<div class="group">
|
|
469
|
+
<a class="group-hover_underline" href="https://react.email">link</a>
|
|
470
|
+
</div>
|
|
471
|
+
<!--/$-->
|
|
472
|
+
</html>
|
|
473
|
+
"
|
|
474
|
+
`);
|
|
475
|
+
});
|
|
476
|
+
|
|
443
477
|
it('recognizes custom responsive screen', async () => {
|
|
444
478
|
const actualOutput = await render(
|
|
445
479
|
<Html>
|
|
@@ -206,4 +206,27 @@ describe('extractRulesPerClass()', async () => {
|
|
|
206
206
|
}
|
|
207
207
|
`);
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
it('does not emit a bare nested rule for group/peer marker classes', async () => {
|
|
211
|
+
const tailwind = await setupTailwind({});
|
|
212
|
+
const classes = ['group', 'group-hover:underline'];
|
|
213
|
+
tailwind.addUtilities(classes);
|
|
214
|
+
|
|
215
|
+
const stylesheet = tailwind.getStyleSheet();
|
|
216
|
+
const { inlinable, nonInlinable } = extractRulesPerClass(
|
|
217
|
+
stylesheet,
|
|
218
|
+
classes,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Only the real utility is keyed; the `group` marker must not produce a
|
|
222
|
+
// duplicate, parentless `&:is(...)` rule.
|
|
223
|
+
expect(Object.keys(convertToComparable(inlinable))).toEqual([]);
|
|
224
|
+
expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
|
|
225
|
+
{
|
|
226
|
+
"group-hover:underline": [
|
|
227
|
+
".group-hover\\:underline{&:is(:where(.group):hover *){@media (hover:hover){text-decoration-line:underline}}}",
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
`);
|
|
231
|
+
});
|
|
209
232
|
});
|
|
@@ -26,8 +26,24 @@ export function extractRulesPerClass(root: CssNode, classes: string[]) {
|
|
|
26
26
|
walk(root, {
|
|
27
27
|
visit: 'Rule',
|
|
28
28
|
enter(rule) {
|
|
29
|
+
// A nested rule (e.g. group/peer's `&:is(:where(.group):hover *)`) belongs
|
|
30
|
+
// to its parent utility; processing it standalone emits a bare, parentless
|
|
31
|
+
// `&` rule into the <style> block, so skip it here.
|
|
32
|
+
const firstSelector =
|
|
33
|
+
rule.prelude.type === 'SelectorList'
|
|
34
|
+
? rule.prelude.children.first
|
|
35
|
+
: null;
|
|
36
|
+
if (
|
|
37
|
+
firstSelector?.type === 'Selector' &&
|
|
38
|
+
firstSelector.children.first?.type === 'NestingSelector'
|
|
39
|
+
) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Only the prelude names the class that owns the rule; classes referenced
|
|
44
|
+
// inside the block (e.g. `.group` in `:where(.group)`) must not key it.
|
|
29
45
|
const selectorClasses: string[] = [];
|
|
30
|
-
walk(rule, {
|
|
46
|
+
walk(rule.prelude, {
|
|
31
47
|
visit: 'ClassSelector',
|
|
32
48
|
enter(classSelector) {
|
|
33
49
|
selectorClasses.push(string.decode(classSelector.name));
|