@pnpm/resolving.npm-resolver 1101.1.0 → 1101.1.1
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/fetch.js +53 -6
- package/lib/index.js +8 -2
- package/lib/pickPackage.js +116 -9
- package/package.json +15 -15
package/lib/fetch.js
CHANGED
|
@@ -3,24 +3,71 @@ import { requestRetryLogger } from '@pnpm/core-loggers';
|
|
|
3
3
|
import { FetchError, PnpmError, } from '@pnpm/error';
|
|
4
4
|
import { globalWarn } from '@pnpm/logger';
|
|
5
5
|
import * as retry from '@zkochan/retry';
|
|
6
|
-
|
|
7
|
-
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/use-ignore-case
|
|
8
|
-
const semverRegex = /(.*)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
6
|
+
import semver from 'semver';
|
|
9
7
|
export class RegistryResponseError extends FetchError {
|
|
10
8
|
pkgName;
|
|
11
9
|
constructor(request, response, pkgName) {
|
|
12
10
|
let hint;
|
|
13
11
|
if (response.status === 404) {
|
|
14
12
|
hint = `${pkgName} is not in the npm registry, or you have no permission to fetch it.`;
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
hint += ` Did you mean ${
|
|
13
|
+
const nameWithoutVersion = stripTrailingSemverSuffix(pkgName);
|
|
14
|
+
if (nameWithoutVersion != null) {
|
|
15
|
+
hint += ` Did you mean ${nameWithoutVersion}?`;
|
|
18
16
|
}
|
|
19
17
|
}
|
|
20
18
|
super(request, response, hint);
|
|
21
19
|
this.pkgName = pkgName;
|
|
22
20
|
}
|
|
23
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Detect when a package name accidentally includes a `<version>` suffix
|
|
24
|
+
* (e.g. `lodash@4.17.21` or `lodash4.17.21`) and return the part before the
|
|
25
|
+
* version. Returns `undefined` when no semver suffix is present.
|
|
26
|
+
*
|
|
27
|
+
* Implemented as an O(n) scan to avoid polynomial backtracking on adversarial
|
|
28
|
+
* input (CodeQL: js/polynomial-redos).
|
|
29
|
+
*/
|
|
30
|
+
function stripTrailingSemverSuffix(pkgName) {
|
|
31
|
+
// Common case: "name@version" – split on the rightmost '@'.
|
|
32
|
+
// `atIdx > 0` rules out the leading '@' of scoped names like '@scope/foo'.
|
|
33
|
+
const atIdx = pkgName.lastIndexOf('@');
|
|
34
|
+
if (atIdx > 0 && semver.valid(pkgName.slice(atIdx + 1)) != null) {
|
|
35
|
+
return pkgName.slice(0, atIdx);
|
|
36
|
+
}
|
|
37
|
+
// Fallback: detect a trailing "<digits>.<digits>.<digits>" appended to a name
|
|
38
|
+
// with no separator (e.g. "foo1.0.0"). We walk backwards through three
|
|
39
|
+
// digit-blocks separated by dots; this is O(n) and free of regex backtracking.
|
|
40
|
+
let i = pkgName.length;
|
|
41
|
+
i = consumeTrailingDigits(pkgName, i);
|
|
42
|
+
if (i === pkgName.length || i === 0 || pkgName.charCodeAt(i - 1) !== 46 /* '.' */)
|
|
43
|
+
return undefined;
|
|
44
|
+
i--;
|
|
45
|
+
const beforePatch = i;
|
|
46
|
+
i = consumeTrailingDigits(pkgName, i);
|
|
47
|
+
if (i === beforePatch || i === 0 || pkgName.charCodeAt(i - 1) !== 46)
|
|
48
|
+
return undefined;
|
|
49
|
+
i--;
|
|
50
|
+
const beforeMinor = i;
|
|
51
|
+
i = consumeTrailingDigits(pkgName, i);
|
|
52
|
+
if (i === beforeMinor || i === 0)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (semver.valid(pkgName.slice(i)) == null)
|
|
55
|
+
return undefined;
|
|
56
|
+
let prefix = pkgName.slice(0, i);
|
|
57
|
+
if (prefix.endsWith('@'))
|
|
58
|
+
prefix = prefix.slice(0, -1);
|
|
59
|
+
return prefix.length > 0 ? prefix : undefined;
|
|
60
|
+
}
|
|
61
|
+
function consumeTrailingDigits(s, end) {
|
|
62
|
+
let i = end;
|
|
63
|
+
while (i > 0) {
|
|
64
|
+
const c = s.charCodeAt(i - 1);
|
|
65
|
+
if (c < 48 || c > 57)
|
|
66
|
+
break;
|
|
67
|
+
i--;
|
|
68
|
+
}
|
|
69
|
+
return i;
|
|
70
|
+
}
|
|
24
71
|
export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHeaderValue, etag: cachedEtag, fullMetadata, modified: cachedModified, registry, }) {
|
|
25
72
|
const uri = toUri(pkgName, registry);
|
|
26
73
|
const op = retry.operation(fetchOpts.retry);
|
package/lib/index.js
CHANGED
|
@@ -180,7 +180,13 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
180
180
|
// Fast path: if we have a current resolution with integrity, try to peek the manifest from the store.
|
|
181
181
|
// This avoids the expensive metadata fetch from the registry.
|
|
182
182
|
// We do this AFTER ensuring the spec is valid for this resolver to avoids hijacking other resolvers.
|
|
183
|
-
|
|
183
|
+
// If publishedBy is set (resolutionMode=time-based or minimumReleaseAge is configured), we only take
|
|
184
|
+
// the fast path when publishedAt is already known from the lockfile's `time:` block; otherwise we
|
|
185
|
+
// fall through to a registry fetch so the cutoff isn't computed from missing data.
|
|
186
|
+
if (ctx.peekManifestFromStore &&
|
|
187
|
+
opts.currentPkg?.resolution &&
|
|
188
|
+
!opts.update &&
|
|
189
|
+
(opts.publishedBy == null || opts.currentPkg.publishedAt != null)) {
|
|
184
190
|
const currentResolution = opts.currentPkg.resolution;
|
|
185
191
|
// Only use this optimization for tarball resolutions with integrity (npm packages)
|
|
186
192
|
if ('tarball' in currentResolution && currentResolution.integrity) {
|
|
@@ -200,7 +206,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
200
206
|
manifest,
|
|
201
207
|
resolution: currentResolution,
|
|
202
208
|
resolvedVia: 'npm-registry',
|
|
203
|
-
publishedAt:
|
|
209
|
+
publishedAt: opts.currentPkg.publishedAt,
|
|
204
210
|
};
|
|
205
211
|
}
|
|
206
212
|
}
|
package/lib/pickPackage.js
CHANGED
|
@@ -123,15 +123,30 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
123
123
|
: ABBREVIATED_META_DIR;
|
|
124
124
|
// Cache key includes fullMetadata to avoid returning abbreviated metadata when full metadata is requested.
|
|
125
125
|
const cacheKey = fullMetadata ? `${spec.name}:full` : spec.name;
|
|
126
|
+
const registryName = getRegistryName(opts.registry);
|
|
127
|
+
const pkgMirror = path.join(ctx.cacheDir, metaDir, registryName, `${encodePkgName(spec.name)}.jsonl`);
|
|
126
128
|
const cachedMeta = ctx.metaCache.get(cacheKey);
|
|
127
129
|
if (cachedMeta != null) {
|
|
130
|
+
// The in-memory cache may hold abbreviated metadata from an earlier call
|
|
131
|
+
// that didn't need `time` (no publishedBy then). If this call has
|
|
132
|
+
// publishedBy and the package was modified recently, upgrade to full
|
|
133
|
+
// metadata so the maturity check runs properly.
|
|
134
|
+
const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, cachedMeta);
|
|
135
|
+
let metaForCache = upgrade.meta;
|
|
136
|
+
if (upgrade.upgradedFrom != null) {
|
|
137
|
+
// Persist the upgraded meta to disk too: the on-disk mirror still holds
|
|
138
|
+
// the abbreviated form, so without this a fresh process would re-trigger
|
|
139
|
+
// the upgrade fetch on its next install.
|
|
140
|
+
metaForCache = opts.dryRun
|
|
141
|
+
? upgrade.meta
|
|
142
|
+
: persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
|
|
143
|
+
ctx.metaCache.set(cacheKey, metaForCache);
|
|
144
|
+
}
|
|
128
145
|
return {
|
|
129
|
-
meta:
|
|
130
|
-
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec,
|
|
146
|
+
meta: metaForCache,
|
|
147
|
+
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaForCache),
|
|
131
148
|
};
|
|
132
149
|
}
|
|
133
|
-
const registryName = getRegistryName(opts.registry);
|
|
134
|
-
const pkgMirror = path.join(ctx.cacheDir, metaDir, registryName, `${encodePkgName(spec.name)}.jsonl`);
|
|
135
150
|
return runLimited(pkgMirror, async (limit) => {
|
|
136
151
|
let metaCachedInStore;
|
|
137
152
|
if (ctx.offline === true || ctx.preferOffline === true || opts.pickLowestVersion) {
|
|
@@ -145,6 +160,17 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
145
160
|
throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`);
|
|
146
161
|
}
|
|
147
162
|
if (metaCachedInStore != null) {
|
|
163
|
+
// Disk-cached meta may be abbreviated; upgrade for the maturity check
|
|
164
|
+
// before letting pickMatchingVersionFinal warn-and-skip on missing time.
|
|
165
|
+
const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, metaCachedInStore);
|
|
166
|
+
metaCachedInStore = upgrade.meta;
|
|
167
|
+
if (upgrade.upgradedFrom != null) {
|
|
168
|
+
// Persist so the next install skips this upgrade fetch entirely.
|
|
169
|
+
if (!opts.dryRun) {
|
|
170
|
+
metaCachedInStore = persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
|
|
171
|
+
}
|
|
172
|
+
ctx.metaCache.set(cacheKey, metaCachedInStore);
|
|
173
|
+
}
|
|
148
174
|
const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore);
|
|
149
175
|
if (pickedPackage) {
|
|
150
176
|
return {
|
|
@@ -169,7 +195,7 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
169
195
|
}
|
|
170
196
|
}
|
|
171
197
|
catch (err) {
|
|
172
|
-
if (ctx.strictPublishedByCheck) {
|
|
198
|
+
if (shouldRethrowFromFastPathCache(err, ctx.strictPublishedByCheck)) {
|
|
173
199
|
throw err;
|
|
174
200
|
}
|
|
175
201
|
}
|
|
@@ -190,10 +216,7 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
190
216
|
}
|
|
191
217
|
}
|
|
192
218
|
catch (err) {
|
|
193
|
-
|
|
194
|
-
// let the code fall through to the network fetch path which will get full metadata.
|
|
195
|
-
if (ctx.strictPublishedByCheck &&
|
|
196
|
-
!(isMissingTimeError(err))) {
|
|
219
|
+
if (shouldRethrowFromFastPathCache(err, ctx.strictPublishedByCheck)) {
|
|
197
220
|
throw err;
|
|
198
221
|
}
|
|
199
222
|
}
|
|
@@ -219,6 +242,19 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
219
242
|
if (fetchResult.notModified) {
|
|
220
243
|
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
|
|
221
244
|
if (metaCachedInStore != null) {
|
|
245
|
+
// The cached metadata may be abbreviated (no per-version `time`).
|
|
246
|
+
// When minimumReleaseAge is active we need `time` for the maturity check,
|
|
247
|
+
// so upgrade to full metadata via a follow-up fetch when warranted.
|
|
248
|
+
// Without this, repeat installs of recently-modified packages would
|
|
249
|
+
// silently bypass the maturity check via the warn-and-skip fallback.
|
|
250
|
+
const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, metaCachedInStore);
|
|
251
|
+
metaCachedInStore = upgrade.meta;
|
|
252
|
+
if (upgrade.upgradedFrom != null && !opts.dryRun) {
|
|
253
|
+
// Persist the upgraded full metadata to disk so subsequent installs
|
|
254
|
+
// skip this upgrade fetch entirely (the cached meta will then have
|
|
255
|
+
// `time` populated, so the upgrade condition won't trigger).
|
|
256
|
+
metaCachedInStore = persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
|
|
257
|
+
}
|
|
222
258
|
ctx.metaCache.set(cacheKey, metaCachedInStore);
|
|
223
259
|
return {
|
|
224
260
|
meta: metaCachedInStore,
|
|
@@ -307,6 +343,77 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
307
343
|
}
|
|
308
344
|
});
|
|
309
345
|
}
|
|
346
|
+
// When `minimumReleaseAge` is active and we have abbreviated metadata (which
|
|
347
|
+
// the npm registry serves by default and which omits per-version `time`),
|
|
348
|
+
// the maturity check can't run on the data we have. If the package has been
|
|
349
|
+
// modified since the maturity cutoff, re-fetch with `fullMetadata: true` so
|
|
350
|
+
// `time` is populated and the check can proceed properly. Without this,
|
|
351
|
+
// `pickMatchingVersionFinal` would fall back to its warn-and-skip path,
|
|
352
|
+
// silently bypassing the minimumReleaseAge guarantee for affected packages.
|
|
353
|
+
//
|
|
354
|
+
// Returns the original meta when no upgrade is needed. When an upgrade
|
|
355
|
+
// happens, returns both the upgraded meta and the underlying fetch result
|
|
356
|
+
// so callers can persist it to disk and avoid re-fetching on next install.
|
|
357
|
+
async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
|
|
358
|
+
if (ctx.offline === true ||
|
|
359
|
+
!opts.publishedBy ||
|
|
360
|
+
meta.time != null ||
|
|
361
|
+
opts.publishedByExclude?.(spec.name) === true) {
|
|
362
|
+
return { meta };
|
|
363
|
+
}
|
|
364
|
+
const modifiedDate = meta.modified ? new Date(meta.modified) : null;
|
|
365
|
+
const isModifiedValid = modifiedDate != null && !Number.isNaN(modifiedDate.getTime());
|
|
366
|
+
if (isModifiedValid && modifiedDate < opts.publishedBy) {
|
|
367
|
+
// The package was last modified before the maturity cutoff. No individual
|
|
368
|
+
// version can be newer than the cutoff, so the abbreviated form is fine.
|
|
369
|
+
return { meta };
|
|
370
|
+
}
|
|
371
|
+
// When `modified` is missing or malformed we fall through to the upgrade
|
|
372
|
+
// fetch: prefer correctness (run the maturity check on real `time` data)
|
|
373
|
+
// over saving a network call when our cached freshness signal is unusable.
|
|
374
|
+
// Forward etag/modified so the registry can answer 304 if the upgraded
|
|
375
|
+
// representation hasn't actually changed (rare on the npm registry where
|
|
376
|
+
// full and abbreviated have distinct etags, but cheap to support).
|
|
377
|
+
const fullFetchResult = await ctx.fetch(spec.name, {
|
|
378
|
+
authHeaderValue: opts.authHeaderValue,
|
|
379
|
+
fullMetadata: true,
|
|
380
|
+
etag: meta.etag,
|
|
381
|
+
modified: meta.modified,
|
|
382
|
+
registry: opts.registry,
|
|
383
|
+
});
|
|
384
|
+
if (fullFetchResult.notModified) {
|
|
385
|
+
// Upgrade fetch came back 304: keep the abbreviated meta. The downstream
|
|
386
|
+
// `pickMatchingVersionFinal` will fall through to its warn-and-skip path.
|
|
387
|
+
return { meta };
|
|
388
|
+
}
|
|
389
|
+
return { meta: fullFetchResult.meta, upgradedFrom: fullFetchResult };
|
|
390
|
+
}
|
|
391
|
+
// Returns true when a fast-path cache catch should rethrow under
|
|
392
|
+
// strictPublishedByCheck. ERR_PNPM_MISSING_TIME is excluded so callers fall
|
|
393
|
+
// through to the network fetch path, which can upgrade abbreviated cached
|
|
394
|
+
// metadata to full and run the maturity check on real `time` data.
|
|
395
|
+
function shouldRethrowFromFastPathCache(err, strictPublishedByCheck) {
|
|
396
|
+
return strictPublishedByCheck === true && !isMissingTimeError(err);
|
|
397
|
+
}
|
|
398
|
+
// Persists upgraded full metadata to the on-disk cache mirror and returns
|
|
399
|
+
// the meta to store in the in-memory cache. When `filterMetadata` is on, the
|
|
400
|
+
// in-memory and on-disk forms are both stripped via `clearMeta`; otherwise
|
|
401
|
+
// the original raw response body is written and the unstripped meta is kept.
|
|
402
|
+
function persistUpgradedMeta(ctx, pkgMirror, upgradedFrom) {
|
|
403
|
+
const metaForCache = ctx.filterMetadata ? clearMeta(upgradedFrom.meta) : upgradedFrom.meta;
|
|
404
|
+
const jsonForDisk = ctx.filterMetadata
|
|
405
|
+
? prepareJsonForDisk(metaForCache, upgradedFrom.etag)
|
|
406
|
+
: prepareJsonForDisk(upgradedFrom.meta, upgradedFrom.etag, upgradedFrom.jsonText);
|
|
407
|
+
runLimited(pkgMirror, (l) => l(async () => {
|
|
408
|
+
try {
|
|
409
|
+
await saveMeta(pkgMirror, jsonForDisk);
|
|
410
|
+
}
|
|
411
|
+
catch (err) { // eslint-disable-line
|
|
412
|
+
// We don't care if this file was not written to the cache
|
|
413
|
+
}
|
|
414
|
+
}));
|
|
415
|
+
return metaForCache;
|
|
416
|
+
}
|
|
310
417
|
function clearMeta(pkg) {
|
|
311
418
|
const versions = {};
|
|
312
419
|
for (const [version, info] of Object.entries(pkg.versions)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/resolving.npm-resolver",
|
|
3
|
-
"version": "1101.1.
|
|
3
|
+
"version": "1101.1.1",
|
|
4
4
|
"description": "Resolver for npm-hosted packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -40,26 +40,26 @@
|
|
|
40
40
|
"semver-utils": "^1.1.4",
|
|
41
41
|
"ssri": "13.0.1",
|
|
42
42
|
"version-selector-type": "^3.0.0",
|
|
43
|
-
"@pnpm/
|
|
43
|
+
"@pnpm/core-loggers": "1100.0.2",
|
|
44
|
+
"@pnpm/constants": "1100.0.0",
|
|
44
45
|
"@pnpm/crypto.hash": "1100.0.1",
|
|
46
|
+
"@pnpm/fs.graceful-fs": "1100.1.0",
|
|
45
47
|
"@pnpm/error": "1100.0.0",
|
|
46
48
|
"@pnpm/fetching.types": "1100.0.1",
|
|
47
|
-
"@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
|
|
48
|
-
"@pnpm/fs.graceful-fs": "1100.1.0",
|
|
49
|
-
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.3",
|
|
50
|
-
"@pnpm/store.cafs": "1100.1.3",
|
|
51
|
-
"@pnpm/store.index": "1100.1.0",
|
|
52
49
|
"@pnpm/resolving.registry.types": "1100.0.3",
|
|
53
|
-
"@pnpm/
|
|
50
|
+
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.3",
|
|
54
51
|
"@pnpm/resolving.resolver-base": "1100.1.3",
|
|
55
|
-
"@pnpm/
|
|
56
|
-
"@pnpm/
|
|
52
|
+
"@pnpm/types": "1101.1.0",
|
|
53
|
+
"@pnpm/store.index": "1100.1.0",
|
|
54
|
+
"@pnpm/store.cafs": "1100.1.4",
|
|
57
55
|
"@pnpm/workspace.spec-parser": "1100.0.0",
|
|
58
|
-
"@pnpm/
|
|
56
|
+
"@pnpm/workspace.range-resolver": "1100.0.1",
|
|
57
|
+
"@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
|
|
58
|
+
"@pnpm/config.pick-registry-for-package": "1100.0.3"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@pnpm/logger": ">=1001.0.0 <1002.0.0",
|
|
62
|
-
"@pnpm/worker": "^1100.1.
|
|
62
|
+
"@pnpm/worker": "^1100.1.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@jest/globals": "30.3.0",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"load-json-file": "^7.0.1",
|
|
71
71
|
"tempy": "3.0.0",
|
|
72
72
|
"@pnpm/config.version-policy": "1100.0.3",
|
|
73
|
+
"@pnpm/network.fetch": "1100.0.4",
|
|
74
|
+
"@pnpm/resolving.npm-resolver": "1101.1.1",
|
|
73
75
|
"@pnpm/logger": "1100.0.0",
|
|
74
|
-
"@pnpm/resolving.npm-resolver": "1101.1.0",
|
|
75
|
-
"@pnpm/network.fetch": "1100.0.3",
|
|
76
76
|
"@pnpm/test-fixtures": "1100.0.0",
|
|
77
|
-
"@pnpm/testing.mock-agent": "1100.0.
|
|
77
|
+
"@pnpm/testing.mock-agent": "1100.0.4"
|
|
78
78
|
},
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=22.13"
|