mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
407 lines
11 KiB
407 lines
11 KiB
|
|
// Walk through the file-system "database" of installed
|
|
// packages, and create a data object related to the
|
|
// installed versions of each package.
|
|
|
|
/*
|
|
This will traverse through all node_modules folders,
|
|
resolving the dependencies object to the object corresponding to
|
|
the package that meets that dep, or just the version/range if
|
|
unmet.
|
|
|
|
Assuming that you had this folder structure:
|
|
|
|
/path/to
|
|
+-- package.json { name = "root" }
|
|
`-- node_modules
|
|
+-- foo {bar, baz, asdf}
|
|
| +-- node_modules
|
|
| +-- bar { baz }
|
|
| `-- baz
|
|
`-- asdf
|
|
|
|
where "foo" depends on bar, baz, and asdf, bar depends on baz,
|
|
and bar and baz are bundled with foo, whereas "asdf" is at
|
|
the higher level (sibling to foo), you'd get this object structure:
|
|
|
|
{ <package.json data>
|
|
, path: "/path/to"
|
|
, parent: null
|
|
, dependencies:
|
|
{ foo :
|
|
{ version: "1.2.3"
|
|
, path: "/path/to/node_modules/foo"
|
|
, parent: <Circular: root>
|
|
, dependencies:
|
|
{ bar:
|
|
{ parent: <Circular: foo>
|
|
, path: "/path/to/node_modules/foo/node_modules/bar"
|
|
, version: "2.3.4"
|
|
, dependencies: { baz: <Circular: foo.dependencies.baz> }
|
|
}
|
|
, baz: { ... }
|
|
, asdf: <Circular: asdf>
|
|
}
|
|
}
|
|
, asdf: { ... }
|
|
}
|
|
}
|
|
|
|
Unmet deps are left as strings.
|
|
Extraneous deps are marked with extraneous:true
|
|
deps that don't meet a requirement are marked with invalid:true
|
|
deps that don't meet a peer requirement are marked with peerInvalid:true
|
|
|
|
to READ(packagefolder, parentobj, name, reqver)
|
|
obj = read package.json
|
|
installed = ./node_modules/*
|
|
if parentobj is null, and no package.json
|
|
obj = {dependencies:{<installed>:ANY}}
|
|
deps = Object.keys(obj.dependencies)
|
|
obj.path = packagefolder
|
|
obj.parent = parentobj
|
|
if name, && obj.name !== name, obj.invalid = true
|
|
if reqver, && obj.version !satisfies reqver, obj.invalid = true
|
|
if !reqver && parentobj, obj.extraneous = true
|
|
for each folder in installed
|
|
obj.dependencies[folder] = READ(packagefolder+node_modules+folder,
|
|
obj, folder, obj.dependencies[folder])
|
|
# walk tree to find unmet deps
|
|
for each dep in obj.dependencies not in installed
|
|
r = obj.parent
|
|
while r
|
|
if r.dependencies[dep]
|
|
if r.dependencies[dep].verion !satisfies obj.dependencies[dep]
|
|
WARN
|
|
r.dependencies[dep].invalid = true
|
|
obj.dependencies[dep] = r.dependencies[dep]
|
|
r = null
|
|
else r = r.parent
|
|
return obj
|
|
|
|
|
|
TODO:
|
|
1. Find unmet deps in parent directories, searching as node does up
|
|
as far as the left-most node_modules folder.
|
|
2. Ignore anything in node_modules that isn't a package folder.
|
|
|
|
*/
|
|
|
|
try {
|
|
var fs = require("graceful-fs")
|
|
} catch (er) {
|
|
var fs = require("fs")
|
|
}
|
|
|
|
var path = require("path")
|
|
var asyncMap = require("slide").asyncMap
|
|
var semver = require("semver")
|
|
var readJson = require("read-package-json")
|
|
var url = require("url")
|
|
var util = require("util")
|
|
var extend = require("util-extend")
|
|
|
|
var debug = require("debuglog")("read-installed")
|
|
|
|
var readdir = require("readdir-scoped-modules")
|
|
|
|
// Sentinel catch-all version constraint used when a dependency is not
|
|
// listed in the package.json file.
|
|
var ANY = {}
|
|
|
|
module.exports = readInstalled
|
|
|
|
function readInstalled (folder, opts, cb) {
|
|
if (typeof opts === 'function') {
|
|
cb = opts
|
|
opts = {}
|
|
} else {
|
|
opts = extend({}, opts)
|
|
}
|
|
|
|
if (typeof opts.depth !== 'number')
|
|
opts.depth = Infinity
|
|
|
|
opts.depth = Math.max(0, opts.depth)
|
|
|
|
if (typeof opts.log !== 'function')
|
|
opts.log = function () {}
|
|
|
|
opts.dev = !!opts.dev
|
|
opts.realpathSeen = {}
|
|
opts.findUnmetSeen = []
|
|
|
|
|
|
readInstalled_(folder, null, null, null, 0, opts, function (er, obj) {
|
|
if (er) return cb(er)
|
|
// now obj has all the installed things, where they're installed
|
|
// figure out the inheritance links, now that the object is built.
|
|
resolveInheritance(obj, opts)
|
|
obj.root = true
|
|
unmarkExtraneous(obj, opts)
|
|
cb(null, obj)
|
|
})
|
|
}
|
|
|
|
function readInstalled_ (folder, parent, name, reqver, depth, opts, cb) {
|
|
var installed
|
|
, obj
|
|
, real
|
|
, link
|
|
, realpathSeen = opts.realpathSeen
|
|
|
|
readdir(path.resolve(folder, "node_modules"), function (er, i) {
|
|
// error indicates that nothing is installed here
|
|
if (er) i = []
|
|
installed = i.filter(function (f) { return f.charAt(0) !== "." })
|
|
next()
|
|
})
|
|
|
|
readJson(path.resolve(folder, "package.json"), function (er, data) {
|
|
obj = copy(data)
|
|
|
|
if (!parent) {
|
|
obj = obj || true
|
|
er = null
|
|
}
|
|
return next(er)
|
|
})
|
|
|
|
fs.lstat(folder, function (er, st) {
|
|
if (er) {
|
|
if (!parent) real = true
|
|
return next(er)
|
|
}
|
|
fs.realpath(folder, function (er, rp) {
|
|
debug("realpath(%j) = %j", folder, rp)
|
|
real = rp
|
|
if (st.isSymbolicLink()) link = rp
|
|
next(er)
|
|
})
|
|
})
|
|
|
|
var errState = null
|
|
, called = false
|
|
function next (er) {
|
|
if (errState) return
|
|
if (er) {
|
|
errState = er
|
|
return cb(null, [])
|
|
}
|
|
debug('next', installed, obj && typeof obj, name, real)
|
|
if (!installed || !obj || !real || called) return
|
|
called = true
|
|
if (realpathSeen[real]) return cb(null, realpathSeen[real])
|
|
if (obj === true) {
|
|
obj = {dependencies:{}, path:folder}
|
|
installed.forEach(function (i) { obj.dependencies[i] = ANY })
|
|
}
|
|
if (name && obj.name !== name) obj.invalid = true
|
|
obj.realName = name || obj.name
|
|
obj.dependencies = obj.dependencies || {}
|
|
|
|
// At this point, figure out what dependencies we NEED to get met
|
|
obj._dependencies = copy(obj.dependencies)
|
|
|
|
if (reqver === ANY) {
|
|
// We were unable to determine the required version of this
|
|
// dependency from the package.json file, but we now know its actual
|
|
// version, so treat that version as the required version to avoid
|
|
// marking the dependency as invalid below. See #40.
|
|
reqver = obj.version;
|
|
}
|
|
|
|
// "foo":"http://blah" and "foo":"latest" are always presumed valid
|
|
if (reqver
|
|
&& semver.validRange(reqver, true)
|
|
&& !semver.satisfies(obj.version, reqver, true)) {
|
|
obj.invalid = true
|
|
}
|
|
|
|
// Mark as extraneous at this point.
|
|
// This will be un-marked in unmarkExtraneous, where we mark as
|
|
// not-extraneous everything that is required in some way from
|
|
// the root object.
|
|
obj.extraneous = true
|
|
|
|
obj.path = obj.path || folder
|
|
obj.realPath = real
|
|
obj.link = link
|
|
if (parent && !obj.link) obj.parent = parent
|
|
realpathSeen[real] = obj
|
|
obj.depth = depth
|
|
//if (depth >= opts.depth) return cb(null, obj)
|
|
asyncMap(installed, function (pkg, cb) {
|
|
var rv = obj.dependencies[pkg]
|
|
if (!rv && obj.devDependencies && opts.dev)
|
|
rv = obj.devDependencies[pkg]
|
|
|
|
if (depth > opts.depth) {
|
|
obj.dependencies = {}
|
|
return cb(null, obj)
|
|
}
|
|
|
|
readInstalled_( path.resolve(folder, "node_modules/"+pkg)
|
|
, obj, pkg, obj.dependencies[pkg], depth + 1, opts
|
|
, cb )
|
|
|
|
}, function (er, installedData) {
|
|
if (er) return cb(er)
|
|
installedData.forEach(function (dep) {
|
|
obj.dependencies[dep.realName] = dep
|
|
})
|
|
|
|
// any strings here are unmet things. however, if it's
|
|
// optional, then that's fine, so just delete it.
|
|
if (obj.optionalDependencies) {
|
|
Object.keys(obj.optionalDependencies).forEach(function (dep) {
|
|
if (typeof obj.dependencies[dep] === "string") {
|
|
delete obj.dependencies[dep]
|
|
}
|
|
})
|
|
}
|
|
return cb(null, obj)
|
|
})
|
|
}
|
|
}
|
|
|
|
// starting from a root object, call findUnmet on each layer of children
|
|
var riSeen = []
|
|
function resolveInheritance (obj, opts) {
|
|
if (typeof obj !== "object") return
|
|
if (riSeen.indexOf(obj) !== -1) return
|
|
riSeen.push(obj)
|
|
if (typeof obj.dependencies !== "object") {
|
|
obj.dependencies = {}
|
|
}
|
|
Object.keys(obj.dependencies).forEach(function (dep) {
|
|
findUnmet(obj.dependencies[dep], opts)
|
|
})
|
|
Object.keys(obj.dependencies).forEach(function (dep) {
|
|
if (typeof obj.dependencies[dep] === "object") {
|
|
resolveInheritance(obj.dependencies[dep], opts)
|
|
} else {
|
|
debug("unmet dep! %s %s@%s", obj.name, dep, obj.dependencies[dep])
|
|
}
|
|
})
|
|
findUnmet(obj, opts)
|
|
}
|
|
|
|
// find unmet deps by walking up the tree object.
|
|
// No I/O
|
|
function findUnmet (obj, opts) {
|
|
var findUnmetSeen = opts.findUnmetSeen
|
|
if (findUnmetSeen.indexOf(obj) !== -1) return
|
|
findUnmetSeen.push(obj)
|
|
debug("find unmet parent=%s obj=", obj.parent && obj.parent.name, obj.name || obj)
|
|
var deps = obj.dependencies = obj.dependencies || {}
|
|
|
|
debug(deps)
|
|
Object.keys(deps)
|
|
.filter(function (d) { return typeof deps[d] === "string" })
|
|
.forEach(function (d) {
|
|
var found = findDep(obj, d)
|
|
debug("finding dep %j", d, found && found.name || found)
|
|
// "foo":"http://blah" and "foo":"latest" are always presumed valid
|
|
if (typeof deps[d] === "string" &&
|
|
semver.validRange(deps[d], true) &&
|
|
found &&
|
|
!semver.satisfies(found.version, deps[d], true)) {
|
|
// the bad thing will happen
|
|
opts.log( "unmet dependency"
|
|
, obj.path + " requires "+d+"@'"+deps[d]
|
|
+ "' but will load\n"
|
|
+ found.path+",\nwhich is version "+found.version )
|
|
found.invalid = true
|
|
}
|
|
if (found) {
|
|
deps[d] = found
|
|
}
|
|
})
|
|
|
|
var peerDeps = obj.peerDependencies = obj.peerDependencies || {}
|
|
Object.keys(peerDeps).forEach(function (d) {
|
|
var dependency
|
|
|
|
if (!obj.parent) {
|
|
dependency = obj.dependencies[d]
|
|
|
|
// read it as a missing dep
|
|
if (!dependency) {
|
|
obj.dependencies[d] = peerDeps[d]
|
|
}
|
|
} else {
|
|
var r = obj.parent
|
|
while (r && !dependency) {
|
|
dependency = r.dependencies && r.dependencies[d]
|
|
r = r.link ? null : r.parent
|
|
}
|
|
}
|
|
|
|
if (!dependency) {
|
|
// mark as a missing dep!
|
|
obj.dependencies[d] = peerDeps[d]
|
|
} else if (!semver.satisfies(dependency.version, peerDeps[d], true)) {
|
|
dependency.peerInvalid = true
|
|
}
|
|
})
|
|
|
|
return obj
|
|
}
|
|
|
|
function unmarkExtraneous (obj, opts) {
|
|
// Mark all non-required deps as extraneous.
|
|
// start from the root object and mark as non-extraneous all modules
|
|
// that haven't been previously flagged as extraneous then propagate
|
|
// to all their dependencies
|
|
|
|
obj.extraneous = false
|
|
|
|
var deps = obj._dependencies || []
|
|
if (opts.dev && obj.devDependencies && (obj.root || obj.link)) {
|
|
Object.keys(obj.devDependencies).forEach(function (k) {
|
|
deps[k] = obj.devDependencies[k]
|
|
})
|
|
}
|
|
|
|
if (obj.peerDependencies) {
|
|
Object.keys(obj.peerDependencies).forEach(function (k) {
|
|
deps[k] = obj.peerDependencies[k]
|
|
})
|
|
}
|
|
|
|
debug("not extraneous", obj._id, deps)
|
|
Object.keys(deps).forEach(function (d) {
|
|
var dep = findDep(obj, d)
|
|
if (dep && dep.extraneous) {
|
|
unmarkExtraneous(dep, opts)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Find the one that will actually be loaded by require()
|
|
// so we can make sure it's valid etc.
|
|
function findDep (obj, d) {
|
|
var r = obj
|
|
, found = null
|
|
while (r && !found) {
|
|
// if r is a valid choice, then use that.
|
|
// kinda weird if a pkg depends on itself, but after the first
|
|
// iteration of this loop, it indicates a dep cycle.
|
|
if (typeof r.dependencies[d] === "object") {
|
|
found = r.dependencies[d]
|
|
}
|
|
if (!found && r.realName === d) found = r
|
|
r = r.link ? null : r.parent
|
|
}
|
|
return found
|
|
}
|
|
|
|
function copy (obj) {
|
|
if (!obj || typeof obj !== 'object') return obj
|
|
if (Array.isArray(obj)) return obj.map(copy)
|
|
|
|
var o = {}
|
|
for (var i in obj) o[i] = copy(obj[i])
|
|
return o
|
|
}
|
|
|