purgecss 6.0.0 → 7.0.2

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