1// add and remove dependency specs to/from pkg manifest 2 3const log = require('proc-log') 4const localeCompare = require('@isaacs/string-locale-compare')('en') 5 6const add = ({ pkg, add, saveBundle, saveType }) => { 7 for (const { name, rawSpec } of add) { 8 let addSaveType = saveType 9 // if the user does not give us a type, we infer which type(s) 10 // to keep based on the same order of priority we do when 11 // building the tree as defined in the _loadDeps method of 12 // the node class. 13 if (!addSaveType) { 14 addSaveType = inferSaveType(pkg, name) 15 } 16 17 if (addSaveType === 'prod') { 18 // a production dependency can only exist as production (rpj ensures it 19 // doesn't coexist w/ optional) 20 deleteSubKey(pkg, 'devDependencies', name, 'dependencies') 21 deleteSubKey(pkg, 'peerDependencies', name, 'dependencies') 22 } else if (addSaveType === 'dev') { 23 // a dev dependency may co-exist as peer, or optional, but not production 24 deleteSubKey(pkg, 'dependencies', name, 'devDependencies') 25 } else if (addSaveType === 'optional') { 26 // an optional dependency may co-exist as dev (rpj ensures it doesn't 27 // coexist w/ prod) 28 deleteSubKey(pkg, 'peerDependencies', name, 'optionalDependencies') 29 } else { // peer or peerOptional is all that's left 30 // a peer dependency may coexist as dev 31 deleteSubKey(pkg, 'dependencies', name, 'peerDependencies') 32 deleteSubKey(pkg, 'optionalDependencies', name, 'peerDependencies') 33 } 34 35 const depType = saveTypeMap.get(addSaveType) 36 37 pkg[depType] = pkg[depType] || {} 38 if (rawSpec !== '*' || pkg[depType][name] === undefined) { 39 pkg[depType][name] = rawSpec 40 } 41 if (addSaveType === 'optional') { 42 // Affordance for previous npm versions that require this behaviour 43 pkg.dependencies = pkg.dependencies || {} 44 pkg.dependencies[name] = pkg.optionalDependencies[name] 45 } 46 47 if (addSaveType === 'peer' || addSaveType === 'peerOptional') { 48 const pdm = pkg.peerDependenciesMeta || {} 49 if (addSaveType === 'peer' && pdm[name] && pdm[name].optional) { 50 pdm[name].optional = false 51 } else if (addSaveType === 'peerOptional') { 52 pdm[name] = pdm[name] || {} 53 pdm[name].optional = true 54 pkg.peerDependenciesMeta = pdm 55 } 56 // peerDeps are often also a devDep, so that they can be tested when 57 // using package managers that don't auto-install peer deps 58 if (pkg.devDependencies && pkg.devDependencies[name] !== undefined) { 59 pkg.devDependencies[name] = pkg.peerDependencies[name] 60 } 61 } 62 63 if (saveBundle && addSaveType !== 'peer' && addSaveType !== 'peerOptional') { 64 // keep it sorted, keep it unique 65 const bd = new Set(pkg.bundleDependencies || []) 66 bd.add(name) 67 pkg.bundleDependencies = [...bd].sort(localeCompare) 68 } 69 } 70 71 return pkg 72} 73 74// Canonical source of both the map between saveType and where it correlates to 75// in the package, and the names of all our dependencies attributes 76const saveTypeMap = new Map([ 77 ['dev', 'devDependencies'], 78 ['optional', 'optionalDependencies'], 79 ['prod', 'dependencies'], 80 ['peerOptional', 'peerDependencies'], 81 ['peer', 'peerDependencies'], 82]) 83 84// Finds where the package is already in the spec and infers saveType from that 85const inferSaveType = (pkg, name) => { 86 for (const saveType of saveTypeMap.keys()) { 87 if (hasSubKey(pkg, saveTypeMap.get(saveType), name)) { 88 if ( 89 saveType === 'peerOptional' && 90 (!hasSubKey(pkg, 'peerDependenciesMeta', name) || 91 !pkg.peerDependenciesMeta[name].optional) 92 ) { 93 return 'peer' 94 } 95 return saveType 96 } 97 } 98 return 'prod' 99} 100 101const hasSubKey = (pkg, depType, name) => { 102 return pkg[depType] && Object.prototype.hasOwnProperty.call(pkg[depType], name) 103} 104 105// Removes a subkey and warns about it if it's being replaced 106const deleteSubKey = (pkg, depType, name, replacedBy) => { 107 if (hasSubKey(pkg, depType, name)) { 108 if (replacedBy) { 109 log.warn('idealTree', `Removing ${depType}.${name} in favor of ${replacedBy}.${name}`) 110 } 111 delete pkg[depType][name] 112 113 // clean up peerDepsMeta if we are removing something from peerDependencies 114 if (depType === 'peerDependencies' && pkg.peerDependenciesMeta) { 115 delete pkg.peerDependenciesMeta[name] 116 if (!Object.keys(pkg.peerDependenciesMeta).length) { 117 delete pkg.peerDependenciesMeta 118 } 119 } 120 121 if (!Object.keys(pkg[depType]).length) { 122 delete pkg[depType] 123 } 124 } 125} 126 127const rm = (pkg, rm) => { 128 for (const depType of new Set(saveTypeMap.values())) { 129 for (const name of rm) { 130 deleteSubKey(pkg, depType, name) 131 } 132 } 133 if (pkg.bundleDependencies) { 134 pkg.bundleDependencies = pkg.bundleDependencies 135 .filter(name => !rm.includes(name)) 136 if (!pkg.bundleDependencies.length) { 137 delete pkg.bundleDependencies 138 } 139 } 140 return pkg 141} 142 143module.exports = { add, rm, saveTypeMap, hasSubKey } 144