@vltpkg/graph 1.3.8 → 1.5.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.
Files changed (45) hide show
  1. package/README.md +59 -3
  2. package/dist/actual/load.js +28 -12
  3. package/dist/build.js +30 -5
  4. package/dist/dependencies.d.ts +11 -0
  5. package/dist/dependencies.js +47 -7
  6. package/dist/filter-nodes-by-query.d.ts +39 -0
  7. package/dist/filter-nodes-by-query.js +100 -0
  8. package/dist/ideal/append-nodes.js +19 -6
  9. package/dist/ideal/build-ideal-from-starting-graph.js +8 -0
  10. package/dist/ideal/get-importer-specs.d.ts +1 -0
  11. package/dist/ideal/get-importer-specs.js +27 -7
  12. package/dist/ideal/refresh-ideal-graph.js +6 -1
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.js +2 -0
  15. package/dist/install.d.ts +23 -8
  16. package/dist/install.js +46 -1
  17. package/dist/lockfile/load-edges.js +10 -4
  18. package/dist/modifiers.d.ts +7 -0
  19. package/dist/modifiers.js +24 -0
  20. package/dist/reify/build.d.ts +7 -0
  21. package/dist/reify/check-needed-build.d.ts +5 -0
  22. package/dist/reify/check-needed-build.js +13 -0
  23. package/dist/reify/index.js +9 -39
  24. package/dist/reify/update-importers-package-json.js +5 -9
  25. package/dist/skills/dirs.d.ts +58 -0
  26. package/dist/skills/dirs.js +211 -0
  27. package/dist/skills/discover.d.ts +73 -0
  28. package/dist/skills/discover.js +234 -0
  29. package/dist/skills/gitignore.d.ts +13 -0
  30. package/dist/skills/gitignore.js +53 -0
  31. package/dist/skills/index.d.ts +123 -0
  32. package/dist/skills/index.js +225 -0
  33. package/dist/skills/lazy.d.ts +8 -0
  34. package/dist/skills/lazy.js +8 -0
  35. package/dist/skills/mask.d.ts +26 -0
  36. package/dist/skills/mask.js +43 -0
  37. package/dist/skills/mounts.d.ts +64 -0
  38. package/dist/skills/mounts.js +136 -0
  39. package/dist/uninstall.d.ts +24 -7
  40. package/dist/uninstall.js +26 -1
  41. package/dist/update.d.ts +24 -4
  42. package/dist/update.js +24 -1
  43. package/dist/visualization/mermaid-output.d.ts +8 -0
  44. package/dist/visualization/mermaid-output.js +25 -8
  45. package/package.json +21 -21
package/README.md CHANGED
@@ -78,15 +78,71 @@ minimal filesystem changes (creating/deleting links, writing
78
78
  lockfiles, hoisting, lifecycle scripts) to make the on-disk install
79
79
  match the Ideal graph. Returns `{ diff, buildQueue }`.
80
80
 
81
- ### `install(options, add?): Promise<{ graph, diff, buildQueue }>`
81
+ ### `install(options, add?): Promise<{ graph, diff, buildQueue, skills? }>`
82
82
 
83
83
  High-level install orchestration that handles graph building, reify,
84
84
  and lockfile management. Supports `--frozen-lockfile`,
