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