UNPKG

fabricator

Version:

Discover collections of constructible instances from strings (filepaths), arrays or objects

224 lines (191 loc) 5.99 kB
'use strict'; var path = require('path') , fs = require('fs'); /** * Expose small fabrication helper. * * Possible options: * * - source: {String} Absolute path to be used to resolve file paths. * - recursive: {Boolean} Should file paths be recursively discovered. * - name: {String} Force a name for a given constructor. * * @param {Mixed} stack String, array or object that contains constructible entities * @param {Object} options Optional options. * @returns {Array} collection of constructors. * @api public */ function fabricator(stack, options) { options = options || {}; // // Empty strings, arrays or objects should not be processed, return early. // if (empty(stack)) return []; switch (is(stack)) { case 'string': stack = read(stack, options); break; case 'object': stack = Object.keys(stack).reduce(iterator(read, stack, options), []); break; case 'array': stack = stack.reduce(iterator(read, null, options), []); break; default: if ('function' !== typeof stack) { throw new Error('Unsupported type, cannot fabricate an: '+ is(stack)); } stack = [init(stack, undefined, options)]; } return (stack || []).filter(Boolean); } /** * Read directory and initialize JavaScript files. * * @param {String} filepath Full directory path. * @param {Object} options Additional configuration. * @return {Array} collection of constructor * @api private */ function read(filepath, options) { if ('string' !== is(filepath)) return fabricator(filepath, options); if (options.source) filepath = path.resolve(options.source, filepath); // // Check if the provided string is a JS file or when recursion is not allowed. // if (js(filepath) || options.recursive === false) return [ init(filepath, path.basename(filepath, '.js'), options) ]; // // Read the directory, only process files. // if (!fs.existsSync(filepath)) return false; return fs.readdirSync(filepath).map(function locate(file) { file = path.resolve(filepath, file); var stat = fs.statSync(file); if (stat.isDirectory() && fs.existsSync(path.join(file, 'index.js'))) { // // Use the directory name instead of `index` for name as it probably has // more meaning then just `index` as a name. // return init(path.join(file, 'index.js'), path.basename(file, '.js'), options); } // // Only allow JS files, init determines if it is a constructible instance. // if (!stat.isFile() || !js(file)) return; return init(file, path.basename(file, '.js'), options); }); } /** * Return iterator for array or object. * * @param {Function} traverse Recursive iterator, called on directories. * @param {Object} obj Original object, if set values are fetched by entity. * @param {Object} options Configuration. * @return {Function} iterator * @api private */ function iterator(traverse, obj, options) { return function reduce(stack, entity) { var base = obj ? obj[entity] : entity , name = options.name || entity; // // Fabricated objects should provide each constructor with the name // of its property on the original object. // if (obj) options.name = entity; // // Run the functions, traverse will handle init. // if (js(base)) { return stack.concat(init( base, 'string' === is(name) ? name : '', options )); } // // When we've been supplied with an array as base assume we want to keep it // as array and do not want it to be merged. // if (Array.isArray(base)) { options.name = name; // Force the name of the entry for all items in array. stack.push(traverse(base, options)); return stack; } return stack.concat(traverse(base, options)); }; } /** * Make sure only valid JavaScript files are used as source. Ignore other files, * like .log files. Also allow constructors. * * @param {String|Function} file Path or constructor function. * @returns {Boolean} allow entity to be used or not. * @api private */ function js(file) { var type = is(file); return 'function' === type || 'string' === type && path.extname(file) === '.js'; } /** * A better alternative to `typeof` checks by trying to figure out the root * class of things. This eliminates the needs for Array.is checks when the type * is an object etc. * * @param {Mixed} obj Unknown thing we need to know. * @returns {String} * @api private */ function is(obj) { return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); } /** * Simple check to see if the provided stack is empty or falsy. * * @param {Mixed} value Entity to check * @return {Boolean} result of check * @api private */ function empty(value) { if (!value) return true; switch (is(value)) { case "object": return !Object.keys(value).length; case "array": return !value.length; default: return !value; } } /** * It's not required to supply resolve with instances, we can just * automatically require them if they are using the: * * module.exports = function Constructor() {}; * * @param {String} constructor * @param {String} name Optional identifier for the constructor. * @returns {Object} initialized object * @api private */ function init(constructor, name, options) { constructor = ('string' === is(constructor)) ? require(constructor) : constructor; // // We really want to have a function/class here. Make sure that we can // construct it using `new constructor` // if (!constructor.prototype) return; name = constructor.prototype.name || name || constructor.name; if (options.name) name = options.name; // // Sets the name on the prototype to a string. // if ('name' in constructor.prototype) { constructor.prototype.name = name.toString(); } return constructor; } // // Expose the module. // module.exports = fabricator;