Skip to content

Commit 3a3d958

Browse files
feat: add option to ouput unused css (#763)
* naive implementation of output-unused-css * undo part of the changes, since i broke some tests * add missing change to types * setup basic test * attempt to fix test that is failing because of line ending drama * test if rejected and rejectedCss stay in sync * update the docs * add a test case to preserve empty parent nodes Co-authored-by: Ffloriel <florielfedry@gmail.com>
1 parent d80600f commit 3a3d958

File tree

12 files changed

+114
-12
lines changed

12 files changed

+114
-12
lines changed

docs/CLI.md

+17-10
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,25 @@ npm i -g purgecss
3737
To see the available options for the CLI: `purgecss --help`
3838

3939
```text
40-
Usage: purgecss --css <css> --content <content> [options]
40+
Usage: purgecss --css <css...> --content <content...> [options]
41+
42+
Remove unused css selectors
4143
4244
Options:
43-
-con, --content <files> glob of content files
44-
-css, --css <files> glob of css files
45-
-c, --config <path> path to the configuration file
46-
-o, --output <path> file path directory to write purged css files to
47-
-font, --font-face option to remove unused font-faces
48-
-keyframes, --keyframes option to remove unused keyframes
49-
-rejected, --rejected option to output rejected selectors
50-
-s, --safelist <list> list of classes that should not be removed
51-
-h, --help display help for command
45+
-V, --version output the version number
46+
-con, --content <files...> glob of content files
47+
-css, --css <files...> glob of css files
48+
-c, --config <path> path to the configuration file
49+
-o, --output <path> file path directory to write purged css files to
50+
-font, --font-face option to remove unused font-faces
51+
-keyframes, --keyframes option to remove unused keyframes
52+
-v, --variables option to remove unused variables
53+
-rejected, --rejected option to output rejected selectors
54+
-rejected-css, --rejected-css option to output rejected css
55+
-s, --safelist <list...> list of classes that should not be removed
56+
-b, --blocklist <list...> list of selectors that should be removed
57+
-k, --skippedContentGlobs <list...> list of glob patterns for folders/files that should not be scanned
58+
-h, --help display help for command
5259
```
5360

5461
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.

docs/api.md

+1
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,6 @@ interface ResultPurge {
7474
css: string;
7575
file?: string;
7676
rejected?: string[];
77+
rejectedCss?: string;
7778
}
7879
```

docs/configuration.md

+12
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface UserDefinedOptions {
5757
keyframes?: boolean;
5858
output?: string;
5959
rejected?: boolean;
60+
rejectedCss?: boolean;
6061
stdin?: boolean;
6162
stdout?: boolean;
6263
variables?: boolean;
@@ -236,6 +237,17 @@ await new PurgeCSS().purge({
236237
rejected: true
237238
})
238239
```
240+
- **rejectedCss \(default: false\)**
241+
242+
If you would like to keep the discarded CSS you can do so by using the `rejectedCss` option.
243+
244+
```js
245+
await new PurgeCSS().purge({
246+
content: ['index.html', '**/*.js', '**/*.html', '**/*.vue'],
247+
css: ['css/app.css'],
248+
rejectedCss: true
249+
})
250+
```
239251

240252
- **safelist**
241253

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import PurgeCSS from "./../src/index";
2+
import { ROOT_TEST_EXAMPLES } from "./utils";
3+
4+
describe("rejectedCss", () => {
5+
it("returns the rejected css as part of the result", async () => {
6+
expect.assertions(1);
7+
const resultsPurge = await new PurgeCSS().purge({
8+
content: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.js`],
9+
css: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.css`],
10+
rejectedCss: true,
11+
});
12+
const expected = `
13+
.rejected {
14+
color: blue;
15+
}`;
16+
expect(resultsPurge[0].rejectedCss?.trim()).toBe(expected.trim());
17+
});
18+
it("contains the rejected selectors as part of the rejected css", async () => {
19+
expect.assertions(1);
20+
const resultsPurge = await new PurgeCSS().purge({
21+
content: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.js`],
22+
css: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.css`],
23+
rejected: true,
24+
rejectedCss: true,
25+
});
26+
expect(resultsPurge[0].rejectedCss?.trim()).toContain(resultsPurge[0].rejected?.[0]);
27+
});
28+
/**
29+
* https://github.com/FullHuman/purgecss/pull/763#discussion_r754618902
30+
*/
31+
it("preserves the node correctly when having an empty parent node", async () => {
32+
expect.assertions(1);
33+
const resultsPurge = await new PurgeCSS().purge({
34+
content: [`${ROOT_TEST_EXAMPLES}rejectedCss/empty-parent-node.js`],
35+
css: [`${ROOT_TEST_EXAMPLES}rejectedCss/empty-parent-node.css`],
36+
rejectedCss: true,
37+
});
38+
const expected = `@media (max-width: 66666px) {\n .unused-class, .unused-class2 {\n color: black;\n }\n}`;
39+
expect(resultsPurge[0].rejectedCss?.trim()).toEqual(expected);
40+
});
41+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@media (max-width: 66666px) {
2+
.unused-class, .unused-class2 {
3+
color: black;
4+
}
5+
}

packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.js

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.critical {
2+
color: red;
3+
}
4+
5+
.rejected {
6+
color: blue;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
"critical"

packages/purgecss/src/bin.ts

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type CommandOptions = {
2525
keyframes?: boolean;
2626
variables?: boolean;
2727
rejected?: boolean;
28+
rejectedCss?: boolean;
2829
safelist?: string[];
2930
blocklist?: string[];
3031
skippedContentGlobs: string[];
@@ -48,6 +49,7 @@ function parseCommandOptions() {
4849
.option("-keyframes, --keyframes", "option to remove unused keyframes")
4950
.option("-v, --variables", "option to remove unused variables")
5051
.option("-rejected, --rejected", "option to output rejected selectors")
52+
.option("-rejected-css, --rejected-css", "option to output rejected css")
5153
.option(
5254
"-s, --safelist <list...>",
5355
"list of classes that should not be removed"
@@ -77,6 +79,7 @@ async function run() {
7779
keyframes,
7880
variables,
7981
rejected,
82+
rejectedCss,
8083
safelist,
8184
blocklist,
8285
skippedContentGlobs,
@@ -99,6 +102,7 @@ async function run() {
99102
if (fontFace) options.fontFace = fontFace;
100103
if (keyframes) options.keyframes = keyframes;
101104
if (rejected) options.rejected = rejected;
105+
if (rejectedCss) options.rejectedCss = rejectedCss;
102106
if (variables) options.variables = variables;
103107
if (safelist) options.safelist = standardizeSafelist(safelist);
104108
if (blocklist) options.blocklist = blocklist;

packages/purgecss/src/index.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ class PurgeCSS {
268268
private usedAnimations: Set<string> = new Set();
269269
private usedFontFaces: Set<string> = new Set();
270270
public selectorsRemoved: Set<string> = new Set();
271+
public removedNodes: postcss.Node[] = [];
271272
private variablesStructure: VariablesStructure = new VariablesStructure();
272273

273274
public options: Options = defaultOptions;
@@ -456,6 +457,7 @@ class PurgeCSS {
456457
}
457458

458459
let keepSelector = true;
460+
const originalSelector = node.selector;
459461
node.selector = selectorParser((selectorsParsed) => {
460462
selectorsParsed.walk((selector) => {
461463
if (selector.type !== "selector") {
@@ -465,8 +467,9 @@ class PurgeCSS {
465467
keepSelector = this.shouldKeepSelector(selector, selectors);
466468

467469
if (!keepSelector) {
468-
if (this.options.rejected)
470+
if (this.options.rejected) {
469471
this.selectorsRemoved.add(selector.toString());
472+
}
470473
selector.remove();
471474
}
472475
});
@@ -482,7 +485,19 @@ class PurgeCSS {
482485

483486
// remove empty rules
484487
const parent = node.parent;
485-
if (!node.selector) node.remove();
488+
if (!node.selector) {
489+
node.remove();
490+
if (this.options.rejectedCss) {
491+
node.selector = originalSelector;
492+
if (parent && isRuleEmpty(parent)) {
493+
const clone = parent.clone();
494+
clone.append(node);
495+
this.removedNodes.push(clone);
496+
} else {
497+
this.removedNodes.push(node);
498+
}
499+
}
500+
}
486501
if (isRuleEmpty(parent)) parent?.remove();
487502
}
488503

@@ -538,6 +553,10 @@ class PurgeCSS {
538553
this.selectorsRemoved.clear();
539554
}
540555

556+
if (this.options.rejectedCss) {
557+
result.rejectedCss = postcss.root({ nodes: this.removedNodes }).toString();
558+
}
559+
541560
sources.push(result);
542561
}
543562
return sources;

packages/purgecss/src/options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const defaultOptions: Options = {
99
fontFace: false,
1010
keyframes: false,
1111
rejected: false,
12+
rejectedCss: false,
1213
stdin: false,
1314
stdout: false,
1415
variables: false,

packages/purgecss/src/types/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface UserDefinedOptions {
6363
keyframes?: boolean;
6464
output?: string;
6565
rejected?: boolean;
66+
rejectedCss?: boolean;
6667
stdin?: boolean;
6768
stdout?: boolean;
6869
variables?: boolean;
@@ -81,6 +82,7 @@ export interface Options {
8182
keyframes: boolean;
8283
output?: string;
8384
rejected: boolean;
85+
rejectedCss: boolean;
8486
stdin: boolean;
8587
stdout: boolean;
8688
variables: boolean;
@@ -92,6 +94,7 @@ export interface Options {
9294

9395
export interface ResultPurge {
9496
css: string;
97+
rejectedCss?: string;
9598
file?: string;
9699
rejected?: string[];
97100
}

0 commit comments

Comments
 (0)