mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-24 07:38:12 -05:00
feat: set minimumReleaseAge to delay new versions of dependencies from being installed (#9957)
close #9921
This commit is contained in:
22
.changeset/full-dolls-invite.md
Normal file
22
.changeset/full-dolls-invite.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": minor
|
||||
"@pnpm/resolve-dependencies": minor
|
||||
"@pnpm/store-connection-manager": minor
|
||||
"@pnpm/npm-resolver": minor
|
||||
"@pnpm/core": minor
|
||||
"@pnpm/config": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
There have been several incidents recently where popular packages were successfully attacked. To reduce the risk of installing a compromised version, we are introducing a new setting that delays the installation of newly released dependencies. In most cases, such attacks are discovered quickly and the malicious versions are removed from the registry within an hour.
|
||||
|
||||
The new setting is called `minimumReleaseAge`. It specifies the number of minutes that must pass after a version is published before pnpm will install it. For example, setting `minimumReleaseAge: 1440` ensures that only packages released at least one day ago can be installed.
|
||||
|
||||
If you set `minimumReleaseAge` but need to disable this restriction for certain dependencies, you can list them under the `minimumReleaseAgeExclude` setting. For instance, with the following configuration pnpm will always install the latest version of webpack, regardless of its release time:
|
||||
|
||||
```yaml
|
||||
minimumReleaseAgeExclude:
|
||||
- webpack
|
||||
```
|
||||
|
||||
Related issue: [#9921](https://github.com/pnpm/pnpm/issues/9921).
|
||||
@@ -227,6 +227,8 @@ export interface Config extends OptionsFromRootManifest {
|
||||
dangerouslyAllowAllBuilds: boolean
|
||||
ci: boolean
|
||||
preserveAbsolutePaths?: boolean
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
}
|
||||
|
||||
export interface ConfigWithDeprecatedSettings extends Config {
|
||||
|
||||
@@ -63,6 +63,8 @@ export const types = Object.assign({
|
||||
maxsockets: Number,
|
||||
'modules-cache-max-age': Number,
|
||||
'dlx-cache-max-age': Number,
|
||||
'minimum-release-age': Number,
|
||||
'minimum-release-age-exclude': [String, Array],
|
||||
'modules-dir': String,
|
||||
'network-concurrency': Number,
|
||||
'node-linker': ['pnp', 'isolated', 'hoisted'],
|
||||
|
||||
@@ -165,6 +165,8 @@ export interface StrictInstallOptions {
|
||||
returnListOfDepsRequiringBuild?: boolean
|
||||
injectWorkspacePackages?: boolean
|
||||
ci?: boolean
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
}
|
||||
|
||||
export type InstallOptions =
|
||||
|
||||
@@ -1179,6 +1179,8 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
supportedArchitectures: opts.supportedArchitectures,
|
||||
peersSuffixMaxLength: opts.peersSuffixMaxLength,
|
||||
injectWorkspacePackages: opts.injectWorkspacePackages,
|
||||
minimumReleaseAge: opts.minimumReleaseAge,
|
||||
minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude,
|
||||
}
|
||||
)
|
||||
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {
|
||||
|
||||
24
pkg-manager/core/test/install/minimumReleaseAge.ts
Normal file
24
pkg-manager/core/test/install/minimumReleaseAge.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { addDependenciesToPackage } from '@pnpm/core'
|
||||
import { testDefaults } from '../utils/index.js'
|
||||
|
||||
const isOdd011ReleaseDate = new Date(2016, 11, 7 - 2) // 0.1.1 was released at 2016-12-07T07:18:01.205Z
|
||||
const diff = Date.now() - isOdd011ReleaseDate.getTime()
|
||||
const minimumReleaseAge = diff / (60 * 1000) // converting to minutes
|
||||
|
||||
test('minimumReleaseAge prevents installation of versions that do not meet the required publish date cutoff', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-odd@0.1'], testDefaults({ minimumReleaseAge }))
|
||||
|
||||
expect(manifest.dependencies!['is-odd']).toEqual('~0.1.0')
|
||||
})
|
||||
|
||||
test('minimumReleaseAge is ignored for packages in the minimumReleaseAgeExclude array', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const opts = testDefaults({ minimumReleaseAge, minimumReleaseAgeExclude: ['is-odd'] })
|
||||
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-odd@0.1'], opts)
|
||||
|
||||
expect(manifest.dependencies!['is-odd']).toEqual('~0.1.2')
|
||||
})
|
||||
@@ -400,6 +400,21 @@ test('add: fail trying to install @pnpm/exe', async () => {
|
||||
expect(err.code).toBe('ERR_PNPM_GLOBAL_PNPM_INSTALL')
|
||||
})
|
||||
|
||||
test('minimumReleaseAge makes install fail if there is no version that was published before the cutoff', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const isOdd011ReleaseDate = new Date(2016, 11, 7 - 2) // 0.1.1 was released at 2016-12-07T07:18:01.205Z
|
||||
const diff = Date.now() - isOdd011ReleaseDate.getTime()
|
||||
const minimumReleaseAge = diff / (60 * 1000) // converting to minutes
|
||||
|
||||
await expect(add.handler({
|
||||
...DEFAULT_OPTIONS,
|
||||
dir: path.resolve('project'),
|
||||
minimumReleaseAge,
|
||||
linkWorkspacePackages: false,
|
||||
}, ['is-odd@0.1.1'])).rejects.toThrow('No matching version found')
|
||||
})
|
||||
|
||||
describeOnLinuxOnly('filters optional dependencies based on pnpm.supportedArchitectures.libc', () => {
|
||||
test.each([
|
||||
['glibc', '@pnpm.e2e+only-linux-x64-glibc@1.0.0', '@pnpm.e2e+only-linux-x64-musl@1.0.0'],
|
||||
|
||||
@@ -176,6 +176,8 @@ export interface ResolutionContext {
|
||||
workspacePackages?: WorkspacePackages
|
||||
missingPeersOfChildrenByPkgId: Record<PkgResolutionId, { depth: number, missingPeersOfChildren: MissingPeersOfChildren }>
|
||||
hoistPeers?: boolean
|
||||
maximumPublishedBy?: Date
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
}
|
||||
|
||||
export interface MissingPeerInfo {
|
||||
@@ -485,6 +487,9 @@ async function resolveDependenciesOfImporters (
|
||||
time = result.newTime
|
||||
}
|
||||
}
|
||||
if (ctx.maximumPublishedBy && (publishedBy == null || publishedBy > ctx.maximumPublishedBy)) {
|
||||
publishedBy = ctx.maximumPublishedBy
|
||||
}
|
||||
const pkgAddressesByImportersWithoutPeers = await Promise.all(zipWith(async (importer, { pkgAddresses, postponedResolutionsQueue, postponedPeersResolutionQueue }) => {
|
||||
const newPreferredVersions = Object.create(importer.preferredVersions) as PreferredVersions
|
||||
const currentParentPkgAliases: Record<string, PkgAddress | true> = {}
|
||||
@@ -589,6 +594,7 @@ async function resolveDependenciesOfImporterDependency (
|
||||
parentPkgAliases: importer.parentPkgAliases,
|
||||
pickLowestVersion: pickLowestVersion && !importer.updatePackageManifest,
|
||||
pinnedVersion: importer.pinnedVersion,
|
||||
publishedBy: ctx.maximumPublishedBy,
|
||||
},
|
||||
extendedWantedDep
|
||||
)
|
||||
@@ -1299,6 +1305,17 @@ async function resolveDependency (
|
||||
if (!options.updateRequested && options.preferredVersion != null) {
|
||||
wantedDependency.bareSpecifier = replaceVersionInBareSpecifier(wantedDependency.bareSpecifier, options.preferredVersion)
|
||||
}
|
||||
let publishedBy: Date | undefined
|
||||
if (
|
||||
options.publishedBy &&
|
||||
(
|
||||
ctx.minimumReleaseAgeExclude == null ||
|
||||
wantedDependency.alias == null ||
|
||||
!ctx.minimumReleaseAgeExclude.includes(wantedDependency.alias)
|
||||
)
|
||||
) {
|
||||
publishedBy = options.publishedBy
|
||||
}
|
||||
pkgResponse = await ctx.storeController.requestPackage(wantedDependency, {
|
||||
alwaysTryWorkspacePackages: ctx.linkWorkspacePackagesDepth >= options.currentDepth,
|
||||
currentPkg: currentPkg
|
||||
@@ -1312,7 +1329,7 @@ async function resolveDependency (
|
||||
expectedPkg: currentPkg,
|
||||
defaultTag: ctx.defaultTag,
|
||||
ignoreScripts: ctx.ignoreScripts,
|
||||
publishedBy: options.publishedBy,
|
||||
publishedBy,
|
||||
pickLowestVersion: options.pickLowestVersion,
|
||||
downloadPriority: -options.currentDepth,
|
||||
lockfileDir: ctx.lockfileDir,
|
||||
|
||||
@@ -133,6 +133,8 @@ export interface ResolveDependenciesOptions {
|
||||
workspacePackages: WorkspacePackages
|
||||
supportedArchitectures?: SupportedArchitectures
|
||||
peersSuffixMaxLength: number
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
}
|
||||
|
||||
export interface ResolveDependencyTreeResult {
|
||||
@@ -193,6 +195,8 @@ export async function resolveDependencyTree<T> (
|
||||
missingPeersOfChildrenByPkgId: {},
|
||||
hoistPeers: autoInstallPeers || opts.dedupePeerDependents,
|
||||
allPeerDepNames: new Set(),
|
||||
maximumPublishedBy: opts.minimumReleaseAge ? new Date(Date.now() - opts.minimumReleaseAge * 60 * 1000) : undefined,
|
||||
minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude,
|
||||
}
|
||||
|
||||
const resolveArgs: ImporterToResolve[] = importers.map((importer) => {
|
||||
|
||||
@@ -75,6 +75,7 @@ export interface ResolverFactoryOptions {
|
||||
registries: Registries
|
||||
saveWorkspaceProtocol?: boolean | 'rolling'
|
||||
preserveAbsolutePaths?: boolean
|
||||
strictPublishedByCheck?: boolean
|
||||
}
|
||||
|
||||
export interface NpmResolveResult extends ResolveResult {
|
||||
@@ -132,6 +133,7 @@ export function createNpmResolver (
|
||||
offline: opts.offline,
|
||||
preferOffline: opts.preferOffline,
|
||||
cacheDir: opts.cacheDir,
|
||||
strictPublishedByCheck: opts.strictPublishedByCheck,
|
||||
}),
|
||||
registries: opts.registries,
|
||||
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
|
||||
|
||||
@@ -25,6 +25,10 @@ export interface PackageMeta {
|
||||
cachedAt?: number
|
||||
}
|
||||
|
||||
export interface PackageMetaWithTime extends PackageMeta {
|
||||
time: PackageMetaTime
|
||||
}
|
||||
|
||||
export type PackageMetaTime = Record<string, string> & {
|
||||
unpublished?: {
|
||||
time: string
|
||||
@@ -90,6 +94,15 @@ export interface PickPackageOptions {
|
||||
updateToLatest?: boolean
|
||||
}
|
||||
|
||||
function pickPackageFromMetaUsingTimeStrict (
|
||||
spec: RegistryPackageSpec,
|
||||
preferredVersionSelectors: VersionSelectors | undefined,
|
||||
meta: PackageMeta,
|
||||
publishedBy?: Date
|
||||
): PackageInRegistry | null {
|
||||
return pickPackageFromMeta(pickVersionByVersionRange, spec, preferredVersionSelectors, meta, publishedBy)
|
||||
}
|
||||
|
||||
function pickPackageFromMetaUsingTime (
|
||||
spec: RegistryPackageSpec,
|
||||
preferredVersionSelectors: VersionSelectors | undefined,
|
||||
@@ -98,7 +111,7 @@ function pickPackageFromMetaUsingTime (
|
||||
): PackageInRegistry | null {
|
||||
const pickedPackage = pickPackageFromMeta(pickVersionByVersionRange, spec, preferredVersionSelectors, meta, publishedBy)
|
||||
if (pickedPackage) return pickedPackage
|
||||
return pickPackageFromMeta(pickLowestVersionByVersionRange, spec, preferredVersionSelectors, meta, publishedBy)
|
||||
return pickPackageFromMeta(pickLowestVersionByVersionRange, spec, preferredVersionSelectors, meta)
|
||||
}
|
||||
|
||||
export async function pickPackage (
|
||||
@@ -110,6 +123,7 @@ export async function pickPackage (
|
||||
offline?: boolean
|
||||
preferOffline?: boolean
|
||||
filterMetadata?: boolean
|
||||
strictPublishedByCheck?: boolean
|
||||
},
|
||||
spec: RegistryPackageSpec,
|
||||
opts: PickPackageOptions
|
||||
@@ -117,7 +131,7 @@ export async function pickPackage (
|
||||
opts = opts || {}
|
||||
let _pickPackageFromMeta =
|
||||
opts.publishedBy
|
||||
? pickPackageFromMetaUsingTime
|
||||
? (ctx.strictPublishedByCheck ? pickPackageFromMetaUsingTimeStrict : pickPackageFromMetaUsingTime)
|
||||
: (pickPackageFromMeta.bind(null, opts.pickLowestVersion ? pickLowestVersionByVersionRange : pickVersionByVersionRange))
|
||||
|
||||
if (opts.updateToLatest) {
|
||||
@@ -186,11 +200,17 @@ export async function pickPackage (
|
||||
if (opts.publishedBy) {
|
||||
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror))
|
||||
if (metaCachedInStore?.cachedAt && new Date(metaCachedInStore.cachedAt) >= opts.publishedBy) {
|
||||
const pickedPackage = _pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy)
|
||||
if (pickedPackage) {
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
pickedPackage,
|
||||
try {
|
||||
const pickedPackage = _pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy)
|
||||
if (pickedPackage) {
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
pickedPackage,
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (ctx.strictPublishedByCheck) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { globalWarn } from '@pnpm/logger'
|
||||
import { type VersionSelectors } from '@pnpm/resolver-base'
|
||||
import semver from 'semver'
|
||||
import util from 'util'
|
||||
import { type RegistryPackageSpec } from './parseBareSpecifier.js'
|
||||
import { type PackageInRegistry, type PackageMeta } from './pickPackage.js'
|
||||
import { type PackageInRegistry, type PackageMeta, type PackageMetaWithTime } from './pickPackage.js'
|
||||
|
||||
export type PickVersionByVersionRange = (
|
||||
meta: PackageMeta,
|
||||
versionRange: string,
|
||||
preferredVerSels?: VersionSelectors,
|
||||
export interface PickVersionByVersionRangeOptions {
|
||||
meta: PackageMeta
|
||||
versionRange: string
|
||||
preferredVersionSelectors?: VersionSelectors
|
||||
publishedBy?: Date
|
||||
) => string | null
|
||||
}
|
||||
|
||||
export type PickVersionByVersionRange = (options: PickVersionByVersionRangeOptions) => string | null
|
||||
|
||||
export function pickPackageFromMeta (
|
||||
pickVersionByVersionRangeFn: PickVersionByVersionRange,
|
||||
@@ -19,6 +22,10 @@ export function pickPackageFromMeta (
|
||||
meta: PackageMeta,
|
||||
publishedBy?: Date
|
||||
): PackageInRegistry | null {
|
||||
if (publishedBy) {
|
||||
assertMetaHasTime(meta)
|
||||
meta = filterMetaByPublishedDate(meta, publishedBy)
|
||||
}
|
||||
if ((!meta.versions || Object.keys(meta.versions).length === 0) && !publishedBy) {
|
||||
// Unfortunately, the npm registry doesn't return the time field in the abbreviated metadata.
|
||||
// So we won't always know if the package was unpublished.
|
||||
@@ -37,7 +44,12 @@ export function pickPackageFromMeta (
|
||||
version = meta['dist-tags'][spec.fetchSpec]
|
||||
break
|
||||
case 'range':
|
||||
version = pickVersionByVersionRangeFn(meta, spec.fetchSpec, preferredVersionSelectors, publishedBy)
|
||||
version = pickVersionByVersionRangeFn({
|
||||
meta,
|
||||
versionRange: spec.fetchSpec,
|
||||
preferredVersionSelectors,
|
||||
publishedBy,
|
||||
})
|
||||
break
|
||||
}
|
||||
if (!version) return null
|
||||
@@ -67,6 +79,12 @@ export function pickPackageFromMeta (
|
||||
}
|
||||
}
|
||||
|
||||
function assertMetaHasTime (meta: PackageMeta): asserts meta is PackageMetaWithTime {
|
||||
if (meta.time == null) {
|
||||
throw new PnpmError('MISSING_TIME', `The metadata of ${meta.name} is missing the "time" field`)
|
||||
}
|
||||
}
|
||||
|
||||
const semverRangeCache = new Map<string, semver.Range | null>()
|
||||
|
||||
// This is a performance optimization; working with string-ish semver
|
||||
@@ -95,12 +113,10 @@ function semverSatisfiesLoose (version: string, range: string): boolean {
|
||||
}
|
||||
|
||||
export function pickLowestVersionByVersionRange (
|
||||
meta: PackageMeta,
|
||||
versionRange: string,
|
||||
preferredVerSels?: VersionSelectors
|
||||
{ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions
|
||||
): string | null {
|
||||
if (preferredVerSels != null && Object.keys(preferredVerSels).length > 0) {
|
||||
const prioritizedPreferredVersions = prioritizePreferredVersions(meta, versionRange, preferredVerSels)
|
||||
if (preferredVersionSelectors != null && Object.keys(preferredVersionSelectors).length > 0) {
|
||||
const prioritizedPreferredVersions = prioritizePreferredVersions(meta, versionRange, preferredVersionSelectors)
|
||||
for (const preferredVersions of prioritizedPreferredVersions) {
|
||||
const preferredVersion = semver.minSatisfying(preferredVersions, versionRange, true)
|
||||
if (preferredVersion) {
|
||||
@@ -114,16 +130,11 @@ export function pickLowestVersionByVersionRange (
|
||||
return semver.minSatisfying(Object.keys(meta.versions), versionRange, true)
|
||||
}
|
||||
|
||||
export function pickVersionByVersionRange (
|
||||
meta: PackageMeta,
|
||||
versionRange: string,
|
||||
preferredVerSels?: VersionSelectors,
|
||||
publishedBy?: Date
|
||||
): string | null {
|
||||
let latest: string | undefined = meta['dist-tags'].latest
|
||||
export function pickVersionByVersionRange ({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null {
|
||||
const latest: string | undefined = meta['dist-tags'].latest
|
||||
|
||||
if (preferredVerSels != null && Object.keys(preferredVerSels).length > 0) {
|
||||
const prioritizedPreferredVersions = prioritizePreferredVersions(meta, versionRange, preferredVerSels)
|
||||
if (preferredVersionSelectors != null && Object.keys(preferredVersionSelectors).length > 0) {
|
||||
const prioritizedPreferredVersions = prioritizePreferredVersions(meta, versionRange, preferredVersionSelectors)
|
||||
for (const preferredVersions of prioritizedPreferredVersions) {
|
||||
if (preferredVersions.includes(latest) && semverSatisfiesLoose(latest, versionRange)) {
|
||||
return latest
|
||||
@@ -135,16 +146,7 @@ export function pickVersionByVersionRange (
|
||||
}
|
||||
}
|
||||
|
||||
let versions = Object.keys(meta.versions)
|
||||
if (publishedBy) {
|
||||
if (meta.time == null) {
|
||||
throw new PnpmError('MISSING_TIME', `The metadata of ${meta.name} is missing the "time" field`)
|
||||
}
|
||||
versions = versions.filter(version => new Date(meta.time![version]) <= publishedBy)
|
||||
if (!versions.includes(latest)) {
|
||||
latest = undefined
|
||||
}
|
||||
}
|
||||
const versions = Object.keys(meta.versions)
|
||||
if (latest && (versionRange === '*' || semverSatisfiesLoose(latest, versionRange))) {
|
||||
// Not using semver.satisfies in case of * because it does not select beta versions.
|
||||
// E.g.: 1.0.0-beta.1. See issue: https://github.com/pnpm/pnpm/issues/865
|
||||
@@ -225,3 +227,59 @@ class PreferredVersionsPrioritizer {
|
||||
.map((weight) => versionsByWeight[parseInt(weight, 10)])
|
||||
}
|
||||
}
|
||||
|
||||
function filterMetaByPublishedDate (meta: PackageMetaWithTime, publishedBy: Date): PackageMeta {
|
||||
const versionsWithinDate: PackageMeta['versions'] = {}
|
||||
for (const version in meta.versions) {
|
||||
if (!Object.prototype.hasOwnProperty.call(meta.versions, version)) continue
|
||||
const timeStr = meta.time[version]
|
||||
if (timeStr && new Date(timeStr) <= publishedBy) {
|
||||
versionsWithinDate[version] = meta.versions[version]
|
||||
}
|
||||
}
|
||||
|
||||
const distTagsWithinDate: PackageMeta['dist-tags'] = {}
|
||||
const allDistTags = meta['dist-tags'] ?? {}
|
||||
for (const tag in allDistTags) {
|
||||
if (!Object.prototype.hasOwnProperty.call(allDistTags, tag)) continue
|
||||
const distTagVersion = allDistTags[tag]
|
||||
if (versionsWithinDate[distTagVersion]) {
|
||||
distTagsWithinDate[tag] = distTagVersion
|
||||
continue
|
||||
}
|
||||
// Repopulate the tag to the highest version available within date that has the same major as the original tag's version
|
||||
let originalSemVer: semver.SemVer | null = null
|
||||
try {
|
||||
originalSemVer = new semver.SemVer(distTagVersion, true)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
const originalMajor = originalSemVer.major
|
||||
let bestVersion: string | undefined
|
||||
const originalMajorPrefix = `${originalMajor}.`
|
||||
for (const candidate in versionsWithinDate) {
|
||||
if (!Object.prototype.hasOwnProperty.call(versionsWithinDate, candidate)) continue
|
||||
if (!candidate.startsWith(originalMajorPrefix)) continue
|
||||
if (!bestVersion) {
|
||||
bestVersion = candidate
|
||||
} else {
|
||||
try {
|
||||
if (semver.gt(candidate, bestVersion, true)) {
|
||||
bestVersion = candidate
|
||||
}
|
||||
} catch (err) {
|
||||
globalWarn(`Failed to compare semver versions ${candidate} and ${bestVersion} from packument of ${meta.name}, skipping candidate version.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestVersion) {
|
||||
distTagsWithinDate[tag] = bestVersion
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...meta,
|
||||
versions: versionsWithinDate,
|
||||
'dist-tags': distTagsWithinDate,
|
||||
}
|
||||
}
|
||||
|
||||
115
resolving/npm-resolver/test/distTagsByDate.test.ts
Normal file
115
resolving/npm-resolver/test/distTagsByDate.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { createFetchFromRegistry } from '@pnpm/fetch'
|
||||
import { createNpmResolver } from '@pnpm/npm-resolver'
|
||||
import { type Registries } from '@pnpm/types'
|
||||
import nock from 'nock'
|
||||
import tempy from 'tempy'
|
||||
|
||||
const registries: Registries = {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
}
|
||||
|
||||
const fetch = createFetchFromRegistry({})
|
||||
const getAuthHeader = () => undefined
|
||||
const createResolveFromNpm = createNpmResolver.bind(null, fetch, getAuthHeader)
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll()
|
||||
nock.disableNetConnect()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
nock.enableNetConnect()
|
||||
})
|
||||
|
||||
test('repopulate dist-tag to highest same-major version within the date cutoff', async () => {
|
||||
const name = 'dist-tag-date'
|
||||
const meta = {
|
||||
name,
|
||||
versions: {
|
||||
'3.0.0': {
|
||||
name,
|
||||
version: '3.0.0',
|
||||
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.0.0.tgz` },
|
||||
},
|
||||
'3.1.0': {
|
||||
name,
|
||||
version: '3.1.0',
|
||||
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.1.0.tgz` },
|
||||
},
|
||||
'3.2.0': {
|
||||
name,
|
||||
version: '3.2.0',
|
||||
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.2.0.tgz` },
|
||||
},
|
||||
'2.9.9': {
|
||||
name,
|
||||
version: '2.9.9',
|
||||
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-2.9.9.tgz` },
|
||||
},
|
||||
},
|
||||
'dist-tags': {
|
||||
latest: '3.2.0',
|
||||
},
|
||||
time: {
|
||||
'2.9.9': '2020-01-01T00:00:00.000Z',
|
||||
'3.0.0': '2020-02-01T00:00:00.000Z',
|
||||
'3.1.0': '2020-03-01T00:00:00.000Z',
|
||||
'3.2.0': '2020-05-01T00:00:00.000Z',
|
||||
},
|
||||
}
|
||||
|
||||
// Cutoff before 3.2.0, so latest must be remapped to 3.1.0 (same major 3)
|
||||
const cutoff = new Date('2020-04-01T00:00:00.000Z')
|
||||
|
||||
nock(registries.default)
|
||||
.get(`/${name}`)
|
||||
.reply(200, meta)
|
||||
|
||||
const cacheDir = tempy.directory()
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
cacheDir,
|
||||
fullMetadata: true,
|
||||
registries,
|
||||
})
|
||||
|
||||
const res = await resolveFromNpm({ alias: name, bareSpecifier: 'latest' }, {
|
||||
publishedBy: cutoff,
|
||||
})
|
||||
|
||||
expect(res!.id).toBe(`${name}@3.1.0`)
|
||||
})
|
||||
|
||||
test('keep dist-tag if original version is within the date cutoff', async () => {
|
||||
const name = 'dist-tag-date-keep'
|
||||
const meta = {
|
||||
name,
|
||||
versions: {
|
||||
'1.0.0': {
|
||||
name,
|
||||
version: '1.0.0',
|
||||
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-1.0.0.tgz` },
|
||||
},
|
||||
},
|
||||
'dist-tags': { latest: '1.0.0' },
|
||||
time: { '1.0.0': '2020-01-01T00:00:00.000Z' },
|
||||
}
|
||||
|
||||
const cutoff = new Date('2020-02-01T00:00:00.000Z')
|
||||
|
||||
nock(registries.default)
|
||||
.get(`/${name}`)
|
||||
.reply(200, meta)
|
||||
|
||||
const cacheDir = tempy.directory()
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
cacheDir,
|
||||
fullMetadata: true,
|
||||
registries,
|
||||
})
|
||||
|
||||
const res = await resolveFromNpm({ alias: name, bareSpecifier: 'latest' }, {
|
||||
publishedBy: cutoff,
|
||||
})
|
||||
|
||||
expect(res!.id).toBe(`${name}@1.0.0`)
|
||||
})
|
||||
@@ -29,6 +29,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
|
||||
| 'key'
|
||||
| 'localAddress'
|
||||
| 'maxSockets'
|
||||
| 'minimumReleaseAge'
|
||||
| 'networkConcurrency'
|
||||
| 'noProxy'
|
||||
| 'offline'
|
||||
@@ -53,7 +54,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
|
||||
export async function createNewStoreController (
|
||||
opts: CreateNewStoreControllerOptions
|
||||
): Promise<{ ctrl: StoreController, dir: string }> {
|
||||
const fullMetadata = opts.fetchFullMetadata ?? (opts.resolutionMode === 'time-based' && !opts.registrySupportsTimeField)
|
||||
const fullMetadata = opts.fetchFullMetadata ?? ((opts.resolutionMode === 'time-based' || Boolean(opts.minimumReleaseAge)) && !opts.registrySupportsTimeField)
|
||||
const { resolve, fetchers, clearResolutionCache } = createClient({
|
||||
customFetchers: opts.hooks?.fetchers,
|
||||
userConfig: opts.userConfig,
|
||||
@@ -94,6 +95,7 @@ export async function createNewStoreController (
|
||||
includeOnlyPackageFiles: !opts.deployAllFiles,
|
||||
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
|
||||
preserveAbsolutePaths: opts.preserveAbsolutePaths,
|
||||
strictPublishedByCheck: Boolean(opts.minimumReleaseAge),
|
||||
})
|
||||
await fs.mkdir(opts.storeDir, { recursive: true })
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user