85
- `--clean-install`, and `--lockfile-only` modes.
85
+ `--clean-install`, and `--lockfile-only` modes. With `allowSkills` (a
86
+ DSS query whose selectors may end with `:skill()`, validated up
87
+ front), links the agent skills of matching packages afterwards (see
88
+ `linkSkills`) into `skillsDirs` (unset: detected) and returns the
89
+ result as `skills`. Unset or `:not(*)`: no skills work, the skills
90
+ code is not loaded. `update` and `uninstall` take `allowSkills` too.
91
+
92
+ ### `listSkills(options): Promise<{ project, graph, skills, dirs, skipped? }>`
93
+
94
+ Lists the agent skills (dirs with a `SKILL.md`, see
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.
103
+
104
+ ### `linkSkills(options): Promise<LinkSkillsResult>`
105
+
106
+ Links the skills picked by `options.target` (as in `listSkills`; comma
107
+ lists are a union) into each agent skill dir (junctions on Windows):
108
+ `options.skillsDirs`, else `.agents/skills`, plus `<folder>/skills`
109
+ for each agent folder at the project root, `.claude/skills` inside
110
+ Claude Code (`options.env`), and existing `.<name>/skills` dirs. Dirs
111
+ must resolve inside the project, not in `node_modules` (else
112
+ `skipped`, as are fs errors) and are created only when a skill is
113
+ picked. One package per name, among picked skills, in every dir: the
114
+ 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).
130
+
131
+ ### `assertQuery(selector, flag, partSelectors?): Promise<void>`
132
+
133
+ Throws `EUSAGE` unless `selector` is a valid DSS query.
134
+ `partSelectors` (e.g. `['skill']`) allows `:skill()`.
135
+
136
+ ### `hasSkills(node): Promise<boolean>`
137
+
138
+ `:skills` provider for `Query`: true when an installed package ships
139
+ agent skills, as `listSkills` finds them.
86
140
 
87
141
  ### `mermaidOutput(graph): string`
88
142
 
89
- Generates Mermaid flowchart syntax from graph data.
143
+ Generates Mermaid flowchart syntax from graph data. `graph.children`
144
+ (`Map<DepID, { label, edge? }[]>`) draws leaves under nodes, e.g.
145
+ `vlt skills list` skills.
90
146
 
91
147
  ### `humanReadableOutput(graph, options): string`
92
148
 
@@ -2,7 +2,7 @@ import { asDepID, hydrate, joinDepIDTuple, joinExtra, splitDepID, splitExtra, }
2
2
  import { Spec } from '@vltpkg/spec';
3
3
  import { graphStep } from '@vltpkg/output';
4
4
  import { isObject } from '@vltpkg/types';
5
- import { shorten, getRawDependencies, getDependencies, } from "../dependencies.js";
5
+ import { shorten, getRawDependencies, getDependencies, asDependencySpecError, } from "../dependencies.js";
6
6
  import { Graph } from "../graph.js";
7
7
  import { loadHidden } from "../lockfile/load.js";
8
8
  import { saveHidden } from "../lockfile/save.js";
