@vltpkg/graph 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/actual/load.js +1 -0
  2. package/dist/dependencies.d.ts +16 -0
  3. package/dist/dependencies.js +24 -0
  4. package/dist/fixup-added-names.js +4 -2
  5. package/dist/graph.d.ts +41 -4
  6. package/dist/graph.js +145 -25
  7. package/dist/ideal/append-nodes.d.ts +2 -2
  8. package/dist/ideal/append-nodes.js +226 -60
  9. package/dist/ideal/build-ideal-from-starting-graph.js +28 -0
  10. package/dist/ideal/canonicalize-peer-ids.d.ts +44 -0
  11. package/dist/ideal/canonicalize-peer-ids.js +421 -0
  12. package/dist/ideal/get-importer-specs.d.ts +5 -1
  13. package/dist/ideal/get-importer-specs.js +51 -24
  14. package/dist/ideal/move-peer-store-dirs.d.ts +7 -0
  15. package/dist/ideal/move-peer-store-dirs.js +59 -0
  16. package/dist/ideal/peers.d.ts +16 -5
  17. package/dist/ideal/peers.js +187 -31
  18. package/dist/ideal/refresh-ideal-graph.d.ts +11 -2
  19. package/dist/ideal/refresh-ideal-graph.js +62 -4
  20. package/dist/ideal/remove-satisfied-specs.d.ts +11 -2
  21. package/dist/ideal/remove-satisfied-specs.js +18 -1
  22. package/dist/ideal/types.d.ts +20 -0
  23. package/dist/install.js +22 -3
  24. package/dist/lockfile/format-options-change.d.ts +5 -0
  25. package/dist/lockfile/format-options-change.js +20 -0
  26. package/dist/lockfile/load-nodes.js +3 -1
  27. package/dist/lockfile/load.d.ts +7 -1
  28. package/dist/lockfile/load.js +63 -7
  29. package/dist/lockfile/types.d.ts +17 -0
  30. package/dist/modifiers.d.ts +10 -0
  31. package/dist/modifiers.js +45 -9
  32. package/dist/node.d.ts +12 -0
  33. package/dist/node.js +42 -0
  34. package/dist/reify/index.js +30 -18
  35. package/dist/reify/update-importers-package-json.js +12 -2
  36. package/dist/transfer-data/load.js +3 -3
  37. package/dist/uninstall.js +2 -0
  38. 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 (e.g. modifiers, registries, catalogs)');
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
- lockfile.save({ ...options, graph, modifiers });
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,5 @@
1
+ import type { OptionsChange } from './types.ts';
2
+ /**
3
+ * Renders an {@link OptionsChange} as a single diagnostic line.
4
+ */
5
+ export declare const formatOptionsChange: ({ section, key, from, to, }: OptionsChange) => string;
@@ -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 */
@@ -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;
@@ -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 optionsChanged = JSON.stringify(currentOptions) !== JSON.stringify(lockfileOptions);
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
- catalogs,
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.optionsChanged = optionsChanged;
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(lockfileOptions),
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
  };
@@ -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
@@ -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
- const { first } = modifier.breadcrumb;
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, semver } = spec;
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?.comparator({
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 &&
@@ -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
- if (!hasLockfiles) {
82
- saveHidden(options);
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 { add, remove, packageInfo, packageJson, scurry, allowScripts, saveExact, savePrefix, } = options;
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 n = node.edgesOut.get(name)?.to;
61
- if (!n) {
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({