Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisaacs <i@izs.me>2012-02-25 06:27:11 +0400
committerisaacs <i@izs.me>2012-02-25 06:27:11 +0400
commit61da7a3cf9608015333bac2387dfeab7cd15fb35 (patch)
tree33bbd59d49dadef286b332bfe6b238eb73467c41
parent9274bc9f7aa3d073e28310c3861c518cfd9666c9 (diff)
parent8d71fcfd8555cdd33d4a407ef5e83814475f1d26 (diff)
Merge branch 'shrinkwrap'
-rw-r--r--doc/api/ls.md6
-rw-r--r--doc/api/shrinkwrap.md20
-rw-r--r--doc/cli/config.md11
-rw-r--r--doc/cli/install.md5
-rw-r--r--doc/cli/list.md7
-rw-r--r--doc/cli/shrinkwrap.md171
-rw-r--r--lib/install.js278
-rw-r--r--lib/link.js4
-rw-r--r--lib/ls.js76
-rw-r--r--lib/npm.js1
-rw-r--r--lib/outdated.js4
-rw-r--r--lib/shrinkwrap.js49
-rw-r--r--lib/utils/config-defs.js2
13 files changed, 554 insertions, 80 deletions
diff --git a/doc/api/ls.md b/doc/api/ls.md
index a6c0a1382..ed890ff14 100644
--- a/doc/api/ls.md
+++ b/doc/api/ls.md
@@ -21,6 +21,12 @@ It will print out extraneous, missing, and invalid packages.
If the silent parameter is set to true, nothing will be output to the screen,
but the data will still be returned.
+Callback is provided an error if one occurred, the full data about which
+packages are installed and which dependencies they will receive, and a
+"lite" data object which just shows which versions are installed where.
+Note that the full data object is a circular structure, so care must be
+taken if it is serialized to JSON.
+
## CONFIGURATION
### long
diff --git a/doc/api/shrinkwrap.md b/doc/api/shrinkwrap.md
new file mode 100644
index 000000000..6584d6a0d
--- /dev/null
+++ b/doc/api/shrinkwrap.md
@@ -0,0 +1,20 @@
+npm-shrinkwrap(3) -- programmatically generate package shrinkwrap file
+====================================================
+
+## SYNOPSIS
+
+ npm.commands.shrinkwrap(args, [silent,] callback)
+
+## DESCRIPTION
+
+This acts much the same ways as shrinkwrapping on the command-line.
+
+This command does not take any arguments, but 'args' must be defined.
+Beyond that, if any arguments are passed in, npm will politely warn that it
+does not take positional arguments.
+
+If the 'silent' parameter is set to true, nothing will be output to the screen,
+but the shrinkwrap file will still be written.
+
+Finally, 'callback' is a function that will be called when the shrinkwrap has
+been saved.
diff --git a/doc/cli/config.md b/doc/cli/config.md
index 5c4750072..9d90f4024 100644
--- a/doc/cli/config.md
+++ b/doc/cli/config.md
@@ -372,6 +372,17 @@ The value `npm init` should use by default for the package author's email.
The value `npm init` should use by default for the package author's homepage.
+### json
+
+* Default: false
+* Type: Boolean
+
+Whether or not to output JSON data, rather than the normal output.
+
+This feature is currently experimental, and the output data structures
+for many commands is either not implemented in JSON yet, or subject to
+change. Only the output from `npm ls --json` is currently valid.
+
### link
* Default: false
diff --git a/doc/cli/install.md b/doc/cli/install.md
index 22eb8234e..903844a41 100644
--- a/doc/cli/install.md
+++ b/doc/cli/install.md
@@ -14,7 +14,9 @@ npm-install(1) -- Install a package
## DESCRIPTION
-This command installs a package, and any packages that it depends on.
+This command installs a package, and any packages that it depends on. If the
+package has a shrinkwrap file, the installation of dependencies will be driven
+by that. See npm-shrinkwrap(1).
A `package` is:
@@ -199,3 +201,4 @@ affects a real use-case, it will be investigated.
* npm-folders(1)
* npm-tag(1)
* npm-rm(1)
+* npm-shrinkwrap(1)
diff --git a/doc/cli/list.md b/doc/cli/list.md
index 596349a81..93d86cd83 100644
--- a/doc/cli/list.md
+++ b/doc/cli/list.md
@@ -22,6 +22,13 @@ When run as `ll` or `la`, it shows extended information by default.
## CONFIGURATION
+### json
+
+* Default: false
+* Type: Boolean
+
+Show information in JSON format.
+
### long
* Default: false
diff --git a/doc/cli/shrinkwrap.md b/doc/cli/shrinkwrap.md
new file mode 100644
index 000000000..3b60b13f7
--- /dev/null
+++ b/doc/cli/shrinkwrap.md
@@ -0,0 +1,171 @@
+npm-shrinkwrap(1) -- Lock down dependency versions
+=====================================================
+
+## SYNOPSIS
+
+ npm shrinkwrap
+
+## DESCRIPTION
+
+This command locks down the versions of a package's dependencies so that you can
+control exactly which versions of each dependency will be used when your package
+is installed.
+
+By default, "npm install" recursively installs the target's dependencies (as
+specified in package.json), choosing the latest available version that satisfies
+the dependency's semver pattern. In some situations, particularly when shipping
+software where each change is tightly managed, it's desirable to fully specify
+each version of each dependency recursively so that subsequent builds and
+deploys do not inadvertently pick up newer versions of a dependency that satisfy
+the semver pattern. Specifying specific semver patterns in each dependency's
+package.json would facilitate this, but that's not always possible or desirable,
+as when another author owns the npm package. It's also possible to check
+dependencies directly into source control, but that may be undesirable for other
+reasons.
+
+As an example, consider package A:
+
+ {
+ "name": "A",
+ "version": "0.1.0",
+ "dependencies": {
+ "B": "<0.1.0"
+ }
+ }
+
+package B:
+
+ {
+ "name": "B",
+ "version": "0.0.1",
+ "dependencies": {
+ "C": "<0.1.0"
+ }
+ }
+
+and package C:
+
+ {
+ "name": "C,
+ "version": "0.0.1"
+ }
+
+If these are the only versions of A, B, and C available in the registry, then
+a normal "npm install A" will install:
+
+ A@0.1.0
+ `-- B@0.0.1
+ `-- C@0.0.1
+
+However, if B@0.0.2 is published, then a fresh "npm install A" will install:
+
+ A@0.1.0
+ `-- B@0.0.2
+ `-- C@0.0.1
+
+assuming the new version did not modify B's dependencies. Of course, the new
+version of B could include a new version of C and any number of new
+dependencies. If such changes are undesirable, the author of A could specify a
+dependency on B@0.0.1. However, if A's author and B's author are not the same
+person, there's no way for A's author to say that he or she does not want to
+pull in newly published versions of C when B hasn't changed at all.
+
+In this case, A's author can run
+
+ npm shrinkwrap
+
+This generates npm-shrinkwrap.json, which will look something like this:
+
+ {
+ "name": "A",
+ "version": "0.1.0",
+ "dependencies": {
+ "B": {
+ "version": "0.0.1",
+ "dependencies": {
+ "C": {
+ "version": "0.1.0"
+ }
+ }
+ }
+ }
+ }
+
+The shrinkwrap command has locked down the dependencies based on what's
+currently installed in node_modules. When "npm install" installs a package with
+a npm-shrinkwrap.json file in the package root, the shrinkwrap file (rather than
+package.json files) completely drives the installation of that package and all
+of its dependencies (recursively). So now the author publishes A@0.1.0, and
+subsequent installs of this package will use B@0.0.1 and C@0.1.0, regardless the
+dependencies and versions listed in A's, B's, and C's package.json files.
+
+
+### Using shrinkwrapped packages
+
+Using a shrinkwrapped package is no different than using any other package: you
+can "npm install" it by hand, or add a dependency to your package.json file and
+"npm install" it.
+
+### Building shrinkwrapped packages
+
+To shrinkwrap an existing package:
+
+1. Run "npm install" in the package root to install the current versions of all
+ dependencies.
+2. Validate that the package works as expected with these versions.
+3. Run "npm shrinkwrap", add npm-shrinkwrap.json to git, and publish your
+ package.
+
+To add or update a dependency in a shrinkwrapped package:
+
+1. Run "npm install" in the package root to install the current versions of all
+ dependencies.
+2. Add or update dependencies. "npm install" each new or updated package
+ individually and then update package.json. Note that they must be
+ explicitly named in order to be installed: running `npm install` with
+ no arguments will merely reproduce the existing shrinkwrap.
+3. Validate that the package works as expected with the new dependencies.
+4. Run "npm shrinkwrap", commit the new npm-shrinkwrap.json, and publish your
+ package.
+
+You can use npm-outdated(1) to view dependencies with newer versions available.
+
+### Other Notes
+
+Since "npm shrinkwrap" uses the locally installed packages to construct the
+shrinkwrap file, devDependencies will be included if and only if you've
+installed them already when you make the shrinkwrap.
+
+A shrinkwrap file must be consistent with the package's package.json file. "npm
+shrinkwrap" will fail if required dependencies are not already installed, since
+that would result in a shrinkwrap that wouldn't actually work. Similarly, the
+command will fail if there are extraneous packages (not referenced by
+package.json), since that would indicate that package.json is not correct.
+
+If shrinkwrapped package A depends on shrinkwrapped package B, B's shrinkwrap
+will not be used as part of the installation of A. However, because A's
+shrinkwrap is constructed from a valid installation of B and recursively
+specifies all dependencies, the contents of B's shrinkwrap will implicitly be
+included in A's shrinkwrap.
+
+### Caveats
+
+Shrinkwrap files only lock down package versions, not actual package contents.
+While discouraged, a package author can republish an existing version of a
+package, causing shrinkwrapped packages using that version to pick up different
+code than they were before. If you want to avoid any risk that a byzantine
+author replaces a package you're using with code that breaks your application,
+you could modify the shrinkwrap file to use git URL references rather than
+version numbers so that npm always fetches all packages from git.
+
+If you wish to lock down the specific bytes included in a package, for
+example to have 100% confidence in being able to reproduce a deployment
+or build, then you ought to check your dependencies into source control,
+or pursue some other mechanism that can verify contents rather than
+versions.
+
+## SEE ALSO
+
+* npm-install(1)
+* npm-json(1)
+* npm-list(1)
diff --git a/lib/install.js b/lib/install.js
index 211a6612e..b493775c1 100644
--- a/lib/install.js
+++ b/lib/install.js
@@ -3,12 +3,14 @@
//
// See doc/install.md for more description
-// Managing "family" lists...
-// every time we dive into a deeper node_modules folder, the "family"
-// list that gets passed along uses the previous "family" list as
-// it's __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.
+// 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
@@ -20,7 +22,9 @@ install.usage = "npm install <tarball file>"
+ "\nnpm install <pkg>@<version>"
+ "\nnpm install <pkg>@<version range>"
+ "\n\nCan specify one or more: npm install ./foo.tgz bar@stable /some/folder"
- + "\nInstalls dependencies in ./package.json if no argument supplied"
+ + "\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.
@@ -109,33 +113,107 @@ function install (args, cb_) {
// or install current folder globally
if (!args.length) {
if (npm.config.get("global")) args = ["."]
- else return readJson( path.resolve(where, "package.json")
- , { dev: !npm.config.get("production") }
- , function (er, data) {
+ 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 family = {}
- , ancestors = {}
- family[data.name] = ancestors[data.name] = data.version
+ 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, family, ancestors, false, data, cb)
+ }), where, context, cb)
})
}
// initial "family" is the name:version of the root, if it's got
- // a pacakge.json file.
+ // a package.json file.
readJson(path.resolve(where, "package.json"), function (er, data) {
if (er) data = null
- var family = {}
- , ancestors = {}
- if (data) family[data.name] = ancestors[data.name] = data.version
+ 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, family, ancestors, true, data, cb)
+ 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) {
+ rv.dependencies[key] = wrap[key].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) {
+ rv.dependencies[key] = newwrap.dependencies[key].version
+ })
+ log.verbose([rv.dependencies], "readDependencies: returned deps")
+ return cb(null, rv, newwrap.dependencies)
})
})
}
@@ -187,8 +265,10 @@ function save (where, installed, tree, pretty, cb) {
// 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.
+// TODO: Show the complete tree, ls-style, but only if --long is provided
function prettify (tree, installed) {
+ // XXX This should match the data structure provided by npm ls --json
+ if (npm.config.get("json")) return JSON.stringify(tree, null, 2)
if (npm.config.get("parseable")) return parseable(installed)
return Object.keys(tree).map(function (p) {
p = tree[p]
@@ -255,10 +335,9 @@ function treeify (installed) {
// just like installMany, but also add the existing packages in
// where/node_modules to the family object.
-function installManyTop (what, where, family, ancestors, explicit, parent, cb_) {
-
+function installManyTop (what, where, context, cb_) {
function cb (er, d) {
- if (explicit || er) return 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) {
@@ -266,7 +345,7 @@ function installManyTop (what, where, family, ancestors, explicit, parent, cb_)
})
}
- if (explicit) return next()
+ if (context.explicit) return next()
readJson(path.join(where, "package.json"), function (er, data) {
if (er) return next(er)
@@ -275,21 +354,21 @@ function installManyTop (what, where, family, ancestors, explicit, parent, cb_)
function next (er) {
if (er) return cb(er)
- installManyTop_(what, where, family, ancestors, explicit, parent, cb)
+ installManyTop_(what, where, context, cb)
}
}
-function installManyTop_ (what, where, family, ancestors, explicit, parent, cb) {
+function installManyTop_ (what, where, context, cb) {
var nm = path.resolve(where, "node_modules")
- , names = explicit
+ , names = context.explicit
? what.map(function (w) { return w.split(/@/).shift() })
: []
fs.readdir(nm, function (er, pkgs) {
- if (er) return installMany(what, where, family, ancestors, explicit, parent, cb)
+ if (er) return installMany(what, where, context, cb)
pkgs = pkgs.filter(function (p) {
return !p.match(/^[\._-]/)
- && (!explicit || names.indexOf(p) === -1)
+ && (!context.explicit || names.indexOf(p) === -1)
})
asyncMap(pkgs.map(function (p) {
return path.resolve(nm, p, "package.json")
@@ -302,36 +381,44 @@ function installManyTop_ (what, where, family, ancestors, explicit, parent, cb)
// add all the existing packages to the family list.
// however, do not add to the ancestors list.
packages.forEach(function (p) {
- family[p[0]] = p[1]
+ context.family[p[0]] = p[1]
})
- return installMany(what, where, family, ancestors, explicit, parent, cb)
+ return installMany(what, where, context, cb)
})
})
}
-function installMany (what, where, family, ancestors, explicit, parent, cb) {
- // 'npm install foo' should install the version of foo
- // that satisfies the dep in the current folder.
- // This will typically return immediately, since we already read
- // this file family, and it'll be cached.
- readJson(path.resolve(where, "package.json"), function (er, data) {
+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 = {}
- d = data.dependencies || {}
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, family, ancestors, explicit, d, parent)
+ , 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(family)
- , newAnc = Object.create(ancestors)
+ var newPrev = Object.create(context.family)
+ , newAnc = Object.create(context.ancestors)
newAnc[data.name] = data.version
targets.forEach(function (t) {
@@ -343,17 +430,25 @@ function installMany (what, where, family, ancestors, explicit, parent, cb) {
})
asyncMap(targets, function (target, cb) {
log(target._id, "installOne")
- installOne(target, where, newPrev, newAnc, parent, cb)
+ 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, family, ancestors, explicit, deps, parent) {
- var alreadyInstalledManually = explicit ? [] : null
+function targetResolver (where, context, deps) {
+ var alreadyInstalledManually = context.explicit ? [] : null
, nm = path.resolve(where, "node_modules")
+ , parent = context.parent
+ , wrap = context.wrap
- if (!explicit) fs.readdir(nm, function (er, inst) {
+ 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) {
@@ -381,11 +476,29 @@ function targetResolver (where, family, ancestors, explicit, deps, parent) {
return cb(null, [])
}
- if (family[what] && semver.satisfies(family[what], deps[what] || "")) {
- 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("using existing "+what+" (matches shrinkwrap)")
+ return cb(null, [])
+ }
+
+ if (!wrap && semver.satisfies(context.family[what], deps[what] || "")) {
+ log.verbose("using existing "+what+" (no shrinkwrap)")
+ return cb(null, [])
+ }
}
- if (deps[what]) {
+ if (wrap) {
+ name = what.split(/@/).shift()
+ if (wrap[name]) {
+ log.verbose("shrinkwrap: resolving "+what+" to "+wrap[name].version)
+ what = name + "@" + wrap[name].version
+ } else {
+ log.verbose("shrinkwrap: skipping "+what+" (not in shrinkwrap)")
+ }
+ } else if (deps[what]) {
what = what + "@" + deps[what]
}
@@ -395,7 +508,7 @@ function targetResolver (where, family, ancestors, explicit, deps, parent) {
log.warn(what, "optional dependency failed, continuing")
return cb(null, [])
}
- if (!er && data && family[data.name] === data.version) {
+ if (!er && data && context.family[data.name] === data.version) {
return cb(null, [])
}
return cb(er, data)
@@ -405,20 +518,21 @@ function targetResolver (where, family, ancestors, explicit, deps, parent) {
// we've already decided to install this. if anything's in the way,
// then uninstall it first.
-function installOne (target, where, family, ancestors, parent, cb) {
+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, family, ancestors, parent, cb)
+ return localLink(target, where, context, cb)
}
- installOne_(target, where, family, ancestors, parent, cb)
+ installOne_(target, where, context, cb)
}
-function localLink (target, where, family, ancestors, parent, cb) {
+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) {
@@ -440,7 +554,7 @@ function localLink (target, where, family, ancestors, parent, cb) {
} else {
log.verbose(target._id, "install locally (no link)")
- installOne_(target, where, family, ancestors, parent, cb)
+ installOne_(target, where, context, cb)
}
})
}
@@ -464,18 +578,19 @@ function resultList (target, where, parentId) {
, parentId && prettyWhere ]
}
-function installOne_ (target, where, family, ancestors, parent, cb) {
+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]
- , [checkCycle, target, ancestors]
+ , [checkCycle, target, context.ancestors]
, [checkGit, targetFolder]
- , [write, target, targetFolder, family, ancestors] ]
+ , [write, target, targetFolder, context] ]
, function (er, d) {
log.verbose(target._id, "installOne cb")
if (er) return cb(er)
@@ -559,10 +674,11 @@ function checkGit_ (folder, cb) {
})
}
-function write (target, targetFolder, family, ancestors, 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
@@ -586,23 +702,33 @@ function write (target, targetFolder, family, ancestors, cb_) {
// up until this point, since we really don't care about it.
, function (er) {
if (er) return cb(er)
- var deps = Object.keys(target.dependencies || {})
- installMany(deps.filter(function (d) {
- // prefer to not install things that are satisfied by
- // something in the "family" list.
- return !semver.satisfies(family[d], target.dependencies[d])
- }).map(function (d) {
- var t = target.dependencies[d]
- , parsed = url.parse(t.replace(/^git\+/, "git"))
- t = d + "@" + t
- return t
- }), targetFolder, family, ancestors, false, target, 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) })
+
+ // before continuing to installing dependencies, check for a shrinkwrap.
+ readDependencies(context, targetFolder, {}, function (er, data, wrap) {
+ var deps = Object.keys(data.dependencies || {})
+ 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) })
+ })
})
- } )
+ })
}
diff --git a/lib/link.js b/lib/link.js
index fea660666..3049884ca 100644
--- a/lib/link.js
+++ b/lib/link.js
@@ -141,6 +141,7 @@ function resultPrinter (pkg, src, dest, rp, cb) {
var where = relativize(dest, path.resolve(process.cwd(),"x"))
rp = (rp || "").trim()
src = (src || "").trim()
+ // XXX If --json is set, then look up the data from the package.json
if (npm.config.get("parseable")) {
return parseableOutput(dest, rp || src, cb)
}
@@ -150,6 +151,9 @@ function resultPrinter (pkg, src, dest, rp, cb) {
}
function parseableOutput (dest, rp, cb) {
+ // XXX this should match ls --parseable and install --parseable
+ // look up the data from package.json, format it the same way.
+ //
// link is always effectively "long", since it doesn't help much to
// *just* print the target folder.
// However, we don't actually ever read the version number, so
diff --git a/lib/ls.js b/lib/ls.js
index 6bf0617c3..a4ffc331a 100644
--- a/lib/ls.js
+++ b/lib/ls.js
@@ -26,13 +26,83 @@ function ls (args, silent, cb) {
var dir = path.resolve(npm.dir, "..")
readInstalled(dir, function (er, data) {
- if (er || silent) return cb(er, data)
+ var lite = getLite(bfsify(data))
+ if (er || silent) return cb(er, data, lite)
+
var long = npm.config.get("long")
- var out = makePretty(bfsify(data), long, dir).join("\n")
- output.write(out, function (er) { cb(er, data) })
+ , json = npm.config.get("json")
+ , out
+ if (json) {
+ var seen = []
+ var d = long ? bfsify(data) : lite
+ // the raw data can be circular
+ out = JSON.stringify(d, function (k, o) {
+ if (typeof o === "object") {
+ if (-1 !== seen.indexOf(o)) return "[Circular]"
+ seen.push(o)
+ }
+ return o
+ }, 2)
+ } else {
+ out = makePretty(bfsify(data), long, dir).join("\n")
+ }
+ output.write(out, function (er) { cb(er, data, lite) })
})
}
+function getLite (data, noname) {
+ var lite = {}
+ , maxDepth = npm.config.get("depth")
+
+ if (!noname && data.name) lite.name = data.name
+ if (data.version) lite.version = data.version
+ if (data.extraneous) {
+ lite.extraneous = true
+ lite.problems = lite.problems || []
+ lite.problems.push( "extraneous: "
+ + data.name + "@" + data.version
+ + " " + (data.path || "") )
+ }
+
+ if (data.invalid) {
+ lite.invalid = true
+ lite.problems = lite.problems || []
+ lite.problems.push( "invalid: "
+ + data.name + "@" + data.version
+ + " " + (data.path || "") )
+ }
+
+ if (data.dependencies) {
+ var deps = Object.keys(data.dependencies)
+ if (deps.length) lite.dependencies = deps.map(function (d) {
+ var dep = data.dependencies[d]
+ if (typeof dep === "string") {
+ lite.problems = lite.problems || []
+ var p
+ if (data.depth >= maxDepth) {
+ p = "max depth reached: "
+ } else {
+ p = "missing: "
+ }
+ p += d + "@" + dep
+ + ", required by "
+ + data.name + "@" + data.version
+ lite.problems.push(p)
+ return [d, { required: dep, missing: true }]
+ }
+ return [d, getLite(dep, true)]
+ }).reduce(function (deps, d) {
+ if (d[1].problems) {
+ lite.problems = lite.problems || []
+ lite.problems.push.apply(lite.problems, d[1].problems)
+ }
+ deps[d[0]] = d[1]
+ return deps
+ }, {})
+ }
+ return lite
+}
+
function bfsify (root, current, queue, seen) {
// walk over the data, and turn it from this:
// +-- a
diff --git a/lib/npm.js b/lib/npm.js
index de68393d3..53197082c 100644
--- a/lib/npm.js
+++ b/lib/npm.js
@@ -138,6 +138,7 @@ var commandCache = {}
, "unpublish"
, "owner"
, "deprecate"
+ , "shrinkwrap"
, "help"
, "help-search"
diff --git a/lib/outdated.js b/lib/outdated.js
index 496dfbd42..e883abd35 100644
--- a/lib/outdated.js
+++ b/lib/outdated.js
@@ -48,6 +48,10 @@ function makePretty (p) {
, dir = path.resolve(p[0], "node_modules", dep)
, has = p[2]
, want = p[3]
+
+ // XXX add --json support
+ // Should match (more or less) the output of ls --json
+
if (parseable) {
var str = dir
if (npm.config.get("long")) {
diff --git a/lib/shrinkwrap.js b/lib/shrinkwrap.js
new file mode 100644
index 000000000..59942d586
--- /dev/null
+++ b/lib/shrinkwrap.js
@@ -0,0 +1,49 @@
+// emit JSON describing versions of all packages currently installed (for later
+// use with shrinkwrap install)
+
+module.exports = exports = shrinkwrap
+
+var npm = require("./npm.js")
+ , output = require("./utils/output.js")
+ , log = require("./utils/log.js")
+ , fs = require("fs")
+ , path = require("path")
+
+shrinkwrap.usage = "npm shrinkwrap"
+
+function shrinkwrap (args, silent, cb) {
+ if (typeof cb !== "function") cb = silent, silent = false
+
+ if (args.length) {
+ log.warn("shrinkwrap doesn't take positional args.")
+ }
+
+ npm.commands.ls([], true, function (er, _, pkginfo) {
+ if (er) return cb(er)
+ shrinkwrap_(pkginfo, silent, cb)
+ })
+}
+
+function shrinkwrap_ (pkginfo, silent, cb) {
+ if (pkginfo.problems) {
+ return cb(new Error("Problems were encountered\n"
+ +"Please correct and try again.\n"
+ +pkginfo.problems.join("\n")))
+ }
+ try {
+ var swdata = JSON.stringify(pkginfo, null, 2) + "\n"
+ } catch (er) {
+ log.error("Error converting package info to json")
+ return cb(er)
+ }
+
+ var file = path.resolve(npm.prefix, "npm-shrinkwrap.json")
+
+ fs.writeFile(file, swdata, function (er) {
+ if (er) return cb(er)
+ if (silent) return cb(null, pkginfo)
+ output.write("wrote npm-shrinkwrap.json", function (er) {
+ cb(er, pkginfo)
+ })
+ })
+}
diff --git a/lib/utils/config-defs.js b/lib/utils/config-defs.js
index 3cecb5052..9c5074135 100644
--- a/lib/utils/config-defs.js
+++ b/lib/utils/config-defs.js
@@ -161,6 +161,7 @@ Object.defineProperty(exports, "defaults", {get: function () {
, "init.author.name" : ""
, "init.author.email" : ""
, "init.author.url" : ""
+ , json: false
, link: false
, logfd : 2
, loglevel : "http"
@@ -239,6 +240,7 @@ exports.types =
, "init.author.name" : String
, "init.author.email" : String
, "init.author.url" : ["", url]
+ , json: Boolean
, link: Boolean
, logfd : [Number, Stream]
, loglevel : ["silent","win","error","warn","http","info","verbose","silly"]