From c4053cb22183f53813a4d7097b52dc5bbf3793be Mon Sep 17 00:00:00 2001 From: Vasilii A <3757319+vsn4ik@users.noreply.github.com> Date: Sat, 22 Aug 2026 06:27:43 +0000 Subject: [PATCH] fix(#1347): purge of CSS variables in nested media queries --- .../__tests__/media-queries-variables.test.ts | 45 +++++++++++++++++++ .../__tests__/supports-variables.test.ts | 43 ++++++++++++++++++ .../media_queries_variables.css | 31 +++++++++++++ .../media_queries_variables.html | 4 ++ .../supports-variables/supports_variables.css | 31 +++++++++++++ .../supports_variables.html | 4 ++ packages/purgecss/src/index.ts | 15 ++++++- 7 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 packages/purgecss/__tests__/media-queries-variables.test.ts create mode 100644 packages/purgecss/__tests__/supports-variables.test.ts create mode 100644 packages/purgecss/__tests__/test_examples/media-queries-variables/media_queries_variables.css create mode 100644 packages/purgecss/__tests__/test_examples/media-queries-variables/media_queries_variables.html create mode 100644 packages/purgecss/__tests__/test_examples/supports-variables/supports_variables.css create mode 100644 packages/purgecss/__tests__/test_examples/supports-variables/supports_variables.html diff --git a/packages/purgecss/__tests__/media-queries-variables.test.ts b/packages/purgecss/__tests__/media-queries-variables.test.ts new file mode 100644 index 00000000..34977130 --- /dev/null +++ b/packages/purgecss/__tests__/media-queries-variables.test.ts @@ -0,0 +1,45 @@ +import { PurgeCSS, type ResultPurge } from "./../src/index"; +import { ROOT_TEST_EXAMPLES } from "./utils"; + +describe("media queries variables", () => { + let purgecssResult: ResultPurge; + beforeAll(async () => { + const purgecss = await new PurgeCSS().purge({ + content: [ + `${ROOT_TEST_EXAMPLES}media-queries-variables/media_queries_variables.html`, + ], + css: [ + `${ROOT_TEST_EXAMPLES}media-queries-variables/media_queries_variables.css`, + ], + variables: true, + rejected: true, + rejectedCss: true, + }); + purgecssResult = purgecss[0]; + }); + + it("keeps '--font-size-*'", () => { + expect(purgecssResult.css).toContain("--font-size-1:"); + expect(purgecssResult.css).toContain("--font-size-2:"); + expect(purgecssResult.css).toContain("--font-size-3:"); + }); + + it("removes '--unused-font-size-*'", () => { + expect(purgecssResult.rejectedCss).toContain("--unused-font-size-1"); + expect(purgecssResult.rejectedCss).toContain("--unused-font-size-2"); + expect(purgecssResult.css).not.toContain("--unused-font-size-1"); + expect(purgecssResult.css).not.toContain("--unused-font-size-2"); + }); + + it("finds .used-class-*", () => { + expect(purgecssResult.css).toContain(".used-class-1"); + expect(purgecssResult.css).toContain(".used-class-2"); + }); + + it("removes .unused-class-*", () => { + expect(purgecssResult.rejected).toContain(".unused-class-1"); + expect(purgecssResult.rejected).toContain(".unused-class-2"); + expect(purgecssResult.css).not.toContain(".unused-class-1"); + expect(purgecssResult.css).not.toContain(".unused-class-2"); + }); +}); diff --git a/packages/purgecss/__tests__/supports-variables.test.ts b/packages/purgecss/__tests__/supports-variables.test.ts new file mode 100644 index 00000000..268f9a2f --- /dev/null +++ b/packages/purgecss/__tests__/supports-variables.test.ts @@ -0,0 +1,43 @@ +import { PurgeCSS, type ResultPurge } from "./../src/index"; +import { ROOT_TEST_EXAMPLES } from "./utils"; + +describe("supports variables", () => { + let purgecssResult: ResultPurge; + beforeAll(async () => { + const purgecss = await new PurgeCSS().purge({ + content: [ + `${ROOT_TEST_EXAMPLES}supports-variables/supports_variables.html`, + ], + css: [`${ROOT_TEST_EXAMPLES}supports-variables/supports_variables.css`], + variables: true, + rejected: true, + rejectedCss: true, + }); + purgecssResult = purgecss[0]; + }); + + it("keeps '--font-size-*'", () => { + expect(purgecssResult.css).toContain("--font-size-1:"); + expect(purgecssResult.css).toContain("--font-size-2:"); + expect(purgecssResult.css).toContain("--font-size-3:"); + }); + + it("removes '--unused-font-size-*'", () => { + expect(purgecssResult.rejectedCss).toContain("--unused-font-size-1"); + expect(purgecssResult.rejectedCss).toContain("--unused-font-size-2"); + expect(purgecssResult.css).not.toContain("--unused-font-size-1"); + expect(purgecssResult.css).not.toContain("--unused-font-size-2"); + }); + + it("finds .used-class-*", () => { + expect(purgecssResult.css).toContain(".used-class-1"); + expect(purgecssResult.css).toContain(".used-class-2"); + }); + + it("removes .unused-class-*", () => { + expect(purgecssResult.rejected).toContain(".unused-class-1"); + expect(purgecssResult.rejected).toContain(".unused-class-2"); + expect(purgecssResult.css).not.toContain(".unused-class-1"); + expect(purgecssResult.css).not.toContain(".unused-class-2"); + }); +}); diff --git a/packages/purgecss/__tests__/test_examples/media-queries-variables/media_queries_variables.css b/packages/purgecss/__tests__/test_examples/media-queries-variables/media_queries_variables.css new file mode 100644 index 00000000..dedd8dbb --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/media-queries-variables/media_queries_variables.css @@ -0,0 +1,31 @@ +:root { + --font-size-1: 1.5rem; + --font-size-2: 1.5rem; + --font-size-3: 1.5rem; + --unused-font-size-1: 1.5rem; + --unused-font-size-2: 1.5rem; +} + +.used-class-1 { + .used-class-2 { + font-size: var(--font-size-1); + } + + .unused-class-1 { + font-size: var(--unused-font-size-1); + } + + @media (max-width: 768px) { + font-size: var(--font-size-2); + + .used-class-3 { + @media (min-width: 576px) { + font-size: var(--font-size-3); + } + } + + .unused-class-2 { + font-size: var(--unused-font-size-2); + } + } +} diff --git a/packages/purgecss/__tests__/test_examples/media-queries-variables/media_queries_variables.html b/packages/purgecss/__tests__/test_examples/media-queries-variables/media_queries_variables.html new file mode 100644 index 00000000..9ae6ee77 --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/media-queries-variables/media_queries_variables.html @@ -0,0 +1,4 @@ +
+ span 1 + span 2 +
diff --git a/packages/purgecss/__tests__/test_examples/supports-variables/supports_variables.css b/packages/purgecss/__tests__/test_examples/supports-variables/supports_variables.css new file mode 100644 index 00000000..695d7f62 --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/supports-variables/supports_variables.css @@ -0,0 +1,31 @@ +:root { + --font-size-1: 1.5rem; + --font-size-2: 1.5rem; + --font-size-3: 1.5rem; + --unused-font-size-1: 1.5rem; + --unused-font-size-2: 1.5rem; +} + +.used-class-1 { + .used-class-2 { + font-size: var(--font-size-1); + } + + .unused-class-1 { + font-size: var(--unused-font-size-1); + } + + @supports (display: grid) { + font-size: var(--font-size-2); + + .used-class-3 { + @supports (backdrop-filter: blur(10px)) { + font-size: var(--font-size-3); + } + } + + .unused-class-2 { + font-size: var(--unused-font-size-2); + } + } +} diff --git a/packages/purgecss/__tests__/test_examples/supports-variables/supports_variables.html b/packages/purgecss/__tests__/test_examples/supports-variables/supports_variables.html new file mode 100644 index 00000000..9ae6ee77 --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/supports-variables/supports_variables.html @@ -0,0 +1,4 @@ +
+ span 1 + span 2 +
diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts index 8ea76009..a82b1ce5 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -509,6 +509,19 @@ class PurgeCSS { * @param node - node of postcss AST */ private evaluateAtRule(node: postcss.AtRule): void { + // when tracking variables, collect var() usages from declarations + // nested inside at-rules (e.g. @media, @supports, ...) + if ( + this.options.variables && + (node.name === "media" || node.name === "supports") && + node.nodes + ) { + for (const childNode of node.nodes) { + if (childNode.type !== "decl") continue; + this.collectDeclarationsData(childNode); + } + } + // keyframes if (this.options.keyframes && node.name.endsWith("keyframes")) { this.atRules.keyframes.push(node); @@ -615,7 +628,7 @@ class PurgeCSS { }).processSync(node.selector); // declarations - if (node.selector && typeof node.nodes !== "undefined") { + if (node.selector && node.nodes) { for (const childNode of node.nodes) { if (childNode.type !== "decl") continue; this.collectDeclarationsData(childNode);