mirror of https://github.com/lukechilds/node.git
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
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
|
|
}
|
|
|