var path = require('path') var assert = require('assert') var fs = require('graceful-fs') var http = require('http') var log = require('npmlog') var semver = require('semver') var readJson = require('read-package-json') var url = require('url') var npm = require('../npm.js') var deprCheck = require('../utils/depr-check.js') var inflight = require('inflight') var addRemoteTarball = require('./add-remote-tarball.js') var cachedPackageRoot = require('./cached-package-root.js') var mapToRegistry = require('../utils/map-to-registry.js') var pulseTillDone = require('../utils/pulse-till-done.js') module.exports = addNamed function getOnceFromRegistry (name, from, next, done) { function fixName (err, data, json, resp) { // this is only necessary until npm/npm-registry-client#80 is fixed if (err && err.pkgid && err.pkgid !== name) { err.message = err.message.replace( new RegExp(': ' + err.pkgid.replace(/(\W)/g, '\\$1') + '$'), ': ' + name ) err.pkgid = name } next(err, data, json, resp) } mapToRegistry(name, npm.config, function (er, uri, auth) { if (er) return done(er) var key = 'registry:' + uri next = inflight(key, next) if (!next) return log.verbose(from, key, 'already in flight; waiting') else log.verbose(from, key, 'not in flight; fetching') npm.registry.get(uri, { auth: auth }, pulseTillDone('fetchRegistry', fixName)) }) } function addNamed (name, version, data, cb_) { assert(typeof name === 'string', 'must have module name') assert(typeof cb_ === 'function', 'must have callback') var key = name + '@' + version log.silly('addNamed', key) function cb (er, data) { if (data && !data._fromHosted) data._from = key cb_(er, data) } if (semver.valid(version, true)) { log.verbose('addNamed', JSON.stringify(version), 'is a plain semver version for', name) addNameVersion(name, version, data, cb) } else if (semver.validRange(version, true)) { log.verbose('addNamed', JSON.stringify(version), 'is a valid semver range for', name) addNameRange(name, version, data, cb) } else { log.verbose('addNamed', JSON.stringify(version), 'is being treated as a dist-tag for', name) addNameTag(name, version, data, cb) } } function addNameTag (name, tag, data, cb) { log.info('addNameTag', [name, tag]) var explicit = true if (!tag) { explicit = false tag = npm.config.get('tag') } getOnceFromRegistry(name, 'addNameTag', next, cb) function next (er, data, json, resp) { if (!er) er = errorResponse(name, resp) if (er) return cb(er) log.silly('addNameTag', 'next cb for', name, 'with tag', tag) engineFilter(data) if (data['dist-tags'] && data['dist-tags'][tag] && data.versions[data['dist-tags'][tag]]) { var ver = data['dist-tags'][tag] return addNamed(name, ver, data.versions[ver], cb) } if (!explicit && Object.keys(data.versions).length) { return addNamed(name, '*', data, cb) } er = installTargetsError(tag, data) return cb(er) } } function engineFilter (data) { var npmv = npm.version var nodev = npm.config.get('node-version') var strict = npm.config.get('engine-strict') if (!nodev || npm.config.get('force')) return data Object.keys(data.versions || {}).forEach(function (v) { var eng = data.versions[v].engines if (!eng) return if (!strict && !data.versions[v].engineStrict) return if (eng.node && !semver.satisfies(nodev, eng.node, true) || eng.npm && !semver.satisfies(npmv, eng.npm, true)) { delete data.versions[v] } }) } function addNameVersion (name, v, data, cb) { var ver = semver.valid(v, true) if (!ver) return cb(new Error('Invalid version: ' + v)) var response if (data) { response = null return next() } getOnceFromRegistry(name, 'addNameVersion', setData, cb) function setData (er, d, json, resp) { if (!er) { er = errorResponse(name, resp) } if (er) return cb(er) data = d && d.versions[ver] if (!data) { er = new Error('version not found: ' + name + '@' + ver) er.package = name er.statusCode = 404 return cb(er) } response = resp next() } function next () { deprCheck(data) var dist = data.dist if (!dist) return cb(new Error('No dist in ' + data._id + ' package')) if (!dist.tarball) { return cb(new Error( 'No dist.tarball in ' + data._id + ' package' )) } if ((response && response.statusCode !== 304) || npm.config.get('force')) { return fetchit() } // we got cached data, so let's see if we have a tarball. var pkgroot = cachedPackageRoot({ name: name, version: ver }) var pkgtgz = path.join(pkgroot, 'package.tgz') var pkgjson = path.join(pkgroot, 'package', 'package.json') fs.stat(pkgtgz, function (er) { if (!er) { readJson(pkgjson, function (er, data) { if (er && er.code !== 'ENOENT' && er.code !== 'ENOTDIR') return cb(er) if (data) { if (!data.name) return cb(new Error('No name provided')) if (!data.version) return cb(new Error('No version provided')) // check the SHA of the package we have, to ensure it wasn't installed // from somewhere other than the registry (eg, a fork) if (data._shasum && dist.shasum && data._shasum !== dist.shasum) { return fetchit() } } if (er) return fetchit() else return cb(null, data) }) } else return fetchit() }) function fetchit () { mapToRegistry(name, npm.config, function (er, _, auth, ruri) { if (er) return cb(er) // Use the same protocol as the registry. https registry --> https // tarballs, but only if they're the same hostname, or else detached // tarballs may not work. var tb = url.parse(dist.tarball) var rp = url.parse(ruri) if (tb.hostname === rp.hostname && tb.protocol !== rp.protocol) { tb.protocol = rp.protocol delete tb.href } tb = url.format(tb) // Only add non-shasum'ed packages if --forced. Only ancient things // would lack this for good reasons nowadays. if (!dist.shasum && !npm.config.get('force')) { return cb(new Error('package lacks shasum: ' + data._id)) } addRemoteTarball(tb, data, dist.shasum, auth, cb) }) } } } function addNameRange (name, range, data, cb) { range = semver.validRange(range, true) if (range === null) { return cb(new Error( 'Invalid version range: ' + range )) } log.silly('addNameRange', { name: name, range: range, hasData: !!data }) if (data) return next() getOnceFromRegistry(name, 'addNameRange', setData, cb) function setData (er, d, json, resp) { if (!er) { er = errorResponse(name, resp) } if (er) return cb(er) data = d next() } function next () { log.silly( 'addNameRange', 'number 2', { name: name, range: range, hasData: !!data } ) engineFilter(data) log.silly('addNameRange', 'versions' , [data.name, Object.keys(data.versions || {})]) // if the tagged version satisfies, then use that. var tagged = data['dist-tags'][npm.config.get('tag')] if (tagged && data.versions[tagged] && semver.satisfies(tagged, range, true)) { return addNamed(name, tagged, data.versions[tagged], cb) } // find the max satisfying version. var versions = Object.keys(data.versions || {}) var ms = semver.maxSatisfying(versions, range, true) if (!ms) { return cb(installTargetsError(range, data)) } // if we don't have a registry connection, try to see if // there's a cached copy that will be ok. addNamed(name, ms, data.versions[ms], cb) } } function installTargetsError (requested, data) { var targets = Object.keys(data['dist-tags']).filter(function (f) { return (data.versions || {}).hasOwnProperty(f) }).concat(Object.keys(data.versions || {})) requested = data.name + (requested ? "@'" + requested + "'" : '') targets = targets.length ? 'Valid install targets:\n' + JSON.stringify(targets) + '\n' : 'No valid targets found.\n' + 'Perhaps not compatible with your version of node?' var er = new Error('No compatible version found: ' + requested + '\n' + targets) er.code = 'ETARGET' return er } function errorResponse (name, response) { var er if (response.statusCode >= 400) { er = new Error(http.STATUS_CODES[response.statusCode]) er.statusCode = response.statusCode er.code = 'E' + er.statusCode er.pkgid = name } return er }