• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2var npm = require('../npm.js')
3var validate = require('aproba')
4var npa = require('npm-package-arg')
5var flattenTree = require('./flatten-tree.js')
6var isOnlyDev = require('./is-only-dev.js')
7var log = require('npmlog')
8var path = require('path')
9var ssri = require('ssri')
10var moduleName = require('../utils/module-name.js')
11var isOnlyOptional = require('./is-only-optional.js')
12
13// we don't use get-requested because we're operating on files on disk, and
14// we don't want to extrapolate from what _should_ be there.
15function pkgRequested (pkg) {
16  return pkg._requested || (pkg._resolved && npa(pkg._resolved)) || (pkg._from && npa(pkg._from))
17}
18
19function nonRegistrySource (requested) {
20  if (fromGit(requested)) return true
21  if (fromLocal(requested)) return true
22  if (fromRemote(requested)) return true
23  return false
24}
25
26function fromRemote (requested) {
27  if (requested.type === 'remote') return true
28}
29
30function fromLocal (requested) {
31  // local is an npm@3 type that meant "file"
32  if (requested.type === 'file' || requested.type === 'directory' || requested.type === 'local') return true
33  return false
34}
35
36function fromGit (requested) {
37  if (requested.type === 'hosted' || requested.type === 'git') return true
38  return false
39}
40
41function pkgIntegrity (pkg) {
42  try {
43    // dist is provided by the registry
44    var sri = (pkg.dist && pkg.dist.integrity) ||
45              // _integrity is provided by pacote
46              pkg._integrity ||
47              // _shasum is legacy
48              (pkg._shasum && ssri.fromHex(pkg._shasum, 'sha1').toString())
49    if (!sri) return
50    var integrity = ssri.parse(sri)
51    if (Object.keys(integrity).length === 0) return
52    return integrity
53  } catch (ex) {
54
55  }
56}
57
58function sriMatch (aa, bb) {
59  if (!aa || !bb) return false
60  for (let algo of Object.keys(aa)) {
61    if (!bb[algo]) continue
62    for (let aaHash of aa[algo]) {
63      for (let bbHash of bb[algo]) {
64        return aaHash.digest === bbHash.digest
65      }
66    }
67  }
68  return false
69}
70
71function pkgAreEquiv (aa, bb) {
72  // coming in we know they share a path…
73
74  // if one is inside a link and the other is not, then they are not equivalent
75  // this happens when we're replacing a linked dep with a non-linked version
76  if (aa.isInLink !== bb.isInLink) return false
77  // if they share package metadata _identity_, they're the same thing
78  if (aa.package === bb.package) return true
79  // if they share integrity information, they're the same thing
80  var aaIntegrity = pkgIntegrity(aa.package)
81  var bbIntegrity = pkgIntegrity(bb.package)
82  if (aaIntegrity || bbIntegrity) return sriMatch(aaIntegrity, bbIntegrity)
83
84  // if they're links and they share the same target, they're the same thing
85  if (aa.isLink && bb.isLink) return aa.realpath === bb.realpath
86
87  // if we can't determine both their sources then we have no way to know
88  // if they're the same thing, so we have to assume they aren't
89  var aaReq = pkgRequested(aa.package)
90  var bbReq = pkgRequested(bb.package)
91  if (!aaReq || !bbReq) return false
92
93  if (fromGit(aaReq) && fromGit(bbReq)) {
94    // if both are git and share a _resolved specifier (one with the
95    // comittish replaced by a commit hash) then they're the same
96    return aa.package._resolved && bb.package._resolved &&
97           aa.package._resolved === bb.package._resolved
98  }
99
100  // we have to give up trying to find matches for non-registry sources at this point…
101  if (nonRegistrySource(aaReq) || nonRegistrySource(bbReq)) return false
102
103  // finally, if they ARE a registry source then version matching counts
104  return aa.package.version === bb.package.version
105}
106
107function pushAll (aa, bb) {
108  Array.prototype.push.apply(aa, bb)
109}
110
111module.exports = function (oldTree, newTree, differences, log, next) {
112  validate('OOAOF', arguments)
113  pushAll(differences, sortActions(diffTrees(oldTree, newTree)))
114  log.finish()
115  next()
116}
117
118function isNotTopOrExtraneous (node) {
119  return !node.isTop && !node.userRequired && !node.existing
120}
121
122var sortActions = module.exports.sortActions = function (differences) {
123  var actions = {}
124  differences.forEach(function (action) {
125    var child = action[1]
126    actions[child.location] = action
127  })
128
129  var sorted = []
130  var added = {}
131
132  var sortedlocs = Object.keys(actions).sort(sortByLocation)
133
134  // We're going to sort the actions taken on top level dependencies first, before
135  // considering the order of transitive deps. Because we're building our list
136  // from the bottom up, this means we will return a list with top level deps LAST.
137  // This is important in terms of keeping installations as consistent as possible
138  // as folks add new dependencies.
139  var toplocs = sortedlocs.filter(function (location) {
140    var mod = actions[location][1]
141    if (!mod.requiredBy) return true
142    // If this module is required by any non-top level module
143    // or by any extraneous module, eg user requested or existing
144    // then we don't want to give this priority sorting.
145    return !mod.requiredBy.some(isNotTopOrExtraneous)
146  })
147
148  toplocs.concat(sortedlocs).forEach(function (location) {
149    sortByDeps(actions[location])
150  })
151
152  function sortByLocation (aa, bb) {
153    return bb.localeCompare(aa)
154  }
155  function sortModuleByLocation (aa, bb) {
156    return sortByLocation(aa && aa.location, bb && bb.location)
157  }
158  function sortByDeps (action) {
159    var mod = action[1]
160    if (added[mod.location]) return
161    added[mod.location] = action
162    if (!mod.requiredBy) mod.requiredBy = []
163    mod.requiredBy.sort(sortModuleByLocation).forEach(function (mod) {
164      if (actions[mod.location]) sortByDeps(actions[mod.location])
165    })
166    sorted.unshift(action)
167  }
168
169  // safety net, anything excluded above gets tacked on the end
170  differences.forEach((_) => {
171    if (sorted.indexOf(_) === -1) sorted.push(_)
172  })
173
174  return sorted
175}
176
177function setAction (differences, action, pkg) {
178  differences.push([action, pkg])
179}
180
181var diffTrees = module.exports._diffTrees = function (oldTree, newTree) {
182  validate('OO', arguments)
183  var differences = []
184  var flatOldTree = flattenTree(oldTree)
185  var flatNewTree = flattenTree(newTree)
186  var toRemove = {}
187  var toRemoveByName = {}
188
189  // Build our tentative remove list.  We don't add remove actions yet
190  // because we might resuse them as part of a move.
191  Object.keys(flatOldTree).forEach(function (flatname) {
192    if (flatname === '/') return
193    if (flatNewTree[flatname]) return
194    var pkg = flatOldTree[flatname]
195    if (pkg.isInLink && /^[.][.][/\\]/.test(path.relative(newTree.realpath, pkg.realpath))) return
196
197    toRemove[flatname] = pkg
198    var name = moduleName(pkg)
199    if (!toRemoveByName[name]) toRemoveByName[name] = []
200    toRemoveByName[name].push({flatname: flatname, pkg: pkg})
201  })
202
203  // generate our add/update/move actions
204  Object.keys(flatNewTree).forEach(function (flatname) {
205    if (flatname === '/') return
206    var pkg = flatNewTree[flatname]
207    var oldPkg = pkg.oldPkg = flatOldTree[flatname]
208    if (oldPkg) {
209      // if the versions are equivalent then we don't need to update… unless
210      // the user explicitly asked us to.
211      if (!pkg.userRequired && pkgAreEquiv(oldPkg, pkg)) return
212      setAction(differences, 'update', pkg)
213    } else {
214      var name = moduleName(pkg)
215      // find any packages we're removing that share the same name and are equivalent
216      var removing = (toRemoveByName[name] || []).filter((rm) => pkgAreEquiv(rm.pkg, pkg))
217      var bundlesOrFromBundle = pkg.fromBundle || pkg.package.bundleDependencies
218      // if we have any removes that match AND we're not working with a bundle then upgrade to a move
219      if (removing.length && !bundlesOrFromBundle) {
220        var toMv = removing.shift()
221        toRemoveByName[name] = toRemoveByName[name].filter((rm) => rm !== toMv)
222        pkg.fromPath = toMv.pkg.path
223        setAction(differences, 'move', pkg)
224        delete toRemove[toMv.flatname]
225      // we don't generate add actions for things found in links (which already exist on disk)
226      } else if (!pkg.isInLink || !(pkg.fromBundle && pkg.fromBundle.isLink)) {
227        setAction(differences, 'add', pkg)
228      }
229    }
230  })
231
232  // finally generate our remove actions from any not consumed by moves
233  Object
234    .keys(toRemove)
235    .map((flatname) => toRemove[flatname])
236    .forEach((pkg) => setAction(differences, 'remove', pkg))
237
238  return filterActions(differences)
239}
240
241function filterActions (differences) {
242  const includeOpt = npm.config.get('optional')
243  const includeDev = npm.config.get('dev') ||
244    (!/^prod(uction)?$/.test(npm.config.get('only')) && !npm.config.get('production')) ||
245    /^dev(elopment)?$/.test(npm.config.get('only')) ||
246    /^dev(elopment)?$/.test(npm.config.get('also'))
247  const includeProd = !/^dev(elopment)?$/.test(npm.config.get('only'))
248  if (includeProd && includeDev && includeOpt) return differences
249
250  log.silly('diff-trees', 'filtering actions:', 'includeDev', includeDev, 'includeProd', includeProd, 'includeOpt', includeOpt)
251  return differences.filter((diff) => {
252    const pkg = diff[1]
253    const pkgIsOnlyDev = isOnlyDev(pkg)
254    const pkgIsOnlyOpt = isOnlyOptional(pkg)
255    if (!includeProd && pkgIsOnlyDev) return true
256    if (includeDev && pkgIsOnlyDev) return true
257    if (includeProd && !pkgIsOnlyDev && (includeOpt || !pkgIsOnlyOpt)) return true
258    return false
259  })
260}
261