• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1
2// Walk through the file-system "database" of installed
3// packages, and create a data object related to the
4// installed versions of each package.
5
6/*
7This will traverse through all node_modules folders,
8resolving the dependencies object to the object corresponding to
9the package that meets that dep, or just the version/range if
10unmet.
11
12Assuming that you had this folder structure:
13
14/path/to
15+-- package.json { name = "root" }
16`-- node_modules
17    +-- foo {bar, baz, asdf}
18    | +-- node_modules
19    |   +-- bar { baz }
20    |   `-- baz
21    `-- asdf
22
23where "foo" depends on bar, baz, and asdf, bar depends on baz,
24and bar and baz are bundled with foo, whereas "asdf" is at
25the higher level (sibling to foo), you'd get this object structure:
26
27{ <package.json data>
28, path: "/path/to"
29, parent: null
30, dependencies:
31  { foo :
32    { version: "1.2.3"
33    , path: "/path/to/node_modules/foo"
34    , parent: <Circular: root>
35    , dependencies:
36      { bar:
37        { parent: <Circular: foo>
38        , path: "/path/to/node_modules/foo/node_modules/bar"
39        , version: "2.3.4"
40        , dependencies: { baz: <Circular: foo.dependencies.baz> }
41        }
42      , baz: { ... }
43      , asdf: <Circular: asdf>
44      }
45    }
46  , asdf: { ... }
47  }
48}
49
50Unmet deps are left as strings.
51Extraneous deps are marked with extraneous:true
52deps that don't meet a requirement are marked with invalid:true
53deps that don't meet a peer requirement are marked with peerInvalid:true
54
55to READ(packagefolder, parentobj, name, reqver)
56obj = read package.json
57installed = ./node_modules/*
58if parentobj is null, and no package.json
59  obj = {dependencies:{<installed>:ANY}}
60deps = Object.keys(obj.dependencies)
61obj.path = packagefolder
62obj.parent = parentobj
63if name, && obj.name !== name, obj.invalid = true
64if reqver, && obj.version !satisfies reqver, obj.invalid = true
65if !reqver && parentobj, obj.extraneous = true
66for each folder in installed
67  obj.dependencies[folder] = READ(packagefolder+node_modules+folder,
68                                  obj, folder, obj.dependencies[folder])
69# walk tree to find unmet deps
70for each dep in obj.dependencies not in installed
71  r = obj.parent
72  while r
73    if r.dependencies[dep]
74      if r.dependencies[dep].verion !satisfies obj.dependencies[dep]
75        WARN
76        r.dependencies[dep].invalid = true
77      obj.dependencies[dep] = r.dependencies[dep]
78      r = null
79    else r = r.parent
80return obj
81
82
83TODO:
841. Find unmet deps in parent directories, searching as node does up
85as far as the left-most node_modules folder.
862. Ignore anything in node_modules that isn't a package folder.
87
88*/
89
90try {
91  var fs = require("graceful-fs")
92} catch (er) {
93  var fs = require("fs")
94}
95
96var path = require("path")
97var asyncMap = require("slide").asyncMap
98var semver = require("semver")
99var readJson = require("read-package-json")
100var url = require("url")
101var util = require("util")
102var extend = require("util-extend")
103
104var debug = require("debuglog")("read-installed")
105
106var readdir = require("readdir-scoped-modules")
107
108// Sentinel catch-all version constraint used when a dependency is not
109// listed in the package.json file.
110var ANY = {}
111
112module.exports = readInstalled
113
114function readInstalled (folder, opts, cb) {
115  if (typeof opts === 'function') {
116    cb = opts
117    opts = {}
118  } else {
119    opts = extend({}, opts)
120  }
121
122  if (typeof opts.depth !== 'number')
123    opts.depth = Infinity
124
125  opts.depth = Math.max(0, opts.depth)
126
127  if (typeof opts.log !== 'function')
128    opts.log = function () {}
129
130  opts.dev = !!opts.dev
131  opts.realpathSeen = {}
132  opts.findUnmetSeen = []
133
134
135  readInstalled_(folder, null, null, null, 0, opts, function (er, obj) {
136    if (er) return cb(er)
137    // now obj has all the installed things, where they're installed
138    // figure out the inheritance links, now that the object is built.
139    resolveInheritance(obj, opts)
140    obj.root = true
141    unmarkExtraneous(obj, opts)
142    cb(null, obj)
143  })
144}
145
146function readInstalled_ (folder, parent, name, reqver, depth, opts, cb) {
147  var installed
148    , obj
149    , real
150    , link
151    , realpathSeen = opts.realpathSeen
152
153  readdir(path.resolve(folder, "node_modules"), function (er, i) {
154    // error indicates that nothing is installed here
155    if (er) i = []
156    installed = i.filter(function (f) { return f.charAt(0) !== "." })
157    next()
158  })
159
160  readJson(path.resolve(folder, "package.json"), function (er, data) {
161    obj = copy(data)
162
163    if (!parent) {
164      obj = obj || true
165      er = null
166    }
167    return next(er)
168  })
169
170  fs.lstat(folder, function (er, st) {
171    if (er) {
172      if (!parent) real = true
173      return next(er)
174    }
175    fs.realpath(folder, function (er, rp) {
176      debug("realpath(%j) = %j", folder, rp)
177      real = rp
178      if (st.isSymbolicLink()) link = rp
179      next(er)
180    })
181  })
182
183  var errState = null
184    , called = false
185  function next (er) {
186    if (errState) return
187    if (er) {
188      errState = er
189      return cb(null, [])
190    }
191    debug('next', installed, obj && typeof obj, name, real)
192    if (!installed || !obj || !real || called) return
193    called = true
194    if (realpathSeen[real]) return cb(null, realpathSeen[real])
195    if (obj === true) {
196      obj = {dependencies:{}, path:folder}
197      installed.forEach(function (i) { obj.dependencies[i] = ANY })
198    }
199    if (name && obj.name !== name) obj.invalid = true
200    obj.realName = name || obj.name
201    obj.dependencies = obj.dependencies || {}
202
203    // At this point, figure out what dependencies we NEED to get met
204    obj._dependencies = copy(obj.dependencies)
205
206    if (reqver === ANY) {
207      // We were unable to determine the required version of this
208      // dependency from the package.json file, but we now know its actual
209      // version, so treat that version as the required version to avoid
210      // marking the dependency as invalid below. See #40.
211      reqver = obj.version;
212    }
213
214    // "foo":"http://blah" and "foo":"latest" are always presumed valid
215    if (reqver
216        && semver.validRange(reqver, true)
217        && !semver.satisfies(obj.version, reqver, true)) {
218      obj.invalid = true
219    }
220
221    // Mark as extraneous at this point.
222    // This will be un-marked in unmarkExtraneous, where we mark as
223    // not-extraneous everything that is required in some way from
224    // the root object.
225    obj.extraneous = true
226
227    obj.path = obj.path || folder
228    obj.realPath = real
229    obj.link = link
230    if (parent && !obj.link) obj.parent = parent
231    realpathSeen[real] = obj
232    obj.depth = depth
233    //if (depth >= opts.depth) return cb(null, obj)
234    asyncMap(installed, function (pkg, cb) {
235      var rv = obj.dependencies[pkg]
236      if (!rv && obj.devDependencies && opts.dev)
237        rv = obj.devDependencies[pkg]
238
239      if (depth > opts.depth) {
240        obj.dependencies = {}
241        return cb(null, obj)
242      }
243
244      readInstalled_( path.resolve(folder, "node_modules/"+pkg)
245                    , obj, pkg, obj.dependencies[pkg], depth + 1, opts
246                    , cb )
247
248    }, function (er, installedData) {
249      if (er) return cb(er)
250      installedData.forEach(function (dep) {
251        obj.dependencies[dep.realName] = dep
252      })
253
254      // any strings here are unmet things.  however, if it's
255      // optional, then that's fine, so just delete it.
256      if (obj.optionalDependencies) {
257        Object.keys(obj.optionalDependencies).forEach(function (dep) {
258          if (typeof obj.dependencies[dep] === "string") {
259            delete obj.dependencies[dep]
260          }
261        })
262      }
263      return cb(null, obj)
264    })
265  }
266}
267
268// starting from a root object, call findUnmet on each layer of children
269var riSeen = []
270function resolveInheritance (obj, opts) {
271  if (typeof obj !== "object") return
272  if (riSeen.indexOf(obj) !== -1) return
273  riSeen.push(obj)
274  if (typeof obj.dependencies !== "object") {
275    obj.dependencies = {}
276  }
277  Object.keys(obj.dependencies).forEach(function (dep) {
278    findUnmet(obj.dependencies[dep], opts)
279  })
280  Object.keys(obj.dependencies).forEach(function (dep) {
281    if (typeof obj.dependencies[dep] === "object") {
282      resolveInheritance(obj.dependencies[dep], opts)
283    } else {
284      debug("unmet dep! %s %s@%s", obj.name, dep, obj.dependencies[dep])
285    }
286  })
287  findUnmet(obj, opts)
288}
289
290// find unmet deps by walking up the tree object.
291// No I/O
292function findUnmet (obj, opts) {
293  var findUnmetSeen = opts.findUnmetSeen
294  if (findUnmetSeen.indexOf(obj) !== -1) return
295  findUnmetSeen.push(obj)
296  debug("find unmet parent=%s obj=", obj.parent && obj.parent.name, obj.name || obj)
297  var deps = obj.dependencies = obj.dependencies || {}
298
299  debug(deps)
300  Object.keys(deps)
301    .filter(function (d) { return typeof deps[d] === "string" })
302    .forEach(function (d) {
303      var found = findDep(obj, d)
304      debug("finding dep %j", d, found && found.name || found)
305      // "foo":"http://blah" and "foo":"latest" are always presumed valid
306      if (typeof deps[d] === "string" &&
307          semver.validRange(deps[d], true) &&
308          found &&
309          !semver.satisfies(found.version, deps[d], true)) {
310        // the bad thing will happen
311        opts.log( "unmet dependency"
312                , obj.path + " requires "+d+"@'"+deps[d]
313                + "' but will load\n"
314                + found.path+",\nwhich is version "+found.version )
315        found.invalid = true
316      }
317      if (found) {
318        deps[d] = found
319      }
320    })
321
322  var peerDeps = obj.peerDependencies = obj.peerDependencies || {}
323  Object.keys(peerDeps).forEach(function (d) {
324    var dependency
325
326    if (!obj.parent) {
327      dependency = obj.dependencies[d]
328
329      // read it as a missing dep
330      if (!dependency) {
331        obj.dependencies[d] = peerDeps[d]
332      }
333    } else {
334      var r = obj.parent
335      while (r && !dependency) {
336        dependency = r.dependencies && r.dependencies[d]
337        r = r.link ? null : r.parent
338      }
339    }
340
341    if (!dependency) {
342      // mark as a missing dep!
343      obj.dependencies[d] = peerDeps[d]
344    } else if (!semver.satisfies(dependency.version, peerDeps[d], true)) {
345      dependency.peerInvalid = true
346    }
347  })
348
349  return obj
350}
351
352function unmarkExtraneous (obj, opts) {
353  // Mark all non-required deps as extraneous.
354  // start from the root object and mark as non-extraneous all modules
355  // that haven't been previously flagged as extraneous then propagate
356  // to all their dependencies
357
358  obj.extraneous = false
359
360  var deps = obj._dependencies || []
361  if (opts.dev && obj.devDependencies && (obj.root || obj.link)) {
362    Object.keys(obj.devDependencies).forEach(function (k) {
363      deps[k] = obj.devDependencies[k]
364    })
365  }
366
367  if (obj.peerDependencies) {
368    Object.keys(obj.peerDependencies).forEach(function (k) {
369      deps[k] = obj.peerDependencies[k]
370    })
371  }
372
373  debug("not extraneous", obj._id, deps)
374  Object.keys(deps).forEach(function (d) {
375    var dep = findDep(obj, d)
376    if (dep && dep.extraneous) {
377      unmarkExtraneous(dep, opts)
378    }
379  })
380}
381
382// Find the one that will actually be loaded by require()
383// so we can make sure it's valid etc.
384function findDep (obj, d) {
385  var r = obj
386    , found = null
387  while (r && !found) {
388    // if r is a valid choice, then use that.
389    // kinda weird if a pkg depends on itself, but after the first
390    // iteration of this loop, it indicates a dep cycle.
391    if (typeof r.dependencies[d] === "object") {
392      found = r.dependencies[d]
393    }
394    if (!found && r.realName === d) found = r
395    r = r.link ? null : r.parent
396  }
397  return found
398}
399
400function copy (obj) {
401  if (!obj || typeof obj !== 'object') return obj
402  if (Array.isArray(obj)) return obj.map(copy)
403
404  var o = {}
405  for (var i in obj) o[i] = copy(obj[i])
406  return o
407}
408