esformatter
Version:
ECMAScript code beautifier/formatter
255 lines (211 loc) • 6.17 kB
JavaScript
;
var esformatter = require('../lib/esformatter');
var fs = require('fs');
var glob = require('glob');
var merge = require('mout/object/merge');
var minimist = require('minimist');
var minimatch = require('minimatch');
var options = require('./options');
var path = require('path');
var stdin = require('stdin');
var supportsColor = require('supports-color');
var parseOpts = {
'boolean': [
'help',
'version',
'i',
'diff',
'diff-unified',
'no-color',
'color'
],
'alias': {
'h': 'help',
'v': 'version',
'c': 'config',
'p': 'preset'
},
'string': [
'ignore'
]
};
// exit code should be !== 0 if we find any errors during execution
exports.exitCode = 0;
// allow mocking/replacing the stdout/stderr
exports.stdout = process.stdout;
exports.stderr = process.stderr;
exports.parse = function(arr) {
var argv = minimist(arr, parseOpts);
if (argv.plugins) {
argv.plugins = argv.plugins.split(',');
}
return argv;
};
exports.run = function(argv) {
// reset error flag at each run
exports.exitCode = 0;
if (argv.help) {
var help = fs.readFileSync(path.join(__dirname, '../doc/cli.txt'));
exports.stderr.write(help);
return;
}
if (argv.version) {
var pkg = require('../package.json');
exports.stderr.write('esformatter v' + pkg.version + '\n');
return;
}
if (argv.i && (argv.diff || argv['diff-unified'])) {
logError(
'Error: "--diff" and "--diff-unified" flags ' +
'can\'t be used together with the "-i" flag.'
);
return;
}
run(argv);
};
function run(argv) {
var files = argv._;
if (!files.length) {
stdin(function(source) {
toConsole(source, null, argv);
});
return;
}
files = expandGlobs(files, argv.ignore);
if (argv.i) {
files.forEach(function(file) {
formatToSelf(file, argv);
});
return;
}
files.forEach(function(file) {
toConsole(getSource(file), file, argv);
});
}
// we are handling errors this way instead of prematurely terminating the
// program because user might be editing multiple files at once and error
// might only be present on a single file
function logError(e) {
var msg = typeof e === 'string' ? e : e.message;
// esprima.parse errors are in the format 'Line 123: Unexpected token &'
// we support both formats since users might replace the parser
if ((/Line \d+:/).test(msg)) {
// convert into "Error: <filepath>:<line> <error_message>"
msg = 'Error: ' + msg.replace(/[^\d]+(\d+): (.+)/, e.file + ':$1 $2');
} else {
// babylon.parse errors are in the format 'Unexpected token (0:4)'
var m = (/^([^\(]+)\s\((\d+):(\d+)\)/).exec(msg);
if (m) {
// convert into "Error: <filepath>:<line>:<char> <error_message>"
msg = 'Error: ' + e.file + ':' + m[2] + ':' + m[3] + ' ' + m[1];
} else if (msg.indexOf('Error:') < 0) {
msg = 'Error: ' + (e.file ? e.file + ' ' : '') + msg;
}
}
// set the error flag to true to use an exit code !== 0
exports.exitCode = 1;
// we don't call console.error directly to make it possible to mock during
// unit tests
exports.stderr.write(msg + '\n');
if (e.stack) {
exports.stderr.write(e.stack + '\n');
}
}
function expandGlobs(filePaths, ignore) {
return filePaths.reduce(function(arr, file) {
// if file path contains "magical chars" (glob) we expand it, otherwise we
// simply use the file path (`push` is faster than `concat` and avoid `fs`)
if (glob.hasMagic(file)) {
return arr.concat(glob.sync(file, {
ignore: ignore,
// we want to return the glob itself to report that it didn't find any
// files, better to giver clear error messages than to fail silently
nonull: true,
nodir: true
}));
}
if (
!ignore ||
!minimatch(file, ignore, {
// `dot:true` to follow same behavior as `glob.sync:ignore`
dot: true
})
) {
arr.push(file);
}
return arr;
}, []);
}
function getSource(file) {
try {
return fs.readFileSync(file).toString();
} catch (e) {
logError('Can\'t read source file. Exception: ' + e.message);
}
}
function getConfig(filePath, argv) {
// if user sets the "preset" we don't load any other config file
// we assume the "preset" overrides any user settings
if (argv.preset || argv.root) {
return argv;
}
try {
// we only load ".esformatter" or "package.json" file if user did not
// provide a config file as argument, that way we allow user to override
// the behavior
var config = argv.config ?
options.loadAndParseConfig(argv.config) :
options.getRc(filePath);
// we always merge the argv to allow user to override the default settings
return merge(config, argv);
} catch (e) {
logError({
message: e.message,
file: filePath,
});
}
}
function toConsole(source, file, argv) {
var config = getConfig(file, argv);
if (!source || !config) return;
try {
var result;
if (argv.diff || argv['diff-unified']) {
var method = argv.diff ? 'chars' : 'unified';
if (!supportsColor) {
method = 'unifiedNoColor';
}
result = esformatter.diff[method](source, config, file);
if (result) {
exports.exitCode = 1;
// we are using stdout even tho it's considered an "error" because user
// might want to pipe multiple tools and diff(1) also outputs to stdout
exports.stdout.write(result);
}
return;
}
result = esformatter.format(source, config);
// do not use console.log since it adds a line break at the end
exports.stdout.write(result);
} catch (e) {
logError({
stack: e.stack,
message: e.message,
file: (file || 'stdin')
});
}
}
function formatToSelf(file, argv) {
var source = getSource(file);
var config = getConfig(file, argv);
if (!source || !config) return;
try {
fs.writeFileSync(file, esformatter.format(source, config));
} catch (e) {
logError({
stack: e.stack,
message: e.message,
file: file
});
}
}