@vltpkg/cli-sdk 1.3.7 → 1.4.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.
@@ -56,8 +56,8 @@ declare const usageDef: {
56
56
  readonly description: "Check global store entries against their cached\n tarballs (file list, contents, exec bits) and remove\n any that differ or have no cached tarball, e.g. after\n a file in `node_modules` was edited in place.";
57
57
  };
58
58
  readonly 'prune-store': {
59
- readonly usage: "";
60
- readonly description: "Remove global store entries that no `node_modules`\n folder links to. Entries ever copied from (install\n scripts, `store-linker=copy`, a cache on another\n drive) stay: copies do not show use.";
59
+ readonly usage: "[<date>]";
60
+ readonly description: "Remove global store entries that no `node_modules`\n folder links to. Copies do not show use: an entry\n copied or cloned from (install scripts,\n `store-linker=copy`, macOS, a cache on another\n drive) stays if that last happened after\n `<date>`, or in the last 30 days\n without one.";
61
61
  };
62
62
  };
63
63
  readonly examples: {
@@ -1,7 +1,7 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
2
  import { CacheEntry, assertOk } from '@vltpkg/registry-client';
3
3
  import { Spec } from '@vltpkg/spec';
4
- import { removeStoreEntry, storeEntryCopied, storeEntryLinked, storeEntryNames, storeEntryTime, verifyStoreEntry, } from '@vltpkg/tar/store-entry';
4
+ import { removeStoreEntry, storeEntryCopied, storeEntryLastCopied, storeEntryLinked, storeEntryNames, storeEntryTime, verifyStoreEntry, } from '@vltpkg/tar/store-entry';
5
5
  import { readStoreIndex } from '@vltpkg/tar/store-index';
6
6
  import { integrityHex } from '@vltpkg/types';
7
7
  import { readFileSync } from 'node:fs';
@@ -24,6 +24,8 @@ export class CacheView extends ViewClass {
24
24
  export const views = {
25
25
  human: CacheView,
26
26
  };
27
+ /** `prune-store`: days a copied entry stays, by default */
28
+ const pruneDays = 30;
27
29
  const usageDef = {
28
30
  command: 'cache',
29
31
  usage: '<command> [flags]',
@@ -76,11 +78,14 @@ const usageDef = {
76
78
  a file in \`node_modules\` was edited in place.`,
77
79
  },
78
80
  'prune-store': {
79
- usage: '',
81
+ usage: '[<date>]',
80
82
  description: `Remove global store entries that no \`node_modules\`
81
- folder links to. Entries ever copied from (install
82
- scripts, \`store-linker=copy\`, a cache on another
83
- drive) stay: copies do not show use.`,
83
+ folder links to. Copies do not show use: an entry
84
+ copied or cloned from (install scripts,
85
+ \`store-linker=copy\`, macOS, a cache on another
86
+ drive) stays if that last happened after
87
+ \`<date>\`, or in the last ${pruneDays} days
88
+ without one.`,
84
89
  },
85
90
  },
86
91
  examples: {
@@ -335,14 +340,24 @@ const verify = async (conf, specs, view) => {
335
340
  ...(!all && { missing }),
336
341
  };
337
342
  };
338
- const pruneStore = async (conf, _, view) => {
343
+ const pruneStore = async (conf, args, view) => {
344
+ const since = args.length ?
345
+ new Date(args.join(' ')).getTime()
346
+ : Date.now() - pruneDays * 24 * 60 * 60 * 1000;
347
+ if (Number.isNaN(since)) {
348
+ throw error('Invalid date', {
349
+ code: 'EUSAGE',
350
+ found: args.join(' '),
351
+ });
352
+ }
339
353
  const { storeRoot } = conf.options;
340
354
  const names = storeEntryNames(storeRoot);
341
355
  const removed = {};
342
356
  for (const hex of names) {
343
357
  const entry = resolve(storeRoot, hex);
344
358
  const index = readStoreIndex(entry);
345
- if (storeEntryCopied(entry, index) ||
359
+ if ((storeEntryCopied(entry, index) &&
360
+ storeEntryLastCopied(entry) >= since) ||
346
361
  storeEntryLinked(entry, index)) {
347
362
  continue;
348
363
  }
@@ -44,6 +44,11 @@ export const usage = () => commandUsage({
44
44
  If the appropriate excutable cannot be determined, then
45
45
  an error will be raised.
46
46
 
47
+ If the package is a path to a local directory (eg, \`.\`
48
+ or \`../pkg\`, relative to the current directory), its
49
+ bins are run in place. Nothing is installed, and its own
50
+ \`node_modules\` is used as-is.
51
+
47
52
  At no point will \`vlt exec\` change the locally installed
48
53
  dependencies. Any installs it performs is done in vlt's XDG
49
54
  data directory.
@@ -58,6 +63,9 @@ export const usage = () => commandUsage({
58
63
  'eslint@9.24 src/file.js': {
59
64
  description: 'Run the default bin provided by eslint version 9.24',
60
65
  },
66
+ '.': {
67
+ description: 'Run the default bin of the package in the current directory',
68
+ },
61
69
  'create-react-app --call="echo $PWD"': {
62
70
  description: 'Install create-react-app and run an arbitrary command with its bins in PATH',
63
71
  },
@@ -60,13 +60,11 @@ export const command = async (conf) => {
60
60
  const { config: parsedWSConfig, source } = resolveWSConfig(conf.options.projectRoot, conf.options.packageJson);
61
61
  // what gets written back to vlt.json. The raw vlt.json value is
62
62
  // reused as-is so its existing shape is preserved. When the
63
- // definitions live in package.json instead, seed vlt.json with them
64
- // first -- otherwise adding a `workspaces` field here would shadow
65
- // every workspace package.json already declares.
63
+ // definitions live in package.json or pnpm-workspace.yaml instead,
64
+ // seed vlt.json with them first -- otherwise adding a `workspaces`
65
+ // field here would shadow every workspace already declared there.
66
66
  const workspacesConfig = load('workspaces', assertWSConfig) ??
67
- (source === 'package.json' ?
68
- Object.values(parsedWSConfig).flat()
69
- : undefined);
67
+ (source ? Object.values(parsedWSConfig).flat() : undefined);
70
68
  const results = [];
71
69
  const addToConfig = [];
72
70
  // create a new package.json file for every workspace
@@ -3,6 +3,7 @@ import { install } from '@vltpkg/graph';
3
3
  import { parseAddArgs } from "../parse-add-remove-args.js";
4
4
  import { planSpecConfigPersist } from "../persist-spec-config.js";
5
5
  import { asUnknownSpecPrefix } from "../require-registry.js";
6
+ import { addTypesDeps } from "../save-types.js";
6
7
  import { trackInstall } from "../telemetry.js";
7
8
  import { lazyView } from "../view.js";
8
9
  export const needsRegistry = true;
@@ -32,6 +33,9 @@ export const usage = () => commandUsage({
32
33
  'save-prod': {
33
34
  description: 'Save installed packages to package.json as dependencies.',
34
35
  },
36
+ 'save-types': {
37
+ description: 'Also save matching @types/* packages to devDependencies for added packages that ship no types.',
38
+ },
35
39
  'save-config': {
36
40
  description: 'Save registry and git host options given on the command line or via env (e.g. `--registries name=url`) to the project vlt.json, or the user vlt.json with --config=user.',
37
41
  },
@@ -130,6 +134,8 @@ export const command = async (conf) => {
130
134
  const scurry = conf.options.scurry;
131
135
  const { add } = parseAddArgs(conf, scurry, monorepo);
132
136
  const persist = planSpecConfigPersist(conf);
137
+ if (conf.values['save-types'])
138
+ await addTypesDeps(conf, add);
133
139
  const frozenLockfile = conf.options['frozen-lockfile'];
134
140
  const expectLockfile = conf.options['expect-lockfile'];
135
141
  const lockfileOnly = conf.options['lockfile-only'];
@@ -166,7 +166,9 @@ const explain = (entry) => {
166
166
  };
167
167
  /**
168
168
  * The commands that perform the upgrades, one `vlt install` per set of
169
- * flags so that the specs can be combined.
169
+ * flags so that the specs can be combined. Several entries can call
170
+ * for the same step, such as every workspace that shares a catalog
171
+ * entry, so each step is listed once.
170
172
  */
171
173
  const suggestActions = (result) => {
172
174
  const lines = [];
@@ -175,26 +177,28 @@ const suggestActions = (result) => {
175
177
  lines.push(`Run \`vlt update\` to pick up ${plural(updates, 'in-range update')}.`);
176
178
  }
177
179
  const installs = new Map();
180
+ const edits = new Set();
178
181
  for (const { action } of result) {
179
- if (!action?.startsWith('vlt install '))
182
+ if (!action)
180
183
  continue;
181
- const [spec = '', ...flags] = action
182
- .slice('vlt install '.length)
183
- .split(' ');
184
- const key = flags.join(' ');
185
- const specs = installs.get(key) ?? [];
186
- specs.push(spec);
187
- installs.set(key, specs);
184
+ if (action.startsWith('vlt install ')) {
185
+ const [spec = '', ...flags] = action
186
+ .slice('vlt install '.length)
187
+ .split(' ');
188
+ const key = flags.join(' ');
189
+ const specs = installs.get(key) ?? new Set();
190
+ specs.add(spec);
191
+ installs.set(key, specs);
192
+ }
193
+ else if (!action.startsWith('vlt ')) {
194
+ edits.add(`${action.charAt(0).toUpperCase()}${action.slice(1)}.`);
195
+ }
188
196
  }
189
197
  for (const [flags, specs] of installs) {
190
198
  const args = [...specs, ...(flags ? [flags] : [])].join(' ');
191
199
  lines.push(`Run \`vlt install ${args}\` to move to latest.`);
192
200
  }
193
- for (const { action } of result) {
194
- if (action && !action.startsWith('vlt ')) {
195
- lines.push(`${action.charAt(0).toUpperCase()}${action.slice(1)}.`);
196
- }
197
- }
201
+ lines.push(...edits);
198
202
  return lines;
199
203
  };
200
204
  export const views = {
@@ -336,7 +340,10 @@ const actionFor = (edge, entry, { savePrefix }) => {
336
340
  return 'vlt update';
337
341
  const newRange = `${savePrefix}${latest}`;
338
342
  if (edge.spec.type === 'catalog') {
339
- return `set the catalog entry for ${edge.name} in vlt.json to ${newRange}`;
343
+ const catalog = edge.spec.catalog ?
344
+ `"${edge.spec.catalog}" catalog`
345
+ : 'catalog';
346
+ return `set the ${catalog} entry for ${edge.name} in vlt.json to ${newRange}`;
340
347
  }
341
348
  const workspace = edge.from.mainImporter ?
342
349
  ''
@@ -9,6 +9,7 @@ import { actual } from '@vltpkg/graph';
9
9
  import { Query } from '@vltpkg/query';
10
10
  import { error } from '@vltpkg/error-cause';
11
11
  import { createHostContextsMap } from "../query-host-contexts.js";
12
+ import { sortScopeLocations } from "../sort-scope-locations.js";
12
13
  import { minimatch } from 'minimatch';
13
14
  export const needsRegistry = true;
14
15
  export const usage = () => commandUsage({
@@ -97,7 +98,7 @@ const scopeLocations = async (queryString, conf) => {
97
98
  }));
98
99
  locations.push(resolve(projectRoot, location));
99
100
  }
100
- return locations;
101
+ return sortScopeLocations(locations, options.monorepo);
101
102
  };
102
103
  export const command = async (conf) => {
103
104
  const { options, projectRoot } = conf;
@@ -9,6 +9,7 @@ import { SecurityArchive } from '@vltpkg/security-archive';
9
9
  import { views as initViews } from "./init.js";
10
10
  import { commandUsage } from "../config/usage.js";
11
11
  import { createHostContextsMap } from "../query-host-contexts.js";
12
+ import { sortScopeLocations } from "../sort-scope-locations.js";
12
13
  import { minimatch } from 'minimatch';
13
14
  export const views = {
14
15
  human: (results, _, config) => {
@@ -129,15 +130,17 @@ export const command = async (conf) => {
129
130
  const { nodes: resultNodes } = await query.search(queryString, {
130
131
  signal: new AbortController().signal,
131
132
  });
133
+ const found = [];
132
134
  for (const node of resultNodes) {
133
135
  const location = node.location;
134
136
  assert(location, error(`node ${node.id} has no location`, {
135
137
  found: node,
136
138
  }));
137
139
  if (node.id !== VIRTUAL_ROOT_ID) {
138
- locations.push(resolve(node.projectRoot, location));
140
+ found.push(resolve(node.projectRoot, location));
139
141
  }
140
142
  }
143
+ locations.push(...sortScopeLocations(found, options.monorepo));
141
144
  }
142
145
  else if (paths?.length || groups?.length || recursive) {
143
146
  for (const workspace of options.monorepo ?? []) {
@@ -10,6 +10,7 @@ import prettyBytes from 'pretty-bytes';
10
10
  import { actual } from '@vltpkg/graph';
11
11
  import { Query } from '@vltpkg/query';
12
12
  import { createHostContextsMap } from "../query-host-contexts.js";
13
+ import { sortScopeLocations } from "../sort-scope-locations.js";
13
14
  import { minimatch } from 'minimatch';
14
15
  import { resolveRegistry } from "../require-registry.js";
15
16
  import { stderr } from "../output.js";
@@ -114,13 +115,15 @@ export const command = async (conf) => {
114
115
  const { nodes } = await query.search(queryString, {
115
116
  signal: new AbortController().signal,
116
117
  });
118
+ const found = [];
117
119
  for (const node of nodes) {
118
120
  const { location } = node.toJSON();
119
121
  assert(location, error(`node ${node.id} has no location`, {
120
122
  found: node,
121
123
  }));
122
- locations.push(resolve(projectRoot, location));
124
+ found.push(resolve(projectRoot, location));
123
125
  }
126
+ locations.push(...sortScopeLocations(found, options.monorepo));
124
127
  }
125
128
  else if (paths?.length || groups?.length || recursive) {
126
129
  for (const workspace of options.monorepo ?? []) {
@@ -10,8 +10,17 @@ export const usage = () => commandUsage({
10
10
  command: 'update',
11
11
  usage: '',
12
12
  description: `Update dependencies to their latest in-range versions.
13
- Discards the lockfile and resolves dependencies from scratch.`,
13
+ Discards the lockfile and resolves dependencies from scratch
14
+ (with -w: only matching workspaces' dependencies).`,
14
15
  options: {
16
+ workspace: {
17
+ value: '<path|glob>',
18
+ description: 'Limit update to matching workspaces. Others, and deps they share, keep their locked versions.',
19
+ },
20
+ 'workspace-group': {
21
+ value: '<name>',
22
+ description: 'Limit update to workspace groups. Others, and deps they share, keep their locked versions.',
23
+ },
15
24
  'save-config': {
16
25
  description: 'Save registry and git host options given on the command line or via env (e.g. `--registries name=url`) to the project vlt.json, or the user vlt.json with --config=user.',
17
26
  },
@@ -8,6 +8,7 @@ import assert from 'node:assert';
8
8
  import { actual } from '@vltpkg/graph';
9
9
  import { Query } from '@vltpkg/query';
10
10
  import { createHostContextsMap } from "../query-host-contexts.js";
11
+ import { sortScopeLocations } from "../sort-scope-locations.js";
11
12
  import { minimatch } from 'minimatch';
12
13
  /**
13
14
  * `--no-git-tag-version` skips the whole git dance, matching npm.
@@ -223,13 +224,15 @@ export const command = async (conf) => {
223
224
  const { nodes } = await query.search(queryString, {
224
225
  signal: new AbortController().signal,
225
226
  });
227
+ const found = [];
226
228
  for (const node of nodes) {
227
229
  const location = node.location;
228
230
  assert(location, error(`node ${node.id} has no location`, {
229
231
  found: node,
230
232
  }));
231
- locations.push(resolve(node.projectRoot, location));
233
+ found.push(resolve(node.projectRoot, location));
232
234
  }
235
+ locations.push(...sortScopeLocations(found, options.monorepo));
233
236
  }
234
237
  else if (paths?.length || groups?.length || recursive) {
235
238
  for (const workspace of options.monorepo ?? []) {
@@ -22,7 +22,11 @@ export const usage = () => commandUsage({
22
22
  (e.g., \`dist-tags.latest\`, \`dependencies.lodash\`).
23
23
 
24
24
  Security data from the vlt security archive is shown when
25
- available, including scores and alerts.`,
25
+ available, including scores and alerts.
26
+
27
+ Use \`.\` (or no args) for the package in the current directory
28
+ (nearest package.json), \`.@<version>\` to pick a version.
29
+ Private packages show local package.json info.`,
26
30
  examples: {
27
31
  express: {
28
32
  description: 'View info about the latest version of express',
@@ -42,6 +46,12 @@ export const usage = () => commandUsage({
42
46
  'express dist-tags.latest': {
43
47
  description: 'Show the latest dist-tag value',
44
48
  },
49
+ '.': {
50
+ description: 'View the package in the current directory',
51
+ },
52
+ '. version': {
53
+ description: 'Show the latest published version of the current package',
54
+ },
45
55
  },
46
56
  options: {
47
57
  view: {
@@ -114,7 +124,7 @@ const formatHuman = (result) => {
114
124
  // Name and version
115
125
  const name = manifest.name ?? packument.name;
116
126
  const version = manifest.version ?? '';
117
- lines.push(`${name}@${version}`);
127
+ lines.push(version ? `${name}@${version}` : name);
118
128
  // Description
119
129
  if (manifest.description) {
120
130
  lines.push(manifest.description);
@@ -263,15 +273,30 @@ const lookupField = (result, path) => {
263
273
  // Default: look up in manifest
264
274
  return dotProp.get(result.manifest, path);
265
275
  };
266
- export const command = async (conf) => {
267
- const specArg = conf.positionals[0];
268
- if (!specArg) {
269
- throw error('view requires a package spec argument', {
276
+ const readLocalManifest = (conf) => {
277
+ const file = conf.options.packageJson.find(process.cwd());
278
+ if (!file) {
279
+ throw error('No local package.json found', {
270
280
  code: 'EUSAGE',
281
+ path: process.cwd(),
271
282
  });
272
283
  }
273
- const fieldPath = conf.positionals[1];
274
- const spec = Spec.parseArgs(specArg, conf.options);
284
+ const manifest = conf.options.packageJson.read(file);
285
+ const { name } = manifest;
286
+ if (!name) {
287
+ throw error('No package name found in package.json', {
288
+ code: 'EUSAGE',
289
+ found: file,
290
+ });
291
+ }
292
+ return { name, manifest };
293
+ };
294
+ // nothing published: no dist-tags or versions
295
+ const localView = (name, manifest) => ({
296
+ manifest,
297
+ packument: { name, 'dist-tags': {}, versions: {} },
298
+ });
299
+ const fetchView = async (spec, conf) => {
275
300
  const pic = new PackageInfoClient(conf.options);
276
301
  // Fetch the full packument (needs time, maintainers) and resolved manifest
277
302
  const [packument, resolvedManifest] = await Promise.all([
@@ -295,11 +320,34 @@ export const command = async (conf) => {
295
320
  // Security data is optional, don't fail the command
296
321
  }
297
322
  }
298
- const result = {
299
- packument,
300
- manifest,
301
- security,
302
- };
323
+ return { packument, manifest, security };
324
+ };
325
+ export const command = async (conf) => {
326
+ // `.` (or no args) is the package in the current directory
327
+ const specArg = conf.positionals[0] || '.';
328
+ const fieldPath = conf.positionals[1];
329
+ let result;
330
+ if (specArg === '.' || specArg.startsWith('.@')) {
331
+ const { name, manifest } = readLocalManifest(conf);
332
+ const rest = specArg.slice(1);
333
+ if (manifest.private) {
334
+ // never query the registry for private pkgs
335
+ if (rest) {
336
+ throw error('Cannot select a version of a private package', {
337
+ code: 'EUSAGE',
338
+ found: specArg,
339
+ });
340
+ }
341
+ result = localView(name, manifest);
342
+ }
343
+ else {
344
+ // keep name literal; rest is '' or '@<spec>'
345
+ result = await fetchView(Spec.parse(name, rest.slice(1), conf.options), conf);
346
+ }
347
+ }
348
+ else {
349
+ result = await fetchView(Spec.parseArgs(specArg, conf.options), conf);
350
+ }
303
351
  // If a field path is provided, resolve it
304
352
  if (fieldPath !== undefined) {
305
353
  result.fieldPath = fieldPath;
@@ -75,7 +75,7 @@ export declare const recordFields: readonly ["git-hosts", "registries", "git-hos
75
75
  export type RecordField = (typeof recordFields)[number];
76
76
  export declare const isRecordField: (s: string) => s is RecordField;
77
77
  /** Valid `store-linker` values. */
78
- export declare const storeLinkers: readonly ["auto", "hardlink", "copy", "unpack"];
78
+ export declare const storeLinkers: readonly ["auto", "hardlink", "clone", "copy", "unpack"];
79
79
  export declare const definition: import("jackspeak").Jack<{
80
80
  color: {
81
81
  short: string;
@@ -139,7 +139,7 @@ export declare const definition: import("jackspeak").Jack<{
139
139
  'store-linker': {
140
140
  hint: string;
141
141
  description: string;
142
- validOptions: readonly ["auto", "hardlink", "copy", "unpack"];
142
+ validOptions: readonly ["auto", "hardlink", "clone", "copy", "unpack"];
143
143
  default: string;
144
144
  };
145
145
  tag: {
@@ -365,6 +365,9 @@ export declare const definition: import("jackspeak").Jack<{
365
365
  short: string;
366
366
  description: string;
367
367
  };
368
+ 'save-types': {
369
+ description: string;
370
+ };
368
371
  'save-config': {
369
372
  description: string;
370
373
  };
@@ -374,6 +377,7 @@ export declare const definition: import("jackspeak").Jack<{
374
377
  'save-optional': import("jackspeak").ConfigOption<"boolean", false, undefined>;
375
378
  'save-peer': import("jackspeak").ConfigOption<"boolean", false, undefined>;
376
379
  'save-prod': import("jackspeak").ConfigOption<"boolean", false, undefined>;
380
+ 'save-types': import("jackspeak").ConfigOption<"boolean", false, undefined>;
377
381
  'save-config': import("jackspeak").ConfigOption<"boolean", false, undefined>;
378
382
  } & {
379
383
  'save-prefix': {
@@ -114,6 +114,7 @@ export const isRecordField = (s) => recordFields.includes(s);
114
114
  export const storeLinkers = [
115
115
  'auto',
116
116
  'hardlink',
117
+ 'clone',
117
118
  'copy',
118
119
  'unpack',
119
120
  ];
@@ -226,8 +227,11 @@ export const definition = j
226
227
 
227
228
  There is **no** built-in \`npm\` registry URL; the \`npm\`
228
229
  alias must be configured (e.g. via \`vlt setup\`) before it
229
- can be used. The \`gh\` and \`jsr\` aliases have built-in
230
- defaults that can be overridden here.
230
+ can be used. The \`gh\` alias has a built-in default
231
+ that can be overridden here. A name used by
232
+ \`--jsr-registries\` or \`--git-hosts\` (e.g. \`jsr\`,
233
+ \`github\`), or a built-in protocol (e.g. \`file\`), is
234
+ an error.
231
235
 
232
236
  Aliases given on the command line or in
233
237
  \`VLT_REGISTRIES\` (newline-delimited) merge with the
@@ -317,28 +321,35 @@ export const definition = j
317
321
  default: cacheDir,
318
322
  },
319
323
  'store-linker': {
320
- hint: 'auto | hardlink | copy | unpack',
324
+ hint: 'auto | hardlink | clone | copy | unpack',
321
325
  description: `How packages are placed in \`node_modules\`. Every
322
326
  linker but \`unpack\` fills a global store under
323
327
  \`cache\` in the background, then places packages
324
328
  from it.
325
329
 
326
330
  - auto: Default. On Linux, hardlink from the global
327
- store, copying any file that cannot be linked.
328
- Elsewhere, same as \`unpack\`: a hardlink is the
329
- slowest way to place a file on APFS, so macOS and
330
- Windows unpack by default.
331
+ store, copying any file that cannot be linked. On
332
+ macOS, clone from it, on a Node with \`node:ffi\`
333
+ (26.1 and later, and \`--allow-ffi\` under
334
+ \`--permission\`); a hardlink is the slowest way
335
+ to place a file on APFS, so macOS without it, and
336
+ Windows, unpack by default.
331
337
  - hardlink: Hardlink from the global store on every
332
338
  platform.
339
+ - clone: Clone each package directory from the
340
+ global store copy-on-write (\`clonefile\`, macOS
341
+ on APFS). Files share storage with the store
342
+ until edited, so editing in place is safe.
343
+ Unpacks where cloning fails.
333
344
  - copy: Copy from the global store. Use this when
334
345
  editing files in \`node_modules\` in place, since
335
346
  a hardlink shares its content with every project.
336
347
  - unpack: Unpack each package tarball, skipping the
337
348
  global store.
338
349
 
339
- A package is copied right before its install
340
- scripts run, so they don't change its files in the
341
- store.
350
+ A hardlinked package is copied right before its
351
+ install scripts run, so they don't change its files
352
+ in the store; a cloned one needs no copy.
342
353
  An invalid \`VLT_STORE_LINKER\` warns and uses
343
354
  \`unpack\`.`,
344
355
  validOptions: storeLinkers,
@@ -702,6 +713,11 @@ export const definition = j
702
713
  devDependencies or optionalDependencies, but you want to
703
714
  move it to be a non-optional production dependency.`,
704
715
  },
716
+ 'save-types': {
717
+ description: `Also save the matching \`@types/*\` package as a
718
+ devDependency for each added registry package that
719
+ ships no TypeScript types of its own.`,
720
+ },
705
721
  'save-config': {
706
722
  description: `Save the registry and git host options given on the
707
723
  command line or via \`VLT_*\` env (\`--registry\`,
@@ -85,6 +85,8 @@ export type ConfigOptions = ConfigOptionsNoExtras & Pick<SpecOptions, 'catalog'
85
85
  storeRoot: string;
86
86
  monorepo?: Monorepo;
87
87
  packageInfo: PackageInfoClient;
88
+ /** aborted when the command fails */
89
+ signal: AbortSignal;
88
90
  };
89
91
  /**
90
92
  * The base config definition set as a type
@@ -125,6 +127,8 @@ export declare class Config {
125
127
  * to recalculate the options.
126
128
  */
127
129
  resetOptions(projectRoot?: string): void;
130
+ /** Abort requests still in flight */
131
+ abort(): void;
128
132
  /**
129
133
  * positional arguments to the vlt process
130
134
  */
@@ -24,6 +24,7 @@ import { error } from '@vltpkg/error-cause';
24
24
  import { PackageInfoClient } from '@vltpkg/package-info';
25
25
  import { PackageJson } from '@vltpkg/package-json';
26
26
  import { storeRoot } from '@vltpkg/registry-client/store-root';
27
+ import { cloneAvailable } from '@vltpkg/tar/clonefile';
27
28
  import { resetCaches } from '@vltpkg/dep-id';
28
29
  import { getOptions } from '@vltpkg/spec';
29
30
  import { assertRecordStringString, assertRecordStringT, isObject, isRecordStringString, } from '@vltpkg/types';
@@ -35,6 +36,7 @@ import { PathScurry } from 'path-scurry';
35
36
  import { commands, definition, defaultValues, getCommand, isRecordField, recordFields, storeLinkers, } from "./definition.js";
36
37
  import { merge } from "./merge.js";
37
38
  import { cloneLayer, mergeLayers } from "./merge-layers.js";
39
+ import { assertSpecAliases } from "./spec-aliases.js";
38
40
  export { commands, definition, isRecordField, recordFields, };
39
41
  export const kCustomInspect = Symbol.for('nodejs.util.inspect.custom');
40
42
  // turn a set of pairs into a Record object.
@@ -172,6 +174,7 @@ export class Config {
172
174
  const asRecords = pairsToRecords(this.parse().values);
173
175
  const extras = {
174
176
  projectRoot: this.projectRoot,
177
+ signal: this.#abort.signal,
175
178
  storeRoot: storeRoot(asRecords.cache),
176
179
  scurry,
177
180
  packageJson,
@@ -197,7 +200,8 @@ export class Config {
197
200
  return Object.fromEntries(Object.entries(options).filter(([k]) => k !== 'monorepo' &&
198
201
  k !== 'scurry' &&
199
202
  k !== 'packageJson' &&
200
- k !== 'packageInfo'));
203
+ k !== 'packageInfo' &&
204
+ k !== 'signal'));
201
205
  },
202
206
  });
203
207
  return this.#options;
@@ -213,6 +217,11 @@ export class Config {
213
217
  }
214
218
  // memoized options() getter value
215
219
  #options;
220
+ #abort = new AbortController();
221
+ /** Abort requests still in flight */
222
+ abort() {
223
+ this.#abort.abort();
224
+ }
216
225
  /**
217
226
  * positional arguments to the vlt process
218
227
  */
@@ -277,13 +286,16 @@ export class Config {
277
286
  // ok, applied cmd-specific defaults, do rest of the parse
278
287
  this.jack.applyDefaults(p);
279
288
  // `auto` only hardlinks from the global store on linux: on APFS a
280
- // hardlink is the slowest way to place a file, so darwin and win32
281
- // unpack by default. the env layer is rewritten too, so `explicit`
282
- // records the resolved value.
289
+ // hardlink is the slowest way to place a file, so darwin clones
290
+ // instead, when its node can load `node:ffi` to call clonefile(2)
291
+ // with, and win32 (and darwin without it) unpack by default. the
292
+ // env layer is rewritten too, so `explicit` records the resolved
293
+ // value.
283
294
  if (p.values['store-linker'] === 'auto' &&
284
295
  process.platform !== 'linux') {
285
- p.values['store-linker'] = 'unpack';
286
- envValues['store-linker'] = 'unpack';
296
+ const linker = cloneAvailable() ? 'clone' : 'unpack';
297
+ p.values['store-linker'] = linker;
298
+ envValues['store-linker'] = linker;
287
299
  }
288
300
  // what was set on the cli or env for this run. record fields hold
289
301
  // only those pairs, env then cli, so later ones win per key.
@@ -317,6 +329,10 @@ export class Config {
317
329
  const base = (k in cmdPairs ? cmdPairs[k] : fileValues[k]) ?? [];
318
330
  values[k] = dedupePairs([...base, ...pairs]);
319
331
  }
332
+ // skipped for `vlt config`, so a bad alias can be removed
333
+ if (cmdOrFallback !== 'config') {
334
+ assertSpecAliases(pairsToRecords(p.values));
335
+ }
320
336
  this.jack.writeEnv(p);
321
337
  process.env[kParentEnv] = JSON.stringify({
322
338
  explicit,
@@ -0,0 +1,22 @@
1
+ declare const builtins: {
2
+ registries: {
3
+ gh: string;
4
+ };
5
+ 'jsr-registries': {
6
+ jsr: string;
7
+ };
8
+ 'git-hosts': {
9
+ github: string;
10
+ bitbucket: string;
11
+ gitlab: string;
12
+ gist: string;
13
+ };
14
+ };
15
+ type SpecAliasField = keyof typeof builtins;
16
+ /**
17
+ * Throw `ECONFIG` if a spec prefix alias is in more than one of
18
+ * `registries` / `jsr-registries` / `git-hosts` (built-ins included),
19
+ * or is a built-in protocol. Overriding a built-in in its own field is ok.
20
+ */
21
+ export declare const assertSpecAliases: (values: Partial<Record<SpecAliasField, Record<string, string>>>) => void;
22
+ export {};
@@ -0,0 +1,47 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { builtinProtocols, defaultGitHosts, defaultJsrRegistries, defaultRegistries, } from '@vltpkg/spec';
3
+ const builtins = {
4
+ registries: defaultRegistries,
5
+ 'jsr-registries': defaultJsrRegistries,
6
+ 'git-hosts': defaultGitHosts,
7
+ };
8
+ const fields = Object.keys(builtins);
9
+ /**
10
+ * Throw `ECONFIG` if a spec prefix alias is in more than one of
11
+ * `registries` / `jsr-registries` / `git-hosts` (built-ins included),
12
+ * or is a built-in protocol. Overriding a built-in in its own field is ok.
13
+ */
14
+ export const assertSpecAliases = (values) => {
15
+ const owners = new Map();
16
+ for (const field of fields) {
17
+ for (const key of Object.keys(builtins[field])) {
18
+ if (values[field]?.[key] === undefined) {
19
+ owners.set(key, { field, builtin: true });
20
+ }
21
+ }
22
+ }
23
+ for (const field of fields) {
24
+ for (const key of Object.keys(values[field] ?? {})) {
25
+ const name = `${field}.${key}`;
26
+ if (builtinProtocols.has(key)) {
27
+ throw error(`${name} shadows the built-in \`${key}:\` protocol`, {
28
+ code: 'ECONFIG',
29
+ name,
30
+ found: key,
31
+ wanted: 'a name that is not a built-in protocol',
32
+ });
33
+ }
34
+ const owner = owners.get(key);
35
+ if (owner) {
36
+ const where = `${owner.field}.${key}${owner.builtin ? ' (built-in)' : ''}`;
37
+ throw error(`\`${key}:\` spec prefix defined in both ${where} and ${name}`, {
38
+ code: 'ECONFIG',
39
+ name,
40
+ found: key,
41
+ wanted: `a ${field} name not used by ${owner.field}`,
42
+ });
43
+ }
44
+ owners.set(key, { field, builtin: false });
45
+ }
46
+ }
47
+ };
@@ -14,6 +14,7 @@ import { stderr, stdout, styleTextStdout } from "./output.js";
14
14
  import assert from 'node:assert';
15
15
  import { resolve } from 'node:path';
16
16
  import { createHostContextsMap } from "./query-host-contexts.js";
17
+ import { sortScopeLocations } from "./sort-scope-locations.js";
17
18
  const isScriptSet = (o) => {
18
19
  if (!o || typeof o !== 'object')
19
20
  return false;
@@ -98,11 +99,13 @@ export class ExecCommand {
98
99
  this.#defaultIgnoreMissing = true;
99
100
  const mainManifest = conf.options.packageJson.maybeRead(this.projectRoot);
100
101
  let graph;
102
+ let monorepo;
101
103
  if (mainManifest) {
104
+ monorepo = Monorepo.load(this.projectRoot);
102
105
  graph = actual.load({
103
106
  ...conf.options,
104
107
  mainManifest,
105
- monorepo: Monorepo.load(this.projectRoot),
108
+ monorepo,
106
109
  loadManifests: false,
107
110
  });
108
111
  }
@@ -120,14 +123,15 @@ export class ExecCommand {
120
123
  const { nodes } = await query.search(queryString, {
121
124
  signal: new AbortController().signal,
122
125
  });
123
- this.#nodes = [];
126
+ const locations = [];
124
127
  for (const node of nodes) {
125
128
  const { location } = node.toJSON();
126
129
  assert(location, error(`node ${node.id} has no location`, {
127
130
  found: node,
128
131
  }));
129
- this.#nodes.push(location);
132
+ locations.push(location);
130
133
  }
134
+ this.#nodes = sortScopeLocations(locations, monorepo);
131
135
  }
132
136
  else if (paths?.length || groups?.length || recursive) {
133
137
  this.#defaultIgnoreMissing = true;
@@ -163,8 +167,9 @@ export class ExecCommand {
163
167
  }
164
168
  // run across workspaces
165
169
  let failed = false;
166
- const runInDir = async (cwd, label) => {
167
- const result = await this.bg(this.bgArg(cwd)).catch((er) => {
170
+ const runInDir = async (cwd, label, signal) => {
171
+ const arg = { ...this.bgArg(cwd), signal };
172
+ const result = await this.bg(arg).catch((er) => {
168
173
  if (isErrorWithCause(er) && isRunResult(er.cause)) {
169
174
  this.printResult(label, er.cause);
170
175
  }
@@ -189,7 +194,8 @@ export class ExecCommand {
189
194
  }
190
195
  }
191
196
  else if (this.#monorepo) {
192
- const wsResultMap = await this.#monorepo.run(ws => runInDir(ws.fullpath, ws.path));
197
+ // aborted on the first failure, killing the rest
198
+ const wsResultMap = await this.#monorepo.run((ws, signal) => runInDir(ws.fullpath, ws.path, signal));
193
199
  for (const [ws, result] of wsResultMap) {
194
200
  if (result)
195
201
  resultMap.set(ws.path, result);
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { format } from 'node:util';
2
- import { asRootError } from '@vltpkg/output/error';
2
+ import { asRootError, findRootError } from '@vltpkg/output/error';
3
3
  import { loadPackageJson } from 'package-json-from-dist';
4
4
  import { getSortedCliOptions, getSortedKeys, } from "./config/definition.js";
5
5
  import { Config } from "./config/index.js";
@@ -12,13 +12,14 @@ const loadVlt = async (cwd, argv) => {
12
12
  return await Config.load(cwd, argv);
13
13
  }
14
14
  catch (e) {
15
- const err = asRootError(e, { code: 'JACKSPEAK' });
15
+ const code = findRootError(e, { code: 'ECONFIG' }) ? 'ECONFIG' : 'JACKSPEAK';
16
+ const err = asRootError(e, { code });
16
17
  const { found, path, wanted, name } = err.cause;
17
18
  const isConfigFile = typeof path === 'string';
18
- const msg = isConfigFile ?
19
- `Problem in Config File ${path}`
20
- : 'Invalid Option Flag';
21
- const validOptions = wanted ? undefined
19
+ const msg = isConfigFile ? `Problem in Config File ${path}`
20
+ : code === 'ECONFIG' ? 'Config Error'
21
+ : 'Invalid Option Flag';
22
+ const validOptions = wanted || code === 'ECONFIG' ? undefined
22
23
  : isConfigFile ? getSortedKeys()
23
24
  : getSortedCliOptions();
24
25
  stderr(msg);
@@ -34,7 +35,10 @@ const loadVlt = async (cwd, argv) => {
34
35
  stderr(indent('Valid Options:'));
35
36
  stderr(indent(validOptions.join('\n'), 4));
36
37
  }
37
- stderr(indent(`Run 'vlt help' for more information about available options.`));
38
+ // `vlt help` would fail the same way
39
+ if (code !== 'ECONFIG') {
40
+ stderr(indent(`Run 'vlt help' for more information about available options.`));
41
+ }
38
42
  return flushAndExit(process.exitCode || 1);
39
43
  }
40
44
  };
@@ -57,7 +61,7 @@ const run = async () => {
57
61
  if (vlt.command !== 'init' &&
58
62
  (vlt.get('workspace') || vlt.get('workspace-group')) &&
59
63
  ![...(monorepo?.values() ?? [])].length) {
60
- stderr(`Error: No matching workspaces found. Make sure the vlt.json config, or the "workspaces" field of the root package.json, contains the correct workspaces.`);
64
+ stderr(`Error: No matching workspaces found. Make sure the vlt.json config, the "workspaces" field of the root package.json, or the "packages" list of pnpm-workspace.yaml contains the correct workspaces.`);
61
65
  if (vlt.get('workspace')) {
62
66
  stderr(indent(`Workspace: ${format(vlt.get('workspace'))}`));
63
67
  }
package/dist/output.js CHANGED
@@ -227,6 +227,7 @@ export const outputCommand = async (cliCommand, conf, { start, vltVersion } = {
227
227
  ...formatOptions,
228
228
  colors: stderrColor,
229
229
  });
230
- await flushAndExit(process.exitCode);
230
+ // stop pending requests, so the process can exit on its own
231
+ conf.abort();
231
232
  }
232
233
  };
@@ -2,7 +2,8 @@ import type { GetAuthHeader } from '@vltpkg/query';
2
2
  import type { LoadedConfig } from './config/index.ts';
3
3
  /**
4
4
  * Returns a {@link GetAuthHeader} implementation that looks up
5
- * authorization tokens from the local keychain / env, used by
5
+ * authorization tokens from the local keychain / env (incl.
6
+ * `VLT_TOKEN_<key>` for configured registries), used by
6
7
  * network-reliant query selectors, e.g: `:outdated()`.
7
8
  */
8
9
  export declare const createGetAuthHeader: (conf: LoadedConfig) => GetAuthHeader;
@@ -1,7 +1,11 @@
1
- import { getTokenByURL } from '@vltpkg/registry-client';
1
+ import { getTokenByURL, registryKeys } from '@vltpkg/registry-client';
2
2
  /**
3
3
  * Returns a {@link GetAuthHeader} implementation that looks up
4
- * authorization tokens from the local keychain / env, used by
4
+ * authorization tokens from the local keychain / env (incl.
5
+ * `VLT_TOKEN_<key>` for configured registries), used by
5
6
  * network-reliant query selectors, e.g: `:outdated()`.
6
7
  */
7
- export const createGetAuthHeader = (conf) => async (url) => getTokenByURL(url, conf.options.identity);
8
+ export const createGetAuthHeader = (conf) => {
9
+ const keys = registryKeys(conf.options);
10
+ return async (url) => getTokenByURL(url, conf.options.identity, keys);
11
+ };
@@ -0,0 +1,20 @@
1
+ import type { AddImportersDependenciesMap } from '@vltpkg/graph';
2
+ import type { Manifest } from '@vltpkg/types';
3
+ import type { LoadedConfig } from './config/index.ts';
4
+ type TypedManifest = Pick<Manifest, 'exports'> & {
5
+ types?: unknown;
6
+ typings?: unknown;
7
+ typesVersions?: unknown;
8
+ };
9
+ /** `foo` → `@types/foo`, `@scope/foo` → `@types/scope__foo` */
10
+ export declare const typesName: (name: string) => string;
11
+ /** Does the manifest declare its own TypeScript types? */
12
+ export declare const hasOwnTypes: (m: TypedManifest) => boolean;
13
+ /**
14
+ * For each added registry package that ships no types, add the newest
15
+ * non-deprecated `@types/*` package in the same major (major.minor for
16
+ * 0.x), else its older-major `latest`, to that importer's
17
+ * devDependencies.
18
+ */
19
+ export declare const addTypesDeps: (conf: LoadedConfig, add: AddImportersDependenciesMap) => Promise<void>;
20
+ export {};
@@ -0,0 +1,105 @@
1
+ import { splitDepID } from '@vltpkg/dep-id';
2
+ import { addKey, asDependency } from '@vltpkg/graph';
3
+ import { parse } from '@vltpkg/semver';
4
+ import { Spec } from '@vltpkg/spec';
5
+ import { isErrorWithCause, isObject, longDependencyTypes, } from '@vltpkg/types';
6
+ import { resolve } from 'node:path';
7
+ /** `foo` → `@types/foo`, `@scope/foo` → `@types/scope__foo` */
8
+ export const typesName = (name) => `@types/${name.startsWith('@') ? name.slice(1).replace('/', '__') : name}`;
9
+ const exportsHaveTypes = (e) => !!e &&
10
+ typeof e === 'object' &&
11
+ Object.entries(e).some(([k, v]) => k === 'types' || k.startsWith('types@') || exportsHaveTypes(v));
12
+ // root entry only: typed subpaths don't type `import 'pkg'`
13
+ const rootExport = (e) => isObject(e) && Object.keys(e).some(k => k.startsWith('.')) ?
14
+ e['.']
15
+ : e;
16
+ /** Does the manifest declare its own TypeScript types? */
17
+ export const hasOwnTypes = (m) => !!(m.types || m.typings || m.typesVersions) ||
18
+ exportsHaveTypes(rootExport(m.exports));
19
+ const isResolveError = (er) => isErrorWithCause(er) &&
20
+ isObject(er.cause) &&
21
+ er.cause.code === 'ERESOLVE';
22
+ /**
23
+ * For each added registry package that ships no types, add the newest
24
+ * non-deprecated `@types/*` package in the same major (major.minor for
25
+ * 0.x), else its older-major `latest`, to that importer's
26
+ * devDependencies.
27
+ */
28
+ export const addTypesDeps = async (conf, add) => {
29
+ const { options } = conf;
30
+ const { packageInfo, packageJson, projectRoot } = options;
31
+ const exact = conf.values['save-exact'];
32
+ const prefix = conf.values['save-prefix'];
33
+ const lookups = new Map();
34
+ const lookup = async (spec, name) => {
35
+ const types = (range) => packageInfo.manifest(Spec.parse(name, range, options));
36
+ try {
37
+ const m = await packageInfo.manifest(spec);
38
+ const { version = '' } = m;
39
+ const v = parse(version);
40
+ if (!v || hasOwnTypes(m))
41
+ return;
42
+ let tm;
43
+ try {
44
+ // @types track lib major (major.minor for 0.x); x-range, `^0`
45
+ // would only match 0.0.0
46
+ tm = await types(v.major ? String(v.major) : `0.${v.minor}`);
47
+ }
48
+ catch (er) {
49
+ if (!isResolveError(er))
50
+ throw er;
51
+ // @types often lag lib majors: take `latest` if older
52
+ tm = await types('latest');
53
+ if (!parse(tm.version ?? '')?.lessThan(v))
54
+ return;
55
+ }
56
+ if (!tm.version || tm.deprecated)
57
+ return;
58
+ // abbreviated packuments omit types fields: recheck on full one
59
+ const { versions } = await packageInfo.packument(spec, {
60
+ full: true,
61
+ });
62
+ if (hasOwnTypes({ ...versions[version] }))
63
+ return;
64
+ return Spec.parse(name, exact ? tm.version : `${prefix}${tm.version}`, options);
65
+ }
66
+ catch (er) {
67
+ if (isResolveError(er))
68
+ return;
69
+ throw er;
70
+ }
71
+ };
72
+ await Promise.all([...add].map(async ([importer, deps]) => {
73
+ const mani = packageJson.maybeRead(resolve(projectRoot, splitDepID(importer)[1])) ?? {};
74
+ const found = await Promise.all([...deps.values()].map(async ({ spec }) => {
75
+ const { final } = spec;
76
+ const name = typesName(spec.name);
77
+ if (final.type !== 'registry' ||
78
+ final.name !== spec.name ||
79
+ spec.name.startsWith('@types/') ||
80
+ deps.has(name) ||
81
+ [...longDependencyTypes].some(t => mani[t]?.[name] !== undefined) ||
82
+ // other registry: its @types/<name> is a different pkg
83
+ final.registry !==
84
+ Spec.parse(name, '', options).final.registry) {
85
+ return;
86
+ }
87
+ const key = String(spec);
88
+ let p = lookups.get(key);
89
+ if (!p)
90
+ lookups.set(key, (p = lookup(spec, name)));
91
+ return p;
92
+ }));
93
+ const types = found.filter(s => s !== undefined);
94
+ if (!types.length)
95
+ return;
96
+ // fresh Map: importers may share one deps Map
97
+ add.set(importer, new Map([
98
+ ...deps,
99
+ ...types.map(spec => [
100
+ addKey(spec),
101
+ asDependency({ spec, type: 'dev' }),
102
+ ]),
103
+ ]));
104
+ }));
105
+ };
@@ -0,0 +1,11 @@
1
+ import type { Monorepo } from '@vltpkg/workspaces';
2
+ /**
3
+ * Reorder `--scope` target locations so each workspace follows the
4
+ * workspaces it depends on (directly or via unmatched workspaces),
5
+ * matching --workspace/--recursive. Ties broken by query order (a dep
6
+ * may move ahead of unrelated matches); cycles broken
7
+ * deterministically. Non-workspace
8
+ * locations (root, installed deps) keep their index. Accepts absolute
9
+ * or projectRoot-relative locations; returns the original strings.
10
+ */
11
+ export declare const sortScopeLocations: (locations: string[], monorepo?: Monorepo) => string[];
@@ -0,0 +1,57 @@
1
+ import { resolve } from 'node:path';
2
+ import { graphRunSync } from '@vltpkg/graph-run';
3
+ /**
4
+ * Reorder `--scope` target locations so each workspace follows the
5
+ * workspaces it depends on (directly or via unmatched workspaces),
6
+ * matching --workspace/--recursive. Ties broken by query order (a dep
7
+ * may move ahead of unrelated matches); cycles broken
8
+ * deterministically. Non-workspace
9
+ * locations (root, installed deps) keep their index. Accepts absolute
10
+ * or projectRoot-relative locations; returns the original strings.
11
+ */
12
+ export const sortScopeLocations = (locations, monorepo) => {
13
+ if (!monorepo || locations.length < 2)
14
+ return locations;
15
+ // resolve first: workspaces are also keyed by name and rel path
16
+ const wsAt = (loc) => monorepo.get(resolve(monorepo.projectRoot, loc));
17
+ const slots = locations.map(loc => [loc, wsAt(loc)]);
18
+ const locOf = new Map();
19
+ for (const [loc, ws] of slots) {
20
+ if (ws && !locOf.has(ws))
21
+ locOf.set(ws, loc);
22
+ }
23
+ const [first, ...rest] = locOf.keys();
24
+ if (!first || !rest.length)
25
+ return locations;
26
+ // matched deps, walking through unmatched workspaces
27
+ const getDeps = (ws) => {
28
+ const found = [];
29
+ const seen = new Set([ws]);
30
+ const queue = monorepo.getDeps(ws);
31
+ for (const dep of queue) {
32
+ if (seen.has(dep))
33
+ continue;
34
+ seen.add(dep);
35
+ if (locOf.has(dep))
36
+ found.push(dep);
37
+ else
38
+ queue.push(...monorepo.getDeps(dep));
39
+ }
40
+ return found;
41
+ };
42
+ // DFS post-order: deps first, ties in query order
43
+ const sorted = graphRunSync({
44
+ graph: [first, ...rest],
45
+ getDeps,
46
+ visit: () => { },
47
+ }).keys();
48
+ const placed = new Set();
49
+ return slots.map(([loc, ws]) => {
50
+ if (!ws || placed.has(ws))
51
+ return loc;
52
+ placed.add(ws);
53
+ const next = sorted.next().value;
54
+ /* c8 ignore next - same count as placed slots */
55
+ return (next && locOf.get(next)) ?? loc;
56
+ });
57
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/cli-sdk",
3
3
  "description": "The source for the vlt CLI",
4
- "version": "1.3.7",
4
+ "version": "1.4.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -14,33 +14,33 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@resvg/resvg-wasm": "^2.6.2",
17
- "@vltpkg/config": "1.3.7",
18
- "@vltpkg/dep-id": "1.3.7",
19
- "@vltpkg/dot-prop": "1.3.7",
20
- "@vltpkg/error-cause": "1.3.7",
21
- "@vltpkg/git": "1.3.7",
22
- "@vltpkg/graph": "1.3.7",
23
- "@vltpkg/graph-run": "1.3.7",
24
- "@vltpkg/init": "1.3.7",
25
- "@vltpkg/output": "1.3.7",
26
- "@vltpkg/package-info": "1.3.7",
27
- "@vltpkg/package-json": "1.3.7",
28
- "@vltpkg/pick-manifest": "1.3.7",
29
- "@vltpkg/promise-spawn": "1.3.7",
30
- "@vltpkg/query": "1.3.7",
31
- "@vltpkg/registry-client": "1.3.7",
32
- "@vltpkg/rollback-remove": "1.3.7",
33
- "@vltpkg/run": "1.3.7",
34
- "@vltpkg/security-archive": "1.3.7",
35
- "@vltpkg/semver": "1.3.7",
36
- "@vltpkg/spec": "1.3.7",
37
- "@vltpkg/tar": "1.3.7",
38
- "@vltpkg/types": "1.3.7",
39
- "@vltpkg/url-open": "1.3.7",
40
- "@vltpkg/vlt-json": "1.3.7",
41
- "@vltpkg/vlx": "1.3.7",
42
- "@vltpkg/workspaces": "1.3.7",
43
- "@vltpkg/xdg": "1.3.7",
17
+ "@vltpkg/config": "1.4.0",
18
+ "@vltpkg/dep-id": "1.4.0",
19
+ "@vltpkg/dot-prop": "1.4.0",
20
+ "@vltpkg/error-cause": "1.4.0",
21
+ "@vltpkg/git": "1.4.0",
22
+ "@vltpkg/graph": "1.4.0",
23
+ "@vltpkg/graph-run": "1.4.0",
24
+ "@vltpkg/init": "1.4.0",
25
+ "@vltpkg/output": "1.4.0",
26
+ "@vltpkg/package-info": "1.4.0",
27
+ "@vltpkg/package-json": "1.4.0",
28
+ "@vltpkg/pick-manifest": "1.4.0",
29
+ "@vltpkg/promise-spawn": "1.4.0",
30
+ "@vltpkg/query": "1.4.0",
31
+ "@vltpkg/registry-client": "1.4.0",
32
+ "@vltpkg/rollback-remove": "1.4.0",
33
+ "@vltpkg/run": "1.4.0",
34
+ "@vltpkg/security-archive": "1.4.0",
35
+ "@vltpkg/semver": "1.4.0",
36
+ "@vltpkg/spec": "1.4.0",
37
+ "@vltpkg/tar": "1.4.0",
38
+ "@vltpkg/types": "1.4.0",
39
+ "@vltpkg/url-open": "1.4.0",
40
+ "@vltpkg/vlt-json": "1.4.0",
41
+ "@vltpkg/vlx": "1.4.0",
42
+ "@vltpkg/workspaces": "1.4.0",
43
+ "@vltpkg/xdg": "1.4.0",
44
44
  "ansi-to-pre": "^1.0.6",
45
45
  "beautiful-mermaid": "^1.1.3",
46
46
  "chalk": "^5.6.2",