@vltpkg/graph 1.5.0 → 1.6.1

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.
package/README.md CHANGED
@@ -93,13 +93,14 @@ code is not loaded. `update` and `uninstall` take `allowSkills` too.
93
93
 
94
94
  Lists the agent skills (dirs with a `SKILL.md`, see
95
95
  https://agentskills.io/specification) shipped by installed packages in
96
- the vlt store (a root `SKILL.md`, then `<dir>/SKILL.md` in `skills/`,
97
- `dist/skills/` and `.agents/skills/`; the first skill of a name wins),
98
- from one version per package (a direct dep, else the highest), with
99
- the agent skill dirs each is linked in, `project`: the main importer
100
- name, and the `graph`. `options.target` is a DSS query selecting
101
- packages (default `*`); a selector ending with `:skill(<name>, …)`
102
- keeps only those skills of its packages. Creates nothing.
96
+ the vlt store and the project's workspaces (a root `SKILL.md`, then
97
+ `<dir>/SKILL.md` in `skills/`, `dist/skills/` and `.agents/skills/`;
98
+ the first skill of a name wins), from one version per package (a
99
+ direct dep or workspace, else the highest), with the agent skill dirs
100
+ each is linked in, `project`: the main importer name, and the `graph`.
101
+ `options.target` is a DSS query selecting packages (default `*`); a
102
+ selector ending with `:skill(<name>, …)` keeps only those skills of
103
+ its packages. Creates nothing.
103
104
 
104
105
  ### `linkSkills(options): Promise<LinkSkillsResult>`
105
106
 
@@ -112,21 +113,23 @@ must resolve inside the project, not in `node_modules` (else
112
113
  `skipped`, as are fs errors) and are created only when a skill is
113
114
  picked. One package per name, among picked skills, in every dir: the
114
115
  one the first existing link points to while its skill is installed,
115
- else a direct dep, then DepID; the others are reported in `conflicts`
116
- as `<mount> (<package>)`. Installs keep a dir's link to another
117
- installed skill. A link follows its package to the picked version.
118
- Then removes links whose skill is no longer installed: dangling,
119
- package not in the graph, or no longer shipped. With `options.apply`,
120
- also removes every other vlt link in the scanned dirs. Only links
121
- pointing into a `node_modules/.vlt` store are ever replaced or
122
- removed; anything else in the way is reported in `conflicts`. Keeps a
123
- marked block of link names in each dir's `.gitignore` (`gitignore`
124
- lists the ones it could not edit). Linked packages get private copies
125
- of files hardlinked from the global store. `:skill()` patterns that
126
- match no skill of their packages are returned in `unmatched`. Security
127
- queries fail closed: skills of npm registry packages with no security
128
- data are left out (in `listSkills` too) and returned in `unscanned`
129
- with `unscannedReason` (by `linkSkills`: unless still linked).
116
+ else a direct dep or workspace, then DepID; the others are reported in
117
+ `conflicts` as `<mount> (<package>)`. Installs keep a dir's link to
118
+ another installed skill. A link follows its package to the picked
119
+ version. Then removes links whose skill is no longer installed:
120
+ dangling, package not in the graph, or no longer shipped. With
121
+ `options.apply`, also removes every other vlt link in the scanned
122
+ dirs. Only vlt links (into a `node_modules/.vlt` store, or listed in
123
+ the dir's `.gitignore` block and pointing to a workspace or dangling)
124
+ are ever replaced or removed; anything else in the way is reported in
125
+ `conflicts`. Keeps a marked block of link names in each dir's
126
+ `.gitignore` (`gitignore` lists the ones it could not edit). Linked
127
+ packages get private copies of files hardlinked from the global store.
128
+ `:skill()` patterns that match no skill of their packages are returned
129
+ in `unmatched`. Security queries fail closed: skills of npm registry
130
+ packages with no security data are left out (in `listSkills` too) and
131
+ returned in `unscanned` with `unscannedReason` (by `linkSkills`:
132
+ unless still linked).
130
133
 
131
134
  ### `assertQuery(selector, flag, partSelectors?): Promise<void>`
132
135
 
package/dist/graph.js CHANGED
@@ -347,6 +347,9 @@ export class Graph {
347
347
  }
348
348
  return edge;
349
349
  }
