Skip to content

Commit

Permalink
fix: do not update to pre-releases (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
hasundue authored Nov 9, 2023
1 parent ca89c0a commit 7a2e4bd
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 85 deletions.
5 changes: 3 additions & 2 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { URI } from "./lib/uri.ts";
import { DependencyUpdate } from "./lib/update.ts";
import { writeAll } from "./lib/file.ts";
import { GitCommitSequence } from "./lib/git.ts";
import { Dependency, parseSemVer } from "./lib/dependency.ts";
import { Dependency } from "./lib/dependency.ts";
import { SemVerString } from "./lib/semver.ts";

const { gray, yellow, bold, cyan } = colors;

Expand Down Expand Up @@ -61,7 +62,7 @@ const main = new Command()
});

async function versionCommand() {
const version = parseSemVer(import.meta.url) ??
const version = SemVerString.parse(import.meta.url) ??
await $.progress("Fetching version info").with(async () => {
const latest = await Dependency.resolveLatest(
Dependency.parse(new URL("https://deno.land/x/molt/cli.ts")),
Expand Down
5 changes: 5 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 23 additions & 49 deletions lib/dependency.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,10 @@
import { assertExists } from "./std/assert.ts";
import { Mutex } from "./x/async.ts";
import type { Path, SemVerString } from "./types.ts";
import { ensure, is } from "./x/unknownutil.ts";
import type { Path } from "./types.ts";
import { SemVerString } from "./semver.ts";
import { URI } from "./uri.ts";

// Ref: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
const SEMVER_REGEXP =
/@v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/g;

/**
* Parse a semver string from the given import specifier.
*
* @param specifier The import specifier to parse.
*
* @returns The semver string parsed from the specifier, or `undefined` if no
* semver string is found.
*
* @example
* ```ts
* const version = parseSemVer("https://deno.land/std@0.205.0/version.ts");
* // -> "1"
* ```
*/
export function parseSemVer(
specifier: string,
): SemVerString | undefined {
const match = specifier.match(SEMVER_REGEXP);
if (!match) {
return undefined;
}
if (match.length > 1) {
console.warn(
"Multiple semvers in a single specifier is not supported:",
specifier,
);
return undefined;
}
return match[0].slice(1) as SemVerString;
}

/**
* Properties of a dependency parsed from an import specifier.
*/
Expand Down Expand Up @@ -101,7 +68,7 @@ export const Dependency = {
parse(url: URL): Dependency {
const scheme = url.protocol === "npm:" ? "npm:" : url.protocol + "//";
const body = url.hostname + url.pathname;
const semver = parseSemVer(url.href);
const semver = SemVerString.parse(url.href);
if (!semver) {
return { scheme, name: body } as Dependency;
}
Expand Down Expand Up @@ -179,21 +146,27 @@ async function _resolveLatest(
`Failed to fetch npm registry: ${response.statusText}`,
);
}
const json = await response.json();
if (!json["dist-tags"]?.latest) {
throw new Error(
`Could not find the latest version of ${dependency.name} from registry.`,
);
}
const latestSemVer = json["dist-tags"].latest as SemVerString;
if (latestSemVer === dependency.version) {
// The dependency is up to date
const pkg = ensure(
await response.json(),
is.ObjectOf({
"dist-tags": is.ObjectOf({
latest: is.String,
}),
}),
{ message: `Invalid response from NPM registry: ${response.url}` },
);
const latest = SemVerString.parse(pkg["dist-tags"].latest);
if (
latest === undefined || // The latest version is not a semver
latest === dependency.version || // The dependency is already up to date
SemVerString.isPreRelease(latest)
) {
LatestDependencyCache.set(dependency.name, null);
return;
}
return LatestDependencyCache.set(
dependency.name,
{ ...dependency, version: latestSemVer },
{ ...dependency, version: latest },
);
}
case "http://":
Expand All @@ -210,8 +183,9 @@ async function _resolveLatest(
}
const latest = Dependency.parse(new URL(response.url));
if (
!latest.version || // The redirected URL has no semver
latest.version === dependency.version // The dependency is already up to date
latest.version === undefined || // The redirected URL has no semver
latest.version === dependency.version || // The dependency is already up to date
SemVerString.isPreRelease(latest.version)
) {
LatestDependencyCache.set(dependency.name, null);
return;
Expand Down
58 changes: 41 additions & 17 deletions lib/dependency_test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { beforeAll, describe, it } from "./std/testing.ts";
import { afterAll, beforeAll, describe, it } from "./std/testing.ts";
import { assertEquals } from "./std/assert.ts";
import { Dependency, parseSemVer } from "./dependency.ts";
import { Path, SemVerString } from "./types.ts";
import type { Path } from "./types.ts";
import type { SemVerString } from "./semver.ts";
import { Dependency } from "./dependency.ts";
import { LatestSemVerStub } from "./testing.ts";

describe("parseSemVer()", () => {
it("https://deno.land/std", () =>
assertEquals(
parseSemVer("https://deno.land/std@0.1.0"),
"0.1.0",
));
it("https://deno.land/std (no semver)", () =>
assertEquals(
parseSemVer("https://deno.land/std"),
undefined,
));
});

describe("Dependency.parse()", () => {
it("https://deno.land/std", () =>
assertEquals(
Expand Down Expand Up @@ -99,9 +87,15 @@ describe("Dependency.toURI()", () => {

describe("Dependency.resolveLatest()", () => {
const LATEST = "123.456.789" as SemVerString;
let stub: LatestSemVerStub;

beforeAll(() => {
LatestSemVerStub.create(LATEST);
stub = LatestSemVerStub.create(LATEST);
});
afterAll(() => {
stub.restore();
});

it("https://deno.land/std/version.ts", async () =>
assertEquals(
await Dependency.resolveLatest(
Expand Down Expand Up @@ -146,3 +140,33 @@ describe("Dependency.resolveLatest()", () => {
),
);
});

describe("Dependency.resolveLatest() - pre-release", () => {
let stub: LatestSemVerStub;

beforeAll(() => {
stub = LatestSemVerStub.create("123.456.789-alpha.1" as SemVerString);
});
afterAll(() => {
stub.restore();
});

it("deno.land", async () =>
assertEquals(
await Dependency.resolveLatest(
Dependency.parse(
new URL("https://deno.land/x/deno_graph@0.50.0/mod.ts"),
),
),
undefined,
));
it("npm", async () =>
assertEquals(
await Dependency.resolveLatest(
Dependency.parse(
new URL("npm:node-emoji@1.0.0"),
),
),
undefined,
));
});
2 changes: 1 addition & 1 deletion lib/file_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { assertSnapshot } from "./testing.ts";
import { DependencyUpdate } from "./update.ts";
import { FileUpdate } from "./file.ts";
import { LatestSemVerStub } from "./testing.ts";
import { SemVerString } from "./types.ts";
import { SemVerString } from "./semver.ts";

const LATEST = "123.456.789" as SemVerString;
LatestSemVerStub.create(LATEST);
Expand Down
6 changes: 4 additions & 2 deletions lib/import_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { type ImportMapJson, parseFromJson } from "./x/import_map.ts";
import { is } from "./x/unknownutil.ts";
import type { Maybe } from "./types.ts";
import { URI } from "./uri.ts";
import { URIScheme } from "./types.ts";

export type { ImportMapJson };

const URI_SCHEMES = ["http", "https", "file", "npm", "node"] as const;
type URIScheme = typeof URI_SCHEMES[number];

export interface ImportMapResolveResult {
/** The full specifier resolved from the import map. */
specifier: URI<URIScheme>;
Expand Down Expand Up @@ -71,7 +73,7 @@ async function readFromJson(specifier: URI<"file">): Promise<Maybe<ImportMap>> {
URI.ensure("file")(resolved);
}
return {
specifier: URI.ensure(...URIScheme.values)(resolved),
specifier: URI.ensure(...URI_SCHEMES)(resolved),
...replacement,
};
},
Expand Down
55 changes: 55 additions & 0 deletions lib/semver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { parse as parseSemVer } from "./std/semver.ts";
import { Brand } from "./types.ts";

// Ref: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
const SEMVER_REGEXP =
/v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/g;

/** A string that represents a semver (e.g. `v1.0.0`.) */
export type SemVerString = Brand<string, "SemVerString">;

export const SemVerString = {
/**
* Parse a semver string from the given import specifier.
*
* @param specifier The import specifier to parse.
*
* @returns The semver string parsed from the specifier, or `undefined` if no
* semver string is found.
*
* @example
* ```ts
* parse("https://deno.land/std@0.205.0/version.ts");
* // -> "0.205.0"
* ```
*/
parse(from: string): SemVerString | undefined {
const match = from.match(SEMVER_REGEXP);
if (!match) {
return undefined;
}
if (match.length > 1) {
console.warn(
"Multiple semvers in a single specifier is not supported:",
from,
);
return undefined;
}
return match[0] as SemVerString;
},

/**
* Check if the given semver string represents a pre-release.
* @example
* ```ts
* isPreRelease("0.1.0" as SemVerString); // -> false
* isPreRelease("0.1.0-alpha.1" as SemVerString); // -> true
*/
isPreRelease(semver: SemVerString): boolean {
const parsed = parseSemVer(semver);
if (!parsed) {
throw new TypeError(`Invalid semver: ${semver}`);
}
return parsed.prerelease.length > 0;
},
};
32 changes: 32 additions & 0 deletions lib/semver_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { assertEquals } from "./std/assert.ts";
import { SemVerString } from "./semver.ts";

Deno.test("SemVerString.parse()", () => {
assertEquals(
SemVerString.parse("https://deno.land/std@0.1.0"),
"0.1.0",
);
assertEquals(
SemVerString.parse("https://deno.land/std"),
undefined,
);
assertEquals(
SemVerString.parse("https://deno.land/std@1.0.0-rc.1"),
"1.0.0-rc.1",
);
});

Deno.test("SemVerString.isPreRelease()", () => {
assertEquals(
SemVerString.isPreRelease("0.1.0" as SemVerString),
false,
);
assertEquals(
SemVerString.isPreRelease("0.1.0-alpha.1" as SemVerString),
true,
);
assertEquals(
SemVerString.isPreRelease("0.1.0-rc.1" as SemVerString),
true,
);
});
1 change: 1 addition & 0 deletions lib/std/semver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { parse } from "https://deno.land/std@0.205.0/semver/parse.ts";
5 changes: 2 additions & 3 deletions lib/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import { AssertionError } from "./std/assert.ts";
import { EOL, formatEOL } from "./std/fs.ts";
import { fromFileUrl } from "./std/path.ts";
import { URI } from "./uri.ts";
import { parseSemVer } from "./dependency.ts";
import { SemVerString } from "./types.ts";
import { SemVerString } from "./semver.ts";

export const assertSnapshot = createAssertSnapshot({
dir: fromFileUrl(new URL("../test/snapshots/", import.meta.url)),
Expand Down Expand Up @@ -125,7 +124,7 @@ export const LatestSemVerStub = {
}
const response = await init.original(request);
await response.arrayBuffer();
const semver = parseSemVer(response.url);
const semver = SemVerString.parse(response.url);
if (!semver) {
return response;
}
Expand Down
9 changes: 0 additions & 9 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,3 @@ export type Brand<T, B> = T & { __brand: B };

/** A string that represents a path segment (e.g. `src/lib.ts`.) */
export type Path = Brand<string, "Path">;

/** A string that represents a semver (e.g. `v1.0.0`.) */
export type SemVerString = Brand<string, "SemVerString">;

const URI_SCHEMES = ["http", "https", "file", "npm", "node"] as const;
export type URIScheme = typeof URI_SCHEMES[number];
export const URIScheme = {
values: URI_SCHEMES,
};
2 changes: 1 addition & 1 deletion lib/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Dependency, LatestDependency } from "./dependency.ts";

type DependencyJson = NonNullable<ModuleJson["dependencies"]>[number];

/** Representation of a dependency update. */
/** Representation of an update to a dependency. */
export interface DependencyUpdate {
/** Properties of the dependency being updated. */
from: Dependency;
Expand Down
2 changes: 1 addition & 1 deletion lib/update_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { URI } from "./uri.ts";
import { _create, DependencyUpdate } from "./update.ts";
import { ImportMap } from "./import_map.ts";
import { LatestSemVerStub } from "./testing.ts";
import { SemVerString } from "./types.ts";
import type { SemVerString } from "./semver.ts";

describe("DependencyUpdate", () => {
const LATEST = "123.456.789" as SemVerString;
Expand Down

0 comments on commit 7a2e4bd

Please sign in to comment.