mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Consider the following situation:
- app uses package P
- Every version of P contains `api.use('s', 'server')`
- Every version of S contains `api.use('c', 'client')`
- There's nothing else around using S or C
When we bundle our app, we will not end up putting any unibuilds from C
into the bundle. That's fine.
The previous version of the constraint solver understood this, and so C
wasn't even part of the constraint solver solution.
HOWEVER, even though C does not contribute any unibuilds to the app
bundle, we still need to compile C in order to compile S. That's because
our current implementation never compiles only part of an isopack, even
if only part of the isopack will be needed for the app.
The structured project initialization done via ProjectContext will thus
decide to not build or load C, which will make it fail to compile S when
it gets around to compiling the client unibuilds in S.
We could make the model more complex by making it possible to compile
only part of S.
Instead, we'll make the Meteor package model simpler. Constraints, as
far as the constraint solver is concerned, are now at a package level.
So in this case, "C" will actually be part of the project (will end up
in .meteor/versions, etc) even though it will continue to not provide
any part of any of the bundled client or server programs.
This means that nodes in the constraint solver's graph will now just be
package versions, not unibuild versions. That's already the language
that the constraint solver spoke in as far as its inputs, outputs, and
error messages were concerned!
An example of an app that couldn't be built on the isopack-cache branch
before this commit and can be now is
https://github.com/glasser/precise-constraints-example
(It can be built with 1.0, but only by compiling a version of `c` that
isn't part of .meteor/versions!)
Note that this also means that .meteor/versions expresses enough to
allow us to implement a simpler constraint check that doesn't need to do
the full tree walk of the constraint solver. Such a checker would be
implemented as:
- gather root dependencies and constraints (project constraints,
release constraints, etc)
- add the dependencies and constraints from all versions mentioned
in .meteor/versions
- see if the choices made in .meteor/versions satisfy these
dependencies and constraints
This algorithm did NOT work previously, because you couldn't just look
at the constraints coming from `s@0.0.0` and say "they're satifisfied"
because you had to know to "ignore" the constraint on c#web.browser
because s#web.browser is not part of the app, which is not a fact that
actually got recorded in .meteor/versions.
(This commit does not implement this simpler constraint check, though.)
351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
// This file is in tools/package-version-parser.js and is symlinked into
|
|
// packages/package-version-parser/package-version-parser.js. It's part of both
|
|
// the tool and the package! We don't use an isopacket for it because it used
|
|
// to be required as part of building isopackets (though that may no longer be
|
|
// true).
|
|
var inTool = typeof Package === 'undefined';
|
|
|
|
var PV;
|
|
if (inTool) {
|
|
PV = exports;
|
|
} else {
|
|
PackageVersion = PV = {};
|
|
}
|
|
|
|
var semver = inTool ? require ('semver') : Npm.require('semver');
|
|
var __ = inTool ? require('underscore') : _;
|
|
|
|
// Takes in a meteor version, for example 1.2.3-rc5_1+12345.
|
|
//
|
|
// Returns an object composed of the following:
|
|
// semver: (ex: 1.2.3)
|
|
// wrapNum: 0 or a valid wrap number.
|
|
//
|
|
// Throws if the wrapNumber is invalid, or if the version cannot be split
|
|
// reasonably.
|
|
var extractSemverPart = function (versionString) {
|
|
if (!versionString) return { semver: "", wrapNum: -1 };
|
|
var noBuild = versionString.split('+');
|
|
var splitVersion = noBuild[0].split('_');
|
|
var wrapNum = 0;
|
|
// If we find two +s, or two _, that's super invalid.
|
|
if (noBuild.length > 2 || splitVersion.length > 2) {
|
|
throwVersionParserError(
|
|
"Version string must look like semver (eg '1.2.3'), not '"
|
|
+ versionString + "'.");
|
|
} else if (splitVersion.length > 1) {
|
|
wrapNum = splitVersion[1];
|
|
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.");
|
|
}
|
|
}
|
|
return {
|
|
semver: (noBuild.length > 1) ?
|
|
splitVersion[0] + "+" + noBuild[1] :
|
|
splitVersion[0],
|
|
wrapNum: parseInt(wrapNum, 10)
|
|
};
|
|
};
|
|
|
|
// 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.
|
|
PV.versionMagnitude = function (versionString) {
|
|
var version = extractSemverPart(versionString);
|
|
var v = semver.parse(version.semver);
|
|
|
|
return v.major * 100 * 100 +
|
|
v.minor * 100 +
|
|
v.patch +
|
|
version.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 __.reduce(prerelease, function (memo, part, index) {
|
|
var digit;
|
|
if (typeof part === 'number') {
|
|
digit = part+1;
|
|
} else if (typeof part === 'string') {
|
|
var VALID_CHARACTERS = "-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
|
|
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);
|
|
}
|
|
|
|
// 3000 > 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(3000, index+1);
|
|
}, -1);
|
|
};
|
|
|
|
// Takes in two meteor versions. Returns true if the first one is less than the second.
|
|
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) {
|
|
var version = extractSemverPart(versionString).semver;
|
|
var parsed = semver.parse(version);
|
|
if (! parsed)
|
|
throwVersionParserError("not a valid version: " + version);
|
|
return parsed.major;
|
|
};
|
|
|
|
// Takes in two meteor versions. Returns 0 if equal, 1 if v1 is greater, -1 if
|
|
// v2 is greater.
|
|
PV.compare = function (versionOne, versionTwo) {
|
|
var meteorVOne = extractSemverPart(versionOne);
|
|
var meteorVTwo = extractSemverPart(versionTwo);
|
|
|
|
// Wrap numbers only matter if the semver is equal, so if they don't even have
|
|
// wrap numbers, or if their semver is not equal, then we should let the
|
|
// semver library resolve this one.
|
|
if (meteorVOne.semver !== meteorVTwo.semver) {
|
|
return semver.compare(meteorVOne.semver, meteorVTwo.semver);
|
|
}
|
|
|
|
// If their semver components are equal, then the one with the smaller wrap
|
|
// numbers is smaller.
|
|
return meteorVOne.wrapNum - meteorVTwo.wrapNum;
|
|
};
|
|
|
|
// Conceptually we have three types of constraints:
|
|
// 1. "compatible-with" - A@x.y.z - constraints package A to version x.y.z or
|
|
// higher, as long as the version is backwards compatible with x.y.z.
|
|
// "pick A compatible with x.y.z"
|
|
// It is the default type.
|
|
// 2. "exactly" - A@=x.y.z - constraints package A only to version x.y.z and
|
|
// nothing else.
|
|
// "pick A exactly at x.y.z"
|
|
// 3. "any-reasonable" - "A"
|
|
// Basically, this means any version of A ... other than ones that have
|
|
// dashes in the version (ie, are prerelease) ... unless the prerelease
|
|
// version has been explicitly selected (which at this stage in the game
|
|
// means they are mentioned in a top-level constraint in the top-level
|
|
// call to the resolver).
|
|
//
|
|
// Options:
|
|
// removeBuildIDs: Remove the build ID at the end of the version.
|
|
PV.parseVersionConstraint = function (versionString, options) {
|
|
options = options || {};
|
|
var versionDesc = { version: null, type: "any-reasonable" };
|
|
|
|
if (!versionString) {
|
|
return versionDesc;
|
|
}
|
|
|
|
if (versionString.charAt(0) === '=') {
|
|
versionDesc.type = "exactly";
|
|
versionString = versionString.substr(1);
|
|
} else {
|
|
versionDesc.type = "compatible-with";
|
|
}
|
|
|
|
// This will throw if the version string is invalid.
|
|
PV.getValidServerVersion(versionString);
|
|
|
|
if (options.removeBuildIDs) {
|
|
versionString = PV.removeBuildID(versionString);
|
|
}
|
|
|
|
versionDesc.version = versionString;
|
|
|
|
return versionDesc;
|
|
};
|
|
|
|
|
|
// 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) {
|
|
|
|
// Strip out the wrapper num, if present and check that it is valid.
|
|
var version = extractSemverPart(meteorVersionString);
|
|
|
|
var versionString = version.semver;
|
|
// 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.
|
|
var mainVersion = versionString.split('+')[0].split('-')[0];
|
|
if (! /^\d+\.\d+\.\d+$/.test(mainVersion)) {
|
|
throwVersionParserError(
|
|
"Version string must look like semver (eg '1.2.3'), not '"
|
|
+ versionString + "'.");
|
|
};
|
|
|
|
var cleanVersion = semver.valid(versionString);
|
|
if (! cleanVersion ) {
|
|
throwVersionParserError(
|
|
"Version string must look like semver (eg '1.2.3'), not '"
|
|
+ versionString + "'.");
|
|
}
|
|
|
|
if (version.wrapNum) {
|
|
cleanVersion = cleanVersion + "_" + version.wrapNum;
|
|
}
|
|
|
|
return cleanVersion;
|
|
};
|
|
|
|
|
|
PV.parseConstraint = function (constraintString, options) {
|
|
if (typeof constraintString !== "string")
|
|
throw new TypeError("constraintString must be a string");
|
|
options = options || {};
|
|
|
|
var splitted = constraintString.split('@');
|
|
|
|
var name = splitted[0];
|
|
var versionString = splitted[1] || '';
|
|
|
|
if (splitted.length > 2) {
|
|
// throw error complaining about @
|
|
PV.validatePackageName('a@');
|
|
}
|
|
|
|
if (options.archesOK) {
|
|
var newNames = name.split('#');
|
|
if (newNames.length > 2) {
|
|
// It is invalid and should register as such. This will throw.
|
|
PV.validatePackageName(name);
|
|
}
|
|
PV.validatePackageName(newNames[0]);
|
|
} else {
|
|
PV.validatePackageName(name);
|
|
}
|
|
|
|
if (splitted.length === 2 && !versionString) {
|
|
throwVersionParserError(
|
|
"Version constraint for package '" + name +
|
|
"' cannot be empty; leave off the @ if you don't want to constrain " +
|
|
"the version.");
|
|
}
|
|
|
|
var constraint = {
|
|
name: name
|
|
};
|
|
|
|
// Before we parse through versionString, we save it for future output.
|
|
constraint.constraintString = versionString;
|
|
|
|
// If we did not specify a version string, then our only constraint is
|
|
// any-reasonable, so we are going to return that.
|
|
if (!versionString) {
|
|
constraint.constraints =
|
|
[ { version: null, type: "any-reasonable" } ];
|
|
return constraint;
|
|
}
|
|
|
|
// Let's parse out the versionString.
|
|
var versionConstraints = versionString.split(/ *\|\| */);
|
|
constraint.constraints = [];
|
|
__.each(versionConstraints, function (versionCon) {
|
|
constraint.constraints.push(
|
|
PV.parseVersionConstraint(versionCon, options));
|
|
});
|
|
|
|
return constraint;
|
|
};
|
|
|
|
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 names must contain a lowercase ASCII letter.");
|
|
}
|
|
if (packageName[0] === '.') {
|
|
throwVersionParserError("Package names may not begin with a dot.");
|
|
}
|
|
};
|
|
|
|
var throwVersionParserError = function (message) {
|
|
var e = new Error(message);
|
|
e.versionParserError = true;
|
|
throw e;
|
|
};
|
|
|
|
PV.constraintToFullString = function (parsedConstraint) {
|
|
return parsedConstraint.name + "@" + parsedConstraint.constraintString;
|
|
};
|
|
|
|
|
|
// 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 "+local" if present.
|
|
PV.removeBuildID = function (versionString) {
|
|
return versionString.replace(/\+.*$/, '');
|
|
};
|