1 var path = require("path")
2 , assert = require("assert")
3 , fs = require("graceful-fs")
4 , http = require("http")
5 , log = require("npmlog")
6 , semver = require("semver")
7 , readJson = require("read-package-json")
9 , npm = require("../npm.js")
10 , deprCheck = require("../utils/depr-check.js")
11 , inflight = require("inflight")
12 , addRemoteTarball = require("./add-remote-tarball.js")
13 , cachedPackageRoot = require("./cached-package-root.js")
14 , mapToRegistry = require("../utils/map-to-registry.js")
17 module.exports = addNamed
19 function getOnceFromRegistry (name, from, next, done) {
20 function fixName(err, data, json, resp) {
21 // this is only necessary until npm/npm-registry-client#80 is fixed
22 if (err && err.pkgid && err.pkgid !== name) {
23 err.message = err.message.replace(
24 new RegExp(': ' + err.pkgid.replace(/(\W)/g, '\\$1') + '$'),
29 next(err, data, json, resp)
32 mapToRegistry(name, npm.config, function (er, uri, auth) {
33 if (er) return done(er)
35 var key = "registry:" + uri
36 next = inflight(key, next)
37 if (!next) return log.verbose(from, key, "already in flight; waiting")
38 else log.verbose(from, key, "not in flight; fetching")
40 npm.registry.get(uri, { auth : auth }, fixName)
44 function addNamed (name, version, data, cb_) {
45 assert(typeof name === "string", "must have module name")
46 assert(typeof cb_ === "function", "must have callback")
48 var key = name + "@" + version
49 log.silly("addNamed", key)
51 function cb (er, data) {
52 if (data && !data._fromGithub) data._from = key
56 if (semver.valid(version, true)) {
57 log.verbose('addNamed', JSON.stringify(version), 'is a plain semver version for', name)
58 addNameVersion(name, version, data, cb)
59 } else if (semver.validRange(version, true)) {
60 log.verbose('addNamed', JSON.stringify(version), 'is a valid semver range for', name)
61 addNameRange(name, version, data, cb)
63 log.verbose('addNamed', JSON.stringify(version), 'is being treated as a dist-tag for', name)
64 addNameTag(name, version, data, cb)
68 function addNameTag (name, tag, data, cb) {
69 log.info("addNameTag", [name, tag])
73 tag = npm.config.get("tag")
76 getOnceFromRegistry(name, "addNameTag", next, cb)
78 function next (er, data, json, resp) {
79 if (!er) er = errorResponse(name, resp)
82 log.silly("addNameTag", "next cb for", name, "with tag", tag)
85 if (data["dist-tags"] && data["dist-tags"][tag]
86 && data.versions[data["dist-tags"][tag]]) {
87 var ver = data["dist-tags"][tag]
88 return addNamed(name, ver, data.versions[ver], cb)
90 if (!explicit && Object.keys(data.versions).length) {
91 return addNamed(name, "*", data, cb)
94 er = installTargetsError(tag, data)
99 function engineFilter (data) {
100 var npmv = npm.version
101 , nodev = npm.config.get("node-version")
102 , strict = npm.config.get("engine-strict")
104 if (!nodev || npm.config.get("force")) return data
106 Object.keys(data.versions || {}).forEach(function (v) {
107 var eng = data.versions[v].engines
109 if (!strict && !data.versions[v].engineStrict) return
110 if (eng.node && !semver.satisfies(nodev, eng.node, true)
111 || eng.npm && !semver.satisfies(npmv, eng.npm, true)) {
112 delete data.versions[v]
117 function addNameVersion (name, v, data, cb) {
118 var ver = semver.valid(v, true)
119 if (!ver) return cb(new Error("Invalid version: "+v))
128 getOnceFromRegistry(name, "addNameVersion", setData, cb)
130 function setData (er, d, json, resp) {
132 er = errorResponse(name, resp)
134 if (er) return cb(er)
135 data = d && d.versions[ver]
137 er = new Error("version not found: "+name+"@"+ver)
150 if (!dist) return cb(new Error("No dist in "+data._id+" package"))
152 if (!dist.tarball) return cb(new Error(
153 "No dist.tarball in " + data._id + " package"))
155 if ((response && response.statusCode !== 304) || npm.config.get("force")) {
159 // we got cached data, so let's see if we have a tarball.
160 var pkgroot = cachedPackageRoot({name : name, version : ver})
161 var pkgtgz = path.join(pkgroot, "package.tgz")
162 var pkgjson = path.join(pkgroot, "package", "package.json")
163 fs.stat(pkgtgz, function (er) {
165 readJson(pkgjson, function (er, data) {
166 if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er)
169 if (!data.name) return cb(new Error("No name provided"))
170 if (!data.version) return cb(new Error("No version provided"))
172 // check the SHA of the package we have, to ensure it wasn't installed
173 // from somewhere other than the registry (eg, a fork)
174 if (data._shasum && dist.shasum && data._shasum !== dist.shasum) {
179 if (er) return fetchit()
180 else return cb(null, data)
182 } else return fetchit()
185 function fetchit () {
186 mapToRegistry(name, npm.config, function (er, _, auth, ruri) {
187 if (er) return cb(er)
189 // Use the same protocol as the registry. https registry --> https
190 // tarballs, but only if they're the same hostname, or else detached
191 // tarballs may not work.
192 var tb = url.parse(dist.tarball)
193 var rp = url.parse(ruri)
194 if (tb.hostname === rp.hostname && tb.protocol !== rp.protocol) {
195 tb.protocol = rp.protocol
196 // If a different port is associated with the other protocol
197 // we need to update that as well
198 if (rp.port !== tb.port) {
206 // Only add non-shasum'ed packages if --forced. Only ancient things
207 // would lack this for good reasons nowadays.
208 if (!dist.shasum && !npm.config.get("force")) {
209 return cb(new Error("package lacks shasum: " + data._id))
212 addRemoteTarball(tb, data, dist.shasum, auth, cb)
218 function addNameRange (name, range, data, cb) {
219 range = semver.validRange(range, true)
220 if (range === null) return cb(new Error(
221 "Invalid version range: " + range
224 log.silly("addNameRange", {name:name, range:range, hasData:!!data})
226 if (data) return next()
228 getOnceFromRegistry(name, "addNameRange", setData, cb)
230 function setData (er, d, json, resp) {
232 er = errorResponse(name, resp)
234 if (er) return cb(er)
240 log.silly( "addNameRange", "number 2"
241 , {name:name, range:range, hasData:!!data})
244 log.silly("addNameRange", "versions"
245 , [data.name, Object.keys(data.versions || {})])
247 // if the tagged version satisfies, then use that.
248 var tagged = data["dist-tags"][npm.config.get("tag")]
250 && data.versions[tagged]
251 && semver.satisfies(tagged, range, true)) {
252 return addNamed(name, tagged, data.versions[tagged], cb)
255 // find the max satisfying version.
256 var versions = Object.keys(data.versions || {})
257 var ms = semver.maxSatisfying(versions, range, true)
259 if (range === "*" && versions.length) {
260 return addNameTag(name, "latest", data, cb)
262 return cb(installTargetsError(range, data))
266 // if we don't have a registry connection, try to see if
267 // there's a cached copy that will be ok.
268 addNamed(name, ms, data.versions[ms], cb)
272 function installTargetsError (requested, data) {
273 var targets = Object.keys(data["dist-tags"]).filter(function (f) {
274 return (data.versions || {}).hasOwnProperty(f)
275 }).concat(Object.keys(data.versions || {}))
277 requested = data.name + (requested ? "@'" + requested + "'" : "")
279 targets = targets.length
280 ? "Valid install targets:\n" + JSON.stringify(targets) + "\n"
281 : "No valid targets found.\n"
282 + "Perhaps not compatible with your version of node?"
284 var er = new Error( "No compatible version found: "
285 + requested + "\n" + targets)
290 function errorResponse (name, response) {
292 if (response.statusCode >= 400) {
293 er = new Error(http.STATUS_CODES[response.statusCode])
294 er.statusCode = response.statusCode
295 er.code = "E" + er.statusCode