@vltpkg/package-info 1.1.1 → 1.3.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 +62 -4
- package/dist/index.js +284 -57
- 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,27 @@ 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;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve to the registry's Brotli (`.tar.br`) tarball when it
|
|
72
|
+
* advertises one in `dist.alternates`. Defaults to true; set false to
|
|
73
|
+
* always resolve to the gzip `.tgz`.
|
|
74
|
+
*/
|
|
75
|
+
'brotli-tarballs'?: boolean;
|
|
40
76
|
};
|
|
41
77
|
export type PackageInfoClientRequestOptions = PickManifestOptions & RegistryClientRequestOptions & {
|
|
42
78
|
/** dir to resolve `file://` specifiers against. Defaults to projectRoot. */
|
|
43
79
|
from?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Fetch the full packument (readme, maintainers, `dist.integrity`)
|
|
82
|
+
* rather than the abbreviated one. Bypasses the disk cache, which is
|
|
83
|
+
* keyed by URL alone, so the two representations never mix.
|
|
84
|
+
*/
|
|
85
|
+
full?: boolean;
|
|
44
86
|
};
|
|
45
87
|
export type PackageInfoClientExtractOptions = PackageInfoClientRequestOptions & {
|
|
46
88
|
integrity?: Integrity;
|
|
@@ -48,10 +90,15 @@ export type PackageInfoClientExtractOptions = PackageInfoClientRequestOptions &
|
|
|
48
90
|
/**
|
|
49
91
|
* When true, indicates that integrity + resolved came from a
|
|
50
92
|
* lockfile (i.e. they were already verified on first install).
|
|
51
|
-
* Skips
|
|
52
|
-
*
|
|
93
|
+
* Skips re-hashing a refetched body of an already verified url.
|
|
94
|
+
* Other network bodies are always checked.
|
|
53
95
|
*/
|
|
54
96
|
fromLockfile?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The manifest declares install scripts: copy the package from the
|
|
99
|
+
* global store, never link it, even if its package.json has none.
|
|
100
|
+
*/
|
|
101
|
+
installScripts?: boolean;
|
|
55
102
|
};
|
|
56
103
|
export declare class PackageInfoClient {
|
|
57
104
|
#private;
|
|
@@ -59,9 +106,15 @@ export declare class PackageInfoClient {
|
|
|
59
106
|
packageJson: PackageJson;
|
|
60
107
|
monorepo?: Monorepo;
|
|
61
108
|
getRegistryClient(): Promise<RegistryClient>;
|
|
109
|
+
/**
|
|
110
|
+
* The vlt extensions `registry` serves, from its
|
|
111
|
+
* `GET /-/vlt/capabilities` document. A registry that does not answer
|
|
112
|
+
* one reads as an empty document, so a missing key means unsupported.
|
|
113
|
+
*/
|
|
114
|
+
capabilities(registry: string): Promise<Capabilities>;
|
|
62
115
|
getTarPool(): Promise<Pool>;
|
|
63
116
|
constructor(options?: PackageInfoClientOptions);
|
|
64
|
-
extract(spec: Spec | string, target: string, options?: PackageInfoClientExtractOptions): Promise<
|
|
117
|
+
extract(spec: Spec | string, target: string, options?: PackageInfoClientExtractOptions): Promise<ExtractResolution>;
|
|
65
118
|
/**
|
|
66
119
|
* Conditionally return the path to the manifest cache file. The logic
|
|
67
120
|
* to determine if caching should be skipped aligns with `pickManifest`
|
|
@@ -70,6 +123,11 @@ export declare class PackageInfoClient {
|
|
|
70
123
|
_manifestCachePath(spec: Spec, options: PackageInfoClientRequestOptions): string | undefined;
|
|
71
124
|
tarball(spec: Spec | string, options?: PackageInfoClientExtractOptions): Promise<Buffer>;
|
|
72
125
|
manifest(spec: Spec | string, options?: PackageInfoClientRequestOptions): Promise<Manifest | import("@vltpkg/types").Override<Manifest, import("@vltpkg/types").NormalizedFields>>;
|
|
126
|
+
/**
|
|
127
|
+
* The packument for `spec`, with every version the registry has. Callers
|
|
128
|
+
* that only pick a manifest out of it go through `#packument` instead,
|
|
129
|
+
* which can ask for the prerelease-free one.
|
|
130
|
+
*/
|
|
73
131
|
packument(spec: Spec | string, options?: PackageInfoClientRequestOptions): Promise<Packument>;
|
|
74
132
|
resolve(spec: Spec | string, options?: PackageInfoClientRequestOptions): Promise<Resolution>;
|
|
75
133
|
}
|
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, brotliTarballUrl, integrityHex, tarballFormat, } 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;
|
|
@@ -99,6 +178,18 @@ export class PackageInfoClient {
|
|
|
99
178
|
});
|
|
100
179
|
return this.#tarPoolPromise;
|
|
101
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* The absolute URL of a version's Brotli (`.tar.br`) tarball, or
|
|
183
|
+
* undefined when the registry advertised none, advertised one this
|
|
184
|
+
* client does not use (see {@link brotliTarballUrl}), or
|
|
185
|
+
* `--no-brotli-tarballs` is set. `dist.tarball` is already absolute
|
|
186
|
+
* here -- see `absolutizeTarballs`.
|
|
187
|
+
*/
|
|
188
|
+
#brotliTarball(tarball, alternates) {
|
|
189
|
+
if (this.options['brotli-tarballs'] === false)
|
|
190
|
+
return undefined;
|
|
191
|
+
return brotliTarballUrl(tarball, alternates);
|
|
192
|
+
}
|
|
102
193
|
constructor(options = {}) {
|
|
103
194
|
this.options = options;
|
|
104
195
|
this.#projectRoot = options.projectRoot || process.cwd();
|
|
@@ -116,6 +207,10 @@ export class PackageInfoClient {
|
|
|
116
207
|
packageJson: this.packageJson,
|
|
117
208
|
});
|
|
118
209
|
this.#cachePath = options.cache ?? xdg.cache();
|
|
210
|
+
this.#storeRoot = options.storeRoot ?? storeRoot(this.#cachePath);
|
|
211
|
+
const linker = options['store-linker'];
|
|
212
|
+
this.#storeLinker =
|
|
213
|
+
linker && storeLinkers.has(linker) ? linker : 'unpack';
|
|
119
214
|
// optionally create its cache directory if it doesn't exist
|
|
120
215
|
void mkdir(pathResolve(this.#cachePath, 'package-info'), {
|
|
121
216
|
recursive: true,
|
|
@@ -124,13 +219,22 @@ export class PackageInfoClient {
|
|
|
124
219
|
async extract(spec, target, options = {}) {
|
|
125
220
|
if (typeof spec === 'string')
|
|
126
221
|
spec = Spec.parse(spec, this.options);
|
|
127
|
-
const { from = this.#projectRoot, integrity, resolved, fromLockfile = false, } = options;
|
|
222
|
+
const { from = this.#projectRoot, integrity, resolved, fromLockfile = false, installScripts = false, } = options;
|
|
128
223
|
const f = spec.final;
|
|
129
224
|
// If the caller already provides both integrity and resolved
|
|
130
|
-
// (from lockfile or prior resolution), skip re-resolving.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
225
|
+
// (from lockfile or prior resolution), skip re-resolving. A
|
|
226
|
+
// `.tar.br` needs only the URL: its hash is not knowable before the
|
|
227
|
+
// download, so the graph can never hand one over, and `Repr-Digest`
|
|
228
|
+
// is what pins it -- `required`, so a registry that serves the
|
|
229
|
+
// alternate unlabelled is rejected rather than trusted.
|
|
230
|
+
const brotli = !!resolved && tarballFormat(resolved) === 'brotli';
|
|
231
|
+
const r = resolved && (integrity || brotli) ?
|
|
232
|
+
{
|
|
233
|
+
resolved,
|
|
234
|
+
integrity,
|
|
235
|
+
spec,
|
|
236
|
+
...(brotli && !integrity ? { digestRequired: true } : {}),
|
|
237
|
+
}
|
|
134
238
|
: await this.resolve(spec, options);
|
|
135
239
|
switch (f.type) {
|
|
136
240
|
case 'git': {
|
|
@@ -160,16 +264,58 @@ export class PackageInfoClient {
|
|
|
160
264
|
// fallthrough if a remote tarball url present
|
|
161
265
|
}
|
|
162
266
|
case 'registry': {
|
|
267
|
+
const pool = await this.getTarPool();
|
|
268
|
+
// brotli bytes carry no signature of their own, so every unpack
|
|
269
|
+
// below has to be told; gzip and raw tar are sniffed.
|
|
270
|
+
const format = tarballFormat(r.resolved);
|
|
271
|
+
// git tarballs keep the unpack path
|
|
272
|
+
const hex = f.type === 'registry' && this.#storeLinker !== 'unpack' ?
|
|
273
|
+
integrityHex(r.integrity)
|
|
274
|
+
: undefined;
|
|
275
|
+
const copy = this.#storeLinker === 'copy' || installScripts;
|
|
276
|
+
if (hex) {
|
|
277
|
+
if (debug.enabled && !this.#storeHitRateLogged) {
|
|
278
|
+
this.#storeHitRateLogged = true;
|
|
279
|
+
process.once('beforeExit', this.#logStoreHitRate);
|
|
280
|
+
}
|
|
281
|
+
const linked = await pool.linkFromStore(pathResolve(this.#storeRoot, hex), target, { copy });
|
|
282
|
+
if (linked) {
|
|
283
|
+
const { how, index } = linked;
|
|
284
|
+
this.#storeHits[how]++;
|
|
285
|
+
// a copy is no link: report it as a cache hit
|
|
286
|
+
logRequest(r.resolved, how === 'link' ? 'store' : 'cache');
|
|
287
|
+
return {
|
|
288
|
+
...r,
|
|
289
|
+
manifest: index.manifest,
|
|
290
|
+
// implies scripts, so most packages skip the scan
|
|
291
|
+
bindingGyp: index.scripts &&
|
|
292
|
+
index.files.some(f => f[0] === 'binding.gyp'),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
this.#storeMisses++;
|
|
296
|
+
}
|
|
163
297
|
// if the tarball is already on disk, unpack it straight from
|
|
164
298
|
// the cache file: it never has to be held in the client's
|
|
165
299
|
// in-memory cache. anything unexpected falls through to the
|
|
166
300
|
// fetch path, which throws its own error if the body is
|
|
167
301
|
// genuinely bad.
|
|
168
|
-
const
|
|
302
|
+
const rc = await this.getRegistryClient();
|
|
303
|
+
const cached = rc.cachedBody(r.resolved, {
|
|
304
|
+
integrity: r.integrity,
|
|
305
|
+
});
|
|
169
306
|
if (cached) {
|
|
170
307
|
try {
|
|
171
|
-
await
|
|
308
|
+
await pool.unpack(cached.body, target, format);
|
|
172
309
|
logRequest(r.resolved, 'cache');
|
|
310
|
+
r.integrity ??= cached.integrity;
|
|
311
|
+
// a warm install writes nothing to the cache, so queue the
|
|
312
|
+
// store miss here or an existing cache never converges,
|
|
313
|
+
// and a gzipped body with no store link, to unzip it.
|
|
314
|
+
// (a brotli body is never rewritten: cache-unzip only
|
|
315
|
+
// un-gzips, and leaving it compressed keeps the cache small.)
|
|
316
|
+
if (hex || cached.gzip) {
|
|
317
|
+
rc.queueForStore(cached.key, r.integrity);
|
|
318
|
+
}
|
|
173
319
|
return r;
|
|
174
320
|
}
|
|
175
321
|
catch (er) {
|
|
@@ -184,6 +330,7 @@ export class PackageInfoClient {
|
|
|
184
330
|
const response = await (await this.getRegistryClient()).request(r.resolved, {
|
|
185
331
|
integrity: r.integrity,
|
|
186
332
|
trustIntegrity,
|
|
333
|
+
verifyDigest: r.digestRequired ? 'required' : true,
|
|
187
334
|
...(useCache === false ? { useCache } : {}),
|
|
188
335
|
});
|
|
189
336
|
if (response.statusCode !== 200) {
|
|
@@ -195,33 +342,42 @@ export class PackageInfoClient {
|
|
|
195
342
|
response,
|
|
196
343
|
});
|
|
197
344
|
}
|
|
345
|
+
// checkIntegrity() hashes the body unless its url is trusted
|
|
346
|
+
const verified = !trustIntegrity &&
|
|
347
|
+
response.checkIntegrity({ spec, url: resolved });
|
|
198
348
|
// if it's not trusted already, but valid, start trusting
|
|
199
|
-
if (
|
|
200
|
-
response.checkIntegrity({ spec, url: resolved })) {
|
|
349
|
+
if (verified) {
|
|
201
350
|
this.#trustedIntegrities.set(r.resolved, response.integrity);
|
|
202
351
|
}
|
|
203
352
|
const buf = response.buffer();
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
wanted: r.integrity,
|
|
221
|
-
found: computed,
|
|
222
|
-
});
|
|
353
|
+
if (r.integrity) {
|
|
354
|
+
// hash only network bytes checkIntegrity() skipped (trusted
|
|
355
|
+
// url). cache bodies were verified when cached.
|
|
356
|
+
if (!verified && !fromLockfile && !response.fromCache) {
|
|
357
|
+
const hash = createHash('sha512');
|
|
358
|
+
hash.update(buf);
|
|
359
|
+
const computed = `sha512-${hash.digest('base64')}`;
|
|
360
|
+
if (computed !== r.integrity) {
|
|
361
|
+
throw error('Tarball integrity check failed', {
|
|
362
|
+
code: 'EINTEGRITY',
|
|
363
|
+
spec,
|
|
364
|
+
url: r.resolved,
|
|
365
|
+
wanted: r.integrity,
|
|
366
|
+
found: computed,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
223
369
|
}
|
|
224
|
-
|
|
370
|
+
}
|
|
371
|
+
else if (response.fromCache) {
|
|
372
|
+
// the hash the body was stored under
|
|
373
|
+
r.integrity = response.integrity;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
// no dist.integrity: the registry client checked the body
|
|
377
|
+
// against the digest the registry sent with it (verifyDigest
|
|
378
|
+
// above). hand the hash back so the lockfile pins it from
|
|
379
|
+
// now on
|
|
380
|
+
r.integrity = response.integrityActual;
|
|
225
381
|
}
|
|
226
382
|
return buf;
|
|
227
383
|
};
|
|
@@ -245,7 +401,7 @@ export class PackageInfoClient {
|
|
|
245
401
|
}
|
|
246
402
|
}
|
|
247
403
|
try {
|
|
248
|
-
await
|
|
404
|
+
await pool.unpack(buf, target, format);
|
|
249
405
|
}
|
|
250
406
|
catch (er) {
|
|
251
407
|
throw this.#resolveError(spec, options, 'tar unpack failed', { cause: er });
|
|
@@ -421,6 +577,11 @@ export class PackageInfoClient {
|
|
|
421
577
|
...options,
|
|
422
578
|
integrity,
|
|
423
579
|
trustIntegrity,
|
|
580
|
+
// no dist.integrity: checked against the digest the registry
|
|
581
|
+
// sent with it instead, before the client caches it
|
|
582
|
+
verifyDigest: this.#vltPackuments.has(`${f.registry}${f.name}`) ?
|
|
583
|
+
'required'
|
|
584
|
+
: true,
|
|
424
585
|
...(useCache === false ? { useCache } : {}),
|
|
425
586
|
});
|
|
426
587
|
if (response.statusCode !== 200) {
|
|
@@ -429,19 +590,19 @@ export class PackageInfoClient {
|
|
|
429
590
|
`version may have been unpublished, or the registry may ` +
|
|
430
591
|
`be misconfigured or unreachable.`, { response, url: tarball });
|
|
431
592
|
}
|
|
593
|
+
const verified = !trustIntegrity &&
|
|
594
|
+
response.checkIntegrity({ spec, url: tarball });
|
|
432
595
|
// if we don't already trust it, but it's valid, start
|
|
433
596
|
// trusting it
|
|
434
|
-
if (
|
|
435
|
-
response.checkIntegrity({ spec, url: tarball })) {
|
|
597
|
+
if (verified) {
|
|
436
598
|
this.#trustedIntegrities.set(tarball, response.integrity);
|
|
437
599
|
}
|
|
438
600
|
const buf = response.buffer();
|
|
439
|
-
// Same as extract()
|
|
440
|
-
if (integrity && !response.fromCache) {
|
|
601
|
+
// Same as extract()
|
|
602
|
+
if (integrity && !verified && !response.fromCache) {
|
|
441
603
|
const hash = createHash('sha512');
|
|
442
604
|
hash.update(buf);
|
|
443
605
|
const computed = `sha512-${hash.digest('base64')}`;
|
|
444
|
-
/* c8 ignore start - defense-in-depth (see extract) */
|
|
445
606
|
if (computed !== integrity) {
|
|
446
607
|
throw error('Tarball integrity check failed', {
|
|
447
608
|
code: 'EINTEGRITY',
|
|
@@ -451,7 +612,6 @@ export class PackageInfoClient {
|
|
|
451
612
|
found: computed,
|
|
452
613
|
});
|
|
453
614
|
}
|
|
454
|
-
/* c8 ignore stop */
|
|
455
615
|
}
|
|
456
616
|
return buf;
|
|
457
617
|
};
|
|
@@ -545,7 +705,8 @@ export class PackageInfoClient {
|
|
|
545
705
|
try {
|
|
546
706
|
// Cache file exists, read and return it. Freshness is
|
|
547
707
|
// tracked via the file's mtime, so the file content is
|
|
548
|
-
//
|
|
708
|
+
// the manifest, plus a marker when it came from a vlt
|
|
709
|
+
// packument, and can be returned as parsed.
|
|
549
710
|
const [st, cached] = await Promise.all([
|
|
550
711
|
stat(cachePath),
|
|
551
712
|
readFile(cachePath, 'utf8'),
|
|
@@ -561,13 +722,18 @@ export class PackageInfoClient {
|
|
|
561
722
|
void unlink(cachePath).catch(() => { });
|
|
562
723
|
throw new Error('manifest cache expired');
|
|
563
724
|
}
|
|
725
|
+
// the tarball digest stays required across processes
|
|
726
|
+
if (json.__VLT_PACKUMENT) {
|
|
727
|
+
this.#vltPackuments.add(`${f.registry}${f.name}`);
|
|
728
|
+
delete json.__VLT_PACKUMENT;
|
|
729
|
+
}
|
|
564
730
|
return json;
|
|
565
731
|
}
|
|
566
732
|
catch {
|
|
567
733
|
// Cache miss, fetch from packument
|
|
568
734
|
}
|
|
569
735
|
}
|
|
570
|
-
const mani = pickManifest(await this
|
|
736
|
+
const mani = pickManifest(await this.#packument(f, options, this.#stable(f)), spec, options);
|
|
571
737
|
if (!mani)
|
|
572
738
|
throw this.#resolveError(spec, options);
|
|
573
739
|
// Cache the manifest data. Skip paths already written this
|
|
@@ -575,7 +741,8 @@ export class PackageInfoClient {
|
|
|
575
741
|
// and racing writers for the same path.
|
|
576
742
|
if (cachePath && !this.#manifestWritePaths.has(cachePath)) {
|
|
577
743
|
this.#manifestWritePaths.add(cachePath);
|
|
578
|
-
|
|
744
|
+
const vlt = this.#vltPackuments.has(`${f.registry}${f.name}`);
|
|
745
|
+
void this.#writeManifestCache(cachePath, JSON.stringify(vlt ? { ...mani, __VLT_PACKUMENT: true } : mani));
|
|
579
746
|
}
|
|
580
747
|
return mani;
|
|
581
748
|
}
|
|
@@ -647,7 +814,15 @@ export class PackageInfoClient {
|
|
|
647
814
|
}
|
|
648
815
|
}
|
|
649
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* The packument for `spec`, with every version the registry has. Callers
|
|
819
|
+
* that only pick a manifest out of it go through `#packument` instead,
|
|
820
|
+
* which can ask for the prerelease-free one.
|
|
821
|
+
*/
|
|
650
822
|
async packument(spec, options = {}) {
|
|
823
|
+
return this.#packument(spec, options, false);
|
|
824
|
+
}
|
|
825
|
+
async #packument(spec, options, stable) {
|
|
651
826
|
if (typeof spec === 'string')
|
|
652
827
|
spec = Spec.parse(spec, this.options);
|
|
653
828
|
const f = spec.final;
|
|
@@ -684,10 +859,21 @@ export class PackageInfoClient {
|
|
|
684
859
|
const { registry, name } = f;
|
|
685
860
|
if (!registry)
|
|
686
861
|
throw noRegistryError(spec);
|
|
687
|
-
//
|
|
688
|
-
|
|
862
|
+
// a full representation neither reads nor feeds the coalescing
|
|
863
|
+
// map, which holds the abbreviated one
|
|
864
|
+
if (options.full)
|
|
865
|
+
return this.#fetchPackument(spec, options, new URL(name, registry));
|
|
866
|
+
// The only representation component of the coalescing key is
|
|
867
|
+
// `?stable`; everything else about the request is fixed (see
|
|
868
|
+
// #fetchPackument).
|
|
869
|
+
const fullKey = `${registry}${name}`;
|
|
870
|
+
const packumentKey = stable ? `${fullKey}?stable` : fullKey;
|
|
689
871
|
const forced = isMovingSelector(f);
|
|
690
|
-
const inflight = this.#packumentPromises.get(packumentKey)
|
|
872
|
+
const inflight = this.#packumentPromises.get(packumentKey) ??
|
|
873
|
+
// the full packument is a superset of the stable one, so an
|
|
874
|
+
// in-flight full request answers a stable ask too, and a package
|
|
875
|
+
// wanted both ways is still fetched once
|
|
876
|
+
(stable ? this.#packumentPromises.get(fullKey) : undefined);
|
|
691
877
|
// a moving selector must not ride along on a non-forced request:
|
|
692
878
|
// that one can settle to a fresh-but-stale cache hit, which is
|
|
693
879
|
// exactly what forceRevalidate exists to avoid. the other
|
|
@@ -696,7 +882,8 @@ export class PackageInfoClient {
|
|
|
696
882
|
// when both shapes are asked for at once.
|
|
697
883
|
if (inflight && (!forced || inflight.forced))
|
|
698
884
|
return inflight.promise;
|
|
699
|
-
|
|
885
|
+
// `?stable` is a distinct URL, so it gets its own disk cache entry
|
|
886
|
+
const pakuURL = new URL(stable ? `${name}?stable` : name, registry);
|
|
700
887
|
const promise = this.#fetchPackument(spec, options, pakuURL);
|
|
701
888
|
const record = { promise, forced };
|
|
702
889
|
this.#packumentPromises.set(packumentKey, record);
|
|
@@ -735,8 +922,12 @@ export class PackageInfoClient {
|
|
|
735
922
|
// packument request must use this same accept header, and the SWR
|
|
736
923
|
// revalidation child re-requests the representation it was given.
|
|
737
924
|
const response = await (await this.getRegistryClient()).request(pakuURL, {
|
|
738
|
-
headers: {
|
|
739
|
-
|
|
925
|
+
headers: {
|
|
926
|
+
accept: options.full ? 'application/json' : PACKUMENT_ACCEPT,
|
|
927
|
+
},
|
|
928
|
+
...(useCache === false || options.full ?
|
|
929
|
+
{ useCache: false }
|
|
930
|
+
: {}),
|
|
740
931
|
// costs a conditional GET per moving selector on an otherwise warm
|
|
741
932
|
// cache, install included. 304s are cheap but not free; the
|
|
742
933
|
// alternative is serving a dist tag that moved (#1656).
|
|
@@ -750,8 +941,9 @@ export class PackageInfoClient {
|
|
|
750
941
|
response,
|
|
751
942
|
});
|
|
752
943
|
}
|
|
944
|
+
let paku;
|
|
753
945
|
try {
|
|
754
|
-
|
|
946
|
+
paku = response.json();
|
|
755
947
|
}
|
|
756
948
|
catch (er) {
|
|
757
949
|
if (useCache !== false) {
|
|
@@ -759,6 +951,14 @@ export class PackageInfoClient {
|
|
|
759
951
|
}
|
|
760
952
|
throw er;
|
|
761
953
|
}
|
|
954
|
+
const { registry, name } = spec.final;
|
|
955
|
+
if (response.contentType.startsWith(VLT_PACKUMENT_MIME)) {
|
|
956
|
+
this.#vltPackuments.add(`${registry}${name}`);
|
|
957
|
+
}
|
|
958
|
+
/* c8 ignore next - registry specs always have a registry */
|
|
959
|
+
if (registry)
|
|
960
|
+
absolutizeTarballs(paku, registry);
|
|
961
|
+
return paku;
|
|
762
962
|
}
|
|
763
963
|
async resolve(spec, options = {}) {
|
|
764
964
|
const memoKey = String(spec);
|
|
@@ -798,14 +998,22 @@ export class PackageInfoClient {
|
|
|
798
998
|
case 'registry': {
|
|
799
999
|
const mani = await this.manifest(spec, options);
|
|
800
1000
|
if (mani.dist) {
|
|
801
|
-
const { integrity, tarball, signatures } = mani.dist;
|
|
1001
|
+
const { integrity, tarball, signatures, alternates } = mani.dist;
|
|
802
1002
|
if (tarball) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1003
|
+
// A `.tar.br` is a distinct artifact, not a re-encoding of the
|
|
1004
|
+
// `.tgz`: it hashes differently, so `dist.integrity` and the
|
|
1005
|
+
// signatures over it describe the wrong bytes and are dropped.
|
|
1006
|
+
// What pins it instead is its own `Repr-Digest`, which the
|
|
1007
|
+
// registry that advertised it always sends -- hence `required`.
|
|
1008
|
+
const brotli = this.#brotliTarball(tarball, alternates);
|
|
1009
|
+
const r = brotli ?
|
|
1010
|
+
{ resolved: brotli, spec, digestRequired: true }
|
|
1011
|
+
: { resolved: tarball, integrity, signatures, spec };
|
|
1012
|
+
if (!brotli &&
|
|
1013
|
+
!integrity &&
|
|
1014
|
+
this.#vltPackuments.has(`${f.registry}${f.name}`)) {
|
|
1015
|
+
r.digestRequired = true;
|
|
1016
|
+
}
|
|
809
1017
|
this.#resolutions.set(memoKey, r);
|
|
810
1018
|
return r;
|
|
811
1019
|
}
|
|
@@ -883,7 +1091,13 @@ export class PackageInfoClient {
|
|
|
883
1091
|
// error resolving
|
|
884
1092
|
#resolveError(spec, options = {}, message = 'Could not resolve', extra = {}) {
|
|
885
1093
|
const { from = this.#projectRoot } = options;
|
|
886
|
-
|
|
1094
|
+
// Every registry failure here carries its response, so the advice is
|
|
1095
|
+
// attached once rather than at each throw site. The cast is because
|
|
1096
|
+
// `response` is typed loosely enough to include a `fetch` Response.
|
|
1097
|
+
const advice = spec?.final.type === 'registry' ?
|
|
1098
|
+
tokenRefusalAdvice(extra.response, extra.url)
|
|
1099
|
+
: undefined;
|
|
1100
|
+
const er = error(advice ? `${message}\n⚠️ ${advice}` : message, {
|
|
887
1101
|
code: 'ERESOLVE',
|
|
888
1102
|
spec,
|
|
889
1103
|
from,
|
|
@@ -892,3 +1106,16 @@ export class PackageInfoClient {
|
|
|
892
1106
|
return er;
|
|
893
1107
|
}
|
|
894
1108
|
}
|
|
1109
|
+
// vlt packuments carry dist.tarball relative to the registry base
|
|
1110
|
+
// (`foo/-/foo-1.0.0.tgz`), the form conventionalRegistryTarball builds.
|
|
1111
|
+
// Nothing downstream sees a relative URL: the manifest cache, the graph
|
|
1112
|
+
// and the lockfile all get the absolute one.
|
|
1113
|
+
const absolutizeTarballs = (paku, registry) => {
|
|
1114
|
+
const base = registry.endsWith('/') ? registry : registry + '/';
|
|
1115
|
+
for (const { dist } of Object.values(paku.versions)) {
|
|
1116
|
+
const tarball = dist?.tarball;
|
|
1117
|
+
if (tarball && !/^[a-z][a-z0-9+.-]*:/i.test(tarball)) {
|
|
1118
|
+
dist.tarball = String(new URL(tarball, base));
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
};
|
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.3.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.3.0",
|
|
17
|
+
"@vltpkg/git": "1.3.0",
|
|
18
|
+
"@vltpkg/output": "1.3.0",
|
|
19
|
+
"@vltpkg/package-json": "1.3.0",
|
|
20
|
+
"@vltpkg/pick-manifest": "1.3.0",
|
|
21
|
+
"@vltpkg/registry-client": "1.3.0",
|
|
22
|
+
"@vltpkg/spec": "1.3.0",
|
|
23
|
+
"@vltpkg/tar": "1.3.0",
|
|
24
|
+
"@vltpkg/types": "1.3.0",
|
|
25
|
+
"@vltpkg/workspaces": "1.3.0",
|
|
26
|
+
"@vltpkg/xdg": "1.3.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.3.0",
|
|
36
|
+
"@vltpkg/vlt-json": "1.3.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"
|