mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Refactor update checking
Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
parent
e5d9de3206
commit
c332e2a0d9
@ -25,6 +25,7 @@ export interface DownloadFileOptions {
|
||||
url: string;
|
||||
gzip?: boolean;
|
||||
timeout?: number;
|
||||
headers?: request.Headers;
|
||||
}
|
||||
|
||||
export interface DownloadFileTicket<T> {
|
||||
@ -33,9 +34,9 @@ export interface DownloadFileTicket<T> {
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket<Buffer> {
|
||||
export function downloadFile({ url, timeout, gzip = true, headers = {}}: DownloadFileOptions): DownloadFileTicket<Buffer> {
|
||||
const fileChunks: Buffer[] = [];
|
||||
const req = request(url, { gzip, timeout });
|
||||
const req = request(url, { gzip, timeout, headers });
|
||||
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
fileChunks.push(chunk);
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { DownloadFileOptions } from "../../common/utils/downloadFile";
|
||||
import { GitHubVersionChecker } from "../github-latest-version-checker";
|
||||
|
||||
const latestRelease = {
|
||||
tag_name: "v1.0.0",
|
||||
assets: [{
|
||||
browser_download_url: "https://foo.bar",
|
||||
}],
|
||||
};
|
||||
|
||||
describe("GitHubVersionChecker", () => {
|
||||
describe("getLatestVersion", () => {
|
||||
it("returns null if homepage does not point to github", async () => {
|
||||
const checker = new GitHubVersionChecker();
|
||||
|
||||
const version = await checker.getLatestVersion({
|
||||
name: "foo",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
expect(version).toBeNull();
|
||||
});
|
||||
|
||||
it("fetches latest release from github", async () => {
|
||||
const downloadJson = (args: DownloadFileOptions) => {
|
||||
expect(args).toEqual({
|
||||
url: "https://api.github.com/repos/lens/extension/releases/latest",
|
||||
headers: {
|
||||
"user-agent": "Lens IDE",
|
||||
},
|
||||
});
|
||||
|
||||
return { promise: new Promise((resolve) => {
|
||||
resolve(latestRelease);
|
||||
}) };
|
||||
};
|
||||
|
||||
const checker = new GitHubVersionChecker(downloadJson);
|
||||
|
||||
const version = await checker.getLatestVersion({
|
||||
name: "foo",
|
||||
version: "0.1.0",
|
||||
homepage: "https://github.com/lens/extension",
|
||||
});
|
||||
|
||||
expect(version).toEqual({
|
||||
input: "https://foo.bar",
|
||||
version: "1.0.0",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { LensExtensionLatestVersionChecker } from "../lens-extension-latest-version-checker";
|
||||
import { LensExtensionAvailableUpdate, LensExtensionUpdateChecker } from "../lens-extension-update-checker";
|
||||
|
||||
class TestLatestVersionChecker implements LensExtensionLatestVersionChecker {
|
||||
async getLatestVersion(): Promise<LensExtensionAvailableUpdate> {
|
||||
return {
|
||||
input: "foo",
|
||||
version: "1.0.0",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestLatestVersionChecker2 implements LensExtensionLatestVersionChecker {
|
||||
async getLatestVersion(): Promise<LensExtensionAvailableUpdate> {
|
||||
return {
|
||||
input: "bar",
|
||||
version: "0.5.0",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let updateChecker: LensExtensionUpdateChecker;
|
||||
const versionChecker1 = new TestLatestVersionChecker();
|
||||
const versionChecker2 = new TestLatestVersionChecker2();
|
||||
|
||||
describe("LensExtensionUpdateChecker", () => {
|
||||
beforeEach(() => {
|
||||
updateChecker = new LensExtensionUpdateChecker({
|
||||
foo: versionChecker1,
|
||||
bar: versionChecker2,
|
||||
});
|
||||
});
|
||||
|
||||
describe("run", () => {
|
||||
it("checks latest version from version checker", async () => {
|
||||
const versionCheckerSpy = jest.spyOn(versionChecker1, "getLatestVersion");
|
||||
const versionCheckerSpy2 = jest.spyOn(versionChecker2, "getLatestVersion");
|
||||
|
||||
await updateChecker.run({
|
||||
name: "foo-bar",
|
||||
version: "0.1.1",
|
||||
});
|
||||
|
||||
expect(versionCheckerSpy).toHaveBeenCalled();
|
||||
expect(versionCheckerSpy2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns latest version from version checkers", async () => {
|
||||
const update = await updateChecker.run({
|
||||
name: "foo-bar",
|
||||
version: "0.1.1",
|
||||
});
|
||||
|
||||
expect(update.version).toEqual("1.0.0");
|
||||
expect(update.input).toEqual("foo");
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@ -22,6 +22,7 @@
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import { LensExtensionUpdateChecker } from "../lens-extension-update-checker";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
@ -48,4 +49,61 @@ describe("lens extension", () => {
|
||||
expect(ext.name).toBe("foo-bar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkForUpdate", () => {
|
||||
it("runs update checker", async () => {
|
||||
const updateChecker = new LensExtensionUpdateChecker({});
|
||||
|
||||
ext = new LensExtension({
|
||||
manifest: {
|
||||
name: "foo-bar",
|
||||
version: "0.1.1",
|
||||
},
|
||||
id: "/this/is/fake/package.json",
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
isCompatible: true,
|
||||
}, updateChecker);
|
||||
|
||||
const updateSpy = jest.spyOn(updateChecker, "run");
|
||||
|
||||
await ext.checkForUpdate();
|
||||
|
||||
expect(updateSpy).toHaveBeenCalledWith({
|
||||
name: "foo-bar",
|
||||
version: "0.1.1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("returns available update", async () => {
|
||||
const updateChecker = new LensExtensionUpdateChecker({});
|
||||
|
||||
jest.spyOn(updateChecker, "run").mockResolvedValue({
|
||||
input: "foo",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
ext = new LensExtension({
|
||||
manifest: {
|
||||
name: "foo-bar",
|
||||
version: "0.1.1",
|
||||
},
|
||||
id: "/this/is/fake/package.json",
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
isCompatible: true,
|
||||
}, updateChecker);
|
||||
|
||||
const availableUpdate = await ext.checkForUpdate();
|
||||
|
||||
expect(availableUpdate).toEqual({
|
||||
input: "foo",
|
||||
version: "1.0.0",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { DownloadFileOptions } from "../../common/utils/downloadFile";
|
||||
import { NpmJsVersionChecker } from "../npmjs-latest-version.checker";
|
||||
|
||||
const npmPackage = {
|
||||
versions: {
|
||||
"1.0.0": {
|
||||
name: "foo",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("NpmJsVersionChecker", () => {
|
||||
describe("getLatestVersion", () => {
|
||||
|
||||
it("returns null if versions not present", async () => {
|
||||
const downloadJson = () => {
|
||||
return { promise: new Promise((resolve) => {
|
||||
resolve({});
|
||||
}) };
|
||||
};
|
||||
|
||||
const checker = new NpmJsVersionChecker(downloadJson);
|
||||
|
||||
|
||||
const version = await checker.getLatestVersion({
|
||||
name: "foo",
|
||||
version: "0.1.0",
|
||||
});
|
||||
|
||||
expect(version).toBeNull();
|
||||
});
|
||||
|
||||
it("fetches latest release from npmjs", async () => {
|
||||
const downloadJson = (args: DownloadFileOptions) => {
|
||||
expect(args).toEqual({
|
||||
url: "https://registry.npmjs.com/foo",
|
||||
});
|
||||
|
||||
return { promise: new Promise((resolve) => {
|
||||
resolve(npmPackage);
|
||||
}) };
|
||||
};
|
||||
|
||||
const checker = new NpmJsVersionChecker(downloadJson);
|
||||
|
||||
const version = await checker.getLatestVersion({
|
||||
name: "foo",
|
||||
version: "0.1.0",
|
||||
});
|
||||
|
||||
expect(version).toEqual({
|
||||
input: "foo",
|
||||
version: "1.0.0",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -21,22 +21,23 @@
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
import { EventEmitter } from "events";
|
||||
import _ from "lodash";
|
||||
import { isEqual } from "lodash";
|
||||
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||
import path from "path";
|
||||
import { AppPaths } from "../../common/app-paths";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
|
||||
import { Disposer, downloadJson, toJS } from "../../common/utils";
|
||||
import { Disposer, toJS } from "../../common/utils";
|
||||
import logger from "../../main/logger";
|
||||
import type { KubernetesCluster } from "../common-api/catalog";
|
||||
import type { InstalledExtension } from "../extension-discovery";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
import { GitHubVersionChecker } from "../github-latest-version-checker";
|
||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
||||
import { LensExtensionUpdateChecker } from "../lens-extension-update-checker";
|
||||
import type { LensMainExtension } from "../lens-main-extension";
|
||||
import type { LensRendererExtension } from "../lens-renderer-extension";
|
||||
import { NpmJsVersionChecker } from "../npmjs-latest-version.checker";
|
||||
import * as registries from "../registries";
|
||||
import { SemVer } from "semver";
|
||||
import URLParse from "url-parse";
|
||||
|
||||
export function extensionPackagesRoot() {
|
||||
return path.join(AppPaths.get("userData"));
|
||||
@ -72,13 +73,20 @@ export class ExtensionLoader {
|
||||
// emits event "remove" of type LensExtension when the extension is removed
|
||||
private events = new EventEmitter();
|
||||
|
||||
private extensionUpdateSources = {
|
||||
github: new GitHubVersionChecker(),
|
||||
npmJs: new NpmJsVersionChecker(),
|
||||
};
|
||||
|
||||
@observable isLoaded = false;
|
||||
private extensionUpdateChecker: LensExtensionUpdateChecker;
|
||||
|
||||
get whenLoaded() {
|
||||
return when(() => this.isLoaded);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.extensionUpdateChecker = new LensExtensionUpdateChecker(this.extensionUpdateSources);
|
||||
makeObservable(this);
|
||||
observe(this.instances, change => {
|
||||
switch (change.type) {
|
||||
@ -251,81 +259,15 @@ export class ExtensionLoader {
|
||||
});
|
||||
}
|
||||
|
||||
async getAvailableExtensionUpdates(): Promise<{ name: string; version: string }[]> {
|
||||
const availableUpdates: { name: string; version: string }[] = [];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
for (const [_, extension] of this.extensions) {
|
||||
console.log(`Check for update: ${extension.manifest.name}`);
|
||||
|
||||
const availableUpdate = await this.getLatestVersionFromNpmJs(extension) || await this.getLatestVersionFromGithub(extension);
|
||||
|
||||
if (availableUpdate) {
|
||||
if (new SemVer(extension.manifest.version, { loose: true, includePrerelease: true }).compare(availableUpdate.version) === -1) {
|
||||
extension.availableUpdate = {
|
||||
version: availableUpdate.version,
|
||||
input: availableUpdate.updateInput,
|
||||
};
|
||||
availableUpdates.push({ name: extension.manifest.name, version: availableUpdate.version });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return availableUpdates;
|
||||
}
|
||||
|
||||
protected async getLatestVersionFromNpmJs(extension: InstalledExtension) {
|
||||
const name = extension.manifest.name;
|
||||
const registryUrl = new URLParse("https://registry.npmjs.com").set("pathname", name).toString();
|
||||
const { promise } = downloadJson({ url: registryUrl });
|
||||
const json = await promise.catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true, includePrerelease: true }))
|
||||
// ignore pre-releases for auto picking the version
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
|
||||
const version = _.reduce(versions, (prev, curr) => (
|
||||
prev.compareMain(curr) === -1
|
||||
? curr
|
||||
: prev
|
||||
)).format();
|
||||
|
||||
return {
|
||||
updateInput: name,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
protected async getLatestVersionFromGithub(extension: InstalledExtension) {
|
||||
|
||||
const repo = extension.manifest.homepage?.replace("https://github.com/", "");
|
||||
|
||||
const registryUrl = `https://api.github.com/repos/${repo}/releases/latest`;
|
||||
|
||||
const { promise } = downloadJson({ url: registryUrl });
|
||||
const json = await promise.catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
if (!json || json.error || json.prerelease || !json.tag_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
updateInput: json.assets[0].browser_download_url,
|
||||
version: new SemVer(json.tag_name).version,
|
||||
};
|
||||
}
|
||||
|
||||
loadOnMain() {
|
||||
this.autoInitExtensions(() => Promise.resolve([]));
|
||||
this.autoInitExtensions(async (extension: LensMainExtension) => {
|
||||
// Check for update for the extension on main process that does not have renderer script
|
||||
if (!extension.manifest.renderer) {
|
||||
this.checkForExtensionUpdate(extension);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
}
|
||||
|
||||
loadOnClusterManagerRenderer() {
|
||||
@ -352,6 +294,8 @@ export class ExtensionLoader {
|
||||
}
|
||||
});
|
||||
|
||||
this.checkForExtensionUpdate(extension);
|
||||
|
||||
return removeItems;
|
||||
});
|
||||
}
|
||||
@ -386,6 +330,10 @@ export class ExtensionLoader {
|
||||
});
|
||||
}
|
||||
|
||||
protected async checkForExtensionUpdate(extension: LensExtension) {
|
||||
this.extensions.get(extension.id).availableUpdate = await extension.checkForUpdate();
|
||||
}
|
||||
|
||||
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
||||
const loadingExtensions: { isBundled: boolean, loaded: Promise<void> }[] = [];
|
||||
|
||||
@ -402,7 +350,7 @@ export class ExtensionLoader {
|
||||
continue;
|
||||
}
|
||||
|
||||
const instance = new LensExtensionClass(extension);
|
||||
const instance = new LensExtensionClass(extension, this.extensionUpdateChecker);
|
||||
|
||||
const loaded = instance.enable(register).catch((err) => {
|
||||
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
|
||||
|
||||
68
src/extensions/github-latest-version-checker.ts
Normal file
68
src/extensions/github-latest-version-checker.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { SemVer } from "semver";
|
||||
import logger from "../common/logger";
|
||||
import { DownloadFileOptions, downloadJson } from "../common/utils";
|
||||
import type { LensExtensionManifest } from "./lens-extension";
|
||||
import type { LensExtensionLatestVersionChecker } from "./lens-extension-latest-version-checker";
|
||||
|
||||
export class GitHubVersionChecker implements LensExtensionLatestVersionChecker {
|
||||
protected downloadJson;
|
||||
|
||||
constructor(downloadJsonOverride?: (args: DownloadFileOptions) => any) {
|
||||
this.downloadJson = downloadJsonOverride || downloadJson;
|
||||
}
|
||||
|
||||
public async getLatestVersion(manifest: LensExtensionManifest) {
|
||||
if (!manifest.homepage?.includes("https://github.com")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repo = manifest.homepage?.replace("https://github.com/", "");
|
||||
const registryUrl = `https://api.github.com/repos/${repo}/releases/latest`;
|
||||
const json = await this.getJson(registryUrl);
|
||||
|
||||
if (!json || json.error || json.prerelease || !json.tag_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug(`Found new version (${json.tag_name}) from GitHub`);
|
||||
|
||||
return {
|
||||
input: json.assets[0].browser_download_url,
|
||||
version: new SemVer(json.tag_name).version,
|
||||
};
|
||||
}
|
||||
|
||||
protected async getJson(url: string) {
|
||||
const headers = {
|
||||
"user-agent": "Lens IDE",
|
||||
};
|
||||
|
||||
const { promise } = this.downloadJson({ url, headers });
|
||||
const json = await promise.catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
27
src/extensions/lens-extension-latest-version-checker.ts
Normal file
27
src/extensions/lens-extension-latest-version-checker.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { LensExtensionManifest } from "./lens-extension";
|
||||
import type { LensExtensionAvailableUpdate } from "./lens-extension-update-checker";
|
||||
|
||||
export interface LensExtensionLatestVersionChecker {
|
||||
getLatestVersion(manifest: LensExtensionManifest): Promise<LensExtensionAvailableUpdate>
|
||||
}
|
||||
88
src/extensions/lens-extension-update-checker.ts
Normal file
88
src/extensions/lens-extension-update-checker.ts
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import _ from "lodash";
|
||||
import { SemVer } from "semver";
|
||||
import logger from "../common/logger";
|
||||
import type { LensExtensionManifest } from "./lens-extension";
|
||||
import type { LensExtensionLatestVersionChecker } from "./lens-extension-latest-version-checker";
|
||||
|
||||
export type LensExtensionAvailableUpdate = {
|
||||
input: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
|
||||
export class LensExtensionUpdateChecker {
|
||||
protected updateSources: {
|
||||
[key: string]: LensExtensionLatestVersionChecker;
|
||||
};
|
||||
|
||||
constructor(updateSources: {
|
||||
[key: string]: LensExtensionLatestVersionChecker;
|
||||
}) {
|
||||
this.updateSources = updateSources;
|
||||
}
|
||||
|
||||
public async run(manifest: LensExtensionManifest): Promise<LensExtensionAvailableUpdate|undefined> {
|
||||
const { name, version } = manifest;
|
||||
|
||||
logger.debug(`Check update for extension ${name}`);
|
||||
|
||||
const versions: LensExtensionAvailableUpdate[] = [];
|
||||
|
||||
for(const checker of Object.values(this.updateSources)) {
|
||||
const latestVersionFromSource = await checker.getLatestVersion(manifest);
|
||||
|
||||
if (latestVersionFromSource && this.isUpdate(version, latestVersionFromSource.version)) {
|
||||
versions.push(latestVersionFromSource);
|
||||
}
|
||||
}
|
||||
|
||||
const latestVersion = this.getLatestVersion(versions);
|
||||
|
||||
if (latestVersion) {
|
||||
logger.debug(`Found new version ${latestVersion}`);
|
||||
}
|
||||
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
private isUpdate(currentVersion: string, availableVersion: string) {
|
||||
return new SemVer(currentVersion, { loose: true, includePrerelease: true }).compare(availableVersion) === -1;
|
||||
}
|
||||
|
||||
private getLatestVersion(versions: LensExtensionAvailableUpdate[]) {
|
||||
if (versions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _.reduce(versions, (prev, curr) => {
|
||||
const previousVersion = new SemVer(prev.version, { loose: true, includePrerelease: true });
|
||||
const currentVersion = new SemVer(curr.version, { loose: true, includePrerelease: true });
|
||||
|
||||
return previousVersion.compareMain(currentVersion) === -1
|
||||
? curr
|
||||
: prev;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -26,6 +26,7 @@ import logger from "../main/logger";
|
||||
import type { ProtocolHandlerRegistration } from "./registries";
|
||||
import type { PackageJson } from "type-fest";
|
||||
import { Disposer, disposer } from "../common/utils";
|
||||
import type { LensExtensionUpdateChecker } from "./lens-extension-update-checker";
|
||||
|
||||
export type LensExtensionId = string; // path to manifest (package.json)
|
||||
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
||||
@ -45,6 +46,8 @@ export class LensExtension {
|
||||
readonly manifestPath: string;
|
||||
readonly isBundled: boolean;
|
||||
|
||||
private updateChecker: LensExtensionUpdateChecker;
|
||||
|
||||
protocolHandlers: ProtocolHandlerRegistration[] = [];
|
||||
|
||||
@observable private _isEnabled = false;
|
||||
@ -55,12 +58,14 @@ export class LensExtension {
|
||||
|
||||
[Disposers] = disposer();
|
||||
|
||||
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
||||
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension, updateChecker?: LensExtensionUpdateChecker) {
|
||||
makeObservable(this);
|
||||
this.id = id;
|
||||
this.manifest = manifest;
|
||||
this.manifestPath = manifestPath;
|
||||
this.isBundled = !!isBundled;
|
||||
|
||||
this.updateChecker = updateChecker;
|
||||
}
|
||||
|
||||
get name() {
|
||||
@ -120,6 +125,10 @@ export class LensExtension {
|
||||
}
|
||||
}
|
||||
|
||||
public async checkForUpdate() {
|
||||
return this.updateChecker?.run(this.manifest);
|
||||
}
|
||||
|
||||
protected onActivate(): Promise<void> | void {
|
||||
return;
|
||||
}
|
||||
|
||||
71
src/extensions/npmjs-latest-version.checker.ts
Normal file
71
src/extensions/npmjs-latest-version.checker.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import _ from "lodash";
|
||||
import { SemVer } from "semver";
|
||||
import URLParse from "url-parse";
|
||||
import { DownloadFileOptions, downloadJson } from "../common/utils/";
|
||||
import type { LensExtensionManifest } from "./lens-extension";
|
||||
import type { LensExtensionLatestVersionChecker } from "./lens-extension-latest-version-checker";
|
||||
|
||||
export class NpmJsVersionChecker implements LensExtensionLatestVersionChecker {
|
||||
protected downloadJson;
|
||||
|
||||
constructor(downloadJsonOverride?: (args: DownloadFileOptions) => any) {
|
||||
this.downloadJson = downloadJsonOverride || downloadJson;
|
||||
}
|
||||
|
||||
public async getLatestVersion(manifest: LensExtensionManifest) {
|
||||
const { name } = manifest;
|
||||
const registryUrl = new URLParse("https://registry.npmjs.com").set("pathname", name).toString();
|
||||
const json = await this.getJson(registryUrl);
|
||||
|
||||
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO refactor into helpoer method
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true, includePrerelease: true }))
|
||||
// ignore pre-releases for auto picking the version
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
|
||||
const version = _.reduce(versions, (prev, curr) => (
|
||||
prev.compareMain(curr) === -1
|
||||
? curr
|
||||
: prev
|
||||
)).format();
|
||||
|
||||
return {
|
||||
input: name,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
protected async getJson(url: string) {
|
||||
const { promise } = this.downloadJson({ url });
|
||||
const json = await promise.catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@ -281,7 +281,6 @@ app.on("ready", async () => {
|
||||
});
|
||||
|
||||
extensionLoader.initExtensions(extensions);
|
||||
extensionLoader.getAvailableExtensionUpdates();
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
||||
console.error(error);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user