UNPKG

32.6 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 request = require('request');
20var ui = require('./ui');
21var semver = require('./semver');
22var asp = require('bluebird').Promise.promisify;
23var config = require('./config');
24var mkdirp = require('mkdirp');
25var rimraf = require('rimraf');
26var path = require('path');
27var registry = require('./registry');
28var PackageName = require('./package-name');
29var globalConfig = require('./config/global-config');
30var readJSON = require('./common').readJSON;
31var ncp = require('ncp');
32var fs = require('graceful-fs');
33var glob = require('glob');
34var minimatch = require('minimatch');
35var md5 = require('./common').md5;
36var processDeps = require('./common').processDeps;
37var extend = require('./common').extend;
38var HOME = require('./common').HOME;
39var Promise = require('bluebird');
40var newLine = require('./common').newLine;
41
42var jspmVersion = require('../package.json').version.split('.').splice(0, 2).join('.');
43
44// we cache registry lookups here to allow reductions in config saving
45var registryCache = exports.registryCache = {};
46
47// given a name like 'jquery', 'github:repo/thatwasmoved'
48// add the default registry endpoint to the name
49// so we now have 'jspm:jquery', 'github:repo/thatwasmoved'
50// then run the locate hook (if provided) of the registry
51// following redirects until the locate hook converges
52// getting 'github:components/jquery' and 'github:repo/redirected'
53// at this point, we have the final name for the target
54var locateCache = {};
55// target is a PackageName object
56exports.locate = locate;
57function locate(target) {
58 if (!target.registry) {
59 target = new PackageName(target.exactName);
60 target.setRegistry(globalConfig.config.defaultRegistry);
61 }
62
63 var endpoint = registry.load(target.registry);
64
65 if (!endpoint.locate)
66 return Promise.resolve(target);
67
68 locateCache[target.registry] = locateCache[target.registry] || {};
69
70 // NB enable versioned locate
71 return Promise.resolve()
72 .then(function() {
73 if (locateCache[target.registry][target.package])
74 return locateCache[target.registry][target.package];
75
76 return (locateCache[target.registry][target.package] = Promise.resolve(endpoint.locate(target.package))
77 .then(function(located) {
78 // NB support versioned registry
79 if (target.registry === globalConfig.config.defaultRegistry)
80 registryCache[target.package] = located.redirect;
81 return located;
82 }));
83 })
84 .then(function(located) {
85 if (!located)
86 return target;
87
88 if (located.redirect) {
89 var redirectPkg = new PackageName(located.redirect);
90
91 // mutate the target by the redirect
92 // this ensures we always store resolved targets
93 target.setRegistry(redirectPkg.registry);
94 target.setPackage(redirectPkg.package);
95
96 return locate(target);
97 }
98
99 if (located.notfound)
100 throw 'Repo `' + target.name + '` not found.' +
101 (target.registry != 'npm' && target.package.split('/').length == 1 ? ' Perhaps try %jspm install npm:' + target.package + '%.' : '');
102
103 throw 'Invalid registry locate response for %' + target.registry + '%';
104 }, function(e) {
105 if (e)
106 ui.log('err', e.stack || e);
107 throw 'Error locating `' + target.name + '`.';
108 });
109}
110
111var lookupPromises = {};
112var lookups = {};
113
114exports.lookup = lookup;
115function lookup(pkg, edge) {
116 return Promise.resolve()
117 // load the version map
118 .then(function() {
119 // already loaded
120 if (lookups[pkg.name])
121 return { versions: lookups[pkg.name] };
122
123 // already loading
124 if (lookupPromises[pkg.name])
125 return lookupPromises[pkg.name];
126
127 ui.log('debug', 'Looking up `' + pkg.name + '`');
128 return (lookupPromises[pkg.name] = Promise.resolve(registry.load(pkg.registry).lookup(pkg.package)));
129 })
130 .then(function(lookup) {
131 if (lookup.notfound)
132 throw 'Repo `' + pkg.name + '` not found!';
133
134 if (!lookup.versions)
135 throw 'Invalid registry lookup response for %' + pkg.registry + '%';
136
137 lookups[pkg.name] = lookup.versions;
138
139 return function(version) {
140 var opts = {edge: edge, latestVersion: lookup.latest};
141 var lookupObj = getVersionMatch(version, lookup.versions, opts);
142 if (!lookupObj)
143 return;
144
145 return new PackageName(pkg.name + '@' + lookupObj.version);
146 };
147 }, function(e) {
148 if (e)
149 ui.log('err', e.stack || e);
150
151 throw 'Error looking up `' + pkg.name + '`.';
152 });
153}
154
155// exported for unit testing
156exports.getVersionMatch = getVersionMatch;
157function getVersionMatch(pkgVersion, versions, options) {
158 // unescape pkgVersion for comparison
159 if (pkgVersion)
160 pkgVersion = decodeURIComponent(pkgVersion);
161
162 if (versions[pkgVersion]) {
163 versions[pkgVersion].version = pkgVersion;
164 return versions[pkgVersion];
165 }
166
167 var version;
168 var stableSemver = [];
169 var unstableSemver = [];
170 var stableExact = [];
171 var unstableExact = [];
172 var edge = options && options.edge;
173
174 Object.keys(versions).forEach(function(v) {
175 version = versions[v];
176 var stable = version.stable;
177 var semverMatch = v.match(semver.semverRegEx);
178 var valid = semverMatch && semverMatch[1] && semverMatch[2] && semverMatch[3];
179 var pre = valid && semverMatch[4];
180
181 // store a reverse lookup
182 version.version = v;
183
184 // ignore non-semver or prerelease, unless explictly marked as stable
185 if (!valid) {
186 // unstable unless explicitly stable. in --edge prioritize all after 'master'
187 if (stable && !edge)
188 stableExact.push(v);
189 else
190 unstableExact.push(v);
191 }
192 // stable unless explicitly unstable or indetermate and a prerelease
193 // --edge considers all semver to be stable
194 else if (!edge && (stable === false || (stable !== true && pre)))
195 unstableSemver.push(v);
196 else
197 stableSemver.push(v);
198 });
199
200 function compareDesc(a, b) {
201 return semver.compare(b, a);
202 }
203
204 if (!pkgVersion) {
205 var latest = options && options.latestVersion && versions[options.latestVersion];
206 if (!edge && latest)
207 return latest;
208 stableSemver.sort(compareDesc);
209
210 if (stableSemver[0])
211 return versions[stableSemver[0]];
212
213 unstableSemver.sort(compareDesc);
214 if (unstableSemver[0])
215 return versions[unstableSemver[0]];
216
217 if (latest)
218 return latest;
219
220 stableExact.sort();
221 if (stableExact[0])
222 return versions[stableExact[0]];
223
224 // an ugly practicality. ideally designed out in future.
225 if (versions.master)
226 return versions.master;
227
228 unstableExact.sort();
229 if (unstableExact[0])
230 return versions[unstableExact[0]];
231 }
232 else {
233 var i, ver;
234 stableSemver.sort(compareDesc);
235 // find highest stable match in tags
236 for (i = 0; i < stableSemver.length; i++) {
237 ver = stableSemver[i];
238 var match = edge ? semver.matchUnstable : semver.match;
239 if (match(pkgVersion, ver))
240 return versions[ver];
241 }
242 unstableSemver.sort(compareDesc);
243 for (i = 0; i < unstableSemver.length; i++) {
244 ver = unstableSemver[i];
245 if (semver.match(pkgVersion, ver))
246 return versions[ver];
247 }
248 }
249}
250
251function getOverride(pkg, manualOverride, alreadyInstalled) {
252 return Promise.resolve()
253 .then(function() {
254 // if the package is not installed, but we have a local override match, then use that
255 var existingOverride = config.pjson.overrides[pkg.exactName];
256 if (!alreadyInstalled && !existingOverride) {
257 var existingOverrideVersion = Object.keys(config.pjson.overrides)
258 .filter(function(overrideName) {
259 return overrideName.startsWith(pkg.name + '@');
260 })
261 .map(function(overrideName) {
262 return overrideName.split('@').pop();
263 })
264 .filter(function(overrideVersion) {
265 return semver.match('^' + overrideVersion, pkg.version);
266 })
267 .sort(semver.compare).pop();
268 if (existingOverrideVersion)
269 existingOverride = JSON.parse(JSON.stringify(config.pjson.overrides[pkg.name + '@' + existingOverrideVersion]));
270 }
271
272 if ((alreadyInstalled || existingOverride) && !manualOverride)
273 return Promise.resolve(config.pjson.overrides[pkg.exactName] = existingOverride || {});
274
275 // otherwise use the registry override + manual override
276 var endpoint = registry.load(globalConfig.config.defaultRegistry);
277 return endpoint.getOverride(pkg.registry, pkg.package, pkg.version, manualOverride)
278 .then(function(override) {
279 for (var p in override)
280 if (override[p] === undefined)
281 delete override[p];
282
283 override = override || {};
284 // persist the override for reproducibility
285 config.pjson.overrides[pkg.exactName] = override;
286 return override;
287 });
288 })
289 .then(function(override) {
290 var packageConfig = typeof override.systemjs == 'object' ? override.systemjs : override;
291 upgradePackageConfig(packageConfig);
292 return override;
293 });
294}
295
296exports.upgradePackageConfig = upgradePackageConfig;
297function upgradePackageConfig(packageConfig) {
298 // 0.16 Package Config Override Upgrade Path:
299 // shim becomes meta
300 if (packageConfig.shim) {
301 packageConfig.meta = packageConfig.meta || {};
302
303 for (var s in packageConfig.shim) {
304 // ignore shim if there is any meta config for this module
305 if (packageConfig.meta[s + '.js'])
306 continue;
307
308 var curShim = packageConfig.shim[s];
309 var curMeta = packageConfig.meta[s + '.js'] = {};
310
311 if (curShim instanceof Array) {
312 curMeta.deps = curShim;
313 }
314 else if (typeof curShim == 'object') {
315 if (curShim.deps)
316 curMeta.deps = curShim.deps;
317 else if (curShim.imports)
318 curMeta.deps = curShim.imports;
319
320 if (curShim.exports)
321 curMeta.exports = curShim.exports;
322 }
323
324 curMeta.format = 'global';
325
326 if (typeof curMeta.deps === 'string')
327 curMeta.deps = [curMeta.deps];
328 }
329 delete packageConfig.shim;
330 }
331}
332
333var injecting = {};
334exports.inject = function(pkg, depLoad) {
335 if (injecting[pkg.exactName]) {
336 injecting[pkg.exactName].depLoad.then(function(depMap) {
337 depLoad(depMap);
338 return depMap;
339 });
340 return injecting[pkg.exactName].promise;
341 }
342
343 injecting[pkg.exactName] = {};
344
345 var depResolve, depReject;
346 injecting[pkg.exactName].depLoad = new Promise(function(resolve, reject) {
347 depResolve = resolve;
348 depReject = reject;
349 })
350 .then(function(depMap) {
351 depLoad(depMap);
352 return depMap;
353 });
354
355 var remote = registry.load(pkg.registry).remote;
356
357 if (!remote)
358 throw 'Cannot inject from registry %' + pkg.registry + '% as it has no remote.';
359
360 // NB remove rejectUnauthorized
361 var url = remote + (remote.endsWith('/') ? '' : '/') + pkg.exactName.substr(pkg.exactName.indexOf(':') + 1) + '/.jspm.json';
362 injecting[pkg.exactName].promise = asp(request)({
363 method: 'get',
364 url: url,
365 rejectUnauthorized: false
366 }).then(function(res) {
367 if (res.statusCode !== 200)
368 throw new Error('Error requesting package config for `' + pkg.exactName + '` at %' + url + '%.');
369
370 try {
371 return JSON.parse(res.body);
372 }
373 catch(e) {
374 throw new Error('Unable to parse package config');
375 }
376 })
377 .then(function(pjson) {
378 depResolve(processDeps(pjson.dependencies, pjson.registry));
379 return pjson;
380 }, depReject);
381 return injecting[pkg.exactName].promise;
382};
383
384
385
386// note if it is a symlink, we leave it unaltered
387var downloading = {};
388// options.override
389// options.quick
390// options.force
391// options.linked
392exports.download = function(pkg, options, depsCallback) {
393 // download queue
394 if (downloading[pkg.exactName]) {
395 downloading[pkg.exactName].preloadCallbacks.push(depsCallback);
396 downloading[pkg.exactName].postloadCallbacks.push(depsCallback);
397 return downloading[pkg.exactName].promise;
398 }
399
400 // track the deps sent to the deps callback to avoid duplication between preload and postload
401 var sentDeps = [];
402 function doDepsCallback(depRanges) {
403 var sendRanges = { deps: {}, peerDeps: {} };
404 Object.keys(depRanges.deps).forEach(function(dep) {
405 if (sentDeps.indexOf(dep) == -1)
406 sendRanges.deps[dep] = depRanges.deps[dep];
407 });
408 Object.keys(depRanges.peerDeps).forEach(function(dep) {
409 if (sentDeps.indexOf(dep) == -1)
410 sendRanges.peerDeps[dep] = depRanges.peerDeps[dep];
411 });
412 sentDeps = sentDeps.concat(Object.keys(sendRanges.deps)).concat(Object.keys(sendRanges.peerDeps));
413 depsCallback(sendRanges);
414 }
415
416 // callbacks as we need a synchronous guarantee of resolution
417 // before the download promise
418 var preloadCallbacks = [doDepsCallback];
419 var postloadCallbacks = [doDepsCallback];
420 function preloadResolve(depRanges) {
421 preloadCallbacks.forEach(function(cb) {
422 cb(depRanges);
423 });
424 }
425 function postloadResolve(depRanges) {
426 postloadCallbacks.forEach(function(cb) {
427 cb(depRanges);
428 });
429 }
430
431 downloading[pkg.exactName] = {
432 preloadCallbacks: preloadCallbacks,
433 postloadCallbacks: postloadCallbacks
434 };
435
436 // download
437 var downloadDir = pkg.getPath();
438 var getPackageConfigError;
439 var getPackageConfigPromise;
440 var override;
441
442 return (downloading[pkg.exactName].promise = Promise.resolve()
443 .then(function() {
444 // determine the override
445 return getOverride(pkg, options.override, options.alreadyInstalled);
446 })
447 .then(function(_override) {
448 override = _override;
449
450 // check if the folder exists
451 return new Promise(function(resolve) {
452 fs.exists(downloadDir, resolve);
453 });
454 })
455 .then(function(dirExists) {
456 // quick skips actual hash checks and dependency reinstalls
457 if (options.quick && !options.override && dirExists)
458 return true;
459
460 var fullHash, cfgHash;
461
462 // check freshness
463 return Promise.resolve()
464 .then(function() {
465 // ensure lookup data is present
466 if (!lookups[pkg.name] && !options.linked)
467 return lookup(pkg);
468 })
469 .then(function() {
470 if ((!lookups[pkg.name] || !lookups[pkg.name][pkg.version]) && !options.linked)
471 throw 'Unable to resolve version %' + pkg.version + '% for `' + pkg.package + '`.';
472
473 return getPackageHash(downloadDir);
474 })
475 .then(function(hashes) {
476 // if the package exists locally, hash the package@x.y.z.json file to see if it has been changed
477 // if it has been changed then add the altered package@x.y.z.json contents
478 // to the overrides as an override, and note that we have done this
479 if (hashes[1]) {
480 return readJSON(downloadDir + '.json')
481 .then(function(pkgConfig) {
482 if (computeConfigHash(pkgConfig) == hashes[1])
483 return;
484
485 for (var c in pkgConfig) {
486 if (JSON.stringify(pkgConfig[c]) !== JSON.stringify(override[c]))
487 override[c] = pkgConfig[c];
488 }
489 // overrides usually extend, so to indicate that we want this to be the final override
490 // we set empty values explicitly
491 // if (!override.defaultExtension)
492 // override.defaultExtension = false;
493 if (!override.format)
494 override.format = 'detect';
495 if (!override.meta)
496 override.meta = {};
497 if (!override.map)
498 override.map = {};
499
500 ui.log('ok', 'The package configuration file `' + path.relative(path.dirname(config.pjsonPath), downloadDir + '.json') +
501 '` has been edited manually. To avoid overwriting the change, it has been added to the package.json overrides.');
502
503 return hashes[0];
504 }, function(err) {
505 if (typeof err === 'string' && err.startsWith('Error parsing') || err.code == 'ENOENT')
506 return null;
507 throw err;
508 });
509 }
510
511 return hashes[0];
512 })
513 .then(function(pkgHash) {
514 if (options.force)
515 return false;
516
517 fullHash = computePackageHash(pkg, override);
518
519 return pkgHash === fullHash;
520 })
521 .then(function(fresh) {
522 if (fresh && config[pkg.exactName]) {
523 // this can't trigger twice, so if its a second call its just a noop
524 preloadResolve(config.deps[pkg.exactName]);
525 return true;
526 }
527
528 // clear stored dependency cache for download
529 delete config.deps[pkg.exactName];
530
531 // if linked, process the symlinked folder
532 if (options.linked && !options.unlink) {
533 ui.log('info', 'Processing linked package `' + pkg.exactName + '`');
534
535 return Promise.resolve(readJSON(path.resolve(downloadDir, 'package.json')))
536 .then(function(packageConfig) {
537 return derivePackageConfig(pkg, packageConfig, override);
538 })
539 .then(function(packageConfig) {
540 return processPackage(pkg, downloadDir, packageConfig, options.linked);
541 }, function(err) {
542 if (err)
543 ui.log('err', err && err.stack || err);
544 throw 'Error processing linked package `' + pkg.exactName + '`.';
545 })
546 .then(function(packageConfig) {
547 return createLoaderConfig(pkg, packageConfig, downloadDir)
548 .then(function() {
549 postloadResolve(getDepRanges(pkg, packageConfig));
550 });
551 })
552 .then(function() {
553 return false;
554 });
555 }
556
557 if (pkg.registry == 'local')
558 throw '`' + pkg.exactName + '` must be linked to be used in installs.';
559
560 var cacheDir = pkg.getPath(path.resolve(HOME, '.jspm', 'packages'));
561
562 // ensure global cache is fresh / download if not
563 return Promise.resolve()
564 .then(function() {
565 if (config.force)
566 return false;
567
568 return getPackageHash(cacheDir)
569 .then(function(hashes) {
570 return hashes[0] && hashes[0] === fullHash;
571 });
572 })
573 .then(function(cacheFresh) {
574 // global cache is fresh
575 // read the cache .deps.json file containing the deps ranges
576 if (cacheFresh)
577 return readJSON(cacheDir + '.deps.json')
578 .then(function(depJSON) {
579 var depRanges = getDepRanges(pkg, depJSON);
580 if (!depRanges.deps)
581 throw new TypeError('Invalid deps format!');
582 preloadResolve(depRanges);
583 return null;
584 });
585
586 ui.log('info', 'Downloading `' + pkg.exactName + '`');
587
588 var endpoint = registry.load(pkg.registry);
589 var lookupObj = lookups[pkg.name][pkg.version];
590
591 getPackageConfigPromise = Promise.resolve()
592 .then(function() {
593 if (endpoint.getPackageConfig)
594 return endpoint.getPackageConfig(pkg.package, pkg.version, lookupObj.hash, lookupObj.meta);
595 })
596 .then(function(packageConfig) {
597 if (!packageConfig)
598 return;
599 return derivePackageConfig(pkg, packageConfig, override)
600 .then(function(packageConfig) {
601 preloadResolve(getDepRanges(pkg, packageConfig));
602 return packageConfig;
603 });
604 })
605 .catch(function(err) {
606 getPackageConfigError = err;
607 });
608
609 return Promise.resolve(cacheDir)
610 // ensure the download directory exists
611 .then(asp(mkdirp))
612 // clear the directory
613 .then(function() {
614 return asp(rimraf)(cacheDir);
615 })
616 .then(function() {
617 try {
618 fs.unlinkSync(cacheDir + '.js');
619 }
620 catch(e) {}
621 })
622 // create it
623 .then(function() {
624 return asp(mkdirp)(cacheDir);
625 })
626 // do the download
627 .then(function() {
628 return endpoint.download(pkg.package, pkg.version, lookupObj.hash, lookupObj.meta, cacheDir);
629 })
630
631 // process the package fully
632 .then(function(downloadPackageConfig) {
633 if (getPackageConfigError)
634 return Promise.reject(getPackageConfigError);
635
636 return getPackageConfigPromise
637 .then(function(packageConfig) {
638 // if we have a getPackageConfig, we always use that packageConfig
639 if (packageConfig)
640 return packageConfig;
641
642 // otherwise get from the repo
643 return Promise.resolve(downloadPackageConfig || readJSON(path.resolve(cacheDir, 'package.json')))
644 .then(function(packageConfig) {
645 return derivePackageConfig(pkg, packageConfig, override)
646 .then(function(packageConfig) {
647 preloadResolve(getDepRanges(pkg, packageConfig));
648 return packageConfig;
649 });
650 });
651 });
652 })
653 .then(function(packageConfig) {
654 // recompute hash in case override was deduped
655 fullHash = computePackageHash(pkg, override);
656 return processPackage(pkg, cacheDir, packageConfig);
657 })
658 // create the config file in the cache folder
659 .then(function(packageConfig) {
660 return createLoaderConfig(pkg, packageConfig, cacheDir)
661 .then(function(loaderConfig) {
662 cfgHash = computeConfigHash(loaderConfig);
663 return packageConfig;
664 });
665 })
666 // create the deps file in the cache folder
667 .then(function(packageConfig) {
668 var depRanges = getDepRanges(pkg, packageConfig);
669 var rangeMap = { dependencies: {}, peerDependencies: {} };
670 Object.keys(depRanges.deps).forEach(function(dep) {
671 rangeMap.dependencies[dep] = depRanges.deps[dep].exactName;
672 });
673 Object.keys(depRanges.peerDeps).forEach(function(dep) {
674 rangeMap.peerDependencies[dep] = depRanges.peerDeps[dep].exactName;
675 });
676 fs.writeFileSync(cacheDir + '.deps.json', JSON.stringify(rangeMap, null, 2));
677 fs.writeFileSync(path.resolve(cacheDir, '.jspm-hash'), fullHash + newLine + cfgHash);
678
679 // postloadResolve creates a promise so we need to return null for Bluebird warnings
680 postloadResolve(depRanges);
681 return null;
682 });
683 })
684
685 // copy global cache to local install
686 // clear the directory
687 .then(function() {
688 // in case it was linked, try and remove
689 try {
690 fs.unlinkSync(downloadDir);
691 }
692 catch(e) {
693 if (e.code === 'EISDIR' || e.code === 'EPERM' || e.code === 'ENOENT')
694 return;
695 throw e;
696 }
697 })
698 .then(function() {
699 return asp(mkdirp)(downloadDir);
700 })
701 .then(function() {
702 return asp(rimraf)(downloadDir);
703 })
704 .then(function() {
705 return asp(ncp)(cacheDir, downloadDir);
706 })
707 .then(function() {
708 // copy config file from cached folder
709 return asp(ncp)(cacheDir + '.json', downloadDir + '.json');
710 })
711 .then(function() {
712 // bump the modified time of the .jspm-hash so that it matches the config file time
713 return asp(fs.utimes)(path.resolve(downloadDir, '.jspm-hash'), new Date() / 1000, new Date() / 1000);
714 })
715 .then(function() {
716 return fresh;
717 });
718 });
719 }));
720};
721
722function getPackageHash(dir) {
723 return new Promise(function(resolve) {
724 fs.exists(dir + '.json', function(exists) {
725 resolve(exists);
726 });
727 })
728 .then(function(hasConfig) {
729 if (!hasConfig)
730 return [];
731
732 // otherwise do the hash check
733 return asp(fs.readFile)(path.resolve(dir, '.jspm-hash'))
734 .then(function(hash) {
735 return hash.toString().split(/\n|\r/);
736 }, function(err) {
737 if (err.code === 'ENOENT')
738 return [];
739 throw err;
740 });
741 });
742}
743
744function computePackageHash(pkg, override) {
745 // determine the full hash of the package
746 var hash = lookups[pkg.name] && lookups[pkg.name][pkg.version].hash || 'link';
747 var endpoint = registry.load(pkg.registry);
748
749 return hash + md5(JSON.stringify(override)) + endpoint.versionString + 'jspm@' + jspmVersion;
750}
751
752function computeConfigHash(pkgConfig) {
753 return md5(JSON.stringify(pkgConfig));
754}
755
756// return the dependency install range object
757// while also setting it on the config.deps cache
758// note it is important that we return the config.deps by reference
759// that is because when installed, locate redirects mutate this to ensure
760// targets all resolve out of registry mappings for reproducibility
761function getDepRanges(pkg, packageConfig) {
762 var depRanges = config.deps[pkg.exactName] = config.deps[pkg.exactName] || { deps: {}, peerDeps: {} };
763
764 var mainDepRanges = processDeps(packageConfig.dependencies, packageConfig.registry, pkg.exactName);
765 var peerDepRanges = processDeps(packageConfig.peerDependencies, packageConfig.registry, pkg.exactName);
766
767 // treat optional dependencies as peerDependencies
768 // when supported in jspm via https://github.com/jspm/jspm-cli/issues/1441,
769 // optionalDependencies will be optional peer dependencies
770 var optionalDepRanges = processDeps(packageConfig.optionalDependencies, packageConfig.registry, pkg.exactName);
771 Object.keys(optionalDepRanges).forEach(function(dep) {
772 if (!peerDepRanges[dep])
773 peerDepRanges[dep] = optionalDepRanges[dep];
774 });
775
776 // deps that are both normal deps and peer deps treated as just peer deps
777 Object.keys(peerDepRanges).forEach(function(dep) {
778 if (mainDepRanges[dep])
779 delete mainDepRanges[dep];
780 });
781
782 // ensure depRanges is an exact reference to the config.deps ranges
783 // so we can mutate the resolutions
784 Object.keys(mainDepRanges).forEach(function(dep) {
785 if (depRanges.deps[dep])
786 return;
787 depRanges.deps[dep] = mainDepRanges[dep];
788 });
789 Object.keys(peerDepRanges).forEach(function(dep) {
790 if (depRanges.peerDeps[dep])
791 return;
792 depRanges.peerDeps[dep] = peerDepRanges[dep];
793 });
794
795 return depRanges;
796}
797
798// like config.derivePackageConfig, but applies the
799// registry processPackageConfig operation as well
800function derivePackageConfig(pkg, packageConfig, override) {
801 packageConfig = extend({}, packageConfig);
802
803 // first derive the override
804 if (override || packageConfig.jspm)
805 packageConfig.jspm = extend({}, packageConfig.jspm || {});
806
807 if (override) {
808 // override is by reference, so we remove properties that don't apply
809 // and clone properties that do
810 for (var p in override) {
811 var stringified = JSON.stringify(override[p]);
812 if (p in packageConfig.jspm ? JSON.stringify(packageConfig.jspm[p]) !== stringified : JSON.stringify(packageConfig[p]) !== stringified)
813 packageConfig.jspm[p] = JSON.parse(stringified);
814 else
815 delete override[p];
816 }
817 }
818
819 // then apply the override
820 if (packageConfig.jspm)
821 extend(packageConfig, packageConfig.jspm);
822
823 var endpoint = registry.load(packageConfig.registry || pkg.registry);
824 return Promise.resolve()
825 .then(function() {
826 if (endpoint.processPackageConfig)
827 return endpoint.processPackageConfig(packageConfig, pkg.exactName);
828
829 return packageConfig;
830 })
831 .then(function(packageConfig) {
832 if (!packageConfig)
833 throw new Error('processPackageConfig must return the processed configuration object.');
834 packageConfig.registry = packageConfig.registry || pkg.registry || 'jspm';
835 return packageConfig;
836 });
837}
838
839
840// apply registry process to package config given completed download folder and config
841function processPackage(pkg, dir, packageConfig, linked) {
842 // any package which takes longer than 10 seconds to process
843 var timeout = setTimeout(function() {
844 ui.log('warn', 'It\'s taking a long time to process the dependencies of `' + pkg.exactName + '`.\n' +
845 'This package may need an %ignore% property to indicate test or example folders for jspm to skip.\n');
846 }, 10000);
847 var endpoint = registry.load(packageConfig.registry || pkg.registry);
848
849 return Promise.resolve()
850 .then(function() {
851 if (linked)
852 return;
853
854 return filterIgnoreAndFiles(dir, packageConfig.ignore, packageConfig.files);
855 })
856 .then(function() {
857 if (linked)
858 return;
859
860 var distDir;
861
862 if (packageConfig.directories)
863 distDir = packageConfig.directories.dist || packageConfig.directories.lib;
864
865 if (distDir)
866 return collapseDistDir(dir, distDir);
867 })
868 .then(function() {
869 // now that we have the derived packageConfig, do the registry build
870 if (endpoint.processPackage)
871 return endpoint.processPackage(packageConfig, pkg.exactName, dir)
872 .catch(function(e) {
873 throw 'Error building package `' + pkg.exactName + '`.\n' + (e && e.stack || e);
874 })
875 .then(function(packageConfig) {
876 if (!packageConfig)
877 throw new Error('Registry endpoint processPackage of ' + pkg.exactName + ' did not return package config.');
878 return packageConfig;
879 });
880 else
881 return packageConfig;
882 })
883
884 .then(function(packageConfig) {
885
886 upgradePackageConfig(packageConfig);
887
888 clearTimeout(timeout);
889 return packageConfig;
890 });
891}
892
893// filter files in a downloaded package to just the [files] and [ignore]
894var inDir = require('./common').inDir;
895function filterIgnoreAndFiles(dir, ignore, files) {
896 if (!ignore && !files)
897 return Promise.resolve();
898
899 return asp(glob)(dir + path.sep + '**' + path.sep + '*', {dot: true})
900 .then(function(allFiles) {
901 var removeFiles = [];
902
903 allFiles.forEach(function(file) {
904 var fileName = path.relative(dir, file).replace(/\\/g, '/');
905
906 // if files, remove all files except those in the files list
907 if (files && !files.some(function(keepFile) {
908 if (typeof keepFile != 'string')
909 return;
910 if (keepFile.startsWith('./'))
911 keepFile = keepFile.substr(2);
912 if (keepFile.endsWith('/*') && keepFile[keepFile.length - 3] != '*')
913 keepFile = keepFile.substr(0, keepFile.length - 2) + '/**/*';
914
915 // this file is in a keep dir, or a keep file, don't exclude
916 if (inDir(fileName, keepFile, false, '/') || minimatch(fileName, keepFile, { dot: true }))
917 return true;
918 }))
919 return removeFiles.push(fileName);
920
921 // if ignore, ensure removed
922 if (ignore && ignore.some(function(ignoreFile) {
923 if (typeof ignoreFile != 'string')
924 return;
925 if (ignoreFile.startsWith('./'))
926 ignoreFile = ignoreFile.substr(2);
927 // this file is in an ignore dir or an ignore file, ignore
928 if (inDir(fileName, ignoreFile, false) || minimatch(fileName, ignoreFile))
929 return true;
930 }))
931 removeFiles.push(fileName);
932 });
933
934 // do removal
935 return Promise.all(removeFiles.map(function(removeFile) {
936 return asp(fs.unlink)(path.resolve(dir, removeFile)).catch(function(e) {
937 if (e.code === 'EPERM' || e.code === 'EISDIR' || e.code === 'ENOENT')
938 return;
939 throw e;
940 });
941 }));
942 });
943}
944function collapseDistDir(dir, subDir) {
945 if (subDir.endsWith('/'))
946 subDir = subDir.substr(0, subDir.length - 1);
947
948 var tmpDir = path.resolve(dir, '..', '.tmp-' + dir.split(path.sep).pop());
949
950 // move subDir to tmpDir
951 fs.renameSync(path.normalize(dir + path.sep + subDir), tmpDir);
952
953 // remove everything in dir
954 return asp(rimraf)(dir)
955 .then(function() {
956 fs.renameSync(tmpDir, dir);
957 });
958}
959
960
961var loaderConfigProperties = ['baseDir', 'defaultExtension', 'format', 'meta', 'map', 'main'];
962
963function createLoaderConfig(pkg, packageConfig, downloadDir) {
964 // systemjs prefix property in package.json takes preference
965 if (typeof packageConfig.systemjs == 'object')
966 packageConfig = packageConfig.systemjs;
967
968 var loaderConfig = {};
969 for (var p in packageConfig)
970 if (loaderConfigProperties.indexOf(p) != -1)
971 loaderConfig[p] = packageConfig[p];
972
973 if (packageConfig.modules && !packageConfig.meta)
974 loaderConfig.meta = packageConfig.modules;
975
976 if (!packageConfig.main) {
977 if (packageConfig.main === false)
978 delete loaderConfig.main;
979 else
980 ui.log('warn', 'Package `' + pkg.exactName + '` has no "main" entry point set in its package config.');
981 }
982
983 fs.writeFileSync(downloadDir + '.json', JSON.stringify(loaderConfig, null, 2));
984 return Promise.resolve(loaderConfig);
985}