@@ -178,13 +178,19 @@ const parseDir = (options, scurry, packageJson, depsFound, graph, fromNode, curr
178
178
  const type = deps?.type || 'dependencies';
179
179
  const bareSpec = deps?.bareSpec || '*';
180
180
  const depType = shorten(type, alias, fromNode.manifest);
181
- let spec = Spec.parse(alias, bareSpec, {
182
- ...options,
183
- // fall back to options.registry (which the lockfile merges into)
184
- // so importer-level edges don't silently revert to the default
185
- // npm registry. see vltpkg/vltpkg#1580.
186
- registry: fromNode.registry ?? options.registry,
187
- });
181
+ let spec;
182
+ try {
183
+ spec = Spec.parse(alias, bareSpec, {
184
+ ...options,
185
+ // fall back to options.registry (which the lockfile merges into)
186
+ // so importer-level edges don't silently revert to the default
187
+ // npm registry. see vltpkg/vltpkg#1580.
188
+ registry: fromNode.registry ?? options.registry,
189
+ });
190
+ }
191
+ catch (er) {
192
+ throw asDependencySpecError(er, fromNode, depType);
193
+ }
188
194
  // Check for active modifiers and replace spec if a modifier is complete
189
195
  const { spec: modifiedSpec, queryModifier } = maybeApplyModifierToSpec(spec, alias, modifierRefs);
190
196
  spec = modifiedSpec;
@@ -240,12 +246,22 @@ const parseDir = (options, scurry, packageJson, depsFound, graph, fromNode, curr
240
246
  for (const { name, type, bareSpec } of dependencies.values()) {
241
247
  if (!seenDeps.has(name)) {
242
248
  const depType = shorten(type, name, fromNode.manifest);
243
- let spec = Spec.parse(name, bareSpec, {
244
- ...options,
245
- registry: fromNode.registry ?? options.registry,
246
- });
249
+ let spec;
250
+ try {
251
+ spec = Spec.parse(name, bareSpec, {
252
+ ...options,
253
+ registry: fromNode.registry ?? options.registry,
254
+ });
255
+ }
256
+ catch (er) {
257
+ throw asDependencySpecError(er, fromNode, depType);
258
+ }
247
259
  // Check for active modifiers and replace spec for missing dependencies
248
260
  const { spec: modifiedSpec, queryModifier } = maybeApplyModifierToSpec(spec, name, modifierRefs);
261
+ // a `-` modifier removes the dependency, so it is not missing
262
+ if (modifiedSpec !== spec && modifiedSpec.bareSpec === '-') {
263
+ continue;
264
+ }
249
265
  spec = modifiedSpec;
250
266
  graph.placePackage(fromNode, depType, spec, undefined, undefined, joinExtra({ modifier: queryModifier }));
251
267
  }
package/dist/build.js CHANGED
@@ -4,10 +4,12 @@ import { build as reifyBuild } from "./reify/build.js";
4
4
  import { Diff } from "./diff.js";
5
5
  import { Graph } from "./graph.js";
6
6
  import { Query } from '@vltpkg/query';
7
- import { SecurityArchive } from '@vltpkg/security-archive';
7
+ import { SecurityArchive, usesNpmRegistry, } from '@vltpkg/security-archive';
8
8
  import { saveHidden } from "./lockfile/save.js";
9
+ import { hasSkills } from "./skills/lazy.js";
9
10
  /**
10
- * Filter nodes using a DSS query string
11
+ * Filter nodes using a DSS query string. `skipped` holds matched nodes
12
+ * left out because they have no security data, `skippedReason` says why.
11
13
  */
12
14
  const filterNodesByQuery = async (targetQuery, graph) => {
13
15
  /* c8 ignore start */
@@ -25,11 +27,30 @@ const filterNodesByQuery = async (targetQuery, graph) => {
25
27
  nodes,
26
28
  importers,
27
29
  securityArchive,
30
+ hasSkills,
28
31
  });
29
32
  const { nodes: resultNodes } = await query.search(targetQuery, {
30
33
  signal: new AbortController().signal,
31
34
  });
32
- return new Set(resultNodes.map(node => node.id));
35
+ // fail closed: never run scripts of pkgs with no security data, be it
36
+ // from a timed out request or not scanned by socket.dev yet
37
+ const allowed = new Set();
38
+ const skipped = new Set();
39
+ for (const node of resultNodes) {
40
+ if (securityArchive &&
41
+ !securityArchive.has(node.id) &&
42
+ usesNpmRegistry(node)) {
43
+ skipped.add(node.id);
44
+ }
45
+ else {
46
+ allowed.add(node.id);
47
+ }
48
+ }
49
+ return {
50
+ allowed,
51
+ skipped,
52
+ skippedReason: securityArchive?.timedOut ? 'timeout' : 'unscanned',
53
+ };
33
54
  };
34
55
  /**
35
56
  * Build the project based on actual graph state and build state from lockfile
@@ -54,7 +75,7 @@ export const build = async (options) => {
54
75
  loadManifests: true,
55
76
  });
56
77
  // Filter nodes using target query provided
57
- const targetFilteredNodes = await filterNodesByQuery(target, actualGraph);
78
+ const { allowed: targetFilteredNodes, skipped, skippedReason, } = await filterNodesByQuery(target, actualGraph);
58
79
  // Create a total diff including the actual graph as 'to'
59
80
  const diff = new Diff(new Graph({
60
81
  ...options,
@@ -83,5 +104,9 @@ export const build = async (options) => {
83
104
  graph: actualGraph,
84
105
  });
85
106
  }
86
- return buildResult;
107
+ // needed builds left out for lack of security data
108
+ const skippedNodes = [...diff.nodes.add].filter(node => skipped.has(node.id));
109
+ return skippedNodes.length ?
110
+ { ...buildResult, skipped: skippedNodes, skippedReason }
111
+ : buildResult;
87
112
  };
@@ -74,6 +74,17 @@ export declare const shorten: (typeLong: DependencyTypeLong, name?: string, mani
74
74
  */
75
75
  export declare const shouldInstallDepType: (node: NodeLike, depType: DependencyTypeLong) => boolean;
76
76
  export declare const getRawDependencies: (node: NodeLike) => Map<string, RawDependency>;
77
+ /**
78
+ * Maps short dependency types to the `package.json` field declaring them.
79
+ */
80
+ export declare const longDependencyTypeNames: Record<DependencyTypeShort, DependencyTypeLong>;
81
+ /**
82
+ * Point an `ECONFIG` spec error (eg: an undefined `catalog:`) at the
83
+ * package.json & dependency type that declared it. `source` names the
84
+ * file the spec was read from when not the manifest (eg: a lockfile).
85
+ * Other values are returned as is.
86
+ */
87
+ export declare const asDependencySpecError: (er: unknown, from: NodeLike, type: DependencyTypeShort, source?: string) => unknown;
77
88
  /**
78
89
  * Retrieves a map of all dependencies, of all types, that can be inferred
79
90
  * from a given node manifest, including missing dependencies.
@@ -1,6 +1,6 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
2
  import { Spec } from '@vltpkg/spec/browser';
3
- import { dependencyTypes, longDependencyTypes, shortDependencyTypes, } from '@vltpkg/types';
3
+ import { isErrorWithCause, isObject, dependencyTypes, longDependencyTypes, shortDependencyTypes, } from '@vltpkg/types';
4
4
  export const isDependencyTypeShort = (obj) => shortDependencyTypes.has(obj);
5
5
  export const isDependencySaveType = (obj) => shortDependencyTypes.has(obj) ||
6
6
  obj === 'implicit';
@@ -113,6 +113,40 @@ export const getRawDependencies = (node) => {
113
113
  }
114
114
  return dependencies;
115
115
  };
116
+ /**
117
+ * Maps short dependency types to the `package.json` field declaring them.
118
+ */
119
+ export const longDependencyTypeNames = {
120
+ prod: 'dependencies',
121
+ dev: 'devDependencies',
122
+ optional: 'optionalDependencies',
123
+ peer: 'peerDependencies',
124
+ peerOptional: 'peerDependencies',
125
+ };
126
+ // `./`-relative unless already relative or absolute
127
+ const manifestPath = ({ location = '.' }) => `${/^(?:\.\.?(?:\/|$)|\/|[a-z]:)/i.test(location) ? '' : './'}${location}/package.json`;
128
+ /**
129
+ * Point an `ECONFIG` spec error (eg: an undefined `catalog:`) at the
130
+ * package.json & dependency type that declared it. `source` names the
131
+ * file the spec was read from when not the manifest (eg: a lockfile).
132
+ * Other values are returned as is.
133
+ */
134
+ export const asDependencySpecError = (er, from, type, source) => {
135
+ if (!isErrorWithCause(er) ||
136
+ !isObject(er.cause) ||
137
+ er.cause.code !== 'ECONFIG') {
138
+ return er;
139
+ }
140
+ const pj = manifestPath(from);
141
+ const depType = longDependencyTypeNames[type];
142
+ return error(er.message, {
143
+ ...er.cause,
144
+ from: source ?
145
+ `${source} (${pj} ${depType})`
146
+ : `${pj} (${depType})`,
147
+ cause: er,
148
+ }, asDependencySpecError);
149
+ };
116
150
  /**
117
151
  * Retrieves a map of all dependencies, of all types, that can be inferred
118
152
  * from a given node manifest, including missing dependencies.
@@ -122,12 +156,18 @@ export const getDependencies = (node, options) => {
122
156
  const dependencies = getRawDependencies(node);
123
157
  for (const { name, type, bareSpec } of dependencies.values()) {
124
158
  const depType = shorten(type, name, node.manifest);
125
- const spec = Spec.parse(name, bareSpec, {
126
- ...options,
127
- // fall back to options.registry so deps don't lose the configured
128
- // registry when the node itself has none. see vltpkg/vltpkg#1580.
129
- registry: node.registry ?? options.registry,
130
- });
159
+ let spec;
160
+ try {
161
+ spec = Spec.parse(name, bareSpec, {
162
+ ...options,
163
+ // fall back to options.registry so deps don't lose the configured
164
+ // registry when the node itself has none. see vltpkg/vltpkg#1580.
165
+ registry: node.registry ?? options.registry,
166
+ });
167
+ }
168
+ catch (er) {
169
+ throw asDependencySpecError(er, node, depType);
170
+ }
131
171
  res.set(name, {
132
172
  spec,
133
173
  type: depType,
@@ -0,0 +1,39 @@
1
+ import type { QueryOptions } from '@vltpkg/query';
2
+ import type { DepID } from '@vltpkg/dep-id';
3
+ import type { Graph } from './graph.ts';
4
+ /**
5
+ * One top-level selector of a query: the ids it matched, and its
6
+ * `:skill()` patterns (unset: every skill).
7
+ */
8
+ export type QueryPartIds = {
9
+ ids: Set<DepID>;
10
+ patterns?: string[];
11
+ };
12
+ /**
13
+ * Matched npm registry pkgs with no security data, and why: the
14
+ * request timed out, or socket.dev has not scanned them yet.
15
+ */
16
+ export type Unscanned = {
17
+ ids: Set<DepID>;
18
+ reason: 'timeout' | 'unscanned';
19
+ };
20
+ /**
21
+ * Filter nodes using a DSS query string
22
+ */
23
+ export declare const filterNodesByQuery: (graph: Graph, selector?: string) => Promise<Set<DepID>>;
24
+ /**
25
+ * {@link filterNodesByQuery} for `allow-skills`: accepts `:skill()`,
26
+ * and also returns one entry per top-level selector. Unlike it, keeps
27
+ * `unscanned` pkgs: callers must drop them (fail closed).
28
+ */
29
+ export declare const filterNodesByQueryParts: (graph: Graph, selector?: string) => Promise<{
30
+ ids: Set<DepID>;
31
+ parts: QueryPartIds[];
32
+ unscanned?: Unscanned;
33
+ }>;
34
+ /**
35
+ * Throw `EUSAGE` unless `selector` is a valid DSS query. Runs it on an
36
+ * empty graph: no fs, network or security data needed. `partSelectors`
37
+ * allows e.g. `:skill()`.
38
+ */
39
+ export declare const assertQuery: (selector: string, flag: string, partSelectors?: QueryOptions["partSelectors"]) => Promise<void>;
@@ -0,0 +1,100 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { Query } from '@vltpkg/query';
3
+ import { SecurityArchive, usesNpmRegistry, } from '@vltpkg/security-archive';
4
+ import { asError } from '@vltpkg/types';
5
+ import { hasSkills } from "./skills/lazy.js";
6
+ const toIds = (nodes) => new Set(nodes.map(n => n.id));
7
+ const search = async (graph, selector, partSelectors) => {
8
+ /* c8 ignore start */
9
+ const securityArchive = Query.hasSecuritySelectors(selector) ?
10
+ await SecurityArchive.start({
11
+ nodes: [...graph.nodes.values()],
12
+ })
13
+ : undefined;
14
+ /* c8 ignore stop */
15
+ const res = await new Query({
16
+ edges: graph.edges,
17
+ nodes: new Set(graph.nodes.values()),
18
+ importers: graph.importers,
19
+ securityArchive,
20
+ hasSkills,
21
+ partSelectors,
22
+ }).search(selector, { signal: new AbortController().signal });
23
+ if (!securityArchive)
24
+ return res;
25
+ // fail closed: callers skip pkgs with no security data, be it from a
26
+ // timed out request or not scanned by socket.dev yet
27
+ const ids = toIds(res.nodes.filter(n => !securityArchive.has(n.id) && usesNpmRegistry(n)));
28
+ if (!ids.size)
29
+ return res;
30
+ const reason = securityArchive.timedOut ? 'timeout' : 'unscanned';
31
+ return { ...res, unscanned: { ids, reason } };
32
+ };
33
+ /**
34
+ * Filter nodes using a DSS query string
35
+ */
36
+ export const filterNodesByQuery = async (graph, selector) => {
37
+ // shortcut no packages included
38
+ if (selector === ':not(*)' /* c8 ignore next */ || !selector) {
39
+ return new Set();
40
+ }
41
+ // shortcut all packages included
42
+ if (selector === '*') {
43
+ return new Set(graph.nodes.keys());
44
+ }
45
+ const { nodes, unscanned } = await search(graph, selector);
46
+ return toIds(nodes.filter(n => !unscanned?.ids.has(n.id)));
47
+ };
48
+ /**
49
+ * {@link filterNodesByQuery} for `allow-skills`: accepts `:skill()`,
50
+ * and also returns one entry per top-level selector. Unlike it, keeps
51
+ * `unscanned` pkgs: callers must drop them (fail closed).
52
+ */
53
+ export const filterNodesByQueryParts = async (graph, selector) => {
54
+ if (!selector || selector === ':not(*)') {
55
+ return { ids: new Set(), parts: [] };
56
+ }
57
+ if (selector === '*') {
58
+ const ids = new Set(graph.nodes.keys());
59
+ return { ids, parts: [{ ids }] };
60
+ }
61
+ const res = await search(graph, selector, ['skill']);
62
+ return {
63
+ ids: toIds(res.nodes),
64
+ /* c8 ignore next -- always set when opted in */
65
+ parts: (res.parts ?? []).map(p => ({
66
+ ids: toIds(p.nodes),
67
+ ...(p.patterns && { patterns: p.patterns }),
68
+ })),
69
+ ...(res.unscanned && { unscanned: res.unscanned }),
70
+ };
71
+ };
72
+ /**
73
+ * Throw `EUSAGE` unless `selector` is a valid DSS query. Runs it on an
74
+ * empty graph: no fs, network or security data needed. `partSelectors`
75
+ * allows e.g. `:skill()`.
76
+ */
77
+ export const assertQuery = async (selector, flag, partSelectors) => {
78
+ try {
79
+ await new Query({
80
+ edges: new Set(),
81
+ nodes: new Set(),
82
+ importers: new Set(),
83
+ securityArchive: new Map(),
84
+ hasSkills,
85
+ partSelectors,
86
+ }).search(selector, { signal: new AbortController().signal });
87
+ }
88
+ catch (cause) {
89
+ const { message, cause: c } = asError(cause);
90
+ // a typo's suggestions, e.g. `:skill` for `:skills`
91
+ const validOptions = c?.validOptions ??
92
+ (c?.wanted === undefined ? undefined : [c.wanted]);
93
+ throw error(`Invalid --${flag} query: ${message}`, {
94
+ code: 'EUSAGE',
95
+ found: selector,
96
+ cause,
97
+ ...(validOptions && { validOptions }),
98
+ }, assertQuery);
99
+ }
100
+ };
@@ -5,7 +5,7 @@ import { satisfies } from '@vltpkg/satisfies';
5
5
  import { parse as parseVersion } from '@vltpkg/semver';
6
6
  import { longDependencyTypes, normalizeManifest } from '@vltpkg/types';
7
7
  import { fixupAddedNames } from "../fixup-added-names.js";
8
- import { addKey, shorten, shouldInstallDepType, } from "../dependencies.js";
8
+ import { addKey, asDependencySpecError, shorten, shouldInstallDepType, } from "../dependencies.js";
9
9
  import { removeOptionalSubgraph } from "../remove-optional-subgraph.js";
10
10
  import { isPathSecurityError } from "../path-security-error.js";
11
11
  import { extractNode } from "../reify/extract-node.js";
@@ -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;
@@ -541,15 +549,20 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
541
549
  // might need to skip already placed peer deps here
542
550
  if (bundled.has(name))
543
551
  continue;
544
- const dep = {
545
- type: shorten(depTypeName, name, manifest),
546
- spec: Spec.parse(name, bareSpec, {
552
+ const type = shorten(depTypeName, name, manifest);
553
+ let depSpec;
554
+ try {
555
+ depSpec = Spec.parse(name, bareSpec, {
547
556
  ...options,
548
557
  // git/file/remote parents have no spec.registry, so fall
549
558
  // back to the configured one. see vltpkg/vltpkg#1580.
550
559
  registry: spec.registry ?? options.registry,
551
- }),
552
- };
560
+ });
561
+ }
562
+ catch (er) {
563
+ throw asDependencySpecError(er, node, type);
564
+ }
565
+ const dep = { type, spec: depSpec };
553
566
  if (depTypeName === 'peerDependencies') {
554
567
  nextPeerDeps.set(name, dep);
555
568
  }
@@ -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;
@@ -1,5 +1,5 @@
1
1
  import { longDependencyTypes } from '@vltpkg/types';
2
- import { shorten, asDependency } from "../dependencies.js";
2
+ import { shorten, asDependency, asDependencySpecError, } from "../dependencies.js";
3
3
  import { removeSatisfiedSpecs } from "./remove-satisfied-specs.js";
4
4
  import { Spec } from '@vltpkg/spec';
5
5
  const hasDepName = (importer, edge) => {
@@ -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
@@ -70,7 +72,20 @@ export const getImporterSpecs = (options) => {
70
72
  edge.type === type) {
71
73
  continue;
72
74
  }
73
- const spec = Spec.parse(depName, depSpec, options);
75
+ let spec;
76
+ try {
77
+ spec = Spec.parse(depName, depSpec, options);
78
+ }
79
+ catch (er) {
80
+ throw asDependencySpecError(er, importer, type);
81
+ }
82
+ // a `-` modifier removes this dependency, nothing to place; an
83
+ // edge from before (e.g. a qualifier now matching) has to go
84
+ if (options.modifiers?.removesImporterEdge(importer, spec)) {
85
+ if (edge)
86
+ removedEdges.add(edge);
87
+ continue;
88
+ }
74
89
  // if a workspace dep references a workspace that no longer exists
75
90
  // (folder was removed), mark it for removal instead of trying to
76
91
  // resolve it — the workspace can't be satisfied
@@ -128,11 +143,15 @@ export const getImporterSpecs = (options) => {
128
143
  if (edge?.to && edge.spec.bareSpec === depSpec)
129
144
  continue;
130
145
  // add the dependency to the addDeps map
131
- const dependency = asDependency({
132
- spec: Spec.parse(depName, depSpec, options),
133
- type: shorten(depType, depName, manifest),
134
- });
135
- addDeps.set(depName, dependency);
146
+ const type = shorten(depType, depName, manifest);
147
+ let spec;
148
+ try {
149
+ spec = Spec.parse(depName, depSpec, options);
150
+ }
151
+ catch (er) {
152
+ throw asDependencySpecError(er, node, type);
153
+ }
154
+ addDeps.set(depName, asDependency({ spec, type }));
136
155
  }
137
156
  }
138
157
  // store in transientAdd if there are any dependencies
@@ -212,6 +231,7 @@ export const getImporterSpecs = (options) => {
212
231
  return {
213
232
  add: addResult,
214
233
  remove: removeResult,
234
+ removedEdges,
215
235
  staleSpecs,
216
236
  transientAdd,
217
237
  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/index.d.ts CHANGED
@@ -15,6 +15,9 @@ export * from './update.ts';
15
15
  export * from './diff.ts';
16
16
  export * from './modifiers.ts';
17
17
  export * from './virtual-root.ts';
18
+ export type { DiscoveredSkills, LinkSkillsOptions, LinkSkillsResult, ListSkillsResult, MountResult, Skill, SkillFrontmatter, SkillMount, SkillRow, SkillsDir, SkillsOptions, UnmatchedPattern, UnscannedSkills, } from './skills/index.ts';
19
+ export { hasSkills, linkSkills, listSkills } from './skills/lazy.ts';
20
+ export { assertQuery } from './filter-nodes-by-query.ts';
18
21
  import type { LoadOptions as ActualLoadOptions } from './actual/load.ts';
19
22
  export declare const actual: {
20
23
  load: (options: ActualLoadOptions) => import("./graph.ts").Graph;
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ export * from "./update.js";
15
15
  export * from "./diff.js";
16
16
  export * from "./modifiers.js";
17
17
  export * from "./virtual-root.js";
18
+ export { hasSkills, linkSkills, listSkills } from "./skills/lazy.js";
19
+ export { assertQuery } from "./filter-nodes-by-query.js";
18
20
  import { load as actualLoad } from "./actual/load.js";
19
21
  export const actual = { load: actualLoad };
20
22
  import { load as lockfileLoad, loadData as lockfileLoadData, } from "./lockfile/load.js";