purgecss 6.0.0 → 7.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/purgecss.js +1254 -1
- package/lib/purgecss.esm.js +1024 -1
- package/lib/purgecss.js +1055 -1
- package/package.json +7 -9
package/lib/purgecss.js
CHANGED
|
@@ -1 +1,1055 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("fs"),t=require("glob"),s=require("path"),r=require("postcss"),o=require("postcss-selector-parser"),i=require("util");function a(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}function n(e){if(e&&e.__esModule)return e;var t=Object.create(null);return e&&Object.keys(e).forEach((function(s){if("default"!==s){var r=Object.getOwnPropertyDescriptor(e,s);Object.defineProperty(t,s,r.get?r:{enumerable:!0,get:function(){return e[s]}})}})),t.default=e,Object.freeze(t)}var u=n(e),c=n(t),l=n(s),d=n(r),h=a(o);function f(e,t){t&&t.forEach(e.add,e)}class ExtractorResultSets{constructor(e){this.undetermined=new Set,this.attrNames=new Set,this.attrValues=new Set,this.classes=new Set,this.ids=new Set,this.tags=new Set,this.merge(e)}merge(e){return Array.isArray(e)?f(this.undetermined,e):e instanceof ExtractorResultSets?(f(this.undetermined,e.undetermined),f(this.attrNames,e.attrNames),f(this.attrValues,e.attrValues),f(this.classes,e.classes),f(this.ids,e.ids),f(this.tags,e.tags)):(f(this.undetermined,e.undetermined),e.attributes&&(f(this.attrNames,e.attributes.names),f(this.attrValues,e.attributes.values)),f(this.classes,e.classes),f(this.ids,e.ids),f(this.tags,e.tags)),this}hasAttrName(e){return this.attrNames.has(e)||this.undetermined.has(e)}someAttrValue(e){for(const t of this.attrValues)if(e(t))return!0;for(const t of this.undetermined)if(e(t))return!0;return!1}hasAttrPrefix(e){return this.someAttrValue((t=>t.startsWith(e)))}hasAttrSuffix(e){return this.someAttrValue((t=>t.endsWith(e)))}hasAttrSubstr(e){return e.trim().split(" ").every((e=>this.someAttrValue((t=>t.includes(e)))))}hasAttrValue(e){return this.attrValues.has(e)||this.undetermined.has(e)}hasClass(e){return this.classes.has(e)||this.undetermined.has(e)}hasId(e){return this.ids.has(e)||this.undetermined.has(e)}hasTag(e){return this.tags.has(e)||this.undetermined.has(e)}}const p=["*",":root",":after",":before"],m={css:[],content:[],defaultExtractor:e=>e.match(/[A-Za-z0-9_-]+/g)||[],extractors:[],fontFace:!1,keyframes:!1,rejected:!1,rejectedCss:!1,sourceMap:!1,stdin:!1,stdout:!1,variables:!1,safelist:{standard:[],deep:[],greedy:[],variables:[],keyframes:[]},blocklist:[],skippedContentGlobs:[],dynamicAttributes:[]};class VariableNode{constructor(e){this.nodes=[],this.isUsed=!1,this.value=e}}class VariablesStructure{constructor(){this.nodes=new Map,this.usedVariables=new Set,this.safelist=[]}addVariable(e){const{prop:t}=e;if(this.nodes.has(t)){const s=new VariableNode(e),r=this.nodes.get(t)||[];this.nodes.set(t,[...r,s])}else{const s=new VariableNode(e);this.nodes.set(t,[s])}}addVariableUsage(e,t){const{prop:s}=e,r=this.nodes.get(s);for(const e of t){const t=e[1];if(this.nodes.has(t)){const e=this.nodes.get(t);null==r||r.forEach((t=>{null==e||e.forEach((e=>t.nodes.push(e)))}))}}}addVariableUsageInProperties(e){for(const t of e){const e=t[1];this.usedVariables.add(e)}}setAsUsed(e){const t=this.nodes.get(e);if(t){const e=[...t];for(;0!==e.length;){const t=e.pop();t&&!t.isUsed&&(t.isUsed=!0,e.push(...t.nodes))}}}removeUnused(){for(const e of this.usedVariables){const t=this.nodes.get(e);if(t)for(const e of t){const t=e.value.value.matchAll(/var\((.+?)[,)]/g);for(const e of t)this.usedVariables.has(e[1])||this.usedVariables.add(e[1])}}for(const e of this.usedVariables)this.setAsUsed(e);for(const[e,t]of this.nodes)for(const s of t)s.isUsed||this.isVariablesSafelisted(e)||s.value.remove()}isVariablesSafelisted(e){return this.safelist.some((t=>"string"==typeof t?t===e:t.test(e)))}}const g={access:i.promisify(u.access),readFile:i.promisify(u.readFile)};function v(e=[]){return Array.isArray(e)?{...m.safelist,standard:e}:{...m.safelist,...e}}async function S(e="purgecss.config.js"){let t;try{const s=l.resolve(process.cwd(),e);t=await function(e){return Promise.resolve().then((function(){return n(require(e))}))}(s)}catch(e){if(e instanceof Error)throw new Error(`Error loading the config file ${e.message}`);throw new Error}return{...m,...t,safelist:v(t.safelist)}}async function y(e,t){return new ExtractorResultSets(await t(e))}function b(e,t){switch(t){case"next":return e.text.includes("purgecss ignore");case"start":return e.text.includes("purgecss start ignore");case"end":return e.text.includes("purgecss end ignore")}}function w(...e){const t=new ExtractorResultSets([]);return e.forEach(t.merge,t),t}function x(e){return e.replace(/(^["'])|(["']$)/g,"")}function V(e,t){if(!t.hasAttrName(e.attribute))return!1;if(void 0===e.value)return!0;switch(e.operator){case"$=":return t.hasAttrSuffix(e.value);case"~=":case"*=":return t.hasAttrSubstr(e.value);case"=":return t.hasAttrValue(e.value);case"|=":case"^=":return t.hasAttrPrefix(e.value);default:return!0}}function A(e,t){return t.hasId(e.value)}function k(e,t){return t.hasTag(e.value)}function F(e){return"atrule"===(null==e?void 0:e.type)}function E(e){return"rule"===(null==e?void 0:e.type)}exports.ExtractorResultSets=ExtractorResultSets,exports.PurgeCSS=class PurgeCSS{constructor(){this.ignore=!1,this.atRules={fontFace:[],keyframes:[]},this.usedAnimations=new Set,this.usedFontFaces=new Set,this.selectorsRemoved=new Set,this.removedNodes=[],this.variablesStructure=new VariablesStructure,this.options=m}collectDeclarationsData(e){const{prop:t,value:s}=e;if(this.options.variables){const r=s.matchAll(/var\((.+?)[,)]/g);t.startsWith("--")?(this.variablesStructure.addVariable(e),this.variablesStructure.addVariableUsage(e,r)):this.variablesStructure.addVariableUsageInProperties(r)}if(!this.options.keyframes||"animation"!==t&&"animation-name"!==t)if(this.options.fontFace){if("font-family"===t)for(const e of s.split(",")){const t=x(e.trim());this.usedFontFaces.add(t)}}else;else for(const e of s.split(/[\s,]+/))this.usedAnimations.add(e)}getFileExtractor(e,t){const s=t.find((t=>t.extensions.find((t=>e.endsWith(t)))));return void 0===s?this.options.defaultExtractor:s.extractor}async extractSelectorsFromFiles(e,t){const s=new ExtractorResultSets([]),r=[];for(const t of e)try{await g.access(t,u.constants.F_OK),r.push(t)}catch(e){r.push(...c.sync(t,{nodir:!0,ignore:this.options.skippedContentGlobs.map((e=>e.replace(/^\.\//,"")))}))}e.length>0&&0===r.length&&console.warn("No files found from the passed PurgeCSS option 'content'.");for(const e of r){const r=await g.readFile(e,"utf-8"),o=this.getFileExtractor(e,t),i=await y(r,o);s.merge(i)}return s}async extractSelectorsFromString(e,t){const s=new ExtractorResultSets([]);for(const{raw:r,extension:o}of e){const e=this.getFileExtractor(`.${o}`,t),i=await y(r,e);s.merge(i)}return s}evaluateAtRule(e){if(this.options.keyframes&&e.name.endsWith("keyframes"))this.atRules.keyframes.push(e);else if(this.options.fontFace&&"font-face"===e.name&&e.nodes)for(const t of e.nodes)"decl"===t.type&&"font-family"===t.prop&&this.atRules.fontFace.push({name:x(t.value),node:e})}evaluateRule(e,t){if(this.ignore)return;const s=e.prev();if(function(e){return"comment"===(null==e?void 0:e.type)}(s)&&b(s,"next"))return void s.remove();if(e.parent&&F(e.parent)&&e.parent.name.endsWith("keyframes"))return;if(!E(e))return;if(function(e){let t=!1;return e.walkComments((e=>{e&&"comment"===e.type&&e.text.includes("purgecss ignore current")&&(t=!0,e.remove())})),t}(e))return;const r=[];if(e.selector=h.default((e=>{e.walk((e=>{if("selector"!==e.type)return;this.shouldKeepSelector(e,t)||(this.options.rejected&&this.selectorsRemoved.add(e.toString()),this.options.rejectedCss&&r.push(e.toString()),e.remove())})),e.walk((e=>{"selector"===e.type&&e.toString()&&/(:where)|(:is)/.test(e.toString())&&e.walk((t=>{"pseudo"===t.type&&(":where"!==t.value&&":is"!==t.value||0===t.nodes.length&&e.remove())}))}))})).processSync(e.selector),e.selector&&void 0!==e.nodes)for(const t of e.nodes)"decl"===t.type&&this.collectDeclarationsData(t);const o=e.parent;if(e.selector||e.remove(),function(e){return!!(E(e)&&!e.selector||(null==e?void 0:e.nodes)&&!e.nodes.length||F(e)&&(!e.nodes&&!e.params||!e.params&&e.nodes&&!e.nodes.length))}(o)&&(null==o||o.remove()),this.options.rejectedCss&&r.length>0){const t=e.clone(),s=null==o?void 0:o.clone().removeAll().append(t);t.selectors=r;const i=s||t;this.removedNodes.push(i)}}async getPurgedCSS(e,t){var s;const r=[],o=[];for(const t of e)"string"==typeof t?o.push(...c.sync(t,{nodir:!0,ignore:this.options.skippedContentGlobs})):o.push(t);for(const e of o){const o="string"==typeof e?this.options.stdin?e:await g.readFile(e,"utf-8"):e.raw,i="string"==typeof e&&!this.options.stdin,a=d.parse(o,{from:i?e:void 0});this.walkThroughCSS(a,t),this.options.fontFace&&this.removeUnusedFontFaces(),this.options.keyframes&&this.removeUnusedKeyframes(),this.options.variables&&this.removeUnusedCSSVariables();const n=a.toResult({map:this.options.sourceMap,to:"object"==typeof this.options.sourceMap?this.options.sourceMap.to:void 0}),u={css:n.toString(),file:"string"==typeof e?e:e.name};this.options.sourceMap&&(u.sourceMap=null===(s=n.map)||void 0===s?void 0:s.toString()),this.options.rejected&&(u.rejected=Array.from(this.selectorsRemoved),this.selectorsRemoved.clear()),this.options.rejectedCss&&(u.rejectedCss=d.root({nodes:this.removedNodes}).toString()),r.push(u)}return r}isKeyframesSafelisted(e){return this.options.safelist.keyframes.some((t=>"string"==typeof t?t===e:t.test(e)))}isSelectorBlocklisted(e){return this.options.blocklist.some((t=>"string"==typeof t?t===e:t.test(e)))}isSelectorSafelisted(e){const t=this.options.safelist.standard.some((t=>"string"==typeof t?t===e:t.test(e))),s=/^::.*/.test(e);return p.includes(e)||s||t}isSelectorSafelistedDeep(e){return this.options.safelist.deep.some((t=>t.test(e)))}isSelectorSafelistedGreedy(e){return this.options.safelist.greedy.some((t=>t.test(e)))}async purge(e){this.options="object"!=typeof e?await S(e):{...m,...e,safelist:v(e.safelist)};const{content:t,css:s,extractors:r,safelist:o}=this.options;this.options.variables&&(this.variablesStructure.safelist=o.variables||[]);const i=t.filter((e=>"string"==typeof e)),a=t.filter((e=>"object"==typeof e)),n=await this.extractSelectorsFromFiles(i,r),u=await this.extractSelectorsFromString(a,r);return this.getPurgedCSS(s,w(n,u))}removeUnusedCSSVariables(){this.variablesStructure.removeUnused()}removeUnusedFontFaces(){for(const{name:e,node:t}of this.atRules.fontFace)this.usedFontFaces.has(e)||t.remove()}removeUnusedKeyframes(){for(const e of this.atRules.keyframes)this.usedAnimations.has(e.params)||this.isKeyframesSafelisted(e.params)||e.remove()}getSelectorValue(e){return"attribute"===e.type&&e.attribute||e.value}shouldKeepSelector(e,t){if(function(e){return e.parent&&"pseudo"===e.parent.type&&e.parent.value.startsWith(":")||!1}(e)&&!function(e){return e.parent&&"pseudo"===e.parent.type&&(":where"===e.parent.value||":is"===e.parent.value)||!1}(e))return!0;if(this.options.safelist.greedy.length>0){if(e.nodes.map(this.getSelectorValue).some((e=>e&&this.isSelectorSafelistedGreedy(e))))return!0}let s=!1;for(const o of e.nodes){const e=this.getSelectorValue(o);if(e&&this.isSelectorSafelistedDeep(e))return!0;if(e&&(p.includes(e)||this.isSelectorSafelisted(e)))s=!0;else{if(e&&this.isSelectorBlocklisted(e))return!1;switch(o.type){case"attribute":s=!![...this.options.dynamicAttributes,"value","checked","selected","open"].includes(o.attribute)||V(o,t);break;case"class":r=o,s=t.hasClass(r.value);break;case"id":s=A(o,t);break;case"tag":s=k(o,t);break;default:continue}if(!s)return!1}}var r;return s}walkThroughCSS(e,t){e.walk((e=>"rule"===e.type?this.evaluateRule(e,t):"atrule"===e.type?this.evaluateAtRule(e):void("comment"===e.type&&(b(e,"start")?(this.ignore=!0,e.remove()):b(e,"end")&&(this.ignore=!1,e.remove())))))}},exports.VariableNode=VariableNode,exports.VariablesStructure=VariablesStructure,exports.defaultOptions=m,exports.mergeExtractorSelectors=w,exports.setOptions=S,exports.standardizeSafelist=v;
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var glob = require('glob');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var postcss = require('postcss');
|
|
7
|
+
var selectorParser = require('postcss-selector-parser');
|
|
8
|
+
var util = require('util');
|
|
9
|
+
|
|
10
|
+
function _interopNamespaceDefault(e) {
|
|
11
|
+
var n = Object.create(null);
|
|
12
|
+
if (e) {
|
|
13
|
+
Object.keys(e).forEach(function (k) {
|
|
14
|
+
if (k !== 'default') {
|
|
15
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
16
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () { return e[k]; }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
n.default = e;
|
|
24
|
+
return Object.freeze(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
28
|
+
var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
|
|
29
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
30
|
+
var postcss__namespace = /*#__PURE__*/_interopNamespaceDefault(postcss);
|
|
31
|
+
|
|
32
|
+
const IGNORE_ANNOTATION_CURRENT = "purgecss ignore current";
|
|
33
|
+
const IGNORE_ANNOTATION_NEXT = "purgecss ignore";
|
|
34
|
+
const IGNORE_ANNOTATION_START = "purgecss start ignore";
|
|
35
|
+
const IGNORE_ANNOTATION_END = "purgecss end ignore";
|
|
36
|
+
const CONFIG_FILENAME = "purgecss.config.js";
|
|
37
|
+
// Error Message
|
|
38
|
+
const ERROR_CONFIG_FILE_LOADING = "Error loading the config file";
|
|
39
|
+
|
|
40
|
+
function mergeSets(into, from) {
|
|
41
|
+
if (from) {
|
|
42
|
+
from.forEach(into.add, into);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
class ExtractorResultSets {
|
|
49
|
+
constructor(er) {
|
|
50
|
+
this.undetermined = new Set();
|
|
51
|
+
this.attrNames = new Set();
|
|
52
|
+
this.attrValues = new Set();
|
|
53
|
+
this.classes = new Set();
|
|
54
|
+
this.ids = new Set();
|
|
55
|
+
this.tags = new Set();
|
|
56
|
+
this.merge(er);
|
|
57
|
+
}
|
|
58
|
+
merge(that) {
|
|
59
|
+
if (Array.isArray(that)) {
|
|
60
|
+
mergeSets(this.undetermined, that);
|
|
61
|
+
}
|
|
62
|
+
else if (that instanceof ExtractorResultSets) {
|
|
63
|
+
mergeSets(this.undetermined, that.undetermined);
|
|
64
|
+
mergeSets(this.attrNames, that.attrNames);
|
|
65
|
+
mergeSets(this.attrValues, that.attrValues);
|
|
66
|
+
mergeSets(this.classes, that.classes);
|
|
67
|
+
mergeSets(this.ids, that.ids);
|
|
68
|
+
mergeSets(this.tags, that.tags);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// ExtractorResultDetailed:
|
|
72
|
+
mergeSets(this.undetermined, that.undetermined);
|
|
73
|
+
if (that.attributes) {
|
|
74
|
+
mergeSets(this.attrNames, that.attributes.names);
|
|
75
|
+
mergeSets(this.attrValues, that.attributes.values);
|
|
76
|
+
}
|
|
77
|
+
mergeSets(this.classes, that.classes);
|
|
78
|
+
mergeSets(this.ids, that.ids);
|
|
79
|
+
mergeSets(this.tags, that.tags);
|
|
80
|
+
}
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
hasAttrName(name) {
|
|
84
|
+
return this.attrNames.has(name) || this.undetermined.has(name);
|
|
85
|
+
}
|
|
86
|
+
someAttrValue(predicate) {
|
|
87
|
+
for (const val of this.attrValues) {
|
|
88
|
+
if (predicate(val))
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
for (const val of this.undetermined) {
|
|
92
|
+
if (predicate(val))
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
hasAttrPrefix(prefix) {
|
|
98
|
+
return this.someAttrValue((value) => value.startsWith(prefix));
|
|
99
|
+
}
|
|
100
|
+
hasAttrSuffix(suffix) {
|
|
101
|
+
return this.someAttrValue((value) => value.endsWith(suffix));
|
|
102
|
+
}
|
|
103
|
+
hasAttrSubstr(substr) {
|
|
104
|
+
const wordSubstr = substr.trim().split(" ");
|
|
105
|
+
return wordSubstr.every((word) => this.someAttrValue((value) => value.includes(word)));
|
|
106
|
+
}
|
|
107
|
+
hasAttrValue(value) {
|
|
108
|
+
return this.attrValues.has(value) || this.undetermined.has(value);
|
|
109
|
+
}
|
|
110
|
+
hasClass(name) {
|
|
111
|
+
return this.classes.has(name) || this.undetermined.has(name);
|
|
112
|
+
}
|
|
113
|
+
hasId(id) {
|
|
114
|
+
return this.ids.has(id) || this.undetermined.has(id);
|
|
115
|
+
}
|
|
116
|
+
hasTag(tag) {
|
|
117
|
+
return this.tags.has(tag) || this.undetermined.has(tag);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const CSS_SAFELIST = ["*", ":root", ":after", ":before"];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @public
|
|
125
|
+
*/
|
|
126
|
+
const defaultOptions = {
|
|
127
|
+
css: [],
|
|
128
|
+
content: [],
|
|
129
|
+
defaultExtractor: (content) => content.match(/[A-Za-z0-9_-]+/g) || [],
|
|
130
|
+
extractors: [],
|
|
131
|
+
fontFace: false,
|
|
132
|
+
keyframes: false,
|
|
133
|
+
rejected: false,
|
|
134
|
+
rejectedCss: false,
|
|
135
|
+
sourceMap: false,
|
|
136
|
+
stdin: false,
|
|
137
|
+
stdout: false,
|
|
138
|
+
variables: false,
|
|
139
|
+
safelist: {
|
|
140
|
+
standard: [],
|
|
141
|
+
deep: [],
|
|
142
|
+
greedy: [],
|
|
143
|
+
variables: [],
|
|
144
|
+
keyframes: [],
|
|
145
|
+
},
|
|
146
|
+
blocklist: [],
|
|
147
|
+
skippedContentGlobs: [],
|
|
148
|
+
dynamicAttributes: [],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @public
|
|
153
|
+
*/
|
|
154
|
+
class VariableNode {
|
|
155
|
+
constructor(declaration) {
|
|
156
|
+
this.nodes = [];
|
|
157
|
+
this.isUsed = false;
|
|
158
|
+
this.value = declaration;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* @public
|
|
163
|
+
*/
|
|
164
|
+
class VariablesStructure {
|
|
165
|
+
constructor() {
|
|
166
|
+
this.nodes = new Map();
|
|
167
|
+
this.usedVariables = new Set();
|
|
168
|
+
this.safelist = [];
|
|
169
|
+
}
|
|
170
|
+
addVariable(declaration) {
|
|
171
|
+
const { prop } = declaration;
|
|
172
|
+
if (!this.nodes.has(prop)) {
|
|
173
|
+
const node = new VariableNode(declaration);
|
|
174
|
+
this.nodes.set(prop, [node]);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const node = new VariableNode(declaration);
|
|
178
|
+
const variableNodes = this.nodes.get(prop) || [];
|
|
179
|
+
this.nodes.set(prop, [...variableNodes, node]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
addVariableUsage(declaration, matchedVariables) {
|
|
183
|
+
const { prop } = declaration;
|
|
184
|
+
const nodes = this.nodes.get(prop);
|
|
185
|
+
for (const variableMatch of matchedVariables) {
|
|
186
|
+
// capturing group containing the variable is in index 1
|
|
187
|
+
const variableName = variableMatch[1];
|
|
188
|
+
if (this.nodes.has(variableName)) {
|
|
189
|
+
const usedVariableNodes = this.nodes.get(variableName);
|
|
190
|
+
nodes === null || nodes === void 0 ? void 0 : nodes.forEach((node) => {
|
|
191
|
+
usedVariableNodes === null || usedVariableNodes === void 0 ? void 0 : usedVariableNodes.forEach((usedVariableNode) => node.nodes.push(usedVariableNode));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
addVariableUsageInProperties(matchedVariables) {
|
|
197
|
+
for (const variableMatch of matchedVariables) {
|
|
198
|
+
// capturing group containing the variable is in index 1
|
|
199
|
+
const variableName = variableMatch[1];
|
|
200
|
+
this.usedVariables.add(variableName);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
setAsUsed(variableName) {
|
|
204
|
+
const nodes = this.nodes.get(variableName);
|
|
205
|
+
if (nodes) {
|
|
206
|
+
const queue = [...nodes];
|
|
207
|
+
while (queue.length !== 0) {
|
|
208
|
+
const currentNode = queue.pop();
|
|
209
|
+
if (currentNode && !currentNode.isUsed) {
|
|
210
|
+
currentNode.isUsed = true;
|
|
211
|
+
queue.push(...currentNode.nodes);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
removeUnused() {
|
|
217
|
+
// check unordered usage
|
|
218
|
+
for (const used of this.usedVariables) {
|
|
219
|
+
const usedNodes = this.nodes.get(used);
|
|
220
|
+
if (usedNodes) {
|
|
221
|
+
for (const usedNode of usedNodes) {
|
|
222
|
+
const usedVariablesMatchesInDeclaration = usedNode.value.value.matchAll(/var\((.+?)[,)]/g);
|
|
223
|
+
for (const usage of usedVariablesMatchesInDeclaration) {
|
|
224
|
+
if (!this.usedVariables.has(usage[1])) {
|
|
225
|
+
this.usedVariables.add(usage[1]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const used of this.usedVariables) {
|
|
232
|
+
this.setAsUsed(used);
|
|
233
|
+
}
|
|
234
|
+
for (const [name, declarations] of this.nodes) {
|
|
235
|
+
for (const declaration of declarations) {
|
|
236
|
+
if (!declaration.isUsed && !this.isVariablesSafelisted(name)) {
|
|
237
|
+
declaration.value.remove();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
isVariablesSafelisted(variable) {
|
|
243
|
+
return this.safelist.some((safelistItem) => {
|
|
244
|
+
return typeof safelistItem === "string"
|
|
245
|
+
? safelistItem === variable
|
|
246
|
+
: safelistItem.test(variable);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Core package of PurgeCSS
|
|
253
|
+
*
|
|
254
|
+
* Contains the core methods to analyze the files, remove unused CSS.
|
|
255
|
+
*
|
|
256
|
+
* @packageDocumentation
|
|
257
|
+
*/
|
|
258
|
+
const asyncFs = {
|
|
259
|
+
access: util.promisify(fs__namespace.access),
|
|
260
|
+
readFile: util.promisify(fs__namespace.readFile),
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Format the user defined safelist into a standardized safelist object
|
|
264
|
+
*
|
|
265
|
+
* @param userDefinedSafelist - the user defined safelist
|
|
266
|
+
* @returns the formatted safelist object that can be used in the PurgeCSS options
|
|
267
|
+
*
|
|
268
|
+
* @public
|
|
269
|
+
*/
|
|
270
|
+
function standardizeSafelist(userDefinedSafelist = []) {
|
|
271
|
+
if (Array.isArray(userDefinedSafelist)) {
|
|
272
|
+
return {
|
|
273
|
+
...defaultOptions.safelist,
|
|
274
|
+
standard: userDefinedSafelist,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
...defaultOptions.safelist,
|
|
279
|
+
...userDefinedSafelist,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Load the configuration file from the path
|
|
284
|
+
*
|
|
285
|
+
* @param configFile - Path of the config file
|
|
286
|
+
* @returns The options from the configuration file
|
|
287
|
+
*
|
|
288
|
+
* @throws Error
|
|
289
|
+
* This exception is thrown if the configuration file was not imported
|
|
290
|
+
*
|
|
291
|
+
* @public
|
|
292
|
+
*/
|
|
293
|
+
async function setOptions(configFile = CONFIG_FILENAME) {
|
|
294
|
+
let options;
|
|
295
|
+
try {
|
|
296
|
+
const t = path__namespace.resolve(process.cwd(), configFile);
|
|
297
|
+
options = await import(t);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
if (err instanceof Error) {
|
|
301
|
+
throw new Error(`${ERROR_CONFIG_FILE_LOADING} ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
throw new Error();
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
...defaultOptions,
|
|
307
|
+
...options,
|
|
308
|
+
safelist: standardizeSafelist(options.safelist),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Use the extract function to get the list of selectors
|
|
313
|
+
*
|
|
314
|
+
* @param content - content (e.g. html file)
|
|
315
|
+
* @param extractor - PurgeCSS extractor used to extract the selectors
|
|
316
|
+
* @returns the sets containing the result of the extractor function
|
|
317
|
+
*/
|
|
318
|
+
async function extractSelectors(content, extractor) {
|
|
319
|
+
return new ExtractorResultSets(await extractor(content));
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check if the node is a css comment indication to ignore the selector rule
|
|
323
|
+
*
|
|
324
|
+
* @param node - node of postcss AST
|
|
325
|
+
* @param type - type of css comment
|
|
326
|
+
* @returns true if the node is a PurgeCSS ignore comment
|
|
327
|
+
*/
|
|
328
|
+
function isIgnoreAnnotation(node, type) {
|
|
329
|
+
switch (type) {
|
|
330
|
+
case "next":
|
|
331
|
+
return node.text.includes(IGNORE_ANNOTATION_NEXT);
|
|
332
|
+
case "start":
|
|
333
|
+
return node.text.includes(IGNORE_ANNOTATION_START);
|
|
334
|
+
case "end":
|
|
335
|
+
return node.text.includes(IGNORE_ANNOTATION_END);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Check if the node correspond to an empty css rule
|
|
340
|
+
*
|
|
341
|
+
* @param node - node of postcss AST
|
|
342
|
+
* @returns true if the rule is empty
|
|
343
|
+
*/
|
|
344
|
+
function isRuleEmpty(node) {
|
|
345
|
+
if ((isPostCSSRule(node) && !node.selector) ||
|
|
346
|
+
((node === null || node === void 0 ? void 0 : node.nodes) && !node.nodes.length) ||
|
|
347
|
+
(isPostCSSAtRule(node) &&
|
|
348
|
+
((!node.nodes && !node.params) ||
|
|
349
|
+
(!node.params && node.nodes && !node.nodes.length)))) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Check if the node has a css comment indicating to ignore the current selector rule
|
|
356
|
+
*
|
|
357
|
+
* @param rule - rule of postcss AST
|
|
358
|
+
*/
|
|
359
|
+
function hasIgnoreAnnotation(rule) {
|
|
360
|
+
let found = false;
|
|
361
|
+
rule.walkComments((node) => {
|
|
362
|
+
if (node &&
|
|
363
|
+
node.type === "comment" &&
|
|
364
|
+
node.text.includes(IGNORE_ANNOTATION_CURRENT)) {
|
|
365
|
+
found = true;
|
|
366
|
+
node.remove();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
return found;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Merge two extractor selectors
|
|
373
|
+
*
|
|
374
|
+
* @param extractorSelectorsA - extractor selectors A
|
|
375
|
+
* @param extractorSelectorsB - extractor selectors B
|
|
376
|
+
* @returns the merged extractor result sets
|
|
377
|
+
*
|
|
378
|
+
* @public
|
|
379
|
+
*/
|
|
380
|
+
function mergeExtractorSelectors(...extractors) {
|
|
381
|
+
const result = new ExtractorResultSets([]);
|
|
382
|
+
extractors.forEach(result.merge, result);
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Strips quotes of a string
|
|
387
|
+
*
|
|
388
|
+
* @param str - string to be stripped
|
|
389
|
+
*/
|
|
390
|
+
function stripQuotes(str) {
|
|
391
|
+
return str.replace(/(^["'])|(["']$)/g, "");
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Returns true if the attribute is found in the extractor selectors
|
|
395
|
+
*
|
|
396
|
+
* @param attributeNode - node of type `attribute`
|
|
397
|
+
* @param selectors - extractor selectors
|
|
398
|
+
*/
|
|
399
|
+
function isAttributeFound(attributeNode, selectors) {
|
|
400
|
+
if (!selectors.hasAttrName(attributeNode.attribute)) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
if (typeof attributeNode.value === "undefined") {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
switch (attributeNode.operator) {
|
|
407
|
+
case "$=":
|
|
408
|
+
return selectors.hasAttrSuffix(attributeNode.value);
|
|
409
|
+
case "~=":
|
|
410
|
+
case "*=":
|
|
411
|
+
return selectors.hasAttrSubstr(attributeNode.value);
|
|
412
|
+
case "=":
|
|
413
|
+
return selectors.hasAttrValue(attributeNode.value);
|
|
414
|
+
case "|=":
|
|
415
|
+
case "^=":
|
|
416
|
+
return selectors.hasAttrPrefix(attributeNode.value);
|
|
417
|
+
default:
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Returns true if the class is found in the extractor selectors
|
|
423
|
+
*
|
|
424
|
+
* @param classNode - node of type `class`
|
|
425
|
+
* @param selectors - extractor selectors
|
|
426
|
+
*/
|
|
427
|
+
function isClassFound(classNode, selectors) {
|
|
428
|
+
return selectors.hasClass(classNode.value);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Returns true if the identifier is found in the extractor selectors
|
|
432
|
+
*
|
|
433
|
+
* @param identifierNode - node of type `identifier`
|
|
434
|
+
* @param selectors - extractor selectors
|
|
435
|
+
*/
|
|
436
|
+
function isIdentifierFound(identifierNode, selectors) {
|
|
437
|
+
return selectors.hasId(identifierNode.value);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Returns true if the tag is found in the extractor selectors
|
|
441
|
+
*
|
|
442
|
+
* @param tagNode - node of type `tag`
|
|
443
|
+
* @param selectors - extractor selectors
|
|
444
|
+
*/
|
|
445
|
+
function isTagFound(tagNode, selectors) {
|
|
446
|
+
return selectors.hasTag(tagNode.value);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Returns true if the selector is inside a pseudo class
|
|
450
|
+
* (e.g. :nth-child, :nth-of-type, :only-child, :not)
|
|
451
|
+
*
|
|
452
|
+
* @param selector - selector
|
|
453
|
+
*/
|
|
454
|
+
function isInPseudoClass(selector) {
|
|
455
|
+
return ((selector.parent &&
|
|
456
|
+
selector.parent.type === "pseudo" &&
|
|
457
|
+
selector.parent.value.startsWith(":")) ||
|
|
458
|
+
false);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Returns true if the selector is inside the pseudo classes :where() or :is()
|
|
462
|
+
* @param selector - selector
|
|
463
|
+
*/
|
|
464
|
+
function isInPseudoClassWhereOrIs(selector) {
|
|
465
|
+
return ((selector.parent &&
|
|
466
|
+
selector.parent.type === "pseudo" &&
|
|
467
|
+
(selector.parent.value === ":where" ||
|
|
468
|
+
selector.parent.value === ":is")) ||
|
|
469
|
+
false);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Returns true if the selector is a pseudo class at the root level
|
|
473
|
+
* Pseudo classes checked: :where, :is, :has, :not
|
|
474
|
+
* @param selector - selector
|
|
475
|
+
*/
|
|
476
|
+
function isPseudoClassAtRootLevel(selector) {
|
|
477
|
+
var _a;
|
|
478
|
+
let result = false;
|
|
479
|
+
if (selector.type === "selector" &&
|
|
480
|
+
((_a = selector.parent) === null || _a === void 0 ? void 0 : _a.type) === "root" &&
|
|
481
|
+
selector.nodes.length === 1) {
|
|
482
|
+
selector.walk((node) => {
|
|
483
|
+
if (node.type === "pseudo" &&
|
|
484
|
+
(node.value === ":where" ||
|
|
485
|
+
node.value === ":is" ||
|
|
486
|
+
node.value === ":has" ||
|
|
487
|
+
node.value === ":not")) {
|
|
488
|
+
result = true;
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
function isPostCSSAtRule(node) {
|
|
495
|
+
return (node === null || node === void 0 ? void 0 : node.type) === "atrule";
|
|
496
|
+
}
|
|
497
|
+
function isPostCSSRule(node) {
|
|
498
|
+
return (node === null || node === void 0 ? void 0 : node.type) === "rule";
|
|
499
|
+
}
|
|
500
|
+
function isPostCSSComment(node) {
|
|
501
|
+
return (node === null || node === void 0 ? void 0 : node.type) === "comment";
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Class used to instantiate PurgeCSS and can then be used
|
|
505
|
+
* to purge CSS files.
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```ts
|
|
509
|
+
* await new PurgeCSS().purge({
|
|
510
|
+
* content: ['index.html'],
|
|
511
|
+
* css: ['css/app.css']
|
|
512
|
+
* })
|
|
513
|
+
* ```
|
|
514
|
+
*
|
|
515
|
+
* @public
|
|
516
|
+
*/
|
|
517
|
+
class PurgeCSS {
|
|
518
|
+
constructor() {
|
|
519
|
+
this.ignore = false;
|
|
520
|
+
this.atRules = {
|
|
521
|
+
fontFace: [],
|
|
522
|
+
keyframes: [],
|
|
523
|
+
};
|
|
524
|
+
this.usedAnimations = new Set();
|
|
525
|
+
this.usedFontFaces = new Set();
|
|
526
|
+
this.selectorsRemoved = new Set();
|
|
527
|
+
this.removedNodes = [];
|
|
528
|
+
this.variablesStructure = new VariablesStructure();
|
|
529
|
+
this.options = defaultOptions;
|
|
530
|
+
}
|
|
531
|
+
collectDeclarationsData(declaration) {
|
|
532
|
+
const { prop, value } = declaration;
|
|
533
|
+
// collect css properties data
|
|
534
|
+
if (this.options.variables) {
|
|
535
|
+
const usedVariablesMatchesInDeclaration = value.matchAll(/var\((.+?)[,)]/g);
|
|
536
|
+
if (prop.startsWith("--")) {
|
|
537
|
+
this.variablesStructure.addVariable(declaration);
|
|
538
|
+
this.variablesStructure.addVariableUsage(declaration, usedVariablesMatchesInDeclaration);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
this.variablesStructure.addVariableUsageInProperties(usedVariablesMatchesInDeclaration);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// collect keyframes data
|
|
545
|
+
if (this.options.keyframes) {
|
|
546
|
+
if (prop === "animation" || prop === "animation-name") {
|
|
547
|
+
for (const word of value.split(/[\s,]+/)) {
|
|
548
|
+
this.usedAnimations.add(word);
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// collect font faces data
|
|
554
|
+
if (this.options.fontFace) {
|
|
555
|
+
if (prop === "font-family") {
|
|
556
|
+
for (const fontName of value.split(",")) {
|
|
557
|
+
const cleanedFontFace = stripQuotes(fontName.trim());
|
|
558
|
+
this.usedFontFaces.add(cleanedFontFace);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Get the extractor corresponding to the extension file
|
|
566
|
+
* @param filename - Name of the file
|
|
567
|
+
* @param extractors - Array of extractors definition
|
|
568
|
+
*/
|
|
569
|
+
getFileExtractor(filename, extractors) {
|
|
570
|
+
const extractorObj = extractors.find((extractor) => extractor.extensions.find((ext) => filename.endsWith(ext)));
|
|
571
|
+
return typeof extractorObj === "undefined"
|
|
572
|
+
? this.options.defaultExtractor
|
|
573
|
+
: extractorObj.extractor;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Extract the selectors present in the files using a PurgeCSS extractor
|
|
577
|
+
*
|
|
578
|
+
* @param files - Array of files path or glob pattern
|
|
579
|
+
* @param extractors - Array of extractors
|
|
580
|
+
*/
|
|
581
|
+
async extractSelectorsFromFiles(files, extractors) {
|
|
582
|
+
const selectors = new ExtractorResultSets([]);
|
|
583
|
+
const filesNames = [];
|
|
584
|
+
for (const globFile of files) {
|
|
585
|
+
try {
|
|
586
|
+
await asyncFs.access(globFile, fs__namespace.constants.F_OK);
|
|
587
|
+
filesNames.push(globFile);
|
|
588
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
filesNames.push(...glob__namespace.sync(globFile, {
|
|
592
|
+
nodir: true,
|
|
593
|
+
ignore: this.options.skippedContentGlobs.map((glob) => glob.replace(/^\.\//, "")),
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (files.length > 0 && filesNames.length === 0) {
|
|
598
|
+
console.warn("No files found from the passed PurgeCSS option 'content'.");
|
|
599
|
+
}
|
|
600
|
+
for (const file of filesNames) {
|
|
601
|
+
const content = await asyncFs.readFile(file, "utf-8");
|
|
602
|
+
const extractor = this.getFileExtractor(file, extractors);
|
|
603
|
+
const extractedSelectors = await extractSelectors(content, extractor);
|
|
604
|
+
selectors.merge(extractedSelectors);
|
|
605
|
+
}
|
|
606
|
+
return selectors;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Extract the selectors present in the passed string using a PurgeCSS extractor
|
|
610
|
+
*
|
|
611
|
+
* @param content - Array of content
|
|
612
|
+
* @param extractors - Array of extractors
|
|
613
|
+
*/
|
|
614
|
+
async extractSelectorsFromString(content, extractors) {
|
|
615
|
+
const selectors = new ExtractorResultSets([]);
|
|
616
|
+
for (const { raw, extension } of content) {
|
|
617
|
+
const extractor = this.getFileExtractor(`.${extension}`, extractors);
|
|
618
|
+
const extractedSelectors = await extractSelectors(raw, extractor);
|
|
619
|
+
selectors.merge(extractedSelectors);
|
|
620
|
+
}
|
|
621
|
+
return selectors;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Evaluate at-rule and register it for future reference
|
|
625
|
+
* @param node - node of postcss AST
|
|
626
|
+
*/
|
|
627
|
+
evaluateAtRule(node) {
|
|
628
|
+
// keyframes
|
|
629
|
+
if (this.options.keyframes && node.name.endsWith("keyframes")) {
|
|
630
|
+
this.atRules.keyframes.push(node);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// font-face
|
|
634
|
+
if (this.options.fontFace && node.name === "font-face" && node.nodes) {
|
|
635
|
+
for (const childNode of node.nodes) {
|
|
636
|
+
if (childNode.type === "decl" && childNode.prop === "font-family") {
|
|
637
|
+
this.atRules.fontFace.push({
|
|
638
|
+
name: stripQuotes(childNode.value),
|
|
639
|
+
node,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Evaluate css selector and decide if it should be removed or not
|
|
647
|
+
*
|
|
648
|
+
* @param node - node of postcss AST
|
|
649
|
+
* @param selectors - selectors used in content files
|
|
650
|
+
*/
|
|
651
|
+
evaluateRule(node, selectors) {
|
|
652
|
+
// exit if is in ignoring state activated by an ignore comment
|
|
653
|
+
if (this.ignore) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
// exit if the previous annotation is a ignore next line comment
|
|
657
|
+
const annotation = node.prev();
|
|
658
|
+
if (isPostCSSComment(annotation) &&
|
|
659
|
+
isIgnoreAnnotation(annotation, "next")) {
|
|
660
|
+
annotation.remove();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
// exit if it is inside a keyframes
|
|
664
|
+
if (node.parent &&
|
|
665
|
+
isPostCSSAtRule(node.parent) &&
|
|
666
|
+
node.parent.name.endsWith("keyframes")) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
// exit if it is not a rule
|
|
670
|
+
if (!isPostCSSRule(node)) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// exit if it has an ignore rule comment inside
|
|
674
|
+
if (hasIgnoreAnnotation(node)) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const selectorsRemovedFromRule = [];
|
|
678
|
+
// selector transformer, walk over the list of the parsed selectors twice.
|
|
679
|
+
// First pass will remove the unused selectors. It goes through
|
|
680
|
+
// pseudo-classes like :where() and :is() and remove the unused
|
|
681
|
+
// selectors inside of them, but will not remove the pseudo-classes
|
|
682
|
+
// themselves. Second pass will remove selectors containing empty
|
|
683
|
+
// :where and :is.
|
|
684
|
+
node.selector = selectorParser((selectorsParsed) => {
|
|
685
|
+
selectorsParsed.walk((selector) => {
|
|
686
|
+
if (selector.type !== "selector") {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const keepSelector = this.shouldKeepSelector(selector, selectors);
|
|
690
|
+
if (!keepSelector) {
|
|
691
|
+
if (this.options.rejected) {
|
|
692
|
+
this.selectorsRemoved.add(selector.toString());
|
|
693
|
+
}
|
|
694
|
+
if (this.options.rejectedCss) {
|
|
695
|
+
selectorsRemovedFromRule.push(selector.toString());
|
|
696
|
+
}
|
|
697
|
+
selector.remove();
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
// removes selectors containing empty :where and :is
|
|
701
|
+
selectorsParsed.walk((selector) => {
|
|
702
|
+
if (selector.type !== "selector") {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (selector.toString() && /(:where)|(:is)/.test(selector.toString())) {
|
|
706
|
+
selector.walk((node) => {
|
|
707
|
+
if (node.type !== "pseudo")
|
|
708
|
+
return;
|
|
709
|
+
if (node.value !== ":where" && node.value !== ":is")
|
|
710
|
+
return;
|
|
711
|
+
if (node.nodes.length === 0) {
|
|
712
|
+
selector.remove();
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}).processSync(node.selector);
|
|
718
|
+
// declarations
|
|
719
|
+
if (node.selector && typeof node.nodes !== "undefined") {
|
|
720
|
+
for (const childNode of node.nodes) {
|
|
721
|
+
if (childNode.type !== "decl")
|
|
722
|
+
continue;
|
|
723
|
+
this.collectDeclarationsData(childNode);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// remove empty rules
|
|
727
|
+
const parent = node.parent;
|
|
728
|
+
if (!node.selector) {
|
|
729
|
+
node.remove();
|
|
730
|
+
}
|
|
731
|
+
if (isRuleEmpty(parent))
|
|
732
|
+
parent === null || parent === void 0 ? void 0 : parent.remove();
|
|
733
|
+
// rebuild the rule with the removed selectors and optionally its parent
|
|
734
|
+
if (this.options.rejectedCss) {
|
|
735
|
+
if (selectorsRemovedFromRule.length > 0) {
|
|
736
|
+
const clone = node.clone();
|
|
737
|
+
const parentClone = parent === null || parent === void 0 ? void 0 : parent.clone().removeAll().append(clone);
|
|
738
|
+
clone.selectors = selectorsRemovedFromRule;
|
|
739
|
+
const nodeToPreserve = parentClone ? parentClone : clone;
|
|
740
|
+
this.removedNodes.push(nodeToPreserve);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Get the purged version of the css based on the files
|
|
746
|
+
*
|
|
747
|
+
* @param cssOptions - css options, files or raw strings
|
|
748
|
+
* @param selectors - set of extracted css selectors
|
|
749
|
+
*/
|
|
750
|
+
async getPurgedCSS(cssOptions, selectors) {
|
|
751
|
+
var _a;
|
|
752
|
+
const sources = [];
|
|
753
|
+
// resolve any globs
|
|
754
|
+
const processedOptions = [];
|
|
755
|
+
for (const option of cssOptions) {
|
|
756
|
+
if (typeof option === "string") {
|
|
757
|
+
processedOptions.push(...glob__namespace.sync(option, {
|
|
758
|
+
nodir: true,
|
|
759
|
+
ignore: this.options.skippedContentGlobs,
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
processedOptions.push(option);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
for (const option of processedOptions) {
|
|
767
|
+
const cssContent = typeof option === "string"
|
|
768
|
+
? this.options.stdin
|
|
769
|
+
? option
|
|
770
|
+
: await asyncFs.readFile(option, "utf-8")
|
|
771
|
+
: option.raw;
|
|
772
|
+
const isFromFile = typeof option === "string" && !this.options.stdin;
|
|
773
|
+
const root = postcss__namespace.parse(cssContent, {
|
|
774
|
+
from: isFromFile ? option : undefined,
|
|
775
|
+
});
|
|
776
|
+
// purge unused selectors
|
|
777
|
+
this.walkThroughCSS(root, selectors);
|
|
778
|
+
if (this.options.fontFace)
|
|
779
|
+
this.removeUnusedFontFaces();
|
|
780
|
+
if (this.options.keyframes)
|
|
781
|
+
this.removeUnusedKeyframes();
|
|
782
|
+
if (this.options.variables)
|
|
783
|
+
this.removeUnusedCSSVariables();
|
|
784
|
+
const postCSSResult = root.toResult({
|
|
785
|
+
map: this.options.sourceMap,
|
|
786
|
+
to: typeof this.options.sourceMap === "object"
|
|
787
|
+
? this.options.sourceMap.to
|
|
788
|
+
: undefined,
|
|
789
|
+
});
|
|
790
|
+
const result = {
|
|
791
|
+
css: postCSSResult.toString(),
|
|
792
|
+
file: typeof option === "string" ? option : option.name,
|
|
793
|
+
};
|
|
794
|
+
if (this.options.sourceMap) {
|
|
795
|
+
result.sourceMap = (_a = postCSSResult.map) === null || _a === void 0 ? void 0 : _a.toString();
|
|
796
|
+
}
|
|
797
|
+
if (this.options.rejected) {
|
|
798
|
+
result.rejected = Array.from(this.selectorsRemoved);
|
|
799
|
+
this.selectorsRemoved.clear();
|
|
800
|
+
}
|
|
801
|
+
if (this.options.rejectedCss) {
|
|
802
|
+
result.rejectedCss = postcss__namespace
|
|
803
|
+
.root({ nodes: this.removedNodes })
|
|
804
|
+
.toString();
|
|
805
|
+
}
|
|
806
|
+
sources.push(result);
|
|
807
|
+
}
|
|
808
|
+
return sources;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Check if the keyframe is safelisted with the option safelist keyframes
|
|
812
|
+
*
|
|
813
|
+
* @param keyframesName - name of the keyframe animation
|
|
814
|
+
*/
|
|
815
|
+
isKeyframesSafelisted(keyframesName) {
|
|
816
|
+
return this.options.safelist.keyframes.some((safelistItem) => {
|
|
817
|
+
return typeof safelistItem === "string"
|
|
818
|
+
? safelistItem === keyframesName
|
|
819
|
+
: safelistItem.test(keyframesName);
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Check if the selector is blocklisted with the option blocklist
|
|
824
|
+
*
|
|
825
|
+
* @param selector - css selector
|
|
826
|
+
*/
|
|
827
|
+
isSelectorBlocklisted(selector) {
|
|
828
|
+
return this.options.blocklist.some((blocklistItem) => {
|
|
829
|
+
return typeof blocklistItem === "string"
|
|
830
|
+
? blocklistItem === selector
|
|
831
|
+
: blocklistItem.test(selector);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Check if the selector is safelisted with the option safelist standard
|
|
836
|
+
*
|
|
837
|
+
* @param selector - css selector
|
|
838
|
+
*/
|
|
839
|
+
isSelectorSafelisted(selector) {
|
|
840
|
+
const isSafelisted = this.options.safelist.standard.some((safelistItem) => {
|
|
841
|
+
return typeof safelistItem === "string"
|
|
842
|
+
? safelistItem === selector
|
|
843
|
+
: safelistItem.test(selector);
|
|
844
|
+
});
|
|
845
|
+
const isPseudoElement = /^::.*/.test(selector);
|
|
846
|
+
return CSS_SAFELIST.includes(selector) || isPseudoElement || isSafelisted;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Check if the selector is safelisted with the option safelist deep
|
|
850
|
+
*
|
|
851
|
+
* @param selector - selector
|
|
852
|
+
*/
|
|
853
|
+
isSelectorSafelistedDeep(selector) {
|
|
854
|
+
return this.options.safelist.deep.some((safelistItem) => safelistItem.test(selector));
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Check if the selector is safelisted with the option safelist greedy
|
|
858
|
+
*
|
|
859
|
+
* @param selector - selector
|
|
860
|
+
*/
|
|
861
|
+
isSelectorSafelistedGreedy(selector) {
|
|
862
|
+
return this.options.safelist.greedy.some((safelistItem) => safelistItem.test(selector));
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Remove unused CSS
|
|
866
|
+
*
|
|
867
|
+
* @param userOptions - PurgeCSS options or path to the configuration file
|
|
868
|
+
* @returns an array of object containing the filename and the associated CSS
|
|
869
|
+
*
|
|
870
|
+
* @example Using a configuration file named purgecss.config.js
|
|
871
|
+
* ```ts
|
|
872
|
+
* const purgeCSSResults = await new PurgeCSS().purge()
|
|
873
|
+
* ```
|
|
874
|
+
*
|
|
875
|
+
* @example Using a custom path to the configuration file
|
|
876
|
+
* ```ts
|
|
877
|
+
* const purgeCSSResults = await new PurgeCSS().purge('./purgecss.config.js')
|
|
878
|
+
* ```
|
|
879
|
+
*
|
|
880
|
+
* @example Using the PurgeCSS options
|
|
881
|
+
* ```ts
|
|
882
|
+
* const purgeCSSResults = await new PurgeCSS().purge({
|
|
883
|
+
* content: ['index.html', '**\/*.js', '**\/*.html', '**\/*.vue'],
|
|
884
|
+
* css: ['css/app.css']
|
|
885
|
+
* })
|
|
886
|
+
* ```
|
|
887
|
+
*/
|
|
888
|
+
async purge(userOptions) {
|
|
889
|
+
this.options =
|
|
890
|
+
typeof userOptions !== "object"
|
|
891
|
+
? await setOptions(userOptions)
|
|
892
|
+
: {
|
|
893
|
+
...defaultOptions,
|
|
894
|
+
...userOptions,
|
|
895
|
+
safelist: standardizeSafelist(userOptions.safelist),
|
|
896
|
+
};
|
|
897
|
+
const { content, css, extractors, safelist } = this.options;
|
|
898
|
+
if (this.options.variables) {
|
|
899
|
+
this.variablesStructure.safelist = safelist.variables || [];
|
|
900
|
+
}
|
|
901
|
+
const fileFormatContents = content.filter((o) => typeof o === "string");
|
|
902
|
+
const rawFormatContents = content.filter((o) => typeof o === "object");
|
|
903
|
+
const cssFileSelectors = await this.extractSelectorsFromFiles(fileFormatContents, extractors);
|
|
904
|
+
const cssRawSelectors = await this.extractSelectorsFromString(rawFormatContents, extractors);
|
|
905
|
+
return this.getPurgedCSS(css, mergeExtractorSelectors(cssFileSelectors, cssRawSelectors));
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Remove unused CSS variables
|
|
909
|
+
*/
|
|
910
|
+
removeUnusedCSSVariables() {
|
|
911
|
+
this.variablesStructure.removeUnused();
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Remove unused font-faces
|
|
915
|
+
*/
|
|
916
|
+
removeUnusedFontFaces() {
|
|
917
|
+
for (const { name, node } of this.atRules.fontFace) {
|
|
918
|
+
if (!this.usedFontFaces.has(name)) {
|
|
919
|
+
node.remove();
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Remove unused keyframes
|
|
925
|
+
*/
|
|
926
|
+
removeUnusedKeyframes() {
|
|
927
|
+
for (const node of this.atRules.keyframes) {
|
|
928
|
+
if (!this.usedAnimations.has(node.params) &&
|
|
929
|
+
!this.isKeyframesSafelisted(node.params)) {
|
|
930
|
+
node.remove();
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Transform a selector node into a string
|
|
936
|
+
*/
|
|
937
|
+
getSelectorValue(selector) {
|
|
938
|
+
return ((selector.type === "attribute" && selector.attribute) || selector.value);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Determine if the selector should be kept, based on the selectors found in the files
|
|
942
|
+
*
|
|
943
|
+
* @param selector - set of css selectors found in the content files or string
|
|
944
|
+
* @param selectorsFromExtractor - selectors in the css rule
|
|
945
|
+
*
|
|
946
|
+
* @returns true if the selector should be kept in the processed CSS
|
|
947
|
+
*/
|
|
948
|
+
shouldKeepSelector(selector, selectorsFromExtractor) {
|
|
949
|
+
// selectors in pseudo classes are ignored except :where() and :is(). For those pseudo-classes, we are treating the selectors inside the same way as they would be outside.
|
|
950
|
+
if (isInPseudoClass(selector) && !isInPseudoClassWhereOrIs(selector)) {
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
if (isPseudoClassAtRootLevel(selector)) {
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
// if there is any greedy safelist pattern, run all the selector parts through them
|
|
957
|
+
// if there is any match, return true
|
|
958
|
+
if (this.options.safelist.greedy.length > 0) {
|
|
959
|
+
const selectorParts = selector.nodes.map(this.getSelectorValue);
|
|
960
|
+
if (selectorParts.some((selectorPart) => selectorPart && this.isSelectorSafelistedGreedy(selectorPart))) {
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
let isPresent = false;
|
|
965
|
+
for (const selectorNode of selector.nodes) {
|
|
966
|
+
const selectorValue = this.getSelectorValue(selectorNode);
|
|
967
|
+
// if the selector is safelisted with children
|
|
968
|
+
// returns true to keep all children selectors
|
|
969
|
+
if (selectorValue && this.isSelectorSafelistedDeep(selectorValue)) {
|
|
970
|
+
return true;
|
|
971
|
+
}
|
|
972
|
+
// The selector is found in the internal and user-defined safelist
|
|
973
|
+
if (selectorValue &&
|
|
974
|
+
(CSS_SAFELIST.includes(selectorValue) ||
|
|
975
|
+
this.isSelectorSafelisted(selectorValue))) {
|
|
976
|
+
isPresent = true;
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
// The selector is present in the blocklist
|
|
980
|
+
if (selectorValue && this.isSelectorBlocklisted(selectorValue)) {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
switch (selectorNode.type) {
|
|
984
|
+
case "attribute":
|
|
985
|
+
// `value` is a dynamic attribute, highly used in input element
|
|
986
|
+
// the choice is to always leave `value` as it can change based on the user
|
|
987
|
+
// idem for `checked`, `selected`, `open`
|
|
988
|
+
isPresent = [
|
|
989
|
+
...this.options.dynamicAttributes,
|
|
990
|
+
"value",
|
|
991
|
+
"checked",
|
|
992
|
+
"selected",
|
|
993
|
+
"open",
|
|
994
|
+
].includes(selectorNode.attribute)
|
|
995
|
+
? true
|
|
996
|
+
: isAttributeFound(selectorNode, selectorsFromExtractor);
|
|
997
|
+
break;
|
|
998
|
+
case "class":
|
|
999
|
+
isPresent = isClassFound(selectorNode, selectorsFromExtractor);
|
|
1000
|
+
break;
|
|
1001
|
+
case "id":
|
|
1002
|
+
isPresent = isIdentifierFound(selectorNode, selectorsFromExtractor);
|
|
1003
|
+
break;
|
|
1004
|
+
case "tag":
|
|
1005
|
+
isPresent = isTagFound(selectorNode, selectorsFromExtractor);
|
|
1006
|
+
break;
|
|
1007
|
+
default:
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
// selector is not safelisted
|
|
1011
|
+
// and it has not been found as an attribute/class/id/tag
|
|
1012
|
+
if (!isPresent) {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return isPresent;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Walk through the CSS AST and remove unused CSS
|
|
1020
|
+
*
|
|
1021
|
+
* @param root - root node of the postcss AST
|
|
1022
|
+
* @param selectors - selectors used in content files
|
|
1023
|
+
*/
|
|
1024
|
+
walkThroughCSS(root, selectors) {
|
|
1025
|
+
root.walk((node) => {
|
|
1026
|
+
if (node.type === "rule") {
|
|
1027
|
+
return this.evaluateRule(node, selectors);
|
|
1028
|
+
}
|
|
1029
|
+
if (node.type === "atrule") {
|
|
1030
|
+
return this.evaluateAtRule(node);
|
|
1031
|
+
}
|
|
1032
|
+
if (node.type === "comment") {
|
|
1033
|
+
if (isIgnoreAnnotation(node, "start")) {
|
|
1034
|
+
this.ignore = true;
|
|
1035
|
+
// remove ignore annotation
|
|
1036
|
+
node.remove();
|
|
1037
|
+
}
|
|
1038
|
+
else if (isIgnoreAnnotation(node, "end")) {
|
|
1039
|
+
this.ignore = false;
|
|
1040
|
+
// remove ignore annotation
|
|
1041
|
+
node.remove();
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
exports.ExtractorResultSets = ExtractorResultSets;
|
|
1049
|
+
exports.PurgeCSS = PurgeCSS;
|
|
1050
|
+
exports.VariableNode = VariableNode;
|
|
1051
|
+
exports.VariablesStructure = VariablesStructure;
|
|
1052
|
+
exports.defaultOptions = defaultOptions;
|
|
1053
|
+
exports.mergeExtractorSelectors = mergeExtractorSelectors;
|
|
1054
|
+
exports.setOptions = setOptions;
|
|
1055
|
+
exports.standardizeSafelist = standardizeSafelist;
|