react-email 6.3.2 → 6.4.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 +8 -0
- package/dist/cli/index.mjs +1 -1
- package/dist/index.cjs +67 -29
- package/dist/index.mjs +67 -29
- package/package.json +1 -1
- package/src/components/tailwind/utils/css/make-inline-styles-for.ts +2 -62
- package/src/components/tailwind/utils/css/resolve-all-css-variables.spec.ts +16 -0
- package/src/components/tailwind/utils/css/resolve-all-css-variables.ts +37 -0
- package/src/components/tailwind/utils/css/sanitize-non-inlinable-rules.spec.ts +22 -1
- package/src/components/tailwind/utils/css/sanitize-non-inlinable-rules.ts +14 -1
- package/src/components/tailwind/utils/css/strip-empty-tailwind-vars.spec.ts +128 -0
- package/src/components/tailwind/utils/css/strip-empty-tailwind-vars.ts +70 -0
- package/tsconfig.json +6 -1
package/CHANGELOG.md
CHANGED
package/dist/cli/index.mjs
CHANGED
|
@@ -6523,7 +6523,7 @@ const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFil
|
|
|
6523
6523
|
//#region package.json
|
|
6524
6524
|
var package_default = {
|
|
6525
6525
|
name: "react-email",
|
|
6526
|
-
version: "6.
|
|
6526
|
+
version: "6.4.0",
|
|
6527
6527
|
description: "A live preview of your emails right in your browser.",
|
|
6528
6528
|
bin: { "email": "./dist/cli/index.mjs" },
|
|
6529
6529
|
type: "module",
|
package/dist/index.cjs
CHANGED
|
@@ -37771,6 +37771,55 @@ function getReactProperty(prop) {
|
|
|
37771
37771
|
return fromDashCaseToCamelCase(modifiedProp);
|
|
37772
37772
|
}
|
|
37773
37773
|
//#endregion
|
|
37774
|
+
//#region src/components/tailwind/utils/css/strip-empty-tailwind-vars.ts
|
|
37775
|
+
/**
|
|
37776
|
+
* Tailwind v4 emits variant-stacking idioms like
|
|
37777
|
+
* font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) tabular-nums var(--tw-numeric-fraction,)
|
|
37778
|
+
* filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-invert,) ...
|
|
37779
|
+
* where each var() has an empty fallback so missing variants collapse to nothing.
|
|
37780
|
+
* Tailwind deliberately leaves these variant vars undefined until used, so they
|
|
37781
|
+
* stay in the output here and produce unresolvable custom properties in email HTML
|
|
37782
|
+
* (no email client supports CSS custom properties reliably). Per the CSS spec
|
|
37783
|
+
* (https://www.w3.org/TR/css-variables-1/#using-variables) an empty fallback means
|
|
37784
|
+
* "use empty string if the variable is undefined", which is exactly what we want.
|
|
37785
|
+
*
|
|
37786
|
+
* Scoped to the `--tw-` prefix so any user-authored empty-fallback var() refs
|
|
37787
|
+
* are left untouched.
|
|
37788
|
+
*
|
|
37789
|
+
* Uses post-order traversal so an outer var(--tw-X, var(--tw-Y,)) collapses
|
|
37790
|
+
* correctly after the inner var() has been removed.
|
|
37791
|
+
*/
|
|
37792
|
+
function stripEmptyTailwindVars(node) {
|
|
37793
|
+
walk(node, {
|
|
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
|
+
}
|
|
37822
|
+
//#endregion
|
|
37774
37823
|
//#region src/components/tailwind/utils/css/unwrap-value.ts
|
|
37775
37824
|
function unwrapValue(value) {
|
|
37776
37825
|
if (value.type === "Value" && value.children.size === 1) return value.children.first ?? value;
|
|
@@ -37815,34 +37864,7 @@ function makeInlineStylesFor(inlinableRules, customProperties) {
|
|
|
37815
37864
|
visit: "Declaration",
|
|
37816
37865
|
enter(declaration) {
|
|
37817
37866
|
if (declaration.property.startsWith("--")) return;
|
|
37818
|
-
|
|
37819
|
-
visit: "Function",
|
|
37820
|
-
leave(func, funcItem, funcList) {
|
|
37821
|
-
if (func.name !== "var") return;
|
|
37822
|
-
let variableName;
|
|
37823
|
-
walk(func, {
|
|
37824
|
-
visit: "Identifier",
|
|
37825
|
-
enter(identifier) {
|
|
37826
|
-
variableName = identifier.name;
|
|
37827
|
-
return this.break;
|
|
37828
|
-
}
|
|
37829
|
-
});
|
|
37830
|
-
if (!variableName?.startsWith("--tw-")) return;
|
|
37831
|
-
let sawComma = false;
|
|
37832
|
-
let hasFallbackContent = false;
|
|
37833
|
-
func.children.forEach((child) => {
|
|
37834
|
-
if (!sawComma) {
|
|
37835
|
-
if (child.type === "Operator" && child.value === ",") sawComma = true;
|
|
37836
|
-
return;
|
|
37837
|
-
}
|
|
37838
|
-
let childValue = generate(child).trim();
|
|
37839
|
-
if (child.type === "Raw") childValue = childValue.replaceAll(/var\(--tw-[^,()]+,\s*\)/g, "").trim();
|
|
37840
|
-
if (childValue.length > 0) hasFallbackContent = true;
|
|
37841
|
-
});
|
|
37842
|
-
if (!sawComma || hasFallbackContent) return;
|
|
37843
|
-
funcList.remove(funcItem);
|
|
37844
|
-
}
|
|
37845
|
-
});
|
|
37867
|
+
stripEmptyTailwindVars(declaration.value);
|
|
37846
37868
|
styles[getReactProperty(declaration.property)] = generate(declaration.value).trim() + (declaration.important ? "!important" : "");
|
|
37847
37869
|
}
|
|
37848
37870
|
});
|
|
@@ -37933,6 +37955,11 @@ function resolveAllCssVariables(node) {
|
|
|
37933
37955
|
hasReplaced = true;
|
|
37934
37956
|
break;
|
|
37935
37957
|
}
|
|
37958
|
+
if (use.path[0]?.type === "Block" && use.path[1]?.type === "Atrule" && use.path[2]?.type === "Block" && use.path[3]?.type === "Rule" && definition.path[0]?.type === "Block" && definition.path[1]?.type === "Atrule" && definition.path[2]?.type === "Block" && definition.path[3]?.type === "Rule" && use.path[1].name === definition.path[1].name && (use.path[1].prelude ? definition.path[1].prelude ? generate(use.path[1].prelude) === generate(definition.path[1].prelude) : false : definition.path[1].prelude === null) && doSelectorsIntersect(use.path[3].prelude, definition.path[3].prelude)) {
|
|
37959
|
+
use.declaration.value = parse(generate(use.declaration.value).replaceAll(use.raw, definition.definition), { context: "value" });
|
|
37960
|
+
hasReplaced = true;
|
|
37961
|
+
break;
|
|
37962
|
+
}
|
|
37936
37963
|
}
|
|
37937
37964
|
if (!hasReplaced && use.fallback) use.declaration.value = parse(generate(use.declaration.value).replaceAll(use.raw, use.fallback), { context: "value" });
|
|
37938
37965
|
}
|
|
@@ -38498,6 +38525,12 @@ function sanitizeClassName(className) {
|
|
|
38498
38525
|
* What it does:
|
|
38499
38526
|
* 1. Converts all declarations in all rules into important ones
|
|
38500
38527
|
* 2. Sanitizes class selectors of all non-inlinable rules
|
|
38528
|
+
* 3. Removes --tw-* custom property declarations — by this point all CSS
|
|
38529
|
+
* variables have been resolved, so these are dead weight in email HTML.
|
|
38530
|
+
* 4. Strips empty-fallback var(--tw-*,) refs that Tailwind v4 emits for
|
|
38531
|
+
* variant-stacking idioms (filter, font-variant-numeric, etc.) — email
|
|
38532
|
+
* clients can't resolve CSS custom properties reliably, so any --tw-*
|
|
38533
|
+
* left as a bare empty-fallback ref would reach the client broken.
|
|
38501
38534
|
*/
|
|
38502
38535
|
function sanitizeNonInlinableRules(node) {
|
|
38503
38536
|
walk(node, {
|
|
@@ -38509,8 +38542,13 @@ function sanitizeNonInlinableRules(node) {
|
|
|
38509
38542
|
});
|
|
38510
38543
|
walk(rule, {
|
|
38511
38544
|
visit: "Declaration",
|
|
38512
|
-
enter(declaration) {
|
|
38545
|
+
enter(declaration, item, list) {
|
|
38546
|
+
if (declaration.property.startsWith("--tw-")) {
|
|
38547
|
+
list.remove(item);
|
|
38548
|
+
return;
|
|
38549
|
+
}
|
|
38513
38550
|
declaration.important = true;
|
|
38551
|
+
stripEmptyTailwindVars(declaration.value);
|
|
38514
38552
|
}
|
|
38515
38553
|
});
|
|
38516
38554
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -37750,6 +37750,55 @@ function getReactProperty(prop) {
|
|
|
37750
37750
|
return fromDashCaseToCamelCase(modifiedProp);
|
|
37751
37751
|
}
|
|
37752
37752
|
//#endregion
|
|
37753
|
+
//#region src/components/tailwind/utils/css/strip-empty-tailwind-vars.ts
|
|
37754
|
+
/**
|
|
37755
|
+
* Tailwind v4 emits variant-stacking idioms like
|
|
37756
|
+
* font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) tabular-nums var(--tw-numeric-fraction,)
|
|
37757
|
+
* filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-invert,) ...
|
|
37758
|
+
* where each var() has an empty fallback so missing variants collapse to nothing.
|
|
37759
|
+
* Tailwind deliberately leaves these variant vars undefined until used, so they
|
|
37760
|
+
* stay in the output here and produce unresolvable custom properties in email HTML
|
|
37761
|
+
* (no email client supports CSS custom properties reliably). Per the CSS spec
|
|
37762
|
+
* (https://www.w3.org/TR/css-variables-1/#using-variables) an empty fallback means
|
|
37763
|
+
* "use empty string if the variable is undefined", which is exactly what we want.
|
|
37764
|
+
*
|
|
37765
|
+
* Scoped to the `--tw-` prefix so any user-authored empty-fallback var() refs
|
|
37766
|
+
* are left untouched.
|
|
37767
|
+
*
|
|
37768
|
+
* Uses post-order traversal so an outer var(--tw-X, var(--tw-Y,)) collapses
|
|
37769
|
+
* correctly after the inner var() has been removed.
|
|
37770
|
+
*/
|
|
37771
|
+
function stripEmptyTailwindVars(node) {
|
|
37772
|
+
walk(node, {
|
|
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
|
+
}
|
|
37801
|
+
//#endregion
|
|
37753
37802
|
//#region src/components/tailwind/utils/css/unwrap-value.ts
|
|
37754
37803
|
function unwrapValue(value) {
|
|
37755
37804
|
if (value.type === "Value" && value.children.size === 1) return value.children.first ?? value;
|
|
@@ -37794,34 +37843,7 @@ function makeInlineStylesFor(inlinableRules, customProperties) {
|
|
|
37794
37843
|
visit: "Declaration",
|
|
37795
37844
|
enter(declaration) {
|
|
37796
37845
|
if (declaration.property.startsWith("--")) return;
|
|
37797
|
-
|
|
37798
|
-
visit: "Function",
|
|
37799
|
-
leave(func, funcItem, funcList) {
|
|
37800
|
-
if (func.name !== "var") return;
|
|
37801
|
-
let variableName;
|
|
37802
|
-
walk(func, {
|
|
37803
|
-
visit: "Identifier",
|
|
37804
|
-
enter(identifier) {
|
|
37805
|
-
variableName = identifier.name;
|
|
37806
|
-
return this.break;
|
|
37807
|
-
}
|
|
37808
|
-
});
|
|
37809
|
-
if (!variableName?.startsWith("--tw-")) return;
|
|
37810
|
-
let sawComma = false;
|
|
37811
|
-
let hasFallbackContent = false;
|
|
37812
|
-
func.children.forEach((child) => {
|
|
37813
|
-
if (!sawComma) {
|
|
37814
|
-
if (child.type === "Operator" && child.value === ",") sawComma = true;
|
|
37815
|
-
return;
|
|
37816
|
-
}
|
|
37817
|
-
let childValue = generate(child).trim();
|
|
37818
|
-
if (child.type === "Raw") childValue = childValue.replaceAll(/var\(--tw-[^,()]+,\s*\)/g, "").trim();
|
|
37819
|
-
if (childValue.length > 0) hasFallbackContent = true;
|
|
37820
|
-
});
|
|
37821
|
-
if (!sawComma || hasFallbackContent) return;
|
|
37822
|
-
funcList.remove(funcItem);
|
|
37823
|
-
}
|
|
37824
|
-
});
|
|
37846
|
+
stripEmptyTailwindVars(declaration.value);
|
|
37825
37847
|
styles[getReactProperty(declaration.property)] = generate(declaration.value).trim() + (declaration.important ? "!important" : "");
|
|
37826
37848
|
}
|
|
37827
37849
|
});
|
|
@@ -37912,6 +37934,11 @@ function resolveAllCssVariables(node) {
|
|
|
37912
37934
|
hasReplaced = true;
|
|
37913
37935
|
break;
|
|
37914
37936
|
}
|
|
37937
|
+
if (use.path[0]?.type === "Block" && use.path[1]?.type === "Atrule" && use.path[2]?.type === "Block" && use.path[3]?.type === "Rule" && definition.path[0]?.type === "Block" && definition.path[1]?.type === "Atrule" && definition.path[2]?.type === "Block" && definition.path[3]?.type === "Rule" && use.path[1].name === definition.path[1].name && (use.path[1].prelude ? definition.path[1].prelude ? generate(use.path[1].prelude) === generate(definition.path[1].prelude) : false : definition.path[1].prelude === null) && doSelectorsIntersect(use.path[3].prelude, definition.path[3].prelude)) {
|
|
37938
|
+
use.declaration.value = parse(generate(use.declaration.value).replaceAll(use.raw, definition.definition), { context: "value" });
|
|
37939
|
+
hasReplaced = true;
|
|
37940
|
+
break;
|
|
37941
|
+
}
|
|
37915
37942
|
}
|
|
37916
37943
|
if (!hasReplaced && use.fallback) use.declaration.value = parse(generate(use.declaration.value).replaceAll(use.raw, use.fallback), { context: "value" });
|
|
37917
37944
|
}
|
|
@@ -38477,6 +38504,12 @@ function sanitizeClassName(className) {
|
|
|
38477
38504
|
* What it does:
|
|
38478
38505
|
* 1. Converts all declarations in all rules into important ones
|
|
38479
38506
|
* 2. Sanitizes class selectors of all non-inlinable rules
|
|
38507
|
+
* 3. Removes --tw-* custom property declarations — by this point all CSS
|
|
38508
|
+
* variables have been resolved, so these are dead weight in email HTML.
|
|
38509
|
+
* 4. Strips empty-fallback var(--tw-*,) refs that Tailwind v4 emits for
|
|
38510
|
+
* variant-stacking idioms (filter, font-variant-numeric, etc.) — email
|
|
38511
|
+
* clients can't resolve CSS custom properties reliably, so any --tw-*
|
|
38512
|
+
* left as a bare empty-fallback ref would reach the client broken.
|
|
38480
38513
|
*/
|
|
38481
38514
|
function sanitizeNonInlinableRules(node) {
|
|
38482
38515
|
walk(node, {
|
|
@@ -38488,8 +38521,13 @@ function sanitizeNonInlinableRules(node) {
|
|
|
38488
38521
|
});
|
|
38489
38522
|
walk(rule, {
|
|
38490
38523
|
visit: "Declaration",
|
|
38491
|
-
enter(declaration) {
|
|
38524
|
+
enter(declaration, item, list) {
|
|
38525
|
+
if (declaration.property.startsWith("--tw-")) {
|
|
38526
|
+
list.remove(item);
|
|
38527
|
+
return;
|
|
38528
|
+
}
|
|
38492
38529
|
declaration.important = true;
|
|
38530
|
+
stripEmptyTailwindVars(declaration.value);
|
|
38493
38531
|
}
|
|
38494
38532
|
});
|
|
38495
38533
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CssNode, type Declaration, generate, walk } from 'css-tree';
|
|
2
2
|
import { getReactProperty } from '../compatibility/get-react-property.js';
|
|
3
3
|
import type { CustomProperties } from './get-custom-properties.js';
|
|
4
|
+
import { stripEmptyTailwindVars } from './strip-empty-tailwind-vars.js';
|
|
4
5
|
import { unwrapValue } from './unwrap-value.js';
|
|
5
6
|
|
|
6
7
|
export function makeInlineStylesFor(
|
|
@@ -59,68 +60,7 @@ export function makeInlineStylesFor(
|
|
|
59
60
|
if (declaration.property.startsWith('--')) {
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
|
-
|
|
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
|
-
});
|
|
63
|
+
stripEmptyTailwindVars(declaration.value);
|
|
124
64
|
|
|
125
65
|
styles[getReactProperty(declaration.property)] =
|
|
126
66
|
generate(declaration.value).trim() +
|
|
@@ -237,6 +237,22 @@ describe('resolveAllCSSVariables', () => {
|
|
|
237
237
|
);
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
it('does not leak variables across sibling nested at-rules within the same rule', () => {
|
|
241
|
+
const root = parse(`.print_invert {
|
|
242
|
+
@media print {
|
|
243
|
+
--tw-invert: invert(100%);
|
|
244
|
+
filter: var(--tw-invert, none);
|
|
245
|
+
}
|
|
246
|
+
@media screen {
|
|
247
|
+
filter: var(--tw-invert, none);
|
|
248
|
+
}
|
|
249
|
+
}`);
|
|
250
|
+
resolveAllCssVariables(root);
|
|
251
|
+
expect(generate(root)).toMatchInlineSnapshot(
|
|
252
|
+
`".print_invert{@media print{--tw-invert: invert(100%);filter:invert(100%)}@media screen{filter:none}}"`,
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
240
256
|
it('handles selectors with asterisks in attribute selectors and pseudo-functions', () => {
|
|
241
257
|
const root = parse(`* {
|
|
242
258
|
--global-color: red;
|
|
@@ -187,6 +187,43 @@ export function resolveAllCssVariables(node: CssNode) {
|
|
|
187
187
|
hasReplaced = true;
|
|
188
188
|
break;
|
|
189
189
|
}
|
|
190
|
+
|
|
191
|
+
// Both use and definition live inside the same nested @media (or other
|
|
192
|
+
// at-rule) block of the same rule — e.g. Tailwind v4's
|
|
193
|
+
// .print_invert { @media print { --tw-invert: invert(100%); filter: var(--tw-invert,) ... } }
|
|
194
|
+
// The previous two checks only cover a Rule directly containing the
|
|
195
|
+
// declaration; this one covers Rule → Block → Atrule → Block → Declaration
|
|
196
|
+
// on both sides.
|
|
197
|
+
if (
|
|
198
|
+
use.path[0]?.type === 'Block' &&
|
|
199
|
+
use.path[1]?.type === 'Atrule' &&
|
|
200
|
+
use.path[2]?.type === 'Block' &&
|
|
201
|
+
use.path[3]?.type === 'Rule' &&
|
|
202
|
+
definition.path[0]?.type === 'Block' &&
|
|
203
|
+
definition.path[1]?.type === 'Atrule' &&
|
|
204
|
+
definition.path[2]?.type === 'Block' &&
|
|
205
|
+
definition.path[3]?.type === 'Rule' &&
|
|
206
|
+
use.path[1].name === definition.path[1].name &&
|
|
207
|
+
(use.path[1].prelude
|
|
208
|
+
? definition.path[1].prelude
|
|
209
|
+
? generate(use.path[1].prelude) ===
|
|
210
|
+
generate(definition.path[1].prelude)
|
|
211
|
+
: false
|
|
212
|
+
: definition.path[1].prelude === null) &&
|
|
213
|
+
doSelectorsIntersect(use.path[3].prelude, definition.path[3].prelude)
|
|
214
|
+
) {
|
|
215
|
+
use.declaration.value = parse(
|
|
216
|
+
generate(use.declaration.value).replaceAll(
|
|
217
|
+
use.raw,
|
|
218
|
+
definition.definition,
|
|
219
|
+
),
|
|
220
|
+
{
|
|
221
|
+
context: 'value',
|
|
222
|
+
},
|
|
223
|
+
) as Raw | Value;
|
|
224
|
+
hasReplaced = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
190
227
|
}
|
|
191
228
|
|
|
192
229
|
if (!hasReplaced && use.fallback) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generate } from 'css-tree';
|
|
2
|
+
import { sanitizeStyleSheet } from '../../sanitize-stylesheet.js';
|
|
2
3
|
import { setupTailwind } from '../tailwindcss/setup-tailwind.js';
|
|
3
4
|
import { sanitizeNonInlinableRules } from './sanitize-non-inlinable-rules.js';
|
|
4
5
|
|
|
@@ -27,7 +28,27 @@ describe('sanitizeNonInlinableRules()', () => {
|
|
|
27
28
|
|
|
28
29
|
sanitizeNonInlinableRules(stylesheet);
|
|
29
30
|
expect(generate(stylesheet)).toMatchInlineSnapshot(
|
|
30
|
-
`"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-sky-600: oklch(58.8% 0.158 241.966)!important;--color-gray-100: oklch(96.7% 0.003 264.542)!important}}@layer utilities{.hover_text-sky-600{&:hover{@media (hover:hover){color:var(--color-sky-600)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{
|
|
31
|
+
`"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-sky-600: oklch(58.8% 0.158 241.966)!important;--color-gray-100: oklch(96.7% 0.003 264.542)!important}}@layer utilities{.hover_text-sky-600{&:hover{@media (hover:hover){color:var(--color-sky-600)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{outline-style:none!important}}}.md_hover_bg-gray-100{@media (width>=48rem){&:hover{@media (hover:hover){background-color:var(--color-gray-100)!important}}}}.lg_focus_underline{@media (width>=64rem){&:focus{text-decoration-line:underline!important}}}}"`,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('strips Tailwind v4 variant-stacking var() refs with empty fallbacks inside print: media queries', async () => {
|
|
36
|
+
// Mirrors the inline-style behavior asserted in make-inline-styles-for.spec.ts.
|
|
37
|
+
// `print:invert` compiles to a filter value that is a chain of var(--tw-...,)
|
|
38
|
+
// with empty fallbacks. After resolveAllCssVariables the filter is concrete;
|
|
39
|
+
// sanitizeNonInlinableRules then drops the leftover --tw-* declarations and
|
|
40
|
+
// any remaining empty-fallback var() refs.
|
|
41
|
+
const tailwind = await setupTailwind({});
|
|
42
|
+
tailwind.addUtilities(['md:block', 'print:invert']);
|
|
43
|
+
const stylesheet = tailwind.getStyleSheet();
|
|
44
|
+
|
|
45
|
+
sanitizeStyleSheet(stylesheet);
|
|
46
|
+
sanitizeNonInlinableRules(stylesheet);
|
|
47
|
+
const result = generate(stylesheet);
|
|
48
|
+
|
|
49
|
+
expect(result).not.toMatch(/var\(--tw-[^,()]+,\s*\)/);
|
|
50
|
+
expect(result).toMatchInlineSnapshot(
|
|
51
|
+
`"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer utilities{.md_block{@media (width>=48rem){display:block!important}}.print_invert{@media print{filter:invert(100%)!important}}}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}"`,
|
|
31
52
|
);
|
|
32
53
|
});
|
|
33
54
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CssNode, string, walk } from 'css-tree';
|
|
2
2
|
import { sanitizeClassName } from '../compatibility/sanitize-class-name.js';
|
|
3
3
|
import { isRuleInlinable } from './is-rule-inlinable.js';
|
|
4
|
+
import { stripEmptyTailwindVars } from './strip-empty-tailwind-vars.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* This function goes through a few steps to ensure the best email client support and
|
|
@@ -10,6 +11,12 @@ import { isRuleInlinable } from './is-rule-inlinable.js';
|
|
|
10
11
|
* What it does:
|
|
11
12
|
* 1. Converts all declarations in all rules into important ones
|
|
12
13
|
* 2. Sanitizes class selectors of all non-inlinable rules
|
|
14
|
+
* 3. Removes --tw-* custom property declarations — by this point all CSS
|
|
15
|
+
* variables have been resolved, so these are dead weight in email HTML.
|
|
16
|
+
* 4. Strips empty-fallback var(--tw-*,) refs that Tailwind v4 emits for
|
|
17
|
+
* variant-stacking idioms (filter, font-variant-numeric, etc.) — email
|
|
18
|
+
* clients can't resolve CSS custom properties reliably, so any --tw-*
|
|
19
|
+
* left as a bare empty-fallback ref would reach the client broken.
|
|
13
20
|
*/
|
|
14
21
|
export function sanitizeNonInlinableRules(node: CssNode) {
|
|
15
22
|
walk(node, {
|
|
@@ -25,8 +32,14 @@ export function sanitizeNonInlinableRules(node: CssNode) {
|
|
|
25
32
|
|
|
26
33
|
walk(rule, {
|
|
27
34
|
visit: 'Declaration',
|
|
28
|
-
enter(declaration) {
|
|
35
|
+
enter(declaration, item, list) {
|
|
36
|
+
if (declaration.property.startsWith('--tw-')) {
|
|
37
|
+
list.remove(item);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
declaration.important = true;
|
|
42
|
+
stripEmptyTailwindVars(declaration.value);
|
|
30
43
|
},
|
|
31
44
|
});
|
|
32
45
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Atrule,
|
|
3
|
+
type Declaration,
|
|
4
|
+
generate,
|
|
5
|
+
parse,
|
|
6
|
+
type Rule,
|
|
7
|
+
type StyleSheet,
|
|
8
|
+
walk,
|
|
9
|
+
} from 'css-tree';
|
|
10
|
+
import { sanitizeStyleSheet } from '../../sanitize-stylesheet.js';
|
|
11
|
+
import { setupTailwind } from '../tailwindcss/setup-tailwind.js';
|
|
12
|
+
import { isRuleInlinable } from './is-rule-inlinable.js';
|
|
13
|
+
import { sanitizeNonInlinableRules } from './sanitize-non-inlinable-rules.js';
|
|
14
|
+
import { stripEmptyTailwindVars } from './strip-empty-tailwind-vars.js';
|
|
15
|
+
|
|
16
|
+
describe('stripEmptyTailwindVars()', () => {
|
|
17
|
+
it('removes empty-fallback var(--tw-*,) refs from declaration values', () => {
|
|
18
|
+
const stylesheet = parse(`
|
|
19
|
+
.tabular-nums {
|
|
20
|
+
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) tabular-nums var(--tw-numeric-fraction,);
|
|
21
|
+
}
|
|
22
|
+
`) as StyleSheet;
|
|
23
|
+
|
|
24
|
+
const rule = stylesheet.children.first as Rule;
|
|
25
|
+
const declaration = rule.block.children.first as Declaration;
|
|
26
|
+
stripEmptyTailwindVars(declaration.value);
|
|
27
|
+
|
|
28
|
+
expect(generate(declaration.value)).toBe('tabular-nums');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('does not remove var() refs with non-empty fallbacks or non --tw- names', () => {
|
|
32
|
+
const stylesheet = parse(`
|
|
33
|
+
.thing {
|
|
34
|
+
line-height: var(--tw-leading, var(--text-lg--line-height));
|
|
35
|
+
color: var(--my-color,);
|
|
36
|
+
}
|
|
37
|
+
`) as StyleSheet;
|
|
38
|
+
|
|
39
|
+
const rule = stylesheet.children.first as Rule;
|
|
40
|
+
const leading = rule.block.children.first as Declaration;
|
|
41
|
+
const color = rule.block.children.last as Declaration;
|
|
42
|
+
|
|
43
|
+
stripEmptyTailwindVars(leading.value);
|
|
44
|
+
stripEmptyTailwindVars(color.value);
|
|
45
|
+
|
|
46
|
+
expect(generate(leading.value)).toBe(
|
|
47
|
+
'var(--tw-leading, var(--text-lg--line-height))',
|
|
48
|
+
);
|
|
49
|
+
expect(generate(color.value)).toBe('var(--my-color,)');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('does not remove --tw-* custom property declarations (only var() usages in values)', () => {
|
|
53
|
+
const stylesheet = parse(`
|
|
54
|
+
.print_border-solid {
|
|
55
|
+
@media print {
|
|
56
|
+
--tw-border-style: solid;
|
|
57
|
+
border-style: var(--tw-border-style,);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
`) as StyleSheet;
|
|
61
|
+
|
|
62
|
+
const rule = stylesheet.children.first as Rule;
|
|
63
|
+
const atrule = rule.block.children.first as Atrule;
|
|
64
|
+
const twDeclaration = atrule.block!.children.first as Declaration;
|
|
65
|
+
const borderDeclaration = atrule.block!.children.last as Declaration;
|
|
66
|
+
|
|
67
|
+
stripEmptyTailwindVars(borderDeclaration.value);
|
|
68
|
+
|
|
69
|
+
expect(twDeclaration.property).toBe('--tw-border-style');
|
|
70
|
+
expect(generate(twDeclaration.value).trim()).toBe('solid');
|
|
71
|
+
expect(generate(borderDeclaration.value)).toBe('');
|
|
72
|
+
expect(generate(stylesheet)).toContain('--tw-border-style: solid');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('stripEmptyTailwindVars() with non-inlinable print: rules', () => {
|
|
77
|
+
it('print:border-solid still leaves --tw-* declarations if only stripEmptyTailwindVars runs', async () => {
|
|
78
|
+
const tailwind = await setupTailwind({});
|
|
79
|
+
tailwind.addUtilities(['print:border-solid']);
|
|
80
|
+
const stylesheet = tailwind.getStyleSheet();
|
|
81
|
+
|
|
82
|
+
sanitizeStyleSheet(stylesheet);
|
|
83
|
+
|
|
84
|
+
walkDeclarationsInNonInlinableRules(stylesheet, (declaration) => {
|
|
85
|
+
stripEmptyTailwindVars(declaration.value);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = generate(stylesheet);
|
|
89
|
+
|
|
90
|
+
expect(result).not.toMatch(/var\(--tw-[^,()]+,\s*\)/);
|
|
91
|
+
expect(result).toMatch(/--tw-[^:]+:/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('print:border-solid is clean after sanitizeNonInlinableRules removes resolved --tw-* declarations', async () => {
|
|
95
|
+
const tailwind = await setupTailwind({});
|
|
96
|
+
tailwind.addUtilities(['print:border-solid']);
|
|
97
|
+
const stylesheet = tailwind.getStyleSheet();
|
|
98
|
+
|
|
99
|
+
sanitizeStyleSheet(stylesheet);
|
|
100
|
+
sanitizeNonInlinableRules(stylesheet);
|
|
101
|
+
const result = generate(stylesheet);
|
|
102
|
+
|
|
103
|
+
expect(result).not.toMatch(/var\(--tw-[^,()]+,\s*\)/);
|
|
104
|
+
expect(result).not.toMatch(/--tw-[^:]+:/);
|
|
105
|
+
expect(result).toMatchInlineSnapshot(
|
|
106
|
+
`"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer utilities{.print_border-solid{@media print{border-style:solid!important}}}"`,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function walkDeclarationsInNonInlinableRules(
|
|
112
|
+
node: StyleSheet,
|
|
113
|
+
onDeclaration: (declaration: Declaration) => void,
|
|
114
|
+
) {
|
|
115
|
+
walk(node, {
|
|
116
|
+
visit: 'Rule',
|
|
117
|
+
enter(rule) {
|
|
118
|
+
if (!isRuleInlinable(rule)) {
|
|
119
|
+
walk(rule, {
|
|
120
|
+
visit: 'Declaration',
|
|
121
|
+
enter(declaration) {
|
|
122
|
+
onDeclaration(declaration);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type CssNode, generate, walk } from 'css-tree';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tailwind v4 emits variant-stacking idioms like
|
|
5
|
+
* font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) tabular-nums var(--tw-numeric-fraction,)
|
|
6
|
+
* filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-invert,) ...
|
|
7
|
+
* where each var() has an empty fallback so missing variants collapse to nothing.
|
|
8
|
+
* Tailwind deliberately leaves these variant vars undefined until used, so they
|
|
9
|
+
* stay in the output here and produce unresolvable custom properties in email HTML
|
|
10
|
+
* (no email client supports CSS custom properties reliably). Per the CSS spec
|
|
11
|
+
* (https://www.w3.org/TR/css-variables-1/#using-variables) an empty fallback means
|
|
12
|
+
* "use empty string if the variable is undefined", which is exactly what we want.
|
|
13
|
+
*
|
|
14
|
+
* Scoped to the `--tw-` prefix so any user-authored empty-fallback var() refs
|
|
15
|
+
* are left untouched.
|
|
16
|
+
*
|
|
17
|
+
* Uses post-order traversal so an outer var(--tw-X, var(--tw-Y,)) collapses
|
|
18
|
+
* correctly after the inner var() has been removed.
|
|
19
|
+
*/
|
|
20
|
+
export function stripEmptyTailwindVars(node: CssNode) {
|
|
21
|
+
walk(node, {
|
|
22
|
+
visit: 'Function',
|
|
23
|
+
leave(func, funcItem, funcList) {
|
|
24
|
+
if (func.name !== 'var') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let variableName: string | undefined;
|
|
29
|
+
walk(func, {
|
|
30
|
+
visit: 'Identifier',
|
|
31
|
+
enter(identifier) {
|
|
32
|
+
variableName = identifier.name;
|
|
33
|
+
return this.break;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (!variableName?.startsWith('--tw-')) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let sawComma = false;
|
|
41
|
+
let hasFallbackContent = false;
|
|
42
|
+
func.children.forEach((child) => {
|
|
43
|
+
if (!sawComma) {
|
|
44
|
+
if (child.type === 'Operator' && child.value === ',') {
|
|
45
|
+
sawComma = true;
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let childValue = generate(child).trim();
|
|
51
|
+
if (child.type === 'Raw') {
|
|
52
|
+
const emptyTailwindVarPattern = /var\(--tw-[^,()]+,\s*\)/g;
|
|
53
|
+
childValue = childValue
|
|
54
|
+
.replaceAll(emptyTailwindVarPattern, '')
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (childValue.length > 0) {
|
|
59
|
+
hasFallbackContent = true;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!sawComma || hasFallbackContent) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
funcList.remove(funcItem);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
package/tsconfig.json
CHANGED