1const { depth } = require('treeverse')
2
3const calcDepFlags = (tree, resetRoot = true) => {
4  if (resetRoot) {
5    tree.dev = false
6    tree.optional = false
7    tree.devOptional = false
8    tree.peer = false
9  }
10  const ret = depth({
11    tree,
12    visit: node => calcDepFlagsStep(node),
13    filter: node => node,
14    getChildren: (node, tree) =>
15      [...tree.edgesOut.values()].map(edge => edge.to),
16  })
17  return ret
18}
19
20const calcDepFlagsStep = (node) => {
21  // This rewalk is necessary to handle cases where devDep and optional
22  // or normal dependency graphs overlap deep in the dep graph.
23  // Since we're only walking through deps that are not already flagged
24  // as non-dev/non-optional, it's typically a very shallow traversal
25  node.extraneous = false
26  resetParents(node, 'extraneous')
27  resetParents(node, 'dev')
28  resetParents(node, 'peer')
29  resetParents(node, 'devOptional')
30  resetParents(node, 'optional')
31
32  // for links, map their hierarchy appropriately
33  if (node.isLink) {
34    node.target.dev = node.dev
35    node.target.optional = node.optional
36    node.target.devOptional = node.devOptional
37    node.target.peer = node.peer
38    return calcDepFlagsStep(node.target)
39  }
40
41  node.edgesOut.forEach(({ peer, optional, dev, to }) => {
42    // if the dep is missing, then its flags are already maximally unset
43    if (!to) {
44      return
45    }
46
47    // everything with any kind of edge into it is not extraneous
48    to.extraneous = false
49
50    // devOptional is the *overlap* of the dev and optional tree.
51    // however, for convenience and to save an extra rewalk, we leave
52    // it set when we are in *either* tree, and then omit it from the
53    // package-lock if either dev or optional are set.
54    const unsetDevOpt = !node.devOptional && !node.dev && !node.optional && !dev && !optional
55
56    // if we are not in the devOpt tree, then we're also not in
57    // either the dev or opt trees
58    const unsetDev = unsetDevOpt || !node.dev && !dev
59    const unsetOpt = unsetDevOpt || !node.optional && !optional
60    const unsetPeer = !node.peer && !peer
61
62    if (unsetPeer) {
63      unsetFlag(to, 'peer')
64    }
65
66    if (unsetDevOpt) {
67      unsetFlag(to, 'devOptional')
68    }
69
70    if (unsetDev) {
71      unsetFlag(to, 'dev')
72    }
73
74    if (unsetOpt) {
75      unsetFlag(to, 'optional')
76    }
77  })
78
79  return node
80}
81
82const resetParents = (node, flag) => {
83  if (node[flag]) {
84    return
85  }
86
87  for (let p = node; p && (p === node || p[flag]); p = p.resolveParent) {
88    p[flag] = false
89  }
90}
91
92// typically a short walk, since it only traverses deps that have the flag set.
93const unsetFlag = (node, flag) => {
94  if (node[flag]) {
95    node[flag] = false
96    depth({
97      tree: node,
98      visit: node => {
99        node.extraneous = node[flag] = false
100        if (node.isLink) {
101          node.target.extraneous = node.target[flag] = false
102        }
103      },
104      getChildren: node => {
105        const children = []
106        for (const edge of node.target.edgesOut.values()) {
107          if (edge.to && edge.to[flag] &&
108            (flag !== 'peer' && edge.type === 'peer' || edge.type === 'prod')
109          ) {
110            children.push(edge.to)
111          }
112        }
113        return children
114      },
115    })
116  }
117}
118
119module.exports = calcDepFlags
120