Use valid semvers internally (#436)

* Update test position after upgrading Prettier

* Use valid semvers internally
This commit is contained in:
Jack Bates
2022-04-21 14:26:08 -07:00
committed by GitHub
parent e2ac9d36de
commit 575fd10bc8
20 changed files with 115 additions and 270 deletions

View File

@@ -23,7 +23,8 @@
"@definitelytyped/typescript-versions": "^0.0.112-next.9",
"@definitelytyped/utils": "^0.0.112-next.9",
"@types/node": "^14.14.35",
"fs-extra": "^9.1.0"
"fs-extra": "^9.1.0",
"semver": "^7.3.7"
},
"devDependencies": {
"@types/fs-extra": "^9.0.8"

View File

@@ -2,18 +2,19 @@ import { ParseDefinitionsOptions } from "./get-definitely-typed";
import { TypingsData, AllPackages, formatTypingVersion } from "./packages";
import {
assertDefined,
best,
mapDefined,
nAtATime,
FS,
logger,
writeLog,
Logger,
Semver,
UncachedNpmInfoClient,
NpmInfoRawVersions,
NpmInfoVersion,
max,
min,
} from "@definitelytyped/utils";
import * as semver from "semver";
export async function checkParseResults(
includeNpmChecks: false,
@@ -132,21 +133,19 @@ async function checkNpm(
}
const versions = getRegularVersions(info.versions);
const firstTypedVersion = best(
const firstTypedVersion = min(
mapDefined(versions, ({ hasTypes, version }) => (hasTypes ? version : undefined)),
(a, b) => b.greaterThan(a)
semver.compare
);
// A package might have added types but removed them later, so check the latest version too
if (firstTypedVersion === undefined || !best(versions, (a, b) => a.version.greaterThan(b.version))!.hasTypes) {
if (firstTypedVersion === undefined || !max(versions, (a, b) => semver.compare(a.version, b.version))!.hasTypes) {
return;
}
const ourVersion = `${major}.${minor}`;
log("");
log(
`Typings already defined for ${name} (${libraryName}) as of ${firstTypedVersion.versionString} (our version: ${ourVersion})`
);
log(`Typings already defined for ${name} (${libraryName}) as of ${firstTypedVersion} (our version: ${ourVersion})`);
const contributorUrls = contributors
.map((c) => {
const gh = "https://github.com/";
@@ -155,14 +154,14 @@ async function checkNpm(
.join(", ");
log(" To fix this:");
log(` git checkout -b not-needed-${name}`);
const yarnargs = [name, firstTypedVersion.versionString, projectName];
const yarnargs = [name, firstTypedVersion, projectName];
if (libraryName !== name) {
yarnargs.push(JSON.stringify(libraryName));
}
log(" yarn not-needed " + yarnargs.join(" "));
log(` git add --all && git commit -m "${name}: Provides its own types" && git push -u origin not-needed-${name}`);
log(` And comment PR: This will deprecate \`@types/${name}\` in favor of just \`${name}\`. CC ${contributorUrls}`);
if (new Semver(major, minor, 0).greaterThan(firstTypedVersion)) {
if (semver.gt(`${major}.${minor}.0`, firstTypedVersion)) {
log(" WARNING: our version is greater!");
}
if (dependedOn.has(name)) {
@@ -177,11 +176,11 @@ export async function packageHasTypes(packageName: string, client: UncachedNpmIn
function getRegularVersions(
versions: NpmInfoRawVersions
): readonly { readonly version: Semver; readonly hasTypes: boolean }[] {
return mapDefined(Object.entries(versions), ([versionString, info]) => {
const version = Semver.tryParse(versionString);
return version === undefined ? undefined : { version, hasTypes: versionHasTypes(info) };
});
): readonly { readonly version: semver.SemVer; readonly hasTypes: boolean }[] {
return Object.entries(versions).map(([versionString, info]) => ({
version: new semver.SemVer(versionString),
hasTypes: versionHasTypes(info),
}));
}
function versionHasTypes(info: NpmInfoVersion): boolean {

View File

@@ -17,10 +17,10 @@ import {
FS,
consoleLogger,
assertDefined,
Semver,
UncachedNpmInfoClient,
NpmInfo,
} from "@definitelytyped/utils";
import * as semver from "semver";
import { getAffectedPackages } from "./get-affected-packages";
export interface GitDiff {
@@ -145,20 +145,18 @@ export function checkNotNeededPackage(
Unneeded packages have to be replaced with a package on npm.`
);
typings = assertDefined(typings, `Unexpected error: @types package not found for ${unneeded.fullNpmName}`);
const latestTypings = Semver.parse(
assertDefined(
typings.distTags.get("latest"),
`Unexpected error: ${unneeded.fullNpmName} is missing the "latest" tag.`
)
const latestTypings = assertDefined(
typings.distTags.get("latest"),
`Unexpected error: ${unneeded.fullNpmName} is missing the "latest" tag.`
);
assert(
unneeded.version.greaterThan(latestTypings),
`The specified version ${unneeded.version.versionString} of ${unneeded.libraryName} must be newer than the version
it is supposed to replace, ${latestTypings.versionString} of ${unneeded.fullNpmName}.`
semver.gt(unneeded.version, latestTypings),
`The specified version ${unneeded.version} of ${unneeded.libraryName} must be newer than the version
it is supposed to replace, ${latestTypings} of ${unneeded.fullNpmName}.`
);
assert(
source.versions.has(unneeded.version.versionString),
`The specified version ${unneeded.version.versionString} of ${unneeded.libraryName} is not on npm.`
source.versions.has(String(unneeded.version)),
`The specified version ${unneeded.version} of ${unneeded.libraryName} is not on npm.`
);
}

View File

@@ -1,5 +1,6 @@
import { parseHeaderOrFail } from "@definitelytyped/header-parser";
import { Dir, FS, InMemoryFS, mangleScopedPackage, Semver } from "@definitelytyped/utils";
import { Dir, FS, InMemoryFS, mangleScopedPackage } from "@definitelytyped/utils";
import * as semver from "semver";
class DTMock {
public readonly fs: FS;
@@ -43,7 +44,7 @@ class DTMock {
const index = latestDir.get("index.d.ts") as string;
const latestHeader = parseHeaderOrFail(index);
const latestVersion = `${latestHeader.libraryMajorVersion}.${latestHeader.libraryMinorVersion}`;
const olderVersionParsed = Semver.parse(olderVersion, true)!;
const olderVersionParsed = semver.coerce(olderVersion)!;
const oldDir = latestDir.subdir(`v${olderVersion}`);
const tsconfig = JSON.parse(latestDir.get("tsconfig.json") as string);

View File

@@ -1,15 +1,8 @@
import assert = require("assert");
import { Author } from "@definitelytyped/header-parser";
import {
FS,
mapValues,
assertSorted,
unmangleScopedPackage,
Semver,
assertDefined,
unique,
} from "@definitelytyped/utils";
import { FS, mapValues, assertSorted, unmangleScopedPackage, assertDefined, unique } from "@definitelytyped/utils";
import { AllTypeScriptVersion, TypeScriptVersion } from "@definitelytyped/typescript-versions";
import * as semver from "semver";
import { readDataFile } from "./data-file";
import { scopeName, typesDirectoryName } from "./lib/settings";
import { parseVersionFromDirectoryName } from "./lib/definition-parser";
@@ -261,7 +254,7 @@ interface NotNeededPackageRaw extends BaseRaw {
}
export class NotNeededPackage extends PackageBase {
readonly version: Semver;
readonly version: semver.SemVer;
get license(): License.MIT {
return License.MIT;
@@ -286,7 +279,7 @@ export class NotNeededPackage extends PackageBase {
constructor(readonly name: string, readonly libraryName: string, asOfVersion: string) {
super({ libraryName });
assert(libraryName && name && asOfVersion);
this.version = Semver.parse(asOfVersion);
this.version = new semver.SemVer(asOfVersion);
}
get major(): number {
@@ -496,22 +489,20 @@ export function getLicenseFromPackageJson(packageJsonLicense: unknown): License
}
export class TypingsVersions {
private readonly map: ReadonlyMap<Semver, TypingsData>;
private readonly map: ReadonlyMap<semver.SemVer, TypingsData>;
/**
* Sorted from latest to oldest.
*/
private readonly versions: Semver[];
private readonly versions: semver.SemVer[];
constructor(data: TypingsVersionsRaw) {
/**
* Sorted from latest to oldest so that we publish the current version first.
* This is important because older versions repeatedly reset the "latest" tag to the current version.
*/
this.versions = Object.keys(data)
.map((key) => Semver.parse(key, true))
.sort(Semver.compare)
.reverse();
this.versions = Object.keys(data).map((key) => new semver.SemVer(`${key}.0`));
this.versions.sort(semver.rcompare);
this.map = new Map(
this.versions.map((version, i) => [version, new TypingsData(data[`${version.major}.${version.minor}`], !i)])

View File

@@ -219,7 +219,7 @@ describe(NotNeededPackage, () => {
expect(data.license).toBe(License.MIT);
expect(data.name).toBe("types-package");
expect(data.libraryName).toBe("real-package");
expect(data.version).toEqual({
expect(data.version).toMatchObject({
major: 1,
minor: 0,
patch: 0,

View File

@@ -21,6 +21,7 @@
"@octokit/rest": "^16.33.0",
"@types/node": "^12.12.29",
"markdown-table": "^1.1.3",
"semver": "^7.3.7",
"typescript": "^4.1.0",
"yargs": "^15.4.1"
},

View File

@@ -16,7 +16,9 @@ import {
MeasureBatchCompilationChildProcessResult,
} from "./measureBatchCompilationWorker";
import { AllPackages, HeaderParsedTypingVersion } from "@definitelytyped/definitions-parser";
import { runWithListeningChildProcesses, runWithChildProcesses, Semver } from "@definitelytyped/utils";
import { TypeScriptVersion } from "@definitelytyped/typescript-versions";
import { runWithListeningChildProcesses, runWithChildProcesses } from "@definitelytyped/utils";
import * as semver from "semver";
export interface MeasurePerfOptions {
packageName: string;
@@ -253,14 +255,12 @@ export async function measurePerf({
}
function getLatestTypesVersionForTypeScriptVersion(
typesVersions: readonly string[],
typesVersions: readonly TypeScriptVersion[],
typeScriptVersion: string
): string | undefined {
const tsVersion = Semver.parse(typeScriptVersion.replace(/-dev.*$/, ""));
const tsVersion = new semver.SemVer(typeScriptVersion);
for (let i = typesVersions.length - 1; i > 0; i--) {
const [major, minor] = typesVersions[i].split(".").map(Number); // e.g. '3.5'
const typesVersion = new Semver(major, minor, 0);
if (tsVersion.greaterThan(typesVersion)) {
if (semver.gte(tsVersion, `${typesVersions[i]}.0-`)) {
return typesVersions[i];
}
}

View File

@@ -21,6 +21,7 @@
"hh-mm-ss": "^1.2.0",
"longjohn": "^0.2.11",
"oboe": "^2.1.3",
"semver": "^7.3.7",
"source-map-support": "^0.4.0",
"typescript": "^4.1.0",
"yargs": "15.3.1"

View File

@@ -213,7 +213,7 @@ function dependencySemver(dependency: DependencyVersion): string {
export function createNotNeededPackageJSON({ libraryName, license, fullNpmName, version }: NotNeededPackage): string {
const out = {
name: fullNpmName,
version: version.versionString,
version: String(version),
typings: null, // tslint:disable-line no-null-keyword
description: `Stub TypeScript definitions entry for ${libraryName}, which provides its own types definitions`,
main: "",

View File

@@ -1,5 +1,6 @@
import { NotNeededPackage } from "@definitelytyped/definitions-parser";
import { Logger, assertDefined, Semver, best, CachedNpmInfoClient } from "@definitelytyped/utils";
import { Logger, assertDefined, CachedNpmInfoClient, max } from "@definitelytyped/utils";
import * as semver from "semver";
/**
* When we fail to publish a deprecated package, it leaves behind an entry in the time property.
@@ -10,27 +11,19 @@ export function skipBadPublishes(pkg: NotNeededPackage, client: CachedNpmInfoCli
// because this is called right after isAlreadyDeprecated, we can rely on the cache being up-to-date
const info = assertDefined(client.getNpmInfoFromCache(pkg.fullEscapedNpmName));
const notNeeded = pkg.version;
const latest = Semver.parse(findActualLatest(info.time));
if (
latest.equals(notNeeded) ||
latest.greaterThan(notNeeded) ||
(info.versions.has(notNeeded.versionString) &&
!assertDefined(info.versions.get(notNeeded.versionString)).deprecated)
) {
const plusOne = new Semver(latest.major, latest.minor, latest.patch + 1);
log(`Deprecation of ${notNeeded.versionString} failed, instead using ${plusOne.versionString}.`);
return new NotNeededPackage(pkg.name, pkg.libraryName, plusOne.versionString);
const latest = new semver.SemVer(findActualLatest(info.time));
if (semver.lte(notNeeded, latest)) {
const plusOne = semver.inc(latest, "patch")!;
log(`Deprecation of ${notNeeded} failed, instead using ${plusOne}.`);
return new NotNeededPackage(pkg.name, pkg.libraryName, plusOne);
}
return pkg;
}
function findActualLatest(times: Map<string, string>) {
const actual = best(times, ([k, v], [bestK, bestV]) =>
bestK === "modified" || bestK === "created"
? true
: k === "modified" || k === "created"
? false
: new Date(v).getTime() > new Date(bestV).getTime()
const actual = max(
[...times].filter(([version]) => version !== "modified" && version !== "created"),
([, a], [, b]) => (new Date(a) as never) - (new Date(b) as never)
);
if (!actual) {
throw new Error("failed to find actual latest");

View File

@@ -51,9 +51,9 @@ export async function deprecateNotNeededPackage(
): Promise<void> {
const name = pkg.fullNpmName;
if (dry) {
log("(dry) Skip deprecate not needed package " + name + " at " + pkg.version.versionString);
log("(dry) Skip deprecate not needed package " + name + " at " + pkg.version);
} else {
log(`Deprecating ${name} at ${pkg.version.versionString} with message: ${pkg.deprecatedMessage()}.`);
await client.deprecate(name, pkg.version.versionString, pkg.deprecatedMessage());
log(`Deprecating ${name} at ${pkg.version} with message: ${pkg.deprecatedMessage()}.`);
await client.deprecate(name, String(pkg.version), pkg.deprecatedMessage());
}
}

View File

@@ -13,12 +13,10 @@ import {
} from "@definitelytyped/definitions-parser";
import {
assertDefined,
best,
computeHash,
execAndThrowErrors,
joinPaths,
logUncaughtErrors,
mapDefined,
loggerWithErrors,
FS,
logger,
@@ -29,13 +27,14 @@ import {
sleep,
npmInstallFlags,
readJson,
Semver,
UncachedNpmInfoClient,
withNpmCache,
NpmPublishClient,
CachedNpmInfoClient,
isObject,
max,
} from "@definitelytyped/utils";
import * as semver from "semver";
import { getSecret, Secret } from "./lib/secrets";
const typesRegistry = "types-registry";
@@ -75,7 +74,7 @@ export default async function publishRegistry(
);
const registry = JSON.stringify(registryJsonData);
const newContentHash = computeHash(registry);
const newVersion = `0.1.${npmVersion.patch + 1}`;
const newVersion = semver.inc(npmVersion, "patch")!;
const isTimeForNewVersion = isSevenDaysAfter(lastModified);
await publishToRegistry();
@@ -88,13 +87,13 @@ export default async function publishRegistry(
const token = await getSecret(Secret.NPM_TOKEN);
const publishClient = () => NpmPublishClient.create(token, { defaultTag: "next" });
if (!highestSemverVersion.equals(npmVersion)) {
if (!semver.eq(highestSemverVersion, npmVersion)) {
// There was an error in the last publish and types-registry wasn't validated.
// This may have just been due to a timeout, so test if types-registry@next is a subset of the one we're about to publish.
// If so, we should just update it to "latest" now.
log("Old version of types-registry was never tagged latest, so updating");
await validateIsSubset(readNotNeededPackages(dt), log);
await (await publishClient()).tag(typesRegistry, highestSemverVersion.versionString, "latest", dry, log);
await (await publishClient()).tag(typesRegistry, String(highestSemverVersion), "latest", dry, log);
} else if (npmContentHash !== newContentHash && isTimeForNewVersion) {
log("New packages have been added, so publishing a new registry.");
await publish(await publishClient(), typesRegistry, packageJson, newVersion, dry, log);
@@ -204,10 +203,9 @@ function assertJsonNewer(newer: { [s: string]: any }, older: { [s: string]: any
}
switch (typeof newer[key]) {
case "string":
const newerver = Semver.tryParse(newer[key]);
const olderver = Semver.tryParse(older[key]);
const condition =
newerver && olderver ? newerver.greaterThan(olderver) || newerver.equals(olderver) : newer[key] >= older[key];
const newerver = semver.parse(newer[key]);
const olderver = semver.parse(older[key]);
const condition = newerver && olderver ? semver.gte(newerver, olderver) : newer[key] >= older[key];
assert(condition, `${key} in ${parent} did not match: newer[key] (${newer[key]}) < older[key] (${older[key]})`);
break;
case "number":
@@ -281,8 +279,8 @@ async function generateRegistry(typings: readonly TypingsData[], client: CachedN
}
interface ProcessedNpmInfo {
readonly npmVersion: Semver;
readonly highestSemverVersion: Semver;
readonly npmVersion: semver.SemVer;
readonly highestSemverVersion: semver.SemVer;
readonly npmContentHash: string;
readonly lastModified: Date;
}
@@ -292,16 +290,13 @@ async function fetchAndProcessNpmInfo(
client: UncachedNpmInfoClient
): Promise<ProcessedNpmInfo> {
const info = assertDefined(await client.fetchNpmInfo(escapedPackageName));
const npmVersion = Semver.parse(assertDefined(info.distTags.get("latest")));
const npmVersion = new semver.SemVer(assertDefined(info.distTags.get("latest")));
const { distTags, versions, time } = info;
const highestSemverVersion = getLatestVersion(versions.keys());
assert.strictEqual(highestSemverVersion.versionString, distTags.get("next"));
const npmContentHash = versions.get(npmVersion.versionString)!.typesPublisherContentHash || "";
const highestSemverVersion = max(
Array.from(versions.keys(), (v) => new semver.SemVer(v)),
semver.compare
)!;
assert.strictEqual(String(highestSemverVersion), distTags.get("next"));
const npmContentHash = versions.get(String(npmVersion))!.typesPublisherContentHash || "";
return { npmVersion, highestSemverVersion, npmContentHash, lastModified: new Date(time.get("modified")!) };
}
function getLatestVersion(versions: Iterable<string>): Semver {
return best(
mapDefined(versions, (v) => Semver.tryParse(v)),
(a, b) => a.greaterThan(b)
)!;
}

View File

@@ -21,6 +21,7 @@
"@definitelytyped/definitions-parser": "^0.0.112-next.9",
"@definitelytyped/typescript-versions": "^0.0.112-next.9",
"@definitelytyped/utils": "^0.0.112-next.9",
"semver": "^7.3.7",
"yargs": "^15.3.1"
},
"devDependencies": {

View File

@@ -15,9 +15,6 @@ import {
consoleLogger,
NpmInfoVersion,
logUncaughtErrors,
Semver,
best,
mapDefined,
loggerWithErrors,
LoggerWithErrors,
nAtATime,
@@ -30,6 +27,7 @@ import {
parseDefinitions,
getDefinitelyTyped,
} from "@definitelytyped/definitions-parser";
import * as semver from "semver";
if (!module.parent) {
logUncaughtErrors(main);
@@ -128,19 +126,17 @@ export async function fetchTypesPackageVersionInfo(
): Promise<{ version: string; needsPublish: boolean }> {
let info = client.getNpmInfoFromCache(pkg.fullEscapedNpmName);
let latestVersion = info && getHighestVersionForMajor(info.versions, pkg);
let latestVersionInfo = latestVersion && assertDefined(info!.versions.get(latestVersion.versionString));
let latestVersionInfo = latestVersion && assertDefined(info!.versions.get(latestVersion));
if (!latestVersionInfo || latestVersionInfo.typesPublisherContentHash !== pkg.contentHash) {
if (log) {
log.info(
`Version info not cached for ${pkg.desc}@${latestVersion ? latestVersion.versionString : "(no latest version)"}`
);
log.info(`Version info not cached for ${pkg.desc}@${latestVersion || "(no latest version)"}`);
}
info = await client.fetchAndCacheNpmInfo(pkg.fullEscapedNpmName);
latestVersion = info && getHighestVersionForMajor(info.versions, pkg);
latestVersionInfo = latestVersion && assertDefined(info!.versions.get(latestVersion.versionString));
if (!latestVersionInfo) {
return { version: versionString(pkg, /*patch*/ 0), needsPublish: true };
if (!latestVersion) {
return { version: `${pkg.major}.${pkg.minor}.0`, needsPublish: true };
}
latestVersionInfo = assertDefined(info!.versions.get(latestVersion));
}
if (latestVersionInfo.deprecated) {
@@ -151,39 +147,12 @@ export async function fetchTypesPackageVersionInfo(
);
}
const needsPublish = canPublish && pkg.contentHash !== latestVersionInfo.typesPublisherContentHash;
const patch = needsPublish
? latestVersion!.minor === pkg.minor
? latestVersion!.patch + 1
: 0
: latestVersion!.patch;
return { version: versionString(pkg, patch), needsPublish };
}
function versionString(pkg: TypingsData, patch: number): string {
return new Semver(pkg.major, pkg.minor, patch).versionString;
return { version: needsPublish ? semver.inc(latestVersion!, "patch")! : `${pkg.major}.${pkg.minor}.0`, needsPublish };
}
function getHighestVersionForMajor(
versions: ReadonlyMap<string, NpmInfoVersion>,
{ major, minor }: TypingsData
): Semver | undefined {
const patch = latestPatchMatchingMajorAndMinor(versions.keys(), major, minor);
return patch === undefined ? undefined : new Semver(major, minor, patch);
}
/** Finds the version with matching major/minor with the latest patch version. */
function latestPatchMatchingMajorAndMinor(
versions: Iterable<string>,
newMajor: number,
newMinor: number
): number | undefined {
const versionsWithTypings = mapDefined(versions, (v) => {
const semver = Semver.tryParse(v);
if (!semver) {
return undefined;
}
const { major, minor, patch } = semver;
return major === newMajor && minor === newMinor ? patch : undefined;
});
return best(versionsWithTypings, (a, b) => a > b);
): string | null {
return semver.maxSatisfying([...versions.keys()], `~${major}.${minor}`);
}

View File

@@ -205,27 +205,26 @@ export function mapToRecord<T, U>(map: Map<string, T>, cb?: (t: T) => U): Record
return o;
}
/**
* Returns the input that is better than all others, or `undefined` if there are no inputs.
* @param isBetter Returns true if `a` should be preferred over `b`.
*/
export function best<T>(inputs: Iterable<T>, isBetter: (a: T, b: T) => boolean): T | undefined {
const iter = inputs[Symbol.iterator]();
const first = iter.next();
if (first.done) {
return undefined;
}
let res = first.value;
while (true) {
const { value, done } = iter.next();
if (done) {
break;
}
if (isBetter(value, res)) {
res = value;
}
}
return res;
export function min<T>(array: readonly [T, ...(T | undefined)[]]): T;
export function min<T>(array: readonly T[], compare?: (a: T, b: T) => number): T | undefined;
export function min<T>(array: readonly T[], compare?: (a: T, b: T) => number) {
return array.length === 0
? undefined
: array.reduce((previousValue, currentValue) =>
(compare ? compare(currentValue, previousValue) < 0 : currentValue < previousValue)
? currentValue
: previousValue
);
}
export function max<T>(array: readonly [T, ...(T | undefined)[]]): T;
export function max<T>(array: readonly T[], compare?: (a: T, b: T) => number): T | undefined;
export function max<T>(array: readonly T[], compare?: (a: T, b: T) => number) {
return array.length === 0
? undefined
: array.reduce((previousValue, currentValue) =>
(compare ? compare(currentValue, previousValue) > 0 : currentValue > previousValue)
? currentValue
: previousValue
);
}

View File

@@ -8,5 +8,4 @@ export * from "./miscellany";
export * from "./npm";
export * from "./process";
export * from "./progress";
export * from "./semver";
export * from "./typescript-installer";

View File

@@ -1,81 +0,0 @@
/** Version of a package published to NPM. */
export class Semver {
static parse(semver: string, coerce?: boolean): Semver {
const result = Semver.tryParse(semver, coerce);
if (!result) {
throw new Error(`Unexpected semver: ${semver}`);
}
return result;
}
static fromRaw({ major, minor, patch }: { major: number; minor: number; patch: number }): Semver {
return new Semver(major, minor, patch);
}
/**
* Returns 0 if equal, 1 if x > y, -1 if x < y
*/
static compare(x: Semver, y: Semver) {
const versions: [number, number][] = [
[x.major, y.major],
[x.minor, y.minor],
[x.patch, y.patch],
];
for (const [componentX, componentY] of versions) {
if (componentX > componentY) {
return 1;
}
if (componentX < componentY) {
return -1;
}
}
return 0;
}
/**
* Per the semver spec <http://semver.org/#spec-item-2>:
*
* A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative integers, and MUST NOT contain leading zeroes.
*
* @note This must parse the output of `versionString`.
*
* @param semver The version string.
* @param coerce Without this optional parameter the version MUST follow the above semver spec. However, when set to `true` components after the
* major version may be omitted. I.e. `1` equals `1.0` and `1.0.0`.
*/
static tryParse(semver: string, coerce?: boolean): Semver | undefined {
const rgx = /^(\d+)(\.(\d+))?(\.(\d+))?$/;
const match = rgx.exec(semver);
if (match) {
const { 1: major, 3: minor, 5: patch } = match;
if ((minor !== undefined && patch !== undefined) || coerce) {
// tslint:disable-line:strict-type-predicates
return new Semver(intOfString(major), intOfString(minor || "0"), intOfString(patch || "0"));
}
}
return undefined;
}
constructor(readonly major: number, readonly minor: number, readonly patch: number) {}
get versionString(): string {
const { major, minor, patch } = this;
return `${major}.${minor}.${patch}`;
}
equals(other: Semver): boolean {
return Semver.compare(this, other) === 0;
}
greaterThan(other: Semver): boolean {
return Semver.compare(this, other) === 1;
}
}
function intOfString(str: string): number {
const n = Number.parseInt(str, 10);
if (Number.isNaN(n)) {
throw new Error(`Error in parseInt(${JSON.stringify(str)})`);
}
return n;
}

View File

@@ -1,29 +0,0 @@
import { Semver } from "../src/semver";
it("returns a formatted description", () => {
expect(new Semver(1, 2, 3).versionString).toEqual("1.2.3");
});
it("parses semver versions", () => {
expect(Semver.parse("0.42.1").versionString).toEqual("0.42.1");
});
it("parses versions that do not strictly adhere to semver", () => {
expect(Semver.parse("1", true).versionString).toEqual("1.0.0");
expect(Semver.parse("0.42", true).versionString).toEqual("0.42.0");
});
it("throws when a version cannot be parsed", () => {
expect(() => Semver.parse("1")).toThrow();
expect(() => Semver.parse("1", false)).toThrow();
});
it("returns whether or not it's equal to another Semver", () => {
expect(Semver.parse("1.2.3").equals(new Semver(1, 2, 3))).toBe(true);
expect(Semver.parse("1.2.3").equals(new Semver(3, 2, 1))).toBe(false);
});
it("returns whether or not it's greater than another Semver", () => {
expect(Semver.parse("1.2.3").greaterThan(new Semver(1, 2, 2))).toBe(true);
expect(Semver.parse("1.2.3").equals(new Semver(1, 2, 4))).toBe(false);
});

View File

@@ -7463,6 +7463,13 @@ semver@6.x, semver@^6.0.0, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.7:
version "7.3.7"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
dependencies:
lru-cache "^6.0.0"
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"