analyze-css
Advanced tools
Comparing version 0.12.12 to 0.12.13
@@ -8,24 +8,36 @@ #!/usr/bin/env node | ||
*/ | ||
'use strict'; | ||
"use strict"; | ||
const { program } = require('commander'); | ||
const { program } = require("commander"); | ||
var analyzer = require('./../lib/index'), | ||
debug = require('debug')('analyze-css:bin'), | ||
runner = require('./../lib/runner'), | ||
runnerOpts = {}; | ||
var analyzer = require("./../lib/index"), | ||
debug = require("debug")("analyze-css:bin"), | ||
runner = require("./../lib/runner"), | ||
runnerOpts = {}; | ||
// parse options | ||
program | ||
.version(analyzer.version) | ||
.usage('--url <url> [options]') | ||
// https://www.npmjs.com/package/commander#common-option-types-boolean-and-value | ||
.option('--url <url>', 'Set URL of CSS to analyze') | ||
.option('--file <file>', 'Set local CSS file to analyze') | ||
.option('--ignore-ssl-errors', 'Ignores SSL errors, such as expired or self-signed certificate errors') | ||
.option('-p, --pretty', 'Causes JSON with the results to be pretty-printed') | ||
.option('-N, --no-offenders', 'Show only the metrics without the offenders part') | ||
.option('--auth-user <user>', 'Sets the user name used for HTTP authentication') | ||
.option('--auth-pass <pass>', 'Sets the password used for HTTP authentication') | ||
.option('-x, --proxy <proxy>', 'Sets the HTTP proxy'); | ||
.version(analyzer.version) | ||
.usage("--url <url> [options]") | ||
// https://www.npmjs.com/package/commander#common-option-types-boolean-and-value | ||
.option("--url <url>", "Set URL of CSS to analyze") | ||
.option("--file <file>", "Set local CSS file to analyze") | ||
.option( | ||
"--ignore-ssl-errors", | ||
"Ignores SSL errors, such as expired or self-signed certificate errors" | ||
) | ||
.option("-p, --pretty", "Causes JSON with the results to be pretty-printed") | ||
.option( | ||
"-N, --no-offenders", | ||
"Show only the metrics without the offenders part" | ||
) | ||
.option( | ||
"--auth-user <user>", | ||
"Sets the user name used for HTTP authentication" | ||
) | ||
.option( | ||
"--auth-pass <pass>", | ||
"Sets the password used for HTTP authentication" | ||
) | ||
.option("-x, --proxy <proxy>", "Sets the HTTP proxy"); | ||
@@ -35,61 +47,68 @@ // parse it | ||
debug('analyze-css v%s', analyzer.version); | ||
debug("analyze-css v%s", analyzer.version); | ||
// support stdin (issue #28) | ||
if (process.argv.indexOf('-') > -1) { | ||
runnerOpts.stdin = true; | ||
if (process.argv.indexOf("-") > -1) { | ||
runnerOpts.stdin = true; | ||
} | ||
// --url | ||
else if (program.url) { | ||
runnerOpts.url = program.url; | ||
runnerOpts.url = program.url; | ||
} | ||
// --file | ||
else if (program.file) { | ||
runnerOpts.file = program.file; | ||
runnerOpts.file = program.file; | ||
} | ||
// either --url or --file or - (stdin) needs to be provided | ||
else { | ||
console.log(program.helpInformation()); | ||
process.exit(analyzer.EXIT_NEED_OPTIONS); | ||
console.log(program.helpInformation()); | ||
process.exitCode = analyzer.EXIT_NEED_OPTIONS; | ||
return; | ||
} | ||
runnerOpts.ignoreSslErrors = program['ignore-ssl-errors']; | ||
runnerOpts.ignoreSslErrors = program["ignore-ssl-errors"]; | ||
runnerOpts.noOffenders = program.offenders === false; | ||
runnerOpts.authUser = program['auth-user']; | ||
runnerOpts.authPass = program['auth-pass']; | ||
runnerOpts.authUser = program["auth-user"]; | ||
runnerOpts.authPass = program["auth-pass"]; | ||
runnerOpts.proxy = program.proxy; | ||
debug('opts: %j', runnerOpts); | ||
debug("opts: %j", runnerOpts); | ||
// run the analyzer | ||
runner(runnerOpts, function(err, res) { | ||
var output, exitCode; | ||
runner(runnerOpts, function (err, res) { | ||
var output, exitCode; | ||
// emit an error and die | ||
if (err) { | ||
exitCode = err.code || 1; | ||
debug('Exiting with exit code #%d', exitCode); | ||
// emit an error and die | ||
if (err) { | ||
exitCode = err.code || 1; | ||
debug("Exiting with exit code #%d", exitCode); | ||
console.error(err.toString()); | ||
process.exit(exitCode); | ||
} | ||
console.error(err.toString()); | ||
process.exitCode = exitCode; | ||
return; | ||
} | ||
// make offenders flat (and append position if possible - issue #25) | ||
if (typeof res.offenders !== 'undefined') { | ||
Object.keys(res.offenders).forEach(function(metricName) { | ||
res.offenders[metricName] = res.offenders[metricName].map(function(offender) { | ||
var position = offender.position && offender.position.start; | ||
return offender.message + (position ? ' @ ' + position.line + ':' + position.column : ''); | ||
}); | ||
}); | ||
} | ||
// make offenders flat (and append position if possible - issue #25) | ||
if (typeof res.offenders !== "undefined") { | ||
Object.keys(res.offenders).forEach(function (metricName) { | ||
res.offenders[metricName] = res.offenders[metricName].map(function ( | ||
offender | ||
) { | ||
var position = offender.position && offender.position.start; | ||
return ( | ||
offender.message + | ||
(position ? " @ " + position.line + ":" + position.column : "") | ||
); | ||
}); | ||
}); | ||
} | ||
// format the results | ||
if (program.pretty === true) { | ||
output = JSON.stringify(res, null, ' '); | ||
} else { | ||
output = JSON.stringify(res); | ||
} | ||
// format the results | ||
if (program.pretty === true) { | ||
output = JSON.stringify(res, null, " "); | ||
} else { | ||
output = JSON.stringify(res); | ||
} | ||
process.stdout.write(output); | ||
process.stdout.write(output); | ||
}); |
/** | ||
* Push items and count them | ||
*/ | ||
'use strict'; | ||
"use strict"; | ||
function collection() { | ||
this.items = {}; | ||
this.items = {}; | ||
} | ||
collection.prototype = { | ||
push: function(item) { | ||
if (typeof this.items[item] === 'undefined') { | ||
this.items[item] = { | ||
cnt: 1 | ||
}; | ||
} else { | ||
this.items[item].cnt++; | ||
} | ||
}, | ||
push: function (item) { | ||
if (typeof this.items[item] === "undefined") { | ||
this.items[item] = { | ||
cnt: 1, | ||
}; | ||
} else { | ||
this.items[item].cnt++; | ||
} | ||
}, | ||
sort: function() { | ||
var newItems = {}, | ||
sortedKeys; | ||
sort: function () { | ||
var newItems = {}, | ||
sortedKeys; | ||
// sort in descending order (by cnt) | ||
sortedKeys = Object.keys(this.items).sort((function(a, b) { | ||
return this.items[b].cnt - this.items[a].cnt; | ||
}).bind(this)); | ||
// sort in descending order (by cnt) | ||
sortedKeys = Object.keys(this.items).sort( | ||
function (a, b) { | ||
return this.items[b].cnt - this.items[a].cnt; | ||
}.bind(this) | ||
); | ||
// build new items dictionary | ||
sortedKeys.forEach(function(key) { | ||
newItems[key] = this.items[key]; | ||
}, this); | ||
// build new items dictionary | ||
sortedKeys.forEach(function (key) { | ||
newItems[key] = this.items[key]; | ||
}, this); | ||
this.items = newItems; | ||
return this; | ||
}, | ||
this.items = newItems; | ||
return this; | ||
}, | ||
forEach: function(callback) { | ||
Object.keys(this.items).forEach(function(key) { | ||
callback(key, this.items[key].cnt); | ||
}, this); | ||
} | ||
forEach: function (callback) { | ||
Object.keys(this.items).forEach(function (key) { | ||
callback(key, this.items[key].cnt); | ||
}, this); | ||
}, | ||
}; | ||
module.exports = collection; |
526
lib/index.js
/** | ||
* analyze-css CommonJS module | ||
*/ | ||
'use strict'; | ||
"use strict"; | ||
var cssParser = require('css').parse, | ||
debug = require('debug')('analyze-css'), | ||
fs = require('fs'), | ||
path = require('path'), | ||
preprocessors = new(require('./preprocessors'))(), | ||
slickParse = require('slick').parse, | ||
VERSION = require('./../package').version; | ||
var cssParser = require("css").parse, | ||
debug = require("debug")("analyze-css"), | ||
fs = require("fs"), | ||
path = require("path"), | ||
preprocessors = new (require("./preprocessors"))(), | ||
slickParse = require("slick").parse, | ||
VERSION = require("./../package").version; | ||
function analyzer(css, options, callback) { | ||
var res; | ||
var res; | ||
// options can be omitted | ||
if (typeof options === 'function') { | ||
callback = options; | ||
options = {}; | ||
} | ||
// options can be omitted | ||
if (typeof options === "function") { | ||
callback = options; | ||
options = {}; | ||
} | ||
this.options = options; | ||
debug('opts: %j', this.options); | ||
this.options = options; | ||
debug("opts: %j", this.options); | ||
if (typeof css !== 'string') { | ||
callback(this.error('css parameter passed is not a string!', analyzer.EXIT_CSS_PASSED_IS_NOT_STRING), null); | ||
return; | ||
} | ||
if (typeof css !== "string") { | ||
callback( | ||
this.error( | ||
"css parameter passed is not a string!", | ||
analyzer.EXIT_CSS_PASSED_IS_NOT_STRING | ||
), | ||
null | ||
); | ||
return; | ||
} | ||
// preprocess the CSS (issue #3) | ||
if (typeof options.preprocessor === 'string') { | ||
debug('Using "%s" preprocessor', options.preprocessor); | ||
// preprocess the CSS (issue #3) | ||
if (typeof options.preprocessor === "string") { | ||
debug('Using "%s" preprocessor', options.preprocessor); | ||
var preprocessor = preprocessors.get(options.preprocessor); | ||
var preprocessor = preprocessors.get(options.preprocessor); | ||
try { | ||
css = preprocessor.process(css, options); | ||
} catch (ex) { | ||
throw new Error('Preprocessing failed: ' + ex); | ||
} | ||
try { | ||
css = preprocessor.process(css, options); | ||
} catch (ex) { | ||
throw new Error("Preprocessing failed: " + ex); | ||
} | ||
debug('Preprocessing completed'); | ||
} | ||
debug("Preprocessing completed"); | ||
} | ||
res = this.analyze(css); | ||
res = this.analyze(css); | ||
// error handling | ||
if (res !== true) { | ||
callback(res, null); | ||
return; | ||
} | ||
// error handling | ||
if (res !== true) { | ||
callback(res, null); | ||
return; | ||
} | ||
// return the results | ||
res = { | ||
generator: 'analyze-css v' + VERSION, | ||
metrics: this.metrics, | ||
}; | ||
// return the results | ||
res = { | ||
generator: "analyze-css v" + VERSION, | ||
metrics: this.metrics, | ||
}; | ||
// disable offenders output if requested (issue #64) | ||
if (options.noOffenders !== true) { | ||
res.offenders = this.offenders; | ||
} | ||
// disable offenders output if requested (issue #64) | ||
if (options.noOffenders !== true) { | ||
res.offenders = this.offenders; | ||
} | ||
callback(null, res); | ||
callback(null, res); | ||
} | ||
@@ -71,4 +77,4 @@ | ||
// @see https://github.com/macbre/phantomas/issues/664 | ||
analyzer.path = path.normalize(__dirname + '/..'); | ||
analyzer.pathBin = analyzer.path + '/bin/analyze-css.js'; | ||
analyzer.path = path.normalize(__dirname + "/.."); | ||
analyzer.pathBin = analyzer.path + "/bin/analyze-css.js"; | ||
@@ -84,262 +90,280 @@ // exit codes | ||
analyzer.prototype = { | ||
emitter: false, | ||
tree: false, | ||
emitter: false, | ||
tree: false, | ||
metrics: {}, | ||
offenders: {}, | ||
metrics: {}, | ||
offenders: {}, | ||
error: function(msg, code) { | ||
var err = new Error(msg); | ||
err.code = code; | ||
error: function (msg, code) { | ||
var err = new Error(msg); | ||
err.code = code; | ||
return err; | ||
}, | ||
return err; | ||
}, | ||
// emit given event | ||
emit: function( /* eventName, arg1, arg2, ... */ ) { | ||
//debug('Event %s emitted', arguments[0]); | ||
this.emitter.emit.apply(this.emitter, arguments); | ||
}, | ||
// emit given event | ||
emit: function (/* eventName, arg1, arg2, ... */) { | ||
//debug('Event %s emitted', arguments[0]); | ||
this.emitter.emit.apply(this.emitter, arguments); | ||
}, | ||
// bind to a given event | ||
on: function(ev, fn) { | ||
this.emitter.on(ev, fn); | ||
}, | ||
// bind to a given event | ||
on: function (ev, fn) { | ||
this.emitter.on(ev, fn); | ||
}, | ||
setMetric: function(name, value) { | ||
value = value || 0; | ||
setMetric: function (name, value) { | ||
value = value || 0; | ||
//debug('setMetric(%s) = %d', name, value); | ||
this.metrics[name] = value; | ||
}, | ||
//debug('setMetric(%s) = %d', name, value); | ||
this.metrics[name] = value; | ||
}, | ||
// increements given metric by given number (default is one) | ||
incrMetric: function(name, incr /* =1 */ ) { | ||
var currVal = this.metrics[name] || 0; | ||
incr = incr || 1; | ||
// increements given metric by given number (default is one) | ||
incrMetric: function (name, incr /* =1 */) { | ||
var currVal = this.metrics[name] || 0; | ||
incr = incr || 1; | ||
//debug('incrMetric(%s) += %d', name, incr); | ||
this.setMetric(name, currVal + incr); | ||
}, | ||
//debug('incrMetric(%s) += %d', name, incr); | ||
this.setMetric(name, currVal + incr); | ||
}, | ||
addOffender: function(metricName, msg, position /* = undefined */ ) { | ||
if (typeof this.offenders[metricName] === 'undefined') { | ||
this.offenders[metricName] = []; | ||
} | ||
addOffender: function (metricName, msg, position /* = undefined */) { | ||
if (typeof this.offenders[metricName] === "undefined") { | ||
this.offenders[metricName] = []; | ||
} | ||
this.offenders[metricName].push({ | ||
'message': msg, | ||
'position': position || this.currentPosition | ||
}); | ||
}, | ||
this.offenders[metricName].push({ | ||
message: msg, | ||
position: position || this.currentPosition, | ||
}); | ||
}, | ||
setCurrentPosition: function(position) { | ||
this.currentPosition = position; | ||
}, | ||
setCurrentPosition: function (position) { | ||
this.currentPosition = position; | ||
}, | ||
initRules: function() { | ||
var debug = require('debug')('analyze-css:rules'), | ||
re = /\.js$/, | ||
rules = []; | ||
initRules: function () { | ||
var debug = require("debug")("analyze-css:rules"), | ||
re = /\.js$/, | ||
rules = []; | ||
// init events emitter | ||
this.emitter = new(require('events').EventEmitter)(); | ||
this.emitter.setMaxListeners(200); | ||
// init events emitter | ||
this.emitter = new (require("events").EventEmitter)(); | ||
this.emitter.setMaxListeners(200); | ||
// load all rules | ||
rules = fs.readdirSync(fs.realpathSync(__dirname + '/../rules/')) | ||
// filter out all non *.js files | ||
.filter(function(file) { | ||
return re.test(file); | ||
}) | ||
// remove file extensions to get just names | ||
.map(function(file) { | ||
return file.replace(re, ''); | ||
}); | ||
// load all rules | ||
rules = fs | ||
.readdirSync(fs.realpathSync(__dirname + "/../rules/")) | ||
// filter out all non *.js files | ||
.filter(function (file) { | ||
return re.test(file); | ||
}) | ||
// remove file extensions to get just names | ||
.map(function (file) { | ||
return file.replace(re, ""); | ||
}); | ||
debug('Rules to be loaded: %s', rules.join(', ')); | ||
debug("Rules to be loaded: %s", rules.join(", ")); | ||
rules.forEach(function(name) { | ||
var rule = require('./../rules/' + name); | ||
rule(this); | ||
rules.forEach(function (name) { | ||
var rule = require("./../rules/" + name); | ||
rule(this); | ||
debug('"%s" loaded: %s', name, rule.description || '-'); | ||
}, this); | ||
}, | ||
debug('"%s" loaded: %s', name, rule.description || "-"); | ||
}, this); | ||
}, | ||
parseCss: function(css) { | ||
var debug = require('debug')('analyze-css:parser'); | ||
debug('Going to parse %s kB of CSS', (css.length / 1024).toFixed(2)); | ||
parseCss: function (css) { | ||
var debug = require("debug")("analyze-css:parser"); | ||
debug("Going to parse %s kB of CSS", (css.length / 1024).toFixed(2)); | ||
if (css.trim() === '') { | ||
return this.error('Empty CSS was provided', analyzer.EXIT_EMPTY_CSS); | ||
} | ||
if (css.trim() === "") { | ||
return this.error("Empty CSS was provided", analyzer.EXIT_EMPTY_CSS); | ||
} | ||
this.tree = cssParser(css, { | ||
// errors are listed in the parsingErrors property instead of being thrown (#84) | ||
silent: true | ||
}); | ||
this.tree = cssParser(css, { | ||
// errors are listed in the parsingErrors property instead of being thrown (#84) | ||
silent: true, | ||
}); | ||
debug('CSS parsed'); | ||
return true; | ||
}, | ||
debug("CSS parsed"); | ||
return true; | ||
}, | ||
parseRules: function(rules) { | ||
rules.forEach(function(rule, idx) { | ||
debug('%j', rule); | ||
parseRules: function (rules) { | ||
rules.forEach(function (rule) { | ||
debug("%j", rule); | ||
// store the default current position | ||
// | ||
// it will be used when this.addOffender is called from within the rule | ||
// it can be overridden by providing a "custom" position via a call to this.setCurrentPosition | ||
this.setCurrentPosition(rule.position); | ||
// store the default current position | ||
// | ||
// it will be used when this.addOffender is called from within the rule | ||
// it can be overridden by providing a "custom" position via a call to this.setCurrentPosition | ||
this.setCurrentPosition(rule.position); | ||
switch (rule.type) { | ||
// { | ||
// "type":"media" | ||
// "media":"screen and (min-width: 1370px)", | ||
// "rules":[{"type":"rule","selectors":["#foo"],"declarations":[]}] | ||
// } | ||
case 'media': | ||
this.emit('media', rule.media, rule.rules); | ||
switch (rule.type) { | ||
// { | ||
// "type":"media" | ||
// "media":"screen and (min-width: 1370px)", | ||
// "rules":[{"type":"rule","selectors":["#foo"],"declarations":[]}] | ||
// } | ||
case "media": | ||
this.emit("media", rule.media, rule.rules); | ||
// now run recursively to parse rules within the media query | ||
if (rule.rules) { | ||
this.parseRules(rule.rules); | ||
} | ||
// now run recursively to parse rules within the media query | ||
if (rule.rules) { | ||
this.parseRules(rule.rules); | ||
} | ||
this.emit('mediaEnd', rule.media, rule.rules); | ||
break; | ||
this.emit("mediaEnd", rule.media, rule.rules); | ||
break; | ||
// { | ||
// "type":"rule", | ||
// "selectors":[".ui-header .ui-btn-up-a",".ui-header .ui-btn-hover-a"], | ||
// "declarations":[{"type":"declaration","property":"border","value":"0"},{"type":"declaration","property":"background","value":"none"}] | ||
// } | ||
case 'rule': | ||
if (!rule.selectors || !rule.declarations) { | ||
return; | ||
} | ||
// { | ||
// "type":"rule", | ||
// "selectors":[".ui-header .ui-btn-up-a",".ui-header .ui-btn-hover-a"], | ||
// "declarations":[{"type":"declaration","property":"border","value":"0"},{"type":"declaration","property":"background","value":"none"}] | ||
// } | ||
case "rule": | ||
if (!rule.selectors || !rule.declarations) { | ||
return; | ||
} | ||
this.emit('rule', rule); | ||
this.emit("rule", rule); | ||
// analyze each selector and declaration | ||
rule.selectors.forEach(function(selector) { | ||
var parsedSelector, | ||
expressions = [], | ||
i, len; | ||
// analyze each selector and declaration | ||
rule.selectors.forEach(function (selector) { | ||
var parsedSelector, | ||
expressions = [], | ||
i, | ||
len; | ||
// "#features > div:first-child" will become two expressions: | ||
// {"combinator":" ","tag":"*","id":"features"} | ||
// {"combinator":">","tag":"div","pseudos":[{"key":"first-child","value":null}]} | ||
parsedSelector = slickParse(selector)[0]; | ||
// "#features > div:first-child" will become two expressions: | ||
// {"combinator":" ","tag":"*","id":"features"} | ||
// {"combinator":">","tag":"div","pseudos":[{"key":"first-child","value":null}]} | ||
parsedSelector = slickParse(selector)[0]; | ||
if (typeof parsedSelector === 'undefined') { | ||
var positionDump = "Rule position start @ " + rule.position.start.line + ':' + rule.position.start.column + ", end @ " + rule.position.end.line + ':' + rule.position.end.column; | ||
throw this.error('Unable to parse "' + selector + '" selector. ' + positionDump, analyzer.EXIT_PARSING_FAILED); | ||
} | ||
if (typeof parsedSelector === "undefined") { | ||
var positionDump = | ||
"Rule position start @ " + | ||
rule.position.start.line + | ||
":" + | ||
rule.position.start.column + | ||
", end @ " + | ||
rule.position.end.line + | ||
":" + | ||
rule.position.end.column; | ||
throw this.error( | ||
'Unable to parse "' + selector + '" selector. ' + positionDump, | ||
analyzer.EXIT_PARSING_FAILED | ||
); | ||
} | ||
// convert object with keys to array with numeric index | ||
for (i = 0, len = parsedSelector.length; i < len; i++) { | ||
expressions.push(parsedSelector[i]); | ||
} | ||
// convert object with keys to array with numeric index | ||
for (i = 0, len = parsedSelector.length; i < len; i++) { | ||
expressions.push(parsedSelector[i]); | ||
} | ||
this.emit('selector', rule, selector, expressions); | ||
this.emit("selector", rule, selector, expressions); | ||
expressions.forEach(function(expression) { | ||
this.emit('expression', selector, expression); | ||
}, this); | ||
}, this); | ||
expressions.forEach(function (expression) { | ||
this.emit("expression", selector, expression); | ||
}, this); | ||
}, this); | ||
rule.declarations.forEach(function(declaration) { | ||
this.setCurrentPosition(declaration.position); | ||
rule.declarations.forEach(function (declaration) { | ||
this.setCurrentPosition(declaration.position); | ||
switch (declaration.type) { | ||
case 'declaration': | ||
this.emit('declaration', rule, declaration.property, declaration.value); | ||
break; | ||
switch (declaration.type) { | ||
case "declaration": | ||
this.emit( | ||
"declaration", | ||
rule, | ||
declaration.property, | ||
declaration.value | ||
); | ||
break; | ||
case 'comment': | ||
this.emit('comment', declaration.comment); | ||
break; | ||
} | ||
}, this); | ||
break; | ||
case "comment": | ||
this.emit("comment", declaration.comment); | ||
break; | ||
} | ||
}, this); | ||
break; | ||
// {"type":"comment","comment":" Cached as static-css-r518-9b0f5ab4632defb55d67a1d672aa31bd120f4414 "} | ||
case 'comment': | ||
this.emit('comment', rule.comment); | ||
break; | ||
// {"type":"comment","comment":" Cached as static-css-r518-9b0f5ab4632defb55d67a1d672aa31bd120f4414 "} | ||
case "comment": | ||
this.emit("comment", rule.comment); | ||
break; | ||
// {"type":"font-face","declarations":[{"type":"declaration","property":"font-family","value":"myFont"... | ||
case 'font-face': | ||
this.emit('font-face', rule); | ||
break; | ||
// {"type":"font-face","declarations":[{"type":"declaration","property":"font-family","value":"myFont"... | ||
case "font-face": | ||
this.emit("font-face", rule); | ||
break; | ||
// {"type":"import","import":"url('/css/styles.css')"} | ||
case 'import': | ||
this.emit('import', rule.import); | ||
break; | ||
} | ||
}, this); | ||
}, | ||
// {"type":"import","import":"url('/css/styles.css')"} | ||
case "import": | ||
this.emit("import", rule.import); | ||
break; | ||
} | ||
}, this); | ||
}, | ||
run: function() { | ||
var stylesheet = this.tree && this.tree.stylesheet, | ||
rules = stylesheet && stylesheet.rules; | ||
run: function () { | ||
var stylesheet = this.tree && this.tree.stylesheet, | ||
rules = stylesheet && stylesheet.rules; | ||
this.emit('stylesheet', stylesheet); | ||
this.emit("stylesheet", stylesheet); | ||
// check for parsing errors (#84) | ||
stylesheet.parsingErrors.forEach(function(err) { | ||
debug('error: %j', err); | ||
// check for parsing errors (#84) | ||
stylesheet.parsingErrors.forEach(function (err) { | ||
debug("error: %j", err); | ||
var pos = { | ||
line: err.line, | ||
column: err.column | ||
}; | ||
this.setCurrentPosition({ | ||
start: pos, | ||
end: pos | ||
}); | ||
var pos = { | ||
line: err.line, | ||
column: err.column, | ||
}; | ||
this.setCurrentPosition({ | ||
start: pos, | ||
end: pos, | ||
}); | ||
this.emit('error', err); | ||
}, this); | ||
this.emit("error", err); | ||
}, this); | ||
this.parseRules(rules); | ||
}, | ||
this.parseRules(rules); | ||
}, | ||
analyze: function(css) { | ||
var res, | ||
then = Date.now(); | ||
analyze: function (css) { | ||
var res, | ||
then = Date.now(); | ||
this.metrics = {}; | ||
this.offenders = {}; | ||
this.metrics = {}; | ||
this.offenders = {}; | ||
// load and init all rules | ||
this.initRules(); | ||
// load and init all rules | ||
this.initRules(); | ||
// parse CSS | ||
res = this.parseCss(css); | ||
// parse CSS | ||
res = this.parseCss(css); | ||
if (res !== true) { | ||
return res; | ||
} | ||
if (res !== true) { | ||
return res; | ||
} | ||
this.emit('css', css); | ||
this.emit("css", css); | ||
// now go through parsed CSS tree and emit events for rules | ||
try { | ||
this.run(); | ||
} catch (ex) { | ||
return ex; | ||
} | ||
// now go through parsed CSS tree and emit events for rules | ||
try { | ||
this.run(); | ||
} catch (ex) { | ||
return ex; | ||
} | ||
this.emit('report'); | ||
this.emit("report"); | ||
debug('Completed in %d ms', Date.now() - then); | ||
return true; | ||
} | ||
debug("Completed in %d ms", Date.now() - then); | ||
return true; | ||
}, | ||
}; | ||
module.exports = analyzer; |
/** | ||
* A wrapper for preprocessors | ||
*/ | ||
'use strict'; | ||
"use strict"; | ||
var debug = require('debug')('analyze-css:preprocessors'), | ||
glob = require('glob'); | ||
var debug = require("debug")("analyze-css:preprocessors"), | ||
glob = require("glob"); | ||
var preprocessors = function() { | ||
var preprocessors = function () {}; | ||
}; | ||
preprocessors.prototype = { | ||
get: function (name) { | ||
return require(__dirname + "/preprocessors/" + name + ".js"); | ||
}, | ||
get: function(name) { | ||
return require(__dirname + '/preprocessors/' + name + '.js'); | ||
}, | ||
// return instances of all available preprocessors | ||
getAll: function () { | ||
var files, | ||
res = []; | ||
// return instances of all available preprocessors | ||
getAll: function() { | ||
var files, | ||
res = []; | ||
files = glob.sync(__dirname + "/preprocessors/*.js"); | ||
debug("Initializing..."); | ||
files = glob.sync(__dirname + '/preprocessors/*.js'); | ||
debug('Initializing...'); | ||
if (Array.isArray(files)) { | ||
files.forEach(function (file) { | ||
res.push(require(file)); | ||
}); | ||
} | ||
if (Array.isArray(files)) { | ||
files.forEach(function(file) { | ||
res.push(require(file)); | ||
}); | ||
} | ||
return res; | ||
}, | ||
return res; | ||
}, | ||
// get name of matching preprocessor | ||
findMatchingByFileName: function (fileName) { | ||
var matching = false; | ||
// get name of matching preprocessor | ||
findMatchingByFileName: function(fileName) { | ||
var matching = false; | ||
this.getAll().forEach(function (preprocessor) { | ||
if (preprocessor.matchesFileName(fileName)) { | ||
matching = preprocessor.name; | ||
this.getAll().forEach(function(preprocessor) { | ||
if (preprocessor.matchesFileName(fileName)) { | ||
matching = preprocessor.name; | ||
debug('%s matches "%s" preprocessor', fileName, matching); | ||
return false; | ||
} | ||
}); | ||
debug('%s matches "%s" preprocessor', fileName, matching); | ||
return false; | ||
} | ||
}); | ||
return matching; | ||
} | ||
return matching; | ||
}, | ||
}; | ||
module.exports = preprocessors; |
@@ -6,54 +6,51 @@ /** | ||
*/ | ||
'use strict'; | ||
"use strict"; | ||
var debug = require('debug')('analyze-css:preprocessors:sass'); | ||
var debug = require("debug")("analyze-css:preprocessors:sass"); | ||
module.exports = { | ||
name: "sass", | ||
matchesFileName: function (fileName) { | ||
return /\.(scss|sass)$/.test(fileName); | ||
}, | ||
process: function (css, options) { | ||
var path = require("path"), | ||
sass, | ||
out; | ||
name: 'sass', | ||
matchesFileName: function(fileName) { | ||
return /\.(scss|sass)$/.test(fileName); | ||
}, | ||
process: function(css, options) { | ||
var path = require('path'), | ||
sass, | ||
out; | ||
// check the presense of the optional "node-sass" module (#118) | ||
try { | ||
sass = require("node-sass"); | ||
} catch (e) { | ||
throw new Error( | ||
"Can't process SASS/SCSS, please run 'npm install node-sass'" | ||
); | ||
} | ||
// check the presense of the optional "node-sass" module (#118) | ||
try { | ||
sass = require('node-sass'); | ||
} catch (e) { | ||
throw new Error("Can't process SASS/SCSS, please run 'npm install node-sass'"); | ||
} | ||
var includeDir = options.file ? path.dirname(options.file) : undefined; | ||
debug('Using "%s" include path', includeDir); | ||
var includeDir = options.file ? path.dirname(options.file) : undefined; | ||
debug('Using "%s" include path', includeDir); | ||
try { | ||
// 1: try to parse using SCSS syntax (i.e. with brackets) | ||
debug("Parsing using SCSS syntax"); | ||
try { | ||
// 1: try to parse using SCSS syntax (i.e. with brackets) | ||
debug('Parsing using SCSS syntax'); | ||
out = sass.renderSync({ | ||
data: css, | ||
indentedSyntax: false, | ||
includePaths: [includeDir], | ||
}); | ||
} catch (e) { | ||
// 2: try to parse using SASS syntax (i.e. with indends) - issue #79 | ||
debug("Exception: %s", e.toString().trim()); | ||
debug("Parsing using SASS syntax as a fallback"); | ||
out = sass.renderSync({ | ||
data: css, | ||
indentedSyntax: false, | ||
includePaths: [ | ||
includeDir | ||
] | ||
}); | ||
} catch (e) { | ||
// 2: try to parse using SASS syntax (i.e. with indends) - issue #79 | ||
debug('Exception: %s', e.toString().trim()); | ||
debug('Parsing using SASS syntax as a fallback'); | ||
out = sass.renderSync({ | ||
data: css, | ||
indentedSyntax: true, | ||
includePaths: [includeDir], | ||
}); | ||
} | ||
out = sass.renderSync({ | ||
data: css, | ||
indentedSyntax: true, | ||
includePaths: [ | ||
includeDir | ||
] | ||
}); | ||
} | ||
return out.css.toString(); | ||
} | ||
return out.css.toString(); | ||
}, | ||
}; |
@@ -6,10 +6,10 @@ /** | ||
*/ | ||
'use strict'; | ||
"use strict"; | ||
var cli = require('cli'), | ||
debug = require('debug')('analyze-css:runner'), | ||
fs = require('fs'), | ||
resolve = require('path').resolve, | ||
analyzer = require('./index'), | ||
preprocessors = new(require('./preprocessors'))(); | ||
var cli = require("cli"), | ||
debug = require("debug")("analyze-css:runner"), | ||
fs = require("fs"), | ||
resolve = require("path").resolve, | ||
analyzer = require("./index"), | ||
preprocessors = new (require("./preprocessors"))(); | ||
@@ -20,13 +20,13 @@ /** | ||
function getUserAgent() { | ||
var format = require('util').format, | ||
version = require('../package').version; | ||
var format = require("util").format, | ||
version = require("../package").version; | ||
return format( | ||
'analyze-css/%s (%s %s, %s %s)', | ||
version, | ||
process.release.name, | ||
process.version, | ||
process.platform, | ||
process.arch | ||
); | ||
return format( | ||
"analyze-css/%s (%s %s, %s %s)", | ||
version, | ||
process.release.name, | ||
process.version, | ||
process.platform, | ||
process.arch | ||
); | ||
} | ||
@@ -40,28 +40,33 @@ | ||
function request(requestOptions, callback) { | ||
var debug = require('debug')('analyze-css:http'), | ||
fetch = require('node-fetch'); | ||
var debug = require("debug")("analyze-css:http"), | ||
fetch = require("node-fetch"); | ||
debug('GET %s', requestOptions.url); | ||
debug('Options: %j', requestOptions); | ||
debug("GET %s", requestOptions.url); | ||
debug("Options: %j", requestOptions); | ||
fetch(requestOptions.url, requestOptions). | ||
then(function(resp) { | ||
debug('HTTP %d %s', resp.status, resp.statusText); | ||
debug('Headers: %j', resp.headers._headers); | ||
fetch(requestOptions.url, requestOptions) | ||
.then(function (resp) { | ||
debug("HTTP %d %s", resp.status, resp.statusText); | ||
debug("Headers: %j", resp.headers._headers); | ||
if (!resp.ok) { | ||
var err = new Error('HTTP request failed: ' + (err ? err.toString() : 'received HTTP ' + resp.status + ' ' + resp.statusText)); | ||
callback(err); | ||
} else { | ||
return resp.text(); // a promise | ||
} | ||
}). | ||
then(function(body) { | ||
debug('Received %d bytes of CSS', body.length); | ||
callback(null, body); | ||
}). | ||
catch(function(err) { | ||
debug(err); | ||
callback(err); | ||
}); | ||
if (!resp.ok) { | ||
var err = new Error( | ||
"HTTP request failed: " + | ||
(err | ||
? err.toString() | ||
: "received HTTP " + resp.status + " " + resp.statusText) | ||
); | ||
callback(err); | ||
} else { | ||
return resp.text(); // a promise | ||
} | ||
}) | ||
.then(function (body) { | ||
debug("Received %d bytes of CSS", body.length); | ||
callback(null, body); | ||
}) | ||
.catch(function (err) { | ||
debug(err); | ||
callback(err); | ||
}); | ||
} | ||
@@ -73,92 +78,102 @@ | ||
function runner(options, callback) { | ||
// call CommonJS module | ||
var analyzerOpts = { | ||
'noOffenders': options.noOffenders, | ||
'preprocessor': false, | ||
}; | ||
// call CommonJS module | ||
var analyzerOpts = { | ||
noOffenders: options.noOffenders, | ||
preprocessor: false, | ||
}; | ||
function analyze(css) { | ||
new analyzer(css, analyzerOpts, callback); | ||
} | ||
function analyze(css) { | ||
new analyzer(css, analyzerOpts, callback); | ||
} | ||
if (options.url) { | ||
debug('Fetching remote CSS file: %s', options.url); | ||
if (options.url) { | ||
debug("Fetching remote CSS file: %s", options.url); | ||
// @see https://www.npmjs.com/package/node-fetch#options | ||
var agentOptions = {}, | ||
requestOptions = { | ||
url: options.url, | ||
headers: { | ||
'User-Agent': getUserAgent() | ||
} | ||
}; | ||
// @see https://www.npmjs.com/package/node-fetch#options | ||
var agentOptions = {}, | ||
requestOptions = { | ||
url: options.url, | ||
headers: { | ||
"User-Agent": getUserAgent(), | ||
}, | ||
}; | ||
// handle options | ||
// handle options | ||
// @see https://github.com/bitinn/node-fetch/issues/15 | ||
// @see https://nodejs.org/api/https.html#https_https_request_options_callback | ||
if (options.ignoreSslErrors) { | ||
agentOptions.rejectUnauthorized = false; | ||
} | ||
// @see https://github.com/bitinn/node-fetch/issues/15 | ||
// @see https://nodejs.org/api/https.html#https_https_request_options_callback | ||
if (options.ignoreSslErrors) { | ||
agentOptions.rejectUnauthorized = false; | ||
} | ||
// @see https://gist.github.com/cojohn/1772154 | ||
if (options.authUser && options.authPass) { | ||
requestOptions.headers.Authorization = "Basic " + new Buffer(options.authUser + ":" + options.authPass, "utf8").toString("base64"); | ||
} | ||
// @see https://gist.github.com/cojohn/1772154 | ||
if (options.authUser && options.authPass) { | ||
requestOptions.headers.Authorization = | ||
"Basic " + | ||
Buffer.from(options.authUser + ":" + options.authPass, "utf8").toString( | ||
"base64" | ||
); | ||
} | ||
// @see https://nodejs.org/api/http.html#http_class_http_agent | ||
var client = require(/^https:/.test(options.url) ? 'https' : 'http'); | ||
requestOptions.agent = new client.Agent(agentOptions); | ||
// @see https://nodejs.org/api/http.html#http_class_http_agent | ||
var client = require(/^https:/.test(options.url) ? "https" : "http"); | ||
requestOptions.agent = new client.Agent(agentOptions); | ||
// @see http://stackoverflow.com/a/5810547 | ||
options.proxy = options.proxy || process.env.HTTP_PROXY; | ||
// @see http://stackoverflow.com/a/5810547 | ||
options.proxy = options.proxy || process.env.HTTP_PROXY; | ||
if (options.proxy) { | ||
debug('Using HTTP proxy: %s', options.proxy); | ||
if (options.proxy) { | ||
debug("Using HTTP proxy: %s", options.proxy); | ||
requestOptions.agent = new(require('http-proxy-agent'))(options.proxy); | ||
} | ||
requestOptions.agent = new (require("http-proxy-agent"))(options.proxy); | ||
} | ||
request(requestOptions, function(err, css) { | ||
if (err) { | ||
err.code = analyzer.EXIT_URL_LOADING_FAILED; | ||
request(requestOptions, function (err, css) { | ||
if (err) { | ||
err.code = analyzer.EXIT_URL_LOADING_FAILED; | ||
debug(err); | ||
callback(err); | ||
} else { | ||
analyze(css); | ||
} | ||
}); | ||
} else if (options.file) { | ||
// resolve to the full path | ||
options.file = resolve(process.cwd(), options.file); | ||
debug('Loading local CSS file: %s', options.file); | ||
debug(err); | ||
callback(err); | ||
} else { | ||
analyze(css); | ||
} | ||
}); | ||
} else if (options.file) { | ||
// resolve to the full path | ||
options.file = resolve(process.cwd(), options.file); | ||
debug("Loading local CSS file: %s", options.file); | ||
fs.readFile(options.file, { | ||
encoding: 'utf-8' | ||
}, function(err, css) { | ||
if (err) { | ||
err = new Error('Loading CSS file failed: ' + err.toString()); | ||
err.code = analyzer.EXIT_FILE_LOADING_FAILED; | ||
fs.readFile( | ||
options.file, | ||
{ | ||
encoding: "utf-8", | ||
}, | ||
function (err, css) { | ||
if (err) { | ||
err = new Error("Loading CSS file failed: " + err.toString()); | ||
err.code = analyzer.EXIT_FILE_LOADING_FAILED; | ||
debug(err); | ||
callback(err); | ||
} else { | ||
// find the matching preprocessor and use it | ||
if (analyzerOpts.preprocessor === false) { | ||
analyzerOpts.preprocessor = preprocessors.findMatchingByFileName(options.file); | ||
} | ||
debug(err); | ||
callback(err); | ||
} else { | ||
// find the matching preprocessor and use it | ||
if (analyzerOpts.preprocessor === false) { | ||
analyzerOpts.preprocessor = preprocessors.findMatchingByFileName( | ||
options.file | ||
); | ||
} | ||
// pass the name of the file being analyzed | ||
analyzerOpts.file = options.file; | ||
// pass the name of the file being analyzed | ||
analyzerOpts.file = options.file; | ||
analyze(css); | ||
} | ||
}); | ||
} else if (options.stdin) { | ||
debug('Reading from stdin'); | ||
cli.withStdin(analyze); | ||
} | ||
analyze(css); | ||
} | ||
} | ||
); | ||
} else if (options.stdin) { | ||
debug("Reading from stdin"); | ||
cli.withStdin(analyze); | ||
} | ||
} | ||
module.exports = runner; |
{ | ||
"name": "analyze-css", | ||
"version": "0.12.12", | ||
"version": "0.12.13", | ||
"author": "Maciej Brencz <maciej.brencz@gmail.com> (https://github.com/macbre)", | ||
@@ -24,7 +24,7 @@ "description": "CSS selectors complexity and performance analyzer", | ||
"cli": "^1.0.1", | ||
"commander": "^6.0.0", | ||
"commander": "^7.0.0", | ||
"css": "^3.0.0", | ||
"css-shorthand-properties": "^1.1.1", | ||
"debug": "^4.1.1", | ||
"fast-stats": "0.0.5", | ||
"fast-stats": "0.0.6", | ||
"glob": "^7.1.6", | ||
@@ -38,17 +38,23 @@ "http-proxy-agent": "^4.0.1", | ||
"devDependencies": { | ||
"autoprefixer-core": "^6.0.1", | ||
"autoprefixer": "^10.2.4", | ||
"browserslist": "^4.11.1", | ||
"js-beautify": "^1.11.0", | ||
"jshint": "^2.11.0", | ||
"eslint": "^7.12.1", | ||
"eslint-config-prettier": "8.0.0", | ||
"eslint-plugin-node": "^11.1.0", | ||
"mocha": "^8.0.1", | ||
"nyc": "^15.1.0" | ||
"nyc": "^15.1.0", | ||
"postcss": "^8.2.6", | ||
"prettier": "2.2.1" | ||
}, | ||
"optionalDependencies": { | ||
"node-sass": "5.0.0" | ||
}, | ||
"bin": "./bin/analyze-css.js", | ||
"preferGlobal": true, | ||
"scripts": { | ||
"test": "mocha -R spec && npm run lint", | ||
"lint": "jshint --verbose bin/ lib/ rules/ test/", | ||
"beautify": "js-beautify -r rules/*.js test/*.js test/rules/*.js bin/*.js lib/*.js lib/preprocessors/*.js", | ||
"test": "mocha -R spec", | ||
"lint": "eslint .", | ||
"prettier": "npx prettier --write .", | ||
"coverage": "nyc mocha -R spec", | ||
"prefixes": "DEBUG=* node data/prefixes.js" | ||
"prefixes": "npx browserslist@latest --update-db; DEBUG=* node data/prefixes.js" | ||
}, | ||
@@ -55,0 +61,0 @@ "jshintConfig": { |
@@ -16,5 +16,5 @@ analyze-css | ||
```sh | ||
$ npm install --global analyze-css | ||
``` | ||
npm install --global analyze-css | ||
``` | ||
@@ -27,7 +27,7 @@ ## Usage | ||
``` | ||
analyze-css --file examples/elecena.css | ||
```sh | ||
$ analyze-css --file examples/elecena.css | ||
analyze-css --url http://s3.macbre.net/analyze-css/propertyResets.css | ||
analyze-css --url https://s3.macbre.net/analyze-css/propertyResets.css --ignore-ssl-errors | ||
$ analyze-css --url http://s3.macbre.net/analyze-css/propertyResets.css | ||
$ analyze-css --url https://s3.macbre.net/analyze-css/propertyResets.css --ignore-ssl-errors | ||
``` | ||
@@ -37,5 +37,5 @@ | ||
```sh | ||
$ echo ".foo {margin: 0 \!important}" | analyze-css - | ||
``` | ||
echo ".foo {margin: 0 \!important}" | analyze-css - | ||
``` | ||
@@ -71,3 +71,3 @@ This will emit JSON formatted results on ``stdout``. Use ``--pretty`` (or ``-p`` shortcut) option to make the output readable for human beings. | ||
console.log(results); // example? see below | ||
});``` | ||
}); | ||
``` | ||
@@ -79,5 +79,5 @@ | ||
```sh | ||
$ npm i grunt-contrib-analyze-css | ||
``` | ||
npm i grunt-contrib-analyze-css | ||
``` | ||
@@ -192,10 +192,14 @@ It uses configurable threshold and compares the analyze-css result with it. | ||
```sh | ||
$ npm test && npm run-script lint | ||
``` | ||
npm test && npm run-script lint | ||
``` | ||
Turning on debug mode (i.e. verbose logging to stderr via [debug module](https://npmjs.org/package/debug)): | ||
```sh | ||
$ DEBUG=analyze-css* analyze-css ... | ||
``` | ||
DEBUG=analyze-css* analyze-css ... | ||
``` | ||
## Stargazers over time | ||
[![Stargazers over time](https://starchart.cc/macbre/analyze-css.svg)](https://starchart.cc/macbre/analyze-css) |
@@ -1,32 +0,39 @@ | ||
'use strict'; | ||
"use strict"; | ||
var format = require('util').format, | ||
MAX_LENGTH = 4 * 1024; | ||
var format = require("util").format, | ||
MAX_LENGTH = 4 * 1024; | ||
function rule(analyzer) { | ||
// @see http://stackoverflow.com/a/11335500 | ||
var re = /data:.+\/(.+);base64,(.*)\)/; | ||
analyzer.setMetric('base64Length'); | ||
// @see http://stackoverflow.com/a/11335500 | ||
var re = /data:.+\/(.+);base64,(.*)\)/; | ||
analyzer.setMetric("base64Length"); | ||
analyzer.on('declaration', function(rule, property, value) { | ||
var base64, | ||
buf, | ||
matches; | ||
analyzer.on("declaration", function (rule, property, value) { | ||
var base64, buf, matches; | ||
if (re.test(value)) { | ||
// parse data URI | ||
matches = value.match(re); | ||
base64 = matches[2]; | ||
buf = new Buffer(base64, 'base64'); | ||
if (re.test(value)) { | ||
// parse data URI | ||
matches = value.match(re); | ||
base64 = matches[2]; | ||
buf = Buffer.from(base64, "base64"); | ||
analyzer.incrMetric('base64Length', base64.length); | ||
analyzer.incrMetric("base64Length", base64.length); | ||
if (base64.length > MAX_LENGTH) { | ||
analyzer.addOffender('base64Length', format('%s { %s: ... } // base64: %s kB, raw: %s kB', rule.selectors.join(', '), property, (base64.length / 1024).toFixed(2), (buf.length / 1024).toFixed(2))); | ||
} | ||
} | ||
}); | ||
if (base64.length > MAX_LENGTH) { | ||
analyzer.addOffender( | ||
"base64Length", | ||
format( | ||
"%s { %s: ... } // base64: %s kB, raw: %s kB", | ||
rule.selectors.join(", "), | ||
property, | ||
(base64.length / 1024).toFixed(2), | ||
(buf.length / 1024).toFixed(2) | ||
) | ||
); | ||
} | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports on base64-encoded images'; | ||
rule.description = "Reports on base64-encoded images"; | ||
module.exports = rule; |
@@ -1,59 +0,63 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
analyzer.setMetric('redundantBodySelectors'); | ||
analyzer.setMetric("redundantBodySelectors"); | ||
analyzer.on('selector', function(rule, selector, expressions) { | ||
var noExpressions = expressions.length; | ||
analyzer.on("selector", function (rule, selector, expressions) { | ||
var noExpressions = expressions.length; | ||
// check more complex selectors only | ||
if (noExpressions < 2) { | ||
return; | ||
} | ||
// check more complex selectors only | ||
if (noExpressions < 2) { | ||
return; | ||
} | ||
var firstTag = expressions[0].tag, | ||
firstHasClass = !!expressions[0].classList, | ||
isDescendantCombinator = (expressions[1].combinator === '>'), | ||
isShortExpression = (noExpressions === 2), | ||
isRedundant = true; // always expect the worst ;) | ||
var firstTag = expressions[0].tag, | ||
firstHasClass = !!expressions[0].classList, | ||
isDescendantCombinator = expressions[1].combinator === ">", | ||
isShortExpression = noExpressions === 2, | ||
isRedundant = true; // always expect the worst ;) | ||
// first, let's find the body tag selector in the expression | ||
var bodyIndex = expressions. | ||
map(function(item) { | ||
return item.tag; | ||
}). | ||
indexOf('body'); | ||
// first, let's find the body tag selector in the expression | ||
var bodyIndex = expressions | ||
.map(function (item) { | ||
return item.tag; | ||
}) | ||
.indexOf("body"); | ||
// body selector not found - skip the rules that follow | ||
if (bodyIndex < 0) { | ||
return; | ||
} | ||
// body selector not found - skip the rules that follow | ||
if (bodyIndex < 0) { | ||
return; | ||
} | ||
// matches "html > body" | ||
// matches "html.modal-popup-mode body" (issue #44) | ||
if ((firstTag === 'html') && (bodyIndex === 1) && (isDescendantCombinator || isShortExpression)) { | ||
isRedundant = false; | ||
} | ||
// matches "body > .bar" (issue #82) | ||
else if ((bodyIndex === 0) && isDescendantCombinator) { | ||
isRedundant = false; | ||
} | ||
// matches "body.foo ul li a" | ||
else if ((bodyIndex === 0) && firstHasClass) { | ||
isRedundant = false; | ||
} | ||
// matches ".has-modal > body" (issue #49) | ||
else if (firstHasClass && (bodyIndex === 1) && isDescendantCombinator) { | ||
isRedundant = false; | ||
} | ||
// matches "html > body" | ||
// matches "html.modal-popup-mode body" (issue #44) | ||
if ( | ||
firstTag === "html" && | ||
bodyIndex === 1 && | ||
(isDescendantCombinator || isShortExpression) | ||
) { | ||
isRedundant = false; | ||
} | ||
// matches "body > .bar" (issue #82) | ||
else if (bodyIndex === 0 && isDescendantCombinator) { | ||
isRedundant = false; | ||
} | ||
// matches "body.foo ul li a" | ||
else if (bodyIndex === 0 && firstHasClass) { | ||
isRedundant = false; | ||
} | ||
// matches ".has-modal > body" (issue #49) | ||
else if (firstHasClass && bodyIndex === 1 && isDescendantCombinator) { | ||
isRedundant = false; | ||
} | ||
// report he redundant body selector | ||
if (isRedundant) { | ||
analyzer.incrMetric('redundantBodySelectors'); | ||
analyzer.addOffender('redundantBodySelectors', selector); | ||
} | ||
}); | ||
// report he redundant body selector | ||
if (isRedundant) { | ||
analyzer.incrMetric("redundantBodySelectors"); | ||
analyzer.addOffender("redundantBodySelectors", selector); | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports redundant body selectors'; | ||
rule.description = "Reports redundant body selectors"; | ||
module.exports = rule; |
@@ -1,75 +0,75 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
// definition of redundant child nodes selectors (see #51 for the initial idea): | ||
// ul li | ||
// ol li | ||
// table tr | ||
// table th | ||
var redundantChildSelectors = { | ||
'ul': ['li'], | ||
'ol': ['li'], | ||
'select': ['option'], | ||
'table': ['tr', 'th'], // e.g. table can not be followed by any of tr / th | ||
'tr': ['td', 'th'], | ||
}; | ||
// definition of redundant child nodes selectors (see #51 for the initial idea): | ||
// ul li | ||
// ol li | ||
// table tr | ||
// table th | ||
var redundantChildSelectors = { | ||
ul: ["li"], | ||
ol: ["li"], | ||
select: ["option"], | ||
table: ["tr", "th"], // e.g. table can not be followed by any of tr / th | ||
tr: ["td", "th"], | ||
}; | ||
analyzer.setMetric('redundantChildNodesSelectors'); | ||
analyzer.setMetric("redundantChildNodesSelectors"); | ||
analyzer.on('selector', function(rule, selector, expressions) { | ||
var noExpressions = expressions.length; | ||
analyzer.on("selector", function (rule, selector, expressions) { | ||
var noExpressions = expressions.length; | ||
// check more complex selectors only | ||
if (noExpressions < 2) { | ||
return; | ||
} | ||
// check more complex selectors only | ||
if (noExpressions < 2) { | ||
return; | ||
} | ||
// converts "ul#foo > li.test" selector into ['ul', 'li'] list | ||
var selectorNodeNames = expressions.map(function(item) { | ||
return item.tag; | ||
}); | ||
// converts "ul#foo > li.test" selector into ['ul', 'li'] list | ||
var selectorNodeNames = expressions.map(function (item) { | ||
return item.tag; | ||
}); | ||
Object.keys(redundantChildSelectors).forEach(function(nodeName) { | ||
var nodeIndex = selectorNodeNames.indexOf(nodeName), | ||
nextNode, | ||
curExpression, | ||
combinator, | ||
redundantNodes = redundantChildSelectors[nodeName]; | ||
Object.keys(redundantChildSelectors).forEach(function (nodeName) { | ||
var nodeIndex = selectorNodeNames.indexOf(nodeName), | ||
nextNode, | ||
curExpression, | ||
combinator, | ||
redundantNodes = redundantChildSelectors[nodeName]; | ||
if ((nodeIndex > -1) && (nodeIndex < noExpressions - 1)) { | ||
// skip cases like the following: "article > ul li" | ||
if (expressions[nodeIndex].combinator !== ' ') { | ||
return; | ||
} | ||
if (nodeIndex > -1 && nodeIndex < noExpressions - 1) { | ||
// skip cases like the following: "article > ul li" | ||
if (expressions[nodeIndex].combinator !== " ") { | ||
return; | ||
} | ||
// we've found the possible offender, get the next node in the selector | ||
// and compare it against rules in redundantChildSelectors | ||
nextNode = selectorNodeNames[nodeIndex + 1]; | ||
// we've found the possible offender, get the next node in the selector | ||
// and compare it against rules in redundantChildSelectors | ||
nextNode = selectorNodeNames[nodeIndex + 1]; | ||
if (redundantNodes.indexOf(nextNode) > -1) { | ||
// skip selectors that match: | ||
// - by attributes - foo[class*=bar] | ||
// - by pseudo attributes - foo:lang(fo) | ||
curExpression = expressions[nodeIndex]; | ||
if (redundantNodes.indexOf(nextNode) > -1) { | ||
// skip selectors that match: | ||
// - by attributes - foo[class*=bar] | ||
// - by pseudo attributes - foo:lang(fo) | ||
curExpression = expressions[nodeIndex]; | ||
if (curExpression.pseudos || curExpression.attributes) { | ||
return; | ||
} | ||
if (curExpression.pseudos || curExpression.attributes) { | ||
return; | ||
} | ||
// only the following combinator can match: | ||
// ul li | ||
// ul > li | ||
combinator = expressions[nodeIndex + 1].combinator; | ||
// only the following combinator can match: | ||
// ul li | ||
// ul > li | ||
combinator = expressions[nodeIndex + 1].combinator; | ||
if ((combinator === ' ') || (combinator === '>')) { | ||
analyzer.incrMetric('redundantChildNodesSelectors'); | ||
analyzer.addOffender('redundantChildNodesSelectors', selector); | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
if (combinator === " " || combinator === ">") { | ||
analyzer.incrMetric("redundantChildNodesSelectors"); | ||
analyzer.addOffender("redundantChildNodesSelectors", selector); | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
} | ||
rule.description = 'Reports redundant child nodes selectors'; | ||
rule.description = "Reports redundant child nodes selectors"; | ||
module.exports = rule; |
@@ -1,7 +0,7 @@ | ||
'use strict'; | ||
"use strict"; | ||
var collection = require('../lib/collection'), | ||
debug = require('debug')('analyze-css:colors'), | ||
format = require('util').format, | ||
onecolor = require('onecolor'); | ||
var collection = require("../lib/collection"), | ||
debug = require("debug")("analyze-css:colors"), | ||
format = require("util").format, | ||
onecolor = require("onecolor"); | ||
@@ -11,54 +11,54 @@ /** | ||
*/ | ||
var regex = /(((rgba?|hsl)\([^\)]+\))|#(\w{3,6}))/g; | ||
var regex = /(((rgba?|hsl)\([^)]+\))|#(\w{3,6}))/g; | ||
function extractColors(value) { | ||
var matches = value.match(regex); | ||
return matches || false; | ||
var matches = value.match(regex); | ||
return matches || false; | ||
} | ||
function rule(analyzer) { | ||
// store unique colors with the counter | ||
var colors = new collection(); | ||
// store unique colors with the counter | ||
var colors = new collection(); | ||
analyzer.setMetric('colors'); | ||
analyzer.setMetric("colors"); | ||
analyzer.on('declaration', function(rule, property, value) { | ||
var extractedColors = extractColors(value); | ||
analyzer.on("declaration", function (rule, property, value) { | ||
var extractedColors = extractColors(value); | ||
if (extractedColors === false) { | ||
return; | ||
} | ||
if (extractedColors === false) { | ||
return; | ||
} | ||
debug('%s: %s -> %j', property, value, extractedColors); | ||
debug("%s: %s -> %j", property, value, extractedColors); | ||
extractedColors. | ||
map(function(item) { | ||
var color = onecolor(item); | ||
extractedColors | ||
.map(function (item) { | ||
var color = onecolor(item); | ||
// handle parsing errors | ||
if (color === false) { | ||
return false; | ||
} | ||
// handle parsing errors | ||
if (color === false) { | ||
return false; | ||
} | ||
// return either rgba(0,0,0,0.25) or #000000 | ||
return (color.alpha() < 1.0) ? color.cssa() : color.hex(); | ||
}). | ||
forEach(function(color) { | ||
if (color !== false) { | ||
colors.push(color); | ||
} | ||
}); | ||
}); | ||
// return either rgba(0,0,0,0.25) or #000000 | ||
return color.alpha() < 1.0 ? color.cssa() : color.hex(); | ||
}) | ||
.forEach(function (color) { | ||
if (color !== false) { | ||
colors.push(color); | ||
} | ||
}); | ||
}); | ||
analyzer.on('report', function() { | ||
analyzer.setCurrentPosition(undefined); | ||
analyzer.on("report", function () { | ||
analyzer.setCurrentPosition(undefined); | ||
colors.sort().forEach(function(color, cnt) { | ||
analyzer.incrMetric('colors'); | ||
analyzer.addOffender('colors', format('%s (%d times)', color, cnt)); | ||
}); | ||
}); | ||
colors.sort().forEach(function (color, cnt) { | ||
analyzer.incrMetric("colors"); | ||
analyzer.addOffender("colors", format("%s (%d times)", color, cnt)); | ||
}); | ||
}); | ||
} | ||
rule.description = 'Reports number of unique colors used in CSS'; | ||
rule.description = "Reports number of unique colors used in CSS"; | ||
module.exports = rule; | ||
@@ -65,0 +65,0 @@ |
@@ -1,22 +0,29 @@ | ||
'use strict'; | ||
"use strict"; | ||
var format = require('util').format, | ||
MAX_LENGTH = 256; | ||
var format = require("util").format, | ||
MAX_LENGTH = 256; | ||
function rule(analyzer) { | ||
analyzer.setMetric('comments'); | ||
analyzer.setMetric('commentsLength'); | ||
analyzer.setMetric("comments"); | ||
analyzer.setMetric("commentsLength"); | ||
analyzer.on('comment', function(comment) { | ||
analyzer.incrMetric('comments'); | ||
analyzer.incrMetric('commentsLength', comment.length); | ||
analyzer.on("comment", function (comment) { | ||
analyzer.incrMetric("comments"); | ||
analyzer.incrMetric("commentsLength", comment.length); | ||
// report too long comments | ||
if (comment.length > MAX_LENGTH) { | ||
analyzer.addOffender('comments', format('"%s" is too long (%d characters)', comment.substr(0, 100), comment.length)); | ||
} | ||
}); | ||
// report too long comments | ||
if (comment.length > MAX_LENGTH) { | ||
analyzer.addOffender( | ||
"comments", | ||
format( | ||
'"%s" is too long (%d characters)', | ||
comment.substr(0, 100), | ||
comment.length | ||
) | ||
); | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports too long CSS comments'; | ||
rule.description = "Reports too long CSS comments"; | ||
module.exports = rule; |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,14 +6,14 @@ var COMPLEX_SELECTOR_THRESHOLD = 3; | ||
function rule(analyzer) { | ||
analyzer.setMetric('complexSelectors'); | ||
analyzer.setMetric("complexSelectors"); | ||
// #foo .bar ul li a | ||
analyzer.on('selector', function(rule, selector, expressions) { | ||
if (expressions.length > COMPLEX_SELECTOR_THRESHOLD) { | ||
analyzer.incrMetric('complexSelectors'); | ||
analyzer.addOffender('complexSelectors', selector); | ||
} | ||
}); | ||
// #foo .bar ul li a | ||
analyzer.on("selector", function (rule, selector, expressions) { | ||
if (expressions.length > COMPLEX_SELECTOR_THRESHOLD) { | ||
analyzer.incrMetric("complexSelectors"); | ||
analyzer.addOffender("complexSelectors", selector); | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports too complex CSS selectors'; | ||
rule.description = "Reports too complex CSS selectors"; | ||
module.exports = rule; |
@@ -1,96 +0,112 @@ | ||
'use strict'; | ||
"use strict"; | ||
var collection = require('../lib/collection'), | ||
debug = require('debug')('analyze-css:duplicated'), | ||
format = require('util').format; | ||
var collection = require("../lib/collection"), | ||
debug = require("debug")("analyze-css:duplicated"), | ||
format = require("util").format; | ||
function rule(analyzer) { | ||
var selectors = new collection(), | ||
mediaQueryStack = [], | ||
browserPrefixRegEx = /^-(moz|o|webkit|ms)-/; | ||
var selectors = new collection(), | ||
mediaQueryStack = [], | ||
browserPrefixRegEx = /^-(moz|o|webkit|ms)-/; | ||
analyzer.setMetric('duplicatedSelectors'); | ||
analyzer.setMetric('duplicatedProperties'); | ||
analyzer.setMetric("duplicatedSelectors"); | ||
analyzer.setMetric("duplicatedProperties"); | ||
// handle nested media queries | ||
analyzer.on('media', function(query) { | ||
mediaQueryStack.push(query); | ||
debug('push: %j', mediaQueryStack); | ||
}); | ||
// handle nested media queries | ||
analyzer.on("media", function (query) { | ||
mediaQueryStack.push(query); | ||
debug("push: %j", mediaQueryStack); | ||
}); | ||
analyzer.on('mediaEnd', function(query) { | ||
mediaQueryStack.pop(query); | ||
debug('pop: %j', mediaQueryStack); | ||
}); | ||
analyzer.on("mediaEnd", function (query) { | ||
mediaQueryStack.pop(query); | ||
debug("pop: %j", mediaQueryStack); | ||
}); | ||
// register each rule's selectors | ||
analyzer.on('rule', function(rule) { | ||
selectors.push( | ||
// @media foo | ||
(mediaQueryStack.length > 0 ? '@media ' + mediaQueryStack.join(' @media ') + ' ' : '') + | ||
// #foo | ||
rule.selectors.join(', ') | ||
); | ||
}); | ||
// register each rule's selectors | ||
analyzer.on("rule", function (rule) { | ||
selectors.push( | ||
// @media foo | ||
(mediaQueryStack.length > 0 | ||
? "@media " + mediaQueryStack.join(" @media ") + " " | ||
: "") + | ||
// #foo | ||
rule.selectors.join(", ") | ||
); | ||
}); | ||
// find duplicated properties (issue #60) | ||
analyzer.on('rule', function(rule) { | ||
var propertiesHash = {}; | ||
// find duplicated properties (issue #60) | ||
analyzer.on("rule", function (rule) { | ||
var propertiesHash = {}; | ||
if (rule.declarations) { | ||
rule.declarations.forEach(function(declaration) { | ||
var propertyName; | ||
if (rule.declarations) { | ||
rule.declarations.forEach(function (declaration) { | ||
var propertyName; | ||
if (declaration.type === 'declaration') { | ||
propertyName = declaration.property; | ||
if (declaration.type === "declaration") { | ||
propertyName = declaration.property; | ||
// skip properties that require browser prefixes | ||
// background-image:-moz-linear-gradient(...) | ||
// background-image:-webkit-gradient(...) | ||
if (browserPrefixRegEx.test(declaration.value) === true) { | ||
return; | ||
} | ||
// skip properties that require browser prefixes | ||
// background-image:-moz-linear-gradient(...) | ||
// background-image:-webkit-gradient(...) | ||
if (browserPrefixRegEx.test(declaration.value) === true) { | ||
return; | ||
} | ||
// property was already used in the current selector - report it | ||
if (propertiesHash[propertyName] === true) { | ||
// report the position of the offending property | ||
analyzer.setCurrentPosition(declaration.position); | ||
// property was already used in the current selector - report it | ||
if (propertiesHash[propertyName] === true) { | ||
// report the position of the offending property | ||
analyzer.setCurrentPosition(declaration.position); | ||
analyzer.incrMetric('duplicatedProperties'); | ||
analyzer.addOffender('duplicatedProperties', format('%s {%s: %s}', rule.selectors.join(', '), declaration.property, declaration.value)); | ||
} else { | ||
// mark given property as defined in the context of the current selector | ||
propertiesHash[propertyName] = true; | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
analyzer.incrMetric("duplicatedProperties"); | ||
analyzer.addOffender( | ||
"duplicatedProperties", | ||
format( | ||
"%s {%s: %s}", | ||
rule.selectors.join(", "), | ||
declaration.property, | ||
declaration.value | ||
) | ||
); | ||
} else { | ||
// mark given property as defined in the context of the current selector | ||
propertiesHash[propertyName] = true; | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
// special handling for @font-face (#52) | ||
// include URL when detecting duplicates | ||
analyzer.on('font-face', function(rule) { | ||
rule.declarations.forEach(function(declaration) { | ||
if (declaration.property === 'src') { | ||
selectors.push('@font-face src: ' + declaration.value); | ||
// special handling for @font-face (#52) | ||
// include URL when detecting duplicates | ||
analyzer.on("font-face", function (rule) { | ||
rule.declarations.forEach(function (declaration) { | ||
if (declaration.property === "src") { | ||
selectors.push("@font-face src: " + declaration.value); | ||
debug('special handling for @font-face, provided src: %s', declaration.value); | ||
return false; | ||
} | ||
}); | ||
}); | ||
debug( | ||
"special handling for @font-face, provided src: %s", | ||
declaration.value | ||
); | ||
return false; | ||
} | ||
}); | ||
}); | ||
analyzer.on('report', function() { | ||
analyzer.setCurrentPosition(undefined); | ||
analyzer.on("report", function () { | ||
analyzer.setCurrentPosition(undefined); | ||
selectors.sort().forEach(function(selector, cnt) { | ||
if (cnt > 1) { | ||
analyzer.incrMetric('duplicatedSelectors'); | ||
analyzer.addOffender('duplicatedSelectors', format('%s (%d times)', selector, cnt)); | ||
} | ||
}); | ||
}); | ||
selectors.sort().forEach(function (selector, cnt) { | ||
if (cnt > 1) { | ||
analyzer.incrMetric("duplicatedSelectors"); | ||
analyzer.addOffender( | ||
"duplicatedSelectors", | ||
format("%s (%d times)", selector, cnt) | ||
); | ||
} | ||
}); | ||
}); | ||
} | ||
rule.description = 'Reports duplicated CSS selectors and properties'; | ||
rule.description = "Reports duplicated CSS selectors and properties"; | ||
module.exports = rule; |
@@ -1,19 +0,19 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
analyzer.setMetric('emptyRules'); | ||
analyzer.setMetric("emptyRules"); | ||
analyzer.on('rule', function(rule) { | ||
var properties = rule.declarations.filter(function(item) { | ||
return item.type === 'declaration'; | ||
}); | ||
analyzer.on("rule", function (rule) { | ||
var properties = rule.declarations.filter(function (item) { | ||
return item.type === "declaration"; | ||
}); | ||
if (properties.length === 0) { | ||
analyzer.incrMetric('emptyRules'); | ||
analyzer.addOffender('emptyRules', rule.selectors.join(', ')); | ||
} | ||
}); | ||
if (properties.length === 0) { | ||
analyzer.incrMetric("emptyRules"); | ||
analyzer.addOffender("emptyRules", rule.selectors.join(", ")); | ||
} | ||
}); | ||
} | ||
rule.description = 'Total number of empty CSS rules'; | ||
rule.description = "Total number of empty CSS rules"; | ||
module.exports = rule; |
@@ -1,19 +0,22 @@ | ||
'use strict'; | ||
"use strict"; | ||
var format = require('util').format; | ||
var format = require("util").format; | ||
function rule(analyzer) { | ||
var re = /^expression/i; | ||
var re = /^expression/i; | ||
analyzer.setMetric('expressions'); | ||
analyzer.setMetric("expressions"); | ||
analyzer.on('declaration', function(rule, property, value) { | ||
if (re.test(value)) { | ||
analyzer.incrMetric('expressions'); | ||
analyzer.addOffender('expressions', format('%s {%s: %s}', rule.selectors.join(', '), property, value)); | ||
} | ||
}); | ||
analyzer.on("declaration", function (rule, property, value) { | ||
if (re.test(value)) { | ||
analyzer.incrMetric("expressions"); | ||
analyzer.addOffender( | ||
"expressions", | ||
format("%s {%s: %s}", rule.selectors.join(", "), property, value) | ||
); | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports CSS expressions'; | ||
rule.description = "Reports CSS expressions"; | ||
module.exports = rule; |
@@ -1,4 +0,4 @@ | ||
'use strict'; | ||
"use strict"; | ||
var format = require('util').format; | ||
var format = require("util").format; | ||
@@ -11,33 +11,37 @@ /** | ||
function rule(analyzer) { | ||
var re = { | ||
property: /^(\*|-ms-filter)/, | ||
selector: /^(\* html|html\s?>\s?body) /, | ||
value: /progid:DXImageTransform\.Microsoft|!ie$/ | ||
}; | ||
var re = { | ||
property: /^(\*|-ms-filter)/, | ||
selector: /^(\* html|html\s?>\s?body) /, | ||
value: /progid:DXImageTransform\.Microsoft|!ie$/, | ||
}; | ||
analyzer.setMetric('oldIEFixes'); | ||
analyzer.setMetric("oldIEFixes"); | ||
// * html // below IE7 fix | ||
// html>body // IE6 excluded fix | ||
// @see http://blogs.msdn.com/b/ie/archive/2005/09/02/460115.aspx | ||
analyzer.on('selector', function(rule, selector) { | ||
if (re.selector.test(selector)) { | ||
analyzer.incrMetric('oldIEFixes'); | ||
analyzer.addOffender('oldIEFixes', selector); | ||
} | ||
}); | ||
// * html // below IE7 fix | ||
// html>body // IE6 excluded fix | ||
// @see http://blogs.msdn.com/b/ie/archive/2005/09/02/460115.aspx | ||
analyzer.on("selector", function (rule, selector) { | ||
if (re.selector.test(selector)) { | ||
analyzer.incrMetric("oldIEFixes"); | ||
analyzer.addOffender("oldIEFixes", selector); | ||
} | ||
}); | ||
// *foo: bar // IE7 and below fix | ||
// -ms-filter // IE9 and below specific property | ||
// !ie // IE 7 and below equivalent of !important | ||
// @see http://www.impressivewebs.com/ie7-ie8-css-hacks/ | ||
analyzer.on('declaration', function(rule, property, value) { | ||
if (re.property.test(property) || re.value.test(value)) { | ||
analyzer.incrMetric('oldIEFixes'); | ||
analyzer.addOffender('oldIEFixes', format('%s {%s: %s}', rule.selectors.join(', '), property, value)); | ||
} | ||
}); | ||
// *foo: bar // IE7 and below fix | ||
// -ms-filter // IE9 and below specific property | ||
// !ie // IE 7 and below equivalent of !important | ||
// @see http://www.impressivewebs.com/ie7-ie8-css-hacks/ | ||
analyzer.on("declaration", function (rule, property, value) { | ||
if (re.property.test(property) || re.value.test(value)) { | ||
analyzer.incrMetric("oldIEFixes"); | ||
analyzer.addOffender( | ||
"oldIEFixes", | ||
format("%s {%s: %s}", rule.selectors.join(", "), property, value) | ||
); | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports fixes for old versions of Internet Explorer (IE9 and below)'; | ||
rule.description = | ||
"Reports fixes for old versions of Internet Explorer (IE9 and below)"; | ||
module.exports = rule; |
@@ -1,13 +0,13 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
analyzer.setMetric('imports'); | ||
analyzer.setMetric("imports"); | ||
analyzer.on('import', function(url) { | ||
analyzer.incrMetric('imports'); | ||
analyzer.addOffender('imports', url); | ||
}); | ||
analyzer.on("import", function (url) { | ||
analyzer.incrMetric("imports"); | ||
analyzer.addOffender("imports", url); | ||
}); | ||
} | ||
rule.description = 'Number of @import rules'; | ||
rule.description = "Number of @import rules"; | ||
module.exports = rule; |
@@ -1,17 +0,20 @@ | ||
'use strict'; | ||
"use strict"; | ||
var format = require('util').format; | ||
var format = require("util").format; | ||
function rule(analyzer) { | ||
analyzer.setMetric('importants'); | ||
analyzer.setMetric("importants"); | ||
analyzer.on('declaration', function(rule, property, value) { | ||
if (value.indexOf('!important') > -1) { | ||
analyzer.incrMetric('importants'); | ||
analyzer.addOffender('importants', format('%s {%s: %s}', rule.selectors.join(', '), property, value)); | ||
} | ||
}); | ||
analyzer.on("declaration", function (rule, property, value) { | ||
if (value.indexOf("!important") > -1) { | ||
analyzer.incrMetric("importants"); | ||
analyzer.addOffender( | ||
"importants", | ||
format("%s {%s: %s}", rule.selectors.join(", "), property, value) | ||
); | ||
} | ||
}); | ||
} | ||
rule.description = 'Number of properties with value forced by !important'; | ||
rule.description = "Number of properties with value forced by !important"; | ||
module.exports = rule; |
@@ -1,10 +0,10 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
analyzer.on('css', function(css) { | ||
analyzer.setMetric('length', css.length); | ||
}); | ||
analyzer.on("css", function (css) { | ||
analyzer.setMetric("length", css.length); | ||
}); | ||
} | ||
rule.description = 'Length of CSS file'; | ||
rule.description = "Length of CSS file"; | ||
module.exports = rule; |
@@ -1,15 +0,18 @@ | ||
'use strict'; | ||
"use strict"; | ||
var format = require('util').format; | ||
var format = require("util").format; | ||
function rule(analyzer) { | ||
analyzer.setMetric('mediaQueries'); | ||
analyzer.setMetric("mediaQueries"); | ||
analyzer.on('media', function(query, rules) { | ||
analyzer.incrMetric('mediaQueries'); | ||
analyzer.addOffender('mediaQueries', format('@media %s (%d rules)', query, rules && rules.length || 0)); | ||
}); | ||
analyzer.on("media", function (query, rules) { | ||
analyzer.incrMetric("mediaQueries"); | ||
analyzer.addOffender( | ||
"mediaQueries", | ||
format("@media %s (%d rules)", query, (rules && rules.length) || 0) | ||
); | ||
}); | ||
} | ||
rule.description = 'Reports media queries'; | ||
rule.description = "Reports media queries"; | ||
module.exports = rule; |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -7,21 +7,21 @@ /** | ||
function rule(analyzer) { | ||
analyzer.setMetric('notMinified'); | ||
analyzer.setMetric("notMinified"); | ||
/** | ||
* A simple CSS minification detector | ||
*/ | ||
function isMinified(css) { | ||
// analyze the first 1024 characters | ||
css = css.trim().substring(0, 1024); | ||
/** | ||
* A simple CSS minification detector | ||
*/ | ||
function isMinified(css) { | ||
// analyze the first 1024 characters | ||
css = css.trim().substring(0, 1024); | ||
// there should be no newline in minified file | ||
return /\n/.test(css) === false; | ||
} | ||
// there should be no newline in minified file | ||
return /\n/.test(css) === false; | ||
} | ||
analyzer.on('css', function(css) { | ||
analyzer.setMetric('notMinified', isMinified(css) ? 0 : 1); | ||
}); | ||
analyzer.on("css", function (css) { | ||
analyzer.setMetric("notMinified", isMinified(css) ? 0 : 1); | ||
}); | ||
} | ||
rule.description = 'Reports not minified CSS '; | ||
rule.description = "Reports not minified CSS "; | ||
module.exports = rule; |
@@ -1,15 +0,18 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
analyzer.setMetric('multiClassesSelectors'); | ||
analyzer.setMetric("multiClassesSelectors"); | ||
analyzer.on('expression', function(selector, expression) { | ||
if (expression.classList && expression.classList.length > 1) { | ||
analyzer.incrMetric('multiClassesSelectors'); | ||
analyzer.addOffender('multiClassesSelectors', '.' + expression.classList.join('.')); | ||
} | ||
}); | ||
analyzer.on("expression", function (selector, expression) { | ||
if (expression.classList && expression.classList.length > 1) { | ||
analyzer.incrMetric("multiClassesSelectors"); | ||
analyzer.addOffender( | ||
"multiClassesSelectors", | ||
"." + expression.classList.join(".") | ||
); | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports selectors with multiple classes'; | ||
rule.description = "Reports selectors with multiple classes"; | ||
module.exports = rule; |
@@ -1,13 +0,13 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
analyzer.setMetric('parsingErrors'); | ||
analyzer.setMetric("parsingErrors"); | ||
analyzer.on('error', function(err) { | ||
analyzer.incrMetric('parsingErrors'); | ||
analyzer.addOffender('parsingErrors', err.reason); | ||
}); | ||
analyzer.on("error", function (err) { | ||
analyzer.incrMetric("parsingErrors"); | ||
analyzer.addOffender("parsingErrors", err.reason); | ||
}); | ||
} | ||
rule.description = 'CSS parsing errors'; | ||
rule.description = "CSS parsing errors"; | ||
module.exports = rule; |
@@ -1,25 +0,34 @@ | ||
'use strict'; | ||
"use strict"; | ||
var debug = require('debug')('analyze-css:prefixes'), | ||
format = require('util').format; | ||
var debug = require("debug")("analyze-css:prefixes"), | ||
format = require("util").format; | ||
function rule(analyzer) { | ||
var data = require('./prefixes.json'), | ||
prefixes = data.prefixes; | ||
var data = require("./prefixes.json"), | ||
prefixes = data.prefixes; | ||
debug('Using data generated on %s', data.generated); | ||
analyzer.setMetric('oldPropertyPrefixes'); | ||
debug("Using data generated on %s", data.generated); | ||
analyzer.setMetric("oldPropertyPrefixes"); | ||
analyzer.on('declaration', function(rule, property, value) { | ||
var prefixData = prefixes[property]; | ||
analyzer.on("declaration", function (rule, property, value) { | ||
var prefixData = prefixes[property]; | ||
// prefix needs to be kept | ||
if (prefixData && !prefixData.keep) { | ||
analyzer.incrMetric('oldPropertyPrefixes'); | ||
analyzer.addOffender('oldPropertyPrefixes', format('%s { %s: %s } // %s', rule.selectors.join(', '), property, value, prefixData.msg)); | ||
} | ||
}); | ||
// prefix needs to be kept | ||
if (prefixData && !prefixData.keep) { | ||
analyzer.incrMetric("oldPropertyPrefixes"); | ||
analyzer.addOffender( | ||
"oldPropertyPrefixes", | ||
format( | ||
"%s { %s: %s } // %s", | ||
rule.selectors.join(", "), | ||
property, | ||
value, | ||
prefixData.msg | ||
) | ||
); | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports outdated vendor prefixes'; | ||
rule.description = "Reports outdated vendor prefixes"; | ||
module.exports = rule; |
@@ -1,6 +0,5 @@ | ||
'use strict'; | ||
"use strict"; | ||
var debug = require('debug')('analyze-css:propertyResets'), | ||
format = require('util').format, | ||
shorthandProperties = require('css-shorthand-properties'); | ||
var format = require("util").format, | ||
shorthandProperties = require("css-shorthand-properties"); | ||
@@ -13,49 +12,61 @@ /** | ||
function rule(analyzer) { | ||
var debug = require('debug'); | ||
var debug = require("debug"); | ||
analyzer.setMetric('propertyResets'); | ||
analyzer.setMetric("propertyResets"); | ||
analyzer.on('selector', function(rule, selector) { | ||
var declarations = rule.declarations || [], | ||
properties; | ||
analyzer.on("selector", function (rule, selector) { | ||
var declarations = rule.declarations || [], | ||
properties; | ||
// prepare the list of properties used in this selector | ||
properties = declarations. | ||
map(function(declaration) { | ||
return (declaration.type === 'declaration') ? declaration.property : false; | ||
}). | ||
filter(function(item) { | ||
return item !== false; | ||
}); | ||
// prepare the list of properties used in this selector | ||
properties = declarations | ||
.map(function (declaration) { | ||
return declaration.type === "declaration" | ||
? declaration.property | ||
: false; | ||
}) | ||
.filter(function (item) { | ||
return item !== false; | ||
}); | ||
debug('%s: %j', selector, properties); | ||
debug("%s: %j", selector, properties); | ||
// iterate through all properties, expand shorthand properties and | ||
// check if there's no expanded version of it earlier in the array | ||
properties.forEach(function(property, idx) { | ||
var expanded; | ||
// iterate through all properties, expand shorthand properties and | ||
// check if there's no expanded version of it earlier in the array | ||
properties.forEach(function (property, idx) { | ||
var expanded; | ||
// skip if the current property is not the shorthand version | ||
if (typeof shorthandProperties.shorthandProperties[property] === 'undefined') { | ||
return; | ||
} | ||
// skip if the current property is not the shorthand version | ||
if ( | ||
typeof shorthandProperties.shorthandProperties[property] === "undefined" | ||
) { | ||
return; | ||
} | ||
// property = 'margin' | ||
// expanded = [ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ] | ||
expanded = shorthandProperties.expand(property); | ||
debug('%s: %s', property, expanded.join(', ')); | ||
// property = 'margin' | ||
// expanded = [ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ] | ||
expanded = shorthandProperties.expand(property); | ||
debug("%s: %s", property, expanded.join(", ")); | ||
expanded.forEach(function(expandedProperty) { | ||
var propertyPos = properties.indexOf(expandedProperty); | ||
expanded.forEach(function (expandedProperty) { | ||
var propertyPos = properties.indexOf(expandedProperty); | ||
if (propertyPos > -1 && propertyPos < idx) { | ||
analyzer.incrMetric('propertyResets'); | ||
analyzer.addOffender('propertyResets', format('%s: "%s" resets "%s" property set earlier', selector, property, expandedProperty)); | ||
} | ||
}); | ||
}); | ||
}); | ||
if (propertyPos > -1 && propertyPos < idx) { | ||
analyzer.incrMetric("propertyResets"); | ||
analyzer.addOffender( | ||
"propertyResets", | ||
format( | ||
'%s: "%s" resets "%s" property set earlier', | ||
selector, | ||
property, | ||
expandedProperty | ||
) | ||
); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
rule.description = 'Reports accidental property resets'; | ||
rule.description = "Reports accidental property resets"; | ||
module.exports = rule; |
@@ -1,27 +0,27 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
analyzer.setMetric('qualifiedSelectors'); | ||
analyzer.setMetric("qualifiedSelectors"); | ||
// @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Writing_efficient_CSS | ||
analyzer.on('expression', function(selector, expression) { | ||
var hasId = expression.id, | ||
hasTag = expression.tag && expression.tag !== '*', | ||
hasClass = expression.classList; | ||
// @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Writing_efficient_CSS | ||
analyzer.on("expression", function (selector, expression) { | ||
var hasId = expression.id, | ||
hasTag = expression.tag && expression.tag !== "*", | ||
hasClass = expression.classList; | ||
if ( | ||
// tag#id | ||
(hasId && hasTag) || | ||
// .class#id | ||
(hasId && hasClass) || | ||
// tag.class | ||
(hasClass && hasTag) | ||
) { | ||
analyzer.incrMetric('qualifiedSelectors'); | ||
analyzer.addOffender('qualifiedSelectors', selector); | ||
} | ||
}); | ||
if ( | ||
// tag#id | ||
(hasId && hasTag) || | ||
// .class#id | ||
(hasId && hasClass) || | ||
// tag.class | ||
(hasClass && hasTag) | ||
) { | ||
analyzer.incrMetric("qualifiedSelectors"); | ||
analyzer.addOffender("qualifiedSelectors", selector); | ||
} | ||
}); | ||
} | ||
rule.description = 'Reports qualified selectors'; | ||
rule.description = "Reports qualified selectors"; | ||
module.exports = rule; |
@@ -1,55 +0,57 @@ | ||
'use strict'; | ||
"use strict"; | ||
var debug = require('debug')('analyze-css:specificity'), | ||
specificity = require('specificity'), | ||
stats = require('fast-stats').Stats; | ||
var debug = require("debug")("analyze-css:specificity"), | ||
specificity = require("specificity"), | ||
stats = require("fast-stats").Stats; | ||
function rule(analyzer) { | ||
var types = ['Id', 'Class', 'Tag'], | ||
typesLen = types.length, | ||
values = []; | ||
var types = ["Id", "Class", "Tag"], | ||
values = []; | ||
// prepare metrics and stacks for values | ||
types.forEach(function(type) { | ||
analyzer.setMetric('specificity' + type + 'Avg'); | ||
analyzer.setMetric('specificity' + type + 'Total'); | ||
// prepare metrics and stacks for values | ||
types.forEach(function (type) { | ||
analyzer.setMetric("specificity" + type + "Avg"); | ||
analyzer.setMetric("specificity" + type + "Total"); | ||
values.push(new stats()); | ||
}); | ||
values.push(new stats()); | ||
}); | ||
analyzer.on('selector', function(rule, selector, expressions) { | ||
var selectorSpecificity = specificity.calculate(selector), | ||
parts; | ||
analyzer.on("selector", function (rule, selector) { | ||
var selectorSpecificity = specificity.calculate(selector), | ||
parts; | ||
if (!selectorSpecificity) { | ||
debug('not counted for %s!', selector); | ||
return; | ||
} | ||
if (!selectorSpecificity) { | ||
debug("not counted for %s!", selector); | ||
return; | ||
} | ||
// parse the results | ||
parts = selectorSpecificity[0].specificity. | ||
split(','). | ||
slice(1). | ||
map(function(i) { | ||
return parseInt(i, 10); | ||
}); | ||
// parse the results | ||
parts = selectorSpecificity[0].specificity | ||
.split(",") | ||
.slice(1) | ||
.map(function (i) { | ||
return parseInt(i, 10); | ||
}); | ||
debug('%s: %s', selector, parts.join('')); | ||
debug("%s: %s", selector, parts.join("")); | ||
// add each piece to a separate stack | ||
parts.forEach(function(val, idx) { | ||
values[idx].push(val); | ||
}); | ||
}); | ||
// add each piece to a separate stack | ||
parts.forEach(function (val, idx) { | ||
values[idx].push(val); | ||
}); | ||
}); | ||
analyzer.on('report', function() { | ||
debug('Gathering stats...'); | ||
analyzer.on("report", function () { | ||
debug("Gathering stats..."); | ||
types.forEach(function(type, idx) { | ||
analyzer.setMetric('specificity' + type + 'Avg', parseFloat(values[idx].amean().toFixed(2))); | ||
analyzer.setMetric('specificity' + type + 'Total', values[idx].Σ()); | ||
}); | ||
types.forEach(function (type, idx) { | ||
analyzer.setMetric( | ||
"specificity" + type + "Avg", | ||
parseFloat(values[idx].amean().toFixed(2)) | ||
); | ||
analyzer.setMetric("specificity" + type + "Total", values[idx].Σ()); | ||
}); | ||
debug('Done'); | ||
}); | ||
debug("Done"); | ||
}); | ||
} | ||
@@ -59,3 +61,3 @@ | ||
// @see http://css-tricks.com/specifics-on-css-specificity/ | ||
rule.description = 'Reports rules specificity'; | ||
rule.description = "Reports rules specificity"; | ||
module.exports = rule; |
@@ -1,63 +0,63 @@ | ||
'use strict'; | ||
"use strict"; | ||
function rule(analyzer) { | ||
var selectors = 0, | ||
selectorsLength = 0; | ||
var selectors = 0, | ||
selectorsLength = 0; | ||
analyzer.setMetric('selectors'); | ||
analyzer.setMetric('selectorLengthAvg'); | ||
analyzer.setMetric("selectors"); | ||
analyzer.setMetric("selectorLengthAvg"); | ||
analyzer.setMetric('selectorsByAttribute'); | ||
analyzer.setMetric('selectorsByClass'); | ||
analyzer.setMetric('selectorsById'); | ||
analyzer.setMetric('selectorsByPseudo'); | ||
analyzer.setMetric('selectorsByTag'); | ||
analyzer.setMetric("selectorsByAttribute"); | ||
analyzer.setMetric("selectorsByClass"); | ||
analyzer.setMetric("selectorsById"); | ||
analyzer.setMetric("selectorsByPseudo"); | ||
analyzer.setMetric("selectorsByTag"); | ||
analyzer.on('rule', function() { | ||
analyzer.incrMetric('rules'); | ||
}); | ||
analyzer.on("rule", function () { | ||
analyzer.incrMetric("rules"); | ||
}); | ||
analyzer.on('selector', function(rule, selector, expressions) { | ||
selectors += 1; | ||
selectorsLength += expressions.length; | ||
}); | ||
analyzer.on("selector", function (rule, selector, expressions) { | ||
selectors += 1; | ||
selectorsLength += expressions.length; | ||
}); | ||
analyzer.on('declaration', function() { | ||
analyzer.incrMetric('declarations'); | ||
}); | ||
analyzer.on("declaration", function () { | ||
analyzer.incrMetric("declarations"); | ||
}); | ||
analyzer.on('expression', function(selector, expression) { | ||
// a[href] | ||
if (expression.attributes) { | ||
analyzer.incrMetric('selectorsByAttribute'); | ||
} | ||
analyzer.on("expression", function (selector, expression) { | ||
// a[href] | ||
if (expression.attributes) { | ||
analyzer.incrMetric("selectorsByAttribute"); | ||
} | ||
// .bar | ||
if (expression.classList) { | ||
analyzer.incrMetric('selectorsByClass'); | ||
} | ||
// .bar | ||
if (expression.classList) { | ||
analyzer.incrMetric("selectorsByClass"); | ||
} | ||
// @foo | ||
if (expression.id) { | ||
analyzer.incrMetric('selectorsById'); | ||
} | ||
// @foo | ||
if (expression.id) { | ||
analyzer.incrMetric("selectorsById"); | ||
} | ||
// a:hover | ||
if (expression.pseudos) { | ||
analyzer.incrMetric('selectorsByPseudo'); | ||
} | ||
// a:hover | ||
if (expression.pseudos) { | ||
analyzer.incrMetric("selectorsByPseudo"); | ||
} | ||
// header | ||
if (expression.tag && expression.tag !== '*') { | ||
analyzer.incrMetric('selectorsByTag'); | ||
} | ||
}); | ||
// header | ||
if (expression.tag && expression.tag !== "*") { | ||
analyzer.incrMetric("selectorsByTag"); | ||
} | ||
}); | ||
analyzer.on('report', function() { | ||
analyzer.setMetric('selectors', selectors); | ||
analyzer.setMetric('selectorLengthAvg', selectorsLength / selectors); | ||
}); | ||
analyzer.on("report", function () { | ||
analyzer.setMetric("selectors", selectors); | ||
analyzer.setMetric("selectorLengthAvg", selectorsLength / selectors); | ||
}); | ||
} | ||
rule.description = 'Emit CSS stats'; | ||
rule.description = "Emit CSS stats"; | ||
module.exports = rule; |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
123406
3708
200
7
13
9
32
+ Addedabbrev@1.1.1(transitive)
+ Addedajv@6.12.6(transitive)
+ Addedamdefine@1.0.1(transitive)
+ Addedansi-regex@2.1.14.1.1(transitive)
+ Addedansi-styles@2.2.13.2.1(transitive)
+ Addedaproba@1.2.0(transitive)
+ Addedare-we-there-yet@1.1.7(transitive)
+ Addedarray-find-index@1.0.2(transitive)
+ Addedasn1@0.2.6(transitive)
+ Addedassert-plus@1.0.0(transitive)
+ Addedasync-foreach@0.1.3(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedaws-sign2@0.7.0(transitive)
+ Addedaws4@1.13.0(transitive)
+ Addedbcrypt-pbkdf@1.0.2(transitive)
+ Addedcamelcase@2.1.15.3.1(transitive)
+ Addedcamelcase-keys@2.1.0(transitive)
+ Addedcaseless@0.12.0(transitive)
+ Addedchalk@1.1.3(transitive)
+ Addedchownr@2.0.0(transitive)
+ Addedcliui@5.0.0(transitive)
+ Addedcode-point-at@1.1.0(transitive)
+ Addedcolor-convert@1.9.3(transitive)
+ Addedcolor-name@1.1.3(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcommander@7.2.0(transitive)
+ Addedconsole-control-strings@1.1.0(transitive)
+ Addedcore-util-is@1.0.21.0.3(transitive)
+ Addedcross-spawn@7.0.3(transitive)
+ Addedcurrently-unhandled@0.4.1(transitive)
+ Addeddashdash@1.14.1(transitive)
+ Addeddecamelize@1.2.0(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addeddelegates@1.0.0(transitive)
+ Addedecc-jsbn@0.1.2(transitive)
+ Addedemoji-regex@7.0.3(transitive)
+ Addedenv-paths@2.2.1(transitive)
+ Addederror-ex@1.3.2(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedextend@3.0.2(transitive)
+ Addedextsprintf@1.3.0(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedfast-stats@0.0.6(transitive)
+ Addedfind-up@1.1.23.0.0(transitive)
+ Addedforever-agent@0.6.1(transitive)
+ Addedform-data@2.3.3(transitive)
+ Addedfs-minipass@2.1.0(transitive)
+ Addedfunction-bind@1.1.2(transitive)
+ Addedgauge@2.7.4(transitive)
+ Addedgaze@1.1.3(transitive)
+ Addedget-caller-file@2.0.5(transitive)
+ Addedget-stdin@4.0.1(transitive)
+ Addedgetpass@0.1.7(transitive)
+ Addedglob@7.1.7(transitive)
+ Addedglobule@1.3.4(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedhar-schema@2.0.0(transitive)
+ Addedhar-validator@5.1.5(transitive)
+ Addedhas-ansi@2.0.0(transitive)
+ Addedhas-unicode@2.0.1(transitive)
+ Addedhasown@2.0.2(transitive)
+ Addedhosted-git-info@2.8.9(transitive)
+ Addedhttp-signature@1.2.0(transitive)
+ Addedindent-string@2.1.0(transitive)
+ Addedis-arrayish@0.2.1(transitive)
+ Addedis-core-module@2.14.0(transitive)
+ Addedis-finite@1.1.0(transitive)
+ Addedis-fullwidth-code-point@1.0.02.0.0(transitive)
+ Addedis-typedarray@1.0.0(transitive)
+ Addedis-utf8@0.2.1(transitive)
+ Addedisarray@1.0.0(transitive)
+ Addedisexe@2.0.0(transitive)
+ Addedisstream@0.1.2(transitive)
+ Addedjs-base64@2.6.4(transitive)
+ Addedjsbn@0.1.1(transitive)
+ Addedjson-schema@0.4.0(transitive)
+ Addedjson-schema-traverse@0.4.1(transitive)
+ Addedjson-stringify-safe@5.0.1(transitive)
+ Addedjsprim@1.4.2(transitive)
+ Addedload-json-file@1.1.0(transitive)
+ Addedlocate-path@3.0.0(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedloud-rejection@1.6.0(transitive)
+ Addedmap-obj@1.0.1(transitive)
+ Addedmeow@3.7.0(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedminimatch@3.0.8(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedminipass@3.3.65.0.0(transitive)
+ Addedminizlib@2.1.2(transitive)
+ Addedmkdirp@0.5.61.0.4(transitive)
+ Addednan@2.20.0(transitive)
+ Addednode-gyp@7.1.2(transitive)
+ Addednode-sass@5.0.0(transitive)
+ Addednopt@5.0.0(transitive)
+ Addednormalize-package-data@2.5.0(transitive)
+ Addednpmlog@4.1.2(transitive)
+ Addednumber-is-nan@1.0.1(transitive)
+ Addedoauth-sign@0.9.0(transitive)
+ Addedobject-assign@4.1.1(transitive)
+ Addedp-limit@2.3.0(transitive)
+ Addedp-locate@3.0.0(transitive)
+ Addedp-try@2.2.0(transitive)
+ Addedparse-json@2.2.0(transitive)
+ Addedpath-exists@2.1.03.0.0(transitive)
+ Addedpath-key@3.1.1(transitive)
+ Addedpath-parse@1.0.7(transitive)
+ Addedpath-type@1.1.0(transitive)
+ Addedperformance-now@2.1.0(transitive)
+ Addedpify@2.3.0(transitive)
+ Addedpinkie@2.0.4(transitive)
+ Addedpinkie-promise@2.0.1(transitive)
+ Addedprocess-nextick-args@2.0.1(transitive)
+ Addedpsl@1.9.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedqs@6.5.3(transitive)
+ Addedread-pkg@1.1.0(transitive)
+ Addedread-pkg-up@1.0.1(transitive)
+ Addedreadable-stream@2.3.8(transitive)
+ Addedredent@1.0.0(transitive)
+ Addedrepeating@2.0.1(transitive)
+ Addedrequest@2.88.2(transitive)
+ Addedrequire-directory@2.1.1(transitive)
+ Addedrequire-main-filename@2.0.0(transitive)
+ Addedresolve@1.22.8(transitive)
+ Addedrimraf@3.0.2(transitive)
+ Addedsafe-buffer@5.1.2(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsass-graph@2.2.5(transitive)
+ Addedscss-tokenizer@0.2.3(transitive)
+ Addedsemver@5.7.27.6.2(transitive)
+ Addedset-blocking@2.0.0(transitive)
+ Addedshebang-command@2.0.0(transitive)
+ Addedshebang-regex@3.0.0(transitive)
+ Addedsignal-exit@3.0.7(transitive)
+ Addedsource-map@0.4.4(transitive)
+ Addedspdx-correct@3.2.0(transitive)
+ Addedspdx-exceptions@2.5.0(transitive)
+ Addedspdx-expression-parse@3.0.1(transitive)
+ Addedspdx-license-ids@3.0.18(transitive)
+ Addedsshpk@1.18.0(transitive)
+ Addedstdout-stream@1.4.1(transitive)
+ Addedstring-width@1.0.23.1.0(transitive)
+ Addedstring_decoder@1.1.1(transitive)
+ Addedstrip-ansi@3.0.15.2.0(transitive)
+ Addedstrip-bom@2.0.0(transitive)
+ Addedstrip-indent@1.0.1(transitive)
+ Addedsupports-color@2.0.0(transitive)
+ Addedsupports-preserve-symlinks-flag@1.0.0(transitive)
+ Addedtar@6.2.1(transitive)
+ Addedtough-cookie@2.5.0(transitive)
+ Addedtrim-newlines@1.0.0(transitive)
+ Addedtrue-case-path@1.0.3(transitive)
+ Addedtunnel-agent@0.6.0(transitive)
+ Addedtweetnacl@0.14.5(transitive)
+ Addeduri-js@4.4.1(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addeduuid@3.4.0(transitive)
+ Addedvalidate-npm-package-license@3.0.4(transitive)
+ Addedverror@1.10.0(transitive)
+ Addedwhich@2.0.2(transitive)
+ Addedwhich-module@2.0.1(transitive)
+ Addedwide-align@1.1.5(transitive)
+ Addedwrap-ansi@5.1.0(transitive)
+ Addedy18n@4.0.3(transitive)
+ Addedyallist@4.0.0(transitive)
+ Addedyargs@13.3.2(transitive)
+ Addedyargs-parser@13.1.2(transitive)
- Removedcommander@6.2.1(transitive)
- Removedfast-stats@0.0.5(transitive)
Updatedcommander@^7.0.0
Updatedfast-stats@0.0.6