UNPKG

phantomas

Version:

Headless Chromium-based web performance metrics collector and monitoring tool

222 lines (193 loc) 9.73 kB
/** * Adds CSS complexity metrics using analyze-css npm module. * * Run phantomas with --analyze-css option to use this module * * setMetric('cssBase64Length') @desc total length of base64-encoded data in CSS source (will warn about base64-encoded data bigger than 4 kB) @optional @offenders * setMetric('cssRedundantBodySelectors') @desc number of redundant body selectors (e.g. body .foo, section body h2, but not body > h1) @optional @offenders * setMetric('redundantChildNodesSelectors') @desc number of redundant child nodes selectors @optional @offenders * setMetric('cssColors') @desc number of unique colors used in CSS @optional @offenders * setMetric('cssComments') @desc number of comments in CSS source @optional @offenders * setMetric('cssCommentsLength') @desc length of comments content in CSS source @optional * setMetric('cssComplexSelectorsByAttribute') @desc [number] number of selectors with complex matching by attribute (e.g. [class$="foo"]) @optional @offenders * setMetric('cssDuplicatedSelectors') @desc number of CSS selectors defined more than once in CSS source @optional @offenders * setMetric('cssDuplicatedProperties') @desc number of CSS property definitions duplicated within a selector @optional @offenders * setMetric('cssEmptyRules') @desc number of rules with no properties (e.g. .foo { }) @optional @offenders * setMetric('cssExpressions') @desc number of rules with CSS expressions (e.g. expression( document.body.clientWidth > 600 ? "600px" : "auto" )) @optional @offenders * setMetric('cssOldIEFixes') @desc number of fixes for old versions of Internet Explorer (e.g. * html .foo {} and .foo { *zoom: 1 }) @optional @offenders * setMetric('cssImports') @desc number of @import rules @optional @offenders * setMetric('cssImportants') @desc number of properties with value forced by !important @optional @offenders * setMetric('cssMediaQueries') @desc number of media queries (e.g. @media screen and (min-width: 1370px)) @optional @offenders * setMetric('cssMultiClassesSelectors') @desc number of selectors with multiple classes (e.g. span.foo.bar) @optional @offenders * setMetric('cssOldPropertyPrefixes') @desc number of properties with no longer needed vendor prefix, powered by data provided by autoprefixer (e.g. --moz-border-radius) @optional @offenders * setMetric('cssQualifiedSelectors') @desc number of qualified selectors (e.g. header#nav, .foo#bar, h1.title) @optional @offenders * setMetric('cssSpecificityIdAvg') @desc average specificity for ID @optional @offenders * setMetric('cssSpecificityIdTotal') @desc total specificity for ID @optional * setMetric('cssSpecificityClassAvg') @desc average specificity for class, pseudo-class or attribute @optional @offenders * setMetric('cssSpecificityClassTotal') @desc total specificity for class, pseudo-class or attribute @optional * setMetric('cssSpecificityTagAvg') @desc average specificity for element @optional @offenders * setMetric('cssSpecificityTagTotal') @desc total specificity for element @optional * setMetric('cssSelectorsByAttribute') @desc [number] number of selectors by attribute (e.g. .foo[value=bar]) @optional * setMetric('cssSelectorsByClass') @desc number of selectors by class @optional * setMetric('cssSelectorsById') @desc number of selectors by ID @optional * setMetric('cssSelectorsByPseudo') @desc number of pseudo-selectors (e,g. :hover) @optional * setMetric('cssSelectorsByTag') @desc number of selectors by tag name @optional * setMetric('cssLength') @desc length of CSS source (in bytes) @optional @offenders * setMetric('cssRules') @desc number of rules (e.g. .foo, .bar { color: red } is counted as one rule) @optional @offenders * setMetric('cssSelectors') @desc number of selectors (e.g. .foo, .bar { color: red } is counted as two selectors - .foo and .bar) @optional @offenders * setMetric('cssDeclarations') @desc number of declarations (e.g. .foo, .bar { color: red } is counted as one declaration - color: red) @optional @offenders * setMetric('cssNotMinified') @desc [number] set to 1 if the provided CSS is not minified @optional @offenders * setMetric('cssSelectorLengthAvg') @desc [number] average length of selector (e.g. for ``.foo .bar, #test div > span { color: red }`` will be set as 2.5) @optional @offenders */ "use strict"; module.exports = function (phantomas) { if (phantomas.getParam("analyze-css") !== true) { phantomas.log( "To enable CSS in-depth metrics please run phantomas with --analyze-css option" ); return; } // load analyze-css module // https://www.npmjs.com/package/analyze-css const analyzer = require("analyze-css"); phantomas.log("Using version %s", analyzer.version); phantomas.setMetric("cssParsingErrors"); // @desc number of CSS files (or embeded CSS) that failed to be parse by analyze-css @optional phantomas.setMetric("cssInlineStyles"); // @desc number of inline styles @optional function ucfirst(str) { // http://kevin.vanzonneveld.net // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) // + bugfixed by: Onno Marsman // + improved by: Brett Zamir (http://brett-zamir.me) // * example 1: ucfirst('kevin van zonneveld'); // * returns 1: 'Kevin van zonneveld' str += ""; var f = str.charAt(0).toUpperCase(); return f + str.substr(1); } async function analyzeCss(css, context) { /** // force JSON output format options.push('--json'); // set basic auth if needed if (phantomas.getParam('auth-user') && phantomas.getParam('auth-pass')) { options.push('--auth-user', phantomas.getParam('auth-user')); options.push('--auth-pass', phantomas.getParam('auth-pass')); } // HTTP proxy (#500) var proxy = phantomas.getParam('proxy', false, 'string'); if (proxy !== false) { if (proxy.indexOf('http:') < 0) { proxy = 'http://' + proxy; // http-proxy-agent (used by analyze-css) expects a protocol as well } options.push('--proxy', proxy); } **/ // https://www.npmjs.com/package/analyze-css#commonjs-module var options = {}; let results; try { results = await analyzer(css, options); } catch (err) { phantomas.log("analyzeCss: sub-process failed! - %s", err); // report failed CSS parsing (issue #494( var offender = offenderSrc; if (err.message) { // Error object returned if (err.message.indexOf("Unable to parse JSON string") > 0) { offender += " (analyzeCss output error)"; } } else { // Error string returned (stderror) if ( err.indexOf("CSS parsing failed") > 0 || err.indexOf("is an invalid expression") > 0 ) { offender += " (" + err.trim() + ")"; } else if (err.indexOf("Empty CSS was provided") > 0) { offender += " (Empty CSS was provided)"; } } phantomas.incrMetric("cssParsingErrors"); phantomas.addOffender("cssParsingErrors", offender); return; } var offenderSrc = context || "[inline CSS]"; phantomas.log("Got results for %s from %s", offenderSrc, results.generator); const metrics = results.metrics, offenders = results.offenders; Object.keys(metrics).forEach((metric) => { var metricPrefixed = "css" + ucfirst(metric); if (/Avg$/.test(metricPrefixed)) { // update the average value (see #641) phantomas.addToAvgMetric(metricPrefixed, metrics[metric]); } else { // increase metrics phantomas.incrMetric(metricPrefixed, metrics[metric]); } // and add offenders if (typeof offenders[metric] !== "undefined") { offenders[metric].forEach((offender) => { phantomas.addOffender(metricPrefixed, { url: offenderSrc, value: { message: offender.message, position: { ...offender.position, source: offender.source || "undefined", }, // cast to object }, }); }); } // add more offenders (#578) else { switch (metricPrefixed) { case "cssLength": case "cssRules": case "cssSelectors": case "cssDeclarations": case "cssNotMinified": case "cssSelectorLengthAvg": case "cssSpecificityIdAvg": case "cssSpecificityClassAvg": case "cssSpecificityTagAvg": phantomas.addOffender(metricPrefixed, { url: offenderSrc, value: metrics[metric], }); break; } } }); } // prepare a list of CSS stylesheets (both external and inline) var stylesheets = []; phantomas.on("recv", async (entry, res) => { if (entry.isCSS) { // defer getting the response content and pass it to the analyze-css module stylesheets.push({ content: res.getContent, url: entry.url }); } }); phantomas.on("inlinecss", (css) => stylesheets.push({ inline: css })); // ok, now let's analyze the collect CSS phantomas.on("beforeClose", () => { const promises = []; stylesheets.forEach((entry) => { promises.push( new Promise(async (resolve) => { let css = entry.inline; phantomas.log("Analyzing %s", entry.url || "inline CSS"); if (entry.content) { css = await entry.content(); } if (css) { await analyzeCss(css, entry.url); } resolve(); }) ); }); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise return Promise.all(promises); }); };