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.
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 |
|