From 9e0833c3cc865df74981cba77f9bb58deeb217a4 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Sun, 19 Apr 2026 00:22:32 +0200 Subject: [PATCH] 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. --- ...minimum-release-age-ignore-missing-time.md | 11 ++ config/reader/src/Config.ts | 1 + config/reader/src/configFileKey.ts | 1 + config/reader/src/index.ts | 1 + config/reader/src/localConfig.ts | 1 + config/reader/src/types.ts | 1 + .../commands/src/fetchPackageInfo.ts | 4 +- .../outdated/src/createManifestGetter.ts | 2 + exec/commands/src/dlx.ts | 1 + resolving/npm-resolver/src/index.ts | 6 +- resolving/npm-resolver/src/pickPackage.ts | 165 +++++++++++++----- .../npm-resolver/src/pickPackageFromMeta.ts | 4 +- .../npm-resolver/test/publishedBy.test.ts | 107 ++++++++++++ .../src/createNewStoreController.ts | 2 + testing/command-defaults/src/index.ts | 1 + 15 files changed, 262 insertions(+), 46 deletions(-) create mode 100644 .changeset/minimum-release-age-ignore-missing-time.md diff --git a/.changeset/minimum-release-age-ignore-missing-time.md b/.changeset/minimum-release-age-ignore-missing-time.md new file mode 100644 index 0000000000..d8539b3d0b --- /dev/null +++ b/.changeset/minimum-release-age-ignore-missing-time.md @@ -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. diff --git a/config/reader/src/Config.ts b/config/reader/src/Config.ts index ec9232bbc0..21532e2566 100644 --- a/config/reader/src/Config.ts +++ b/config/reader/src/Config.ts @@ -259,6 +259,7 @@ export interface Config extends OptionsFromRootManifest { preserveAbsolutePaths?: boolean minimumReleaseAge?: number minimumReleaseAgeExclude?: string[] + minimumReleaseAgeIgnoreMissingTime?: boolean minimumReleaseAgeStrict?: boolean fetchWarnTimeoutMs?: number fetchMinSpeedKiBps?: number diff --git a/config/reader/src/configFileKey.ts b/config/reader/src/configFileKey.ts index a64015e166..8213aec684 100644 --- a/config/reader/src/configFileKey.ts +++ b/config/reader/src/configFileKey.ts @@ -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', diff --git a/config/reader/src/index.ts b/config/reader/src/index.ts index 4326da7a50..2800876c3d 100644 --- a/config/reader/src/index.ts +++ b/config/reader/src/index.ts @@ -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', diff --git a/config/reader/src/localConfig.ts b/config/reader/src/localConfig.ts index 7a15d8c9c6..c870f1b42d 100644 --- a/config/reader/src/localConfig.ts +++ b/config/reader/src/localConfig.ts @@ -77,6 +77,7 @@ const AUTH_CFG_KEYS = [ const SECURITY_POLICY_CFG_KEYS = [ 'minimumReleaseAge', 'minimumReleaseAgeExclude', + 'minimumReleaseAgeIgnoreMissingTime', 'minimumReleaseAgeStrict', 'trustPolicy', 'trustPolicyExclude', diff --git a/config/reader/src/types.ts b/config/reader/src/types.ts index 0d99194a83..bf830cd62c 100644 --- a/config/reader/src/types.ts +++ b/config/reader/src/types.ts @@ -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, diff --git a/deps/inspection/commands/src/fetchPackageInfo.ts b/deps/inspection/commands/src/fetchPackageInfo.ts index 2d43830a99..bba10be945 100644 --- a/deps/inspection/commands/src/fetchPackageInfo.ts +++ b/deps/inspection/commands/src/fetchPackageInfo.ts @@ -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}`) diff --git a/deps/inspection/outdated/src/createManifestGetter.ts b/deps/inspection/outdated/src/createManifestGetter.ts index cae742d3a5..0084149057 100644 --- a/deps/inspection/outdated/src/createManifestGetter.ts +++ b/deps/inspection/outdated/src/createManifestGetter.ts @@ -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 diff --git a/exec/commands/src/dlx.ts b/exec/commands/src/dlx.ts index 12b447ede8..d4d61552fa 100644 --- a/exec/commands/src/dlx.ts +++ b/exec/commands/src/dlx.ts @@ -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, diff --git a/resolving/npm-resolver/src/index.ts b/resolving/npm-resolver/src/index.ts index 67d549e183..497bb261ec 100644 --- a/resolving/npm-resolver/src/index.ts +++ b/resolving/npm-resolver/src/index.ts @@ -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) { diff --git a/resolving/npm-resolver/src/pickPackage.ts b/resolving/npm-resolver/src/pickPackage.ts index 75ce170f7f..b086ca649c 100644 --- a/resolving/npm-resolver/src/pickPackage.ts +++ b/resolving/npm-resolver/src/pickPackage.ts @@ -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() + +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 { try { const stat = await fs.stat(filePath) diff --git a/resolving/npm-resolver/src/pickPackageFromMeta.ts b/resolving/npm-resolver/src/pickPackageFromMeta.ts index 4bef57e996..a75c48c424 100644 --- a/resolving/npm-resolver/src/pickPackageFromMeta.ts +++ b/resolving/npm-resolver/src/pickPackageFromMeta.ts @@ -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 diff --git a/resolving/npm-resolver/test/publishedBy.test.ts b/resolving/npm-resolver/test/publishedBy.test.ts index 4eec09d4a2..1f80b4d540 100644 --- a/resolving/npm-resolver/test/publishedBy.test.ts +++ b/resolving/npm-resolver/test/publishedBy.test.ts @@ -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 diff --git a/store/connection-manager/src/createNewStoreController.ts b/store/connection-manager/src/createNewStoreController.ts index 4a8e5211fe..3645a20017 100644 --- a/store/connection-manager/src/createNewStoreController.ts +++ b/store/connection-manager/src/createNewStoreController.ts @@ -34,6 +34,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick