mirror of
https://github.com/bower/bower.git
synced 2026-01-12 07:48:03 -05:00
1034 lines
35 KiB
JavaScript
1034 lines
35 KiB
JavaScript
var glob = require('glob');
|
|
var path = require('path');
|
|
var fs = require('../util/fs');
|
|
var Q = require('q');
|
|
var mout = require('mout');
|
|
var rimraf = require('../util/rimraf');
|
|
var endpointParser = require('bower-endpoint-parser');
|
|
var Logger = require('bower-logger');
|
|
var md5 = require('md5-hex');
|
|
var Manager = require('./Manager');
|
|
var semver = require('../util/semver');
|
|
var createError = require('../util/createError');
|
|
var readJson = require('../util/readJson');
|
|
var validLink = require('../util/validLink');
|
|
var scripts = require('./scripts');
|
|
var relativeToBaseDir = require('../util/relativeToBaseDir');
|
|
|
|
function Project(config, logger) {
|
|
this._config = config;
|
|
this._logger = logger || new Logger();
|
|
this._manager = new Manager(this._config, this._logger);
|
|
|
|
this._options = {};
|
|
}
|
|
|
|
// -----------------
|
|
|
|
Project.prototype.install = function(decEndpoints, options, config) {
|
|
var that = this;
|
|
var targets = [];
|
|
var resolved = {};
|
|
var incompatibles = [];
|
|
|
|
// If already working, error out
|
|
if (this._working) {
|
|
return Q.reject(createError('Already working', 'EWORKING'));
|
|
}
|
|
|
|
this._options = options || {};
|
|
this._config = config || {};
|
|
this._working = true;
|
|
|
|
// Analyse the project
|
|
return this._analyse()
|
|
.spread(function(json, tree) {
|
|
// It shows an error when issuing `bower install`
|
|
// and no bower.json is present in current directory
|
|
if (!that._jsonFile && decEndpoints.length === 0) {
|
|
throw createError('No bower.json present', 'ENOENT');
|
|
}
|
|
|
|
// Recover tree
|
|
that.walkTree(
|
|
tree,
|
|
function(node, name) {
|
|
if (node.incompatible) {
|
|
incompatibles.push(node);
|
|
} else if (
|
|
node.missing ||
|
|
node.different ||
|
|
that._config.force
|
|
) {
|
|
targets.push(node);
|
|
} else {
|
|
resolved[name] = node;
|
|
}
|
|
},
|
|
true
|
|
);
|
|
|
|
// Add decomposed endpoints as targets
|
|
decEndpoints = decEndpoints || [];
|
|
decEndpoints.forEach(function(decEndpoint) {
|
|
// Mark as new so that a conflict for this target
|
|
// always require a choice
|
|
// Also allows for the target to be converted in case
|
|
// of being *
|
|
decEndpoint.newly = true;
|
|
targets.push(decEndpoint);
|
|
});
|
|
|
|
// Bootstrap the process
|
|
return that._bootstrap(targets, resolved, incompatibles);
|
|
})
|
|
.then(function() {
|
|
return that._manager.preinstall(that._json);
|
|
})
|
|
.then(function() {
|
|
return that._manager.install(that._json);
|
|
})
|
|
.then(function(installed) {
|
|
// Handle save and saveDev options
|
|
if (
|
|
that._options.save ||
|
|
that._options.saveDev ||
|
|
that._options.saveExact ||
|
|
that._config.save ||
|
|
that._config.saveExact
|
|
) {
|
|
// Cycle through the specified endpoints
|
|
decEndpoints.forEach(function(decEndpoint) {
|
|
var jsonEndpoint;
|
|
|
|
jsonEndpoint = endpointParser.decomposed2json(decEndpoint);
|
|
|
|
if (that._options.saveExact || that._config.saveExact) {
|
|
if (decEndpoint.name !== decEndpoint.source) {
|
|
jsonEndpoint[decEndpoint.name] =
|
|
decEndpoint.source +
|
|
'#' +
|
|
decEndpoint.pkgMeta.version;
|
|
} else {
|
|
jsonEndpoint[decEndpoint.name] =
|
|
decEndpoint.pkgMeta.version;
|
|
}
|
|
}
|
|
|
|
if (that._options.saveDev) {
|
|
that._json.devDependencies = mout.object.mixIn(
|
|
that._json.devDependencies || {},
|
|
jsonEndpoint
|
|
);
|
|
} else {
|
|
that._json.dependencies = mout.object.mixIn(
|
|
that._json.dependencies || {},
|
|
jsonEndpoint
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Save JSON, might contain changes to dependencies and resolutions
|
|
return that.saveJson().then(function() {
|
|
return that._manager.postinstall(that._json).then(function() {
|
|
return installed;
|
|
});
|
|
});
|
|
})
|
|
.fin(function() {
|
|
that._installed = null;
|
|
that._working = false;
|
|
});
|
|
};
|
|
|
|
Project.prototype.update = function(names, options) {
|
|
var that = this;
|
|
var targets = [];
|
|
var resolved = {};
|
|
var incompatibles = [];
|
|
|
|
// If already working, error out
|
|
if (this._working) {
|
|
return Q.reject(createError('Already working', 'EWORKING'));
|
|
}
|
|
|
|
this._options = options || {};
|
|
this._working = true;
|
|
|
|
// Analyse the project
|
|
return this._analyse()
|
|
.spread(function(json, tree, flattened) {
|
|
// If no names were specified, update every package
|
|
if (!names) {
|
|
// Mark each root dependency as targets
|
|
that.walkTree(
|
|
tree,
|
|
function(node) {
|
|
// We don't know the real source of linked packages
|
|
// Instead we read its dependencies
|
|
if (node.linked) {
|
|
targets.push.apply(
|
|
targets,
|
|
mout.object.values(node.dependencies)
|
|
);
|
|
} else {
|
|
targets.push(node);
|
|
}
|
|
|
|
return false;
|
|
},
|
|
true
|
|
);
|
|
// Otherwise, selectively update the specified ones
|
|
} else {
|
|
// Error out if some of the specified names
|
|
// are not installed
|
|
names.forEach(function(name) {
|
|
if (!flattened[name]) {
|
|
throw createError(
|
|
'Package ' + name + ' is not installed',
|
|
'ENOTINS',
|
|
{
|
|
name: name
|
|
}
|
|
);
|
|
}
|
|
});
|
|
|
|
// Add packages whose names are specified to be updated
|
|
that.walkTree(
|
|
tree,
|
|
function(node, name) {
|
|
if (names.indexOf(name) !== -1) {
|
|
// We don't know the real source of linked packages
|
|
// Instead we read its dependencies
|
|
if (node.linked) {
|
|
targets.push.apply(
|
|
targets,
|
|
mout.object.values(node.dependencies)
|
|
);
|
|
} else {
|
|
targets.push(node);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
},
|
|
true
|
|
);
|
|
|
|
// Recover tree
|
|
that.walkTree(
|
|
tree,
|
|
function(node, name) {
|
|
if (node.missing || node.different) {
|
|
targets.push(node);
|
|
} else if (node.incompatible) {
|
|
incompatibles.push(node);
|
|
} else {
|
|
resolved[name] = node;
|
|
}
|
|
},
|
|
true
|
|
);
|
|
}
|
|
|
|
// Bootstrap the process
|
|
return that
|
|
._bootstrap(targets, resolved, incompatibles)
|
|
.then(function() {
|
|
return that._manager.preinstall(that._json);
|
|
})
|
|
.then(function() {
|
|
return that._manager.install(that._json);
|
|
})
|
|
.then(function(installed) {
|
|
// Save JSON, might contain changes to resolutions
|
|
return that.saveJson().then(function() {
|
|
return that._manager
|
|
.postinstall(that._json)
|
|
.then(function() {
|
|
return installed;
|
|
});
|
|
});
|
|
});
|
|
})
|
|
.fin(function() {
|
|
that._installed = null;
|
|
that._working = false;
|
|
});
|
|
};
|
|
|
|
function resolveUrlNames(names, flattened) {
|
|
for (var i = 0; i < names.length; i++)
|
|
if (!flattened[names[i]]) {
|
|
var url = names[i].trim().replace(/\/$/, '');
|
|
var packName;
|
|
for (packName in flattened)
|
|
if (!!flattened[packName].source)
|
|
if (
|
|
url ==
|
|
flattened[packName].source.trim().replace(/\/$/, '')
|
|
)
|
|
names[i] = packName;
|
|
}
|
|
}
|
|
|
|
Project.prototype.uninstall = function(names, options) {
|
|
var that = this;
|
|
var packages = {};
|
|
|
|
// If already working, error out
|
|
if (this._working) {
|
|
return Q.reject(createError('Already working', 'EWORKING'));
|
|
}
|
|
|
|
this._options = options || {};
|
|
this._working = true;
|
|
|
|
// Analyse the project
|
|
return (
|
|
this._analyse()
|
|
// Fill in the packages to be uninstalled
|
|
.spread(function(json, tree, flattened) {
|
|
var promise = Q.resolve();
|
|
resolveUrlNames(names, flattened);
|
|
|
|
names.forEach(function(name) {
|
|
var decEndpoint = flattened[name];
|
|
|
|
// Check if it is not installed
|
|
if (!decEndpoint || decEndpoint.missing) {
|
|
packages[name] = null;
|
|
return;
|
|
}
|
|
|
|
promise = promise.then(function() {
|
|
var message;
|
|
var data;
|
|
var dependantsNames;
|
|
var dependants = [];
|
|
|
|
// Walk the down the tree, gathering dependants of the package
|
|
that.walkTree(
|
|
tree,
|
|
function(node, nodeName) {
|
|
if (name === nodeName) {
|
|
dependants.push.apply(
|
|
dependants,
|
|
mout.object.values(node.dependants)
|
|
);
|
|
}
|
|
},
|
|
true
|
|
);
|
|
|
|
// Remove duplicates
|
|
dependants = mout.array.unique(dependants);
|
|
|
|
// Note that the root is filtered from the dependants
|
|
// as well as other dependants marked to be uninstalled
|
|
dependants = dependants.filter(function(dependant) {
|
|
return (
|
|
!dependant.root &&
|
|
names.indexOf(dependant.name) === -1
|
|
);
|
|
});
|
|
|
|
// If the package has no dependants or the force config is enabled,
|
|
// mark it to be removed
|
|
if (!dependants.length || that._config.force) {
|
|
packages[name] = decEndpoint.canonicalDir;
|
|
return;
|
|
}
|
|
|
|
// Otherwise we need to figure it out if the user really wants to remove it,
|
|
// even with dependants
|
|
// As such we need to prompt the user with a meaningful message
|
|
dependantsNames = dependants.map(function(dep) {
|
|
return dep.name;
|
|
});
|
|
dependantsNames.sort(function(name1, name2) {
|
|
return name1.localeCompare(name2);
|
|
});
|
|
dependantsNames = mout.array.unique(dependantsNames);
|
|
dependants = dependants.map(function(dependant) {
|
|
return that._manager.toData(dependant);
|
|
});
|
|
message =
|
|
dependantsNames.join(', ') +
|
|
' depends on ' +
|
|
decEndpoint.name;
|
|
data = {
|
|
name: decEndpoint.name,
|
|
dependants: dependants
|
|
};
|
|
|
|
// If interactive is disabled, error out
|
|
if (!that._config.interactive) {
|
|
throw createError(message, 'ECONFLICT', {
|
|
data: data
|
|
});
|
|
}
|
|
|
|
that._logger.conflict('mutual', message, data);
|
|
|
|
// Prompt the user
|
|
return Q.nfcall(
|
|
that._logger.prompt.bind(that._logger),
|
|
{
|
|
type: 'confirm',
|
|
message: 'Continue anyway?',
|
|
default: true
|
|
}
|
|
).then(function(confirmed) {
|
|
// If the user decided to skip it, remove from the array so that it won't
|
|
// influence subsequent dependants
|
|
if (!confirmed) {
|
|
mout.array.remove(names, name);
|
|
} else {
|
|
packages[name] = decEndpoint.canonicalDir;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
return promise;
|
|
})
|
|
// Remove packages
|
|
.then(function() {
|
|
return that._removePackages(packages);
|
|
})
|
|
.fin(function() {
|
|
that._installed = null;
|
|
that._working = false;
|
|
})
|
|
);
|
|
};
|
|
|
|
Project.prototype.getTree = function(options) {
|
|
this._options = options || {};
|
|
|
|
return this._analyse().spread(
|
|
function(json, tree, flattened) {
|
|
var extraneous = [];
|
|
var additionalKeys = [
|
|
'missing',
|
|
'extraneous',
|
|
'different',
|
|
'linked'
|
|
];
|
|
|
|
// Convert tree
|
|
tree = this._manager.toData(tree, additionalKeys);
|
|
|
|
// Mark incompatibles
|
|
this.walkTree(
|
|
tree,
|
|
function(node) {
|
|
var version;
|
|
var target = node.endpoint.target;
|
|
|
|
if (node.pkgMeta && semver.validRange(target)) {
|
|
version = node.pkgMeta.version;
|
|
|
|
// Ignore if target is '*' and resolved to a non-semver release
|
|
if (!version && target === '*') {
|
|
return;
|
|
}
|
|
|
|
if (!version || !semver.satisfies(version, target)) {
|
|
node.incompatible = true;
|
|
}
|
|
}
|
|
},
|
|
true
|
|
);
|
|
|
|
// Convert extraneous
|
|
mout.object.forOwn(
|
|
flattened,
|
|
function(pkg) {
|
|
if (pkg.extraneous) {
|
|
extraneous.push(
|
|
this._manager.toData(pkg, additionalKeys)
|
|
);
|
|
}
|
|
},
|
|
this
|
|
);
|
|
|
|
// Convert flattened
|
|
flattened = mout.object.map(
|
|
flattened,
|
|
function(node) {
|
|
return this._manager.toData(node, additionalKeys);
|
|
},
|
|
this
|
|
);
|
|
|
|
return [tree, flattened, extraneous];
|
|
}.bind(this)
|
|
);
|
|
};
|
|
|
|
Project.prototype.walkTree = function(node, fn, onlyOnce) {
|
|
var result;
|
|
var dependencies;
|
|
var queue = mout.object.values(node.dependencies);
|
|
|
|
if (onlyOnce === true) {
|
|
onlyOnce = [];
|
|
}
|
|
|
|
while (queue.length) {
|
|
node = queue.shift();
|
|
result = fn(node, node.endpoint ? node.endpoint.name : node.name);
|
|
|
|
// Abort traversal if result is false
|
|
if (result === false) {
|
|
continue;
|
|
}
|
|
|
|
// Add dependencies to the queue
|
|
dependencies = mout.object.values(node.dependencies);
|
|
|
|
// If onlyOnce was true, do not add if already traversed
|
|
if (onlyOnce) {
|
|
dependencies = dependencies.filter(function(dependency) {
|
|
return !mout.array.find(onlyOnce, function(stacked) {
|
|
if (dependency.endpoint) {
|
|
return mout.object.equals(
|
|
dependency.endpoint,
|
|
stacked.endpoint
|
|
);
|
|
}
|
|
|
|
return (
|
|
dependency.name === stacked.name &&
|
|
dependency.source === stacked.source &&
|
|
dependency.target === stacked.target
|
|
);
|
|
});
|
|
});
|
|
|
|
onlyOnce.push.apply(onlyOnce, dependencies);
|
|
}
|
|
|
|
queue.unshift.apply(queue, dependencies);
|
|
}
|
|
};
|
|
|
|
Project.prototype.saveJson = function(forceCreate) {
|
|
var file;
|
|
var jsonStr = JSON.stringify(this._json, null, ' ') + '\n';
|
|
var jsonHash = md5(jsonStr);
|
|
|
|
// Save only if there's something different
|
|
if (jsonHash === this._jsonHash) {
|
|
return Q.resolve();
|
|
}
|
|
|
|
// Error out if the json file does not exist, unless force create
|
|
// is true
|
|
if (!this._jsonFile && !forceCreate) {
|
|
this._logger.warn(
|
|
'no-json',
|
|
'No bower.json file to save to, use bower init to create one'
|
|
);
|
|
return Q.resolve();
|
|
}
|
|
|
|
file = this._jsonFile || path.join(this._config.cwd, 'bower.json');
|
|
return Q.nfcall(fs.writeFile, file, jsonStr).then(
|
|
function() {
|
|
this._jsonHash = jsonHash;
|
|
this._jsonFile = file;
|
|
return this._json;
|
|
}.bind(this)
|
|
);
|
|
};
|
|
|
|
Project.prototype.hasJson = function() {
|
|
return this._readJson().then(
|
|
function(json) {
|
|
return json ? this._jsonFile : false;
|
|
}.bind(this)
|
|
);
|
|
};
|
|
|
|
Project.prototype.getJson = function() {
|
|
return this._readJson();
|
|
};
|
|
|
|
Project.prototype.getManager = function() {
|
|
return this._manager;
|
|
};
|
|
|
|
Project.prototype.getPackageRepository = function() {
|
|
return this._manager.getPackageRepository();
|
|
};
|
|
|
|
// -----------------
|
|
|
|
Project.prototype._analyse = function() {
|
|
return Q.all([
|
|
this._readJson(),
|
|
this._readInstalled(),
|
|
this._readLinks()
|
|
]).spread(
|
|
function(json, installed, links) {
|
|
var root;
|
|
var jsonCopy = mout.lang.deepClone(json);
|
|
|
|
root = {
|
|
name: json.name,
|
|
source: this._config.cwd,
|
|
target: json.version || '*',
|
|
pkgMeta: jsonCopy,
|
|
canonicalDir: this._config.cwd,
|
|
root: true
|
|
};
|
|
|
|
mout.object.mixIn(installed, links);
|
|
|
|
// Mix direct extraneous as dependencies
|
|
// (dependencies installed without --save/--save-dev)
|
|
jsonCopy.dependencies = jsonCopy.dependencies || {};
|
|
jsonCopy.devDependencies = jsonCopy.devDependencies || {};
|
|
mout.object.forOwn(installed, function(decEndpoint, key) {
|
|
var pkgMeta = decEndpoint.pkgMeta;
|
|
var isSaved =
|
|
jsonCopy.dependencies[key] || jsonCopy.devDependencies[key];
|
|
|
|
// The _direct property is saved by the manager when .newly is specified
|
|
// It may happen pkgMeta is undefined if package is uninstalled
|
|
if (!isSaved && pkgMeta && pkgMeta._direct) {
|
|
decEndpoint.extraneous = true;
|
|
|
|
if (decEndpoint.linked) {
|
|
jsonCopy.dependencies[key] = pkgMeta.version || '*';
|
|
} else {
|
|
jsonCopy.dependencies[key] =
|
|
(pkgMeta._originalSource || pkgMeta._source) +
|
|
'#' +
|
|
pkgMeta._target;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Restore the original dependencies cross-references,
|
|
// that is, the parent-child relationships
|
|
this._restoreNode(root, installed, 'dependencies');
|
|
// Do the same for the dev dependencies
|
|
if (!this._options.production) {
|
|
this._restoreNode(root, installed, 'devDependencies');
|
|
}
|
|
|
|
// Restore the rest of the extraneous (not installed directly)
|
|
mout.object.forOwn(
|
|
installed,
|
|
function(decEndpoint, name) {
|
|
if (!decEndpoint.dependants) {
|
|
decEndpoint.extraneous = true;
|
|
this._restoreNode(
|
|
decEndpoint,
|
|
installed,
|
|
'dependencies'
|
|
);
|
|
// Note that it has no dependants, just dependencies!
|
|
root.dependencies[name] = decEndpoint;
|
|
}
|
|
},
|
|
this
|
|
);
|
|
|
|
// Remove root from the flattened tree
|
|
delete installed[json.name];
|
|
|
|
return [json, root, installed];
|
|
}.bind(this)
|
|
);
|
|
};
|
|
|
|
Project.prototype._bootstrap = function(targets, resolved, incompatibles) {
|
|
var installed = mout.object.map(this._installed, function(decEndpoint) {
|
|
return decEndpoint.pkgMeta;
|
|
});
|
|
|
|
this._json.resolutions = this._json.resolutions || {};
|
|
|
|
// Configure the manager and kick in the resolve process
|
|
return this._manager
|
|
.configure({
|
|
targets: targets,
|
|
resolved: resolved,
|
|
incompatibles: incompatibles,
|
|
resolutions: this._json.resolutions,
|
|
installed: installed,
|
|
forceLatest: this._options.forceLatest
|
|
})
|
|
.resolve()
|
|
.then(
|
|
function() {
|
|
// If the resolutions is empty, delete key
|
|
if (!mout.object.size(this._json.resolutions)) {
|
|
delete this._json.resolutions;
|
|
}
|
|
}.bind(this)
|
|
);
|
|
};
|
|
|
|
Project.prototype._readJson = function() {
|
|
var that = this;
|
|
|
|
if (this._json) {
|
|
return Q.resolve(this._json);
|
|
}
|
|
|
|
return Q.fcall(function() {
|
|
// This will throw if package.json does not exist
|
|
return fs.readFileSync(path.join(that._config.cwd, 'package.json'));
|
|
})
|
|
.then(function(buffer) {
|
|
// If package.json exists, use it's values as defaults
|
|
var defaults = {},
|
|
npm = JSON.parse(buffer.toString());
|
|
|
|
defaults.name =
|
|
npm.name || path.basename(that._config.cwd) || 'root';
|
|
defaults.description = npm.description;
|
|
defaults.main = npm.main;
|
|
defaults.authors = npm.contributors || npm.author;
|
|
defaults.license = npm.license;
|
|
defaults.keywords = npm.keywords;
|
|
|
|
return defaults;
|
|
})
|
|
.catch(function(err) {
|
|
// Most likely no package.json so just set default name
|
|
return { name: path.basename(that._config.cwd) || 'root' };
|
|
})
|
|
.then(function(defaults) {
|
|
return (that._json = readJson(that._config.cwd, {
|
|
assume: defaults,
|
|
logger: that._logger
|
|
}));
|
|
})
|
|
.spread(function(json, deprecated, assumed) {
|
|
var jsonStr;
|
|
|
|
if (deprecated) {
|
|
that._logger.warn(
|
|
'deprecated',
|
|
'You are using the deprecated ' + deprecated + ' file'
|
|
);
|
|
}
|
|
|
|
if (!assumed) {
|
|
that._jsonFile = path.join(
|
|
that._config.cwd,
|
|
deprecated ? deprecated : 'bower.json'
|
|
);
|
|
}
|
|
|
|
jsonStr = JSON.stringify(json, null, ' ') + '\n';
|
|
that._jsonHash = md5(jsonStr);
|
|
return (that._json = json);
|
|
});
|
|
};
|
|
|
|
Project.prototype._readInstalled = function() {
|
|
var componentsDir;
|
|
var that = this;
|
|
|
|
if (this._installed) {
|
|
return Q.resolve(this._installed);
|
|
}
|
|
|
|
// Gather all folders that are actual packages by
|
|
// looking for the package metadata file
|
|
componentsDir = relativeToBaseDir(this._config.cwd)(this._config.directory);
|
|
return (this._installed = Q.nfcall(glob, '*/.bower.json', {
|
|
cwd: componentsDir,
|
|
dot: true
|
|
}).then(function(filenames) {
|
|
var promises;
|
|
var decEndpoints = {};
|
|
|
|
// Foreach bower.json found
|
|
promises = filenames.map(function(filename) {
|
|
var name = path.dirname(filename);
|
|
var metaFile = path.join(componentsDir, filename);
|
|
|
|
// Read package metadata
|
|
return readJson(metaFile).spread(function(pkgMeta) {
|
|
decEndpoints[name] = {
|
|
name: name,
|
|
source: pkgMeta._originalSource || pkgMeta._source,
|
|
target: pkgMeta._target,
|
|
canonicalDir: path.dirname(metaFile),
|
|
pkgMeta: pkgMeta
|
|
};
|
|
});
|
|
});
|
|
|
|
// Wait until all files have been read
|
|
// and resolve with the decomposed endpoints
|
|
return Q.all(promises).then(function() {
|
|
return (that._installed = decEndpoints);
|
|
});
|
|
}));
|
|
};
|
|
|
|
Project.prototype._readLinks = function() {
|
|
var componentsDir;
|
|
var that = this;
|
|
|
|
// Read directory, looking for links
|
|
componentsDir = relativeToBaseDir(this._config.cwd)(this._config.directory);
|
|
return Q.nfcall(fs.readdir, componentsDir).then(
|
|
function(filenames) {
|
|
var promises;
|
|
var decEndpoints = {};
|
|
|
|
promises = filenames.map(function(filename) {
|
|
var dir = path.join(componentsDir, filename);
|
|
|
|
// Filter only those that are valid links
|
|
return validLink(dir).spread(function(valid, err) {
|
|
var name;
|
|
|
|
if (!valid) {
|
|
if (err) {
|
|
that._logger.debug(
|
|
'read-link',
|
|
'Link ' + dir + ' is invalid',
|
|
{
|
|
filename: dir,
|
|
error: err
|
|
}
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Skip links to files (see #783)
|
|
if (!valid.isDirectory()) {
|
|
return;
|
|
}
|
|
|
|
name = path.basename(dir);
|
|
return readJson(dir, {
|
|
assume: { name: name }
|
|
}).spread(function(json, deprecated) {
|
|
if (deprecated) {
|
|
that._logger.warn(
|
|
'deprecated',
|
|
'Package ' +
|
|
name +
|
|
' is using the deprecated ' +
|
|
deprecated
|
|
);
|
|
}
|
|
|
|
json._direct = true; // Mark as a direct dep of root
|
|
decEndpoints[name] = {
|
|
name: name,
|
|
source: dir,
|
|
target: '*',
|
|
canonicalDir: dir,
|
|
pkgMeta: json,
|
|
linked: true
|
|
};
|
|
});
|
|
});
|
|
});
|
|
|
|
// Wait until all links have been read
|
|
// and resolve with the decomposed endpoints
|
|
return Q.all(promises).then(function() {
|
|
return decEndpoints;
|
|
});
|
|
// Ignore if folder does not exist
|
|
},
|
|
function(err) {
|
|
if (err.code !== 'ENOENT') {
|
|
throw err;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
);
|
|
};
|
|
|
|
Project.prototype._removePackages = function(packages) {
|
|
var that = this;
|
|
var promises = [];
|
|
|
|
return (
|
|
scripts
|
|
.preuninstall(
|
|
that._config,
|
|
that._logger,
|
|
packages,
|
|
that._installed,
|
|
that._json
|
|
)
|
|
.then(function() {
|
|
mout.object.forOwn(packages, function(dir, name) {
|
|
var promise;
|
|
|
|
// Delete directory
|
|
if (!dir) {
|
|
promise = Q.resolve();
|
|
that._logger.warn(
|
|
'not-installed',
|
|
"'" +
|
|
name +
|
|
"'" +
|
|
' cannot be uninstalled as it is not currently installed',
|
|
{
|
|
name: name
|
|
}
|
|
);
|
|
} else {
|
|
promise = Q.nfcall(rimraf, dir);
|
|
that._logger.action('uninstall', name, {
|
|
name: name,
|
|
dir: dir
|
|
});
|
|
}
|
|
|
|
// Remove from json only if successfully deleted
|
|
if (
|
|
(that._options.save || that._config.save) &&
|
|
that._json.dependencies
|
|
) {
|
|
promise = promise.then(function() {
|
|
delete that._json.dependencies[name];
|
|
});
|
|
}
|
|
|
|
if (that._options.saveDev && that._json.devDependencies) {
|
|
promise = promise.then(function() {
|
|
delete that._json.devDependencies[name];
|
|
});
|
|
}
|
|
|
|
promises.push(promise);
|
|
});
|
|
|
|
return Q.all(promises);
|
|
})
|
|
.then(function() {
|
|
return that.saveJson();
|
|
})
|
|
// Run post-uninstall hook before resolving with removed packages.
|
|
.then(
|
|
scripts.postuninstall.bind(
|
|
null,
|
|
that._config,
|
|
that._logger,
|
|
packages,
|
|
that._installed,
|
|
that._json
|
|
)
|
|
)
|
|
// Resolve with removed packages
|
|
.then(function() {
|
|
return mout.object.filter(packages, function(dir) {
|
|
return !!dir;
|
|
});
|
|
})
|
|
);
|
|
};
|
|
|
|
Project.prototype._restoreNode = function(node, flattened, jsonKey, processed) {
|
|
var deps;
|
|
|
|
// Do not restore if the node is missing
|
|
if (node.missing) {
|
|
return;
|
|
}
|
|
|
|
node.dependencies = node.dependencies || {};
|
|
node.dependants = node.dependants || {};
|
|
processed = processed || {};
|
|
|
|
// Only process deps that are not yet processed
|
|
deps = mout.object.filter(node.pkgMeta[jsonKey], function(value, key) {
|
|
return !processed[node.name + ':' + key];
|
|
});
|
|
|
|
mout.object.forOwn(
|
|
deps,
|
|
function(value, key) {
|
|
var local = flattened[key];
|
|
var json = endpointParser.json2decomposed(key, value);
|
|
var restored;
|
|
var compatible;
|
|
var originalSource;
|
|
|
|
// Check if the dependency is not installed
|
|
if (!local) {
|
|
flattened[key] = restored = json;
|
|
restored.missing = true;
|
|
// Even if it is installed, check if it's compatible
|
|
// Note that linked packages are interpreted as compatible
|
|
// This might change in the future: #673
|
|
} else {
|
|
compatible =
|
|
local.linked ||
|
|
(!local.missing && json.target === local.pkgMeta._target);
|
|
|
|
if (!compatible) {
|
|
restored = json;
|
|
|
|
if (!local.missing) {
|
|
restored.pkgMeta = local.pkgMeta;
|
|
restored.canonicalDir = local.canonicalDir;
|
|
restored.incompatible = true;
|
|
} else {
|
|
restored.missing = true;
|
|
}
|
|
} else {
|
|
restored = local;
|
|
mout.object.mixIn(local, json);
|
|
}
|
|
|
|
// Check if source changed, marking as different if it did
|
|
// We only do this for direct root dependencies that are compatible
|
|
if (node.root && compatible) {
|
|
originalSource = mout.object.get(
|
|
local,
|
|
'pkgMeta._originalSource'
|
|
);
|
|
if (originalSource && originalSource !== json.source) {
|
|
restored.different = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cross reference
|
|
node.dependencies[key] = restored;
|
|
processed[node.name + ':' + key] = true;
|
|
|
|
restored.dependants = restored.dependants || {};
|
|
restored.dependants[node.name] = mout.object.mixIn({}, node); // We need to clone due to shared objects in the manager!
|
|
|
|
// Call restore for this dependency
|
|
this._restoreNode(restored, flattened, 'dependencies', processed);
|
|
|
|
// Do the same for the incompatible local package
|
|
if (local && restored !== local) {
|
|
this._restoreNode(local, flattened, 'dependencies', processed);
|
|
}
|
|
},
|
|
this
|
|
);
|
|
};
|
|
|
|
module.exports = Project;
|