@snyk/ruby-semver
Version:
node-semver compatible API with RubyGems semantics
348 lines • 13.3 kB
JavaScript
"use strict";
// -----------------------------
// The Version class processes string versions into comparable
// values. A version string should normally be a series of numbers
// separated by periods. Each part (digits separated by periods) is
// considered its own number, and these are used for sorting. So for
// instance, 3.10 sorts higher than 3.2 because ten is greater than
// two.
//
// If any part contains letters (currently only a-z are supported) then
// that version is considered prerelease. Versions with a prerelease
// part in the Nth part sort less than versions with N-1
// parts. Prerelease parts are sorted alphabetically using the normal
// Ruby string sorting rules. If a prerelease part contains both
// letters and numbers, it will be broken into multiple parts to
// provide expected sort behavior (1.0.a10 becomes 1.0.a.10, and is
// greater than 1.0.a9).
//
// Prereleases sort between real releases (newest to oldest):
//
// 1. 1.0
// 2. 1.0.b1
// 3. 1.0.a.2
// 4. 0.9
//
// If you want to specify a version restriction that includes both prereleases
// and regular releases of the 1.x series this is the best way:
//
// s.add_dependency 'example', '>= 1.0.0.a', '< 2.0.0'
//
// == How Software Changes
//
// Users expect to be able to specify a version constraint that gives them
// some reasonable expectation that new versions of a library will work with
// their software if the version constraint is true, and not work with their
// software if the version constraint is false. In other words, the perfect
// system will accept all compatible versions of the library and reject all
// incompatible versions.
//
// Libraries change in 3 ways (well, more than 3, but stay focused here!).
//
// 1. The change may be an implementation detail only and have no effect on
// the client software.
// 2. The change may add new features, but do so in a way that client software
// written to an earlier version is still compatible.
// 3. The change may change the public interface of the library in such a way
// that old software is no longer compatible.
//
// Some examples are appropriate at this point. Suppose I have a Stack class
// that supports a <tt>push</tt> and a <tt>pop</tt> method.
//
// === Examples of Category 1 changes:
//
// * Switch from an array based implementation to a linked-list based
// implementation.
// * Provide an automatic (and transparent) backing store for large stacks.
//
// === Examples of Category 2 changes might be:
//
// * Add a <tt>depth</tt> method to return the current depth of the stack.
// * Add a <tt>top</tt> method that returns the current top of stack (without
// changing the stack).
// * Change <tt>push</tt> so that it returns the item pushed (previously it
// had no usable return value).
//
// === Examples of Category 3 changes might be:
//
// * Changes <tt>pop</tt> so that it no longer returns a value (you must use
// <tt>top</tt> to get the top of the stack).
// * Rename the methods to <tt>push_item</tt> and <tt>pop_item</tt>.
//
// == RubyGems Rational Versioning
//
// * Versions shall be represented by three non-negative integers, separated
// by periods (e.g. 3.1.4). The first integers is the "major" version
// number, the second integer is the "minor" version number, and the third
// integer is the "build" number.
//
// * A category 1 change (implementation detail) will increment the build
// number.
//
// * A category 2 change (backwards compatible) will increment the minor
// version number and reset the build number.
//
// * A category 3 change (incompatible) will increment the major build number
// and reset the minor and build numbers.
//
// * Any "public" release of a gem should have a different version. Normally
// that means incrementing the build number. This means a developer can
// generate builds all day long, but as soon as they make a public release,
// the version must be updated.
//
// === Examples
//
// Let's work through a project lifecycle using our Stack example from above.
//
// Version 0.0.1:: The initial Stack class is release.
// Version 0.0.2:: Switched to a linked=list implementation because it is
// cooler.
// Version 0.1.0:: Added a <tt>depth</tt> method.
// Version 1.0.0:: Added <tt>top</tt> and made <tt>pop</tt> return nil
// (<tt>pop</tt> used to return the old top item).
// Version 1.1.0:: <tt>push</tt> now returns the value pushed (it used it
// return nil).
// Version 1.1.1:: Fixed a bug in the linked list implementation.
// Version 1.1.2:: Fixed a bug introduced in the last fix.
//
// Client A needs a stack with basic push/pop capability. They write to the
// original interface (no <tt>top</tt>), so their version constraint looks like:
//
// gem 'stack', '>= 0.0'
//
// Essentially, any version is OK with Client A. An incompatible change to
// the library will cause them grief, but they are willing to take the chance
// (we call Client A optimistic).
//
// Client B is just like Client A except for two things: (1) They use the
// <tt>depth</tt> method and (2) they are worried about future
// incompatibilities, so they write their version constraint like this:
//
// gem 'stack', '~> 0.1'
//
// The <tt>depth</tt> method was introduced in version 0.1.0, so that version
// or anything later is fine, as long as the version stays below version 1.0
// where incompatibilities are introduced. We call Client B pessimistic
// because they are worried about incompatible future changes (it is OK to be
// pessimistic!).
//
// == Preventing Version Catastrophe:
//
// From: http://blog.zenspider.com/2008/10/rubygems-howto-preventing-cata.html
//
// Let's say you're depending on the fnord gem version 2.y.z. If you
// specify your dependency as ">= 2.0.0" then, you're good, right? What
// happens if fnord 3.0 comes out and it isn't backwards compatible
// with 2.y.z? Your stuff will break as a result of using ">=". The
// better route is to specify your dependency with an "approximate" version
// specifier ("~>"). They're a tad confusing, so here is how the dependency
// specifiers work:
//
// Specification From ... To (exclusive)
// ">= 3.0" 3.0 ... ∞
// "~> 3.0" 3.0 ... 4.0
// "~> 3.0.0" 3.0.0 ... 3.1
// "~> 3.5" 3.5 ... 4.0
// "~> 3.5.0" 3.5.0 ... 3.6
// "~> 3" 3.0 ... 4.0
//
// For the last example, single-digit versions are automatically extended with
// a zero to give a sensible result.
Object.defineProperty(exports, "__esModule", { value: true });
const VERSION_PATTERN = '[0-9]+(\\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?';
const ANCHORED_VERSION_PATTERN = new RegExp(`^\\s*(${VERSION_PATTERN})?\\s*$`);
class GemVersion {
// @@all = {}
// def self.new version # :nodoc:
// return super unless GemVersion == self
// @@all[version] ||= super
// end
// -----------------------------
// Constructs a Version from the +version+ string. A version string is a
// series of digits or ASCII letters separated by dots.
constructor(version) {
if (!GemVersion.isCorrect(version)) {
throw new Error(`Malformed version number string ${version}`);
}
this.version = String(version).trim().replace('-', '.pre.');
}
// include Comparable
// -----------------------------
// A string representation of this Version.
toString() {
return this.version;
}
// -----------------------------
// True if the +version+ string matches RubyGems' requirements.
static isCorrect(version) {
return ANCHORED_VERSION_PATTERN.test(version);
}
// -----------------------------
// Factory method to create a Version object. Input may be a Version
// or a String. Intended to simplify client code.
//
// ver1 = Version.create('1.3.17') // -> (Version object)
// ver2 = Version.create(ver1) // -> (ver1)
// ver3 = Version.create(nil) // -> nil
static create(input) {
if (input instanceof GemVersion) {
return input;
}
if (!input) {
return undefined;
}
return new GemVersion(input);
}
// -----------------------------
// Return a new version object where the next to the last revision
// number is one greater (e.g., 5.3.1 => 5.4).
//
// Pre-release (alpha) parts, e.g, 5.3.1.b.2 => 5.4, are ignored.
// def bump
// @bump ||= begin
// segments = self.segments.dup
// segments.pop while segments.any? { |s| String === s }
// segments.pop if segments.size > 1
// segments[-1] = segments[-1].succ
// self.class.new segments.join(".")
// end
// end
bump() {
if (!this._bump) {
const segments = this.getSegments();
while (segments.some((x) => typeof x === 'string')) {
segments.pop();
}
if (segments.length > 1) {
segments.pop();
}
const last = segments.pop();
segments.push(Number(last) + 1);
this._bump = new GemVersion(segments.join('.'));
}
return this._bump;
}
// -----------------------------
// A Version is only eql? to another version if it's specified to the
// same precision. Version "1.0" is not the same as version "1".
isIdentical(other) {
return other instanceof GemVersion && other.version === this.version;
}
// def hash # :nodoc:
// @version.hash
// end
// def init_with coder # :nodoc:
// yaml_initialize coder.tag, coder.map
// end
// def inspect # :nodoc:
// "#<#{self.class} #{version.inspect}>"
// end
// -----------------------------
// Dump only the raw version string, not the complete object. It's a
// string for backwards (RubyGems 1.3.5 and earlier) compatibility.
// def marshal_dump
// [version]
// end
// -----------------------------
// Load custom marshal format. It's a string for backwards (RubyGems
// 1.3.5 and earlier) compatibility.
// def marshal_load array
// initialize array[0]
// end
// def yaml_initialize(tag, map) # :nodoc:
// @version = map['version']
// @segments = nil
// @hash = nil
// end
// def to_yaml_properties # :nodoc:
// ["@version"]
// end
// def encode_with coder # :nodoc:
// coder.add 'version', @version
// end
// -----------------------------
// A version is considered a prerelease if it contains a letter.
isPrerelease() {
if (this._isPrerelease === undefined) {
this._isPrerelease = /[a-zA-Z]/.test(this.version);
}
return this._isPrerelease;
}
// def pretty_print q # :nodoc:
// q.text "GemVersion.new(#{version.inspect})"
// end
// -----------------------------
// The release for this version (e.g. 1.2.0.a -> 1.2.0).
// Non-prerelease versions return themselves.
release() {
if (!this._release) {
if (this.isPrerelease) {
const segments = this.getSegments();
while (segments.some((x) => typeof x === 'string')) {
segments.pop();
}
this._release = new GemVersion(segments.join('.'));
}
else {
this._release = this;
}
}
return this._release;
}
// def segments # :nodoc:
// // segments is lazy so it can pick up version values that come from
// // old marshaled versions, which don't go through marshal_load.
// @segments ||= @version.scan(/[0-9]+|[a-z]+/i).map do |s|
// /^\d+$/ =~ s ? s.to_i : s
// end
// end
getSegments() {
return this.version
.match(/[0-9]+|[a-z]+/gi)
.map((s) => (/^\d+$/.test(s) ? Number(s) : s));
}
compare(other) {
if (!(other instanceof GemVersion)) {
return undefined;
}
if (other.version === this.version) {
return 0;
}
const lhsegments = this.getSegments();
const rhsegments = other.getSegments();
const lhsize = lhsegments.length;
const rhsize = rhsegments.length;
const limit = (lhsize > rhsize ? lhsize : rhsize) - 1;
let i = 0;
while (i <= limit) {
const lhs = lhsegments[i] || 0;
const rhs = rhsegments[i] || 0;
i += 1;
if (lhs == rhs) {
continue;
}
if (isString(lhs) && isNumber(rhs)) {
return -1;
}
if (isNumber(lhs) && isString(rhs)) {
return 1;
}
if (lhs < rhs) {
return -1;
}
if (lhs > rhs) {
return 1;
}
}
return 0;
}
}
exports.GemVersion = GemVersion;
function isString(val) {
return typeof val === 'string';
}
function isNumber(val) {
return typeof val === 'number';
}
GemVersion.VERSION_PATTERN = VERSION_PATTERN;
//# sourceMappingURL=gem-version.js.map