-
-
Notifications
You must be signed in to change notification settings - Fork 141
/
analyzeCss.js
201 lines (172 loc) · 8.89 KB
/
analyzeCss.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/**
* 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);
}
function analyzeCss(css, context, callback) {
/**
// 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 = {};
new analyzer(css, options, function(err, results) {
var offenderSrc = context || '[inline CSS]';
if (err !== null) {
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);
callback();
return;
}
phantomas.log('Got results for %s from %s', offenderSrc, results.generator);
var metrics = results.metrics || {},
offenders = results.offenders || {};
Object.keys(metrics).forEach(function(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(function(msg) {
phantomas.addOffender(metricPrefixed, {url: offenderSrc, value: msg});
});
}
// 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;
}
}
});
callback();
});
}
// 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', () => {
var promises = [];
stylesheets.forEach(entry => {
promises.push(
new Promise(async resolve => {
var css = entry.inline;
phantomas.log('Analyzing %s', entry.url || 'inline CSS');
if (entry.content) {
css = await entry.content();
}
analyzeCss(css, entry.url, resolve);
})
);
});
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
return Promise.all(promises);
});
};