The data contained in this repository can be downloaded to your computer using one of several clients.
Please see the documentation of your version control software client for more information.

Please select the desired protocol below to get the URL.

This URL has Read-Only access.

Statistics
| Branch: | Revision:

main_repo / deps / npm / lib / dedupe.js @ 5aef65a9

History | View | Annotate | Download (9.92 KB)

1
// traverse the node_modules/package.json tree
2
// looking for duplicates.  If any duplicates are found,
3
// then move them up to the highest level necessary
4
// in order to make them no longer duplicated.
5
//
6
// This is kind of ugly, and really highlights the need for
7
// much better "put pkg X at folder Y" abstraction.  Oh well,
8
// whatever.  Perfect enemy of the good, and all that.
9

    
10
var fs = require("fs")
11
var asyncMap = require("slide").asyncMap
12
var path = require("path")
13
var readJson = require("read-package-json")
14
var archy = require("archy")
15
var util = require("util")
16
var RegClient = require("npm-registry-client")
17
var npmconf = require("npmconf")
18
var npm = require("npm")
19
var semver = require("semver")
20
var npm = require("npm")
21
var rimraf = require("rimraf")
22
var log = require("npmlog")
23
var npm = require("./npm.js")
24

    
25
module.exports = dedupe
26

    
27
dedupe.usage = "npm dedupe [pkg pkg...]"
28

    
29
function dedupe (args, silent, cb) {
30
  if (typeof silent === "function") cb = silent, silent = false
31
  var dryrun = false
32
  if (npm.command.match(/^find/)) dryrun = true
33
  return dedupe_(npm.prefix, args, {}, dryrun, silent, cb)
34
}
35

    
36
function dedupe_ (dir, filter, unavoidable, dryrun, silent, cb) {
37
  readInstalled(path.resolve(dir), {}, null, function (er, data, counter) {
38
    if (er) {
39
      return cb(er)
40
    }
41

    
42
    if (!data) {
43
      return cb()
44
    }
45

    
46
    // find out which things are dupes
47
    var dupes = Object.keys(counter || {}).filter(function (k) {
48
      if (filter.length && -1 === filter.indexOf(k)) return false
49
      return counter[k] > 1 && !unavoidable[k]
50
    }).reduce(function (s, k) {
51
      s[k] = []
52
      return s
53
    }, {})
54

    
55
    // any that are unavoidable need to remain as they are.  don't even
56
    // try to touch them or figure it out.  Maybe some day, we can do
57
    // something a bit more clever here, but for now, just skip over it,
58
    // and all its children.
59
    ;(function U (obj) {
60
      if (unavoidable[obj.name]) {
61
        obj.unavoidable = true
62
      }
63
      if (obj.parent && obj.parent.unavoidable) {
64
        obj.unavoidable = true
65
      }
66
      Object.keys(obj.children).forEach(function (k) {
67
        U(obj.children[k])
68
      })
69
    })
70

    
71
    // then collect them up and figure out who needs them
72
    ;(function C (obj) {
73
      if (dupes[obj.name] && !obj.unavoidable) {
74
        dupes[obj.name].push(obj)
75
        obj.duplicate = true
76
      }
77
      obj.dependents = whoDepends(obj)
78
      Object.keys(obj.children).forEach(function (k) {
79
        C(obj.children[k])
80
      })
81
    })(data)
82

    
83
    if (dryrun) {
84
      var k = Object.keys(dupes)
85
      if (!k.length) return cb()
86
      return npm.commands.ls(k, silent, cb)
87
    }
88

    
89
    var summary = Object.keys(dupes).map(function (n) {
90
      return [n, dupes[n].filter(function (d) {
91
        return d && d.parent && !d.parent.duplicate && !d.unavoidable
92
      }).map(function M (d) {
93
        return [d.path, d.version, d.dependents.map(function (k) {
94
          return [k.path, k.version, k.dependencies[d.name] || ""]
95
        })]
96
      })]
97
    }).map(function (item) {
98
      var name = item[0]
99
      var set = item[1]
100

    
101
      var ranges = set.map(function (i) {
102
        return i[2].map(function (d) {
103
          return d[2]
104
        })
105
      }).reduce(function (l, r) {
106
        return l.concat(r)
107
      }, []).map(function (v, i, set) {
108
        if (set.indexOf(v) !== i) return false
109
        return v
110
      }).filter(function (v) {
111
        return v !== false
112
      })
113

    
114
      var locs = set.map(function (i) {
115
        return i[0]
116
      })
117

    
118
      var versions = set.map(function (i) {
119
        return i[1]
120
      }).filter(function (v, i, set) {
121
        return set.indexOf(v) === i
122
      })
123

    
124
      var has = set.map(function (i) {
125
        return [i[0], i[1]]
126
      }).reduce(function (set, kv) {
127
        set[kv[0]] = kv[1]
128
        return set
129
      }, {})
130

    
131
      var loc = locs.length ? locs.reduce(function (a, b) {
132
        // a=/path/to/node_modules/foo/node_modules/bar
133
        // b=/path/to/node_modules/elk/node_modules/bar
134
        // ==/path/to/node_modules/bar
135
        a = a.split(/\/node_modules\//)
136
        b = b.split(/\/node_modules\//)
137
        var name = a.pop()
138
        b.pop()
139
        // find the longest chain that both A and B share.
140
        // then push the name back on it, and join by /node_modules/
141
        var res = []
142
        for (var i = 0, al = a.length, bl = b.length; i < al && i < bl && a[i] === b[i]; i++);
143
        return a.slice(0, i).concat(name).join("/node_modules/")
144
      }) : undefined
145

    
146
      return [item[0], { item: item
147
                       , ranges: ranges
148
                       , locs: locs
149
                       , loc: loc
150
                       , has: has
151
                       , versions: versions
152
                       }]
153
    }).filter(function (i) {
154
      return i[1].loc
155
    })
156

    
157
    findVersions(npm, summary, function (er, set) {
158
      if (er) return cb(er)
159
      if (!set.length) return cb()
160
      installAndRetest(set, filter, dir, unavoidable, silent, cb)
161
    })
162
  })
163
}
164

    
165
function installAndRetest (set, filter, dir, unavoidable, silent, cb) {
166
  //return cb(null, set)
167
  var remove = []
168

    
169
  asyncMap(set, function (item, cb) {
170
    // [name, has, loc, locMatch, regMatch, others]
171
    var name = item[0]
172
    var has = item[1]
173
    var where = item[2]
174
    var locMatch = item[3]
175
    var regMatch = item[4]
176
    var others = item[5]
177

    
178
    // nothing to be done here.  oh well.  just a conflict.
179
    if (!locMatch && !regMatch) {
180
      log.warn("unavoidable conflict", item[0], item[1])
181
      log.warn("unavoidable conflict", "Not de-duplicating")
182
      unavoidable[item[0]] = true
183
      return cb()
184
    }
185

    
186
    // nothing to do except to clean up the extraneous deps
187
    if (locMatch && has[where] === locMatch) {
188
      remove.push.apply(remove, others)
189
      return cb()
190
    }
191

    
192
    if (regMatch) {
193
      var what = name + "@" + regMatch
194
      // where is /path/to/node_modules/foo/node_modules/bar
195
      // for package "bar", but we need it to be just
196
      // /path/to/node_modules/foo
197
      where = where.split(/\/node_modules\//)
198
      where.pop()
199
      where = where.join("/node_modules/")
200
      remove.push.apply(remove, others)
201

    
202
      return npm.commands.install(where, what, cb)
203
    }
204

    
205
    // hrm?
206
    return cb(new Error("danger zone\n" + name + " " +
207
                        + regMatch + " " + locMatch))
208

    
209
  }, function (er, installed) {
210
    if (er) return cb(er)
211
    asyncMap(remove, rimraf, function (er) {
212
      if (er) return cb(er)
213
      remove.forEach(function (r) {
214
        log.info("rm", r)
215
      })
216
      dedupe_(dir, filter, unavoidable, false, silent, cb)
217
    })
218
  })
219
}
220

    
221
function findVersions (npm, summary, cb) {
222
  // now, for each item in the summary, try to find the maximum version
223
  // that will satisfy all the ranges.  next step is to install it at
224
  // the specified location.
225
  asyncMap(summary, function (item, cb) {
226
    var name = item[0]
227
    var data = item[1]
228
    var loc = data.loc
229
    var locs = data.locs.filter(function (l) {
230
      return l !== loc
231
    })
232

    
233
    // not actually a dupe, or perhaps all the other copies were
234
    // children of a dupe, so this'll maybe be picked up later.
235
    if (locs.length === 0) {
236
      return cb(null, [])
237
    }
238

    
239
    // { <folder>: <version> }
240
    var has = data.has
241

    
242
    // the versions that we already have.
243
    // if one of these is ok, then prefer to use that.
244
    // otherwise, try fetching from the registry.
245
    var versions = data.versions
246

    
247
    var ranges = data.ranges
248
    npm.registry.get(name, function (er, data) {
249
      var regVersions = er ? [] : Object.keys(data.versions)
250
      var locMatch = bestMatch(versions, ranges)
251
      var regMatch = bestMatch(regVersions, ranges)
252

    
253
      cb(null, [[name, has, loc, locMatch, regMatch, locs]])
254
    })
255
  }, cb)
256
}
257

    
258
function bestMatch (versions, ranges) {
259
  return versions.filter(function (v) {
260
    return !ranges.some(function (r) {
261
      return !semver.satisfies(v, r)
262
    })
263
  }).sort(semver.compare).pop()
264
}
265

    
266

    
267
function readInstalled (dir, counter, parent, cb) {
268
  var pkg, children, realpath
269

    
270
  fs.realpath(dir, function (er, rp) {
271
    realpath = rp
272
    next()
273
  })
274

    
275
  readJson(path.resolve(dir, "package.json"), function (er, data) {
276
    if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er)
277
    if (er) return cb() // not a package, probably.
278
    counter[data.name] = counter[data.name] || 0
279
    counter[data.name]++
280
    pkg =
281
      { _id: data._id
282
      , name: data.name
283
      , version: data.version
284
      , dependencies: data.dependencies || {}
285
      , optionalDependencies: data.optionalDependencies || {}
286
      , devDependencies: data.devDependencies || {}
287
      , bundledDependencies: data.bundledDependencies || []
288
      , path: dir
289
      , realPath: dir
290
      , children: {}
291
      , parent: parent
292
      , family: Object.create(parent ? parent.family : null)
293
      , unavoidable: false
294
      , duplicate: false
295
      }
296
    if (parent) {
297
      parent.children[data.name] = pkg
298
      parent.family[data.name] = pkg
299
    }
300
    next()
301
  })
302

    
303
  fs.readdir(path.resolve(dir, "node_modules"), function (er, c) {
304
    children = c || [] // error is ok, just means no children.
305
    children = children.filter(function (p) {
306
      return !p.match(/^[\._-]/)
307
    })
308
    next()
309
  })
310

    
311
  function next () {
312
    if (!children || !pkg || !realpath) return
313

    
314
    // ignore devDependencies.  Just leave them where they are.
315
    children = children.filter(function (c) {
316
      return !pkg.devDependencies.hasOwnProperty(c)
317
    })
318

    
319
    pkg.realPath = realpath
320
    if (pkg.realPath !== pkg.path) children = []
321
    var d = path.resolve(dir, "node_modules")
322
    asyncMap(children, function (child, cb) {
323
      readInstalled(path.resolve(d, child), counter, pkg, cb)
324
    }, function (er) {
325
      cb(er, pkg, counter)
326
    })
327
  }
328
}
329

    
330
function whoDepends (pkg) {
331
  var start = pkg.parent || pkg
332
  return whoDepends_(pkg, [], start)
333
}
334

    
335
function whoDepends_ (pkg, who, test) {
336
  if (test !== pkg &&
337
      test.dependencies[pkg.name] &&
338
      test.family[pkg.name] === pkg) {
339
    who.push(test)
340
  }
341
  Object.keys(test.children).forEach(function (n) {
342
    whoDepends_(pkg, who, test.children[n])
343
  })
344
  return who
345
}
346