@vltpkg/package-info 1.0.8 → 1.0.10
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.js +63 -12
- package/package.json +14 -13
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { error } from '@vltpkg/error-cause';
|
|
2
2
|
import { clone, resolve as gitResolve, revs } from '@vltpkg/git';
|
|
3
|
+
import { logRequest } from '@vltpkg/output';
|
|
3
4
|
import { PackageJson } from '@vltpkg/package-json';
|
|
4
5
|
import { pickManifest } from '@vltpkg/pick-manifest';
|
|
5
6
|
import { Spec } from '@vltpkg/spec';
|
|
@@ -10,8 +11,10 @@ import { XDG } from '@vltpkg/xdg';
|
|
|
10
11
|
import { createHash, randomBytes } from 'node:crypto';
|
|
11
12
|
import { mkdir, readFile, rm, stat, symlink, unlink, writeFile, } from 'node:fs/promises';
|
|
12
13
|
import { basename, dirname, resolve as pathResolve, relative, } from 'node:path';
|
|
14
|
+
import { debuglog } from 'node:util';
|
|
13
15
|
import { create as tarC } from 'tar';
|
|
14
16
|
import { rename } from "./rename.js";
|
|
17
|
+
const debug = debuglog('vlt');
|
|
15
18
|
const xdg = new XDG('vlt');
|
|
16
19
|
export const delimiter = '~';
|
|
17
20
|
// the maximum duration of a manifest cache file
|
|
@@ -23,6 +26,15 @@ const manifestCacheMaxAge = 5 * 60 * 1000;
|
|
|
23
26
|
const noRegistryError = (spec) => error('No registry configured to resolve this spec. Set "registry" in ' +
|
|
24
27
|
'vlt.json, pass --registry, or run `vlt login --registry=<url>`. ' +
|
|
25
28
|
'See https://docs.vlt.sh/cli', { code: 'ECONFIG', spec });
|
|
29
|
+
/**
|
|
30
|
+
* A selector that can point at a different version tomorrow: a dist tag,
|
|
31
|
+
* or a range matching anything (`*`, empty string). Manifest results for
|
|
32
|
+
* these are not cached to disk, and packument requests for them force a
|
|
33
|
+
* revalidation of the registry client's cache entry.
|
|
34
|
+
*
|
|
35
|
+
* Takes a *final* spec (`spec.final`), same as `pickManifest` sees.
|
|
36
|
+
*/
|
|
37
|
+
const isMovingSelector = (f) => !!(f.distTag || f.range?.isAny);
|
|
26
38
|
export class PackageInfoClient {
|
|
27
39
|
#registryClient;
|
|
28
40
|
#projectRoot;
|
|
@@ -36,7 +48,9 @@ export class PackageInfoClient {
|
|
|
36
48
|
#cachePath;
|
|
37
49
|
// In-flight coalescing key is `${registry}${name}` — no representation
|
|
38
50
|
// component. Safe only because every caller requests the same full
|
|
39
|
-
// packument (see #fetchPackument).
|
|
51
|
+
// packument (see #fetchPackument). The one thing that does vary per
|
|
52
|
+
// caller is forceRevalidate, so record it and let a moving selector
|
|
53
|
+
// reuse a forced promise but never a non-forced one (see packument()).
|
|
40
54
|
#packumentPromises = new Map();
|
|
41
55
|
// unique temp file names for atomic manifest cache writes
|
|
42
56
|
#manifestWriteRandom = randomBytes(6).toString('hex');
|
|
@@ -129,6 +143,25 @@ export class PackageInfoClient {
|
|
|
129
143
|
// fallthrough if a remote tarball url present
|
|
130
144
|
}
|
|
131
145
|
case 'registry': {
|
|
146
|
+
// if the tarball is already on disk, unpack it straight from
|
|
147
|
+
// the cache file: it never has to be held in the client's
|
|
148
|
+
// in-memory cache. anything unexpected falls through to the
|
|
149
|
+
// fetch path, which throws its own error if the body is
|
|
150
|
+
// genuinely bad.
|
|
151
|
+
const cached = (await this.getRegistryClient()).cachedBody(r.resolved, { integrity: r.integrity });
|
|
152
|
+
if (cached) {
|
|
153
|
+
try {
|
|
154
|
+
await (await this.getTarPool()).unpack(cached.body, target);
|
|
155
|
+
logRequest(r.resolved, 'cache');
|
|
156
|
+
return r;
|
|
157
|
+
}
|
|
158
|
+
catch (er) {
|
|
159
|
+
// a systematically failing fast path (every entry still
|
|
160
|
+
// gzipped, EACCES, ENOSPC...) doubles the IO of the fetch
|
|
161
|
+
// path, so leave a trace behind. NODE_DEBUG=vlt to see it.
|
|
162
|
+
debug('cached tarball unpack failed: %s: %s', cached.path, er);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
132
165
|
const fetchTarball = async (useCache) => {
|
|
133
166
|
const trustIntegrity = this.#trustedIntegrities.get(r.resolved) === r.integrity;
|
|
134
167
|
const response = await (await this.getRegistryClient()).request(r.resolved, {
|
|
@@ -244,7 +277,7 @@ export class PackageInfoClient {
|
|
|
244
277
|
const st = await stat(path);
|
|
245
278
|
if (st.isFile()) {
|
|
246
279
|
try {
|
|
247
|
-
await (await this.getTarPool()).
|
|
280
|
+
await (await this.getTarPool()).unpackFile(path, target);
|
|
248
281
|
}
|
|
249
282
|
catch (er) {
|
|
250
283
|
throw this.#resolveError(spec, options, 'tar unpack failed', { cause: er });
|
|
@@ -311,11 +344,9 @@ export class PackageInfoClient {
|
|
|
311
344
|
if (options.before) {
|
|
312
345
|
return;
|
|
313
346
|
}
|
|
314
|
-
//
|
|
315
|
-
// matches any range (such as a semver range of `*` or empty string)
|
|
316
|
-
// then we skip caching
|
|
347
|
+
// a moving selector's result is variable, so don't cache it
|
|
317
348
|
const f = spec.final;
|
|
318
|
-
if (f
|
|
349
|
+
if (isMovingSelector(f)) {
|
|
319
350
|
return;
|
|
320
351
|
}
|
|
321
352
|
const key = this.#manifestCacheKey(f, options);
|
|
@@ -586,7 +617,7 @@ export class PackageInfoClient {
|
|
|
586
617
|
const s = spec;
|
|
587
618
|
return await this.#tmpdir(async (dir) => {
|
|
588
619
|
try {
|
|
589
|
-
await (await this.getTarPool()).
|
|
620
|
+
await (await this.getTarPool()).unpackFile(path, dir);
|
|
590
621
|
}
|
|
591
622
|
catch (er) {
|
|
592
623
|
throw this.#resolveError(s, options, 'tar unpack failed', { cause: er });
|
|
@@ -638,16 +669,30 @@ export class PackageInfoClient {
|
|
|
638
669
|
throw noRegistryError(spec);
|
|
639
670
|
// Coalescing key has no representation component (see #fetchPackument).
|
|
640
671
|
const packumentKey = `${registry}${name}`;
|
|
672
|
+
const forced = isMovingSelector(f);
|
|
641
673
|
const inflight = this.#packumentPromises.get(packumentKey);
|
|
642
|
-
|
|
643
|
-
|
|
674
|
+
// a moving selector must not ride along on a non-forced request:
|
|
675
|
+
// that one can settle to a fresh-but-stale cache hit, which is
|
|
676
|
+
// exactly what forceRevalidate exists to avoid. the other
|
|
677
|
+
// direction is fine -- a forced result is never staler.
|
|
678
|
+
// costs at most one extra concurrent GET for the same packument
|
|
679
|
+
// when both shapes are asked for at once.
|
|
680
|
+
if (inflight && (!forced || inflight.forced))
|
|
681
|
+
return inflight.promise;
|
|
644
682
|
const pakuURL = new URL(name, registry);
|
|
645
683
|
const promise = this.#fetchPackument(spec, options, pakuURL);
|
|
646
|
-
|
|
647
|
-
|
|
684
|
+
const record = { promise, forced };
|
|
685
|
+
this.#packumentPromises.set(packumentKey, record);
|
|
686
|
+
// Clean up once settled so we don't leak memory, unless a forced
|
|
687
|
+
// request has since taken the slot over.
|
|
648
688
|
// Use .then/.catch instead of .finally to avoid creating
|
|
649
689
|
// an unhandled rejection from the derived promise.
|
|
650
|
-
|
|
690
|
+
const clear = () => {
|
|
691
|
+
if (this.#packumentPromises.get(packumentKey) === record) {
|
|
692
|
+
this.#packumentPromises.delete(packumentKey);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
promise.then(clear, clear);
|
|
651
696
|
return promise;
|
|
652
697
|
}
|
|
653
698
|
}
|
|
@@ -681,6 +726,12 @@ export class PackageInfoClient {
|
|
|
681
726
|
const response = await (await this.getRegistryClient()).request(pakuURL, {
|
|
682
727
|
headers: { accept: 'application/json' },
|
|
683
728
|
...(useCache === false ? { useCache } : {}),
|
|
729
|
+
// costs a conditional GET per moving selector on an otherwise warm
|
|
730
|
+
// cache, install included. 304s are cheap but not free; the
|
|
731
|
+
// alternative is serving a dist tag that moved (#1656).
|
|
732
|
+
...(isMovingSelector(spec.final) ?
|
|
733
|
+
{ forceRevalidate: true }
|
|
734
|
+
: {}),
|
|
684
735
|
});
|
|
685
736
|
if (response.statusCode !== 200) {
|
|
686
737
|
throw this.#resolveError(spec, options, 'failed to fetch packument', {
|
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.
|
|
4
|
+
"version": "1.0.10",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vltpkg/vltpkg.git",
|
|
@@ -13,16 +13,17 @@
|
|
|
13
13
|
"url": "http://vlt.sh"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@vltpkg/error-cause": "1.0.
|
|
17
|
-
"@vltpkg/git": "1.0.
|
|
18
|
-
"@vltpkg/
|
|
19
|
-
"@vltpkg/
|
|
20
|
-
"@vltpkg/
|
|
21
|
-
"@vltpkg/
|
|
22
|
-
"@vltpkg/
|
|
23
|
-
"@vltpkg/
|
|
24
|
-
"@vltpkg/
|
|
25
|
-
"@vltpkg/
|
|
16
|
+
"@vltpkg/error-cause": "1.0.10",
|
|
17
|
+
"@vltpkg/git": "1.0.10",
|
|
18
|
+
"@vltpkg/output": "1.0.10",
|
|
19
|
+
"@vltpkg/package-json": "1.0.10",
|
|
20
|
+
"@vltpkg/pick-manifest": "1.0.10",
|
|
21
|
+
"@vltpkg/registry-client": "1.0.10",
|
|
22
|
+
"@vltpkg/spec": "1.0.10",
|
|
23
|
+
"@vltpkg/tar": "1.0.10",
|
|
24
|
+
"@vltpkg/types": "1.0.10",
|
|
25
|
+
"@vltpkg/workspaces": "1.0.10",
|
|
26
|
+
"@vltpkg/xdg": "1.0.10",
|
|
26
27
|
"ssri": "^13.0.0",
|
|
27
28
|
"tar": "^7.5.2"
|
|
28
29
|
},
|
|
@@ -31,8 +32,8 @@
|
|
|
31
32
|
"@types/node": "^22.19.2",
|
|
32
33
|
"@types/pacote": "^11.1.8",
|
|
33
34
|
"@vltpkg/benchmark": "0.0.0",
|
|
34
|
-
"@vltpkg/cache-unzip": "1.0.
|
|
35
|
-
"@vltpkg/vlt-json": "1.0.
|
|
35
|
+
"@vltpkg/cache-unzip": "1.0.10",
|
|
36
|
+
"@vltpkg/vlt-json": "1.0.10",
|
|
36
37
|
"eslint": "^9.39.1",
|
|
37
38
|
"pacote": "^21.0.4",
|
|
38
39
|
"prettier": "^3.7.4",
|