@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.
- package/README.md +59 -3
- package/dist/actual/load.js +28 -12
- package/dist/build.js +30 -5
- package/dist/dependencies.d.ts +11 -0
- package/dist/dependencies.js +47 -7
- package/dist/filter-nodes-by-query.d.ts +39 -0
- package/dist/filter-nodes-by-query.js +100 -0
- package/dist/ideal/append-nodes.js +19 -6
- package/dist/ideal/build-ideal-from-starting-graph.js +8 -0
- package/dist/ideal/get-importer-specs.d.ts +1 -0
- package/dist/ideal/get-importer-specs.js +27 -7
- package/dist/ideal/refresh-ideal-graph.js +6 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/install.d.ts +23 -8
- package/dist/install.js +46 -1
- package/dist/lockfile/load-edges.js +10 -4
- package/dist/modifiers.d.ts +7 -0
- package/dist/modifiers.js +24 -0
- package/dist/reify/build.d.ts +7 -0
- package/dist/reify/check-needed-build.d.ts +5 -0
- package/dist/reify/check-needed-build.js +13 -0
- package/dist/reify/index.js +9 -39
- package/dist/reify/update-importers-package-json.js +5 -9
- package/dist/skills/dirs.d.ts +58 -0
- package/dist/skills/dirs.js +211 -0
- package/dist/skills/discover.d.ts +73 -0
- package/dist/skills/discover.js +234 -0
- package/dist/skills/gitignore.d.ts +13 -0
- package/dist/skills/gitignore.js +53 -0
- package/dist/skills/index.d.ts +123 -0
- package/dist/skills/index.js +225 -0
- package/dist/skills/lazy.d.ts +8 -0
- package/dist/skills/lazy.js +8 -0
- package/dist/skills/mask.d.ts +26 -0
- package/dist/skills/mask.js +43 -0
- package/dist/skills/mounts.d.ts +64 -0
- package/dist/skills/mounts.js +136 -0
- package/dist/uninstall.d.ts +24 -7
- package/dist/uninstall.js +26 -1
- package/dist/update.d.ts +24 -4
- package/dist/update.js +24 -1
- package/dist/visualization/mermaid-output.d.ts +8 -0
- package/dist/visualization/mermaid-output.js +25 -8
- 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
|
|
package/dist/actual/load.js
CHANGED
|
@@ -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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/dependencies.d.ts
CHANGED
|
@@ -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.
|
package/dist/dependencies.js
CHANGED
|
@@ -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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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";
|