@vltpkg/graph 1.5.0 → 1.6.1

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.
@@ -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
+ };
@@ -1,6 +1,7 @@
1
1
  import type { PackageJson } from '@vltpkg/package-json';
2
2
  import type { AddImportersDependenciesMap, RemoveImportersDependenciesMap } from '../dependencies.ts';
3
3
  import type { Graph } from '../graph.ts';
4
+ import type { GraphModifier } from '../modifiers.ts';
4
5
  export type UpdatePackageJsonOptions = {
5
6
  /**
6
7
  * A `Map` in which keys are {@link DepID} linking to another `Map` in which
@@ -37,9 +38,13 @@ export type UpdatePackageJsonOptions = {
37
38
  * Defaults to `^` if not specified. Ignored when `saveExact` is true.
38
39
  */
39
40
  savePrefix?: string;
41
+ /**
42
+ * Graph modifiers, importer edges they govern keep the modifier value.
43
+ */
44
+ modifiers?: GraphModifier;
40
45
  };
41
46
  /**
42
47
  * Updates nodes of a provided {@link Graph} accordingly to the
43
48
  * provided add or remove arguments.
44
49
  */
45
- export declare const updatePackageJson: ({ add, graph, packageJson, remove, saveExact, savePrefix, }: UpdatePackageJsonOptions) => () => void;
50
+ export declare const updatePackageJson: ({ add, graph, packageJson, remove, saveExact, savePrefix, modifiers, }: UpdatePackageJsonOptions) => () => void;
@@ -5,7 +5,7 @@ import { longDependencyTypes } from '@vltpkg/types';
5
5
  import { longDependencyTypeNames } from "../dependencies.js";
6
6
  import { resolveSaveType } from "../resolve-save-type.js";
7
7
  import { calculateSaveValue } from "./calculate-save-value.js";
