diff options
author | isaacs <i@izs.me> | 2012-02-25 06:27:11 +0400 |
---|---|---|
committer | isaacs <i@izs.me> | 2012-02-25 06:27:11 +0400 |
commit | 61da7a3cf9608015333bac2387dfeab7cd15fb35 (patch) | |
tree | 33bbd59d49dadef286b332bfe6b238eb73467c41 | |
parent | 9274bc9f7aa3d073e28310c3861c518cfd9666c9 (diff) | |
parent | 8d71fcfd8555cdd33d4a407ef5e83814475f1d26 (diff) |
Merge branch 'shrinkwrap'
-rw-r--r-- | doc/api/ls.md | 6 | ||||
-rw-r--r-- | doc/api/shrinkwrap.md | 20 | ||||
-rw-r--r-- | doc/cli/config.md | 11 | ||||
-rw-r--r-- | doc/cli/install.md | 5 | ||||
-rw-r--r-- | doc/cli/list.md | 7 | ||||
-rw-r--r-- | doc/cli/shrinkwrap.md | 171 | ||||
-rw-r--r-- | lib/install.js | 278 | ||||
-rw-r--r-- | lib/link.js | 4 | ||||
-rw-r--r-- | lib/ls.js | 76 | ||||
-rw-r--r-- | lib/npm.js | 1 | ||||
-rw-r--r-- | lib/outdated.js | 4 | ||||
-rw-r--r-- | lib/shrinkwrap.js | 49 | ||||
-rw-r--r-- | lib/utils/config-defs.js | 2 |
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 @@ -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"] |