diff --git a/README.md b/README.md index 2bfac6c..4862d5c 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,7 @@ The CLI usage is not implemented yet :scream:. At the moment there is no need of - [ ] :star: cookie includes - [ ] :star: wildcards - [ ] :grey_question: positioning of critical css rules +- [ ] :+1: compress output option - [x] :fire: return of the remaining css aswell - [x] :grey_question: multi selector partial matches - [x] :tea: returning of remaining css aswell (optional) @@ -219,7 +220,7 @@ None yet ## Troubleshooting -### WSL / Windows Linux Subsystem Support +#### WSL / Windows Linux Subsystem Support Some unkown reasons prevent puppeteer to run properly in a WSL environment. If you have any errors please try to use your default OS command shell or equivalent. If the error still exists don't hesitate to create an issue ticket. ## Inspiration diff --git a/gulpfile.js b/gulpfile.js index 07eabc6..7a2a258 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -7,7 +7,6 @@ const clean = require('gulp-clean'); const srcDir = path.join("./", "src", "**"); const libDir = path.join("./", "lib"); - gulp.task('cleanup', () => { return gulp.src(libDir, {read: false}) .pipe(clean()); diff --git a/index.js b/index.js index b5bfa24..cf1ea24 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,12 @@ const log = require("signale"); const path = require("path"); const NODE_ENV = process.env.NODE_ENV || "production"; -const pathToCrittr = NODE_ENV === "development" ? "src" : "lib"; +let IS_NPM_PACKAGE = false; +try { + IS_NPM_PACKAGE = !!require.resolve("crittr"); +} catch (e) {} + +const pathToCrittr = NODE_ENV === "development" && !IS_NPM_PACKAGE ? "src" : "lib"; const Crittr = require(path.join(__dirname, pathToCrittr, 'classes', 'Crittr.class.js')); /** @@ -14,8 +19,8 @@ const Crittr = require(path.join(__dirname, pathToCrittr, 'classes', 'Crit module.exports = (options) => { return new Promise(async (resolve, reject) => { log.time("Crittr Run"); - const crttr = new Crittr(options); - let resultObj = {critical: null, rest: null}; + const crttr = new Crittr(options); + let resultObj = {critical: null, rest: null}; try { (resultObj = await crttr.run()); } catch (err) { diff --git a/lib/classes/Crittr.class.js b/lib/classes/Crittr.class.js index 0060186..46c9fec 100644 --- a/lib/classes/Crittr.class.js +++ b/lib/classes/Crittr.class.js @@ -1 +1 @@ -"use strict";const fs=require("fs-extra"),util=require("util"),readFilePromise=util.promisify(fs.readFile),debug=require("debug")("Crittr Class"),log=require("signale"),chalk=require("chalk"),merge=require("deepmerge"),Queue=require("run-queue"),puppeteer=require("puppeteer"),devices=require("puppeteer/DeviceDescriptors"),mqpacker=require("css-mqpacker"),sortCSSmq=require("sort-css-media-queries"),DEFAULTS=require("../Constants"),Ast=require("./Ast.class"),CssTransformator=require("./CssTransformator.class"),extractCriticalCss_script=require("../evaluation/extract_critical_with_css");class Crittr{constructor(e){this.options={css:null,urls:[],timeout:DEFAULTS.TIMEOUT,pageLoadTimeout:DEFAULTS.PAGE_LOAD_TIMEOUT,outputRemainingCss:DEFAULTS.OUTPUT_REMAINING_CSS,browser:{userAgent:DEFAULTS.BROWSER_USER_AGENT,isCacheEnabled:DEFAULTS.BROWSER_CACHE_ENABLED,isJsEnabled:DEFAULTS.BROWSER_JS_ENABLED,concurrentTabs:DEFAULTS.BROWSER_CONCURRENT_TABS},device:{width:DEFAULTS.DEVICE_WIDTH,height:DEFAULTS.DEVICE_HEIGHT,scaleFactor:DEFAULTS.DEVICE_SCALE_FACTOR,isMobile:DEFAULTS.DEVICE_IS_MOBILE,hasTouch:DEFAULTS.DEVICE_HAS_TOUCH,isLandscape:DEFAULTS.DEVICE_IS_LANDSCAPE},puppeteer:{browser:null,chromePath:null,headless:DEFAULTS.PUPPETEER_HEADLESS},printBrowserConsole:DEFAULTS.PRINT_BROWSER_CONSOLE,dropKeyframes:DEFAULTS.DROP_KEYFRAMES,keepSelectors:[],removeSelectors:[],blockRequests:["maps.gstatic.com","maps.googleapis.com","googletagmanager.com","google-analytics.com","google.","googleadservices.com","generaltracking.de","bing.com","doubleclick.net"]},this.options=merge(this.options,e),this._browser=null,this._cssTransformator=new CssTransformator,"string"==typeof this.options.device&&(devices[this.options.device]?this.options.device=devices[this.options.device].viewport:log.error("Option 'device' is set as string but has an unknown value. Use devices of puppeteer (https://github.com/GoogleChrome/puppeteer/blob/master/DeviceDescriptors.js) or an object!"));const t=this.validateOptions();t.length>0&&(t.forEach(({message:e})=>{log.error(e)}),process.exit(1))}validateOptions(){const e=[];return Array.isArray(this.options.urls)||e.push({message:"Url not valid"}),"string"!=typeof this.options.css&&e.push({message:"css not valid. Expected string got "+typeof this.options.css}),e}run(){return new Promise(async(e,t)=>{debug("run - Starting run ...");let r="",s="",i=[];try{debug("run - Get css content ..."),this._cssContent=await this.getCssContent(),debug("run - Get css content done!")}catch(e){debug("run - ERROR while extracting css content"),t(e)}try{debug("run - Starting browser ..."),this._browser=await this.getBrowser(),debug("run - Browser started!")}catch(e){debug("run - ERROR: Browser could not be launched ... abort!"),t(e)}try{debug("run - Starting critical css extraction ..."),[r,s,i]=await this.getCriticalCssFromUrls(),i.length>0&&(log.warn("Some of the urls had errors. Please review them below!"),this.printErrors(i)),debug("run - Finished critical css extraction!")}catch(e){debug("run - ERROR while critical css extraction"),t(e)}try{debug("run - Browser closing ..."),await this._browser.close(),debug("run - Browser closed!")}catch(e){debug("run - ERROR: Browser could not be closed -> already closed?")}debug("run - Extraction ended!"),e({critical:r,rest:s})})}getBrowser(){return new Promise(async(e,t)=>{try{null!==this.options.puppeteer.browser&&e(this.options.puppeteer.browser),e(await puppeteer.launch({ignoreHTTPSErrors:!0,args:["--disable-setuid-sandbox","--no-sandbox","--ignore-certificate-errors","--disable-dev-shm-usage"],dumpio:!1,headless:this.options.puppeteer.headless,executablePath:this.options.puppeteer.chromePath}).then(e=>e))}catch(e){t(e)}})}gracefulClosePage(e,t){return new Promise(async(r,s)=>{this.printErrors(t);try{debug("gracefulClosePage - Closing page after error gracefully ..."),await e.close(),debug("gracefulClosePage - Page closed gracefully!")}catch(e){debug("gracefulClosePage - Error while closing page -> already closed?")}r()})}printErrors(e){if(e)if(log.warn(chalk.red("Errors occured during processing. Please have a look and report them if necessary")),Array.isArray(e))for(let t of e)log.error(t);else log.error(e)}getPage(){return this._browser.newPage()}getCssContent(){return new Promise(async(e,t)=>{if("string"==typeof this.options.css){let r="";if(this.options.css.endsWith(".css"))try{0===(r=await readFilePromise(this.options.css,"utf8")).length&&t(new Error("No CSS content in file exists -> exit!"))}catch(e){t(e)}else r=this.options.css;e(r)}else e(!1)})}getCriticalCssFromUrls(){return new Promise(async(e,t)=>{let r=[];const s=this.options.urls,i=new Set,o=new Set,a=this._cssTransformator.getAst(this._cssContent),l=new Queue({maxConcurrency:this.options.browser.concurrentTabs}),c=async(e,t,s,i)=>{try{debug("getCriticalCssFromUrls - Try to get critical ast from "+e);const[o,a]=await this.evaluateUrl(e,t);s.add(o),i.add(a),debug("getCriticalCssFromUrls - Successfully extracted critical ast!")}catch(t){debug("getCriticalCssFromUrls - ERROR getting critical ast from promise"),log.error("Could not get critical ast for url "+e),log.error(t),r.push(t)}};for(let e of s)l.add(1,c,[e,a,i,o]);l.run().then(async()=>{0===i.size&&t(r),debug("getCriticalCssFromUrls - Merging multiple atf ast objects. Size: "+i.size);let s=new Map;for(let e of i)try{s=Ast.generateRuleMap(e,s)}catch(e){debug("getCriticalCssFromUrls - ERROR merging multiple atf ast objects"),t(e)}debug("getCriticalCssFromUrls - Merging multiple atf ast objects - finished");let a=new Map;if(this.options.outputRemainingCss){debug("getCriticalCssFromUrls - Merging multiple rest ast objects. Size: "+o.size);for(let e of o)try{a=Ast.generateRuleMap(e,a)}catch(e){debug("getCriticalCssFromUrls - ERROR merging multiple rest ast objects"),t(e)}debug("getCriticalCssFromUrls - Merging multiple rest ast objects - finished"),debug("getCriticalCssFromUrls - Filter duplicates of restMap");for(const[e,t]of s)if(a.has(e)){let r=a.get(e);(r=r.filter(e=>!t.some(t=>e.hash===t.hash))).length>0?a.set(e,r):a.delete(e)}debug("getCriticalCssFromUrls - Filter duplicates of restMap - finished")}debug("getCriticalCssFromUrls - Creating AST Object of atf ruleMap");let l=Ast.getAstOfRuleMap(s),c=this._cssTransformator.getCssFromAst(l).code;c=mqpacker.pack(c,{sort:sortCSSmq}),debug("getCriticalCssFromUrls - Creating AST Object of atf ruleMap - Finished");let n=null,g="";this.options.outputRemainingCss&&(debug("getCriticalCssFromUrls - Creating AST Object of remaining ruleMap"),n=Ast.getAstOfRuleMap(a),debug("getCriticalCssFromUrls - Creating AST Object of remaining ruleMap - Finished"),g=this._cssTransformator.getCssFromAst(n).code,g=mqpacker.pack(g,{sort:sortCSSmq})),e([c,g,r])}).catch(e=>{t(e)})})}evaluateUrl(e,t){return new Promise(async(r,s)=>{let i=3,o=!1,a=null,l=new Map,c=null,n=null;const g=async()=>new Promise((e,t)=>{try{this.getPage().then(t=>{e(t)}).catch(r=>{i-- >0?(log.warn("Could not get page from browser. Retry "+i+" times."),e(g())):(log.warn("Tried to get page but failed. Abort now ..."),t(r))})}catch(e){t(e)}});try{debug("evaluateUrl - Open new Page-Tab ..."),a=await g(),!0===this.options.printBrowserConsole&&(a.on("console",e=>{const t=e.args();for(let e=0;e{log.log("Page error: "+e.toString())}),a.on("error",e=>{log.log("Error: "+e.toString())})),debug("evaluateUrl - Page-Tab opened!")}catch(e){debug("evaluateUrl - Error while opening page tab -> abort!"),o=e}if(!1===o)try{let e=this.options.browser,t=this.options.device;debug("evaluateUrl - Set page properties ..."),await a.setCacheEnabled(e.isCacheEnabled),await a.setJavaScriptEnabled(e.isJsEnabled),await a.setRequestInterception(!0);const r=this.options.blockRequests;a.on("request",e=>{const t=e.url();if(r)for(const s of r)if(t.includes(s))return void e.abort();e.continue()}),await a.emulate({viewport:{width:t.width,height:t.height,deviceScaleFactor:t.scaleFactor,isMobile:t.isMobile,hasTouch:t.hasTouch,isLandscape:t.isLandscape},userAgent:e.userAgent}),debug("evaluateUrl - Page properties set!")}catch(e){debug("evaluateUrl - Error while setting page properties -> abort!"),o=e}if(!1===o)try{debug("evaluateUrl - Navigating page to "+e),await a.goto(e,{timeout:this.options.timeout,waitUntil:["networkidle2"]}),debug("evaluateUrl - Page navigated")}catch(t){debug("evaluateUrl - Error while page.goto -> "+e),o=t}if(!1===o){try{debug("evaluateUrl - Extracting critical selectors"),await a.waitFor(250),l=new Map(await a.evaluate(extractCriticalCss_script,{sourceAst:t,loadTimeout:this.options.pageLoadTimeout,keepSelectors:this.options.keepSelectors,removeSelectors:this.options.removeSelectors,dropKeyframes:this.options.dropKeyframes})),debug("evaluateUrl - Extracting critical selectors - successful! Length: "+l.size)}catch(e){debug("evaluateUrl - Error while extracting critical selectors -> not good!"),o=e}debug("evaluateUrl - cleaning up AST with criticalSelectorMap");const[e,r]=this._cssTransformator.filterByMap(t,l);c=e,n=r,debug("evaluateUrl - cleaning up AST with criticalSelectorMap - END")}if(!1===o)try{debug("evaluateUrl - Closing page ..."),await a.close(),debug("evaluateUrl - Page closed")}catch(e){o=e,debug("evaluateUrl - Error while closing page -> already closed?")}if(!1!==o){try{await this.gracefulClosePage(a,o)}catch(e){}s(o)}r([c,n])})}}module.exports=Crittr; \ No newline at end of file +"use strict";const fs=require("fs-extra"),util=require("util"),readFilePromise=util.promisify(fs.readFile),debug=require("debug")("Crittr Class"),log=require("signale"),chalk=require("chalk"),merge=require("deepmerge"),Queue=require("run-queue"),puppeteer=require("puppeteer"),devices=require("puppeteer/DeviceDescriptors"),mqpacker=require("css-mqpacker"),sortCSSmq=require("sort-css-media-queries"),DEFAULTS=require("../Constants"),Ast=require("./Ast.class"),CssTransformator=require("./CssTransformator.class"),extractCriticalCss_script=require("../evaluation/extract_critical_with_css");class Crittr{constructor(e){this.options={css:null,urls:[],timeout:DEFAULTS.TIMEOUT,pageLoadTimeout:DEFAULTS.PAGE_LOAD_TIMEOUT,outputRemainingCss:DEFAULTS.OUTPUT_REMAINING_CSS,browser:{userAgent:DEFAULTS.BROWSER_USER_AGENT,isCacheEnabled:DEFAULTS.BROWSER_CACHE_ENABLED,isJsEnabled:DEFAULTS.BROWSER_JS_ENABLED,concurrentTabs:DEFAULTS.BROWSER_CONCURRENT_TABS},device:{width:DEFAULTS.DEVICE_WIDTH,height:DEFAULTS.DEVICE_HEIGHT,scaleFactor:DEFAULTS.DEVICE_SCALE_FACTOR,isMobile:DEFAULTS.DEVICE_IS_MOBILE,hasTouch:DEFAULTS.DEVICE_HAS_TOUCH,isLandscape:DEFAULTS.DEVICE_IS_LANDSCAPE},puppeteer:{browser:null,chromePath:null,headless:DEFAULTS.PUPPETEER_HEADLESS},printBrowserConsole:DEFAULTS.PRINT_BROWSER_CONSOLE,dropKeyframes:DEFAULTS.DROP_KEYFRAMES,keepSelectors:[],removeSelectors:[],blockRequests:["maps.gstatic.com","maps.googleapis.com","googletagmanager.com","google-analytics.com","google.","googleadservices.com","generaltracking.de","bing.com","doubleclick.net"]},this.options=merge(this.options,e),this._browser=null,this._cssTransformator=new CssTransformator,"string"==typeof this.options.device&&(devices[this.options.device]?this.options.device=devices[this.options.device].viewport:log.error("Option 'device' is set as string but has an unknown value. Use devices of puppeteer (https://github.com/GoogleChrome/puppeteer/blob/master/DeviceDescriptors.js) or an object!"));const t=this.validateOptions();t.length>0&&(t.forEach(({message:e})=>{log.error(e)}),process.exit(1))}validateOptions(){const e=[];return Array.isArray(this.options.urls)||e.push({message:"Url not valid"}),"string"!=typeof this.options.css&&e.push({message:"css not valid. Expected string got "+typeof this.options.css}),e}run(){return new Promise(async(e,t)=>{debug("run - Starting run ...");let r="",s="",i=[];try{debug("run - Get css content ..."),this._cssContent=await this.getCssContent(),debug("run - Get css content done!")}catch(e){debug("run - ERROR while extracting css content"),t(e)}try{debug("run - Starting browser ..."),this._browser=await this.getBrowser(),debug("run - Browser started!")}catch(e){debug("run - ERROR: Browser could not be launched ... abort!"),t(e)}try{debug("run - Starting critical css extraction ..."),[r,s,i]=await this.getCriticalCssFromUrls(),i.length>0&&(log.warn("Some of the urls had errors. Please review them below!"),this.printErrors(i)),debug("run - Finished critical css extraction!")}catch(e){debug("run - ERROR while critical css extraction"),t(e)}try{debug("run - Browser closing ..."),await this._browser.close(),debug("run - Browser closed!")}catch(e){debug("run - ERROR: Browser could not be closed -> already closed?")}debug("run - Extraction ended!"),e({critical:r,rest:s})})}getBrowser(){return new Promise(async(e,t)=>{try{null!==this.options.puppeteer.browser&&e(this.options.puppeteer.browser),e(await puppeteer.launch({ignoreHTTPSErrors:!0,args:["--disable-setuid-sandbox","--no-sandbox","--ignore-certificate-errors","--disable-dev-shm-usage"],dumpio:!1,headless:this.options.puppeteer.headless,executablePath:this.options.puppeteer.chromePath}).then(e=>e))}catch(e){t(e)}})}gracefulClosePage(e,t){return new Promise(async(r,s)=>{this.printErrors(t);try{debug("gracefulClosePage - Closing page after error gracefully ..."),await e.close(),debug("gracefulClosePage - Page closed gracefully!")}catch(e){debug("gracefulClosePage - Error while closing page -> already closed?")}r()})}printErrors(e){if(e)if(log.warn(chalk.red("Errors occured during processing. Please have a look and report them if necessary")),Array.isArray(e))for(let t of e)log.error(t);else log.error(e)}getPage(){return this._browser.newPage()}getCssContent(){return new Promise(async(e,t)=>{if("string"==typeof this.options.css){let r="";if(this.options.css.endsWith(".css"))try{0===(r=await readFilePromise(this.options.css,"utf8")).length&&t(new Error("No CSS content in file exists -> exit!"))}catch(e){t(e)}else r=this.options.css;e(r)}else e(!1)})}getCriticalCssFromUrls(){return new Promise(async(e,t)=>{let r=[];const s=this.options.urls,i=new Set,o=new Set,a=this._cssTransformator.getAst(this._cssContent),l=new Queue({maxConcurrency:this.options.browser.concurrentTabs}),c=async(e,t,s,i)=>{try{debug("getCriticalCssFromUrls - Try to get critical ast from "+e);const[o,a]=await this.evaluateUrl(e,t);s.add(o),i.add(a),debug("getCriticalCssFromUrls - Successfully extracted critical ast!")}catch(t){debug("getCriticalCssFromUrls - ERROR getting critical ast from promise"),log.error("Could not get critical ast for url "+e),log.error(t),r.push(t)}};for(let e of s)l.add(1,c,[e,a,i,o]);l.run().then(async()=>{0===i.size&&t(r),debug("getCriticalCssFromUrls - Merging multiple atf ast objects. Size: "+i.size);let s=new Map;for(let e of i)try{s=Ast.generateRuleMap(e,s)}catch(e){debug("getCriticalCssFromUrls - ERROR merging multiple atf ast objects"),t(e)}debug("getCriticalCssFromUrls - Merging multiple atf ast objects - finished");let a=new Map;if(this.options.outputRemainingCss){debug("getCriticalCssFromUrls - Merging multiple rest ast objects. Size: "+o.size);for(let e of o)try{a=Ast.generateRuleMap(e,a)}catch(e){debug("getCriticalCssFromUrls - ERROR merging multiple rest ast objects"),t(e)}debug("getCriticalCssFromUrls - Merging multiple rest ast objects - finished"),debug("getCriticalCssFromUrls - Filter duplicates of restMap");for(const[e,t]of s)if(a.has(e)){let r=a.get(e);(r=r.filter(e=>!t.some(t=>e.hash===t.hash))).length>0?a.set(e,r):a.delete(e)}debug("getCriticalCssFromUrls - Filter duplicates of restMap - finished")}debug("getCriticalCssFromUrls - Creating AST Object of atf ruleMap");let l=Ast.getAstOfRuleMap(s),c=this._cssTransformator.getCssFromAst(l).code;c=mqpacker.pack(c,{sort:sortCSSmq}).css,debug("getCriticalCssFromUrls - Creating AST Object of atf ruleMap - Finished");let n=null,g="";this.options.outputRemainingCss&&(debug("getCriticalCssFromUrls - Creating AST Object of remaining ruleMap"),n=Ast.getAstOfRuleMap(a),debug("getCriticalCssFromUrls - Creating AST Object of remaining ruleMap - Finished"),g=this._cssTransformator.getCssFromAst(n).code,g=mqpacker.pack(g,{sort:sortCSSmq}).css),e([c,g,r])}).catch(e=>{t(e)})})}evaluateUrl(e,t){return new Promise(async(r,s)=>{let i=3,o=!1,a=null,l=new Map,c=null,n=null;const g=async()=>new Promise((e,t)=>{try{this.getPage().then(t=>{e(t)}).catch(r=>{i-- >0?(log.warn("Could not get page from browser. Retry "+i+" times."),e(g())):(log.warn("Tried to get page but failed. Abort now ..."),t(r))})}catch(e){t(e)}});try{debug("evaluateUrl - Open new Page-Tab ..."),a=await g(),!0===this.options.printBrowserConsole&&(a.on("console",e=>{const t=e.args();for(let e=0;e{log.log("Page error: "+e.toString())}),a.on("error",e=>{log.log("Error: "+e.toString())})),debug("evaluateUrl - Page-Tab opened!")}catch(e){debug("evaluateUrl - Error while opening page tab -> abort!"),o=e}if(!1===o)try{let e=this.options.browser,t=this.options.device;debug("evaluateUrl - Set page properties ..."),await a.setCacheEnabled(e.isCacheEnabled),await a.setJavaScriptEnabled(e.isJsEnabled),await a.setRequestInterception(!0);const r=this.options.blockRequests;a.on("request",e=>{const t=e.url();if(r)for(const s of r)if(t.includes(s))return void e.abort();e.continue()}),await a.emulate({viewport:{width:t.width,height:t.height,deviceScaleFactor:t.scaleFactor,isMobile:t.isMobile,hasTouch:t.hasTouch,isLandscape:t.isLandscape},userAgent:e.userAgent}),debug("evaluateUrl - Page properties set!")}catch(e){debug("evaluateUrl - Error while setting page properties -> abort!"),o=e}if(!1===o)try{debug("evaluateUrl - Navigating page to "+e),await a.goto(e,{timeout:this.options.timeout,waitUntil:["networkidle2"]}),debug("evaluateUrl - Page navigated")}catch(t){debug("evaluateUrl - Error while page.goto -> "+e),o=t}if(!1===o){try{debug("evaluateUrl - Extracting critical selectors"),await a.waitFor(250),l=new Map(await a.evaluate(extractCriticalCss_script,{sourceAst:t,loadTimeout:this.options.pageLoadTimeout,keepSelectors:this.options.keepSelectors,removeSelectors:this.options.removeSelectors,dropKeyframes:this.options.dropKeyframes})),debug("evaluateUrl - Extracting critical selectors - successful! Length: "+l.size)}catch(e){debug("evaluateUrl - Error while extracting critical selectors -> not good!"),o=e}debug("evaluateUrl - cleaning up AST with criticalSelectorMap");const[e,r]=this._cssTransformator.filterByMap(t,l);c=e,n=r,debug("evaluateUrl - cleaning up AST with criticalSelectorMap - END")}if(!1===o)try{debug("evaluateUrl - Closing page ..."),await a.close(),debug("evaluateUrl - Page closed")}catch(e){o=e,debug("evaluateUrl - Error while closing page -> already closed?")}if(!1!==o){try{await this.gracefulClosePage(a,o)}catch(e){}s(o)}r([c,n])})}}module.exports=Crittr; \ No newline at end of file diff --git a/lib/classes/CssTransformator.class.js b/lib/classes/CssTransformator.class.js index 82d5a66..4e1d8d5 100644 --- a/lib/classes/CssTransformator.class.js +++ b/lib/classes/CssTransformator.class.js @@ -1 +1 @@ -"use strict";const _=require("lodash"),debug=require("debug")("Crittr CSSTransformator"),log=require("signale"),merge=require("deepmerge"),css=require("css"),Rule=require("./Rule.class");class CssTransformator{constructor(e){e=e||{},this.options={silent:!0,source:null},this.options=merge(this.options,e);this._TYPES_TO_REMOVE=["comment","keyframes","keyframe"],this._TYPES_TO_KEEP=["charset","font-face"];const s=[":before",":after",":visited",":first-letter",":first-line"].map(e=>":?"+e).join("|");this._PSUEDO_SELECTOR_REGEXP=new RegExp(s,"g")}getAst(e){let s=null;try{debug("getAst - Try parsing css to ast ..."),s=css.parse(e,{silent:this.options.silent,source:this.options.source}),debug("getAst - Css successfully parsed to ast ...")}catch(e){log.error(e)}return s}getCssFromAst(e){return css.stringify(e,{indent:" ",compress:!1,sourcemap:!0,inputSourcemaps:!0})}filterSelector(e,s){if(!Array.isArray(s))return log.warn("removeSelectors have to be an array to be processed"),!1;let t=e;e.stylesheet?t=e.stylesheet.rules:e.rules&&(t=e.rules);const r=(e,s)=>s-e,l=[];for(const e in t)if(t.hasOwnProperty(e)){const o=t[e];if(Rule.isMediaRule(o))this.filterSelector(o,s);else{const t=o.selectors,i=[];for(let r in t)if(t.hasOwnProperty(r)){const o=t[r];s.includes(o)&&(t.length>1?i.push(r):l.push(e))}i.sort(r);for(let e of i)t.splice(e,1)}}l.sort(r);for(let e of l)t.splice(e,1);return e}filterByMap(e,s){let t=JSON.parse(JSON.stringify(e)),r=JSON.parse(JSON.stringify(e));const l=t.stylesheet,o=r.stylesheet;let i=[];const n=new Map,u=(e,s,t)=>{s=s||"";const r=Rule.generateRuleKey(e,s);if(t.has(r)){const s=t.get(r);return e.selectors.filter(e=>s.selectors.includes(e))}return[]};let c=l.rules.filter(e=>!this._TYPES_TO_REMOVE.includes(e.type));c=(c=c.map((e,t,r)=>{if("media"===e.type)e.rules&&(e.rules=e.rules.map(t=>{const r=Rule.generateRuleKey(t,e.media);return t.selectors=u(t,e.media,s),n.set(r,t.selectors),t}).filter(e=>void 0!==e.selectors&&e.selectors.length>0),0===e.rules.length&&i.push(e));else if("rule"===e.type){const t=Rule.generateRuleKey(e);e.selectors=u(e,"",s),n.set(t,e.selectors),0===e.selectors.length&&i.push(e)}return e})).filter(e=>!i.includes(e)),i=[];let a=o.rules.map(e=>{const s="media"===e.type?e.media:"";if("media"===e.type)e.rules&&(e.rules=e.rules.map(e=>{const t=Rule.generateRuleKey(e,s);if(n.has(t)){const s=n.get(t);e.selectors=e.selectors.filter(e=>!s.includes(e))}return e}).filter(e=>void 0!==e.selectors&&e.selectors.length>0),0===e.rules.length&&i.push(e));else if("rule"===e.type){const t=Rule.generateRuleKey(e,s);if(n.has(t)){const s=n.get(t);e.selectors=e.selectors.filter(e=>!s.includes(e))}0===e.selectors.length&&i.push(e)}return e});return a=a.filter(e=>!(i.includes(e)||this._TYPES_TO_KEEP.includes(e.type))),l.rules=c,o.rules=a,[t,r]}}module.exports=CssTransformator; \ No newline at end of file +"use strict";const _=require("lodash"),debug=require("debug")("Crittr CSSTransformator"),log=require("signale"),merge=require("deepmerge"),css=require("css"),Rule=require("./Rule.class");class CssTransformator{constructor(e){e=e||{},this.options={silent:!0,source:null},this.options=merge(this.options,e);this.CRITICAL_TYPES_TO_KEEP=["media","rule","charset","font-face"];const s=[":before",":after",":visited",":first-letter",":first-line"].map(e=>":?"+e).join("|");this._PSUEDO_SELECTOR_REGEXP=new RegExp(s,"g")}getAst(e){let s=null;try{debug("getAst - Try parsing css to ast ..."),s=css.parse(e,{silent:this.options.silent,source:this.options.source}),debug("getAst - Css successfully parsed to ast ...")}catch(e){log.error(e)}return s}getCssFromAst(e){return css.stringify(e,{indent:" ",compress:!1,sourcemap:!0,inputSourcemaps:!0})}filterByMap(e,s){let t=JSON.parse(JSON.stringify(e)),r=JSON.parse(JSON.stringify(e));const l=t.stylesheet,i=r.stylesheet;let u=[];const o=new Map,n=(e,s,t)=>{s=s||"";const r=Rule.generateRuleKey(e,s);if(t.has(r)){const s=t.get(r);return e.selectors.filter(e=>s.selectors.includes(e))}return[]};let c=l.rules.filter(e=>this.CRITICAL_TYPES_TO_KEEP.includes(e.type));c=(c=c.map((e,t,r)=>{if("media"===e.type)e.rules&&(e.rules=e.rules.map(t=>{const r=Rule.generateRuleKey(t,e.media);return t.selectors=n(t,e.media,s),o.set(r,t.selectors),t}).filter(e=>void 0!==e.selectors&&e.selectors.length>0),0===e.rules.length&&u.push(e));else if("rule"===e.type){const t=Rule.generateRuleKey(e);e.selectors=n(e,"",s),o.set(t,e.selectors),0===e.selectors.length&&u.push(e)}return e})).filter(e=>!u.includes(e)),u=[];let a=i.rules.map(e=>{const s="media"===e.type?e.media:"";if("media"===e.type)e.rules&&(e.rules=e.rules.map(e=>{const t=Rule.generateRuleKey(e,s);if(o.has(t)){const s=o.get(t);e.selectors=e.selectors.filter(e=>!s.includes(e))}return e}).filter(e=>void 0!==e.selectors&&e.selectors.length>0),0===e.rules.length&&u.push(e));else if("rule"===e.type){const t=Rule.generateRuleKey(e,s);if(o.has(t)){const s=o.get(t);e.selectors=e.selectors.filter(e=>!s.includes(e))}0===e.selectors.length&&u.push(e)}return e});return a=a.filter(e=>!u.includes(e)),l.rules=c,i.rules=a,[t,r]}}module.exports=CssTransformator; \ No newline at end of file diff --git a/lib/classes/Rule.class.js b/lib/classes/Rule.class.js index cdc987a..18125e8 100644 --- a/lib/classes/Rule.class.js +++ b/lib/classes/Rule.class.js @@ -1 +1 @@ -const _=require("lodash"),log=require("signale"),CONSTANTS=require("../Constants");class Rule{static isRuleDuplicate(e,t,s){return s=s||[],_.isEqualWith(e,t,(e,t,r)=>{if(s.includes(r))return!0})}static isSameRuleType(e,t){return e.type===t.type}static isMediaRule(e){return"media"===e.type}static isRule(e){return"rule"===e.type}static isKeyframe(e){return"keyframe"===e.type}static isKeyframes(e){return"keyframes"===e.type}static isCharset(e){return"charset"===e.type}static isComment(e){return"comment"===e.type}static isFontFace(e){return"font-face"===e.type}static isStylesheet(e){return"stylesheet"===e.type}static isImportantRule(e){return Rule.isMediaRule(e)||Rule.isRule(e)}static isMatchingMediaRuleSelector(e,t){return e===t||e===t.replace("all and ","")||t===e.replace("all and ","")||e.replace("all and ","")===t.replace("all and ","")}static generateRuleKey(e,t="",s=!1){const r=s?CONSTANTS.RULE_SEPARATOR:"";let i;if(Rule.isRule(e)&&e.selectors)i=e.selectors.join();else if(Rule.isCharset(e))i=e.charset;else if(Rule.isKeyframes(e))i=e.name;else if(Rule.isKeyframe(e))i=e.values.join();else if(Rule.isMediaRule(e))i=e.media;else{if(!Rule.isFontFace(e))return!Rule.isComment(e)&&(log.error("Can not generate rule key of rule without selectors! Maybe this is a media query?",e),!1);i=e.type}return t+r+i}}module.exports=Rule; \ No newline at end of file +const _=require("lodash"),log=require("signale"),CONSTANTS=require("../Constants");class Rule{static isRuleDuplicate(e,t,s){return s=s||[],_.isEqualWith(e,t,(e,t,i)=>{if(s.includes(i))return!0})}static isSameRuleType(e,t){return e.type===t.type}static isMediaRule(e){return"media"===e.type}static isRule(e){return"rule"===e.type}static isKeyframe(e){return"keyframe"===e.type}static isKeyframes(e){return"keyframes"===e.type}static isCharset(e){return"charset"===e.type}static isComment(e){return"comment"===e.type}static isFontFace(e){return"font-face"===e.type}static isStylesheet(e){return"stylesheet"===e.type}static isImportantRule(e){return Rule.isMediaRule(e)||Rule.isRule(e)}static isMatchingMediaRuleSelector(e,t){return e===t||e===t.replace("all and ","")||t===e.replace("all and ","")||e.replace("all and ","")===t.replace("all and ","")}static generateRuleKey(e,t="",s=!1){const i=s?CONSTANTS.RULE_SEPARATOR:"";let a="default";if(Rule.isRule(e)&&e.selectors)a=e.selectors.join();else if(Rule.isCharset(e))a=e.charset;else if(Rule.isKeyframes(e))a=e.name;else if(Rule.isKeyframe(e))a=e.values.join();else if(Rule.isMediaRule(e))a=e.media;else{if(!Rule.isFontFace(e))return!Rule.isComment(e)&&a;a=e.type}return t+i+a}}module.exports=Rule; \ No newline at end of file diff --git a/package.json b/package.json index 88f94c8..e11e233 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ ], "husky": { "hooks": { - "pre-commit": "npm test" + "pre-commit": "npm run build && git add lib/* && npm test" } } } diff --git a/src/classes/Crittr.class.js b/src/classes/Crittr.class.js index b25e475..be49cd5 100644 --- a/src/classes/Crittr.class.js +++ b/src/classes/Crittr.class.js @@ -417,7 +417,7 @@ class Crittr { // Handle sorting by option finalCss = mqpacker.pack(finalCss, { sort: sortCSSmq - }); + }).css; debug("getCriticalCssFromUrls - Creating AST Object of atf ruleMap - Finished"); // Handle restCSS @@ -432,7 +432,7 @@ class Crittr { finalRestCss = mqpacker.pack(finalRestCss, { sort: sortCSSmq - }); + }).css; } resolve([finalCss, finalRestCss, errors]); diff --git a/src/classes/CssTransformator.class.js b/src/classes/CssTransformator.class.js index b4bb652..af96edd 100644 --- a/src/classes/CssTransformator.class.js +++ b/src/classes/CssTransformator.class.js @@ -28,14 +28,11 @@ class CssTransformator { ':first-line' ]; - this._TYPES_TO_REMOVE = [ - "comment", - "keyframes", - "keyframe" - ]; - this._TYPES_TO_KEEP = [ + this.CRITICAL_TYPES_TO_KEEP = [ + "media", + "rule", "charset", - "font-face" + "font-face", ]; // detect these selectors regardless of whether one or two semicolons are used @@ -71,84 +68,6 @@ class CssTransformator { }) } - /** - * Remove all selectors that match one of the removeSelectors. - * Mutates the original Object - * - * @param ast {Object} - * @param removeSelectors {Array} - * @returns {Object} - */ - filterSelector(ast, removeSelectors) { - if (!Array.isArray(removeSelectors)) { - log.warn("removeSelectors have to be an array to be processed"); - return false; - } - - let rules = ast; - - // Get Rules of ast object and keep reference - if (ast.stylesheet) { - rules = ast.stylesheet.rules; - } else if (ast.rules) { - rules = ast.rules; - } - - const compareFn = (a, b) => { - return b - a; - }; - - const removeableRules = []; - - for (const ruleIndex in rules) { - if (rules.hasOwnProperty(ruleIndex)) { - const rule = rules[ruleIndex]; - - if (Rule.isMediaRule(rule)) { - // Recursive check of CSSMediaRule - this.filterSelector(rule, removeSelectors); - } else { - // CSSRule - const selectors = rule.selectors; - const removeableSelectors = []; - - for (let selectorIndex in selectors) { - if (selectors.hasOwnProperty(selectorIndex)) { - const selector = selectors[selectorIndex]; - - // TODO: deal with wildcards - if (removeSelectors.includes(selector)) { - // More than one selector in there. Only remove the match and keep the other one. - // If only one selector exists remove the whole rule - if (selectors.length > 1) { - removeableSelectors.push(selectorIndex); - } else { - removeableRules.push(ruleIndex); - } - } - } - } - - // Sort the removeableSelectors DESC to remove them properly from the selectors end to start - removeableSelectors.sort(compareFn); - // Now remove them - for (let selectorIndex of removeableSelectors) { - selectors.splice(selectorIndex, 1); - } - } - } - } - - // Sort the removeableRules DESC to remove them properly from the rules end to start - removeableRules.sort(compareFn); - // Now remove them - for (let ruleIndex of removeableRules) { - rules.splice(ruleIndex, 1); - } - - return ast; - } - /** * Filters the AST Object with the selectorMap containing selectors. * Returns a new AST Object without those selectors. Does NOT mutate the AST. @@ -177,9 +96,9 @@ class CssTransformator { return []; }; - // Filter rule types we don't want + // Filter rule types we don't want in critical let newRules = _astRoot.rules.filter(rule => { - return !this._TYPES_TO_REMOVE.includes(rule.type); + return this.CRITICAL_TYPES_TO_KEEP.includes(rule.type); }); // HANDLE CRITICAL CSS @@ -261,7 +180,7 @@ class CssTransformator { // Process removeables restRules = restRules.filter(rule => { - return !(removeableRules.includes(rule) || this._TYPES_TO_KEEP.includes(rule.type)); + return !(removeableRules.includes(rule)); }); _astRoot.rules = newRules; diff --git a/src/classes/Rule.class.js b/src/classes/Rule.class.js index 4b6f655..1f021d0 100644 --- a/src/classes/Rule.class.js +++ b/src/classes/Rule.class.js @@ -103,7 +103,7 @@ class Rule { static generateRuleKey(rule, media = "", withKeySeparator = false) { const keySeparator = withKeySeparator ? CONSTANTS.RULE_SEPARATOR : ""; - let ruleStr; + let ruleStr = "default"; if (Rule.isRule(rule) && rule.selectors) { ruleStr = rule.selectors.join(); @@ -120,8 +120,8 @@ class Rule { } else if (Rule.isComment(rule)) { return false; } else { - log.error("Can not generate rule key of rule without selectors! Maybe this is a media query?", rule); - return false; + //log.error("Can not generate rule key of rule without selectors! Setting default key!", rule); + return ruleStr; } return media + keySeparator + ruleStr; diff --git a/test/basis.test.js b/test/basis.test.js index a9c9794..31ff662 100644 --- a/test/basis.test.js +++ b/test/basis.test.js @@ -47,6 +47,13 @@ describe('Basis Test', () => { console.warn("Unkown rule type -> not recognized: ", rule.type); } } + } else { + if (criticalSelectorRules.has(rule.type)) { + let count = criticalSelectorRules.get(rule.type); + criticalSelectorRules.set(rule.type, ++count); + } else { + criticalSelectorRules.set(rule.type, 1); + } } } @@ -270,5 +277,10 @@ describe('Basis Test', () => { expect(exists).toBeTruthy(); }); + + test("Font-Face should be in critical css", () => { + const exists = criticalSelectorRules.has("font-face"); + expect(exists).toBeTruthy(); + }); }); }); \ No newline at end of file diff --git a/test/data/test.css b/test/data/test.css index d35b76f..9b8a36f 100644 --- a/test/data/test.css +++ b/test/data/test.css @@ -1,5 +1,9 @@ @charset "UTF-8"; +@font-face { + font-family: Keep Critical; +} + .standard-selector { color: black; }