Skip to content

Commit

Permalink
feat: Add option to load config in sync mode (#67)
Browse files Browse the repository at this point in the history
* feat: Add option to load config in sync mode

* docs: Clarify usage for transform func in async, sync

* refactor: Use funcRunner for chaining funcs and avoid code repetition

* chore: Correct comment in creteExplorer for cache

* docs: Add returns annotation for funcRunner

* refactor: Set func name to funcRunner

* test: Add tests for funcRunner

* test: rm mysterious test failing on node <= 4
  • Loading branch information
sudo-suhas authored and davidtheclark committed Jul 18, 2017
1 parent 334065d commit bed5dc0
Show file tree
Hide file tree
Showing 18 changed files with 1,262 additions and 485 deletions.
20 changes: 16 additions & 4 deletions README.md
Expand Up @@ -14,7 +14,7 @@ For example, if your module's name is "soursocks," cosmiconfig will search out c
- a `soursocks.config.js` file exporting a JS object (anywhere down the file tree)
- a CLI `--config` argument

cosmiconfig continues to search in these places all the way down the file tree until it finds acceptable configuration (or hits the home directory). And it does all this asynchronously, so it shouldn't get in your way.
cosmiconfig continues to search in these places all the way down the file tree until it finds acceptable configuration (or hits the home directory). And it does all this asynchronously by default, so it shouldn't get in your way.

Additionally, all of these search locations are configurable: you can customize filenames or turn off any location.

Expand Down Expand Up @@ -49,6 +49,8 @@ explorer.load(yourSearchPath)
The function `cosmiconfig()` searches for a configuration object and returns a Promise,
which resolves with an object containing the information you're looking for.

You can also pass option `sync: true` to load the config synchronously.

So let's say `var yourModuleName = 'goldengrahams'` — here's how cosmiconfig will work:

- Starting from `process.cwd()` (or some other directory defined by the `searchPath` argument to `load()`), it looks for configuration objects in three places, in this order:
Expand Down Expand Up @@ -168,11 +170,21 @@ Default: `true`

If `false`, no caches will be used.

##### sync

Type: `boolean`
Default: `false`

If `true`, config will be loaded synchronously.

##### transform

Type: `Function` returning a Promise
Type: `Function`

A function that transforms the parsed configuration. Receives the result object with `config` and `filepath` properties.

A function that transforms the parsed configuration. Receives the result object with `config` and `filepath` properties, and must return a Promise that resolves with the transformed result.
If the option `sync` is `false`(default), the function must return a Promise that resolves with the transformed result.
Otherwise, `transform` should be a synchronous function which returns the transformed result.

The reason you might use this option instead of simply applying your transform function some other way is that *the transformed result will be cached*. If your transformation involves additional filesystem I/O or other potentially slow processing, you can use this option to avoid repeating those steps every time a given configuration is loaded.

Expand Down Expand Up @@ -217,7 +229,7 @@ Performs both `clearFileCache()` and `clearDirectoryCache()`.
- Built-in support for JSON, YAML, and CommonJS formats.
- Stops at the first configuration found, instead of finding all that can be found down the filetree and merging them automatically.
- Options.
- Asynchronicity.
- Asynchronous by default, can be forced to use synchronous mode.

## Contributing & Development

Expand Down
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -17,6 +17,7 @@ module.exports = function (moduleName, options) {
rcStrictJson: false,
stopDir: oshomedir(),
cache: true,
sync: false,
}, options);

