Skip to content

Commit aaa1cbe

Browse files
authored
feat: correctly sort bazel module versions (#145)
1 parent db12297 commit aaa1cbe

File tree

6 files changed

+178
-58
lines changed

6 files changed

+178
-58
lines changed

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
"extract-zip": "^2.0.1",
2929
"gcp-metadata": "^6.0.0",
3030
"nodemailer": "^6.7.8",
31-
"semver": "^7.5.4",
3231
"simple-git": "^3.16.0",
3332
"source-map-support": "^0.5.21",
3433
"tar": "^6.2.0",
@@ -43,7 +42,6 @@
4342
"@types/mailparser": "^3.4.4",
4443
"@types/node": "^18.6.2",
4544
"@types/nodemailer": "^6.4.5",
46-
"@types/semver": "^7.5.6",
4745
"@types/source-map-support": "^0.5.4",
4846
"@types/tar": "^6.1.10",
4947
"@types/uuid": "^9.0.0",

src/domain/metadata-file.spec.ts

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ describe("constructor", () => {
284284
expect(metadata.maintainers[0].github).toBeUndefined();
285285
});
286286

287-
test("sorts versions by semver", () => {
287+
test("sorts semver versions", () => {
288288
mockMetadataFile(`\
289289
{
290290
"homepage": "https://foo.bar",
@@ -333,8 +333,8 @@ describe("constructor", () => {
333333
]);
334334
});
335335

336-
test("sorts non-semver versions above semver versions", () => {
337-
// See: https://docs.bazel.build/versions/5.0.0/bzlmod.html#version-format
336+
test("sorts versions with a different number of identifiers", () => {
337+
// See: https://bazel.build/external/module#version_format
338338
mockMetadataFile(`\
339339
{
340340
"homepage": "https://foo.bar",
@@ -352,11 +352,11 @@ describe("constructor", () => {
352352
`);
353353
const metadata = new MetadataFile("metadata.json");
354354

355-
expect(metadata.versions).toEqual(["20210324.2", "1.0.0", "2.0.0"]);
355+
expect(metadata.versions).toEqual(["1.0.0", "2.0.0", "20210324.2"]);
356356
});
357357

358-
test("sorts non-semver versions lexicographically", () => {
359-
// See: https://docs.bazel.build/versions/5.0.0/bzlmod.html#version-format
358+
test("sorts non-numeric versions lexicographically", () => {
359+
// See: https://bazel.build/external/module#version_format
360360
mockMetadataFile(`\
361361
{
362362
"homepage": "https://foo.bar",
@@ -365,38 +365,16 @@ describe("constructor", () => {
365365
"github:bar/rules_foo"
366366
],
367367
"versions": [
368-
"55",
369-
"12.4.2.1.1",
370-
"20210324.2"
368+
"xyz",
369+
"abc.e",
370+
"abc.d"
371371
],
372372
"yanked_versions": {}
373373
}
374374
`);
375375
const metadata = new MetadataFile("metadata.json");
376376

377-
expect(metadata.versions).toEqual(["12.4.2.1.1", "20210324.2", "55"]);
378-
});
379-
380-
test("sorts non-semver versions that look like semver as non-semver", () => {
381-
// https://github.com/bazel-contrib/publish-to-bcr/issues/97
382-
mockMetadataFile(`\
383-
{
384-
"homepage": "https://foo.bar",
385-
"maintainers": [],
386-
"repository": [
387-
"github:bar/rules_foo"
388-
],
389-
"versions": [
390-
"1.0.0-rc0",
391-
"1.0.0-rc1",
392-
"1.0.0rc1"
393-
],
394-
"yanked_versions": {}
395-
}
396-
`);
397-
const metadata = new MetadataFile("metadata.json");
398-
399-
expect(metadata.versions).toEqual(["1.0.0rc1", "1.0.0-rc0", "1.0.0-rc1"]);
377+
expect(metadata.versions).toEqual(["abc.d", "abc.e", "xyz"]);
400378
});
401379
});
402380

src/domain/metadata-file.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fs from "node:fs";
2-
import { compare as semverCompare, valid as validSemver } from "semver";
2+
import { compareVersions } from "./version.js";
33

44
export class MetadataFileError extends Error {
55
constructor(path: string, message: string) {
@@ -62,7 +62,7 @@ export class MetadataFile {
6262
}
6363

6464
this.metadata = json;
65-
this.sortVersions();
65+
this.metadata.versions.sort(compareVersions);
6666
}
6767

6868
public get maintainers(): ReadonlyArray<Maintainer> {
@@ -87,7 +87,7 @@ export class MetadataFile {
8787

8888
public addVersions(...versions: ReadonlyArray<string>): void {
8989
this.metadata.versions.push(...versions);
90-
this.sortVersions();
90+
this.metadata.versions.sort(compareVersions);
9191
}
9292

9393
public addYankedVersions(yankedVersions: {
@@ -135,20 +135,4 @@ export class MetadataFile {
135135

136136
return [];
137137
}
138-
139-
private sortVersions(): void {
140-
const semver = this.metadata.versions.filter(
141-
(v: string) => !!validSemver(v, { loose: false })
142-
);
143-
const nonSemver = this.metadata.versions.filter(
144-
(v: string) => !validSemver(v)
145-
);
146-
147-
this.metadata.versions = [
148-
...nonSemver.sort(),
149-
...semver.sort((a: string, b: string) =>
150-
semverCompare(a, b, { loose: false })
151-
),
152-
];
153-
}
154138
}

src/domain/version.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { compareVersions } from "./version";
2+
3+
describe("compareVersions", () => {
4+
it("should sort semvers", () => {
5+
expect(
6+
[
7+
"2.0.0",
8+
"1.0.0",
9+
"0.32.1",
10+
"0.32.11",
11+
"2.11.0",
12+
"1.0.0-rc1",
13+
"1.0.0-rc0",
14+
"1.0.0-rc23",
15+
"1.0.1-rc1",
16+
"2.10.1",
17+
].sort(compareVersions)
18+
).toEqual([
19+
"0.32.1",
20+
"0.32.11",
21+
"1.0.0-rc0",
22+
"1.0.0-rc1",
23+
"1.0.0-rc23",
24+
"1.0.0",
25+
"1.0.1-rc1",
26+
"2.0.0",
27+
"2.10.1",
28+
"2.11.0",
29+
]);
30+
});
31+
32+
it("should sort versions with more than 3 components", () => {
33+
expect(["6.4.0.2", "6.4.0", "6.4.0.2-rc0"].sort(compareVersions)).toEqual([
34+
"6.4.0",
35+
"6.4.0.2-rc0",
36+
"6.4.0.2",
37+
]);
38+
});
39+
40+
it("should sort duplciates", () => {
41+
expect(["1.0.0", "2.0.0", "1.0.0"].sort(compareVersions)).toEqual([
42+
"1.0.0",
43+
"1.0.0",
44+
"2.0.0",
45+
]);
46+
});
47+
48+
it("should sort versions with non-numeric identifiers", () => {
49+
expect(
50+
["z", "b.aa.b", "a.ab.b-rcfoo", "a.ab.b", "a.ab.a", "a.aa.b", "x.y"].sort(
51+
compareVersions
52+
)
53+
).toEqual([
54+
"a.aa.b",
55+
"a.ab.a",
56+
"a.ab.b-rcfoo",
57+
"a.ab.b",
58+
"b.aa.b",
59+
"x.y",
60+
"z",
61+
]);
62+
});
63+
64+
it("should sort numeric and non-numeric identifiers", () => {
65+
expect(["x.7.z", "1.2.3", "x.6.y", "a.b.c"].sort(compareVersions)).toEqual([
66+
"1.2.3",
67+
"a.b.c",
68+
"x.6.y",
69+
"x.7.z",
70+
]);
71+
});
72+
});

src/domain/version.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Compare bazel module versions
3+
*
4+
* Adapted from https://github.com/bazelbuild/bazel-central-registry/blob/127d91703baf4e39eb66fc907d255b37d6162792/tools/registry.py#L85
5+
*/
6+
export function compareVersions(a: string, b: string) {
7+
return Version.compare(new Version(a), new Version(b));
8+
}
9+
10+
class Version {
11+
public static compare(a: Version, b: Version): number {
12+
const result = Version.compareIdentifiers(a.release, b.release);
13+
if (result) {
14+
return result;
15+
}
16+
17+
if (a.prerelease.length === 0) {
18+
return 1;
19+
}
20+
if (b.prerelease.length === 0) {
21+
return -1;
22+
}
23+
24+
return Version.compareIdentifiers(a.prerelease, b.prerelease);
25+
}
26+
27+
private static compareIdentifiers(a: Identifier[], b: Identifier[]) {
28+
const l = Math.min(a.length, b.length);
29+
for (let i = 0; i < l; i++) {
30+
const result = Identifier.compare(a[i], b[i]);
31+
if (result) {
32+
return result;
33+
}
34+
}
35+
36+
if (a.length > b.length) {
37+
return 1;
38+
} else if (b.length > a.length) {
39+
return -1;
40+
}
41+
42+
return 0;
43+
}
44+
45+
private readonly prerelease: Identifier[];
46+
private readonly release: Identifier[];
47+
48+
public constructor(version: string) {
49+
const pattern =
50+
/^([a-zA-Z0-9.]+)(?:-([a-zA-Z0-9.-]+))?(?:\+[a-zA-Z0-9.-]+)?$/;
51+
const match = version.match(pattern);
52+
if (!match) {
53+
throw new Error(`Invalid module version '${version}'`);
54+
}
55+
56+
this.release = this.convertToIdentifiers(match[1]);
57+
this.prerelease = this.convertToIdentifiers(match[2]);
58+
}
59+
60+
private convertToIdentifiers(version: string): Identifier[] {
61+
return (version && version.split(".").map((i) => new Identifier(i))) || [];
62+
}
63+
}
64+
65+
class Identifier {
66+
public static compare(a: Identifier, b: Identifier): number {
67+
if (typeof a.value !== typeof b.value) {
68+
if (typeof a.value === "number") {
69+
return -1;
70+
} else {
71+
return 1;
72+
}
73+
}
74+
75+
if (typeof a.value === "string") {
76+
if (a.value < b.value) {
77+
return -1;
78+
} else if (a.value === b.value) {
79+
return 0;
80+
}
81+
return 1;
82+
} else {
83+
return a.value - (b.value as number);
84+
}
85+
}
86+
87+
private readonly value: string | number;
88+
89+
public constructor(value: string) {
90+
const numeric = parseInt(value);
91+
this.value = isNaN(numeric) ? value : numeric;
92+
}
93+
}

yarn.lock

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,11 +1348,6 @@
13481348
"@types/tough-cookie" "*"
13491349
form-data "^2.5.0"
13501350

1351-
"@types/semver@^7.5.6":
1352-
version "7.5.6"
1353-
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
1354-
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
1355-
13561351
"@types/send@*":
13571352
version "0.17.4"
13581353
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"

0 commit comments

Comments
 (0)