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