@vltpkg/graph 1.4.0 → 1.6.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 +62 -3
- package/dist/actual/load.js +24 -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/graph.js +3 -0
- package/dist/ideal/append-nodes.js +29 -11
- package/dist/ideal/get-importer-specs.js +17 -7
- package/dist/ideal/remove-satisfied-specs.js +18 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/install.d.ts +25 -8
- package/dist/install.js +25 -1
- package/dist/lockfile/load-edges.js +10 -2
- package/dist/modifiers.d.ts +11 -0
- package/dist/modifiers.js +105 -21
- package/dist/reify/add-edge.d.ts +4 -0
- package/dist/reify/add-edge.js +13 -7
- 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/delete-edge.js +16 -14
- package/dist/reify/delete-nodes.d.ts +2 -1
- package/dist/reify/delete-nodes.js +4 -2
- package/dist/reify/index.d.ts +2 -0
- package/dist/reify/index.js +11 -39
- package/dist/reify/relink-bins.d.ts +9 -0
- package/dist/reify/relink-bins.js +39 -0
- package/dist/reify/update-importers-package-json.d.ts +6 -1
- package/dist/reify/update-importers-package-json.js +16 -15
- 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 +238 -0
- package/dist/skills/gitignore.d.ts +18 -0
- package/dist/skills/gitignore.js +82 -0
- package/dist/skills/index.d.ts +124 -0
- package/dist/skills/index.js +254 -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 +78 -0
- package/dist/skills/mounts.js +162 -0
- package/dist/uninstall.d.ts +27 -7
- package/dist/uninstall.js +25 -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,74 @@ 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 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.
|
|
104
|
+
|
|
105
|
+
### `linkSkills(options): Promise<LinkSkillsResult>`
|
|
106
|
+
|
|
107
|
+
Links the skills picked by `options.target` (as in `listSkills`; comma
|
|
108
|
+
lists are a union) into each agent skill dir (junctions on Windows):
|
|
109
|
+
`options.skillsDirs`, else `.agents/skills`, plus `<folder>/skills`
|
|
110
|
+
for each agent folder at the project root, `.claude/skills` inside
|
|
111
|
+
Claude Code (`options.env`), and existing `.<name>/skills` dirs. Dirs
|
|
112
|
+
must resolve inside the project, not in `node_modules` (else
|
|
113
|
+
`skipped`, as are fs errors) and are created only when a skill is
|
|
114
|
+
picked. One package per name, among picked skills, in every dir: the
|
|
115
|
+
one the first existing link points to while its skill is installed,
|
|
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).
|
|
133
|
+
|
|
134
|
+
### `assertQuery(selector, flag, partSelectors?): Promise<void>`
|
|
135
|
+
|
|
136
|
+
Throws `EUSAGE` unless `selector` is a valid DSS query.
|
|
137
|
+
`partSelectors` (e.g. `['skill']`) allows `:skill()`.
|
|
138
|
+
|
|
139
|
+
### `hasSkills(node): Promise<boolean>`
|
|
140
|
+
|
|
141
|
+
`:skills` provider for `Query`: true when an installed package ships
|
|
142
|
+
agent skills, as `listSkills` finds them.
|
|
86
143
|
|
|
87
144
|
### `mermaidOutput(graph): string`
|
|
88
145
|
|
|
89
|
-
Generates Mermaid flowchart syntax from graph data.
|
|
146
|
+
Generates Mermaid flowchart syntax from graph data. `graph.children`
|
|
147
|
+
(`Map<DepID, { label, edge? }[]>`) draws leaves under nodes, e.g.
|
|
148
|
+
`vlt skills list` skills.
|
|
90
149
|
|
|
91
150
|
### `humanReadableOutput(graph, options): string`
|
|
92
151
|
|
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,10 +246,16 @@ 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);
|
|
249
261
|
// a `-` modifier removes the dependency, so it is not missing
|
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
|
+
};
|
package/dist/graph.js
CHANGED
|
@@ -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";
|
|
@@ -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
|
|
445
|
-
//
|
|
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 {
|
|
@@ -549,15 +562,20 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
|
|
|
549
562
|
// might need to skip already placed peer deps here
|
|
550
563
|
if (bundled.has(name))
|
|
551
564
|
continue;
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
565
|
+
const type = shorten(depTypeName, name, manifest);
|
|
566
|
+
let depSpec;
|
|
567
|
+
try {
|
|
568
|
+
depSpec = Spec.parse(name, bareSpec, {
|
|
555
569
|
...options,
|
|
556
570
|
// git/file/remote parents have no spec.registry, so fall
|
|
557
571
|
// back to the configured one. see vltpkg/vltpkg#1580.
|
|
558
572
|
registry: spec.registry ?? options.registry,
|
|
559
|
-
})
|
|
560
|
-
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
catch (er) {
|
|
576
|
+
throw asDependencySpecError(er, node, type);
|
|
577
|
+
}
|
|
578
|
+
const dep = { type, spec: depSpec };
|
|
561
579
|
if (depTypeName === 'peerDependencies') {
|
|
562
580
|
nextPeerDeps.set(name, dep);
|
|
563
581
|
}
|
|
@@ -733,7 +751,7 @@ export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, op
|
|
|
733
751
|
graph.addEdge(type, spec, fromNode, toNode);
|
|
734
752
|
}
|
|
735
753
|
// Place nodes and collect child deps
|
|
736
|
-
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);
|
|
737
755
|
levelResults.push(placed);
|
|
738
756
|
}
|
|
739
757
|
// ============================================================
|
|
@@ -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) => {
|
|
@@ -72,7 +72,13 @@ export const getImporterSpecs = (options) => {
|
|
|
72
72
|
edge.type === type) {
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
let spec;
|
|
76
|
+
try {
|
|
77
|
+
spec = Spec.parse(depName, depSpec, options);
|
|
78
|
+
}
|
|
79
|
+
catch (er) {
|
|
80
|
+
throw asDependencySpecError(er, importer, type);
|
|
81
|
+
}
|
|
76
82
|
// a `-` modifier removes this dependency, nothing to place; an
|
|
77
83
|
// edge from before (e.g. a qualifier now matching) has to go
|
|
78
84
|
if (options.modifiers?.removesImporterEdge(importer, spec)) {
|
|
@@ -137,11 +143,15 @@ export const getImporterSpecs = (options) => {
|
|
|
137
143
|
if (edge?.to && edge.spec.bareSpec === depSpec)
|
|
138
144
|
continue;
|
|
139
145
|
// add the dependency to the addDeps map
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 }));
|
|
145
155
|
}
|
|
146
156
|
}
|
|
147
157
|
// store in transientAdd if there are any dependencies
|
|
@@ -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
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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/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";
|