diff-trees.js 9.1 KB

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