Files
meteor/packages/package-version-parser/package-version-parser.js
2023-11-16 16:52:09 -03:00

489 lines
18 KiB
JavaScript

var inTool = typeof Package === "undefined";
// Provided by dev_bundle/server-lib/node_modules/semver.
var semver = inTool
? module.parent.require("semver")
: require("semver");
// Takes in a meteor version string, for example 1.2.3-rc.5_1+12345.
//
// Returns an object composed of the following:
// * major (integer >= 0)
// * minor (integer >= 0)
// * patch (integer >= 0)
// * prerelease (Array of Number-or-String, possibly empty)
// * wrapNum (integer >= 0)
// * build (Array of String, possibly empty)
// * raw (String), the raw meteor version string
// * version (String), canonical meteor version without build ID
// * semver (String), canonical semver version with build ID but no wrap num
//
// The input string "1.2.3-rc.5_1+12345" has a (major, minor, patch) of
// (1, 2, 3), a prerelease of ["rc", 5], a wrapNum of 1, a build of
// ["12345"], a raw of "1.2.3-rc.5_1+12345", a version of
// "1.2.3-rc.5_1", and a semver of "1.2.3-rc.5+12345".
//
// Throws if the version string is invalid in any way.
//
// You can write `PV.parse("1.2.3")` as an alternative to `new PV("1.2.3")`
var PV = function (versionString) {
if (! (typeof versionString === 'string')) {
throw new Error("Invalid PackageVersion argument: " + versionString);
}
if (! versionString) {
throwVersionParserError("Empty string is not a valid version");
}
// The buildID ("+foo" suffix) is part of semver, but split it off
// because it comes after the wrapNum. The wrapNum ("_123" suffix)
// is a Meteor extension to semver.
var plusSplit = versionString.split('+');
var wrapSplit = plusSplit[0].split('_');
var wrapNum = 0;
if (plusSplit.length > 2) {
throwVersionParserError("Can't have two + in version: " + versionString);
}
if (wrapSplit.length > 2) {
throwVersionParserError("Can't have two _ in version: " + versionString);
}
if (wrapSplit.length > 1) {
wrapNum = wrapSplit[1];
if (! wrapNum) {
throwVersionParserError("A wrap number must follow _");
} else if (!/^\d+$/.test(wrapNum)) {
throwVersionParserError(
"The wrap number (after _) must contain only digits, so " +
versionString + " is invalid.");
} else if (wrapNum[0] === "0") {
throwVersionParserError(
"The wrap number (after _) must not have a leading zero, so " +
versionString + " is invalid.");
}
wrapNum = parseInt(wrapNum, 10);
}
// semverPart is everything but the wrapNum, so for "1.0.0_2+xyz",
// it is "1.0.0+xyz".
var semverPart = wrapSplit[0];
if (plusSplit.length > 1) {
semverPart += "+" + plusSplit[1];
}
// NPM's semver spec supports things like 'v1.0.0' and considers them valid,
// but we don't. Everything before the + or - should be of the x.x.x form.
if (! /^\d+\.\d+\.\d+(\+|-|$)/.test(semverPart)) {
throwVersionParserError(
"Version string must look like semver (eg '1.2.3'), not '"
+ versionString + "'.");
};
var semverParse = semver.parse(semverPart);
if (! semverParse) {
throwVersionParserError(
"Version string must look like semver (eg '1.2.3'), not '"
+ semverPart + "'.");
}
this.major = semverParse.major; // Number
this.minor = semverParse.minor; // Number
this.patch = semverParse.patch; // Number
this.prerelease = semverParse.prerelease; // [OneOf(Number, String)]
this.wrapNum = wrapNum; // Number
this.build = semverParse.build; // [String]
this.raw = versionString; // the entire version string
// `.version` is everything but the build ID ("+foo"), and it
// has been run through semver's canonicalization, ie "cleaned"
// (for whatever that's worth)
this.version = semverParse.version + (wrapNum ? '_' + wrapNum : '');
// everything but the wrapnum ("_123")
this.semver = semverParse.version + (
semverParse.build.length ? '+' + semverParse.build.join('.') : '');
this._semverParsed = null; // populate lazily
};
// Set module.exports for tools/packaging/package-version-parser.js and
// module.exports.PackageVersion for api.export("PackageVersion").
PV.PackageVersion = module.exports = PV;
PV.parse = function (versionString) {
return new PV(versionString);
};
// Converts a meteor version into a large floating point number, which
// is (more or less [*]) unique to that version. Satisfies the
// following guarantee: If PV.lessThan(v1, v2) then
// PV.versionMagnitude(v1) < PV.versionMagnitude(v2) [*]
//
// [* XXX!] We don't quite satisfy the uniqueness and comparison properties in
// these cases:
// 1. If any of the version parts are greater than 100 (pretty unlikely?)
// 2. If we're dealing with a prerelease version, we only look at the
// first two characters of each prerelease part. So, "1.0.0-beta" and
// "1.0.0-bear" will have the same magnitude.
// 3. If we're dealing with a prerelease version with more than two parts, eg
// "1.0.0-rc.0.1". In this comparison may fail since we'd get to the limit
// of JavaScript floating point precision.
//
// If we wanted to fix this, we'd make this function return a BigFloat
// instead of a vanilla JavaScript number. That will make the
// constraint solver slower (by how much?), and would require some
// careful thought.
// (Or it could just return some sort of tuple, and ensure that
// the cost functions that consume this can deal with tuples...)
PV.versionMagnitude = function (versionString) {
var v = PV.parse(versionString);
return v.major * 100 * 100 +
v.minor * 100 +
v.patch +
v.wrapNum / 100 +
prereleaseIdentifierToFraction(v.prerelease) / 100 / 100;
};
// Accepts an array, eg ["rc", 2, 3]. Returns a number in the range
// (-1, 0]. An empty array returns 0. A non-empty string returns a
// number that is "as large" as the its precedence.
var prereleaseIdentifierToFraction = function (prerelease) {
if (prerelease.length === 0)
return 0;
return prerelease.reduce(function (memo, part, index) {
var digit;
if (typeof part === 'number') {
digit = part+1;
} else if (typeof part === 'string') {
var VALID_CHARACTERS =
"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var validCharToNumber = function (ch) {
var result = VALID_CHARACTERS.indexOf(ch);
if (result === -1)
throw new Error("Unexpected character in prerelease identifier: " + ch);
else
return result;
};
digit = 101 + // Numeric parts always have lower precedence than non-numeric parts.
validCharToNumber(part[0]) * VALID_CHARACTERS.length +
(part[1] ? validCharToNumber(part[1]) : 0);
} else {
throw new Error("Unexpected prerelease identifier part: " + part + " of type " + typeof part);
}
// 4100 > 101 + VALID_CHARACTERS.length *
// VALID_CHARACTERS.length. And there's a test to verify this
// ("test the edges of `versionMagnitude`")
return memo + digit / Math.pow(4100, index+1);
}, -1);
};
// Takes in two meteor versions. Returns true if the first one is less than the second.
// Versions are strings or PackageVersion objects.
PV.lessThan = function (versionOne, versionTwo) {
return PV.compare(versionOne, versionTwo) < 0;
};
// Given a string version, returns its major version (the first section of the
// semver), as an integer. Two versions are compatible if they have the same
// version number.
//
// versionString: valid meteor version string.
PV.majorVersion = function (versionString) {
return PV.parse(versionString).major;
};
// Takes in two meteor versions. Returns 0 if equal, a positive number if v1
// is greater, a negative number if v2 is greater.
// Versions are strings or PackageVersion objects.
PV.compare = function (versionOne, versionTwo) {
var v1 = versionOne;
if (typeof v1 === 'string') {
v1 = PV.parse(v1);
}
var v2 = versionTwo;
if (typeof v2 === 'string') {
v2 = PV.parse(v2);
}
// If the semver parts are different, use the semver library to compare,
// ignoring wrap numbers. (The semver library will ignore the build ID
// per the semver spec.)
if (v1.semver !== v2.semver) {
if (! v1._semverParsed) {
v1._semverParsed = semver.parse(v1.semver);
}
if (! v2._semverParsed) {
v2._semverParsed = semver.parse(v2.semver);
}
return semver.compare(v1._semverParsed, v2._semverParsed);
} else {
// If the semver components are equal, then the one with the smaller wrap
// numbers is smaller.
return v1.wrapNum - v2.wrapNum;
}
};
// Conceptually we have four types of simple constraints:
//
// 1. "any-reasonable" - "A" - any version of A is allowed (other than
// prerelease versions that contain dashes, unless a prerelease version
// has been explicitly selected elsewhere).
//
// 2. "compatible-with" (major) - "A@x.y.z" - constrains package A to
// version x.y.z or higher, and requires the major version of package A
// to match x. This is the most common kind of version constraint.
//
// 3. "compatible-with" (minor) - "A@~x.y.z" - constrains package A to
// version x.y.z or higher, and requires the major and minor versions
// of package A to match x and y, respectively. This style is allowed
// anywhere, but is used most often to constrain the minor versions of
// Meteor core packages, according to the current Meteor release.
//
// 4. "exactly" - A@=x.y.z - constrains package A to version x.y.z and
// nothing else.
//
// If a top-level constraint (e.g. in .meteor/packages) ends with a '!'
// character, any other constraints on that package will be weakened to
// accept any version of the package that is not less than the constraint,
// regardless of whether the major/minor versions match.
function parseSimpleConstraint(constraintString) {
if (! constraintString) {
throw new Error("Non-empty string required");
}
var result = {};
var needToCheckValidity = true;
if (constraintString.charAt(0) === '=') {
result.type = "exactly";
result.versionString = constraintString.slice(1);
} else {
result.type = "compatible-with";
if (constraintString.charAt(0) === "~") {
var semversion = PV.parse(
result.versionString = constraintString.slice(1)
).semver;
var range = new semver.Range("~" + semversion);
result.test = function (version) {
return range.test(PV.parse(version).semver);
};
// Already checked by calling PV.parse above.
needToCheckValidity = false;
} else {
result.versionString = constraintString;
}
}
if (needToCheckValidity) {
// This will throw if the version string is invalid.
PV.getValidServerVersion(result.versionString);
}
return result;
}
// Check to see if the versionString that we pass in is a valid meteor version.
//
// Returns a valid meteor version string that can be included in the
// server. That means that it has everything EXCEPT the build id. Throws if the
// entered string was invalid.
PV.getValidServerVersion = function (meteorVersionString) {
return PV.parse(meteorVersionString).version;
};
PV.VersionConstraint = function (vConstraintString) {
var alternatives;
// If there is no version string ("" or null), then our only
// constraint is any-reasonable.
if (! vConstraintString) {
// .versionString === null is relied on in the tool
alternatives =
[ { type: "any-reasonable", versionString: null } ];
vConstraintString = "";
} else {
if (vConstraintString.endsWith("!")) {
// If a top-level constraint (e.g. from .meteor/packages) ends with
// a '!' character, any other constraints on that package will be
// weakened to accept any version of the package that is not less
// than the constraint, regardless of whether the major/minor
// versions actually match. See packages/constraint-solver/solver.js
// for implementation details.
this.override = true;
vConstraintString =
vConstraintString.slice(0, vConstraintString.length - 1);
}
// Parse out the versionString.
var parts = vConstraintString.split(/ *\|\| */);
alternatives = parts.map(function (alt) {
if (! alt) {
throwVersionParserError("Invalid constraint string: " +
vConstraintString);
}
return parseSimpleConstraint(alt);
});
}
this.raw = vConstraintString;
this.alternatives = alternatives;
};
PV.parseVersionConstraint = function (constraintString) {
return new PV.VersionConstraint(constraintString);
};
// A PackageConstraint consists of a package name and a version constraint.
// Call either with args (package, versionConstraintString) or
// (packageConstraintString), or (package, versionConstraint).
// That is, ("foo", "1.2.3") or ("foo@1.2.3"), or ("foo", vc) where vc
// is instanceof PV.VersionConstraint.
PV.PackageConstraint = function (part1, part2) {
if ((typeof part1 !== "string") ||
(part2 && (typeof part2 !== "string") &&
! (part2 instanceof PV.VersionConstraint))) {
throw new Error("constraintString must be a string");
}
var packageName, versionConstraint, vConstraintString;
if (part2) {
packageName = part1;
if (part2 instanceof PV.VersionConstraint) {
versionConstraint = part2;
} else {
vConstraintString = part2;
}
} else if (part1.indexOf("@") >= 0) {
// Shave off last part after @, with "a@b@c" becoming ["a@b", "c"].
// Validating the package name will catch extra @.
var parts = part1.match(/^(.*)@([^@]*)$/).slice(1);
packageName = parts[0];
vConstraintString = parts[1];
if (! vConstraintString) {
throwVersionParserError(
"Version constraint for package '" + packageName +
"' cannot be empty; leave off the @ if you don't want to constrain " +
"the version.");
}
} else {
packageName = part1;
vConstraintString = "";
}
PV.validatePackageName(packageName);
if (versionConstraint) {
vConstraintString = versionConstraint.raw;
} else {
versionConstraint = PV.parseVersionConstraint(vConstraintString);
}
this.package = packageName;
this.constraintString = vConstraintString;
this.versionConstraint = versionConstraint;
};
PV.PackageConstraint.prototype.toString = function () {
var ret = this.package;
if (this.constraintString) {
ret += "@" + this.constraintString;
}
return ret;
};
// Structure of a parsed constraint:
//
// /*PV.PackageConstraint*/
// { package: String,
// constraintString: String,
// versionConstraint: /*PV.VersionConstraint*/ {
// raw: String,
// alternatives: [{versionString: String|null,
// type: String}]}}
PV.parsePackageConstraint = function (part1, part2) {
return new PV.PackageConstraint(part1, part2);
};
PV.validatePackageName = function (packageName, options) {
options = options || {};
var badChar = packageName.match(/[^a-z0-9:.\-]/);
if (badChar) {
if (options.detailedColonExplanation) {
throwVersionParserError(
"Bad character in package name: " + JSON.stringify(badChar[0]) +
".\n\nPackage names can only contain lowercase ASCII alphanumerics, " +
"dash, or dot.\nIf you plan to publish a package, it must be " +
"prefixed with your\nMeteor Developer Account username and a colon.");
}
throwVersionParserError(
"Package names can only contain lowercase ASCII alphanumerics, dash, " +
"dot, or colon, not " + JSON.stringify(badChar[0]) + ".");
}
if (!/[a-z]/.test(packageName)) {
throwVersionParserError("Package name must contain a lowercase ASCII letter: "
+ JSON.stringify(packageName));
}
if (packageName[0] === '.') {
throwVersionParserError("Package name may not begin with a dot: "
+ JSON.stringify(packageName));
}
if (packageName.slice(-1) === '.') {
throwVersionParserError("Package name may not end with a dot: "
+ JSON.stringify(packageName));
}
if (packageName.slice(-1) === '.') {
throwVersionParserError("Package names may not end with a dot: " +
JSON.stringify(packageName));
}
if (packageName.indexOf('..') >= 0) {
throwVersionParserError("Package names may not contain two consecutive dots: " +
JSON.stringify(packageName));
}
if (packageName[0] === '-') {
throwVersionParserError("Package names may not begin with a hyphen: " +
JSON.stringify(packageName));
}
// (There is already a package ending with a `-` and one with two consecutive `-`
// in troposphere, though they both look like typos.)
if (packageName.startsWith(":") ||
packageName.endsWith(":")) {
throwVersionParserError("Package names may not start or end with a colon: " +
JSON.stringify(packageName));
}
};
var throwVersionParserError = function (message) {
var e = new Error(message);
e.versionParserError = true;
throw e;
};
// Return true if the version constraint was invalid prior to 0.9.3
// (adding _ and || support)
//
// NOTE: this is not used on the client yet. This package is used by the
// package server to determine what is valid.
PV.invalidFirstFormatConstraint = function (validConstraint) {
if (!validConstraint) return false;
// We can check this easily right now, because we introduced some new
// characters. Anything with those characters is invalid prior to
// 0.9.3. XXX: If we ever have to go through these, we should write a more
// complicated regex.
return (/_/.test(validConstraint) ||
/\|/.test(validConstraint));
};
// Remove a suffix like "+foo" if present.
PV.removeBuildID = function (versionString) {
return versionString.replace(/\+.*$/, '');
};