| // 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" |
| + "\nnpm install <pkg>" |
| + "\nnpm install <pkg>@<tag>" |
| + "\nnpm install <pkg>@<version>" |
| + "\nnpm install <pkg>@<version range>" |
| + "\nnpm install <folder>" |
| + "\nnpm install <tarball file>" |
| + "\nnpm install <tarball url>" |
| + "\nnpm install <git:// url>" |
| + "\nnpm install <github username>/<github project>" |
| + "\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 |
| if (/^https?:\/\//.test(opts.partialWord)) { |
| // do not complete to URLs |
| return cb(null, []) |
| } |
| |
| if (/\//.test(opts.partialWord)) { |
| // Complete fully to folder if there is exactly one match and it |
| // is a folder containing a package.json file. If that is not the |
| // case we return 0 matches, which will trigger the default bash |
| // complete. |
| var lastSlashIdx = opts.partialWord.lastIndexOf("/") |
| var partialName = opts.partialWord.slice(lastSlashIdx + 1) |
| var partialPath = opts.partialWord.slice(0, lastSlashIdx) |
| if (partialPath === "") partialPath = "/" |
| |
| function annotatePackageDirMatch (sibling, cb) { |
| var fullPath = path.join(partialPath, sibling) |
| if (sibling.slice(0, partialName.length) !== partialName) { |
| return cb(null, null) // not name match |
| } |
| fs.readdir(fullPath, function (err, contents) { |
| if (err) return cb(null, { isPackage: false }) |
| |
| cb( |
| null, |
| { |
| fullPath: fullPath, |
| isPackage: contents.indexOf("package.json") !== -1 |
| } |
| ) |
| }) |
| } |
| |
| return fs.readdir(partialPath, function (err, siblings) { |
| if (err) return cb(null, []) // invalid dir: no matching |
| |
| asyncMap(siblings, annotatePackageDirMatch, function (err, matches) { |
| if (err) return cb(err) |
| |
| var cleaned = matches.filter(function (x) { return x !== null }) |
| if (cleaned.length !== 1) return cb(null, []) |
| if (!cleaned[0].isPackage) return cb(null, []) |
| |
| // Success - only one match and it is a package dir |
| return cb(null, [cleaned[0].fullPath]) |
| }) |
| }) |
| } |
| |
| // FIXME: there used to be registry completion here, but it stopped making |
| // sense somewhere around 50,000 packages on the registry |
| cb() |
| } |
| |
| var npm = require("./npm.js") |
| , semver = require("semver") |
| , readJson = require("read-package-json") |
| , readInstalled = require("read-installed") |
| , log = require("npmlog") |
| , path = require("path") |
| , fs = require("graceful-fs") |
| , writeFileAtomic = require("write-file-atomic") |
| , cache = require("./cache.js") |
| , asyncMap = require("slide").asyncMap |
| , chain = require("slide").chain |
| , url = require("url") |
| , mkdir = require("mkdirp") |
| , lifecycle = require("./utils/lifecycle.js") |
| , archy = require("archy") |
| , npmInstallChecks = require("npm-install-checks") |
| , sortedObject = require("sorted-object") |
| , mapToRegistry = require("./utils/map-to-registry.js") |
| , npa = require("npm-package-arg") |
| , inflight = require("inflight") |
| , locker = require("./utils/locker.js") |
| , lock = locker.lock |
| , unlock = locker.unlock |
| , warnStrict = require("./utils/warn-deprecated.js")("engineStrict") |
| , warnPeers = require("./utils/warn-deprecated.js")("peerDependencies") |
| |
| function install (args, cb_) { |
| var hasArguments = !!args.length |
| |
| function cb (er, installed) { |
| if (er) return cb_(er) |
| |
| validateInstall(where, function (er, problem) { |
| if (er) return cb_(er) |
| |
| if (problem) { |
| var peerInvalidError = new Error("The package " + problem._id + |
| " does not satisfy its siblings' peerDependencies requirements!") |
| peerInvalidError.code = "EPEERINVALID" |
| peerInvalidError.packageName = problem.name |
| peerInvalidError.packageVersion = problem.version |
| peerInvalidError.peersDepending = problem.peersDepending |
| return cb(peerInvalidError) |
| } |
| |
| var tree = treeify(installed || []) |
| , pretty = prettify(tree, installed).trim() |
| |
| if (pretty) console.log(pretty) |
| save(where, installed, tree, pretty, hasArguments, 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("install", "where, what", [where, args]) |
| } |
| |
| if (!npm.config.get("global")) { |
| args = args.filter(function (a) { |
| return path.resolve(a) !== where |
| }) |
| } |
| |
| mkdir(where, function (er) { |
| if (er) return cb(er) |
| // install dependencies locally by default, |
| // or install current folder globally |
| if (!args.length) { |
| var opt = { dev: npm.config.get("dev") || !npm.config.get("production") } |
| |
| if (npm.config.get("global")) args = ["."] |
| else return readDependencies(null, where, opt, function (er, data) { |
| if (er) { |
| log.error("install", "Couldn't read dependencies") |
| return cb(er) |
| } |
| var deps = Object.keys(data.dependencies || {}) |
| log.verbose("install", "where, deps", [where, deps]) |
| |
| // FIXME: Install peerDependencies as direct dependencies, but only at |
| // the top level. Should only last until peerDependencies are nerfed to |
| // no longer implicitly install themselves. |
| var peers = [] |
| Object.keys(data.peerDependencies || {}).forEach(function (dep) { |
| if (!data.dependencies[dep]) { |
| log.verbose( |
| "install", |
| "peerDependency", dep, "wasn't going to be installed; adding" |
| ) |
| warnPeers([ |
| "The peer dependency "+dep+" included from "+data.name+" will no", |
| "longer be automatically installed to fulfill the peerDependency ", |
| "in npm 3+. Your application will need to depend on it explicitly." |
| ], dep+","+data.name) |
| peers.push(dep) |
| } |
| }) |
| log.verbose("install", "where, peers", [where, peers]) |
| |
| var context = { family: {} |
| , ancestors: {} |
| , explicit: false |
| , parent: data |
| , root: true |
| , wrap: null } |
| |
| if (data.name === path.basename(where) && |
| path.basename(path.dirname(where)) === "node_modules") { |
| // Only include in ancestry if it can actually be required. |
| // Otherwise, it does not count. |
| context.family[data.name] = |
| context.ancestors[data.name] = data.version |
| } |
| |
| installManyTop(deps.map(function (dep) { |
| var target = data.dependencies[dep] |
| return dep + "@" + target |
| }).concat(peers.map(function (dep) { |
| var target = data.peerDependencies[dep] |
| return dep + "@" + target |
| })), where, context, function(er, results) { |
| if (er || npm.config.get("production")) return cb(er, results) |
| lifecycle(data, "prepublish", where, function(er) { |
| return cb(er, results) |
| }) |
| }) |
| }) |
| } |
| |
| // initial "family" is the name:version of the root, if it's got |
| // a package.json file. |
| var jsonPath = path.resolve(where, "package.json") |
| log.verbose('install', 'initial load of', jsonPath) |
| readJson(jsonPath, log.warn, function (er, data) { |
| if (er |
| && er.code !== "ENOENT" |
| && er.code !== "ENOTDIR") return cb(er) |
| if (er) data = null |
| var context = { family: {} |
| , ancestors: {} |
| , explicit: true |
| , parent: data |
| , root: true |
| , wrap: null } |
| if (data && data.name === path.basename(where) && |
| path.basename(path.dirname(where)) === "node_modules") { |
| context.family[data.name] = context.ancestors[data.name] = data.version |
| } |
| var fn = npm.config.get("global") ? installMany : installManyTop |
| fn(args, where, context, cb) |
| }) |
| }) |
| } |
| |
| function validateInstall (where, cb) { |
| var jsonPath = path.resolve(where, 'package.json') |
| log.verbose('validateInstall', 'loading', jsonPath, 'for validation') |
| readJson(jsonPath, log.warn, function (er, data) { |
| if (er |
| && er.code !== 'ENOENT' |
| && er.code !== 'ENOTDIR') return cb(er) |
| |
| if (data && data.engineStrict) { |
| warnStrict([ |
| "Per-package engineStrict (found in this package's package.json) ", |
| "won't be used in npm 3+. Use the config setting `engine-strict` instead." |
| ], data.name) |
| } |
| |
| readInstalled(where, { log: log.warn, dev: true }, function (er, data) { |
| if (er) return cb(er) |
| |
| cb(null, findPeerInvalid_(data.dependencies, [])) |
| }) |
| }) |
| } |
| |
| function findPeerInvalid_ (packageMap, fpiList) { |
| if (fpiList.indexOf(packageMap) !== -1) |
| return undefined |
| |
| fpiList.push(packageMap) |
| |
| for (var packageName in packageMap) { |
| var pkg = packageMap[packageName] |
| |
| if (pkg.peerInvalid) { |
| var peersDepending = {} |
| for (var peerName in packageMap) { |
| var peer = packageMap[peerName] |
| if (peer.peerDependencies && peer.peerDependencies[packageName]) { |
| peersDepending[peer.name + "@" + peer.version] = |
| peer.peerDependencies[packageName] |
| } |
| } |
| return { name: pkg.name, peersDepending: peersDepending, version: pkg.version, _id: pkg._id } |
| } |
| |
| if (pkg.dependencies) { |
| var invalid = findPeerInvalid_(pkg.dependencies, fpiList) |
| if (invalid) |
| return invalid |
| } |
| } |
| |
| return null |
| } |
| |
| // 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 |
| |
| var jsonPath = path.resolve(where, 'package.json') |
| log.verbose('readDependencies', 'loading dependencies from', jsonPath) |
| readJson(jsonPath, log.warn, function (er, data) { |
| if (er && er.code === "ENOENT") er.code = "ENOPACKAGEJSON" |
| if (er) return cb(er) |
| |
| if (opts && opts.dev) { |
| if (!data.dependencies) data.dependencies = {} |
| Object.keys(data.devDependencies || {}).forEach(function (k) { |
| if (data.dependencies[k]) { |
| log.warn("package.json", "Dependency '%s' exists in both dependencies " + |
| "and devDependencies, using '%s@%s' from dependencies", |
| k, k, data.dependencies[k]) |
| } else { |
| data.dependencies[k] = data.devDependencies[k] |
| } |
| }) |
| } |
| |
| if (!npm.config.get("optional") && data.optionalDependencies) { |
| Object.keys(data.optionalDependencies).forEach(function (d) { |
| delete data.dependencies[d] |
| }) |
| } |
| |
| // User has opted out of shrinkwraps entirely |
| if (npm.config.get("shrinkwrap") === false) |
| return cb(null, data, null) |
| |
| if (wrap) { |
| log.verbose("readDependencies: using existing wrap", [where, wrap]) |
| var rv = {} |
| Object.keys(data).forEach(function (key) { |
| rv[key] = data[key] |
| }) |
| rv.dependencies = {} |
| Object.keys(wrap).forEach(function (key) { |
| log.verbose("from wrap", [key, wrap[key]]) |
| rv.dependencies[key] = readWrap(wrap[key]) |
| }) |
| log.verbose("readDependencies returned deps", rv.dependencies) |
| return cb(null, rv, wrap) |
| } |
| |
| var wrapfile = path.resolve(where, "npm-shrinkwrap.json") |
| |
| fs.readFile(wrapfile, "utf8", function (er, wrapjson) { |
| if (er) return cb(null, data, null) |
| |
| log.verbose("readDependencies", "npm-shrinkwrap.json is overriding dependencies") |
| var newwrap |
| try { |
| newwrap = JSON.parse(wrapjson) |
| } catch (ex) { |
| return cb(ex) |
| } |
| |
| log.info("shrinkwrap", "file %j", wrapfile) |
| var rv = {} |
| Object.keys(data).forEach(function (key) { |
| rv[key] = data[key] |
| }) |
| rv.dependencies = {} |
| Object.keys(newwrap.dependencies || {}).forEach(function (key) { |
| rv.dependencies[key] = readWrap(newwrap.dependencies[key]) |
| }) |
| |
| // fold in devDependencies if not already present, at top level |
| if (opts && opts.dev) { |
| Object.keys(data.devDependencies || {}).forEach(function (k) { |
| rv.dependencies[k] = rv.dependencies[k] || data.devDependencies[k] |
| }) |
| } |
| |
| log.verbose("readDependencies returned deps", rv.dependencies) |
| return cb(null, rv, newwrap.dependencies) |
| }) |
| }) |
| } |
| |
| function readWrap (w) { |
| return (w.resolved) ? w.resolved |
| : (w.from && url.parse(w.from).protocol) ? w.from |
| : w.version |
| } |
| |
| // if the -S|--save option is specified, then write installed packages |
| // as dependencies to a package.json file. |
| function save (where, installed, tree, pretty, hasArguments, cb) { |
| if (!hasArguments || |
| !npm.config.get("save") && |
| !npm.config.get("save-dev") && |
| !npm.config.get("save-optional") || |
| npm.config.get("global")) { |
| return cb(null, installed, tree, pretty) |
| } |
| |
| var saveBundle = npm.config.get("save-bundle") |
| var savePrefix = npm.config.get("save-prefix") |
| |
| // 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") |
| |
| asyncMap(Object.keys(tree), function (k, cb) { |
| // if "from" is remote, git, or hosted, then save that instead. |
| var t = tree[k] |
| , f = npa(t.from) |
| , a = npa(t.what) |
| , w = [a.name, a.spec] |
| |
| |
| fs.stat(t.from, function (er){ |
| if (!er) { |
| w[1] = "file:" + t.from |
| } else if (['hosted', 'git', 'remote'].indexOf(f.type) !== -1) { |
| w[1] = t.from |
| } |
| cb(null, [w]) |
| }) |
| } |
| , function (er, arr) { |
| var things = arr.reduce(function (set, k) { |
| var rangeDescriptor = semver.valid(k[1], true) && |
| semver.gte(k[1], "0.1.0", true) && |
| !npm.config.get("save-exact") |
| ? savePrefix : "" |
| 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" |
| |
| if (saveBundle) { |
| var bundle = data.bundleDependencies || data.bundledDependencies |
| delete data.bundledDependencies |
| if (!Array.isArray(bundle)) bundle = [] |
| data.bundleDependencies = bundle.sort() |
| } |
| |
| log.verbose("save", "saving", things) |
| data[deps] = data[deps] || {} |
| Object.keys(things).forEach(function (t) { |
| data[deps][t] = things[t] |
| if (saveBundle) { |
| var i = bundle.indexOf(t) |
| if (i === -1) bundle.push(t) |
| data.bundleDependencies = bundle.sort() |
| } |
| }) |
| |
| data[deps] = sortedObject(data[deps]) |
| |
| log.silly("save", "writing", saveTarget) |
| data = JSON.stringify(data, null, 2) + "\n" |
| writeFileAtomic(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) { |
| function red (set, kv) { |
| set[kv[0]] = kv[1] |
| return set |
| } |
| |
| if (npm.config.get("json")) { |
| tree = Object.keys(tree).map(function (p) { |
| if (!tree[p]) return null |
| var what = npa(tree[p].what) |
| , name = what.name |
| , version = what.spec |
| , o = { name: name, version: version, from: tree[p].from } |
| o.dependencies = tree[p].children.map(function P (dep) { |
| var what = npa(dep.what) |
| , name = what.name |
| , version = what.spec |
| , 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 |
| }) |
| }, "", { unicode: npm.config.get("unicode") }) |
| }).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("install", 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() |
| |
| var jsonPath = path.join(where, 'package.json') |
| log.verbose('installManyTop', 'reading for lifecycle', jsonPath) |
| readJson(jsonPath, log.warn, 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") |
| |
| fs.readdir(nm, function (er, pkgs) { |
| if (er) return installMany(what, where, context, cb) |
| |
| var scopes = [], unscoped = [] |
| pkgs.filter(function (p) { |
| return !p.match(/^[\._-]/) |
| }).forEach(function (p) { |
| // @names deserve deeper investigation |
| if (p[0] === "@") { |
| scopes.push(p) |
| } |
| else { |
| unscoped.push(p) |
| } |
| }) |
| |
| maybeScoped(scopes, nm, function (er, scoped) { |
| if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er) |
| // recombine unscoped with @scope/package packages |
| asyncMap(unscoped.concat(scoped).map(function (p) { |
| return path.resolve(nm, p, "package.json") |
| }), function (jsonPath, cb) { |
| log.verbose('installManyTop', 'reading scoped package data from', jsonPath) |
| readJson(jsonPath, log.info, function (er, data) { |
| if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er) |
| if (er) return cb(null, []) |
| cb(null, [[data.name, data.version]]) |
| }) |
| }, function (er, packages) { |
| // if there's nothing in node_modules, then don't freak out. |
| if (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] |
| }) |
| installMany(what, where, context, cb) |
| }) |
| }) |
| }) |
| } |
| |
| function maybeScoped (scopes, where, cb) { |
| // find packages in scopes |
| asyncMap(scopes, function (scope, cb) { |
| fs.readdir(path.resolve(where, scope), function (er, scoped) { |
| if (er) return cb(er) |
| var paths = scoped.map(function (p) { |
| return path.join(scope, p) |
| }) |
| cb(null, paths) |
| }) |
| }, 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. |
| var opt = { dev: npm.config.get("dev") } |
| readDependencies(context, where, opt, function (er, data, wrap) { |
| if (er) data = {} |
| |
| var parent = data |
| |
| // 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 |
| |
| var deps = data.dependencies || {} |
| var devDeps = data.devDependencies || {} |
| |
| // what is a list of things. |
| // resolve each one. |
| asyncMap( what |
| , targetResolver(where, context, deps, devDeps) |
| , function (er, targets) { |
| |
| if (er) return cb(er) |
| |
| var bundled = data.bundleDependencies || data.bundledDependencies || [] |
| // only take the hit for readInstalled if there are probably bundled |
| // dependencies to read |
| if (bundled.length) { |
| readInstalled(where, { dev: true }, andBuildResolvedTree) |
| } else { |
| andBuildResolvedTree() |
| } |
| |
| function andBuildResolvedTree (er, current) { |
| 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) |
| |
| if (!context.root) { |
| newAnc[data.name] = data.version |
| } |
| bundled.forEach(function (bundle) { |
| var bundleData = current.dependencies[bundle] |
| if ((!bundleData || !bundleData.version) && current.devDependencies) { |
| log.verbose( |
| 'installMany', bundle, 'was bundled with', |
| data.name + '@' + data.version + |
| ", but wasn't found in dependencies. Trying devDependencies" |
| ) |
| bundleData = current.devDependencies[bundle] |
| } |
| |
| if (!bundleData || !bundleData.version) { |
| log.warn( |
| 'installMany', bundle, 'was bundled with', |
| data.name + '@' + data.version + |
| ", but bundled package wasn't found in unpacked tree" |
| ) |
| } else { |
| log.verbose( |
| 'installMany', bundle + '@' + bundleData.version, |
| 'was bundled with', data.name + '@' + data.version |
| ) |
| newPrev[bundle] = bundleData.version |
| } |
| }) |
| targets.forEach(function (t) { |
| newPrev[t.name] = t.version |
| }) |
| log.silly("install resolved", targets) |
| targets.filter(function (t) { return t }).forEach(function (t) { |
| log.info("install", "%s into %s", t._id, where) |
| }) |
| asyncMap(targets, function (target, cb) { |
| log.info("installOne", target._id) |
| var wrapData = wrap ? wrap[target.name] : null |
| var newWrap = wrapData && wrapData.dependencies |
| ? 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, devDeps) { |
| var alreadyInstalledManually = [] |
| , resolveLeft = 0 |
| , nm = path.resolve(where, "node_modules") |
| , parent = context.parent |
| , wrap = context.wrap |
| |
| if (!context.explicit) readdir(nm) |
| |
| function readdir(name) { |
| resolveLeft++ |
| fs.readdir(name, function (er, inst) { |
| if (er) return resolveLeft-- |
| |
| // don't even mess with non-package looking things |
| inst = inst.filter(function (p) { |
| if (!p.match(/^[@\._-]/)) return true |
| // scoped packages |
| readdir(path.join(name, p)) |
| }) |
| |
| asyncMap(inst, function (pkg, cb) { |
| var jsonPath = path.resolve(name, pkg, 'package.json') |
| log.verbose('targetResolver', 'reading package data from', jsonPath) |
| readJson(jsonPath, log.info, function (er, d) { |
| if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er) |
| // 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 |
| var isBundled = bd && bd.indexOf(d.name) !== -1 |
| var expectedVersion = deps[d.name] || (devDeps && devDeps[d.name]) || "*" |
| var currentIsSatisfactory = semver.satisfies(d.version, expectedVersion, true) |
| if (isBundled || currentIsSatisfactory || deps[d.name] === d._resolved) { |
| return cb(null, d.name) |
| } |
| |
| // see if the package had been previously linked |
| fs.lstat(path.resolve(nm, pkg), function(err, s) { |
| if (err) return cb(null, []) |
| if (s.isSymbolicLink()) { |
| 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 = alreadyInstalledManually.concat(inst) |
| resolveLeft-- |
| }) |
| }) |
| } |
| |
| var to = 0 |
| return function resolver (what, cb) { |
| if (resolveLeft) 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(npa(what).name) !== -1) { |
| log.verbose("already installed", "skipping %s %s", what, 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]) { |
| log.verbose('install', what, 'is installed as', context.family[what]) |
| if (wrap && wrap[what].version === context.family[what]) { |
| log.verbose("shrinkwrap", "use existing", what) |
| 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("install", "Refusing to install %s as a dependency of itself" |
| , what) |
| return cb(null, []) |
| } |
| |
| if (wrap) { |
| var name = npa(what).name |
| if (wrap[name]) { |
| var wrapTarget = readWrap(wrap[name]) |
| what = name + "@" + wrapTarget |
| } else { |
| log.verbose("shrinkwrap", "skipping %s (not in shrinkwrap)", what) |
| } |
| } else if (deps[what]) { |
| what = what + "@" + deps[what] |
| } |
| |
| // This is where we actually fetch the package, if it's not already |
| // in the cache. |
| // If it's a git repo, then we want to install it, even if the parent |
| // already has a matching copy. |
| // If it's not a git repo, and the parent already has that pkg, then |
| // we can skip installing it again. |
| var pkgroot = path.resolve(npm.prefix, (parent && parent._from) || "") |
| cache.add(what, null, pkgroot, false, function (er, data) { |
| if (er && parent && parent.optionalDependencies && |
| parent.optionalDependencies.hasOwnProperty(npa(what).name)) { |
| log.warn("optional dep failed, continuing", what) |
| log.verbose("optional dep failed, continuing", [what, er]) |
| return cb(null, []) |
| } |
| |
| var type = npa(what).type |
| var isGit = type === "git" || type === "hosted" |
| |
| if (!er && |
| data && |
| !context.explicit && |
| context.family[data.name] === data.version && |
| !npm.config.get("force") && |
| !isGit) { |
| log.info("already installed", data.name + "@" + data.version) |
| return cb(null, []) |
| } |
| |
| |
| if (data && !data._from) data._from = what |
| if (er && parent && parent.name) er.parent = parent.name |
| 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. |
| var isGit = false |
| var type = npa(target._from).type |
| if (target && target._from) isGit = type === 'git' || type === 'hosted' |
| |
| if (where === npm.prefix && npm.config.get("link") |
| && !npm.config.get("global") && !isGit) { |
| 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("optional dep failed, continuing", target._id) |
| log.verbose("optional dep failed, continuing", [target._id, er]) |
| er = null |
| } |
| |
| cb(er, installedWhat) |
| }) |
| |
| } |
| |
| function localLink (target, where, context, cb) { |
| log.verbose("localLink", target._id) |
| var jsonPath = path.resolve(npm.globalDir, target.name , 'package.json') |
| var parent = context.parent |
| |
| log.verbose('localLink', 'reading data to link', target.name, 'from', jsonPath) |
| readJson(jsonPath, log.warn, function (er, data) { |
| function thenLink () { |
| npm.commands.link([target.name], function (er, d) { |
| log.silly("localLink", "back from link", [er, d]) |
| cb(er, [resultList(target, where, parent && parent._id)]) |
| }) |
| } |
| |
| if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er) |
| 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() |
| } else { |
| log.verbose("localLink", "install locally (no link)", target._id) |
| installOne_(target, where, context, cb) |
| } |
| }) |
| } |
| |
| function resultList (target, where, parentId) { |
| var nm = path.resolve(where, "node_modules") |
| , targetFolder = path.resolve(nm, target.name) |
| , prettyWhere = where |
| |
| if (!npm.config.get("global")) { |
| prettyWhere = path.relative(process.cwd(), where) |
| } |
| |
| if (prettyWhere === ".") prettyWhere = null |
| |
| if (!npm.config.get("global")) { |
| // print out the folder relative to where we are right now. |
| targetFolder = path.relative(process.cwd(), targetFolder) |
| } |
| |
| return [ target._id |
| , targetFolder |
| , prettyWhere && parentId |
| , parentId && prettyWhere |
| , target._from ] |
| } |
| |
| var installed = Object.create(null) |
| |
| function installOne_ (target, where, context, cb_) { |
| var nm = path.resolve(where, "node_modules") |
| , targetFolder = path.resolve(nm, target.name) |
| , prettyWhere = path.relative(process.cwd(), where) |
| , parent = context.parent |
| |
| if (prettyWhere === ".") prettyWhere = null |
| |
| cb_ = inflight(target.name + ":" + where, cb_) |
| if (!cb_) { |
| return log.verbose("installOne", "of", target.name, "to", where, "already in flight; waiting") |
| } |
| else { |
| log.verbose("installOne", "of", target.name, "to", where, "not in flight; installing") |
| } |
| |
| function cb(er, data) { |
| unlock(nm, target.name, function () { cb_(er, data) }) |
| } |
| |
| lock(nm, target.name, function (er) { |
| if (er) return cb(er) |
| |
| if (targetFolder in installed) { |
| log.error("install", "trying to install", target.version, "to", targetFolder) |
| log.error("install", "but already installed versions", installed[targetFolder]) |
| installed[targetFolder].push(target.version) |
| } |
| else { |
| installed[targetFolder] = [target.version] |
| } |
| |
| var force = npm.config.get("force") |
| , nodeVersion = npm.config.get("node-version") |
| , strict = npm.config.get("engine-strict") |
| , c = npmInstallChecks |
| |
| chain( |
| [ [c.checkEngine, target, npm.version, nodeVersion, force, strict] |
| , [c.checkPlatform, target, force] |
| , [c.checkCycle, target, context.ancestors] |
| , [c.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 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 (npm.config.get("rollback") === false) return cb_(er) |
| npm.rollbacks.push(targetFolder) |
| cb_(er, data) |
| } |
| |
| var bundled = [] |
| |
| log.silly("install write", "writing", target.name, target.version, "to", targetFolder) |
| chain( |
| [ [ cache.unpack, target.name, target.version, targetFolder, null, null, user, group ], |
| function writePackageJSON (cb) { |
| var jsonPath = path.resolve(targetFolder, 'package.json') |
| log.verbose('write', 'writing to', jsonPath) |
| writeFileAtomic(jsonPath, JSON.stringify(target, null, 2) + '\n', cb) |
| }, |
| [ lifecycle, target, "preinstall", targetFolder ], |
| function collectBundled (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. |
| var opt = { dev: npm.config.get("dev") } |
| readDependencies(context, targetFolder, opt, function (er, data, wrap) { |
| if (er) return cb(er); |
| var deps = prepareForInstallMany(data, "dependencies", bundled, wrap, |
| family) |
| var depsTargetFolder = targetFolder |
| var depsContext = { family: family |
| , ancestors: context.ancestors |
| , parent: target |
| , explicit: false |
| , wrap: wrap } |
| |
| var actions = |
| [ [ installManyAndBuild, deps, depsTargetFolder, depsContext ] ] |
| |
| // FIXME: This is an accident waiting to happen! |
| // |
| // 1. If multiple children at the same level of the tree share a |
| // peerDependency that's not in the parent's dependencies, because |
| // the peerDeps don't get added to the family, they will keep |
| // getting reinstalled (worked around by inflighting installOne). |
| // 2. The installer can't safely build at the parent level because |
| // that's already being done by the parent's installAndBuild. This |
| // runs the risk of the peerDependency never getting built. |
| // |
| // The fix: Don't install peerDependencies; require them to be |
| // included as explicit dependencies / devDependencies, and warn |
| // or error when they're missing. See #5080 for more arguments in |
| // favor of killing implicit peerDependency installs with fire. |
| var peerDeps = prepareForInstallMany(data, "peerDependencies", bundled, |
| wrap, family) |
| peerDeps.forEach(function (pd) { |
| warnPeers([ |
| "The peer dependency "+pd+" included from "+data.name+" will no", |
| "longer be automatically installed to fulfill the peerDependency ", |
| "in npm 3+. Your application will need to depend on it explicitly." |
| ], pd+","+data.name) |
| }) |
| |
| // Package scopes cause an addditional tree level which needs to be |
| // considered when resolving a peerDependency's target folder. |
| var pdTargetFolder |
| if (npa(target.name).scope) { |
| pdTargetFolder = path.resolve(targetFolder, '../../..') |
| } else { |
| pdTargetFolder = path.resolve(targetFolder, '../..') |
| } |
| |
| var pdContext = context |
| if (peerDeps.length > 0) { |
| actions.push( |
| [ installMany, peerDeps, pdTargetFolder, pdContext ] |
| ) |
| } |
| |
| chain(actions, cb) |
| }) |
| }) |
| } |
| |
| function installManyAndBuild (deps, targetFolder, context, cb) { |
| installMany(deps, targetFolder, context, function (er, d) { |
| log.verbose("about to build", targetFolder) |
| if (er) return cb(er) |
| npm.commands.build( [targetFolder] |
| , npm.config.get("global") |
| , true |
| , function (er) { return cb(er, d) }) |
| }) |
| } |
| |
| function prepareForInstallMany (packageData, depsKey, bundled, wrap, family) { |
| var deps = Object.keys(packageData[depsKey] || {}) |
| |
| // don't install bundleDependencies, unless they're missing. |
| if (packageData.bundleDependencies) { |
| deps = deps.filter(function (d) { |
| return packageData.bundleDependencies.indexOf(d) === -1 || |
| bundled.indexOf(d) === -1 |
| }) |
| } |
| |
| return 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. |
| if (depsKey !== "peerDependencies" && wrap) return true |
| if (semver.validRange(family[d], true)) { |
| return !semver.satisfies(family[d], packageData[depsKey][d], true) |
| } |
| return true |
| }).map(function (d) { |
| var v = packageData[depsKey][d] |
| var t = d + "@" + v |
| log.silly("prepareForInstallMany", "adding", t, "from", packageData.name, depsKey) |
| return t |
| }) |
| } |