@vltpkg/graph 1.0.0-rc.9 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +101 -22
  2. package/dist/{esm/actual → actual}/load.d.ts +8 -1
  3. package/dist/{esm/actual → actual}/load.js +29 -6
  4. package/dist/{esm/browser.d.ts → browser.d.ts} +0 -1
  5. package/dist/{esm/browser.js → browser.js} +0 -1
  6. package/dist/{esm/build.d.ts → build.d.ts} +0 -1
  7. package/dist/{esm/build.js → build.js} +0 -1
  8. package/dist/{esm/dependencies.d.ts → dependencies.d.ts} +0 -1
  9. package/dist/{esm/dependencies.js → dependencies.js} +3 -2
  10. package/dist/{esm/diff.d.ts → diff.d.ts} +2 -1
  11. package/dist/{esm/diff.js → diff.js} +5 -2
  12. package/dist/{esm/edge.d.ts → edge.d.ts} +0 -1
  13. package/dist/{esm/edge.js → edge.js} +0 -1
  14. package/dist/{esm/fixup-added-names.d.ts → fixup-added-names.d.ts} +4 -2
  15. package/dist/{esm/fixup-added-names.js → fixup-added-names.js} +16 -1
  16. package/dist/{esm/graph.d.ts → graph.d.ts} +13 -1
  17. package/dist/{esm/graph.js → graph.js} +49 -12
  18. package/dist/{esm/ideal → ideal}/append-nodes.d.ts +14 -3
  19. package/dist/{esm/ideal → ideal}/append-nodes.js +252 -64
  20. package/dist/{esm/ideal → ideal}/build-ideal-from-starting-graph.d.ts +0 -1
  21. package/dist/{esm/ideal → ideal}/build-ideal-from-starting-graph.js +0 -1
  22. package/dist/{esm/ideal → ideal}/build.d.ts +0 -1
  23. package/dist/{esm/ideal → ideal}/build.js +15 -2
  24. package/dist/{esm/ideal → ideal}/get-importer-specs.d.ts +0 -1
  25. package/dist/{esm/ideal → ideal}/get-importer-specs.js +13 -2
  26. package/dist/ideal/peers.d.ts +160 -0
  27. package/dist/ideal/peers.js +696 -0
  28. package/dist/{esm/ideal → ideal}/refresh-ideal-graph.d.ts +0 -5
  29. package/dist/{esm/ideal → ideal}/refresh-ideal-graph.js +11 -26
  30. package/dist/{esm/ideal → ideal}/remove-satisfied-specs.d.ts +0 -1
  31. package/dist/{esm/ideal → ideal}/remove-satisfied-specs.js +8 -1
  32. package/dist/ideal/sorting.d.ts +45 -0
  33. package/dist/ideal/sorting.js +70 -0
  34. package/dist/{esm/ideal → ideal}/types.d.ts +1 -6
  35. package/dist/ideal/types.js +1 -0
  36. package/dist/{esm/index.d.ts → index.d.ts} +0 -1
  37. package/dist/{esm/index.js → index.js} +0 -1
  38. package/dist/{esm/install.d.ts → install.d.ts} +2 -1
  39. package/dist/{esm/install.js → install.js} +24 -4
  40. package/dist/{esm/lockfile → lockfile}/load-edges.d.ts +0 -1
  41. package/dist/{esm/lockfile → lockfile}/load-edges.js +16 -6
  42. package/dist/{esm/lockfile → lockfile}/load-nodes.d.ts +0 -1
  43. package/dist/{esm/lockfile → lockfile}/load-nodes.js +6 -3
  44. package/dist/{esm/lockfile → lockfile}/load.d.ts +0 -1
  45. package/dist/lockfile/load.js +171 -0
  46. package/dist/{esm/lockfile → lockfile}/save.d.ts +1 -2
  47. package/dist/{esm/lockfile → lockfile}/save.js +17 -11
  48. package/dist/{esm/lockfile → lockfile}/types.d.ts +7 -1
  49. package/dist/{esm/lockfile → lockfile}/types.js +6 -1
  50. package/dist/{esm/modifiers.d.ts → modifiers.d.ts} +0 -1
  51. package/dist/{esm/modifiers.js → modifiers.js} +0 -1
  52. package/dist/{esm/node.d.ts → node.d.ts} +2 -1
  53. package/dist/{esm/node.js → node.js} +5 -1
  54. package/dist/{esm/non-empty-list.d.ts → non-empty-list.d.ts} +0 -1
  55. package/dist/{esm/non-empty-list.js → non-empty-list.js} +0 -1
  56. package/dist/path-security-error.d.ts +5 -0
  57. package/dist/path-security-error.js +13 -0
  58. package/dist/{esm/reify → reify}/add-edge.d.ts +0 -1
  59. package/dist/{esm/reify → reify}/add-edge.js +10 -4
  60. package/dist/{esm/reify → reify}/add-edges.d.ts +0 -1
  61. package/dist/{esm/reify → reify}/add-edges.js +0 -1
  62. package/dist/{esm/reify → reify}/add-nodes.d.ts +0 -1
  63. package/dist/{esm/reify → reify}/add-nodes.js +0 -1
  64. package/dist/{esm/reify → reify}/bin-chmod.d.ts +0 -1
  65. package/dist/{esm/reify → reify}/bin-chmod.js +0 -1
  66. package/dist/{esm/reify → reify}/build.d.ts +0 -1
  67. package/dist/{esm/reify → reify}/build.js +12 -4
  68. package/dist/{esm/reify → reify}/calculate-save-value.d.ts +1 -2
  69. package/dist/{esm/reify → reify}/calculate-save-value.js +11 -5
  70. package/dist/{esm/reify → reify}/check-needed-build.d.ts +10 -1
  71. package/dist/reify/check-needed-build.js +71 -0
  72. package/dist/{esm/reify → reify}/delete-edge.d.ts +0 -1
  73. package/dist/{esm/reify → reify}/delete-edge.js +0 -1
  74. package/dist/{esm/reify → reify}/delete-edges.d.ts +0 -1
  75. package/dist/{esm/reify → reify}/delete-edges.js +0 -1
  76. package/dist/{esm/reify → reify}/delete-nodes.d.ts +0 -1
  77. package/dist/{esm/reify → reify}/delete-nodes.js +0 -1
  78. package/dist/{esm/reify → reify}/extract-node.d.ts +0 -1
  79. package/dist/{esm/reify → reify}/extract-node.js +10 -3
  80. package/dist/{esm/reify → reify}/index.d.ts +3 -1
  81. package/dist/{esm/reify → reify}/index.js +25 -5
  82. package/dist/{esm/reify → reify}/internal-hoist.d.ts +0 -1
  83. package/dist/{esm/reify → reify}/internal-hoist.js +0 -1
  84. package/dist/{esm/reify → reify}/optional-fail.d.ts +0 -1
  85. package/dist/{esm/reify → reify}/optional-fail.js +0 -1
  86. package/dist/{esm/reify → reify}/rollback.d.ts +0 -1
  87. package/dist/{esm/reify → reify}/rollback.js +0 -1
  88. package/dist/{esm/reify → reify}/update-importers-package-json.d.ts +11 -2
  89. package/dist/{esm/reify → reify}/update-importers-package-json.js +4 -5
  90. package/dist/{esm/remove-optional-subgraph.d.ts → remove-optional-subgraph.d.ts} +0 -1
  91. package/dist/{esm/remove-optional-subgraph.js → remove-optional-subgraph.js} +0 -1
  92. package/dist/{esm/resolve-save-type.d.ts → resolve-save-type.d.ts} +0 -1
  93. package/dist/{esm/resolve-save-type.js → resolve-save-type.js} +0 -1
  94. package/dist/{esm/stringify-node.d.ts → stringify-node.d.ts} +0 -1
  95. package/dist/{esm/stringify-node.js → stringify-node.js} +0 -1
  96. package/dist/{esm/transfer-data → transfer-data}/load.d.ts +0 -1
  97. package/dist/{esm/transfer-data → transfer-data}/load.js +8 -5
  98. package/dist/{esm/uninstall.d.ts → uninstall.d.ts} +0 -1
  99. package/dist/{esm/uninstall.js → uninstall.js} +17 -7
  100. package/dist/{esm/update.d.ts → update.d.ts} +0 -1
  101. package/dist/{esm/update.js → update.js} +1 -1
  102. package/dist/{esm/virtual-root.d.ts → virtual-root.d.ts} +0 -1
  103. package/dist/{esm/virtual-root.js → virtual-root.js} +0 -1
  104. package/dist/{esm/visualization → visualization}/human-readable-output.d.ts +0 -1
  105. package/dist/{esm/visualization → visualization}/human-readable-output.js +0 -1
  106. package/dist/{esm/visualization → visualization}/json-output.d.ts +0 -1
  107. package/dist/{esm/visualization → visualization}/json-output.js +0 -1
  108. package/dist/{esm/visualization → visualization}/mermaid-output.d.ts +2 -2
  109. package/dist/{esm/visualization → visualization}/mermaid-output.js +27 -21
  110. package/dist/{esm/visualization → visualization}/object-like-output.d.ts +0 -1
  111. package/dist/{esm/visualization → visualization}/object-like-output.js +0 -1
  112. package/package.json +57 -62
  113. package/dist/esm/actual/load.d.ts.map +0 -1
  114. package/dist/esm/actual/load.js.map +0 -1
  115. package/dist/esm/browser.d.ts.map +0 -1
  116. package/dist/esm/browser.js.map +0 -1
  117. package/dist/esm/build.d.ts.map +0 -1
  118. package/dist/esm/build.js.map +0 -1
  119. package/dist/esm/dependencies.d.ts.map +0 -1
  120. package/dist/esm/dependencies.js.map +0 -1
  121. package/dist/esm/diff.d.ts.map +0 -1
  122. package/dist/esm/diff.js.map +0 -1
  123. package/dist/esm/edge.d.ts.map +0 -1
  124. package/dist/esm/edge.js.map +0 -1
  125. package/dist/esm/fixup-added-names.d.ts.map +0 -1
  126. package/dist/esm/fixup-added-names.js.map +0 -1
  127. package/dist/esm/graph.d.ts.map +0 -1
  128. package/dist/esm/graph.js.map +0 -1
  129. package/dist/esm/ideal/append-nodes.d.ts.map +0 -1
  130. package/dist/esm/ideal/append-nodes.js.map +0 -1
  131. package/dist/esm/ideal/build-ideal-from-starting-graph.d.ts.map +0 -1
  132. package/dist/esm/ideal/build-ideal-from-starting-graph.js.map +0 -1
  133. package/dist/esm/ideal/build.d.ts.map +0 -1
  134. package/dist/esm/ideal/build.js.map +0 -1
  135. package/dist/esm/ideal/get-importer-specs.d.ts.map +0 -1
  136. package/dist/esm/ideal/get-importer-specs.js.map +0 -1
  137. package/dist/esm/ideal/get-ordered-dependencies.d.ts +0 -10
  138. package/dist/esm/ideal/get-ordered-dependencies.d.ts.map +0 -1
  139. package/dist/esm/ideal/get-ordered-dependencies.js +0 -42
  140. package/dist/esm/ideal/get-ordered-dependencies.js.map +0 -1
  141. package/dist/esm/ideal/peers.d.ts +0 -71
  142. package/dist/esm/ideal/peers.d.ts.map +0 -1
  143. package/dist/esm/ideal/peers.js +0 -318
  144. package/dist/esm/ideal/peers.js.map +0 -1
  145. package/dist/esm/ideal/refresh-ideal-graph.d.ts.map +0 -1
  146. package/dist/esm/ideal/refresh-ideal-graph.js.map +0 -1
  147. package/dist/esm/ideal/remove-satisfied-specs.d.ts.map +0 -1
  148. package/dist/esm/ideal/remove-satisfied-specs.js.map +0 -1
  149. package/dist/esm/ideal/types.d.ts.map +0 -1
  150. package/dist/esm/ideal/types.js +0 -2
  151. package/dist/esm/ideal/types.js.map +0 -1
  152. package/dist/esm/index.d.ts.map +0 -1
  153. package/dist/esm/index.js.map +0 -1
  154. package/dist/esm/install.d.ts.map +0 -1
  155. package/dist/esm/install.js.map +0 -1
  156. package/dist/esm/lockfile/load-edges.d.ts.map +0 -1
  157. package/dist/esm/lockfile/load-edges.js.map +0 -1
  158. package/dist/esm/lockfile/load-nodes.d.ts.map +0 -1
  159. package/dist/esm/lockfile/load-nodes.js.map +0 -1
  160. package/dist/esm/lockfile/load.d.ts.map +0 -1
  161. package/dist/esm/lockfile/load.js +0 -64
  162. package/dist/esm/lockfile/load.js.map +0 -1
  163. package/dist/esm/lockfile/save.d.ts.map +0 -1
  164. package/dist/esm/lockfile/save.js.map +0 -1
  165. package/dist/esm/lockfile/types.d.ts.map +0 -1
  166. package/dist/esm/lockfile/types.js.map +0 -1
  167. package/dist/esm/modifiers.d.ts.map +0 -1
  168. package/dist/esm/modifiers.js.map +0 -1
  169. package/dist/esm/node.d.ts.map +0 -1
  170. package/dist/esm/node.js.map +0 -1
  171. package/dist/esm/non-empty-list.d.ts.map +0 -1
  172. package/dist/esm/non-empty-list.js.map +0 -1
  173. package/dist/esm/package.json +0 -3
  174. package/dist/esm/reify/add-edge.d.ts.map +0 -1
  175. package/dist/esm/reify/add-edge.js.map +0 -1
  176. package/dist/esm/reify/add-edges.d.ts.map +0 -1
  177. package/dist/esm/reify/add-edges.js.map +0 -1
  178. package/dist/esm/reify/add-nodes.d.ts.map +0 -1
  179. package/dist/esm/reify/add-nodes.js.map +0 -1
  180. package/dist/esm/reify/bin-chmod.d.ts.map +0 -1
  181. package/dist/esm/reify/bin-chmod.js.map +0 -1
  182. package/dist/esm/reify/build.d.ts.map +0 -1
  183. package/dist/esm/reify/build.js.map +0 -1
  184. package/dist/esm/reify/calculate-save-value.d.ts.map +0 -1
  185. package/dist/esm/reify/calculate-save-value.js.map +0 -1
  186. package/dist/esm/reify/check-needed-build.d.ts.map +0 -1
  187. package/dist/esm/reify/check-needed-build.js +0 -50
  188. package/dist/esm/reify/check-needed-build.js.map +0 -1
  189. package/dist/esm/reify/delete-edge.d.ts.map +0 -1
  190. package/dist/esm/reify/delete-edge.js.map +0 -1
  191. package/dist/esm/reify/delete-edges.d.ts.map +0 -1
  192. package/dist/esm/reify/delete-edges.js.map +0 -1
  193. package/dist/esm/reify/delete-nodes.d.ts.map +0 -1
  194. package/dist/esm/reify/delete-nodes.js.map +0 -1
  195. package/dist/esm/reify/extract-node.d.ts.map +0 -1
  196. package/dist/esm/reify/extract-node.js.map +0 -1
  197. package/dist/esm/reify/index.d.ts.map +0 -1
  198. package/dist/esm/reify/index.js.map +0 -1
  199. package/dist/esm/reify/internal-hoist.d.ts.map +0 -1
  200. package/dist/esm/reify/internal-hoist.js.map +0 -1
  201. package/dist/esm/reify/optional-fail.d.ts.map +0 -1
  202. package/dist/esm/reify/optional-fail.js.map +0 -1
  203. package/dist/esm/reify/rollback.d.ts.map +0 -1
  204. package/dist/esm/reify/rollback.js.map +0 -1
  205. package/dist/esm/reify/update-importers-package-json.d.ts.map +0 -1
  206. package/dist/esm/reify/update-importers-package-json.js.map +0 -1
  207. package/dist/esm/remove-optional-subgraph.d.ts.map +0 -1
  208. package/dist/esm/remove-optional-subgraph.js.map +0 -1
  209. package/dist/esm/resolve-save-type.d.ts.map +0 -1
  210. package/dist/esm/resolve-save-type.js.map +0 -1
  211. package/dist/esm/stringify-node.d.ts.map +0 -1
  212. package/dist/esm/stringify-node.js.map +0 -1
  213. package/dist/esm/transfer-data/load.d.ts.map +0 -1
  214. package/dist/esm/transfer-data/load.js.map +0 -1
  215. package/dist/esm/uninstall.d.ts.map +0 -1
  216. package/dist/esm/uninstall.js.map +0 -1
  217. package/dist/esm/update.d.ts.map +0 -1
  218. package/dist/esm/update.js.map +0 -1
  219. package/dist/esm/virtual-root.d.ts.map +0 -1
  220. package/dist/esm/virtual-root.js.map +0 -1
  221. package/dist/esm/visualization/human-readable-output.d.ts.map +0 -1
  222. package/dist/esm/visualization/human-readable-output.js.map +0 -1
  223. package/dist/esm/visualization/json-output.d.ts.map +0 -1
  224. package/dist/esm/visualization/json-output.js.map +0 -1
  225. package/dist/esm/visualization/mermaid-output.d.ts.map +0 -1
  226. package/dist/esm/visualization/mermaid-output.js.map +0 -1
  227. package/dist/esm/visualization/object-like-output.d.ts.map +0 -1
  228. package/dist/esm/visualization/object-like-output.js.map +0 -1
