react-email 6.0.4 → 6.0.6

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,13 @@
1
1
  # react-email
2
2
 
3
+ ## 6.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 84bb7ab: collapse empty-fallback var() refs in inline styles
8
+
9
+ ## 6.0.5
10
+
3
11
  ## 6.0.4
4
12
 
5
13
  ### Patch Changes
@@ -6522,7 +6522,7 @@ const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFil
6522
6522
  //#region package.json
6523
6523
  var package_default = {
6524
6524
  name: "react-email",
6525
- version: "6.0.4",
6525
+ version: "6.0.6",
6526
6526
  description: "A live preview of your emails right in your browser.",
6527
6527
  bin: { "email": "./dist/cli/index.mjs" },
6528
6528
  type: "module",
package/dist/index.cjs CHANGED
@@ -37790,7 +37790,35 @@ function makeInlineStylesFor(inlinableRules, customProperties) {
37790
37790
  visit: "Declaration",
37791
37791
  enter(declaration) {
37792
37792
  if (declaration.property.startsWith("--")) return;
37793
- styles[getReactProperty(declaration.property)] = generate(declaration.value) + (declaration.important ? "!important" : "");
37793
+ walk(declaration.value, {
37794
+ visit: "Function",
37795
+ leave(func, funcItem, funcList) {
37796
+ if (func.name !== "var") return;
37797
+ let variableName;
37798
+ walk(func, {
37799
+ visit: "Identifier",
37800
+ enter(identifier) {
37801
+ variableName = identifier.name;
37802
+ return this.break;
37803
+ }
37804
+ });
37805
+ if (!variableName?.startsWith("--tw-")) return;
37806
+ let sawComma = false;
37807
+ let hasFallbackContent = false;
37808
+ func.children.forEach((child) => {
37809
+ if (!sawComma) {
37810
+ if (child.type === "Operator" && child.value === ",") sawComma = true;
37811
+ return;
37812
+ }
37813
+ let childValue = generate(child).trim();
37814
+ if (child.type === "Raw") childValue = childValue.replaceAll(/var\(--tw-[^,()]+,\s*\)/g, "").trim();
37815
+ if (childValue.length > 0) hasFallbackContent = true;
37816
+ });
37817
+ if (!sawComma || hasFallbackContent) return;
37818
+ funcList.remove(funcItem);
37819
+ }
37820
+ });
37821
+ styles[getReactProperty(declaration.property)] = generate(declaration.value).trim() + (declaration.important ? "!important" : "");
37794
37822
  }
37795
37823
  });
37796
37824
  }
package/dist/index.mjs CHANGED
@@ -37769,7 +37769,35 @@ function makeInlineStylesFor(inlinableRules, customProperties) {
37769
37769
  visit: "Declaration",
37770
37770
  enter(declaration) {
37771
37771
  if (declaration.property.startsWith("--")) return;
37772
- styles[getReactProperty(declaration.property)] = generate(declaration.value) + (declaration.important ? "!important" : "");
37772
+ walk(declaration.value, {
37773
+ visit: "Function",
37774
+ leave(func, funcItem, funcList) {
37775
+ if (func.name !== "var") return;
37776
+ let variableName;
37777
+ walk(func, {
37778
+ visit: "Identifier",
37779
+ enter(identifier) {
37780
+ variableName = identifier.name;
37781
+ return this.break;
37782
+ }
37783
+ });
37784
+ if (!variableName?.startsWith("--tw-")) return;
37785
+ let sawComma = false;
37786
+ let hasFallbackContent = false;
37787
+ func.children.forEach((child) => {
37788
+ if (!sawComma) {
37789
+ if (child.type === "Operator" && child.value === ",") sawComma = true;
37790
+ return;
37791
+ }
37792
+ let childValue = generate(child).trim();
37793
+ if (child.type === "Raw") childValue = childValue.replaceAll(/var\(--tw-[^,()]+,\s*\)/g, "").trim();
37794
+ if (childValue.length > 0) hasFallbackContent = true;
37795
+ });
37796
+ if (!sawComma || hasFallbackContent) return;
37797
+ funcList.remove(funcItem);
37798
+ }
37799
+ });
37800
+ styles[getReactProperty(declaration.property)] = generate(declaration.value).trim() + (declaration.important ? "!important" : "");
37773
37801
  }
37774
37802
  });
37775
37803
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "6.0.4",
3
+ "version": "6.0.6",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.mjs"
@@ -41,11 +41,84 @@ describe('makeInlineStylesFor()', async () => {
41
41
  ),
42
42
  ).toMatchInlineSnapshot(`
43
43
  {
44
- "backgroundColor": " #3490dc",
44
+ "backgroundColor": "#3490dc",
45
45
  "borderRadius": "0.25rem",
46
- "color": " #fff",
46
+ "color": "#fff",
47
47
  "padding": "0.5rem 1rem",
48
48
  }
49
49
  `);
50
50
  });
