issue
Version:
Command line tool for displaying issues using the issuemd library
413 lines (339 loc) • 14.3 kB
JavaScript
/**
* issue-config module
*
* main purpose is to layer `.issurc` config files, and command line options to generate app config
* also, exposes api for main app, and plugins to interact with app config for reading and writing
*
* Usage:
*
* config() // get fully layered config (all config files, command line args, and auto generated config)
* config('deeply.nested.item') // get value for single dot notation specified key
* config('deeply.nested.item','value') // set value for single dot notation specified key
* config('deeply.nested.item','value', true) // set value of home config for single dot notation specified key
* config('deeply.nested.item',{full:'object'}) // set value as object for single dot notation specified key
* config('item', null) // remove item specified by key
* config('item', null, true) // remove item from home config specified by key
*
*/
;
! function () {
var path = require('path'),
fs = require('fs'),
_ = require('underscore'),
underscoreDeepExtend = require('underscore-deep-extend');
var argv;
_.mixin({
deepExtend: underscoreDeepExtend(_)
});
module.exports = function () {
var internalConfig,
persistedPath = process.cwd(),
persistedBase = null;
var config = function (key, value, home) {
if (!!key && typeof value !== 'undefined') {
if (value === null) {
return remove(key, home);
} else {
return add(key, value, home);
}
} else if (!!key) {
return read(key);
} else {
return getConfig();
}
};
config.list = list;
config.init = init;
if (process.env.TESTING) {
config.read = read;
config.add = add;
config.remove = remove;
config.getConfig = getConfig;
config.setPersistedPathBase = setPersistedPathBase;
config.getFileConfig = getFileConfig;
config.getConfigFiles = getConfigFiles;
config.getJSONFromFiles = getJSONFromFiles;
config.getLocalConfig = getLocalConfig;
config.setFromDotNotation = setFromDotNotation;
config.getPaths = getPaths;
config.parseArgv = parseArgv;
config.getParams = getParams;
config.configObjectFromParamsOptions = configObjectFromParamsOptions;
}
return config;
/*** utility functions placed inside `module.exports` enclosure to gain access to `utils` and `argv` ***/
function init(argvIn) {
argv = argvIn;
internalConfig = false;
return config;
}
function setPersistedPathBase(pathIn, baseIn) {
// if paths change, internal config must be re-calculated
internalConfig = false;
persistedPath = pathIn || process.cwd();
persistedBase = baseIn || null;
}
function getPaths() {
var paths = [],
currentPath = path.resolve(persistedPath),
nextPath = currentPath;
// 99 chosen as adequite depth to ascend through folder tree
for (var i = 0; i < 99; i++) {
currentPath = nextPath;
paths.push(path.relative(process.cwd(), currentPath));
nextPath = path.join(currentPath, '..');
if ((currentPath === nextPath) || persistedBase && currentPath === path.resolve(persistedBase)) {
break;
}
}
if (process.platform === 'win32') {
paths.push(process.env.USERPROFILE);
}
return paths;
}
function getConfigFiles() {
var configs = [];
_.each(getPaths(), function (mypath) {
if (fs.existsSync(path.join(mypath, '.issuerc'))) {
configs.push(path.join(mypath, '.issuerc'));
}
});
return configs;
}
function getAutoConfig() {
var ini = require('ini');
var myConfig = {};
_.each(getPaths(), function (mypath) {
if (!(myConfig.git && myConfig.git.projectPath) && fs.existsSync(path.join(mypath, '.git/config'))) {
myConfig.git = myConfig.git || {};
myConfig.git.projectPath = mypath;
var gitConfig = ini.parse(fs.readFileSync(path.join(mypath, '.git/config'), 'utf-8'));
if (gitConfig['remote "origin"']) {
if (gitConfig['remote "origin"'].url) {
myConfig.git.remote = gitConfig['remote "origin"'].url;
}
}
}
});
myConfig = _.defaults(myConfig, {
// subtract one form column width on windows to avoid insertion of empty lines
// See here for more CLI window size hints: http://stackoverflow.com/a/15854865/665261
width: process.stdout.columns - (process.platform === 'win32' ? 1 : 0)
});
return myConfig;
}
function getJSONFromFiles(files) {
var jsons = [];
_.each(files, function (file) {
var fileJson = fs.readFileSync(file).toString('UTF-8');
try {
jsons.push(JSON.parse(fileJson));
} catch (e) {
throw new Error('failed to parse json from file: ' + file);
}
});
return jsons;
}
function getFileConfig() {
return _.defaults.apply(null, getJSONFromFiles(getConfigFiles()));
}
function setFromDotNotation(configIn, pathIn, valueIn) {
var myPath = pathIn.split('.'),
destination = configIn,
value;
for (var i = 0; i < myPath.length - 1; i++) {
destination = (destination[myPath[i]]) ? destination[myPath[i]] : destination[myPath[i]] = {};
}
try {
var parsed = JSON.parse(valueIn);
value = _.isObject(parsed) ? _.deepExtend({}, destination[myPath[myPath.length - 1]], parsed) : parsed;
} catch (e) {
value = valueIn;
}
destination[myPath[myPath.length - 1]] = value;
return configIn;
}
// inspired by: http://tokenposts.blogspot.com.au/2012/04/javascript-objectkeys-browser.html
function objectKeys(object) {
var keys = [],
property;
for (property in object) {
if (Object.prototype.hasOwnProperty.call(object, property)) {
keys.push(property);
}
}
return keys;
}
function removeFromDotNotation(configIn, pathIn) {
var myPath = pathIn.split('.'),
destination = configIn;
for (var i = 0; i < myPath.length - 1; i++) {
destination = (destination[myPath[i]]) ? destination[myPath[i]] : destination[myPath[i]] = {};
}
delete destination[myPath[myPath.length - 1]];
// if removing path leaves object empty, remove the parent (recursively)
if (objectKeys(destination).length === 0) {
removeFromDotNotation(configIn, myPath.slice(0, -1).join('.'));
}
return configIn;
}
function parseArgv(argvIn) {
var consuming = false,
out = {
params: [],
options: {}
};
function sanitize(input) {
var value;
switch (input) {
case 'true':
case 'on':
value = true;
break;
case 'null':
value = null;
break;
case 'false':
case 'off':
value = false;
break;
default:
value = input;
break;
}
return value;
}
argvIn.slice(2).forEach(function (input) {
var matches = input.match(/^-?-(\w.*)/);
if (matches) {
if (consuming) {
out.options = setFromDotNotation(out.options, consuming, true);
}
consuming = matches[1];
} else if (consuming) {
out.options = setFromDotNotation(out.options, consuming, sanitize(input));
consuming = false;
} else {
out.params.push(sanitize(input));
}
});
// if last option does not explicitly set value, assume true
if (consuming) {
out.options = setFromDotNotation(out.options, consuming, true);
}
return out;
}
function getParams() {
return parseArgv(argv);
}
function configObjectFromParamsOptions(optionsIn) {
var localConfig = {};
_.each(optionsIn, function (value, key) {
setFromDotNotation(localConfig, key, value);
});
return localConfig;
}
function deleteNull(myObject) {
for (var i in myObject) {
if (myObject[i] === null) {
delete myObject[i];
} else if (typeof myObject[i] === 'object') {
deleteNull(myObject[i]);
}
}
}
function getConfig() {
if (!internalConfig) {
var paramsConfig = configObjectFromParamsOptions(getParams());
paramsConfig.options.params = paramsConfig.params;
var configs = [
getAutoConfig(),
paramsConfig.options
].concat(
getJSONFromFiles(getConfigFiles())
);
configs = getJSONFromFiles(getConfigFiles()).reverse();
configs.unshift(paramsConfig.options);
configs.unshift(getAutoConfig());
internalConfig = _.deepExtend.apply(null, configs);
internalConfig = _.deepExtend(internalConfig, paramsConfig.options);
if (internalConfig.project && internalConfig.projects[internalConfig.project]) {
internalConfig = _.deepExtend(internalConfig, internalConfig.projects[internalConfig.project]);
}
deleteNull(internalConfig);
}
return internalConfig;
}
function getLocalConfig() {
return getJSONFromFiles(getConfigFiles())[0] || {};
}
function getHomeConfigFilename() {
// heavily inspired by: https://github.com/npm/osenv/blob/769ada6737026254372e3013b702c450a9b781e9/osenv.js#L52
return path.join((process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME), '.issuerc');
}
// from: http://stackoverflow.com/a/13218838/665261
function list(configIn) {
var myConfig = configIn || config();
var res = [];
function recurse(myConfig, current) {
var newKey, value;
for (var key in myConfig) {
value = myConfig[key];
if (!!current) {
if (/^\d+$/.test(key)) {
newKey = current + '[' + key + ']';
} else {
newKey = current + '.' + key;
}
} else {
newKey = key;
}
if (value && typeof value === 'object') {
recurse(value, newKey); // it's a nested object, so do it again
} else {
res.push(newKey + '=' + value);
}
}
}
recurse(myConfig);
return res.join('\n');
}
/* Dot notation object read/write (for config) */
// heavily inspired by: http://stackoverflow.com/a/9338230/665261
function read(pathIn) {
var myConfig = configObjectFromParamsOptions(getConfig());
var myPath = pathIn.split('.');
for (var i = 0; i < myPath.length; i++) {
myConfig = typeof myConfig[myPath[i]] !== 'undefined' ? myConfig[myPath[i]] : myConfig[myPath[i]] = {};
}
var x = {};
x[pathIn] = myConfig;
return typeof myConfig === 'object' ? list(x) : pathIn + '=' + myConfig;
}
function add(pathIn, value, home) {
var myConfig = !!home ? getJSONFromFiles([getHomeConfigFilename()])[0] : getLocalConfig();
setFromDotNotation(myConfig, pathIn, value);
if (internalConfig) {
setFromDotNotation(internalConfig, pathIn, value);
}
return write(myConfig, home);
}
function remove(pathIn, home) {
var myConfig = !!home ? getJSONFromFiles([getHomeConfigFilename()])[0] : getLocalConfig();
removeFromDotNotation(myConfig, pathIn);
if (internalConfig) {
removeFromDotNotation(internalConfig, pathIn);
}
return write(myConfig, home);
}
function write(myConfig, home) {
var currentConfigFile = home ? getHomeConfigFilename() : getConfigFiles()[0];
try {
fs.writeFileSync(currentConfigFile, JSON.stringify(myConfig, null, 4));
return 0;
} catch (e) {
return 1;
}
}
}();
}.call();