Skip to content

Commit

Permalink
Update CLI internals
Browse files Browse the repository at this point in the history
*   Refactor CLI to depend on unified-engine, which comes
    with lots of fixes and simplifications;
*   Add proper CLI tests.
  • Loading branch information
wooorm committed Sep 13, 2016
1 parent 4e30acb commit 4ea6f67
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 289 deletions.
6 changes: 1 addition & 5 deletions .alexignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
# Both `node_modules` and `bower_components` are
# ignored by default:
# node_modules/
# bower_components/

# `node_modules` is ignored by default.
example.md
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
*.log
node_modules/
.nyc_output/
coverage/
alex.js
alex.min.js
310 changes: 57 additions & 253 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,19 @@

'use strict';

/* eslint-disable handle-callback-err */

/* Dependencies. */
var fs = require('fs');
var bail = require('bail');
var PassThrough = require('stream').PassThrough;
var notifier = require('update-notifier');
var meow = require('meow');
var globby = require('globby');
var hasMagic = require('glob').hasMagic;
var minimatch = require('minimatch');
var getStdin = require('get-stdin');
var findDown = require('vfile-find-down');
var findUp = require('vfile-find-up');
var format = require('vfile-reporter');
var toFile = require('to-vfile');
var engine = require('unified-engine');
var unified = require('unified');
var markdown = require('remark-parse');
var english = require('retext-english');
var equality = require('retext-equality');
var profanities = require('retext-profanities');
var remark2retext = require('remark-retext');
var report = require('vfile-reporter');
var pack = require('./package');
var alex = require('./');

/* Methods. */
var readFile = fs.readFileSync;
var stat = fs.statSync;

/* Constants. */
var expextPipeIn = !process.stdin.isTTY;
var IGNORE = '.alexignore';
var RC = '.alexrc';
var PACKAGE = 'package.json';
var PACKAGE_FIELD = 'alex';
var ENCODING = 'utf-8';
var BACKSLASH = '\\';
var SLASH = '/';
var CD = './';
var HASH = '#';
var EMPTY = '';

var defaultIgnore = [
'node_modules/',
'bower_components/'
];

