UNPKG

46.3 kBJavaScriptView Raw
1/*
2 * Copyright 2014-2016 Guy Bedford (http://guybedford.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17require('core-js/es6/string');
18
19var Promise = require('bluebird');
20
21var config = require('./config');
22var asp = require('bluebird').Promise.promisify;
23var pkg = require('./package');
24var semver = require('./semver');
25var PackageName = require('./package-name');
26var ui = require('./ui');
27var path = require('path');
28var globalConfig = require('./config/global-config');
29
30var rimraf = require('rimraf');
31
32var alphabetize = require('./common').alphabetize;
33var cascadeDelete = require('./common').cascadeDelete;
34var hasProperties = require('./common').hasProperties;
35var processDeps = require('./common').processDeps;
36var extend = require('./common').extend;
37
38var Loader = require('../api').Loader;
39
40var fs = require('graceful-fs');
41
42var primaryRanges = {};
43var secondaryRanges;
44
45var installedResolves = {};
46var installingResolves = {};
47
48var installed;
49var installing = {
50 baseMap: {},
51 depMap: {}
52};
53
54function runHook(name) {
55 var hooks = config.pjson.hooks;
56 if (hooks && hooks[name]) {
57 return new Loader().import(hooks[name])
58 .then(function (m) {
59 if (m.default && typeof m.default === 'function') {
60 ui.log('info', 'Running ' + name + ' hook...');
61 return Promise.resolve(m.default());
62 }
63 })
64 .catch(function (error) {
65 ui.log('err', 'Error during ' + name + ' hook');
66 if (error) {
67 ui.log('err', error);
68 }
69 });
70 }
71}
72
73/*
74 * Main install API wrapper
75 *
76 * install('jquery')
77 * install('jquery', {options})
78 * install('jquery', 'github:components/jquery')
79 * install('jquery', 'github:components/jquery', {options})
80 * install(true) - from package.json
81 * install(true, {options}) - from package.json
82 *
83 * options.force - skip cache
84 * options.inject
85 * options.lock - lock existing tree dependencies
86 * options.latest - new install tree has all deps installed to latest - no rollback deduping
87 * options.unlink - if linked, unlink and install from registry source
88 * options.quick - lock and skip hash checks
89 * options.dev - store in devDependencies
90 * options.production - only install dependencies, not devDependencies
91 * options.peer - store in peerDependencies
92 * options.update - we're updating the targets
93 *
94 * options.summary - show fork and resolution summary
95 */
96exports.install = function(targets, options) {
97 if (targets === undefined)
98 targets = true;
99 if (typeof targets === 'string') {
100 var name = targets;
101 targets = {};
102 targets[name] = typeof options === 'string' ? options : '';
103 options = typeof options === 'object' ? options : arguments[2];
104 }
105 options = options || {};
106
107 return config.load()
108 .then(function () {
109 return runHook('preinstall');
110 })
111 .then(function() {
112 installed = installed || config.loader;
113 secondaryRanges = secondaryRanges || config.deps;
114
115 if (options.force)
116 config.force = true;
117
118 if (options.quick)
119 options.lock = true;
120
121 var d, existingTargets = {};
122
123 if (!options.production) {
124 for (d in config.pjson.devDependencies)
125 existingTargets[d] = config.pjson.devDependencies[d];
126 }
127
128 for (d in config.pjson.devDependencies)
129 existingTargets[d] = config.pjson.devDependencies[d];
130 for (d in config.pjson.dependencies)
131 existingTargets[d] = config.pjson.dependencies[d];
132 for (d in config.pjson.peerDependencies)
133 existingTargets[d] = config.pjson.peerDependencies[d];
134
135 var bulk = targets === true;
136 if (bulk ) {
137 targets = existingTargets;
138 if (!options.update) {
139 options.lock = true;
140 // cant bullk assign dev or peer
141 options.dev = options.peer = false;
142 }
143 }
144 // check and set targets for update
145 else if (targets && options.update) {
146 for (d in targets) {
147 if (!existingTargets[d])
148 throw '%' + d + '% is not an existing dependency to update.';
149 targets[d] = existingTargets[d];
150 }
151 }
152
153 targets = processDeps(targets, globalConfig.config.defaultRegistry);
154
155 return Promise.all(Object.keys(targets).map(function(name) {
156 var opts = extend({}, options);
157
158 // set config.peer / config.dev per package
159 if (bulk || options.update) {
160 if (config.pjson.peerDependencies[name])
161 opts.peer = true;
162 else if (config.pjson.devDependencies[name])
163 opts.dev = true;
164 }
165
166 return install(name, targets[name], opts);
167 }))
168 .then(function() {
169 return saveInstall();
170 })
171 .then(function () {
172 return runHook('postinstall');
173 })
174 .then(function() {
175 // after every install, show fork and resolution summary
176 if (options.summary !== false)
177 showVersions(true);
178 });
179 });
180};
181
182/*
183 * install('jquery', 'jquery', { latest, lock, parent, inject, unlink, override, peer } [, seen])
184 *
185 * Install modes:
186 * - Default a. The new install tree is set to use exact latest versions of primaries,
187 * including for existing primaries.
188 * Secondaries tend to their latest ideal version.
189 * b. Forks within the new tree are deduped for secondaries by checking for
190 * rollback of the higher version.
191 * c. Forks against the existing tree are handled by upgrading the existing
192 * tree, at both primary and secondary levels, with the secondary fork
193 * potentially rolling back as well.
194 * (this is `jspm install package`)
195 *
196 * - Lock No existing dependencies are altered.
197 * New installs otherwise follow default behaviour for secondary deduping.
198 * (this is reproducible installs, `jspm install` without arguments)
199 *
200 * - Latest Secondaries set exactly to latest.
201 * Forks against existing tree follow default behaviour.
202 * (this is `jspm update`)
203 *
204 * Lock and Latest can be combined, which won't do anything for existing
205 * installs but will give latest secondaries on new installs.
206 *
207 * Secondary installs are those with a parent.
208 *
209 * Seen allows correct completion with circular package installs
210 *
211 */
212
213/*
214 * jspm.install('jquery')
215 * jspm.install('jquery', 'github:components/jquery@^2.0.0')
216 * jspm.install('jquery', '2')
217 * jspm.install('jquery', 'github:components/jquery')
218 * jspm.install('jquery', { force: true });
219 * jspm.install({ jquery: '1.2.3' }, { force: true })
220 */
221function install(name, target, options, seen) {
222 // we install a target range, to an exact version resolution
223 var resolution;
224
225 var dependencyDownloads, dependencyDownloadsError;
226
227 var linked;
228 var alreadyInstalled;
229
230 return Promise.resolve()
231 .then(function() {
232 return pkg.locate(target);
233 })
234 .then(function(located) {
235 target = located;
236
237 // peerDependencies are dependencies which are installed as primary dependencies
238 // even though they are not represented in the package.json install ranges
239 // these will conflict as its a single namespace, so when they do we must resolve that
240 if (!options.peer || !options.parent)
241 return;
242
243 var existing = installing.baseMap[name] || installed.baseMap[name];
244
245 // when no existing version, do a normal lookup thing
246 if (!existing)
247 return;
248
249 if (options.peerResolved)
250 return;
251 return (options.lock ? Promise.resolve() : resolvePeerConflicts(name, options.parent, target, existing))
252 .then(function(targetRange) {
253 if (targetRange) {
254 target = targetRange;
255 return;
256 }
257
258 // continue to use exsiting version
259 target = config.pjson.peerDependencies[name] || config.pjson.devDependencies[name] || config.pjson.dependencies[name] || target;
260 return existing;
261 });
262 })
263 .then(function(peerResolution) {
264
265 // get the natural installed match before doing any network lookups
266 var installedResolution = peerResolution || getInstalledMatch(target, options.parent, name);
267
268 if (!installedResolution || options.unlink)
269 return;
270
271 // check if it is linked (linked packages aren't updated and lock by default)
272 return asp(fs.lstat)(installedResolution.getPath())
273 .then(function(stats) {
274 return stats.isSymbolicLink();
275 }, function(err) {
276 if (err.code == 'ENOENT')
277 return false;
278 throw err;
279 })
280 .then(function(isLinked) {
281 // are linked -> note we are linked, and use lock to it
282 if (isLinked) {
283 linked = true;
284 return installedResolution;
285 }
286
287 // not linked -> only use installedResolution if options.lock or peerResolution
288 if (options.lock || peerResolution) {
289 return installedResolution;
290 }
291 });
292 })
293 .then(function(lockResolution) {
294 if (lockResolution) {
295 resolution = lockResolution;
296 return;
297 }
298
299 // perform a full version lookup
300 return pkg.lookup(target, options.edge);
301 })
302 .then(function(getLatestMatch) {
303 if (!getLatestMatch)
304 return storeResolution();
305
306 // --- version constraint solving ---
307
308 // a. The new install tree is set to use exact latest versions of primaries, including for existing primaries.
309 // Secondaries tend to their latest ideal version.
310 resolution = getLatestMatch(target.version);
311
312 if (!resolution) {
313 if (options.parent)
314 throw 'Installing `' + options.parent + '`, no version match for `' + target.exactName + '`';
315 else
316 throw 'No version match found for `' + target.exactName + '`';
317 }
318
319 if (options.exact) {
320 target.setVersion(resolution.version);
321 }
322 // if no version range was specified on install, install to semver-compatible with the latest
323 else if (!options.parent && !target.version) {
324 if (resolution.version.match(semver.semverRegEx))
325 target.setVersion('^' + resolution.version);
326 else
327 target.setVersion(resolution.version);
328 }
329 else if (options.edge && !options.parent) {
330 // use strictest compatible semver range if installing --edge without target, or
331 // with a range that does not include the resolved version
332 if (!target.version || !semver.match(target.version, resolution.version)) {
333 target.setVersion('^' + resolution.version);
334 }
335 }
336
337 var forkVersions = [];
338
339 // load our fork ranges to do a resolution
340 return loadExistingForkRanges(resolution, name, options.parent, options.inject)
341 .then(function() {
342 // here, alter means upgrade or rollback
343
344 // if we've consolidated with another resolution, we don't do altering
345 var consolidated = false;
346
347 // b. Forks within the new tree are deduped for secondaries by checking for rollback of the higher version
348 if (!options.latest)
349 resolveForks(installing, installed, name, options.parent, resolution, function(forkVersion, forkRanges, allSecondary) {
350 forkVersions.push(forkVersion);
351
352 // alter the other secondaries to this primary or secondary
353 if (allSecondary && forkRanges.every(function(forkRange) {
354 return semver.match(forkRange, resolution.version);
355 })) {
356 consolidated = true;
357 return resolution.version;
358 }
359
360 // alter this secondary install to the other primary or secondary
361 if (!consolidated && options.parent && semver.match(target.version, forkVersion)) {
362 consolidated = true;
363 if (forkVersion !== resolution.version) {
364 var newResolution = resolution.copy().setVersion(forkVersion);
365 logResolution(installingResolves, resolution, newResolution);
366 resolution = newResolution;
367 }
368 }
369 });
370
371 // c. Forks against the existing tree are handled by upgrading the existing tree,
372 // at both primary and secondary levels, with the secondary fork potentially rolling back as well.
373 resolveForks(installed, installing, name, options.parent, resolution, function(forkVersion, forkRanges) {
374 forkVersions.push(forkVersion);
375
376 if (options.latest && semver.compare(forkVersion, resolution.version) === 1)
377 return;
378
379 if (forkRanges.every(function(forkRange) {
380 return semver.match(forkRange, resolution.version);
381 })) {
382 consolidated = true;
383 return resolution.version;
384 }
385
386 // find the best upgrade of all the fork ranges for rollback of secondaries
387 if (!consolidated && options.parent && !options.latest) {
388 var bestSecondaryRollback = resolution;
389 forkRanges.forEach(function(forkRange) {
390 var forkLatest = getLatestMatch(forkRange);
391 if (semver.compare(bestSecondaryRollback.version, forkLatest.version) === 1)
392 bestSecondaryRollback = forkLatest;
393 });
394
395 if (semver.compare(bestSecondaryRollback.version, forkVersion) === -1)
396 bestSecondaryRollback = getLatestMatch(forkVersion);
397
398 if (semver.match(target.version, bestSecondaryRollback.version)) {
399 consolidated = true;
400 logResolution(installingResolves, resolution, bestSecondaryRollback);
401 resolution = bestSecondaryRollback;
402 return bestSecondaryRollback.version;
403 }
404 }
405 });
406
407 // solve and save resolution solution synchronously - this lock avoids solution conflicts
408 storeResolution();
409
410 // if we've already installed to the semver range of this dependency
411 // then note this "version" is already installed
412 return forkVersions.some(function(forkVersion) {
413 return semver.match('^' + forkVersion, resolution.version);
414 });
415 });
416 })
417 .then(function(_alreadyInstalled) {
418 alreadyInstalled = _alreadyInstalled;
419
420 // -- handle circular installs --
421 seen = seen || [];
422 if (seen.indexOf(resolution.exactName) !== -1)
423 return;
424 seen.push(resolution.exactName);
425
426 // -- download --
427 // we support custom resolution maps to non-registries!
428 if (!resolution.registry)
429 return;
430
431 config.loader.ensureRegistry(resolution.registry, options.inject);
432
433 return Promise.resolve()
434 .then(function() {
435 if (options.inject)
436 return pkg.inject(resolution, depsCallback);
437
438 return pkg.download(resolution, {
439 unlink: options.unlink,
440 linked: linked,
441 override: options.override,
442 quick: options.quick,
443 force: options.force
444 }, depsCallback);
445 })
446 .then(function(fresh) {
447 resolution.fresh = fresh;
448 // log sub-dependencies before child completion for nicer output
449 if (options.parent)
450 logInstall(name, target, resolution, linked, options);
451
452 return dependencyDownloads;
453 })
454 .then(function() {
455 if (dependencyDownloadsError)
456 return Promise.reject(dependencyDownloadsError);
457
458 if (!options.parent)
459 logInstall(name, target, resolution, linked, options);
460 });
461 });
462
463 // store resolution in config
464 function storeResolution() {
465 // support custom install maps
466 if (!resolution.registry)
467 return;
468
469 var alreadyInstalled;
470
471 if (options.parent && !options.peer) {
472 installing.depMap[options.parent] = installing.depMap[options.parent] || {};
473
474 alreadyInstalled = !!installing.depMap[options.parent][name] && installing.depMap[options.parent][name].exactName == resolution.exactName;
475 installing.depMap[options.parent][name] = resolution.copy();
476 }
477 else {
478 alreadyInstalled = installing.baseMap[name] == resolution.exactName;
479 installing.baseMap[name] = resolution.copy();
480 }
481
482 // update the dependency range tree
483 if (!options.parent || options.peer) {
484 if (!primaryRanges[name] || primaryRanges[name].exactName !== target.exactName)
485 primaryRanges[name] = target.copy();
486
487 // peer dependency of a dev dependency is a dev dependency unless it was already a peer dependency or dependency
488 if (options.parent && options.peer && options.dev) {
489 if (config.pjson.peerDependencies[name])
490 options.dev = false;
491 else if (config.pjson.dependencies[name])
492 options.peer = options.dev = false;
493 else
494 options.peer = false;
495 }
496 // primary install that is a peer dependency remains a peer dependency
497 else if (!options.parent && !options.peer && !options.dev) {
498 if (config.pjson.peerDependencies[name])
499 options.peer = true;
500 }
501
502 if (options.peer)
503 config.pjson.peerDependencies[name] = primaryRanges[name];
504 else if (options.dev)
505 config.pjson.devDependencies[name] = primaryRanges[name];
506 else
507 config.pjson.dependencies[name] = primaryRanges[name];
508
509 // remove any alternative installs of this dependency
510 if (!options.dev)
511 delete config.pjson.devDependencies[name];
512 if (!options.peer)
513 delete config.pjson.peerDependencies[name];
514 if (options.dev && !options.parent || options.peer)
515 delete config.pjson.dependencies[name];
516 }
517 else {
518 // update the secondary ranges
519 secondaryRanges[options.parent] = secondaryRanges[options.parent] || { deps: {}, peerDeps: {} };
520 if (!secondaryRanges[options.parent].deps[name])
521 secondaryRanges[options.parent].deps[name] = target.copy();
522 else
523 if (secondaryRanges[options.parent].deps[name] && secondaryRanges[options.parent].deps[name].exactName !== target.exactName)
524 ui.log('warn', 'Currently installed dependency ranges of `' + options.parent + '` are not consistent ( %' + secondaryRanges[options.parent].deps[name].exactName + '% should be %' + target.exactName + '%)');
525 }
526
527 return alreadyInstalled;
528 }
529
530 // trigger dependency downloads
531 // this can be triggered twice
532 // - once by initial preload, and once post-build if additional dependencies are discovered
533 function depsCallback(depRanges) {
534 dependencyDownloads = (dependencyDownloads || Promise.resolve()).then(function() {
535 return Promise.all(
536 Object.keys(depRanges.deps).map(function(dep) {
537 return doInstall(dep, depRanges.deps[dep], false);
538 })
539 .concat(Object.keys(depRanges.peerDeps).map(function(peerDep) {
540 return doInstall(peerDep, depRanges.peerDeps[peerDep], true);
541 }))
542 )
543 .catch(function(e) {
544 dependencyDownloadsError = e;
545 });
546 });
547
548 function doInstall(dep, range, isPeer) {
549 return install(dep, range, {
550 force: options.force,
551 latest: options.latest,
552 lock: options.lock,
553 parent: resolution.exactNameEncoded,
554 inject: options.inject,
555 quick: options.quick,
556 peer: isPeer,
557 peerResolved: isPeer && alreadyInstalled,
558 dev: options.dev
559 }, seen);
560 }
561 }
562}
563
564function getInstalledMatch(target, parent, name) {
565 // use the config lock if provided
566 // installing beats installed
567 if (parent) {
568 if (installing.depMap[parent] && installing.depMap[parent][name])
569 return installing.depMap[parent][name];
570 if (installed.depMap[parent] && installed.depMap[parent][name])
571 return installed.depMap[parent][name];
572 }
573 else {
574 if (installing.baseMap[name])
575 return installing.baseMap[name];
576 if (installed.baseMap[name])
577 return installed.baseMap[name];
578 }
579
580 // otherwise seek an installed match
581 var match;
582 function checkMatch(pkg) {
583 if (pkg.name !== target.name)
584 return;
585 if (semver.match(target.version, pkg.version)) {
586 if (!match || match && semver.compare(pkg.version, match.version) === 1)
587 match = pkg.copy();
588 }
589 }
590 Object.keys(installed.baseMap).forEach(function(name) {
591 checkMatch(installed.baseMap[name]);
592 });
593 Object.keys(installed.depMap).forEach(function(parent) {
594 var depMap = installed.depMap[parent];
595 Object.keys(depMap).forEach(function(name) {
596 checkMatch(depMap[name]);
597 });
598 });
599 Object.keys(installing.baseMap).forEach(function(name) {
600 checkMatch(installing.baseMap[name]);
601 });
602 Object.keys(installing.depMap).forEach(function(parent) {
603 var depMap = installing.depMap[parent];
604 Object.keys(depMap).forEach(function(name) {
605 checkMatch(depMap[name]);
606 });
607 });
608 return match;
609}
610
611// track peer dependency conflicts so we only prompt once
612var peerConflicts = {};
613
614// resolve peer dependency conflicts given the target install for a peer dependency
615// and the existing dependency
616function resolvePeerConflicts(name, parent, target, existing) {
617 // wait if there is already a peer dependency conflict for this version
618 return (peerConflicts[name] = Promise.resolve(peerConflicts[name])
619 .then(function(acceptedRange) {
620
621 if (acceptedRange) {
622 // if there is already a new accepted range, then continue to use it if it is compatible
623 // NB this can be relaxed to ensure they have the same latest version overlap
624 if (acceptedRange.exactName == target.exactName)
625 return acceptedRange;
626 }
627 else {
628 // if the existing version matches the peer dependency expectation then continue to use what we have
629 if (existing.name == target.name && semver.match(target.version, existing.version))
630 return;
631 }
632
633 // conflict resolution!
634 return ui.confirm('Install the peer %' + name + '% from ' + getUpdateRangeText(acceptedRange || existing, target) + '?', true, {
635 info: 'Peer dependency conflict for %' + name + '%:\n' +
636 ' Package `' + parent + '` requires `' + target.exactName + '`.\n' +
637 ' Currently resolving to `' + (acceptedRange || existing).exactName + '`.\n\n' +
638 'Please select how you would like to resolve the version conflict.',
639 hideInfo: false
640 })
641 .then(function(useNew) {
642 if (useNew)
643 return target;
644
645 var existingTarget = config.pjson.peerDependencies[name] || config.pjson.devDependencies[name] || config.pjson.dependencies[name] || target;
646
647 return ui.confirm('Keep the `' + (acceptedRange || existing).exactName + '` resolution?', true)
648 .then(function(useExisting) {
649 if (useExisting)
650 return acceptedRange || existingTarget;
651
652 return ui.input('Enter any custom package resolution range', (acceptedRange || existingTarget).exactName, {
653 edit: true
654 })
655 .then(function(customRange) {
656 return pkg.locate(new PackageName(customRange));
657 });
658 });
659 });
660 }));
661}
662
663function saveInstall() {
664 return Promise.resolve()
665 .then(function() {
666
667 // merge the installing tree into the installed
668 Object.keys(installing.baseMap).forEach(function(p) {
669 installed.baseMap[p] = installing.baseMap[p];
670 });
671
672 Object.keys(installing.depMap).forEach(function(p) {
673 installed.depMap[p] = installed.depMap[p] || {};
674 for (var q in installing.depMap[p])
675 installed.depMap[p][q] = installing.depMap[p][q];
676 });
677
678 return clean();
679 })
680 .then(function() {
681 if (hasProperties(installedResolves)) {
682 ui.log('');
683 ui.log('info', 'The following existing package versions were altered by install deduping:');
684 ui.log('');
685 Object.keys(installedResolves).forEach(function(pkg) {
686 var pkgName = new PackageName(pkg);
687 ui.log('info', ' %' + pkgName.package + '% ' + getUpdateRangeText(pkgName, new PackageName(installedResolves[pkg])));
688 });
689 ui.log('');
690 installedResolves = {};
691 ui.log('info', 'To keep existing dependencies locked during install, use the %--lock% option.');
692 }
693
694 if (hasProperties(installingResolves)) {
695 ui.log('');
696 ui.log('info', 'The following new package versions were substituted by install deduping:');
697 ui.log('');
698 Object.keys(installingResolves).forEach(function(pkg) {
699 var pkgName = new PackageName(pkg);
700 ui.log('info', ' %' + pkgName.package + '% ' + getUpdateRangeText(pkgName, new PackageName(installingResolves[pkg])));
701 });
702 ui.log('') ;
703 installingResolves = {};
704 }
705
706 // then save
707 return config.save();
708 });
709}
710
711var logged = {};
712function logInstall(name, target, resolution, linked, options) {
713 if (logged[target.exactName + '=' + resolution.exactName])
714 return;
715
716 // don't log secondary fresh
717 if (options.parent && resolution.fresh)
718 return;
719
720 logged[target.exactName + '=' + resolution.exactName] = true;
721
722 var verb;
723 if (options.inject)
724 verb = 'Injected ';
725
726 else if (!resolution.fresh) {
727 if (!linked)
728 verb = 'Installed ';
729 else
730 verb = 'Symlinked ';
731 }
732 else {
733 if (options.quick)
734 return;
735 verb = '';
736 }
737
738 if (options.dev)
739 verb += (verb ? 'd' : 'D') + 'ev dependency ';
740 else if (options.peer)
741 verb += (verb ? 'p' : 'P') + 'eer ';
742
743 var actual = resolution.version;
744 if (resolution.package != target.package)
745 actual = resolution.package + (actual ? '@' + actual : '');
746 if (resolution.registry != target.registry && resolution.registry)
747 actual = resolution.registry + ':' + actual;
748
749 if (options.parent && !options.peer)
750 ui.log('ok', verb + '`' + target.exactName + '` (' + actual + ')');
751 else
752 ui.log('ok', verb + '%' + name + '% ' + (linked ? 'as': 'to') + ' `' + target.exactName + '` (' + actual + ')');
753}
754
755function getUpdateRangeText(existing, update) {
756 if (existing.name === update.name)
757 return '`' + existing.version + '` -> `' + update.version + '`';
758 else
759 return '`' + existing.exactName + '` -> `' + update.exactName + '`';
760}
761
762// go through the baseMap and depMap, changing FROM to TO
763// keep a log of what we did in resolveLog
764function doResolution(tree, from, to) {
765 if (from.exactName === to.exactName)
766 return;
767
768 // add this to the resolve log, including deep-updating resolution chains
769 logResolution(tree === installed ? installedResolves : installingResolves, from, to);
770
771 Object.keys(tree.baseMap).forEach(function(dep) {
772 if (tree.baseMap[dep].exactName === from.exactName)
773 tree.baseMap[dep] = to.copy();
774 });
775
776 Object.keys(tree.depMap).forEach(function(parent) {
777 var curMap = tree.depMap[parent];
778 Object.keys(curMap).forEach(function(dep) {
779 if (curMap[dep].exactName === from.exactName)
780 curMap[dep] = to.copy();
781 });
782 });
783}
784
785function logResolution(resolveLog, from, to) {
786 resolveLog[from.exactName] = to.exactName;
787
788 // find re-resolved
789 Object.keys(resolveLog).forEach(function(resolveFrom) {
790 if (resolveLog[resolveFrom] === from.exactName) {
791 // non-circular get updated
792 if (resolveFrom !== to.exactName) {
793 resolveLog[resolveFrom] = to.exactName;
794 }
795 // circular get removed
796 else {
797 delete resolveLog[resolveFrom];
798 // remove entirely if it never was an install to begin with
799 var tree = resolveLog === installedResolves ? installed : installing;
800 var inInstallResolution = Object.keys(tree.baseMap).some(function(dep) {
801 return tree.baseMap[dep].exactName === from.exactName;
802 }) || Object.keys(tree.depMap).some(function(parent) {
803 var curMap = tree.depMap[parent];
804 return Object.keys(curMap).some(function(dep) {
805 return curMap[dep].exactname == from.exactName;
806 });
807 });
808 if (!inInstallResolution)
809 delete resolveLog[from.exactName];
810 }
811 }
812 });
813}
814
815// name and parentName are the existing resolution target
816// so we only look up forks and not the original as well
817function loadExistingForkRanges(resolution, name, parentName, inject) {
818 var tree = installed;
819 return Promise.all(Object.keys(tree.baseMap).map(function(dep) {
820 if (!parentName && dep === name)
821 return;
822
823 var primary = tree.baseMap[dep];
824 if (primary.name !== resolution.name)
825 return;
826
827 return loadExistingRange(dep, null, inject);
828 }))
829 .then(function() {
830 return Promise.all(Object.keys(tree.depMap).map(function(parent) {
831 var curDepMap = tree.depMap[parent];
832
833 return Promise.all(Object.keys(curDepMap).map(function(dep) {
834 if (parent === parentName && dep === name)
835 return;
836
837 var secondary = curDepMap[dep];
838
839 if (secondary.name !== resolution.name)
840 return;
841
842 return loadExistingRange(dep, parent, inject);
843 }));
844 }));
845 });
846}
847
848function visitForkRanges(tree, resolution, name, parentName, visit) {
849 // now that we've got all the version ranges we need for consideration,
850 // go through and run resolutions against the fork list
851 Object.keys(tree.baseMap).forEach(function(dep) {
852 var primary = tree.baseMap[dep];
853 if (primary.name !== resolution.name)
854 return;
855
856 visit(dep, null, primary, primaryRanges[dep]);
857 });
858
859 Object.keys(tree.depMap).forEach(function(parent) {
860 var curDepMap = tree.depMap[parent];
861
862 Object.keys(curDepMap).forEach(function(dep) {
863 var secondary = curDepMap[dep];
864
865 if (secondary.name !== resolution.name)
866 return;
867
868 // its not a fork of itself
869 if (dep === name && parent === parentName)
870 return;
871
872 // skip if we don't have a range
873 var ranges = secondaryRanges[parent];
874 if (!ranges || !ranges.deps || !ranges.deps[dep])
875 return;
876
877 visit(dep, parent, secondary, ranges.deps[dep]);
878 });
879 });
880}
881
882// find all forks of this resolution in the tree
883// calling resolve(forkVersion, forkRanges, allSecondary)
884// for each unique fork version
885// sync resolution to avoid conflicts
886function resolveForks(tree, secondaryTree, name, parentName, resolution, resolve) {
887 // forks is a map from fork versions to an object, { ranges, hasPrimary }
888 // hasPrimary indicates whether any of these ranges are primary ranges
889 var forks = {};
890 var forkVersions = [];
891
892 function rangeVisitor(dep, parent, resolved, range) {
893 if (!range)
894 return;
895
896 // we only work with stuff within it's own matching range
897 // not user overrides
898 if (range.name !== resolved.name || !semver.match(range.version, resolved.version))
899 return;
900
901 var forkObj = forks[resolved.version];
902 if (!forkObj) {
903 forkObj = forks[resolved.version] = { ranges: [], allSecondary: true };
904 forkVersions.push(resolved.version);
905 }
906
907 if (!parent)
908 forkObj.allSecondary = false;
909
910 forkObj.ranges.push(range.version);
911 }
912
913 visitForkRanges(tree, resolution, name, parentName, rangeVisitor);
914
915 // we include the secondary tree ranges as part of the fork ranges (but not fork versions)
916 // this is because to break a secondary tree range is still to introduce a fork
917 visitForkRanges(secondaryTree, resolution, name, parentName, function(dep, parent, resolved, range) {
918 if (forks[resolved.version])
919 rangeVisitor(dep, parent, resolved, range);
920 });
921
922 // now run through and resolve the forks
923 forkVersions.sort(semver.compare).reverse().forEach(function(forkVersion) {
924 var forkObj = forks[forkVersion];
925
926 var newVersion = resolve(forkVersion, forkObj.ranges, forkObj.allSecondary);
927 if (!newVersion || newVersion === forkVersion)
928 return;
929
930 var from = resolution.copy().setVersion(forkVersion);
931 var to = resolution.copy().setVersion(newVersion);
932
933 doResolution(tree, from, to);
934 });
935}
936
937var secondaryDepsPromises = {};
938function loadExistingRange(name, parent, inject) {
939 if (parent && secondaryRanges[parent])
940 return;
941 else if (!parent && primaryRanges[name])
942 return;
943
944 var _target;
945
946 return Promise.resolve()
947 .then(function() {
948 if (!parent)
949 return config.pjson.dependencies[name] || config.pjson.peerDependencies[name] || config.pjson.devDependencies[name];
950
951 return Promise.resolve()
952 .then(function() {
953 if (secondaryDepsPromises[parent])
954 return secondaryDepsPromises[parent];
955
956 return Promise.resolve()
957 .then(function() {
958 var parentPkg = new PackageName(parent, true);
959
960 // if the package is installed but not in jspm_packages
961 // then we wait on the getPackageConfig or download of the package here
962 return (secondaryDepsPromises[parent] = new Promise(function(resolve, reject) {
963 if (inject)
964 return pkg.inject(parentPkg, resolve).catch(reject);
965
966 pkg.download(parentPkg, {}, resolve).then(resolve, reject);
967 })
968 .then(function(depMap) {
969 if (depMap)
970 return depMap;
971
972 return config.deps[new PackageName(parent, true).exactName];
973 }));
974 });
975 })
976 .then(function(deps) {
977 return deps.deps[name];
978 });
979 })
980 .then(function(target) {
981 if (!target) {
982 if (parent && installed.depMap[parent] && installed.depMap[parent].name) {
983 delete installed.depMap[parent].name;
984 ui.log('warn', '%' + parent + '% dependency %' + name + '% was removed from the config file to reflect the installed package.');
985 }
986 else if (!parent) {
987 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.');
988 }
989 return;
990 }
991
992 _target = target.copy();
993 // locate the target
994 return pkg.locate(_target)
995 .then(function(located) {
996 if (parent) {
997 secondaryRanges[parent] = secondaryRanges[parent] || { deps: {}, peerDeps: {} };
998 secondaryRanges[parent].deps[name] = located;
999 }
1000 else {
1001 primaryRanges[name] = located;
1002 }
1003 });
1004 });
1005}
1006
1007
1008// given an exact package, find all the forks, and display the ranges
1009function showInstallGraph(pkg) {
1010 installed = installed || config.loader;
1011 secondaryRanges = secondaryRanges || config.deps;
1012 pkg = new PackageName(pkg);
1013 var lastParent;
1014 var found;
1015 return loadExistingForkRanges(pkg, config.loader.local)
1016 .then(function() {
1017 ui.log('info', '\nInstalled versions of %' + pkg.name + '%');
1018 visitForkRanges(installed, pkg, null, null, function(name, parent, resolved, range) {
1019 found = true;
1020 if (range.version === '')
1021 range.version = '*';
1022 var rangeVersion = range.name === resolved.name ? range.version : range.exactName;
1023 if (range.version === '*')
1024 range.version = '';
1025
1026 if (!parent)
1027 ui.log('info', '\n %' + name + '% `' + resolved.version + '` (' + rangeVersion + ')');
1028 else {
1029 if (lastParent !== parent) {
1030 ui.log('info', '\n ' + parent);
1031 lastParent = parent;
1032 }
1033 ui.log('info', ' ' + name + ' `' + resolved.version + '` (' + rangeVersion + ')');
1034 }
1035 });
1036 if (!found)
1037 ui.log('warn', 'Package `' + pkg.name + '` not found.');
1038 ui.log('');
1039 });
1040}
1041exports.showInstallGraph = showInstallGraph;
1042
1043
1044function showVersions(forks) {
1045 installed = installed || config.loader;
1046
1047 var versions = {};
1048 var haveLinked = false;
1049 var linkedVersions = {};
1050
1051 function addDep(dep) {
1052 var vList = versions[dep.name] = versions[dep.name] || [];
1053 var version = dep.version;
1054 try {
1055 if (fs.readlinkSync(dep.getPath()))
1056 linkedVersions[dep.exactName] = true;
1057 }
1058 catch(e) {}
1059 if (vList.indexOf(version) === -1)
1060 vList.push(version);
1061 }
1062
1063 Object.keys(installed.baseMap).forEach(function(dep) {
1064 addDep(installed.baseMap[dep]);
1065 });
1066 Object.keys(installed.depMap).forEach(function(parent) {
1067 var curMap = installed.depMap[parent];
1068 Object.keys(curMap).forEach(function(dep) {
1069 addDep(curMap[dep]);
1070 });
1071 });
1072
1073 versions = alphabetize(versions);
1074
1075 var vLen = 0;
1076
1077 var shownIntro = false;
1078
1079 var logLines = [];
1080
1081 Object.keys(versions).forEach(function(dep) {
1082 var vList = versions[dep].sort(semver.compare).map(function(version) {
1083 if (linkedVersions[dep + '@' + version]) {
1084 haveLinked = true;
1085 return '%' + version + '%';
1086 }
1087 else
1088 return '`' + version + '`';
1089 });
1090
1091 if (forks && vList.length === 1) {
1092 haveLinked = false;
1093 return;
1094 }
1095
1096 if (!shownIntro) {
1097 ui.log('info', 'Installed ' + (forks ? 'Forks' : 'Versions') + '\n');
1098 shownIntro = true;
1099 }
1100
1101 vLen = Math.max(vLen, dep.length);
1102 logLines.push([dep, vList.join(' ')]);
1103 });
1104
1105 logLines.forEach(function(cols) {
1106 var padding = vLen - cols[0].length;
1107 var paddingString = ' ';
1108 while(padding--)
1109 paddingString += ' ';
1110
1111 ui.log('info', paddingString + '%' + cols[0] + '% ' + cols[1]);
1112 });
1113
1114 if (haveLinked) {
1115 ui.log('info', '\nBold versions are linked. To unlink use %jspm install --unlink [name]%.');
1116 }
1117 if (shownIntro) {
1118 ui.log('info', '\nTo inspect individual package constraints, use %jspm inspect registry:name%.\n');
1119 }
1120 else if (forks) {
1121 ui.log('info', 'Install tree has no forks.');
1122 }
1123}
1124exports.showVersions = showVersions;
1125
1126/*
1127 * Configuration cleaning
1128 *
1129 * 1. Construct list of all packages in main tree tracing from package.json primary installs
1130 * 2. Remove all orphaned dependencies not in this list
1131 * 3. Remove any package.json overrides that will never match this list
1132 * 4. Remove packages in .dependencies.json that aren't used at all
1133 * 5. Remove anything from jspm_packages not in this list
1134 *
1135 * Hard clean will do extra steps:
1136 * * Any dependencies of packages that don't have ranges are cleared
1137 * (that is extra map configs added to packages)
1138 * * Config file saving is enforced (even if no changes were made)
1139 *
1140 */
1141function clean(hard) {
1142 var packageList = [];
1143
1144 return config.load()
1145 .then(function() {
1146
1147 // include the local package as a package
1148 if (config.loader.package && config.pjson.name)
1149 packageList.push(config.pjson.name);
1150
1151 // Hard clean - remove dependencies not installed to parent goal ranges
1152 if (hard) {
1153 Object.keys(config.loader.baseMap).forEach(function(dep) {
1154 var pkg = config.loader.baseMap[dep];
1155 if (!config.pjson.dependencies[dep] && !config.pjson.devDependencies[dep] && !config.pjson.peerDependencies[dep] && pkg.registry)
1156 delete config.loader.baseMap[dep];
1157 });
1158
1159 Object.keys(config.loader.depMap).forEach(function(parent) {
1160 var depMap = config.loader.depMap[parent];
1161 var secondaryRanges = config.deps[parent];
1162 if (secondaryRanges)
1163 Object.keys(depMap).forEach(function(dep) {
1164 if (!secondaryRanges.deps[dep]) {
1165 if (depMap[dep].registry) {
1166 var name = depMap[dep].exactName;
1167 ui.log('info', 'Clearing undeclared dependency `' + name + '` of `' + parent + '`.');
1168 delete depMap[dep];
1169 }
1170 }
1171 });
1172 });
1173 }
1174
1175 // 1. getDependentPackages for each of baseMap
1176 Object.keys(config.loader.baseMap).forEach(function(dep) {
1177 getDependentPackages(config.loader.baseMap[dep], packageList);
1178 });
1179
1180 // 2. now that we have the package list, remove everything not in it
1181 Object.keys(config.loader.depMap).forEach(function(dep) {
1182 var depUnencoded = new PackageName(dep, true).exactName;
1183 if (packageList.indexOf(depUnencoded) === -1) {
1184 ui.log('info', 'Clearing configuration for `' + depUnencoded + '`');
1185 config.loader.removePackage(dep);
1186 }
1187 });
1188
1189 // 3. remove package.json overrides which will never match any packages
1190 var usedOverrides = [];
1191 packageList.forEach(function(pkgName) {
1192 if (config.pjson.overrides[pkgName])
1193 usedOverrides.push(pkgName);
1194 });
1195 Object.keys(config.pjson.overrides).forEach(function(overrideName) {
1196 if (usedOverrides.indexOf(overrideName) == -1 && hasProperties(config.pjson.overrides[overrideName])) {
1197 ui.log('info', 'Removing unused package.json override `' + overrideName + '`');
1198 delete config.pjson.overrides[overrideName];
1199 }
1200 });
1201 })
1202
1203 .then(function() {
1204 return asp(fs.lstat)(config.pjson.packages)
1205 .catch(function(e) {
1206 if (e.code == 'ENOENT')
1207 return;
1208 throw e;
1209 }).then(function(stats) {
1210 // Skip if jspm_packages is symlinked or not existing
1211 if (!stats || stats.isSymbolicLink())
1212 return;
1213
1214 // 4. Remove packages in .dependencies.json that aren't used at all
1215 Object.keys(config.deps).forEach(function(dep) {
1216 if (packageList.indexOf(dep) == -1)
1217 delete config.deps[dep];
1218 });
1219
1220 // 5. Remove anything from jspm_packages not in this list
1221 return readDirWithDepth(config.pjson.packages, function(dirname) {
1222 if (dirname.split(path.sep).pop().indexOf('@') <= 0)
1223 return true;
1224 })
1225 .then(function(packageDirs) {
1226 return Promise.all(
1227 packageDirs
1228 .filter(function(dir) {
1229 var exactName = path.relative(config.pjson.packages, dir).replace(path.sep, ':').replace(/\\/g, '/'); // (win)
1230 exactName = new PackageName(exactName, true).exactName;
1231 var remove = packageList.indexOf(exactName) === -1;
1232 if (remove)
1233 ui.log('info', 'Removing package files for `' + exactName + '`');
1234 return remove;
1235 })
1236 .map(function(dir) {
1237 return asp(rimraf)(dir)
1238 .then(function() {
1239 var filename = dir + '.json';
1240 return new Promise(function(resolve) {
1241 fs.exists(filename, resolve);
1242 }).then(function(exists) {
1243 if (exists) return asp(fs.unlink)(filename);
1244 });
1245 })
1246 // NB deprecate with 0.16
1247 .then(function() {
1248 var filename = dir + '.js';
1249 return new Promise(function(resolve) {
1250 fs.exists(filename, resolve);
1251 }).then(function(exists) {
1252 if (exists) return asp(fs.unlink)(filename);
1253 });
1254 })
1255 .then(function() {
1256 return cascadeDelete(dir);
1257 });
1258 }));
1259 });
1260 });
1261 })
1262 .then(function() {
1263 if (hard) {
1264 config.pjson.file.changed = true;
1265 config.loader.file.changed = true;
1266 if (config.loader.devFile)
1267 config.loader.devFile.changed = true;
1268 if (config.loader.browserFile)
1269 config.loader.browserFile.changed = true;
1270 if (config.loader.nodeFile)
1271 config.loader.nodeFile.changed = true;
1272 }
1273
1274 return config.save();
1275 });
1276}
1277exports.clean = clean;
1278
1279// depthCheck returns true to keep going (files being ignored), false to add the dir to the flat list
1280function readDirWithDepth(dir, depthCheck) {
1281 var flatDirs = [];
1282 return asp(fs.readdir)(dir)
1283 .then(function(files) {
1284 if (!files)
1285 return [];
1286 return Promise.all(files.map(function(file) {
1287 var filepath = path.resolve(dir, file);
1288
1289 // ensure it is a directory or symlink
1290 return asp(fs.lstat)(filepath)
1291 .then(function(fileInfo) {
1292 if (!fileInfo.isDirectory() && !fileInfo.isSymbolicLink())
1293 return;
1294
1295 if (!depthCheck(filepath))
1296 return flatDirs.push(filepath);
1297
1298 // keep going
1299 return readDirWithDepth(filepath, depthCheck)
1300 .then(function(items) {
1301 items.forEach(function(item) {
1302 flatDirs.push(item);
1303 });
1304 });
1305 });
1306 }));
1307 })
1308 .then(function() {
1309 return flatDirs;
1310 });
1311}
1312
1313
1314function getDependentPackages(pkg, packages) {
1315 packages.push(pkg.exactName);
1316 // get all immediate children of this package
1317 // for those children not already seen (in packages list),
1318 // run getDependentPackages in turn on those
1319 var depMap = config.loader.depMap[pkg.exactNameEncoded];
1320 if (!depMap)
1321 return;
1322 Object.keys(depMap).forEach(function(dep) {
1323 var curPkg = depMap[dep];
1324 if (packages.indexOf(curPkg.exactName) !== -1)
1325 return;
1326 getDependentPackages(curPkg, packages);
1327 });
1328
1329 return packages;
1330}
1331
1332exports.uninstall = function(names, peer) {
1333 if (!(names instanceof Array))
1334 names = [names];
1335
1336 return config.load()
1337 .then(function() {
1338 if (names.length == 0 && peer)
1339 names = Object.keys(config.pjson.peerDependencies);
1340
1341 installed = installed || config.loader;
1342
1343 names.forEach(function(name) {
1344 delete config.pjson.dependencies[name];
1345 delete config.pjson.devDependencies[name];
1346 delete config.pjson.peerDependencies[name];
1347 delete installed.baseMap[name];
1348 });
1349
1350 return clean();
1351 });
1352};
1353
1354/*
1355 * Resolve all installs of the given package to a specific version
1356 */
1357exports.resolveOnly = function(pkg) {
1358 pkg = new PackageName(pkg);
1359
1360 if (!pkg.version || !pkg.registry) {
1361 ui.log('warn', 'Resolve --only must take an exact package of the form `registry:pkg@version`.');
1362 return Promise.reject();
1363 }
1364
1365 var didSomething = false;
1366
1367 return config.load()
1368 .then(function() {
1369 Object.keys(config.loader.baseMap).forEach(function(name) {
1370 var curPkg = config.loader.baseMap[name];
1371 if (curPkg.registry === pkg.registry && curPkg.package === pkg.package && curPkg.version !== pkg.version) {
1372 didSomething = true;
1373 ui.log('info', 'Primary install ' + getUpdateRangeText(curPkg, pkg));
1374 config.loader.baseMap[name] = pkg.copy();
1375 }
1376 });
1377
1378 Object.keys(config.loader.depMap).forEach(function(parent) {
1379 var curMap = config.loader.depMap[parent];
1380 Object.keys(curMap).forEach(function(name) {
1381 var curPkg = curMap[name];
1382 if (curPkg.registry === pkg.registry && curPkg.package === pkg.package && curPkg.version !== pkg.version) {
1383 didSomething = true;
1384 ui.log('info', 'In %' + parent + '% ' + getUpdateRangeText(curPkg, pkg));
1385 curMap[name] = pkg.copy();
1386 }
1387 });
1388 });
1389
1390 return config.save();
1391 })
1392 .then(function() {
1393 if (didSomething)
1394 ui.log('ok', 'Resolution to only use `' + pkg.exactName + '` completed successfully.');
1395 else
1396 ui.log('ok', '`' + pkg.exactName + '` is already the only version of the package in use.');
1397 });
1398};