@vltpkg/package-info 1.1.0 → 1.2.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/README.md +23 -0
- package/dist/capabilities.d.ts +32 -0
- package/dist/capabilities.js +93 -0
- package/dist/index.d.ts +54 -2
- package/dist/index.js +239 -37
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -36,3 +36,26 @@ const tarballBuffer = await tarball('foo@1.x')
|
|
|
36
36
|
|
|
37
37
|
const { resolved, integrity } = await resolve('bar@latest')
|
|
38
38
|
```
|
|
39
|
+
|
|
40
|
+
## Global store
|
|
41
|
+
|
|
42
|
+
With the `store-linker` option set to `auto`, `hardlink` or `copy`,
|
|
43
|
+
`extract()` places a registry package from its global store entry
|
|
44
|
+
(`<storeRoot>/<integrity hex>`, `storeRoot` defaults to
|
|
45
|
+
`<cache>/store/v1`) when there is one. On a miss it unpacks the cached
|
|
46
|
+
tarball as usual and queues it for the background child, so the next
|
|
47
|
+
install finds it in the global store. Missing or unknown
|
|
48
|
+
`store-linker` means `unpack`. The vlt CLI defaults to `auto`, which
|
|
49
|
+
it resolves to `unpack` on platforms other than Linux, so `extract()`
|
|
50
|
+
only ever sees `auto` from the CLI on Linux. A gzipped cached tarball
|
|
51
|
+
that is not store-linked (`unpack`, git or remote tarballs, no sha512
|
|
52
|
+
integrity) is queued too, so the child un-gzips it.
|
|
53
|
+
|
|
54
|
+
Packages with install scripts are copied, never linked: when the store
|
|
55
|
+
index says so, or with the `installScripts` extract option.
|
|
56
|
+
|
|
57
|
+
A store link logs its request as `store`, a copy from the store as
|
|
58
|
+
`cache`. Both also return `manifest` (the package.json as JSON text,
|
|
59
|
+
if the index has it) and `bindingGyp`, so reify need not read them
|
|
60
|
+
back from disk. With `NODE_DEBUG=vlt`, linked / copied / missed counts
|
|
61
|
+
and the store hit rate are printed at exit.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RegistryClient } from '@vltpkg/registry-client';
|
|
2
|
+
/**
|
|
3
|
+
* What `GET /-/vlt/capabilities` answers: the vlt extensions a registry
|
|
4
|
+
* serves. Every field is optional — a registry that has never heard of the
|
|
5
|
+
* document, or that is down when it is asked, reads as an empty one.
|
|
6
|
+
*/
|
|
7
|
+
export type Capabilities = {
|
|
8
|
+
/** contract version of `/-/vlt/manifests`, the batch manifest endpoint */
|
|
9
|
+
manifests?: string;
|
|
10
|
+
/** contract version of `/-/vlt/resolve`, server-side range resolution */
|
|
11
|
+
resolve?: string;
|
|
12
|
+
/** contract version of the `?stable` packument filter */
|
|
13
|
+
'stable-filter'?: string;
|
|
14
|
+
/** packument media types the registry serves, most preferred first */
|
|
15
|
+
mimeTypes?: string[];
|
|
16
|
+
};
|
|
17
|
+
/** Forget every registry that has been asked. Exposed for tests. */
|
|
18
|
+
export declare const resetCapabilities: () => void;
|
|
19
|
+
/**
|
|
20
|
+
* What `registry` answered, if it has answered already, and `undefined`
|
|
21
|
+
* while the question is still open. Asking starts the request when nothing
|
|
22
|
+
* has asked yet, so a caller can peek now and get an answer on a later call
|
|
23
|
+
* without ever waiting for one.
|
|
24
|
+
*/
|
|
25
|
+
export declare const peekCapabilities: (client: RegistryClient, registry: string) => Capabilities | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The vlt extensions `registry` serves. Anything short of a capability
|
|
28
|
+
* document — a registry that 404s it, a request that fails, a body that
|
|
29
|
+
* does not parse — answers an empty one, so a caller that reads a missing
|
|
30
|
+
* key as unsupported treats those registries as plain npm registries.
|
|
31
|
+
*/
|
|
32
|
+
export declare const getCapabilities: (client: RegistryClient, registry: string) => Promise<Capabilities>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** What a registry with no vlt extensions supports. */
|
|
2
|
+
const none = Object.freeze({});
|
|
3
|
+
/**
|
|
4
|
+
* The document each registry answered, by capabilities URL. This coalesces
|
|
5
|
+
* the concurrent asks a single install makes; what keeps the document off
|
|
6
|
+
* the network is the RegistryClient disk cache behind it.
|
|
7
|
+
*/
|
|
8
|
+
const asked = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* The documents that have come back, by capabilities URL. A caller that
|
|
11
|
+
* cannot afford to wait reads this instead of the promise above.
|
|
12
|
+
*/
|
|
13
|
+
const known = new Map();
|
|
14
|
+
/** Forget every registry that has been asked. Exposed for tests. */
|
|
15
|
+
export const resetCapabilities = () => {
|
|
16
|
+
asked.clear();
|
|
17
|
+
known.clear();
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* What `registry` answered, if it has answered already, and `undefined`
|
|
21
|
+
* while the question is still open. Asking starts the request when nothing
|
|
22
|
+
* has asked yet, so a caller can peek now and get an answer on a later call
|
|
23
|
+
* without ever waiting for one.
|
|
24
|
+
*/
|
|
25
|
+
export const peekCapabilities = (client, registry) => {
|
|
26
|
+
const url = capabilitiesUrl(registry);
|
|
27
|
+
const seen = known.get(url);
|
|
28
|
+
if (seen)
|
|
29
|
+
return seen;
|
|
30
|
+
void getCapabilities(client, registry);
|
|
31
|
+
return undefined;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The vlt extensions `registry` serves. Anything short of a capability
|
|
35
|
+
* document — a registry that 404s it, a request that fails, a body that
|
|
36
|
+
* does not parse — answers an empty one, so a caller that reads a missing
|
|
37
|
+
* key as unsupported treats those registries as plain npm registries.
|
|
38
|
+
*/
|
|
39
|
+
export const getCapabilities = async (client, registry) => {
|
|
40
|
+
const url = capabilitiesUrl(registry);
|
|
41
|
+
const seen = asked.get(url);
|
|
42
|
+
if (seen)
|
|
43
|
+
return seen;
|
|
44
|
+
const doc = fetchCapabilities(client, url).then(caps => {
|
|
45
|
+
known.set(url, caps);
|
|
46
|
+
return caps;
|
|
47
|
+
});
|
|
48
|
+
asked.set(url, doc);
|
|
49
|
+
return doc;
|
|
50
|
+
};
|
|
51
|
+
const capabilitiesUrl = (registry) => String(new URL('-/vlt/capabilities', registry));
|
|
52
|
+
const fetchCapabilities = async (client, url) => {
|
|
53
|
+
try {
|
|
54
|
+
// No `useCache: false`: the registry serves this with a day of
|
|
55
|
+
// max-age, so the RegistryClient disk cache answers it for the rest
|
|
56
|
+
// of the day, including from later processes. One GET per registry
|
|
57
|
+
// per day, not one per install.
|
|
58
|
+
const response = await client.request(url, {
|
|
59
|
+
headers: { accept: 'application/json' },
|
|
60
|
+
});
|
|
61
|
+
if (response.statusCode !== 200)
|
|
62
|
+
return none;
|
|
63
|
+
return asCapabilities(response.json());
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return none;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Read a parsed body as a capability document, dropping any field that did
|
|
71
|
+
* not arrive in the shape the contract gives it.
|
|
72
|
+
*/
|
|
73
|
+
const asCapabilities = (body) => {
|
|
74
|
+
if (typeof body !== 'object' || body === null)
|
|
75
|
+
return none;
|
|
76
|
+
const doc = body;
|
|
77
|
+
const caps = {};
|
|
78
|
+
for (const key of [
|
|
79
|
+
'manifests',
|
|
80
|
+
'resolve',
|
|
81
|
+
'stable-filter',
|
|
82
|
+
]) {
|
|
83
|
+
const version = doc[key];
|
|
84
|
+
if (typeof version === 'string')
|
|
85
|
+
caps[key] = version;
|
|
86
|
+
}
|
|
87
|
+
const { mimeTypes } = doc;
|
|
88
|
+
if (Array.isArray(mimeTypes) &&
|
|
89
|
+
mimeTypes.every(type => typeof type === 'string')) {
|
|
90
|
+
caps.mimeTypes = mimeTypes;
|
|
91
|
+
}
|
|
92
|
+
return caps;
|
|
93
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,20 @@ import type { PickManifestOptions } from '@vltpkg/pick-manifest';
|
|
|
3
3
|
import type { RegistryClient, RegistryClientOptions, RegistryClientRequestOptions } from '@vltpkg/registry-client';
|
|
4
4
|
import type { SpecOptions } from '@vltpkg/spec';
|
|
5
5
|
import { Spec } from '@vltpkg/spec';
|
|
6
|
-
import type { Pool } from '@vltpkg/tar';
|
|
6
|
+
import type { Pool, StoreLinker } from '@vltpkg/tar';
|
|
7
7
|
import type { Integrity, Manifest, Packument } from '@vltpkg/types';
|
|
8
8
|
import { Monorepo } from '@vltpkg/workspaces';
|
|
9
|
+
import type { Capabilities } from './capabilities.ts';
|
|
10
|
+
export type { Capabilities } from './capabilities.ts';
|
|
11
|
+
export { getCapabilities, peekCapabilities, resetCapabilities, } from './capabilities.ts';
|
|
9
12
|
export declare const delimiter = "~";
|
|
13
|
+
/**
|
|
14
|
+
* vlt's abbreviated packument: corgi plus the fields the graph relies on
|
|
15
|
+
* (`license`, `time`, `hasInstallScript`), minus `dist.integrity` (the
|
|
16
|
+
* tarball response carries a `Repr-Digest` instead) and with
|
|
17
|
+
* `dist.tarball` relative to the registry base.
|
|
18
|
+
*/
|
|
19
|
+
export declare const VLT_PACKUMENT_MIME = "application/vnd.vlt.packument-v1+json";
|
|
10
20
|
/**
|
|
11
21
|
* Accept header for packument requests. Prefers vlt's abbreviated
|
|
12
22
|
* packument and falls back to the full one on registries that do not
|
|
@@ -26,6 +36,21 @@ export type Resolution = {
|
|
|
26
36
|
integrity?: Integrity;
|
|
27
37
|
signatures?: Exclude<Manifest['dist'], undefined>['signatures'];
|
|
28
38
|
spec: Spec;
|
|
39
|
+
/**
|
|
40
|
+
* The manifest came from a vlt packument, which carries no
|
|
41
|
+
* `dist.integrity`: the tarball response must carry a `Repr-Digest`.
|
|
42
|
+
*/
|
|
43
|
+
digestRequired?: boolean;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* {@link PackageInfoClient.extract} result. A global store link also
|
|
47
|
+
* carries what its index knows, so reify need not read it from disk.
|
|
48
|
+
*/
|
|
49
|
+
export type ExtractResolution = Resolution & {
|
|
50
|
+
/** the package.json as JSON text, if the index has it */
|
|
51
|
+
manifest?: string;
|
|
52
|
+
/** true if the package has a root binding.gyp */
|
|
53
|
+
bindingGyp?: boolean;
|
|
29
54
|
};
|
|
30
55
|
export type PackageInfoClientOptions = RegistryClientOptions & SpecOptions & {
|
|
31
56
|
/** root of the project. Defaults to process.cwd() */
|
|
@@ -37,10 +62,21 @@ export type PackageInfoClientOptions = RegistryClientOptions & SpecOptions & {
|
|
|
37
62
|
'workspace-group'?: string[];
|
|
38
63
|
/** workspace paths to load, irrelevant if Monorepo provided */
|
|
39
64
|
workspace?: string[];
|
|
65
|
+
/**
|
|
66
|
+
* How registry packages are placed: `unpack` (default) unpacks the
|
|
67
|
+
* tarball, anything else goes through the global store first.
|
|
68
|
+
*/
|
|
69
|
+
'store-linker'?: StoreLinker;
|
|
40
70
|
};
|
|
41
71
|
export type PackageInfoClientRequestOptions = PickManifestOptions & RegistryClientRequestOptions & {
|
|
42
72
|
/** dir to resolve `file://` specifiers against. Defaults to projectRoot. */
|
|
43
73
|
from?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Fetch the full packument (readme, maintainers, `dist.integrity`)
|
|
76
|
+
* rather than the abbreviated one. Bypasses the disk cache, which is
|
|
77
|
+
* keyed by URL alone, so the two representations never mix.
|
|
78
|
+
*/
|
|
79
|
+
full?: boolean;
|
|
44
80
|
};
|
|
45
81
|
export type PackageInfoClientExtractOptions = PackageInfoClientRequestOptions & {
|
|
46
82
|
integrity?: Integrity;
|
|
@@ -52,6 +88,11 @@ export type PackageInfoClientExtractOptions = PackageInfoClientRequestOptions &
|
|
|
52
88
|
* Defaults to false — fresh installs always verify integrity.
|
|
53
89
|
*/
|
|
54
90
|
fromLockfile?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The manifest declares install scripts: copy the package from the
|
|
93
|
+
* global store, never link it, even if its package.json has none.
|
|
94
|
+
*/
|
|
95
|
+
installScripts?: boolean;
|
|
55
96
|
};
|
|
56
97
|
export declare class PackageInfoClient {
|
|
57
98
|
#private;
|
|
@@ -59,9 +100,15 @@ export declare class PackageInfoClient {
|
|
|
59
100
|
packageJson: PackageJson;
|
|
60
101
|
monorepo?: Monorepo;
|
|
61
102
|
getRegistryClient(): Promise<RegistryClient>;
|
|
103
|
+
/**
|
|
104
|
+
* The vlt extensions `registry` serves, from its
|
|
105
|
+
* `GET /-/vlt/capabilities` document. A registry that does not answer
|
|
106
|
+
* one reads as an empty document, so a missing key means unsupported.
|
|
107
|
+
*/
|
|
108
|
+
capabilities(registry: string): Promise<Capabilities>;
|
|
62
109
|
getTarPool(): Promise<Pool>;
|
|
63
110
|
constructor(options?: PackageInfoClientOptions);
|
|
64
|
-
extract(spec: Spec | string, target: string, options?: PackageInfoClientExtractOptions): Promise<
|
|
111
|
+
extract(spec: Spec | string, target: string, options?: PackageInfoClientExtractOptions): Promise<ExtractResolution>;
|
|
65
112
|
/**
|
|
66
113
|
* Conditionally return the path to the manifest cache file. The logic
|
|
67
114
|
* to determine if caching should be skipped aligns with `pickManifest`
|
|
@@ -70,6 +117,11 @@ export declare class PackageInfoClient {
|
|
|
70
117
|
_manifestCachePath(spec: Spec, options: PackageInfoClientRequestOptions): string | undefined;
|
|
71
118
|
tarball(spec: Spec | string, options?: PackageInfoClientExtractOptions): Promise<Buffer>;
|
|
72
119
|
manifest(spec: Spec | string, options?: PackageInfoClientRequestOptions): Promise<Manifest | import("@vltpkg/types").Override<Manifest, import("@vltpkg/types").NormalizedFields>>;
|
|
120
|
+
/**
|
|
121
|
+
* The packument for `spec`, with every version the registry has. Callers
|
|
122
|
+
* that only pick a manifest out of it go through `#packument` instead,
|
|
123
|
+
* which can ask for the prerelease-free one.
|
|
124
|
+
*/
|
|
73
125
|
packument(spec: Spec | string, options?: PackageInfoClientRequestOptions): Promise<Packument>;
|
|
74
126
|
resolve(spec: Spec | string, options?: PackageInfoClientRequestOptions): Promise<Resolution>;
|
|
75
127
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,10 @@ import { PackageJson } from '@vltpkg/package-json';
|
|
|
5
5
|
import { pickManifest } from '@vltpkg/pick-manifest';
|
|
6
6
|
// subpath import: keeps the lazy `import('@vltpkg/registry-client')`
|
|
7
7
|
// below from becoming an eager dependency on the whole client.
|
|
8
|
-
import { registryErrorMessage } from '@vltpkg/registry-client/registry-error';
|
|
8
|
+
import { registryErrorMessage, tokenRefusalAdvice, } from '@vltpkg/registry-client/registry-error';
|
|
9
|
+
import { storeRoot } from '@vltpkg/registry-client/store-root';
|
|
9
10
|
import { Spec } from '@vltpkg/spec';
|
|
10
|
-
import { asPackument } from '@vltpkg/types';
|
|
11
|
+
import { asPackument, integrityHex } from '@vltpkg/types';
|
|
11
12
|
import ssri from 'ssri';
|
|
12
13
|
import { Monorepo } from '@vltpkg/workspaces';
|
|
13
14
|
import { XDG } from '@vltpkg/xdg';
|
|
@@ -16,10 +17,19 @@ import { mkdir, readFile, rm, stat, symlink, unlink, writeFile, } from 'node:fs/
|
|
|
16
17
|
import { basename, dirname, resolve as pathResolve, relative, } from 'node:path';
|
|
17
18
|
import { debuglog } from 'node:util';
|
|
18
19
|
import { create as tarC } from 'tar';
|
|
20
|
+
import { getCapabilities, peekCapabilities } from "./capabilities.js";
|
|
19
21
|
import { rename } from "./rename.js";
|
|
22
|
+
export { getCapabilities, peekCapabilities, resetCapabilities, } from "./capabilities.js";
|
|
20
23
|
const debug = debuglog('vlt');
|
|
21
24
|
const xdg = new XDG('vlt');
|
|
22
25
|
export const delimiter = '~';
|
|
26
|
+
/**
|
|
27
|
+
* vlt's abbreviated packument: corgi plus the fields the graph relies on
|
|
28
|
+
* (`license`, `time`, `hasInstallScript`), minus `dist.integrity` (the
|
|
29
|
+
* tarball response carries a `Repr-Digest` instead) and with
|
|
30
|
+
* `dist.tarball` relative to the registry base.
|
|
31
|
+
*/
|
|
32
|
+
export const VLT_PACKUMENT_MIME = 'application/vnd.vlt.packument-v1+json';
|
|
23
33
|
/**
|
|
24
34
|
* Accept header for packument requests. Prefers vlt's abbreviated
|
|
25
35
|
* packument and falls back to the full one on registries that do not
|
|
@@ -33,7 +43,7 @@ export const delimiter = '~';
|
|
|
33
43
|
* only so a registry that rejects what it cannot satisfy exactly still
|
|
34
44
|
* has something to match.
|
|
35
45
|
*/
|
|
36
|
-
export const PACKUMENT_ACCEPT =
|
|
46
|
+
export const PACKUMENT_ACCEPT = `${VLT_PACKUMENT_MIME}; q=1.0, application/json; q=0.8, */*; q=0.1`;
|
|
37
47
|
// the maximum duration of a manifest cache file
|
|
38
48
|
const manifestCacheMaxAge = 5 * 60 * 1000;
|
|
39
49
|
/**
|
|
@@ -52,6 +62,29 @@ const noRegistryError = (spec) => error('No registry configured to resolve this
|
|
|
52
62
|
* Takes a *final* spec (`spec.final`), same as `pickManifest` sees.
|
|
53
63
|
*/
|
|
54
64
|
const isMovingSelector = (f) => !!(f.distTag || f.range?.isAny);
|
|
65
|
+
/**
|
|
66
|
+
* A selector that cannot land on a prerelease, and so can be answered from
|
|
67
|
+
* the registry's `?stable` packument.
|
|
68
|
+
*
|
|
69
|
+
* A dist tag has no range, and is out either way: it can point at a
|
|
70
|
+
* prerelease, which the stable packument drops along with the tag. `*` and
|
|
71
|
+
* an empty range are out for the same reason -- they resolve through
|
|
72
|
+
* `latest`. What is left is a range that names no prerelease of its own,
|
|
73
|
+
* which standard semver never matches against one.
|
|
74
|
+
*
|
|
75
|
+
* Takes a *final* spec (`spec.final`), same as `isMovingSelector`.
|
|
76
|
+
*/
|
|
77
|
+
const isStableSelector = (f) => {
|
|
78
|
+
const { range } = f;
|
|
79
|
+
if (!range)
|
|
80
|
+
return false;
|
|
81
|
+
// A prerelease comparator always carries a `-`, and so does a hyphen
|
|
82
|
+
// range; reading that as "might be a prerelease" only ever gives up the
|
|
83
|
+
// smaller packument.
|
|
84
|
+
return !range.isAny && !range.raw.includes('-');
|
|
85
|
+
};
|
|
86
|
+
// anything else, eg an unvalidated env value, means `unpack`
|
|
87
|
+
const storeLinkers = new Set(['auto', 'hardlink', 'copy']);
|
|
55
88
|
export class PackageInfoClient {
|
|
56
89
|
#registryClient;
|
|
57
90
|
#projectRoot;
|
|
@@ -61,8 +94,20 @@ export class PackageInfoClient {
|
|
|
61
94
|
packageJson;
|
|
62
95
|
monorepo;
|
|
63
96
|
#trustedIntegrities = new Map();
|
|
97
|
+
// `${registry}${name}` of every packument served as VLT_PACKUMENT_MIME
|
|
98
|
+
#vltPackuments = new Set();
|
|
64
99
|
#manifestCacheMinAge = Date.now() - manifestCacheMaxAge;
|
|
65
100
|
#cachePath;
|
|
101
|
+
#storeRoot;
|
|
102
|
+
#storeLinker;
|
|
103
|
+
#storeHits = { link: 0, copy: 0 };
|
|
104
|
+
#storeMisses = 0;
|
|
105
|
+
#storeHitRateLogged = false;
|
|
106
|
+
#logStoreHitRate = () => {
|
|
107
|
+
const { link, copy } = this.#storeHits;
|
|
108
|
+
const n = Math.max(1, link + copy + this.#storeMisses);
|
|
109
|
+
debug('global store: linked=%d copied=%d missed=%d hit rate=%s%%', link, copy, this.#storeMisses, (((link + copy) / n) * 100).toFixed(1));
|
|
110
|
+
};
|
|
66
111
|
// In-flight coalescing key is `${registry}${name}` — no representation
|
|
67
112
|
// component. Safe only because every caller requests the same full
|
|
68
113
|
// packument (see #fetchPackument). The one thing that does vary per
|
|
@@ -90,6 +135,40 @@ export class PackageInfoClient {
|
|
|
90
135
|
});
|
|
91
136
|
return this.#registryClientPromise;
|
|
92
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The vlt extensions `registry` serves, from its
|
|
140
|
+
* `GET /-/vlt/capabilities` document. A registry that does not answer
|
|
141
|
+
* one reads as an empty document, so a missing key means unsupported.
|
|
142
|
+
*/
|
|
143
|
+
async capabilities(registry) {
|
|
144
|
+
return getCapabilities(await this.getRegistryClient(), registry);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether the packument for `f` can be fetched with `?stable`: the
|
|
148
|
+
* selector has to be one a prerelease cannot answer, and the registry
|
|
149
|
+
* must not have told us it does not serve the filter.
|
|
150
|
+
*
|
|
151
|
+
* Never waits on the capability document, which would put a round trip in
|
|
152
|
+
* front of the first packument of every cold install. Until that document
|
|
153
|
+
* arrives the answer is yes: a registry that does not know `?stable`
|
|
154
|
+
* ignores the parameter and serves the full packument, which resolution
|
|
155
|
+
* reads just as well. Once the document does arrive it is authoritative,
|
|
156
|
+
* so a registry that does not serve the filter stops being asked with it.
|
|
157
|
+
*/
|
|
158
|
+
#stable(f) {
|
|
159
|
+
const { registry } = f;
|
|
160
|
+
if (!registry || !isStableSelector(f))
|
|
161
|
+
return false;
|
|
162
|
+
const client = this.#registryClient;
|
|
163
|
+
if (!client) {
|
|
164
|
+
// the registry client is built lazily, so the first caller starts it
|
|
165
|
+
// and the document along with it, and asks optimistically meanwhile
|
|
166
|
+
void this.capabilities(registry).catch(() => { });
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
const caps = peekCapabilities(client, registry);
|
|
170
|
+
return !caps || !!caps['stable-filter'];
|
|
171
|
+
}
|
|
93
172
|
async getTarPool() {
|
|
94
173
|
if (this.#tarPool)
|
|
95
174
|
return this.#tarPool;
|
|
@@ -116,6 +195,10 @@ export class PackageInfoClient {
|
|
|
116
195
|
packageJson: this.packageJson,
|
|
117
196
|
});
|
|
118
197
|
this.#cachePath = options.cache ?? xdg.cache();
|
|
198
|
+
this.#storeRoot = options.storeRoot ?? storeRoot(this.#cachePath);
|
|
199
|
+
const linker = options['store-linker'];
|
|
200
|
+
this.#storeLinker =
|
|
201
|
+
linker && storeLinkers.has(linker) ? linker : 'unpack';
|
|
119
202
|
// optionally create its cache directory if it doesn't exist
|
|
120
203
|
void mkdir(pathResolve(this.#cachePath, 'package-info'), {
|
|
121
204
|
recursive: true,
|
|
@@ -124,7 +207,7 @@ export class PackageInfoClient {
|
|
|
124
207
|
async extract(spec, target, options = {}) {
|
|
125
208
|
if (typeof spec === 'string')
|
|
126
209
|
spec = Spec.parse(spec, this.options);
|
|
127
|
-
const { from = this.#projectRoot, integrity, resolved, fromLockfile = false, } = options;
|
|
210
|
+
const { from = this.#projectRoot, integrity, resolved, fromLockfile = false, installScripts = false, } = options;
|
|
128
211
|
const f = spec.final;
|
|
129
212
|
// If the caller already provides both integrity and resolved
|
|
130
213
|
// (from lockfile or prior resolution), skip re-resolving.
|
|
@@ -160,16 +243,53 @@ export class PackageInfoClient {
|
|
|
160
243
|
// fallthrough if a remote tarball url present
|
|
161
244
|
}
|
|
162
245
|
case 'registry': {
|
|
246
|
+
const pool = await this.getTarPool();
|
|
247
|
+
// git tarballs keep the unpack path
|
|
248
|
+
const hex = f.type === 'registry' && this.#storeLinker !== 'unpack' ?
|
|
249
|
+
integrityHex(r.integrity)
|
|
250
|
+
: undefined;
|
|
251
|
+
const copy = this.#storeLinker === 'copy' || installScripts;
|
|
252
|
+
if (hex) {
|
|
253
|
+
if (debug.enabled && !this.#storeHitRateLogged) {
|
|
254
|
+
this.#storeHitRateLogged = true;
|
|
255
|
+
process.once('beforeExit', this.#logStoreHitRate);
|
|
256
|
+
}
|
|
257
|
+
const linked = await pool.linkFromStore(pathResolve(this.#storeRoot, hex), target, { copy });
|
|
258
|
+
if (linked) {
|
|
259
|
+
const { how, index } = linked;
|
|
260
|
+
this.#storeHits[how]++;
|
|
261
|
+
// a copy is no link: report it as a cache hit
|
|
262
|
+
logRequest(r.resolved, how === 'link' ? 'store' : 'cache');
|
|
263
|
+
return {
|
|
264
|
+
...r,
|
|
265
|
+
manifest: index.manifest,
|
|
266
|
+
// implies scripts, so most packages skip the scan
|
|
267
|
+
bindingGyp: index.scripts &&
|
|
268
|
+
index.files.some(f => f[0] === 'binding.gyp'),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
this.#storeMisses++;
|
|
272
|
+
}
|
|
163
273
|
// if the tarball is already on disk, unpack it straight from
|
|
164
274
|
// the cache file: it never has to be held in the client's
|
|
165
275
|
// in-memory cache. anything unexpected falls through to the
|
|
166
276
|
// fetch path, which throws its own error if the body is
|
|
167
277
|
// genuinely bad.
|
|
168
|
-
const
|
|
278
|
+
const rc = await this.getRegistryClient();
|
|
279
|
+
const cached = rc.cachedBody(r.resolved, {
|
|
280
|
+
integrity: r.integrity,
|
|
281
|
+
});
|
|
169
282
|
if (cached) {
|
|
170
283
|
try {
|
|
171
|
-
await
|
|
284
|
+
await pool.unpack(cached.body, target);
|
|
172
285
|
logRequest(r.resolved, 'cache');
|
|
286
|
+
r.integrity ??= cached.integrity;
|
|
287
|
+
// a warm install writes nothing to the cache, so queue the
|
|
288
|
+
// store miss here or an existing cache never converges,
|
|
289
|
+
// and a gzipped body with no store link, to unzip it
|
|
290
|
+
if (hex || cached.gzip) {
|
|
291
|
+
rc.queueForStore(cached.key, r.integrity);
|
|
292
|
+
}
|
|
173
293
|
return r;
|
|
174
294
|
}
|
|
175
295
|
catch (er) {
|
|
@@ -184,6 +304,7 @@ export class PackageInfoClient {
|
|
|
184
304
|
const response = await (await this.getRegistryClient()).request(r.resolved, {
|
|
185
305
|
integrity: r.integrity,
|
|
186
306
|
trustIntegrity,
|
|
307
|
+
verifyDigest: r.digestRequired ? 'required' : true,
|
|
187
308
|
...(useCache === false ? { useCache } : {}),
|
|
188
309
|
});
|
|
189
310
|
if (response.statusCode !== 200) {
|
|
@@ -201,27 +322,40 @@ export class PackageInfoClient {
|
|
|
201
322
|
this.#trustedIntegrities.set(r.resolved, response.integrity);
|
|
202
323
|
}
|
|
203
324
|
const buf = response.buffer();
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
325
|
+
if (r.integrity) {
|
|
326
|
+
// Verify network-delivered tarball bytes against dist.integrity.
|
|
327
|
+
// Skip cache-served bodies: they were verified on the fetch that
|
|
328
|
+
// populated the cache, and cache-unzip rewrites them un-gzipped
|
|
329
|
+
// so the gzip-hash can never match. Skip lockfile-sourced
|
|
330
|
+
// integrity: it was verified on first install.
|
|
331
|
+
if (!fromLockfile && !response.fromCache) {
|
|
332
|
+
const hash = createHash('sha512');
|
|
333
|
+
hash.update(buf);
|
|
334
|
+
const computed = `sha512-${hash.digest('base64')}`;
|
|
335
|
+
/* c8 ignore start - defense-in-depth: registry client's
|
|
336
|
+
* checkIntegrity() usually catches mismatches first. */
|
|
337
|
+
if (computed !== r.integrity) {
|
|
338
|
+
throw error('Tarball integrity check failed', {
|
|
339
|
+
code: 'EINTEGRITY',
|
|
340
|
+
spec,
|
|
341
|
+
url: r.resolved,
|
|
342
|
+
wanted: r.integrity,
|
|
343
|
+
found: computed,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/* c8 ignore stop */
|
|
223
347
|
}
|
|
224
|
-
|
|
348
|
+
}
|
|
349
|
+
else if (response.fromCache) {
|
|
350
|
+
// the hash the body was stored under
|
|
351
|
+
r.integrity = response.integrity;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// no dist.integrity: the registry client checked the body
|
|
355
|
+
// against the digest the registry sent with it (verifyDigest
|
|
356
|
+
// above). hand the hash back so the lockfile pins it from
|
|
357
|
+
// now on
|
|
358
|
+
r.integrity = response.integrityActual;
|
|
225
359
|
}
|
|
226
360
|
return buf;
|
|
227
361
|
};
|
|
@@ -421,6 +555,11 @@ export class PackageInfoClient {
|
|
|
421
555
|
...options,
|
|
422
556
|
integrity,
|
|
423
557
|
trustIntegrity,
|
|
558
|
+
// no dist.integrity: checked against the digest the registry
|
|
559
|
+
// sent with it instead, before the client caches it
|
|
560
|
+
verifyDigest: this.#vltPackuments.has(`${f.registry}${f.name}`) ?
|
|
561
|
+
'required'
|
|
562
|
+
: true,
|
|
424
563
|
...(useCache === false ? { useCache } : {}),
|
|
425
564
|
});
|
|
426
565
|
if (response.statusCode !== 200) {
|
|
@@ -545,7 +684,8 @@ export class PackageInfoClient {
|
|
|
545
684
|
try {
|
|
546
685
|
// Cache file exists, read and return it. Freshness is
|
|
547
686
|
// tracked via the file's mtime, so the file content is
|
|
548
|
-
//
|
|
687
|
+
// the manifest, plus a marker when it came from a vlt
|
|
688
|
+
// packument, and can be returned as parsed.
|
|
549
689
|
const [st, cached] = await Promise.all([
|
|
550
690
|
stat(cachePath),
|
|
551
691
|
readFile(cachePath, 'utf8'),
|
|
@@ -561,13 +701,18 @@ export class PackageInfoClient {
|
|
|
561
701
|
void unlink(cachePath).catch(() => { });
|
|
562
702
|
throw new Error('manifest cache expired');
|
|
563
703
|
}
|
|
704
|
+
// the tarball digest stays required across processes
|
|
705
|
+
if (json.__VLT_PACKUMENT) {
|
|
706
|
+
this.#vltPackuments.add(`${f.registry}${f.name}`);
|
|
707
|
+
delete json.__VLT_PACKUMENT;
|
|
708
|
+
}
|
|
564
709
|
return json;
|
|
565
710
|
}
|
|
566
711
|
catch {
|
|
567
712
|
// Cache miss, fetch from packument
|
|
568
713
|
}
|
|
569
714
|
}
|
|
570
|
-
const mani = pickManifest(await this
|
|
715
|
+
const mani = pickManifest(await this.#packument(f, options, this.#stable(f)), spec, options);
|
|
571
716
|
if (!mani)
|
|
572
717
|
throw this.#resolveError(spec, options);
|
|
573
718
|
// Cache the manifest data. Skip paths already written this
|
|
@@ -575,7 +720,8 @@ export class PackageInfoClient {
|
|
|
575
720
|
// and racing writers for the same path.
|
|
576
721
|
if (cachePath && !this.#manifestWritePaths.has(cachePath)) {
|
|
577
722
|
this.#manifestWritePaths.add(cachePath);
|
|
578
|
-
|
|
723
|
+
const vlt = this.#vltPackuments.has(`${f.registry}${f.name}`);
|
|
724
|
+
void this.#writeManifestCache(cachePath, JSON.stringify(vlt ? { ...mani, __VLT_PACKUMENT: true } : mani));
|
|
579
725
|
}
|
|
580
726
|
return mani;
|
|
581
727
|
}
|
|
@@ -647,7 +793,15 @@ export class PackageInfoClient {
|
|
|
647
793
|
}
|
|
648
794
|
}
|
|
649
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* The packument for `spec`, with every version the registry has. Callers
|
|
798
|
+
* that only pick a manifest out of it go through `#packument` instead,
|
|
799
|
+
* which can ask for the prerelease-free one.
|
|
800
|
+
*/
|
|
650
801
|
async packument(spec, options = {}) {
|
|
802
|
+
return this.#packument(spec, options, false);
|
|
803
|
+
}
|
|
804
|
+
async #packument(spec, options, stable) {
|
|
651
805
|
if (typeof spec === 'string')
|
|
652
806
|
spec = Spec.parse(spec, this.options);
|
|
653
807
|
const f = spec.final;
|
|
@@ -684,10 +838,21 @@ export class PackageInfoClient {
|
|
|
684
838
|
const { registry, name } = f;
|
|
685
839
|
if (!registry)
|
|
686
840
|
throw noRegistryError(spec);
|
|
687
|
-
//
|
|
688
|
-
|
|
841
|
+
// a full representation neither reads nor feeds the coalescing
|
|
842
|
+
// map, which holds the abbreviated one
|
|
843
|
+
if (options.full)
|
|
844
|
+
return this.#fetchPackument(spec, options, new URL(name, registry));
|
|
845
|
+
// The only representation component of the coalescing key is
|
|
846
|
+
// `?stable`; everything else about the request is fixed (see
|
|
847
|
+
// #fetchPackument).
|
|
848
|
+
const fullKey = `${registry}${name}`;
|
|
849
|
+
const packumentKey = stable ? `${fullKey}?stable` : fullKey;
|
|
689
850
|
const forced = isMovingSelector(f);
|
|
690
|
-
const inflight = this.#packumentPromises.get(packumentKey)
|
|
851
|
+
const inflight = this.#packumentPromises.get(packumentKey) ??
|
|
852
|
+
// the full packument is a superset of the stable one, so an
|
|
853
|
+
// in-flight full request answers a stable ask too, and a package
|
|
854
|
+
// wanted both ways is still fetched once
|
|
855
|
+
(stable ? this.#packumentPromises.get(fullKey) : undefined);
|
|
691
856
|
// a moving selector must not ride along on a non-forced request:
|
|
692
857
|
// that one can settle to a fresh-but-stale cache hit, which is
|
|
693
858
|
// exactly what forceRevalidate exists to avoid. the other
|
|
@@ -696,7 +861,8 @@ export class PackageInfoClient {
|
|
|
696
861
|
// when both shapes are asked for at once.
|
|
697
862
|
if (inflight && (!forced || inflight.forced))
|
|
698
863
|
return inflight.promise;
|
|
699
|
-
|
|
864
|
+
// `?stable` is a distinct URL, so it gets its own disk cache entry
|
|
865
|
+
const pakuURL = new URL(stable ? `${name}?stable` : name, registry);
|
|
700
866
|
const promise = this.#fetchPackument(spec, options, pakuURL);
|
|
701
867
|
const record = { promise, forced };
|
|
702
868
|
this.#packumentPromises.set(packumentKey, record);
|
|
@@ -735,8 +901,12 @@ export class PackageInfoClient {
|
|
|
735
901
|
// packument request must use this same accept header, and the SWR
|
|
736
902
|
// revalidation child re-requests the representation it was given.
|
|
737
903
|
const response = await (await this.getRegistryClient()).request(pakuURL, {
|
|
738
|
-
headers: {
|
|
739
|
-
|
|
904
|
+
headers: {
|
|
905
|
+
accept: options.full ? 'application/json' : PACKUMENT_ACCEPT,
|
|
906
|
+
},
|
|
907
|
+
...(useCache === false || options.full ?
|
|
908
|
+
{ useCache: false }
|
|
909
|
+
: {}),
|
|
740
910
|
// costs a conditional GET per moving selector on an otherwise warm
|
|
741
911
|
// cache, install included. 304s are cheap but not free; the
|
|
742
912
|
// alternative is serving a dist tag that moved (#1656).
|
|
@@ -750,8 +920,9 @@ export class PackageInfoClient {
|
|
|
750
920
|
response,
|
|
751
921
|
});
|
|
752
922
|
}
|
|
923
|
+
let paku;
|
|
753
924
|
try {
|
|
754
|
-
|
|
925
|
+
paku = response.json();
|
|
755
926
|
}
|
|
756
927
|
catch (er) {
|
|
757
928
|
if (useCache !== false) {
|
|
@@ -759,6 +930,14 @@ export class PackageInfoClient {
|
|
|
759
930
|
}
|
|
760
931
|
throw er;
|
|
761
932
|
}
|
|
933
|
+
const { registry, name } = spec.final;
|
|
934
|
+
if (response.contentType.startsWith(VLT_PACKUMENT_MIME)) {
|
|
935
|
+
this.#vltPackuments.add(`${registry}${name}`);
|
|
936
|
+
}
|
|
937
|
+
/* c8 ignore next - registry specs always have a registry */
|
|
938
|
+
if (registry)
|
|
939
|
+
absolutizeTarballs(paku, registry);
|
|
940
|
+
return paku;
|
|
762
941
|
}
|
|
763
942
|
async resolve(spec, options = {}) {
|
|
764
943
|
const memoKey = String(spec);
|
|
@@ -806,6 +985,10 @@ export class PackageInfoClient {
|
|
|
806
985
|
signatures,
|
|
807
986
|
spec,
|
|
808
987
|
};
|
|
988
|
+
if (!integrity &&
|
|
989
|
+
this.#vltPackuments.has(`${f.registry}${f.name}`)) {
|
|
990
|
+
r.digestRequired = true;
|
|
991
|
+
}
|
|
809
992
|
this.#resolutions.set(memoKey, r);
|
|
810
993
|
return r;
|
|
811
994
|
}
|
|
@@ -883,7 +1066,13 @@ export class PackageInfoClient {
|
|
|
883
1066
|
// error resolving
|
|
884
1067
|
#resolveError(spec, options = {}, message = 'Could not resolve', extra = {}) {
|
|
885
1068
|
const { from = this.#projectRoot } = options;
|
|
886
|
-
|
|
1069
|
+
// Every registry failure here carries its response, so the advice is
|
|
1070
|
+
// attached once rather than at each throw site. The cast is because
|
|
1071
|
+
// `response` is typed loosely enough to include a `fetch` Response.
|
|
1072
|
+
const advice = spec?.final.type === 'registry' ?
|
|
1073
|
+
tokenRefusalAdvice(extra.response, extra.url)
|
|
1074
|
+
: undefined;
|
|
1075
|
+
const er = error(advice ? `${message}\n⚠️ ${advice}` : message, {
|
|
887
1076
|
code: 'ERESOLVE',
|
|
888
1077
|
spec,
|
|
889
1078
|
from,
|
|
@@ -892,3 +1081,16 @@ export class PackageInfoClient {
|
|
|
892
1081
|
return er;
|
|
893
1082
|
}
|
|
894
1083
|
}
|
|
1084
|
+
// vlt packuments carry dist.tarball relative to the registry base
|
|
1085
|
+
// (`foo/-/foo-1.0.0.tgz`), the form conventionalRegistryTarball builds.
|
|
1086
|
+
// Nothing downstream sees a relative URL: the manifest cache, the graph
|
|
1087
|
+
// and the lockfile all get the absolute one.
|
|
1088
|
+
const absolutizeTarballs = (paku, registry) => {
|
|
1089
|
+
const base = registry.endsWith('/') ? registry : registry + '/';
|
|
1090
|
+
for (const { dist } of Object.values(paku.versions)) {
|
|
1091
|
+
const tarball = dist?.tarball;
|
|
1092
|
+
if (tarball && !/^[a-z][a-z0-9+.-]*:/i.test(tarball)) {
|
|
1093
|
+
dist.tarball = String(new URL(tarball, base));
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
};
|
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.
|
|
4
|
+
"version": "1.2.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.
|
|
17
|
-
"@vltpkg/git": "1.
|
|
18
|
-
"@vltpkg/output": "1.
|
|
19
|
-
"@vltpkg/package-json": "1.
|
|
20
|
-
"@vltpkg/pick-manifest": "1.
|
|
21
|
-
"@vltpkg/registry-client": "1.
|
|
22
|
-
"@vltpkg/spec": "1.
|
|
23
|
-
"@vltpkg/tar": "1.
|
|
24
|
-
"@vltpkg/types": "1.
|
|
25
|
-
"@vltpkg/workspaces": "1.
|
|
26
|
-
"@vltpkg/xdg": "1.
|
|
16
|
+
"@vltpkg/error-cause": "1.2.0",
|
|
17
|
+
"@vltpkg/git": "1.2.0",
|
|
18
|
+
"@vltpkg/output": "1.2.0",
|
|
19
|
+
"@vltpkg/package-json": "1.2.0",
|
|
20
|
+
"@vltpkg/pick-manifest": "1.2.0",
|
|
21
|
+
"@vltpkg/registry-client": "1.2.0",
|
|
22
|
+
"@vltpkg/spec": "1.2.0",
|
|
23
|
+
"@vltpkg/tar": "1.2.0",
|
|
24
|
+
"@vltpkg/types": "1.2.0",
|
|
25
|
+
"@vltpkg/workspaces": "1.2.0",
|
|
26
|
+
"@vltpkg/xdg": "1.2.0",
|
|
27
27
|
"ssri": "^13.0.0",
|
|
28
28
|
"tar": "^7.5.2"
|
|
29
29
|
},
|
|
@@ -32,12 +32,12 @@
|
|
|
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.
|
|
36
|
-
"@vltpkg/vlt-json": "1.
|
|
35
|
+
"@vltpkg/cache-unzip": "1.2.0",
|
|
36
|
+
"@vltpkg/vlt-json": "1.2.0",
|
|
37
37
|
"eslint": "^9.39.1",
|
|
38
38
|
"pacote": "^21.0.4",
|
|
39
39
|
"prettier": "^3.7.4",
|
|
40
|
-
"tap": "^21.
|
|
40
|
+
"tap": "^21.8.0",
|
|
41
41
|
"typedoc": "~0.27.9",
|
|
42
42
|
"typescript": "5.7.3",
|
|
43
43
|
"typescript-eslint": "^8.49.0"
|