mirror of
https://github.com/zhigang1992/npm.git
synced 2026-06-15 10:18:01 +08:00
This lets you do stuff like `npm install foo` to get the latest version supported by your version of node, even if that's not actually foo@latest.
295 lines
9.8 KiB
JavaScript
295 lines
9.8 KiB
JavaScript
|
|
// npm install <pkg> <pkg> <pkg>
|
|
// npm install <pkg@version> <pkg@"1.0.0 - 1.99.99"> <pkg[@latest]> <pkg@tagname>
|
|
|
|
// 1. fetch the data for that package/tag into the cache
|
|
// 2. if it has any dependents, which are not yet installed,
|
|
// then add those to the list, and fetch their data.
|
|
// 3. when all the pkgs are fetched to the cache, and we have a set
|
|
// of packages that are either installed or fetched which
|
|
// will satisfy everyone's dependencies, then untar into the
|
|
// target directories for each of them.
|
|
// 4. build each of the packages that aren't already installed
|
|
|
|
module.exports = install
|
|
|
|
install.usage = "npm install <tarball file>"
|
|
+ "\nnpm install <tarball url>"
|
|
+ "\nnpm install <folder>"
|
|
+ "\nnpm install <pkg>"
|
|
+ "\nnpm install <pkg>@<tag>"
|
|
+ "\nnpm install <pkg>@<version>"
|
|
+ "\nnpm install <pkg>@<version range>"
|
|
+ "\n\nCan specify one or more: npm install ./foo.tgz bar@stable /some/folder"
|
|
+ "\nInstalls '.' if no argument supplied"
|
|
|
|
install.completion = function (args, index, cb) {
|
|
var remotePkgs = require("./utils/completion/remote-packages")
|
|
remotePkgs(args, index, true, true, true, cb)
|
|
}
|
|
|
|
var registry = require("./utils/registry")
|
|
, npm = require("../npm")
|
|
, readInstalled = require("./utils/read-installed")
|
|
, installedPackages
|
|
, semver = require("./utils/semver")
|
|
, url = require("url")
|
|
, fetch = require("./utils/fetch")
|
|
, mkdir = require("./utils/mkdir-p")
|
|
, readJson = require("./utils/read-json")
|
|
, log = require("./utils/log")
|
|
, path = require("path")
|
|
, fs = require("./utils/graceful-fs")
|
|
, cache = require("./cache")
|
|
, asyncMap = require("./utils/async-map")
|
|
|
|
function install (pkglist, cb) {
|
|
log.verbose(pkglist, "install")
|
|
if (pkglist.length === 0) pkglist = ["."]
|
|
// it's helpful to know what we have already
|
|
if (!installedPackages) return readInstalled([], function (er, data) {
|
|
if (er) return cb(er)
|
|
installedPackages = data || {}
|
|
install(pkglist, cb)
|
|
})
|
|
|
|
log.verbose(pkglist, "install pkglist")
|
|
var mustInstall = npm.config.get("must-install") ? pkglist.slice(0) : []
|
|
|
|
// three lists: "pkglist", "next", and "reg"
|
|
// asyncMap over the "left" list: for each "it"
|
|
// find out what it is
|
|
// if it's version/range installed or on "reg" list, continue.
|
|
// if it's a url, fetch to cache and add the name/version to "next"
|
|
// if it's a tag, fetch the json, add the version to "next"
|
|
// if it's a version(range) not installed, then fetch the json,
|
|
// add url to "next"
|
|
// if it's a specific version in cache, then unpack, add to "reg"
|
|
// list, add its deps to "next"
|
|
// if the "next" list is not empty, then pkglist=next,next=[], and repeat.
|
|
// if it is, then build all the "reg" folders.
|
|
|
|
var reg = Object.create(installedPackages)
|
|
, seen = {}
|
|
log.verbose(mustInstall, "must install")
|
|
asyncMap(pkglist, function (pkg, cb) {
|
|
install_(pkg, reg, seen, mustInstall.indexOf(pkg) !== -1, pkglist, cb)
|
|
}, function (er) {
|
|
if (er) return cb(er)
|
|
buildAll(reg, cb)
|
|
})
|
|
}
|
|
|
|
// call the cb with the "next" thing(s) to look up for this one, or nothing
|
|
function install_ (pkg, reg, seen, mustHave, pkglist, cb) {
|
|
log.verbose(pkg, "install_")
|
|
if (seen[pkg]) return cb() // repeat, skip it
|
|
seen[pkg] = true
|
|
|
|
// it's a local thing or a url if it has a / in it.
|
|
if (pkg.indexOf("/") !== -1 || pkg === ".") {
|
|
log.silly(pkg, "install local")
|
|
return cache.add(pkg, finisher(pkg, reg, pkglist, cb))
|
|
}
|
|
|
|
// now we know it's not a URL or file,
|
|
// so handle it like a tag, version, or range.
|
|
pkg = pkg.split("@")
|
|
var name = pkg[0]
|
|
, defTag = npm.config.get("tag")
|
|
, ver = pkg.slice(1).join("@").trim() || defTag
|
|
, range = semver.validRange(ver)
|
|
, exact = semver.valid(ver)
|
|
, tag = !exact && !range && range !== "" && ver
|
|
log.verbose([pkg, mustHave], "must install?")
|
|
pkg = pkg.join("@")
|
|
seen[name+"@"+ver] = true
|
|
|
|
// if there is a satisfying version already, then simply move on.
|
|
if (!tag && findSatisfying(pkg, name, range, mustHave, reg)) {
|
|
return cb()
|
|
}
|
|
|
|
// at this point, assume that it has to be installed.
|
|
if (exact) {
|
|
log.verbose("exact", pkg)
|
|
// just pull the data out of the cache to ensure it's there
|
|
return cache.read(name, exact, finisher(pkg, reg, pkglist, cb))
|
|
}
|
|
|
|
getData(name, function (er, data) {
|
|
if (er) return cb(er)
|
|
log.silly(data, pkg)
|
|
if (tag) {
|
|
log.verbose(tag, pkg+" tag")
|
|
var tags = data["dist-tags"]
|
|
if (!tags[tag]) cb(new Error(
|
|
"Tag not found: "+data.name+"@"+tag
|
|
+"\nValid install targets for "+data.name+": "
|
|
+installTargets(data)))
|
|
install_(data.name+"@"+tags[tag], reg, seen, mustHave, pkglist, cb)
|
|
} else {
|
|
log(pkg, "range")
|
|
// prefer the default tag version.
|
|
var defTag = npm.config.get("tag")
|
|
, satis
|
|
defTag = defTag && data["dist-tags"] && data["dist-tags"][defTag]
|
|
if (semver.satisfies(defTag, range)) satis = defTag
|
|
else satis = semver.maxSatisfying(Object.keys(data.versions), range)
|
|
|
|
if (!satis) return cb(new Error(
|
|
"No satisfying version found for '"+data.name+"'@'"+range+"'"
|
|
+"\nValid install targets for "+data.name+": "
|
|
+installTargets(data)))
|
|
install_(data.name+"@"+satis, reg, seen, mustHave, pkglist, cb)
|
|
}
|
|
})
|
|
}
|
|
function installTargets (data) {
|
|
return Object.keys(data["dist-tags"])
|
|
.concat(Object.keys(data.versions))
|
|
.map(JSON.stringify)
|
|
.join(", ") || "(none)"
|
|
}
|
|
|
|
function getData (name, cb) {
|
|
var data = npm.get(name)
|
|
if (data) return cb(null, data)
|
|
registry.get(name, function (er, data) {
|
|
if (er) return cb(er)
|
|
if (!data._id) data._id = name
|
|
for (var ver in data.versions) {
|
|
try {
|
|
readJson.processJson(data.versions[ver])
|
|
} catch (ex) {
|
|
log(ver, "ignoring invalid version")
|
|
delete data.versions[ver]
|
|
}
|
|
}
|
|
filterNodeVersion(data)
|
|
npm.set(data)
|
|
cb(null, data)
|
|
})
|
|
}
|
|
// see if there is a satisfying version already
|
|
function findSatisfying (pkg, name, range, mustHave, reg) {
|
|
if (mustHave) return null
|
|
return semver.maxSatisfying
|
|
( Object.keys(reg[name] || {})
|
|
.concat(Object.keys(Object.getPrototypeOf(reg[name] || {})))
|
|
, range
|
|
)
|
|
}
|
|
|
|
function finisher (pkg, reg, pkglist, cb) { return function (er, data) {
|
|
if (er) return log.er(cb, "Error installing "+pkg)(er)
|
|
if (!data._nodeSupported) cb(
|
|
data.name+"@"+data.version+" not compatible with your version of node\n"+
|
|
"Requires: node@"+data.engines.node+"\n"+
|
|
"You have: node@"+process.version)
|
|
|
|
if (!reg.hasOwnProperty(data.name)) {
|
|
reg[data.name] = Object.create(reg[data.name] || Object.prototype)
|
|
}
|
|
if (!reg[data.name].hasOwnProperty(data.version)) {
|
|
reg[data.name][data.version] = {}
|
|
}
|
|
reg[data.name][data.version] = data
|
|
|
|
// also add the dependencies.
|
|
if (!pkglist) pkglist = []
|
|
getDeps(data, function (er, deps) {
|
|
if (!Array.isArray(pkglist)) pkglist = [pkglist]
|
|
pkglist.push.apply(pkglist, Object.keys(deps).map(function (dep) {
|
|
var v = deps[dep].trim()
|
|
, d = dep.trim()
|
|
return v ? d + "@" + v : d
|
|
}))
|
|
cb()
|
|
})
|
|
}}
|
|
|
|
function getDeps (data, cb) {
|
|
var deps = data.dependencies || {}
|
|
, devDeps = data.devDependencies || {}
|
|
if (npm.config.get("dev")) {
|
|
Object.keys(devDeps).forEach(function (d) { deps[d] = devDeps[d] })
|
|
}
|
|
// now see if any of them are already bundled.
|
|
// if so, omit them from the list.
|
|
var bundle = path.join( npm.cache, data.name, data.version
|
|
, "package", "node_modules" )
|
|
fs.readdir(bundle, function (er, bundles) {
|
|
bundles = bundles || []
|
|
bundles.forEach(function (b) { delete deps[b] })
|
|
cb(null, deps)
|
|
})
|
|
}
|
|
|
|
function filterNodeVersion (data) {
|
|
var supported = []
|
|
Object.keys(data.versions).forEach(function (v) {
|
|
if (!data.versions[v]._nodeSupported) {
|
|
log.verbose(data.versions[v]._id, "not supported on node@"+process.version)
|
|
delete data.versions[v]
|
|
} else supported.push(data.versions[v]._id)
|
|
})
|
|
if (!supported.length) {
|
|
log.warn(data._id, "Not supported on node@"+process.version)
|
|
} else log.verbose(supported, "Supported versions")
|
|
// might have deleted the special "latest" tag. if so, replace it.
|
|
var v = data["dist-tags"].latest
|
|
if (v && !data.versions[v] && supported.length) {
|
|
data["dist-tags"].latest = Object.keys(data.versions)
|
|
.sort(semver.compare).pop()
|
|
log("not supported by node@"+process.version,
|
|
"latest = "+data.name+"@"+v)
|
|
}
|
|
}
|
|
|
|
function buildAll (installed, cb) {
|
|
var list = []
|
|
Object.keys(installed).forEach(function (i) {
|
|
Object.keys(installed[i]).forEach(function (v) {
|
|
list.push([i, v])
|
|
})
|
|
})
|
|
log.verbose(list, "install list")
|
|
cb = rollbackFailure(list, cb)
|
|
var buildList = []
|
|
asyncMap(list, function (i, cb) {
|
|
var target = path.join(npm.dir, i[0], i[1])
|
|
cache.unpack(i[0], i[1], target, cb)
|
|
buildList.push(target)
|
|
}, function (er) {
|
|
if (er) return cb(er)
|
|
log.verbose(list.join("\n"), "unpacked, building")
|
|
if (buildList.length) npm.commands.build(buildList, cb)
|
|
else {
|
|
log.info("Nothing to do", "install")
|
|
cb()
|
|
}
|
|
})
|
|
}
|
|
function rollbackFailure (installList, cb) { return function (er) {
|
|
if (!er) return log.verbose(installList.map(function (i) {
|
|
return i.join("@")
|
|
}).join("\n"), "installed", cb)
|
|
// error happened, roll back
|
|
installList = installList.map(function (p) {
|
|
return p.join('@')
|
|
})
|
|
npm.ROLLBACK = true
|
|
log.error(er, "install failed")
|
|
log("rollback", "install failed")
|
|
log(installList, "uninstall")
|
|
return npm.commands.uninstall
|
|
( installList
|
|
, function (er_) {
|
|
if (er_) log.error(er_, "rollback failed")
|
|
else log("rolled back", "install failed")
|
|
cb(er)
|
|
}
|
|
)
|
|
}}
|