51
+
52
+ it('strips Tailwind v4 variant-stacking var() refs with empty fallbacks', () => {
53
+ // Tailwind v4 compiles `tabular-nums` to a font-variant-numeric value
54
+ // where every optional variant slot is represented by an unresolved
55
+ // var(--tw-..., ) with an empty fallback. Email clients do not support
56
+ // CSS custom properties reliably, so these must collapse at inline time
57
+ // (per CSS spec, an empty fallback resolves to empty string).
58
+ const tailwindStyles = parse(`
59
+ .tabular-nums {
60
+ font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) tabular-nums var(--tw-numeric-fraction,);
61
+ }
62
+ `) as StyleSheet;
63
+
64
+ expect(
65
+ makeInlineStylesFor(
66
+ tailwindStyles.children.toArray(),
67
+ getCustomProperties(tailwindStyles),
68
+ ),
69
+ ).toMatchInlineSnapshot(`
70
+ {
71
+ "fontVariantNumeric": "tabular-nums",
72
+ }
73
+ `);
74
+ });
75
+
76
+ it('preserves user-authored empty-fallback var() refs (non --tw- prefix)', () => {
77
+ // The collapse is scoped to Tailwind's --tw-* variant-stacking idiom.
78
+ // A user-authored var(--my-color,) with an empty fallback must pass
79
+ // through unchanged even though it syntactically matches the idiom --
80
+ // the user opted into that semantic and the render target may define
81
+ // --my-color at a higher scope.
82
+ const userStyles = parse(`
83
+ .thing {
84
+ color: var(--my-color,);
85
+ background: var(--brand,) var(--tw-custom,);
86
+ }
87
+ `) as StyleSheet;
88
+
89
+ expect(
90
+ makeInlineStylesFor(
91
+ userStyles.children.toArray(),
92
+ getCustomProperties(userStyles),
93
+ ),
94
+ ).toMatchInlineSnapshot(`
95
+ {
96
+ "background": "var(--brand,)",
97
+ "color": "var(--my-color,)",
98
+ }
99
+ `);
100
+ });
101
+
102
+ it('collapses outer --tw-* var() that becomes empty after inner --tw-* var() collapses', () => {
103
+ // Regression test for cubic-dev-ai P2 review on PR #3359:
104
+ // pre-order traversal misses outer var(--tw-X, var(--tw-Y,)) because the
105
+ // outer's fallback only becomes empty AFTER the inner collapses. Post-order
106
+ // traversal fixes this.
107
+ const tailwindStyles = parse(`
108
+ .nested {
109
+ font-variant-numeric: var(--tw-outer, var(--tw-inner,)) tabular-nums;
110
+ }
111
+ `) as StyleSheet;
112
+
113
+ expect(
114
+ makeInlineStylesFor(
115
+ tailwindStyles.children.toArray(),
116
+ getCustomProperties(tailwindStyles),
117
+ ),
118
+ ).toMatchInlineSnapshot(`
119
+ {
120
+ "fontVariantNumeric": "tabular-nums",
121
+ }
122
+ `);
123
+ });
51
124
  });
@@ -59,8 +59,71 @@ export function makeInlineStylesFor(
59
59
  if (declaration.property.startsWith('--')) {
60
60
  return;
61
61
  }
62
+ // Tailwind v4 emits variant-stacking idioms like
63
+ // font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) tabular-nums var(--tw-numeric-fraction,)
64
+ // where each var() has an empty fallback so missing variants collapse to nothing.
65
+ // The walker above replaces var() calls with an initialValue when one is defined,
66
+ // but Tailwind deliberately leaves these variant vars undefined until used, so they
67
+ // stay in the output here and produce unresolvable custom properties in email HTML
68
+ // (no email client supports CSS custom properties reliably). Per the CSS spec
69
+ // (https://www.w3.org/TR/css-variables-1/#using-variables) an empty fallback means
70
+ // "use empty string if the variable is undefined", which is exactly what we want at
71
+ // inline-style time.
72
+ //
73
+ // Scoped to the `--tw-` prefix so any user-authored empty-fallback var() refs
74
+ // (even ones used inside tailwind utilities) are left untouched.
75
+ walk(declaration.value, {
76
+ visit: 'Function',
77
+ leave(func, funcItem, funcList) {
78
+ if (func.name !== 'var') {
79
+ return;
80
+ }
81
+
82
+ let variableName: string | undefined;
83
+ walk(func, {
84
+ visit: 'Identifier',
85
+ enter(identifier) {
86
+ variableName = identifier.name;
87
+ return this.break;
88
+ },
89
+ });
90
+ if (!variableName?.startsWith('--tw-')) {
91
+ return;
92
+ }
93
+
94
+ let sawComma = false;
95
+ let hasFallbackContent = false;
96
+ func.children.forEach((child) => {
97
+ if (!sawComma) {
98
+ if (child.type === 'Operator' && child.value === ',') {
99
+ sawComma = true;
100
+ }
101
+ return;
102
+ }
103
+
104
+ let childValue = generate(child).trim();
105
+ if (child.type === 'Raw') {
106
+ const emptyTailwindVarPattern = /var\(--tw-[^,()]+,\s*\)/g;
107
+ childValue = childValue
108
+ .replaceAll(emptyTailwindVarPattern, '')
109
+ .trim();
110
+ }
111
+
112
+ if (childValue.length > 0) {
113
+ hasFallbackContent = true;
114
+ }
115
+ });
116
+
117
+ if (!sawComma || hasFallbackContent) {
118
+ return;
119
+ }
120
+
121
+ funcList.remove(funcItem);
122
+ },
123
+ });
124
+
62
125
  styles[getReactProperty(declaration.property)] =
63
- generate(declaration.value) +
126
+ generate(declaration.value).trim() +
64
127
  (declaration.important ? '!important' : '');
65
128
  },
66
129
  });