fix(tailwind): pseudo selectors (#2963)

Co-authored-by: gabriel miranda <gabrielmfern@outlook.com>
This commit is contained in:
Bu Kinoshita
2026-02-18 13:23:12 +00:00
committed by GitHub
parent 1360dd9fac
commit 698f96228f
3 changed files with 53 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@react-email/tailwind": patch
---
fix mixed pseudo selectors not being split
@@ -112,4 +112,35 @@ describe('extractRulesPerClass()', async () => {
}
`);
});
it('treats rules with pseudo-selectors as fully non-inlinable', async () => {
const tailwind = await setupTailwind({
plugins: [
{
handler: (api) => {
api.addUtilities({
'.btn:hover': {
color: 'red',
},
});
},
},
],
});
const classes = ['btn'];
tailwind.addUtilities(classes);
const stylesheet = tailwind.getStyleSheet();
const { inlinable, nonInlinable } = extractRulesPerClass(
stylesheet,
classes,
);
expect(convertToComparable(inlinable)).toMatchInlineSnapshot('{}');
expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
{
"btn": ".btn{&:hover{color:red}}",
}
`);
});
});
@@ -1,4 +1,4 @@
import { type CssNode, clone, List, type Rule } from 'css-tree';
import { type CssNode, clone, find, List, type Rule } from 'css-tree';
import { isPartInlinable } from './is-part-inlinable';
/**
@@ -12,6 +12,22 @@ export function splitMixedRule(rule: Rule): {
inlinablePart: Rule | null;
nonInlinablePart: Rule | null;
} {
// If the selector itself contains pseudo-selectors, every declaration in the
// block only applies in that pseudo context — splitting would incorrectly
// inline them as base styles. Return the whole rule as non-inlinable.
const selectorHasPseudo =
rule.prelude !== null &&
find(
rule.prelude,
(node) =>
node.type === 'PseudoClassSelector' ||
node.type === 'PseudoElementSelector',
) !== null;
if (selectorHasPseudo) {
return { inlinablePart: null, nonInlinablePart: clone(rule) as Rule };
}
const ruleCloneInlinable = clone(rule) as Rule;
const ruleCloneNonInlinable = clone(rule) as Rule;