Skip to content

Commit bb22255

Browse files
runyasakantfu
andauthored
feat: add maturity period option to filter newly released packages (#205)
Co-authored-by: Anthony Fu <[email protected]> Co-authored-by: Anthony Fu <[email protected]>
1 parent 031d72e commit bb22255

File tree

7 files changed

+78
-12
lines changed

7 files changed

+78
-12
lines changed

src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ cli
3636
.option('--timediff', 'show time difference between the current and the updated version')
3737
.option('--nodecompat', 'show package compatibility with current node version')
3838
.option('--peer', 'Include peerDependencies in the update process')
39+
.option('--maturity-period [days]', 'wait period in days before upgrading to newly released packages (default: 7 when flag is used, 0 when not used)')
3940
.action(async (mode: RangeMode | undefined, options: Partial<CheckOptions>) => {
4041
if (mode) {
4142
if (!MODE_CHOICES.includes(mode)) {
@@ -44,6 +45,11 @@ cli
4445
}
4546
options.mode = mode
4647
}
48+
49+
if ('maturityPeriod' in options && typeof options.maturityPeriod !== 'number') {
50+
options.maturityPeriod = 7
51+
}
52+
4753
const resolved = await resolveConfig(options)
4854

4955
let exitCode

src/commands/check/interactive.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ export async function promptInteractive(pkgs: PackageMeta[], options: CheckOptio
142142
dep: ResolvedDepChange,
143143
): InteractiveRenderer {
144144
const versions = Object.entries({
145-
minor: getVersionOfRange(dep, 'minor'),
146-
patch: getVersionOfRange(dep, 'patch'),
145+
minor: getVersionOfRange(dep, 'minor', options),
146+
patch: getVersionOfRange(dep, 'patch', options),
147147
...dep.pkgData.tags,
148148
})
149149
.map(([name, version]) => {

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ export const DEFAULT_CHECK_OPTIONS: CheckOptions = {
4040
group: true,
4141
includeLocked: false,
4242
nodecompat: true,
43+
maturityPeriod: 0,
4344
}

src/io/resolves.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getPackageMode } from '../utils/config'
1111
import { getNpmConfig } from '../utils/npm'
1212
import { parsePnpmPackagePath, parseYarnPackagePath } from '../utils/package'
1313
import { fetchJsrPackageMeta, fetchPackage } from '../utils/packument'
14-
import { filterDeprecatedVersions, getMaxSatisfying, getPrefixedVersion } from '../utils/versions'
14+
import { filterDeprecatedVersions, filterVersionsByMaturityPeriod, getMaxSatisfying, getPrefixedVersion } from '../utils/versions'
1515

1616
const debug = {
1717
cache: _debug('taze:cache'),
@@ -94,18 +94,24 @@ export async function getPackageData(name: string, protocol: Protocol = 'npm'):
9494
}
9595
}
9696

97-
export function getVersionOfRange(dep: ResolvedDepChange, range: RangeMode) {
98-
const { versions, tags, deprecated } = dep.pkgData
97+
export function getVersionOfRange(dep: ResolvedDepChange, range: RangeMode, options: CheckOptions) {
98+
const { versions, tags, deprecated, time } = dep.pkgData
9999

100-
const nonDeprecatedVersions = deprecated && Object.keys(deprecated).length > 0
101-
? filterDeprecatedVersions(versions, deprecated)
102-
: versions
100+
let filteredVersions = versions
103101

104-
if (nonDeprecatedVersions.length === 0) {
102+
if (deprecated && Object.keys(deprecated).length > 0) {
103+
filteredVersions = filterDeprecatedVersions(filteredVersions, deprecated)
104+
}
105+
106+
if (options.maturityPeriod && options.maturityPeriod > 0) {
107+
filteredVersions = filterVersionsByMaturityPeriod(filteredVersions, time, options.maturityPeriod)
108+
}
109+
110+
if (filteredVersions.length === 0) {
105111
return undefined
106112
}
107113

108-
return getMaxSatisfying(nonDeprecatedVersions, dep.currentVersion, range, tags)
114+
return getMaxSatisfying(filteredVersions, dep.currentVersion, range, tags)
109115
}
110116

111117
export function updateTargetVersion(
@@ -249,7 +255,7 @@ export async function resolveDependency(
249255
return dep
250256
}
251257

252-
target = getVersionOfRange(dep, mergeMode as RangeMode)
258+
target = getVersionOfRange(dep, mergeMode as RangeMode, options)
253259

254260
if (!target) {
255261
dep.diff = null

src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@ export interface CheckOptions extends CommonOptions {
157157
* @default true
158158
*/
159159
nodecompat?: boolean
160+
/**
161+
* Wait period in days before upgrading to newly released packages
162+
* This helps avoid potentially malicious packages released recently
163+
*
164+
* @default 0 (no waiting period)
165+
*/
166+
maturityPeriod?: number
160167
}
161168

162169
interface BasePackageMeta {

src/utils/versions.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,28 @@ export function getMaxSatisfying(versions: string[], current: string, mode: Rang
104104
export function filterDeprecatedVersions(versions: string[], deprecated: Record<string, string | boolean>): string[] {
105105
return versions.filter(version => !deprecated[version])
106106
}
107+
108+
export function filterVersionsByMaturityPeriod(
109+
versions: string[],
110+
time: Record<string, string> | undefined,
111+
maturityPeriodDays: number,
112+
): string[] {
113+
if (!time || maturityPeriodDays <= 0) {
114+
return versions
115+
}
116+
117+
const now = new Date()
118+
const cutoffDate = new Date(now.getTime() - (maturityPeriodDays * 24 * 60 * 60 * 1000))
119+
120+
return versions.filter((version) => {
121+
const versionTime = time[version]
122+
if (!versionTime) {
123+
return true
124+
}
125+
126+
const releaseDate = new Date(versionTime)
127+
const isMature = releaseDate < cutoffDate
128+
129+
return isMature
130+
})
131+
}

test/versions.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, it } from 'vitest'
22
import { getPackageData } from '../src/io/resolves'
3-
import { filterDeprecatedVersions, getMaxSatisfying, getVersionRangePrefix } from '../src/utils/versions'
3+
import { filterDeprecatedVersions, filterVersionsByMaturityPeriod, getMaxSatisfying, getVersionRangePrefix } from '../src/utils/versions'
44

55
it('getVersionRange', () => {
66
expect('~').toBe(getVersionRangePrefix('~1.2.3'))
@@ -129,3 +129,24 @@ it('deprecated filter', () => {
129129
const noDeprecated = filterDeprecatedVersions(versions, {})
130130
expect(noDeprecated).toEqual(versions)
131131
})
132+
133+
it('maturity period filter', () => {
134+
const now = new Date()
135+
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
136+
const eightDaysAgo = new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000)
137+
138+
const versions = ['1.0.0', '1.1.0', '2.0.0']
139+
const time = {
140+
'1.0.0': eightDaysAgo.toISOString(),
141+
'1.1.0': oneDayAgo.toISOString(),
142+
'2.0.0': now.toISOString(),
143+
}
144+
145+
// Test with 7 days - should filter out recent versions
146+
const filtered = filterVersionsByMaturityPeriod(versions, time, 7)
147+
expect(filtered).toEqual(['1.0.0'])
148+
149+
// Test with 0 days - should return all versions
150+
const noFilter = filterVersionsByMaturityPeriod(versions, time, 0)
151+
expect(noFilter).toEqual(versions)
152+
})

0 commit comments

Comments
 (0)