@vltpkg/graph 1.3.8 → 1.4.0

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.
@@ -246,6 +246,10 @@ const parseDir = (options, scurry, packageJson, depsFound, graph, fromNode, curr
246
246
  });
247
247
  // Check for active modifiers and replace spec for missing dependencies
248
248
  const { spec: modifiedSpec, queryModifier } = maybeApplyModifierToSpec(spec, name, modifierRefs);
249
+ // a `-` modifier removes the dependency, so it is not missing
250
+ if (modifiedSpec !== spec && modifiedSpec.bareSpec === '-') {
251
+ continue;
252
+ }
249
253
  spec = modifiedSpec;
250
254
  graph.placePackage(fromNode, depType, spec, undefined, undefined, joinExtra({ modifier: queryModifier }));
251
255
  }
@@ -228,6 +228,10 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
228
228
  spec = activeModifier.modifier.spec;
229
229
  // bareSpec of '-' means "remove this dependency"
230
230
  if (spec.bareSpec === '-') {
231
+ // nothing to install or save for an explicit request
232
+ if (isExplicit) {
233
+ throw error(`Cannot add ${originalSpec.name}: it is removed by a graph modifier`, { code: 'ECONFIG', found: queryModifier });
234
+ }
231
235
  continue;
232
236
  }
233
237
  }
@@ -366,6 +370,8 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
366
370
  }
367
371
  throw er;
368
372
  });
373
+ // awaited below; a throw before that must not leave it unhandled
374
+ manifestPromise.catch(() => { });
369
375
  const fetchTask = {
370
376
  spec,
371
377
  type,
@@ -380,6 +386,8 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
380
386
  };
381
387
  fetchTasks.push(fetchTask);
382
388
  }
389
+ // fail on the first rejection, not once the ones before it settle
390
+ await Promise.all(fetchTasks.map(t => t.manifestPromise));
383
391
  // Create placement tasks from fetch tasks
