cubx-dependency-resolver
Version:
Npm package to resolve the dependencies of a list of artifacts
624 lines (559 loc) • 24.6 kB
JavaScript
/* globals __dirname */
(function () {
'use strict';
var path = require('path');
var axios = require('axios');
var DependencyTree = require(path.resolve(__dirname, 'DependencyTree.js'));
var ResponseCache = require(path.resolve(__dirname, 'ResponseCache.js'));
var DepReference = require(path.resolve(__dirname, 'DepReference.js'));
var Resource = require(path.resolve(__dirname, 'Resource.js'));
var DEFAULT_RUNTIME_MODE = 'prod';
var ArtifactsDepsResolver = function () {
this._axios = axios.create({
responseType: 'json'
});
this._responseCache = new ResponseCache();
this._enableACR = false;
};
ArtifactsDepsResolver.prototype.disableACR = function () {
this._enableACR = false;
};
ArtifactsDepsResolver.prototype.enableACR = function () {
this._enableACR = true;
};
ArtifactsDepsResolver.prototype.setBaseUrl = function (baseUrl) {
this._baseUrl = baseUrl && baseUrl.endsWith('/')
? baseUrl.substring(0, baseUrl.length - 1)
: baseUrl;
};
ArtifactsDepsResolver.prototype.resolveDependencies = function (rootDependencies, baseUrl, rootDependencyExcludes) {
this.rootDependencies = rootDependencies;
this.setBaseUrl(baseUrl);
return new Promise(function (resolve, reject) {
this.buildRawDependencyTree(rootDependencies, baseUrl)
.then(function (rawDepTree) {
return this._checkDepTreeForExcludes(rawDepTree, baseUrl);
}.bind(this))
.then(function (depTree) {
if (Array.isArray(rootDependencyExcludes)) {
rootDependencyExcludes.forEach(function (exclude) {
depTree.applyGlobalExclude(exclude.webpackageId, exclude.artifactId);
});
}
if (this._enableACR) {
depTree.enableACR();
} else {
depTree.disableACR();
}
depTree.applyExcludes();
depTree.removeDuplicates();
depTree.removeExcludes();
this.resolvedDepTree = depTree;
resolve(this.resolvedDepTree);
}.bind(this), function (error) {
console.error('Error while building and processing DependencyTree: ', error);
reject(error);
});
}.bind(this));
};
ArtifactsDepsResolver.prototype.resolveResourcesList = function (rootDependencies, baseUrl, runtimeMode, rootDependencyExcludes) {
this.rootDependencies = rootDependencies;
this.setBaseUrl(baseUrl);
this._runtimeMode = DEFAULT_RUNTIME_MODE;
if (runtimeMode) {
if (this._isValidRuntimeMode(runtimeMode)) {
this._runtimeMode = runtimeMode;
} else {
console.error('Using default value for runtimeMode: \'' + DEFAULT_RUNTIME_MODE + '\' ' +
'since an invalid value was provided: \'' + runtimeMode + '\'. It should be \'dev\' or \'prod\'');
}
}
return new Promise(function (resolve, reject) {
this.resolveDependencies(rootDependencies, baseUrl, rootDependencyExcludes)
.then(function (resolvedDepTree) {
var allDependencies = this._getDependencyListFromTree(resolvedDepTree);
this.resourceList = this._calculateResourceList(allDependencies);
resolve(this.resourceList);
}.bind(this), function (error) {
console.error('Error while building resources list: ', error);
reject(error);
});
}.bind(this));
};
ArtifactsDepsResolver.prototype.resolveWpList = function (rootDependencies, baseUrl, rootDependencyExcludes) {
this.rootDependencies = rootDependencies;
this.setBaseUrl(baseUrl);
return new Promise(function (resolve, reject) {
this.resolveDependencies(rootDependencies, baseUrl, rootDependencyExcludes)
.then(function (resolvedDepTree) {
var allDependencies = this._getDependencyListFromTree(resolvedDepTree);
this.wpList = this._calculateWpList(allDependencies);
resolve(this.wpList);
}.bind(this), function (error) {
console.error('Error while building webpackages list: ', error);
reject(error);
});
}.bind(this));
};
ArtifactsDepsResolver.prototype.resolveManifestsList = function (rootDependencies, baseUrl, rootDependencyExcludes) {
this.rootDependencies = rootDependencies;
this.setBaseUrl(baseUrl);
return new Promise(function (resolve, reject) {
this.resolveDependencies(rootDependencies, baseUrl, rootDependencyExcludes)
.then(function (resolvedDepTree) {
this.manifestList = this._responseCache.getItems();
resolve(this.manifestList);
}.bind(this), function (error) {
console.error('Error while building manifest list: ', error);
reject(error);
});
}.bind(this));
};
ArtifactsDepsResolver.prototype._isValidRuntimeMode = function (runtimeMode) {
return typeof runtimeMode === 'string' && (runtimeMode === 'dev' || runtimeMode === 'prod');
};
ArtifactsDepsResolver.prototype.getRawDependencyTree = function () {
return this.rawDepTree;
};
ArtifactsDepsResolver.prototype.getResolvedDependencyTree = function () {
return this.resolvedDepTree;
};
ArtifactsDepsResolver.prototype.getResourceList = function () {
return this.resourceList;
};
ArtifactsDepsResolver.prototype.getWpList = function () {
return this.wpList;
};
ArtifactsDepsResolver.prototype.buildRawDependencyTree = function (rootDependencies, baseUrl) {
rootDependencies = this._createDepReferenceListFromArtifactDependencies(rootDependencies);
if (!Array.isArray(rootDependencies)) {
throw new TypeError('Parameter \'rootDependencies\' needs to be an array');
}
return new Promise(function (resolve, reject) {
var depTree = new DependencyTree();
var rootNodes = [];
// first create rootNodes in dependency tree based on rootDependencies
rootDependencies.forEach(function (rootDependency) {
var node = new DependencyTree.Node();
node.data = rootDependency;
depTree.insertNode(node);
rootNodes.push(node);
});
// define recursively called function for resolving dependencyTree level by level
// parentNodes is an array of same length as dependencies. parentNodes[i] is a parentNode reference for
// dependencies[i]
(function resolveDependencies (dependencies, parentNodes) {
var resolutionsQueue = [];
var nodes = [];
dependencies.forEach(function (dep) {
try {
resolutionsQueue.push(this._resolveDepReferenceDependencies(dep, baseUrl));
} catch (error) {
console.error('Could not resolve Dependency ', dep.getId());
reject(error);
}
}.bind(this));
Promise.all(resolutionsQueue).then(function (results) {
var unresolvedDependencies = [];
// empty resolutionsQueue
resolutionsQueue = [];
// create and insert node in DependencyTree for each resolved dependency
results.forEach(function (result, index) {
var parentNode = parentNodes[index];
parentNode.data.resources = result.resources;
result.dependencies.forEach(function (depRefItem) {
var node = new DependencyTree.Node();
node.data = depRefItem;
depTree.insertNode(node, parentNode);
unresolvedDependencies.push(depRefItem);
nodes.push(node);
});
});
if (unresolvedDependencies.length > 0) {
resolveDependencies.bind(this, unresolvedDependencies, nodes)();
} else {
this.rawDepTree = depTree.clone();
resolve(depTree);
}
}.bind(this), function (error) {
console.error('Could not resolve Dependency: ', error);
reject(error);
});
}.bind(this))(rootDependencies, rootNodes);
}.bind(this));
};
/**
* Searches for Conflicts in given DependencyTree create warning logs in console for each conflict.
* @memberOf ArtifactsDepsResolver
* @param {object} depTree DependencyTree. Should already be cleaned (duplicates and excludes are removed).
* @private
*/
ArtifactsDepsResolver.prototype._logDependencyConflicts = function (depTree) {
var conflicts = depTree.getConflictedNodes();
conflicts.some(function (conflict) {
switch (conflict.type) {
case DependencyTree.conflictTypes.VERSION:
if (conflict.resolved) {
console.log(
'Artifact',
conflict.node.data.artifactId,
'is defined in multiple versions of the same webpackage: [',
conflict.node.data.webpackageId, ',',
conflict.conflictedNode.data.webpackageId, '].',
'Using webpackage ',
conflict.node.data.webpackageId
);
} else {
console.warn(
'Artifact',
conflict.node.data.artifactId,
'is defined in multiple versions of the same webpackage: [',
conflict.node.data.webpackageId, ',',
conflict.conflictedNode.data.webpackageId, '].'
);
}
break;
case DependencyTree.conflictTypes.NAME:
console.warn(
'Artifact',
conflict.node.data.artifactId,
'is defined in multiple webpackages: [',
conflict.node.data.webpackageId, ',',
conflict.conflictedNode.data.webpackageId, '].',
'This could cause unexpeted behaviour!'
);
break;
}
});
};
ArtifactsDepsResolver.prototype._checkDepTreeForExcludes = function (depTree, baseUrl) {
// make some type checking
if (!(depTree instanceof DependencyTree)) {
throw new TypeError('parameter \'depTree\' needs to be an instance of ArtifactsDepsResolver.DependencyTree');
}
if (typeof baseUrl !== 'string') {
throw new TypeError('parameter \'baseUrl\' needs to be of type string');
}
return new Promise(function (resolve, reject) {
// traverse through tree and request manifest for each node used for adding dependencyExcludes, if there are any.
var nodes = [];
var promises = [];
depTree.traverseBF(function (node) {
nodes.push(node);
// if given node is a rootNode check global rootDependencies for existing excludes.
if (node.parent === null) {
this._checkAndAddExcludesForRootDependencies(node);
}
promises.push(this._getManifestForDepReference(node.data, baseUrl));
}.bind(this));
if (promises.length === 0) {
resolve(depTree);
}
Promise.all(promises)
.then(function (results) {
results.forEach(function (manifest, index) {
try {
this._checkAndAddExcludesToDepReference(nodes[index].data, manifest);
} catch (e) {
reject(e);
}
}.bind(this));
resolve(depTree);
}.bind(this))
.catch(function (error) {
reject(error);
});
}.bind(this));
};
ArtifactsDepsResolver.prototype._checkAndAddExcludesForRootDependencies = function (node) {
// make some type checking of param node
if (!(node instanceof DependencyTree.Node)) {
throw new TypeError('parameter \'node\' needs to be an instance of DependencyTree.Node');
}
var rootDependencies = this._createDepReferenceListFromArtifactDependencies(this.rootDependencies, null);
var rootDep = rootDependencies.find(function (dep) {
return node.data.artifactId === dep.artifactId && node.data.webpackageId === dep.webpackageId;
});
if (rootDep.hasOwnProperty('dependencyExcludes')) {
node.data.dependencyExcludes = rootDep.dependencyExcludes;
}
return node;
};
ArtifactsDepsResolver.prototype._getManifestForDepReference = function (depReference, baseUrl) {
// make some type checking
if (!(depReference instanceof DepReference)) {
throw new TypeError('parameter \'depReference\' needs to be an instance of ArtifactsDepsResolver.DepReference');
}
return new Promise(function (resolve, reject) {
var webpackageId = depReference.webpackageId;
if (this._responseCache.get(webpackageId) !== null) {
resolve(this._responseCache.get(webpackageId));
} else if (depReference.manifest) {
resolve(depReference.manifest);
} else if (typeof baseUrl === 'string') {
// append / to baseUrl if necessary
var url = baseUrl.lastIndexOf('/') !== baseUrl.length - 1 ? baseUrl + '/' : baseUrl;
this._fetchManifest(url + webpackageId + '/manifest.webpackage').then(function (result) {
resolve(result.data);
}, function (error) {
reject(error);
});
} else {
reject(new TypeError('parameter \'baseUrl\' needs to be a valid url'));
}
}.bind(this));
};
ArtifactsDepsResolver.prototype._checkAndAddExcludesToDepReference = function (depReference, manifest) {
// make some parameter type checking
if (!(depReference instanceof DepReference)) {
throw new TypeError('parameter \'depReference\' needs to be an instance of ArtifactsDepsResolver.DepReference');
}
if (typeof manifest !== 'object') {
throw new TypeError('parameter \'manifest\' needs to be a an object representing a webpackage.manifest');
}
// find artifact in given manifest that corresponds with given depReference
var artifact = this._extractArtifact(depReference, manifest);
if (artifact.hasOwnProperty('dependencyExcludes') && artifact.dependencyExcludes.length > 0) {
depReference.dependencyExcludes = depReference.dependencyExcludes.concat(JSON.parse(JSON.stringify(artifact.dependencyExcludes)));
}
return depReference;
};
ArtifactsDepsResolver.prototype._getDependencyListFromTree = function (depTree) {
var nodeList = [];
var depList = [];
depTree.traverseBF(function (nodeToInsert) {
// find index of first node in nodeList, which is an ancestor for given node
var index = -1;
nodeList.some(function (element, currentIndex) {
if (element.isAncestorOf(nodeToInsert)) {
index = currentIndex;
return true;
}
});
// we need to insert node before all ancestors of this node to make sure all of it's ancestors can use node
// as dependency. We ensure this by just inserting the node right before the first ancestor found in current nodeList
if (index > -1) {
nodeList.splice(index, 0, nodeToInsert);
} else {
// if there is no ancestor of node in current nodeList yet we can just push node to current nodeList
nodeList.push(nodeToInsert);
}
});
depList = nodeList.map(function (node) {
return node.data;
});
return depList;
};
ArtifactsDepsResolver.prototype._calculateResourceList = function (depList) {
var resourceList = [];
for (var i = 0; i < depList.length; i++) {
var currentDepRef = depList[i];
for (var j = 0; j < currentDepRef.resources.length; j++) {
// remove endpoint appendix from artifactId if there was one added by the manifestConverter
var qualifiedArtifactId = currentDepRef.artifactId.indexOf('#') > -1
? currentDepRef.webpackageId + '/' + currentDepRef.artifactId.split('#')[0]
: currentDepRef.webpackageId + '/' + currentDepRef.artifactId;
var resource = this._createResourceFromItem(qualifiedArtifactId, currentDepRef.resources[j],
this._runtimeMode, currentDepRef.referrer);
if (resource) {
resourceList.push(resource);
}
}
}
return resourceList;
};
ArtifactsDepsResolver.prototype._calculateWpList = function (depList) {
var wpList = [];
for (var i = 0; i < depList.length; i++) {
var currentDepRef = depList[i];
if (wpList.indexOf(currentDepRef.webpackageId) === -1) {
wpList.push(currentDepRef.webpackageId);
}
}
return wpList;
};
ArtifactsDepsResolver.prototype._createResourceFromItem = function (qualifiedArtifactId, item, runtimeMode, referrer) {
// if item does not contain a prod-dev structure, map it into that - to simplify further processing
if (typeof item === 'string') {
item = {
prod: item,
dev: item
};
}
if (!item.hasOwnProperty('prod') || !item.hasOwnProperty('dev') ||
typeof item.prod !== 'string' || typeof item.dev !== 'string') {
throw new TypeError('parameter "item" needs to have string properties "prod" and "dev"');
}
if (qualifiedArtifactId === null || typeof qualifiedArtifactId !== 'string') {
throw new TypeError('parameter "qualifiedArtifactId" needs to be of type string');
}
var file;
if (item[runtimeMode].indexOf('http') === 0 || item[runtimeMode].indexOf('blob') === 0) {
file = item[runtimeMode];
} else {
file = this._baseUrl + '/' + qualifiedArtifactId + '/' + item[runtimeMode];
}
var resMetaObj = this._determineResourceType(file);
if (!resMetaObj.fileType) {
console.warn('The following resource will be ignored, because the type of the resource is unknown. It should be "js", "html" or "css". (' + item[runtimeMode] + ')');
return;
}
return new Resource(resMetaObj.fileName, resMetaObj.fileType.name, referrer);
};
ArtifactsDepsResolver.prototype._determineResourceType = function (fileName) {
var fileType;
var paramTypeIndex = fileName.indexOf('?type=');
var paramType;
if (paramTypeIndex > 0) {
paramType = fileName.substr(paramTypeIndex + 6);
fileName = fileName.substring(0, paramTypeIndex);
}
var fileEnding = paramType || fileName.split('.')[fileName.split('.').length - 1];
for (var property in Resource._types) {
if (Resource._types.hasOwnProperty(property)) {
var type = Resource._types[property];
for (var i = 0; i < type.fileEndings.length; i++) {
if (type.fileEndings[i] === fileEnding) {
fileType = type;
break;
}
}
if (fileType) {
break;
}
}
}
return {
fileType: fileType,
fileName: fileName
};
};
ArtifactsDepsResolver.prototype._fetchManifest = function (url) {
return this._axios.request({
url: url
});
};
ArtifactsDepsResolver.prototype._resolveDepReferenceDependencies = function (depReference, baseUrl) {
// check depReference
if (!(depReference instanceof DepReference)) {
throw new TypeError('parameter \'depReference\' need to be an instance of DepReference');
}
// check baseUrl
if (typeof baseUrl !== 'string') {
throw new TypeError('parameter \'baseUrl\' needs to be of type string');
}
return new Promise(function (resolve, reject) {
var dependencies = [];
var processManifest = function (manifest, cache) {
if (cache) {
this._responseCache.addItem(depReference.webpackageId, manifest);
}
var artifact = this._extractArtifact(depReference, manifest);
try {
if (artifact.hasOwnProperty('dependencies') && artifact.dependencies.length > 0) {
dependencies = this._createDepReferenceListFromArtifactDependencies(artifact.dependencies, depReference);
}
resolve({
resources: artifact.resources || [],
dependencies: dependencies
});
} catch (e) {
console.error('The artifact \'' + depReference.artifactId + '\' is not defined in manifest:',
'\n\tDependency reference: ', depReference, '\n\tManifest: ', manifest);
reject(new Error('Artifact not defined.'));
}
}.bind(this);
// append '/' to baseUrl if not present
baseUrl = baseUrl.lastIndexOf('/') === baseUrl.length - 1 ? baseUrl : baseUrl + '/';
if (depReference.webpackageId && this._responseCache.get(depReference.webpackageId) !== null) { // use manifest from responseCache if available
processManifest(this._responseCache.get(depReference.webpackageId), false);
} else if (typeof depReference.manifest === 'object') { // use inline manifest from depReference if set
// TODO: transform manifest to ensure backwards compatibility with modelVersion > < 9
processManifest(depReference.manifest);
} else { // default case: request manifest using ajax
var url = baseUrl + depReference.webpackageId + '/manifest.webpackage';
this._fetchManifest(url).then(
function (response) {
processManifest(response.data, true);
},
function (error) {
reject(error);
}
);
}
}.bind(this));
};
ArtifactsDepsResolver.prototype._extractArtifact = function (depReference, manifest) {
var requestedArtifact;
if (manifest) {
// for apps, elementaryComponents etc.
// console.log(JSON.stringify(manifest))
Object.keys(manifest.artifacts).some(function (artifactType) {
manifest.artifacts[artifactType].some(function (artifact) {
if (artifact.artifactId === depReference.artifactId) {
requestedArtifact = artifact;
}
});
});
}
return requestedArtifact;
};
ArtifactsDepsResolver.prototype._createDepReferenceListFromArtifactDependencies = function (dependencies, referrer) {
var self = this;
var depList = [];
if (!dependencies) {
return depList;
}
// check given referrer is of type object
if (!referrer || (referrer && typeof referrer !== 'object')) {
console.warn('Expect parameter "referrer" to be null or of type object: ', referrer, '. Will use "root" as fallback referrer');
// if referrer is invalid set it to null. A referrer with value 'null' is interpreted as 'root'
referrer = null;
}
// console.log(typeof dependencies)
dependencies.forEach(function (dependency) {
var valid = true; // just a flag used for skipping the processing of current dependency when it is invalid
// check if dependency is of type 'object' and has at least string property 'artifactId'
if (!(typeof dependency === 'object' && dependency.hasOwnProperty('artifactId') && typeof dependency.artifactId === 'string')) {
console.error('Expected parameter to be an object containing at least string property "artifactId": ', dependency);
valid = false;
}
// check if dependencies manifest property is of type object (in case there is a manifest property)
if (dependency.hasOwnProperty('manifest') && typeof dependency.manifest !== 'object') {
console.error('Expected parameter to be an object containing at least string property "artifactId": ', dependency);
valid = false;
}
// continue processing of current dependency only if it's a valid one
if (valid) {
var depReferenceInitObject = {
referrer: referrer,
artifactId: dependency.artifactId,
webpackageId: self._determineWebpackageId(dependency, referrer)
};
// add manifest if available
if (dependency.manifest) {
depReferenceInitObject.manifest = dependency.manifest;
}
var depRef = new DepReference(depReferenceInitObject);
// dependencyExcludes if available
if (dependency.hasOwnProperty('dependencyExcludes')) {
depRef.dependencyExcludes = dependency.dependencyExcludes;
}
depList.push(depRef);
}
});
return depList;
};
ArtifactsDepsResolver.prototype._determineWebpackageId = function (dependency, referrer) {
if (dependency.hasOwnProperty('webpackageId') && typeof dependency.webpackageId === 'string') {
return dependency.webpackageId;
} else if (referrer && typeof referrer === 'object' && referrer.hasOwnProperty('webpackageId') && referrer.hasOwnProperty('artifactId')) {
// if there is no webpackageId given then we assume that the dependency resides in the same webpackage like the referrer
return referrer.webpackageId;
} else {
console.error('Could not determine webpackageId for dependency: ', dependency, ' and referrer: ', referrer);
return '';
}
};
module.exports = ArtifactsDepsResolver;
}());