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 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
@@ -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-emails-directory-metadata.ts
6466
- const isFileAnEmail = async (fullPath) => {
6467
- let fileHandle;
6523
+ //#region src/cli/utils/get-tracing-root-dir.ts
6524
+ const getTracingRootDir = async (usersProjectLocation) => {
6468
6525
  try {
6469
- fileHandle = await fs.promises.open(fullPath, "r");
6470
- } catch (exception) {
6471
- console.warn(exception);
6472
- return false;
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.7",
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
- let rootDir = "userProjectLocation";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "6.6.7",
3
+ "version": "6.6.9",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.mjs"
@@ -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));