8
- const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
8
+ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix, modifiers) => {
9
9
  const node = graph.nodes.get(nodeId);
10
10
  if (!node) {
11
11
  throw error('Failed to retrieve node', {
@@ -56,7 +56,10 @@ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
56
56
  }
57
57
  const edge = node.edgesOut.get(name);
58
58
  if (!edge?.to) {
59
- throw error('Dependency node could not be found');
59
+ throw error(`Dependency node could not be found: ${name}`, {
60
+ spec: dep.spec,
61
+ from: node.location,
62
+ });
60
63
  }
61
64
  const n = edge.to;
62
65
  const [nodeType] = splitDepID(n.id);
@@ -90,8 +93,10 @@ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
90
93
  // the lockfile has to carry the value package.json carries, or a
91
94
  // later install reads back `latest` where the manifest says
92
95
  // `^1.2.3`. anything keyed by the old spec from here on must be
93
- // invalidated at this point.
94
- if (saveValue !== edge.spec.bareSpec) {
96
+ // invalidated at this point. governed edges keep the modifier
97
+ // value instead.
98
+ if (saveValue !== edge.spec.bareSpec &&
99
+ !modifiers?.targetsImporterEdge(node, dep.spec)) {
95
100
  edge.spec = Spec.parse(name, saveValue, dep.spec.options);
96
101
  graph.lockfileStale = true;
97
102
  }
@@ -103,7 +108,7 @@ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
103
108
  * Updates nodes of a provided {@link Graph} accordingly to the
104
109
  * provided add or remove arguments.
105
110
  */
106
- export const updatePackageJson = ({ add, graph, packageJson, remove, saveExact, savePrefix, }) => {
111
+ export const updatePackageJson = ({ add, graph, packageJson, remove, saveExact, savePrefix, modifiers, }) => {
107
112
  const manifestsToUpdate = new Set();
108
113
  const operations = new Set([add, remove]);
109
114
  for (const operation of operations) {
@@ -112,7 +117,7 @@ export const updatePackageJson = ({ add, graph, packageJson, remove, saveExact,
112
117
  // that are nested folders from which the user can also add new
113
118
  // dependencies to
114
119
  for (const nodeId of operation.keys()) {
115
- const manifest = addOrRemoveDeps(nodeId, graph, operation, saveExact, savePrefix);
120
+ const manifest = addOrRemoveDeps(nodeId, graph, operation, saveExact, savePrefix, modifiers);
116
121
  if (manifest) {
117
122
  manifestsToUpdate.add(manifest);
118
123
  }
@@ -61,13 +61,13 @@ export declare const shipsSkills: (node: Node) => boolean;
61
61
  /**
62
62
  * Find the agent skills shipped by the given installed packages.
63
63
  *
64
- * Only packages in the vlt store are scanned, for a root `SKILL.md`,
65
- * then `<dir>/SKILL.md` in `skills/`, `dist/skills/` and
66
- * `.agents/skills/`: the first skill of a name wins. One version per
67
- * package is picked, even if it ships none: a dep of the main
68
- * importer, else of any importer, else the highest version, then
69
- * DepID. Same-name skills of different packages are all returned,
70
- * sorted by rank then DepID (`linkSkills` resolves them). `mounts`
71
- * set each skill's `linked` dirs.
64
+ * Only packages in the vlt store and the project's workspaces (not its
65
+ * root) are scanned, for a root `SKILL.md`, then `<dir>/SKILL.md` in
66
+ * `skills/`, `dist/skills/` and `.agents/skills/`: the first skill of a
67
+ * name wins. One version per package is picked, even if it ships none:
68
+ * a workspace or dep of the main importer, else a dep of any importer,
69
+ * else the highest version, then DepID. Same-name skills of different
70
+ * packages are all returned, sorted by rank then DepID (`linkSkills`
71
+ * resolves them). `mounts` set each skill's `linked` dirs.
72
72
  */
73
73
  export declare const discoverSkills: (nodes: Iterable<Node>, scurry: PathScurry, mounts?: SkillMount[]) => DiscoveredSkills;
@@ -134,8 +134,11 @@ const scan = (dir, pkgName) => {
134
134
  }
135
135
  return found;
136
136
  };
137
- // 0: dep of the main importer, 1: of any importer, 2: transitive
137
+ // 0: a workspace or dep of the main importer, 1: dep of any
138
+ // importer, 2: transitive
138
139
  const rank = (node) => {
140
+ if (node.importer)
141
+ return 0;
139
142
  let r = 2;
140
143
  for (const { from } of node.edgesIn) {
141
144
  if (from.mainImporter)
@@ -162,10 +165,11 @@ const aliasOf = (node) => {
162
165
  edges.find(e => e.from.importer))?.name;
163
166
  return n === node.name ? undefined : n;
164
167
  };
165
- // store pkgs only: lockfile keeps a tarball's path as its location
166
- const scannable = (node) => !node.importer &&
167
- node.inVltStore() &&
168
- splitDepID(node.id)[0] !== 'file';
168
+ // store pkgs and workspaces, not the project root; lockfile keeps a
169
+ // tarball's path as its location
170
+ const scannable = (node) => node.importer ?
171
+ !node.mainImporter
172
+ : node.inVltStore() && splitDepID(node.id)[0] !== 'file';
169
173
  /** Ships agent skills, as `discoverSkills` finds them. */
170
174
  export const shipsSkills = (node) => scannable(node) &&
171
175
  scan(realOr(resolve(node.projectRoot, node.location)), node.name)
@@ -173,14 +177,14 @@ export const shipsSkills = (node) => scannable(node) &&
173
177
  /**
174
178
  * Find the agent skills shipped by the given installed packages.
175
179
  *
176
- * Only packages in the vlt store are scanned, for a root `SKILL.md`,
177
- * then `<dir>/SKILL.md` in `skills/`, `dist/skills/` and
178
- * `.agents/skills/`: the first skill of a name wins. One version per
179
- * package is picked, even if it ships none: a dep of the main
180
- * importer, else of any importer, else the highest version, then
181
- * DepID. Same-name skills of different packages are all returned,
182
- * sorted by rank then DepID (`linkSkills` resolves them). `mounts`
183
- * set each skill's `linked` dirs.
180
+ * Only packages in the vlt store and the project's workspaces (not its
181
+ * root) are scanned, for a root `SKILL.md`, then `<dir>/SKILL.md` in
182
+ * `skills/`, `dist/skills/` and `.agents/skills/`: the first skill of a
183
+ * name wins. One version per package is picked, even if it ships none:
184
+ * a workspace or dep of the main importer, else a dep of any importer,
185
+ * else the highest version, then DepID. Same-name skills of different
186
+ * packages are all returned, sorted by rank then DepID (`linkSkills`
187
+ * resolves them). `mounts` set each skill's `linked` dirs.
184
188
  */
185
189
  export const discoverSkills = (nodes, scurry, mounts = []) => {
186
190
  const key = (name, path) => `${lc(name)}\0${norm(path)}`;
@@ -11,3 +11,8 @@ export declare const editGitignore: (dir: string, edit: {
11
11
  add: string[];
12
12
  remove: string[];
13
13
  }) => "ok" | "refused";
14
+ /**
15
+ * Names in vlt's block of `<dir>/.gitignore`: vlt's record of its links
16
+ * in `dir`. Empty if none or unreadable.
17
+ */
18
+ export declare const gitignoreNames: (dir: string) => Set<string>;
@@ -5,6 +5,12 @@ export const GITIGNORE_END = '# <<< vlt agent skills';
5
5
  const sorted = (names) => [...names]
6
6
  .sort((a, b) => a.localeCompare(b, 'en'))
7
7
  .map(n => `/${n}`);
8
+ // vlt's block: `end` is -1 if none
9
+ const findBlock = (lines) => {
10
+ const begin = lines.findIndex(l => l.trim() === GITIGNORE_BEGIN);
11
+ const end = lines.findIndex((l, i) => begin !== -1 && i > begin && l.trim() === GITIGNORE_END);
12
+ return { begin, end };
13
+ };
8
14
  const update = (file, { add, remove }) => {
9
15
  const st = lstatSync(file, { throwIfNoEntry: false });
10
16
  if (st && !st.isFile())
@@ -12,8 +18,7 @@ const update = (file, { add, remove }) => {
12
18
  const text = st ? readFileSync(file, 'utf8') : '';
13
19
  const eol = text.includes('\r\n') ? '\r\n' : '\n';
14
20
  const lines = text.split(eol);
15
- const begin = lines.findIndex(l => l.trim() === GITIGNORE_BEGIN);
16
- const end = lines.findIndex((l, i) => begin !== -1 && i > begin && l.trim() === GITIGNORE_END);
21
+ const { begin, end } = findBlock(lines);
17
22
  let out;
18
23
  if (end === -1) {
19
24
  if (!add.length)
@@ -51,3 +56,27 @@ export const editGitignore = (dir, edit) => {
51
56
  return 'refused';
52
57
  }
53
58
  };
59
+ /**
60
+ * Names in vlt's block of `<dir>/.gitignore`: vlt's record of its links
61
+ * in `dir`. Empty if none or unreadable.
62
+ */
63
+ export const gitignoreNames = (dir) => {
64
+ try {
65
+ const file = join(dir, '.gitignore');
66
+ if (!lstatSync(file, { throwIfNoEntry: false })?.isFile()) {
67
+ return new Set();
68
+ }
69
+ const lines = readFileSync(file, 'utf8').split(/\r?\n/);
70
+ const { begin, end } = findBlock(lines);
71
+ if (end === -1)
72
+ return new Set();
73
+ return new Set(lines
74
+ .slice(begin + 1, end)
75
+ .map(l => l.trim())
76
+ .filter(l => l.startsWith('/'))
77
+ .map(l => l.slice(1)));
78
+ }
79
+ catch {
80
+ return new Set();
81
+ }
82
+ };
@@ -109,15 +109,16 @@ export declare const listSkills: (o: SkillsOptions) => Promise<ListSkillsResult>
109
109
  * project's agent skill dirs (see `skillsDirs`), created only when a
110
110
  * skill is picked. `target` selectors may end with `:skill()`. One
111
111
  * package per name, among picked skills, in every dir: the package of
112
- * the first existing link (in dir order), else rank (main importer
113
- * dep, importer dep, transitive), then DepID; the others are
112
+ * the first existing link (in dir order), else rank (workspace or main
113
+ * importer dep, importer dep, transitive), then DepID; the others are
114
114
  * reported. Installs keep a dir's link to another installed skill.
115
115
  * A link follows its package to the picked version, in every scanned
116
116
  * dir. Then remove vlt links whose skill is no longer installed (with
117
117
  * `apply`: every vlt link not made here), and keep each dir's
118
- * `.gitignore` block in sync. Linked packages get private copies of
119
- * files hardlinked from the global store. Security queries fail
120
- * closed: skills of packages with no security data are not linked,
121
- * and reported in `unscanned` unless still linked.
118
+ * `.gitignore` block in sync (also vlt's record of workspace links).
119
+ * Linked packages get private copies of files hardlinked from the
120
+ * global store. Security queries fail closed: skills of packages with
121
+ * no security data are not linked, and reported in `unscanned` unless
122
+ * still linked.
122
123
  */
123
124
  export declare const linkSkills: (o: LinkSkillsOptions) => Promise<LinkSkillsResult>;
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from 'node:fs';
2
+ import { Monorepo } from '@vltpkg/workspaces';
2
3
  import { load as loadActual } from "../actual/load.js";
3
4
  import { filterNodesByQueryParts } from "../filter-nodes-by-query.js";
4
5
  import { unshare } from "../reify/unshare.js";
@@ -6,7 +7,7 @@ import { skillsDirs } from "./dirs.js";
6
7
  import { discoverSkills } from "./discover.js";
7
8
  import { editGitignore } from "./gitignore.js";
8
9
  import { maskSkills } from "./mask.js";
9
- import { mountSkill, packageKey, readMounts, samePath, unmountSkill, } from "./mounts.js";
10
+ import { mountSkill, packageKey, readMounts, realOr, samePath, unmountSkill, } from "./mounts.js";
10
11
  export * from "./discover.js";
11
12
  const lc = (s) => s.toLowerCase();
12
13
  const selectNodes = async (o) => {
@@ -21,6 +22,23 @@ const dirsOf = (o, create = false) => skillsDirs({
21
22
  env: o.env,
22
23
  create,
23
24
  });
25
+ // every workspace, selected or not: owners of vlt links
26
+ const workspaceDirs = (graph, scurry) => [...graph.importers]
27
+ .filter(n => !n.mainImporter)
28
+ .map(n => ({
29
+ id: n.id,
30
+ path: realOr(n.resolvedLocation(scurry)),
31
+ }));
32
+ // every workspace, loaded or not (`-w`)
33
+ const allWorkspaces = (m) => m ?
34
+ [
35
+ ...Monorepo.load(m.projectRoot, {
36
+ config: m.config,
37
+ scurry: m.scurry,
38
+ packageJson: m.packageJson,
39
+ }).values(),
40
+ ].map(ws => ({ id: ws.id, path: realOr(ws.fullpath) }))
41
+ : [];
24
42
  // packages behind live vlt links
25
43
  const liveOwners = (mounts) => mounts.flatMap(m => m.owner && existsSync(m.target) ? [m.owner] : []);
26
44
  // private copies of store-hardlinked files: edits via skill links
@@ -61,7 +79,7 @@ const report = (dropped, unscanned) => unscanned && dropped.length ?
61
79
  export const listSkills = async (o) => {
62
80
  const { graph, nodes, parts, unscanned } = await selectNodes(o);
63
81
  const { targets, scan, skipped } = dirsOf(o);
64
- const { skills } = discoverSkills(nodes, o.scurry, readMounts(scan));
82
+ const { skills } = discoverSkills(nodes, o.scurry, readMounts(scan, { workspaces: workspaceDirs(graph, o.scurry) }));
65
83
  const { picked, dropped } = pick(skills, parts, unscanned);
66
84
  return {
67
85
  project: graph.mainImporter.name,
@@ -77,16 +95,17 @@ export const listSkills = async (o) => {
77
95
  * project's agent skill dirs (see `skillsDirs`), created only when a
78
96
  * skill is picked. `target` selectors may end with `:skill()`. One
79
97
  * package per name, among picked skills, in every dir: the package of
80
- * the first existing link (in dir order), else rank (main importer
81
- * dep, importer dep, transitive), then DepID; the others are
98
+ * the first existing link (in dir order), else rank (workspace or main
99
+ * importer dep, importer dep, transitive), then DepID; the others are
82
100
  * reported. Installs keep a dir's link to another installed skill.
83
101
  * A link follows its package to the picked version, in every scanned
84
102
  * dir. Then remove vlt links whose skill is no longer installed (with
85
103
  * `apply`: every vlt link not made here), and keep each dir's
86
- * `.gitignore` block in sync. Linked packages get private copies of
87
- * files hardlinked from the global store. Security queries fail
88
- * closed: skills of packages with no security data are not linked,
89
- * and reported in `unscanned` unless still linked.
104
+ * `.gitignore` block in sync (also vlt's record of workspace links).
105
+ * Linked packages get private copies of files hardlinked from the
106
+ * global store. Security queries fail closed: skills of packages with
107
+ * no security data are not linked, and reported in `unscanned` unless
108
+ * still linked.
90
109
  */
91
110
  export const linkSkills = async (o) => {
92
111
  const { graph, nodes, parts, unscanned } = await selectNodes(o);
@@ -101,7 +120,8 @@ export const linkSkills = async (o) => {
101
120
  conflicts: [],
102
121
  dirs: targets.map(d => d.dir),
103
122
  };
104
- const mounts = readMounts(scan);
123
+ const workspaces = workspaceDirs(graph, o.scurry);
124
+ const mounts = readMounts(scan, { workspaces });
105
125
  const byName = new Map(mounts.map(m => [`${m.dir.dir}\0${lc(m.name)}`, m]));
106
126
  // the link's skill is no longer installed: dangling, orphan owner,
107
127
  // or a scanned owner that no longer ships it
@@ -163,7 +183,10 @@ export const linkSkills = async (o) => {
163
183
  // names taken over by entries not vlt's
164
184
  const foreign = [];
165
185
  const kept = new Set();
186
+ // links made here, recorded even if not yet in `.gitignore`
187
+ const made = new Set();
166
188
  const where = new Map();
189
+ let every;
167
190
  for (const { dir, winner, losers, m } of picks) {
168
191
  if (winner) {
169
192
  // replaced link named in another case (case-sensitive fs)
@@ -172,10 +195,16 @@ export const linkSkills = async (o) => {
172
195
  gone.push(m);
173
196
  }
174
197
  const mount = `${dir.dir}/${winner.skill}`;
175
- const r = mountSkill(winner, dir);
198
+ const r = mountSkill(winner, dir, m?.name === winner.skill);
199
+ if (r === 'linked')
200
+ made.add(mount);
176
201
  if (r === 'conflict') {
177
202
  res.conflicts.push(mount);
178
- foreign.push({ dir, name: winner.skill });
203
+ // vlt's link into a workspace not loaded (`-w`) keeps its line
204
+ every ??= allWorkspaces(o.monorepo);
205
+ if (!readMounts([dir], { workspaces: every }).some(l => l.name === winner.skill)) {
206
+ foreign.push({ dir, name: winner.skill });
207
+ }
179
208
  }
180
209
  else {
181
210
  const k = `${winner.id}\0${winner.skill}`;
@@ -189,7 +218,7 @@ export const linkSkills = async (o) => {
189
218
  for (const s of losers)
190
219
  res.conflicts.push(`${dir.dir}/${s.skill} (${label(s)})`);
191
220
  }
192
- const after = readMounts(scan);
221
+ const after = readMounts(scan, { workspaces, made });
193
222
  const present = [];
194
223
  for (const m of after) {
195
224
  if (o.apply ? kept.has(m.mount) : !stale(m)) {
@@ -16,9 +16,17 @@ export type SkillMount = {
16
16
  link: string;
17
17
  /** absolute resolved link target */
18
18
  target: string;
19
- /** DepID of the store dir the link points into */
19
+ /** DepID of the store dir or workspace the link points into */
20
20
  owner?: DepID;
21
21
  };
22
+ /**
23
+ * A workspace of the project.
24
+ */
25
+ export type WorkspaceDir = {
26
+ id: DepID;
27
+ /** realpath */
28
+ path: string;
29
+ };
22
30
  /**
23
31
  * Outcome of mounting a single skill.
24
32
  */
@@ -50,14 +58,20 @@ export declare const ownerOf: (target: string) => DepID | undefined;
50
58
  */
51
59
  export declare const packageKey: (id: DepID) => string;
52
60
  /**
53
- * All vlt links in the given dirs. Anything else is ignored.
61
+ * All vlt links in the given dirs: links into a `node_modules/.vlt`
62
+ * store, and links listed in the dir's `.gitignore` block (or `made`)
63
+ * into a workspace or dangling. Anything else is ignored.
54
64
  */
55
- export declare const readMounts: (scan: SkillsDir[]) => SkillMount[];
65
+ export declare const readMounts: (scan: SkillsDir[], { workspaces, made, }?: {
66
+ workspaces?: WorkspaceDir[];
67
+ made?: Set<string>;
68
+ }) => SkillMount[];
56
69
  /**
57
- * Link a skill at `<dir>/<skill>`. Never touches anything not managed by
58
- * vlt: returns `'conflict'` instead.
70
+ * Link a skill at `<dir>/<skill>`. `'unchanged'` if the entry already
71
+ * is the skill. Never touches anything not managed by vlt (a store
72
+ * link, or `owned`): returns `'conflict'` instead.
59
73
  */
60
- export declare const mountSkill: (skill: Pick<Skill, "skill" | "path">, dir: SkillsDir) => MountResult;
74
+ export declare const mountSkill: (skill: Pick<Skill, "skill" | "path">, dir: SkillsDir, owned?: boolean) => MountResult;
61
75
  /**
62
76
  * Remove a vlt link. Its dir is kept.
63
77
  */
@@ -1,7 +1,8 @@
1
- import { lstatSync, readdirSync, readlinkSync, realpathSync, unlinkSync, } from 'node:fs';
1
+ import { existsSync, lstatSync, readdirSync, readlinkSync, realpathSync, unlinkSync, } from 'node:fs';
2
2
  import { dirname, join, relative, resolve, sep } from 'node:path';
3
3
  import { baseDepID, isDepID, joinDepIDTuple, splitDepID, } from '@vltpkg/dep-id';
4
4
  import { symlinkSyncMkdirp } from "../reify/symlink-sync.js";
5
+ import { gitignoreNames } from "./gitignore.js";
5
6
  /** `lstat`, undefined when missing or unreadable (eg, ENOTDIR) */
6
7
  export const lstat = (path) => {
7
8
  try {
@@ -30,6 +31,15 @@ export const realOr = (p) => {
30
31
  */
31
32
  export const samePath = (a, b) => norm(realOr(a)) === norm(realOr(b));
32
33
  const storeSeg = `${sep}node_modules${sep}.vlt${sep}`;
34
+ // resolved target of a symlink (or junction)
35
+ const linkTarget = (link) => {
36
+ if (!lstat(link)?.isSymbolicLink())
37
+ return undefined;
38
+ let raw = readlinkSync(link);
39
+ if (raw.startsWith('\\\\?\\'))
40
+ raw = raw.slice(4);
41
+ return resolve(dirname(link), raw);
42
+ };
33
43
  /**
34
44
  * The target of `link` if it is a symlink (or junction) pointing into
35
45
  * a `node_modules/.vlt` store, otherwise undefined. Any store, not just
@@ -37,13 +47,20 @@ const storeSeg = `${sep}node_modules${sep}.vlt${sep}`;
37
47
  * store after the project is moved or copied.
38
48
  */
39
49
  export const mountTarget = (link) => {
40
- if (!lstat(link)?.isSymbolicLink())
41
- return undefined;
42
- let raw = readlinkSync(link);
43
- if (raw.startsWith('\\\\?\\'))
44
- raw = raw.slice(4);
45
- const target = resolve(dirname(link), raw);
46
- return norm(target).includes(storeSeg) ? target : undefined;
50
+ const target = linkTarget(link);
51
+ return target && norm(target).includes(storeSeg) ?
52
+ target
53
+ : undefined;
54
+ };
55
+ // the deepest workspace holding `target`
56
+ const workspaceOf = (target, workspaces) => {
57
+ const t = norm(realOr(target));
58
+ return workspaces
59
+ .filter(({ path }) => {
60
+ const p = norm(path);
61
+ return t === p || t.startsWith(p + sep);
62
+ })
63
+ .sort((a, b) => b.path.length - a.path.length)[0];
47
64
  };
48
65
  /**
49
66
  * The DepID of the last `node_modules/.vlt/<DepID>` segment of a raw
@@ -76,9 +93,11 @@ export const packageKey = (id) => {
76
93
  ]);
77
94
  };
78
95
  /**
79
- * All vlt links in the given dirs. Anything else is ignored.
96
+ * All vlt links in the given dirs: links into a `node_modules/.vlt`
97
+ * store, and links listed in the dir's `.gitignore` block (or `made`)
98
+ * into a workspace or dangling. Anything else is ignored.
80
99
  */
81
- export const readMounts = (scan) => {
100
+ export const readMounts = (scan, { workspaces = [], made, } = {}) => {
82
101
  const mounts = [];
83
102
  for (const dir of scan) {
84
103
  let entries;
@@ -88,35 +107,42 @@ export const readMounts = (scan) => {
88
107
  catch {
89
108
  continue;
90
109
  }
110
+ const listed = gitignoreNames(dir.real);
91
111
  for (const name of entries) {
92
112
  const link = join(dir.real, name);
113
+ const mount = `${dir.dir}/${name}`;
114
+ const m = { dir, name, mount, link };
93
115
  const target = mountTarget(link);
94
116
  if (target) {
95
- mounts.push({
96
- dir,
97
- name,
98
- mount: `${dir.dir}/${name}`,
99
- link,
100
- target,
101
- owner: ownerOf(target),
102
- });
117
+ mounts.push({ ...m, target, owner: ownerOf(target) });
118
+ continue;
103
119
  }
120
+ const wsTarget = (listed.has(name) || made?.has(mount)) && linkTarget(link);
121
+ if (!wsTarget)
122
+ continue;
123
+ if (!existsSync(wsTarget)) {
124
+ mounts.push({ ...m, target: wsTarget });
125
+ continue;
126
+ }
127
+ const ws = workspaceOf(wsTarget, workspaces);
128
+ if (ws)
129
+ mounts.push({ ...m, target: wsTarget, owner: ws.id });
104
130
  }
105
131
  }
106
132
  return mounts.sort((a, b) => a.mount.localeCompare(b.mount, 'en'));
107
133
  };
108
134
  /**
109
- * Link a skill at `<dir>/<skill>`. Never touches anything not managed by
110
- * vlt: returns `'conflict'` instead.
135
+ * Link a skill at `<dir>/<skill>`. `'unchanged'` if the entry already
136
+ * is the skill. Never touches anything not managed by vlt (a store
137
+ * link, or `owned`): returns `'conflict'` instead.
111
138
  */
112
- export const mountSkill = (skill, dir) => {
139
+ export const mountSkill = (skill, dir, owned = false) => {
113
140
  const link = join(dir.real, skill.skill);
114
141
  if (lstat(link)) {
115
- const target = mountTarget(link);
116
- if (!target)
117
- return 'conflict';
118
- if (samePath(target, skill.path))
142
+ if (samePath(link, skill.path))
119
143
  return 'unchanged';
144
+ if (!owned && !mountTarget(link))
145
+ return 'conflict';
120
146
  unlinkSync(link);
121
147
  }
122
148
  // junctions need no admin rights, but want an absolute target
@@ -1,3 +1,4 @@
1
+ import type { DepID } from '@vltpkg/dep-id';
1
2
  import type { PackageInfoClient } from '@vltpkg/package-info';
2
3
  import type { LoadOptions } from './actual/load.ts';
3
4
  import type { RemoveImportersDependenciesMap } from './dependencies.ts';
@@ -18,6 +19,8 @@ export type UninstallOptions = LoadOptions & {
18
19
  * none
19
20
  */
20
21
  skillsDirs?: string[];
22
+ /** store dirs of these nodes are never removed, eg the running vlt */
23
+ keepNodes?: ReadonlySet<DepID>;
21
24
  };
22
25
  /**
23
26
  * Result of {@link uninstall}.
package/dist/uninstall.js CHANGED
@@ -68,6 +68,7 @@ export const uninstall = async (options, remove) => {
68
68
  ...options,
69
69
  remove,
70
70
  graph,
71
+ modifiers,
71
72
  })
72
73
  : undefined;
73
74
  saveImportersPackageJson?.();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/graph",
3
3
  "description": "A library that helps understanding & expressing what happens on an install",
4
- "version": "1.5.0",
4
+ "version": "1.6.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,26 +13,26 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/cmd-shim": "1.5.0",
17
- "@vltpkg/dep-id": "1.5.0",
18
- "@vltpkg/dss-breadcrumb": "1.5.0",
19
- "@vltpkg/error-cause": "1.5.0",
20
- "@vltpkg/fast-split": "1.5.0",
21
- "@vltpkg/graph-run": "1.5.0",
22
- "@vltpkg/init": "1.5.0",
23
- "@vltpkg/output": "1.5.0",
24
- "@vltpkg/package-info": "1.5.0",
25
- "@vltpkg/package-json": "1.5.0",
26
- "@vltpkg/pick-manifest": "1.5.0",
27
- "@vltpkg/query": "1.5.0",
28
- "@vltpkg/rollback-remove": "1.5.0",
29
- "@vltpkg/run": "1.5.0",
30
- "@vltpkg/satisfies": "1.5.0",
31
- "@vltpkg/security-archive": "1.5.0",
32
- "@vltpkg/spec": "1.5.0",
33
- "@vltpkg/types": "1.5.0",
34
- "@vltpkg/vlt-json": "1.5.0",
35
- "@vltpkg/workspaces": "1.5.0",
16
+ "@vltpkg/cmd-shim": "1.6.1",
17
+ "@vltpkg/dep-id": "1.6.1",
18
+ "@vltpkg/dss-breadcrumb": "1.6.1",
19
+ "@vltpkg/error-cause": "1.6.1",
20
+ "@vltpkg/fast-split": "1.6.1",
21
+ "@vltpkg/graph-run": "1.6.1",
22
+ "@vltpkg/init": "1.6.1",
23
+ "@vltpkg/output": "1.6.1",
24
+ "@vltpkg/package-info": "1.6.1",
25
+ "@vltpkg/package-json": "1.6.1",
26
+ "@vltpkg/pick-manifest": "1.6.1",
27
+ "@vltpkg/query": "1.6.1",
28
+ "@vltpkg/rollback-remove": "1.6.1",
29
+ "@vltpkg/run": "1.6.1",
30
+ "@vltpkg/satisfies": "1.6.1",
31
+ "@vltpkg/security-archive": "1.6.1",
32
+ "@vltpkg/spec": "1.6.1",
33
+ "@vltpkg/types": "1.6.1",
34
+ "@vltpkg/vlt-json": "1.6.1",
35
+ "@vltpkg/workspaces": "1.6.1",
36
36
  "path-scurry": "^2.0.1",
37
37
  "promise-call-limit": "^3.0.2"
38
38
  },