UNPKG

jspm

Version:

Registry and format agnostic JavaScript package manager

1,088 lines (933 loc) 34.8 kB
/* * Copyright 2014-2015 Guy Bedford (http://guybedford.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ require('core-js/es6/string'); var config = require('./config'); var Promise = require('rsvp').Promise; var asp = require('rsvp').denodeify; var pkg = require('./package'); var semver = require('./semver'); var PackageName = require('./config/package-name'); var ui = require('./ui'); var path = require('path'); var link = require('./link'); var globalConfig = require('./global-config'); var rimraf = require('rimraf'); var alphabetize = require('./common').alphabetize; var cascadeDelete = require('./common').cascadeDelete; var hasProperties = require('./common').hasProperties; var fs = require('graceful-fs'); var primaryRanges = {}; var secondaryRanges = {}; var installedResolves = {}; var installingResolves = {}; var installed; var installing = { baseMap: {}, depMap: {} }; // NB remove assertions for release // function assert(statement, name) { // if (!statement) // throw new TypeError('Assertion Failed: ' + name); // } /* * Main install API wrapper * * install('jquery') * install('jquery', {options}) * install('jquery', 'github:components/jquery') * install('jquery', 'github:components/jquery', {options}) * install(true) - from package.json * install(true, {options}) - from package.json * * options.force - skip cache * options.inject * options.link means symlink linked versions in ranges to jspm_packages where available * options.lock - lock existing tree dependencies * options.latest - new install tree has all deps installed to latest - no rollback deduping * options.unlink * options.quick - lock and skip hash checks * options.dev - store in devDependencies * options.production - only install dependencies, not devDependencies * options.update - we're updating the targets * * options.summary - show fork and resolution summary */ exports.install = function(targets, options) { if (typeof targets === 'string') { var name = targets; targets = {}; targets[name] = typeof options === 'string' ? options : ''; options = typeof options === 'object' ? options : arguments[2]; } options = options || {}; return config.load() .then(function() { installed = installed || config.loader; if (options.force) config.force = true; if (options.link || options.quick) options.lock = true; var d, existingTargets = {}; if (!options.production) { for (d in config.pjson.devDependencies) existingTargets[d] = config.pjson.devDependencies[d]; } for (d in config.pjson.dependencies) existingTargets[d] = config.pjson.dependencies[d]; if (targets === true) targets = existingTargets; // check and set targets for update else if (targets && options.update) for (d in targets) { if (!existingTargets[d]) throw '%' + d + '% is not an existing dependency to update.'; targets[d] = existingTargets[d]; } targets = pkg.processDeps(targets, globalConfig.config.defaultRegistry); return Promise.all(Object.keys(targets).map(function(name) { return install(name, targets[name], options); })) .then(function() { return saveInstall(); }) .then(function() { // after every install, show fork and resolution summary if (options.summary !== false) showVersions(true); }); }); }; /* * install('jquery', 'jquery', { latest, lock, parent, inject, link, unlink, override } [, seen]) * * Install modes: * - Default a. The new install tree is set to use exact latest versions of primaries, * including for existing primaries. * Secondaries tend to their latest ideal version. * b. Forks within the new tree are deduped for secondaries by checking for * rollback of the higher version. * c. Forks against the existing tree are handled by upgrading the existing * tree, at both primary and secondary levels, with the secondary fork * potentially rolling back as well. * (this is `jspm install package`) * * - Lock No existing dependencies are altered. * New installs otherwise follow default behaviour for secondary deduping. * (this is reproducible installs, `jspm install` without arguments) * * - Latest Secondaries set exactly to latest. * Forks against existing tree follow default behaviour. * (this is `jspm update`) * * Lock and Latest can be combined, which won't do anything for existing * installs but will give latest secondaries on new installs. * * Secondary installs are those with a parent. * * Seen allows correct completion with circular package installs * */ /* * jspm.install('jquery') * jspm.install('jquery', 'github:components/jquery@^2.0.0') * jspm.install('jquery', '2') * jspm.install('jquery', 'github:components/jquery') * jspm.install('jquery', { force: true }); * jspm.install({ jquery: '1.2.3' }, { force: true }) */ function install(name, target, options, seen) { // we install a target range, to an exact version resolution var resolution; var dependencyDownloads; var existing; return Promise.resolve() .then(function() { if (options.link) return Promise.resolve(target); return pkg.locate(target); }) .then(function(located) { target = located; config.loader.ensureRegistry(located.registry, options.inject); if (options.link) return link.lookup(target, options.edge); // lock if necessary if (options.lock && (resolution = getInstalledMatch(target, options.parent, name))) return Promise.resolve(); // perform a full version lookup return pkg.lookup(target, options.edge); }) .then(function(getLatestMatch) { if (!getLatestMatch) return storeResolution(); // --- version constraint solving --- // a. The new install tree is set to use exact latest versions of primaries, including for existing primaries. // Secondaries tend to their latest ideal version. resolution = getLatestMatch(target.version); if (!resolution) { if (options.parent) throw 'Installing `' + options.parent + '`, no version match for `' + target.exactName + '`'; else throw 'No version match found for `' + target.exactName + '`'; } // if no version range was specified on install, install to semver-compatible with the latest if (!options.parent && !target.version && !options.link) { if (resolution.version.match(semver.semverRegEx)) target.setVersion('^' + resolution.version); else target.setVersion(resolution.version); } else if (options.edge && !options.parent && !options.link) { // use strictest compatible semver range if installing --edge without target, or // with a range that does not include the resolved version if (!target.version || !semver.match(target.version, resolution.version)) { target.setVersion('^' + resolution.version); } } // load our fork ranges to do a resolution return loadExistingForkRanges(resolution, name, options.parent, options.inject) .then(function() { // here, alter means upgrade or rollback // if we've consolidated with another resolution, we don't do altering var consolidated = false; // b. Forks within the new tree are deduped for secondaries by checking for rollback of the higher version if (!options.latest) resolveForks(installing, name, options.parent, resolution, function(forkVersion, forkRanges, allSecondary) { // alter the other secondaries to this primary or secondary if (allSecondary && forkRanges.every(function(forkRange) { return semver.match(forkRange, resolution.version); })) { consolidated = true; return resolution.version; } // alter this secondary install to the other primary or secondary if (!consolidated && options.parent && semver.match(target.version, forkVersion)) { consolidated = true; if (forkVersion !== resolution.version) { var newResolution = resolution.copy().setVersion(forkVersion); logResolution(installingResolves, resolution, newResolution); resolution = newResolution; } } }); // c. Forks against the existing tree are handled by upgrading the existing tree, // at both primary and secondary levels, with the secondary fork potentially rolling back as well. resolveForks(installed, name, options.parent, resolution, function(forkVersion, forkRanges) { if (options.latest && semver.compare(forkVersion, resolution.version) === 1) return; if (forkRanges.every(function(forkRange) { return semver.match(forkRange, resolution.version); })) { consolidated = true; return resolution.version; } // find the best upgrade of all the fork ranges for rollback of secondaries if (!consolidated && options.parent && !options.latest) { var bestSecondaryRollback = resolution; forkRanges.forEach(function(forkRange) { var forkLatest = getLatestMatch(forkRange); if (semver.compare(bestSecondaryRollback.version, forkLatest.version) === 1) bestSecondaryRollback = forkLatest; }); if (semver.compare(bestSecondaryRollback.version, forkVersion) === -1) bestSecondaryRollback = getLatestMatch(forkVersion); if (semver.match(target.version, bestSecondaryRollback.version)) { consolidated = true; logResolution(installingResolves, resolution, bestSecondaryRollback); resolution = bestSecondaryRollback; return bestSecondaryRollback.version; } } }); // solve and save resolution solution synchronously - this lock avoids solution conflicts storeResolution(); }); }) .then(function() { // -- handle circular installs -- seen = seen || []; if (seen.indexOf(resolution.exactName) !== -1) return; seen.push(resolution.exactName); // -- download -- return Promise.resolve() .then(function() { if (options.link) return link.symlink(resolution, downloadDeps); if (options.inject) return pkg.inject(resolution, downloadDeps); // override, quick, unlink options passed return pkg.download(resolution, options, downloadDeps); }) .then(function(fresh) { resolution.fresh = fresh; // log sub-dependencies before child completion for nicer output if (options.parent) logInstall(name, target, resolution, options); return dependencyDownloads; }) .then(function() { if (!options.parent) logInstall(name, target, resolution, options); }); }); // store resolution in config function storeResolution() { var curMap; if (options.parent) { curMap = (existing ? installed : installing).depMap; curMap[options.parent] = curMap[options.parent] || {}; curMap[options.parent][name] = resolution.copy(); } else { curMap = (existing ? installed : installing).baseMap; curMap[name] = resolution.copy(); } // update the dependency range tree if (!options.parent) { if (!primaryRanges[name] || primaryRanges[name].exactName !== target.exactName) primaryRanges[name] = target.copy(); // store in package.json if (!options.link) { if (name in config.pjson.dependencies) config.pjson.dependencies[name] = primaryRanges[name]; else if (name in config.pjson.devDependencies) config.pjson.devDependencies[name] = primaryRanges[name]; else if (!options.dev) { config.pjson.dependencies[name] = primaryRanges[name]; } else { config.pjson.devDependencies[name] = primaryRanges[name]; } if (options.override) config.pjson.overrides[resolution.exactName] = options.override; } } else { // update the secondary ranges secondaryRanges[options.parent] = secondaryRanges[options.parent] || {}; if (!secondaryRanges[options.parent][name]) secondaryRanges[options.parent][name] = target.copy(); else if (secondaryRanges[options.parent][name] && secondaryRanges[options.parent][name].exactName !== target.exactName) ui.log('warn', 'Currently installed dependency ranges of `' + options.parent + '` are not consistent ( %' + secondaryRanges[options.parent][name].exactName + '% should be %' + target.exactName + '%)'); } } // trigger dependency downloads // this can be triggered twice // - once by initial preload, and once post-build if additional dependencies are discovered function downloadDeps(depMap) { // clear existing dependencies on first run for existing installs if (!dependencyDownloads && existing) installed.depMap[resolution.exactName] = {}; dependencyDownloads = (dependencyDownloads || Promise.resolve()).then(function() { return Promise.all(Object.keys(depMap).map(function(dep) { return install(dep, depMap[dep], { latest: options.latest, lock: options.lock, parent: resolution.exactName, inject: options.inject, quick: options.quick }, seen); })); }); } } function getInstalledMatch(target, parent, name) { // use the config lock if provided if (parent) { if (installed.depMap[parent] && installed.depMap[parent][name]) return installed.depMap[parent][name]; } else { if (installed.baseMap[name]) return installed.baseMap[name]; } // otherwise seek an installed match var match; function checkMatch(pkg) { if (pkg.name !== target.name) return; if (semver.match(target.version, pkg.version)) { if (!match || match && semver.compare(pkg.version, match.version) === 1) match = pkg.copy(); } } Object.keys(installed.baseMap).forEach(function(name) { checkMatch(installed.baseMap[name]); }); Object.keys(installed.depMap).forEach(function(parent) { var depMap = installed.depMap[parent]; Object.keys(depMap).forEach(function(name) { checkMatch(depMap[name]); }); }); return match; } function saveInstall() { return Promise.resolve() .then(function() { // merge the installing tree into the installed Object.keys(installing.baseMap).forEach(function(p) { installed.baseMap[p] = installing.baseMap[p]; }); Object.keys(installing.depMap).forEach(function(p) { installed.depMap[p] = installing.depMap[p]; }); return clean(); }) .then(function() { if (hasProperties(installedResolves)) { ui.log(''); ui.log('info', 'The following existing package versions were altered by install deduping:'); ui.log(''); Object.keys(installedResolves).forEach(function(pkg) { var pkgName = new PackageName(pkg); ui.log('info', ' %' + pkgName.package + '% ' + getUpdateRangeText(pkgName, new PackageName(installedResolves[pkg]))); }); ui.log(''); installedResolves = {}; ui.log('info', 'To keep existing dependencies locked during install, use the %--lock% option.'); } if (hasProperties(installingResolves)) { ui.log(''); ui.log('info', 'The following new package versions were substituted by install deduping:'); ui.log(''); Object.keys(installingResolves).forEach(function(pkg) { var pkgName = new PackageName(pkg); ui.log('info', ' %' + pkgName.package + '% ' + getUpdateRangeText(pkgName, new PackageName(installingResolves[pkg]))); }); ui.log('') ; installingResolves = {}; } // then save return config.save(); }); } var logged = {}; function logInstall(name, target, resolution, options) { if (logged[target.exactName + '=' + resolution.exactName]) return; // don't log secondary fresh if (options.parent && resolution.fresh) return; logged[target.exactName + '=' + resolution.exactName] = true; var verb; if (options.inject) verb = 'Injected'; else if (!resolution.fresh) { if (!options.link) verb = 'Installed'; else verb = 'Linked'; } else { if (options.quick) return; if (!options.link) verb = 'Up to date -'; else verb = 'Already linked -'; } if (options.parent) ui.log('ok', verb + ' `' + target.exactName + '` (' + resolution.version + ')'); else ui.log('ok', verb + ' %' + name + '% as `' + target.exactName + '` (' + resolution.version + ')'); } function getUpdateRangeText(existing, update) { if (existing.name === update.name) return '`' + existing.version + '` -> `' + update.version + '`'; else return '`' + existing.exactName + '` -> `' + update.exactName + '`'; } // go through the baseMap and depMap, changing FROM to TO // keep a log of what we did in resolveLog function doResolution(tree, from, to) { if (from.exactName === to.exactName) return; // add this to the resolve log, including deep-updating resolution chains logResolution(tree === installed ? installedResolves : installingResolves, from, to); Object.keys(tree.baseMap).forEach(function(dep) { if (tree.baseMap[dep].exactName === from.exactName) tree.baseMap[dep] = to.copy(); }); Object.keys(tree.depMap).forEach(function(parent) { var curMap = tree.depMap[parent]; Object.keys(curMap).forEach(function(dep) { if (curMap[dep].exactName === from.exactName) curMap[dep] = to.copy(); }); }); } function logResolution(resolveLog, from, to) { resolveLog[from.exactName] = to.exactName; Object.keys(resolveLog).forEach(function(resolveFrom) { if (resolveLog[resolveFrom] === from.exactName) resolveLog[resolveFrom] = to.exactName; }); } // name and parentName are the existing resolution target // so we only look up forks and not the original as well function loadExistingForkRanges(resolution, name, parentName, inject) { var tree = installed; return Promise.all(Object.keys(tree.baseMap).map(function(dep) { if (!parentName && dep === name) return; var primary = tree.baseMap[dep]; if (primary.name !== resolution.name) return; return loadExistingRange(dep, null, inject); })) .then(function() { return Promise.all(Object.keys(tree.depMap).map(function(parent) { var curDepMap = tree.depMap[parent]; return Promise.all(Object.keys(curDepMap).map(function(dep) { if (parent === parentName && dep === name) return; var secondary = curDepMap[dep]; if (secondary.name !== resolution.name) return; return loadExistingRange(dep, parent, inject); })); })); }); } function visitForkRanges(tree, resolution, name, parentName, visit) { // now that we've got all the version ranges we need for consideration, // go through and run resolutions against the fork list Object.keys(tree.baseMap).forEach(function(dep) { var primary = tree.baseMap[dep]; if (primary.name !== resolution.name) return; visit(dep, null, primary, primaryRanges[dep]); }); Object.keys(tree.depMap).forEach(function(parent) { var curDepMap = tree.depMap[parent]; Object.keys(curDepMap).forEach(function(dep) { var secondary = curDepMap[dep]; if (secondary.name !== resolution.name) return; // its not a fork of itself if (dep === name && parent === parentName) return; // skip if we don't have a range if (!secondaryRanges[parent]) return; visit(dep, parent, secondary, secondaryRanges[parent][dep]); }); }); } // find all forks of this resolution in the tree // calling resolve(forkVersion, forkRanges, allSecondary) // for each unique fork version // sync resolution to avoid conflicts function resolveForks(tree, name, parentName, resolution, resolve) { // forks is a map from fork versions to an object, { ranges, hasPrimary } // hasPrimary indicates whether any of these ranges are primary ranges var forks = {}; var forkVersions = []; visitForkRanges(tree, resolution, name, parentName, function(dep, parent, resolved, range) { if (!range) return; // we only work with stuff within it's own matching range // not user overrides if (range.name !== resolved.name || !semver.match(range.version, resolved.version)) return; var forkObj = forks[resolved.version]; if (!forkObj) { forkObj = forks[resolved.version] = { ranges: [], allSecondary: true }; forkVersions.push(resolved.version); } if (!parent) forkObj.allSecondary = false; forkObj.ranges.push(range.version); }); // now run through and resolve the forks forkVersions.sort(semver.compare).reverse().forEach(function(forkVersion) { var forkObj = forks[forkVersion]; var newVersion = resolve(forkVersion, forkObj.ranges, forkObj.allSecondary); if (!newVersion || newVersion === forkVersion) return; var from = resolution.copy().setVersion(forkVersion); var to = resolution.copy().setVersion(newVersion); doResolution(tree, from, to); }); } var secondaryDepsPromises = {}; function loadExistingRange(name, parent, inject) { if (parent && secondaryRanges[parent] && secondaryRanges[parent][name]) return; else if (!parent && primaryRanges[name]) return; var _target; return Promise.resolve() .then(function() { if (!parent) return config.pjson.dependencies[name] || config.pjson.devDependencies[name]; return Promise.resolve() .then(function() { if (secondaryDepsPromises[parent]) return secondaryDepsPromises[parent]; return Promise.resolve() .then(function() { var parentPkg = new PackageName(parent); // if the package is installed but not in jspm_packages // then we wait on the getPackageConfig or download of the package here return (secondaryDepsPromises[parent] = new Promise(function(resolve, reject) { if (inject) return pkg.inject(parentPkg, resolve).catch(reject); if (config.deps[parentPkg.exactName]) return resolve(); pkg.download(parentPkg, {}, resolve).then(resolve, reject); }) .then(function(depMap) { if (depMap) return depMap; return config.deps; })); }); }) .then(function(deps) { return deps[name]; }); }) .then(function(target) { if (!target) { if (parent && installed.depMap[parent] && installed.depMap[parent].name) { delete installed.depMap[parent].name; ui.log('warn', '%' + parent + '% dependency %' + name + '% was removed from the config file to reflect the installed package.'); } else if (!parent) { ui.log('warn', '%' + name + '% is installed in the config file, but is not a dependency in the package.json. It is advisable to add it to the package.json file.'); } return; } _target = target.copy(); // locate the target return pkg.locate(_target) .then(function(located) { if (parent) { secondaryRanges[parent] = secondaryRanges[parent] || {}; secondaryRanges[parent][name] = located; } else { primaryRanges[name] = located; } }); }); } // given an exact package, find all the forks, and display the ranges function showInstallGraph(pkg) { installed = installed || config.loader; pkg = new PackageName(pkg); var lastParent; var found; return loadExistingForkRanges(pkg, config.loader.local) .then(function() { ui.log('info', '\nInstalled versions of %' + pkg.name + '%'); visitForkRanges(installed, pkg, null, null, function(name, parent, resolved, range) { found = true; if (range.version === '') range.version = '*'; var rangeVersion = range.name === resolved.name ? range.version : range.exactName; if (range.version === '*') range.version = ''; if (!parent) ui.log('info', '\n %' + name + '% `' + resolved.version + '` (' + rangeVersion + ')'); else { if (lastParent !== parent) { ui.log('info', '\n ' + parent); lastParent = parent; } ui.log('info', ' ' + name + ' `' + resolved.version + '` (' + rangeVersion + ')'); } }); if (!found) ui.log('warn', 'Package `' + pkg.name + '` not found.'); ui.log(''); }); } exports.showInstallGraph = showInstallGraph; function showVersions(forks) { installed = installed || config.loader; var versions = {}; var haveLinked = false; var linkedVersions = {}; function addDep(dep) { var vList = versions[dep.name] = versions[dep.name] || []; var version = dep.version; try { if (fs.readlinkSync(dep.getPath())) linkedVersions[dep.exactName] = true; } catch(e) {} if (vList.indexOf(version) === -1) vList.push(version); } Object.keys(installed.baseMap).forEach(function(dep) { addDep(installed.baseMap[dep]); }); Object.keys(installed.depMap).forEach(function(parent) { var curMap = installed.depMap[parent]; Object.keys(curMap).forEach(function(dep) { addDep(curMap[dep]); }); }); versions = alphabetize(versions); var vLen = Object.keys(versions).map(function(dep) { return dep.length; }).reduce(function(a, b) { return Math.max(a, b); }, 0); var shownIntro = false; Object.keys(versions).forEach(function(dep) { var vList = versions[dep].sort(semver.compare).map(function(version) { if (linkedVersions[dep + '@' + version]) { haveLinked = true; return '%' + version + '%'; } else return '`' + version + '`'; }); if (forks && vList.length === 1) return; if (!shownIntro) { ui.log('info', 'Installed ' + (forks ? 'Forks' : 'Versions') + '\n'); shownIntro = true; } var padding = vLen - dep.length; var paddingString = ''; while(padding--) paddingString += ' '; ui.log('info', ' ' + paddingString + '%' + dep + '% ' + vList.join(' ')); }); if (haveLinked) { ui.log('info', '\nBold versions are linked. To unlink use %jspm install --unlink [name]%.'); } if (shownIntro) { ui.log('info', '\nTo inspect individual package constraints, use %jspm inspect registry:name%.\n'); } else if (forks) { ui.log('ok', 'Install tree has no forks.'); } } exports.showVersions = showVersions; /* * Configuration cleaning * * 1. Construct list of all packages in main tree tracing from package.json primary installs * 2. Remove all orphaned dependencies not in this list * 3. Remove any package.json overrides that will never match this list * 4. Remove packages in .dependencies.json that aren't used at all * 5. Remove anything from jspm_packages not in this list * */ function clean() { var packageList = []; return config.load() .then(function() { // 1. getDependentPackages for each of baseMap Object.keys(config.loader.baseMap).forEach(function(dep) { getDependentPackages(config.loader.baseMap[dep].exactName, packageList); }); // 2. now that we have the package list, remove everything not in it Object.keys(config.loader.depMap).forEach(function(dep) { if (packageList.indexOf(dep) === -1) { ui.log('info', 'Clearing configuration for `' + dep + '`'); delete config.loader.depMap[dep]; } }); // 3. remove package.json overrides which will never match any packages var usedOverrides = []; packageList.forEach(function(pkgName) { var pkgVersion = pkgName.split('@').pop(); pkgName = pkgName.substr(0, pkgName.length - pkgVersion.length - 1); var overrideVersion = Object.keys(config.pjson.overrides) .filter(function(overrideName) { return overrideName.startsWith(pkgName + '@'); }) .map(function(overrideName) { return overrideName.split('@').pop(); }) .filter(function(overrideVersion) { return semver.match('^' + overrideVersion, pkgVersion); }) .sort(semver.compare).pop(); if (overrideVersion) usedOverrides.push(pkgName + '@' + overrideVersion); }); Object.keys(config.pjson.overrides).forEach(function(overrideName) { if (usedOverrides.indexOf(overrideName) == -1) { ui.log('info', 'Removing unused package.json override `' + overrideName + '`'); delete config.pjson.overrides[overrideName]; } }); }) .then(function() { return asp(fs.lstat)(config.pjson.packages) .catch(function(e) { if (e.code == 'ENOENT') return; throw e; }).then(function(stats) { // Skip if jspm_packages is symlinked or not existing if (!stats || stats.isSymbolicLink()) return; // 4. Remove packages in .dependencies.json that aren't used at all Object.keys(config.deps).forEach(function(dep) { if (packageList.indexOf(dep) == -1) delete config.deps[dep]; }); // 5. Remove anything from jspm_packages not in this list return readDirWithDepth(config.pjson.packages, function(dirname) { if (dirname.split(path.sep).pop().indexOf('@') <= 0) return true; }) .then(function(packageDirs) { return Promise.all( packageDirs .filter(function(dir) { var exactName = path.relative(config.pjson.packages, dir).replace(path.sep, ':').replace(/\\/g, '/'); // (win) var remove = packageList.indexOf(exactName) === -1; if (remove) ui.log('info', 'Removing package files for `' + exactName + '`'); return remove; }) .map(function(dir) { return asp(rimraf)(dir) .then(function() { var filename = dir + '.js'; return new Promise(function(resolve) { fs.exists(filename, resolve); }).then(function(exists) { if (exists) return asp(fs.unlink)(filename); }); }) .then(function() { return cascadeDelete(dir); }); })); }); }); }) .then(function() { return config.save(); }); } exports.clean = clean; // depthCheck returns true to keep going (files being ignored), false to add the dir to the flat list function readDirWithDepth(dir, depthCheck) { var flatDirs = []; return asp(fs.readdir)(dir) .then(function(files) { if (!files) return []; return Promise.all(files.map(function(file) { var filepath = path.resolve(dir, file); // ensure it is a directory return asp(fs.lstat)(filepath) .then(function(fileInfo) { if (!fileInfo.isDirectory()) return; if (!depthCheck(filepath)) return flatDirs.push(filepath); // keep going return readDirWithDepth(filepath, depthCheck) .then(function(items) { items.forEach(function(item) { flatDirs.push(item); }); }); }); })); }) .then(function() { return flatDirs; }); } function getDependentPackages(pkg, packages) { packages.push(pkg); // get all immediate children of this package // for those children not already seen (in packages list), // run getDependentPackages in turn on those var depMap = config.loader.depMap[pkg]; if (!depMap) return; Object.keys(depMap).forEach(function(dep) { var curPkg = depMap[dep].exactName; if (packages.indexOf(curPkg) !== -1) return; getDependentPackages(curPkg, packages); }); return packages; } exports.uninstall = function(names) { if (!(names instanceof Array)) names = [names]; return config.load() .then(function() { installed = installed || config.loader; names.forEach(function(name) { if (!config.pjson.dependencies[name] && !config.pjson.devDependencies[name]) ui.log('warn', 'Dependency %' + name + '% is not an existing primary install.'); delete config.pjson.dependencies[name]; delete config.pjson.devDependencies[name]; delete installed.baseMap[name]; }); return clean(); }); }; /* * Resolve all installs of the given package to a specific version */ exports.resolveOnly = function(pkg) { pkg = new PackageName(pkg); if (!pkg.version || !pkg.registry) { ui.log('warn', 'Resolve --only must take an exact package of the form `registry:pkg@version`.'); return Promise.reject(); } var didSomething = false; return config.load() .then(function() { Object.keys(config.loader.baseMap).forEach(function(name) { var curPkg = config.loader.baseMap[name]; if (curPkg.registry === pkg.registry && curPkg.package === pkg.package && curPkg.version !== pkg.version) { didSomething = true; ui.log('info', 'Primary install ' + getUpdateRangeText(curPkg, pkg)); config.loader.baseMap[name] = pkg.copy(); } }); Object.keys(config.loader.depMap).forEach(function(parent) { var curMap = config.loader.depMap[parent]; Object.keys(curMap).forEach(function(name) { var curPkg = curMap[name]; if (curPkg.registry === pkg.registry && curPkg.package === pkg.package && curPkg.version !== pkg.version) { didSomething = true; ui.log('info', 'In %' + parent + '% ' + getUpdateRangeText(curPkg, pkg)); curMap[name] = pkg.copy(); } }); }); return config.save(); }) .then(function() { if (didSomething) ui.log('ok', 'Resolution to only use `' + pkg.exactName + '` completed successfully.'); else ui.log('ok', '`' + pkg.exactName + '` is already the only version of the package in use.'); }); };