diff --git a/src/common/utils/downloadFile.ts b/src/common/utils/downloadFile.ts index 6b02b8741f..fb27b8bf83 100644 --- a/src/common/utils/downloadFile.ts +++ b/src/common/utils/downloadFile.ts @@ -25,6 +25,7 @@ export interface DownloadFileOptions { url: string; gzip?: boolean; timeout?: number; + headers?: request.Headers; } export interface DownloadFileTicket { @@ -33,9 +34,9 @@ export interface DownloadFileTicket { cancel(): void; } -export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket { +export function downloadFile({ url, timeout, gzip = true, headers = {}}: DownloadFileOptions): DownloadFileTicket { const fileChunks: Buffer[] = []; - const req = request(url, { gzip, timeout }); + const req = request(url, { gzip, timeout, headers }); const promise: Promise = new Promise((resolve, reject) => { req.on("data", (chunk: Buffer) => { fileChunks.push(chunk); diff --git a/src/extensions/__tests__/github-latest-version-checker.test.ts b/src/extensions/__tests__/github-latest-version-checker.test.ts new file mode 100644 index 0000000000..fc22091763 --- /dev/null +++ b/src/extensions/__tests__/github-latest-version-checker.test.ts @@ -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", + }); + }); + }); +}); diff --git a/src/extensions/__tests__/lens-extension-update-checker.test.ts b/src/extensions/__tests__/lens-extension-update-checker.test.ts new file mode 100644 index 0000000000..a5e795cb39 --- /dev/null +++ b/src/extensions/__tests__/lens-extension-update-checker.test.ts @@ -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 { + return { + input: "foo", + version: "1.0.0", + }; + } +} + +class TestLatestVersionChecker2 implements LensExtensionLatestVersionChecker { + async getLatestVersion(): Promise { + 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"); + }); + + }); +}); diff --git a/src/extensions/__tests__/lens-extension.test.ts b/src/extensions/__tests__/lens-extension.test.ts index 97f5b3c913..c5d6ba08ad 100644 --- a/src/extensions/__tests__/lens-extension.test.ts +++ b/src/extensions/__tests__/lens-extension.test.ts @@ -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", + }); + }); }); diff --git a/src/extensions/__tests__/npmjs-latest-version-checker.test.ts b/src/extensions/__tests__/npmjs-latest-version-checker.test.ts new file mode 100644 index 0000000000..589e20df9a --- /dev/null +++ b/src/extensions/__tests__/npmjs-latest-version-checker.test.ts @@ -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", + }); + }); + }); +}); diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 9e0f00c967..329020f5fa 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -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) { const loadingExtensions: { isBundled: boolean, loaded: Promise }[] = []; @@ -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 }); diff --git a/src/extensions/github-latest-version-checker.ts b/src/extensions/github-latest-version-checker.ts new file mode 100644 index 0000000000..a1ac13ac2c --- /dev/null +++ b/src/extensions/github-latest-version-checker.ts @@ -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; + } +} diff --git a/src/extensions/lens-extension-latest-version-checker.ts b/src/extensions/lens-extension-latest-version-checker.ts new file mode 100644 index 0000000000..66779d576a --- /dev/null +++ b/src/extensions/lens-extension-latest-version-checker.ts @@ -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 +} diff --git a/src/extensions/lens-extension-update-checker.ts b/src/extensions/lens-extension-update-checker.ts new file mode 100644 index 0000000000..a83b148136 --- /dev/null +++ b/src/extensions/lens-extension-update-checker.ts @@ -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 { + 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; + }); + } + +} diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 7b6ca8c94f..2e516bfa0a 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -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) => 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 { return; } diff --git a/src/extensions/npmjs-latest-version.checker.ts b/src/extensions/npmjs-latest-version.checker.ts new file mode 100644 index 0000000000..421f331f46 --- /dev/null +++ b/src/extensions/npmjs-latest-version.checker.ts @@ -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; + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 778e3d34bc..1f7b26e773 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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);