if (options.argv && parsedCliArgs[options.argv]) {
Expand Down
90 changes: 54 additions & 36 deletions lib/createExplorer.js
Expand Up @@ -6,12 +6,14 @@ var loadPackageProp = require('./loadPackageProp');
var loadRc = require('./loadRc');
var loadJs = require('./loadJs');
var loadDefinedFile = require('./loadDefinedFile');
var funcRunner = require('./funcRunner');

module.exports = function (options) {
// When `options.sync` is `false`(default),
// These cache Promises that resolve with results, not the results themselves
var fileCache = (options.cache) ? new Map() : null;
var directoryCache = (options.cache) ? new Map() : null;
var transform = options.transform || identityPromise;
var transform = options.transform || (options.sync) ? identitySync : identityPromise;

function clearFileCache() {
if (fileCache) fileCache.clear();
Expand All @@ -32,56 +34,68 @@ module.exports = function (options) {
if (fileCache && fileCache.has(absoluteConfigPath)) {
return fileCache.get(absoluteConfigPath);
}
var result = loadDefinedFile(absoluteConfigPath, options)
.then(transform);
var result = (!options.sync)
? loadDefinedFile(absoluteConfigPath, options)
.then(transform)
: transform(loadDefinedFile(absoluteConfigPath, options));
if (fileCache) fileCache.set(absoluteConfigPath, result);
return result;
}

if (!searchPath) return Promise.resolve(null);
if (!searchPath) return (!options.sync) ? Promise.resolve(null) : null;

var absoluteSearchPath = path.resolve(process.cwd(), searchPath);

return isDirectory(absoluteSearchPath)
.then(function (searchPathIsDirectory) {
var directory = (searchPathIsDirectory)
return (!options.sync)
? isDirectory(absoluteSearchPath)
.then(function (searchPathIsDirectory) {
var directory = (searchPathIsDirectory)
? absoluteSearchPath
: path.dirname(absoluteSearchPath);
return searchDirectory(directory);
})
: searchDirectory(
isDir.sync(absoluteSearchPath)
? absoluteSearchPath
: path.dirname(absoluteSearchPath);
return searchDirectory(directory);
});
: path.dirname(absoluteSearchPath)
);
}

function searchDirectory(directory) {
if (directoryCache && directoryCache.has(directory)) {
return directoryCache.get(directory);
}

var result = Promise.resolve()
.then(function () {
if (!options.packageProp) return;
return loadPackageProp(directory, options);
})
.then(function (result) {
if (result || !options.rc) return result;
return loadRc(path.join(directory, options.rc), options);
})
.then(function (result) {
if (result || !options.js) return result;
return loadJs(path.join(directory, options.js));
})
.then(function (result) {
if (result) return result;

var splitPath = directory.split(path.sep);
var nextDirectory = (splitPath.length > 1)
? splitPath.slice(0, -1).join(path.sep)
: null;

if (!nextDirectory || directory === options.stopDir) return null;

return searchDirectory(nextDirectory);
})
.then(transform);
var result = funcRunner(
!options.sync ? Promise.resolve() : undefined,
[
function () {
if (!options.packageProp) return;
return loadPackageProp(directory, options);
},
function (result) {
if (result || !options.rc) return result;
return loadRc(path.join(directory, options.rc), options);
},
function (result) {
if (result || !options.js) return result;
return loadJs(path.join(directory, options.js), options);
},
function (result) {
if (result) return result;

var splitPath = directory.split(path.sep);
var nextDirectory = (splitPath.length > 1)
? splitPath.slice(0, -1).join(path.sep)
: null;

if (!nextDirectory || directory === options.stopDir) return null;

return searchDirectory(nextDirectory);
},
transform,
]
);

if (directoryCache) directoryCache.set(directory, result);
return result;
Expand All @@ -107,3 +121,7 @@ function isDirectory(filepath) {
function identityPromise(x) {
return Promise.resolve(x);
}

function identitySync(x) {
return x;
}
25 changes: 25 additions & 0 deletions lib/funcRunner.js
@@ -0,0 +1,25 @@
'use strict';

var isPromise = require('is-promise');

/**
* Runs given functions. If the `init` param is a promise, functions are
* chained using `p.then()`. Otherwise, functions are chained by passing
* the result of each function to the next.
*
* @param {*} init
* @param {Array<Function>} funcs
* @returns {*} A promise if `init` was one, otherwise result of function
* chain execution.
*/
module.exports = function funcRunner(init, funcs) {
var async = isPromise(init);

var res = init;
funcs.forEach(function (func) {
if (async === true) res = res.then(func);
else res = func(res);
});

return res;
};
9 changes: 7 additions & 2 deletions lib/loadDefinedFile.js
Expand Up @@ -6,7 +6,7 @@ var readFile = require('./readFile');
var parseJson = require('./parseJson');

module.exports = function (filepath, options) {
return readFile(filepath, { throwNotFound: true }).then(function (content) {
function parseContent(content) {
var parsedConfig = (function () {
switch (options.format) {
case 'json':
Expand All @@ -32,7 +32,12 @@ module.exports = function (filepath, options) {
config: parsedConfig,
filepath: filepath,
};
});
}

return (!options.sync)
? readFile(filepath, { throwNotFound: true })
.then(parseContent)
: parseContent(readFile.sync(filepath, { throwNotFound: true }));
};

function tryAllParsing(content, filepath) {
Expand Down
10 changes: 7 additions & 3 deletions lib/loadJs.js
Expand Up @@ -3,13 +3,17 @@
var requireFromString = require('require-from-string');
var readFile = require('./readFile');

module.exports = function (filepath) {
return readFile(filepath).then(function (content) {
module.exports = function (filepath, options) {
function parseJsFile(content) {
if (!content) return null;

return {
config: requireFromString(content, filepath),
filepath: filepath,
};
});
}

return (!options.sync)
? readFile(filepath).then(parseJsFile)
: parseJsFile(readFile.sync(filepath));
};
8 changes: 6 additions & 2 deletions lib/loadPackageProp.js
Expand Up @@ -7,7 +7,7 @@ var parseJson = require('./parseJson');
module.exports = function (packageDir, options) {
var packagePath = path.join(packageDir, 'package.json');

return readFile(packagePath).then(function (content) {
function parseContent(content) {
if (!content) return null;
var parsedContent = parseJson(content, packagePath);
var packagePropValue = parsedContent[options.packageProp];
Expand All @@ -17,5 +17,9 @@ module.exports = function (packageDir, options) {
config: packagePropValue,
filepath: packagePath,
};
});
}

return (!options.sync)
? readFile(packagePath).then(parseContent)
: parseContent(readFile.sync(packagePath));
};
42 changes: 32 additions & 10 deletions lib/loadRc.js
Expand Up @@ -4,16 +4,21 @@ var yaml = require('js-yaml');
var requireFromString = require('require-from-string');
var readFile = require('./readFile');
var parseJson = require('./parseJson');
var funcRunner = require('./funcRunner');

module.exports = function (filepath, options) {
return loadExtensionlessRc().then(function (result) {
function afterLoadExtensionlessRc(result) {
if (result) return result;
if (options.rcExtensions) return loadRcWithExtensions();
return null;
});
}

return (!options.sync)
? loadExtensionlessRc().then(afterLoadExtensionlessRc)
: afterLoadExtensionlessRc(loadExtensionlessRc());

function loadExtensionlessRc() {
return readRcFile().then(function (content) {
function parseExtensionlessRcFile(content) {
if (!content) return null;

var pasedConfig = (options.rcStrictJson)
Expand All @@ -25,11 +30,15 @@ module.exports = function (filepath, options) {
config: pasedConfig,
filepath: filepath,
};
});
}

return (!options.sync)
? readRcFile().then(parseExtensionlessRcFile)
: parseExtensionlessRcFile(readRcFile());
}

function loadRcWithExtensions() {
return readRcFile('json').then(function (content) {
function parseJsonRcFile(content) {
if (content) {
var successFilepath = filepath + '.json';
return {
Expand All @@ -40,7 +49,9 @@ module.exports = function (filepath, options) {
// If not content was found in the file with extension,
// try the next possible extension
return readRcFile('yaml');
}).then(function (content) {
}

function parseYmlRcFile(content) {
if (content) {
// If the previous check returned an object with a config
// property, then it succeeded and this step can be skipped
Expand All @@ -53,7 +64,9 @@ module.exports = function (filepath, options) {
};
}
return readRcFile('yml');
}).then(function (content) {
}

function parseYamlRcFile(content) {
if (content) {
if (content.config) return content;
var successFilepath = filepath + '.yml';
Expand All @@ -63,7 +76,9 @@ module.exports = function (filepath, options) {
};
}
return readRcFile('js');
}).then(function (content) {
}

function parseJsRcFile(content) {
if (content) {
if (content.config) return content;
var successFilepath = filepath + '.js';
Expand All @@ -73,13 +88,20 @@ module.exports = function (filepath, options) {
};
}
return null;
});
}

return funcRunner(
readRcFile('json'),
[parseJsonRcFile, parseYmlRcFile, parseYamlRcFile, parseJsRcFile]
);
}

function readRcFile(extension) {
var filepathWithExtension = (extension)
? filepath + '.' + extension
: filepath;
return readFile(filepathWithExtension);
return (!options.sync)
? readFile(filepathWithExtension)
: readFile.sync(filepathWithExtension);
}
};

0 comments on commit bed5dc0

Please sign in to comment.