@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.
Files changed (53) hide show
  1. package/README.md +62 -3
  2. package/dist/actual/load.js +24 -12
  3. package/dist/build.js +30 -5
  4. package/dist/dependencies.d.ts +11 -0
  5. package/dist/dependencies.js +47 -7
  6. package/dist/filter-nodes-by-query.d.ts +39 -0
  7. package/dist/filter-nodes-by-query.js +100 -0
  8. package/dist/graph.js +3 -0
  9. package/dist/ideal/append-nodes.js +29 -11
  10. package/dist/ideal/get-importer-specs.js +17 -7
  11. package/dist/ideal/remove-satisfied-specs.js +18 -10
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +2 -0
  14. package/dist/install.d.ts +25 -8
  15. package/dist/install.js +25 -1
  16. package/dist/lockfile/load-edges.js +10 -2
  17. package/dist/modifiers.d.ts +11 -0
  18. package/dist/modifiers.js +105 -21
  19. package/dist/reify/add-edge.d.ts +4 -0
  20. package/dist/reify/add-edge.js +13 -7
  21. package/dist/reify/build.d.ts +7 -0
  22. package/dist/reify/check-needed-build.d.ts +5 -0
  23. package/dist/reify/check-needed-build.js +13 -0
  24. package/dist/reify/delete-edge.js +16 -14
  25. package/dist/reify/delete-nodes.d.ts +2 -1
  26. package/dist/reify/delete-nodes.js +4 -2
  27. package/dist/reify/index.d.ts +2 -0
  28. package/dist/reify/index.js +11 -39
  29. package/dist/reify/relink-bins.d.ts +9 -0
  30. package/dist/reify/relink-bins.js +39 -0
  31. package/dist/reify/update-importers-package-json.d.ts +6 -1
  32. package/dist/reify/update-importers-package-json.js +16 -15
  33. package/dist/skills/dirs.d.ts +58 -0
  34. package/dist/skills/dirs.js +211 -0
  35. package/dist/skills/discover.d.ts +73 -0
  36. package/dist/skills/discover.js +238 -0
  37. package/dist/skills/gitignore.d.ts +18 -0
  38. package/dist/skills/gitignore.js +82 -0
  39. package/dist/skills/index.d.ts +124 -0
  40. package/dist/skills/index.js +254 -0
  41. package/dist/skills/lazy.d.ts +8 -0
  42. package/dist/skills/lazy.js +8 -0
  43. package/dist/skills/mask.d.ts +26 -0
  44. package/dist/skills/mask.js +43 -0
  45. package/dist/skills/mounts.d.ts +78 -0
  46. package/dist/skills/mounts.js +162 -0
  47. package/dist/uninstall.d.ts +27 -7
  48. package/dist/uninstall.js +25 -1
  49. package/dist/update.d.ts +24 -4
  50. package/dist/update.js +24 -1
  51. package/dist/visualization/mermaid-output.d.ts +8 -0
  52. package/dist/visualization/mermaid-output.js +25 -8
  53. package/package.json +21 -21
package/dist/install.d.ts CHANGED
@@ -3,19 +3,36 @@ import type { LoadOptions } from './actual/load.ts';
3
3
  import type { AddImportersDependenciesMap } from './dependencies.ts';
4
4
  import type { DepID } from '@vltpkg/dep-id';
5
5
  import type { Graph } from './index.ts';
6
+ import type { Diff } from './diff.ts';
7
+ import type { LinkSkillsResult } from './skills/index.ts';
6
8
  export type InstallOptions = LoadOptions & {
7
9
  packageInfo: PackageInfoClient;
8
10
  cleanInstall?: boolean;
9
11
  allowScripts: string;
10
12
  saveExact?: boolean;
11
13
  savePrefix?: string;
14
+ /**
15
+ * DSS query: packages whose agent skills get linked into the
16
+ * project's agent skill dirs after install. Selectors may end with
17
+ * `:skill()`. Unset or `:not(*)`: none.
18
+ */
19
+ allowSkills?: string;
20
+ /**
21
+ * agent skill dirs, relative to projectRoot. Unset: detect; `[]`:
22
+ * none
23
+ */
24
+ skillsDirs?: string[];
25
+ /** store dirs of these nodes are never removed, eg the running vlt */
26
+ keepNodes?: ReadonlySet<DepID>;
12
27
  };
