'use strict';
const _ = require('lodash');
const fs = require('nofs');
const Promise = require('yaku');
const yutils = require('yaku/lib/utils');
const nodeUrl = require('url');
const kit = {};
/**
* Nokit extends all the functions of [nofs](https://github.com/ysmood/nofs)
* and [`yaku/lib/utils`](https://github.com/ysmood/yaku#utils).
* You can use it as same as nofs. For more info, see the doc:
*
* [Offline Documentation](?gotoDoc=nofs/readme.md)
* @example
* ```js
* kit.readFile('test.txt', 'utf8').then((str) =>
* console.log(str)
* );
*
* kit.outputFile('a.txt', 'test')
* .then(() => kit.log('done'));
*
* kit.writeJSON('b.json', { a: 10 })
* .then(() => kit.log('done'))
*
* kit.mkdirs('b.json', { a: 10 })
* .then(() => kit.log('done'));
* ```
*/
const Overview = 'overview'; // eslint-disable-line
_.extend(kit, fs, yutils, {
/**
* The [lodash](https://lodash.com) lib.
* @type {Object}
* @example
* ```js
* kit._.map([1, 2, 3]);
* ```
*/
_,
requireCache: {},
/**
* The browser helper. It helps you to live reload the page and log remotely.
* @static
* @param {Object} opts The options of the client, defaults:
* ```js
* {
* host: '', // The host of the event source.
* useJs: false // By default the function will return html string
* }
* ```
* @return {String} The code of client helper.
* @example
* When the client code is loaded on the browser, you can use
* the `nb.log` to log anything to server's terminal.
* The server will auto-format and log the information to the terminal.
* It's convinient for mobile development when remote debug is not possible.
* ```js
* // The nb is assigned to the "window" object.
* nb.log({ a: 10 });
* nb.log(10);
* nb.es.addEventListener('fileModified', () =>
* console.log('file changed')
* );
* ```
*/
browserHelper(opts) {
if (opts == null) {
opts = {};
}
const helper = kit.browserHelper.cache ||
kit.require('./browserHelper', __dirname).toString();
const optsStr = JSON.stringify(_.defaults(opts, {
host: ''
}));
const js = `
if (!window.nokit) window.nokit = (${helper})(${optsStr});\n\
`;
if (opts.useJs) {
return js;
} else {
return `
\n\n
\n\n\
`;
}
},
/**
* Generate styled string for terminal.
* It's disabled when `process.env.NODE_ENV == 'production'`.
* @example
* ```js
* let br = kit.require('brush');
* kit.log(br.red('error info'));
*
* // Disable color globally.
* br.isEnabled = false;
*
* // To see all the available brushes.
* kit.log(Object.keys(br));
* ```
*/
brush: null,
/**
* A fast file cache helper. It uses hard link to cache files.
* @param {Object} info Not optional.
* ```js
* {
* // The first item is the key path, others are
* // its dependencies.
* deps: Array,
*
* // The path of the output file.
* // If it's undefined, depsCache will try to get cache.
* dests: Array,
*
* cacheDir: '.nokit'
* }
* ```
* @return {Promise} Resolve a info object.
* ```js
* {
* isNewer: Boolean,
*
* // { path: mtime }
* deps: Object,
*
* // { destPath: cachePath }
* dests: Object,
*
* cacheError: undefined || Error
* }
* ```
* @example
* ```js
* // Set cache
* kit.depsCache({
* dests: ['index.css'],
* deps: ['index.less', 'b.less', 'c.less']
* });
*
* // Get cache
* // You don't have to sepecify 'b.less', 'c.less'.
* kit.depsCache({ deps: ['index.less'] })
* .then((cache) => {
* if (cache.isNewer) {
* kit.log('cache is newer');
* kit.log(cache.dests);
* }
* });
* ```
*/
depsCache(opts) {
let info;
_.defaults(opts, {
cacheDir: '.nokit'
});
if (kit.depsCache.jhash == null) {
kit.depsCache.jhash = new(kit.require('jhash').constructor);
}
const hashPath = function (path) {
const hash = kit.depsCache.jhash.hash(path, true) + '-' +
kit.path.basename(path);
path = kit.path.join(opts.cacheDir, hash);
return {
cache: path,
info: path + '.json'
};
};
const key = hashPath(opts.deps[0]);
if (opts.dests) {
info = {
dests: {},
deps: {}
};
const saveLink = (from, to) =>
kit.mkdirs(opts.cacheDir).then(() =>
kit.link(from, to)
.catch(function (err) {
if (err.code !== 'EEXIST') {
return Promise.reject(err);
}
return kit.unlink(to).then(() => kit.link(from, to));
})
);
const saveInfo = infoPath =>
Promise.all(opts.deps.map(function (path, i) {
if (i === 0) {
return info.deps[path] = Date.now();
}
return kit.stat(path).catch(function () {}).then(function (stats) {
if (!stats) {
return;
}
return info.deps[path] = stats.mtime.getTime();
});
})).then(() => kit.outputJson(infoPath, info)).then(() =>
Promise.all(opts.deps.slice(1).map(dep => saveLink(infoPath, hashPath(dep).info)))
);
const saveContents = () =>
Promise.all(opts.dests.map(function (dest) {
const hashed = hashPath(dest);
info.dests[dest] = hashed.cache;
return saveLink(dest, hashed.cache);
}));
return Promise.all([
saveContents(),
saveInfo(key.info)
]);
} else {
info = {};
return kit.readJson(key.info)
.then(function (data) {
info = data;
return Promise.all(_(info.deps).keys().map(path =>
kit.stat(path).then(stats =>
// cache mtime file mtime
info.deps[path] >= stats.mtime.getTime())
).value());
}).then(function (latestList) {
info.deps = _.keys(info.deps);
return info.isNewer = _.every(latestList);
}).catch(err => info.cacheError = err)
.then(() => info);
}
},
/**
* Daemonize a program. Just a shortcut usage of `kit.spawn`.
* @param {Object} opts Defaults:
* ```js
* {
* bin: 'node',
* args: ['app.js'],
* stdout: 'stdout.log', // Can also be a fd
* stderr: 'stderr.log' // Can also be a fd
* }
* ```
* @return {Porcess} The daemonized process.
*/
daemonize(opts) {
let errLog, outLog;
if (opts == null) {
opts = {};
}
_.defaults(opts, {
bin: 'node',
args: ['app.js'],
stdout: 'stdout.log',
stderr: 'stderr.log'
});
if (_.isString(opts.stdout)) {
outLog = kit.fs.openSync(opts.stdout, 'a');
}
if (_.isString(opts.stderr)) {
errLog = kit.fs.openSync(opts.stderr, 'a');
}
const p = kit.spawn(opts.bin, opts.args, {
detached: true,
stdio: ['ignore', outLog, errLog]
}).process;
p.unref();
return p;
},
/**
* A simple decrypt helper. Cross-version of node.
* @param {Any} data
* @param {String | Buffer} password
* @param {String} algorithm Default is 'aes128'.
* @return {Buffer}
*/
decrypt(data, password, algorithm) {
if (algorithm == null) {
algorithm = 'aes128';
}
const crypto = kit.require('crypto', __dirname);
const decipher = crypto.createDecipher(algorithm, password);
if (!Buffer.isBuffer(data)) {
data = Buffer.from(data);
}
return Buffer.concat([decipher.update(data), decipher.final()]);
},
/**
* The warp drives.
* You must `kit.require 'drives'` before using it.
* For more information goto the `Drives` section.
* @type {Object}
*/
drives: null,
/**
* A simple encrypt helper. Cross-version of node.
* @param {Any} data
* @param {String | Buffer} password
* @param {String} algorithm Default is 'aes128'.
* @return {Buffer}
*/
encrypt(data, password, algorithm) {
if (algorithm == null) {
algorithm = 'aes128';
}
const crypto = kit.require('crypto', __dirname);
const cipher = crypto.createCipher(algorithm, password);
if (!Buffer.isBuffer(data)) {
data = Buffer.from(data);
}
return Buffer.concat([cipher.update(data), cipher.final()]);
},
/**
* A error log shortcut for `kit.log(msg, 'error', opts)`
* @param {Any} msg
* @param {Object} opts
*/
err(msg, opts) {
if (opts == null) {
opts = {};
}
return kit.log(msg, 'error', opts);
},
/**
* Shortcut for logging multiple error infos.
* @param {Any} args...
* @example
* ```js
* kit.errs('test1', 'test2', 'test3');
* // => [2015-02-07 08:31:49] test1 test2 test3
* ```
*/
errs(...args) {
let out = '';
const last = args.length - 1;
for (var i = 0; i < args.length; i++) {
const arg = args[i];
kit.log(arg, {
isShowTime: false,
log(str) {
return out += str + (i === last ? '' : ' ');
}
});
}
return kit.log(out, 'error');
},
/**
* A better `child_process.exec`. Supports multi-line shell script.
* For supporting old version of node, it will create 3 temp files,
* the temp files will be removed after the execution.
* @param {String} cmd Shell commands.
* @param {String} shell Shell name. Such as `bash`, `zsh`. Optinal.
* @return {Promise} Resolves when the process's stdio is drained.
* The resolve value is like:
* ```js
* {
* code: 0,
* signal: null,
* stdout: 'hello world',
* stderr: ''
* }
* ```
* @example
* ```js
* kit.exec(`
* a='hello world'
* echo $a
* `).then(({code, stdout}) => {
* kit.log code // output => 0
* kit.log stdout // output => "hello world"
* });
*
* // Bash doesn't support "**" recusive match pattern.
* let p = kit.exec(`
* echo **\/*.css
* `, 'zsh');
*
* // Get the child process object.
* p.process.then((proc) =>
* kit.log(proc.pid)
* );
* ```
*/
exec(cmd, shell) {
const os = kit.require('os', __dirname);
if (shell == null) {
shell = process.env.SHELL ||
process.env.ComSpec ||
process.env.COMSPEC;
}
const randName = Date.now() + Math.random();
const paths = ['.in', '.out', '.err']
.map(type => kit.path.join(os.tmpdir(), `nokit-${randName}${type}`));
const [stdinPath, stdoutPath, stderrPath] = Array.from(paths);
let fileHandlers = [];
const clean = () =>
Promise.all(fileHandlers.map(f => kit.close(f)))
.then(() => Promise.all(paths.map(p => kit.remove(p))));
let proc = null;
const processPromise = kit.outputFile(stdinPath, cmd + '\n')
.then(() =>
Promise.all([
kit.fs.open(stdinPath, 'r'),
kit.fs.open(stdoutPath, 'w'),
kit.fs.open(stderrPath, 'w')
]))
.then(function (stdio) {
fileHandlers = fileHandlers.concat(stdio);
const p = kit.spawn(shell, [], {
stdio
});
({
process: proc
} = p);
return p;
});
const promise = processPromise.then(msg =>
kit.readFile(stdoutPath, 'utf8')
.then(stdout => _.extend(msg, {
stdout
})))
.catch(msg =>
kit.readFile(stderrPath, 'utf8')
.then(function (stderr) {
_.extend(msg, {
stderr
});
return Promise.reject(msg);
})
);
promise.process = processPromise.then(() => proc);
promise.then(clean).catch(clean);
return promise;
},
/**
* Format the parsed comments array to a markdown string.
* @param {Array} comments
* @param {Object} opts Defaults:
* ```js
* {
* indent: 0,
* name: ({ name }) => String,
* tag: ({ tagName, name, type }) => String
* }
* ```
* @return {String}
*/
formatComment(comments, opts) {
if (opts == null) {
opts = {};
}
_.defaults(opts, {
indent: 0,
name({
name
}) {
name = name.replace('self.', '');
return `- #### ${name}\n\n`;
},
tag({
tagName,
name,
type
}) {
const tname = name ? ` \`${name}\`` : '';
const ttype = type ? ` { _${type}_ }` : '';
return `- **${tagName}**:${tname}${ttype}`;
}
});
let all = '';
for (let cmt of Array.from(comments)) {
if (_.some(cmt.tags, {
tagName: 'private'
})) {
continue;
}
const paramList = _(cmt.tags)
.filter(tag => tag.tagName === 'param').map('name')
.value();
if (paramList.length > 0) {
cmt.name += `(${paramList.join(', ')})`;
} else if (_.find(cmt.tags, {
tagName: 'return'
})) {
cmt.name += "()";
}
let cmtStr = opts.name(cmt);
if (cmt.description) {
cmtStr += kit.indent(cmt.description, 4);
cmtStr += '\n\n';
}
for (let tag of Array.from(cmt.tags)) {
cmtStr += kit.indent(opts.tag(tag), 4);
cmtStr += '\n\n';
if (tag.description) {
cmtStr += kit.indent(tag.description, 8);
cmtStr += '\n\n';
}
}
all += cmtStr;
}
// Remove tailing space
all = all.replace(/[ \t]+$/mg, '');
return kit.indent(all, opts.indent);
},
/**
* See my project [nofs](https://github.com/ysmood/nofs).
*
* [Offline Documentation](?gotoDoc=nofs/readme.md)
*/
fs,
/**
* Fuzzy search a string list by a key word.
* @param {String} keys The key word.
* @param {Array} list The list of string to search.
* @param {Object} opts Defaults:
* ```js
* {
* result: (wrappedList) =>
* wrappedList.minBy('distance').words,
* threshold: (cOffset, keyLen, cIndex) =>
* Infinity,
* notFound: (cOffset, keyLen, cIndex) =>
* Infinity,
* span: (cOffset, keyLen, cIndex) =>
* cOffset,
* found: (cOffset, keyLen, cIndex) =>
* (Math.exp(cOffset + 1) - 1) * (keyLen - cIndex),
* tail: (cOffset, keyLen, cIndex, tailLen) =>
* tailLen
* }
* ```
* @return {String} The best matched one. If not found,
* return undefined.
* @example
* ```js
* kit.fuzzySearch('hw', ['test', 'hello world', 'hey world'])
* // output => 'hey world'
*
* // To get a sortable weighted list.
* kit.fuzzySearch('hw', ['test', 'hello world', 'hey world'], {
* result: (wrappedList) => wrappedList.value()
* });
* // output => [
* // { distance: Infinity }
* // { words: 'hello world', distance: 1110.069 }
* // { words: 'hey world', distance: 159.849 }
* // ]
* ```
*/
fuzzySearch(key, list, opts) {
if (opts == null) {
opts = {};
}
_.defaults(opts, {
result(list) {
return list.minBy('distance').words;
},
threshold() {
return Infinity;
},
notFound() {
return Infinity;
},
span(cOffset) {
return cOffset;
},
found(cOffset, keyLen, cIndex) {
return (Math.exp(cOffset + 1) - 1) * (keyLen - cIndex);
},
tail(cOffset, keyLen, cIndex, tailLen) {
return tailLen;
}
});
const wrappedList = _(list)
.map(function (words) {
let cIndex, cOffset;
let distance = 0;
const keyLen = key.length;
for (cIndex = 0; cIndex < key.length; cIndex++) {
const c = key[cIndex];
cOffset = words.indexOf(c, cOffset + 1);
distance = cOffset < 0 ?
opts.notFound(cOffset, keyLen, cIndex) :
distance + opts.found(cOffset, keyLen, cIndex);
distance += opts.span(cOffset, keyLen, cIndex);
if (distance >= opts.threshold(cOffset, keyLen, cIndex)) {
return {
distance: Infinity
};
}
}
distance += opts.tail(cOffset, keyLen,
cIndex, words.slice(cOffset).length);
if (distance >= opts.threshold(cOffset, keyLen, cIndex)) {
return {
distance: Infinity
};
}
return {
words,
distance
};
});
return opts.result(wrappedList);
},
/**
* Generate a list of module paths from a name and a directory.
* @param {String} moduleName The module name.
* @param {String} dir The root path. Default is current working dir.
* @param {String} modDir Default is 'node_modules'.
* @return {Array} Paths
* @example
* ```js
* // Suppose current working directory is '/home/a'
* kit.genModulePaths('test')
* // output => ['/home/a/node_modules/test', '/home/node_modules/test', '/node_modules/test']
* ```
*/
genModulePaths(moduleName, dir, modDir) {
if (dir == null) {
dir = process.cwd();
}
if (modDir == null) {
modDir = 'node_modules';
}
const names = [];
while (true) { // eslint-disable-line
names.push(kit.path.join(dir, modDir, moduleName));
const pDir = kit.path.dirname(dir);
if (dir === pDir) {
break;
}
dir = pDir;
}
names.push(moduleName);
return names;
},
/**
* Indent a text block.
* @param {String} text
* @param {Int} num
* @param {String} char
* @param {RegExp} reg Default is `/^/mg`.
* @return {String} The indented text block.
* @example
* ```js
* // Increase
* kit.indent("one\ntwo", 2)
* // => " one\n two"
*
* // Decrease
* kit.indent("--one\n--two", 0, '', /^--/mg)
* // => "one\ntwo"
* ```
*/
indent(text, num, char, reg) {
if (num == null) {
num = 0;
}
if (char == null) {
char = ' ';
}
if (reg == null) {
reg = /^/mg;
}
const prefix = _.repeat(char, num);
return text.replace(reg, prefix);
},
/**
* Nokit use it to check the running mode of the app.
* Overwrite it if you want to control the check logic.
* By default it returns the `rocess.env.NODE_ENV == 'development'`.
* @return {Boolean}
*/
isDevelopment() {
return process.env.NODE_ENV === 'development';
},
/**
* Nokit use it to check the running mode of the app.
* Overwrite it if you want to control the check logic.
* By default it returns the `rocess.env.NODE_ENV == 'production'`.
* @return {Boolean}
*/
isProduction() {
return process.env.NODE_ENV === 'production';
},
/**
* A fast helper to hash string or binary file.
* See my [jhash](https://github.com/ysmood/jhash) project.
* You must `kit.require 'jhash'` before using it.
*
* [Offline Documentation](?gotoDoc=jhash/readme.md)
* @example
* ```js
* kit.require('jhash');
* kit.jhash.hash('test'); // output => '349o'
*
* jhash.hash(kit.readFileSync('a.jpg'));
*
* // Control the hash char set.
* kit.jhash.setSymbols('abcdef');
* kit.jhash.hash('test'); // output => 'decfddfe'
*
* // Control the max length of the result hash value. Unit is bit.
* jhash.setMaskLen(10);
* jhash.hash('test'); // output => 'ede'
* ```
*/
jhash: null,
/**
* A better log for debugging, it uses the `kit.xinspect` to log.
*
* Use terminal command like `logReg='pattern' node app.js` to
* filter the log info.
*
* Use `logTrace='on' node app.js` to force each log end with a
* stack trace.
* @param {Any} msg Your log message.
* @param {String} action 'log', 'error', 'warn'.
* @param {Object} opts Default is same with `kit.xinspect`,
* but with some extra options:
* ```js
* {
* isShowTime: true,
* logReg: process.env.logReg && new RegExp(process.env.logReg),
* logTrace: process.env.logTrace === 'on',
*
* // Custom log method
* log: (str, action) => console[action](str)
* }
* ```
* @example
* ```js
* kit.log('test');
* // => '[2015-02-07 08:31:49] test'
*
* kit.log('test', { isShowTime: false });
* // => 'test'
*
* kit.log('test', { logReg: /a/ });
* // => ''
*
* kit.log('%s %s %d', ['a', 'b', 10]);
* // => '[2015-02-07 08:31:49] a b 10'
* ```
*/
log(...args) {
let action, formats, opts, time, timeDelta;
const br = kit.require('brush');
if (_.isObject(action)) {
opts = action;
action = 'log';
}
let msg = args[0];
({
action,
formats,
opts
} = kit.defaultArgs(args.slice(1), {
action: {
String: 'log'
},
formats: {
Array: null
},
opts: {
Object: {}
}
}));
_.defaults(opts, {
isShowTime: true,
logReg: process.env.logReg && new RegExp(process.env.logReg),
logTrace: process.env.logTrace === 'on',
log: null
});
if (!kit.lastLogTime) {
kit.lastLogTime = new Date;
if (opts.logReg) {
kit.logReg = opts.logReg;
}
}
if (opts.isShowTime) {
time = new Date();
timeDelta = br.grey((+time - +kit.lastLogTime) + 'ms');
kit.lastLogTime = time;
time = br.grey([
[
[time.getFullYear(), 4, '0'],
[time.getMonth() + 1, 2, '0'],
[time.getDate(), 2, '0']
].map(e => _.padStart.apply(0, e)).join('-'), [
[time.getHours(), 2, '0'],
[time.getMinutes(), 2, '0'],
[time.getSeconds(), 2, '0']
].map(e => _.padStart.apply(0, e)).join(':')
].join(' '));
}
const log = function (str, time) {
if (opts.isShowTime) {
str = str + ' ' + time;
}
if (kit.logReg && !kit.logReg.test(str)) {
return;
}
if (opts.log) {
opts.log(str, action);
} else {
console[action](str);
}
if (opts.logTrace) {
const err = br.grey((new Error).stack)
.replace(/.+\n.+\n.+/, '\nStack trace:');
return console.log(err);
}
};
if (_.isObject(msg)) {
if (opts.isShowTime) {
log(`[${time}] ->\n` + kit.xinspect(msg, opts), timeDelta);
} else {
log(kit.xinspect(msg, opts));
}
} else {
if (formats) {
formats.unshift(msg);
const util = kit.require('util', __dirname);
msg = util.format.apply(0, formats);
}
if (opts.isShowTime) {
log(`[${time}] ` + msg, timeDelta);
} else {
log(msg, timeDelta);
}
}
if (action === 'error') {
process.stderr.write("\u0007");
}
},
/**
* Shortcut for logging multiple infos.
* @param {Any} args...
* @example
* ```js
* kit.logs('test1', 'test2', 'test3');
* // => [2015-02-07 08:31:49] test1 test2 test3
* ```
*/
logs(...args) {
let out = '';
const last = args.length - 1;
for (var i = 0; i < args.length; i++) {
const arg = args[i];
kit.log(arg, {
isShowTime: false,
log(str) {
return out += str + (i === last ? '' : ' ');
}
});
}
return kit.log(out);
},
/**
* Monitor an application and automatically restart it when file changed.
* Even when the monitored app exit with error, the monitor will still wait
* for your file change to restart the application. Not only nodejs, but also
* other programs like ruby or python.
* It will print useful infomation when it application unexceptedly.
* @param {Object} opts Defaults:
* ```js
* {
* bin: 'node',
* args: ['index.js'],
* prefix: 'string', // see the `kit.spawn` for details
* watchList: [], // By default, the same with the "args".
* isNodeDeps: true,
* opts: {}, // Same as the opts of 'kit.spawn'.
*
* // The option of `kit.parseDependency`
* parseDependency: {},
*
* // A hook for restarting the program, run the function "start" to
* // restart.
* retry: (start) => {},
*
* onStart: =>
* kit.log("Monitor: " + opts.watchList),
* onRestart: (path) =>
* kit.log("Reload app, modified: " + path),
* onWatchFiles: (paths) =>
* kit.log('Watching:' + paths.join(', ')),
* onNormalExit: ({ code, signal }) =>
* kit.log('EXIT' +
* ` code: ${code} signal: ${signal}`),
* onErrorExit: ({ code, signal }) =>
* kit.err('EXIT' +
* ` code: ${code} signal: ${signal}\n` +
* 'Process closed. Edit and save the watched file to restart.'),
* }
* ```
* @return {Object} Properties:
* ```js
* {
* // Call it to stop monitor.
* stop: => {},
*
* // Resolve a list of watch handlers.
* watchPromise: Promise
* }
* ```
* @example
* ```js
* kit.monitorApp({
* bin: 'coffee',
* args: ['main.coffee']
* });
*
* kit.monitorApp({
* bin: 'ruby'
* args: ['app.rb', 'lib\/**\/*.rb']
* isNodeDeps: false
* });
* ```
*/
monitorApp(opts) {
const br = kit.require('brush');
const treeKill = kit.require('treeKill');
_.defaults(opts, {
bin: 'node',
args: ['index.js'],
retry() {},
watchList: null,
watchRoot: null,
isNodeDeps: true,
parseDependency: {},
opts: {},
onStart() {
return kit.logs(br.yellow("Monitor:"), opts.bin, opts.watchList);
},
onRestart(path) {
return kit.log(br.yellow("Reload app, modified: ") + path);
},
onWatchFiles(paths) {
const cwd = process.cwd();
return kit.log(br.yellow('Watching: ') +
paths.map(p => kit.path.relative(cwd, p)).join(', ')
);
},
onNormalExit({
code,
signal
}) {
kit.log(br.yellow('EXIT') +
` code: ${br.cyan(code)} signal: ${br.cyan(signal)}`
);
return console.log('\n');
},
onErrorExit({
code,
signal
}) {
kit.err(br.yellow('EXIT') +
` code: ${br.cyan(code)} ` +
`signal: ${br.cyan(signal)}\n` +
br.red(`Process closed. Edit and save the watched file to restart.`)
);
return console.log('\n');
}
});
if (opts.watchList == null) {
opts.watchList = opts.args;
}
let childPromise = null;
var start = function () {
childPromise = kit.spawn(
opts.bin,
opts.args,
opts.opts
);
return childPromise.then(msg => opts.onNormalExit(msg)).catch(function (err) {
if (err.stack) {
return Promise.reject(err.stack);
}
return opts.onErrorExit(err);
}).then(() => opts.retry(start));
};
let watchedList = [];
const watcher = _.debounce(function (path) {
opts.onRestart(path);
childPromise.catch(function () {}).then(start);
return treeKill(childPromise.process.pid, 'SIGINT', _.noop);
}, 50);
const stop = function (sig) {
if (sig == null) {
sig = 'SIGINT';
}
treeKill(childPromise.process.pid, sig, _.noop);
return watchPromise.then(() => Array.from(watchedList).map((w) => kit.unwatchFile(w.path, w.handler)));
};
const watch = function (paths) {
if (_.isString(paths)) {
paths = [paths];
}
paths = _.difference(
(paths.map(p => kit.path.resolve(p))),
(watchedList.map(w => kit.path.resolve(w.path)))
);
if (paths.length > 0) {
opts.onWatchFiles(paths);
}
return kit.watchFiles(paths, {
handler: watcher
})
.then(ws => watchedList = watchedList.concat(ws));
};
process.on('SIGINT', function () {
// it will unconditionally terminate Node.js on all platforms
stop('SIGKILL');
return process.exit();
});
var watchPromise = opts.watchRoot ?
kit.watchDir(opts.watchRoot, {
patterns: opts.watchList,
handler(type, path) {
return watcher(path);
}
}) :
opts.isNodeDeps ?
kit.parseDependency(opts.watchList, opts.parseDependency)
.then(watch) :
kit.watchFiles(opts.watchList, {
handler: watcher
});
opts.onStart();
start();
return {
watchPromise,
stop,
watch
};
},
/**
* Node version. Such as `v0.10.23` is `0.1023`, `v0.10.1` is `0.1001`.
* @return {Float}
*/
nodeVersion() {
if (kit.nodeVersion.ver) {
return kit.nodeVersion.ver;
}
const ms = process.versions.node.match(/(\d+)\.(\d+)\.(\d+)/);
const str = ms[1] + '.' + _.padStart(ms[2], 2, '0') + _.padStart(ms[3], 2, '0');
return kit.nodeVersion.ver = +str;
},
/**
* A helper for arguments type based function override.
* @param {Array | Object} args The arguments to set.
* @param {Object} defaults The default argument settings.
* The key value of the setting is the argument name, the value
* is an object, and the key is the type of the argument, the
* value is the default value of the argument.
* @return {Object}
* @example
* ```js
* let foo = () => {
* kit.defaultArgs(arguments, {
* name: { String: 'A' },
* brush: { Array: [] },
* family: { String: null },
* isReal: { Boolean: false },
* fn: { Function: => 'callback' }
* });
* };
*
* kit.log(foo('test', false, ['red'], -> 'nothing'));
* // Here the logged value will deeply equal:
* { name: 'test', brush: ['red'], family: null, fn: => 'nothing' }
* ```
*/
defaultArgs(args, defaults) {
const set = _(args).toArray().groupBy(function (e) {
if (e) {
if (e.constructor.name === 'AsyncFunction') {
return 'Function';
} else {
return e.constructor.name;
}
}
}).value();
const ret = {};
for (let name in defaults) {
var val = defaults[name];
var [type] = Array.from(_.keys(val));
ret[name] = (() => {
if (set[type]) {
const [v] = Array.from(set[type].splice(0, 1));
if (v) {
return v;
} else {
return val[type];
}
} else {
return val[type];
}
})();
}
return ret;
},
/**
* A comments parser for javascript and coffee-script.
* Used to generate documentation from source code automatically.
* It will traverse through all the comments of a coffee file.
* @param {String} code Coffee source code.
* @param {Object} opts Parser options:
* ```js
* {
* commentReg: RegExp,
* splitReg: RegExp,
* tagNameReg: RegExp,
* typeReg: RegExp,
* nameReg: RegExp,
* nameTags: ['param', 'property'],
* descriptionReg: RegExp
* }
* ```
* @return {Array} The parsed comments. Each item is something like:
* ```js
* {
* name: 'parseComment',
* description: 'A comments parser for coffee-script.',
* tags: [
* {
* tagName: 'param',
* type: 'string',
* name: 'code',
* description: 'The name of the module it belongs to.',
* index: 256, // The target char index in the file.
* line: 32 // The line number of the target in the file.
* }
* ]
* }
* ```
*/
parseComment(code, opts) {
if (opts == null) {
opts = {};
}
_.defaults(opts, {
commentReg: new RegExp(`(?:\\#\\#\\#|\\/\\*)\\*([\\s\\S]+?)(?:\\#\\#\\#|\\*\\/)\\s+(?:var\\s|let\\s|const\\s|function\\s+)?['"]?([$@\\w\\.-]+)['"]?`, 'g'),
splitReg: /^\s+\* @/m,
tagNameReg: /^([\w.]+)\s*/,
typeReg: /^\{(.+?)\}\s*/,
nameReg: /^(\[.+\]|\w+)\s*/,
nameTags: ['param', 'property'],
descriptionReg: /^([\s\S]*)/
});
const parseInfo = function (block) {
// Unescape '\/'
block = block.replace(/\\\//g, '/');
// Clean the prefix '*'
const arr = block.split(opts.splitReg).map(el => el.replace(/^[ \t]+\*[ \t]?/mg, '').trim());
return {
description: arr[0] || '',
tags: arr.slice(1).map(function (el) {
const parseTag = function (reg) {
const m = el.match(reg);
if (m && m[1]) {
el = el.slice(m[0].length);
return m[1];
} else {
return null;
}
};
const tag = {};
tag.tagName = parseTag(opts.tagNameReg);
const type = parseTag(opts.typeReg);
if (type) {
tag.type = type;
if (Array.from(opts.nameTags).includes(tag.tagName)) {
tag.name = parseTag(opts.nameReg);
}
tag.description = parseTag(opts.descriptionReg) || '';
} else {
tag.description = parseTag(opts.descriptionReg) || '';
}
return tag;
})
};
};
const comments = [];
let m = null;
while ((m = opts.commentReg.exec(code)) !== null) {
const info = parseInfo(m[1]);
comments.push({
name: m[2],
description: info.description,
tags: info.tags,
index: opts.commentReg.lastIndex,
line: _.reduce(code.slice(0, opts.commentReg.lastIndex), function (count, char) {
if (char === '\n') {
count++;
}
return count;
}, 1)
});
}
return comments;
},
/**
* Parse dependency tree by regex. The dependency relationships
* is not a tree, but a graph. To avoid dependency cycle, this
* function only return an linear array of the dependencies,
* from which you won't get the detail relationshops between files.
* @param {String | Array} entryPaths The file to begin with.
* @param {Object} opts Defaults:
* ```js
* {
* // It will match `require`, `import` statements.
* depReg: RegExp,
*
* // It will handle all the matched paths.
* // Return false value if you don't want this match.
* handle: (path) => path
* }
* ```
* @return {Promise} It resolves the dependency path array.
* @example
* ```js
* kit.parseDependency('main.', {
* depReg: /require\s*\(?['"](.+)['"]\)?/gm,
* handle: (path) => {
* if (path.match(/^(?:\.|\/|[a-z]:)/i)) return path;
* }
* })
* .then((markdownStr) =>
* kit.log(markdownStr)
* );
* ```
*/
parseDependency(entryPaths, opts, depPaths) {
if (opts == null) {
opts = {};
}
if (depPaths == null) {
depPaths = {};
}
_.defaults(opts, {
depReg: kit.parseDependencyReg,
handle: _.identity,
visitedPaths: {}
});
const winSep = /\\/g;
if (_.isString(entryPaths)) {
entryPaths = [entryPaths];
}
entryPaths = entryPaths.reduce(function (s, p) {
if (opts.visitedPaths[p]) {
return s;
} else {
opts.visitedPaths[p] = true;
}
if (kit.path.extname(p)) {
s.push(p);
} else {
s.push(p + '{/index.,}*');
}
return s;
}, []);
// Parse file.
return kit.glob(entryPaths).then(paths =>
Promise.all(paths.map(function (path) {
// Prevent the recycle dependencies.
if (depPaths[path]) {
return;
}
return kit.readFile(path, 'utf8')
.then(function (str) {
// The point to add path to watch list.
depPaths[path.replace(winSep, '/')] = true;
const dir = kit.path.dirname(path);
entryPaths = [];
str.replace(opts.depReg, function (n0, ...rest) {
const adjustedLength = Math.max(rest.length, 2),
ms = rest.slice(0, adjustedLength - 2);
const p = opts.handle(_.find(ms, _.isString));
if (!p) {
return;
}
return entryPaths.push(kit.path.join(dir, p));
});
return kit.parseDependency(entryPaths, opts, depPaths);
}).catch(_.noop);
}))).then(() => _.keys(depPaths));
},
parseDependencyReg: new RegExp(`require\\s*\\(?['"](.+)['"]\\)?|^\\s*import\\s+['"](.+)['"][;\\s]*$|^\\s*import[\\s\\S]+?from\\s+['"](.+)['"]`, 'mg'),
/**
* io.js native module `path`. See `nofs` for more information.
*/
path: fs.path,
/**
* The promise lib. Now, it uses Yaku as ES5 polyfill.
* In the future, the Yaku will be replaced with native
* ES6 Promise. Please don't use any API other than the ES6 spec.
* @type {Object}
*/
Promise,
/**
* The `proxy` module.
* You must `kit.require 'proxy'` before using it.
* For more information goto the `Proxy` section.
*/
proxy: null,
/**
* Reduce a string via a regex.
* @param {RegExp} reg
* @param {String} str
* @param {Function} iter `(init, matchGroup) -> init`, default is `_.iteratee`.
* @param {Any} init
* @return {Any}
* @example
* ```js
* let out = kit.regexReduce(/\w(\d+)/g, 'a1, b10, c3', (ret, ms) => {
* ret.push(ms[1]);
* return ret;
* }, []);
*
* kit.log(out); // => [1, 10, 3]
* ```
*/
regexReduce(reg, str, iter, init) {
iter = _.iteratee(iter);
let ms = null;
if (reg.global) {
while ((ms = reg.exec(str)) !== null) {
init = iter(init, ms);
}
} else {
return iter(init, reg.exec(str));
}
return init;
},
/**
* Map a string via a regex.
* @param {RegExp} reg
* @param {String} str
* @param {Function} iter `(matchGroup) ->`, default is `_.iteratee`.
* @return {Array}
* @example
* ```js
* let out = kit.regexMap(/\w(\d+)/g, 'a1, b10, c3', 1);
*
* kit.log(out) // => [1, 10, 3]
* ```
*/
regexMap(reg, str, iter) {
iter = _.iteratee(iter);
let ms = null;
const init = [];
if (reg.global) {
while ((ms = reg.exec(str)) !== null) {
init.push(iter(ms));
}
} else {
return iter.push(reg.exec(str));
}
return init;
},
/**
* An async string replace function.
* @param {String} str The string to replace
* @param {String | Regex} pattern
* @param {Function} iter It can return a promise
* @return {Promise}
*/
replace(str, pattern, iter) {
const promises = [];
if (iter == null) {
iter = (p, m) => m;
}
str.replace(pattern, function () {
const offset = arguments[arguments.length - 2];
const start = offset + arguments[0].length;
promises.push(
Promise.resolve(iter.apply(null, arguments))
.then(res => [offset, start, res])
);
});
return Promise.all(promises).then(function (list) {
let out = '';
let start = 0;
let end = 0;
for (let item of Array.from(list)) {
end = item[0];
out += str.slice(start, end) + item[2];
start = item[1];
}
out += str.slice(start);
return out;
});
},
/**
* An async string replace function, each replacement process will run in line.
* @param {String} str The string to replace
* @param {String | Regex} pattern
* @param {Function} iter It can return a promise
* @return {Promise}
*/
replaceSync(str, pattern, iter) {
let out = '';
let promise = Promise.resolve();
let start = 0;
let end = 0;
if (iter == null) {
iter = (p, m) => m;
}
str.replace(pattern, function () {
const arr = _.toArray(arguments);
const offset = arr[arr.length - 2];
promise = promise.then(() => iter.apply(null, arr)).then(function (res) {
end = offset;
out += str.slice(start, end) + res;
start = offset + arr[0].length;
});
});
return promise.then(function () {
out += str.slice(start);
return out;
});
},
/**
* Much faster than the native require of node, but you should
* follow some rules to use it safely.
* Use it to load nokit's internal module.
* @param {String} moduleName The module path or name.
* @param {String} dir Current absolute file path. Not optional, expect when
* requiring nokit's internal modules.
* On most times, just pass `__dirname` to it is enough.
* @param {Function} loaded Run only the first time after the module loaded.
* @return {Module} The module that you require.
* @example
* Use it to load nokit's internal module.
* ```js
* kit.require('jhash');
* // Then you can use the module, or it will be null.
* kit.jhash.hash('test');
* ```
* To load a relative path, or you own module,
* the second parameter 'dir' is required.
* ```js
* let mod = kit.require('./mod', __dirname);
*
* // Or load your own 'jhash', rather than nokit's.
* let jhash = kit.require('jhash', __dirname);
* ```
*/
require(moduleName, dir, loaded) {
let e, modPath;
if (_.isFunction(dir)) {
loaded = dir;
dir = null;
}
const key = moduleName + (dir ? `@${dir}` : '');
if (kit.requireCache[key]) {
return kit.requireCache[key];
}
if ((dir == null)) {
if ((moduleName[0] === '.') || (kit[moduleName] !== null)) {
const err = new Error(
"[kit.require] argument 'dir' is not defined: " +
moduleName
);
err.source = 'nokit';
throw err;
}
try {
modPath = require.resolve(`./${moduleName}`);
} catch (error) {
e = error;
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
}
if (modPath) {
return kit[moduleName] =
(kit.requireCache[key] =
require(modPath));
}
return kit[moduleName] =
(kit.requireCache[key] =
require(moduleName));
}
const names = moduleName[0] === '.' ? [kit.path.join(dir, moduleName)] :
kit.genModulePaths(moduleName, dir)
.concat(process.env.NODE_PATH ?
Array.from(process.env.NODE_PATH.split(kit.path.delimiter)).map((p) =>
kit.path.join(p, moduleName)) :
[]);
for (let name of Array.from(names)) {
try {
modPath = require.resolve(name);
} catch (error1) {
e = error1;
if (e.code === 'MODULE_NOT_FOUND') {
modPath = null;
} else {
throw e;
}
}
if (modPath) {
kit.requireCache[key] = require(modPath);
if (typeof loaded === 'function') {
loaded(kit.requireCache[key]);
}
break;
}
}
if (!kit.requireCache[key]) {
e = new Error(`Module not found: ${moduleName}`);
e.code = 'MODULE_NOT_FOUND';
throw e;
}
if (kit[moduleName] === null) {
kit[moduleName] = kit.requireCache[key];
}
return kit.requireCache[key];
},
/**
* Require an optional package. If not found, it will
* warn the user to npm install it, and exit the process.
* When `kit.requireOptional.autoInstall` is set to `true`, the package will
* be auto installed if it's missed.
* @param {String} name Package name
* @param {String} dir Current absolute file path. Not optional.
* On most times, just pass `__dirname` to it is enough.
* @param {String} semver Specify what version you need,
* such as `^0.3.1` or `>=1.2.3`, ect.
* @return {Any} The required package.
*/
requireOptional(name, dir, semver) {
const key = semver ? name + '@' + semver : name;
if (kit.requireCache[key]) {
return kit.requireCache[key];
}
try {
if (semver) {
kit.require('semver');
const {
version
} = kit.require(name + '/package.json', dir);
if (!kit.semver.satisfies(version, semver)) {
const info = `expect ${name} version ` +
`'${semver}', but get '${version}'`;
name = `${name}@"${semver}"`;
throw new Error(info);
}
}
return kit.require(name, dir);
} catch (err) {
const br = kit.require('brush');
if (kit.requireOptional.autoInstall) {
const {
spawnSync
} = kit.require('child_process', __dirname);
const whichSync = kit.require('whichSync');
spawnSync(whichSync('npm'), ['i', key], {
cwd: dir,
stdio: 'inherit'
});
try {
return kit.require(name, dir);
} catch (err) {
if (err && err.code === 'MODULE_NOT_FOUND') {
kit.errs(br.red('Optional module installed, please rerun the program.'), err.stack)
process.exit(1)
} else {
throw err
}
}
}
if (err.source === 'nokit') {
throw err;
}
kit.err(
(br.red("Optional module required. Please " +
br.green(`'npm install -S ${name}'` + br.red(" first.\n")))) +
err.stack, {
isShowTime: false
});
return process.exit(1);
}
},
/**
* A handy extended combination of `http.request` and `https.request`.
* @param {Object} opts The same as the [http.request](http://nodejs.org/api/http.html#httpHttpRequestOptionsCallback),
* but with some extra options:
* ```js
* {
* // String or Url Object.
* url: String | Object,
*
* // Other than return `res` with `res.body`,return `body` directly.
* body: true,
*
* // Max times of auto redirect. If 0, no auto redirect.
* redirect: 0,
*
* // Timeout of the socket of the http connection.
* // If timeout happens, the promise will reject.
* // Zero means no timeout.
* timeout: 0,
*
* // The key of headers should be lowercased.
* headers: {},
*
* protocol: 'http:' or 'https:',
*
* agent: null,
*
* // Auto set "transfer-encoding" header to 'chunked' if the `reqData` is
* // stream and the 'Content-Length' header is not set.
* autoTE: true,
*
* // Set null to use buffer, optional.
* // It supports GBK, ShiftJIS etc.
* // For more info, see https://github.com/ashtuchkin/iconv-lite
* resEncoding: 'auto',
*
* // Whether to unzip gzip / deflate.
* autoUnzip: true,
*
* // It's string, object, stream or buffer, it's optional. When it's an object,
* // The request will be 'application/x-www-form-urlencoded'.
* reqData: null,
*
* // auto end the request.
* autoEndReq: true,
*
* // Writable stream.
* resPipe: null,
*
* // Handle resPipe before it's piped.
* // Its returned value will be assigned to `opts.resPipe`. So you can return
* // null to make the request resolve the `body`.
* handleResPipe: (res, resPipe) => resPipe,
*
* /// The progress of the request.
* reqProgress: (complete, total) => {},
*
* // The progress of the response.
* resProgress: (complete, total) => {},
*
* resPipeError: (res) => res.end()
* }
* ```
* And if set opts as string, it will be treated as the url.
* @return {Promise} Contains the http response object,
* it has an extra `body` property.
* You can also get the request object by using `Promise.req`.
* @example
* ```js
* let p = kit.request('http://test.com');
* p.req.on('response', (res) =>
* kit.log res.headers['content-length']
* );
* p.then((body) =>
* kit.log(body); // html or buffer
* );
*
* kit.request({
* url: {
* protocol: 'https', hostname: 'test.com',
* port: 8123, path: '/a.mp3?s=1'
* },
* body: false,
* resProgress: (complete, total) =>
* kit.log(`Progress: ${complete} / ${total}`)
* })
* .then((res) => {
* kit.log(res.body.length);
* kit.log(res.headers);
* });
*
* // Send form-data.
* let form = new require('form-data');
* form.append('image', Buffer.alloc(0), {
* filename: 'a.jpg', contentType: 'image/jpg'
* });
* form.append('key', 'value');
* kit.request({
* url: 'a.com',
* method: 'POST',
* headers: form.getHeaders(),
*
* reqData: form
* })
* .then((body) =>
* kit.log(body)
* );
* ```
*/
request(opts) {
let reqBuf;
if (_.isString(opts)) {
opts = {
url: opts
};
}
let url = opts.url || {};
if (_.isObject(url)) {
let hostSepIndex;
if (url.protocol == null) {
url.protocol = 'http:';
}
if (url.host && ((hostSepIndex = url.host.indexOf(':')) > -1)) {
url.hostname = url.host.slice(0, hostSepIndex);
url.port = url.host.slice(hostSepIndex + 1);
}
} else {
if (url.indexOf('http') !== 0) {
url = `http://${url}`;
}
url = nodeUrl.parse(url);
if (url.protocol == null) {
url.protocol = 'http:';
}
delete url.host;
}
_.defaults(opts, url);
let request = null;
switch (opts.protocol) {
case 'http:':
({
request
} = kit.require('http', __dirname));
break;
case 'https:':
({
request
} = kit.require('https', __dirname));
break;
default:
Promise.reject(new Error(`Protocol not supported: ${opts.protocol}`));
}
_.defaults(opts, {
body: true,
resEncoding: 'auto',
reqData: null,
autoEndReq: true,
autoUnzip: true,
reqProgress: null,
resProgress: null,
autoTE: true
});
if (opts.headers == null) {
opts.headers = {};
}
if (Buffer.isBuffer(opts.reqData)) {
reqBuf = opts.reqData;
} else if (_.isString(opts.reqData)) {
reqBuf = Buffer.from(opts.reqData);
} else if (_.isObject(opts.reqData)) {
if (opts.reqData && _.isFunction(opts.reqData.pipe)) {
opts.reqPipe = opts.reqData;
} else {
if (opts.headers['content-type'] == null) {
opts.headers['content-type'] =
'application/x-www-form-urlencoded; charset=utf-8';
}
reqBuf = Buffer.from(
_.map(opts.reqData, (v, k) => [encodeURIComponent(k), encodeURIComponent(v)].join('=')).join('&')
);
}
} else {
reqBuf = undefined;
}
if (reqBuf !== undefined) {
if (opts.headers['content-length'] == null) {
opts.headers['content-length'] = reqBuf.length;
}
}
if (opts.autoTE && (!('content-length' in opts.headers)) &&
(!('Content-Length' in opts.headers)) && opts.reqPipe) {
opts.headers['transfer-encoding'] = 'chunked';
}
let req = null;
const promise = new Promise(function (resolve, reject) {
let resPipeError;
req = request(opts, function (res) {
let unzip;
let resStream = res;
if ((opts.redirect > 0) && res.headers.location) {
opts.redirect--;
url = nodeUrl.resolve(
nodeUrl.format(opts),
res.headers.location
);
kit.request(_.extend(opts, nodeUrl.parse(url)))
.then(resolve)
.catch(reject);
return;
}
if (opts.resProgress) {
(function () {
const total = +res.headers['content-length'];
let complete = 0;
return resStream.on('data', function (chunk) {
complete += chunk.length;
return opts.resProgress(complete, total);
});
})();
}
if (_.isFunction(opts.handleResPipe)) {
opts.resPipe = opts.handleResPipe(res, opts.resPipe);
}
if (opts.autoUnzip) {
unzip = (() => {
switch (res.headers['content-encoding']) {
case 'gzip':
return unzip = kit.require('zlib', __dirname).createGunzip();
case 'deflate':
return unzip = kit.require('zlib', __dirname).createInflateRaw();
}
})();
if (unzip) {
let isEmptyZipPipe = true;
resStream = res.pipe(unzip);
unzip.on('data', () => isEmptyZipPipe = false);
unzip.on('error', function (err) {
// Empty pipe to gzip
if (isEmptyZipPipe) {
return resolver(buf);
} else {
return reject(err);
}
});
}
}
if (resPipeError) {
resStream.on('error', resPipeError);
}
if (opts.resPipe) {
resStream.pipe(opts.resPipe);
opts.resPipe.on('error', resPipeError);
return resStream.on('end', () => resolve(res));
} else {
var buf = Buffer.alloc(0);
resStream.on('data', chunk => buf = Buffer.concat([buf, chunk]));
var resolver = function (body) {
if (opts.body) {
return resolve(body);
} else {
res.body = body;
return resolve(res);
}
};
resStream.on('error', reject);
return resStream.on('end', function () {
if (opts.resEncoding) {
let encoding;
if (opts.resEncoding === 'auto') {
encoding = null;
const cType = res.headers['content-type'];
if (/text|javascript|css|json|xml/.test(cType)) {
encoding = 'utf8';
}
if (!opts.autoUnzip && /gzip|deflate/.test(res.headers['content-encoding'])) {
encoding = null;
}
} else {
encoding = opts.resEncoding;
}
const decode = function (buf) {
if (!encoding || !buf) {
return buf;
}
try {
if (encoding === 'utf8') {
return buf.toString();
} else {
return kit.requireOptional('iconv-lite', __dirname)
.decode(buf, encoding);
}
} catch (err) {
return reject(err);
}
};
return resolver(decode(buf));
} else {
return resolver(buf);
}
});
}
});
if (opts.resPipe) {
resPipeError = function (err) {
if (opts.resPipeError) {
opts.resPipeError(opts.resPipe);
} else {
opts.resPipe.end();
}
return reject(err);
};
}
req.on('error', function (err) {
if (opts.resPipe) {
resPipeError(err);
}
return reject(err);
});
if (opts.timeout > 0) {
req.setTimeout(opts.timeout, () => req.emit('error', new Error('timeout')));
}
if (opts.reqPipe) {
if (opts.reqProgress) {
(function () {
const total = +opts.headers['content-length'];
let complete = 0;
return opts.reqPipe.on('data', function (chunk) {
complete += chunk.length;
return opts.reqProgress(complete, total);
});
})();
}
return opts.reqPipe.pipe(req);
} else {
if (opts.autoEndReq) {
return req.end(reqBuf);
}
}
});
promise.req = req;
return promise;
},
/**
* The semantic versioner for npm, known as [semver](https://github.com/npm/node-semver).
* You must `kit.require 'semver'` before using it.
* @type {Object}
*/
semver: null,
/**
* A safer version of `child_process.spawn` to cross-platform run
* a process. In some conditions, it may be more convenient
* to use the `kit.exec`.
* It will automatically add `node_modules/.bin` to the `PATH`
* environment variable.
* @param {String} cmd Path or name of an executable program.
* @param {Array} args CLI arguments. If any of the item is an object,
* it will be converted to string by `JSON.stringify`.
* @param {Object} opts Process options.
* Almost the same with the Node.js official documentation.
* It will inherit the parent's stdio by default.
* An extra `prefix` option, if it's enabled, all stdout and stderr
* will be prefix with the specified string, you can also specify the
* color like `web:red`, `web:blue`, if no color found, a random color
* will be used.
* @return {Promise} The `promise.process` is the spawned child
* process object.
* **Resolves** when the process's stdio is drained and the exit
* code is either `0` or `130`. The resolve value
* is like:
* ```js
* {
* code: 0,
* signal: null
* }
* ```
* @example
* ```js
* kit.spawn('git', ['commit', '-m', '42 is the answer to everything'])
* .then(({code}) => kit.log code);
* ```
*/
spawn(cmd, args, opts) {
let prefix;
if (args == null) {
args = [];
}
if (opts == null) {
opts = {};
}
let PATH = (opts.env && opts.env.PATH) ?
opts.env.PATH :
process.env.PATH || process.env.Path;
[
kit.path.normalize(__dirname + '/../node_modules/.bin'),
kit.path.normalize(process.cwd() + '/node_modules/.bin')
].forEach(function (path) {
if ((PATH.indexOf(path) < 0) && kit.fs.existsSync(path)) {
return PATH = [path, PATH].join(kit.path.delimiter);
}
});
_.defaultsDeep(opts, {
stdio: 'inherit',
env: process.env
});
opts.env.PATH = PATH;
if (process.platform === 'win32') {
kit.require('whichSync');
cmd = kit.whichSync(cmd);
if (cmd.slice(-3).toLowerCase() === 'cmd') {
const cmdSrc = kit.fs.readFileSync(cmd, 'utf8');
const m = cmdSrc.match(/node\s+"%~dp0\\(\.\.\\.+)"/);
if (m && m[1]) {
cmd = kit.path.join(cmd, '..', m[1]);
cmd = kit.path.normalize(cmd);
args = [cmd].concat(args);
cmd = 'node';
}
}
}
const {
spawn
} = kit.require('child_process', __dirname);
let ps = null;
for (let k in args) {
const v = args[k];
if (_.isObject(v)) {
args[k] = JSON.stringify(v);
}
}
if (opts.prefix) {
let color;
const br = kit.require('brush');
[prefix, color] = Array.from(opts.prefix.split(':'));
if (color) {
prefix = br[color](prefix);
} else {
prefix = br.random(prefix);
}
prefix += '$&';
opts.stdio = [process.stdin, 'pipe', 'pipe'];
}
const promise = new Promise(function (resolve, reject) {
try {
ps = spawn(cmd, args, opts);
} catch (error) {
const err = error;
reject(err);
}
if (opts.prefix) {
const prefixReg = /.*\n/g;
ps.stdout.on('data', d =>
process.stdout.write(
(d + '').replace(prefixReg, prefix)
)
);
ps.stderr.on('data', d =>
process.stderr.write(
(d + '').replace(prefixReg, prefix)
)
);
}
ps.on('error', err => reject(err));
return ps.on('close', function (code, signal) {
if ((code === null) || (code === 0) || (code === 130)) {
return resolve({
code,
signal
});
} else {
return reject({
code,
signal
});
}
});
});
promise.process = ps;
return promise;
},
/**
* The `sse` module.
* You must `kit.require 'sse'` before using it.
* For more information goto the `sse` section.
*/
sse: null,
/**
* Sequencing and executing tasks and dependencies concurrently.
* @param {String} name The task name.
* @param {Object} opts Optional. Defaults:
* ```js
* {
* deps: String | Array,
* description: String,
* logStart: () => (),
* logEnd: () => (),
*
* // Whether to run dependency in a row.
* isSequential: false
* }
* ```
* @param {Function} fn `(val) -> Promise | Any` The task function.
* If it is a async task, it should return a promise.
* It will get its dependency tasks' resolved values.
* @property {Function} run Use it to start tasks. Each task will only run once.
* `(names = 'default', opts) ->`. The `names` can be a string or array.
* The default opts:
* ```js
* {
* isSequential: false,
*
* // Will be passed as the first task's argument.
* init: undefined,
*
* // To stop the run currently in process. Set the `$stop`
* // reference to true. It will reject a "runStopped" error.
* warp: { $stop: false }
* }
* ```
* @property {Object} list The defined task functions.
* @return {Promise} Resolve with the last task's resolved value.
* When `isSequential == true`, it resolves a value, else it resolves
* an array.
* @example
* ```js
* kit.task('default', { deps: 'build' }, () =>
* kit.log('run defaults...')
* );
*
* kit.task('build', { deps: ['clean'] }, (isFull) =>
* isFull ? 'do something' : 'do something else'
* );
*
* kit.task('clean', (opts) =>
* opts.isForce ?
* kit.remove('dist/**', { isForce: true }) :
* kit.remove('dist/**')
* );
*
* kit.task.run()
* .then(() =>
* kit.log('All Done!')
* );
* ```
*/
task(name, opts, fn) {
const br = kit.require('brush');
if (_.isFunction(opts)) {
fn = opts;
opts = {};
}
_.defaults(opts, {
isSequential: false,
description: '',
logStart() {
return kit.log(br.cyan('Task Start >> ') +
br.green(`[${name}] `) + this.description
);
},
logEnd() {
return kit.log(br.cyan('Task Done >> ') +
br.green(`[${name}] `) + this.description
);
}
});
if (_.isString(opts.deps)) {
opts.deps = [opts.deps];
}
if (kit.task.list == null) {
kit.task.list = {};
}
// Here we use some curry functions to deal with the race condition.
const runTask = warp => name => function (val) {
if (warp[name]) {
return warp[name];
} else {
if (!kit.task.list[name]) {
return Promise.reject(new Error(`task not found: ${name}`));
}
return warp[name] = kit.task.list[name](warp)(val);
}
};
kit.task.list[name] = warp => function (val) {
if (warp.$stop) {
return Promise.reject(new Error('runStopped'));
}
opts.logStart();
const p = ((() => {
if (!opts.deps || (opts.deps.length < 1)) {
return Promise.resolve(val);
} else {
const depTasks = opts.deps.map(runTask(warp));
if (opts.isSequential) {
return kit.flow(depTasks)(val);
} else {
return Promise.all(depTasks.map(task => task(val)));
}
}
})()).then(fn);
p.then(opts.logEnd.bind(opts));
return p;
};
kit.task.list[name].opts = opts;
return kit.task.run != null ? kit.task.run : (kit.task.run = function (names, opts) {
if (names == null) {
names = 'default';
}
if (opts == null) {
opts = {};
}
if (_.isString(names)) {
names = [names];
}
_.defaults(opts, {
isSequential: false,
init: undefined,
warp: {
$stop: false
}
});
const task = runTask(opts.warp);
if (opts.isSequential) {
return kit.flow(names.map(task))(opts.init);
} else {
return Promise.all(names.map(name => task(name)(opts.init)));
}
});
},
/**
* Cross-platform kill process tree by root process id.
* @param {Number} pid
* @param {String | Number} signal Such as 'SIGINT'
* @param {Function} callback
*/
treeKill: null,
/**
* The `url` module of node.
* You must `kit.require 'url'` before using it.
*/
url: null,
/**
* Works much like `gulp.src`, but with Promise instead.
* The warp control and error handling is more pleasant.
* @param {String} from Glob pattern string.
* @param {Object} opts It extends the options of `nofs.glob`, but
* with some extra proptereis. Defaults:
* ```js
* {
* // The base directory of the pattern.
* baseDir: String
* }
* ```
* @return {Object} The returned warp object has these members:
* ```js
* {
* // The drive can also be a promise that will resolve a drive.
* load: (drive) => fileInfo | null,
*
* run: (path) => Promise
* }
* ```
* Each piped drive will recieve a
* object that extends `nofs`'s fileInfo object:
* ```js
* {
* // Set the contents and return self.
* set: (String | Buffer) => fileInfo,
*
* // The src file path.
* path: String,
*
* // The dest root path.
* to: String,
*
* baseDir: String,
*
* // The destination path.
* // Alter it if you want to change the output file's location.
* // You can set it to string, warp will auto-convert it to object.
* // It's "valueOf" will return "kit.path.join dir, name + ext".
* dest: { root, dir, base, ext, name },
*
* // The file content.
* contents: String | Buffer,
*
* isDir: Boolean,
*
* stats: fs.Stats,
*
* // Alter it to control the left drives dynamically.
* drives: [Function],
*
* // All the globbed files.
* list: Array,
*
* driveList: Array,
*
* // The opts you passed to "kit.warp", it will be extended.
* opts: Object
* }
* ```
*
* Each drive can have a `onEnd: (fileInfo) -> Any | Promise` function,
* which will be called after a file's whole warp is ended.
*
* The drive can have a `isReader` property, which will make the drive
* override the default file reader.
*
* The drive can have a `isWriter` property, which will make the drive
* override the default file writer.
*
* If a drive overrides another, it can call `fileInfo.super()` to use it again.
* @example
* ```js
* // Define a simple workflow.
* kit.warp('src/**\/*.js')
* .load((fileInfo) =>
* fileInfo.set('/* Lisence Info *\/' + fileInfo.contents)
* )
* .load(jslint())
* .load(minify())
* .run('build/minified');
*
* // Override warp's file reader with a custom one.
* let myReader = kit._.extend((f) =>
* kit.readFile(f.path, 'hex').then(f.path)
* ), {
* // This will tell warp you want use your own reader.
* isReader: true
* });
*
* // Override writer.
* let myWriter = kit._.extend((f) => {
* if (f.dest === 'a.js') return;
*
* // Call the overrided writer.
* f.super();
* }, { isWriter: true, onEnd: () => {
* super();
* kit.log(this.list);
* });
*
* kit.warp('src/**\/*.js')
* .load(myWriter)
* .run('dist');
*
* // Use nokit's built-in warp drives.
* let drives = kit.require('drives');
* kit.warp('src/**\/*.coffee')
* .load(drives.coffee());
* .run('dist');
* ```
*/
warp(from, opts) {
let warpper;
if (opts == null) {
opts = {};
}
const drives = kit.require('drives');
const driveList = [];
let reader = drives.reader();
let writer = drives.writer();
var runDrive = drive => function (info) {
const run = function (drive) {
if (_.isString(info.dest)) {
info.dest = _.extend(kit.path.parse(info.dest), {
valueOf() {
return kit.path.join(this.dir, this.name + this.ext);
}
});
}
if (drive.super) {
info.super = () => runDrive(drive.super)(info);
}
return Promise.resolve(drive.call(info, info))
.then(() => info);
};
if (_.isFunction(drive.then)) {
return drive.then(run);
} else {
return run(drive);
}
};
const initInfo = function (info) {
if (opts.baseDir) {
info.baseDir = opts.baseDir;
}
if (info.path != null) {
info.dest = kit.path.join(info.to,
kit.path.relative(info.baseDir, info.path));
}
return _.extend(info, {
driveList,
opts,
set(contents) {
return info.contents = contents;
}
});
};
return warpper = {
load(drive) {
if (drive.isReader || drive.isWriter) {
if (drive.isWriter) {
drive.super = writer;
drive.onEnd.super = writer.onEnd;
writer = drive;
}
if (drive.isReader) {
drive.super = reader;
reader = drive;
}
} else {
driveList.push(drive);
}
return warpper;
},
run(to) {
if (to == null) {
to = '.';
}
driveList.unshift(reader);
driveList.push(writer);
const globOpts = _.extend({}, opts, {
iter(info, list) {
list.push(info);
if (opts.baseDir) {
info.baseDir = opts.baseDir;
}
_.extend(info, {
drives: _.clone(driveList),
to,
list
});
return kit.flow({
next() {
const drive = info.drives.shift();
return {
value: drive && runDrive(drive),
done: !drive
};
}
})(initInfo(info));
}
});
return kit.glob(from, globOpts)
.then(list =>
Promise.all(driveList.map(function (drive) {
if (!drive.onEnd) {
return;
}
return runDrive(drive.onEnd)(initInfo({
to,
list
}));
})));
}
};
},
/**
* Same as the unix `which` command.
* You must `kit.require 'which'` before using it.
* @param {String} name The command.
* @return {Promise}
*/
which: null,
/**
* Sync version of `which`.
* You must `kit.require 'whichSync'` before using it.
* @type {Function}
*/
whichSync: null,
/**
* For debugging. Dump a colorful object.
* @param {Object} obj Your target object.
* @param {Object} opts Options. Default:
* ```js
* { colors: true, depth: 7 }
* ```
* @return {String}
*/
xinspect(obj, opts) {
if (opts == null) {
opts = {};
}
const util = kit.require('util', __dirname);
_.defaults(opts, {
colors: kit.isDevelopment(),
depth: 7
});
return util.inspect(obj, opts);
},
/**
* Open a thing that your system can recognize.
* Now only support Windows, OSX or system that installed 'xdg-open'.
* @param {String | Array} cmds The thing you want to open.
* @param {Object} opts The options of the node native
* `child_process.exec`.
* @return {Promise} When the child process exists.
* @example
* Open a webpage with the default browser.
* ```js
* kit.open('http://ysmood.org');
* ```
*/
xopen(cmds, opts) {
if (opts == null) {
opts = {};
}
const child_process = kit.require('child_process', __dirname);
if (_.isString(cmds)) {
cmds = [cmds];
}
return (Promise.resolve((() => {
switch (process.platform) {
case 'darwin':
return 'open';
case 'win32':
child_process.exec(`start ${cmds.join(' ')}`);
return null;
default:
try {
kit.require('whichSync');
return kit.whichSync('xdg-open');
} catch (error) {
return null;
}
}
})())).then(function (starter) {
if (!starter) {
return;
}
return kit.spawn(starter, cmds);
});
}
});
module.exports = kit;