@@ -1,12 +1,15 @@
1
1
  import { joinDepIDTuple, joinExtra } from '@vltpkg/dep-id';
2
2
  import { error } from '@vltpkg/error-cause';
3
3
  import { Spec } from '@vltpkg/spec';
4
+ import { satisfies } from '@vltpkg/satisfies';
4
5
  import { longDependencyTypes, normalizeManifest } from '@vltpkg/types';
5
6
  import { fixupAddedNames } from "../fixup-added-names.js";
6
7
  import { shorten } from "../dependencies.js";
7
8
  import { removeOptionalSubgraph } from "../remove-optional-subgraph.js";
9
+ import { isPathSecurityError } from "../path-security-error.js";
8
10
  import { extractNode } from "../reify/extract-node.js";
9
- import { endPeerPlacement, postPlacementPeerCheck, startPeerPlacement, } from "./peers.js";
11
+ import { checkPeerEdgesCompatible, endPeerPlacement, forkPeerContext, postPlacementPeerCheck, startPeerPlacement, } from "./peers.js";
12
+ import { compareByHasPeerDeps } from "./sorting.js";
10
13
  /**
11
14
  * Only install devDeps for git dependencies and importers
12
15
  * Everything else always gets installed
@@ -39,18 +42,96 @@ const getFileTypeInfo = (spec, fromNode, scurry) => {
39
42
  };
40
43
  };
41
44
  const isStringArray = (a) => Array.isArray(a) && !a.some(b => typeof b !== 'string');
45
+ /**
46
+ * Try to find a compatible resolution for a dependency, checking peer context.
47
+ * If the first resolution candidate is incompatible with the peer context,
48
+ * try other candidates.
49
+ */
50
+ const findCompatibleResolution = (spec, fromNode, graph, peerContext, queryModifier, _peer) => {
51
+ // Hoist invariants once
52
+ const fromLoc = fromNode.location;
53
+ const projectRoot = graph.projectRoot;
54
+ const monorepo = graph.monorepo;
55
+ const final = spec.final;
56
+ // Memoize satisfies() results per-node within this resolution attempt
57
+ const satisfiesCache = new Map();
58
+ const satisfiesFinal = (n) => {
59
+ const key = n.id;
60
+ const cached = satisfiesCache.get(key);
61
+ /* c8 ignore next 3 - optimization: cache hit when same node checked multiple times */
62
+ if (cached !== undefined) {
63
+ return cached;
64
+ }
65
+ const result = satisfies(key, final, fromLoc, projectRoot, monorepo);
66
+ satisfiesCache.set(key, result);
67
+ return result;
68
+ };
69
+ // Prefer existing edge target if it satisfies the spec.
70
+ // This ensures lockfile resolutions are preserved when still valid,
71
+ // rather than potentially picking a different satisfying version.
72
+ const existingEdge = fromNode.edgesOut.get(spec.name);
73
+ let existingNode;
74
+ if (existingEdge?.to &&
75
+ !existingEdge.to.detached &&
76
+ satisfiesFinal(existingEdge.to)) {
77
+ existingNode = existingEdge.to;
78
+ }
79
+ else {
80
+ existingNode = graph.findResolution(spec, fromNode, queryModifier);
81
+ }
82
+ let peerCompatResult = existingNode ?
83
+ checkPeerEdgesCompatible(existingNode, fromNode, peerContext, graph)
84
+ : { compatible: true };
85
+ // CANDIDATE FALLBACK: If first candidate is peer-incompatible, try others
86
+ // Lazy-load candidates only when fallback needed
87
+ if (existingNode && !peerCompatResult.compatible) {
88
+ const candidates = graph.nodesByName.get(final.name);
89
+ if (candidates && candidates.size > 1) {
90
+ for (const candidate of candidates) {
91
+ if (candidate === existingNode)
92
+ continue;
93
+ if (candidate.detached)
94
+ continue;
95
+ if (!satisfiesFinal(candidate))
96
+ continue;
97
+ const compat = checkPeerEdgesCompatible(candidate, fromNode, peerContext, graph);
98
+ if (compat.compatible) {
99
+ existingNode = candidate;
100
+ peerCompatResult = compat;
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return { existingNode, peerCompatResult };
107
+ };
42
108
  /**
43
109
  * Fetch manifests for dependencies and create placement tasks.
110
+ *
111
+ * This is Phase 1 of the breadth-first graph building process. For each
112
+ * dependency at the current level:
113
+ * 1. Apply any active modifiers (spec swapping)
114
+ * 2. Try to find an existing node to reuse (with peer compatibility check)
115
+ * 3. If no reusable node, start a manifest fetch (in parallel)
116
+ * 4. Create placement tasks for Phase 2
117
+ *
118
+ * The result is sorted to process non-peer-dependent packages first,
119
+ * ensuring peer dependencies can resolve to already-placed siblings.
120
+ *
121
+ * **Read-only**: This function no longer mutates the graph. It returns
122
+ * tasks that will be applied serially in the BFS loop for deterministic ordering.
44
123
  */
45
124
  const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry, peerContext, modifierRefs, depth = 0) => {
46
- // Create fetch tasks for all dependencies at this level
47
125
  const fetchTasks = [];
48
126
  const placementTasks = [];
127
+ const reuseTasks = [];
128
+ const forkRequests = [];
49
129
  for (const { spec: originalSpec, type } of deps) {
50
130
  let spec = originalSpec;
51
131
  const fileTypeInfo = getFileTypeInfo(spec, fromNode, scurry);
52
132
  const activeModifier = modifierRefs?.get(spec.name);
53
- // here is the place we swap specs if a edge modifier was defined
133
+ // MODIFIER HANDLING: Swap spec if an edge modifier is fully matched
134
+ // Example: `vlt install --override "react:^19"` changes react's spec
54
135
  const queryModifier = activeModifier?.modifier.query;
55
136
  const completeModifier = activeModifier &&
56
137
  activeModifier.interactiveBreadcrumb.current ===
@@ -59,30 +140,55 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
59
140
  completeModifier &&
60
141
  'spec' in activeModifier.modifier) {
61
142
  spec = activeModifier.modifier.spec;
143
+ // bareSpec of '-' means "remove this dependency"
62
144
  if (spec.bareSpec === '-') {
63
145
  continue;
64
146
  }
65
147
  }
66
148
  const peer = type === 'peer' || type === 'peerOptional';
67
- // skip reusing nodes for peer deps since their reusability
68
- // is handled ahead-of-time during its parent's placement
69
- const existingNode = graph.findResolution(spec, fromNode, queryModifier);
149
+ // NODE REUSE LOGIC with peer compatibility
150
+ const { existingNode, peerCompatResult } = findCompatibleResolution(spec, fromNode, graph, peerContext, queryModifier, peer);
151
+ // Accumulate fork request if incompatible peer edges detected (defer actual fork)
152
+ const effectivePeerContext = peerContext;
153
+ /* c8 ignore start */
154
+ if (!peerCompatResult.compatible && peerCompatResult.forkEntry) {
155
+ forkRequests.push(peerCompatResult.forkEntry);
156
+ // All fork entries from this fromNode will be applied together in Phase B
157
+ }
158
+ /* c8 ignore stop */
70
159
  // defines what nodes are eligible to be reused
71
160
  const validExistingNode = existingNode &&
72
161
  !existingNode.detached &&
73
- // Regular deps can always reuse
162
+ // Regular deps can always reuse.
163
+ // Peer deps can reuse as well if their peer edges are compatible.
74
164
  /* c8 ignore start */
75
- (!peer ||
76
- // otherwise reusing peer deps only in case of a peerSetHash matche
77
- existingNode.peerSetHash === fromNode.peerSetHash);
165
+ (!peer || peerCompatResult.compatible) &&
166
+ // Check if existing node's peer edges are compatible with new parent
167
+ peerCompatResult.compatible;
78
168
  /* c8 ignore stop */
79
169
  if (validExistingNode ||
80
- // importers are handled at the ./add-nodes.ts top-level
170
+ // importers are handled at the ./refresh-ideal-graph.ts top-level
81
171
  // so we should just skip whenever we find one
82
172
  existingNode?.importer) {
83
- graph.addEdge(type, spec, fromNode, existingNode);
173
+ // Defer edge creation to Phase B for deterministic ordering.
174
+ // Previously added immediately, but this caused race conditions when
175
+ // parallel fetches completed in different orders.
176
+ reuseTasks.push({ type, spec, fromNode, toNode: existingNode });
84
177
  continue;
85
178
  }
179
+ // workspace: specs can only resolve to local workspace packages.
180
+ // If we didn't find a matching workspace node above, it means either
181
+ // the workspace doesn't exist or its version doesn't satisfy the range.
182
+ if (spec.final.type === 'workspace') {
183
+ const wsVersion = graph.nodesByName
184
+ .get(spec.name)
185
+ ?.values()
186
+ .next().value?.version;
187
+ throw error(wsVersion ?
188
+ `workspace dependency ${spec} does not match the ` +
189
+ `local workspace version (${wsVersion})`
190
+ : `no workspace found matching ${spec}`, { spec });
191
+ }
86
192
  // is the current edge pointint go an optional dependency?
87
193
  const edgeOptional = type === 'optional' || type === 'peerOptional';
88
194
  // Start manifest fetch immediately for parallel processing
@@ -90,7 +196,7 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
90
196
  // the "detached" node state means that it has already been load as
91
197
  // part of a graph (either lockfile or actual) and it has valid manifest
92
198
  // data so we shortcut the package info manifest fetch here
93
- existingNode?.detached ?
199
+ existingNode?.detached && existingNode.manifest ?
94
200
  Promise.resolve(existingNode.manifest)
95
201
  // this is the entry point to fetch calls to retrieve manifests
96
202
  // from the build ideal graph point of view
@@ -98,7 +204,10 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
98
204
  .manifest(spec, { from: scurry.resolve(fromNode.location) })
99
205
  .then(manifest => manifest)
100
206
  .catch((er) => {
101
- // optional deps ignored if inaccessible
207
+ // optional deps ignored if inaccessible, but a dep that tried
208
+ // to escape the project is never silently dropped
209
+ if (isPathSecurityError(er))
210
+ throw er;
102
211
  if (edgeOptional || fromNode.optional) {
103
212
  return undefined;
104
213
  }
@@ -114,7 +223,7 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
114
223
  edgeOptional,
115
224
  manifestPromise,
116
225
  depth,
117
- peerContext,
226
+ peerContext: effectivePeerContext,
118
227
  };
119
228
  fetchTasks.push(fetchTask);
120
229
  }
@@ -130,34 +239,30 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
130
239
  // so that peer dependencies can easily reuse already placed regular
131
240
  // dependencies as part of peer context set resolution also makes sure to
132
241
  // sort by the manifest name for deterministic order.
133
- placementTasks.sort((a, b) => {
134
- const aIsPeer = (a.manifest?.peerDependencies &&
135
- Object.keys(a.manifest.peerDependencies).length > 0) ?
136
- 1
137
- : 0;
138
- const bIsPeer = (b.manifest?.peerDependencies &&
139
- Object.keys(b.manifest.peerDependencies).length > 0) ?
140
- 1
141
- : 0;
142
- // regular dependencies first, peer dependencies last
143
- if (aIsPeer !== bIsPeer) {
144
- return aIsPeer - bIsPeer;
145
- }
146
- // if both are in the same group,
147
- // sort alphabetically by manifest name (fallback to spec.name)
148
- const aName = a.manifest?.name /* c8 ignore next */ || a.fetchTask.spec.name;
149
- const bName = b.manifest?.name || b.fetchTask.spec.name;
150
- return aName.localeCompare(bName, 'en');
151
- });
152
- return placementTasks;
242
+ placementTasks.sort(compareByHasPeerDeps);
243
+ return { placementTasks, reuseTasks, forkRequests };
153
244
  };
154
245
  /**
155
- * Process placement tasks and collect child dependencies, this is the
156
- * second step of the appendNodes operation after manifest fetching in
157
- * which the final graph data structure is actually built.
246
+ * Process placement tasks and collect child dependencies.
247
+ *
248
+ * This is Phase 2 of the breadth-first graph building process. For each
249
+ * resolved manifest:
250
+ * 1. Handle missing manifests (optional vs required deps)
251
+ * 2. Start peer placement process (collect sibling context)
252
+ * 3. Place the node in the graph with appropriate flags
253
+ * 4. Trigger early extraction if eligible (performance optimization)
254
+ * 5. Collect child dependencies for the next BFS level
255
+ * 6. End peer placement (setup context update functions)
256
+ *
257
+ * Early extraction: When `actual` graph is provided, nodes destined for the
258
+ * vlt store are extracted immediately (in parallel) instead of waiting for
259
+ * the full ideal graph to be built. This significantly improves install time.
158
260
  */
159
261
  const processPlacementTasks = async (graph, options, placementTasks, add, modifiers, scurry, packageInfo, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove) => {
160
262
  const childDepsToProcess = [];
263
+ // Note: placementTasks are already sorted by fetchManifestsForDeps
264
+ // using compareByHasPeerDeps to ensure non-peer deps are processed first.
265
+ // We don't sort again here to preserve that ordering.
161
266
  for (const placementTask of placementTasks) {
162
267
  const { fetchTask, manifest } = placementTask;
163
268
  let { activeModifier, edgeOptional, fileTypeInfo, fromNode, peerContext, queryModifier, spec, type, } = fetchTask;
@@ -193,7 +298,9 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
193
298
  const peerSetHash = peerPlacement.peerSetHash;
194
299
  const queuedEntries = peerPlacement.queuedEntries;
195
300
  // places a new node in the graph representing a newly seen dependency
196
- const node = graph.placePackage(fromNode, type, spec, normalizeManifest(manifest), fileTypeInfo?.id, joinExtra({ peerSetHash, modifier: queryModifier }));
301
+ const node = graph.placePackage(fromNode, type, spec,
302
+ // report broken manifest names against the dependency spec
303
+ normalizeManifest(manifest, String(spec)), fileTypeInfo?.id, joinExtra({ peerSetHash, modifier: queryModifier }));
197
304
  /* c8 ignore start - not possible, already ensured manifest */
198
305
  if (!node) {
199
306
  throw error('failed to place package', {
@@ -237,7 +344,13 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
237
344
  if (fileTypeInfo?.path && fileTypeInfo.isDirectory) {
238
345
  node.location = fileTypeInfo.path;
239
346
  }
240
- node.setResolved();
347
+ // Do not clobber lockfile-provided resolved values.
348
+ // `setResolved()` cannot infer a tarball without a manifest,
349
+ // which can cause `resolved` to become undefined and the
350
+ // main lockfile to mutate across installs.
351
+ if (!node.resolved) {
352
+ node.setResolved();
353
+ }
241
354
  // collect child dependencies for processing in the next level
242
355
  const nextPeerDeps = new Map();
243
356
  // compute deps normally
@@ -254,7 +367,9 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
254
367
  for (const depTypeName of longDependencyTypes) {
255
368
  const depRecord = manifest[depTypeName];
256
369
  if (depRecord && shouldInstallDepType(node, depTypeName)) {
257
- for (const [name, bareSpec] of Object.entries(depRecord)) {
370
+ // Sort Object.entries for deterministic iteration
371
+ const sortedEntries = Object.entries(depRecord).sort(([a], [b]) => a.localeCompare(b, 'en'));
372
+ for (const [name, bareSpec] of sortedEntries) {
258
373
  // might need to skip already placed peer deps here
259
374
  if (bundled.has(name))
260
375
  continue;
@@ -262,7 +377,9 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
262
377
  type: shorten(depTypeName, name, manifest),
263
378
  spec: Spec.parse(name, bareSpec, {
264
379
  ...options,
265
- registry: spec.registry,
380
+ // git/file/remote parents have no spec.registry, so fall
381
+ // back to the configured one. see vltpkg/vltpkg#1580.
382
+ registry: spec.registry ?? options.registry,
266
383
  }),
267
384
  };
268
385
  if (depTypeName === 'peerDependencies') {
@@ -324,22 +441,43 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
324
441
  * Append new nodes in the given `graph` for dependencies specified at `add`
325
442
  * and missing dependencies from the `deps` parameter.
326
443
  *
327
- * It also applies any modifiers that applies to a given node as it processes
328
- * and builds the graph.
444
+ * Uses **breadth-first traversal** (BFS) with **deterministic ordering** to
445
+ * ensure reproducible builds. The algorithm:
446
+ *
447
+ * 1. Process all deps at the current level in parallel
448
+ * 2. After each level, run `postPlacementPeerCheck` to handle peer contexts
449
+ * 3. Collect child deps for the next level
450
+ * 4. Repeat until no more deps to process
451
+ *
452
+ * **Peer Context Isolation**: Each workspace importer gets its own peer context
453
+ * to prevent cross-workspace leakage. Without this, `react@^18` from workspace A
454
+ * could incorrectly satisfy `react@^19` peer deps in workspace B.
455
+ *
456
+ * **Early Extraction**: When `actual` graph is provided, nodes are extracted
457
+ * to the vlt store during graph construction (not after), improving performance.
329
458
  */
330
459
  export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, options, seen, add, modifiers, modifierRefs, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove) => {
460
+ // Cycle detection: skip if already processed
331
461
  /* c8 ignore next */
332
462
  if (seen.has(fromNode.id))
333
463
  return;
334
464
  seen.add(fromNode.id);
335
- // Get the initial peer context from the graph
336
- const [initialPeerContext] = graph.peerContexts;
465
+ // PEER CONTEXT ISOLATION: Each workspace importer needs its own context
466
+ // to prevent peer targets from one workspace affecting another.
467
+ // The main importer (index 0) uses the initial context; others get fresh ones.
468
+ let initialPeerContext = graph.peerContexts[0];
337
469
  /* c8 ignore start - impossible */
338
- if (!initialPeerContext) {
470
+ if (!initialPeerContext)
339
471
  throw error('no initial peer context found in graph');
340
- }
341
472
  /* c8 ignore stop */
342
- // Use a queue for breadth-first processing
473
+ if (fromNode.importer && fromNode !== graph.mainImporter) {
474
+ // Create isolated peer context for this workspace importer
475
+ const nextPeerContext = new Map();
476
+ nextPeerContext.index = graph.nextPeerContextIndex();
477
+ graph.peerContexts[nextPeerContext.index] = nextPeerContext;
478
+ initialPeerContext = nextPeerContext;
479
+ }
480
+ // BFS queue: process deps level by level for deterministic builds
343
481
  let currentLevelDeps = [
344
482
  {
345
483
  node: fromNode,
@@ -355,27 +493,78 @@ export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, op
355
493
  /* c8 ignore stop */
356
494
  },
357
495
  ];
496
+ // BFS MAIN LOOP: Process level by level until no more deps
358
497
  while (currentLevelDeps.length > 0) {
359
498
  const nextLevelDeps = [];
360
- // Process all nodes at the current level in parallel
361
- const levelResults = await Promise.all(currentLevelDeps.map(async ({ node, deps: nodeDeps, modifierRefs: nodeModifierRefs, peerContext, depth, }) => {
362
- // Mark node as seen when we start processing its dependencies
499
+ // ============================================================
500
+ // PHASE A: PARALLEL FETCH (READ-ONLY)
501
+ // ============================================================
502
+ // Fetch all manifests at this level in parallel without mutating the graph.
503
+ // This phase is read-only to avoid race conditions from network timing.
504
+ const fetchResults = await Promise.all(currentLevelDeps.map(async ({ node, deps: nodeDeps, modifierRefs: nodeModifierRefs, peerContext, depth, }) => {
505
+ // Cycle prevention: mark as seen when starting to process
363
506
  seen.add(node.id);
364
- // Fetch manifests for this node's dependencies
365
- const placementTasks = await fetchManifestsForDeps(packageInfo, graph, node,
366
- // Sort dependencies by spec.name for deterministic ordering
507
+ // Fetch manifests and collect tasks (no graph mutations)
508
+ const result = await fetchManifestsForDeps(packageInfo, graph, node,
509
+ // Sort by name for deterministic ordering (reproducible builds)
367
510
  nodeDeps.sort((a, b) => a.spec.name.localeCompare(b.spec.name, 'en')), scurry, peerContext, nodeModifierRefs, depth);
368
- // Process the placement tasks and get child dependencies
369
- return await processPlacementTasks(graph, options, placementTasks, add, modifiers, scurry, packageInfo, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove);
511
+ return {
512
+ entry: {
513
+ node,
514
+ deps: nodeDeps,
515
+ modifierRefs: nodeModifierRefs,
516
+ peerContext,
517
+ depth,
518
+ },
519
+ result,
520
+ };
370
521
  }));
371
- // Traverse the queued up children dependencies, adding and tracking
372
- // dependencies on the peer context set, forking the context as needed
373
- // and resolving any peer dependency that is able to be resolved using
374
- // the current peer context set
522
+ // ============================================================
523
+ // PHASE B: SERIAL MUTATIONS (DETERMINISTIC ORDER)
524
+ // ============================================================
525
+ // Sort results by stable identifiers to ensure deterministic ordering
526
+ // regardless of which manifest fetch completed first
527
+ const sortedResults = fetchResults.sort((a, b) => {
528
+ // Sort by node ID (DepID-based, stable) and depth
529
+ const keyA = `${a.entry.node.id}::${a.entry.depth}`;
530
+ const keyB = `${b.entry.node.id}::${b.entry.depth}`;
531
+ return keyA.localeCompare(keyB, 'en');
532
+ });
533
+ // Apply all mutations serially in deterministic order
534
+ const levelResults = [];
535
+ for (const { entry, result } of sortedResults) {
536
+ // Apply accumulated fork requests if any (from Phase A deferred forks)
537
+ if (result.forkRequests.length > 0) {
538
+ const forkedContext = forkPeerContext(graph, entry.peerContext, result.forkRequests);
539
+ entry.peerContext = forkedContext;
540
+ // Update peer context in all placement tasks to use forked context
541
+ for (const task of result.placementTasks) {
542
+ task.fetchTask.peerContext = forkedContext;
543
+ }
544
+ }
545
+ // Apply reuse edges in deterministic order (before placement)
546
+ // Sort reuse tasks by spec name for stability
547
+ const sortedReuseTasks = result.reuseTasks.sort((a, b) => a.spec.name.localeCompare(b.spec.name, 'en'));
548
+ for (const { type, spec, fromNode, toNode, } of sortedReuseTasks) {
549
+ graph.addEdge(type, spec, fromNode, toNode);
550
+ }
551
+ // Place nodes and collect child deps
552
+ const placed = await processPlacementTasks(graph, options, result.placementTasks, add, modifiers, scurry, packageInfo, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove);
553
+ levelResults.push(placed);
554
+ }
555
+ // ============================================================
556
+ // PHASE C: POST-PLACEMENT PEER CHECK
557
+ // ============================================================
558
+ // After all nodes at this level are placed, update peer contexts,
559
+ // fork as needed, and resolve peer deps that can be satisfied.
560
+ // This must happen AFTER placement so sibling nodes are available.
375
561
  postPlacementPeerCheck(graph, levelResults);
376
- // Collect all child dependencies for the next level
562
+ // ============================================================
563
+ // STEP 3: COLLECT CHILD DEPS FOR NEXT LEVEL
564
+ // ============================================================
377
565
  for (const childDepsToProcess of levelResults) {
378
566
  for (const childDep of childDepsToProcess) {
567
+ // Skip already-seen nodes (cycle prevention)
379
568
  if (!seen.has(childDep.node.id)) {
380
569
  /* c8 ignore next */
381
570
  const currentDepth = currentLevelDeps[0]?.depth ?? 0;
@@ -386,8 +575,7 @@ export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, op
386
575
  }
387
576
  }
388
577
  }
389
- // Move to the next level
578
+ // Advance to next BFS level
390
579
  currentLevelDeps = nextLevelDeps;
391
580
  }
392
581
  };
393
- //# sourceMappingURL=append-nodes.js.map
@@ -12,4 +12,3 @@ export type BuildIdealFromStartingGraphOptions = RefreshIdealGraphOptions & {
12
12
  * listed in the `add` and `remove` properties.
13
13
  */
14
14
  export declare const buildIdealFromStartingGraph: (options: BuildIdealFromStartingGraphOptions) => Promise<Graph>;
15
- //# sourceMappingURL=build-ideal-from-starting-graph.d.ts.map
@@ -67,4 +67,3 @@ export const buildIdealFromStartingGraph = async (options) => {
67
67
  options.graph.gc();
68
68
  return options.graph;
69
69
  };
70
- //# sourceMappingURL=build-ideal-from-starting-graph.js.map
@@ -38,4 +38,3 @@ export type BuildIdealOptions = LoadActualOptions & {
38
38
  * dependencies listed in the `add` and `remove` properties.
39
39
  */
40
40
  export declare const build: (options: BuildIdealOptions) => Promise<Graph>;
41
- //# sourceMappingURL=build.d.ts.map
@@ -3,6 +3,7 @@ import { graphStep } from '@vltpkg/output';
3
3
  import { load as loadActual } from "../actual/load.js";
4
4
  import { load as loadVirtual } from "../lockfile/load.js";
5
5
  import { buildIdealFromStartingGraph } from "./build-ideal-from-starting-graph.js";
6
+ import { isPathSecurityError } from "../path-security-error.js";
6
7
  import { splitDepID } from '@vltpkg/dep-id';
7
8
  const getMap = (m) => m ?? new Map();
8
9
  /**
@@ -39,7 +40,20 @@ export const build = async (options) => {
39
40
  monorepo,
40
41
  });
41
42
  }
42
- catch {
43
+ catch (err) {
44
+ // a path-safety verdict must not be silently rebuilt away
45
+ if (isPathSecurityError(err))
46
+ throw err;
47
+ // Check if this is a lockfile version mismatch
48
+ const cause = err.cause;
49
+ if (cause?.code === 'ELOCKFILEVERSION') {
50
+ // If lockfile is from a different vlt version, hard fail
51
+ if (typeof cause.found === 'number' &&
52
+ typeof cause.wanted === 'number') {
53
+ throw err;
54
+ }
55
+ // TODO: add warning to CLI layer via @vltpkg/output when available
56
+ }
43
57
  graph = loadActual({
44
58
  ...options,
45
59
  mainManifest,
@@ -72,4 +86,3 @@ export const build = async (options) => {
72
86
  }
73
87
  return res;
74
88
  };
75
- //# sourceMappingURL=build.js.map
@@ -18,4 +18,3 @@ export declare const getImporterSpecs: (options: GetImporterSpecsOptions) => {
18
18
  transientAdd: TransientAddMap;
19
19
  transientRemove: TransientRemoveMap;
20
20
  };
21
- //# sourceMappingURL=get-importer-specs.d.ts.map
@@ -49,8 +49,20 @@ export const getImporterSpecs = (options) => {
49
49
  // skip if the edge exists and already uses the same spec
50
50
  if (edge?.to && depSpec === edge.spec.bareSpec)
51
51
  continue;
52
+ const spec = Spec.parse(depName, depSpec, options);
53
+ // if a workspace dep references a workspace that no longer exists
54
+ // (folder was removed), mark it for removal instead of trying to
55
+ // resolve it — the workspace can't be satisfied
56
+ if (spec.type === 'workspace' && !edge?.to) {
57
+ const wsExists = [...graph.importers].some(n => n.name === depName);
58
+ if (!wsExists) {
59
+ removeDeps.add(depName);
60
+ removeResult.modifiedDependencies = true;
61
+ continue;
62
+ }
63
+ }
52
64
  const dependency = asDependency({
53
- spec: Spec.parse(depName, depSpec, options),
65
+ spec,
54
66
  type: shorten(depType, depName, importer.manifest),
55
67
  });
56
68
  addDeps.set(depName, dependency);
@@ -178,4 +190,3 @@ export const getImporterSpecs = (options) => {
178
190
  transientRemove,
179
191
  };
180
192
  };
181
- //# sourceMappingURL=get-importer-specs.js.map