Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of output-unused-css #763

Merged
merged 9 commits into from Nov 26, 2021
27 changes: 17 additions & 10 deletions docs/CLI.md
Expand Up @@ -37,18 +37,25 @@ npm i -g purgecss
To see the available options for the CLI: `purgecss --help`

```text
Usage: purgecss --css <css> --content <content> [options]
Usage: purgecss --css <css...> --content <content...> [options]

Remove unused css selectors

Options:
-con, --content <files> glob of content files
-css, --css <files> glob of css files
-c, --config <path> path to the configuration file
-o, --output <path> 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> list of classes that should not be removed
-h, --help display help for command
-V, --version output the version number
-con, --content <files...> glob of content files
-css, --css <files...> glob of css files
-c, --config <path> path to the configuration file
-o, --output <path> 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...> list of classes that should not be removed
-b, --blocklist <list...> list of selectors that should be removed
-k, --skippedContentGlobs <list...> 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.
Expand Down
1 change: 1 addition & 0 deletions docs/api.md
Expand Up @@ -74,5 +74,6 @@ interface ResultPurge {
css: string;
file?: string;
rejected?: string[];
rejectedCss?: string;
}
```
12 changes: 12 additions & 0 deletions docs/configuration.md
Expand Up @@ -57,6 +57,7 @@ interface UserDefinedOptions {
keyframes?: boolean;
output?: string;
rejected?: boolean;
rejectedCss?: boolean;
stdin?: boolean;
stdout?: boolean;
variables?: boolean;
Expand Down Expand Up @@ -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**

Expand Down
41 changes: 41 additions & 0 deletions 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);
});
});
@@ -0,0 +1,5 @@
@media (max-width: 66666px) {
.unused-class, .unused-class2 {
color: black;
}
}
@@ -0,0 +1,7 @@
.critical {
color: red;
}

.rejected {
color: blue;
}
@@ -0,0 +1,2 @@

"critical"
4 changes: 4 additions & 0 deletions packages/purgecss/src/bin.ts
Expand Up @@ -25,6 +25,7 @@ type CommandOptions = {
keyframes?: boolean;
variables?: boolean;
rejected?: boolean;
rejectedCss?: boolean;
safelist?: string[];
blocklist?: string[];
skippedContentGlobs: string[];
Expand All @@ -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...>",
"list of classes that should not be removed"
Expand Down Expand Up @@ -77,6 +79,7 @@ async function run() {
keyframes,
variables,
rejected,
rejectedCss,
safelist,
blocklist,
skippedContentGlobs,
Expand All @@ -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;
Expand Down
23 changes: 21 additions & 2 deletions packages/purgecss/src/index.ts
Expand Up @@ -268,6 +268,7 @@ class PurgeCSS {
private usedAnimations: Set<string> = new Set();
private usedFontFaces: Set<string> = new Set();
public selectorsRemoved: Set<string> = new Set();
public removedNodes: postcss.Node[] = [];
private variablesStructure: VariablesStructure = new VariablesStructure();

public options: Options = defaultOptions;
Expand Down Expand Up @@ -456,6 +457,7 @@ class PurgeCSS {
}

let keepSelector = true;
const originalSelector = node.selector;
node.selector = selectorParser((selectorsParsed) => {
selectorsParsed.walk((selector) => {
if (selector.type !== "selector") {
Expand All @@ -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();
}
});
Expand All @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/purgecss/src/options.ts
Expand Up @@ -9,6 +9,7 @@ export const defaultOptions: Options = {
fontFace: false,
keyframes: false,
rejected: false,
rejectedCss: false,
stdin: false,
stdout: false,
variables: false,
Expand Down
3 changes: 3 additions & 0 deletions packages/purgecss/src/types/index.ts
Expand Up @@ -63,6 +63,7 @@ export interface UserDefinedOptions {
keyframes?: boolean;
output?: string;
rejected?: boolean;
rejectedCss?: boolean;
stdin?: boolean;
stdout?: boolean;
variables?: boolean;
Expand All @@ -81,6 +82,7 @@ export interface Options {
keyframes: boolean;
output?: string;
rejected: boolean;
rejectedCss: boolean;
stdin: boolean;
stdout: boolean;
variables: boolean;
Expand All @@ -92,6 +94,7 @@ export interface Options {

export interface ResultPurge {
css: string;
rejectedCss?: string;
file?: string;
rejected?: string[];
}