semver.js
1 'use strict' 2 3 const debug = require('../internal/debug') 4 const { MAX_LENGTH, MAX_SAFE_INTEGER } = require('../internal/constants') 5 const { safeRe: re, t } = require('../internal/re') 6 7 const parseOptions = require('../internal/parse-options') 8 const { compareIdentifiers } = require('../internal/identifiers') 9 class SemVer { 10 constructor (version, options) { 11 options = parseOptions(options) 12 13 if (version instanceof SemVer) { 14 if (version.loose === !!options.loose && 15 version.includePrerelease === !!options.includePrerelease) { 16 return version 17 } else { 18 version = version.version 19 } 20 } else if (typeof version !== 'string') { 21 throw new TypeError(`Invalid version. Must be a string. Got type "${typeof version}".`) 22 } 23 24 if (version.length > MAX_LENGTH) { 25 throw new TypeError( 26 `version is longer than ${MAX_LENGTH} characters` 27 ) 28 } 29 30 debug('SemVer', version, options) 31 this.options = options 32 this.loose = !!options.loose 33 // this isn't actually relevant for versions, but keep it so that we 34 // don't run into trouble passing this.options around. 35 this.includePrerelease = !!options.includePrerelease 36 37 const m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL]) 38 39 if (!m) { 40 throw new TypeError(`Invalid Version: ${version}`) 41 } 42 43 this.raw = version 44 45 // these are actually numbers 46 this.major = +m[1] 47 this.minor = +m[2] 48 this.patch = +m[3] 49 50 if (this.major > MAX_SAFE_INTEGER || this.major < 0) { 51 throw new TypeError('Invalid major version') 52 } 53 54 if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) { 55 throw new TypeError('Invalid minor version') 56 } 57 58 if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) { 59 throw new TypeError('Invalid patch version') 60 } 61 62 // numberify any prerelease numeric ids 63 if (!m[4]) { 64 this.prerelease = [] 65 } else { 66 this.prerelease = m[4].split('.').map((id) => { 67 if (/^[0-9]+$/.test(id)) { 68 const num = +id 69 if (num >= 0 && num < MAX_SAFE_INTEGER) { 70 return num 71 } 72 } 73 return id 74 }) 75 } 76 77 this.build = m[5] ? m[5].split('.') : [] 78 this.format() 79 } 80 81 format () { 82 this.version = `${this.major}.${this.minor}.${this.patch}` 83 if (this.prerelease.length) { 84 this.version += `-${this.prerelease.join('.')}` 85 } 86 return this.version 87 } 88 89 toString () { 90 return this.version 91 } 92 93 compare (other) { 94 debug('SemVer.compare', this.version, this.options, other) 95 if (!(other instanceof SemVer)) { 96 if (typeof other === 'string' && other === this.version) { 97 return 0 98 } 99 other = new SemVer(other, this.options) 100 } 101 102 if (other.version === this.version) { 103 return 0 104 } 105 106 return this.compareMain(other) || this.comparePre(other) 107 } 108 109 compareMain (other) { 110 if (!(other instanceof SemVer)) { 111 other = new SemVer(other, this.options) 112 } 113 114 return ( 115 compareIdentifiers(this.major, other.major) || 116 compareIdentifiers(this.minor, other.minor) || 117 compareIdentifiers(this.patch, other.patch) 118 ) 119 } 120 121 comparePre (other) { 122 if (!(other instanceof SemVer)) { 123 other = new SemVer(other, this.options) 124 } 125 126 // NOT having a prerelease is > having one 127 if (this.prerelease.length && !other.prerelease.length) { 128 return -1 129 } else if (!this.prerelease.length && other.prerelease.length) { 130 return 1 131 } else if (!this.prerelease.length && !other.prerelease.length) { 132 return 0 133 } 134 135 let i = 0 136 do { 137 const a = this.prerelease[i] 138 const b = other.prerelease[i] 139 debug('prerelease compare', i, a, b) 140 if (a === undefined && b === undefined) { 141 return 0 142 } else if (b === undefined) { 143 return 1 144 } else if (a === undefined) { 145 return -1 146 } else if (a === b) { 147 continue 148 } else { 149 return compareIdentifiers(a, b) 150 } 151 } while (++i) 152 } 153 154 compareBuild (other) { 155 if (!(other instanceof SemVer)) { 156 other = new SemVer(other, this.options) 157 } 158 159 let i = 0 160 do { 161 const a = this.build[i] 162 const b = other.build[i] 163 debug('build compare', i, a, b) 164 if (a === undefined && b === undefined) { 165 return 0 166 } else if (b === undefined) { 167 return 1 168 } else if (a === undefined) { 169 return -1 170 } else if (a === b) { 171 continue 172 } else { 173 return compareIdentifiers(a, b) 174 } 175 } while (++i) 176 } 177 178 // preminor will bump the version up to the next minor release, and immediately 179 // down to pre-release. premajor and prepatch work the same way. 180 inc (release, identifier, identifierBase) { 181 if (release.startsWith('pre')) { 182 if (!identifier && identifierBase === false) { 183 throw new Error('invalid increment argument: identifier is empty') 184 } 185 // Avoid an invalid semver results 186 if (identifier) { 187 const match = `-${identifier}`.match(this.options.loose ? re[t.PRERELEASELOOSE] : re[t.PRERELEASE]) 188 if (!match || match[1] !== identifier) { 189 throw new Error(`invalid identifier: ${identifier}`) 190 } 191 } 192 } 193 194 switch (release) { 195 case 'premajor': 196 this.prerelease.length = 0 197 this.patch = 0 198 this.minor = 0 199 this.major++ 200 this.inc('pre', identifier, identifierBase) 201 break 202 case 'preminor': 203 this.prerelease.length = 0 204 this.patch = 0 205 this.minor++ 206 this.inc('pre', identifier, identifierBase) 207 break 208 case 'prepatch': 209 // If this is already a prerelease, it will bump to the next version 210 // drop any prereleases that might already exist, since they are not 211 // relevant at this point. 212 this.prerelease.length = 0 213 this.inc('patch', identifier, identifierBase) 214 this.inc('pre', identifier, identifierBase) 215 break 216 // If the input is a non-prerelease version, this acts the same as 217 // prepatch. 218 case 'prerelease': 219 if (this.prerelease.length === 0) { 220 this.inc('patch', identifier, identifierBase) 221 } 222 this.inc('pre', identifier, identifierBase) 223 break 224 case 'release': 225 if (this.prerelease.length === 0) { 226 throw new Error(`version ${this.raw} is not a prerelease`) 227 } 228 this.prerelease.length = 0 229 break 230 231 case 'major': 232 // If this is a pre-major version, bump up to the same major version. 233 // Otherwise increment major. 234 // 1.0.0-5 bumps to 1.0.0 235 // 1.1.0 bumps to 2.0.0 236 if ( 237 this.minor !== 0 || 238 this.patch !== 0 || 239 this.prerelease.length === 0 240 ) { 241 this.major++ 242 } 243 this.minor = 0 244 this.patch = 0 245 this.prerelease = [] 246 break 247 case 'minor': 248 // If this is a pre-minor version, bump up to the same minor version. 249 // Otherwise increment minor. 250 // 1.2.0-5 bumps to 1.2.0 251 // 1.2.1 bumps to 1.3.0 252 if (this.patch !== 0 || this.prerelease.length === 0) { 253 this.minor++ 254 } 255 this.patch = 0 256 this.prerelease = [] 257 break 258 case 'patch': 259 // If this is not a pre-release version, it will increment the patch. 260 // If it is a pre-release it will bump up to the same patch version. 261 // 1.2.0-5 patches to 1.2.0 262 // 1.2.0 patches to 1.2.1 263 if (this.prerelease.length === 0) { 264 this.patch++ 265 } 266 this.prerelease = [] 267 break 268 // This probably shouldn't be used publicly. 269 // 1.0.0 'pre' would become 1.0.0-0 which is the wrong direction. 270 case 'pre': { 271 const base = Number(identifierBase) ? 1 : 0 272 273 if (this.prerelease.length === 0) { 274 this.prerelease = [base] 275 } else { 276 let i = this.prerelease.length 277 while (--i >= 0) { 278 if (typeof this.prerelease[i] === 'number') { 279 this.prerelease[i]++ 280 i = -2 281 } 282 } 283 if (i === -1) { 284 // didn't increment anything 285 if (identifier === this.prerelease.join('.') && identifierBase === false) { 286 throw new Error('invalid increment argument: identifier already exists') 287 } 288 this.prerelease.push(base) 289 } 290 } 291 if (identifier) { 292 // 1.2.0-beta.1 bumps to 1.2.0-beta.2, 293 // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 294 let prerelease = [identifier, base] 295 if (identifierBase === false) { 296 prerelease = [identifier] 297 } 298 if (compareIdentifiers(this.prerelease[0], identifier) === 0) { 299 if (isNaN(this.prerelease[1])) { 300 this.prerelease = prerelease 301 } 302 } else { 303 this.prerelease = prerelease 304 } 305 } 306 break 307 } 308 default: 309 throw new Error(`invalid increment argument: ${release}`) 310 } 311 this.raw = this.format() 312 if (this.build.length) { 313 this.raw += `+${this.build.join('.')}` 314 } 315 return this 316 } 317 } 318 319 module.exports = SemVer