1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
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 semver = require("semver")
|
19 | var rm = require("./utils/gently-rm.js")
|
20 | var log = require("npmlog")
|
21 | var npm = require("./npm.js")
|
22 |
|
23 | module.exports = dedupe
|
24 |
|
25 | dedupe.usage = "npm dedupe [pkg pkg...]"
|
26 |
|
27 | function dedupe (args, silent, cb) {
|
28 | if (typeof silent === "function") cb = silent, silent = false
|
29 | var dryrun = false
|
30 | if (npm.command.match(/^find/)) dryrun = true
|
31 | return dedupe_(npm.prefix, args, {}, dryrun, silent, cb)
|
32 | }
|
33 |
|
34 | function dedupe_ (dir, filter, unavoidable, dryrun, silent, cb) {
|
35 | readInstalled(path.resolve(dir), {}, null, function (er, data, counter) {
|
36 | if (er) {
|
37 | return cb(er)
|
38 | }
|
39 |
|
40 | if (!data) {
|
41 | return cb()
|
42 | }
|
43 |
|
44 |
|
45 | var dupes = Object.keys(counter || {}).filter(function (k) {
|
46 | if (filter.length && -1 === filter.indexOf(k)) return false
|
47 | return counter[k] > 1 && !unavoidable[k]
|
48 | }).reduce(function (s, k) {
|
49 | s[k] = []
|
50 | return s
|
51 | }, {})
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | ;(function U (obj) {
|
58 | if (unavoidable[obj.name]) {
|
59 | obj.unavoidable = true
|
60 | }
|
61 | if (obj.parent && obj.parent.unavoidable) {
|
62 | obj.unavoidable = true
|
63 | }
|
64 | Object.keys(obj.children).forEach(function (k) {
|
65 | U(obj.children[k])
|
66 | })
|
67 | })
|
68 |
|
69 |
|
70 | ;(function C (obj) {
|
71 | if (dupes[obj.name] && !obj.unavoidable) {
|
72 | dupes[obj.name].push(obj)
|
73 | obj.duplicate = true
|
74 | }
|
75 | obj.dependents = whoDepends(obj)
|
76 | Object.keys(obj.children).forEach(function (k) {
|
77 | C(obj.children[k])
|
78 | })
|
79 | })(data)
|
80 |
|
81 | if (dryrun) {
|
82 | var k = Object.keys(dupes)
|
83 | if (!k.length) return cb()
|
84 | return npm.commands.ls(k, silent, cb)
|
85 | }
|
86 |
|
87 | var summary = Object.keys(dupes).map(function (n) {
|
88 | return [n, dupes[n].filter(function (d) {
|
89 | return d && d.parent && !d.parent.duplicate && !d.unavoidable
|
90 | }).map(function M (d) {
|
91 | return [d.path, d.version, d.dependents.map(function (k) {
|
92 | return [k.path, k.version, k.dependencies[d.name] || ""]
|
93 | })]
|
94 | })]
|
95 | }).map(function (item) {
|
96 | var name = item[0]
|
97 | var set = item[1]
|
98 |
|
99 | var ranges = set.map(function (i) {
|
100 | return i[2].map(function (d) {
|
101 | return d[2]
|
102 | })
|
103 | }).reduce(function (l, r) {
|
104 | return l.concat(r)
|
105 | }, []).map(function (v, i, set) {
|
106 | if (set.indexOf(v) !== i) return false
|
107 | return v
|
108 | }).filter(function (v) {
|
109 | return v !== false
|
110 | })
|
111 |
|
112 | var locs = set.map(function (i) {
|
113 | return i[0]
|
114 | })
|
115 |
|
116 | var versions = set.map(function (i) {
|
117 | return i[1]
|
118 | }).filter(function (v, i, set) {
|
119 | return set.indexOf(v) === i
|
120 | })
|
121 |
|
122 | var has = set.map(function (i) {
|
123 | return [i[0], i[1]]
|
124 | }).reduce(function (set, kv) {
|
125 | set[kv[0]] = kv[1]
|
126 | return set
|
127 | }, {})
|
128 |
|
129 | var loc = locs.length ? locs.reduce(function (a, b) {
|
130 |
|
131 |
|
132 |
|
133 | var nmReg = new RegExp("\\" + path.sep + "node_modules\\" + path.sep)
|
134 | a = a.split(nmReg)
|
135 | b = b.split(nmReg)
|
136 | var name = a.pop()
|
137 | b.pop()
|
138 |
|
139 |
|
140 | var res = []
|
141 | for (var i = 0, al = a.length, bl = b.length; i < al && i < bl && a[i] === b[i]; i++);
|
142 | return a.slice(0, i).concat(name).join(path.sep + "node_modules" + path.sep)
|
143 | }) : undefined
|
144 |
|
145 | return [item[0], { item: item
|
146 | , ranges: ranges
|
147 | , locs: locs
|
148 | , loc: loc
|
149 | , has: has
|
150 | , versions: versions
|
151 | }]
|
152 | }).filter(function (i) {
|
153 | return i[1].loc
|
154 | })
|
155 |
|
156 | findVersions(npm, summary, function (er, set) {
|
157 | if (er) return cb(er)
|
158 | if (!set.length) return cb()
|
159 | installAndRetest(set, filter, dir, unavoidable, silent, cb)
|
160 | })
|
161 | })
|
162 | }
|
163 |
|
164 | function installAndRetest (set, filter, dir, unavoidable, silent, cb) {
|
165 |
|
166 | var remove = []
|
167 |
|
168 | asyncMap(set, function (item, cb) {
|
169 |
|
170 | var name = item[0]
|
171 | var has = item[1]
|
172 | var where = item[2]
|
173 | var locMatch = item[3]
|
174 | var regMatch = item[4]
|
175 | var others = item[5]
|
176 |
|
177 |
|
178 | if (!locMatch && !regMatch) {
|
179 | log.warn("unavoidable conflict", item[0], item[1])
|
180 | log.warn("unavoidable conflict", "Not de-duplicating")
|
181 | unavoidable[item[0]] = true
|
182 | return cb()
|
183 | }
|
184 |
|
185 |
|
186 | if (locMatch && has[where] === locMatch) {
|
187 | remove.push.apply(remove, others)
|
188 | return cb()
|
189 | }
|
190 |
|
191 | if (regMatch) {
|
192 | var what = name + "@" + regMatch
|
193 |
|
194 |
|
195 |
|
196 | var nmReg = new RegExp("\\" + path.sep + "node_modules\\" + path.sep)
|
197 | where = where.split(nmReg)
|
198 | where.pop()
|
199 | where = where.join(path.sep + "node_modules" + path.sep)
|
200 | remove.push.apply(remove, others)
|
201 |
|
202 | return npm.commands.install(where, what, cb)
|
203 | }
|
204 |
|
205 |
|
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, rm, 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 |
|
223 |
|
224 |
|
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 |
|
234 |
|
235 | if (locs.length === 0) {
|
236 | return cb(null, [])
|
237 | }
|
238 |
|
239 |
|
240 | var has = data.has
|
241 |
|
242 |
|
243 |
|
244 |
|
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;
|
252 | var tag = npm.config.get("tag")
|
253 | var distTag = data["dist-tags"] && data["dist-tags"][tag]
|
254 | if (distTag && data.versions[distTag] && matches(distTag, ranges)) {
|
255 | regMatch = distTag
|
256 | } else {
|
257 | regMatch = bestMatch(regVersions, ranges)
|
258 | }
|
259 |
|
260 | cb(null, [[name, has, loc, locMatch, regMatch, locs]])
|
261 | })
|
262 | }, cb)
|
263 | }
|
264 |
|
265 | function matches (version, ranges) {
|
266 | return !ranges.some(function (r) {
|
267 | return !semver.satisfies(version, r, true)
|
268 | })
|
269 | }
|
270 |
|
271 | function bestMatch (versions, ranges) {
|
272 | return versions.filter(function (v) {
|
273 | return matches(v, ranges)
|
274 | }).sort(semver.compareLoose).pop()
|
275 | }
|
276 |
|
277 |
|
278 | function readInstalled (dir, counter, parent, cb) {
|
279 | var pkg, children, realpath
|
280 |
|
281 | fs.realpath(dir, function (er, rp) {
|
282 | realpath = rp
|
283 | next()
|
284 | })
|
285 |
|
286 | readJson(path.resolve(dir, "package.json"), function (er, data) {
|
287 | if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er)
|
288 | if (er) return cb()
|
289 | counter[data.name] = counter[data.name] || 0
|
290 | counter[data.name]++
|
291 | pkg =
|
292 | { _id: data._id
|
293 | , name: data.name
|
294 | , version: data.version
|
295 | , dependencies: data.dependencies || {}
|
296 | , optionalDependencies: data.optionalDependencies || {}
|
297 | , devDependencies: data.devDependencies || {}
|
298 | , bundledDependencies: data.bundledDependencies || []
|
299 | , path: dir
|
300 | , realPath: dir
|
301 | , children: {}
|
302 | , parent: parent
|
303 | , family: Object.create(parent ? parent.family : null)
|
304 | , unavoidable: false
|
305 | , duplicate: false
|
306 | }
|
307 | if (parent) {
|
308 | parent.children[data.name] = pkg
|
309 | parent.family[data.name] = pkg
|
310 | }
|
311 | next()
|
312 | })
|
313 |
|
314 | fs.readdir(path.resolve(dir, "node_modules"), function (er, c) {
|
315 | children = c || []
|
316 | children = children.filter(function (p) {
|
317 | return !p.match(/^[\._-]/)
|
318 | })
|
319 | next()
|
320 | })
|
321 |
|
322 | function next () {
|
323 | if (!children || !pkg || !realpath) return
|
324 |
|
325 |
|
326 | children = children.filter(function (c) {
|
327 | return !pkg.devDependencies.hasOwnProperty(c)
|
328 | })
|
329 |
|
330 | pkg.realPath = realpath
|
331 | if (pkg.realPath !== pkg.path) children = []
|
332 | var d = path.resolve(dir, "node_modules")
|
333 | asyncMap(children, function (child, cb) {
|
334 | readInstalled(path.resolve(d, child), counter, pkg, cb)
|
335 | }, function (er) {
|
336 | cb(er, pkg, counter)
|
337 | })
|
338 | }
|
339 | }
|
340 |
|
341 | function whoDepends (pkg) {
|
342 | var start = pkg.parent || pkg
|
343 | return whoDepends_(pkg, [], start)
|
344 | }
|
345 |
|
346 | function whoDepends_ (pkg, who, test) {
|
347 | if (test !== pkg &&
|
348 | test.dependencies[pkg.name] &&
|
349 | test.family[pkg.name] === pkg) {
|
350 | who.push(test)
|
351 | }
|
352 | Object.keys(test.children).forEach(function (n) {
|
353 | whoDepends_(pkg, who, test.children[n])
|
354 | })
|
355 | return who
|
356 | }
|
357 |
|