mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 18:46:18 -04:00
feat: add minimumReleaseAgeIgnoreMissingTime setting (#11293)
Skips the minimumReleaseAge maturity check when the registry metadata lacks the "time" field, instead of throwing ERR_PNPM_MISSING_TIME. Defaults to true, and prints a warning once per affected package.
This commit is contained in:
11
.changeset/minimum-release-age-ignore-missing-time.md
Normal file
11
.changeset/minimum-release-age-ignore-missing-time.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"@pnpm/config.reader": minor
|
||||
"@pnpm/resolving.npm-resolver": minor
|
||||
"@pnpm/store.connection-manager": patch
|
||||
"@pnpm/deps.inspection.outdated": patch
|
||||
"@pnpm/exec.commands": patch
|
||||
"@pnpm/testing.command-defaults": patch
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added a new setting `minimumReleaseAgeIgnoreMissingTime`, which is `true` by default. When enabled, pnpm skips the `minimumReleaseAge` maturity check if the registry metadata does not include the `time` field. Set to `false` to fail resolution instead.
|
||||
@@ -259,6 +259,7 @@ export interface Config extends OptionsFromRootManifest {
|
||||
preserveAbsolutePaths?: boolean
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
minimumReleaseAgeIgnoreMissingTime?: boolean
|
||||
minimumReleaseAgeStrict?: boolean
|
||||
fetchWarnTimeoutMs?: number
|
||||
fetchMinSpeedKiBps?: number
|
||||
|
||||
@@ -36,6 +36,7 @@ export const pnpmConfigFileKeys = [
|
||||
'dlx-cache-max-age',
|
||||
'minimum-release-age',
|
||||
'minimum-release-age-exclude',
|
||||
'minimum-release-age-ignore-missing-time',
|
||||
'minimum-release-age-strict',
|
||||
'network-concurrency',
|
||||
'noproxy',
|
||||
|
||||
@@ -173,6 +173,7 @@ export async function getConfig (opts: {
|
||||
'link-workspace-packages': false,
|
||||
'lockfile-include-tarball-url': false,
|
||||
'minimum-release-age': 24 * 60, // 1 day
|
||||
'minimum-release-age-ignore-missing-time': true,
|
||||
'modules-cache-max-age': 7 * 24 * 60, // 7 days
|
||||
'dlx-cache-max-age': 24 * 60, // 1 day
|
||||
'node-linker': 'isolated',
|
||||
|
||||
@@ -77,6 +77,7 @@ const AUTH_CFG_KEYS = [
|
||||
const SECURITY_POLICY_CFG_KEYS = [
|
||||
'minimumReleaseAge',
|
||||
'minimumReleaseAgeExclude',
|
||||
'minimumReleaseAgeIgnoreMissingTime',
|
||||
'minimumReleaseAgeStrict',
|
||||
'trustPolicy',
|
||||
'trustPolicyExclude',
|
||||
|
||||
@@ -70,6 +70,7 @@ export const pnpmTypes = {
|
||||
'dlx-cache-max-age': Number,
|
||||
'minimum-release-age': Number,
|
||||
'minimum-release-age-exclude': [String, Array],
|
||||
'minimum-release-age-ignore-missing-time': Boolean,
|
||||
'minimum-release-age-strict': Boolean,
|
||||
'modules-dir': String,
|
||||
'network-concurrency': Number,
|
||||
|
||||
@@ -79,8 +79,8 @@ export async function fetchPackageInfo (
|
||||
const data = pickPackageFromMeta(
|
||||
pickVersionByVersionRange,
|
||||
{ preferredVersionSelectors: undefined },
|
||||
spec,
|
||||
metadata
|
||||
metadata,
|
||||
spec
|
||||
)
|
||||
if (!data) {
|
||||
throw new PnpmError('PACKAGE_NOT_FOUND', `No matching version found for ${packageName}@${spec.fetchSpec}`)
|
||||
|
||||
@@ -12,6 +12,7 @@ interface GetManifestOpts {
|
||||
configByUri: object
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
minimumReleaseAgeIgnoreMissingTime?: boolean
|
||||
minimumReleaseAgeStrict?: boolean
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ export function createManifestGetter (
|
||||
configByUri: opts.configByUri,
|
||||
filterMetadata: false, // We need all the data from metadata for "outdated --long" to work.
|
||||
strictPublishedByCheck: Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true,
|
||||
ignoreMissingTimeField: opts.minimumReleaseAgeIgnoreMissingTime,
|
||||
})
|
||||
|
||||
const publishedBy = opts.minimumReleaseAge
|
||||
|
||||
@@ -108,6 +108,7 @@ export async function handler (
|
||||
fullMetadata,
|
||||
filterMetadata: fullMetadata,
|
||||
strictPublishedByCheck: Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true,
|
||||
ignoreMissingTimeField: opts.minimumReleaseAgeIgnoreMissingTime,
|
||||
retry: {
|
||||
factor: opts.fetchRetryFactor,
|
||||
maxTimeout: opts.fetchRetryMaxtimeout,
|
||||
|
||||
@@ -136,6 +136,7 @@ export interface ResolverFactoryOptions {
|
||||
saveWorkspaceProtocol?: boolean | 'rolling'
|
||||
preserveAbsolutePaths?: boolean
|
||||
strictPublishedByCheck?: boolean
|
||||
ignoreMissingTimeField?: boolean
|
||||
fetchWarnTimeoutMs?: number
|
||||
}
|
||||
|
||||
@@ -224,6 +225,7 @@ export function createNpmResolver (
|
||||
preferOffline: opts.preferOffline,
|
||||
cacheDir: opts.cacheDir,
|
||||
strictPublishedByCheck: opts.strictPublishedByCheck,
|
||||
ignoreMissingTimeField: opts.ignoreMissingTimeField,
|
||||
}),
|
||||
registries: opts.registries,
|
||||
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
|
||||
@@ -358,7 +360,7 @@ async function resolveNpm (
|
||||
dryRun: opts.dryRun === true,
|
||||
preferredVersionSelectors: opts.preferredVersions?.[spec.name],
|
||||
registry,
|
||||
updateToLatest: opts.update === 'latest',
|
||||
includeLatestTag: opts.update === 'latest',
|
||||
optional: wantedDependency.optional,
|
||||
})
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
@@ -500,7 +502,7 @@ async function resolveJsr (
|
||||
dryRun: opts.dryRun === true,
|
||||
preferredVersionSelectors: opts.preferredVersions?.[spec.name],
|
||||
registry,
|
||||
updateToLatest: opts.update === 'latest',
|
||||
includeLatestTag: opts.update === 'latest',
|
||||
})
|
||||
|
||||
if (pickedPackage == null) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ABBREVIATED_META_DIR, FULL_FILTERED_META_DIR, FULL_META_DIR } from '@pn
|
||||
import { createHexHash } from '@pnpm/crypto.hash'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import gfs from '@pnpm/fs.graceful-fs'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { globalWarn, logger } from '@pnpm/logger'
|
||||
import type { PackageInRegistry, PackageMeta } from '@pnpm/resolving.registry.types'
|
||||
import getRegistryName from 'encode-registry'
|
||||
import pLimit, { type LimitFunction } from 'p-limit'
|
||||
@@ -68,22 +68,105 @@ export interface PickPackageOptions extends PickPackageFromMetaOptions {
|
||||
pickLowestVersion?: boolean
|
||||
registry: string
|
||||
dryRun: boolean
|
||||
updateToLatest?: boolean
|
||||
includeLatestTag?: boolean
|
||||
optional?: boolean
|
||||
}
|
||||
|
||||
const pickPackageFromMetaUsingTimeStrict = pickPackageFromMeta.bind(null, pickVersionByVersionRange)
|
||||
interface PickerOptions extends PickPackageFromMetaOptions {
|
||||
pickLowestVersion?: boolean
|
||||
includeLatestTag?: boolean
|
||||
strictPublishedByCheck?: boolean
|
||||
ignoreMissingTimeField?: boolean
|
||||
}
|
||||
|
||||
function pickPackageFromMetaUsingTime (
|
||||
opts: PickPackageFromMetaOptions,
|
||||
// When includeLatestTag is set, the "latest" dist-tag is added as a candidate
|
||||
// alongside the requested spec, and the higher-versioned pick wins.
|
||||
function runPicker (
|
||||
pickerOpts: PickerOptions,
|
||||
spec: RegistryPackageSpec,
|
||||
pickOne: (targetSpec: RegistryPackageSpec) => PackageInRegistry | null
|
||||
): PackageInRegistry | null {
|
||||
const currentPkg = pickOne(spec)
|
||||
if (!pickerOpts.includeLatestTag) return currentPkg
|
||||
const latestPkg = pickOne({ ...spec, type: 'tag', fetchSpec: 'latest' })
|
||||
return pickMax(latestPkg, currentPkg)
|
||||
}
|
||||
|
||||
// Returns whichever pick has the higher version, treating null as "no match".
|
||||
function pickMax (
|
||||
a: PackageInRegistry | null,
|
||||
b: PackageInRegistry | null
|
||||
): PackageInRegistry | null {
|
||||
if (!a) return b
|
||||
if (!b) return a
|
||||
return semver.lt(a.version, b.version) ? b : a
|
||||
}
|
||||
|
||||
const pickHighest = pickPackageFromMeta.bind(null, pickVersionByVersionRange)
|
||||
const pickLowest = pickPackageFromMeta.bind(null, pickLowestVersionByVersionRange)
|
||||
|
||||
// When minimumReleaseAge is active: try the highest mature version; if none
|
||||
// and strictPublishedByCheck is off, fall back to the lowest version in range
|
||||
// without applying the maturity filter.
|
||||
function pickRespectingMinReleaseAge (
|
||||
pickerOpts: PickerOptions,
|
||||
spec: RegistryPackageSpec,
|
||||
meta: PackageMeta
|
||||
): PackageInRegistry | null {
|
||||
const pickedPackage = pickPackageFromMeta(pickVersionByVersionRange, opts, spec, meta)
|
||||
if (pickedPackage) return pickedPackage
|
||||
return pickPackageFromMeta(pickLowestVersionByVersionRange, {
|
||||
preferredVersionSelectors: opts.preferredVersionSelectors,
|
||||
}, spec, meta)
|
||||
return runPicker(pickerOpts, spec, (targetSpec) => {
|
||||
const highest = pickHighest(pickerOpts, meta, targetSpec)
|
||||
if (highest || pickerOpts.strictPublishedByCheck) return highest
|
||||
return pickLowest({
|
||||
preferredVersionSelectors: pickerOpts.preferredVersionSelectors,
|
||||
}, meta, targetSpec)
|
||||
})
|
||||
}
|
||||
|
||||
// When minimumReleaseAge is not active: pick by pickLowestVersion preference.
|
||||
function pickIgnoringReleaseAge (
|
||||
pickerOpts: PickerOptions,
|
||||
spec: RegistryPackageSpec,
|
||||
meta: PackageMeta
|
||||
): PackageInRegistry | null {
|
||||
const pickVersion = pickerOpts.pickLowestVersion ? pickLowest : pickHighest
|
||||
return runPicker(pickerOpts, spec, (targetSpec) => pickVersion(pickerOpts, meta, targetSpec))
|
||||
}
|
||||
|
||||
// Used in shortcut/fall-through paths: if it fails (including with
|
||||
// ERR_PNPM_MISSING_TIME), the caller falls through to the next path — e.g.
|
||||
// the network fetch that can upgrade abbreviated metadata to full.
|
||||
function pickMatchingVersionFast (
|
||||
pickerOpts: PickerOptions,
|
||||
spec: RegistryPackageSpec,
|
||||
meta: PackageMeta
|
||||
): PackageInRegistry | null {
|
||||
return pickerOpts.publishedBy
|
||||
? pickRespectingMinReleaseAge(pickerOpts, spec, meta)
|
||||
: pickIgnoringReleaseAge(pickerOpts, spec, meta)
|
||||
}
|
||||
|
||||
// Used at terminal return sites where no further fallback path exists. When
|
||||
// metadata lacks the per-version `time` field and ignoreMissingTimeField is
|
||||
// enabled, skip the minimumReleaseAge filter with a warning instead of
|
||||
// failing hard.
|
||||
function pickMatchingVersionFinal (
|
||||
pickerOpts: PickerOptions,
|
||||
spec: RegistryPackageSpec,
|
||||
meta: PackageMeta
|
||||
): PackageInRegistry | null {
|
||||
try {
|
||||
return pickMatchingVersionFast(pickerOpts, spec, meta)
|
||||
} catch (err: unknown) {
|
||||
if (pickerOpts.ignoreMissingTimeField && isMissingTimeError(err)) {
|
||||
warnMissingTimeFieldOnce(meta.name)
|
||||
return pickMatchingVersionFast({
|
||||
...pickerOpts,
|
||||
publishedBy: undefined,
|
||||
publishedByExclude: undefined,
|
||||
}, spec, meta)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function pickPackage (
|
||||
@@ -96,35 +179,21 @@ export async function pickPackage (
|
||||
preferOffline?: boolean
|
||||
filterMetadata?: boolean
|
||||
strictPublishedByCheck?: boolean
|
||||
ignoreMissingTimeField?: boolean
|
||||
},
|
||||
spec: RegistryPackageSpec,
|
||||
opts: PickPackageOptions
|
||||
): Promise<{ meta: PackageMeta, pickedPackage: PackageInRegistry | null }> {
|
||||
opts = opts || {}
|
||||
const pickPackageFromMetaBySpec = (
|
||||
opts.publishedBy
|
||||
? (ctx.strictPublishedByCheck ? pickPackageFromMetaUsingTimeStrict : pickPackageFromMetaUsingTime)
|
||||
: (pickPackageFromMeta.bind(null, opts.pickLowestVersion ? pickLowestVersionByVersionRange : pickVersionByVersionRange))
|
||||
).bind(null, {
|
||||
|
||||
const pickerOpts: PickerOptions = {
|
||||
preferredVersionSelectors: opts.preferredVersionSelectors,
|
||||
publishedBy: opts.publishedBy,
|
||||
publishedByExclude: opts.publishedByExclude,
|
||||
})
|
||||
|
||||
let _pickPackageFromMeta!: (meta: PackageMeta) => PackageInRegistry | null
|
||||
if (opts.updateToLatest) {
|
||||
_pickPackageFromMeta = (meta) => {
|
||||
const latestStableSpec: RegistryPackageSpec = { ...spec, type: 'tag', fetchSpec: 'latest' }
|
||||
const latestStable = pickPackageFromMetaBySpec(latestStableSpec, meta)
|
||||
const current = pickPackageFromMetaBySpec(spec, meta)
|
||||
|
||||
if (!latestStable) return current
|
||||
if (!current) return latestStable
|
||||
if (semver.lt(latestStable.version, current.version)) return current
|
||||
return latestStable
|
||||
}
|
||||
} else {
|
||||
_pickPackageFromMeta = pickPackageFromMetaBySpec.bind(null, spec)
|
||||
pickLowestVersion: opts.pickLowestVersion,
|
||||
includeLatestTag: opts.includeLatestTag,
|
||||
strictPublishedByCheck: ctx.strictPublishedByCheck,
|
||||
ignoreMissingTimeField: ctx.ignoreMissingTimeField,
|
||||
}
|
||||
|
||||
validatePackageName(spec.name)
|
||||
@@ -141,7 +210,7 @@ export async function pickPackage (
|
||||
if (cachedMeta != null) {
|
||||
return {
|
||||
meta: cachedMeta,
|
||||
pickedPackage: _pickPackageFromMeta(cachedMeta),
|
||||
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, cachedMeta),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,14 +225,14 @@ export async function pickPackage (
|
||||
if (ctx.offline) {
|
||||
if (metaCachedInStore != null) return {
|
||||
meta: metaCachedInStore,
|
||||
pickedPackage: _pickPackageFromMeta(metaCachedInStore),
|
||||
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore),
|
||||
}
|
||||
|
||||
throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`)
|
||||
}
|
||||
|
||||
if (metaCachedInStore != null) {
|
||||
const pickedPackage = _pickPackageFromMeta(metaCachedInStore)
|
||||
const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore)
|
||||
if (pickedPackage) {
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
@@ -173,13 +242,13 @@ export async function pickPackage (
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.updateToLatest && spec.type === 'version') {
|
||||
if (!opts.includeLatestTag && spec.type === 'version') {
|
||||
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror))
|
||||
// use the cached meta only if it has the required package version
|
||||
// otherwise it is probably out of date
|
||||
if ((metaCachedInStore?.versions?.[spec.fetchSpec]) != null) {
|
||||
try {
|
||||
const pickedPackage = _pickPackageFromMeta(metaCachedInStore)
|
||||
const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore)
|
||||
if (pickedPackage) {
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
@@ -199,7 +268,7 @@ export async function pickPackage (
|
||||
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror))
|
||||
if (metaCachedInStore != null) {
|
||||
try {
|
||||
const pickedPackage = _pickPackageFromMeta(metaCachedInStore)
|
||||
const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore)
|
||||
if (pickedPackage) {
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
@@ -243,7 +312,7 @@ export async function pickPackage (
|
||||
ctx.metaCache.set(cacheKey, metaCachedInStore)
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
pickedPackage: _pickPackageFromMeta(metaCachedInStore),
|
||||
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore),
|
||||
}
|
||||
}
|
||||
throw new PnpmError('CACHE_MISSING_AFTER_304',
|
||||
@@ -315,7 +384,7 @@ export async function pickPackage (
|
||||
ctx.metaCache.set(cacheKey, meta)
|
||||
return {
|
||||
meta,
|
||||
pickedPackage: _pickPackageFromMeta(meta),
|
||||
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, meta),
|
||||
}
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
err.spec = spec
|
||||
@@ -325,7 +394,7 @@ export async function pickPackage (
|
||||
logger.debug({ message: `Using cached meta from ${pkgMirror}` })
|
||||
return {
|
||||
meta,
|
||||
pickedPackage: _pickPackageFromMeta(meta),
|
||||
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, meta),
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -396,6 +465,22 @@ function isMissingTimeError (err: unknown): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
// Cap the size so long-lived processes (daemons, store servers) can't leak
|
||||
// memory via this Set as they resolve ever more distinct packages.
|
||||
const MAX_WARNED_MISSING_TIME = 1024
|
||||
const warnedMissingTimeFor = new Set<string>()
|
||||
|
||||
function warnMissingTimeFieldOnce (pkgName: string): void {
|
||||
if (warnedMissingTimeFor.has(pkgName)) return
|
||||
if (warnedMissingTimeFor.size >= MAX_WARNED_MISSING_TIME) {
|
||||
// Set preserves insertion order, so the first entry is the oldest.
|
||||
const oldest = warnedMissingTimeFor.values().next().value
|
||||
if (oldest != null) warnedMissingTimeFor.delete(oldest)
|
||||
}
|
||||
warnedMissingTimeFor.add(pkgName)
|
||||
globalWarn(`The metadata of ${pkgName} is missing the "time" field; skipping the minimumReleaseAge check for this package.`)
|
||||
}
|
||||
|
||||
async function getFileMtime (filePath: string): Promise<Date | null> {
|
||||
try {
|
||||
const stat = await fs.stat(filePath)
|
||||
|
||||
@@ -31,8 +31,8 @@ export function pickPackageFromMeta (
|
||||
publishedBy,
|
||||
publishedByExclude,
|
||||
}: PickPackageFromMetaOptions,
|
||||
spec: RegistryPackageSpec,
|
||||
meta: PackageMeta
|
||||
meta: PackageMeta,
|
||||
spec: RegistryPackageSpec
|
||||
): PackageInRegistry | null {
|
||||
if (publishedBy) {
|
||||
const excludeResult = publishedByExclude?.(meta.name) ?? false
|
||||
|
||||
@@ -212,6 +212,113 @@ test('re-fetch full metadata when abbreviated modified date is recent', async ()
|
||||
expect(resolveResult!.id).toBe('is-positive@1.0.0')
|
||||
})
|
||||
|
||||
test('ignoreMissingTimeField=true skips maturity check when full metadata has no time field', async () => {
|
||||
const { time: _time, ...metaWithoutTime } = isPositiveMeta
|
||||
|
||||
getMockAgent().get(registries.default.replace(/\/$/, ''))
|
||||
.intercept({ path: '/is-positive', method: 'GET' })
|
||||
.reply(200, metaWithoutTime)
|
||||
|
||||
const cacheDir = temporaryDirectory()
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
storeDir: temporaryDirectory(),
|
||||
cacheDir,
|
||||
filterMetadata: true,
|
||||
fullMetadata: true,
|
||||
registries,
|
||||
ignoreMissingTimeField: true,
|
||||
})
|
||||
const resolveResult = await resolveFromNpm({ alias: 'is-positive', bareSpecifier: '^3.0.0' }, {
|
||||
publishedBy: new Date('2015-08-17T19:26:00.508Z'),
|
||||
})
|
||||
|
||||
expect(resolveResult!.resolvedVia).toBe('npm-registry')
|
||||
expect(resolveResult!.id).toBe('is-positive@3.1.0')
|
||||
})
|
||||
|
||||
test('ignoreMissingTimeField=true still upgrades abbreviated→full when time is missing', async () => {
|
||||
// With ignoreMissingTimeField=true, pnpm should still re-fetch full metadata
|
||||
// when abbreviated metadata lacks time — only falling back to skip+warn if
|
||||
// even the full metadata has no time field. Here the full response DOES have
|
||||
// time, so the maturity check must run (and pick the old 1.0.0, not latest).
|
||||
const recentAbbreviated = {
|
||||
...isPositiveAbbreviatedMeta,
|
||||
modified: '2015-06-10T00:00:00.000Z',
|
||||
}
|
||||
|
||||
const agent = getMockAgent().get(registries.default.replace(/\/$/, ''))
|
||||
agent.intercept({ path: '/is-positive', method: 'GET' })
|
||||
.reply(200, recentAbbreviated)
|
||||
agent.intercept({ path: '/is-positive', method: 'GET' })
|
||||
.reply(200, isPositiveMeta)
|
||||
|
||||
const cacheDir = temporaryDirectory()
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
storeDir: temporaryDirectory(),
|
||||
cacheDir,
|
||||
registries,
|
||||
ignoreMissingTimeField: true,
|
||||
})
|
||||
const resolveResult = await resolveFromNpm({ alias: 'is-positive', bareSpecifier: '^1.0.0' }, {
|
||||
publishedBy: new Date('2015-06-05T00:00:00.000Z'),
|
||||
})
|
||||
|
||||
expect(resolveResult!.resolvedVia).toBe('npm-registry')
|
||||
expect(resolveResult!.id).toBe('is-positive@1.0.0')
|
||||
})
|
||||
|
||||
test('ignoreMissingTimeField=false throws when full metadata has no time field', async () => {
|
||||
const { time: _time, ...metaWithoutTime } = isPositiveMeta
|
||||
|
||||
getMockAgent().get(registries.default.replace(/\/$/, ''))
|
||||
.intercept({ path: '/is-positive', method: 'GET' })
|
||||
.reply(200, metaWithoutTime)
|
||||
|
||||
const cacheDir = temporaryDirectory()
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
storeDir: temporaryDirectory(),
|
||||
cacheDir,
|
||||
filterMetadata: true,
|
||||
fullMetadata: true,
|
||||
registries,
|
||||
ignoreMissingTimeField: false,
|
||||
})
|
||||
await expect(resolveFromNpm({ alias: 'is-positive', bareSpecifier: '^3.0.0' }, {
|
||||
publishedBy: new Date('2015-08-17T19:26:00.508Z'),
|
||||
})).rejects.toThrow(/missing the "time" field/)
|
||||
})
|
||||
|
||||
test('ignoreMissingTimeField=true skips maturity check from disk-cached metadata lacking time', async () => {
|
||||
// Exercise the cached-metadata return path: write full metadata to disk
|
||||
// with the `time` field stripped, and verify that resolution succeeds
|
||||
// (no ERR_PNPM_MISSING_TIME) when the setting is on.
|
||||
const { time: _time, ...metaWithoutTime } = isPositiveMeta
|
||||
|
||||
const cacheDir = temporaryDirectory()
|
||||
const cacheDir2 = path.join(cacheDir, `${FULL_FILTERED_META_DIR}/registry.npmjs.org`)
|
||||
fs.mkdirSync(cacheDir2, { recursive: true })
|
||||
const cachePath = path.join(cacheDir2, 'is-positive.jsonl')
|
||||
fs.writeFileSync(cachePath, `${JSON.stringify({})}\n${JSON.stringify(metaWithoutTime)}`, 'utf8')
|
||||
|
||||
// No mock agent intercepts — test would fail if a network request fired.
|
||||
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
storeDir: temporaryDirectory(),
|
||||
cacheDir,
|
||||
filterMetadata: true,
|
||||
fullMetadata: true,
|
||||
registries,
|
||||
ignoreMissingTimeField: true,
|
||||
offline: true,
|
||||
})
|
||||
const resolveResult = await resolveFromNpm({ alias: 'is-positive', bareSpecifier: '^3.0.0' }, {
|
||||
publishedBy: new Date('2015-08-17T19:26:00.508Z'),
|
||||
})
|
||||
|
||||
expect(resolveResult!.resolvedVia).toBe('npm-registry')
|
||||
expect(resolveResult!.id).toBe('is-positive@3.1.0')
|
||||
})
|
||||
|
||||
test('use cached metadata based on file mtime when publishedBy is set', async () => {
|
||||
const cacheDir = temporaryDirectory()
|
||||
// Write abbreviated metadata to the abbreviated cache dir
|
||||
|
||||
@@ -34,6 +34,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
|
||||
| 'localAddress'
|
||||
| 'maxSockets'
|
||||
| 'minimumReleaseAge'
|
||||
| 'minimumReleaseAgeIgnoreMissingTime'
|
||||
| 'minimumReleaseAgeStrict'
|
||||
| 'networkConcurrency'
|
||||
| 'noProxy'
|
||||
@@ -111,6 +112,7 @@ export async function createNewStoreController (
|
||||
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
|
||||
preserveAbsolutePaths: opts.preserveAbsolutePaths,
|
||||
strictPublishedByCheck: Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true,
|
||||
ignoreMissingTimeField: opts.minimumReleaseAgeIgnoreMissingTime,
|
||||
storeIndex,
|
||||
})
|
||||
return {
|
||||
|
||||
@@ -36,6 +36,7 @@ export const DEFAULT_OPTS = {
|
||||
lock: false,
|
||||
lockStaleDuration: 90,
|
||||
minimumReleaseAge: 0,
|
||||
minimumReleaseAgeIgnoreMissingTime: true,
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
|
||||
Reference in New Issue
Block a user