384
392
  for (const fetchTask of fetchTasks) {
385
393
  const manifest = await fetchTask.manifestPromise;
@@ -79,6 +79,14 @@ export const buildIdealFromStartingGraph = async (options) => {
79
79
  if (importerSpecs.staleSpecs.size) {
80
80
  options.graph.lockfileStale = true;
81
81
  }
82
+ // importer edges a `-` modifier now removes: drop them, gc() takes
83
+ // whatever only they reached
84
+ for (const edge of importerSpecs.removedEdges) {
85
+ edge.from.edgesOut.delete(edge.name);
86
+ edge.to?.edgesIn.delete(edge);
87
+ options.graph.edges.delete(edge);
88
+ options.graph.lockfileStale = true;
89
+ }
82
90
  // refreshs the current graph adding the nodes marked for addition
83
91
  // and removing the ones marked for removal, while also recalculating
84
92
  // peer dependencies and default locations
@@ -18,6 +18,7 @@ export type GetImporterSpecsOptions = BuildIdealAddOptions & BuildIdealFromGraph
18
18
  export declare const getImporterSpecs: (options: GetImporterSpecsOptions) => {
19
19
  add: AddImportersDependenciesMap;
20
20
  remove: RemoveImportersDependenciesMap;
21
+ removedEdges: Set<Edge>;
21
22
  staleSpecs: Map<Edge, Dependency>;
22
23
  transientAdd: TransientAddMap;
23
24
  transientRemove: TransientRemoveMap;
@@ -25,6 +25,8 @@ export const getImporterSpecs = (options) => {
25
25
  const { add, graph, remove } = options;
26
26
  const addResult = new AddImportersDependenciesMapImpl();
27
27
  const removeResult = new RemoveImportersDependenciesMapImpl();
28
+ // importer edges a `-` modifier now removes, package.json keeps them
29
+ const removedEdges = new Set();
28
30
  // traverse the list of importers in the starting graph
29
31
  for (const importer of graph.importers) {
30
32
  // uses a Map keying to the spec.name in order to easily make sure there's
@@ -71,6 +73,13 @@ export const getImporterSpecs = (options) => {
71
73
  continue;
72
74
  }
73
75
  const spec = Spec.parse(depName, depSpec, options);
76
+ // a `-` modifier removes this dependency, nothing to place; an
77
+ // edge from before (e.g. a qualifier now matching) has to go
78
+ if (options.modifiers?.removesImporterEdge(importer, spec)) {
79
+ if (edge)
80
+ removedEdges.add(edge);
81
+ continue;
82
+ }
74
83
  // if a workspace dep references a workspace that no longer exists
75
84
  // (folder was removed), mark it for removal instead of trying to
76
85
  // resolve it — the workspace can't be satisfied
@@ -212,6 +221,7 @@ export const getImporterSpecs = (options) => {
212
221
  return {
213
222
  add: addResult,
214
223
  remove: removeResult,
224
+ removedEdges,
215
225
  staleSpecs,
216
226
  transientAdd,
217
227
  transientRemove,
@@ -99,7 +99,12 @@ export const refreshIdealGraph = async ({ add, graph, modifiers, packageInfo, sc
99
99
  const modifierRefs = modifiers?.tryDependencies(importer, deps);
100
100
  // Add new nodes for packages defined in the dependencies list fetching
101
101
  // metadata from the registry manifests and updating the graph
102
- await appendNodes(packageInfo, graph, importer, deps, scurry, specOptions, seen, addedDeps, modifiers, modifierRefs, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove, explicit);
102
+ await appendNodes(packageInfo, graph, importer, deps, scurry, specOptions, seen, addedDeps, modifiers, modifierRefs, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove, explicit).catch((er) => {
103
+ // extractions still in flight are moot now
104
+ for (const p of extractPromises ?? [])
105
+ p.catch(() => { });
106
+ throw er;
107
+ });
103
108
  }
104
109
  // locked resolutions only apply to the rebuild that captured them
105
110
  graph.lockedResolutions = undefined;
package/dist/install.js CHANGED
@@ -7,6 +7,7 @@ import { error } from '@vltpkg/error-cause';
7
7
  import { asError } from '@vltpkg/types';
8
8
  import { getDependencies } from "./dependencies.js";
9
9
  import { RollbackRemove } from '@vltpkg/rollback-remove';
10
+ import { joinDepIDTuple } from '@vltpkg/dep-id';
10
11
  import { existsSync, rmSync } from 'node:fs';
11
12
  import { resolve } from 'node:path';
12
13
  import { load as loadVirtual, loadData } from "./lockfile/load.js";
@@ -109,6 +110,7 @@ export const install = async (options, add) => {
109
110
  }
110
111
  if (importerSpecs.add.modifiedDependencies ||
111
112
  importerSpecs.remove.modifiedDependencies ||
113
+ importerSpecs.removedEdges.size > 0 ||
112
114
  specChanges.length > 0 ||
113
115
  lockfileGraph.optionsChanged) {
114
116
  const details = [];
@@ -141,6 +143,9 @@ export const install = async (options, add) => {
141
143
  details.push(` ${location}: ${deps.size} ${depLabelRemove} to remove (${depNames.join(', ')})`);
142
144
  }
143
145
  }
146
+ for (const edge of importerSpecs.removedEdges) {
147
+ details.push(` ${edge.from.location}: ${edge.name} is removed by a graph modifier`);
148
+ }
144
149
  const lockfilePath = resolve(options.projectRoot, 'vlt-lock.json');
145
150
  throw error('Lockfile is out of sync with package.json. Run "vlt install" to update.\n' +
146
151
  details.join('\n'), {
@@ -148,6 +153,23 @@ export const install = async (options, add) => {
148
153
  });
149
154
  }
150
155
  }
156
+ // an explicit add of a dep a `-` modifier removes fails before
157
+ // anything on disk is touched
158
+ if (modifiers && add?.modifiedDependencies) {
159
+ const wsIds = new Set([...(fullMonorepo?.values() ?? [])].map(ws => ws.id));
160
+ for (const [id, deps] of add) {
161
+ const mainImporter = id === joinDepIDTuple(['file', '.']);
162
+ // nested folders are no importers, appendNodes checks those
163
+ if (!mainImporter && !wsIds.has(id))
164
+ continue;
165
+ for (const { spec } of deps.values()) {
166
+ const found = modifiers.removesImporterEdge({ importer: true, mainImporter }, spec);
167
+ if (found) {
168
+ throw error(`Cannot add ${spec.name}: it is removed by a graph modifier`, { code: 'ECONFIG', found });
169
+ }
170
+ }
171
+ }
172
+ }
151
173
  const remover = new RollbackRemove();
152
174
  if (options.cleanInstall) {
153
175
  const nodeModulesPath = resolve(options.projectRoot, 'node_modules');
@@ -52,11 +52,9 @@ export const loadEdges = (graph, edges, options, cache) => {
52
52
  const [depType, valRest] = fastSplit(value, ' ', 2);
53
53
  const vrSplit = valRest?.lastIndexOf(' ') ?? -1;
54
54
  // not a valid edge record
55
- /* c8 ignore start */
56
55
  if (!valRest || !depType || !fromId || !specName || vrSplit < 1) {
57
56
  continue;
58
57
  }
59
- /* c8 ignore stop */
60
58
  // Validate dependency type early
61
59
  if (!isDependencyTypeShort(depType)) {
62
60
  throw error('Found unsupported dependency type in lockfile', {
@@ -126,6 +126,13 @@ export declare class GraphModifier {
126
126
  * root's own b edge is still the manifest's to validate and heal.
127
127
  */
128
128
  targetsImporterEdge(importer: Node, spec: Spec): boolean;
129
+ /**
130
+ * Selector of the modifier that wins the direct `importer -> spec.name`
131
+ * edge, if it removes it (value `-`). Same pick as tryNewDependency:
132
+ * complete entries whose qualifier accepts the spec, highest
133
+ * specificity, importer-anchored before lone on ties.
134
+ */
135
+ removesImporterEdge(importer: Pick<Node, 'importer' | 'mainImporter'>, spec: Spec): string | undefined;
129
136
  /**
130
137
  * Loads the modifiers defined in `vlt.json` into memory.
131
138
  */
package/dist/modifiers.js CHANGED
@@ -115,6 +115,30 @@ export class GraphModifier {
115
115
  }
116
116
  return false;
117
117
  }
118
+ /**
119
+ * Selector of the modifier that wins the direct `importer -> spec.name`
120
+ * edge, if it removes it (value `-`). Same pick as tryNewDependency:
121
+ * complete entries whose qualifier accepts the spec, highest
122
+ * specificity, importer-anchored before lone on ties.
123
+ */
124
+ removesImporterEdge(importer, spec) {
125
+ const anchored = [];
126
+ const lone = [];
127
+ for (const mod of this.#modifiers) {
128
+ const { last } = mod.breadcrumb;
129
+ if (last.name !== spec.name || !matchesQualifier(last, spec))
130
+ continue;
131
+ const { prev } = last;
132
+ if (!prev)
133
+ lone.push(mod);
134
+ else if (!prev.prev && matchesImporter(prev, importer))
135
+ anchored.push(mod);
136
+ }
137
+ const all = [...anchored, ...lone];
138
+ const [winner] = specificitySort(all.map(m => m.breadcrumb));
139
+ const mod = all.find(m => m.breadcrumb === winner);
140
+ return mod?.value === '-' ? mod.query : undefined;
141
+ }
118
142
  /**
119
143
  * Loads the modifiers defined in `vlt.json` into memory.
120
144
  */
@@ -89,7 +89,11 @@ export const reify = async (options) => {
89
89
  // Otherwise a first install of an optional-only tree would never extract.
90
90
  const hasLockfiles = !!scurry.lstatSync('vlt-lock.json') &&
91
91
  !!scurry.lstatSync('node_modules/.vlt-lock.json');
92
- const skipOptionalOnly = noModifiedDependencies && diff.optionalOnly;
92
+ // a removal is never optional-only: deleting is what reify is for
93
+ const skipOptionalOnly = noModifiedDependencies &&
94
+ diff.optionalOnly &&
95
+ !diff.nodes.delete.size &&
96
+ !diff.edges.delete.size;
93
97
  const skippable = skipOptionalOnly && !options.update && hasLockfiles;
94
98
  const res = { diff };
95
99
  if (!diff.hasChanges() || skippable) {
package/dist/uninstall.js CHANGED
@@ -38,6 +38,7 @@ export const uninstall = async (options, remove) => {
38
38
  remove,
39
39
  mainManifest,
40
40
  loadManifests: true,
41
+ modifiers,
41
42
  remover,
42
43
  });
43
44
  // If lockfileOnly is enabled, skip reify and only save the lockfile
@@ -67,6 +68,7 @@ export const uninstall = async (options, remove) => {
67
68
  actual: act,
68
69
  graph,
69
70
  loadManifests: true,
71
+ modifiers,
70
72
  remover,
71
73
  });
72
74
  return { graph, diff };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/graph",
3
3
  "description": "A library that helps understanding & expressing what happens on an install",
4
- "version": "1.3.8",
4
+ "version": "1.4.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,26 +13,26 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/cmd-shim": "1.3.8",
17
- "@vltpkg/dep-id": "1.3.8",
18
- "@vltpkg/dss-breadcrumb": "1.3.8",
19
- "@vltpkg/error-cause": "1.3.8",
20
- "@vltpkg/fast-split": "1.3.8",
21
- "@vltpkg/graph-run": "1.3.8",
22
- "@vltpkg/init": "1.3.8",
23
- "@vltpkg/output": "1.3.8",
24
- "@vltpkg/package-info": "1.3.8",
25
- "@vltpkg/package-json": "1.3.8",
26
- "@vltpkg/pick-manifest": "1.3.8",
27
- "@vltpkg/query": "1.3.8",
28
- "@vltpkg/rollback-remove": "1.3.8",
29
- "@vltpkg/run": "1.3.8",
30
- "@vltpkg/satisfies": "1.3.8",
31
- "@vltpkg/security-archive": "1.3.8",
32
- "@vltpkg/spec": "1.3.8",
33
- "@vltpkg/types": "1.3.8",
34
- "@vltpkg/vlt-json": "1.3.8",
35
- "@vltpkg/workspaces": "1.3.8",
16
+ "@vltpkg/cmd-shim": "1.4.0",
17
+ "@vltpkg/dep-id": "1.4.0",
18
+ "@vltpkg/dss-breadcrumb": "1.4.0",
19
+ "@vltpkg/error-cause": "1.4.0",
20
+ "@vltpkg/fast-split": "1.4.0",
21
+ "@vltpkg/graph-run": "1.4.0",
22
+ "@vltpkg/init": "1.4.0",
23
+ "@vltpkg/output": "1.4.0",
24
+ "@vltpkg/package-info": "1.4.0",
25
+ "@vltpkg/package-json": "1.4.0",
26
+ "@vltpkg/pick-manifest": "1.4.0",
27
+ "@vltpkg/query": "1.4.0",
28
+ "@vltpkg/rollback-remove": "1.4.0",
29
+ "@vltpkg/run": "1.4.0",
30
+ "@vltpkg/satisfies": "1.4.0",
31
+ "@vltpkg/security-archive": "1.4.0",
32
+ "@vltpkg/spec": "1.4.0",
33
+ "@vltpkg/types": "1.4.0",
34
+ "@vltpkg/vlt-json": "1.4.0",
35
+ "@vltpkg/workspaces": "1.4.0",
36
36
  "path-scurry": "^2.0.1",
37
37
  "promise-call-limit": "^3.0.2"
38
38
  },