react-email 6.6.7 → 6.6.9
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 +12 -0
- package/dist/cli/index.mjs +69 -60
- package/dist/index.cjs +3 -1
- package/dist/index.mjs +3 -1
- package/package.json +1 -1
- 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,17 @@
|
|
|
1
1
|
# react-email
|
|
2
2
|
|
|
3
|
+
## 6.6.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bc2f7e3: Fix `group` utilities including bad rules that don't work
|
|
8
|
+
|
|
9
|
+
## 6.6.8
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- dca3c01: Fix `email build` computing the wrong output file tracing root in nested-workspace monorepos (e.g. a package one or more directories below the true repo root), which caused Vercel deploys to fail with an ENOENT error on the routes manifest.
|
|
14
|
+
|
|
3
15
|
## 6.6.7
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/cli/index.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import fs, { existsSync, promises, statSync } from "node:fs";
|
|
|
7
7
|
import * as path$2 from "node:path";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import url, { fileURLToPath } from "node:url";
|
|
10
|
+
import logSymbols from "log-symbols";
|
|
11
|
+
import { addDevDependency, installDependencies, runScript } from "nypm";
|
|
10
12
|
import * as fsp$1 from "node:fs/promises";
|
|
11
13
|
import fsp from "node:fs/promises";
|
|
12
14
|
import { F_OK } from "node:constants";
|
|
@@ -15,8 +17,6 @@ import nativeFs from "fs";
|
|
|
15
17
|
import path$1, { basename, dirname, normalize, posix, relative, resolve, sep } from "path";
|
|
16
18
|
import { fileURLToPath as fileURLToPath$1 } from "url";
|
|
17
19
|
import { createRequire as createRequire$1 } from "module";
|
|
18
|
-
import logSymbols from "log-symbols";
|
|
19
|
-
import { addDevDependency, installDependencies, runScript } from "nypm";
|
|
20
20
|
import { createJiti } from "jiti";
|
|
21
21
|
import prompts from "prompts";
|
|
22
22
|
import { Spinner } from "picospinner";
|
|
@@ -59,6 +59,64 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
59
59
|
}) : target, mod));
|
|
60
60
|
var __require$1 = /* @__PURE__ */ createRequire(import.meta.url);
|
|
61
61
|
//#endregion
|
|
62
|
+
//#region src/cli/utils/get-emails-directory-metadata.ts
|
|
63
|
+
const isFileAnEmail = async (fullPath) => {
|
|
64
|
+
let fileHandle;
|
|
65
|
+
try {
|
|
66
|
+
fileHandle = await fs.promises.open(fullPath, "r");
|
|
67
|
+
} catch (exception) {
|
|
68
|
+
console.warn(exception);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if ((await fileHandle.stat()).isDirectory()) {
|
|
72
|
+
await fileHandle.close();
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const { ext } = path.parse(fullPath);
|
|
76
|
+
if (![
|
|
77
|
+
".js",
|
|
78
|
+
".tsx",
|
|
79
|
+
".jsx"
|
|
80
|
+
].includes(ext)) {
|
|
81
|
+
await fileHandle.close();
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const fileContents = await fileHandle.readFile("utf8");
|
|
85
|
+
await fileHandle.close();
|
|
86
|
+
const hasES6DefaultExport = /\bexport\s+default\b/gm.test(fileContents);
|
|
87
|
+
const hasCommonJSExport = /\bmodule\.exports\s*=/gm.test(fileContents);
|
|
88
|
+
const hasNamedExport = /\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test(fileContents);
|
|
89
|
+
return hasES6DefaultExport || hasCommonJSExport || hasNamedExport;
|
|
90
|
+
};
|
|
91
|
+
const mergeDirectoriesWithSubDirectories = (emailsDirectoryMetadata) => {
|
|
92
|
+
let currentResultingMergedDirectory = emailsDirectoryMetadata;
|
|
93
|
+
while (currentResultingMergedDirectory.emailFilenames.length === 0 && currentResultingMergedDirectory.subDirectories.length === 1) {
|
|
94
|
+
const onlySubDirectory = currentResultingMergedDirectory.subDirectories[0];
|
|
95
|
+
currentResultingMergedDirectory = {
|
|
96
|
+
...onlySubDirectory,
|
|
97
|
+
directoryName: path.join(currentResultingMergedDirectory.directoryName, onlySubDirectory.directoryName)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return currentResultingMergedDirectory;
|
|
101
|
+
};
|
|
102
|
+
const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFileExtensions = false, isSubDirectory = false, baseDirectoryPath = absolutePathToEmailsDirectory) => {
|
|
103
|
+
if (!fs.existsSync(absolutePathToEmailsDirectory)) return;
|
|
104
|
+
const dirents = await fs.promises.readdir(absolutePathToEmailsDirectory, { withFileTypes: true });
|
|
105
|
+
const isEmailPredicates = await Promise.all(dirents.map((dirent) => isFileAnEmail(path.join(absolutePathToEmailsDirectory, dirent.name))));
|
|
106
|
+
const emailFilenames = dirents.filter((_, i) => isEmailPredicates[i]).map((dirent) => keepFileExtensions ? dirent.name : dirent.name.replace(path.extname(dirent.name), ""));
|
|
107
|
+
const subDirectories = await Promise.all(dirents.filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith("_") && dirent.name !== "static").map((dirent) => {
|
|
108
|
+
return getEmailsDirectoryMetadata(path.join(absolutePathToEmailsDirectory, dirent.name), keepFileExtensions, true, baseDirectoryPath);
|
|
109
|
+
}));
|
|
110
|
+
const emailsMetadata = {
|
|
111
|
+
absolutePath: absolutePathToEmailsDirectory,
|
|
112
|
+
relativePath: path.relative(baseDirectoryPath, absolutePathToEmailsDirectory),
|
|
113
|
+
directoryName: absolutePathToEmailsDirectory.split(path.sep).pop(),
|
|
114
|
+
emailFilenames,
|
|
115
|
+
subDirectories
|
|
116
|
+
};
|
|
117
|
+
return isSubDirectory ? mergeDirectoriesWithSubDirectories(emailsMetadata) : emailsMetadata;
|
|
118
|
+
};
|
|
119
|
+
//#endregion
|
|
62
120
|
//#region ../../node_modules/.pnpm/fdir@6.5.0_picomatch@4.0.4/node_modules/fdir/dist/index.mjs
|
|
63
121
|
var __require = /* @__PURE__ */ createRequire$1(import.meta.url);
|
|
64
122
|
function cleanPath(path) {
|
|
@@ -6462,68 +6520,20 @@ function validatePackages(packages) {
|
|
|
6462
6520
|
}
|
|
6463
6521
|
}
|
|
6464
6522
|
//#endregion
|
|
6465
|
-
//#region src/cli/utils/get-
|
|
6466
|
-
const
|
|
6467
|
-
let fileHandle;
|
|
6523
|
+
//#region src/cli/utils/get-tracing-root-dir.ts
|
|
6524
|
+
const getTracingRootDir = async (usersProjectLocation) => {
|
|
6468
6525
|
try {
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
return
|
|
6473
|
-
}
|
|
6474
|
-
if ((await fileHandle.stat()).isDirectory()) {
|
|
6475
|
-
await fileHandle.close();
|
|
6476
|
-
return false;
|
|
6477
|
-
}
|
|
6478
|
-
const { ext } = path.parse(fullPath);
|
|
6479
|
-
if (![
|
|
6480
|
-
".js",
|
|
6481
|
-
".tsx",
|
|
6482
|
-
".jsx"
|
|
6483
|
-
].includes(ext)) {
|
|
6484
|
-
await fileHandle.close();
|
|
6485
|
-
return false;
|
|
6486
|
-
}
|
|
6487
|
-
const fileContents = await fileHandle.readFile("utf8");
|
|
6488
|
-
await fileHandle.close();
|
|
6489
|
-
const hasES6DefaultExport = /\bexport\s+default\b/gm.test(fileContents);
|
|
6490
|
-
const hasCommonJSExport = /\bmodule\.exports\s*=/gm.test(fileContents);
|
|
6491
|
-
const hasNamedExport = /\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test(fileContents);
|
|
6492
|
-
return hasES6DefaultExport || hasCommonJSExport || hasNamedExport;
|
|
6493
|
-
};
|
|
6494
|
-
const mergeDirectoriesWithSubDirectories = (emailsDirectoryMetadata) => {
|
|
6495
|
-
let currentResultingMergedDirectory = emailsDirectoryMetadata;
|
|
6496
|
-
while (currentResultingMergedDirectory.emailFilenames.length === 0 && currentResultingMergedDirectory.subDirectories.length === 1) {
|
|
6497
|
-
const onlySubDirectory = currentResultingMergedDirectory.subDirectories[0];
|
|
6498
|
-
currentResultingMergedDirectory = {
|
|
6499
|
-
...onlySubDirectory,
|
|
6500
|
-
directoryName: path.join(currentResultingMergedDirectory.directoryName, onlySubDirectory.directoryName)
|
|
6501
|
-
};
|
|
6526
|
+
const { rootDir } = await getPackages(usersProjectLocation);
|
|
6527
|
+
return rootDir.replaceAll("\\", "/");
|
|
6528
|
+
} catch {
|
|
6529
|
+
return usersProjectLocation.replaceAll("\\", "/");
|
|
6502
6530
|
}
|
|
6503
|
-
return currentResultingMergedDirectory;
|
|
6504
|
-
};
|
|
6505
|
-
const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFileExtensions = false, isSubDirectory = false, baseDirectoryPath = absolutePathToEmailsDirectory) => {
|
|
6506
|
-
if (!fs.existsSync(absolutePathToEmailsDirectory)) return;
|
|
6507
|
-
const dirents = await fs.promises.readdir(absolutePathToEmailsDirectory, { withFileTypes: true });
|
|
6508
|
-
const isEmailPredicates = await Promise.all(dirents.map((dirent) => isFileAnEmail(path.join(absolutePathToEmailsDirectory, dirent.name))));
|
|
6509
|
-
const emailFilenames = dirents.filter((_, i) => isEmailPredicates[i]).map((dirent) => keepFileExtensions ? dirent.name : dirent.name.replace(path.extname(dirent.name), ""));
|
|
6510
|
-
const subDirectories = await Promise.all(dirents.filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith("_") && dirent.name !== "static").map((dirent) => {
|
|
6511
|
-
return getEmailsDirectoryMetadata(path.join(absolutePathToEmailsDirectory, dirent.name), keepFileExtensions, true, baseDirectoryPath);
|
|
6512
|
-
}));
|
|
6513
|
-
const emailsMetadata = {
|
|
6514
|
-
absolutePath: absolutePathToEmailsDirectory,
|
|
6515
|
-
relativePath: path.relative(baseDirectoryPath, absolutePathToEmailsDirectory),
|
|
6516
|
-
directoryName: absolutePathToEmailsDirectory.split(path.sep).pop(),
|
|
6517
|
-
emailFilenames,
|
|
6518
|
-
subDirectories
|
|
6519
|
-
};
|
|
6520
|
-
return isSubDirectory ? mergeDirectoriesWithSubDirectories(emailsMetadata) : emailsMetadata;
|
|
6521
6531
|
};
|
|
6522
6532
|
//#endregion
|
|
6523
6533
|
//#region package.json
|
|
6524
6534
|
var package_default = {
|
|
6525
6535
|
name: "react-email",
|
|
6526
|
-
version: "6.6.
|
|
6536
|
+
version: "6.6.9",
|
|
6527
6537
|
description: "A live preview of your emails right in your browser.",
|
|
6528
6538
|
bin: { "email": "./dist/cli/index.mjs" },
|
|
6529
6539
|
type: "module",
|
|
@@ -6729,14 +6739,13 @@ const stopSpinnerAndPersist = (spinner, display) => {
|
|
|
6729
6739
|
//#region src/cli/commands/build.ts
|
|
6730
6740
|
const isInReactEmailMonorepo = !path.dirname(fileURLToPath(import.meta.url)).includes("node_modules");
|
|
6731
6741
|
const setNextEnvironmentVariablesForBuild = async (emailsDirRelativePath, builtPreviewAppPath, usersProjectLocation) => {
|
|
6732
|
-
|
|
6733
|
-
if (isInReactEmailMonorepo) rootDir = `'${await getPackages(usersProjectLocation).then((p) => p.rootDir.replaceAll("\\", "/"))}'`;
|
|
6742
|
+
const rootDir = await getTracingRootDir(usersProjectLocation);
|
|
6734
6743
|
const nextConfigContents = `
|
|
6735
6744
|
import path from 'path';
|
|
6736
6745
|
const emailsDirRelativePath = path.normalize('${emailsDirRelativePath}');
|
|
6737
6746
|
const userProjectLocation = '${process.cwd().replaceAll("\\", "/")}';
|
|
6738
6747
|
const previewServerLocation = '${builtPreviewAppPath.replaceAll("\\", "/")}';
|
|
6739
|
-
const rootDir = ${rootDir};
|
|
6748
|
+
const rootDir = '${rootDir}';
|
|
6740
6749
|
/** @type {import('next').NextConfig} */
|
|
6741
6750
|
const nextConfig = {
|
|
6742
6751
|
env: {
|
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
|
@@ -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));
|