'use strict';
/**
* @fileOverview The base-resolver/configurations module is used for storing, managing and getting resolver
* configurations. The configurations are passed to the resolver during its construction, either as an array, or as a
* path, requiring which would return an array. If a path is provided, the configuration array is retrieved by the
* {@link module:base-resolver} before feeding to this module.
* @module base-resolver/configurations
* @requires base-resolver/utils
* @requires {@link external:base-logger}
*/
var Q = require('q');
var path = require('path');
var fs = require('fs');
var utils = require('./utils');
var stat = Q.denodeify(fs.stat);
var writeFile = Q.denodeify(fs.writeFile);
var logger = require('base.logger')('RESOLVER/configurations');
/**
* Creates a ConfigurationManager and returns it
* @returns {module:base-resolver/configurations~ConfigurationManager} the configuration manager for managing
* configuration
*/
module.exports = function () {
var configurationsMap = {}, startups = [], /**
* A configurations manager manages the lifecycle of one configuration array for dependency injection in
* resolver.
* @namespace module:base-resolver/configurations~ConfigurationManager
*/
configurator = {
/**
* The setup function configures the ConfigurationManager with an Array of
* {@link module:base-resolver~Configuration} objects.
* @param {Array.<module:base-resolver~Configuration|string>} configArray - the array of configurations to
* use
* @param {string} baseDir - the directory relative to which the component paths should be resolved.
* @returns {external:q} a Promise that gets resolved with this
* {@link base-resolver/configurations~ConfigurationManager} when the setup is complete. This promise is
* rejected with the error if reload execution fails.
* @memberof module:base-resolver/configurations~ConfigurationManager
* @see module:base-resolver~Configuration
*/
setup: function (configArray, baseDir) {
// create a clone of the config array for use. Since, the configuration is not read but required. There
// will otherwise be only one global copy which anyone could change
configArray = utils.clone(configArray);
return Q.all(
configArray.map(
function (config) {
// normalize the configuration so that all of them follow the same syntax
if (typeof config === 'string') {
config = {
path: config
};
}
return Q.Promise(
function (resolve, reject) {
// first get the absolute module path
var resolvedPath, normalizedPath, npmInstall;
if (config.path.indexOf('.') === 0 || config.path.split(/\//).length > 2) {
// if the path is relative and in the local drive, just make the path absolute
logger.debug('fetching component from relative path:', config.path);
resolvedPath = path.resolve(config.path);
normalizedPath = path.normalize(config.path);
if (resolvedPath === normalizedPath) {
// if the path is absolute, we just keep it as is
config.path = normalizedPath;
} else {
// if the path is relative, we make it absolute.
config.path = path.join(baseDir, config.path);
}
return resolve(config.path);
}
// if path does not start with a ., or contains only one / the component is assumed to
// be an npm module or git repo which can be fetched using npm install
logger.debug('fetching component using npm install:', config.path);
logger.debug('installing at path:', baseDir);
// first check if there is a package.json file in the baseDir
Q.Promise(
function (resolve) {
fs.exists(path.join(baseDir, 'package.json'), resolve);
}
).then(
function (exists) {
if (exists) {
return true;
}
// if the file is not there, we create it
return writeFile(
path.join(baseDir, 'package.json'),
JSON.stringify({
name: 'integration-' + parseInt(Math.random() * 9999999999, 10)
})
);
}
).then(
function () {
// once the package.json file is ensured, install the dependencies
npmInstall = require('child_process').exec(
'npm install ' + config.path, {
cwd: baseDir
}
);
npmInstall.stdout.on(
'data', function (data) {
logger.debug(data);
}
);
npmInstall.stderr.on(
'data', function (data) {
logger.warn(data);
}
);
npmInstall.on(
'close', function (code) {
if (code === 0) {
logger.debug('installed', config.path, '...');
config.path = path.join(
baseDir, 'node_modules', config.path.split(/\//).pop()
);
logger.debug('\t at', config.path);
resolve(config.path);
} else {
logger.error('npm install exited with code', code);
reject(new Error('npm install exited with code ' + code));
}
}
);
npmInstall.on(
'error', function (err) {
logger.error('npm install failed with error:', err);
reject(err);
}
);
}
);
}
).then(
function () {
// then we normalize the factory function
var factoryFunction, componentObject;
logger.debug('normalizing the factory functions for:', config.path, '...');
config.factory = require(config.path);
//noinspection JSHint
if (config.native) {
// if the node module is native, just return it from the factory function
logger.debug('\tfactory is a native component.');
componentObject = config.factory;
config.factory = [
function () {
return componentObject;
}
];
config.factory['zest-component'] = componentObject['zest-component'];
} else if (typeof config.factory === 'function') {
// if factory is a normal function, change it to an injector array
logger.debug('\tfactory is a normal function.');
factoryFunction = config.factory;
config.factory = utils.getDependencies(factoryFunction);
logger.debug('\tdependencies:', config.factory);
config.factory.push(factoryFunction);
config.factory['zest-component'] = factoryFunction['zest-component'];
} else if (!utils.isInjectorArray(config.factory)) {
// if component returns an object, return it from the factory function
logger.debug('\tfactory is an object.');
componentObject = config.factory;
config.factory = [
function () {
return componentObject;
}
];
config.factory['zest-component'] = componentObject['zest-component'];
} else {
// if component is an injector array, don't do anything
logger.debug('\tfactory is an injector array. No normalization required.');
logger.debug('\tdependencies:', config.factory.slice(0, -1).join(', '));
}
}
).then(
function () {
// we get the path details to figure if it is a file or directory
return stat(config.path).then(
function (stats) {
return stats;
}, function () {
// an error will come if the component is a file and it is included without an
// extension
return undefined;
}
);
}
).then(
function (stat) {
var packageDetails;
if (stat && stat.isDirectory()) {
try {
// if the path is a directory, try to find the package.json file in the
// directory
packageDetails = require(path.join(config.path, 'package.json'));
// if zest-component is present in package.json, use it
if (packageDetails['zest-component']) {
return packageDetails['zest-component'];
}
// if the component is native, use the name specified in package.json
if (config.native && packageDetails.name) {
return packageDetails.name;
}
} catch (ignore) {
// do nothing here
}
}
// if name cannot be found in package.json, check if the factory has zest-component
// property
if (config.factory['zest-component']) {
return config.factory['zest-component'];
}
// if the component is native and name cannot be retrieved from package.json, pick the
// base name form the path and use it
if (config.native) {
return path.basename(config.path, path.extname(config.path));
}
// if all attempts to find a component name fails, create one!
return 'component-' + parseInt(Math.random() * 9999999999, 10);
}
).then(
function (packageName) {
// finally set the package configuration
logger.debug('resolved package name:', packageName);
logger.debug('\tfor', config.path);
configurationsMap[packageName] = config;
if (config.startup) {
// if we have a startup component, add its name in the startup list
startups.push(packageName);
}
}
);
// if we have a startup component, add its name in the startup list
}
)
).then(
function () {
// finally return the configurator object
return configurator;
}
);
},
/**
* This function gets the configuration corresponding to a dependency name
* @param {string} dependencyName - the dependency name for which we need the configuration
* @returns {*} the configuration object corresponding to the dependnecyName
* @memberof module:base-resolver/configurations~ConfigurationManager
* @see module:base-resolver~Configuration
*/
get: function (dependencyName) {
logger.debug('fetching configuration for:', dependencyName);
return configurationsMap[dependencyName];
},
/**
* The function lists all startup dependencies for the module.
* @returns {Array.<string>} list of dependency names to be resolved
* @memberof module:base-resolver/configurations~ConfigurationManager
* @see module:base-resolver~Configuration
*/
startupDependencies: function () {
logger.debug('fetching startup dependencies...');
logger.debug('\tfound:', startups);
return startups;
}
};
return configurator;
};