Skip to content

Commit dfb488e

Browse files
authored
only use stable versions when resolving by date (#1165)
1 parent 8a4c968 commit dfb488e

File tree

2 files changed

+126
-17
lines changed

2 files changed

+126
-17
lines changed

internal/npm/npm.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,45 @@ func ToTypesPackageName(pkgName string) string {
283283
}
284284

285285

286-
// ResolveVersionByTime finds the latest version published before or at the given time.
286+
// IsStableVersion returns true if the version is a stable release (not experimental, beta, alpha, etc.)
287+
func IsStableVersion(version string) bool {
288+
v := strings.ToLower(version)
289+
// Check for common prerelease identifiers
290+
prereleaseKeywords := []string{
291+
"experimental", "beta", "alpha", "rc", "pre", "preview", "canary", "dev", "nightly",
292+
"snapshot", "test", "unstable", "next", "latest", "edge", "insiders",
293+
}
294+
295+
for _, keyword := range prereleaseKeywords {
296+
if strings.Contains(v, keyword) {
297+
return false
298+
}
299+
}
300+
301+
// Additional check for semver prerelease pattern (e.g., 1.0.0-alpha.1)
302+
if strings.Contains(version, "-") {
303+
parts := strings.Split(version, "-")
304+
if len(parts) > 1 {
305+
prereleaseId := strings.ToLower(parts[1])
306+
// Check if the prerelease identifier starts with a known prerelease keyword
307+
for _, keyword := range prereleaseKeywords {
308+
if strings.HasPrefix(prereleaseId, keyword) {
309+
return false
310+
}
311+
}
312+
// Also check if the entire prerelease identifier is a known keyword
313+
for _, keyword := range prereleaseKeywords {
314+
if prereleaseId == keyword {
315+
return false
316+
}
317+
}
318+
}
319+
}
320+
321+
return true
322+
}
323+
324+
// ResolveVersionByTime finds the latest stable version published before or at the given time.
287325
func ResolveVersionByTime(metadata *PackageMetadata, targetTime time.Time) (string, error) {
288326
type versionTime struct {
289327
version string
@@ -301,6 +339,11 @@ func ResolveVersionByTime(metadata *PackageMetadata, targetTime time.Time) (stri
301339
continue
302340
}
303341

342+
// Skip unstable versions (experimental, beta, alpha, etc.)
343+
if !IsStableVersion(version) {
344+
continue
345+
}
346+
304347
publishTime, err := time.Parse(time.RFC3339, timeStr)
305348
if err != nil {
306349
continue // Skip invalid timestamps
@@ -316,7 +359,7 @@ func ResolveVersionByTime(metadata *PackageMetadata, targetTime time.Time) (stri
316359
}
317360

318361
if len(validVersions) == 0 {
319-
return "", errors.New("no versions found for the specified date")
362+
return "", errors.New("no stable versions found for the specified date")
320363
}
321364

322365
// Sort by publish time, latest first

internal/npm/npm_test.go

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,80 @@ import (
66
)
77

88

9+
func TestIsStableVersion(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
version string
13+
want bool
14+
}{
15+
// Stable versions
16+
{"Simple stable version", "1.0.0", true},
17+
{"Stable version with patch", "1.2.3", true},
18+
{"Stable version with build metadata", "1.0.0+build.1", true},
19+
20+
// Experimental versions
21+
{"Experimental version", "0.0.0-experimental-c5b937576-20231219", false},
22+
{"Experimental with caps", "1.0.0-EXPERIMENTAL", false},
23+
{"Experimental in middle", "1.0.0-experimental.1", false},
24+
25+
// Beta versions
26+
{"Beta version", "1.0.0-beta", false},
27+
{"Beta with number", "1.0.0-beta.1", false},
28+
{"Beta with caps", "1.0.0-BETA", false},
29+
30+
// Alpha versions
31+
{"Alpha version", "1.0.0-alpha", false},
32+
{"Alpha with number", "1.0.0-alpha.1", false},
33+
34+
// RC versions
35+
{"Release candidate", "1.0.0-rc", false},
36+
{"Release candidate with number", "1.0.0-rc.1", false},
37+
38+
// Other prerelease versions
39+
{"Preview version", "1.0.0-preview", false},
40+
{"Canary version", "1.0.0-canary", false},
41+
{"Dev version", "1.0.0-dev", false},
42+
{"Nightly version", "1.0.0-nightly", false},
43+
{"Next version", "1.0.0-next", false},
44+
{"Edge version", "1.0.0-edge", false},
45+
46+
// Version with prerelease in name but not in prerelease position
47+
{"Version with stable name", "1.0.0", true},
48+
{"Version with normal dash", "1.0.0-1", true}, // This should be stable as it's just a build number
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
got := IsStableVersion(tt.version)
54+
if got != tt.want {
55+
t.Errorf("IsStableVersion(%q) = %v, want %v", tt.version, got, tt.want)
56+
}
57+
})
58+
}
59+
}
60+
961
func TestResolveVersionByTime(t *testing.T) {
10-
// Create test metadata with known versions and times
62+
// Create test metadata with known versions and times, including experimental versions
1163
metadata := &PackageMetadata{
1264
Time: map[string]string{
13-
"created": "2020-01-01T00:00:00Z",
14-
"modified": "2025-01-01T00:00:00Z",
15-
"1.0.0": "2020-06-01T00:00:00Z", // 1590969600
16-
"1.1.0": "2021-01-01T00:00:00Z", // 1609459200
17-
"2.0.0": "2022-01-01T00:00:00Z", // 1640995200
18-
"2.1.0": "2023-01-01T00:00:00Z", // 1672531200
19-
"3.0.0": "2024-01-01T00:00:00Z", // 1704067200
65+
"created": "2020-01-01T00:00:00Z",
66+
"modified": "2025-01-01T00:00:00Z",
67+
"1.0.0": "2020-06-01T00:00:00Z",
68+
"0.0.0-experimental-c5b937576-20231219": "2020-12-19T00:00:00Z", // Experimental version between 1.0.0 and 1.1.0
69+
"1.1.0": "2021-01-01T00:00:00Z",
70+
"1.2.0-beta.1": "2021-06-01T00:00:00Z", // Beta version between 1.1.0 and 2.0.0
71+
"2.0.0": "2022-01-01T00:00:00Z",
72+
"2.1.0": "2023-01-01T00:00:00Z",
73+
"3.0.0": "2024-01-01T00:00:00Z",
2074
},
2175
Versions: map[string]PackageJSONRaw{
22-
"1.0.0": {Version: "1.0.0"},
23-
"1.1.0": {Version: "1.1.0"},
24-
"2.0.0": {Version: "2.0.0"},
25-
"2.1.0": {Version: "2.1.0"},
26-
"3.0.0": {Version: "3.0.0"},
76+
"1.0.0": {Version: "1.0.0"},
77+
"0.0.0-experimental-c5b937576-20231219": {Version: "0.0.0-experimental-c5b937576-20231219"},
78+
"1.1.0": {Version: "1.1.0"},
79+
"1.2.0-beta.1": {Version: "1.2.0-beta.1"},
80+
"2.0.0": {Version: "2.0.0"},
81+
"2.1.0": {Version: "2.1.0"},
82+
"3.0.0": {Version: "3.0.0"},
2783
},
2884
}
2985

@@ -45,10 +101,20 @@ func TestResolveVersionByTime(t *testing.T) {
45101
wantVersion: "1.0.0",
46102
},
47103
{
48-
name: "Between versions",
49-
targetTime: time.Unix(1620000000, 0), // 2021-05-02 (between 1.1.0 and 2.0.0)
104+
name: "Skip experimental version, return stable",
105+
targetTime: time.Unix(1608336000, 0), // 2020-12-19 00:00:00 UTC (exact time of experimental version)
106+
wantVersion: "1.0.0", // Should return 1.0.0, not the experimental version
107+
},
108+
{
109+
name: "Between versions, skip experimental",
110+
targetTime: time.Unix(1620000000, 0), // 2021-05-02 (between 1.1.0 and 2.0.0, experimental exists but should be ignored)
50111
wantVersion: "1.1.0",
51112
},
113+
{
114+
name: "Skip beta version, return stable",
115+
targetTime: time.Unix(1622505600, 0), // 2021-06-01 00:00:00 UTC (exact time of beta version)
116+
wantVersion: "1.1.0", // Should return 1.1.0, not the beta version
117+
},
52118
{
53119
name: "Latest available",
54120
targetTime: time.Unix(1735689600, 0), // 2025-01-01 00:00:00 UTC (after all versions)

0 commit comments

Comments
 (0)