var extensions = [
'txt',
Expand All @@ -66,8 +40,7 @@ notifier({pkg: pack}).notify();
/* Set-up meow. */
var cli = meow({
help: [
'Usage: alex [<file> | <dir> ...] [-q, --quiet] [-w, --why] ' +
'[-t, --text]',
'Usage: alex [<glob> ...] [options ...]',
'',
'Options:',
'',
Expand All @@ -83,238 +56,69 @@ var cli = meow({
' $ alex *.md !example.md',
' $ alex'
]
});

/* Set-up. */
var exit = 0;
var result = [];
var why = Boolean(cli.flags.w || cli.flags.why);
var quiet = Boolean(cli.flags.q || cli.flags.quiet);
var fn = (cli.flags.t || cli.flags.text) ? 'text' : 'markdown';
var globs = cli.input.length ? cli.input : [
'{docs/**/,doc/**/,}*.{' + extensions.join(',') + '}'
];

/* Exit. */
process.on('exit', function () {
var report = format(result, {
verbose: why,
quiet: quiet
});

if (report) {
console.log(report);
}, {
alias: {
v: 'version',
h: 'help',
t: 'text',
q: 'quiet',
w: 'why'
}

process.exit(exit);
});

/**
* Log a virtual file processed by alex.
*
* @param {VFile} file - Virtual file.
*/
function log(file) {
result.push(file);

if (!exit && file.messages.length) {
exit = 1;
}
}
/* Set-up. */
var globs = ['{docs/**/,doc/**/,}*.{' + extensions.join(',') + '}'];

/**
* Check if `file` matches `pattern`.
*
* @example
* match('baz.md', '*.md'); // true
*
* @param {string} filePath - File location.
* @param {string} pattern - Glob pattern.
* @return {boolean} - Whether `file` matches.
*/
function match(filePath, pattern) {
return minimatch(filePath, pattern) || minimatch(filePath, pattern + '/**');
/* istanbul ignore next - Bug in tests. Something hangs, at least. */
if (cli.input.length) {
globs = cli.input;
}

/**
* Check if `filePath` is matched included in `patterns`.
*
* @example
* shouldIgnore(['node_modules/'], 'node_modules/foo'); // true
*
* @param {Array.<string>} patterns - Glob patterns.
* @param {string} filePath - File location.
* @return {boolean} - Whether `filePath` should be ignored.
*/
function shouldIgnore(patterns, filePath) {
var normalized = filePath.replace(BACKSLASH, SLASH).replace(CD, EMPTY);

return patterns.reduce(function (isIgnored, pattern) {
var isNegated = pattern.charAt(0) === '!';

if (isNegated) {
pattern = pattern.slice(1);
}
var plain = unified().use(english).use(equality).use(profanities);
var processor = plain;

if (pattern.indexOf(CD) === 0) {
pattern = pattern.slice(CD.length);
}

return match(normalized, pattern) ? !isNegated : isIgnored;
}, false);
if (!cli.flags.text) {
processor = unified().use(markdown).use(remark2retext, plain);
}

/**
* Load an ignore file.
*
* @param {string} ignore - File location.
* @return {Array.<string>} - Patterns.
*/
function loadIgnore(ignore) {
return readFile(ignore, ENCODING).split(/\r?\n/).filter(function (value) {
var line = value.trim();

return line.length && line.charAt(0) !== HASH;
var filter = require.resolve('./filter.js');

engine({
processor: processor,
globs: globs,
extensions: extensions,
configTransform: transform,
output: false,
out: false,
streamError: new PassThrough(),
rcName: '.alexrc',
packageField: 'alex',
ignoreName: '.alexignore',
plugins: [filter],
frail: true
}, function (err, code, result) {
var out = report(err || result.files, {
verbose: cli.flags.why,
quiet: cli.flags.quiet
});
}

/**
* Factory to create a file filter based on bound ignore
* patterns.
*
* @param {Array.<string>} ignore - Ignore patterns.
* @param {Array.<string>} given - List of given file paths.
* @return {Function} - Filter.
*/
function filterFactory(ignore, given) {
/**
* Check whether a virtual file is applicable.
*
* @param {VFile} file - Virtual file.
*/
return function (file) {
var filePath = file.filePath();
var extension = file.extension;
if (out) {
console.error(out);
}

if (given.indexOf(filePath) !== -1 || shouldIgnore(ignore, filePath)) {
return findDown.SKIP;
}
process.exit(code);
});

return extension && extensions.indexOf(extension) !== -1;
};
}
function transform(raw) {
var allow = raw.allow || /* istanbul ignore next */ [];

/**
* Factory to create a file filter based on bound ignore
* patterns.
*
* @param {Array.<VFile>} given - List of given files.
* @param {Array.<string>} allow - List of allowed phrases.
* @return {Function} - Process callback.
*/
function processFactory(given, allow) {
/**
* Process all found files (and failed ones too).
*
* @param {Error} [err] - Finding error (not used by
* vfile-find-down).
* @param {Array.<VFile>} [files] - Virtual files.
*/
return function (err, files) {
given.concat(files || []).forEach(function (file) {
file.quiet = true;
return function (current) {
var plugins = {};

try {
file.contents = readFile(file.filePath(), ENCODING);
} catch (err) {
file.fail(err);
}
current = current.plugins && current.plugins[filter] && current.plugins[filter].allow;

alex[fn](file, allow);
plugins[filter] = {allow: [].concat(allow, current || [])};

log(file);
});
return {plugins: plugins};
};
}

/* Either handle stdin(4) or patterns. */
if (!cli.input.length && expextPipeIn) {
getStdin().then(function (value) {
var file = toFile('<stdin>');

file.contents = value;

alex(file);

log(file);
}, bail);
} else {
globby(globs).then(function (filePaths) {
var files = [];
var given = [];

/* Check whether files are given directly that either
* do not exist or which might not match the default
* search patterns (based on extensions). */
globs.forEach(function (glob) {
if (hasMagic(glob)) {
return;
}

files.push(toFile(glob));
given.push(glob);

try {
if (!stat(glob).isFile()) {
files.pop();
given.pop();
}
} catch (err) { /* Empty. */ }
});

/* Search for an ignore file. */
findUp.one(IGNORE, function (err, file) {
var ignore = [];

try {
ignore = file && loadIgnore(file.filePath());
} catch (err) { /* Empty. */ }

ignore = defaultIgnore.concat(ignore || []);

/* Search for rc files. */
findUp.all([PACKAGE, RC], function (err, configs) {
var allow = [];
var length = configs && configs.length;
var index = -1;
var file;
var contents;

while (++index < length) {
file = configs[index];
file.contents = readFile(file.filePath(), ENCODING);
file.quiet = true;

try {
contents = JSON.parse(file.contents);

if (file.basename() === PACKAGE) {
contents = contents[PACKAGE_FIELD] || {};
}

allow = [].concat.apply(allow, contents.allow);
} catch (err) {
file.warn(err);
files.push(file);
}
}

findDown.all(
filterFactory(ignore, given),
filePaths,
processFactory(files, allow)
);
});
});
}, bail);
}
14 changes: 14 additions & 0 deletions filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

var control = require('remark-message-control');

module.exports = filter;

function filter(proc, options) {
var settings = options || /* istanbul ignore next */ {};
proc.use(control, {
name: 'alex',
disable: settings.allow,
source: ['retext-equality', 'retext-profanities']
});
}

0 comments on commit 4ea6f67

Please sign in to comment.