|
| 1 | +package relaxer |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "slices" |
| 6 | + |
| 7 | + "deps.dev/util/resolve" |
| 8 | + "deps.dev/util/semver" |
| 9 | +) |
| 10 | + |
| 11 | +type NPMRelaxer struct{} |
| 12 | + |
| 13 | +func (r NPMRelaxer) Relax(ctx context.Context, cl resolve.Client, req resolve.RequirementVersion, allowMajor bool) (resolve.RequirementVersion, bool) { |
| 14 | + c, err := semver.NPM.ParseConstraint(req.Version) |
| 15 | + if err != nil { |
| 16 | + // The specified version is not a valid semver constraint |
| 17 | + // Check if it's a version tag (usually 'latest') by seeing if there are matching versions |
| 18 | + vks, err := cl.MatchingVersions(ctx, req.VersionKey) |
| 19 | + if err != nil || len(vks) == 0 { // no matches, cannot relax |
| 20 | + return req, false |
| 21 | + } |
| 22 | + // Use the first matching version (there should only be one) as a pinned version |
| 23 | + c, err = semver.NPM.ParseConstraint(vks[0].Version) |
| 24 | + if err != nil { |
| 25 | + return req, false |
| 26 | + } |
| 27 | + } |
| 28 | + |
| 29 | + // Get all the concrete versions of the package |
| 30 | + allVKs, err := cl.Versions(ctx, req.PackageKey) |
| 31 | + if err != nil { |
| 32 | + return req, false |
| 33 | + } |
| 34 | + var vers []string |
| 35 | + for _, vk := range allVKs { |
| 36 | + if vk.VersionType == resolve.Concrete { |
| 37 | + vers = append(vers, vk.Version) |
| 38 | + } |
| 39 | + } |
| 40 | + slices.SortFunc(vers, semver.NPM.Compare) |
| 41 | + |
| 42 | + // Find the versions on either side of the upper boundary of the requirement |
| 43 | + var lastIdx int // highest version matching constraint |
| 44 | + var nextIdx int = -1 // next version outside of range, preferring non-prerelease |
| 45 | + nextIsPre := true // if the next version is a prerelease version |
| 46 | + for lastIdx = len(vers) - 1; lastIdx >= 0; lastIdx-- { |
| 47 | + v, err := semver.NPM.Parse(vers[lastIdx]) |
| 48 | + if err != nil { |
| 49 | + continue |
| 50 | + } |
| 51 | + if c.MatchVersion(v) { // found the upper bound, stop iterating |
| 52 | + break |
| 53 | + } |
| 54 | + |
| 55 | + // Want to prefer non-prerelease versions, so only select one if we haven't seen any non-prerelease versions |
| 56 | + if !v.IsPrerelease() || nextIsPre { |
| 57 | + nextIdx = lastIdx |
| 58 | + nextIsPre = v.IsPrerelease() |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + // Didn't find any higher versions of the package |
| 63 | + if nextIdx == -1 { |
| 64 | + return req, false |
| 65 | + } |
| 66 | + |
| 67 | + // No versions match the existing constraint, something is wrong |
| 68 | + if lastIdx == -1 { |
| 69 | + return req, false |
| 70 | + } |
| 71 | + |
| 72 | + // Our desired relaxation ordering is |
| 73 | + // 1.2.3 -> 1.2.* -> 1.*.* -> 2.*.* -> 3.*.* -> ... |
| 74 | + // But we want to use npm-like version specifiers e.g. |
| 75 | + // 1.2.3 -> ~1.2.4 -> ^1.4.5 -> ^2.6.7 -> ^3.8.9 -> ... |
| 76 | + // using the latest versions of the ranges |
| 77 | + |
| 78 | + cmpVer := vers[lastIdx] |
| 79 | + _, diff, _ := semver.NPM.Difference(cmpVer, vers[nextIdx]) |
| 80 | + if diff == semver.DiffMajor { |
| 81 | + if !allowMajor { |
| 82 | + return req, false |
| 83 | + } |
| 84 | + // Want to step only one major version at a time |
| 85 | + // Instead of looking for a difference larger than major, |
| 86 | + // we want to look for a major version bump from the first next version |
| 87 | + cmpVer = vers[nextIdx] |
| 88 | + diff = semver.DiffMinor |
| 89 | + } |
| 90 | + |
| 91 | + // Find the highest version with the same difference |
| 92 | + best := vers[nextIdx] |
| 93 | + for i := nextIdx + 1; i < len(vers); i++ { |
| 94 | + _, d, err := semver.NPM.Difference(cmpVer, vers[i]) |
| 95 | + if err != nil { |
| 96 | + continue |
| 97 | + } |
| 98 | + // DiffMajor < DiffMinor < DiffPatch < DiffPrerelease |
| 99 | + // So if d is less than the original diff, it represents a larger change |
| 100 | + if d < diff { |
| 101 | + break |
| 102 | + } |
| 103 | + ver, err := semver.NPM.Parse(vers[i]) |
| 104 | + if err != nil { |
| 105 | + continue |
| 106 | + } |
| 107 | + if !ver.IsPrerelease() || nextIsPre { |
| 108 | + best = vers[i] |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + if diff == semver.DiffPatch { |
| 113 | + req.Version = "~" + best |
| 114 | + } else { |
| 115 | + req.Version = "^" + best |
| 116 | + } |
| 117 | + |
| 118 | + return req, true |
| 119 | +} |
0 commit comments