You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

306 lines
9.0 KiB

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')
var packageId = require('../utils/package-id.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) 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 ' + packageId(data) + ' package'))
if (!dist.tarball) {
return cb(new Error(
'No dist.tarball in ' + packageId(data) + ' 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
// If a different port is associated with the other protocol
// we need to update that as well
if (rp.port !== tb.port) {
tb.port = rp.port
delete tb.host
}
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: ' + packageId(data)))
}
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) {
if (range === '*' && versions.length) {
return addNameTag(name, 'latest', data, cb)
} else {
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' + targets.join(', ') + '\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
}