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.
895 lines
29 KiB
895 lines
29 KiB
// npm install <pkg> <pkg> <pkg>
|
|
//
|
|
// See doc/install.md for more description
|
|
|
|
// Managing contexts...
|
|
// there's a lot of state associated with an "install" operation, including
|
|
// packages that are already installed, parent packages, current shrinkwrap, and
|
|
// so on. We maintain this state in a "context" object that gets passed around.
|
|
// every time we dive into a deeper node_modules folder, the "family" list that
|
|
// gets passed along uses the previous "family" list as its __proto__. Any
|
|
// "resolved precise dependency" things that aren't already on this object get
|
|
// added, and then that's passed to the next generation of installation.
|
|
|
|
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"
|
|
+ "\nIf no argument is supplied and ./npm-shrinkwrap.json is "
|
|
+ "\npresent, installs dependencies specified in the shrinkwrap."
|
|
+ "\nOtherwise, installs dependencies from ./package.json."
|
|
|
|
install.completion = function (opts, cb) {
|
|
// install can complete to a folder with a package.json, or any package.
|
|
// if it has a slash, then it's gotta be a folder
|
|
// if it starts with https?://, then just give up, because it's a url
|
|
// for now, not yet implemented.
|
|
var registry = require("./utils/npm-registry-client/index.js")
|
|
registry.get("/-/short", function (er, pkgs) {
|
|
if (er) return cb()
|
|
if (!opts.partialWord) return cb(null, pkgs)
|
|
|
|
var name = opts.partialWord.split("@").shift()
|
|
pkgs = pkgs.filter(function (p) {
|
|
return p.indexOf(name) === 0
|
|
})
|
|
|
|
if (pkgs.length !== 1 && opts.partialWord === name) {
|
|
return cb(null, pkgs)
|
|
}
|
|
|
|
registry.get(pkgs[0], function (er, d) {
|
|
if (er) return cb()
|
|
return cb(null, Object.keys(d["dist-tags"] || {})
|
|
.concat(Object.keys(d.versions || {}))
|
|
.map(function (t) {
|
|
return pkgs[0] + "@" + t
|
|
}))
|
|
})
|
|
})
|
|
}
|
|
|
|
var npm = require("./npm.js")
|
|
, semver = require("semver")
|
|
, readJson = require("./utils/read-json.js")
|
|
, log = require("./utils/log.js")
|
|
, path = require("path")
|
|
, fs = require("graceful-fs")
|
|
, cache = require("./cache.js")
|
|
, asyncMap = require("slide").asyncMap
|
|
, chain = require("slide").chain
|
|
, relativize = require("./utils/relativize.js")
|
|
, output
|
|
, url = require("url")
|
|
, mkdir = require("mkdirp")
|
|
, lifecycle = require("./utils/lifecycle.js")
|
|
, archy = require("archy")
|
|
|
|
function install (args, cb_) {
|
|
|
|
function cb (er, installed) {
|
|
if (er) return cb_(er)
|
|
|
|
output = output || require("./utils/output.js")
|
|
|
|
var tree = treeify(installed)
|
|
, pretty = prettify(tree, installed).trim()
|
|
|
|
if (pretty) output.write(pretty, afterWrite)
|
|
else afterWrite()
|
|
|
|
function afterWrite (er) {
|
|
if (er) return cb_(er)
|
|
save(where, installed, tree, pretty, cb_)
|
|
}
|
|
}
|
|
|
|
// the /path/to/node_modules/..
|
|
var where = path.resolve(npm.dir, "..")
|
|
|
|
// internal api: install(where, what, cb)
|
|
if (arguments.length === 3) {
|
|
where = args
|
|
args = [].concat(cb_) // pass in [] to do default dep-install
|
|
cb_ = arguments[2]
|
|
log.verbose([where, args], "install(where, what)")
|
|
}
|
|
|
|
if (!npm.config.get("global")) {
|
|
args = args.filter(function (a) {
|
|
return path.resolve(a) !== where
|
|
})
|
|
}
|
|
|
|
mkdir(where, function (er, made) {
|
|
if (er) return cb(er)
|
|
// install dependencies locally by default,
|
|
// or install current folder globally
|
|
if (!args.length) {
|
|
if (npm.config.get("global")) args = ["."]
|
|
else return readDependencies( null
|
|
, where
|
|
, { dev: !npm.config.get("production") }
|
|
, function (er, data) {
|
|
if (er) return log.er(cb, "Couldn't read dependencies.")(er)
|
|
var deps = Object.keys(data.dependencies || {})
|
|
log.verbose([where, deps], "where, deps")
|
|
var context = { family: {}
|
|
, ancestors: {}
|
|
, explicit: false
|
|
, parent: data
|
|
, wrap: null }
|
|
context.family[data.name] = context.ancestors[data.name] = data.version
|
|
installManyTop(deps.map(function (dep) {
|
|
var target = data.dependencies[dep]
|
|
, parsed = url.parse(target.replace(/^git\+/, "git"))
|
|
target = dep + "@" + target
|
|
return target
|
|
}), where, context, cb)
|
|
})
|
|
}
|
|
|
|
// initial "family" is the name:version of the root, if it's got
|
|
// a package.json file.
|
|
readJson(path.resolve(where, "package.json"), function (er, data) {
|
|
if (er) data = null
|
|
var context = { family: {}
|
|
, ancestors: {}
|
|
, explicit: true
|
|
, parent: data
|
|
, wrap: null }
|
|
if (data) {
|
|
context.family[data.name] = context.ancestors[data.name] = data.version
|
|
}
|
|
var fn = npm.config.get("global") ? installMany : installManyTop
|
|
fn(args, where, context, cb)
|
|
})
|
|
})
|
|
}
|
|
|
|
// reads dependencies for the package at "where". There are several cases,
|
|
// depending on our current state and the package's configuration:
|
|
//
|
|
// 1. If "context" is specified, then we examine the context to see if there's a
|
|
// shrinkwrap there. In that case, dependencies are read from the shrinkwrap.
|
|
// 2. Otherwise, if an npm-shrinkwrap.json file is present, dependencies are
|
|
// read from there.
|
|
// 3. Otherwise, dependencies come from package.json.
|
|
//
|
|
// Regardless of which case we fall into, "cb" is invoked with a first argument
|
|
// describing the full package (as though readJson had been used) but with
|
|
// "dependencies" read as described above. The second argument to "cb" is the
|
|
// shrinkwrap to use in processing this package's dependencies, which may be
|
|
// "wrap" (in case 1) or a new shrinkwrap (in case 2).
|
|
function readDependencies (context, where, opts, cb) {
|
|
var wrap = context ? context.wrap : null
|
|
|
|
readJson( path.resolve(where, "package.json")
|
|
, opts
|
|
, function (er, data) {
|
|
if (er) return cb(er)
|
|
|
|
if (wrap) {
|
|
log.verbose([where, wrap], "readDependencies: using existing wrap")
|
|
var rv = {}
|
|
Object.keys(data).forEach(function (key) {
|
|
rv[key] = data[key]
|
|
})
|
|
rv.dependencies = {}
|
|
Object.keys(wrap).forEach(function (key) {
|
|
log.verbose([key, wrap[key]], "from wrap")
|
|
var w = wrap[key]
|
|
rv.dependencies[key] = w.from || w.version
|
|
})
|
|
log.verbose([rv.dependencies], "readDependencies: returned deps")
|
|
return cb(null, rv, wrap)
|
|
}
|
|
|
|
var wrapfile = path.resolve(where, "npm-shrinkwrap.json")
|
|
|
|
fs.readFile(wrapfile, "utf8", function (er, wrapjson) {
|
|
if (er) {
|
|
log.verbose("readDependencies: using package.json deps")
|
|
return cb(null, data, null)
|
|
}
|
|
|
|
try {
|
|
var newwrap = JSON.parse(wrapjson)
|
|
} catch (ex) {
|
|
return cb(ex)
|
|
}
|
|
|
|
log.info(wrapfile, "using shrinkwrap file")
|
|
var rv = {}
|
|
Object.keys(data).forEach(function (key) {
|
|
rv[key] = data[key]
|
|
})
|
|
rv.dependencies = {}
|
|
Object.keys(newwrap.dependencies || {}).forEach(function (key) {
|
|
var w = newwrap.dependencies[key]
|
|
rv.dependencies[key] = w.from || w.version
|
|
})
|
|
log.verbose([rv.dependencies], "readDependencies: returned deps")
|
|
return cb(null, rv, newwrap.dependencies)
|
|
})
|
|
})
|
|
}
|
|
|
|
// if the -S|--save option is specified, then write installed packages
|
|
// as dependencies to a package.json file.
|
|
// This is experimental.
|
|
function save (where, installed, tree, pretty, cb) {
|
|
if (!npm.config.get("save") &&
|
|
!npm.config.get("save-dev") &&
|
|
!npm.config.get("save-optional") ||
|
|
npm.config.get("global")) {
|
|
return cb(null, installed, tree, pretty)
|
|
}
|
|
|
|
// each item in the tree is a top-level thing that should be saved
|
|
// to the package.json file.
|
|
// The relevant tree shape is { <folder>: {what:<pkg>} }
|
|
var saveTarget = path.resolve(where, "package.json")
|
|
, things = Object.keys(tree).map(function (k) {
|
|
// if "what" was a url, then save that instead.
|
|
var t = tree[k]
|
|
, u = url.parse(t.from)
|
|
, w = t.what.split("@")
|
|
if (u && u.protocol) w[1] = t.from
|
|
return w
|
|
}).reduce(function (set, k) {
|
|
var rangeDescriptor = semver.valid(k[1]) &&
|
|
semver.gte(k[1], "0.1.0")
|
|
? "~" : ""
|
|
set[k[0]] = rangeDescriptor + k[1]
|
|
return set
|
|
}, {})
|
|
|
|
// don't use readJson, because we don't want to do all the other
|
|
// tricky npm-specific stuff that's in there.
|
|
fs.readFile(saveTarget, function (er, data) {
|
|
// ignore errors here, just don't save it.
|
|
try {
|
|
data = JSON.parse(data.toString("utf8"))
|
|
} catch (ex) {
|
|
er = ex
|
|
}
|
|
if (er) {
|
|
return cb(null, installed, tree, pretty)
|
|
|
|
}
|
|
|
|
var deps = npm.config.get("save-optional") ? "optionalDependencies"
|
|
: npm.config.get("save-dev") ? "devDependencies"
|
|
: "dependencies"
|
|
|
|
data[deps] = data[deps] || {}
|
|
Object.keys(things).forEach(function (t) {
|
|
data[deps][t] = things[t]
|
|
})
|
|
data = JSON.stringify(data, null, 2) + "\n"
|
|
fs.writeFile(saveTarget, data, function (er) {
|
|
cb(er, installed, tree, pretty)
|
|
})
|
|
})
|
|
}
|
|
|
|
|
|
// Outputting *all* the installed modules is a bit confusing,
|
|
// because the length of the path does not make it clear
|
|
// that the submodules are not immediately require()able.
|
|
// TODO: Show the complete tree, ls-style, but only if --long is provided
|
|
function prettify (tree, installed) {
|
|
if (npm.config.get("json")) {
|
|
function red (set, kv) {
|
|
set[kv[0]] = kv[1]
|
|
return set
|
|
}
|
|
|
|
tree = Object.keys(tree).map(function (p) {
|
|
if (!tree[p]) return null
|
|
var what = tree[p].what.split("@")
|
|
, name = what.shift()
|
|
, version = what.join("@")
|
|
, o = { name: name, version: version, from: tree[p].from }
|
|
o.dependencies = tree[p].children.map(function P (dep) {
|
|
var what = dep.what.split("@")
|
|
, name = what.shift()
|
|
, version = what.join("@")
|
|
, o = { version: version, from: dep.from }
|
|
o.dependencies = dep.children.map(P).reduce(red, {})
|
|
return [name, o]
|
|
}).reduce(red, {})
|
|
return o
|
|
})
|
|
|
|
return JSON.stringify(tree, null, 2)
|
|
}
|
|
if (npm.config.get("parseable")) return parseable(installed)
|
|
|
|
return Object.keys(tree).map(function (p) {
|
|
return archy({ label: tree[p].what + " " + p
|
|
, nodes: (tree[p].children || []).map(function P (c) {
|
|
if (npm.config.get("long")) {
|
|
return { label: c.what, nodes: c.children.map(P) }
|
|
}
|
|
var g = c.children.map(function (g) {
|
|
return g.what
|
|
}).join(", ")
|
|
if (g) g = " (" + g + ")"
|
|
return c.what + g
|
|
})
|
|
})
|
|
}).join("\n")
|
|
}
|
|
|
|
function parseable (installed) {
|
|
var long = npm.config.get("long")
|
|
, cwd = process.cwd()
|
|
return installed.map(function (item) {
|
|
return path.resolve(cwd, item[1]) +
|
|
( long ? ":" + item[0] : "" )
|
|
}).join("\n")
|
|
}
|
|
|
|
function treeify (installed) {
|
|
// each item is [what, where, parent, parentDir]
|
|
// If no parent, then report it.
|
|
// otherwise, tack it into the parent's children list.
|
|
// If the parent isn't a top-level then ignore it.
|
|
var whatWhere = installed.reduce(function (l, r) {
|
|
var parentDir = r[3]
|
|
, parent = r[2]
|
|
, where = r[1]
|
|
, what = r[0]
|
|
, from = r[4]
|
|
l[where] = { parentDir: parentDir
|
|
, parent: parent
|
|
, children: []
|
|
, where: where
|
|
, what: what
|
|
, from: from }
|
|
return l
|
|
}, {})
|
|
|
|
//log.warn(whatWhere, "whatWhere")
|
|
return Object.keys(whatWhere).reduce(function (l, r) {
|
|
var ww = whatWhere[r]
|
|
//log.warn([r, ww], "r, ww")
|
|
if (!ww.parent) {
|
|
l[r] = ww
|
|
} else {
|
|
var p = whatWhere[ww.parentDir]
|
|
if (p) p.children.push(ww)
|
|
else l[r] = ww
|
|
}
|
|
return l
|
|
}, {})
|
|
}
|
|
|
|
|
|
// just like installMany, but also add the existing packages in
|
|
// where/node_modules to the family object.
|
|
function installManyTop (what, where, context, cb_) {
|
|
function cb (er, d) {
|
|
if (context.explicit || er) return cb_(er, d)
|
|
// since this wasn't an explicit install, let's build the top
|
|
// folder, so that `npm install` also runs the lifecycle scripts.
|
|
npm.commands.build([where], false, true, function (er) {
|
|
return cb_(er, d)
|
|
})
|
|
}
|
|
|
|
if (context.explicit) return next()
|
|
|
|
readJson(path.join(where, "package.json"), function (er, data) {
|
|
if (er) return next(er)
|
|
lifecycle(data, "preinstall", where, next)
|
|
})
|
|
|
|
function next (er) {
|
|
if (er) return cb(er)
|
|
installManyTop_(what, where, context, cb)
|
|
}
|
|
}
|
|
|
|
function installManyTop_ (what, where, context, cb) {
|
|
var nm = path.resolve(where, "node_modules")
|
|
, names = context.explicit
|
|
? what.map(function (w) { return w.split(/@/).shift() })
|
|
: []
|
|
|
|
fs.readdir(nm, function (er, pkgs) {
|
|
if (er) return installMany(what, where, context, cb)
|
|
pkgs = pkgs.filter(function (p) {
|
|
return !p.match(/^[\._-]/)
|
|
})
|
|
asyncMap(pkgs.map(function (p) {
|
|
return path.resolve(nm, p, "package.json")
|
|
}), function (jsonfile, cb) {
|
|
readJson(jsonfile, function (er, data) {
|
|
if (er) return cb(null, [])
|
|
return cb(null, [[data.name, data.version]])
|
|
})
|
|
}, function (er, packages) {
|
|
// add all the existing packages to the family list.
|
|
// however, do not add to the ancestors list.
|
|
packages.forEach(function (p) {
|
|
context.family[p[0]] = p[1]
|
|
})
|
|
return installMany(what, where, context, cb)
|
|
})
|
|
})
|
|
}
|
|
|
|
function installMany (what, where, context, cb) {
|
|
// readDependencies takes care of figuring out whether the list of
|
|
// dependencies we'll iterate below comes from an existing shrinkwrap from a
|
|
// parent level, a new shrinkwrap at this level, or package.json at this
|
|
// level, as well as which shrinkwrap (if any) our dependencies should use.
|
|
readDependencies(context, where, {}, function (er, data, wrap) {
|
|
if (er) data = {}
|
|
|
|
var parent = data
|
|
|
|
var d = data.dependencies || {}
|
|
|
|
// if we're explicitly installing "what" into "where", then the shrinkwrap
|
|
// for "where" doesn't apply. This would be the case if someone were adding
|
|
// a new package to a shrinkwrapped package. (data.dependencies will not be
|
|
// used here except to indicate what packages are already present, so
|
|
// there's no harm in using that.)
|
|
if (context.explicit) wrap = null
|
|
|
|
// what is a list of things.
|
|
// resolve each one.
|
|
asyncMap( what
|
|
, targetResolver(where, context, d)
|
|
, function (er, targets) {
|
|
|
|
if (er) return cb(er)
|
|
|
|
// each target will be a data object corresponding
|
|
// to a package, folder, or whatever that is in the cache now.
|
|
var newPrev = Object.create(context.family)
|
|
, newAnc = Object.create(context.ancestors)
|
|
|
|
newAnc[data.name] = data.version
|
|
targets.forEach(function (t) {
|
|
newPrev[t.name] = t.version
|
|
})
|
|
log.silly(targets, "resolved")
|
|
targets.filter(function (t) { return t }).forEach(function (t) {
|
|
log.info(t._id, "into "+where)
|
|
})
|
|
asyncMap(targets, function (target, cb) {
|
|
log.info(target._id, "installOne")
|
|
var newWrap = wrap ? wrap[target.name].dependencies || {} : null
|
|
var newContext = { family: newPrev
|
|
, ancestors: newAnc
|
|
, parent: parent
|
|
, explicit: false
|
|
, wrap: newWrap }
|
|
installOne(target, where, newContext, cb)
|
|
}, cb)
|
|
})
|
|
})
|
|
}
|
|
|
|
function targetResolver (where, context, deps) {
|
|
var alreadyInstalledManually = context.explicit ? [] : null
|
|
, nm = path.resolve(where, "node_modules")
|
|
, parent = context.parent
|
|
, wrap = context.wrap
|
|
|
|
if (!context.explicit) fs.readdir(nm, function (er, inst) {
|
|
if (er) return alreadyInstalledManually = []
|
|
asyncMap(inst, function (pkg, cb) {
|
|
readJson(path.resolve(nm, pkg, "package.json"), function (er, d) {
|
|
// error means it's not a package, most likely.
|
|
if (er) return cb(null, [])
|
|
|
|
// if it's a bundled dep, then assume that anything there is valid.
|
|
// otherwise, make sure that it's a semver match with what we want.
|
|
var bd = parent.bundleDependencies
|
|
if (bd && bd.indexOf(d.name) !== -1 ||
|
|
semver.satisfies(d.version, deps[d.name] || "*")) {
|
|
return cb(null, d.name)
|
|
}
|
|
|
|
// something is there, but it's not satisfactory. Clobber it.
|
|
return cb(null, [])
|
|
})
|
|
}, function (er, inst) {
|
|
// this is the list of things that are valid and should be ignored.
|
|
alreadyInstalledManually = inst
|
|
})
|
|
})
|
|
|
|
var to = 0
|
|
return function resolver (what, cb) {
|
|
if (!alreadyInstalledManually) return setTimeout(function () {
|
|
resolver(what, cb)
|
|
}, to++)
|
|
|
|
// now we know what's been installed here manually,
|
|
// or tampered with in some way that npm doesn't want to overwrite.
|
|
if (alreadyInstalledManually.indexOf(what.split("@").shift()) !== -1) {
|
|
log.verbose("skipping "+what, "already installed in "+where)
|
|
return cb(null, [])
|
|
}
|
|
|
|
// check for a version installed higher in the tree.
|
|
// If installing from a shrinkwrap, it must match exactly.
|
|
if (context.family[what]) {
|
|
if (wrap && wrap[what].version === context.family[what]) {
|
|
log.verbose(what, "using existing (matches shrinkwrap)")
|
|
return cb(null, [])
|
|
}
|
|
}
|
|
|
|
// if it's identical to its parent, then it's probably someone
|
|
// doing `npm install foo` inside of the foo project. Print
|
|
// a warning, and skip it.
|
|
if (parent && parent.name === what && !npm.config.get("force")) {
|
|
log.warn("Refusing to install "+what+" as a dependency of itself"
|
|
,"install")
|
|
return cb(null, [])
|
|
}
|
|
|
|
if (wrap) {
|
|
name = what.split(/@/).shift()
|
|
if (wrap[name]) {
|
|
var wrapTarget = wrap[name].from || wrap[name].version
|
|
log.verbose("resolving "+what+" to "+wrapTarget, "shrinkwrap")
|
|
what = name + "@" + wrapTarget
|
|
} else {
|
|
log.verbose("skipping "+what+" (not in shrinkwrap)", "shrinkwrap")
|
|
}
|
|
} else if (deps[what]) {
|
|
what = what + "@" + deps[what]
|
|
}
|
|
|
|
cache.add(what, function (er, data) {
|
|
if (er && parent && parent.optionalDependencies &&
|
|
parent.optionalDependencies.hasOwnProperty(what.split("@")[0])) {
|
|
log.warn(what, "optional dependency failed, continuing")
|
|
log.verbose([what, er], "optional dependency failed, continuing")
|
|
return cb(null, [])
|
|
}
|
|
|
|
if (!er &&
|
|
data &&
|
|
!context.explicit &&
|
|
context.family[data.name] === data.version &&
|
|
!npm.config.get("force")) {
|
|
log.info(data.name + "@" + data.version, "already installed")
|
|
return cb(null, [])
|
|
}
|
|
|
|
if (data) data._from = what
|
|
|
|
return cb(er, data)
|
|
})
|
|
}
|
|
}
|
|
|
|
// we've already decided to install this. if anything's in the way,
|
|
// then uninstall it first.
|
|
function installOne (target, where, context, cb) {
|
|
// the --link flag makes this a "link" command if it's at the
|
|
// the top level.
|
|
if (where === npm.prefix && npm.config.get("link")
|
|
&& !npm.config.get("global")) {
|
|
return localLink(target, where, context, cb)
|
|
}
|
|
installOne_(target, where, context, function (er, installedWhat) {
|
|
|
|
// check if this one is optional to its parent.
|
|
if (er && context.parent && context.parent.optionalDependencies &&
|
|
context.parent.optionalDependencies.hasOwnProperty(target.name)) {
|
|
log.warn(target._id, "optional dependency failed, continuing")
|
|
log.verbose([target._id, er], "optional dependency failed, continuing")
|
|
er = null
|
|
}
|
|
|
|
cb(er, installedWhat)
|
|
})
|
|
|
|
}
|
|
|
|
function localLink (target, where, context, cb) {
|
|
log.verbose(target._id, "try to link")
|
|
var jsonFile = path.resolve( npm.dir, target.name
|
|
, "package.json" )
|
|
, parent = context.parent
|
|
|
|
readJson(jsonFile, function (er, data) {
|
|
if (er || data._id === target._id) {
|
|
if (er) {
|
|
install( path.resolve(npm.globalDir, "..")
|
|
, target._id
|
|
, function (er) {
|
|
if (er) return cb(er, [])
|
|
thenLink()
|
|
})
|
|
} else thenLink()
|
|
|
|
function thenLink () {
|
|
npm.commands.link([target.name], function (er, d) {
|
|
log.silly([er, d], "back from link")
|
|
cb(er, [resultList(target, where, parent && parent._id)])
|
|
})
|
|
}
|
|
|
|
} else {
|
|
log.verbose(target._id, "install locally (no link)")
|
|
installOne_(target, where, context, cb)
|
|
}
|
|
})
|
|
}
|
|
|
|
function resultList (target, where, parentId) {
|
|
var nm = path.resolve(where, "node_modules")
|
|
, targetFolder = path.resolve(nm, target.name)
|
|
, prettyWhere = relativize(where, process.cwd() + "/x")
|
|
|
|
if (prettyWhere === ".") prettyWhere = null
|
|
|
|
if (!npm.config.get("global")) {
|
|
// print out the folder relative to where we are right now.
|
|
// relativize isn't really made for dirs, so you need this hack
|
|
targetFolder = relativize(targetFolder, process.cwd()+"/x")
|
|
}
|
|
|
|
return [ target._id
|
|
, targetFolder
|
|
, prettyWhere && parentId
|
|
, parentId && prettyWhere
|
|
, target._from ]
|
|
}
|
|
|
|
function installOne_ (target, where, context, cb) {
|
|
var nm = path.resolve(where, "node_modules")
|
|
, targetFolder = path.resolve(nm, target.name)
|
|
, prettyWhere = relativize(where, process.cwd() + "/x")
|
|
, parent = context.parent
|
|
|
|
if (prettyWhere === ".") prettyWhere = null
|
|
|
|
chain
|
|
( [ [checkEngine, target]
|
|
, [checkPlatform, target]
|
|
, [checkCycle, target, context.ancestors]
|
|
, [checkGit, targetFolder]
|
|
, [write, target, targetFolder, context] ]
|
|
, function (er, d) {
|
|
if (er) return cb(er)
|
|
d.push(resultList(target, where, parent && parent._id))
|
|
cb(er, d)
|
|
}
|
|
)
|
|
}
|
|
|
|
function checkEngine (target, cb) {
|
|
var npmv = npm.version
|
|
, force = npm.config.get("force")
|
|
, nodev = force ? null : npm.config.get("node-version")
|
|
, eng = target.engines
|
|
if (!eng) return cb()
|
|
if (nodev && eng.node && !semver.satisfies(nodev, eng.node)
|
|
|| eng.npm && !semver.satisfies(npmv, eng.npm)) {
|
|
var er = new Error("Unsupported")
|
|
er.errno = npm.ENOTSUP
|
|
er.required = eng
|
|
er.pkgid = target._id
|
|
return cb(er)
|
|
}
|
|
return cb()
|
|
}
|
|
|
|
function checkPlatform (target, cb) {
|
|
var platform = process.platform
|
|
, arch = process.arch
|
|
, osOk = true
|
|
, cpuOk = true
|
|
, force = npm.config.get("force")
|
|
|
|
if (force) {
|
|
return cb()
|
|
}
|
|
|
|
if (target.os) {
|
|
osOk = checkList(platform, target.os)
|
|
}
|
|
if (target.cpu) {
|
|
cpuOk = checkList(arch, target.cpu)
|
|
}
|
|
if (!osOk || !cpuOk) {
|
|
var er = new Error("Unsupported")
|
|
er.errno = npm.EBADPLATFORM
|
|
er.os = target.os || ['any']
|
|
er.cpu = target.cpu || ['any']
|
|
er.pkgid = target._id
|
|
return cb(er)
|
|
}
|
|
return cb()
|
|
}
|
|
|
|
function checkList (value, list) {
|
|
var tmp
|
|
, match = false
|
|
, blc = 0
|
|
if (typeof list === "string") {
|
|
list = [list]
|
|
}
|
|
if (list.length === 1 && list[0] === "any") {
|
|
return true;
|
|
}
|
|
for (var i = 0; i < list.length; ++i) {
|
|
tmp = list[i]
|
|
if (tmp[0] === '!') {
|
|
tmp = tmp.slice(1)
|
|
if (tmp === value) {
|
|
return false;
|
|
}
|
|
++blc
|
|
} else {
|
|
match = match || tmp === value
|
|
}
|
|
}
|
|
return match || blc === list.length
|
|
}
|
|
|
|
function checkCycle (target, ancestors, cb) {
|
|
// there are some very rare and pathological edge-cases where
|
|
// a cycle can cause npm to try to install a never-ending tree
|
|
// of stuff.
|
|
// Simplest:
|
|
//
|
|
// A -> B -> A' -> B' -> A -> B -> A' -> B' -> A -> ...
|
|
//
|
|
// Solution: Simply flat-out refuse to install any name@version
|
|
// that is already in the prototype tree of the ancestors object.
|
|
// A more correct, but more complex, solution would be to symlink
|
|
// the deeper thing into the new location.
|
|
// Will do that if anyone whines about this irl.
|
|
//
|
|
// Note: `npm install foo` inside of the `foo` package will abort
|
|
// earlier if `--force` is not set. However, if it IS set, then
|
|
// we need to still fail here, but just skip the first level. Of
|
|
// course, it'll still fail eventually if it's a true cycle, and
|
|
// leave things in an undefined state, but that's what is to be
|
|
// expected when `--force` is used. That is why getPrototypeOf
|
|
// is used *twice* here: to skip the first level of repetition.
|
|
|
|
var p = Object.getPrototypeOf(Object.getPrototypeOf(ancestors))
|
|
, name = target.name
|
|
, version = target.version
|
|
while (p && p !== Object.prototype && p[name] !== version) {
|
|
p = Object.getPrototypeOf(p)
|
|
}
|
|
if (p[name] !== version) return cb()
|
|
|
|
var er = new Error("Unresolvable cycle detected")
|
|
var tree = [target._id, JSON.parse(JSON.stringify(ancestors))]
|
|
, t = Object.getPrototypeOf(ancestors)
|
|
while (t && t !== Object.prototype) {
|
|
if (t === p) t.THIS_IS_P = true
|
|
tree.push(JSON.parse(JSON.stringify(t)))
|
|
t = Object.getPrototypeOf(t)
|
|
}
|
|
log.verbose(tree, "unresolvable dependency tree")
|
|
er.pkgid = target._id
|
|
er.errno = npm.ECYCLE
|
|
return cb(er)
|
|
}
|
|
|
|
function checkGit (folder, cb) {
|
|
// if it's a git repo then don't touch it!
|
|
fs.lstat(folder, function (er, s) {
|
|
if (er || !s.isDirectory()) return cb()
|
|
else checkGit_(folder, cb)
|
|
})
|
|
}
|
|
|
|
function checkGit_ (folder, cb) {
|
|
fs.stat(path.resolve(folder, ".git"), function (er, s) {
|
|
if (!er && s.isDirectory()) {
|
|
var e = new Error("Appears to be a git repo or submodule.")
|
|
e.path = folder
|
|
e.errno = npm.EISGIT
|
|
return cb(e)
|
|
}
|
|
cb()
|
|
})
|
|
}
|
|
|
|
function write (target, targetFolder, context, cb_) {
|
|
var up = npm.config.get("unsafe-perm")
|
|
, user = up ? null : npm.config.get("user")
|
|
, group = up ? null : npm.config.get("group")
|
|
, family = context.family
|
|
|
|
function cb (er, data) {
|
|
// cache.unpack returns the data object, and all we care about
|
|
// is the list of installed packages from that last thing.
|
|
if (!er) return cb_(er, data)
|
|
|
|
if (false === npm.config.get("rollback")) return cb_(er)
|
|
npm.commands.unbuild([targetFolder], function (er2) {
|
|
if (er2) log.error(er2, "error rolling back "+target._id)
|
|
return cb_(er, data)
|
|
})
|
|
}
|
|
|
|
var bundled = []
|
|
|
|
chain
|
|
( [ [ cache.unpack, target.name, target.version, targetFolder
|
|
, null, null, user, group ]
|
|
, [ fs, "writeFile"
|
|
, path.resolve(targetFolder, "package.json")
|
|
, JSON.stringify(target, null, 2) + "\n" ]
|
|
, [ lifecycle, target, "preinstall", targetFolder ]
|
|
, function (cb) {
|
|
if (!target.bundleDependencies) return cb()
|
|
|
|
var bd = path.resolve(targetFolder, "node_modules")
|
|
fs.readdir(bd, function (er, b) {
|
|
// nothing bundled, maybe
|
|
if (er) return cb()
|
|
bundled = b || []
|
|
cb()
|
|
})
|
|
} ]
|
|
|
|
// nest the chain so that we can throw away the results returned
|
|
// up until this point, since we really don't care about it.
|
|
, function X (er) {
|
|
if (er) return cb(er)
|
|
|
|
// before continuing to installing dependencies, check for a shrinkwrap.
|
|
readDependencies(context, targetFolder, {}, function (er, data, wrap) {
|
|
var deps = Object.keys(data.dependencies || {})
|
|
|
|
// don't install bundleDependencies, unless they're missing.
|
|
if (data.bundleDependencies) {
|
|
deps = deps.filter(function (d) {
|
|
return data.bundleDependencies.indexOf(d) === -1 ||
|
|
bundled.indexOf(d) === -1
|
|
})
|
|
}
|
|
|
|
var newcontext = { family: family
|
|
, ancestors: context.ancestors
|
|
, parent: target
|
|
, explicit: false
|
|
, wrap: wrap }
|
|
installMany(deps.filter(function (d) {
|
|
// prefer to not install things that are satisfied by
|
|
// something in the "family" list, unless we're installing
|
|
// from a shrinkwrap.
|
|
return wrap || !semver.satisfies(family[d], data.dependencies[d])
|
|
}).map(function (d) {
|
|
var t = data.dependencies[d]
|
|
, parsed = url.parse(t.replace(/^git\+/, "git"))
|
|
t = d + "@" + t
|
|
return t
|
|
}), targetFolder, newcontext, function (er, d) {
|
|
log.verbose(targetFolder, "about to build")
|
|
if (er) return cb(er)
|
|
npm.commands.build( [targetFolder]
|
|
, npm.config.get("global")
|
|
, true
|
|
, function (er) { return cb(er, d) })
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|