var path = require('path');
var slasp = require('slasp');
var semver = require('semver');
var nijs = require('nijs');
var Source = require('./sources/Source.js').Source;
var inherit = require('nijs/lib/ast/util/inherit.js').inherit;
/**
* Creates a new package object.
*
* @class Package
* @extends NixASTNode
* @classdesc A representation of an NPM package that is obtained from an external source,
* that may have dependencies on other packages, and may bundle packages in its
* node_modules/ sub folder.
*
* @constructor
* @param {DeploymentConfig} deploymentConfig An object capturing global deployment settings
* @param {Object} lock Contents of a package lock file (or undefined if no lock exists)
* @param {Package} parent Reference to the package that embeds the constructed
* package, or null if there is no parent
* @param {String} name Name of a Node.js package
* @param {String} versionSpec Version specifier of a Node.js package, such as
* an exact version number, version range, URL, or GitHub identifier
* @param {String} baseDir Directory in which the referrer's package.json configuration resides
* @param {Boolean} production Indicates whether we deploy in production mode or
development mode. In development mode, also the development dependencies
will be included.
* @param {SourcesCache} sourcesCache Cache that contains references to all sources that need to be obtained
*/
function Package(deploymentConfig, lock, parent, name, versionSpec, baseDir, production, sourcesCache) {
this.deploymentConfig = deploymentConfig;
this.lock = lock;
this.parent = parent;
this.production = production;
this.sourcesCache = sourcesCache;
this.source = Source.constructSource(deploymentConfig.registries, baseDir, deploymentConfig.outputDir, name, versionSpec, deploymentConfig.stripOptionalDependencies);
this.requiredDependencies = {};
this.providedDependencies = {};
}
/* Package inherits from NixASTNode */
inherit(nijs.NixASTNode, Package);
/**
* Recursively checks the enclosing parent packages to see whether a dependency
* exists that fits within the required version range.
*
* @method
* @param {String} name Name of a Node.js package
* @param {String} versionSpec Version specifier of a Node.js package, such as
* an exact version number, version range, URL, or GitHub identifier
* @return {Package} The nearest parent package matching the version specifier
* or null if no such package exists
*/
Package.prototype.findMatchingProvidedDependencyByParent = function(name, versionSpec) {
if(this.parent === null) { // If there is no parent, then we can also not provide a dependency
return null;
} else {
var dependency = this.parent.providedDependencies[name];
if(dependency === undefined) {
return this.parent.findMatchingProvidedDependencyByParent(name, versionSpec); // If the parent does not provide the dependency, try the parent's parent
} else if(dependency === null) {
return null; // If we have encountered a bundled dependency with the same name, consider it a conflict (is not a perfect resolution, but does not result in an error)
} else {
if(semver.satisfies(dependency.source.config.version, versionSpec, true)) { // If we found a dependency with the same name, see if the version fits
return dependency;
} else {
return null; // If there is a version mismatch, then a conflicting version has been encountered
}
}
}
};
/**
* Checks whether a dependency with a given name is already bundled with this
* package.
*
* @method
* @param {String} dependencyName Name of the dependency
* @return {Boolean} true if the dependency is bundled, else false
*/
Package.prototype.isBundledDependency = function(dependencyName) {
// Check the bundledDependencies option
if(Array.isArray(this.source.config.bundledDependencies)) {
for(var i = 0; i < this.source.config.bundledDependencies.length; i++) {
if(dependencyName == this.source.config.bundledDependencies[i])
return true;
}
}
// Check the bundleDependencies option
if(Array.isArray(this.source.config.bundleDependencies)) {
for(var i = 0; i < this.source.config.bundleDependencies.length; i++) {
if(dependencyName == this.source.config.bundleDependencies[i])
return true;
}
}
return false;
};
/**
* Bundles a dependency (that is fetched from an external location) to the
* package (or a parent package if flattening has been enabled). A bundled
* package will appear in the node_modules/ sub folder of the package.
*
* @method
* @param {String} dependencyName Name of the dependency
* @param {Package} pkg Package to bundle in the node_modules/ sub folder
*/
Package.prototype.bundleDependency = function(dependencyName, pkg) {
this.requiredDependencies[dependencyName] = pkg;
if(this.deploymentConfig.flatten) { // In flatten mode, bundle dependency with the highest parent where it is not conflicting
if(this.parent !== null && this.parent.providedDependencies[dependencyName] === undefined && this.parent.requiredDependencies[dependencyName] === undefined) {
this.parent.bundleDependency(dependencyName, pkg);
} else {
pkg.parent = this;
this.providedDependencies[dependencyName] = pkg;
}
} else {
this.providedDependencies[dependencyName] = pkg; // Bundle the dependency with the package
}
};
/**
* Bundles a collection of dependencies with this package (or when flattening
* mode has been enabled) any parent package that does not conflict and
* automatically fetches its metadata by downloading it.
*
* @method
* @param {Array<Package>} resolvedDependencies Memorizes the dependencies that have been resolved so that we can resolve their transitive dependencies later.
* @param {Array<Package>} dependencies Dependencies to bundle with the package
* @param {function(Object)} callback Callback that gets invoked then the work
* is done. The first parameter is set to an error object if the operation
* fails.
*/
Package.prototype.bundleDependencies = function(resolvedDependencies, dependencies, callback) {
if(dependencies === undefined) {
callback();
} else {
var self = this;
slasp.fromEach(function(callback) {
callback(null, dependencies);
}, function(dependencyName, callback) {
var versionSpec = dependencies[dependencyName];
var parentDependency = self.findMatchingProvidedDependencyByParent(dependencyName, versionSpec);
if(self.isBundledDependency(dependencyName)) {
self.requiredDependencies[dependencyName] = null;
callback();
} else if(parentDependency === null) {
var pkg = new Package(self.deploymentConfig, self.lock, self, dependencyName, versionSpec, self.source.baseDir, true /* Never include development dependencies of transitive dependencies */, self.sourcesCache);
slasp.sequence([
function(callback) {
pkg.source.fetch(callback);
},
function(callback) {
self.sourcesCache.addSource(pkg.source);
self.bundleDependency(dependencyName, pkg);
resolvedDependencies[dependencyName] = pkg;
callback();
}
], callback);
} else {
self.requiredDependencies[dependencyName] = parentDependency; // If there is a parent package that provides the requested dependency -> use it
callback();
}
}, callback);
}
};
Package.prototype.resolveDependenciesAndSources = function(callback) {
var self = this;
var resolvedDependencies = {};
slasp.sequence([
function(callback) {
self.bundleDependencies(resolvedDependencies, self.source.config.dependencies, callback);
},
function(callback) {
/* Bundle the development dependencies, if applicable */
if(self.production) {
callback();
} else {
self.bundleDependencies(resolvedDependencies, self.source.config.devDependencies, callback);
}
},
function(callback) {
if(self.deploymentConfig.includePeerDependencies) {
/* Bundle the required peer dependencies, if applicable */
self.bundleDependencies(resolvedDependencies, self.source.config.peerDependencies, callback);
} else {
callback();
}
},
function(callback) {
/* Bundle transitive dependencies */
slasp.fromEach(function(callback) {
callback(null, resolvedDependencies);
}, function(dependencyName, callback) {
var dependency = resolvedDependencies[dependencyName];
dependency.resolveDependenciesAndSources(callback);
}, callback);
}
], callback);
};
Package.prototype.resolveDependenciesFromLockedDependencies = function(dependencyObj, callback) {
var self = this;
if(dependencyObj.dependencies === undefined) {
callback();
} else {
slasp.fromEach(function(callback) {
callback(null, dependencyObj.dependencies);
}, function(dependencyName, callback) {
var dependency = dependencyObj.dependencies[dependencyName];
if(dependency.bundled) { // Bundled dependencies should not be included
callback();
} else if(self.deploymentConfig.stripOptionalDependencies && dependency.optional) { // When the stripping optional dependencies feature has been enabled, remove all optional dependencies
callback();
} else if(self.production && dependency.dev) { // Development dependencies should not be included in production mode
callback();
} else {
var pkg = new Package(self.deploymentConfig, self.lock, self, dependencyName, dependency.version, self.source.baseDir, self.production, self.sourcesCache);
self.providedDependencies[dependencyName] = pkg;
slasp.sequence([
function(callback) {
pkg.source.convertFromLockedDependency(dependency, callback);
},
function(callback) {
self.sourcesCache.addSource(pkg.source);
pkg.resolveDependenciesFromLockedDependencies(dependency, callback);
}
], callback);
}
}, callback);
}
};
/**
* Resolves all dependencies and transitive dependencies of this package.
*
* @method
* @param {function(Object)} callback Callback that gets invoked then the work
* is done. The first parameter is set to an error object if the operation
* fails.
*/
Package.prototype.resolveDependencies = function(callback) {
if(this.lock === undefined) { // If no lock file is present, let the tool fetch all dependencies and metadata
this.resolveDependenciesAndSources(callback);
} else { // If we have a lock file, use that to generate Nix expressions
this.resolveDependenciesFromLockedDependencies(this.lock, callback);
}
};
/**
* Composes an abstract syntax tree for the provided NPM dependencies by this
* package.
*
* @method
* @return {Array} An array representing a list of NPM package dependencies
*/
Package.prototype.generateDependencyAST = function() {
var self = this;
var dependencies = [];
Object.keys(self.providedDependencies).sort().forEach(function(dependencyName) {
var dependency = self.providedDependencies[dependencyName];
// For each dependency, refer to the source attribute set that defines it
var ref = new nijs.NixAttrReference({
attrSetExpr: new nijs.NixExpression("sources"),
refExpr: dependency.source.identifier
});
var transitiveDependencies = dependency.generateDependencyAST();
var dependencyExpr;
if(transitiveDependencies === undefined) {
dependencyExpr = ref; // If a dependency has no dependencies of its own, we just refer to the attribute in the source set
} else {
// If a dependency has dependencies, we augment the reference with the set of dependencies that it needs
dependencyExpr = new nijs.NixMergeAttrs({
left: ref,
right: {
dependencies: transitiveDependencies
}
});
}
dependencies.push(dependencyExpr);
});
if(dependencies.length == 0) {
return undefined; // If no dependencies are required, simply compose no parameter. Though not mandatory, it improves readability of the generated expression
} else {
return dependencies;
}
};
/**
* @see NixASTNode#toNixAST
*/
Package.prototype.toNixAST = function() {
var homepage;
if(typeof this.source.config.homepage == "string" && this.source.config.homepage) {
homepage = this.source.config.homepage;
}
var ast = this.source.toNixAST();
ast.dependencies = this.generateDependencyAST();
ast.buildInputs = new nijs.NixExpression("globalBuildInputs");
ast.meta = {
description: this.source.config.description,
homepage: homepage,
license: this.source.config.license
};
ast.production = this.production;
ast.bypassCache = this.deploymentConfig.bypassCache;
ast.reconstructLock = (this.lock === undefined);
return ast;
};
exports.Package = Package;