diff --git a/src/common/protocol-handler/router.ts b/src/common/protocol-handler/router.ts index afb0f4c2af..a3950ca100 100644 --- a/src/common/protocol-handler/router.ts +++ b/src/common/protocol-handler/router.ts @@ -26,8 +26,8 @@ import { pathToRegexp } from "path-to-regexp"; import logger from "../../main/logger"; import type Url from "url-parse"; import { RoutingError, RoutingErrorType } from "./error"; -import { ExtensionsStore } from "../../extensions/extensions-store"; -import type { ExtensionLoader as ExtensionLoaderType } from "../../extensions/extension-loader/extension-loader"; +import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store"; +import type { ExtensionLoader } from "../../extensions/extension-loader"; import type { LensExtension } from "../../extensions/lens-extension"; import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler"; import { when } from "mobx"; @@ -79,7 +79,8 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R } interface Dependencies { - extensionLoader: ExtensionLoaderType + extensionLoader: ExtensionLoader + extensionsStore: ExtensionsStore } export abstract class LensProtocolRouter { @@ -212,7 +213,7 @@ export abstract class LensProtocolRouter { return name; } - if (!ExtensionsStore.getInstance().isEnabled(extension)) { + if (!this.dependencies.extensionsStore.isEnabled(extension)) { logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); return name; diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index 4a8786e34c..44f80741c0 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -21,11 +21,15 @@ import type { ExtensionLoader } from "../extension-loader"; import { ipcRenderer } from "electron"; -import { ExtensionsStore } from "../extensions-store"; +import type { + ExtensionsStore, +} from "../extensions-store/extensions-store"; import { Console } from "console"; import { stdout, stderr } from "process"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; +import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; +import { AppPaths } from "../../common/app-paths"; console = new Console(stdout, stderr); @@ -33,15 +37,6 @@ const manifestPath = "manifest/path"; const manifestPath2 = "manifest/path2"; const manifestPath3 = "manifest/path3"; -jest.mock("../extensions-store", () => ({ - ExtensionsStore: { - getInstance: () => ({ - whenLoaded: Promise.resolve(true), - mergeState: jest.fn(), - }), - }, -})); - jest.mock( "electron", () => ({ @@ -129,13 +124,25 @@ jest.mock( }, ); +AppPaths.init(); + describe("ExtensionLoader", () => { let extensionLoader: ExtensionLoader; - + let extensionsStoreStub: ExtensionsStore; + beforeEach(() => { const di = getDiForUnitTesting(); extensionLoader = di.inject(extensionLoaderInjectable); + + // TODO: Find out how to either easily create mocks of interfaces with a lot of members or + // introduce design for more minimal interfaces + // @ts-ignore + extensionsStoreStub = { + mergeState: jest.fn(), + }; + + di.override(extensionsStoreInjectable, () => extensionsStoreStub); }); it.only("renderer updates extension after ipc broadcast", async done => { @@ -177,18 +184,18 @@ describe("ExtensionLoader", () => { }); it("updates ExtensionsStore after isEnabled is changed", async () => { - (ExtensionsStore.getInstance().mergeState as any).mockClear(); + (extensionsStoreStub.mergeState as any).mockClear(); // Disable sending events in this test (ipcRenderer.on as any).mockImplementation(); await extensionLoader.init(); - expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled(); + expect(extensionsStoreStub.mergeState).not.toHaveBeenCalled(); Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false; - expect(ExtensionsStore.getInstance().mergeState).toHaveBeenCalledWith({ + expect(extensionsStoreStub.mergeState).toHaveBeenCalledWith({ "manifest/path": { enabled: false, name: "TestExtension", diff --git a/src/extensions/as-legacy-global-function-for-extension-api/as-legacy-global-function-for-extension-api.ts b/src/extensions/as-legacy-global-function-for-extension-api/as-legacy-global-function-for-extension-api.ts new file mode 100644 index 0000000000..8552cb5ec8 --- /dev/null +++ b/src/extensions/as-legacy-global-function-for-extension-api/as-legacy-global-function-for-extension-api.ts @@ -0,0 +1,48 @@ +/** + * 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 { Injectable } from "@ogre-tools/injectable"; + +import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api"; + +type TentativeTuple = T extends object ? [T] : [undefined?]; + +type FactoryType = < + TInjectable extends Injectable, + TInstantiationParameter, + TInstance extends (...args: unknown[]) => any, + TFunction extends (...args: unknown[]) => any = Awaited< + ReturnType + >, +>( + injectableKey: TInjectable, + ...instantiationParameter: TentativeTuple +) => (...args: Parameters) => ReturnType; + +export const asLegacyGlobalFunctionForExtensionApi: FactoryType = + (injectableKey, ...instantiationParameter) => + (...args) => { + const injected = getLegacyGlobalDiForExtensionApi().inject( + injectableKey, + ...instantiationParameter, + ); + + return injected(...args); + }; diff --git a/src/extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api.ts b/src/extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api.ts new file mode 100644 index 0000000000..0d9bc728a5 --- /dev/null +++ b/src/extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api.ts @@ -0,0 +1,29 @@ +/** + * 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 { DependencyInjectionContainer } from "@ogre-tools/injectable"; + +let legacyGlobalDi: DependencyInjectionContainer; + +export const setLegacyGlobalDiForExtensionApi = (di: DependencyInjectionContainer) => { + legacyGlobalDi = di; +}; + +export const getLegacyGlobalDiForExtensionApi = () => legacyGlobalDi; diff --git a/src/extensions/common-api/app.ts b/src/extensions/common-api/app.ts index 401fc4d283..22c081d093 100644 --- a/src/extensions/common-api/app.ts +++ b/src/extensions/common-api/app.ts @@ -20,14 +20,13 @@ */ import { getAppVersion } from "../../common/utils"; -import { ExtensionsStore } from "../extensions-store"; +import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-global-function-for-extension-api/as-legacy-global-function-for-extension-api"; +import getEnabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable"; import * as Preferences from "./user-preferences"; export const version = getAppVersion(); export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"; -export function getEnabledExtensions(): string[] { - return ExtensionsStore.getInstance().enabledExtensions; -} +export const getEnabledExtensions = asLegacyGlobalFunctionForExtensionApi(getEnabledExtensionsInjectable); export { Preferences }; diff --git a/src/extensions/common-api/get-enabled-extensions/get-enabled-extensions.injectable.ts b/src/extensions/common-api/get-enabled-extensions/get-enabled-extensions.injectable.ts new file mode 100644 index 0000000000..41f452a2b5 --- /dev/null +++ b/src/extensions/common-api/get-enabled-extensions/get-enabled-extensions.injectable.ts @@ -0,0 +1,31 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable"; + +const getEnabledExtensionsInjectable = getInjectable({ + instantiate: (di) => () => + di.inject(extensionsStoreInjectable).enabledExtensions, + + lifecycle: lifecycleEnum.singleton, +}); + +export default getEnabledExtensionsInjectable; diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts new file mode 100644 index 0000000000..362281d4b9 --- /dev/null +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -0,0 +1,52 @@ +/** + * 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 { getInjectable } from "@ogre-tools/injectable"; +import { lifecycleEnum } from "@ogre-tools/injectable"; +import { ExtensionDiscovery } from "./extension-discovery"; +import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; +import extensionInstallerInjectable from "../extension-installer/extension-installer.injectable"; +import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable"; +import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable"; +import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; +import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable"; + +const extensionDiscoveryInjectable = getInjectable({ + instantiate: (di) => + new ExtensionDiscovery({ + extensionLoader: di.inject(extensionLoaderInjectable), + extensionInstaller: di.inject(extensionInstallerInjectable), + extensionsStore: di.inject(extensionsStoreInjectable), + + extensionInstallationStateStore: di.inject( + extensionInstallationStateStoreInjectable, + ), + + isCompatibleBundledExtension: di.inject( + isCompatibleBundledExtensionInjectable, + ), + + isCompatibleExtension: di.inject(isCompatibleExtensionInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default extensionDiscoveryInjectable; diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/extension-discovery/extension-discovery.test.ts similarity index 77% rename from src/extensions/__tests__/extension-discovery.test.ts rename to src/extensions/extension-discovery/extension-discovery.test.ts index e4ea1c4451..8de3770ef5 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/extension-discovery/extension-discovery.test.ts @@ -21,15 +21,14 @@ import mockFs from "mock-fs"; import { watch } from "chokidar"; -import { ExtensionsStore } from "../extensions-store"; import path from "path"; -import { ExtensionDiscovery } from "../extension-discovery"; +import type { ExtensionDiscovery } from "./extension-discovery"; import os from "os"; import { Console } from "console"; import { AppPaths } from "../../common/app-paths"; -import type { ExtensionLoader } from "../extension-loader"; -import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import extensionDiscoveryInjectable from "./extension-discovery.injectable"; +import extensionInstallerInjectable from "../extension-installer/extension-installer.injectable"; jest.setTimeout(60_000); @@ -37,12 +36,6 @@ jest.mock("../../common/ipc"); jest.mock("chokidar", () => ({ watch: jest.fn(), })); -jest.mock("../extension-installer", () => ({ - extensionInstaller: { - extensionPackagesRoot: "", - installPackage: jest.fn(), - }, -})); jest.mock("electron", () => ({ app: { getVersion: () => "99.99.99", @@ -65,16 +58,23 @@ console = new Console(process.stdout, process.stderr); // fix mockFS const mockedWatch = watch as jest.MockedFunction; describe("ExtensionDiscovery", () => { - let extensionLoader: ExtensionLoader; + let extensionDiscovery: ExtensionDiscovery; beforeEach(() => { - ExtensionDiscovery.resetInstance(); - ExtensionsStore.resetInstance(); - ExtensionsStore.createInstance(); - const di = getDiForUnitTesting(); - extensionLoader = di.inject(extensionLoaderInjectable); + const extensionInstallerStub = { + installPackages: () => Promise.resolve(), + npm: () => Promise.resolve(), + extensionPackagesRoot: "some-extension-packages-root", + npmPath: "some-npm-path", + installPackage: jest.fn(), + }; + + // @ts-ignore + di.override(extensionInstallerInjectable, () => extensionInstallerStub); + + extensionDiscovery = di.inject(extensionDiscoveryInjectable); }); describe("with mockFs", () => { @@ -103,13 +103,7 @@ describe("ExtensionDiscovery", () => { }), }; - mockedWatch.mockImplementationOnce(() => - (mockWatchInstance) as any, - ); - - const extensionDiscovery = ExtensionDiscovery.createInstance( - extensionLoader, - ); + mockedWatch.mockImplementationOnce(() => mockWatchInstance as any); // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; @@ -119,15 +113,18 @@ describe("ExtensionDiscovery", () => { extensionDiscovery.events.on("add", extension => { expect(extension).toEqual({ absolutePath: expect.any(String), - id: path.normalize("node_modules/my-extension/package.json"), + id: path.normalize("some-extension-packages-root/node_modules/my-extension/package.json"), isBundled: false, isEnabled: false, isCompatible: false, - manifest: { + manifest: { name: "my-extension", }, - manifestPath: path.normalize("node_modules/my-extension/package.json"), + manifestPath: path.normalize( + "some-extension-packages-root/node_modules/my-extension/package.json", + ), }); + done(); }); @@ -148,12 +145,7 @@ describe("ExtensionDiscovery", () => { }), }; - mockedWatch.mockImplementationOnce(() => - (mockWatchInstance) as any, - ); - const extensionDiscovery = ExtensionDiscovery.createInstance( - extensionLoader, - ); + mockedWatch.mockImplementationOnce(() => mockWatchInstance as any); // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts similarity index 87% rename from src/extensions/extension-discovery.ts rename to src/extensions/extension-discovery/extension-discovery.ts index d45c8bde80..4bb50e85c6 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -23,19 +23,35 @@ import { watch } from "chokidar"; import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; import fse from "fs-extra"; -import { observable, reaction, when, makeObservable } from "mobx"; +import { makeObservable, observable, reaction, when } from "mobx"; import os from "os"; import path from "path"; -import { broadcastMessage, ipcMainHandle, ipcRendererOn, requestMain } from "../common/ipc"; -import { Singleton, toJS } from "../common/utils"; -import logger from "../main/logger"; -import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store"; -import { extensionInstaller } from "./extension-installer"; -import { ExtensionsStore } from "./extensions-store"; -import type { ExtensionLoader } from "./extension-loader"; -import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"; -import { isProduction } from "../common/vars"; -import { isCompatibleBundledExtension, isCompatibleExtension } from "./extension-compatibility"; +import { + broadcastMessage, + ipcMainHandle, + ipcRendererOn, + requestMain, +} from "../../common/ipc"; +import { toJS } from "../../common/utils"; +import logger from "../../main/logger"; +import type { ExtensionInstaller } from "../extension-installer/extension-installer"; +import type { ExtensionsStore } from "../extensions-store/extensions-store"; +import type { ExtensionLoader } from "../extension-loader"; +import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; +import { isProduction } from "../../common/vars"; +import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; + +interface Dependencies { + extensionLoader: ExtensionLoader; + + extensionInstaller: ExtensionInstaller; + extensionsStore: ExtensionsStore; + + extensionInstallationStateStore: ExtensionInstallationStateStore; + + isCompatibleBundledExtension: (manifest: LensExtensionManifest) => boolean; + isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; +} export interface InstalledExtension { id: LensExtensionId; @@ -81,7 +97,7 @@ interface LoadFromFolderOptions { * - "add": When extension is added. The event is of type InstalledExtension * - "remove": When extension is removed. The event is of type LensExtensionId */ -export class ExtensionDiscovery extends Singleton { +export class ExtensionDiscovery { protected bundledFolderPath: string; private loadStarted = false; @@ -99,9 +115,7 @@ export class ExtensionDiscovery extends Singleton { public events = new EventEmitter(); - constructor(protected extensionLoader: ExtensionLoader) { - super(); - + constructor(protected dependencies : Dependencies) { makeObservable(this); } @@ -110,11 +124,11 @@ export class ExtensionDiscovery extends Singleton { } get packageJsonPath(): string { - return path.join(extensionInstaller.extensionPackagesRoot, manifestFilename); + return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, manifestFilename); } get inTreeTargetPath(): string { - return path.join(extensionInstaller.extensionPackagesRoot, "extensions"); + return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, "extensions"); } get inTreeFolderPath(): string { @@ -122,7 +136,7 @@ export class ExtensionDiscovery extends Singleton { } get nodeModulesPath(): string { - return path.join(extensionInstaller.extensionPackagesRoot, "node_modules"); + return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, "node_modules"); } /** @@ -197,7 +211,7 @@ export class ExtensionDiscovery extends Singleton { if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) { try { - ExtensionInstallationStateStore.setInstallingFromMain(manifestPath); + this.dependencies.extensionInstallationStateStore.setInstallingFromMain(manifestPath); const absPath = path.dirname(manifestPath); // this.loadExtensionFromPath updates this.packagesJson @@ -217,7 +231,7 @@ export class ExtensionDiscovery extends Singleton { } catch (error) { logger.error(`${logModule}: failed to add extension: ${error}`, { error }); } finally { - ExtensionInstallationStateStore.clearInstallingFromMain(manifestPath); + this.dependencies.extensionInstallationStateStore.clearInstallingFromMain(manifestPath); } } }; @@ -277,7 +291,7 @@ export class ExtensionDiscovery extends Singleton { * @param extensionId The ID of the extension to uninstall. */ async uninstallExtension(extensionId: LensExtensionId): Promise { - const { manifest, absolutePath } = this.extensions.get(extensionId) ?? this.extensionLoader.getExtension(extensionId); + const { manifest, absolutePath } = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtension(extensionId); logger.info(`${logModule} Uninstalling ${manifest.name}`); @@ -295,10 +309,15 @@ export class ExtensionDiscovery extends Singleton { this.loadStarted = true; - logger.info(`${logModule} loading extensions from ${extensionInstaller.extensionPackagesRoot}`); + const extensionPackagesRoot = + this.dependencies.extensionInstaller.extensionPackagesRoot; + + logger.info( + `${logModule} loading extensions from ${extensionPackagesRoot}`, + ); // fs.remove won't throw if path is missing - await fse.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json")); + await fse.remove(path.join(extensionPackagesRoot, "package-lock.json")); try { // Verify write access to static/extensions, which is needed for symlinking @@ -357,11 +376,11 @@ export class ExtensionDiscovery extends Singleton { try { const manifest = await fse.readJson(manifestPath) as LensExtensionManifest; const id = this.getInstalledManifestPath(manifest.name); - const isEnabled = ExtensionsStore.getInstance().isEnabled({ id, isBundled }); + const isEnabled = this.dependencies.extensionsStore.isEnabled({ id, isBundled }); const extensionDir = path.dirname(manifestPath); const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`); const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir; - const isCompatible = (isBundled && isCompatibleBundledExtension(manifest)) || isCompatibleExtension(manifest); + const isCompatible = (isBundled && this.dependencies.isCompatibleBundledExtension(manifest)) || this.dependencies.isCompatibleExtension(manifest); return { id, @@ -417,11 +436,11 @@ export class ExtensionDiscovery extends Singleton { extensions.map(extension => [extension.manifest.name, extension.absolutePath]), ); - return extensionInstaller.installPackages(packageJsonPath, { dependencies }); + return this.dependencies.extensionInstaller.installPackages(packageJsonPath, { dependencies }); } async installPackage(name: string): Promise { - return extensionInstaller.installPackage(name); + return this.dependencies.extensionInstaller.installPackage(name); } async loadBundledExtensions(): Promise { diff --git a/src/extensions/extension-discovery/is-compatible-bundled-extension/is-compatible-bundled-extension.injectable.ts b/src/extensions/extension-discovery/is-compatible-bundled-extension/is-compatible-bundled-extension.injectable.ts new file mode 100644 index 0000000000..6d486a4f41 --- /dev/null +++ b/src/extensions/extension-discovery/is-compatible-bundled-extension/is-compatible-bundled-extension.injectable.ts @@ -0,0 +1,30 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appSemVer } from "../../../common/vars"; +import { isCompatibleBundledExtension } from "./is-compatible-bundled-extension"; + +const isCompatibleBundledExtensionInjectable = getInjectable({ + instantiate: () => isCompatibleBundledExtension({ appSemVer }), + lifecycle: lifecycleEnum.singleton, +}); + +export default isCompatibleBundledExtensionInjectable; diff --git a/src/extensions/extension-discovery/is-compatible-bundled-extension/is-compatible-bundled-extension.ts b/src/extensions/extension-discovery/is-compatible-bundled-extension/is-compatible-bundled-extension.ts new file mode 100644 index 0000000000..23f841a42c --- /dev/null +++ b/src/extensions/extension-discovery/is-compatible-bundled-extension/is-compatible-bundled-extension.ts @@ -0,0 +1,32 @@ +/** + * 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 { isProduction } from "../../../common/vars"; +import type { SemVer } from "semver"; + +interface Dependencies { + appSemVer: SemVer; +} + +export const isCompatibleBundledExtension = + ({ appSemVer }: Dependencies) => + (manifest: LensExtensionManifest): boolean => + !isProduction || manifest.version === appSemVer.raw; diff --git a/src/extensions/extension-discovery/is-compatible-extension/is-compatible-extension.injectable.ts b/src/extensions/extension-discovery/is-compatible-extension/is-compatible-extension.injectable.ts new file mode 100644 index 0000000000..65c58a1a63 --- /dev/null +++ b/src/extensions/extension-discovery/is-compatible-extension/is-compatible-extension.injectable.ts @@ -0,0 +1,30 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appSemVer } from "../../../common/vars"; +import { isCompatibleExtension } from "./is-compatible-extension"; + +const isCompatibleExtensionInjectable = getInjectable({ + instantiate: () => isCompatibleExtension({ appSemVer }), + lifecycle: lifecycleEnum.singleton, +}); + +export default isCompatibleExtensionInjectable; diff --git a/src/extensions/extension-discovery/is-compatible-extension/is-compatible-extension.ts b/src/extensions/extension-discovery/is-compatible-extension/is-compatible-extension.ts new file mode 100644 index 0000000000..920171cfc6 --- /dev/null +++ b/src/extensions/extension-discovery/is-compatible-extension/is-compatible-extension.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, { SemVer } from "semver"; +import type { LensExtensionManifest } from "../../lens-extension"; + +interface Dependencies { + appSemVer: SemVer; +} + +export const isCompatibleExtension = ({ + appSemVer, +}: Dependencies): ((manifest: LensExtensionManifest) => boolean) => { + const { major, minor, patch, prerelease: oldPrelease } = appSemVer; + let prerelease = ""; + + if (oldPrelease.length > 0) { + const [first] = oldPrelease; + + if (first === "alpha" || first === "beta" || first === "rc") { + /** + * Strip the build IDs and "latest" prerelease tag as that is not really + * a part of API version + */ + prerelease = `-${oldPrelease.slice(0, 2).join(".")}`; + } + } + + /** + * We unfortunately have to format as string because the constructor only + * takes an instance or a string. + */ + const strippedVersion = new SemVer( + `${major}.${minor}.${patch}${prerelease}`, + { includePrerelease: true }, + ); + + return (manifest: LensExtensionManifest): boolean => { + if (manifest.engines?.lens) { + /** + * include Lens's prerelease tag in the matching so the extension's + * compatibility is not limited by it + */ + return semver.satisfies(strippedVersion, manifest.engines.lens, { + includePrerelease: true, + }); + } + + return false; + }; +}; diff --git a/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts b/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts new file mode 100644 index 0000000000..e02206e41c --- /dev/null +++ b/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ExtensionInstallationStateStore } from "./extension-installation-state-store"; + +const extensionInstallationStateStoreInjectable = getInjectable({ + instantiate: () => new ExtensionInstallationStateStore(), + lifecycle: lifecycleEnum.singleton, +}); + +export default extensionInstallationStateStoreInjectable; diff --git a/src/extensions/extension-installation-state-store/extension-installation-state-store.ts b/src/extensions/extension-installation-state-store/extension-installation-state-store.ts new file mode 100644 index 0000000000..6a8be92698 --- /dev/null +++ b/src/extensions/extension-installation-state-store/extension-installation-state-store.ts @@ -0,0 +1,260 @@ +/** + * 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 { action, computed, observable } from "mobx"; +import logger from "../../main/logger"; +import { disposer } from "../../renderer/utils"; +import type { ExtendableDisposer } from "../../renderer/utils"; +import * as uuid from "uuid"; +import { broadcastMessage } from "../../common/ipc"; +import { ipcRenderer } from "electron"; + +export enum ExtensionInstallationState { + INSTALLING = "installing", + UNINSTALLING = "uninstalling", + IDLE = "idle", +} + +const Prefix = "[ExtensionInstallationStore]"; + +export class ExtensionInstallationStateStore { + private InstallingFromMainChannel = + "extension-installation-state-store:install"; + + private ClearInstallingFromMainChannel = + "extension-installation-state-store:clear-install"; + + private PreInstallIds = observable.set(); + private UninstallingExtensions = observable.set(); + private InstallingExtensions = observable.set(); + + bindIpcListeners = () => { + ipcRenderer + .on(this.InstallingFromMainChannel, (event, extId) => { + this.setInstalling(extId); + }) + + .on(this.ClearInstallingFromMainChannel, (event, extId) => { + this.clearInstalling(extId); + }); + }; + + /** + * Strictly transitions an extension from not installing to installing + * @param extId the ID of the extension + * @throws if state is not IDLE + */ + @action setInstalling = (extId: string): void => { + logger.debug(`${Prefix}: trying to set ${extId} as installing`); + + const curState = this.getInstallationState(extId); + + if (curState !== ExtensionInstallationState.IDLE) { + throw new Error( + `${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`, + ); + } + + this.InstallingExtensions.add(extId); + }; + + /** + * Broadcasts that an extension is being installed by the main process + * @param extId the ID of the extension + */ + setInstallingFromMain = (extId: string): void => { + broadcastMessage(this.InstallingFromMainChannel, extId); + }; + + /** + * Broadcasts that an extension is no longer being installed by the main process + * @param extId the ID of the extension + */ + clearInstallingFromMain = (extId: string): void => { + broadcastMessage(this.ClearInstallingFromMainChannel, extId); + }; + + /** + * Marks the start of a pre-install phase of an extension installation. The + * part of the installation before the tarball has been unpacked and the ID + * determined. + * @returns a disposer which should be called to mark the end of the install phase + */ + @action startPreInstall = (): ExtendableDisposer => { + const preInstallStepId = uuid.v4(); + + logger.debug( + `${Prefix}: starting a new preinstall phase: ${preInstallStepId}`, + ); + this.PreInstallIds.add(preInstallStepId); + + return disposer(() => { + this.PreInstallIds.delete(preInstallStepId); + logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`); + }); + }; + + /** + * Strictly transitions an extension from not uninstalling to uninstalling + * @param extId the ID of the extension + * @throws if state is not IDLE + */ + @action setUninstalling = (extId: string): void => { + logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`); + + const curState = this.getInstallationState(extId); + + if (curState !== ExtensionInstallationState.IDLE) { + throw new Error( + `${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`, + ); + } + + this.UninstallingExtensions.add(extId); + }; + + /** + * Strictly clears the INSTALLING state of an extension + * @param extId The ID of the extension + * @throws if state is not INSTALLING + */ + @action clearInstalling = (extId: string): void => { + logger.debug(`${Prefix}: trying to clear ${extId} as installing`); + + const curState = this.getInstallationState(extId); + + switch (curState) { + case ExtensionInstallationState.INSTALLING: + return void this.InstallingExtensions.delete(extId); + default: + throw new Error( + `${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`, + ); + } + }; + + /** + * Strictly clears the UNINSTALLING state of an extension + * @param extId The ID of the extension + * @throws if state is not UNINSTALLING + */ + @action clearUninstalling = (extId: string): void => { + logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`); + + const curState = this.getInstallationState(extId); + + switch (curState) { + case ExtensionInstallationState.UNINSTALLING: + return void this.UninstallingExtensions.delete(extId); + default: + throw new Error( + `${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`, + ); + } + }; + + /** + * Returns the current state of the extension. IDLE is default value. + * @param extId The ID of the extension + */ + getInstallationState = (extId: string): ExtensionInstallationState => { + if (this.InstallingExtensions.has(extId)) { + return ExtensionInstallationState.INSTALLING; + } + + if (this.UninstallingExtensions.has(extId)) { + return ExtensionInstallationState.UNINSTALLING; + } + + return ExtensionInstallationState.IDLE; + }; + + /** + * Returns true if the extension is currently INSTALLING + * @param extId The ID of the extension + */ + isExtensionInstalling = (extId: string): boolean => + this.getInstallationState(extId) === ExtensionInstallationState.INSTALLING; + + /** + * Returns true if the extension is currently UNINSTALLING + * @param extId The ID of the extension + */ + isExtensionUninstalling = (extId: string): boolean => + this.getInstallationState(extId) === + ExtensionInstallationState.UNINSTALLING; + + /** + * Returns true if the extension is currently IDLE + * @param extId The ID of the extension + */ + isExtensionIdle = (extId: string): boolean => + this.getInstallationState(extId) === ExtensionInstallationState.IDLE; + + /** + * The current number of extensions installing + */ + @computed get installing(): number { + return this.InstallingExtensions.size; + } + + /** + * The current number of extensions uninstalling + */ + get uninstalling(): number { + return this.UninstallingExtensions.size; + } + + /** + * If there is at least one extension currently installing + */ + get anyInstalling(): boolean { + return this.installing > 0; + } + + /** + * If there is at least one extension currently uninstalling + */ + get anyUninstalling(): boolean { + return this.uninstalling > 0; + } + + /** + * The current number of extensions preinstalling + */ + get preinstalling(): number { + return this.PreInstallIds.size; + } + + /** + * If there is at least one extension currently downloading + */ + get anyPreinstalling(): boolean { + return this.preinstalling > 0; + } + + /** + * If there is at least one installing or preinstalling step taking place + */ + get anyPreInstallingOrInstalling(): boolean { + return this.anyInstalling || this.anyPreinstalling; + } +} diff --git a/src/extensions/extension-installer/extension-installer.injectable.ts b/src/extensions/extension-installer/extension-installer.injectable.ts new file mode 100644 index 0000000000..8d3cb8942c --- /dev/null +++ b/src/extensions/extension-installer/extension-installer.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ExtensionInstaller } from "./extension-installer"; + +const extensionInstallerInjectable = getInjectable({ + instantiate: () => new ExtensionInstaller(), + lifecycle: lifecycleEnum.singleton, +}); + +export default extensionInstallerInjectable; diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer/extension-installer.ts similarity index 95% rename from src/extensions/extension-installer.ts rename to src/extensions/extension-installer/extension-installer.ts index 97eab7999f..234e476b6d 100644 --- a/src/extensions/extension-installer.ts +++ b/src/extensions/extension-installer/extension-installer.ts @@ -23,13 +23,12 @@ import AwaitLock from "await-lock"; import child_process from "child_process"; import fs from "fs-extra"; import path from "path"; -import logger from "../main/logger"; -import { extensionPackagesRoot } from "./extension-loader"; +import logger from "../../main/logger"; +import { extensionPackagesRoot } from "../extension-loader"; import type { PackageJson } from "type-fest"; const logModule = "[EXTENSION-INSTALLER]"; - /** * Installs dependencies for extensions */ @@ -108,5 +107,3 @@ export class ExtensionInstaller { }); } } - -export const extensionInstaller = new ExtensionInstaller(); diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 3bc959bd20..647ccd01e1 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -21,9 +21,13 @@ import { getInjectable } from "@ogre-tools/injectable"; import { lifecycleEnum } from "@ogre-tools/injectable"; import { ExtensionLoader } from "./extension-loader"; +import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; const extensionLoaderInjectable = getInjectable({ - instantiate: () => new ExtensionLoader(), + instantiate: (di) => new ExtensionLoader({ + extensionsStore: di.inject(extensionsStoreInjectable), + }), + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index af5f05c100..072f873927 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -29,8 +29,8 @@ import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle 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 type { InstalledExtension } from "../extension-discovery/extension-discovery"; +import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension"; import type { LensRendererExtension } from "../lens-renderer-extension"; import * as registries from "../registries"; @@ -41,6 +41,10 @@ export function extensionPackagesRoot() { const logModule = "[EXTENSIONS-LOADER]"; +interface Dependencies { + extensionsStore: ExtensionsStore +} + /** * Loads installed extensions to the Lens application */ @@ -75,7 +79,7 @@ export class ExtensionLoader { return when(() => this.isLoaded); } - constructor() { + constructor(protected dependencies : Dependencies) { makeObservable(this); observe(this.instances, change => { switch (change.type) { @@ -156,7 +160,7 @@ export class ExtensionLoader { // save state on change `extension.isEnabled` reaction(() => this.storeState, extensionsState => { - ExtensionsStore.getInstance().mergeState(extensionsState); + this.dependencies.extensionsStore.mergeState(extensionsState); }); } diff --git a/src/extensions/extensions-store/extensions-store.injectable.ts b/src/extensions/extensions-store/extensions-store.injectable.ts new file mode 100644 index 0000000000..dc148b3759 --- /dev/null +++ b/src/extensions/extensions-store/extensions-store.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ExtensionsStore } from "./extensions-store"; + +const extensionsStoreInjectable = getInjectable({ + instantiate: () => ExtensionsStore.createInstance(), + lifecycle: lifecycleEnum.singleton, +}); + +export default extensionsStoreInjectable; diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store/extensions-store.ts similarity index 91% rename from src/extensions/extensions-store.ts rename to src/extensions/extensions-store/extensions-store.ts index a69587e95d..edbf3ce018 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store/extensions-store.ts @@ -19,10 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { LensExtensionId } from "./lens-extension"; -import { BaseStore } from "../common/base-store"; -import { action, computed, observable, makeObservable } from "mobx"; -import { toJS } from "../common/utils"; +import type { LensExtensionId } from "../lens-extension"; +import { action, computed, makeObservable, observable } from "mobx"; +import { toJS } from "../../common/utils"; +import { BaseStore } from "../../common/base-store"; export interface LensExtensionsStoreModel { extensions: Record; diff --git a/src/extensions/getDiForUnitTesting.ts b/src/extensions/getDiForUnitTesting.ts index 5460d8887b..ee821aac3b 100644 --- a/src/extensions/getDiForUnitTesting.ts +++ b/src/extensions/getDiForUnitTesting.ts @@ -26,10 +26,13 @@ import { createContainer, ConfigurableDependencyInjectionContainer, } from "@ogre-tools/injectable"; +import { setLegacyGlobalDiForExtensionApi } from "./as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; export const getDiForUnitTesting = () => { const di: ConfigurableDependencyInjectionContainer = createContainer(); + setLegacyGlobalDiForExtensionApi(di); + getInjectableFilePaths() .map(key => { // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 7b6ca8c94f..0c6026d22d 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { InstalledExtension } from "./extension-discovery"; +import type { InstalledExtension } from "./extension-discovery/extension-discovery"; import { action, observable, makeObservable, computed } from "mobx"; import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; diff --git a/src/main/getDi.ts b/src/main/getDi.ts index 2b59923a6c..2eafe7e93d 100644 --- a/src/main/getDi.ts +++ b/src/main/getDi.ts @@ -20,13 +20,19 @@ */ import { createContainer } from "@ogre-tools/injectable"; +import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; -export const getDi = () => - createContainer( +export const getDi = () => { + const di = createContainer( getRequireContextForMainCode, getRequireContextForCommonExtensionCode, ); + setLegacyGlobalDiForExtensionApi(di); + + return di; +}; + const getRequireContextForMainCode = () => require.context("./", true, /\.injectable\.(ts|tsx)$/); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 06b0588ae2..97f0f777a3 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -26,10 +26,13 @@ import { createContainer, ConfigurableDependencyInjectionContainer, } from "@ogre-tools/injectable"; +import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; export const getDiForUnitTesting = () => { const di: ConfigurableDependencyInjectionContainer = createContainer(); + setLegacyGlobalDiForExtensionApi(di); + getInjectableFilePaths() .map(key => { // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/src/main/index.ts b/src/main/index.ts index 1f7b26e773..c0b86f7f4f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -36,7 +36,7 @@ import { mangleProxyEnv } from "./proxy-env"; import { registerFileProtocol } from "../common/register-protocol"; import logger from "./logger"; import { appEventBus } from "../common/event-bus"; -import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery"; +import type { InstalledExtension } from "../extensions/extension-discovery/extension-discovery"; import type { LensExtensionId } from "../extensions/lens-extension"; import { installDeveloperTools } from "./developer-tools"; import { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils"; @@ -54,7 +54,6 @@ import { ClusterStore } from "../common/cluster-store"; import { HotbarStore } from "../common/hotbar-store"; import { UserStore } from "../common/user-store"; import { WeblinkStore } from "../common/weblink-store"; -import { ExtensionsStore } from "../extensions/extensions-store"; import { FilesystemProvisionerStore } from "./extension-filesystem"; import { SentryInit } from "../common/sentry"; import { ensureDir } from "fs-extra"; @@ -68,6 +67,8 @@ import { getDi } from "./getDi"; import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable"; +import extensionDiscoveryInjectable + from "../extensions/extension-discovery/extension-discovery.injectable"; const di = getDi(); @@ -169,7 +170,6 @@ app.on("ready", async () => { // HotbarStore depends on: ClusterStore HotbarStore.createInstance(); - ExtensionsStore.createInstance(); FilesystemProvisionerStore.createInstance(); WeblinkStore.createInstance(); @@ -231,7 +231,7 @@ app.on("ready", async () => { extensionLoader.init(); - const extensionDiscovery = ExtensionDiscovery.createInstance(extensionLoader); + const extensionDiscovery = di.inject(extensionDiscoveryInjectable); extensionDiscovery.init(); diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index 1d17625b6d..7c81a78711 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -25,7 +25,7 @@ import { broadcastMessage } from "../../../common/ipc"; import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler"; import { delay, noop } from "../../../common/utils"; import { LensExtension } from "../../../extensions/main-api"; -import { ExtensionsStore } from "../../../extensions/extensions-store"; +import { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main"; import mockFs from "mock-fs"; import { AppPaths } from "../../../common/app-paths"; @@ -34,6 +34,8 @@ import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable"; +import extensionsStoreInjectable + from "../../../extensions/extensions-store/extensions-store.injectable"; jest.mock("../../../common/ipc"); @@ -66,16 +68,17 @@ describe("protocol router tests", () => { // Unit tests are allowed to only public interfaces. let extensionLoader: any; let lpr: LensProtocolRouterMain; + let extensionsStore: ExtensionsStore; beforeEach(() => { const di = getDiForUnitTesting(); extensionLoader = di.inject(extensionLoaderInjectable); + extensionsStore = di.inject(extensionsStoreInjectable); mockFs({ "tmp": {}, }); - ExtensionsStore.createInstance(); lpr = di.inject(lensProtocolRouterMainInjectable); @@ -85,7 +88,9 @@ describe("protocol router tests", () => { afterEach(() => { jest.clearAllMocks(); + // TODO: Remove Singleton from BaseStore to achieve independent unit testing ExtensionsStore.resetInstance(); + mockFs.restore(); }); @@ -126,7 +131,7 @@ describe("protocol router tests", () => { }); extensionLoader.instances.set(extId, ext); - (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" }); + (extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" }); lpr.addInternalHandler("/", noop); @@ -205,7 +210,7 @@ describe("protocol router tests", () => { }); extensionLoader.instances.set(extId, ext); - (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); + (extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); try { expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined(); @@ -243,7 +248,7 @@ describe("protocol router tests", () => { }); extensionLoader.instances.set(extId, ext); - (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); + (extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); } { @@ -268,11 +273,11 @@ describe("protocol router tests", () => { }); extensionLoader.instances.set(extId, ext); - (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "icecream" }); + (extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" }); } - (ExtensionsStore.getInstance() as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" }); - (ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" }); + (extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" }); + (extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" }); try { expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined(); diff --git a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts index 9f26ab6a4c..c674ad42d7 100644 --- a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts +++ b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts @@ -21,11 +21,13 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; import { LensProtocolRouterMain } from "./lens-protocol-router-main"; +import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; const lensProtocolRouterMainInjectable = getInjectable({ instantiate: (di) => new LensProtocolRouterMain({ extensionLoader: di.inject(extensionLoaderInjectable), + extensionsStore: di.inject(extensionsStoreInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts index cae62989f4..2b9a2dcf0b 100644 --- a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts +++ b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts @@ -29,6 +29,7 @@ import { ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-h import { disposer, noop } from "../../../common/utils"; import { WindowManager } from "../../window-manager"; import type { ExtensionLoader } from "../../../extensions/extension-loader"; +import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; export interface FallbackHandler { (name: string): Promise; @@ -53,6 +54,7 @@ function checkHost(url: URLParse): boolean { interface Dependencies { extensionLoader: ExtensionLoader + extensionsStore: ExtensionsStore } export class LensProtocolRouterMain extends proto.LensProtocolRouter { diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 1de360d18a..fccbb902b6 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -33,16 +33,13 @@ import { delay } from "../common/utils"; import { isMac, isDevelopment } from "../common/vars"; import { ClusterStore } from "../common/cluster-store"; import { UserStore } from "../common/user-store"; -import { ExtensionDiscovery } from "../extensions/extension-discovery"; import { HelmRepoManager } from "../main/helm/helm-repo-manager"; -import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store"; import { DefaultProps } from "./mui-base-theme"; import configurePackages from "../common/configure-packages"; import * as initializers from "./initializers"; import logger from "../common/logger"; import { HotbarStore } from "../common/hotbar-store"; import { WeblinkStore } from "../common/weblink-store"; -import { ExtensionsStore } from "../extensions/extensions-store"; import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import { ThemeStore } from "./theme.store"; import { SentryInit } from "../common/sentry"; @@ -59,6 +56,10 @@ import bindProtocolAddRouteHandlersInjectable import type { LensProtocolRouterRenderer } from "./protocol-handler"; import lensProtocolRouterRendererInjectable from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; +import extensionDiscoveryInjectable + from "../extensions/extension-discovery/extension-discovery.injectable"; +import extensionInstallationStateStoreInjectable + from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; if (process.isMainFrame) { SentryInit(); @@ -139,7 +140,9 @@ export async function bootstrap(comp: () => Promise, di: Dependenc extensionLoader.init(); - ExtensionDiscovery.createInstance(extensionLoader).init(); + const extensionDiscovery = di.inject(extensionDiscoveryInjectable); + + extensionDiscovery.init(); // ClusterStore depends on: UserStore const clusterStore = ClusterStore.createInstance(); @@ -148,7 +151,6 @@ export async function bootstrap(comp: () => Promise, di: Dependenc // HotbarStore depends on: ClusterStore HotbarStore.createInstance(); - ExtensionsStore.createInstance(); FilesystemProvisionerStore.createInstance(); // ThemeStore depends on: UserStore @@ -158,7 +160,10 @@ export async function bootstrap(comp: () => Promise, di: Dependenc TerminalStore.createInstance(); WeblinkStore.createInstance(); - ExtensionInstallationStateStore.bindIpcListeners(); + const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); + + extensionInstallationStateStore.bindIpcListeners(); + HelmRepoManager.createInstance(); // initialize the manager // Register additional store listeners diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 4ebdb6156f..16c927a796 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -24,10 +24,9 @@ import { fireEvent, waitFor } from "@testing-library/react"; import fse from "fs-extra"; import React from "react"; import { UserStore } from "../../../../common/user-store"; -import { ExtensionDiscovery } from "../../../../extensions/extension-discovery"; +import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery"; import type { ExtensionLoader } from "../../../../extensions/extension-loader"; import { ConfirmDialog } from "../../confirm-dialog"; -import { ExtensionInstallationStateStore } from "../extension-install.store"; import { Extensions } from "../extensions"; import mockFs from "mock-fs"; import { mockWindow } from "../../../../../__mocks__/windowMock"; @@ -36,6 +35,8 @@ import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { DiRender, renderFor } from "../../test-utils/renderFor"; +import extensionDiscoveryInjectable + from "../../../../extensions/extension-discovery/extension-discovery.injectable"; mockWindow(); @@ -78,6 +79,7 @@ AppPaths.init(); describe("Extensions", () => { let extensionLoader: ExtensionLoader; + let extensionDiscovery: ExtensionDiscovery; let render: DiRender; beforeEach(async () => { @@ -87,12 +89,12 @@ describe("Extensions", () => { extensionLoader = di.inject(extensionLoaderInjectable); + extensionDiscovery = di.inject(extensionDiscoveryInjectable); + mockFs({ "tmp": {}, }); - ExtensionInstallationStateStore.reset(); - extensionLoader.addExtension({ id: "extensionId", manifest: { @@ -106,8 +108,6 @@ describe("Extensions", () => { isCompatible: true, }); - const extensionDiscovery = ExtensionDiscovery.createInstance(extensionLoader); - extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve()); UserStore.createInstance(); @@ -116,11 +116,10 @@ describe("Extensions", () => { afterEach(() => { mockFs.restore(); UserStore.resetInstance(); - ExtensionDiscovery.resetInstance(); }); it("disables uninstall and disable buttons while uninstalling", async () => { - ExtensionDiscovery.getInstance().isLoaded = true; + extensionDiscovery.isLoaded = true; const res = render(<>); const table = res.getByTestId("extensions-table"); @@ -137,7 +136,7 @@ describe("Extensions", () => { fireEvent.click(res.getByText("Yes")); await waitFor(() => { - expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled(); + expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled(); fireEvent.click(menuTrigger); expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "true"); expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "true"); @@ -164,14 +163,14 @@ describe("Extensions", () => { }); it("displays spinner while extensions are loading", () => { - ExtensionDiscovery.getInstance().isLoaded = false; + extensionDiscovery.isLoaded = false; const { container } = render(); expect(container.querySelector(".Spinner")).toBeInTheDocument(); }); it("does not display the spinner while extensions are not loading", async () => { - ExtensionDiscovery.getInstance().isLoaded = true; + extensionDiscovery.isLoaded = true; const { container } = render(); expect(container.querySelector(".Spinner")).not.toBeInTheDocument(); diff --git a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts index 8c601230f1..31a29e75c5 100644 --- a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts +++ b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts @@ -22,12 +22,15 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { attemptInstallByInfo } from "./attempt-install-by-info"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable"; +import extensionInstallationStateStoreInjectable + from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; const attemptInstallByInfoInjectable = getInjectable({ instantiate: (di) => attemptInstallByInfo({ attemptInstall: di.inject(attemptInstallInjectable), getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable), + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx index 076737c0fe..58cc8c45ad 100644 --- a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx +++ b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx @@ -18,7 +18,6 @@ * 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 { ExtensionInstallationStateStore } from "../extension-install.store"; import { downloadFile, downloadJson, ExtendableDisposer } from "../../../../common/utils"; import { Notifications } from "../../notifications"; import { ConfirmDialog } from "../../confirm-dialog"; @@ -28,6 +27,7 @@ import { SemVer } from "semver"; import URLParse from "url-parse"; import type { InstallRequest } from "../attempt-install/install-request"; import lodash from "lodash"; +import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; export interface ExtensionInfo { name: string; @@ -38,14 +38,15 @@ export interface ExtensionInfo { interface Dependencies { attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise; getBaseRegistryUrl: () => Promise; + extensionInstallationStateStore: ExtensionInstallationStateStore } -export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl }: Dependencies) => async ({ +export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, extensionInstallationStateStore }: Dependencies) => async ({ name, version, requireConfirmation = false, }: ExtensionInfo) => { - const disposer = ExtensionInstallationStateStore.startPreInstall(); + const disposer = extensionInstallationStateStore.startPreInstall(); const baseUrl = await getBaseRegistryUrl(); const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); let json: any; diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts b/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts index acf21a17e4..0be3533845 100644 --- a/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts +++ b/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts @@ -23,6 +23,11 @@ import extensionLoaderInjectable from "../../../../extensions/extension-loader/e import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable"; import { attemptInstall } from "./attempt-install"; import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable"; +import getExtensionDestFolderInjectable + from "./get-extension-dest-folder/get-extension-dest-folder.injectable"; +import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable"; +import extensionInstallationStateStoreInjectable + from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; const attemptInstallInjectable = getInjectable({ instantiate: (di) => @@ -30,6 +35,9 @@ const attemptInstallInjectable = getInjectable({ extensionLoader: di.inject(extensionLoaderInjectable), uninstallExtension: di.inject(uninstallExtensionInjectable), unpackExtension: di.inject(unpackExtensionInjectable), + createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable), + getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable), + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx index f8aba4e5a9..d4ec51f461 100644 --- a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx +++ b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx @@ -23,10 +23,6 @@ import { disposer, ExtendableDisposer, } from "../../../../common/utils"; -import { - ExtensionInstallationState, - ExtensionInstallationStateStore, -} from "../extension-install.store"; import { Notifications } from "../../notifications"; import { Button } from "../../button"; import type { ExtensionLoader } from "../../../../extensions/extension-loader"; @@ -34,27 +30,43 @@ import type { LensExtensionId } from "../../../../extensions/lens-extension"; import React from "react"; import fse from "fs-extra"; import { shell } from "electron"; -import { - createTempFilesAndValidate, - InstallRequestValidated, -} from "./create-temp-files-and-validate/create-temp-files-and-validate"; -import { getExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder"; +import type { InstallRequestValidated } from "./create-temp-files-and-validate/create-temp-files-and-validate"; import type { InstallRequest } from "./install-request"; +import { + ExtensionInstallationState, + ExtensionInstallationStateStore, +} from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; interface Dependencies { extensionLoader: ExtensionLoader; uninstallExtension: (id: LensExtensionId) => Promise; + unpackExtension: ( request: InstallRequestValidated, disposeDownloading: Disposer, ) => Promise; + + createTempFilesAndValidate: ( + installRequest: InstallRequest, + ) => Promise; + + getExtensionDestFolder: (name: string) => string + + extensionInstallationStateStore: ExtensionInstallationStateStore } export const attemptInstall = - ({ extensionLoader, uninstallExtension, unpackExtension }: Dependencies) => + ({ + extensionLoader, + uninstallExtension, + unpackExtension, + createTempFilesAndValidate, + getExtensionDestFolder, + extensionInstallationStateStore, + }: Dependencies) => async (request: InstallRequest, d?: ExtendableDisposer): Promise => { const dispose = disposer( - ExtensionInstallationStateStore.startPreInstall(), + extensionInstallationStateStore.startPreInstall(), d, ); @@ -65,7 +77,7 @@ export const attemptInstall = } const { name, version, description } = validatedRequest.manifest; - const curState = ExtensionInstallationStateStore.getInstallationState( + const curState = extensionInstallationStateStore.getInstallationState( validatedRequest.id, ); diff --git a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx new file mode 100644 index 0000000000..7f5484822e --- /dev/null +++ b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx @@ -0,0 +1,34 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { createTempFilesAndValidate } from "./create-temp-files-and-validate"; +import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable"; + +const createTempFilesAndValidateInjectable = getInjectable({ + instantiate: (di) => + createTempFilesAndValidate({ + extensionDiscovery: di.inject(extensionDiscoveryInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createTempFilesAndValidateInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.tsx b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.tsx index e692ace784..76481e9dac 100644 --- a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.tsx +++ b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { validatePackage } from "../validate-package/validate-package"; -import { ExtensionDiscovery } from "../../../../../extensions/extension-discovery"; +import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery"; import { getMessageFromError } from "../../get-message-from-error/get-message-from-error"; import logger from "../../../../../main/logger"; import { Notifications } from "../../../notifications"; @@ -41,60 +41,66 @@ export interface InstallRequestValidated { tempFile: string; // temp system path to packed extension for unpacking } -export async function createTempFilesAndValidate({ - fileName, - dataP, -}: InstallRequest): Promise { - // copy files to temp - await fse.ensureDir(getExtensionPackageTemp()); - - // validate packages - const tempFile = getExtensionPackageTemp(fileName); - - try { - const data = await dataP; - - if (!data) { - return null; - } - - await fse.writeFile(tempFile, data); - const manifest = await validatePackage(tempFile); - const id = path.join( - ExtensionDiscovery.getInstance().nodeModulesPath, - manifest.name, - "package.json", - ); - - return { - fileName, - data, - manifest, - tempFile, - id, - }; - } catch (error) { - const message = getMessageFromError(error); - - logger.info( - `[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`, - { error }, - ); - Notifications.error( -
-

- Installing {fileName} has failed, skipping. -

-

- Reason: {message} -

-
, - ); - } - - return null; +interface Dependencies { + extensionDiscovery: ExtensionDiscovery } +export const createTempFilesAndValidate = + ({ extensionDiscovery }: Dependencies) => + async ({ + fileName, + dataP, + }: InstallRequest): Promise => { + // copy files to temp + await fse.ensureDir(getExtensionPackageTemp()); + + // validate packages + const tempFile = getExtensionPackageTemp(fileName); + + try { + const data = await dataP; + + if (!data) { + return null; + } + + await fse.writeFile(tempFile, data); + const manifest = await validatePackage(tempFile); + const id = path.join( + extensionDiscovery.nodeModulesPath, + manifest.name, + "package.json", + ); + + return { + fileName, + data, + manifest, + tempFile, + id, + }; + } catch (error) { + const message = getMessageFromError(error); + + logger.info( + `[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`, + { error }, + ); + Notifications.error( +
+

+ Installing {fileName} has failed, skipping. +

+

+ Reason: {message} +

+
, + ); + } + + return null; + }; + function getExtensionPackageTemp(fileName = "") { return path.join(os.tmpdir(), "lens-extensions", fileName); diff --git a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts new file mode 100644 index 0000000000..d589eda5e3 --- /dev/null +++ b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts @@ -0,0 +1,36 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; + +import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable"; + +import { getExtensionDestFolder } from "./get-extension-dest-folder"; + +const getExtensionDestFolderInjectable = getInjectable({ + instantiate: (di) => + getExtensionDestFolder({ + extensionDiscovery: di.inject(extensionDiscoveryInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default getExtensionDestFolderInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.tsx b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.ts similarity index 77% rename from src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.tsx rename to src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.ts index 141b6d54ec..526531ac15 100644 --- a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.tsx +++ b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.ts @@ -18,11 +18,15 @@ * 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 { ExtensionDiscovery } from "../../../../../extensions/extension-discovery"; +import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery"; import { sanitizeExtensionName } from "../../../../../extensions/lens-extension"; import path from "path"; -export const getExtensionDestFolder = (name: string) => path.join( - ExtensionDiscovery.getInstance().localFolderPath, - sanitizeExtensionName(name), -); +interface Dependencies { + extensionDiscovery: ExtensionDiscovery; +} + +export const getExtensionDestFolder = + ({ extensionDiscovery }: Dependencies) => + (name: string) => + path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name)); diff --git a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx index 05d970c129..5cebbd0220 100644 --- a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx +++ b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx @@ -22,11 +22,17 @@ import { getInjectable } from "@ogre-tools/injectable"; import { lifecycleEnum } from "@ogre-tools/injectable"; import { unpackExtension } from "./unpack-extension"; import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable"; +import getExtensionDestFolderInjectable + from "../get-extension-dest-folder/get-extension-dest-folder.injectable"; +import extensionInstallationStateStoreInjectable + from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; const unpackExtensionInjectable = getInjectable({ instantiate: (di) => unpackExtension({ extensionLoader: di.inject(extensionLoaderInjectable), + getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable), + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.tsx b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.tsx index 73e9f15328..c23c9cccc1 100644 --- a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.tsx +++ b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.tsx @@ -20,93 +20,98 @@ */ import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate"; import { Disposer, extractTar, noop } from "../../../../../common/utils"; -import { ExtensionInstallationStateStore } from "../../extension-install.store"; import { extensionDisplayName } from "../../../../../extensions/lens-extension"; import logger from "../../../../../main/logger"; import type { ExtensionLoader } from "../../../../../extensions/extension-loader"; import { Notifications } from "../../../notifications"; import { getMessageFromError } from "../../get-message-from-error/get-message-from-error"; -import { getExtensionDestFolder } from "../get-extension-dest-folder/get-extension-dest-folder"; import path from "path"; import fse from "fs-extra"; import { when } from "mobx"; import React from "react"; +import type { ExtensionInstallationStateStore } from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store"; interface Dependencies { extensionLoader: ExtensionLoader + getExtensionDestFolder: (name: string) => string + extensionInstallationStateStore: ExtensionInstallationStateStore } -export const unpackExtension = ({ extensionLoader }: Dependencies) => async ( - request: InstallRequestValidated, - disposeDownloading?: Disposer, -) => { - const { - id, - fileName, - tempFile, - manifest: { name, version }, - } = request; +export const unpackExtension = + ({ + extensionLoader, + getExtensionDestFolder, + extensionInstallationStateStore, + }: Dependencies) => + async (request: InstallRequestValidated, disposeDownloading?: Disposer) => { + const { + id, + fileName, + tempFile, + manifest: { name, version }, + } = request; - ExtensionInstallationStateStore.setInstalling(id); - disposeDownloading?.(); + extensionInstallationStateStore.setInstalling(id); + disposeDownloading?.(); - const displayName = extensionDisplayName(name, version); - const extensionFolder = getExtensionDestFolder(name); - const unpackingTempFolder = path.join( - path.dirname(tempFile), - `${path.basename(tempFile)}-unpacked`, - ); + const displayName = extensionDisplayName(name, version); + const extensionFolder = getExtensionDestFolder(name); + const unpackingTempFolder = path.join( + path.dirname(tempFile), + `${path.basename(tempFile)}-unpacked`, + ); - logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile }); + logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile }); - try { - // extract to temp folder first - await fse.remove(unpackingTempFolder).catch(noop); - await fse.ensureDir(unpackingTempFolder); - await extractTar(tempFile, { cwd: unpackingTempFolder }); + try { + // extract to temp folder first + await fse.remove(unpackingTempFolder).catch(noop); + await fse.ensureDir(unpackingTempFolder); + await extractTar(tempFile, { cwd: unpackingTempFolder }); - // move contents to extensions folder - const unpackedFiles = await fse.readdir(unpackingTempFolder); - let unpackedRootFolder = unpackingTempFolder; + // move contents to extensions folder + const unpackedFiles = await fse.readdir(unpackingTempFolder); + let unpackedRootFolder = unpackingTempFolder; - if (unpackedFiles.length === 1) { - // check if %extension.tgz was packed with single top folder, - // e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball - unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]); - } + if (unpackedFiles.length === 1) { + // check if %extension.tgz was packed with single top folder, + // e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball + unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]); + } - await fse.ensureDir(extensionFolder); - await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true }); + await fse.ensureDir(extensionFolder); + await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true }); - // wait for the loader has actually install it - await when(() => extensionLoader.userExtensions.has(id)); + // wait for the loader has actually install it + await when(() => extensionLoader.userExtensions.has(id)); - // Enable installed extensions by default. - extensionLoader.setIsEnabled(id, true); + // Enable installed extensions by default. + extensionLoader.setIsEnabled(id, true); - Notifications.ok( -

- Extension {displayName} successfully installed! -

, - ); - } catch (error) { - const message = getMessageFromError(error); + Notifications.ok( +

+ Extension {displayName} successfully installed! +

, + ); + } catch (error) { + const message = getMessageFromError(error); - logger.info( - `[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`, - { error }, - ); - Notifications.error( -

- Installing extension {displayName} has failed: {message} -

, - ); - } finally { - // Remove install state once finished - ExtensionInstallationStateStore.clearInstalling(id); + logger.info( + `[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`, + { error }, + ); + Notifications.error( +

+ Installing extension {displayName} has failed:{" "} + {message} +

, + ); + } finally { + // Remove install state once finished + extensionInstallationStateStore.clearInstalling(id); - // clean up - fse.remove(unpackingTempFolder).catch(noop); - fse.unlink(tempFile).catch(noop); - } -}; + // clean up + fse.remove(unpackingTempFolder).catch(noop); + fse.unlink(tempFile).catch(noop); + } + }; diff --git a/src/renderer/components/+extensions/attempt-install/validate-package/validate-package.tsx b/src/renderer/components/+extensions/attempt-install/validate-package/validate-package.tsx index d50bd7fb9a..1aba578466 100644 --- a/src/renderer/components/+extensions/attempt-install/validate-package/validate-package.tsx +++ b/src/renderer/components/+extensions/attempt-install/validate-package/validate-package.tsx @@ -20,7 +20,7 @@ */ import type { LensExtensionManifest } from "../../../../../extensions/lens-extension"; import { listTarEntries, readFileFromTar } from "../../../../../common/utils"; -import { manifestFilename } from "../../../../../extensions/extension-discovery"; +import { manifestFilename } from "../../../../../extensions/extension-discovery/extension-discovery"; import path from "path"; export const validatePackage = async ( diff --git a/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.tsx b/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.tsx index fbf1670e29..2c67e1f2af 100644 --- a/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.tsx +++ b/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import React from "react"; -import type { InstalledExtension } from "../../../../extensions/extension-discovery"; +import type { InstalledExtension } from "../../../../extensions/extension-discovery/extension-discovery"; import type { LensExtensionId } from "../../../../extensions/lens-extension"; import { extensionDisplayName } from "../../../../extensions/lens-extension"; import { ConfirmDialog } from "../../confirm-dialog"; diff --git a/src/renderer/components/+extensions/extension-install.store.ts b/src/renderer/components/+extensions/extension-install.store.ts deleted file mode 100644 index 4ca79ec337..0000000000 --- a/src/renderer/components/+extensions/extension-install.store.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * 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 { action, computed, observable } from "mobx"; -import logger from "../../../main/logger"; -import { disposer } from "../../utils"; -import type { ExtendableDisposer } from "../../utils"; -import * as uuid from "uuid"; -import { broadcastMessage } from "../../../common/ipc"; -import { ipcRenderer } from "electron"; - -export enum ExtensionInstallationState { - INSTALLING = "installing", - UNINSTALLING = "uninstalling", - IDLE = "idle", -} - -const Prefix = "[ExtensionInstallationStore]"; - -export class ExtensionInstallationStateStore { - private static InstallingFromMainChannel = "extension-installation-state-store:install"; - private static ClearInstallingFromMainChannel = "extension-installation-state-store:clear-install"; - private static PreInstallIds = observable.set(); - private static UninstallingExtensions = observable.set(); - private static InstallingExtensions = observable.set(); - - static bindIpcListeners() { - ipcRenderer - .on(ExtensionInstallationStateStore.InstallingFromMainChannel, (event, extId) => { - ExtensionInstallationStateStore.setInstalling(extId); - }) - .on(ExtensionInstallationStateStore.ClearInstallingFromMainChannel, (event, extId) => { - ExtensionInstallationStateStore.clearInstalling(extId); - }); - } - - @action static reset() { - logger.warn(`${Prefix}: resetting, may throw errors`); - ExtensionInstallationStateStore.InstallingExtensions.clear(); - ExtensionInstallationStateStore.UninstallingExtensions.clear(); - ExtensionInstallationStateStore.PreInstallIds.clear(); - } - - /** - * Strictly transitions an extension from not installing to installing - * @param extId the ID of the extension - * @throws if state is not IDLE - */ - @action static setInstalling(extId: string): void { - logger.debug(`${Prefix}: trying to set ${extId} as installing`); - - const curState = ExtensionInstallationStateStore.getInstallationState(extId); - - if (curState !== ExtensionInstallationState.IDLE) { - throw new Error(`${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`); - } - - ExtensionInstallationStateStore.InstallingExtensions.add(extId); - } - - /** - * Broadcasts that an extension is being installed by the main process - * @param extId the ID of the extension - */ - static setInstallingFromMain(extId: string): void { - broadcastMessage(ExtensionInstallationStateStore.InstallingFromMainChannel, extId); - } - - /** - * Broadcasts that an extension is no longer being installed by the main process - * @param extId the ID of the extension - */ - static clearInstallingFromMain(extId: string): void { - broadcastMessage(ExtensionInstallationStateStore.ClearInstallingFromMainChannel, extId); - } - - /** - * Marks the start of a pre-install phase of an extension installation. The - * part of the installation before the tarball has been unpacked and the ID - * determined. - * @returns a disposer which should be called to mark the end of the install phase - */ - @action static startPreInstall(): ExtendableDisposer { - const preInstallStepId = uuid.v4(); - - logger.debug(`${Prefix}: starting a new preinstall phase: ${preInstallStepId}`); - ExtensionInstallationStateStore.PreInstallIds.add(preInstallStepId); - - return disposer(() => { - ExtensionInstallationStateStore.PreInstallIds.delete(preInstallStepId); - logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`); - }); - } - - /** - * Strictly transitions an extension from not uninstalling to uninstalling - * @param extId the ID of the extension - * @throws if state is not IDLE - */ - @action static setUninstalling(extId: string): void { - logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`); - - const curState = ExtensionInstallationStateStore.getInstallationState(extId); - - if (curState !== ExtensionInstallationState.IDLE) { - throw new Error(`${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`); - } - - ExtensionInstallationStateStore.UninstallingExtensions.add(extId); - } - - /** - * Strictly clears the INSTALLING state of an extension - * @param extId The ID of the extension - * @throws if state is not INSTALLING - */ - @action static clearInstalling(extId: string): void { - logger.debug(`${Prefix}: trying to clear ${extId} as installing`); - - const curState = ExtensionInstallationStateStore.getInstallationState(extId); - - switch (curState) { - case ExtensionInstallationState.INSTALLING: - return void ExtensionInstallationStateStore.InstallingExtensions.delete(extId); - default: - throw new Error(`${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`); - } - } - - /** - * Strictly clears the UNINSTALLING state of an extension - * @param extId The ID of the extension - * @throws if state is not UNINSTALLING - */ - @action static clearUninstalling(extId: string): void { - logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`); - - const curState = ExtensionInstallationStateStore.getInstallationState(extId); - - switch (curState) { - case ExtensionInstallationState.UNINSTALLING: - return void ExtensionInstallationStateStore.UninstallingExtensions.delete(extId); - default: - throw new Error(`${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`); - } - } - - /** - * Returns the current state of the extension. IDLE is default value. - * @param extId The ID of the extension - */ - static getInstallationState(extId: string): ExtensionInstallationState { - if (ExtensionInstallationStateStore.InstallingExtensions.has(extId)) { - return ExtensionInstallationState.INSTALLING; - } - - if (ExtensionInstallationStateStore.UninstallingExtensions.has(extId)) { - return ExtensionInstallationState.UNINSTALLING; - } - - return ExtensionInstallationState.IDLE; - } - - /** - * Returns true if the extension is currently INSTALLING - * @param extId The ID of the extension - */ - static isExtensionInstalling(extId: string): boolean { - return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.INSTALLING; - } - - /** - * Returns true if the extension is currently UNINSTALLING - * @param extId The ID of the extension - */ - static isExtensionUninstalling(extId: string): boolean { - return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.UNINSTALLING; - } - - /** - * Returns true if the extension is currently IDLE - * @param extId The ID of the extension - */ - static isExtensionIdle(extId: string): boolean { - return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.IDLE; - } - - /** - * The current number of extensions installing - */ - @computed static get installing(): number { - return ExtensionInstallationStateStore.InstallingExtensions.size; - } - - /** - * The current number of extensions uninstalling - */ - static get uninstalling(): number { - return ExtensionInstallationStateStore.UninstallingExtensions.size; - } - - /** - * If there is at least one extension currently installing - */ - static get anyInstalling(): boolean { - return ExtensionInstallationStateStore.installing > 0; - } - - /** - * If there is at least one extension currently uninstalling - */ - static get anyUninstalling(): boolean { - return ExtensionInstallationStateStore.uninstalling > 0; - } - - /** - * The current number of extensions preinstalling - */ - static get preinstalling(): number { - return ExtensionInstallationStateStore.PreInstallIds.size; - } - - /** - * If there is at least one extension currently downloading - */ - static get anyPreinstalling(): boolean { - return ExtensionInstallationStateStore.preinstalling > 0; - } - - /** - * If there is at least one installing or preinstalling step taking place - */ - static get anyPreInstallingOrInstalling(): boolean { - return ExtensionInstallationStateStore.anyInstalling || ExtensionInstallationStateStore.anyPreinstalling; - } -} diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 0ba9deba65..80d7449eeb 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -29,9 +29,8 @@ import { } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; -import type { InstalledExtension } from "../../../extensions/extension-discovery"; +import type { InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery"; import { DropFileInput } from "../input"; -import { ExtensionInstallationStateStore } from "./extension-install.store"; import { Install } from "./install"; import { InstalledExtensions } from "./installed-extensions"; import { Notice } from "./notice"; @@ -48,6 +47,9 @@ import installFromSelectFileDialogInjectable from "./install-from-select-file-di import type { LensExtensionId } from "../../../extensions/lens-extension"; import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable"; import { supportedExtensionFormats } from "./supported-extension-formats"; +import extensionInstallationStateStoreInjectable + from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; interface Dependencies { userExtensions: IComputedValue; @@ -57,6 +59,7 @@ interface Dependencies { installFromInput: (input: string) => Promise; installFromSelectFileDialog: () => Promise; installOnDrop: (files: File[]) => Promise; + extensionInstallationStateStore: ExtensionInstallationStateStore } @observer @@ -73,7 +76,7 @@ class NonInjectedExtensions extends React.Component { reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => { if (curSize > prevSize) { disposeOnUnmount(this, [ - when(() => !ExtensionInstallationStateStore.anyInstalling, () => this.installPath = ""), + when(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""), ]); } }), @@ -134,6 +137,8 @@ export const Extensions = withInjectables( installFromSelectFileDialog: di.inject( installFromSelectFileDialogInjectable, ), + + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), }), }, ); diff --git a/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts b/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts index 2d0f26d786..ed4e714272 100644 --- a/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts +++ b/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts @@ -22,12 +22,15 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; import { installFromInput } from "./install-from-input"; import attemptInstallByInfoInjectable from "../attempt-install-by-info/attempt-install-by-info.injectable"; +import extensionInstallationStateStoreInjectable + from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; const installFromInputInjectable = getInjectable({ instantiate: (di) => installFromInput({ attemptInstall: di.inject(attemptInstallInjectable), attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable), + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/install-from-input/install-from-input.tsx b/src/renderer/components/+extensions/install-from-input/install-from-input.tsx index 05ad14b087..e37f184af6 100644 --- a/src/renderer/components/+extensions/install-from-input/install-from-input.tsx +++ b/src/renderer/components/+extensions/install-from-input/install-from-input.tsx @@ -20,7 +20,6 @@ */ import { downloadFile, ExtendableDisposer } from "../../../../common/utils"; import { InputValidators } from "../../input"; -import { ExtensionInstallationStateStore } from "../extension-install.store"; import { getMessageFromError } from "../get-message-from-error/get-message-from-error"; import logger from "../../../../main/logger"; import { Notifications } from "../../notifications"; @@ -29,20 +28,22 @@ import React from "react"; import { readFileNotify } from "../read-file-notify/read-file-notify"; import type { InstallRequest } from "../attempt-install/install-request"; import type { ExtensionInfo } from "../attempt-install-by-info/attempt-install-by-info"; +import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; interface Dependencies { attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise, - attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise + attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise, + extensionInstallationStateStore: ExtensionInstallationStateStore } -export const installFromInput = ({ attemptInstall, attemptInstallByInfo }: Dependencies) => async (input: string) => { +export const installFromInput = ({ attemptInstall, attemptInstallByInfo, extensionInstallationStateStore }: Dependencies) => async (input: string) => { let disposer: ExtendableDisposer | undefined = undefined; try { // fixme: improve error messages for non-tar-file URLs if (InputValidators.isUrl.validate(input)) { // install via url - disposer = ExtensionInstallationStateStore.startPreInstall(); + disposer = extensionInstallationStateStore.startPreInstall(); const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 }); const fileName = path.basename(input); diff --git a/src/renderer/components/+extensions/install.tsx b/src/renderer/components/+extensions/install.tsx index 2f81d7e950..395e03ec16 100644 --- a/src/renderer/components/+extensions/install.tsx +++ b/src/renderer/components/+extensions/install.tsx @@ -24,11 +24,14 @@ import React from "react"; import { prevDefault } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; +import { observer } from "mobx-react"; import { Input, InputValidator, InputValidators } from "../input"; import { SubTitle } from "../layout/sub-title"; import { TooltipPosition } from "../tooltip"; -import { ExtensionInstallationStateStore } from "./extension-install.store"; -import { observer } from "mobx-react"; +import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; +import extensionInstallationStateStoreInjectable + from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; interface Props { installPath: string; @@ -38,6 +41,10 @@ interface Props { installFromSelectFileDialog: () => void; } +interface Dependencies { + extensionInstallationStateStore: ExtensionInstallationStateStore; +} + const installInputValidators = [ InputValidators.isUrl, InputValidators.isPath, @@ -51,49 +58,72 @@ const installInputValidator: InputValidator = { ), }; -export const Install = observer((props: Props) => { - const { installPath, supportedFormats, onChange, installFromInput, installFromSelectFileDialog } = props; - - return ( -
- -
-
- - } - /> -
-
-
+const NonInjectedInstall: React.FC = ({ + installPath, + supportedFormats, + onChange, + installFromInput, + installFromSelectFileDialog, + extensionInstallationStateStore, +}) => ( +
+ +
+
+ + } + />
- - Pro-Tip: you can drag-n-drop tarball-file to this area - -
- ); -}); +
+
+
+ + Pro-Tip: you can drag-n-drop tarball-file to this area + +
+); +export const Install = withInjectables( + observer(NonInjectedInstall), + { + getProps: (di, props) => ({ + extensionInstallationStateStore: di.inject( + extensionInstallationStateStoreInjectable, + ), + + ...props, + }), + }, +); diff --git a/src/renderer/components/+extensions/installed-extensions.tsx b/src/renderer/components/+extensions/installed-extensions.tsx index 83c12b8603..dcc449afff 100644 --- a/src/renderer/components/+extensions/installed-extensions.tsx +++ b/src/renderer/components/+extensions/installed-extensions.tsx @@ -21,16 +21,25 @@ import styles from "./installed-extensions.module.scss"; import React, { useMemo } from "react"; -import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery"; +import type { + ExtensionDiscovery, + InstalledExtension, +} from "../../../extensions/extension-discovery/extension-discovery"; import { Icon } from "../icon"; import { List } from "../list/list"; import { MenuActions, MenuItem } from "../menu"; import { Spinner } from "../spinner"; -import { ExtensionInstallationStateStore } from "./extension-install.store"; import { cssNames } from "../../utils"; import { observer } from "mobx-react"; import type { Row } from "react-table"; import type { LensExtensionId } from "../../../extensions/lens-extension"; +import extensionDiscoveryInjectable + from "../../../extensions/extension-discovery/extension-discovery.injectable"; + +import { withInjectables } from "@ogre-tools/injectable-react"; +import extensionInstallationStateStoreInjectable + from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; interface Props { extensions: InstalledExtension[]; @@ -39,6 +48,11 @@ interface Props { uninstall: (extension: InstalledExtension) => void; } +interface Dependencies { + extensionDiscovery: ExtensionDiscovery; + extensionInstallationStateStore: ExtensionInstallationStateStore; +} + function getStatus(extension: InstalledExtension) { if (!extension.isCompatible) { return "Incompatible"; @@ -47,7 +61,7 @@ function getStatus(extension: InstalledExtension) { return extension.isEnabled ? "Enabled" : "Disabled"; } -export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => { +const NonInjectedInstalledExtensions : React.FC = (({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }) => { const filters = [ (extension: InstalledExtension) => extension.manifest.name, (extension: InstalledExtension) => getStatus(extension), @@ -93,7 +107,7 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di return extensions.map(extension => { const { id, isEnabled, isCompatible, manifest } = extension; const { name, description, version } = manifest; - const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id); + const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id); return { extension: ( @@ -145,10 +159,10 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di ), }; }); - }, [extensions, ExtensionInstallationStateStore.anyUninstalling], + }, [extensions, extensionInstallationStateStore.anyUninstalling], ); - if (!ExtensionDiscovery.getInstance().isLoaded) { + if (!extensionDiscovery.isLoaded) { return
; } @@ -176,3 +190,16 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di ); }); + +export const InstalledExtensions = withInjectables( + observer(NonInjectedInstalledExtensions), + + { + getProps: (di, props) => ({ + extensionDiscovery: di.inject(extensionDiscoveryInjectable), + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), + + ...props, + }), + }, +); diff --git a/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.injectable.ts b/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.injectable.ts index 3660c58454..f9dfb4f26a 100644 --- a/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.injectable.ts +++ b/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.injectable.ts @@ -21,11 +21,17 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import { uninstallExtension } from "./uninstall-extension"; +import extensionInstallationStateStoreInjectable + from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import extensionDiscoveryInjectable + from "../../../../extensions/extension-discovery/extension-discovery.injectable"; const uninstallExtensionInjectable = getInjectable({ instantiate: (di) => uninstallExtension({ extensionLoader: di.inject(extensionLoaderInjectable), + extensionDiscovery: di.inject(extensionDiscoveryInjectable), + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.tsx b/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.tsx index 5e796e72de..a6219da606 100644 --- a/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.tsx +++ b/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.tsx @@ -21,28 +21,30 @@ import type { ExtensionLoader } from "../../../../extensions/extension-loader"; import { extensionDisplayName, LensExtensionId } from "../../../../extensions/lens-extension"; import logger from "../../../../main/logger"; -import { ExtensionInstallationStateStore } from "../extension-install.store"; -import { ExtensionDiscovery } from "../../../../extensions/extension-discovery"; +import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery"; import { Notifications } from "../../notifications"; import React from "react"; import { when } from "mobx"; import { getMessageFromError } from "../get-message-from-error/get-message-from-error"; +import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; interface Dependencies { extensionLoader: ExtensionLoader + extensionDiscovery: ExtensionDiscovery + extensionInstallationStateStore: ExtensionInstallationStateStore } export const uninstallExtension = - ({ extensionLoader }: Dependencies) => + ({ extensionLoader, extensionDiscovery, extensionInstallationStateStore }: Dependencies) => async (extensionId: LensExtensionId): Promise => { const { manifest } = extensionLoader.getExtension(extensionId); const displayName = extensionDisplayName(manifest.name, manifest.version); try { logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`); - ExtensionInstallationStateStore.setUninstalling(extensionId); + extensionInstallationStateStore.setUninstalling(extensionId); - await ExtensionDiscovery.getInstance().uninstallExtension(extensionId); + await extensionDiscovery.uninstallExtension(extensionId); // wait for the ExtensionLoader to actually uninstall the extension await when(() => !extensionLoader.userExtensions.has(extensionId)); @@ -71,6 +73,6 @@ export const uninstallExtension = return false; } finally { // Remove uninstall state on uninstall failure - ExtensionInstallationStateStore.clearUninstalling(extensionId); + extensionInstallationStateStore.clearUninstalling(extensionId); } }; diff --git a/src/renderer/components/getDi.tsx b/src/renderer/components/getDi.tsx index a0e4615a7c..ba997137f3 100644 --- a/src/renderer/components/getDi.tsx +++ b/src/renderer/components/getDi.tsx @@ -20,13 +20,19 @@ */ import { createContainer } from "@ogre-tools/injectable"; +import { setLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; -export const getDi = () => - createContainer( +export const getDi = () => { + const di = createContainer( getRequireContextForRendererCode, getRequireContextForCommonExtensionCode, ); + setLegacyGlobalDiForExtensionApi(di); + + return di; +}; + const getRequireContextForRendererCode = () => require.context("../", true, /\.injectable\.(ts|tsx)$/); diff --git a/src/renderer/components/getDiForUnitTesting.tsx b/src/renderer/components/getDiForUnitTesting.tsx index fefd120fb4..4ab8a47df5 100644 --- a/src/renderer/components/getDiForUnitTesting.tsx +++ b/src/renderer/components/getDiForUnitTesting.tsx @@ -26,10 +26,13 @@ import { createContainer, ConfigurableDependencyInjectionContainer, } from "@ogre-tools/injectable"; +import { setLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; export const getDiForUnitTesting = () => { const di: ConfigurableDependencyInjectionContainer = createContainer(); + setLegacyGlobalDiForExtensionApi(di); + getInjectableFilePaths() .map(key => { const injectable = require(key).default; diff --git a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts index f96f33fcb6..aef39908c2 100644 --- a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts +++ b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts @@ -21,11 +21,14 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; import { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer"; +import extensionsStoreInjectable + from "../../../extensions/extensions-store/extensions-store.injectable"; const lensProtocolRouterRendererInjectable = getInjectable({ instantiate: (di) => new LensProtocolRouterRenderer({ extensionLoader: di.inject(extensionLoaderInjectable), + extensionsStore: di.inject(extensionsStoreInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx index bd3edc1bd5..43407a793f 100644 --- a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx +++ b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx @@ -27,6 +27,7 @@ import { onCorrect } from "../../../common/ipc"; import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-handler"; import { Notifications } from "../../components/notifications"; import type { ExtensionLoader } from "../../../extensions/extension-loader"; +import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] { if (args.length !== 2) { @@ -49,6 +50,7 @@ function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] { interface Dependencies { extensionLoader: ExtensionLoader + extensionsStore: ExtensionsStore }