13
- export declare const install: (options: InstallOptions, add?: AddImportersDependenciesMap) => Promise<{
28
+ /**
29
+ * Result of {@link install}.
30
+ */
31
+ export type InstallResult = {
32
+ buildQueue?: DepID[];
14
33
  graph: Graph;
15
- diff: undefined;
16
- buildQueue?: undefined;
17
- } | {
18
- buildQueue: DepID[] | undefined;
19
- graph: Graph;
20
- diff: import("./diff.ts").Diff;
21
- }>;
34
+ diff?: Diff;
35
+ /** skill links added or removed, if any */
36
+ skills?: LinkSkillsResult;
37
+ };
38
+ export declare const install: (options: InstallOptions, add?: AddImportersDependenciesMap) => Promise<InstallResult>;
package/dist/install.js CHANGED
@@ -16,11 +16,23 @@ import { getImporterSpecs } from "./ideal/get-importer-specs.js";
16
16
  import { lockfile } from "./index.js";
17
17
  import { updatePackageJson } from "./reify/update-importers-package-json.js";
18
18
  import { Monorepo } from '@vltpkg/workspaces';
19
+ import { assertSkillsDirs } from "./skills/dirs.js";
20
+ import { assertQuery } from "./filter-nodes-by-query.js";
19
21
  export const install = async (options, add) => {
20
22
  // Validate incompatible options
21
23
  if (options.lockfileOnly && options.cleanInstall) {
22
24
  throw error('Cannot use --lockfile-only with --clean-install (ci command). Clean install requires filesystem operations.');
23
25
  }
26
+ // unset or `:not(*)`: no skills work at all, the skills code never
27
+ // loads
28
+ const allowSkills = options.allowSkills === ':not(*)' ?
29
+ undefined
30
+ : options.allowSkills;
31
+ // fail before installing, not after
32
+ if (allowSkills) {
33
+ await assertQuery(allowSkills, 'allow-skills', ['skill']);
34
+ assertSkillsDirs(options.projectRoot, options.skillsDirs);
35
+ }
24
36
  if (options.expectLockfile || options.frozenLockfile) {
25
37
  const lockfilePath = resolve(options.projectRoot, 'vlt-lock.json');
26
38
  if (!existsSync(lockfilePath)) {
@@ -178,6 +190,7 @@ export const install = async (options, add) => {
178
190
  remover.confirm();
179
191
  }
180
192
  }
193
+ let result;
181
194
  try {
182
195
  const remove = Object.assign(new Map(), {
183
196
  modifiedDependencies: false,
@@ -226,6 +239,7 @@ export const install = async (options, add) => {
226
239
  ...options,
227
240
  add,
228
241
  graph,
242
+ modifiers,
229
243
  remove,
230
244
  })
231
245
  : undefined;
@@ -247,7 +261,7 @@ export const install = async (options, add) => {
247
261
  remove,
248
262
  remover,
249
263
  });
250
- return { buildQueue, graph, diff };
264
+ result = { buildQueue, graph, diff };
251
265
  }
252
266
  catch (err) {
253
267
  /* c8 ignore next */
@@ -262,4 +276,14 @@ export const install = async (options, add) => {
262
276
  catch { }
263
277
  throw err;
264
278
  }
279
+ // after the try: a link failure must not roll back the install
280
+ if (!allowSkills)
281
+ return result;
282
+ const { linkSkills } = await import("./skills/index.js");
283
+ result.skills = await linkSkills({
284
+ ...options,
285
+ graph: result.graph,
286
+ target: allowSkills,
287
+ });
288
+ return result;
265
289
  };
@@ -3,7 +3,7 @@ import { error } from '@vltpkg/error-cause';
3
3
  import { fastSplit } from '@vltpkg/fast-split';
4
4
  import { Spec } from '@vltpkg/spec/browser';
5
5
  import { longDependencyTypes } from '@vltpkg/types';
6
- import { isDependencyTypeShort } from "../dependencies.js";
6
+ import { asDependencySpecError, isDependencyTypeShort, } from "../dependencies.js";
7
7
  const retrieveNodeFromGraph = (key, value, graph, fromId, seenNodes) => {
8
8
  const foundNode = graph.nodes.get(asDepID(fromId));
9
9
  if (!foundNode) {
@@ -104,7 +104,15 @@ export const loadEdges = (graph, edges, options, cache) => {
104
104
  // loaded), so overriding here would always inject `undefined`
105
105
  // and silently revert to the default npm registry.
106
106
  // See vltpkg/vltpkg#1580.
107
- const spec = parseEdgeSpec(specName, valRest.substring(0, vrSplit), options, cache);
107
+ let spec;
108
+ try {
109
+ spec = parseEdgeSpec(specName, valRest.substring(0, vrSplit), options, cache);
110
+ }
111
+ catch (er) {
112
+ // spec text is the lockfile's; hidden lockfile errors are
113
+ // swallowed, so only vlt-lock.json surfaces here
114
+ throw asDependencySpecError(er, fromNode, depType, 'vlt-lock.json');
115
+ }
108
116
  if (useOptimizations) {
109
117
  edgeProcessingQueue.push({
110
118
  fromNode,
@@ -101,6 +101,11 @@ export type ModifierActiveEntry = {
101
101
  * for (const entry of entries)
102
102
  * modifier.updateActiveEntry(fromNode, entry)
103
103
  * ```
104
+ *
105
+ * A trailing `:workspace` right after an importer selector, e.g.
106
+ * `:project > :workspace`, is expanded into one `#<name>` entry per
107
+ * named workspace of the graph, on its first `tryImporter` or
108
+ * `importerEdgeModifier` call.
104
109
  */
105
110
  export declare class GraphModifier {
106
111
  #private;
@@ -126,6 +131,12 @@ export declare class GraphModifier {
126
131
  * root's own b edge is still the manifest's to validate and heal.
127
132
  */
128
133
  targetsImporterEdge(importer: Node, spec: Spec): boolean;
134
+ /**
135
+ * The modifier governing the direct `importer -> spec.name` edge, see
136
+ * {@link GraphModifier.targetsImporterEdge}. The most specific one
137
+ * wins, same as during the graph build.
138
+ */
139
+ importerEdgeModifier(importer: Node, spec: Spec): ModifierEntry | undefined;
129
140
  /**
130
141
  * Selector of the modifier that wins the direct `importer -> spec.name`
131
142
  * edge, if it removes it (value `-`). Same pick as tryNewDependency:
package/dist/modifiers.js CHANGED
@@ -7,9 +7,20 @@ import { load } from '@vltpkg/vlt-json';
7
7
  * Does this breadcrumb item select the given importer?
8
8
  */
9
9
  const matchesImporter = (item, importer) => item.importer &&
10
+ importer.importer &&
10
11
  (item.value === ':project' ||
11
- (item.value === ':root' && importer.mainImporter) ||
12
- (item.value === ':workspace' && importer.importer));
12
+ item.value === ':workspace' ||
13
+ (item.value === ':root' && importer.mainImporter));
14
+ /**
15
+ * Is this a trailing `:workspace` item right after a plain importer
16
+ * selector, e.g. `:project > :workspace`? It selects any direct
17
+ * dependency named after a workspace.
18
+ */
19
+ const isWorkspaceTarget = (item) => item.importer &&
20
+ item.value === ':workspace' &&
21
+ !!item.prev?.importer &&
22
+ !item.prev.name &&
23
+ !item.prev.prev;
13
24
  /**
14
25
  * Does this item's qualifier (e.g. `:semver(^1)`, `:v(^1)`) accept the
15
26
  * given spec? Shared by traversal matching and by the frozen / healing
@@ -42,6 +53,11 @@ const matchesQualifier = (item, spec) => !!item?.comparator({ semver: spec.semve
42
53
  * for (const entry of entries)
43
54
  * modifier.updateActiveEntry(fromNode, entry)
44
55
  * ```
56
+ *
57
+ * A trailing `:workspace` right after an importer selector, e.g.
58
+ * `:project > :workspace`, is expanded into one `#<name>` entry per
59
+ * named workspace of the graph, on its first `tryImporter` or
60
+ * `importerEdgeModifier` call.
45
61
  */
46
62
  export class GraphModifier {
47
63
  /** The loaded modifiers configuration */
@@ -73,6 +89,14 @@ export class GraphModifier {
73
89
  activeModifiers = new Set();
74
90
  /** A set of all modifier string values loaded from vlt.json */
75
91
  modifierNames = new Set();
92
+ /** Spec options used when expanding `:workspace` targets */
93
+ #options = {};
94
+ /** `<importer> > :workspace` modifiers, expanded once a graph is known */
95
+ #workspaceTargets = [];
96
+ /** Graphs whose workspaces were already expanded */
97
+ #expandedGraphs = new WeakSet();
98
+ /** Workspace names already expanded */
99
+ #expandedNames = new Set();
76
100
  constructor(options) {
77
101
  this.load(options);
78
102
  }
@@ -96,8 +120,18 @@ export class GraphModifier {
96
120
  * root's own b edge is still the manifest's to validate and heal.
97
121
  */
98
122
  targetsImporterEdge(importer, spec) {
99
- for (const { breadcrumb } of this.#modifiers) {
100
- const { last } = breadcrumb;
123
+ return !!this.importerEdgeModifier(importer, spec);
124
+ }
125
+ /**
126
+ * The modifier governing the direct `importer -> spec.name` edge, see
127
+ * {@link GraphModifier.targetsImporterEdge}. The most specific one
128
+ * wins, same as during the graph build.
129
+ */
130
+ importerEdgeModifier(importer, spec) {
131
+ this.#expandWorkspaceTargets(importer);
132
+ const found = [];
133
+ for (const mod of this.#modifiers) {
134
+ const { last } = mod.breadcrumb;
101
135
  if (last.name !== spec.name)
102
136
  continue;
103
137
  // a qualifier that rejects the spec means the modifier never
@@ -106,14 +140,12 @@ export class GraphModifier {
106
140
  continue;
107
141
  const { prev } = last;
108
142
  // 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;
143
+ if (!prev || (!prev.prev && matchesImporter(prev, importer))) {
144
+ found.push(mod);
145
+ }
115
146
  }
116
- return false;
147
+ const [best] = specificitySort(found.map(m => m.breadcrumb));
148
+ return found.find(m => m.breadcrumb === best);
117
149
  }
118
150
  /**
119
151
  * Selector of the modifier that wins the direct `importer -> spec.name`
@@ -143,16 +175,22 @@ export class GraphModifier {
143
175
  * Loads the modifiers defined in `vlt.json` into memory.
144
176
  */
145
177
  load(options) {
178
+ this.#options = options;
146
179
  for (const [key, value] of Object.entries(this.config)) {
147
180
  this.modifierNames.add(key);
148
181
  const breadcrumb = parseBreadcrumb(key);
149
- /* c8 ignore start - should not be possible */
150
182
  if (!breadcrumb.last.name) {
151
- throw error('Could not find name in breadcrumb', {
183
+ // `<importer> > :workspace` names no package up front, it is
184
+ // expanded into one entry per workspace once a graph is known
185
+ if (isWorkspaceTarget(breadcrumb.last)) {
186
+ this.#workspaceTargets.push({ query: key, value });
187
+ continue;
188
+ }
189
+ throw error('Invalid modifier selector', {
152
190
  found: key,
191
+ wanted: 'a selector ending in #<name>, or :root, :project or :workspace followed by > :workspace',
153
192
  });
154
193
  }
155
- /* c8 ignore stop */
156
194
  let mod;
157
195
  if (typeof value === 'string') {
158
196
  mod = {
@@ -179,13 +217,58 @@ export class GraphModifier {
179
217
  this.#nodeModifiers.add(mod);
180
218
  }
181
219
  /* c8 ignore end */
182
- this.#modifiers.add(mod);
183
- // if the breadcrumb starts with an id, then add it to the
184
- // map of initial entries, so that we can use it to match
185
- if (breadcrumb.first.name) {
186
- const initialSet = this.#initialEntries.get(breadcrumb.first.name) ?? new Set();
187
- initialSet.add(mod);
188
- this.#initialEntries.set(breadcrumb.first.name, initialSet);
220
+ this.#register(mod);
221
+ }
222
+ }
223
+ /**
224
+ * Adds a modifier entry to the set of known modifiers.
225
+ */
226
+ #register(mod) {
227
+ this.#modifiers.add(mod);
228
+ // if the breadcrumb starts with an id, then add it to the
229
+ // map of initial entries, so that we can use it to match
230
+ const { name } = mod.breadcrumb.first;
231
+ if (name) {
232
+ const initialSet = this.#initialEntries.get(name) ?? new Set();
233
+ initialSet.add(mod);
234
+ this.#initialEntries.set(name, initialSet);
235
+ }
236
+ }
237
+ /**
238
+ * Expands `<importer> > :workspace` modifiers into one
239
+ * `<importer> > #<name>` entry per named workspace of the node's graph.
240
+ */
241
+ #expandWorkspaceTargets(node) {
242
+ if (!this.#workspaceTargets.length)
243
+ return;
244
+ const { graph } = node;
245
+ if (this.#expandedGraphs.has(graph))
246
+ return;
247
+ this.#expandedGraphs.add(graph);
248
+ for (const importer of graph.importers) {
249
+ // nameless workspaces are named after their folder, never link those
250
+ const name = importer.manifest?.name;
251
+ if (importer.mainImporter ||
252
+ !name ||
253
+ this.#expandedNames.has(name)) {
254
+ continue;
255
+ }
256
+ this.#expandedNames.add(name);
257
+ for (const { query, value } of this.#workspaceTargets) {
258
+ // fresh breadcrumb per name: items are a linked list and the
259
+ // active entry maps key on the last item's name
260
+ const breadcrumb = parseBreadcrumb(query);
261
+ breadcrumb.last.name = name;
262
+ const mod = {
263
+ breadcrumb,
264
+ query,
265
+ refs: new Set(),
266
+ spec: Spec.parse(name, value, this.#options),
267
+ type: 'edge',
268
+ value,
269
+ };
270
+ this.#edgeModifiers.add(mod);
271
+ this.#register(mod);
189
272
  }
190
273
  }
191
274
  }
@@ -195,6 +278,7 @@ export class GraphModifier {
195
278
  * update the active entry to the current importer node.
196
279
  */
197
280
  tryImporter(importer) {
281
+ this.#expandWorkspaceTargets(importer);
198
282
  for (const modifier of this.#modifiers) {
199
283
  // if the first item in the breadcrumb is an importer and it matches
200
284
  // any of the valid top-level selectors, then register the modifier
@@ -7,3 +7,7 @@ import type { Edge } from '../edge.ts';
7
7
  * until scripts are run, in the case of non-store deps like workspaces
8
8
  */
9
9
  export declare const addEdge: (edge: Edge, scurry: PathScurry, remover: RollbackRemove, bins?: Record<string, string>) => Promise<void>;
10
+ /**
11
+ * link the `key` bin of the package at `path` into `binRoot`
12
+ */
13
+ export declare const linkBin: (path: string, binRoot: string, key: string, val: string, remover: RollbackRemove) => Promise<void>;
@@ -59,15 +59,21 @@ export const addEdge = async (edge, scurry, remover, bins) => {
59
59
  promises.push(p);
60
60
  if (bins) {
61
61
  for (const [key, val] of Object.entries(bins)) {
62
- const link = resolve(binRoot, key);
63
- const absTarget = resolve(path, val);
64
- const target = relative(binRoot, absTarget);
65
- // TODO: bash/cmd/ps1 shims on Windows
66
- promises.push(process.platform === 'win32' ?
67
- cmdShimIfExists(absTarget, link, remover)
68
- : clobberSymlink(target, link, remover));
62
+ promises.push(linkBin(path, binRoot, key, val, remover));
69
63
  }
70
64
  }
71
65
  if (promises.length)
72
66
  await Promise.all(promises);
73
67
  };
68
+ /**
69
+ * link the `key` bin of the package at `path` into `binRoot`
70
+ */
71
+ export const linkBin = (path, binRoot, key, val, remover) => {
72
+ const link = resolve(binRoot, key);
73
+ const absTarget = resolve(path, val);
74
+ const target = relative(binRoot, absTarget);
75
+ // TODO: bash/cmd/ps1 shims on Windows
76
+ return process.platform === 'win32' ?
77
+ cmdShimIfExists(absTarget, link, remover)
78
+ : clobberSymlink(target, link, remover);
79
+ };
@@ -9,5 +9,12 @@ import type { Node } from '../node.ts';
9
9
  export type BuildResult = {
10
10
  success: Node[];
11
11
  failure: Node[];
12
+ /** needed builds skipped as they have no security data */
13
+ skipped?: Node[];
14
+ /**
15
+ * why `skipped` builds have no security data: the request timed
16
+ * out, or socket.dev has not scanned them yet
17
+ */
18
+ skippedReason?: 'timeout' | 'unscanned';
12
19
  };
13
20
  export declare const build: (diff: Diff, packageJson: PackageJson, scurry: PathScurry, allowScriptsNodes: Set<DepID>) => Promise<BuildResult>;
@@ -32,3 +32,8 @@ export type CheckNeededBuildOptions = {
32
32
  * @returns {BuildData} The BuildData object containing Node objects that need building
33
33
  */
34
34
  export declare const checkNeededBuild: (options: CheckNeededBuildOptions) => BuildData;
35
+ /**
36
+ * Copy build state from the actual graph onto ideal nodes that are
37
+ * not re-extracted, since `vlt-lock.json` does not store it.
38
+ */
39
+ export declare const carryBuildState: (diff: Diff) => void;
@@ -70,3 +70,16 @@ export const checkNeededBuild = (options) => {
70
70
  };
71
71
  return buildData;
72
72
  };
73
+ /**
74
+ * Copy build state from the actual graph onto ideal nodes that are
75
+ * not re-extracted, since `vlt-lock.json` does not store it.
76
+ */
77
+ export const carryBuildState = (diff) => {
78
+ for (const [id, node] of diff.to.nodes) {
79
+ if (diff.nodes.add.has(node))
80
+ continue;
81
+ const prev = diff.from.nodes.get(id);
82
+ if (prev)
83
+ node.buildState = prev.buildState;
84
+ }
85
+ };
@@ -1,15 +1,12 @@
1
+ import { readCmdShimIfExists } from '@vltpkg/cmd-shim';
1
2
  import { resolve } from 'node:path';
2
- const rmBinPosix = (remover, bin) => {
3
- return [remover.rm(bin)];
4
- };
5
- const rmBinWin32 = (remover, bin) => {
6
- return [
7
- remover.rm(bin),
8
- remover.rm(bin + '.cmd'),
9
- remover.rm(bin + '.ps1'),
10
- ];
11
- };
12
- const rmBin = process.platform === 'win32' ? rmBinWin32 : rmBinPosix;
3
+ const binPathsPosix = (bin) => [bin];
4
+ const binPathsWin32 = (bin) => [
5
+ bin,
6
+ bin + '.cmd',
7
+ bin + '.ps1',
8
+ ];
9
+ const binPaths = process.platform === 'win32' ? binPathsWin32 : binPathsPosix;
13
10
  export const deleteEdge = async (edge, scurry, remover) => {
14
11
  const { spec: { name }, to, } = edge;
15
12
  const { sep } = scurry.cwd;
@@ -20,9 +17,14 @@ export const deleteEdge = async (edge, scurry, remover) => {
20
17
  promises.push(remover.rm(path));
21
18
  const bins = to?.bins;
22
19
  if (bins) {
23
- for (const key of Object.keys(bins)) {
24
- const bin = resolve(binRoot, key);
25
- promises.push(...rmBin(remover, bin));
20
+ for (const [key, value] of Object.entries(bins)) {
21
+ for (const bin of binPaths(resolve(binRoot, key))) {
22
+ const target = await readCmdShimIfExists(bin);
23
+ // another dependency may provide the same command.
24
+ if (target && target !== resolve(path, value))
25
+ continue;
26
+ promises.push(remover.rm(bin));
27
+ }
26
28
  }
27
29
  }
28
30
  await Promise.all(promises);
@@ -1,4 +1,5 @@
1
+ import type { DepID } from '@vltpkg/dep-id';
1
2
  import type { RollbackRemove } from '@vltpkg/rollback-remove';
2
3
  import type { PathScurry } from 'path-scurry';
3
4
  import type { Diff } from '../diff.ts';
4
- export declare const deleteNodes: (diff: Diff, remover: RollbackRemove, scurry: PathScurry) => Promise<unknown>[];
5
+ export declare const deleteNodes: (diff: Diff, remover: RollbackRemove, scurry: PathScurry, keep?: ReadonlySet<DepID>) => Promise<unknown>[];
@@ -1,5 +1,5 @@
1
1
  import { deleteEdge } from "./delete-edge.js";
2
- export const deleteNodes = (diff, remover, scurry) => {
2
+ export const deleteNodes = (diff, remover, scurry, keep) => {
3
3
  const store = scurry.resolve('node_modules/.vlt');
4
4
  const { sep } = scurry.cwd;
5
5
  const rmActions = [];
@@ -7,7 +7,9 @@ export const deleteNodes = (diff, remover, scurry) => {
7
7
  // do not delete workspaces or link targets
8
8
  if (!node.inVltStore())
9
9
  continue;
10
- rmActions.push(remover.rm(store + sep + node.id));
10
+ if (!keep?.has(node.id)) {
11
+ rmActions.push(remover.rm(store + sep + node.id));
12
+ }
11
13
  for (const edge of node.edgesIn) {
12
14
  rmActions.push(deleteEdge(edge, scurry, remover));
13
15
  }
@@ -18,6 +18,8 @@ export type ReifyOptions = LoadOptions & {
18
18
  update?: boolean;
19
19
  saveExact?: boolean;
20
20
  savePrefix?: string;
21
+ /** store dirs of these nodes are never removed, eg the running vlt */
22
+ keepNodes?: ReadonlySet<DepID>;
21
23
  };
22
24
  export type ReifyResult = {
23
25
  /**
@@ -8,51 +8,17 @@ import { addEdges } from "./add-edges.js";
8
8
  import { addNodes } from "./add-nodes.js";
9
9
  import { build } from "./build.js";
10
10
  import { deleteEdges } from "./delete-edges.js";
11
- import { checkNeededBuild } from "./check-needed-build.js";
11
+ import { relinkBins } from "./relink-bins.js";
12
+ import { carryBuildState, checkNeededBuild, } from "./check-needed-build.js";
12
13
  import { deleteNodes } from "./delete-nodes.js";
13
14
  import { internalHoist } from "./internal-hoist.js";
14
15
  import { pruneUnsupportedOptional } from "./optional-fail.js";
15
16
  import { rollback } from "./rollback.js";
16
17
  import { updatePackageJson } from "./update-importers-package-json.js";
17
18
  import { copyFileSync } from 'node:fs';
18
- import { Query } from '@vltpkg/query';
19
- import { SecurityArchive } from '@vltpkg/security-archive';
20
19
  import { binChmodAll } from "./bin-chmod.js";
20
+ import { filterNodesByQuery } from "../filter-nodes-by-query.js";
21
21
  const limit = Math.max(availableParallelism() - 1, 1) * 8;
22
- /**
23
- * Filter nodes using a DSS query string
24
- */
25
- const filterNodesByQuery = async (graph, allowScriptsQuery) => {
26
- // shortcut no packages included
27
- if (allowScriptsQuery === ':not(*)' /* c8 ignore next */ ||
28
- !allowScriptsQuery) {
29
- return new Set();
30
- }
31
- // shortcut all packages included
32
- if (allowScriptsQuery === '*') {
33
- return new Set(graph.nodes.keys());
34
- }
35
- /* c8 ignore start */
36
- const securityArchive = Query.hasSecuritySelectors(allowScriptsQuery) ?
37
- await SecurityArchive.start({
38
- nodes: [...graph.nodes.values()],
39
- })
40
- : undefined;
41
- /* c8 ignore stop */
42
- const edges = graph.edges;
43
- const nodes = new Set(graph.nodes.values());
44
- const importers = graph.importers;
45
- const query = new Query({
46
- edges,
47
- nodes,
48
- importers,
49
- securityArchive,
50
- });
51
- const { nodes: resultNodes } = await query.search(allowScriptsQuery, {
52
- signal: new AbortController().signal,
53
- });
54
- return new Set(resultNodes.map(node => node.id));
55
- };
56
22
  /**
57
23
  * Make the current project match the supplied graph.
58
24
  */
@@ -65,6 +31,7 @@ export const reify = async (options) => {
65
31
  loadManifests: true,
66
32
  });
67
33
  const diff = new Diff(actual, graph);
34
+ carryBuildState(diff);
68
35
  // normalise importer edge specs and compute the package.json changes
69
36
  // before deciding whether anything needs reifying: an add whose target
70
37
  // is already in place has no node diff but still has a saved value to
@@ -81,6 +48,7 @@ export const reify = async (options) => {
81
48
  packageJson: options.packageJson,
82
49
  saveExact: options.saveExact,
83
50
  savePrefix: options.savePrefix,
51
+ modifiers: options.modifiers,
84
52
  })
85
53
  : undefined;
86
54
  const noModifiedDependencies = !options.add?.modifiedDependencies &&
@@ -161,6 +129,10 @@ const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
161
129
  const edgeActions = addEdges(diff, scurry, remover);
162
130
  if (edgeActions.length)
163
131
  await callLimit(edgeActions, { limit });
132
+ // link bins that deleted edges removed but other deps still declare
133
+ const binActions = relinkBins(diff, scurry, remover);
134
+ if (binActions.length)
135
+ await callLimit(binActions, { limit });
164
136
  await internalHoist(diff.to, options, remover);
165
137
  // looks up all nodes setting buildState = 'needed'
166
138
  // on nodes that require building
@@ -173,7 +145,7 @@ const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
173
145
  await build(diff, packageJson, scurry, allowScriptsNodes);
174
146
  // set the buildQueue on the result object containing
175
147
  // an array with all the ids of nodes that need building
176
- res.buildQueue = [...diff.nodes.add]
148
+ res.buildQueue = [...diff.to.nodes.values()]
177
149
  .filter(node => node.buildState === 'needed')
178
150
  .map(node => node.id);
179
151
  // if we had to change the actual graph along the way,
@@ -189,7 +161,7 @@ const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
189
161
  /* c8 ignore stop */
190
162
  saveHidden(options);
191
163
  // delete garbage from the store.
192
- const rmActions = deleteNodes(diff, remover, scurry);
164
+ const rmActions = deleteNodes(diff, remover, scurry, options.keepNodes);
193
165
  if (rmActions.length)
194
166
  await Promise.all(rmActions);
195
167
  // updates package.json files if anything was added / removed
@@ -0,0 +1,9 @@
1
+ import type { RollbackRemove } from '@vltpkg/rollback-remove';
2
+ import type { PathScurry } from 'path-scurry';
3
+ import type { Diff } from '../diff.ts';
4
+ /**
5
+ * A deleted edge removes the bins it owns, but another dependency
6
+ * that did not change may declare the same bin name. Link the first
7
+ * one found if the bin is still missing once added edges are linked.
8
+ */
9
+ export declare const relinkBins: (diff: Diff, scurry: PathScurry, remover: RollbackRemove) => (() => Promise<unknown>)[];
@@ -0,0 +1,39 @@
1
+ import { resolve } from 'node:path';
2
+ import { linkBin } from "./add-edge.js";
3
+ /**
4
+ * A deleted edge removes the bins it owns, but another dependency
5
+ * that did not change may declare the same bin name. Link the first
6
+ * one found if the bin is still missing once added edges are linked.
7
+ */
8
+ export const relinkBins = (diff, scurry, remover) => {
9
+ const actions = [];
10
+ const seen = new Set();
11
+ const { sep } = scurry.cwd;
12
+ for (const deleted of diff.edges.delete) {
13
+ const node = diff.to.nodes.get(deleted.from.id);
14
+ const bins = deleted.to?.bins;
15
+ if (!node || !bins)
16
+ continue;
17
+ const nm = node.nodeModules(scurry);
18
+ const binRoot = nm + sep + '.bin';
19
+ for (const key of Object.keys(bins)) {
20
+ const link = resolve(binRoot, key);
21
+ if (seen.has(link))
22
+ continue;
23
+ seen.add(link);
24
+ for (const edge of node.edgesOut.values()) {
25
+ const val = edge.to?.bins?.[key];
26
+ if (!val)
27
+ continue;
28
+ const path = nm + sep + edge.spec.name.replace('/', sep);
29
+ actions.push(async () => {
30
+ if (await scurry.lstat(link))
31
+ return;
32
+ await linkBin(path, binRoot, key, val, remover);
33
+ });
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ return actions;
39
+ };