@vltpkg/graph 1.0.7 → 1.0.8
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/dist/actual/load.js +1 -0
- package/dist/dependencies.d.ts +16 -0
- package/dist/dependencies.js +24 -0
- package/dist/fixup-added-names.js +4 -2
- package/dist/graph.d.ts +41 -4
- package/dist/graph.js +145 -25
- package/dist/ideal/append-nodes.d.ts +2 -2
- package/dist/ideal/append-nodes.js +226 -60
- package/dist/ideal/build-ideal-from-starting-graph.js +28 -0
- package/dist/ideal/canonicalize-peer-ids.d.ts +44 -0
- package/dist/ideal/canonicalize-peer-ids.js +421 -0
- package/dist/ideal/get-importer-specs.d.ts +5 -1
- package/dist/ideal/get-importer-specs.js +51 -24
- package/dist/ideal/move-peer-store-dirs.d.ts +7 -0
- package/dist/ideal/move-peer-store-dirs.js +59 -0
- package/dist/ideal/peers.d.ts +16 -5
- package/dist/ideal/peers.js +187 -31
- package/dist/ideal/refresh-ideal-graph.d.ts +11 -2
- package/dist/ideal/refresh-ideal-graph.js +62 -4
- package/dist/ideal/remove-satisfied-specs.d.ts +11 -2
- package/dist/ideal/remove-satisfied-specs.js +18 -1
- package/dist/ideal/types.d.ts +20 -0
- package/dist/install.js +22 -3
- package/dist/lockfile/format-options-change.d.ts +5 -0
- package/dist/lockfile/format-options-change.js +20 -0
- package/dist/lockfile/load-nodes.js +3 -1
- package/dist/lockfile/load.d.ts +7 -1
- package/dist/lockfile/load.js +63 -7
- package/dist/lockfile/types.d.ts +17 -0
- package/dist/modifiers.d.ts +10 -0
- package/dist/modifiers.js +45 -9
- package/dist/node.d.ts +12 -0
- package/dist/node.js +42 -0
- package/dist/reify/index.js +30 -18
- package/dist/reify/update-importers-package-json.js +12 -2
- package/dist/transfer-data/load.js +3 -3
- package/dist/uninstall.js +2 -0
- package/package.json +21 -21
package/dist/install.js
CHANGED
|
@@ -10,6 +10,7 @@ import { RollbackRemove } from '@vltpkg/rollback-remove';
|
|
|
10
10
|
import { existsSync, rmSync } from 'node:fs';
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
import { load as loadVirtual, loadData } from "./lockfile/load.js";
|
|
13
|
+
import { formatOptionsChange } from "./lockfile/format-options-change.js";
|
|
13
14
|
import { getImporterSpecs } from "./ideal/get-importer-specs.js";
|
|
14
15
|
import { lockfile } from "./index.js";
|
|
15
16
|
import { updatePackageJson } from "./reify/update-importers-package-json.js";
|
|
@@ -52,6 +53,10 @@ export const install = async (options, add) => {
|
|
|
52
53
|
});
|
|
53
54
|
const specCache = new Map();
|
|
54
55
|
const lockfileData = options.frozenLockfile ? loadData(options.projectRoot) : undefined;
|
|
56
|
+
// loaded before the frozen check so that the lockfile options compare
|
|
57
|
+
// against a config that includes them, and so that the check knows
|
|
58
|
+
// which dependency specs the modifiers govern
|
|
59
|
+
const modifiers = GraphModifier.maybeLoad(options);
|
|
55
60
|
if (options.frozenLockfile) {
|
|
56
61
|
// validates no add/remove operations are requested
|
|
57
62
|
if (add?.modifiedDependencies) {
|
|
@@ -66,6 +71,7 @@ export const install = async (options, add) => {
|
|
|
66
71
|
const lockfileGraph = loadVirtual({
|
|
67
72
|
...options,
|
|
68
73
|
mainManifest,
|
|
74
|
+
modifiers,
|
|
69
75
|
monorepo: fullMonorepo,
|
|
70
76
|
specCache,
|
|
71
77
|
lockfileData,
|
|
@@ -79,12 +85,17 @@ export const install = async (options, add) => {
|
|
|
79
85
|
add: emptyAdd,
|
|
80
86
|
remove: emptyRemove,
|
|
81
87
|
...options,
|
|
88
|
+
modifiers,
|
|
82
89
|
});
|
|
83
90
|
// Check for spec changes by comparing package.json specs with lockfile edges
|
|
84
91
|
const specChanges = [];
|
|
85
92
|
for (const importer of lockfileGraph.importers) {
|
|
86
93
|
const deps = getDependencies(importer, options);
|
|
87
94
|
for (const [depName, dep] of deps) {
|
|
95
|
+
// the edge text of a governed dependency is the modifier value,
|
|
96
|
+
// never the manifest range; a change to it is an options change
|
|
97
|
+
if (modifiers?.targetsImporterEdge(importer, dep.spec))
|
|
98
|
+
continue;
|
|
88
99
|
const edge = importer.edgesOut.get(depName);
|
|
89
100
|
if (edge?.spec) {
|
|
90
101
|
if (edge.spec.toString() !== dep.spec.toString()) {
|
|
@@ -102,7 +113,10 @@ export const install = async (options, add) => {
|
|
|
102
113
|
lockfileGraph.optionsChanged) {
|
|
103
114
|
const details = [];
|
|
104
115
|
if (lockfileGraph.optionsChanged) {
|
|
105
|
-
details.push(' Configuration options have changed
|
|
116
|
+
details.push(' Configuration options have changed:');
|
|
117
|
+
for (const change of lockfileGraph.optionsChanges) {
|
|
118
|
+
details.push(` ${formatOptionsChange(change)}`);
|
|
119
|
+
}
|
|
106
120
|
}
|
|
107
121
|
if (specChanges.length > 0) {
|
|
108
122
|
details.push(...specChanges);
|
|
@@ -146,7 +160,6 @@ export const install = async (options, add) => {
|
|
|
146
160
|
const remove = Object.assign(new Map(), {
|
|
147
161
|
modifiedDependencies: false,
|
|
148
162
|
});
|
|
149
|
-
const modifiers = GraphModifier.maybeLoad(options);
|
|
150
163
|
let act = actualLoad({
|
|
151
164
|
...options,
|
|
152
165
|
mainManifest,
|
|
@@ -182,7 +195,8 @@ export const install = async (options, add) => {
|
|
|
182
195
|
// through the lockfile - otherwise subsequent installs lose
|
|
183
196
|
// the configured registry and rewrite node/edge DepIDs back
|
|
184
197
|
// to the default npm registry. (See vltpkg/vltpkg#1580.)
|
|
185
|
-
|
|
198
|
+
// normalise importer edge specs before saving, so the lockfile
|
|
199
|
+
// carries the same value package.json gets
|
|
186
200
|
const saveImportersPackageJson =
|
|
187
201
|
/* c8 ignore next */
|
|
188
202
|
add?.modifiedDependencies || remove.modifiedDependencies ?
|
|
@@ -193,7 +207,12 @@ export const install = async (options, add) => {
|
|
|
193
207
|
remove,
|
|
194
208
|
})
|
|
195
209
|
: undefined;
|
|
210
|
+
lockfile.save({ ...options, graph, modifiers });
|
|
196
211
|
saveImportersPackageJson?.();
|
|
212
|
+
// nothing is extracted or moved on this path, so nothing should be
|
|
213
|
+
// parked; confirm anyway so an early return can never leave
|
|
214
|
+
// `.VLT.DELETE.*` behind
|
|
215
|
+
remover.confirm();
|
|
197
216
|
return { graph, diff: undefined };
|
|
198
217
|
}
|
|
199
218
|
const { diff, buildQueue } = await reify({
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces the userinfo of a URL value with `***`. These messages end up
|
|
3
|
+
* in CI logs, and a registry alias can carry a token.
|
|
4
|
+
*/
|
|
5
|
+
const redactUserinfo = (value) => {
|
|
6
|
+
if (!URL.canParse(value))
|
|
7
|
+
return value;
|
|
8
|
+
const url = new URL(value);
|
|
9
|
+
if (!url.username && !url.password)
|
|
10
|
+
return value;
|
|
11
|
+
url.username = '';
|
|
12
|
+
url.password = '';
|
|
13
|
+
return url.href.replace('://', '://***@');
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Renders an {@link OptionsChange} as a single diagnostic line.
|
|
17
|
+
*/
|
|
18
|
+
export const formatOptionsChange = ({ section, key, from, to, }) => `${section}:${key === undefined ? '' : ` ${key}`} ` +
|
|
19
|
+
(from === undefined ? '(missing)' : `"${redactUserinfo(from)}"`) +
|
|
20
|
+
` -> ${to === undefined ? '(removed)' : `"${redactUserinfo(to)}"`}`;
|
|
@@ -31,9 +31,11 @@ export const loadNodes = (graph, nodes, options, actual, throwOnMissingManifest)
|
|
|
31
31
|
}
|
|
32
32
|
// Optimize registry version extraction with caching for large graphs
|
|
33
33
|
let version;
|
|
34
|
+
// scoped names start with '@', so look for the version separator
|
|
35
|
+
// past the first character
|
|
34
36
|
if (type === 'registry' &&
|
|
35
37
|
registrySpec &&
|
|
36
|
-
registrySpec.indexOf('@') > 0) {
|
|
38
|
+
registrySpec.indexOf('@', 1) > 0) {
|
|
37
39
|
if (registryVersionCache) {
|
|
38
40
|
const seenVersion = registryVersionCache.get(registrySpec);
|
|
39
41
|
/* c8 ignore start */
|
package/dist/lockfile/load.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Graph } from '../graph.ts';
|
|
|
4
4
|
import type { PathScurry } from 'path-scurry';
|
|
5
5
|
import type { NormalizedManifest } from '@vltpkg/types';
|
|
6
6
|
import type { SpecOptions } from '@vltpkg/spec';
|
|
7
|
-
import type { LockfileData, SpecCache } from './types.ts';
|
|
7
|
+
import type { LockfileData, OptionsChange, SpecCache } from './types.ts';
|
|
8
8
|
import type { GraphModifier } from '../modifiers.ts';
|
|
9
9
|
export type LoadOptions = SpecOptions & {
|
|
10
10
|
/**
|
|
@@ -51,4 +51,10 @@ export type LoadOptions = SpecOptions & {
|
|
|
51
51
|
export declare const loadData: (projectRoot: string, lockfilePath?: string) => LockfileData;
|
|
52
52
|
export declare const load: (options: LoadOptions) => Graph;
|
|
53
53
|
export declare const loadHidden: (options: LoadOptions) => Graph;
|
|
54
|
+
/**
|
|
55
|
+
* Compares the current config against the `options` block of a lockfile
|
|
56
|
+
* and returns the entries that differ. The comparison is per entry, so
|
|
57
|
+
* reordering keys in `vlt.json` is not a change.
|
|
58
|
+
*/
|
|
59
|
+
export declare const diffLockfileOptions: (options: LoadOptions, lockfileOptions?: LockfileData["options"]) => OptionsChange[];
|
|
54
60
|
export declare const loadObject: (options: LoadOptions, lockfileData: Omit<LockfileData, "options" | "lockfileVersion"> & Partial<Pick<LockfileData, "options" | "lockfileVersion">>) => Graph;
|
package/dist/lockfile/load.js
CHANGED
|
@@ -37,13 +37,13 @@ const removeDefaultItems = (defaultItems, items) => {
|
|
|
37
37
|
}
|
|
38
38
|
return res;
|
|
39
39
|
};
|
|
40
|
+
const hasItems = (o) => !!o && Object.keys(o).length > 0;
|
|
40
41
|
/**
|
|
41
42
|
* Builds a normalized options object from the current runtime config
|
|
42
43
|
* using the same cleaning logic as `lockfile/save.ts` so that it can
|
|
43
44
|
* be compared directly against the options stored in a lockfile.
|
|
44
45
|
*/
|
|
45
46
|
const buildCurrentOptions = (options) => {
|
|
46
|
-
const hasItems = (o) => o && Object.keys(o).length > 0;
|
|
47
47
|
const cleanModifiers = (options.modifiers &&
|
|
48
48
|
isRecordStringString(options.modifiers.config)) ?
|
|
49
49
|
options.modifiers.config
|
|
@@ -97,6 +97,52 @@ const buildCurrentOptions = (options) => {
|
|
|
97
97
|
: undefined),
|
|
98
98
|
};
|
|
99
99
|
};
|
|
100
|
+
const asRecord = (o) => o;
|
|
101
|
+
const unionKeys = (a = {}, b = {}) => [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
|
|
102
|
+
const diffRecords = (section, from = {}, to = {}, changes, prefix) => {
|
|
103
|
+
for (const key of unionKeys(from, to)) {
|
|
104
|
+
if (from[key] !== to[key]) {
|
|
105
|
+
changes.push({
|
|
106
|
+
section,
|
|
107
|
+
key: prefix === undefined ? key : `${prefix} ${key}`,
|
|
108
|
+
from: from[key],
|
|
109
|
+
to: to[key],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Compares the current config against the `options` block of a lockfile
|
|
116
|
+
* and returns the entries that differ. The comparison is per entry, so
|
|
117
|
+
* reordering keys in `vlt.json` is not a change.
|
|
118
|
+
*/
|
|
119
|
+
export const diffLockfileOptions = (options, lockfileOptions = {}) => {
|
|
120
|
+
const current = asRecord(buildCurrentOptions(options));
|
|
121
|
+
const stored = asRecord(lockfileOptions);
|
|
122
|
+
const changes = [];
|
|
123
|
+
for (const section of unionKeys(stored, current)) {
|
|
124
|
+
const from = stored[section];
|
|
125
|
+
const to = current[section];
|
|
126
|
+
if (typeof from === 'string' || typeof to === 'string') {
|
|
127
|
+
if (from !== to) {
|
|
128
|
+
changes.push({
|
|
129
|
+
section,
|
|
130
|
+
from: from,
|
|
131
|
+
to: to,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (section === 'catalogs') {
|
|
136
|
+
for (const name of unionKeys(from, to)) {
|
|
137
|
+
diffRecords(section, from?.[name], to?.[name], changes, name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
diffRecords(section, from, to, changes);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return changes;
|
|
145
|
+
};
|
|
100
146
|
export const loadObject = (options, lockfileData) => {
|
|
101
147
|
const version = lockfileData.lockfileVersion;
|
|
102
148
|
// Lockfile version is required, likely a corrupted lockfile if missing
|
|
@@ -130,15 +176,19 @@ export const loadObject = (options, lockfileData) => {
|
|
|
130
176
|
// Detect whether the current config options differ from those
|
|
131
177
|
// stored in the lockfile. When they do the ideal builder must
|
|
132
178
|
// reset edges and rebuild the graph.
|
|
133
|
-
const currentOptions = buildCurrentOptions(options);
|
|
134
179
|
/* c8 ignore next */
|
|
135
180
|
const lockfileOptions = lockfileData.options ?? {};
|
|
136
|
-
const
|
|
181
|
+
const optionsChanges = diffLockfileOptions(options, lockfileOptions);
|
|
137
182
|
// Optimize options merging - only create new objects when needed
|
|
138
183
|
const mergedOptions = {
|
|
139
184
|
...options,
|
|
140
|
-
catalog
|
|
141
|
-
|
|
185
|
+
// catalog entries are indirections resolved at parse time: prefer
|
|
186
|
+
// the current config so loading a lockfile (e.g. the hidden
|
|
187
|
+
// lockfile cache) yields the same edge specs as a fresh scan of
|
|
188
|
+
// the project; fall back to the stored values so a standalone
|
|
189
|
+
// lockfile still parses when the config no longer defines them
|
|
190
|
+
catalog: hasItems(options.catalog) ? options.catalog : catalog,
|
|
191
|
+
catalogs: hasItems(options.catalogs) ? options.catalogs : catalogs,
|
|
142
192
|
'scoped-registries': scopeRegistries ?
|
|
143
193
|
{ ...options['scoped-registries'], ...scopeRegistries }
|
|
144
194
|
: options['scoped-registries'],
|
|
@@ -159,14 +209,19 @@ export const loadObject = (options, lockfileData) => {
|
|
|
159
209
|
mainManifest,
|
|
160
210
|
monorepo,
|
|
161
211
|
});
|
|
162
|
-
graph.
|
|
212
|
+
graph.optionsChanges = optionsChanges;
|
|
213
|
+
graph.optionsChanged = optionsChanges.length > 0;
|
|
163
214
|
loadNodes(graph, lockfileData.nodes, mergedOptions, options.actual, options.throwOnMissingManifest);
|
|
164
215
|
// Skip interning when modifiers are active — they mutate Spec.overridden
|
|
165
216
|
const specCache = options.modifiers || !options.specCache ?
|
|
166
217
|
undefined
|
|
167
218
|
: {
|
|
168
219
|
specCache: options.specCache,
|
|
169
|
-
prefix: JSON.stringify(
|
|
220
|
+
prefix: JSON.stringify([
|
|
221
|
+
lockfileOptions,
|
|
222
|
+
mergedOptions.catalog,
|
|
223
|
+
mergedOptions.catalogs,
|
|
224
|
+
]),
|
|
170
225
|
};
|
|
171
226
|
loadEdges(graph, lockfileData.edges, mergedOptions, specCache);
|
|
172
227
|
// hydrate missing node-level registry data
|
|
@@ -176,5 +231,6 @@ export const loadObject = (options, lockfileData) => {
|
|
|
176
231
|
node.registry = firstEdge.spec.registry;
|
|
177
232
|
}
|
|
178
233
|
}
|
|
234
|
+
graph.sortNodes();
|
|
179
235
|
return graph;
|
|
180
236
|
};
|
package/dist/lockfile/types.d.ts
CHANGED
|
@@ -25,6 +25,23 @@ export type LockfileData = {
|
|
|
25
25
|
nodes: Record<DepID, LockfileNode>;
|
|
26
26
|
edges: LockfileEdges;
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* A single difference between the config a graph was built with and the
|
|
30
|
+
* `options` block stored in the lockfile it was loaded from.
|
|
31
|
+
*/
|
|
32
|
+
export type OptionsChange = {
|
|
33
|
+
/** the `options` key the difference was found in, e.g. `catalog` */
|
|
34
|
+
section: string;
|
|
35
|
+
/**
|
|
36
|
+
* the entry inside that section, e.g. the catalog name. Absent for
|
|
37
|
+
* scalar sections; `<catalog> <dep>` for `catalogs`.
|
|
38
|
+
*/
|
|
39
|
+
key?: string;
|
|
40
|
+
/** the value stored in the lockfile, absent when the entry is new */
|
|
41
|
+
from?: string;
|
|
42
|
+
/** the current value, absent when the entry is gone */
|
|
43
|
+
to?: string;
|
|
44
|
+
};
|
|
28
45
|
/**
|
|
29
46
|
* Per-install intern cache for parsed lockfile specs. Shared across
|
|
30
47
|
* hidden + main lockfile loads so identical (name, bareSpec) pairs
|
package/dist/modifiers.d.ts
CHANGED
|
@@ -116,6 +116,16 @@ export declare class GraphModifier {
|
|
|
116
116
|
* converting the result into a GraphModifierConfigObject
|
|
117
117
|
*/
|
|
118
118
|
get config(): GraphModifierConfigObject;
|
|
119
|
+
/**
|
|
120
|
+
* Whether a modifier governs the direct `importer -> spec.name` edge,
|
|
121
|
+
* i.e. whether its value comes from config rather than from the
|
|
122
|
+
* manifest that declares it. Only a breadcrumb whose whole scope is
|
|
123
|
+
* that edge counts, and only if its qualifier accepts the spec:
|
|
124
|
+
* `:root > #a > #b` names b but governs an edge under a, and
|
|
125
|
+
* `#b:semver(^1)` leaves a `^2` edge alone, so in both cases the
|
|
126
|
+
* root's own b edge is still the manifest's to validate and heal.
|
|
127
|
+
*/
|
|
128
|
+
targetsImporterEdge(importer: Node, spec: Spec): boolean;
|
|
119
129
|
/**
|
|
120
130
|
* Loads the modifiers defined in `vlt.json` into memory.
|
|
121
131
|
*/
|
package/dist/modifiers.js
CHANGED
|
@@ -3,6 +3,19 @@ import { error } from '@vltpkg/error-cause';
|
|
|
3
3
|
import { Spec } from '@vltpkg/spec';
|
|
4
4
|
import { asNormalizedManifest, assertRecordStringString, normalizeManifest, } from '@vltpkg/types';
|
|
5
5
|
import { load } from '@vltpkg/vlt-json';
|
|
6
|
+
/**
|
|
7
|
+
* Does this breadcrumb item select the given importer?
|
|
8
|
+
*/
|
|
9
|
+
const matchesImporter = (item, importer) => item.importer &&
|
|
10
|
+
(item.value === ':project' ||
|
|
11
|
+
(item.value === ':root' && importer.mainImporter) ||
|
|
12
|
+
(item.value === ':workspace' && importer.importer));
|
|
13
|
+
/**
|
|
14
|
+
* Does this item's qualifier (e.g. `:semver(^1)`, `:v(^1)`) accept the
|
|
15
|
+
* given spec? Shared by traversal matching and by the frozen / healing
|
|
16
|
+
* exemption, so both agree on what a modifier actually governs.
|
|
17
|
+
*/
|
|
18
|
+
const matchesQualifier = (item, spec) => !!item?.comparator({ semver: spec.semver });
|
|
6
19
|
/**
|
|
7
20
|
* Class representing loaded modifiers configuration for a project.
|
|
8
21
|
*
|
|
@@ -73,6 +86,35 @@ export class GraphModifier {
|
|
|
73
86
|
return (this.#config =
|
|
74
87
|
load('modifiers', assertRecordStringString) ?? {});
|
|
75
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether a modifier governs the direct `importer -> spec.name` edge,
|
|
91
|
+
* i.e. whether its value comes from config rather than from the
|
|
92
|
+
* manifest that declares it. Only a breadcrumb whose whole scope is
|
|
93
|
+
* that edge counts, and only if its qualifier accepts the spec:
|
|
94
|
+
* `:root > #a > #b` names b but governs an edge under a, and
|
|
95
|
+
* `#b:semver(^1)` leaves a `^2` edge alone, so in both cases the
|
|
96
|
+
* root's own b edge is still the manifest's to validate and heal.
|
|
97
|
+
*/
|
|
98
|
+
targetsImporterEdge(importer, spec) {
|
|
99
|
+
for (const { breadcrumb } of this.#modifiers) {
|
|
100
|
+
const { last } = breadcrumb;
|
|
101
|
+
if (last.name !== spec.name)
|
|
102
|
+
continue;
|
|
103
|
+
// a qualifier that rejects the spec means the modifier never
|
|
104
|
+
// applies to this edge, so it stays the manifest's to validate
|
|
105
|
+
if (!matchesQualifier(last, spec))
|
|
106
|
+
continue;
|
|
107
|
+
const { prev } = last;
|
|
108
|
+
// a lone `#b` matches an edge to b anywhere, importers included
|
|
109
|
+
if (!prev)
|
|
110
|
+
return true;
|
|
111
|
+
if (prev.prev)
|
|
112
|
+
continue;
|
|
113
|
+
if (matchesImporter(prev, importer))
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
76
118
|
/**
|
|
77
119
|
* Loads the modifiers defined in `vlt.json` into memory.
|
|
78
120
|
*/
|
|
@@ -132,11 +174,7 @@ export class GraphModifier {
|
|
|
132
174
|
for (const modifier of this.#modifiers) {
|
|
133
175
|
// if the first item in the breadcrumb is an importer and it matches
|
|
134
176
|
// any of the valid top-level selectors, then register the modifier
|
|
135
|
-
|
|
136
|
-
const matchRoot = first.value === ':root' && importer.mainImporter;
|
|
137
|
-
const matchWorkspace = first.value === ':workspace' && importer.importer;
|
|
138
|
-
const matchAny = first.value === ':project' || matchRoot || matchWorkspace;
|
|
139
|
-
if (first.importer && matchAny) {
|
|
177
|
+
if (matchesImporter(modifier.breadcrumb.first, importer)) {
|
|
140
178
|
const active = this.newModifier(importer, modifier);
|
|
141
179
|
const single = active.modifier.breadcrumb.single;
|
|
142
180
|
// only the importers will update the active entry right after
|
|
@@ -165,7 +203,7 @@ export class GraphModifier {
|
|
|
165
203
|
* iteration.
|
|
166
204
|
*/
|
|
167
205
|
tryNewDependency(from, spec) {
|
|
168
|
-
const { name
|
|
206
|
+
const { name } = spec;
|
|
169
207
|
// here we use a map instead of a set so that we can associate each
|
|
170
208
|
// modifier active entry with its breadcrumb so that it's easier to
|
|
171
209
|
// pick the correct entry when we sort breadcrbumbs by specificity
|
|
@@ -210,9 +248,7 @@ export class GraphModifier {
|
|
|
210
248
|
return all.get(specificitySort(entries
|
|
211
249
|
// here we filter out any entries that do not match the
|
|
212
250
|
// pseudo selector comparators used in the breadcrumb item
|
|
213
|
-
.filter(i => i.interactiveBreadcrumb.current
|
|
214
|
-
semver,
|
|
215
|
-
}))
|
|
251
|
+
.filter(i => matchesQualifier(i.interactiveBreadcrumb.current, spec))
|
|
216
252
|
.map(i => i.modifier.breadcrumb))[0]);
|
|
217
253
|
}
|
|
218
254
|
/**
|
package/dist/node.d.ts
CHANGED
|
@@ -191,6 +191,11 @@ export declare class Node implements NodeLike {
|
|
|
191
191
|
* example the lockfile that might have already have this info.
|
|
192
192
|
*/
|
|
193
193
|
setResolved(): void;
|
|
194
|
+
/**
|
|
195
|
+
* Update this node's DepID and peer suffix after canonicalization.
|
|
196
|
+
* Resets name/location memos that were derived from the previous id.
|
|
197
|
+
*/
|
|
198
|
+
setPeerIdentity(id: DepID, peerSetHash: string): void;
|
|
194
199
|
setDefaultLocation(): void;
|
|
195
200
|
/**
|
|
196
201
|
* Add an edge from this node connecting it to a direct dependency.
|
|
@@ -235,5 +240,12 @@ export declare class Node implements NodeLike {
|
|
|
235
240
|
};
|
|
236
241
|
toString(): string;
|
|
237
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Copy the tarball metadata `to` is missing from another node for the
|
|
245
|
+
* same package version. Lockfile provenance only carries over once both
|
|
246
|
+
* `integrity` and `resolved` are set, since that pair is what makes it
|
|
247
|
+
* usable without refetching.
|
|
248
|
+
*/
|
|
249
|
+
export declare const copyPackageMetadata: (to: Node, from: Node) => void;
|
|
238
250
|
export declare const isNode: (value: unknown) => value is Node;
|
|
239
251
|
export declare const asNode: (value: unknown) => Node;
|
package/dist/node.js
CHANGED
|
@@ -302,6 +302,35 @@ export class Node {
|
|
|
302
302
|
break;
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Update this node's DepID and peer suffix after canonicalization.
|
|
307
|
+
* Resets name/location memos that were derived from the previous id.
|
|
308
|
+
*/
|
|
309
|
+
setPeerIdentity(id, peerSetHash) {
|
|
310
|
+
const prevId = this.id;
|
|
311
|
+
const prevName = this.name;
|
|
312
|
+
const nameWasId = this.#name === prevId;
|
|
313
|
+
const prevDefault = `./node_modules/.vlt/${prevId}/node_modules/${prevName}`;
|
|
314
|
+
const locWasDefault = !this.#location || this.#location === prevDefault;
|
|
315
|
+
this.id = id;
|
|
316
|
+
this.peerSetHash = peerSetHash;
|
|
317
|
+
if (nameWasId)
|
|
318
|
+
this.#name = undefined;
|
|
319
|
+
const nextName = this.name;
|
|
320
|
+
if (prevName !== nextName) {
|
|
321
|
+
const oldSet = this.graph.nodesByName.get(prevName);
|
|
322
|
+
oldSet?.delete(this);
|
|
323
|
+
if (oldSet?.size === 0) {
|
|
324
|
+
this.graph.nodesByName.delete(prevName);
|
|
325
|
+
}
|
|
326
|
+
const nbn = this.graph.nodesByName.get(nextName) ?? new Set();
|
|
327
|
+
nbn.add(this);
|
|
328
|
+
this.graph.nodesByName.set(nextName, nbn);
|
|
329
|
+
}
|
|
330
|
+
if (locWasDefault) {
|
|
331
|
+
this.location = `./node_modules/.vlt/${id}/node_modules/${nextName}`;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
305
334
|
setDefaultLocation() {
|
|
306
335
|
const def = `./node_modules/.vlt/${this.id}/node_modules/${this.name}`;
|
|
307
336
|
// only relocate if the location is in node_modules already
|
|
@@ -378,6 +407,19 @@ export class Node {
|
|
|
378
407
|
return stringifyNode(this);
|
|
379
408
|
}
|
|
380
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Copy the tarball metadata `to` is missing from another node for the
|
|
412
|
+
* same package version. Lockfile provenance only carries over once both
|
|
413
|
+
* `integrity` and `resolved` are set, since that pair is what makes it
|
|
414
|
+
* usable without refetching.
|
|
415
|
+
*/
|
|
416
|
+
export const copyPackageMetadata = (to, from) => {
|
|
417
|
+
to.integrity ??= from.integrity;
|
|
418
|
+
to.resolved ??= from.resolved;
|
|
419
|
+
if (from.resolvedFromLockfile && to.integrity && to.resolved) {
|
|
420
|
+
to.resolvedFromLockfile = true;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
381
423
|
export const isNode = (value) => {
|
|
382
424
|
return (typeof value === 'object' &&
|
|
383
425
|
value != null &&
|
package/dist/reify/index.js
CHANGED
|
@@ -64,6 +64,24 @@ export const reify = async (options) => {
|
|
|
64
64
|
loadManifests: true,
|
|
65
65
|
});
|
|
66
66
|
const diff = new Diff(actual, graph);
|
|
67
|
+
// normalise importer edge specs and compute the package.json changes
|
|
68
|
+
// before deciding whether anything needs reifying: an add whose target
|
|
69
|
+
// is already in place has no node diff but still has a saved value to
|
|
70
|
+
// persist. `Diff` compares edge targets only, so the rewrite is safe
|
|
71
|
+
// after it was built.
|
|
72
|
+
const saveImportersPackageJson =
|
|
73
|
+
/* c8 ignore next */
|
|
74
|
+
(options.add?.modifiedDependencies ||
|
|
75
|
+
options.remove?.modifiedDependencies) ?
|
|
76
|
+
updatePackageJson({
|
|
77
|
+
add: options.add,
|
|
78
|
+
remove: options.remove,
|
|
79
|
+
graph,
|
|
80
|
+
packageJson: options.packageJson,
|
|
81
|
+
saveExact: options.saveExact,
|
|
82
|
+
savePrefix: options.savePrefix,
|
|
83
|
+
})
|
|
84
|
+
: undefined;
|
|
67
85
|
const noModifiedDependencies = !options.add?.modifiedDependencies &&
|
|
68
86
|
!options.remove?.modifiedDependencies;
|
|
69
87
|
// Skip optional-only diffs only after a prior reify wrote lockfiles.
|
|
@@ -77,12 +95,18 @@ export const reify = async (options) => {
|
|
|
77
95
|
// Even when there are no changes to reify, ensure lockfiles
|
|
78
96
|
// exist on disk. This handles the case where a project has no
|
|
79
97
|
// dependencies (or only workspace importers) but still needs
|
|
80
|
-
// lockfiles written on the first install.
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
// lockfiles written on the first install. A stale flag means an
|
|
99
|
+
// edge spec was rewritten above, which the lockfiles have to carry.
|
|
100
|
+
if (!hasLockfiles || graph.lockfileStale) {
|
|
101
|
+
// with no diff at all the ideal graph is the actual graph, so the
|
|
102
|
+
// hidden lockfile can be written from it; an optional-only diff
|
|
103
|
+
// being skipped may only refresh the main lockfile
|
|
104
|
+
if (!hasLockfiles || !diff.hasChanges())
|
|
105
|
+
saveHidden(options);
|
|
83
106
|
const lfData = lockfileData(options);
|
|
84
107
|
saveData(lfData, scurry.resolve('vlt-lock.json'), false);
|
|
85
108
|
}
|
|
109
|
+
saveImportersPackageJson?.();
|
|
86
110
|
// Even with no reify changes, report nodes that still need building.
|
|
87
111
|
// Build state is persisted in the hidden lockfile and loaded into the
|
|
88
112
|
// actual graph, so check there for pending builds.
|
|
@@ -97,7 +121,7 @@ export const reify = async (options) => {
|
|
|
97
121
|
}
|
|
98
122
|
let success = false;
|
|
99
123
|
try {
|
|
100
|
-
const { buildQueue } = await reify_(options, diff, remover);
|
|
124
|
+
const { buildQueue } = await reify_(options, diff, remover, saveImportersPackageJson);
|
|
101
125
|
remover.confirm();
|
|
102
126
|
success = true;
|
|
103
127
|
res.buildQueue = buildQueue;
|
|
@@ -112,21 +136,9 @@ export const reify = async (options) => {
|
|
|
112
136
|
done();
|
|
113
137
|
return res;
|
|
114
138
|
};
|
|
115
|
-
const reify_ = async (options, diff, remover) => {
|
|
139
|
+
const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
|
|
116
140
|
const res = {};
|
|
117
|
-
const {
|
|
118
|
-
const saveImportersPackageJson =
|
|
119
|
-
/* c8 ignore next */
|
|
120
|
-
add?.modifiedDependencies || remove?.modifiedDependencies ?
|
|
121
|
-
updatePackageJson({
|
|
122
|
-
add,
|
|
123
|
-
remove,
|
|
124
|
-
graph: options.graph,
|
|
125
|
-
packageJson,
|
|
126
|
-
saveExact,
|
|
127
|
-
savePrefix,
|
|
128
|
-
})
|
|
129
|
-
: undefined;
|
|
141
|
+
const { packageInfo, packageJson, scurry, allowScripts } = options;
|
|
130
142
|
// before anything else happens, grab the ideal tree as it was resolved
|
|
131
143
|
// so that we can store it in the lockfile. We do this here so that
|
|
132
144
|
// any failed/removed optional deps are not reflected in the lockfile
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { splitDepID } from '@vltpkg/dep-id';
|
|
2
2
|
import { error } from '@vltpkg/error-cause';
|
|
3
|
+
import { Spec } from '@vltpkg/spec';
|
|
3
4
|
import { longDependencyTypes } from '@vltpkg/types';
|
|
4
5
|
import { resolveSaveType } from "../resolve-save-type.js";
|
|
5
6
|
import { calculateSaveValue } from "./calculate-save-value.js";
|
|
@@ -57,10 +58,11 @@ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
|
|
|
57
58
|
found: dep.type,
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
61
|
+
const edge = node.edgesOut.get(name);
|
|
62
|
+
if (!edge?.to) {
|
|
62
63
|
throw error('Dependency node could not be found');
|
|
63
64
|
}
|
|
65
|
+
const n = edge.to;
|
|
64
66
|
const [nodeType] = splitDepID(n.id);
|
|
65
67
|
for (const dtype of longDependencyTypes) {
|
|
66
68
|
if (dtype === depType || !manifest[dtype])
|
|
@@ -89,6 +91,14 @@ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
|
|
|
89
91
|
const saveValue = calculateSaveValue(nodeType, dep.spec, existing, n.version, saveExact, savePrefix);
|
|
90
92
|
dependencies[name] = saveValue;
|
|
91
93
|
manifestChanged = manifestChanged || saveValue !== existing;
|
|
94
|
+
// the lockfile has to carry the value package.json carries, or a
|
|
95
|
+
// later install reads back `latest` where the manifest says
|
|
96
|
+
// `^1.2.3`. anything keyed by the old spec from here on must be
|
|
97
|
+
// invalidated at this point.
|
|
98
|
+
if (saveValue !== edge.spec.bareSpec) {
|
|
99
|
+
edge.spec = Spec.parse(name, saveValue, dep.spec.options);
|
|
100
|
+
graph.lockfileStale = true;
|
|
101
|
+
}
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
104
|
return manifestChanged ? manifest : undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { error } from '@vltpkg/error-cause';
|
|
2
|
-
import { SecurityArchive } from '@vltpkg/security-archive/browser';
|
|
2
|
+
import { SecurityArchive, usesNpmRegistry, } from '@vltpkg/security-archive/browser';
|
|
3
3
|
import { defaultGitHostArchives, defaultGitHosts, defaultRegistries, defaultRegistryName, defaultScopeRegistries, } from '@vltpkg/spec/browser';
|
|
4
4
|
import { stringifyNode } from "../stringify-node.js";
|
|
5
5
|
import { loadEdges } from "../lockfile/load-edges.js";
|
|
@@ -161,11 +161,11 @@ export const load = (transfered) => {
|
|
|
161
161
|
loadNodes(graph, transfered.lockfile.nodes, specOptions, graph);
|
|
162
162
|
loadEdges(graph, transfered.lockfile.edges, specOptions);
|
|
163
163
|
const securityArchive = SecurityArchive.load(transfered.securityArchive);
|
|
164
|
-
// validates that all nodes have a security archive entry
|
|
164
|
+
// validates that all npm-ecosystem nodes have a security archive entry
|
|
165
165
|
if (securityArchive) {
|
|
166
166
|
securityArchive.ok = true;
|
|
167
167
|
for (const node of graph.nodes.values()) {
|
|
168
|
-
if (!securityArchive.has(node.id)) {
|
|
168
|
+
if (usesNpmRegistry(node) && !securityArchive.has(node.id)) {
|
|
169
169
|
securityArchive.ok = false;
|
|
170
170
|
break;
|
|
171
171
|
}
|
package/dist/uninstall.js
CHANGED
|
@@ -49,6 +49,8 @@ export const uninstall = async (options, remove) => {
|
|
|
49
49
|
})
|
|
50
50
|
: undefined;
|
|
51
51
|
saveImportersPackageJson?.();
|
|
52
|
+
// see install.ts: never leave `.VLT.DELETE.*` behind
|
|
53
|
+
remover.confirm();
|
|
52
54
|
return { graph, diff: undefined };
|
|
53
55
|
}
|
|
54
56
|
const diff = await reify({
|