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** diff --git a/packages/purgecss/__tests__/rejectedCss.test.ts b/packages/purgecss/__tests__/rejectedCss.test.ts new file mode 100644 index 00000000..8ab5bcad --- /dev/null +++ b/packages/purgecss/__tests__/rejectedCss.test.ts @@ -0,0 +1,41 @@ +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?.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]); + }); + /** + * 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/__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" 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 10dde221..e28b2755 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -268,6 +268,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; @@ -456,6 +457,7 @@ class PurgeCSS { } let keepSelector = true; + const originalSelector = node.selector; node.selector = selectorParser((selectorsParsed) => { selectorsParsed.walk((selector) => { if (selector.type !== "selector") { @@ -465,8 +467,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(); } }); @@ -482,7 +485,19 @@ class PurgeCSS { // remove empty rules const parent = node.parent; - if (!node.selector) node.remove(); + if (!node.selector) { + node.remove(); + if (this.options.rejectedCss) { + node.selector = originalSelector; + 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(); } @@ -538,6 +553,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..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; @@ -81,6 +82,7 @@ export interface Options { keyframes: boolean; output?: string; rejected: boolean; + rejectedCss: boolean; stdin: boolean; stdout: boolean; variables: boolean; @@ -92,6 +94,7 @@ export interface Options { export interface ResultPurge { css: string; + rejectedCss?: string; file?: string; rejected?: string[]; }