From 89024cea9b5ff45e1ba8af84a959d51d7bca9902 Mon Sep 17 00:00:00 2001 From: Floriel Date: Tue, 12 Nov 2024 16:11:23 +0100 Subject: [PATCH] fix: fix issue where pseudo classes like :where, :not, :is were always removed at root level #1282 #978 --- .../purgecss/__tests__/pseudo-class.test.ts | 38 +++++++++++++------ .../test_examples/pseudo-class/is.css | 8 ++++ packages/purgecss/src/index.ts | 33 +++++++++++++++- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/purgecss/__tests__/pseudo-class.test.ts b/packages/purgecss/__tests__/pseudo-class.test.ts index 87da3584..6a55e230 100644 --- a/packages/purgecss/__tests__/pseudo-class.test.ts +++ b/packages/purgecss/__tests__/pseudo-class.test.ts @@ -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; @@ -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, + ); }); }); @@ -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, + ); }); }); diff --git a/packages/purgecss/__tests__/test_examples/pseudo-class/is.css b/packages/purgecss/__tests__/test_examples/pseudo-class/is.css index 31b86954..fc612af6 100644 --- a/packages/purgecss/__tests__/test_examples/pseudo-class/is.css +++ b/packages/purgecss/__tests__/test_examples/pseudo-class/is.css @@ -25,4 +25,12 @@ .\[\&\:is\(\.a\)\]\:text-black:is(.a) { color: black; +} + +:is(.b) { + color: black; +} + +:is(.unused) { + color: chartreuse; } \ No newline at end of file diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts index dcd8a0fe..0c8e02c5 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -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"; } @@ -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 @@ -543,7 +569,6 @@ class PurgeCSS { if (selector.type !== "selector") { return; } - const keepSelector = this.shouldKeepSelector(selector, selectors); if (!keepSelector) { @@ -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) {