@pnpm/resolving.npm-resolver 1102.1.0 → 1102.1.2
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/lib/clearMeta.d.ts +16 -0
- package/lib/clearMeta.js +55 -0
- package/lib/fetch.d.ts +8 -1
- package/lib/fetch.js +42 -2
- package/lib/index.d.ts +3 -0
- package/lib/index.js +114 -11
- package/lib/memoizeFetchMetadata.d.ts +24 -0
- package/lib/memoizeFetchMetadata.js +37 -0
- package/lib/parseBareSpecifier.js +9 -3
- package/lib/pickPackage.d.ts +8 -0
- package/lib/pickPackage.js +49 -43
- package/package.json +18 -17
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PackageMeta } from '@pnpm/resolving.registry.types';
|
|
2
|
+
/**
|
|
3
|
+
* Reduces a package metadata document to the abbreviated field set that the
|
|
4
|
+
* resolver actually reads, dropping install-irrelevant fields (scripts,
|
|
5
|
+
* exports, readme, custom `_`-prefixed fields, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Used in two places:
|
|
8
|
+
* - The network layer (`fetch.ts`) normalizes a registry that ignored the
|
|
9
|
+
* abbreviated `Accept` header and returned a full document.
|
|
10
|
+
* - The resolver (`pickPackage.ts`) narrows a deliberately-fetched full
|
|
11
|
+
* document into the `filterMetadata` cache slot.
|
|
12
|
+
*
|
|
13
|
+
* Null-safe on `versions` so it can be called on an unpublished package (no
|
|
14
|
+
* versions), which the abbreviated path can reach.
|
|
15
|
+
*/
|
|
16
|
+
export declare function clearMeta(pkg: PackageMeta): PackageMeta;
|
package/lib/clearMeta.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { pick } from 'ramda';
|
|
2
|
+
// The list taken from https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
|
|
3
|
+
// with the addition of 'libc'
|
|
4
|
+
const ABBREVIATED_VERSION_FIELDS = [
|
|
5
|
+
'name',
|
|
6
|
+
'version',
|
|
7
|
+
'bin',
|
|
8
|
+
'directories',
|
|
9
|
+
'devDependencies',
|
|
10
|
+
'optionalDependencies',
|
|
11
|
+
'dependencies',
|
|
12
|
+
'peerDependencies',
|
|
13
|
+
'dist',
|
|
14
|
+
'engines',
|
|
15
|
+
'peerDependenciesMeta',
|
|
16
|
+
'cpu',
|
|
17
|
+
'os',
|
|
18
|
+
'libc',
|
|
19
|
+
'deprecated',
|
|
20
|
+
'bundleDependencies',
|
|
21
|
+
'bundledDependencies',
|
|
22
|
+
'hasInstallScript',
|
|
23
|
+
'_npmUser',
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Reduces a package metadata document to the abbreviated field set that the
|
|
27
|
+
* resolver actually reads, dropping install-irrelevant fields (scripts,
|
|
28
|
+
* exports, readme, custom `_`-prefixed fields, etc.).
|
|
29
|
+
*
|
|
30
|
+
* Used in two places:
|
|
31
|
+
* - The network layer (`fetch.ts`) normalizes a registry that ignored the
|
|
32
|
+
* abbreviated `Accept` header and returned a full document.
|
|
33
|
+
* - The resolver (`pickPackage.ts`) narrows a deliberately-fetched full
|
|
34
|
+
* document into the `filterMetadata` cache slot.
|
|
35
|
+
*
|
|
36
|
+
* Null-safe on `versions` so it can be called on an unpublished package (no
|
|
37
|
+
* versions), which the abbreviated path can reach.
|
|
38
|
+
*/
|
|
39
|
+
export function clearMeta(pkg) {
|
|
40
|
+
// A null prototype so that a registry-controlled version key named
|
|
41
|
+
// `__proto__` becomes a regular own property instead of mutating the
|
|
42
|
+
// prototype of the map (js/prototype-polluting-assignment).
|
|
43
|
+
const versions = Object.create(null);
|
|
44
|
+
for (const [version, info] of Object.entries(pkg.versions ?? {})) {
|
|
45
|
+
versions[version] = pick(ABBREVIATED_VERSION_FIELDS, info);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
name: pkg.name,
|
|
49
|
+
'dist-tags': pkg['dist-tags'],
|
|
50
|
+
versions,
|
|
51
|
+
time: pkg.time,
|
|
52
|
+
modified: pkg.modified,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=clearMeta.js.map
|
package/lib/fetch.d.ts
CHANGED
|
@@ -3,7 +3,14 @@ import type { FetchFromRegistry, RetryTimeoutOptions } from '@pnpm/fetching.type
|
|
|
3
3
|
import type { PackageMeta } from '@pnpm/resolving.registry.types';
|
|
4
4
|
export interface FetchMetadataResult {
|
|
5
5
|
meta: PackageMeta;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The raw registry response body, used only to mirror the response to disk
|
|
8
|
+
* without re-serializing `meta`. A fresh fetch always sets it, but it
|
|
9
|
+
* reaches only the caller that initiated the request: the phase-long memo
|
|
10
|
+
* cache holds a body-less clone (see memoizeFetchMetadata.ts), so cache
|
|
11
|
+
* hits see `undefined` and the cache never pins the body.
|
|
12
|
+
*/
|
|
13
|
+
jsonText: string | undefined;
|
|
7
14
|
etag?: string;
|
|
8
15
|
notModified?: false;
|
|
9
16
|
}
|
package/lib/fetch.js
CHANGED
|
@@ -5,6 +5,15 @@ import { FetchError, PnpmError, redactUrlCredentials, } from '@pnpm/error';
|
|
|
5
5
|
import { globalWarn } from '@pnpm/logger';
|
|
6
6
|
import * as retry from '@zkochan/retry';
|
|
7
7
|
import semver from 'semver';
|
|
8
|
+
import { clearMeta } from './clearMeta.js';
|
|
9
|
+
/**
|
|
10
|
+
* Content type of an abbreviated (install-oriented) package metadata document.
|
|
11
|
+
* A spec-compliant registry echoes this in the response `Content-Type` when it
|
|
12
|
+
* honors the abbreviated `Accept` header. Its absence signals that the registry
|
|
13
|
+
* ignored the header and served the full document instead.
|
|
14
|
+
* https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md
|
|
15
|
+
*/
|
|
16
|
+
const ABBREVIATED_META_CONTENT_TYPE = 'application/vnd.npm.install-v1+json';
|
|
8
17
|
export class RegistryResponseError extends FetchError {
|
|
9
18
|
pkgName;
|
|
10
19
|
constructor(request, response, pkgName) {
|
|
@@ -125,8 +134,7 @@ export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHe
|
|
|
125
134
|
globalWarn(`Request took ${elapsedMs}ms: ${uri}`);
|
|
126
135
|
}
|
|
127
136
|
resolve({
|
|
128
|
-
meta,
|
|
129
|
-
jsonText,
|
|
137
|
+
...normalizeAbbreviatedResponse({ fullMetadata, meta, jsonText, response }),
|
|
130
138
|
etag: response.headers.get('etag') ?? undefined,
|
|
131
139
|
});
|
|
132
140
|
}
|
|
@@ -156,6 +164,38 @@ export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHe
|
|
|
156
164
|
});
|
|
157
165
|
});
|
|
158
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* When the resolver asked for abbreviated metadata but the registry ignored the
|
|
169
|
+
* `Accept` header and returned the full document (detected via the response
|
|
170
|
+
* `Content-Type`), strip it down to the abbreviated field set so downstream
|
|
171
|
+
* consumers — the in-memory cache, the on-disk mirror, and the resolver — never
|
|
172
|
+
* carry the megabytes of install-irrelevant data (scripts, exports, readme,
|
|
173
|
+
* custom fields) that a full document contains.
|
|
174
|
+
*
|
|
175
|
+
* Registries that honor the header (e.g. the npm registry) echo the abbreviated
|
|
176
|
+
* `Content-Type`, so this is a no-op for them: no re-serialization, no field
|
|
177
|
+
* stripping — the happy path pays nothing.
|
|
178
|
+
*/
|
|
179
|
+
function normalizeAbbreviatedResponse({ fullMetadata, meta, jsonText, response }) {
|
|
180
|
+
if (fullMetadata)
|
|
181
|
+
return { meta, jsonText };
|
|
182
|
+
if (parseMediaType(response.headers.get('content-type')) === ABBREVIATED_META_CONTENT_TYPE)
|
|
183
|
+
return { meta, jsonText };
|
|
184
|
+
const normalized = clearMeta(meta);
|
|
185
|
+
return { meta: normalized, jsonText: JSON.stringify(normalized) };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Extracts the media type from a `Content-Type` header value, dropping
|
|
189
|
+
* parameters such as `; charset=utf-8`. Media types are case-insensitive
|
|
190
|
+
* (RFC 9110 §8.3.1), so the result is lowercased for comparison.
|
|
191
|
+
*/
|
|
192
|
+
function parseMediaType(contentType) {
|
|
193
|
+
if (contentType == null)
|
|
194
|
+
return undefined;
|
|
195
|
+
const semicolonIndex = contentType.indexOf(';');
|
|
196
|
+
const mediaType = semicolonIndex === -1 ? contentType : contentType.slice(0, semicolonIndex);
|
|
197
|
+
return mediaType.trim().toLowerCase();
|
|
198
|
+
}
|
|
159
199
|
function toUri(pkgName, registry) {
|
|
160
200
|
let encodedName;
|
|
161
201
|
if (pkgName[0] === '@') {
|
package/lib/index.d.ts
CHANGED
|
@@ -94,6 +94,8 @@ export interface ResolveFromNpmContext {
|
|
|
94
94
|
name?: string;
|
|
95
95
|
version?: string;
|
|
96
96
|
}) => Promise<DependencyManifest | undefined>;
|
|
97
|
+
/** Deduplicates the held-back-update warning per `(name, picked, preferred)`. */
|
|
98
|
+
warnedHeldBackUpdates: Set<string>;
|
|
97
99
|
}
|
|
98
100
|
export type ResolveFromNpmOptions = {
|
|
99
101
|
alwaysTryWorkspacePackages?: boolean;
|
|
@@ -109,6 +111,7 @@ export type ResolveFromNpmOptions = {
|
|
|
109
111
|
preferredVersions?: PreferredVersions;
|
|
110
112
|
preferWorkspacePackages?: boolean;
|
|
111
113
|
update?: false | 'compatible' | 'latest';
|
|
114
|
+
updateRequested?: boolean;
|
|
112
115
|
updateChecksums?: boolean;
|
|
113
116
|
injectWorkspacePackages?: boolean;
|
|
114
117
|
calcSpecifier?: boolean;
|
package/lib/index.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
3
3
|
import { PnpmError } from '@pnpm/error';
|
|
4
|
+
import { globalWarn } from '@pnpm/logger';
|
|
5
|
+
import { EXISTING_VERSION_SELECTOR_WEIGHT, } from '@pnpm/resolving.resolver-base';
|
|
4
6
|
import { storeIndexKey } from '@pnpm/store.index';
|
|
5
7
|
import { readPkgFromCafs, } from '@pnpm/worker';
|
|
6
8
|
import { resolveWorkspaceRange } from '@pnpm/workspace.range-resolver';
|
|
7
9
|
import { LRUCache } from 'lru-cache';
|
|
8
10
|
import normalize from 'normalize-path';
|
|
9
|
-
import pMemoize, { pMemoizeClear } from 'p-memoize';
|
|
10
11
|
import { clone } from 'ramda';
|
|
11
12
|
import semver from 'semver';
|
|
12
13
|
import ssri from 'ssri';
|
|
13
14
|
import versionSelectorType from 'version-selector-type';
|
|
14
15
|
import { fetchMetadataFromFromRegistry, RegistryResponseError } from './fetch.js';
|
|
16
|
+
import { memoizeFetchMetadata } from './memoizeFetchMetadata.js';
|
|
15
17
|
import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
|
|
16
18
|
import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, parseNamedRegistrySpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
|
|
17
19
|
import { pickPackage, } from './pickPackage.js';
|
|
@@ -73,9 +75,7 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
|
|
|
73
75
|
timeout: opts.timeout ?? 60000,
|
|
74
76
|
fetchWarnTimeoutMs: opts.fetchWarnTimeoutMs ?? 10 * 1000, // 10 sec
|
|
75
77
|
};
|
|
76
|
-
const fetch =
|
|
77
|
-
cacheKey: (...args) => JSON.stringify(args),
|
|
78
|
-
});
|
|
78
|
+
const { fetch, clear: clearFetchCache } = memoizeFetchMetadata(fetchMetadataFromFromRegistry.bind(null, fetchOpts));
|
|
79
79
|
// Track ownership so `clearCache()` below only wipes the in-memory
|
|
80
80
|
// cache when this factory created it. A caller-supplied
|
|
81
81
|
// `opts.metaCache` may be shared with another resolver instance (or
|
|
@@ -130,6 +130,7 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
|
|
|
130
130
|
namedRegistryNames,
|
|
131
131
|
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
|
|
132
132
|
peekManifestFromStore,
|
|
133
|
+
warnedHeldBackUpdates: new Set(),
|
|
133
134
|
};
|
|
134
135
|
const boundResolveFromNpm = resolveNpm.bind(null, ctx);
|
|
135
136
|
const boundResolveFromJsr = resolveJsr.bind(null, ctx);
|
|
@@ -146,10 +147,100 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
|
|
|
146
147
|
if (ownsMetaCache && 'clear' in metaCache && typeof metaCache.clear === 'function') {
|
|
147
148
|
metaCache.clear();
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
+
clearFetchCache();
|
|
150
151
|
},
|
|
151
152
|
};
|
|
152
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The preferred-version selectors to hand the package picker for `pkgName`.
|
|
156
|
+
*
|
|
157
|
+
* When this package is the user's update target (`updateRequested`), the
|
|
158
|
+
* lockfile's contribution to its selectors is removed so the target
|
|
159
|
+
* re-resolves exactly the way a fresh install would after its lockfile
|
|
160
|
+
* entries were deleted. Everything a fresh install applies is preserved:
|
|
161
|
+
* manifest pins, the versions propagated down the dependency chain, and the
|
|
162
|
+
* negative-weight `range` penalties that `pnpm audit --fix` injects to steer
|
|
163
|
+
* resolution away from vulnerable versions.
|
|
164
|
+
*/
|
|
165
|
+
function preferredVersionSelectorsFor(opts, pkgName) {
|
|
166
|
+
const selectors = opts.preferredVersions?.[pkgName];
|
|
167
|
+
if (!opts.updateRequested)
|
|
168
|
+
return selectors;
|
|
169
|
+
return stripLockfileVersionPins(selectors);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Remove the lockfile-derived part of the selectors: the concrete pins
|
|
173
|
+
* `getPreferredVersionsFromLockfileAndManifests` seeds at
|
|
174
|
+
* `EXISTING_VERSION_SELECTOR_WEIGHT` — added onto the manifest weight when a
|
|
175
|
+
* manifest entry pins the same version, so the lockfile weight is subtracted
|
|
176
|
+
* rather than the selector dropped, leaving the manifest contribution in
|
|
177
|
+
* effect. Selectors a fresh install would also apply (manifest pins,
|
|
178
|
+
* chain-propagated versions, `range`/`tag` selectors) pass through unchanged.
|
|
179
|
+
* Returns `undefined` when nothing remains.
|
|
180
|
+
*/
|
|
181
|
+
function stripLockfileVersionPins(selectors) {
|
|
182
|
+
if (selectors == null)
|
|
183
|
+
return undefined;
|
|
184
|
+
let kept;
|
|
185
|
+
for (const [selector, value] of Object.entries(selectors)) {
|
|
186
|
+
let keptValue = value;
|
|
187
|
+
if (typeof value !== 'string' && value.selectorType === 'version' && value.weight >= EXISTING_VERSION_SELECTOR_WEIGHT) {
|
|
188
|
+
const manifestWeight = value.weight - EXISTING_VERSION_SELECTOR_WEIGHT;
|
|
189
|
+
if (manifestWeight <= 0)
|
|
190
|
+
continue;
|
|
191
|
+
keptValue = { selectorType: 'version', weight: manifestWeight };
|
|
192
|
+
}
|
|
193
|
+
// Null-prototype: selector keys come from manifests and the lockfile,
|
|
194
|
+
// and a dist-tag named `__proto__` is a valid selector key.
|
|
195
|
+
kept ??= Object.create(null);
|
|
196
|
+
kept[selector] = keptValue;
|
|
197
|
+
}
|
|
198
|
+
return kept;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* During a targeted update the picker still honors the preferred versions a
|
|
202
|
+
* fresh install would apply (manifest pins and versions propagated down the
|
|
203
|
+
* dependency chain), so the target can legitimately settle below the highest
|
|
204
|
+
* version its range admits. Surface that once per package: reaching the
|
|
205
|
+
* newer version everywhere is an override's job, not an update's.
|
|
206
|
+
*
|
|
207
|
+
* The baseline for "held back" is the pick with only the non-pin selectors
|
|
208
|
+
* applied — `range`/`tag` selectors such as the `pnpm audit --fix`
|
|
209
|
+
* vulnerability penalties steer the baseline too, so the warning never
|
|
210
|
+
* recommends a version those selectors avoid.
|
|
211
|
+
*
|
|
212
|
+
* The recommended override is scoped to the declared range being resolved
|
|
213
|
+
* (`name@<range>`), so applying it can never violate any consumer's range:
|
|
214
|
+
* only declarations of exactly this range match the selector, and the
|
|
215
|
+
* recommended version satisfies it by construction.
|
|
216
|
+
*/
|
|
217
|
+
function warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedVersion) {
|
|
218
|
+
if (!opts.updateRequested || spec.type !== 'range')
|
|
219
|
+
return;
|
|
220
|
+
const selectors = preferredVersionSelectorsFor(opts, spec.name);
|
|
221
|
+
if (selectors == null)
|
|
222
|
+
return;
|
|
223
|
+
let nonPinSelectors;
|
|
224
|
+
for (const [selector, value] of Object.entries(selectors)) {
|
|
225
|
+
if ((typeof value === 'string' ? value : value.selectorType) === 'version')
|
|
226
|
+
continue;
|
|
227
|
+
// Null-prototype for the same reason as in `stripLockfileVersionPins`.
|
|
228
|
+
nonPinSelectors ??= Object.create(null);
|
|
229
|
+
nonPinSelectors[selector] = value;
|
|
230
|
+
}
|
|
231
|
+
const preferred = pickVersionByVersionRange({
|
|
232
|
+
meta,
|
|
233
|
+
versionRange: spec.fetchSpec,
|
|
234
|
+
preferredVersionSelectors: nonPinSelectors,
|
|
235
|
+
});
|
|
236
|
+
if (preferred == null || preferred === pickedVersion)
|
|
237
|
+
return;
|
|
238
|
+
const key = `${spec.name}@${spec.fetchSpec}:${pickedVersion}<${preferred}`;
|
|
239
|
+
if (ctx.warnedHeldBackUpdates.has(key))
|
|
240
|
+
return;
|
|
241
|
+
ctx.warnedHeldBackUpdates.add(key);
|
|
242
|
+
globalWarn(`"${spec.name}@${spec.fetchSpec}" was updated to ${pickedVersion}, not ${preferred}, to match the version preferred by your manifests and already installed dependencies. To use ${preferred}, add an override to pnpm-workspace.yaml: overrides: { "${spec.name}@${spec.fetchSpec}": "${preferred}" }`);
|
|
243
|
+
}
|
|
153
244
|
function isNpmSpec(query, defaultRegistry) {
|
|
154
245
|
const { alias, bareSpecifier } = query.wantedDependency;
|
|
155
246
|
if (!bareSpecifier)
|
|
@@ -285,7 +376,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
285
376
|
publishedByExclude: opts.publishedByExclude,
|
|
286
377
|
authHeaderValue,
|
|
287
378
|
dryRun: opts.dryRun === true,
|
|
288
|
-
preferredVersionSelectors: opts
|
|
379
|
+
preferredVersionSelectors: preferredVersionSelectorsFor(opts, spec.name),
|
|
289
380
|
registry,
|
|
290
381
|
includeLatestTag: opts.update === 'latest',
|
|
291
382
|
updateChecksums: opts.updateChecksums,
|
|
@@ -306,8 +397,13 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
306
397
|
pinnedVersion: opts.pinnedVersion,
|
|
307
398
|
});
|
|
308
399
|
}
|
|
309
|
-
catch {
|
|
310
|
-
//
|
|
400
|
+
catch (workspaceErr) {
|
|
401
|
+
// When the registry doesn't have the package and the workspace has it
|
|
402
|
+
// only at non-matching versions, the mismatch error (which lists the
|
|
403
|
+
// available workspace versions) is more actionable than the raw 404.
|
|
404
|
+
if (err.code === 'ERR_PNPM_FETCH_404' && workspaceErr.code === 'ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE') {
|
|
405
|
+
throw workspaceErr;
|
|
406
|
+
}
|
|
311
407
|
}
|
|
312
408
|
}
|
|
313
409
|
throw err;
|
|
@@ -328,8 +424,13 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
328
424
|
pinnedVersion: opts.pinnedVersion,
|
|
329
425
|
});
|
|
330
426
|
}
|
|
331
|
-
catch {
|
|
332
|
-
//
|
|
427
|
+
catch (workspaceErr) {
|
|
428
|
+
// Neither the registry nor the workspace has a matching version; the
|
|
429
|
+
// workspace mismatch error carries the available local versions,
|
|
430
|
+
// which is the actionable detail here.
|
|
431
|
+
if (workspaceErr.code === 'ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE') {
|
|
432
|
+
throw workspaceErr;
|
|
433
|
+
}
|
|
333
434
|
}
|
|
334
435
|
}
|
|
335
436
|
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
|
|
@@ -370,6 +471,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
370
471
|
};
|
|
371
472
|
}
|
|
372
473
|
}
|
|
474
|
+
warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedPackage.version);
|
|
373
475
|
const id = `${pickedPackage.name}@${pickedPackage.version}`;
|
|
374
476
|
const resolution = {
|
|
375
477
|
integrity: getIntegrity(pickedPackage.dist),
|
|
@@ -489,7 +591,7 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
|
|
|
489
591
|
publishedByExclude: opts.publishedByExclude,
|
|
490
592
|
authHeaderValue,
|
|
491
593
|
dryRun: opts.dryRun === true,
|
|
492
|
-
preferredVersionSelectors: opts
|
|
594
|
+
preferredVersionSelectors: preferredVersionSelectorsFor(opts, spec.name),
|
|
493
595
|
registry,
|
|
494
596
|
includeLatestTag: opts.update === 'latest',
|
|
495
597
|
updateChecksums: opts.updateChecksums,
|
|
@@ -498,6 +600,7 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
|
|
|
498
600
|
if (pickedPackage == null) {
|
|
499
601
|
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
|
|
500
602
|
}
|
|
603
|
+
warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedPackage.version);
|
|
501
604
|
const resolution = {
|
|
502
605
|
integrity: getIntegrity(pickedPackage.dist),
|
|
503
606
|
tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FetchMetadataNotModifiedResult, FetchMetadataOptions, FetchMetadataResult } from './fetch.js';
|
|
2
|
+
export type FetchMetadata = (pkgName: string, opts: FetchMetadataOptions) => Promise<FetchMetadataResult | FetchMetadataNotModifiedResult>;
|
|
3
|
+
export interface MemoizedFetchMetadata {
|
|
4
|
+
fetch: FetchMetadata;
|
|
5
|
+
clear: () => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Memoizes metadata fetches for the whole resolution phase (cleared via
|
|
9
|
+
* `clear`, see `clearResolutionCache`), deduplicating concurrent and repeat
|
|
10
|
+
* requests for the same package.
|
|
11
|
+
*
|
|
12
|
+
* Unlike plain memoization, the cache holds a body-less clone of each result:
|
|
13
|
+
* `jsonText` — the raw registry response body, up to tens of MB for a popular
|
|
14
|
+
* package — reaches only the caller that initiated the fetch, which is the
|
|
15
|
+
* caller that writes the disk mirror. A phase-long cache that kept the bodies
|
|
16
|
+
* would pin hundreds of MB on large cold-cache graphs. A cache-hit caller
|
|
17
|
+
* that also writes the mirror falls back to `JSON.stringify(meta)` in
|
|
18
|
+
* `prepareJsonForDisk`, which is equivalent on read: `loadMeta` re-derives
|
|
19
|
+
* `etag` from the headers line.
|
|
20
|
+
*
|
|
21
|
+
* A rejected fetch is evicted so a transient network failure is retried by
|
|
22
|
+
* the next request instead of being cached for the rest of the phase.
|
|
23
|
+
*/
|
|
24
|
+
export declare function memoizeFetchMetadata(fetch: FetchMetadata): MemoizedFetchMetadata;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memoizes metadata fetches for the whole resolution phase (cleared via
|
|
3
|
+
* `clear`, see `clearResolutionCache`), deduplicating concurrent and repeat
|
|
4
|
+
* requests for the same package.
|
|
5
|
+
*
|
|
6
|
+
* Unlike plain memoization, the cache holds a body-less clone of each result:
|
|
7
|
+
* `jsonText` — the raw registry response body, up to tens of MB for a popular
|
|
8
|
+
* package — reaches only the caller that initiated the fetch, which is the
|
|
9
|
+
* caller that writes the disk mirror. A phase-long cache that kept the bodies
|
|
10
|
+
* would pin hundreds of MB on large cold-cache graphs. A cache-hit caller
|
|
11
|
+
* that also writes the mirror falls back to `JSON.stringify(meta)` in
|
|
12
|
+
* `prepareJsonForDisk`, which is equivalent on read: `loadMeta` re-derives
|
|
13
|
+
* `etag` from the headers line.
|
|
14
|
+
*
|
|
15
|
+
* A rejected fetch is evicted so a transient network failure is retried by
|
|
16
|
+
* the next request instead of being cached for the rest of the phase.
|
|
17
|
+
*/
|
|
18
|
+
export function memoizeFetchMetadata(fetch) {
|
|
19
|
+
const cache = new Map();
|
|
20
|
+
return {
|
|
21
|
+
fetch: (pkgName, opts) => {
|
|
22
|
+
const key = JSON.stringify([pkgName, opts]);
|
|
23
|
+
const cached = cache.get(key);
|
|
24
|
+
if (cached != null)
|
|
25
|
+
return cached;
|
|
26
|
+
const pending = fetch(pkgName, opts);
|
|
27
|
+
const bodiless = pending.then((result) => result.notModified ? result : { ...result, jsonText: undefined });
|
|
28
|
+
bodiless.catch(() => cache.delete(key));
|
|
29
|
+
cache.set(key, bodiless);
|
|
30
|
+
return pending;
|
|
31
|
+
},
|
|
32
|
+
clear: () => {
|
|
33
|
+
cache.clear();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=memoizeFetchMetadata.js.map
|
|
@@ -2,6 +2,7 @@ import { PnpmError } from '@pnpm/error';
|
|
|
2
2
|
import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser';
|
|
3
3
|
import { parseNpmTarballUrl } from 'parse-npm-tarball-url';
|
|
4
4
|
import semver from 'semver';
|
|
5
|
+
import validateNpmPackageName from 'validate-npm-package-name';
|
|
5
6
|
import getVersionSelectorType from 'version-selector-type';
|
|
6
7
|
export function parseBareSpecifier(bareSpecifier, alias, defaultTag, registry) {
|
|
7
8
|
let name = alias;
|
|
@@ -69,6 +70,9 @@ export const BUILTIN_NAMED_REGISTRIES = Object.freeze({
|
|
|
69
70
|
// Parses a named-registry specifier of the shape `<alias>:<body>` into a
|
|
70
71
|
// RegistryPackageSpec. Returns `null` when the specifier does not use one of
|
|
71
72
|
// the configured aliases, so the caller can fall through to other resolvers.
|
|
73
|
+
// Throws INVALID_NAMED_REGISTRY_PACKAGE_NAME when the alias matches but the
|
|
74
|
+
// package name is malformed (missing or empty scope/name segments, path
|
|
75
|
+
// separators inside the name).
|
|
72
76
|
// Supported shapes:
|
|
73
77
|
// - `<alias>:[@<owner>/]<name>[@<version_selector>]`
|
|
74
78
|
// - `<alias>:<version_selector>` paired with a package alias
|
|
@@ -100,9 +104,6 @@ export function parseNamedRegistrySpecifierToRegistryPackageSpec(rawSpecifier, k
|
|
|
100
104
|
pkgName = body.substring(0, index);
|
|
101
105
|
versionSelector = body.substring(index + '@'.length);
|
|
102
106
|
}
|
|
103
|
-
if (pkgName.indexOf('/') === -1 || pkgName.endsWith('/')) {
|
|
104
|
-
throw new PnpmError('INVALID_NAMED_REGISTRY_PACKAGE_NAME', `The package name '${pkgName}' in named registry '${registryName}:' is invalid`);
|
|
105
|
-
}
|
|
106
107
|
}
|
|
107
108
|
else if (packageAlias?.startsWith('@')) {
|
|
108
109
|
// `<alias>:<tag>` paired with a scoped alias — body is a version
|
|
@@ -124,6 +125,11 @@ export function parseNamedRegistrySpecifierToRegistryPackageSpec(rawSpecifier, k
|
|
|
124
125
|
if (!pkgName)
|
|
125
126
|
return null;
|
|
126
127
|
}
|
|
128
|
+
// The name is used in registry URLs and metadata cache file paths, so
|
|
129
|
+
// anything that is not a valid npm package name must never make it through.
|
|
130
|
+
if (!validateNpmPackageName(pkgName).validForOldPackages) {
|
|
131
|
+
throw new PnpmError('INVALID_NAMED_REGISTRY_PACKAGE_NAME', `The package name '${pkgName}' in named registry '${registryName}:' is invalid`);
|
|
132
|
+
}
|
|
127
133
|
const selector = getVersionSelectorType(versionSelector ?? defaultTag);
|
|
128
134
|
if (selector == null)
|
|
129
135
|
return null;
|
package/lib/pickPackage.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ import type { FetchMetadataNotModifiedResult, FetchMetadataResult } from './fetc
|
|
|
3
3
|
import type { RegistryPackageSpec } from './parseBareSpecifier.js';
|
|
4
4
|
import { type PickPackageFromMetaOptions } from './pickPackageFromMeta.js';
|
|
5
5
|
export interface PackageMetaCache {
|
|
6
|
+
/**
|
|
7
|
+
* Must return the same object reference that `set` stored for the key: the
|
|
8
|
+
* resolver tracks whether a cached packument was validated against the
|
|
9
|
+
* registry by object identity (see `unverifiedDiskPackuments`). In a cache
|
|
10
|
+
* that clones or deserializes on read, that provenance is lost and recovery
|
|
11
|
+
* degrades — a stale disk-promoted entry that can't satisfy a spec fails
|
|
12
|
+
* the pick instead of falling through to the registry.
|
|
13
|
+
*/
|
|
6
14
|
get: (key: string) => PackageMeta | undefined;
|
|
7
15
|
set: (key: string, meta: PackageMeta) => void;
|
|
8
16
|
has: (key: string) => boolean;
|
package/lib/pickPackage.js
CHANGED
|
@@ -8,9 +8,9 @@ import { globalWarn, logger } from '@pnpm/logger';
|
|
|
8
8
|
import getRegistryName from 'encode-registry';
|
|
9
9
|
import pLimit, {} from 'p-limit';
|
|
10
10
|
import { fastPathTemp as pathTemp } from 'path-temp';
|
|
11
|
-
import { pick } from 'ramda';
|
|
12
11
|
import { renameOverwrite } from 'rename-overwrite';
|
|
13
12
|
import semver from 'semver';
|
|
13
|
+
import { clearMeta } from './clearMeta.js';
|
|
14
14
|
import { pickLowestVersionByVersionRange, pickPackageFromMeta, pickVersionByVersionRange, } from './pickPackageFromMeta.js';
|
|
15
15
|
import { toRaw } from './toRaw.js';
|
|
16
16
|
/**
|
|
@@ -104,6 +104,29 @@ function pickMatchingVersionFinal(pickerOpts, spec, meta) {
|
|
|
104
104
|
throw err;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Packuments promoted into the in-memory cache straight from the on-disk
|
|
109
|
+
* mirror, without registry validation. The mirror may predate versions the
|
|
110
|
+
* registry has, so when a cache hit on such an entry can't satisfy the
|
|
111
|
+
* requested spec (and the resolver isn't offline), `pickPackage` falls
|
|
112
|
+
* through to the regular flow — a conditional registry request — instead of
|
|
113
|
+
* failing the pick, exactly as it would have before the entry was promoted.
|
|
114
|
+
* Network-fetched and 304-revalidated packuments are never in this set, so
|
|
115
|
+
* hits on them keep returning directly even when the pick fails (the caller
|
|
116
|
+
* then falls back to workspace packages or reports no matching version).
|
|
117
|
+
*/
|
|
118
|
+
const unverifiedDiskPackuments = new WeakSet();
|
|
119
|
+
/**
|
|
120
|
+
* Promote a packument parsed from the on-disk mirror into the in-memory
|
|
121
|
+
* cache, so repeat resolutions of the same package (common across a large
|
|
122
|
+
* dependency graph) don't re-read and re-parse the mirror. The entry is
|
|
123
|
+
* remembered as disk-sourced (see {@link unverifiedDiskPackuments}) because it
|
|
124
|
+
* never went through registry validation.
|
|
125
|
+
*/
|
|
126
|
+
function cacheDiskLoadedMeta(metaCache, cacheKey, meta) {
|
|
127
|
+
unverifiedDiskPackuments.add(meta);
|
|
128
|
+
metaCache.set(cacheKey, meta);
|
|
129
|
+
}
|
|
107
130
|
export async function pickPackage(ctx, spec, opts) {
|
|
108
131
|
opts = opts || {};
|
|
109
132
|
const pickerOpts = {
|
|
@@ -147,21 +170,31 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
147
170
|
: persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
|
|
148
171
|
ctx.metaCache.set(cacheKey, metaForCache);
|
|
149
172
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaForCache);
|
|
174
|
+
if (pickedPackage != null || ctx.offline === true || !unverifiedDiskPackuments.has(metaForCache)) {
|
|
175
|
+
return {
|
|
176
|
+
meta: metaForCache,
|
|
177
|
+
pickedPackage,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Disk-promoted meta that can't satisfy the spec: fall through and
|
|
181
|
+
// revalidate against the registry (see unverifiedDiskPackuments).
|
|
154
182
|
}
|
|
155
183
|
return runLimited(pkgMirror, async (limit) => {
|
|
156
184
|
let metaCachedInStore;
|
|
157
185
|
if (ctx.offline === true || ctx.preferOffline === true || opts.pickLowestVersion) {
|
|
158
186
|
metaCachedInStore = await limit(async () => loadMeta(pkgMirror));
|
|
159
187
|
if (ctx.offline) {
|
|
160
|
-
if (metaCachedInStore != null)
|
|
188
|
+
if (metaCachedInStore != null) {
|
|
189
|
+
// maybeUpgradeAbbreviatedMetaForReleaseAge short-circuits when
|
|
190
|
+
// offline, so a later in-memory cache hit returns this same meta
|
|
191
|
+
// without any network access.
|
|
192
|
+
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
|
|
161
193
|
return {
|
|
162
194
|
meta: metaCachedInStore,
|
|
163
195
|
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore),
|
|
164
196
|
};
|
|
197
|
+
}
|
|
165
198
|
throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`);
|
|
166
199
|
}
|
|
167
200
|
if (metaCachedInStore != null) {
|
|
@@ -178,6 +211,14 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
178
211
|
}
|
|
179
212
|
const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore);
|
|
180
213
|
if (pickedPackage) {
|
|
214
|
+
// A cache hit re-runs maybeUpgradeAbbreviatedMetaForReleaseAge, so
|
|
215
|
+
// serving this meta from memory can't bypass the release-age
|
|
216
|
+
// upgrade. When the upgrade branch above already cached the
|
|
217
|
+
// registry-validated upgraded meta, don't overwrite it with a
|
|
218
|
+
// disk-sourced marking.
|
|
219
|
+
if (upgrade.upgradedFrom == null) {
|
|
220
|
+
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
|
|
221
|
+
}
|
|
181
222
|
return {
|
|
182
223
|
meta: metaCachedInStore,
|
|
183
224
|
pickedPackage,
|
|
@@ -193,7 +234,7 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
193
234
|
try {
|
|
194
235
|
const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore);
|
|
195
236
|
if (pickedPackage) {
|
|
196
|
-
ctx.metaCache
|
|
237
|
+
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
|
|
197
238
|
return {
|
|
198
239
|
meta: metaCachedInStore,
|
|
199
240
|
pickedPackage,
|
|
@@ -303,7 +344,7 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
303
344
|
if (!isModifiedValid || modifiedDate > opts.publishedBy) {
|
|
304
345
|
// Save the abbreviated metadata to the abbreviated cache before re-fetching full.
|
|
305
346
|
if (!opts.dryRun) {
|
|
306
|
-
const abbreviatedJson = prepareJsonForDisk(
|
|
347
|
+
const abbreviatedJson = prepareJsonForDisk(resultToSave.meta, resultToSave.etag, resultToSave.jsonText);
|
|
307
348
|
// Fire-and-forget save to the abbreviated cache path (pkgMirror).
|
|
308
349
|
runLimited(pkgMirror, (limit) => limit(async () => {
|
|
309
350
|
try {
|
|
@@ -431,41 +472,6 @@ function persistUpgradedMeta(ctx, pkgMirror, upgradedFrom) {
|
|
|
431
472
|
}));
|
|
432
473
|
return metaForCache;
|
|
433
474
|
}
|
|
434
|
-
function clearMeta(pkg) {
|
|
435
|
-
const versions = {};
|
|
436
|
-
for (const [version, info] of Object.entries(pkg.versions)) {
|
|
437
|
-
// The list taken from https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
|
|
438
|
-
// with the addition of 'libc'
|
|
439
|
-
versions[version] = pick([
|
|
440
|
-
'name',
|
|
441
|
-
'version',
|
|
442
|
-
'bin',
|
|
443
|
-
'directories',
|
|
444
|
-
'devDependencies',
|
|
445
|
-
'optionalDependencies',
|
|
446
|
-
'dependencies',
|
|
447
|
-
'peerDependencies',
|
|
448
|
-
'dist',
|
|
449
|
-
'engines',
|
|
450
|
-
'peerDependenciesMeta',
|
|
451
|
-
'cpu',
|
|
452
|
-
'os',
|
|
453
|
-
'libc',
|
|
454
|
-
'deprecated',
|
|
455
|
-
'bundleDependencies',
|
|
456
|
-
'bundledDependencies',
|
|
457
|
-
'hasInstallScript',
|
|
458
|
-
'_npmUser',
|
|
459
|
-
], info);
|
|
460
|
-
}
|
|
461
|
-
return {
|
|
462
|
-
name: pkg.name,
|
|
463
|
-
'dist-tags': pkg['dist-tags'],
|
|
464
|
-
versions,
|
|
465
|
-
time: pkg.time,
|
|
466
|
-
modified: pkg.modified,
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
475
|
export function encodePkgName(pkgName) {
|
|
470
476
|
if (pkgName !== pkgName.toLowerCase()) {
|
|
471
477
|
return `${pkgName}_${createHexHash(pkgName)}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/resolving.npm-resolver",
|
|
3
|
-
"version": "1102.1.
|
|
3
|
+
"version": "1102.1.2",
|
|
4
4
|
"description": "Resolver for npm-hosted packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"lru-cache": "^11.5.0",
|
|
35
35
|
"normalize-path": "^3.0.0",
|
|
36
36
|
"p-limit": "^7.3.0",
|
|
37
|
-
"p-memoize": "8.0.0",
|
|
38
37
|
"parse-npm-tarball-url": "^5.0.0",
|
|
39
38
|
"path-temp": "^3.0.0",
|
|
40
39
|
"ramda": "npm:@pnpm/ramda@0.28.1",
|
|
@@ -42,28 +41,29 @@
|
|
|
42
41
|
"semver": "^7.8.4",
|
|
43
42
|
"semver-utils": "^1.1.4",
|
|
44
43
|
"ssri": "13.0.1",
|
|
44
|
+
"validate-npm-package-name": "7.0.2",
|
|
45
45
|
"version-selector-type": "^3.0.0",
|
|
46
46
|
"@pnpm/config.pick-registry-for-package": "1100.0.9",
|
|
47
|
-
"@pnpm/constants": "1100.0.0",
|
|
48
|
-
"@pnpm/config.version-policy": "1100.1.6",
|
|
49
|
-
"@pnpm/core-loggers": "1100.2.1",
|
|
50
47
|
"@pnpm/crypto.hash": "1100.0.1",
|
|
48
|
+
"@pnpm/error": "1100.0.1",
|
|
49
|
+
"@pnpm/config.version-policy": "1100.1.6",
|
|
51
50
|
"@pnpm/fetching.types": "1100.0.2",
|
|
52
|
-
"@pnpm/resolving.
|
|
53
|
-
"@pnpm/fs.graceful-fs": "1100.1.0",
|
|
51
|
+
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.9",
|
|
54
52
|
"@pnpm/resolving.registry.types": "1100.1.3",
|
|
55
|
-
"@pnpm/resolving.
|
|
56
|
-
"@pnpm/
|
|
57
|
-
"@pnpm/store.cafs": "1100.1.11",
|
|
53
|
+
"@pnpm/resolving.jsr-specifier-parser": "1100.0.2",
|
|
54
|
+
"@pnpm/resolving.resolver-base": "1100.5.1",
|
|
58
55
|
"@pnpm/types": "1101.3.2",
|
|
56
|
+
"@pnpm/fs.graceful-fs": "1100.1.0",
|
|
59
57
|
"@pnpm/workspace.range-resolver": "1100.0.2",
|
|
60
|
-
"@pnpm/
|
|
61
|
-
"@pnpm/
|
|
62
|
-
"@pnpm/
|
|
58
|
+
"@pnpm/store.index": "1100.2.1",
|
|
59
|
+
"@pnpm/workspace.spec-parser": "1100.0.0",
|
|
60
|
+
"@pnpm/store.cafs": "1100.1.12",
|
|
61
|
+
"@pnpm/core-loggers": "1100.2.1",
|
|
62
|
+
"@pnpm/constants": "1100.0.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"@pnpm/logger": "^1100.0.0",
|
|
66
|
-
"@pnpm/worker": "^1100.2.
|
|
66
|
+
"@pnpm/worker": "^1100.2.3"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@jest/globals": "30.4.1",
|
|
@@ -71,13 +71,14 @@
|
|
|
71
71
|
"@types/ramda": "0.31.1",
|
|
72
72
|
"@types/semver": "7.7.1",
|
|
73
73
|
"@types/ssri": "^7.1.5",
|
|
74
|
+
"@types/validate-npm-package-name": "^4.0.2",
|
|
74
75
|
"load-json-file": "^7.0.1",
|
|
75
76
|
"tempy": "3.0.0",
|
|
76
|
-
"@pnpm/
|
|
77
|
-
"@pnpm/resolving.npm-resolver": "1102.1.0",
|
|
77
|
+
"@pnpm/resolving.npm-resolver": "1102.1.2",
|
|
78
78
|
"@pnpm/test-fixtures": "1100.0.0",
|
|
79
|
+
"@pnpm/logger": "1100.0.0",
|
|
79
80
|
"@pnpm/testing.mock-agent": "1101.0.4",
|
|
80
|
-
"@pnpm/
|
|
81
|
+
"@pnpm/network.fetch": "1100.1.4"
|
|
81
82
|
},
|
|
82
83
|
"engines": {
|
|
83
84
|
"node": ">=22.13"
|