From 845ad0e7fab3979aae0d5081d942762c6aa98721 Mon Sep 17 00:00:00 2001 From: Vincent Taing Date: Fri, 30 Jul 2021 17:13:42 +0200 Subject: [PATCH 1/4] Add additional .css files --- packages/grunt-purgecss/Gruntfile.js | 4 +++- packages/grunt-purgecss/__tests__/fixtures/src/footer.css | 3 +++ packages/grunt-purgecss/__tests__/fixtures/src/menu.css | 3 +++ packages/grunt-purgecss/__tests__/fixtures/src/profile.css | 0 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 packages/grunt-purgecss/__tests__/fixtures/src/footer.css create mode 100644 packages/grunt-purgecss/__tests__/fixtures/src/menu.css create mode 100644 packages/grunt-purgecss/__tests__/fixtures/src/profile.css diff --git a/packages/grunt-purgecss/Gruntfile.js b/packages/grunt-purgecss/Gruntfile.js index 4c284074..b499976e 100644 --- a/packages/grunt-purgecss/Gruntfile.js +++ b/packages/grunt-purgecss/Gruntfile.js @@ -9,6 +9,8 @@ module.exports = grunt => { content: ['./__tests__/fixtures/src/simple/**/*.html'] }, files: { + '__tests__/tmp/menu.css': ['__tests__/fixtures/src/menu.css'], + '__tests__/tmp/profile.css': ['__tests__/fixtures/src/profile.css'], '__tests__/tmp/simple.css': ['__tests__/fixtures/src/simple/simple.css'] } } @@ -21,4 +23,4 @@ module.exports = grunt => { // By default, lint and run all tests. grunt.registerTask('default', ['purgecss']); -}; \ No newline at end of file +}; diff --git a/packages/grunt-purgecss/__tests__/fixtures/src/footer.css b/packages/grunt-purgecss/__tests__/fixtures/src/footer.css new file mode 100644 index 00000000..cf54a0a2 --- /dev/null +++ b/packages/grunt-purgecss/__tests__/fixtures/src/footer.css @@ -0,0 +1,3 @@ +.footer-unused-class { + background: black; +} diff --git a/packages/grunt-purgecss/__tests__/fixtures/src/menu.css b/packages/grunt-purgecss/__tests__/fixtures/src/menu.css new file mode 100644 index 00000000..8763d0ae --- /dev/null +++ b/packages/grunt-purgecss/__tests__/fixtures/src/menu.css @@ -0,0 +1,3 @@ +.menu-unused-class { + background: red; +} diff --git a/packages/grunt-purgecss/__tests__/fixtures/src/profile.css b/packages/grunt-purgecss/__tests__/fixtures/src/profile.css new file mode 100644 index 00000000..e69de29b From 00c6dac8dd6ecc24a40be538b35955d88d4552c2 Mon Sep 17 00:00:00 2001 From: Vincent Taing Date: Fri, 30 Jul 2021 18:01:23 +0200 Subject: [PATCH 2/4] Add failling test: grunt not processing all files --- packages/grunt-purgecss/Gruntfile.js | 1 + .../__tests__/fixtures/expected/footer.css | 0 .../__tests__/fixtures/expected/menu.css | 0 .../__tests__/fixtures/expected/profile.css | 0 .../__tests__/fixtures/src/profile.css | 3 +++ .../grunt-purgecss/__tests__/index.test.ts | 18 ++++++++++++++++-- 6 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 packages/grunt-purgecss/__tests__/fixtures/expected/footer.css create mode 100644 packages/grunt-purgecss/__tests__/fixtures/expected/menu.css create mode 100644 packages/grunt-purgecss/__tests__/fixtures/expected/profile.css diff --git a/packages/grunt-purgecss/Gruntfile.js b/packages/grunt-purgecss/Gruntfile.js index b499976e..aa34a2dc 100644 --- a/packages/grunt-purgecss/Gruntfile.js +++ b/packages/grunt-purgecss/Gruntfile.js @@ -11,6 +11,7 @@ module.exports = grunt => { files: { '__tests__/tmp/menu.css': ['__tests__/fixtures/src/menu.css'], '__tests__/tmp/profile.css': ['__tests__/fixtures/src/profile.css'], + '__tests__/tmp/footer.css': ['__tests__/fixtures/src/footer.css'], '__tests__/tmp/simple.css': ['__tests__/fixtures/src/simple/simple.css'] } } diff --git a/packages/grunt-purgecss/__tests__/fixtures/expected/footer.css b/packages/grunt-purgecss/__tests__/fixtures/expected/footer.css new file mode 100644 index 00000000..e69de29b diff --git a/packages/grunt-purgecss/__tests__/fixtures/expected/menu.css b/packages/grunt-purgecss/__tests__/fixtures/expected/menu.css new file mode 100644 index 00000000..e69de29b diff --git a/packages/grunt-purgecss/__tests__/fixtures/expected/profile.css b/packages/grunt-purgecss/__tests__/fixtures/expected/profile.css new file mode 100644 index 00000000..e69de29b diff --git a/packages/grunt-purgecss/__tests__/fixtures/src/profile.css b/packages/grunt-purgecss/__tests__/fixtures/src/profile.css index e69de29b..f943f55e 100644 --- a/packages/grunt-purgecss/__tests__/fixtures/src/profile.css +++ b/packages/grunt-purgecss/__tests__/fixtures/src/profile.css @@ -0,0 +1,3 @@ +.profile-unused-class { + background: hotpink; +} diff --git a/packages/grunt-purgecss/__tests__/index.test.ts b/packages/grunt-purgecss/__tests__/index.test.ts index 89ff1386..edc789c9 100644 --- a/packages/grunt-purgecss/__tests__/index.test.ts +++ b/packages/grunt-purgecss/__tests__/index.test.ts @@ -1,5 +1,6 @@ import { execSync } from "child_process"; import fs from "fs"; +import path from "path"; describe("Purgecss grunt plugin", () => { const cwd = process.cwd(); @@ -9,18 +10,31 @@ describe("Purgecss grunt plugin", () => { execSync("npx grunt"); }); + function emptyFolder(directory: string) { + fs.readdir(directory, (err, files) => { + if (err) throw err; + + for (const file of files) { + fs.unlink(path.join(directory, file), err => { + if (err) throw err; + }); + } + }); + } + afterAll(() => { + emptyFolder(`${__dirname}/tmp`); process.chdir(cwd); }); - const files = ["simple.css"]; + const files = ["simple.css", "footer.css", "menu.css", "profile.css"]; for (const file of files) { it(`remove unused css successfully: ${file}`, () => { const actual = fs.readFileSync(`${__dirname}/tmp/${file}`).toString(); const expected = fs .readFileSync(`${__dirname}/fixtures/expected/${file}`) .toString(); - expect(actual).toBe(expected); + expect(actual.replace(/\s/g, '')).toBe(expected.replace(/\s/g, '')); }); } }); From 71baad434a45b0889c8855d2dc5d49439184d27d Mon Sep 17 00:00:00 2001 From: Vincent Taing Date: Fri, 30 Jul 2021 20:59:55 +0200 Subject: [PATCH 3/4] Wait for all files being purged before returning the result --- packages/grunt-purgecss/src/index.ts | 14 +++++++++----- packages/grunt-purgecss/tasks/purgecss.js | 2 +- packages/purgecss/bin/purgecss.js | 2 +- packages/purgecss/src/index.ts | 1 - 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/grunt-purgecss/src/index.ts b/packages/grunt-purgecss/src/index.ts index 63b1528e..a969f946 100644 --- a/packages/grunt-purgecss/src/index.ts +++ b/packages/grunt-purgecss/src/index.ts @@ -19,9 +19,10 @@ function gruntPurgeCSS(grunt: IGrunt): void { grunt.registerMultiTask("purgecss", "Grunt plugin for PurgeCSS", function () { const done = this.async(); const options = this.options(defaultOptions); + const promisedPurgedFiles = []; for (const file of this.files) { const source = getAvailableFiles(grunt, file.src); - new PurgeCSS() + const purgedCss = new PurgeCSS() .purge({ ...options, css: source, @@ -34,12 +35,15 @@ function gruntPurgeCSS(grunt: IGrunt): void { grunt.file.write(file.dest, purgeCSSResults[0].css); // Print a success message grunt.log.writeln(`File "${file.dest}" created.`); - done(); }) - .catch(() => { - done(false); - }); + + promisedPurgedFiles.push(purgedCss); } + Promise.all(promisedPurgedFiles) + .then(() => { + done(); + }).catch(() => done(false)) + }); } diff --git a/packages/grunt-purgecss/tasks/purgecss.js b/packages/grunt-purgecss/tasks/purgecss.js index 749c09bb..2ad96357 100644 --- a/packages/grunt-purgecss/tasks/purgecss.js +++ b/packages/grunt-purgecss/tasks/purgecss.js @@ -1 +1 @@ -"use strict";var t=require("purgecss");function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var n=e(t);function r(t,e=[]){return e.filter((e=>!!t.file.exists(e)||(t.log.warn(`Source file "${e}" not found.`),!1)))}module.exports=function(e){e.registerMultiTask("purgecss","Grunt plugin for PurgeCSS",(function(){const s=this.async(),i=this.options(t.defaultOptions);for(const t of this.files){const o=r(e,t.src);(new n.default).purge({...i,css:o}).then((n=>{if(void 0===t.dest)throw new Error("Destination file not found");e.file.write(t.dest,n[0].css),e.log.writeln(`File "${t.dest}" created.`),s()})).catch((()=>{s(!1)}))}}))}; +"use strict";var t=require("purgecss");function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var s=e(t);function n(t,e=[]){return e.filter((e=>!!t.file.exists(e)||(t.log.warn(`Source file "${e}" not found.`),!1)))}module.exports=function(e){e.registerMultiTask("purgecss","Grunt plugin for PurgeCSS",(function(){const r=this.async(),i=this.options(t.defaultOptions),o=[];for(const t of this.files){const r=n(e,t.src),u=(new s.default).purge({...i,css:r}).then((s=>{if(void 0===t.dest)throw new Error("Destination file not found");e.file.write(t.dest,s[0].css),e.log.writeln(`File "${t.dest}" created.`)}));o.push(u)}Promise.all(o).then((()=>{r()})).catch((()=>r(!1)))}))}; diff --git a/packages/purgecss/bin/purgecss.js b/packages/purgecss/bin/purgecss.js index b42b8b0b..db655301 100755 --- a/packages/purgecss/bin/purgecss.js +++ b/packages/purgecss/bin/purgecss.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -"use strict";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 l=c(t),u=a(t),f=a(s),d=a(r),h=c(o),p=a(i),m="4.0.3",g="Remove unused css selectors";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=["*","::-webkit-scrollbar","::selection",":root","::before","::after"],S={css:[],content:[],defaultExtractor:e=>e.match(/[A-Za-z0-9_-]+/g)||[],extractors:[],fontFace:!1,keyframes:!1,rejected:!1,stdin:!1,stdout:!1,variables:!1,safelist:{standard:[],deep:[],greedy:[],variables:[],keyframes:[]},blocklist:[],skippedContentGlobs:[],dynamicAttributes:[]};function w(e,t){const s=[];return e.replace(t,(function(){const t=arguments,r=Array.prototype.slice.call(t,0,-2);return r.input=t[t.length-1],r.index=t[t.length-2],s.push(r),e})),s}class k{constructor(e){this.nodes=[],this.isUsed=!1,this.value=e}}class F{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 k(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.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)];for(;0!==t.length;){const e=t.pop();e&&!e.isUsed&&(e.isUsed=!0,t.push(...e.nodes))}}removeUnused(){for(const e of this.usedVariables){const t=this.nodes.get(e);if(t){w(t.value.value,/var\((.+?)[,)]/g).forEach((e=>{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)t.isUsed||this.isVariablesSafelisted(e)||t.value.remove()}isVariablesSafelisted(e){return this.safelist.some((t=>"string"==typeof t?t===e:t.test(e)))}}const V={access:n.promisify(l.access),readFile:n.promisify(l.readFile)};function A(e=[]){return Array.isArray(e)?{...S.safelist,standard:e}:{...S.safelist,...e}}async function x(e="purgecss.config.js"){let t;try{const s=d.default.resolve(process.cwd(),e);t=await Promise.resolve().then((function(){return c(require(s))}))}catch(e){throw new Error("Error loading the config file "+e.message)}return{...S,...t,safelist:A(t.safelist)}}async function j(e,t){return new y(await t(e))}function U(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 R(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 P(e,t){return t.hasTag(e.value)}function q(e){return"atrule"===(null==e?void 0:e.type)}function G(e){return"rule"===(null==e?void 0:e.type)}class N{constructor(){this.ignore=!1,this.atRules={fontFace:[],keyframes:[]},this.usedAnimations=new Set,this.usedFontFaces=new Set,this.selectorsRemoved=new Set,this.variablesStructure=new F,this.options=S}collectDeclarationsData(e){const{prop:t,value:s}=e;if(this.options.variables){const r=w(s,/var\((.+?)[,)]/g);t.startsWith("--")?(this.variablesStructure.addVariable(e),r.length>0&&this.variablesStructure.addVariableUsage(e,r)):r.length>0&&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([]);for(const r of e){let e=[];try{await V.access(r,l.constants.F_OK),e.push(r)}catch(t){e=f.default.sync(r,{nodir:!0,ignore:this.options.skippedContentGlobs})}for(const r of e){const e=await V.readFile(r,"utf-8"),o=this.getFileExtractor(r,t),i=await j(e,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 j(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)&&U(s,"next"))return void s.remove();if(e.parent&&q(e.parent)&&"keyframes"===e.parent.name)return;if(!G(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;let r=!0;if(e.selector=p.default((e=>{e.walk((e=>{"selector"===e.type&&(r=this.shouldKeepSelector(e,t),r||(this.options.rejected&&this.selectorsRemoved.add(e.toString()),e.remove()))}))})).processSync(e.selector),r&&void 0!==e.nodes)for(const t of e.nodes)"decl"===t.type&&this.collectDeclarationsData(t);const o=e.parent;e.selector||e.remove(),function(e){return!!(G(e)&&!e.selector||(null==e?void 0:e.nodes)&&!e.nodes.length||q(e)&&(!e.nodes&&!e.params||!e.params&&e.nodes&&!e.nodes.length))}(o)&&(null==o||o.remove())}async getPurgedCSS(e,t){const s=[],r=[];for(const t of e)"string"==typeof t?r.push(...f.default.sync(t,{nodir:!0,ignore:this.options.skippedContentGlobs})):r.push(t);for(const e of r){const r="string"==typeof e?this.options.stdin?e:await V.readFile(e,"utf-8"):e.raw,o=h.parse(r);this.walkThroughCSS(o,t),this.options.fontFace&&this.removeUnusedFontFaces(),this.options.keyframes&&this.removeUnusedKeyframes(),this.options.variables&&this.removeUnusedCSSVariables();const i={css:o.toString(),file:"string"==typeof e?e:e.name};this.options.rejected&&(i.rejected=Array.from(this.selectorsRemoved),this.selectorsRemoved.clear()),s.push(i)}return s}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)));return b.includes(e)||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 x(e):{...S,...e,safelist:A(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))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)||R(o,t);break;case"class":r=o,s=t.hasClass(r.value);break;case"id":s=E(o,t);break;case"tag":s=P(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&&(U(e,"start")?(this.ignore=!0,e.remove()):U(e,"end")&&(this.ignore=!1,e.remove())))))}}async function O(e,t){try{await u.default.promises.writeFile(e,t)}catch(e){console.error(e.message)}}try{!async function(){var t;e.program.description(g).version(m).usage("--css --content [options]"),e.program.option("-con, --content ","glob of content files").option("-css, --css ","glob of css files").option("-c, --config ","path to the configuration file").option("-o, --output ","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("-s, --safelist ","list of classes that should not be removed").option("-b, --blocklist ","list of selectors that should be removed").option("-k, --skippedContentGlobs ","list of glob patterns for folders/files that should not be scanned"),e.program.parse(process.argv);const{config:s,css:r,content:o,output:i,fontFace:n,keyframes:a,variables:c,rejected:l,safelist:u,blocklist:f,skippedContentGlobs:d}=e.program.opts();s||o&&r||e.program.help();let h=S;s&&(h=await x(s)),o&&(h.content=o),r&&(h.css=r),n&&(h.fontFace=n),a&&(h.keyframes=a),l&&(h.rejected=l),c&&(h.variables=c),u&&(h.safelist=A(u)),f&&(h.blocklist=f),d&&(h.skippedContentGlobs=d);const p=await(new N).purge(h),v=h.output||i;if(v){if(1===p.length&&v.endsWith(".css"))return void await O(v,p[0].css);for(const e of p){const s=null===(t=null==e?void 0:e.file)||void 0===t?void 0:t.split("/").pop();await O(`${i}/${s}`,e.css)}}else console.log(JSON.stringify(p))}()}catch(e){console.error(e.message),process.exit(1)} +"use strict";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 l=c(t),u=a(t),f=a(s),d=a(r),h=c(o),p=a(i),m="4.0.3",g="Remove unused css selectors";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=["*","::-webkit-scrollbar","::selection",":root","::before","::after"],S={css:[],content:[],defaultExtractor:e=>e.match(/[A-Za-z0-9_-]+/g)||[],extractors:[],fontFace:!1,keyframes:!1,rejected:!1,stdin:!1,stdout:!1,variables:!1,safelist:{standard:[],deep:[],greedy:[],variables:[],keyframes:[]},blocklist:[],skippedContentGlobs:[],dynamicAttributes:[]};function w(e,t){const s=[];return e.replace(t,(function(){const t=arguments,r=Array.prototype.slice.call(t,0,-2);return r.input=t[t.length-1],r.index=t[t.length-2],s.push(r),e})),s}class k{constructor(e){this.nodes=[],this.isUsed=!1,this.value=e}}class F{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 k(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.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)];for(;0!==t.length;){const e=t.pop();e&&!e.isUsed&&(e.isUsed=!0,t.push(...e.nodes))}}removeUnused(){for(const e of this.usedVariables){const t=this.nodes.get(e);if(t){w(t.value.value,/var\((.+?)[,)]/g).forEach((e=>{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)t.isUsed||this.isVariablesSafelisted(e)||t.value.remove()}isVariablesSafelisted(e){return this.safelist.some((t=>"string"==typeof t?t===e:t.test(e)))}}const V={access:n.promisify(l.access),readFile:n.promisify(l.readFile)};function A(e=[]){return Array.isArray(e)?{...S.safelist,standard:e}:{...S.safelist,...e}}async function x(e="purgecss.config.js"){let t;try{const s=d.default.resolve(process.cwd(),e);t=await Promise.resolve().then((function(){return c(require(s))}))}catch(e){throw new Error(`Error loading the config file ${e.message}`)}return{...S,...t,safelist:A(t.safelist)}}async function j(e,t){return new y(await t(e))}function U(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 R(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 P(e,t){return t.hasTag(e.value)}function q(e){return"atrule"===(null==e?void 0:e.type)}function G(e){return"rule"===(null==e?void 0:e.type)}class N{constructor(){this.ignore=!1,this.atRules={fontFace:[],keyframes:[]},this.usedAnimations=new Set,this.usedFontFaces=new Set,this.selectorsRemoved=new Set,this.variablesStructure=new F,this.options=S}collectDeclarationsData(e){const{prop:t,value:s}=e;if(this.options.variables){const r=w(s,/var\((.+?)[,)]/g);t.startsWith("--")?(this.variablesStructure.addVariable(e),r.length>0&&this.variablesStructure.addVariableUsage(e,r)):r.length>0&&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([]);for(const r of e){let e=[];try{await V.access(r,l.constants.F_OK),e.push(r)}catch(t){e=f.default.sync(r,{nodir:!0,ignore:this.options.skippedContentGlobs})}for(const r of e){const e=await V.readFile(r,"utf-8"),o=this.getFileExtractor(r,t),i=await j(e,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 j(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)&&U(s,"next"))return void s.remove();if(e.parent&&q(e.parent)&&"keyframes"===e.parent.name)return;if(!G(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;let r=!0;if(e.selector=p.default((e=>{e.walk((e=>{"selector"===e.type&&(r=this.shouldKeepSelector(e,t),r||(this.options.rejected&&this.selectorsRemoved.add(e.toString()),e.remove()))}))})).processSync(e.selector),r&&void 0!==e.nodes)for(const t of e.nodes)"decl"===t.type&&this.collectDeclarationsData(t);const o=e.parent;e.selector||e.remove(),function(e){return!!(G(e)&&!e.selector||(null==e?void 0:e.nodes)&&!e.nodes.length||q(e)&&(!e.nodes&&!e.params||!e.params&&e.nodes&&!e.nodes.length))}(o)&&(null==o||o.remove())}async getPurgedCSS(e,t){const s=[],r=[];for(const t of e)"string"==typeof t?r.push(...f.default.sync(t,{nodir:!0,ignore:this.options.skippedContentGlobs})):r.push(t);for(const e of r){const r="string"==typeof e?this.options.stdin?e:await V.readFile(e,"utf-8"):e.raw,o=h.parse(r);this.walkThroughCSS(o,t),this.options.fontFace&&this.removeUnusedFontFaces(),this.options.keyframes&&this.removeUnusedKeyframes(),this.options.variables&&this.removeUnusedCSSVariables();const i={css:o.toString(),file:"string"==typeof e?e:e.name};this.options.rejected&&(i.rejected=Array.from(this.selectorsRemoved),this.selectorsRemoved.clear()),s.push(i)}return s}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)));return b.includes(e)||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 x(e):{...S,...e,safelist:A(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))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)||R(o,t);break;case"class":r=o,s=t.hasClass(r.value);break;case"id":s=E(o,t);break;case"tag":s=P(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&&(U(e,"start")?(this.ignore=!0,e.remove()):U(e,"end")&&(this.ignore=!1,e.remove())))))}}async function O(e,t){try{await u.default.promises.writeFile(e,t)}catch(e){console.error(e.message)}}try{!async function(){var t;e.program.description(g).version(m).usage("--css --content [options]"),e.program.option("-con, --content ","glob of content files").option("-css, --css ","glob of css files").option("-c, --config ","path to the configuration file").option("-o, --output ","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("-s, --safelist ","list of classes that should not be removed").option("-b, --blocklist ","list of selectors that should be removed").option("-k, --skippedContentGlobs ","list of glob patterns for folders/files that should not be scanned"),e.program.parse(process.argv);const{config:s,css:r,content:o,output:i,fontFace:n,keyframes:a,variables:c,rejected:l,safelist:u,blocklist:f,skippedContentGlobs:d}=e.program.opts();s||o&&r||e.program.help();let h=S;s&&(h=await x(s)),o&&(h.content=o),r&&(h.css=r),n&&(h.fontFace=n),a&&(h.keyframes=a),l&&(h.rejected=l),c&&(h.variables=c),u&&(h.safelist=A(u)),f&&(h.blocklist=f),d&&(h.skippedContentGlobs=d);const p=await(new N).purge(h),v=h.output||i;if(v){if(1===p.length&&v.endsWith(".css"))return void await O(v,p[0].css);for(const e of p){const s=null===(t=null==e?void 0:e.file)||void 0===t?void 0:t.split("/").pop();await O(`${i}/${s}`,e.css)}}else console.log(JSON.stringify(p))}()}catch(e){console.error(e.message),process.exit(1)} diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts index 19653763..986a3830 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -348,7 +348,6 @@ class PurgeCSS { extractors: Extractors[] ): Promise { const selectors = new ExtractorResultSets([]); - for (const globfile of files) { let filesNames: string[] = []; From daef07edc1eecddca2d9c844d9ce94d69a36a2e5 Mon Sep 17 00:00:00 2001 From: Vincent Taing Date: Fri, 30 Jul 2021 21:16:48 +0200 Subject: [PATCH 4/4] Manual code styling --- .../grunt-purgecss/__tests__/index.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/grunt-purgecss/__tests__/index.test.ts b/packages/grunt-purgecss/__tests__/index.test.ts index edc789c9..1f798908 100644 --- a/packages/grunt-purgecss/__tests__/index.test.ts +++ b/packages/grunt-purgecss/__tests__/index.test.ts @@ -10,17 +10,17 @@ describe("Purgecss grunt plugin", () => { execSync("npx grunt"); }); - function emptyFolder(directory: string) { - fs.readdir(directory, (err, files) => { - if (err) throw err; + function emptyFolder(directory: string) { + fs.readdir(directory, (err, files) => { + if (err) throw err; - for (const file of files) { - fs.unlink(path.join(directory, file), err => { - if (err) throw err; - }); - } + for (const file of files) { + fs.unlink(path.join(directory, file), err => { + if (err) throw err; }); - } + } + }); + } afterAll(() => { emptyFolder(`${__dirname}/tmp`); @@ -34,7 +34,7 @@ describe("Purgecss grunt plugin", () => { const expected = fs .readFileSync(`${__dirname}/fixtures/expected/${file}`) .toString(); - expect(actual.replace(/\s/g, '')).toBe(expected.replace(/\s/g, '')); + expect(actual.replace(/\s/g, '')).toBe(expected.replace(/\s/g, '')); }); } });