|
1 | | -const { depth } = require('treeverse') |
2 | | - |
| 1 | +// Dep flag (dev, peer, etc.) calculation requires default or reset flags. |
| 2 | +// Flags are true by default and are unset to false as we walk deps. |
| 3 | +// We iterate outward edges looking for dep flags that can |
| 4 | +// be unset based on the current nodes flags and edge type. |
| 5 | +// Examples: |
| 6 | +// - a non-optional node with a non-optional edge out, the edge node should not be optional |
| 7 | +// - a non-peer node with a non-peer edge out, the edge node should not be peer |
| 8 | +// If a node is changed, we add to the queue and continue until no more changes. |
| 9 | +// Flags that remain after all this unsetting should be valid. |
| 10 | +// Examples: |
| 11 | +// - a node still flagged optional must only be reachable via optional edges |
| 12 | +// - a node still flagged peer must only be reachable via peer edges |
3 | 13 | const calcDepFlags = (tree, resetRoot = true) => { |
4 | 14 | if (resetRoot) { |
5 | | - tree.dev = false |
6 | | - tree.optional = false |
7 | | - tree.devOptional = false |
8 | | - tree.peer = false |
| 15 | + tree.unsetDepFlags() |
9 | 16 | } |
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 | | - |
20 | | -const 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 | | - |
26 | | - node.extraneous = false |
27 | | - resetParents(node, 'extraneous') |
28 | | - resetParents(node, 'dev') |
29 | | - resetParents(node, 'peer') |
30 | | - resetParents(node, 'devOptional') |
31 | | - resetParents(node, 'optional') |
32 | | - |
33 | | - // for links, map their hierarchy appropriately |
34 | | - if (node.isLink) { |
35 | | - // node.target can be null, we check to ensure it's not null before proceeding |
36 | | - if (node.target == null) { |
37 | | - return node |
38 | | - } |
39 | | - node.target.dev = node.dev |
40 | | - node.target.optional = node.optional |
41 | | - node.target.devOptional = node.devOptional |
42 | | - node.target.peer = node.peer |
43 | | - return calcDepFlagsStep(node.target) |
44 | | - } |
45 | | - |
46 | | - node.edgesOut.forEach(({ peer, optional, dev, to }) => { |
47 | | - // if the dep is missing, then its flags are already maximally unset |
48 | | - if (!to) { |
49 | | - return |
50 | | - } |
51 | | - // everything with any kind of edge into it is not extraneous |
52 | | - to.extraneous = false |
53 | | - |
54 | | - // If this is a peer edge, mark the target as peer |
55 | | - if (peer) { |
56 | | - to.peer = true |
57 | | - } else if (to.peer && !hasIncomingPeerEdge(to)) { |
58 | | - unsetFlag(to, 'peer') |
59 | | - } |
60 | 17 |
|
61 | | - // devOptional is the *overlap* of the dev and optional tree. |
62 | | - // however, for convenience and to save an extra rewalk, we leave |
63 | | - // it set when we are in *either* tree, and then omit it from the |
64 | | - // package-lock if either dev or optional are set. |
65 | | - const unsetDevOpt = !node.devOptional && !node.dev && !node.optional && !dev && !optional |
| 18 | + const seen = new Set() |
| 19 | + const queue = [tree] |
66 | 20 |
|
67 | | - // if we are not in the devOpt tree, then we're also not in |
68 | | - // either the dev or opt trees |
69 | | - const unsetDev = unsetDevOpt || !node.dev && !dev |
70 | | - const unsetOpt = unsetDevOpt || !node.optional && !optional |
| 21 | + let node |
| 22 | + while (node = queue.pop()) { |
| 23 | + seen.add(node) |
71 | 24 |
|
72 | | - if (unsetDevOpt) { |
73 | | - unsetFlag(to, 'devOptional') |
| 25 | + // Unset extraneous from all parents to avoid removal of children. |
| 26 | + if (!node.extraneous) { |
| 27 | + for (let n = node.resolveParent; n?.extraneous; n = n.resolveParent) { |
| 28 | + n.extraneous = false |
| 29 | + } |
74 | 30 | } |
75 | 31 |
|
76 | | - if (unsetDev) { |
77 | | - unsetFlag(to, 'dev') |
| 32 | + // for links, map their hierarchy appropriately |
| 33 | + if (node.isLink) { |
| 34 | + // node.target can be null, we check to ensure it's not null before proceeding |
| 35 | + if (node.target == null) { |
| 36 | + continue |
| 37 | + } |
| 38 | + node.target.dev = node.dev |
| 39 | + node.target.optional = node.optional |
| 40 | + node.target.devOptional = node.devOptional |
| 41 | + node.target.peer = node.peer |
| 42 | + node.target.extraneous = node.extraneous |
| 43 | + queue.push(node.target) |
| 44 | + continue |
78 | 45 | } |
79 | 46 |
|
80 | | - if (unsetOpt) { |
81 | | - unsetFlag(to, 'optional') |
82 | | - } |
83 | | - }) |
84 | | - |
85 | | - return node |
86 | | -} |
87 | | - |
88 | | -const hasIncomingPeerEdge = (node) => { |
89 | | - const target = node.isLink && node.target ? node.target : node |
90 | | - for (const edge of target.edgesIn) { |
91 | | - if (edge.type === 'peer') { |
92 | | - return true |
| 47 | + for (const { peer, optional, dev, to } of node.edgesOut.values()) { |
| 48 | + // if the dep is missing, then its flags are already maximally unset |
| 49 | + if (!to) { |
| 50 | + continue |
| 51 | + } |
| 52 | + |
| 53 | + let changed = false |
| 54 | + |
| 55 | + // only optional peer dependencies should stay extraneous |
| 56 | + if (to.extraneous && !node.extraneous && !(peer && optional)) { |
| 57 | + to.extraneous = false |
| 58 | + changed = true |
| 59 | + } |
| 60 | + |
| 61 | + if (to.dev && !node.dev && !dev) { |
| 62 | + to.dev = false |
| 63 | + changed = true |
| 64 | + } |
| 65 | + |
| 66 | + if (to.optional && !node.optional && !optional) { |
| 67 | + to.optional = false |
| 68 | + changed = true |
| 69 | + } |
| 70 | + |
| 71 | + // devOptional is the *overlap* of the dev and optional tree. |
| 72 | + // A node may be depended on by separate dev and optional nodes. |
| 73 | + // It SHOULD NOT be removed when pruning dev OR optional. |
| 74 | + // It SHOULD be removed when pruning dev AND optional. |
| 75 | + // We only unset here if a node is not dev AND not optional because |
| 76 | + // if we did unset, it would prevent any overlap deeper in the tree. |
| 77 | + // We correct this later by removing if dev OR optional is set. |
| 78 | + if (to.devOptional && !node.devOptional && !node.dev && !node.optional && !dev && !optional) { |
| 79 | + to.devOptional = false |
| 80 | + changed = true |
| 81 | + } |
| 82 | + |
| 83 | + if (to.peer && !node.peer && !peer) { |
| 84 | + to.peer = false |
| 85 | + changed = true |
| 86 | + } |
| 87 | + |
| 88 | + if (changed) { |
| 89 | + queue.push(to) |
| 90 | + } |
93 | 91 | } |
94 | 92 | } |
95 | | - return false |
96 | | -} |
97 | 93 |
|
98 | | -const resetParents = (node, flag) => { |
99 | | - if (node[flag]) { |
100 | | - return |
101 | | - } |
102 | | - |
103 | | - for (let p = node; p && (p === node || p[flag]); p = p.resolveParent) { |
104 | | - p[flag] = false |
105 | | - } |
106 | | -} |
107 | | - |
108 | | -// typically a short walk, since it only traverses deps that have the flag set. |
109 | | -const unsetFlag = (node, flag) => { |
110 | | - if (node[flag]) { |
111 | | - node[flag] = false |
112 | | - depth({ |
113 | | - tree: node, |
114 | | - visit: node => { |
115 | | - node.extraneous = node[flag] = false |
116 | | - if (node.isLink && node.target) { |
117 | | - node.target.extraneous = node.target[flag] = false |
118 | | - } |
119 | | - }, |
120 | | - getChildren: node => { |
121 | | - const children = [] |
122 | | - const targetNode = node.isLink && node.target ? node.target : node |
123 | | - for (const edge of targetNode.edgesOut.values()) { |
124 | | - if (edge.to?.[flag]) { |
125 | | - // For the peer flag, only follow peer edges to unset the flag |
126 | | - // Don't propagate peer flag through prod/dev/optional edges |
127 | | - if (flag === 'peer') { |
128 | | - if (edge.type === 'peer') { |
129 | | - children.push(edge.to) |
130 | | - } |
131 | | - } else { |
132 | | - // For other flags, follow prod edges (and peer edges for non-peer flags) |
133 | | - if (edge.type === 'prod' || edge.type === 'peer') { |
134 | | - children.push(edge.to) |
135 | | - } |
136 | | - } |
137 | | - } |
138 | | - } |
139 | | - return children |
140 | | - }, |
141 | | - }) |
| 94 | + // Remove incorrect devOptional flags now that we have walked all deps. |
| 95 | + seen.delete(tree) |
| 96 | + for (const node of seen.values()) { |
| 97 | + if (node.devOptional && (node.dev || node.optional)) { |
| 98 | + node.devOptional = false |
| 99 | + } |
142 | 100 | } |
143 | 101 | } |
144 | 102 |
|
|
0 commit comments