UNPKG

3.35 kBPlain TextView Raw
1import debugFactory from 'debug';
2import semver from 'semver';
3
4const debug = debugFactory('clark:lib:version');
5
6/**
7 * Range operators
8 */
9enum RangeOperator {
10 /**
11 * Caret operator
12 */
13 Caret = '^',
14 /**
15 * Tilde operator
16 */
17 Tilde = '~',
18 /**
19 * No operator
20 */
21 Exact = '',
22}
23
24/**
25 * Removes the range operator from a version string and converts it to a SemVer
26 * @param version
27 */
28function extractExactVersion(version: string): string {
29 const exact = semver.clean(
30 version.replace(RangeOperator.Caret, '').replace(RangeOperator.Tilde, ''),
31 );
32
33 if (!exact) {
34 throw new Error(`"${version}" is not a valid semver`);
35 }
36
37 return exact;
38}
39
40/**
41 * Extracts the range modififer from a semver string
42 * @param version
43 */
44function extractRangeOperator(version: string): RangeOperator {
45 if (version.startsWith('^')) {
46 return RangeOperator.Caret;
47 }
48 if (version.startsWith('~')) {
49 return RangeOperator.Tilde;
50 }
51
52 return RangeOperator.Exact;
53}
54
55/**
56 * Determines the most permissive range operator between two version strings
57 * @param left
58 * @param right
59 */
60function extractMostPermissiveOperator(
61 left: string,
62 right: string,
63): RangeOperator {
64 const leftType = extractRangeOperator(left);
65 const rightType = extractRangeOperator(right);
66
67 if (leftType === RangeOperator.Caret || rightType === RangeOperator.Caret) {
68 return RangeOperator.Caret;
69 }
70
71 if (leftType === RangeOperator.Tilde || rightType === RangeOperator.Tilde) {
72 return RangeOperator.Tilde;
73 }
74
75 return RangeOperator.Exact;
76}
77
78/**
79 * Indicates if two version strings have the same range operator
80 * @param left
81 * @param right
82 */
83function hasSameOperator(left: string, right: string): boolean {
84 const leftType = extractRangeOperator(left);
85 const rightType = extractRangeOperator(right);
86 return leftType === rightType;
87}
88
89/**
90 * Selects the greater of two semver ranges combined with their most permissive
91 * range operator
92 * @param left
93 * @param right
94 */
95export function select(left: string | null, right: string | null): string {
96 debug(`checking if "${left}" and "${right}" are compatible`);
97
98 // There are *much* simpler ways to write this, but typescript disagrees.
99
100 if (left === null) {
101 if (right) {
102 return right;
103 }
104 throw new Error('Cannot select a version from "null" and "null"');
105 }
106
107 if (right === null) {
108 if (left) {
109 return left;
110 }
111 throw new Error('Cannot select a version from "null" and "null"');
112 }
113
114 if (!semver.intersects(left, right)) {
115 debug(`"${left}" and "${right}" are not compatible`);
116 throw new Error(`"${left}" and "${right}" are not compatible`);
117 }
118
119 debug(`"${left}" and "${right}" are compatible`);
120
121 const leftExact = extractExactVersion(left);
122 const rightExact = extractExactVersion(right);
123
124 debug(`checking if "${left}" and "${right}" have the same range operator`);
125 if (hasSameOperator(left, right)) {
126 debug(`"${left}" and "${right}" have the same range operator`);
127 if (semver.gt(leftExact, rightExact)) {
128 return left;
129 } else {
130 return right;
131 }
132 }
133
134 debug(`"${left}" and "${right}" do not have the same range operator`);
135
136 const operator = extractMostPermissiveOperator(left, right);
137
138 return `${operator}${
139 semver.gt(leftExact, rightExact) ? leftExact : rightExact
140 }`;
141}