index.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. "use strict";
  2. const npa = require("npm-package-arg");
  3. const { ValidationError } = require("@lerna/validation-error");
  4. const { CyclicPackageGraphNode } = require("./lib/cyclic-package-graph-node");
  5. const { PackageGraphNode } = require("./lib/package-graph-node");
  6. const { reportCycles } = require("./lib/report-cycles");
  7. /** @typedef {import("./lib/package-graph-node").PackageGraphNode} PackageGraphNode */
  8. /**
  9. * A graph of packages in the current project.
  10. *
  11. * @extends {Map<string, PackageGraphNode>}
  12. */
  13. class PackageGraph extends Map {
  14. /**
  15. * @param {import("@lerna/package").Package[]} packages An array of Packages to build the graph out of.
  16. * @param {'allDependencies'|'dependencies'} [graphType]
  17. * Pass "dependencies" to create a graph of only dependencies,
  18. * excluding the devDependencies that would normally be included.
  19. * @param {boolean} [forceLocal] Force all local dependencies to be linked.
  20. */
  21. constructor(packages, graphType = "allDependencies", forceLocal) {
  22. super(packages.map((pkg) => [pkg.name, new PackageGraphNode(pkg)]));
  23. if (packages.length !== this.size) {
  24. // weed out the duplicates
  25. const seen = new Map();
  26. for (const { name, location } of packages) {
  27. if (seen.has(name)) {
  28. seen.get(name).push(location);
  29. } else {
  30. seen.set(name, [location]);
  31. }
  32. }
  33. for (const [name, locations] of seen) {
  34. if (locations.length > 1) {
  35. throw new ValidationError(
  36. "ENAME",
  37. [`Package name "${name}" used in multiple packages:`, ...locations].join("\n\t")
  38. );
  39. }
  40. }
  41. }
  42. this.forEach((currentNode, currentName) => {
  43. const graphDependencies =
  44. graphType === "dependencies"
  45. ? Object.assign({}, currentNode.pkg.optionalDependencies, currentNode.pkg.dependencies)
  46. : Object.assign(
  47. {},
  48. currentNode.pkg.devDependencies,
  49. currentNode.pkg.optionalDependencies,
  50. currentNode.pkg.dependencies
  51. );
  52. Object.keys(graphDependencies).forEach((depName) => {
  53. const depNode = this.get(depName);
  54. // Yarn decided to ignore https://github.com/npm/npm/pull/15900 and implemented "link:"
  55. // As they apparently have no intention of being compatible, we have to do it for them.
  56. // @see https://github.com/yarnpkg/yarn/issues/4212
  57. const spec = graphDependencies[depName].replace(/^link:/, "file:");
  58. const resolved = npa.resolve(depName, spec, currentNode.location);
  59. if (!depNode) {
  60. // it's an external dependency, store the resolution and bail
  61. return currentNode.externalDependencies.set(depName, resolved);
  62. }
  63. if (forceLocal || resolved.fetchSpec === depNode.location || depNode.satisfies(resolved)) {
  64. // a local file: specifier OR a matching semver
  65. currentNode.localDependencies.set(depName, resolved);
  66. depNode.localDependents.set(currentName, currentNode);
  67. } else {
  68. // non-matching semver of a local dependency
  69. currentNode.externalDependencies.set(depName, resolved);
  70. }
  71. });
  72. });
  73. }
  74. get rawPackageList() {
  75. return Array.from(this.values()).map((node) => node.pkg);
  76. }
  77. /**
  78. * Takes a list of Packages and returns a list of those same Packages with any Packages
  79. * they depend on. i.e if packageA depended on packageB `graph.addDependencies([packageA])`
  80. * would return [packageA, packageB].
  81. *
  82. * @param {import("@lerna/package").Package[]} filteredPackages The packages to include dependencies for.
  83. */
  84. addDependencies(filteredPackages) {
  85. return this.extendList(filteredPackages, "localDependencies");
  86. }
  87. /**
  88. * Takes a list of Packages and returns a list of those same Packages with any Packages
  89. * that depend on them. i.e if packageC depended on packageD `graph.addDependents([packageD])`
  90. * would return [packageD, packageC].
  91. *
  92. * @param {import("@lerna/package").Package[]} filteredPackages The packages to include dependents for.
  93. */
  94. addDependents(filteredPackages) {
  95. return this.extendList(filteredPackages, "localDependents");
  96. }
  97. /**
  98. * Extends a list of packages by traversing on a given property, which must refer to a
  99. * `PackageGraphNode` property that is a collection of `PackageGraphNode`s.
  100. * Returns input packages with any additional packages found by traversing `nodeProp`.
  101. *
  102. * @param {import("@lerna/package").Package[]} packageList The list of packages to extend
  103. * @param {'localDependencies'|'localDependents'} nodeProp The property on `PackageGraphNode` used to traverse
  104. */
  105. extendList(packageList, nodeProp) {
  106. // the current list of packages we are expanding using breadth-first-search
  107. const search = new Set(packageList.map(({ name }) => this.get(name)));
  108. // an intermediate list of matched PackageGraphNodes
  109. /** @type {PackageGraphNode[]} */
  110. const result = [];
  111. search.forEach((currentNode) => {
  112. // anything searched for is always a result
  113. result.push(currentNode);
  114. currentNode[nodeProp].forEach((meta, depName) => {
  115. const depNode = this.get(depName);
  116. if (depNode !== currentNode && !search.has(depNode)) {
  117. search.add(depNode);
  118. }
  119. });
  120. });
  121. // actual Package instances, not PackageGraphNodes
  122. return result.map((node) => node.pkg);
  123. }
  124. /**
  125. * Return a tuple of cycle paths and nodes.
  126. *
  127. * @deprecated Use collapseCycles instead.
  128. *
  129. * @param {boolean} rejectCycles Whether or not to reject cycles
  130. * @returns {[Set<string[]>, Set<PackageGraphNode>]}
  131. */
  132. partitionCycles(rejectCycles) {
  133. const cyclePaths = new Set();
  134. const cycleNodes = new Set();
  135. this.forEach((currentNode, currentName) => {
  136. const seen = new Set();
  137. const visits = (walk) => (dependentNode, dependentName, siblingDependents) => {
  138. const step = walk.concat(dependentName);
  139. if (seen.has(dependentNode)) {
  140. return;
  141. }
  142. seen.add(dependentNode);
  143. if (dependentNode === currentNode) {
  144. // a direct cycle
  145. cycleNodes.add(currentNode);
  146. cyclePaths.add(step);
  147. return;
  148. }
  149. if (siblingDependents.has(currentName)) {
  150. // a transitive cycle
  151. const cycleDependentName = Array.from(dependentNode.localDependencies.keys()).find((key) =>
  152. currentNode.localDependents.has(key)
  153. );
  154. const pathToCycle = step.slice().reverse().concat(cycleDependentName);
  155. cycleNodes.add(dependentNode);
  156. cyclePaths.add(pathToCycle);
  157. }
  158. dependentNode.localDependents.forEach(visits(step));
  159. };
  160. currentNode.localDependents.forEach(visits([currentName]));
  161. });
  162. reportCycles(
  163. Array.from(cyclePaths, (cycle) => cycle.join(" -> ")),
  164. rejectCycles
  165. );
  166. return [cyclePaths, cycleNodes];
  167. }
  168. /**
  169. * Returns the cycles of this graph. If two cycles share some elements, they will
  170. * be returned as a single cycle.
  171. *
  172. * @param {boolean} rejectCycles Whether or not to reject cycles
  173. * @returns {Set<CyclicPackageGraphNode>}
  174. */
  175. collapseCycles(rejectCycles) {
  176. /** @type {string[]} */
  177. const cyclePaths = [];
  178. /** @type {Map<PackageGraphNode, CyclicPackageGraphNode>} */
  179. const nodeToCycle = new Map();
  180. /** @type {Set<CyclicPackageGraphNode>} */
  181. const cycles = new Set();
  182. /** @type {(PackageGraphNode | CyclicPackageGraphNode)[]} */
  183. const walkStack = [];
  184. function visits(baseNode, dependentNode) {
  185. if (nodeToCycle.has(baseNode)) {
  186. return;
  187. }
  188. let topLevelDependent = dependentNode;
  189. while (nodeToCycle.has(topLevelDependent)) {
  190. topLevelDependent = nodeToCycle.get(topLevelDependent);
  191. }
  192. if (
  193. topLevelDependent === baseNode ||
  194. (topLevelDependent.isCycle && topLevelDependent.has(baseNode.name))
  195. ) {
  196. const cycle = new CyclicPackageGraphNode();
  197. walkStack.forEach((nodeInCycle) => {
  198. nodeToCycle.set(nodeInCycle, cycle);
  199. cycle.insert(nodeInCycle);
  200. cycles.delete(nodeInCycle);
  201. });
  202. cycles.add(cycle);
  203. cyclePaths.push(cycle.toString());
  204. return;
  205. }
  206. if (walkStack.indexOf(topLevelDependent) === -1) {
  207. // eslint-disable-next-line no-use-before-define
  208. visitWithStack(baseNode, topLevelDependent);
  209. }
  210. }
  211. function visitWithStack(baseNode, currentNode = baseNode) {
  212. walkStack.push(currentNode);
  213. currentNode.localDependents.forEach(visits.bind(null, baseNode));
  214. walkStack.pop();
  215. }
  216. this.forEach((currentNode) => visitWithStack(currentNode));
  217. cycles.forEach((collapsedNode) => visitWithStack(collapsedNode));
  218. reportCycles(cyclePaths, rejectCycles);
  219. return cycles;
  220. }
  221. /**
  222. * Remove cycle nodes.
  223. *
  224. * @deprecated Spread set into prune() instead.
  225. *
  226. * @param {Set<PackageGraphNode>} cycleNodes
  227. */
  228. pruneCycleNodes(cycleNodes) {
  229. return this.prune(...cycleNodes);
  230. }
  231. /**
  232. * Remove all candidate nodes.
  233. * @param {PackageGraphNode[]} candidates
  234. */
  235. prune(...candidates) {
  236. if (candidates.length === this.size) {
  237. return this.clear();
  238. }
  239. candidates.forEach((node) => this.remove(node));
  240. }
  241. /**
  242. * Delete by value (instead of key), as well as removing pointers
  243. * to itself in the other node's internal collections.
  244. * @param {PackageGraphNode} candidateNode instance to remove
  245. */
  246. remove(candidateNode) {
  247. this.delete(candidateNode.name);
  248. this.forEach((node) => {
  249. // remove incoming edges ("indegree")
  250. node.localDependencies.delete(candidateNode.name);
  251. // remove outgoing edges ("outdegree")
  252. node.localDependents.delete(candidateNode.name);
  253. });
  254. }
  255. }
  256. module.exports.PackageGraph = PackageGraph;