350
+ // a stale edgesIn ref would let removing the old target drop
351
+ // the new edge from `from.edgesOut`
352
+ edge.to?.edgesIn.delete(edge);
350
353
  this.edges.delete(edge);
351
354
  }
352
355
  this.mutations++;
@@ -419,7 +419,7 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
419
419
  * vlt store are extracted immediately (in parallel) instead of waiting for
420
420
  * the full ideal graph to be built. This significantly improves install time.
421
421
  */
422
- const processPlacementTasks = async (graph, options, placementTasks, add, modifiers, scurry, packageInfo, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove) => {
422
+ const processPlacementTasks = async (graph, options, placementTasks, add, modifiers, scurry, packageInfo, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove, explicit) => {
423
423
  const childDepsToProcess = [];
424
424
  // Note: placementTasks are already sorted by fetchManifestsForDeps
425
425
  // using compareByHasPeerDeps to ensure non-peer deps are processed first.
@@ -431,18 +431,31 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
431
431
  // reify step to properly update the package.json file dependencies
432
432
  // using the correct names retrieved from the manifest data
433
433
  const additiveMap = fromNode.importer ? add : transientAdd?.get(fromNode.id);
434
+ // a nameless explicit add is known by its fixed up name from here on
435
+ const explicitKeys = explicit?.get(fromNode.id);
436
+ const wasExplicit = explicitKeys?.has(addKey(spec));
434
437
  spec = fixupAddedNames(additiveMap, manifest, options, spec);
438
+ if (wasExplicit)
439
+ explicitKeys?.add(spec.name);
435
440
  // handles missing manifest resolution
436
441
  if (!manifest) {
437
442
  if (!edgeOptional && fromNode.isOptional()) {
438
443
  // failed resolution of a non-optional dep of an optional node
439
444
  // have to clean up the dependents
440
- removeOptionalSubgraph(graph, fromNode);
445
+ for (const n of removeOptionalSubgraph(graph, fromNode)) {
446
+ // pruned importer deps: nothing to save, unless requested
447
+ for (const { from, name } of n.edgesIn) {
448
+ if (from.importer && !explicit?.get(from.id)?.has(name)) {
449
+ add?.delete(name);
450
+ }
451
+ }
452
+ }
441
453
  continue;
442
454
  }
443
455
  else if (edgeOptional) {
444
- // failed resolution of an optional dep, just ignore it,
445
- // nothing to prune because we never added it in the first place.
456
+ // failed resolution of an optional dep: skip it and drop it
457
+ // from the add map, nothing to prune or save
458
+ additiveMap?.delete(addKey(spec));
446
459
  continue;
447
460
  }
448
461
  else {
@@ -738,7 +751,7 @@ export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, op
738
751
  graph.addEdge(type, spec, fromNode, toNode);
739
752
  }
740
753
  // Place nodes and collect child deps
741
- const placed = await processPlacementTasks(graph, options, result.placementTasks, add, modifiers, scurry, packageInfo, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove);
754
+ const placed = await processPlacementTasks(graph, options, result.placementTasks, add, modifiers, scurry, packageInfo, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove, explicit);
742
755
  levelResults.push(placed);
743
756
  }
744
757
  // ============================================================
@@ -31,18 +31,26 @@ export const removeSatisfiedSpecs = ({ add, graph, modifiers, }) => {
31
31
  const depIsCatalog = dependency.spec.type === 'catalog';
32
32
  if (edgeIsCatalog !== depIsCatalog)
33
33
  continue;
34
+ // a governed edge is checked against the modifier value (e.g.
35
+ // `workspace:*` to a versionless workspace), not its own spec: one
36
+ // built before the modifier applied must be rebuilt
37
+ const mod = modifiers?.importerEdgeModifier(importer, dependency.spec);
34
38
  // If the current graph edge is already valid, then we remove that
35
39
  // dependency item from the list of items to be added to the graph
36
- if (satisfies(edge.to?.id, dependency.spec, edge.from.location, graph.projectRoot, graph.monorepo)) {
37
- // a governed edge carries the modifier value by construction:
38
- // healing it to the manifest text would only be undone by the
39
- // rebuild that re-applies the override. `implicit` is not a real
40
- // edge type, it means "keep what the edge already is", so it
41
- // never makes an edge stale
42
- if ((edge.spec.bareSpec !== dependency.spec.bareSpec ||
43
- (dependency.type !== 'implicit' &&
44
- edge.type !== dependency.type)) &&
45
- !modifiers?.targetsImporterEdge(importer, dependency.spec)) {
40
+ if (satisfies(edge.to?.id, mod?.type === 'edge' ? mod.spec : dependency.spec, edge.from.location, graph.projectRoot, graph.monorepo)) {
41
+ // `implicit` is not a real edge type, it means "keep what the
42
+ // edge already is", so it never makes an edge stale
43
+ const typeChanged = dependency.type !== 'implicit' &&
44
+ edge.type !== dependency.type;
45
+ if (mod) {
46
+ // keeps the modifier value, the rebuild would re-apply it
47
+ // anyway; only the type follows the manifest
48
+ if (typeChanged) {
49
+ staleSpecs.set(edge, { ...dependency, spec: edge.spec });
50
+ }
51
+ }
52
+ else if (edge.spec.bareSpec !== dependency.spec.bareSpec ||
53
+ typeChanged) {
46
54
  staleSpecs.set(edge, dependency);
47
55
  }
48
56
  dependencies.delete(name);
package/dist/install.d.ts CHANGED
@@ -22,6 +22,8 @@ export type InstallOptions = LoadOptions & {
22
22
  * none
23
23
  */
24
24
  skillsDirs?: string[];
25
+ /** store dirs of these nodes are never removed, eg the running vlt */
26
+ keepNodes?: ReadonlySet<DepID>;
25
27
  };
26
28
  /**
27
29
  * Result of {@link install}.
package/dist/install.js CHANGED
@@ -47,7 +47,10 @@ export const install = async (options, add) => {
47
47
  }
48
48
  catch (err) {
49
49
  if (asError(err).message === 'Could not read package.json file') {
50
- await init({ cwd: options.projectRoot });
50
+ await init({
51
+ cwd: options.projectRoot,
52
+ packageJson: options.packageJson,
53
+ });
51
54
  mainManifest = options.packageJson.read(options.projectRoot, {
52
55
  reload: true,
53
56
  });
@@ -239,6 +242,7 @@ export const install = async (options, add) => {
239
242
  ...options,
240
243
  add,
241
244
  graph,
245
+ modifiers,
242
246
  remove,
243
247
  })
244
248
  : undefined;
@@ -1,7 +1,6 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
2
  import { asDepID, splitDepID, splitExtra, } from '@vltpkg/dep-id/browser';
3
3
  import { getBooleanFlagsFromNum, getBuildStateFromNum, } from "./types.js";
4
- import { tarballFormat } from '@vltpkg/types';
5
4
  export const loadNodes = (graph, nodes, options, actual, throwOnMissingManifest) => {
6
5
  const entries = Object.entries(nodes);
7
6
  const nodeCount = entries.length;
@@ -76,16 +75,12 @@ export const loadNodes = (graph, nodes, options, actual, throwOnMissingManifest)
76
75
  // before setResolved(), which needs it to pick the extension for a
77
76
  // node whose `resolved` the lockfile left out
78
77
  node.brotli = brotli;
79
- node.integrity = integrity ?? referenceNode?.integrity;
80
78
  // `integrity` is the hash of the artifact `resolved` points at, so a
81
- // reference url only stands in for an elided one when it names the
82
- // kind of tarball the flag bit says the lockfile's own hash is for
83
- const refResolved = referenceNode?.resolved;
84
- const refResolvedUsable = !integrity ||
85
- !refResolved ||
86
- (tarballFormat(refResolved) === 'brotli') === brotli;
79
+ // reference node only fills in for the kind the flag bit names
80
+ const ref = !!referenceNode?.brotli === brotli ? referenceNode : undefined;
81
+ node.integrity = integrity ?? ref?.integrity;
87
82
  node.resolved =
88
- type === 'remote' ? filepath : ((resolved ?? (refResolvedUsable ? refResolved : undefined)));
83
+ type === 'remote' ? filepath : (resolved ?? ref?.resolved);
89
84
  node.resolvedFromLockfile = !!(node.integrity && node.resolved);
90
85
  node.projectRoot = graph.projectRoot;
91
86
  if (!node.resolved)
@@ -101,6 +101,11 @@ export type ModifierActiveEntry = {
101
101
  * for (const entry of entries)
102
102
  * modifier.updateActiveEntry(fromNode, entry)
103
103
  * ```
104
+ *
105
+ * A trailing `:workspace` right after an importer selector, e.g.
106
+ * `:project > :workspace`, is expanded into one `#<name>` entry per
107
+ * named workspace of the graph, on its first `tryImporter` or
108
+ * `importerEdgeModifier` call.
104
109
  */
105
110
  export declare class GraphModifier {
106
111
  #private;
@@ -126,6 +131,12 @@ export declare class GraphModifier {
126
131
  * root's own b edge is still the manifest's to validate and heal.
127
132
  */
128
133
  targetsImporterEdge(importer: Node, spec: Spec): boolean;
134
+ /**
135
+ * The modifier governing the direct `importer -> spec.name` edge, see
136
+ * {@link GraphModifier.targetsImporterEdge}. The most specific one
137
+ * wins, same as during the graph build.
138
+ */
139
+ importerEdgeModifier(importer: Node, spec: Spec): ModifierEntry | undefined;
129
140
  /**
130
141
  * Selector of the modifier that wins the direct `importer -> spec.name`
131
142
  * edge, if it removes it (value `-`). Same pick as tryNewDependency:
package/dist/modifiers.js CHANGED
@@ -7,9 +7,20 @@ import { load } from '@vltpkg/vlt-json';
7
7
  * Does this breadcrumb item select the given importer?
8
8
  */
9
9
  const matchesImporter = (item, importer) => item.importer &&
10
+ importer.importer &&
10
11
  (item.value === ':project' ||
11
- (item.value === ':root' && importer.mainImporter) ||
12
- (item.value === ':workspace' && importer.importer));
12
+ item.value === ':workspace' ||
13
+ (item.value === ':root' && importer.mainImporter));
14
+ /**
15
+ * Is this a trailing `:workspace` item right after a plain importer
16
+ * selector, e.g. `:project > :workspace`? It selects any direct
17
+ * dependency named after a workspace.
18
+ */
19
+ const isWorkspaceTarget = (item) => item.importer &&
20
+ item.value === ':workspace' &&
21
+ !!item.prev?.importer &&
22
+ !item.prev.name &&
23
+ !item.prev.prev;
13
24
  /**
14
25
  * Does this item's qualifier (e.g. `:semver(^1)`, `:v(^1)`) accept the
15
26
  * given spec? Shared by traversal matching and by the frozen / healing
@@ -42,6 +53,11 @@ const matchesQualifier = (item, spec) => !!item?.comparator({ semver: spec.semve
42
53
  * for (const entry of entries)
43
54
  * modifier.updateActiveEntry(fromNode, entry)
44
55
  * ```
56
+ *
57
+ * A trailing `:workspace` right after an importer selector, e.g.
58
+ * `:project > :workspace`, is expanded into one `#<name>` entry per
59
+ * named workspace of the graph, on its first `tryImporter` or
60
+ * `importerEdgeModifier` call.
45
61
  */
46
62
  export class GraphModifier {
47
63
  /** The loaded modifiers configuration */
@@ -73,6 +89,14 @@ export class GraphModifier {
73
89
  activeModifiers = new Set();
74
90
  /** A set of all modifier string values loaded from vlt.json */
75
91
  modifierNames = new Set();
92
+ /** Spec options used when expanding `:workspace` targets */
93
+ #options = {};
94
+ /** `<importer> > :workspace` modifiers, expanded once a graph is known */
95
+ #workspaceTargets = [];
96
+ /** Graphs whose workspaces were already expanded */
97
+ #expandedGraphs = new WeakSet();
98
+ /** Workspace names already expanded */
99
+ #expandedNames = new Set();
76
100
  constructor(options) {
77
101
  this.load(options);
78
102
  }
@@ -96,8 +120,18 @@ export class GraphModifier {
96
120
  * root's own b edge is still the manifest's to validate and heal.
97
121
  */
98
122
  targetsImporterEdge(importer, spec) {
99
- for (const { breadcrumb } of this.#modifiers) {
100
- const { last } = breadcrumb;
123
+ return !!this.importerEdgeModifier(importer, spec);
124
+ }
125
+ /**
126
+ * The modifier governing the direct `importer -> spec.name` edge, see
127
+ * {@link GraphModifier.targetsImporterEdge}. The most specific one
128
+ * wins, same as during the graph build.
129
+ */
130
+ importerEdgeModifier(importer, spec) {
131
+ this.#expandWorkspaceTargets(importer);
132
+ const found = [];
133
+ for (const mod of this.#modifiers) {
134
+ const { last } = mod.breadcrumb;
101
135
  if (last.name !== spec.name)
102
136
  continue;
103
137
  // a qualifier that rejects the spec means the modifier never
@@ -106,14 +140,12 @@ export class GraphModifier {
106
140
  continue;
107
141
  const { prev } = last;
108
142
  // a lone `#b` matches an edge to b anywhere, importers included
109
- if (!prev)
110
- return true;
111
- if (prev.prev)
112
- continue;
113
- if (matchesImporter(prev, importer))
114
- return true;
143
+ if (!prev || (!prev.prev && matchesImporter(prev, importer))) {
144
+ found.push(mod);
145
+ }
115
146
  }
116
- return false;
147
+ const [best] = specificitySort(found.map(m => m.breadcrumb));
148
+ return found.find(m => m.breadcrumb === best);
117
149
  }
118
150
  /**
119
151
  * Selector of the modifier that wins the direct `importer -> spec.name`
@@ -143,16 +175,22 @@ export class GraphModifier {
143
175
  * Loads the modifiers defined in `vlt.json` into memory.
144
176
  */
145
177
  load(options) {
178
+ this.#options = options;
146
179
  for (const [key, value] of Object.entries(this.config)) {
147
180
  this.modifierNames.add(key);
148
181
  const breadcrumb = parseBreadcrumb(key);
149
- /* c8 ignore start - should not be possible */
150
182
  if (!breadcrumb.last.name) {
151
- throw error('Could not find name in breadcrumb', {
183
+ // `<importer> > :workspace` names no package up front, it is
184
+ // expanded into one entry per workspace once a graph is known
185
+ if (isWorkspaceTarget(breadcrumb.last)) {
186
+ this.#workspaceTargets.push({ query: key, value });
187
+ continue;
188
+ }
189
+ throw error('Invalid modifier selector', {
152
190
  found: key,
191
+ wanted: 'a selector ending in #<name>, or :root, :project or :workspace followed by > :workspace',
153
192
  });
154
193
  }
155
- /* c8 ignore stop */
156
194
  let mod;
157
195
  if (typeof value === 'string') {
158
196
  mod = {
@@ -179,13 +217,58 @@ export class GraphModifier {
179
217
  this.#nodeModifiers.add(mod);
180
218
  }
181
219
  /* c8 ignore end */
182
- this.#modifiers.add(mod);
183
- // if the breadcrumb starts with an id, then add it to the
184
- // map of initial entries, so that we can use it to match
185
- if (breadcrumb.first.name) {
186
- const initialSet = this.#initialEntries.get(breadcrumb.first.name) ?? new Set();
187
- initialSet.add(mod);
188
- this.#initialEntries.set(breadcrumb.first.name, initialSet);
220
+ this.#register(mod);
221
+ }
222
+ }
223
+ /**
224
+ * Adds a modifier entry to the set of known modifiers.
225
+ */
226
+ #register(mod) {
227
+ this.#modifiers.add(mod);
228
+ // if the breadcrumb starts with an id, then add it to the
229
+ // map of initial entries, so that we can use it to match
230
+ const { name } = mod.breadcrumb.first;
231
+ if (name) {
232
+ const initialSet = this.#initialEntries.get(name) ?? new Set();
233
+ initialSet.add(mod);
234
+ this.#initialEntries.set(name, initialSet);
235
+ }
236
+ }
237
+ /**
238
+ * Expands `<importer> > :workspace` modifiers into one
239
+ * `<importer> > #<name>` entry per named workspace of the node's graph.
240
+ */
241
+ #expandWorkspaceTargets(node) {
242
+ if (!this.#workspaceTargets.length)
243
+ return;
244
+ const { graph } = node;
245
+ if (this.#expandedGraphs.has(graph))
246
+ return;
247
+ this.#expandedGraphs.add(graph);
248
+ for (const importer of graph.importers) {
249
+ // nameless workspaces are named after their folder, never link those
250
+ const name = importer.manifest?.name;
251
+ if (importer.mainImporter ||
252
+ !name ||
253
+ this.#expandedNames.has(name)) {
254
+ continue;
255
+ }
256
+ this.#expandedNames.add(name);
257
+ for (const { query, value } of this.#workspaceTargets) {
258
+ // fresh breadcrumb per name: items are a linked list and the
259
+ // active entry maps key on the last item's name
260
+ const breadcrumb = parseBreadcrumb(query);
261
+ breadcrumb.last.name = name;
262
+ const mod = {
263
+ breadcrumb,
264
+ query,
265
+ refs: new Set(),
266
+ spec: Spec.parse(name, value, this.#options),
267
+ type: 'edge',
268
+ value,
269
+ };
270
+ this.#edgeModifiers.add(mod);
271
+ this.#register(mod);
189
272
  }
190
273
  }
191
274
  }
@@ -195,6 +278,7 @@ export class GraphModifier {
195
278
  * update the active entry to the current importer node.
196
279
  */
197
280
  tryImporter(importer) {
281
+ this.#expandWorkspaceTargets(importer);
198
282
  for (const modifier of this.#modifiers) {
199
283
  // if the first item in the breadcrumb is an importer and it matches
200
284
  // any of the valid top-level selectors, then register the modifier
package/dist/node.js CHANGED
@@ -371,6 +371,12 @@ export class Node {
371
371
  case 'registry':
372
372
  this.#registryNodeResolved(tuple);
373
373
  break;
374
+ case 'git':
375
+ // a hosted git committish is fetched as the host's tarball
376
+ this.resolved =
377
+ hydrateTuple(tuple, this.#name, this.#options).final
378
+ .remoteURL ?? resolved;
379
+ break;
374
380
  default:
375
381
  this.resolved = resolved;
376
382
  break;
@@ -7,3 +7,7 @@ import type { Edge } from '../edge.ts';
7
7
  * until scripts are run, in the case of non-store deps like workspaces
8
8
  */
9
9
  export declare const addEdge: (edge: Edge, scurry: PathScurry, remover: RollbackRemove, bins?: Record<string, string>) => Promise<void>;
10
+ /**
11
+ * link the `key` bin of the package at `path` into `binRoot`
12
+ */
13
+ export declare const linkBin: (path: string, binRoot: string, key: string, val: string, remover: RollbackRemove) => Promise<void>;
@@ -59,15 +59,21 @@ export const addEdge = async (edge, scurry, remover, bins) => {
59
59
  promises.push(p);
60
60
  if (bins) {
61
61
  for (const [key, val] of Object.entries(bins)) {
62
- const link = resolve(binRoot, key);
63
- const absTarget = resolve(path, val);
64
- const target = relative(binRoot, absTarget);
65
- // TODO: bash/cmd/ps1 shims on Windows
66
- promises.push(process.platform === 'win32' ?
67
- cmdShimIfExists(absTarget, link, remover)
68
- : clobberSymlink(target, link, remover));
62
+ promises.push(linkBin(path, binRoot, key, val, remover));
69
63
  }
70
64
  }
71
65
  if (promises.length)
72
66
  await Promise.all(promises);
73
67
  };
68
+ /**
69
+ * link the `key` bin of the package at `path` into `binRoot`
70
+ */
71
+ export const linkBin = (path, binRoot, key, val, remover) => {
72
+ const link = resolve(binRoot, key);
73
+ const absTarget = resolve(path, val);
74
+ const target = relative(binRoot, absTarget);
75
+ // TODO: bash/cmd/ps1 shims on Windows
76
+ return process.platform === 'win32' ?
77
+ cmdShimIfExists(absTarget, link, remover)
78
+ : clobberSymlink(target, link, remover);
79
+ };
@@ -1,15 +1,12 @@
1
+ import { readCmdShimIfExists } from '@vltpkg/cmd-shim';
1
2
  import { resolve } from 'node:path';
2
- const rmBinPosix = (remover, bin) => {
3
- return [remover.rm(bin)];
4
- };
5
- const rmBinWin32 = (remover, bin) => {
6
- return [
7
- remover.rm(bin),
8
- remover.rm(bin + '.cmd'),
9
- remover.rm(bin + '.ps1'),
10
- ];
11
- };
12
- const rmBin = process.platform === 'win32' ? rmBinWin32 : rmBinPosix;
3
+ const binPathsPosix = (bin) => [bin];
4
+ const binPathsWin32 = (bin) => [
5
+ bin,
6
+ bin + '.cmd',
7
+ bin + '.ps1',
8
+ ];
9
+ const binPaths = process.platform === 'win32' ? binPathsWin32 : binPathsPosix;
13
10
  export const deleteEdge = async (edge, scurry, remover) => {
14
11
  const { spec: { name }, to, } = edge;
15
12
  const { sep } = scurry.cwd;
@@ -20,9 +17,14 @@ export const deleteEdge = async (edge, scurry, remover) => {
20
17
  promises.push(remover.rm(path));
21
18
  const bins = to?.bins;
22
19
  if (bins) {
23
- for (const key of Object.keys(bins)) {
24
- const bin = resolve(binRoot, key);
25
- promises.push(...rmBin(remover, bin));
20
+ for (const [key, value] of Object.entries(bins)) {
21
+ for (const bin of binPaths(resolve(binRoot, key))) {
22
+ const target = await readCmdShimIfExists(bin);
23
+ // another dependency may provide the same command.
24
+ if (target && target !== resolve(path, value))
25
+ continue;
26
+ promises.push(remover.rm(bin));
27
+ }
26
28
  }
27
29
  }
28
30
  await Promise.all(promises);
@@ -1,4 +1,5 @@
1
+ import type { DepID } from '@vltpkg/dep-id';
1
2
  import type { RollbackRemove } from '@vltpkg/rollback-remove';
2
3
  import type { PathScurry } from 'path-scurry';
3
4
  import type { Diff } from '../diff.ts';
4
- export declare const deleteNodes: (diff: Diff, remover: RollbackRemove, scurry: PathScurry) => Promise<unknown>[];
5
+ export declare const deleteNodes: (diff: Diff, remover: RollbackRemove, scurry: PathScurry, keep?: ReadonlySet<DepID>) => Promise<unknown>[];
@@ -1,5 +1,5 @@
1
1
  import { deleteEdge } from "./delete-edge.js";
2
- export const deleteNodes = (diff, remover, scurry) => {
2
+ export const deleteNodes = (diff, remover, scurry, keep) => {
3
3
  const store = scurry.resolve('node_modules/.vlt');
4
4
  const { sep } = scurry.cwd;
5
5
  const rmActions = [];
@@ -7,7 +7,9 @@ export const deleteNodes = (diff, remover, scurry) => {
7
7
  // do not delete workspaces or link targets
8
8
  if (!node.inVltStore())
9
9
  continue;
10
- rmActions.push(remover.rm(store + sep + node.id));
10
+ if (!keep?.has(node.id)) {
11
+ rmActions.push(remover.rm(store + sep + node.id));
12
+ }
11
13
  for (const edge of node.edgesIn) {
12
14
  rmActions.push(deleteEdge(edge, scurry, remover));
13
15
  }
@@ -18,6 +18,8 @@ export type ReifyOptions = LoadOptions & {
18
18
  update?: boolean;
19
19
  saveExact?: boolean;
20
20
  savePrefix?: string;
21
+ /** store dirs of these nodes are never removed, eg the running vlt */
22
+ keepNodes?: ReadonlySet<DepID>;
21
23
  };
22
24
  export type ReifyResult = {
23
25
  /**
@@ -8,6 +8,7 @@ import { addEdges } from "./add-edges.js";
8
8
  import { addNodes } from "./add-nodes.js";
9
9
  import { build } from "./build.js";
10
10
  import { deleteEdges } from "./delete-edges.js";
11
+ import { relinkBins } from "./relink-bins.js";
11
12
  import { carryBuildState, checkNeededBuild, } from "./check-needed-build.js";
12
13
  import { deleteNodes } from "./delete-nodes.js";
13
14
  import { internalHoist } from "./internal-hoist.js";
@@ -47,6 +48,7 @@ export const reify = async (options) => {
47
48
  packageJson: options.packageJson,
48
49
  saveExact: options.saveExact,
49
50
  savePrefix: options.savePrefix,
51
+ modifiers: options.modifiers,
50
52
  })
51
53
  : undefined;
52
54
  const noModifiedDependencies = !options.add?.modifiedDependencies &&
@@ -127,6 +129,10 @@ const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
127
129
  const edgeActions = addEdges(diff, scurry, remover);
128
130
  if (edgeActions.length)
129
131
  await callLimit(edgeActions, { limit });
132
+ // link bins that deleted edges removed but other deps still declare
133
+ const binActions = relinkBins(diff, scurry, remover);
134
+ if (binActions.length)
135
+ await callLimit(binActions, { limit });
130
136
  await internalHoist(diff.to, options, remover);
131
137
  // looks up all nodes setting buildState = 'needed'
132
138
  // on nodes that require building
@@ -155,7 +161,7 @@ const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
155
161
  /* c8 ignore stop */
156
162
  saveHidden(options);
157
163
  // delete garbage from the store.
158
- const rmActions = deleteNodes(diff, remover, scurry);
164
+ const rmActions = deleteNodes(diff, remover, scurry, options.keepNodes);
159
165
  if (rmActions.length)
160
166
  await Promise.all(rmActions);
161
167
  // updates package.json files if anything was added / removed
@@ -0,0 +1,9 @@
1
+ import type { RollbackRemove } from '@vltpkg/rollback-remove';
2
+ import type { PathScurry } from 'path-scurry';
3
+ import type { Diff } from '../diff.ts';
4
+ /**
5
+ * A deleted edge removes the bins it owns, but another dependency
6
+ * that did not change may declare the same bin name. Link the first
7
+ * one found if the bin is still missing once added edges are linked.
8
+ */
9
+ export declare const relinkBins: (diff: Diff, scurry: PathScurry, remover: RollbackRemove) => (() => Promise<unknown>)[];