mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: add support for exact versions in minimumReleaseAgeExclude (#10059)
close #9985 --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
5
.changeset/fruity-parts-trade.md
Normal file
5
.changeset/fruity-parts-trade.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/registry.pkg-metadata-filter": minor
|
||||
---
|
||||
|
||||
Allow excluding certain trusted versions from the date check.
|
||||
5
.changeset/lazy-bananas-cry.md
Normal file
5
.changeset/lazy-bananas-cry.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/types": minor
|
||||
---
|
||||
|
||||
Add `PackageVersionPolicy` function type.
|
||||
8
.changeset/ripe-ducks-lose.md
Normal file
8
.changeset/ripe-ducks-lose.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@pnpm/resolver-base": minor
|
||||
"@pnpm/npm-resolver": minor
|
||||
"@pnpm/store-controller-types": minor
|
||||
"@pnpm/package-requester": minor
|
||||
---
|
||||
|
||||
The npm resolver supports `publishedByExclude` now.
|
||||
17
.changeset/vast-cloths-start.md
Normal file
17
.changeset/vast-cloths-start.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
"@pnpm/resolve-dependencies": minor
|
||||
"@pnpm/outdated": minor
|
||||
"@pnpm/core": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added support for exact versions in `minimumReleaseAgeExclude` [#9985](https://github.com/pnpm/pnpm/issues/9985).
|
||||
|
||||
You can now list one or more specific versions that pnpm should allow to install, even if those versions don’t satisfy the maturity requirement set by `minimumReleaseAge`. For example:
|
||||
|
||||
```yaml
|
||||
minimumReleaseAge: 1440
|
||||
minimumReleaseAgeExclude:
|
||||
- nx@21.6.5
|
||||
- webpack@4.47.0 || 5.102.1
|
||||
```
|
||||
5
.changeset/warm-bears-walk.md
Normal file
5
.changeset/warm-bears-walk.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/matcher": minor
|
||||
---
|
||||
|
||||
Implemented `createPackageVersionPolicy` function.
|
||||
@@ -34,10 +34,14 @@
|
||||
"compile": "tsc --build && pnpm run lint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "catalog:"
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"escape-string-regexp": "catalog:",
|
||||
"semver": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/matcher": "workspace:*"
|
||||
"@pnpm/matcher": "workspace:*",
|
||||
"@types/semver": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { type PackageVersionPolicy } from '@pnpm/types'
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
import semver from 'semver'
|
||||
|
||||
type Matcher = (input: string) => boolean
|
||||
type MatcherWithIndex = (input: string) => number
|
||||
@@ -96,3 +99,65 @@ function matcherWhenOnlyOnePattern (pattern: string): Matcher {
|
||||
const m = matcherFromPattern(ignorePattern)
|
||||
return (input) => !m(input)
|
||||
}
|
||||
|
||||
export function createPackageVersionPolicy (patterns: string[]): PackageVersionPolicy {
|
||||
const rules = patterns.map(parseVersionPolicyRule)
|
||||
return evaluateVersionPolicy.bind(null, rules)
|
||||
}
|
||||
|
||||
function evaluateVersionPolicy (rules: VersionPolicyRule[], pkgName: string): boolean | string[] {
|
||||
for (const { nameMatcher, exactVersions } of rules) {
|
||||
if (!nameMatcher(pkgName)) {
|
||||
continue
|
||||
}
|
||||
if (exactVersions.length === 0) {
|
||||
return true
|
||||
}
|
||||
return exactVersions
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
interface VersionPolicyRule {
|
||||
nameMatcher: Matcher
|
||||
exactVersions: string[]
|
||||
}
|
||||
|
||||
function parseVersionPolicyRule (pattern: string): VersionPolicyRule {
|
||||
const isScoped = pattern.startsWith('@')
|
||||
const atIndex = isScoped ? pattern.indexOf('@', 1) : pattern.indexOf('@')
|
||||
|
||||
if (atIndex === -1) {
|
||||
return { nameMatcher: createMatcher(pattern), exactVersions: [] }
|
||||
}
|
||||
|
||||
const packageName = pattern.slice(0, atIndex)
|
||||
const versionsPart = pattern.slice(atIndex + 1)
|
||||
|
||||
// Parse versions separated by ||
|
||||
const exactVersions: string[] | null = parseExactVersionsUnion(versionsPart)
|
||||
if (exactVersions == null) {
|
||||
throw new PnpmError('INVALID_VERSION_UNION',
|
||||
`Invalid versions union. Found: "${pattern}". Use exact versions only.`)
|
||||
}
|
||||
if (packageName.includes('*')) {
|
||||
throw new PnpmError('NAME_PATTERN_IN_VERSION_UNION', `Name patterns are not allowed with version unions. Found: "${pattern}"`)
|
||||
}
|
||||
|
||||
return {
|
||||
nameMatcher: (pkgName: string) => pkgName === packageName,
|
||||
exactVersions,
|
||||
}
|
||||
}
|
||||
|
||||
function parseExactVersionsUnion (versionsStr: string): string[] | null {
|
||||
const versions: string[] = []
|
||||
for (const versionRaw of versionsStr.split('||')) {
|
||||
const version = semver.valid(versionRaw)
|
||||
if (version == null) {
|
||||
return null
|
||||
}
|
||||
versions.push(version)
|
||||
}
|
||||
return versions
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createMatcher, createMatcherWithIndex } from '@pnpm/matcher'
|
||||
import { createMatcher, createMatcherWithIndex, createPackageVersionPolicy } from '@pnpm/matcher'
|
||||
|
||||
test('matcher()', () => {
|
||||
{
|
||||
@@ -111,3 +111,52 @@ test('createMatcherWithIndex()', () => {
|
||||
expect(match('baz')).toBe(-1)
|
||||
}
|
||||
})
|
||||
|
||||
test('createPackageVersionPolicy()', () => {
|
||||
{
|
||||
const match = createPackageVersionPolicy(['axios@1.12.2'])
|
||||
expect(match('axios')).toStrictEqual(['1.12.2'])
|
||||
}
|
||||
{
|
||||
const match = createPackageVersionPolicy(['is-*'])
|
||||
expect(match('is-odd')).toBe(true)
|
||||
expect(match('is-even')).toBe(true)
|
||||
expect(match('lodash')).toBe(false)
|
||||
}
|
||||
{
|
||||
const match = createPackageVersionPolicy(['@babel/core@7.20.0'])
|
||||
expect(match('@babel/core')).toStrictEqual(['7.20.0'])
|
||||
}
|
||||
{
|
||||
const match = createPackageVersionPolicy(['@babel/core'])
|
||||
expect(match('@babel/core')).toBe(true)
|
||||
}
|
||||
{
|
||||
const match = createPackageVersionPolicy(['axios@1.12.2'])
|
||||
expect(match('is-odd')).toBe(false)
|
||||
}
|
||||
{
|
||||
const match = createPackageVersionPolicy(['axios@1.12.2', 'lodash@4.17.21', 'is-*'])
|
||||
expect(match('axios')).toStrictEqual(['1.12.2'])
|
||||
expect(match('lodash')).toStrictEqual(['4.17.21'])
|
||||
expect(match('is-odd')).toBe(true)
|
||||
}
|
||||
{
|
||||
expect(() => createPackageVersionPolicy(['lodash@^4.17.0'])).toThrow(/Invalid versions union/)
|
||||
expect(() => createPackageVersionPolicy(['lodash@~4.17.0'])).toThrow(/Invalid versions union/)
|
||||
expect(() => createPackageVersionPolicy(['react@>=18.0.0'])).toThrow(/Invalid versions union/)
|
||||
expect(() => createPackageVersionPolicy(['is-*@1.0.0'])).toThrow(/Name patterns are not allowed/)
|
||||
}
|
||||
{
|
||||
const match = createPackageVersionPolicy(['axios@1.12.0 || 1.12.1'])
|
||||
expect(match('axios')).toStrictEqual(['1.12.0', '1.12.1'])
|
||||
}
|
||||
{
|
||||
const match = createPackageVersionPolicy(['@scope/pkg@1.0.0 || 1.0.1'])
|
||||
expect(match('@scope/pkg')).toStrictEqual(['1.0.0', '1.0.1'])
|
||||
}
|
||||
{
|
||||
const match = createPackageVersionPolicy(['pkg@1.0.0||1.0.1 || 1.0.2'])
|
||||
expect(match('pkg')).toStrictEqual(['1.0.0', '1.0.1', '1.0.2'])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,5 +8,12 @@
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": []
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/types"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
packages/types/src/config.ts
Normal file
1
packages/types/src/config.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type PackageVersionPolicy = (pkgName: string) => boolean | string[]
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './config.js'
|
||||
export * from './env.js'
|
||||
export * from './misc.js'
|
||||
export * from './options.js'
|
||||
|
||||
@@ -31,3 +31,42 @@ test('minimumReleaseAge is ignored for packages in the minimumReleaseAgeExclude
|
||||
|
||||
expect(manifest.dependencies!['is-odd']).toEqual('~0.1.2')
|
||||
})
|
||||
|
||||
test('minimumReleaseAge is ignored for specific exact versions in minimumReleaseAgeExclude', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const opts = testDefaults({
|
||||
minimumReleaseAge,
|
||||
minimumReleaseAgeExclude: ['is-odd@0.1.2'],
|
||||
})
|
||||
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-odd@0.1'], opts)
|
||||
|
||||
// 0.1.2 is excluded, so it should be installed despite being newer than minimumReleaseAge
|
||||
expect(manifest.dependencies!['is-odd']).toEqual('~0.1.2')
|
||||
})
|
||||
|
||||
test('minimumReleaseAge applies to versions not in minimumReleaseAgeExclude', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const opts = testDefaults({
|
||||
minimumReleaseAge,
|
||||
minimumReleaseAgeExclude: ['is-odd@0.1.0'],
|
||||
})
|
||||
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-odd@0.1'], opts)
|
||||
|
||||
// 0.1.2 is NOT excluded (only 0.1.0 is), so minimumReleaseAge applies
|
||||
// This should install 0.1.0 which is old enough
|
||||
expect(manifest.dependencies!['is-odd']).toEqual('~0.1.0')
|
||||
})
|
||||
|
||||
test('throws error when semver range is used in minimumReleaseAgeExclude', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
await expect(async () => {
|
||||
const opts = testDefaults({
|
||||
minimumReleaseAge,
|
||||
minimumReleaseAgeExclude: ['is-odd@^0.1.1'],
|
||||
})
|
||||
await addDependenciesToPackage({}, ['is-odd@0.1'], opts)
|
||||
}).rejects.toThrow(/Invalid versions union/)
|
||||
})
|
||||
|
||||
@@ -210,6 +210,7 @@ async function resolveAndFetch (
|
||||
alwaysTryWorkspacePackages: options.alwaysTryWorkspacePackages,
|
||||
defaultTag: options.defaultTag,
|
||||
publishedBy: options.publishedBy,
|
||||
publishedByExclude: options.publishedByExclude,
|
||||
pickLowestVersion: options.pickLowestVersion,
|
||||
lockfileDir: options.lockfileDir,
|
||||
preferredVersions,
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
type Registries,
|
||||
type PkgIdWithPatchHash,
|
||||
type PinnedVersion,
|
||||
type PackageVersionPolicy,
|
||||
} from '@pnpm/types'
|
||||
import * as dp from '@pnpm/dependency-path'
|
||||
import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions'
|
||||
@@ -179,7 +180,7 @@ export interface ResolutionContext {
|
||||
missingPeersOfChildrenByPkgId: Record<PkgResolutionId, { depth: number, missingPeersOfChildren: MissingPeersOfChildren }>
|
||||
hoistPeers?: boolean
|
||||
maximumPublishedBy?: Date
|
||||
minimumReleaseAgeExclude?: (pkgName: string) => boolean
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
}
|
||||
|
||||
export interface MissingPeerInfo {
|
||||
@@ -1307,17 +1308,6 @@ 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(wantedDependency.alias)
|
||||
)
|
||||
) {
|
||||
publishedBy = options.publishedBy
|
||||
}
|
||||
pkgResponse = await ctx.storeController.requestPackage(wantedDependency, {
|
||||
alwaysTryWorkspacePackages: ctx.linkWorkspacePackagesDepth >= options.currentDepth,
|
||||
currentPkg: currentPkg
|
||||
@@ -1331,7 +1321,8 @@ async function resolveDependency (
|
||||
expectedPkg: currentPkg,
|
||||
defaultTag: ctx.defaultTag,
|
||||
ignoreScripts: ctx.ignoreScripts,
|
||||
publishedBy,
|
||||
publishedBy: options.publishedBy,
|
||||
publishedByExclude: ctx.publishedByExclude,
|
||||
pickLowestVersion: options.pickLowestVersion,
|
||||
downloadPriority: -options.currentDepth,
|
||||
lockfileDir: ctx.lockfileDir,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { resolveFromCatalog } from '@pnpm/catalogs.resolver'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type LockfileObject } from '@pnpm/lockfile.types'
|
||||
import { globalWarn } from '@pnpm/logger'
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
import { createPackageVersionPolicy } from '@pnpm/matcher'
|
||||
import { type PatchGroupRecord } from '@pnpm/patching.config'
|
||||
import { type PreferredVersions, type Resolution, type WorkspacePackages } from '@pnpm/resolver-base'
|
||||
import { type StoreController } from '@pnpm/store-controller-types'
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
type ReadPackageHook,
|
||||
type Registries,
|
||||
type ProjectRootDir,
|
||||
type PackageVersionPolicy,
|
||||
} from '@pnpm/types'
|
||||
import partition from 'ramda/src/partition'
|
||||
import zipObj from 'ramda/src/zipObj'
|
||||
@@ -197,7 +199,16 @@ export async function resolveDependencyTree<T> (
|
||||
hoistPeers: autoInstallPeers || opts.dedupePeerDependents,
|
||||
allPeerDepNames: new Set(),
|
||||
maximumPublishedBy: opts.minimumReleaseAge ? new Date(Date.now() - opts.minimumReleaseAge * 60 * 1000) : undefined,
|
||||
minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude ? createMatcher(opts.minimumReleaseAgeExclude) : undefined,
|
||||
publishedByExclude: opts.minimumReleaseAgeExclude ? createPublishedByExclude(opts.minimumReleaseAgeExclude) : undefined,
|
||||
}
|
||||
|
||||
function createPublishedByExclude (patterns: string[]): PackageVersionPolicy {
|
||||
try {
|
||||
return createPackageVersionPolicy(patterns)
|
||||
} catch (err) {
|
||||
if (!err || typeof err !== 'object' || !('message' in err)) throw err
|
||||
throw new PnpmError('INVALID_MIN_RELEASE_AGE_EXCLUDE', `Invalid value in minimumReleaseAgeExclude: ${err.message as string}`)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveArgs: ImporterToResolve[] = importers.map((importer) => {
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -1803,13 +1803,25 @@ importers:
|
||||
|
||||
config/matcher:
|
||||
dependencies:
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/error
|
||||
'@pnpm/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
escape-string-regexp:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0
|
||||
semver:
|
||||
specifier: 'catalog:'
|
||||
version: 7.7.1
|
||||
devDependencies:
|
||||
'@pnpm/matcher':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
'@types/semver':
|
||||
specifier: 'catalog:'
|
||||
version: 7.5.3
|
||||
|
||||
config/normalize-registries:
|
||||
dependencies:
|
||||
|
||||
@@ -2,12 +2,16 @@ import { globalWarn } from '@pnpm/logger'
|
||||
import { type PackageMetadataWithTime } from '@pnpm/registry.types'
|
||||
import semver from 'semver'
|
||||
|
||||
export function filterPkgMetadataByPublishDate (pkgDoc: PackageMetadataWithTime, publishedBy: Date): PackageMetadataWithTime {
|
||||
export function filterPkgMetadataByPublishDate (
|
||||
pkgDoc: PackageMetadataWithTime,
|
||||
publishedBy: Date,
|
||||
trustedVersions?: string[]
|
||||
): PackageMetadataWithTime {
|
||||
const versionsWithinDate: PackageMetadataWithTime['versions'] = {}
|
||||
for (const version in pkgDoc.versions) {
|
||||
if (!Object.hasOwn(pkgDoc.versions, version)) continue
|
||||
const timeStr = pkgDoc.time[version]
|
||||
if (timeStr && new Date(timeStr) <= publishedBy) {
|
||||
if ((timeStr && new Date(timeStr) <= publishedBy) || trustedVersions?.includes(version)) {
|
||||
versionsWithinDate[version] = pkgDoc.versions[version]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
type WorkspacePackages,
|
||||
type WorkspacePackagesByVersion,
|
||||
} from '@pnpm/resolver-base'
|
||||
import { type DependencyManifest, type Registries, type PinnedVersion } from '@pnpm/types'
|
||||
import { type DependencyManifest, type Registries, type PinnedVersion, type PackageVersionPolicy } from '@pnpm/types'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import normalize from 'normalize-path'
|
||||
import pMemoize from 'p-memoize'
|
||||
@@ -178,6 +178,7 @@ export type ResolveFromNpmOptions = {
|
||||
alwaysTryWorkspacePackages?: boolean
|
||||
defaultTag?: string
|
||||
publishedBy?: Date
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
pickLowestVersion?: boolean
|
||||
dryRun?: boolean
|
||||
lockfileDir?: string
|
||||
@@ -234,6 +235,7 @@ async function resolveNpm (
|
||||
pickResult = await ctx.pickPackage(spec, {
|
||||
pickLowestVersion: opts.pickLowestVersion,
|
||||
publishedBy: opts.publishedBy,
|
||||
publishedByExclude: opts.publishedByExclude,
|
||||
authHeaderValue,
|
||||
dryRun: opts.dryRun === true,
|
||||
preferredVersionSelectors: opts.preferredVersions?.[spec.name],
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createHexHash } from '@pnpm/crypto.hash'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import gfs from '@pnpm/graceful-fs'
|
||||
import { type VersionSelectors } from '@pnpm/resolver-base'
|
||||
import { type PackageMeta, type PackageInRegistry } from '@pnpm/registry.types'
|
||||
import getRegistryName from 'encode-registry'
|
||||
import loadJsonFile from 'load-json-file'
|
||||
@@ -14,7 +13,12 @@ import pick from 'ramda/src/pick'
|
||||
import semver from 'semver'
|
||||
import renameOverwrite from 'rename-overwrite'
|
||||
import { toRaw } from './toRaw.js'
|
||||
import { pickPackageFromMeta, pickVersionByVersionRange, pickLowestVersionByVersionRange } from './pickPackageFromMeta.js'
|
||||
import {
|
||||
pickPackageFromMeta,
|
||||
pickVersionByVersionRange,
|
||||
pickLowestVersionByVersionRange,
|
||||
type PickPackageFromMetaOptions,
|
||||
} from './pickPackageFromMeta.js'
|
||||
import { type RegistryPackageSpec } from './parseBareSpecifier.js'
|
||||
|
||||
export interface PackageMetaCache {
|
||||
@@ -56,34 +60,26 @@ async function runLimited<T> (pkgMirror: string, fn: (limit: pLimit.Limit) => Pr
|
||||
}
|
||||
}
|
||||
|
||||
export interface PickPackageOptions {
|
||||
export interface PickPackageOptions extends PickPackageFromMetaOptions {
|
||||
authHeaderValue?: string
|
||||
publishedBy?: Date
|
||||
preferredVersionSelectors: VersionSelectors | undefined
|
||||
pickLowestVersion?: boolean
|
||||
registry: string
|
||||
dryRun: boolean
|
||||
updateToLatest?: boolean
|
||||
}
|
||||
|
||||
function pickPackageFromMetaUsingTimeStrict (
|
||||
spec: RegistryPackageSpec,
|
||||
preferredVersionSelectors: VersionSelectors | undefined,
|
||||
meta: PackageMeta,
|
||||
publishedBy?: Date
|
||||
): PackageInRegistry | null {
|
||||
return pickPackageFromMeta(pickVersionByVersionRange, spec, preferredVersionSelectors, meta, publishedBy)
|
||||
}
|
||||
const pickPackageFromMetaUsingTimeStrict = pickPackageFromMeta.bind(null, pickVersionByVersionRange)
|
||||
|
||||
function pickPackageFromMetaUsingTime (
|
||||
opts: PickPackageFromMetaOptions,
|
||||
spec: RegistryPackageSpec,
|
||||
preferredVersionSelectors: VersionSelectors | undefined,
|
||||
meta: PackageMeta,
|
||||
publishedBy?: Date
|
||||
meta: PackageMeta
|
||||
): PackageInRegistry | null {
|
||||
const pickedPackage = pickPackageFromMeta(pickVersionByVersionRange, spec, preferredVersionSelectors, meta, publishedBy)
|
||||
const pickedPackage = pickPackageFromMeta(pickVersionByVersionRange, opts, spec, meta)
|
||||
if (pickedPackage) return pickedPackage
|
||||
return pickPackageFromMeta(pickLowestVersionByVersionRange, spec, preferredVersionSelectors, meta)
|
||||
return pickPackageFromMeta(pickLowestVersionByVersionRange, {
|
||||
preferredVersionSelectors: opts.preferredVersionSelectors,
|
||||
}, spec, meta)
|
||||
}
|
||||
|
||||
export async function pickPackage (
|
||||
@@ -101,23 +97,30 @@ export async function pickPackage (
|
||||
opts: PickPackageOptions
|
||||
): Promise<{ meta: PackageMeta, pickedPackage: PackageInRegistry | null }> {
|
||||
opts = opts || {}
|
||||
let _pickPackageFromMeta =
|
||||
const pickPackageFromMetaBySpec = (
|
||||
opts.publishedBy
|
||||
? (ctx.strictPublishedByCheck ? pickPackageFromMetaUsingTimeStrict : pickPackageFromMetaUsingTime)
|
||||
: (pickPackageFromMeta.bind(null, opts.pickLowestVersion ? pickLowestVersionByVersionRange : pickVersionByVersionRange))
|
||||
).bind(null, {
|
||||
preferredVersionSelectors: opts.preferredVersionSelectors,
|
||||
publishedBy: opts.publishedBy,
|
||||
publishedByExclude: opts.publishedByExclude,
|
||||
})
|
||||
|
||||
let _pickPackageFromMeta!: (meta: PackageMeta) => PackageInRegistry | null
|
||||
if (opts.updateToLatest) {
|
||||
const _pickPackageBase = _pickPackageFromMeta
|
||||
_pickPackageFromMeta = (spec, ...rest) => {
|
||||
_pickPackageFromMeta = (meta) => {
|
||||
const latestStableSpec: RegistryPackageSpec = { ...spec, type: 'tag', fetchSpec: 'latest' }
|
||||
const latestStable = _pickPackageBase(latestStableSpec, ...rest)
|
||||
const current = _pickPackageBase(spec, ...rest)
|
||||
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)
|
||||
}
|
||||
|
||||
validatePackageName(spec.name)
|
||||
@@ -126,7 +129,7 @@ export async function pickPackage (
|
||||
if (cachedMeta != null) {
|
||||
return {
|
||||
meta: cachedMeta,
|
||||
pickedPackage: _pickPackageFromMeta(spec, opts.preferredVersionSelectors, cachedMeta, opts.publishedBy),
|
||||
pickedPackage: _pickPackageFromMeta(cachedMeta),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,14 +144,14 @@ export async function pickPackage (
|
||||
if (ctx.offline) {
|
||||
if (metaCachedInStore != null) return {
|
||||
meta: metaCachedInStore,
|
||||
pickedPackage: _pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy),
|
||||
pickedPackage: _pickPackageFromMeta(metaCachedInStore),
|
||||
}
|
||||
|
||||
throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`)
|
||||
}
|
||||
|
||||
if (metaCachedInStore != null) {
|
||||
const pickedPackage = _pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy)
|
||||
const pickedPackage = _pickPackageFromMeta(metaCachedInStore)
|
||||
if (pickedPackage) {
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
@@ -164,7 +167,7 @@ export async function pickPackage (
|
||||
// otherwise it is probably out of date
|
||||
if ((metaCachedInStore?.versions?.[spec.fetchSpec]) != null) {
|
||||
try {
|
||||
const pickedPackage = _pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy)
|
||||
const pickedPackage = _pickPackageFromMeta(metaCachedInStore)
|
||||
if (pickedPackage) {
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
@@ -182,7 +185,7 @@ export async function pickPackage (
|
||||
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror))
|
||||
if (metaCachedInStore?.cachedAt && new Date(metaCachedInStore.cachedAt) >= opts.publishedBy) {
|
||||
try {
|
||||
const pickedPackage = _pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy)
|
||||
const pickedPackage = _pickPackageFromMeta(metaCachedInStore)
|
||||
if (pickedPackage) {
|
||||
return {
|
||||
meta: metaCachedInStore,
|
||||
@@ -219,7 +222,7 @@ export async function pickPackage (
|
||||
}
|
||||
return {
|
||||
meta,
|
||||
pickedPackage: _pickPackageFromMeta(spec, opts.preferredVersionSelectors, meta, opts.publishedBy),
|
||||
pickedPackage: _pickPackageFromMeta(meta),
|
||||
}
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
err.spec = spec
|
||||
@@ -229,7 +232,7 @@ export async function pickPackage (
|
||||
logger.debug({ message: `Using cached meta from ${pkgMirror}` })
|
||||
return {
|
||||
meta,
|
||||
pickedPackage: _pickPackageFromMeta(spec, opts.preferredVersionSelectors, meta, opts.publishedBy),
|
||||
pickedPackage: _pickPackageFromMeta(meta),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PnpmError } from '@pnpm/error'
|
||||
import { filterPkgMetadataByPublishDate } from '@pnpm/registry.pkg-metadata-filter'
|
||||
import { type PackageInRegistry, type PackageMeta, type PackageMetaWithTime } from '@pnpm/registry.types'
|
||||
import { type VersionSelectors } from '@pnpm/resolver-base'
|
||||
import { type PackageVersionPolicy } from '@pnpm/types'
|
||||
import semver from 'semver'
|
||||
import util from 'util'
|
||||
import { type RegistryPackageSpec } from './parseBareSpecifier.js'
|
||||
@@ -15,16 +16,29 @@ export interface PickVersionByVersionRangeOptions {
|
||||
|
||||
export type PickVersionByVersionRange = (options: PickVersionByVersionRangeOptions) => string | null
|
||||
|
||||
export interface PickPackageFromMetaOptions {
|
||||
preferredVersionSelectors: VersionSelectors | undefined
|
||||
publishedBy?: Date
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
}
|
||||
|
||||
export function pickPackageFromMeta (
|
||||
pickVersionByVersionRangeFn: PickVersionByVersionRange,
|
||||
{
|
||||
preferredVersionSelectors,
|
||||
publishedBy,
|
||||
publishedByExclude,
|
||||
}: PickPackageFromMetaOptions,
|
||||
spec: RegistryPackageSpec,
|
||||
preferredVersionSelectors: VersionSelectors | undefined,
|
||||
meta: PackageMeta,
|
||||
publishedBy?: Date
|
||||
meta: PackageMeta
|
||||
): PackageInRegistry | null {
|
||||
if (publishedBy) {
|
||||
assertMetaHasTime(meta)
|
||||
meta = filterPkgMetadataByPublishDate(meta, publishedBy)
|
||||
const excludeResult = publishedByExclude?.(meta.name) ?? false
|
||||
if (excludeResult !== true) {
|
||||
const trustedVersions = Array.isArray(excludeResult) ? excludeResult : undefined
|
||||
meta = filterPkgMetadataByPublishDate(meta, publishedBy, trustedVersions)
|
||||
}
|
||||
}
|
||||
if ((!meta.versions || Object.keys(meta.versions).length === 0) && !publishedBy) {
|
||||
// Unfortunately, the npm registry doesn't return the time field in the abbreviated metadata.
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type DependencyManifest,
|
||||
type PkgResolutionId,
|
||||
type PinnedVersion,
|
||||
type PackageVersionPolicy,
|
||||
} from '@pnpm/types'
|
||||
|
||||
export { type PkgResolutionId }
|
||||
@@ -109,6 +110,7 @@ export interface ResolveOptions {
|
||||
defaultTag?: string
|
||||
pickLowestVersion?: boolean
|
||||
publishedBy?: Date
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
projectDir: string
|
||||
lockfileDir: string
|
||||
preferredVersions: PreferredVersions
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
createResolver,
|
||||
type ResolveFunction,
|
||||
} from '@pnpm/client'
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
import { type DependencyManifest } from '@pnpm/types'
|
||||
import { createPackageVersionPolicy } from '@pnpm/matcher'
|
||||
import { type PackageVersionPolicy, type DependencyManifest } from '@pnpm/types'
|
||||
|
||||
interface GetManifestOpts {
|
||||
dir: string
|
||||
@@ -14,13 +14,17 @@ interface GetManifestOpts {
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
}
|
||||
|
||||
export type ManifestGetterOptions = Omit<ClientOptions, 'authConfig'>
|
||||
export type ManifestGetterOptions = Omit<ClientOptions, 'authConfig' | 'minimumReleaseAgeExclude'>
|
||||
& GetManifestOpts
|
||||
& { fullMetadata: boolean, rawConfig: Record<string, string> }
|
||||
|
||||
export function createManifestGetter (
|
||||
opts: ManifestGetterOptions
|
||||
): (packageName: string, bareSpecifier: string) => Promise<DependencyManifest | null> {
|
||||
const publishedByExclude = opts.minimumReleaseAgeExclude
|
||||
? createPackageVersionPolicy(opts.minimumReleaseAgeExclude)
|
||||
: undefined
|
||||
|
||||
const { resolve } = createResolver({
|
||||
...opts,
|
||||
authConfig: opts.rawConfig,
|
||||
@@ -32,15 +36,11 @@ export function createManifestGetter (
|
||||
? new Date(Date.now() - opts.minimumReleaseAge * 60 * 1000)
|
||||
: undefined
|
||||
|
||||
const isExcludedMatcher = opts.minimumReleaseAgeExclude
|
||||
? createMatcher(opts.minimumReleaseAgeExclude)
|
||||
: undefined
|
||||
|
||||
return getManifest.bind(null, {
|
||||
...opts,
|
||||
resolve,
|
||||
publishedBy,
|
||||
isExcludedMatcher,
|
||||
publishedByExclude,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,24 +48,22 @@ export async function getManifest (
|
||||
opts: GetManifestOpts & {
|
||||
resolve: ResolveFunction
|
||||
publishedBy?: Date
|
||||
isExcludedMatcher?: ((packageName: string) => boolean)
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
},
|
||||
packageName: string,
|
||||
bareSpecifier: string
|
||||
): Promise<DependencyManifest | null> {
|
||||
const isExcluded = opts.isExcludedMatcher?.(packageName)
|
||||
const effectivePublishedBy = isExcluded ? undefined : opts.publishedBy
|
||||
|
||||
try {
|
||||
const resolution = await opts.resolve({ alias: packageName, bareSpecifier }, {
|
||||
lockfileDir: opts.lockfileDir,
|
||||
preferredVersions: {},
|
||||
projectDir: opts.dir,
|
||||
publishedBy: effectivePublishedBy,
|
||||
publishedBy: opts.publishedBy,
|
||||
publishedByExclude: opts.publishedByExclude,
|
||||
})
|
||||
return resolution?.manifest ?? null
|
||||
} catch (err) {
|
||||
if ((err as { code?: string }).code === 'ERR_PNPM_NO_MATCHING_VERSION' && effectivePublishedBy) {
|
||||
if ((err as { code?: string }).code === 'ERR_PNPM_NO_MATCHING_VERSION' && opts.publishedBy) {
|
||||
// No versions found that meet the minimumReleaseAge requirement
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -127,12 +127,9 @@ test('getManifest() with minimumReleaseAgeExclude', async () => {
|
||||
}
|
||||
|
||||
const publishedBy = new Date(Date.now() - 10080 * 60 * 1000)
|
||||
const isExcludedMatcher = (packageName: string) => packageName === 'excluded-package'
|
||||
const publishedByExclude = (packageName: string) => packageName === 'excluded-package'
|
||||
|
||||
const resolve: ResolveFunction = jest.fn(async function (wantedPackage, resolveOpts) {
|
||||
// Excluded package should not have publishedBy set
|
||||
expect(resolveOpts.publishedBy).toBeUndefined()
|
||||
|
||||
return {
|
||||
id: 'excluded-package/2.0.0' as PkgResolutionId,
|
||||
latest: '2.0.0',
|
||||
@@ -145,6 +142,6 @@ test('getManifest() with minimumReleaseAgeExclude', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
await getManifest({ ...opts, resolve, isExcludedMatcher, publishedBy }, 'excluded-package', 'latest')
|
||||
await getManifest({ ...opts, resolve, publishedByExclude, publishedBy }, 'excluded-package', 'latest')
|
||||
expect(resolve).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type DependencyManifest,
|
||||
type PackageManifest,
|
||||
type PinnedVersion,
|
||||
type PackageVersionPolicy,
|
||||
} from '@pnpm/types'
|
||||
|
||||
export type { PackageFileInfo, PackageFilesResponse, ImportPackageFunction, ImportPackageFunctionAsync }
|
||||
@@ -118,6 +119,7 @@ export interface RequestPackageOptions {
|
||||
defaultTag?: string
|
||||
pickLowestVersion?: boolean
|
||||
publishedBy?: Date
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
downloadPriority: number
|
||||
ignoreScripts?: boolean
|
||||
projectDir: string
|
||||
|
||||
Reference in New Issue
Block a user