Skip to content

Commit 89024ce

Browse files
committed
fix: fix issue where pseudo classes like :where, :not, :is were always removed at root level
#1282 #978
1 parent 28783b3 commit 89024ce

File tree

3 files changed

+65
-14
lines changed

3 files changed

+65
-14
lines changed

packages/purgecss/__tests__/pseudo-class.test.ts

+26-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PurgeCSS } from "./../src/index";
2-
import { ROOT_TEST_EXAMPLES } from "./utils";
2+
import { findInCSS, ROOT_TEST_EXAMPLES } from "./utils";
33

44
describe(":not pseudo class", () => {
55
let purgedCSS: string;
@@ -116,13 +116,18 @@ describe(":where pseudo class", () => {
116116

117117
it("removes unused selectors", () => {
118118
expect(purgedCSS.includes(".unused")).toBe(false);
119-
expect(purgedCSS.includes(".root :where(.a) .c {")).toBe(true);
120-
expect(purgedCSS.includes(".root:where(.a) .c {")).toBe(true);
121-
expect(
122-
purgedCSS.includes(
119+
});
120+
121+
it("keeps used selectors", () => {
122+
findInCSS(
123+
expect,
124+
[
125+
".root :where(.a) .c {",
126+
".root:where(.a) .c {",
123127
".\\[\\&\\:where\\(\\.a\\)\\]\\:text-black:where(.a) {",
124-
),
125-
).toBe(true);
128+
],
129+
purgedCSS,
130+
);
126131
});
127132
});
128133

@@ -141,10 +146,19 @@ describe(":is pseudo class", () => {
141146

142147
it("removes unused selectors", () => {
143148
expect(purgedCSS.includes(".unused")).toBe(false);
144-
expect(purgedCSS.includes(".root :is(.a) .c {")).toBe(true);
145-
expect(purgedCSS.includes(".root:is(.a) .c {")).toBe(true);
146-
expect(
147-
purgedCSS.includes(".\\[\\&\\:is\\(\\.a\\)\\]\\:text-black:is(.a) {"),
148-
).toBe(true);
149+
expect(purgedCSS.includes(":is(.unused)")).toBe(false);
150+
});
151+
152+
it("keeps used selectors", () => {
153+
findInCSS(
154+
expect,
155+
[
156+
".root :is(.a) .c {",
157+
".root:is(.a) .c {",
158+
".\\[\\&\\:is\\(\\.a\\)\\]\\:text-black:is(.a) {",
159+
":is(.b)",
160+
],
161+
purgedCSS,
162+
);
149163
});
150164
});

packages/purgecss/__tests__/test_examples/pseudo-class/is.css

+8
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,12 @@
2525

2626
.\[\&\:is\(\.a\)\]\:text-black:is(.a) {
2727
color: black;
28+
}
29+
30+
:is(.b) {
31+
color: black;
32+
}
33+
34+
:is(.unused) {
35+
color: chartreuse;
2836
}

packages/purgecss/src/index.ts

+31-2
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,33 @@ function isInPseudoClassWhereOrIs(selector: selectorParser.Node): boolean {
303303
);
304304
}
305305

306+
/**
307+
* Returns true if the selector is a pseudo class at the root level
308+
* Pseudo classes checked: :where, :is, :has, :not
309+
* @param selector - selector
310+
*/
311+
function isPseudoClassAtRootLevel(selector: selectorParser.Node): boolean {
312+
let result = false;
313+
if (
314+
selector.type === "selector" &&
315+
selector.parent?.type === "root" &&
316+
selector.nodes.length === 1
317+
) {
318+
selector.walk((node) => {
319+
if (
320+
node.type === "pseudo" &&
321+
(node.value === ":where" ||
322+
node.value === ":is" ||
323+
node.value === ":has" ||
324+
node.value === ":not")
325+
) {
326+
result = true;
327+
}
328+
});
329+
}
330+
return result;
331+
}
332+
306333
function isPostCSSAtRule(node?: postcss.Node): node is postcss.AtRule {
307334
return node?.type === "atrule";
308335
}
@@ -531,7 +558,6 @@ class PurgeCSS {
531558
}
532559

533560
const selectorsRemovedFromRule: string[] = [];
534-
535561
// selector transformer, walk over the list of the parsed selectors twice.
536562
// First pass will remove the unused selectors. It goes through
537563
// pseudo-classes like :where() and :is() and remove the unused
@@ -543,7 +569,6 @@ class PurgeCSS {
543569
if (selector.type !== "selector") {
544570
return;
545571
}
546-
547572
const keepSelector = this.shouldKeepSelector(selector, selectors);
548573

549574
if (!keepSelector) {
@@ -864,6 +889,10 @@ class PurgeCSS {
864889
return true;
865890
}
866891

892+
if (isPseudoClassAtRootLevel(selector)) {
893+
return true;
894+
}
895+
867896
// if there is any greedy safelist pattern, run all the selector parts through them
868897
// if there is any match, return true
869898
if (this.options.safelist.greedy.length > 0) {

0 commit comments

Comments
 (0)