Skip to content

Commit

Permalink
fix: fix issue where pseudo classes like :where, :not, :is were alway…
Browse files Browse the repository at this point in the history
…s removed at root level

#1282 #978
  • Loading branch information
Ffloriel committed Nov 12, 2024
1 parent 28783b3 commit 89024ce
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 14 deletions.
38 changes: 26 additions & 12 deletions packages/purgecss/__tests__/pseudo-class.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PurgeCSS } from "./../src/index";
import { ROOT_TEST_EXAMPLES } from "./utils";
import { findInCSS, ROOT_TEST_EXAMPLES } from "./utils";

describe(":not pseudo class", () => {
let purgedCSS: string;
Expand Down Expand Up @@ -116,13 +116,18 @@ describe(":where pseudo class", () => {

it("removes unused selectors", () => {
expect(purgedCSS.includes(".unused")).toBe(false);
expect(purgedCSS.includes(".root :where(.a) .c {")).toBe(true);
expect(purgedCSS.includes(".root:where(.a) .c {")).toBe(true);
expect(
purgedCSS.includes(
});

it("keeps used selectors", () => {
findInCSS(
expect,
[
".root :where(.a) .c {",
".root:where(.a) .c {",
".\\[\\&\\:where\\(\\.a\\)\\]\\:text-black:where(.a) {",
),
).toBe(true);
],
purgedCSS,
);
});
});

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

it("removes unused selectors", () => {
expect(purgedCSS.includes(".unused")).toBe(false);
expect(purgedCSS.includes(".root :is(.a) .c {")).toBe(true);
expect(purgedCSS.includes(".root:is(.a) .c {")).toBe(true);
expect(
purgedCSS.includes(".\\[\\&\\:is\\(\\.a\\)\\]\\:text-black:is(.a) {"),
).toBe(true);
expect(purgedCSS.includes(":is(.unused)")).toBe(false);
});

it("keeps used selectors", () => {
findInCSS(
expect,
[
".root :is(.a) .c {",
".root:is(.a) .c {",
".\\[\\&\\:is\\(\\.a\\)\\]\\:text-black:is(.a) {",
":is(.b)",
],
purgedCSS,
);
});
});
8 changes: 8 additions & 0 deletions packages/purgecss/__tests__/test_examples/pseudo-class/is.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@

.\[\&\:is\(\.a\)\]\:text-black:is(.a) {
color: black;
}

:is(.b) {
color: black;
}

:is(.unused) {
color: chartreuse;
}
33 changes: 31 additions & 2 deletions packages/purgecss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,33 @@ function isInPseudoClassWhereOrIs(selector: selectorParser.Node): boolean {
);
}

/**
* Returns true if the selector is a pseudo class at the root level
* Pseudo classes checked: :where, :is, :has, :not
* @param selector - selector
*/
function isPseudoClassAtRootLevel(selector: selectorParser.Node): boolean {
let result = false;
if (
selector.type === "selector" &&
selector.parent?.type === "root" &&
selector.nodes.length === 1
) {
selector.walk((node) => {
if (
node.type === "pseudo" &&
(node.value === ":where" ||
node.value === ":is" ||
node.value === ":has" ||
node.value === ":not")
) {
result = true;
}
});
}
return result;
}

function isPostCSSAtRule(node?: postcss.Node): node is postcss.AtRule {
return node?.type === "atrule";
}
Expand Down Expand Up @@ -531,7 +558,6 @@ class PurgeCSS {
}

const selectorsRemovedFromRule: string[] = [];

// selector transformer, walk over the list of the parsed selectors twice.
// First pass will remove the unused selectors. It goes through
// pseudo-classes like :where() and :is() and remove the unused
Expand All @@ -543,7 +569,6 @@ class PurgeCSS {
if (selector.type !== "selector") {
return;
}

const keepSelector = this.shouldKeepSelector(selector, selectors);

if (!keepSelector) {
Expand Down Expand Up @@ -864,6 +889,10 @@ class PurgeCSS {
return true;
}

if (isPseudoClassAtRootLevel(selector)) {
return true;
}

// if there is any greedy safelist pattern, run all the selector parts through them
// if there is any match, return true
if (this.options.safelist.greedy.length > 0) {
Expand Down

0 comments on commit 89024ce

Please sign in to comment.