fixclosure
Version:
JavaScript dependency checker/fixer for Closure Library based on Esprima
274 lines (238 loc) • 7.08 kB
JavaScript
;
var clc = require('cli-color');
var commander = require('commander');
var fs = require('fs');
var path = require('path');
var _ = require('underscore');
var fix = require('./fix');
var Parser = require('./parser');
var Logger = function(enableColor, stdout, stderr) {
this.color_ = !!enableColor;
this.messages_ = [];
this.stdout = stdout;
this.stderr = stderr;
};
Logger.prototype.raw = function(msg, opt_color) {
this.messages_.push(this.color_ && opt_color ? opt_color(msg) : msg);
};
Logger.prototype.info = function(msg) {
this.raw(msg);
};
Logger.prototype.warn = function(msg) {
this.raw(msg, clc.yellow);
};
Logger.prototype.error = function(msg) {
this.raw(msg, clc.red);
};
Logger.prototype.success = function(msg) {
this.raw(msg, clc.green);
};
Logger.prototype.items = function(items) {
if (items.length === 0) {
items = ['(none)'];
}
this.messages_ = this.messages_.concat(items.map(function(item) {
item = '- ' + item;
return this.color_ ? clc.blackBright(item) : item;
}, this));
};
Logger.prototype.flush = function(success) {
var out = success ? this.stdout: this.stderr;
this.messages_.forEach(function(msg) {
out.write(msg + '\n');
});
this.empty();
};
Logger.prototype.empty = function() {
this.messages_ = [];
};
function list(val) {
return val.split(',');
}
function map(val) {
var mapping = {};
val.split(',').forEach(function(item) {
var entry = item.split(':');
mapping[entry[0]] = entry[1];
});
return mapping;
}
function setCommandOptions(command) {
return command
.version(require('../package.json').version, '-v, --version')
.usage('[options] files...')
.option('-f, --fix-in-place', 'Fix the file in-place.')
.option('--provideRoots <roots>', 'Root namespaces to provide separated by comma.', list)
.option('--requireRoots <roots>', 'Root namespaces to require separated by comma.', list)
.option('--roots <roots>', '(deprecated) Additional root namespaces to provide or require separated by comma.', list)
.option('--namespaceMethods <methods>', 'Methods or properties which are also namespaces separated by comma.', list)
.option('--replaceMap <map>', 'Methods or properties to namespaces mapping like "before1:after1,before2:after2".', map)
.option('--config <file>', '.fixclosurerc file path.')
.option('--showSuccess', 'Show success ouput.', false)
.option('--no-color', 'Disable color highlight.');
}
function getDuplicated(namespaces) {
var dups = [];
namespaces.reduce(function(prev, cur) {
if (prev === cur) {
dups.push(cur);
}
return cur;
}, null);
return dups;
}
/**
* Find .fixclosurerc up from current working dir
*/
function findConfig() {
return findConfig_(process.cwd());
}
var findConfig_ = _.memoize(function(dir) {
var name = '.fixclosurerc';
var filename = path.normalize(path.join(dir, name));
if (fs.existsSync(filename)) {
return filename;
}
var parent = path.resolve(dir, '../');
if (dir === parent) {
return null;
}
return findConfig_(parent);
});
/**
* Load .fixclosurerc to argv
* @param {Array} argv
*/
function loadConfig(argv) {
var command = setCommandOptions(new commander.Command());
command.parse(argv);
var configPath = command.config || findConfig();
if (configPath) {
var opts = fs.readFileSync(configPath, 'utf8').trim().split(/\s+/);
argv = argv.slice(0, 2).concat(opts.concat(argv.slice(2)));
}
return argv;
}
/**
* @param {Array} argv
* @param {Stream} stdout
* @param {Stream} stderr
* @param {function(number?)} exit
*/
function main(argv, stdout, stderr, exit) {
argv = loadConfig(argv);
var program = new commander.Command();
setCommandOptions(program).parse(argv);
if (program.args.length < 1) {
program.outputHelp();
exit(1);
}
var ok = 0;
var ng = 0;
var fixed = 0;
var log = new Logger(program.color, stdout, stderr);
program.args.forEach(function(file) {
log.warn('File: ' + file + '\n');
var src = fs.readFileSync(file, 'utf8');
var options = {
provideRoots: program.provideRoots,
requireRoots: program.requireRoots,
roots: program.roots,
namespaceMethods: program.namespaceMethods,
replaceMap: program.replaceMap
};
var parser = new Parser(options);
var info = parser.parse(src);
log.info('Provided:');
log.items(info.provided.map(function(item) {
return item + (_.contains(info.ignoredProvide, item) ? ' (ignored)' : '');
}));
log.info('');
log.info('Required:');
log.items(info.required.map(function(item) {
return item + (_.contains(info.ignoredRequire, item) ? ' (ignored)' : '');
}));
log.info('');
var needToFix = false;
var dupProvide = getDuplicated(info.provided);
if (dupProvide.length > 0) {
needToFix = true;
log.error('Duplicated Provide:');
log.items(_.uniq(dupProvide));
log.info('');
}
var missingProvide = _.difference(info.toProvide, info.provided);
if (missingProvide.length > 0) {
needToFix = true;
log.error('Missing Provide:');
log.items(missingProvide);
log.info('');
}
var unnecessaryProvide = _.difference(info.provided, info.toProvide);
unnecessaryProvide = _.difference(unnecessaryProvide, info.ignoredProvide);
if (unnecessaryProvide.length > 0) {
needToFix = true;
log.error('Unnecessary Provide:');
log.items(unnecessaryProvide);
log.info('');
}
var dupRequire = getDuplicated(info.required);
if (dupRequire.length > 0) {
needToFix = true;
log.error('Duplicated Require:');
log.items(_.uniq(dupRequire));
log.info('');
}
var missingRequire = _.difference(info.toRequire, info.required);
if (missingRequire.length > 0) {
needToFix = true;
log.error('Missing Require:');
log.items(missingRequire);
log.info('');
}
var unnecessaryRequire = _.difference(info.required, info.toRequire);
unnecessaryRequire = _.difference(unnecessaryRequire, info.ignoredRequire);
if (unnecessaryRequire.length > 0) {
needToFix = true;
log.error('Unnecessary Require:');
log.items(unnecessaryRequire);
log.info('');
}
if (needToFix) {
if (program.fixInPlace) {
fix(file, info);
log.raw(('FIXED!'), clc.cyan);
fixed++;
} else {
log.error('FAIL!');
ng++;
}
log.flush(false);
} else {
ok++;
log.success('GREEN!');
if (program.showSuccess) {
log.flush(true);
} else {
log.empty();
}
}
});
var total = ok + ng + fixed;
log.info('');
if (ng) {
log.error(ng + ' of ' + total + ' files failed');
if (fixed) {
log.warn(fixed + ' files fixed');
}
log.flush(false);
exit(1);
} else {
log.success(ok + ' files completed');
if (fixed) {
log.warn(fixed + ' files fixed');
}
log.flush(true);
}
}
module.exports = main;