From 642560abe880cb2a719ab332fbe9088455b1cb4b Mon Sep 17 00:00:00 2001 From: Kevin Ramharak Date: Sun, 12 Sep 2021 22:33:23 +0200 Subject: [PATCH 1/8] naive implementation of output-unused-css --- packages/purgecss/src/bin.ts | 4 ++++ packages/purgecss/src/index.ts | 17 ++++++++++++++--- packages/purgecss/src/options.ts | 1 + packages/purgecss/src/types/index.ts | 2 ++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/purgecss/src/bin.ts b/packages/purgecss/src/bin.ts index 62bbeb02..fac6f4ab 100644 --- a/packages/purgecss/src/bin.ts +++ b/packages/purgecss/src/bin.ts @@ -25,6 +25,7 @@ type CommandOptions = { keyframes?: boolean; variables?: boolean; rejected?: boolean; + rejectedCss?: boolean; safelist?: string[]; blocklist?: string[]; skippedContentGlobs: string[]; @@ -48,6 +49,7 @@ function parseCommandOptions() { .option("-keyframes, --keyframes", "option to remove unused keyframes") .option("-v, --variables", "option to remove unused variables") .option("-rejected, --rejected", "option to output rejected selectors") + .option("-rejected-css, --rejected-css", "option to output rejected css") .option( "-s, --safelist ", "list of classes that should not be removed" @@ -77,6 +79,7 @@ async function run() { keyframes, variables, rejected, + rejectedCss, safelist, blocklist, skippedContentGlobs, @@ -99,6 +102,7 @@ async function run() { if (fontFace) options.fontFace = fontFace; if (keyframes) options.keyframes = keyframes; if (rejected) options.rejected = rejected; + if (rejectedCss) options.rejectedCss = rejectedCss; if (variables) options.variables = variables; if (safelist) options.safelist = standardizeSafelist(safelist); if (blocklist) options.blocklist = blocklist; diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts index d4ac8cbb..42652e5c 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -267,6 +267,7 @@ class PurgeCSS { private usedAnimations: Set = new Set(); private usedFontFaces: Set = new Set(); public selectorsRemoved: Set = new Set(); + public removedNodes: postcss.Node[] = []; private variablesStructure: VariablesStructure = new VariablesStructure(); public options: Options = defaultOptions; @@ -455,7 +456,7 @@ class PurgeCSS { } let keepSelector = true; - node.selector = selectorParser((selectorsParsed) => { + selectorParser((selectorsParsed) => { selectorsParsed.walk((selector) => { if (selector.type !== "selector") { return; @@ -464,8 +465,9 @@ class PurgeCSS { keepSelector = this.shouldKeepSelector(selector, selectors); if (!keepSelector) { - if (this.options.rejected) + if (this.options.rejected) { this.selectorsRemoved.add(selector.toString()); + } selector.remove(); } }); @@ -481,7 +483,12 @@ class PurgeCSS { // remove empty rules const parent = node.parent; - if (!node.selector) node.remove(); + if (!keepSelector) { + node.remove() + if (this.options.rejectedCss) { + this.removedNodes.push(node); + } + } if (isRuleEmpty(parent)) parent?.remove(); } @@ -537,6 +544,10 @@ class PurgeCSS { this.selectorsRemoved.clear(); } + if (this.options.rejectedCss) { + result.rejectedCss = postcss.root({ nodes: this.removedNodes }).toString(); + } + sources.push(result); } return sources; diff --git a/packages/purgecss/src/options.ts b/packages/purgecss/src/options.ts index 11027f40..9ca31bf3 100644 --- a/packages/purgecss/src/options.ts +++ b/packages/purgecss/src/options.ts @@ -9,6 +9,7 @@ export const defaultOptions: Options = { fontFace: false, keyframes: false, rejected: false, + rejectedCss: false, stdin: false, stdout: false, variables: false, diff --git a/packages/purgecss/src/types/index.ts b/packages/purgecss/src/types/index.ts index feaf3bd8..0b27de31 100644 --- a/packages/purgecss/src/types/index.ts +++ b/packages/purgecss/src/types/index.ts @@ -81,6 +81,7 @@ export interface Options { keyframes: boolean; output?: string; rejected: boolean; + rejectedCss: boolean; stdin: boolean; stdout: boolean; variables: boolean; @@ -92,6 +93,7 @@ export interface Options { export interface ResultPurge { css: string; + rejectedCss?: string; file?: string; rejected?: string[]; } From 4c1b8c9dd4b763bbd078b771bce1ecefca3963cd Mon Sep 17 00:00:00 2001 From: Kevin Ramharak Date: Sun, 12 Sep 2021 22:54:48 +0200 Subject: [PATCH 2/8] undo part of the changes, since i broke some tests --- packages/purgecss/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts index 42652e5c..7a14f41f 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -456,7 +456,8 @@ class PurgeCSS { } let keepSelector = true; - selectorParser((selectorsParsed) => { + const originalSelector = node.selector; + node.selector = selectorParser((selectorsParsed) => { selectorsParsed.walk((selector) => { if (selector.type !== "selector") { return; @@ -483,9 +484,10 @@ class PurgeCSS { // remove empty rules const parent = node.parent; - if (!keepSelector) { + if (!node.selector) { node.remove() if (this.options.rejectedCss) { + node.selector = originalSelector; this.removedNodes.push(node); } } From 1a8895e40804d08b50371aebddadcc2851ccb815 Mon Sep 17 00:00:00 2001 From: Kevin Ramharak Date: Mon, 13 Sep 2021 13:13:43 +0200 Subject: [PATCH 3/8] add missing change to types --- packages/purgecss/src/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/purgecss/src/types/index.ts b/packages/purgecss/src/types/index.ts index 0b27de31..f7619364 100644 --- a/packages/purgecss/src/types/index.ts +++ b/packages/purgecss/src/types/index.ts @@ -63,6 +63,7 @@ export interface UserDefinedOptions { keyframes?: boolean; output?: string; rejected?: boolean; + rejectedCss?: boolean; stdin?: boolean; stdout?: boolean; variables?: boolean; From efe0eda7ced10c9a80f56bb44c097ff7c64f75a3 Mon Sep 17 00:00:00 2001 From: Kevin Ramharak Date: Mon, 13 Sep 2021 13:13:53 +0200 Subject: [PATCH 4/8] setup basic test --- .../purgecss/__tests__/rejectedCss.test.ts | 19 +++++++++++++++++++ .../test_examples/rejectedCss/simple.css | 7 +++++++ .../test_examples/rejectedCss/simple.js | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 packages/purgecss/__tests__/rejectedCss.test.ts create mode 100644 packages/purgecss/__tests__/test_examples/rejectedCss/simple.css create mode 100644 packages/purgecss/__tests__/test_examples/rejectedCss/simple.js diff --git a/packages/purgecss/__tests__/rejectedCss.test.ts b/packages/purgecss/__tests__/rejectedCss.test.ts new file mode 100644 index 00000000..5c3353e6 --- /dev/null +++ b/packages/purgecss/__tests__/rejectedCss.test.ts @@ -0,0 +1,19 @@ +import PurgeCSS from "./../src/index"; +import { ROOT_TEST_EXAMPLES } from "./utils"; + +describe("rejectedCss", () => { + it("returns the rejected css as part of the result", async () => { + expect.assertions(1); + const resultsPurge = await new PurgeCSS().purge({ + content: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.js`], + css: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.css`], + rejectedCss: true, + }); + const expected = ` +.rejected { + color: blue; +} +`; + expect(resultsPurge[0].rejectedCss).toBe(expected); + }); +}); diff --git a/packages/purgecss/__tests__/test_examples/rejectedCss/simple.css b/packages/purgecss/__tests__/test_examples/rejectedCss/simple.css new file mode 100644 index 00000000..f3ab29dd --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/rejectedCss/simple.css @@ -0,0 +1,7 @@ +.critical { + color: red; +} + +.rejected { + color: blue; +} diff --git a/packages/purgecss/__tests__/test_examples/rejectedCss/simple.js b/packages/purgecss/__tests__/test_examples/rejectedCss/simple.js new file mode 100644 index 00000000..71ef7950 --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/rejectedCss/simple.js @@ -0,0 +1,2 @@ + +"critical" From f2f26f2c5153f29d41cf4396a9e0a95959755afb Mon Sep 17 00:00:00 2001 From: Kevin Ramharak Date: Sat, 25 Sep 2021 09:52:36 +0200 Subject: [PATCH 5/8] attempt to fix test that is failing because of line ending drama --- packages/purgecss/__tests__/rejectedCss.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/purgecss/__tests__/rejectedCss.test.ts b/packages/purgecss/__tests__/rejectedCss.test.ts index 5c3353e6..0c82dac8 100644 --- a/packages/purgecss/__tests__/rejectedCss.test.ts +++ b/packages/purgecss/__tests__/rejectedCss.test.ts @@ -12,8 +12,7 @@ describe("rejectedCss", () => { const expected = ` .rejected { color: blue; -} -`; - expect(resultsPurge[0].rejectedCss).toBe(expected); +}`; + expect(resultsPurge[0].rejectedCss?.trim()).toBe(expected.trim()); }); }); From be189b9be0f3704d5d7e5caba87601696b4ebcdc Mon Sep 17 00:00:00 2001 From: Kevin Ramharak Date: Sat, 25 Sep 2021 10:00:45 +0200 Subject: [PATCH 6/8] test if rejected and rejectedCss stay in sync --- packages/purgecss/__tests__/rejectedCss.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/purgecss/__tests__/rejectedCss.test.ts b/packages/purgecss/__tests__/rejectedCss.test.ts index 0c82dac8..e3da1129 100644 --- a/packages/purgecss/__tests__/rejectedCss.test.ts +++ b/packages/purgecss/__tests__/rejectedCss.test.ts @@ -15,4 +15,14 @@ describe("rejectedCss", () => { }`; expect(resultsPurge[0].rejectedCss?.trim()).toBe(expected.trim()); }); + it("contains the rejected selectors as part of the rejected css", async () => { + expect.assertions(1); + const resultsPurge = await new PurgeCSS().purge({ + content: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.js`], + css: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.css`], + rejected: true, + rejectedCss: true, + }); + expect(resultsPurge[0].rejectedCss?.trim()).toContain(resultsPurge[0].rejected?.[0]); + }); }); From 882b4676e9f86377807bba2938e69d4ad044a0cb Mon Sep 17 00:00:00 2001 From: Kevin Ramharak Date: Sat, 25 Sep 2021 10:07:03 +0200 Subject: [PATCH 7/8] update the docs --- docs/CLI.md | 27 +++++++++++++++++---------- docs/api.md | 1 + docs/configuration.md | 12 ++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/CLI.md b/docs/CLI.md index c515f3bc..8b169340 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -37,18 +37,25 @@ npm i -g purgecss To see the available options for the CLI: `purgecss --help` ```text -Usage: purgecss --css --content [options] +Usage: purgecss --css --content [options] + +Remove unused css selectors Options: - -con, --content glob of content files - -css, --css glob of css files - -c, --config path to the configuration file - -o, --output file path directory to write purged css files to - -font, --font-face option to remove unused font-faces - -keyframes, --keyframes option to remove unused keyframes - -rejected, --rejected option to output rejected selectors - -s, --safelist list of classes that should not be removed - -h, --help display help for command + -V, --version output the version number + -con, --content glob of content files + -css, --css glob of css files + -c, --config path to the configuration file + -o, --output file path directory to write purged css files to + -font, --font-face option to remove unused font-faces + -keyframes, --keyframes option to remove unused keyframes + -v, --variables option to remove unused variables + -rejected, --rejected option to output rejected selectors + -rejected-css, --rejected-css option to output rejected css + -s, --safelist list of classes that should not be removed + -b, --blocklist list of selectors that should be removed + -k, --skippedContentGlobs list of glob patterns for folders/files that should not be scanned + -h, --help display help for command ``` The options available through the CLI are similar to the ones available with a configuration file. You can also use the CLI with a configuration file. diff --git a/docs/api.md b/docs/api.md index 276168ce..11d4385b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -74,5 +74,6 @@ interface ResultPurge { css: string; file?: string; rejected?: string[]; + rejectedCss?: string; } ``` diff --git a/docs/configuration.md b/docs/configuration.md index 18f99a72..4f0c48df 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -57,6 +57,7 @@ interface UserDefinedOptions { keyframes?: boolean; output?: string; rejected?: boolean; + rejectedCss?: boolean; stdin?: boolean; stdout?: boolean; variables?: boolean; @@ -236,6 +237,17 @@ await new PurgeCSS().purge({ rejected: true }) ``` +- **rejectedCss \(default: false\)** + +If you would like to keep the discarded CSS you can do so by using the `rejectedCss` option. + +```js +await new PurgeCSS().purge({ + content: ['index.html', '**/*.js', '**/*.html', '**/*.vue'], + css: ['css/app.css'], + rejectedCss: true +}) +``` - **safelist** From c8f780d96a221ed24d788e823f66ca14c033da79 Mon Sep 17 00:00:00 2001 From: Kevin Ramharak Date: Fri, 26 Nov 2021 12:43:02 +0100 Subject: [PATCH 8/8] add a test case to preserve empty parent nodes --- packages/purgecss/__tests__/rejectedCss.test.ts | 13 +++++++++++++ .../test_examples/rejectedCss/empty-parent-node.css | 5 +++++ .../test_examples/rejectedCss/empty-parent-node.js | 0 packages/purgecss/src/index.ts | 10 ++++++++-- 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.css create mode 100644 packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.js diff --git a/packages/purgecss/__tests__/rejectedCss.test.ts b/packages/purgecss/__tests__/rejectedCss.test.ts index e3da1129..8ab5bcad 100644 --- a/packages/purgecss/__tests__/rejectedCss.test.ts +++ b/packages/purgecss/__tests__/rejectedCss.test.ts @@ -25,4 +25,17 @@ describe("rejectedCss", () => { }); expect(resultsPurge[0].rejectedCss?.trim()).toContain(resultsPurge[0].rejected?.[0]); }); + /** + * https://github.com/FullHuman/purgecss/pull/763#discussion_r754618902 + */ + it("preserves the node correctly when having an empty parent node", async () => { + expect.assertions(1); + const resultsPurge = await new PurgeCSS().purge({ + content: [`${ROOT_TEST_EXAMPLES}rejectedCss/empty-parent-node.js`], + css: [`${ROOT_TEST_EXAMPLES}rejectedCss/empty-parent-node.css`], + rejectedCss: true, + }); + const expected = `@media (max-width: 66666px) {\n .unused-class, .unused-class2 {\n color: black;\n }\n}`; + expect(resultsPurge[0].rejectedCss?.trim()).toEqual(expected); + }); }); diff --git a/packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.css b/packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.css new file mode 100644 index 00000000..ff2ae0ed --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.css @@ -0,0 +1,5 @@ +@media (max-width: 66666px) { + .unused-class, .unused-class2 { + color: black; + } +} diff --git a/packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.js b/packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts index ae09f088..e28b2755 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -486,10 +486,16 @@ class PurgeCSS { // remove empty rules const parent = node.parent; if (!node.selector) { - node.remove() + node.remove(); if (this.options.rejectedCss) { node.selector = originalSelector; - this.removedNodes.push(node); + if (parent && isRuleEmpty(parent)) { + const clone = parent.clone(); + clone.append(node); + this.removedNodes.push(clone); + } else { + this.removedNodes.push(node); + } } } if (isRuleEmpty(parent)) parent?.remove();