123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829 |
- var glob = require('glob');
- var path = require('path');
- var fs = require('graceful-fs');
- var Q = require('q');
- var mout = require('mout');
- var rimraf = require('rimraf');
- var endpointParser = require('bower-endpoint-parser');
- var Logger = require('bower-logger');
- var Manager = require('./Manager');
- var defaultConfig = require('../config');
- var semver = require('../util/semver');
- var md5 = require('../util/md5');
- var createError = require('../util/createError');
- var readJson = require('../util/readJson');
- var validLink = require('../util/validLink');
- var scripts = require('./scripts');
- function Project(config, logger) {
- // This is the only architecture component that ensures defaults
- // on config and logger
- // The reason behind it is that users can likely use this component
- // directly if commands do not fulfil their needs
- this._config = defaultConfig(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) {
- // Cycle through the specified endpoints
- decEndpoints.forEach(function (decEndpoint) {
- var jsonEndpoint;
- jsonEndpoint = endpointParser.decomposed2json(decEndpoint);
- if (that._options.save) {
- that._json.dependencies = mout.object.mixIn(that._json.dependencies || {}, jsonEndpoint);
- }
- if (that._options.saveDev) {
- that._json.devDependencies = mout.object.mixIn(that._json.devDependencies || {}, 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;
- });
- };
- 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();
- 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 propery 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);
- }
- // Read local json
- return this._json = readJson(this._config.cwd, {
- assume: { name: path.basename(this._config.cwd) || 'root' }
- })
- .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 = path.join(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 = path.join(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._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();
- })
- // 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;
|