@vltpkg/package-info 1.0.9 → 1.1.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.
package/dist/index.d.ts CHANGED
@@ -7,6 +7,20 @@ import type { Pool } from '@vltpkg/tar';
7
7
  import type { Integrity, Manifest, Packument } from '@vltpkg/types';
8
8
  import { Monorepo } from '@vltpkg/workspaces';
9
9
  export declare const delimiter = "~";
10
+ /**
11
+ * Accept header for packument requests. Prefers vlt's abbreviated
12
+ * packument and falls back to the full one on registries that do not
13
+ * know the type. See `PackageInfoClient.#fetchPackument`.
14
+ *
15
+ * The trailing wildcard range carries an explicit `q=0.1` so that it stays
16
+ * below `application/json`. A media range with no `q` defaults to `q=1.0`
17
+ * (RFC 9110 12.5.1), which on a registry that negotiates strictly by
18
+ * quality would let an unrelated representation — npm's corgi among them
19
+ * — outrank the full packument and drop `license`. The wildcard is kept
20
+ * only so a registry that rejects what it cannot satisfy exactly still
21
+ * has something to match.
22
+ */
23
+ export declare const PACKUMENT_ACCEPT = "application/vnd.vlt.packument-v1+json; q=1.0, application/json; q=0.8, */*; q=0.1";
10
24
  export type Resolution = {
11
25
  resolved: string;
12
26
  integrity?: Integrity;
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@ import { clone, resolve as gitResolve, revs } from '@vltpkg/git';
3
3
  import { logRequest } from '@vltpkg/output';
4
4
  import { PackageJson } from '@vltpkg/package-json';
5
5
  import { pickManifest } from '@vltpkg/pick-manifest';
6
+ // subpath import: keeps the lazy `import('@vltpkg/registry-client')`
7
+ // below from becoming an eager dependency on the whole client.
8
+ import { registryErrorMessage } from '@vltpkg/registry-client/registry-error';
6
9
  import { Spec } from '@vltpkg/spec';
7
10
  import { asPackument } from '@vltpkg/types';
8
11
  import ssri from 'ssri';
@@ -17,6 +20,20 @@ import { rename } from "./rename.js";
17
20
  const debug = debuglog('vlt');
18
21
  const xdg = new XDG('vlt');
19
22
  export const delimiter = '~';
23
+ /**
24
+ * Accept header for packument requests. Prefers vlt's abbreviated
25
+ * packument and falls back to the full one on registries that do not
26
+ * know the type. See `PackageInfoClient.#fetchPackument`.
27
+ *
28
+ * The trailing wildcard range carries an explicit `q=0.1` so that it stays
29
+ * below `application/json`. A media range with no `q` defaults to `q=1.0`
30
+ * (RFC 9110 12.5.1), which on a registry that negotiates strictly by
31
+ * quality would let an unrelated representation — npm's corgi among them
32
+ * — outrank the full packument and drop `license`. The wildcard is kept
33
+ * only so a registry that rejects what it cannot satisfy exactly still
34
+ * has something to match.
35
+ */
36
+ export const PACKUMENT_ACCEPT = 'application/vnd.vlt.packument-v1+json; q=1.0, application/json; q=0.8, */*; q=0.1';
20
37
  // the maximum duration of a manifest cache file
21
38
  const manifestCacheMaxAge = 5 * 60 * 1000;
22
39
  /**
@@ -26,6 +43,15 @@ const manifestCacheMaxAge = 5 * 60 * 1000;
26
43
  const noRegistryError = (spec) => error('No registry configured to resolve this spec. Set "registry" in ' +
27
44
  'vlt.json, pass --registry, or run `vlt login --registry=<url>`. ' +
28
45
  'See https://docs.vlt.sh/cli', { code: 'ECONFIG', spec });
46
+ /**
47
+ * A selector that can point at a different version tomorrow: a dist tag,
48
+ * or a range matching anything (`*`, empty string). Manifest results for
49
+ * these are not cached to disk, and packument requests for them force a
50
+ * revalidation of the registry client's cache entry.
51
+ *
52
+ * Takes a *final* spec (`spec.final`), same as `pickManifest` sees.
53
+ */
54
+ const isMovingSelector = (f) => !!(f.distTag || f.range?.isAny);
29
55
  export class PackageInfoClient {
30
56
  #registryClient;
31
57
  #projectRoot;
@@ -39,7 +65,9 @@ export class PackageInfoClient {
39
65
  #cachePath;
40
66
  // In-flight coalescing key is `${registry}${name}` — no representation
41
67
  // component. Safe only because every caller requests the same full
42
- // packument (see #fetchPackument).
68
+ // packument (see #fetchPackument). The one thing that does vary per
69
+ // caller is forceRevalidate, so record it and let a moving selector
70
+ // reuse a forced promise but never a non-forced one (see packument()).
43
71
  #packumentPromises = new Map();
44
72
  // unique temp file names for atomic manifest cache writes
45
73
  #manifestWriteRandom = randomBytes(6).toString('hex');
@@ -159,7 +187,7 @@ export class PackageInfoClient {
159
187
  ...(useCache === false ? { useCache } : {}),
160
188
  });
161
189
  if (response.statusCode !== 200) {
162
- throw this.#resolveError(spec, options, `Registry returned HTTP ${response.statusCode} when ` +
190
+ throw this.#resolveError(spec, options, `${registryErrorMessage(response)} when ` +
163
191
  `fetching the tarball for ${spec}. The resolved version ` +
164
192
  `may have been unpublished, or the registry may be ` +
165
193
  `misconfigured or unreachable.`, {
@@ -227,7 +255,7 @@ export class PackageInfoClient {
227
255
  case 'remote': {
228
256
  const response = await (await this.getRegistryClient()).request(r.resolved);
229
257
  if (response.statusCode !== 200) {
230
- throw this.#resolveError(spec, options, 'failed to fetch remote tarball', {
258
+ throw this.#resolveError(spec, options, `failed to fetch remote tarball: ${registryErrorMessage(response)}`, {
231
259
  url: r.resolved,
232
260
  response,
233
261
  });
@@ -333,11 +361,9 @@ export class PackageInfoClient {
333
361
  if (options.before) {
334
362
  return;
335
363
  }
336
- // if the final resolved spec is either a dist tag or something that
337
- // matches any range (such as a semver range of `*` or empty string)
338
- // then we skip caching
364
+ // a moving selector's result is variable, so don't cache it
339
365
  const f = spec.final;
340
- if (f.distTag || f.range?.isAny) {
366
+ if (isMovingSelector(f)) {
341
367
  return;
342
368
  }
343
369
  const key = this.#manifestCacheKey(f, options);
@@ -398,7 +424,7 @@ export class PackageInfoClient {
398
424
  ...(useCache === false ? { useCache } : {}),
399
425
  });
400
426
  if (response.statusCode !== 200) {
401
- throw this.#resolveError(spec, options, `Registry returned HTTP ${response.statusCode} when ` +
427
+ throw this.#resolveError(spec, options, `${registryErrorMessage(response)} when ` +
402
428
  `fetching the tarball for ${spec} at ${tarball}. The ` +
403
429
  `version may have been unpublished, or the registry may ` +
404
430
  `be misconfigured or unreachable.`, { response, url: tarball });
@@ -478,7 +504,7 @@ export class PackageInfoClient {
478
504
  }
479
505
  const response = await (await this.getRegistryClient()).request(remoteURL);
480
506
  if (response.statusCode !== 200) {
481
- throw this.#resolveError(spec, options, 'failed to fetch URL', { response, url: remoteURL });
507
+ throw this.#resolveError(spec, options, `failed to fetch URL: ${registryErrorMessage(response)}`, { response, url: remoteURL });
482
508
  }
483
509
  return response.buffer();
484
510
  }
@@ -577,7 +603,7 @@ export class PackageInfoClient {
577
603
  return await this.#tmpdir(async (dir) => {
578
604
  const response = await (await this.getRegistryClient()).request(remoteURL);
579
605
  if (response.statusCode !== 200) {
580
- throw this.#resolveError(s, options, 'failed to fetch URL', { response, url: remoteURL });
606
+ throw this.#resolveError(s, options, `failed to fetch URL: ${registryErrorMessage(response)}`, { response, url: remoteURL });
581
607
  }
582
608
  const buf = response.buffer();
583
609
  // Compute integrity for remote/git-with-tarball deps
@@ -660,52 +686,66 @@ export class PackageInfoClient {
660
686
  throw noRegistryError(spec);
661
687
  // Coalescing key has no representation component (see #fetchPackument).
662
688
  const packumentKey = `${registry}${name}`;
689
+ const forced = isMovingSelector(f);
663
690
  const inflight = this.#packumentPromises.get(packumentKey);
664
- if (inflight)
665
- return inflight;
691
+ // a moving selector must not ride along on a non-forced request:
692
+ // that one can settle to a fresh-but-stale cache hit, which is
693
+ // exactly what forceRevalidate exists to avoid. the other
694
+ // direction is fine -- a forced result is never staler.
695
+ // costs at most one extra concurrent GET for the same packument
696
+ // when both shapes are asked for at once.
697
+ if (inflight && (!forced || inflight.forced))
698
+ return inflight.promise;
666
699
  const pakuURL = new URL(name, registry);
667
700
  const promise = this.#fetchPackument(spec, options, pakuURL);
668
- this.#packumentPromises.set(packumentKey, promise);
669
- // Clean up once settled so we don't leak memory.
701
+ const record = { promise, forced };
702
+ this.#packumentPromises.set(packumentKey, record);
703
+ // Clean up once settled so we don't leak memory, unless a forced
704
+ // request has since taken the slot over.
670
705
  // Use .then/.catch instead of .finally to avoid creating
671
706
  // an unhandled rejection from the derived promise.
672
- promise.then(() => this.#packumentPromises.delete(packumentKey), () => this.#packumentPromises.delete(packumentKey));
707
+ const clear = () => {
708
+ if (this.#packumentPromises.get(packumentKey) === record) {
709
+ this.#packumentPromises.delete(packumentKey);
710
+ }
711
+ };
712
+ promise.then(clear, clear);
673
713
  return promise;
674
714
  }
675
715
  }
676
716
  }
677
717
  async #fetchPackument(spec, options, pakuURL, useCache) {
678
- // Always request the full packument (`application/json`), never the
679
- // abbreviated "corgi" form:
680
- // accept: application/vnd.npm.install-v1+json; q=1.0,
681
- // application/json; q=0.8, */*
682
- //
683
- // Corgi is smaller (~−41% packument bytes, est. 1–3s on clean installs)
684
- // but is disabled for two load-bearing reasons:
685
- //
686
- // 1. Metadata loss reaches the lockfile. The version entry returned
687
- // here is `node.manifest` and is persisted to vlt-lock.json / the
688
- // hidden lockfile. Corgi drops `license` (also `time`, `readme`,
689
- // `maintainers`, `_rev`, `scripts`), so graph queries like
690
- // `[license=MPL-2.0]` cannot match a field that was never stored.
691
- // Shipped in 1.0.0-rc.33 via #1692 (8ba2b10c); 24 false-positive
692
- // edges in the dependency-check job on #1704
693
- // (`@resvg/resvg-wasm@2.6.2` MPL-2.0 in package.json, blank in the
694
- // stored graph); isolated in #1705; reverted in #1707.
718
+ // Request vlt's abbreviated packument, falling back to the full one:
719
+ // accept: application/vnd.vlt.packument-v1+json; q=1.0,
720
+ // application/json; q=0.8, */*; q=0.1
695
721
  //
696
- // 2. The RegistryClient disk cache key is method + URL only — no
697
- // accept, no Vary — so a full-packument request can be served a
698
- // cached abbreviated body (and vice versa).
722
+ // npm's corgi (`application/vnd.npm.install-v1+json`) is never
723
+ // requested. The version entry returned here becomes `node.manifest`
724
+ // and is persisted to the hidden lockfile, and corgi drops `license`
725
+ // and `time`, so graph queries like `[license=MPL-2.0]` could not
726
+ // match a field that was never stored (shipped in 1.0.0-rc.33 via
727
+ // #1692, reverted in #1707). The vlt type guarantees both, and a
728
+ // registry that does not know it ignores it and serves the full
729
+ // packument, so the graph gets every field it relies on either way.
730
+ // The one shape difference is `scripts`, which the vlt type replaces
731
+ // with `hasInstallScript`; reify reads the extracted package.json in
732
+ // that case.
699
733
  //
700
- // To revisit: put the requested representation on both the disk-cache
701
- // key and the in-flight coalescing key, and have the SWR child
702
- // (cache-revalidate / revalidate) re-request the same representation.
734
+ // The RegistryClient disk cache key is method + URL only, so every
735
+ // packument request must use this same accept header, and the SWR
736
+ // revalidation child re-requests the representation it was given.
703
737
  const response = await (await this.getRegistryClient()).request(pakuURL, {
704
- headers: { accept: 'application/json' },
738
+ headers: { accept: PACKUMENT_ACCEPT },
705
739
  ...(useCache === false ? { useCache } : {}),
740
+ // costs a conditional GET per moving selector on an otherwise warm
741
+ // cache, install included. 304s are cheap but not free; the
742
+ // alternative is serving a dist tag that moved (#1656).
743
+ ...(isMovingSelector(spec.final) ?
744
+ { forceRevalidate: true }
745
+ : {}),
706
746
  });
707
747
  if (response.statusCode !== 200) {
708
- throw this.#resolveError(spec, options, 'failed to fetch packument', {
748
+ throw this.#resolveError(spec, options, `failed to fetch packument: ${registryErrorMessage(response)}`, {
709
749
  url: pakuURL,
710
750
  response,
711
751
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/package-info",
3
3
  "description": "Resolve and fetch package metadata and tarballs",
4
- "version": "1.0.9",
4
+ "version": "1.1.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,17 +13,17 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/error-cause": "1.0.9",
17
- "@vltpkg/git": "1.0.9",
18
- "@vltpkg/output": "1.0.9",
19
- "@vltpkg/package-json": "1.0.9",
20
- "@vltpkg/pick-manifest": "1.0.9",
21
- "@vltpkg/registry-client": "1.0.9",
22
- "@vltpkg/spec": "1.0.9",
23
- "@vltpkg/tar": "1.0.9",
24
- "@vltpkg/types": "1.0.9",
25
- "@vltpkg/workspaces": "1.0.9",
26
- "@vltpkg/xdg": "1.0.9",
16
+ "@vltpkg/error-cause": "1.1.0",
17
+ "@vltpkg/git": "1.1.0",
18
+ "@vltpkg/output": "1.1.0",
19
+ "@vltpkg/package-json": "1.1.0",
20
+ "@vltpkg/pick-manifest": "1.1.0",
21
+ "@vltpkg/registry-client": "1.1.0",
22
+ "@vltpkg/spec": "1.1.0",
23
+ "@vltpkg/tar": "1.1.0",
24
+ "@vltpkg/types": "1.1.0",
25
+ "@vltpkg/workspaces": "1.1.0",
26
+ "@vltpkg/xdg": "1.1.0",
27
27
  "ssri": "^13.0.0",
28
28
  "tar": "^7.5.2"
29
29
  },
@@ -32,8 +32,8 @@
32
32
  "@types/node": "^22.19.2",
33
33
  "@types/pacote": "^11.1.8",
34
34
  "@vltpkg/benchmark": "0.0.0",
35
- "@vltpkg/cache-unzip": "1.0.9",
36
- "@vltpkg/vlt-json": "1.0.9",
35
+ "@vltpkg/cache-unzip": "1.1.0",
36
+ "@vltpkg/vlt-json": "1.1.0",
37
37
  "eslint": "^9.39.1",
38
38
  "pacote": "^21.0.4",
39
39
  "prettier": "^3.7.4",