From 7f86a89cc2f82b72d22dbbc76a1ad708dc07359a Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 16 Feb 2023 13:21:15 -0500 Subject: [PATCH] Fully convert ExtensionLoader to be injectable - To fix unit tests Signed-off-by: Sebastian Malton --- .../core/src/common/ipc/extension-handling.ts | 2 - .../src/common/protocol-handler/router.ts | 18 +- .../__tests__/extension-loader.test.ts | 172 -------- .../extension-discovery.injectable.ts | 4 +- .../extension-discovery.ts | 74 ++-- .../extension-loader.injectable.ts | 32 -- .../extension-loader/extension-loader.ts | 395 ------------------ .../update-extensions-state.injectable.ts | 13 - .../extensions-store/extensions-store.ts | 6 +- .../core/src/extensions/lens-extension.ts | 65 +-- .../get-installed-extension.injectable.ts | 21 + .../common/installed-extensions.injectable.ts | 15 + .../common/user-extensions.injectable.ts | 21 + .../common/auto-init-extensions.injectable.ts | 49 +++ .../extensions/loader/common/channels.ts | 8 +- .../extension-updates-listener.injectable.ts | 29 ++ .../finalize-extension-loading.injectable.ts | 54 +++ .../find-instance-by-name.injectable.ts | 42 ++ .../import-installed-extension.injectable.ts | 51 +++ .../load-bundled-extensions.injectable.ts | 55 +++ .../common/load-user-extensions.injectable.ts | 63 +++ .../loader/common/logger.injectable.ts | 13 + .../non-instances-by-name.injectable.ts | 13 + .../common/remove-instance.injectable.ts | 42 ++ .../handle-loaded-extensions.injectable.ts | 18 + ...etup-extensions-broadcasting.injectable.ts | 26 ++ ...nc-enabled-states-with-store.injectable.ts | 33 ++ ...tialize-installed-extensions.injectable.ts | 24 ++ .../request-loaded-extensions.injectable.ts | 18 + ...etup-extensions-broadcasting.injectable.ts | 26 ++ .../navigate/renderer/listener.injectable.ts | 8 +- .../lens-protocol-router-main.injectable.ts | 17 +- .../initialize-extensions.injectable.ts | 28 +- .../+extensions/__tests__/extensions.test.tsx | 13 +- .../attempt-install.injectable.tsx | 220 +++++----- .../unpack-extension.injectable.tsx | 10 +- .../disable-extension.injectable.ts | 23 +- .../enable-extension.injectable.ts | 23 +- .../uninstall-extension.injectable.tsx | 10 +- .../user-extensions.injectable.ts | 6 +- .../extension-discovery/init.injectable.ts | 2 - .../extension-loader/init.injectable.ts | 22 - .../init-cluster-frame.injectable.ts | 4 +- .../frames/load-extensions.injectable.ts | 17 - .../root-frame/init-root-frame.injectable.ts | 6 +- packages/core/src/renderer/ipc/index.ts | 8 +- ...ens-protocol-router-renderer.injectable.ts | 4 +- 47 files changed, 867 insertions(+), 956 deletions(-) delete mode 100644 packages/core/src/extensions/__tests__/extension-loader.test.ts delete mode 100644 packages/core/src/extensions/extension-loader/extension-loader.injectable.ts delete mode 100644 packages/core/src/extensions/extension-loader/extension-loader.ts delete mode 100644 packages/core/src/extensions/extension-loader/update-extensions-state/update-extensions-state.injectable.ts create mode 100644 packages/core/src/features/extensions/common/get-installed-extension.injectable.ts create mode 100644 packages/core/src/features/extensions/common/installed-extensions.injectable.ts create mode 100644 packages/core/src/features/extensions/common/user-extensions.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/auto-init-extensions.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/extension-updates-listener.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/finalize-extension-loading.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/find-instance-by-name.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/import-installed-extension.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/load-bundled-extensions.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/load-user-extensions.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/logger.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/non-instances-by-name.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/common/remove-instance.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/main/handle-loaded-extensions.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/main/setup-extensions-broadcasting.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/main/sync-enabled-states-with-store.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/renderer/initialize-installed-extensions.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/renderer/request-loaded-extensions.injectable.ts create mode 100644 packages/core/src/features/extensions/loader/renderer/setup-extensions-broadcasting.injectable.ts delete mode 100644 packages/core/src/renderer/extension-loader/init.injectable.ts delete mode 100644 packages/core/src/renderer/frames/load-extensions.injectable.ts diff --git a/packages/core/src/common/ipc/extension-handling.ts b/packages/core/src/common/ipc/extension-handling.ts index 0a07a56a29..04d52c0a9f 100644 --- a/packages/core/src/common/ipc/extension-handling.ts +++ b/packages/core/src/common/ipc/extension-handling.ts @@ -4,5 +4,3 @@ */ export const extensionDiscoveryStateChannel = "extension-discovery:state"; -export const extensionLoaderFromMainChannel = "extension-loader:main:state"; -export const extensionLoaderFromRendererChannel = "extension-loader:renderer:state"; diff --git a/packages/core/src/common/protocol-handler/router.ts b/packages/core/src/common/protocol-handler/router.ts index 3b17b2f600..a5a91ac07e 100644 --- a/packages/core/src/common/protocol-handler/router.ts +++ b/packages/core/src/common/protocol-handler/router.ts @@ -11,12 +11,11 @@ import { pathToRegexp } from "path-to-regexp"; import type Url from "url-parse"; import { RoutingError, RoutingErrorType } from "./error"; 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 "./registration"; import { when } from "mobx"; -import { ipcRenderer } from "electron"; import type { Logger } from "../logger"; +import type { FindExtensionInstanceByName } from "../../features/extensions/loader/common/find-instance-by-name.injectable"; // IPC channel for protocol actions. Main broadcasts the open-url events to this channel. export const ProtocolHandlerIpcPrefix = "protocol-handler"; @@ -64,9 +63,9 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R } export interface LensProtocolRouterDependencies { - readonly extensionLoader: ExtensionLoader; readonly extensionsStore: ExtensionsStore; readonly logger: Logger; + findExtensionInstanceByName: FindExtensionInstanceByName; } export abstract class LensProtocolRouter { @@ -184,13 +183,8 @@ export abstract class LensProtocolRouter { const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params; const name = [publisher, partialName].filter(isDefined).join("/"); - const extensionLoader = this.dependencies.extensionLoader; - try { - /** - * Note, if `getInstanceByName` returns `null` that means we won't be getting an instance - */ - await when(() => extensionLoader.getInstanceByName(name) !== void 0, { + await when(() => this.dependencies.findExtensionInstanceByName(name) !== "not-installed", { timeout: 5_000, }); } catch (error) { @@ -201,10 +195,10 @@ export abstract class LensProtocolRouter { return name; } - const extension = extensionLoader.getInstanceByName(name); + const extension = this.dependencies.findExtensionInstanceByName(name); - if (!extension) { - this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`); + if (typeof extension === "string") { + this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but ${extension}`); return name; } diff --git a/packages/core/src/extensions/__tests__/extension-loader.test.ts b/packages/core/src/extensions/__tests__/extension-loader.test.ts deleted file mode 100644 index cd06f80f09..0000000000 --- a/packages/core/src/extensions/__tests__/extension-loader.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { ExtensionLoader } from "../extension-loader"; -import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; -import { runInAction } from "mobx"; -import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable"; -import { delay } from "../../renderer/utils"; -import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting"; -import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable"; -import type { IpcRenderer } from "electron"; -import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import currentlyInClusterFrameInjectable from "../../renderer/routes/currently-in-cluster-frame.injectable"; - -const manifestPath = "manifest/path"; -const manifestPath2 = "manifest/path2"; -const manifestPath3 = "manifest/path3"; - -describe("ExtensionLoader", () => { - let extensionLoader: ExtensionLoader; - let updateExtensionStateMock: jest.Mock; - - beforeEach(() => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); - - di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); - di.override(currentlyInClusterFrameInjectable, () => false); - - di.override(ipcRendererInjectable, () => ({ - invoke: jest.fn(async (channel: string) => { - if (channel === "extension-loader:main:state") { - return [ - [ - manifestPath, - { - manifest: { - name: "TestExtension", - version: "1.0.0", - }, - id: manifestPath, - absolutePath: "/test/1", - manifestPath, - isBundled: false, - isEnabled: true, - }, - ], - [ - manifestPath2, - { - manifest: { - name: "TestExtension2", - version: "2.0.0", - }, - id: manifestPath2, - absolutePath: "/test/2", - manifestPath: manifestPath2, - isBundled: false, - isEnabled: true, - }, - ], - ]; - } - - return []; - }), - - on: (channel: string, listener: (event: any, ...args: any[]) => void) => { - if (channel === "extension-loader:main:state") { - // First initialize with extensions 1 and 2 - // and then broadcast event to remove extension 2 and add extension number 3 - setTimeout(() => { - listener({}, [ - [ - manifestPath, - { - manifest: { - name: "TestExtension", - version: "1.0.0", - }, - id: manifestPath, - absolutePath: "/test/1", - manifestPath, - isBundled: false, - isEnabled: true, - }, - ], - [ - manifestPath3, - { - manifest: { - name: "TestExtension3", - version: "3.0.0", - }, - id: manifestPath3, - absolutePath: "/test/3", - manifestPath: manifestPath3, - isBundled: false, - isEnabled: true, - }, - ], - ]); - }, 10); - } - }, - }) as unknown as IpcRenderer); - - updateExtensionStateMock = jest.fn(); - - di.override(updateExtensionsStateInjectable, () => updateExtensionStateMock); - - extensionLoader = di.inject(extensionLoaderInjectable); - }); - - it("renderer updates extension after ipc broadcast", async () => { - expect(extensionLoader.userExtensions.get().size).toBe(0); - - await extensionLoader.init(); - await delay(10); - - // Assert the extensions after the extension broadcast event - expect(extensionLoader.userExtensions.get()).toEqual(new Map([ - ["manifest/path", { - absolutePath: "/test/1", - id: "manifest/path", - isBundled: false, - isEnabled: true, - manifest: { - name: "TestExtension", - version: "1.0.0", - }, - manifestPath: "manifest/path", - }], - ["manifest/path3", { - absolutePath: "/test/3", - id: "manifest/path3", - isBundled: false, - isEnabled: true, - manifest: { - name: "TestExtension3", - version: "3.0.0", - }, - manifestPath: "manifest/path3", - }], - ])); - }); - - it("updates ExtensionsStore after isEnabled is changed", async () => { - await extensionLoader.init(); - - expect(updateExtensionStateMock).not.toHaveBeenCalled(); - - runInAction(() => { - extensionLoader.setIsEnabled("manifest/path", false); - }); - - expect(updateExtensionStateMock).toHaveBeenCalledWith( - expect.objectContaining({ - "manifest/path": { - enabled: false, - name: "TestExtension", - }, - - "manifest/path2": { - enabled: true, - name: "TestExtension2", - }, - }), - ); - }); -}); diff --git a/packages/core/src/extensions/extension-discovery/extension-discovery.injectable.ts b/packages/core/src/extensions/extension-discovery/extension-discovery.injectable.ts index 5e2a4cd84d..5794279128 100644 --- a/packages/core/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/packages/core/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -4,7 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { ExtensionDiscovery } from "./extension-discovery"; -import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable"; import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable"; @@ -28,12 +27,12 @@ import joinPathsInjectable from "../../common/path/join-paths.injectable"; import removePathInjectable from "../../common/fs/remove.injectable"; import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable"; import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable"; +import installedExtensionsInjectable from "../../features/extensions/common/installed-extensions.injectable"; const extensionDiscoveryInjectable = getInjectable({ id: "extension-discovery", instantiate: (di) => new ExtensionDiscovery({ - extensionLoader: di.inject(extensionLoaderInjectable), extensionsStore: di.inject(extensionsStoreInjectable), extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), isCompatibleExtension: di.inject(isCompatibleExtensionInjectable), @@ -57,6 +56,7 @@ const extensionDiscoveryInjectable = getInjectable({ getRelativePath: di.inject(getRelativePathInjectable), joinPaths: di.inject(joinPathsInjectable), homeDirectoryPath: di.inject(homeDirectoryPathInjectable), + installedExtensions: di.inject(installedExtensionsInjectable), }), }); diff --git a/packages/core/src/extensions/extension-discovery/extension-discovery.ts b/packages/core/src/extensions/extension-discovery/extension-discovery.ts index 59d778ab7c..ab4aa11cd7 100644 --- a/packages/core/src/extensions/extension-discovery/extension-discovery.ts +++ b/packages/core/src/extensions/extension-discovery/extension-discovery.ts @@ -5,11 +5,11 @@ import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; +import type { ObservableMap } from "mobx"; import { makeObservable, observable, reaction, when } from "mobx"; import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc"; -import { isErrnoException, toJS } from "../../common/utils"; +import { isErrnoException, iter, toJS } from "../../common/utils"; import type { ExtensionsStore } from "../extensions-store/extensions-store"; -import type { ExtensionLoader } from "../extension-loader"; import type { BundledLensExtensionManifest, LensExtensionId, LensExtensionManifest } from "../lens-extension"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; @@ -32,7 +32,6 @@ import type { RemovePath } from "../../common/fs/remove.injectable"; import type TypedEventEmitter from "typed-emitter"; interface Dependencies { - readonly extensionLoader: ExtensionLoader; readonly extensionsStore: ExtensionsStore; readonly extensionInstallationStateStore: ExtensionInstallationStateStore; readonly extensionPackageRootDirectory: string; @@ -41,6 +40,7 @@ interface Dependencies { readonly isProduction: boolean; readonly fileSystemSeparator: string; readonly homeDirectoryPath: string; + readonly installedExtensions: ObservableMap; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; installExtension: (name: string) => Promise; readJsonFile: ReadJson; @@ -60,11 +60,6 @@ interface Dependencies { export interface BaseInstalledExtension { readonly id: LensExtensionId; - // Absolute path to the non-symlinked source folder, - // e.g. "/Users/user/.k8slens/extensions/helloworld" - readonly absolutePath: string; - // Absolute to the symlinked package.json file - readonly manifestPath: string; } export interface BundledInstalledExtension extends BaseInstalledExtension { @@ -78,6 +73,11 @@ export interface ExternalInstalledExtension extends BaseInstalledExtension { readonly manifest: LensExtensionManifest; readonly isBundled: false; readonly isCompatible: boolean; + // Absolute path to the non-symlinked source folder, + // e.g. "/Users/user/.k8slens/extensions/helloworld" + readonly absolutePath: string; + // Absolute to the symlinked package.json file + readonly manifestPath: string; isEnabled: boolean; } @@ -99,7 +99,7 @@ const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymboli interface ExtensionDiscoveryEvents { add: (ext: InstalledExtension) => void; - remove: (extId: LensExtensionId) => void; + remove: (ext: InstalledExtension) => void; } /** @@ -116,7 +116,6 @@ export class ExtensionDiscovery { protected bundledFolderPath!: string; private loadStarted = false; - private extensions: Map = new Map(); // True if extensions have been loaded from the disk after app startup @observable isLoaded = false; @@ -177,12 +176,9 @@ export class ExtensionDiscovery { * Watches for added/removed local extensions. * Dependencies are installed automatically after an extension folder is copied. */ - async watchExtensions(): Promise { + watchExtensions() { this.dependencies.logger.info(`${logModule} watching extension add/remove in ${this.localFolderPath}`); - // Wait until .load() has been called and has been resolved - await this.whenLoaded; - this.dependencies.watch(this.localFolderPath, { // For adding and removing symlinks to work, the depth has to be 1. depth: 1, @@ -224,7 +220,7 @@ export class ExtensionDiscovery { // Install dependencies for the new extension await this.dependencies.installExtension(extension.absolutePath); - this.extensions.set(extension.id, extension); + this.dependencies.installedExtensions.set(extension.id, extension); this.dependencies.logger.info(`${logModule} Added extension ${extension.manifest.name}`); this.events.emit("add", extension); } @@ -251,28 +247,23 @@ export class ExtensionDiscovery { return; } - for (const extension of this.extensions.values()) { - if (extension.absolutePath !== filePath) { - continue; - } + const extension = iter.find( + this.dependencies.installedExtensions.values(), + (ext) => !ext.isBundled && ext.absolutePath === filePath, + ); - const extensionName = extension.manifest.name; - - // If the extension is deleted manually while the application is running, also remove the symlink - await this.removeSymlinkByPackageName(extensionName); - - // The path to the manifest file is the lens extension id - // Note: that we need to use the symlinked path - const lensExtensionId = extension.manifestPath; - - this.extensions.delete(extension.id); - this.dependencies.logger.info(`${logModule} removed extension ${extensionName}`); - this.events.emit("remove", lensExtensionId); + if (!extension) { + this.dependencies.logger.warn(`${logModule} extension ${extensionFolderName} not found, can't remove`); return; } - this.dependencies.logger.warn(`${logModule} extension ${extensionFolderName} not found, can't remove`); + // If the extension is deleted manually while the application is running, also remove the symlink + await this.removeSymlinkByPackageName(extension.manifest.name); + + this.dependencies.installedExtensions.delete(extension.id); + this.dependencies.logger.info(`${logModule} removed extension ${extension.manifest.name}`); + this.events.emit("remove", extension); }; /** @@ -291,12 +282,16 @@ export class ExtensionDiscovery { * @param extensionId The ID of the extension to uninstall. */ async uninstallExtension(extensionId: LensExtensionId): Promise { - const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtensionById(extensionId); + const extension = this.dependencies.installedExtensions.get(extensionId); if (!extension) { return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId }); } + if (extension.isBundled) { + return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, is bundled`, { id: extensionId }); + } + const { manifest, absolutePath } = extension; this.dependencies.logger.info(`${logModule} Uninstalling ${manifest.name}`); @@ -307,7 +302,7 @@ export class ExtensionDiscovery { await this.dependencies.removePath(absolutePath); } - async load(): Promise> { + async load() { if (this.loadStarted) { // The class is simplified by only supporting .load() to be called once throw new Error("ExtensionDiscovery.load() can be only be called once"); @@ -323,11 +318,10 @@ export class ExtensionDiscovery { await this.dependencies.ensureDirectory(this.nodeModulesPath); await this.dependencies.ensureDirectory(this.localFolderPath); - const extensions = await this.ensureExtensions(); + const userExtensions = await this.loadFromFolder(this.localFolderPath); + this.dependencies.installedExtensions.replace(userExtensions.map(ext => [ext.id, ext])); this.isLoaded = true; - - return extensions; } /** @@ -385,12 +379,6 @@ export class ExtensionDiscovery { } } - async ensureExtensions(): Promise> { - const userExtensions = await this.loadFromFolder(this.localFolderPath); - - return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension])); - } - async loadFromFolder(folderPath: string): Promise { const extensions: ExternalInstalledExtension[] = []; const paths = await this.dependencies.readDirectory(folderPath); diff --git a/packages/core/src/extensions/extension-loader/extension-loader.injectable.ts b/packages/core/src/extensions/extension-loader/extension-loader.injectable.ts deleted file mode 100644 index 139300f750..0000000000 --- a/packages/core/src/extensions/extension-loader/extension-loader.injectable.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { ExtensionLoader } from "./extension-loader"; -import updateExtensionsStateInjectable from "./update-extensions-state/update-extensions-state.injectable"; -import extensionInstancesInjectable from "./extension-instances.injectable"; -import type { LensExtension } from "../lens-extension"; -import extensionInjectable from "./extension/extension.injectable"; -import loggerInjectable from "../../common/logger.injectable"; -import joinPathsInjectable from "../../common/path/join-paths.injectable"; -import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; -import { bundledExtensionInjectionToken } from "../extension-discovery/bundled-extension-token"; -import { extensionEntryPointNameInjectionToken } from "./entry-point-name"; - -const extensionLoaderInjectable = getInjectable({ - id: "extension-loader", - - instantiate: (di) => new ExtensionLoader({ - updateExtensionsState: di.inject(updateExtensionsStateInjectable), - extensionInstances: di.inject(extensionInstancesInjectable), - getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance), - bundledExtensions: di.injectMany(bundledExtensionInjectionToken), - extensionEntryPointName: di.inject(extensionEntryPointNameInjectionToken), - logger: di.inject(loggerInjectable), - joinPaths: di.inject(joinPathsInjectable), - getDirnameOfPath: di.inject(getDirnameOfPathInjectable), - }), -}); - -export default extensionLoaderInjectable; diff --git a/packages/core/src/extensions/extension-loader/extension-loader.ts b/packages/core/src/extensions/extension-loader/extension-loader.ts deleted file mode 100644 index 7fd3779fe9..0000000000 --- a/packages/core/src/extensions/extension-loader/extension-loader.ts +++ /dev/null @@ -1,395 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { ipcMain, ipcRenderer } from "electron"; -import { isEqual } from "lodash"; -import type { ObservableMap } from "mobx"; -import { runInAction, action, computed, observable, reaction, when } from "mobx"; -import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc"; -import { isDefined, iter, toJS } from "../../common/utils"; -import type { BundledInstalledExtension, ExternalInstalledExtension, InstalledExtension } from "../extension-discovery/extension-discovery"; -import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension"; -import type { LensExtensionState } from "../extensions-store/extensions-store"; -import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling"; -import { requestExtensionLoaderInitialState } from "../../renderer/ipc"; -import assert from "assert"; -import { EventEmitter } from "../../common/event-emitter"; -import type { Extension } from "./extension/extension.injectable"; -import type { Logger } from "../../common/logger"; -import type { JoinPaths } from "../../common/path/join-paths.injectable"; -import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; -import type { BundledExtension } from "../extension-discovery/bundled-extension-token"; - -const logModule = "[EXTENSIONS-LOADER]"; - -interface Dependencies { - readonly extensionInstances: ObservableMap; - readonly bundledExtensions: BundledExtension[]; - readonly logger: Logger; - readonly extensionEntryPointName: "main" | "renderer"; - updateExtensionsState: (extensionsState: Record) => void; - getExtension: (instance: LensExtension) => Extension; - joinPaths: JoinPaths; - getDirnameOfPath: GetDirnameOfPath; -} - -interface ExtensionBeingActivated { - instance: LensExtension; - installedExtension: InstalledExtension; - activated: Promise; -} - -export interface ExtensionLoading { - isBundled: boolean; - loaded: Promise; -} - -/** - * Loads installed extensions to the Lens application - */ -export class ExtensionLoader { - protected readonly extensions = observable.map(); - - /** - * This is the set of extensions that don't come with either - * - Main.LensExtension when running in the main process - * - Renderer.LensExtension when running in the renderer process - */ - protected readonly nonInstancesByName = observable.set(); - - protected readonly instancesByName = computed(() => new Map(( - iter.chain(this.dependencies.extensionInstances.entries()) - .map(([, instance]) => [instance.name, instance]) - ))); - - private readonly onRemoveExtensionId = new EventEmitter<[string]>(); - - readonly isLoaded = observable.box(false); - - constructor(protected readonly dependencies: Dependencies) {} - - readonly userExtensions = computed(() => new Map(( - this.extensions.toJSON() - .filter(([, extension]) => !extension.isBundled) - ))); - - /** - * Get the extension instance by its manifest name - * @param name The name of the extension - * @returns one of the following: - * - the instance of `Main.LensExtension` on the main process if created - * - the instance of `Renderer.LensExtension` on the renderer process if created - * - `null` if no class definition is provided for the current process - * - `undefined` if the name is not known about - */ - getInstanceByName(name: string): LensExtension | null | undefined { - if (this.nonInstancesByName.has(name)) { - return null; - } - - return this.instancesByName.get().get(name); - } - - readonly storeState = computed(() => Object.fromEntries(( - iter.chain(this.userExtensions.get().entries()) - .map(([extId, extension]) => [ - extId, - { - enabled: extension.isEnabled, - name: extension.manifest.name, - }, - ]) - ))); - - async init() { - if (ipcMain) { - await this.initMain(); - } else { - await this.initRenderer(); - } - - await when(() => this.isLoaded.get()); - - // broadcasting extensions between main/renderer processes - reaction(() => this.toJSON(), () => this.broadcastExtensions(), { - fireImmediately: true, - }); - - reaction( - () => this.storeState.get(), - (state) => { - this.dependencies.updateExtensionsState(state); - }, - ); - } - - initExtensions(extensions: Map) { - this.extensions.replace(extensions); - } - - addExtension(extension: InstalledExtension) { - this.extensions.set(extension.id, extension); - } - - @action - removeInstance(lensExtensionId: LensExtensionId) { - this.dependencies.logger.info(`${logModule} deleting extension instance ${lensExtensionId}`); - const instance = this.dependencies.extensionInstances.get(lensExtensionId); - - if (!instance) { - return; - } - - try { - instance.disable(); - - const extension = this.dependencies.getExtension(instance); - - extension.deregister(); - - this.onRemoveExtensionId.emit(instance.id); - this.dependencies.extensionInstances.delete(lensExtensionId); - this.nonInstancesByName.delete(instance.name); - } catch (error) { - this.dependencies.logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); - } - } - - removeExtension(lensExtensionId: LensExtensionId) { - this.removeInstance(lensExtensionId); - - if (!this.extensions.delete(lensExtensionId)) { - throw new Error(`Can't remove extension ${lensExtensionId}, doesn't exist.`); - } - } - - setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) { - const extension = this.extensions.get(lensExtensionId); - - assert(extension, `Must register extension ${lensExtensionId} with before enabling it`); - assert(!extension.isBundled, `Cannot change the enabled state of a bundled extension`); - - extension.isEnabled = isEnabled; - } - - protected async initMain() { - runInAction(() => { - this.isLoaded.set(true); - }); - - await this.autoInitExtensions(); - - ipcMainHandle(extensionLoaderFromMainChannel, () => [...this.toJSON()]); - - ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { - this.syncExtensions(extensions); - }); - } - - protected async initRenderer() { - const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => { - runInAction(() => { - this.isLoaded.set(true); - }); - this.syncExtensions(extensions); - - const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId); - - // Remove deleted extensions in renderer side only - this.extensions.forEach((_, lensExtensionId) => { - if (!receivedExtensionIds.includes(lensExtensionId)) { - this.removeExtension(lensExtensionId); - } - }); - }; - - requestExtensionLoaderInitialState().then(extensionListHandler); - ipcRendererOn(extensionLoaderFromMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { - extensionListHandler(extensions); - }); - } - - broadcastExtensions() { - const channel = ipcRenderer - ? extensionLoaderFromRendererChannel - : extensionLoaderFromMainChannel; - - broadcastMessage(channel, Array.from(this.extensions)); - } - - syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) { - extensions.forEach(([lensExtensionId, extension]) => { - if (!isEqual(this.extensions.get(lensExtensionId), extension)) { - this.extensions.set(lensExtensionId, extension); - } - }); - } - - protected async loadBundledExtensions() { - const bundledExtensions = await Promise.all((this.dependencies.bundledExtensions - .map(async extension => { - try { - const LensExtensionClass = await extension[this.dependencies.extensionEntryPointName](); - - if (!LensExtensionClass) { - return null; - } - - const installedExtension: BundledInstalledExtension = { - absolutePath: "irrelevant", - id: extension.manifest.name, - isBundled: true, - isCompatible: true, - isEnabled: true, - manifest: extension.manifest, - manifestPath: "irrelevant", - }; - const instance = new LensExtensionClass(installedExtension); - - this.dependencies.extensionInstances.set(extension.manifest.name, instance); - - return { - instance, - installedExtension, - activated: instance.activate(), - } as ExtensionBeingActivated; - } catch (err) { - this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err }); - - return null; - } - }) - )); - - return bundledExtensions.filter(isDefined); - } - - protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise { - // We first need to wait until each extension's `onActivate` is resolved or rejected, - // as this might register new catalog categories. Afterwards we can safely .enable the extension. - await Promise.all( - extensions.map(extension => - // If extension activation fails, log error - extension.activated.catch((error) => { - this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error }); - }), - ), - ); - - extensions.forEach(({ instance }) => { - const extension = this.dependencies.getExtension(instance); - - extension.register(); - }); - - return extensions.map(extension => { - const loaded = extension.instance.enable().catch((err) => { - this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err }); - }); - - return { - isBundled: extension.installedExtension.isBundled, - loaded, - }; - }); - } - - protected async loadUserExtensions(installedExtensions: Map) { - // Steps of the function: - // 1. require and call .activate for each Extension - // 2. Wait until every extension's onActivate has been resolved - // 3. Call .enable for each extension - // 4. Return ExtensionLoading[] - - return [...installedExtensions.entries()] - .filter((entry): entry is [string, ExternalInstalledExtension] => !entry[1].isBundled) - .map(([extId, installedExtension]) => { - const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(installedExtension.manifest.name); - - if (installedExtension.isCompatible && installedExtension.isEnabled && !alreadyInit) { - try { - const LensExtensionClass = this.requireExtension(installedExtension); - - if (!LensExtensionClass) { - this.nonInstancesByName.add(installedExtension.manifest.name); - - return null; - } - - const instance = new LensExtensionClass(installedExtension); - - this.dependencies.extensionInstances.set(extId, instance); - - return { - instance, - installedExtension, - activated: instance.activate(), - } as ExtensionBeingActivated; - } catch (err) { - this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: installedExtension, err }); - } - } else if (!installedExtension.isEnabled && alreadyInit) { - this.removeInstance(extId); - } - - return null; - }) - .filter(isDefined); - } - - async autoInitExtensions() { - this.dependencies.logger.info(`${logModule}: auto initializing extensions`); - - const bundledExtensions = await this.loadBundledExtensions(); - const userExtensions = await this.loadUserExtensions(this.toJSON()); - const loadedExtensions = await this.loadExtensions([ - ...bundledExtensions, - ...userExtensions, - ]); - - // Setup reaction to load extensions on JSON changes - reaction(() => this.toJSON(), installedExtensions => { - void (async () => { - const userExtensions = await this.loadUserExtensions(installedExtensions); - - await this.loadExtensions(userExtensions); - })(); - }); - - return loadedExtensions; - } - - protected requireExtension(extension: ExternalInstalledExtension): LensExtensionConstructor | null { - const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName]; - - if (!extRelativePath) { - return null; - } - - const extAbsolutePath = this.dependencies.joinPaths(this.dependencies.getDirnameOfPath(extension.manifestPath), extRelativePath); - - try { - return require(/* webpackIgnore: true */ extAbsolutePath).default; - } catch (error) { - const message = (error instanceof Error ? error.stack : undefined) || error; - - this.dependencies.logger.error(`${logModule}: can't load ${this.dependencies.extensionEntryPointName} for "${extension.manifest.name}": ${message}`, { extension }); - } - - return null; - } - - getExtensionById(extId: LensExtensionId) { - return this.extensions.get(extId); - } - - getInstanceById(extId: LensExtensionId) { - return this.dependencies.extensionInstances.get(extId); - } - - toJSON(): Map { - return toJS(this.extensions); - } -} diff --git a/packages/core/src/extensions/extension-loader/update-extensions-state/update-extensions-state.injectable.ts b/packages/core/src/extensions/extension-loader/update-extensions-state/update-extensions-state.injectable.ts deleted file mode 100644 index 754375dd5a..0000000000 --- a/packages/core/src/extensions/extension-loader/update-extensions-state/update-extensions-state.injectable.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable"; - -const updateExtensionsStateInjectable = getInjectable({ - id: "update-extensions-state", - instantiate: (di) => di.inject(extensionsStoreInjectable).mergeState, -}); - -export default updateExtensionsStateInjectable; diff --git a/packages/core/src/extensions/extensions-store/extensions-store.ts b/packages/core/src/extensions/extensions-store/extensions-store.ts index ce23e98e19..443cf1b9b7 100644 --- a/packages/core/src/extensions/extensions-store/extensions-store.ts +++ b/packages/core/src/extensions/extensions-store/extensions-store.ts @@ -34,7 +34,7 @@ export class ExtensionsStore extends BaseStore { .map(({ name }) => name); } - protected readonly state = observable.map(); + readonly state = observable.map(); isEnabled(extId: LensExtensionId): boolean { // By default false, so that copied extensions are disabled by default. @@ -42,10 +42,6 @@ export class ExtensionsStore extends BaseStore { return this.state.get(extId)?.enabled ?? false; } - mergeState = action((extensionsState: Record | [LensExtensionId, LensExtensionState][]) => { - this.state.merge(extensionsState); - }); - @action protected fromStore({ extensions }: LensExtensionsStoreModel) { this.state.merge(extensions); diff --git a/packages/core/src/extensions/lens-extension.ts b/packages/core/src/extensions/lens-extension.ts index d93f4a23e7..2ad8c164b2 100644 --- a/packages/core/src/extensions/lens-extension.ts +++ b/packages/core/src/extensions/lens-extension.ts @@ -4,12 +4,13 @@ */ import type { BundledInstalledExtension, ExternalInstalledExtension, InstalledExtension } from "./extension-discovery/extension-discovery"; -import { action, computed, makeObservable, observable } from "mobx"; +import { observable } from "mobx"; import { disposer } from "../common/utils"; import type { ProtocolHandlerRegistration } from "../common/protocol-handler/registration"; import type { PackageJson } from "type-fest"; import type { FileSystemProvisionerStore } from "./extension-loader/file-system-provisioner-store/file-system-provisioner-store"; import type { Logger } from "../common/logger"; +import assert from "assert"; export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (ext: ExternalInstalledExtension) => LensExtension; @@ -19,6 +20,10 @@ export interface BundledLensExtensionManifest extends PackageJson { name: string; version: string; publishConfig?: Partial>; + + // Specify extension name used for persisting data. + // Useful if extension is renamed but the data should not be lost. + storeName?: string; } export interface LensExtensionDependencies { @@ -37,35 +42,39 @@ export interface LensExtensionManifest extends BundledLensExtensionManifest { lens: string; // "semver"-package format [x: string]: string | undefined; }; - - // Specify extension name used for persisting data. - // Useful if extension is renamed but the data should not be lost. - storeName?: string; } export const Disposers = Symbol("disposers"); export class LensExtension { - readonly id: LensExtensionId; - readonly manifest: LensExtensionManifest; - readonly manifestPath: string; - readonly isBundled: boolean; + get id() { + return this.extension.id; + } + + get manifest() { + return this.extension.manifest as LensExtensionManifest; + } + + get manifestPath() { + assert(!this.extension.isBundled, "LensExtension.manifestPath doesn't exist for bundled extensions"); + + return this.extension.manifestPath; + } + + get isBundled() { + return this.extension.isBundled; + } get sanitizedExtensionId() { return sanitizeExtensionName(this.name); } - /** - * @ignore - */ - protected readonly dependencies: LensExtensionDependencies; - protocolHandlers: ProtocolHandlerRegistration[] = []; - @observable private _isEnabled = false; + private readonly _isEnabled = observable.box(false); - @computed get isEnabled() { - return this._isEnabled; + get isEnabled() { + return this._isEnabled.get(); } /** @@ -73,13 +82,13 @@ export class LensExtension { */ [Disposers] = disposer(); - constructor(deps: LensExtensionDependencies, { id, manifest, manifestPath, isBundled }: InstalledExtension) { + /** + * @ignore + */ + declare protected readonly dependencies: LensExtensionDependencies; + + constructor(deps: LensExtensionDependencies, private readonly extension: InstalledExtension) { this.dependencies = deps; - this.id = id; - this.manifest = manifest as LensExtensionManifest; - this.manifestPath = manifestPath; - this.isBundled = isBundled; - makeObservable(this); } get name() { @@ -111,23 +120,21 @@ export class LensExtension { return this.dependencies.fileSystemProvisionerStore.requestDirectory(this.storeName); } - @action async enable() { - if (this._isEnabled) { + if (this._isEnabled.get()) { return; } - this._isEnabled = true; + this._isEnabled.set(true); this.dependencies.logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); } - @action async disable() { - if (!this._isEnabled) { + if (!this._isEnabled.get()) { return; } - this._isEnabled = false; + this._isEnabled.set(false); try { await this.onDeactivate(); diff --git a/packages/core/src/features/extensions/common/get-installed-extension.injectable.ts b/packages/core/src/features/extensions/common/get-installed-extension.injectable.ts new file mode 100644 index 0000000000..5fa8a5d2e6 --- /dev/null +++ b/packages/core/src/features/extensions/common/get-installed-extension.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { InstalledExtension } from "../../../extensions/common-api"; +import type { LensExtensionId } from "../../../extensions/lens-extension"; +import installedExtensionsInjectable from "./installed-extensions.injectable"; + +export type GetInstalledExtension = (id: LensExtensionId) => InstalledExtension | undefined; + +const getInstalledExtensionInjectable = getInjectable({ + id: "get-installed-extension", + instantiate: (di): GetInstalledExtension => { + const installedExtensions = di.inject(installedExtensionsInjectable); + + return (id) => installedExtensions.get(id); + }, +}); + +export default getInstalledExtensionInjectable; diff --git a/packages/core/src/features/extensions/common/installed-extensions.injectable.ts b/packages/core/src/features/extensions/common/installed-extensions.injectable.ts new file mode 100644 index 0000000000..fc25f49c45 --- /dev/null +++ b/packages/core/src/features/extensions/common/installed-extensions.injectable.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import type { InstalledExtension } from "../../../extensions/common-api"; +import type { LensExtensionId } from "../../../extensions/lens-extension"; + +const installedExtensionsInjectable = getInjectable({ + id: "installed-extensions", + instantiate: () => observable.map(), +}); + +export default installedExtensionsInjectable; diff --git a/packages/core/src/features/extensions/common/user-extensions.injectable.ts b/packages/core/src/features/extensions/common/user-extensions.injectable.ts new file mode 100644 index 0000000000..1a5033cf2a --- /dev/null +++ b/packages/core/src/features/extensions/common/user-extensions.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import installedExtensionsInjectable from "./installed-extensions.injectable"; + +const installedUserExtensionsInjectable = getInjectable({ + id: "installed-user-extensions", + instantiate: (di) => { + const installedExtensions = di.inject(installedExtensionsInjectable); + + return computed(() => new Map(( + installedExtensions.toJSON() + .filter(([, ext]) => !ext.isBundled) + ))); + }, +}); + +export default installedUserExtensionsInjectable; diff --git a/packages/core/src/features/extensions/loader/common/auto-init-extensions.injectable.ts b/packages/core/src/features/extensions/loader/common/auto-init-extensions.injectable.ts new file mode 100644 index 0000000000..f7838f0e54 --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/auto-init-extensions.injectable.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { reaction } from "mobx"; +import installedExtensionsInjectable from "../../common/installed-extensions.injectable"; +import type { ExtensionLoading } from "./finalize-extension-loading.injectable"; +import finalizeExtensionLoadingInjectable from "./finalize-extension-loading.injectable"; +import loadBundledExtensionsInjectable from "./load-bundled-extensions.injectable"; +import loadUserExtensionsInjectable from "./load-user-extensions.injectable"; +import extensionLoadingLoggerInjectable from "./logger.injectable"; + +export type AutoInitExtensions = () => Promise; + +const autoInitExtensionsInjectable = getInjectable({ + id: "auto-init-extensions", + instantiate: (di): AutoInitExtensions => { + const installedExtensions = di.inject(installedExtensionsInjectable); + const logger = di.inject(extensionLoadingLoggerInjectable); + const loadBundledExtensions = di.inject(loadBundledExtensionsInjectable); + const loadUserExtensions = di.inject(loadUserExtensionsInjectable); + const finalizeExtensionLoading = di.inject(finalizeExtensionLoadingInjectable); + + return async () => { + logger.info("auto initializing extensions"); + + const bundledExtensions = await loadBundledExtensions(); + const userExtensions = await loadUserExtensions(installedExtensions.toJSON()); + const loadedExtensions = await finalizeExtensionLoading([ + ...bundledExtensions, + ...userExtensions, + ]); + + // Setup reaction to load extensions on JSON changes + reaction(() => installedExtensions.toJSON(), installedExtensions => { + void (async () => { + const userExtensions = await loadUserExtensions(installedExtensions); + + await finalizeExtensionLoading(userExtensions); + })(); + }); + + return loadedExtensions; + }; + }, +}); + +export default autoInitExtensionsInjectable; diff --git a/packages/core/src/features/extensions/loader/common/channels.ts b/packages/core/src/features/extensions/loader/common/channels.ts index 72375f1695..c4182a1256 100644 --- a/packages/core/src/features/extensions/loader/common/channels.ts +++ b/packages/core/src/features/extensions/loader/common/channels.ts @@ -12,12 +12,8 @@ export const loadedExtensionsChannel: RequestChannel = { - id: "add-extension", -}; - -export const removeExtensionChannel: MessageChannel = { - id: "remove-extension", +export const extensionStateUpdatesChannel: MessageChannel<[LensExtensionId, InstalledExtension][]> = { + id: "extensions-updated", }; export const bundledExtensionsLoadedChannel: MessageChannel = { diff --git a/packages/core/src/features/extensions/loader/common/extension-updates-listener.injectable.ts b/packages/core/src/features/extensions/loader/common/extension-updates-listener.injectable.ts new file mode 100644 index 0000000000..7160fceb82 --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/extension-updates-listener.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { isEqual } from "lodash"; +import { runInAction } from "mobx"; +import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import installedExtensionsInjectable from "../../common/installed-extensions.injectable"; +import { extensionStateUpdatesChannel } from "./channels"; + +const installedExtensionUpdatesListenerInjectable = getMessageChannelListenerInjectable({ + channel: extensionStateUpdatesChannel, + id: "main", + handler: (di) => { + const installedExtensions = di.inject(installedExtensionsInjectable); + + return (newState) => runInAction(() => { + for (const [extensionId, installedExtension] of newState) { + const oldInstalled = installedExtensions.get(extensionId); + + if (!oldInstalled || !isEqual(oldInstalled, installedExtension)) { + installedExtensions.set(extensionId, installedExtension); + } + } + }); + }, +}); + +export default installedExtensionUpdatesListenerInjectable; diff --git a/packages/core/src/features/extensions/loader/common/finalize-extension-loading.injectable.ts b/packages/core/src/features/extensions/loader/common/finalize-extension-loading.injectable.ts new file mode 100644 index 0000000000..b9165c47eb --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/finalize-extension-loading.injectable.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import extensionInjectable from "../../../../extensions/extension-loader/extension/extension.injectable"; +import type { LensExtension } from "../../../../extensions/lens-extension"; +import extensionLoadingLoggerInjectable from "./logger.injectable"; + +export interface ExtensionLoading { + isBundled: boolean; + loaded: Promise; +} + +export type FinalizeExtensionLoading = (instances: LensExtension[]) => Promise; + +const finalizeExtensionLoadingInjectable = getInjectable({ + id: "finalize-extension-loading", + instantiate: (di): FinalizeExtensionLoading => { + const logger = di.inject(extensionLoadingLoggerInjectable); + + return async (instances) => { + // We first need to wait until each extension's `onActivate` is resolved or rejected, + // as this might register new catalog categories. Afterwards we can safely .enable the extension. + await Promise.all(( + instances + .map(async instance => { + try { + await instance.activate(); + } catch (error) { + logger.error(`activation extension error`, { extId: instance.id, error }); + } + }) + )); + + for (const extension of instances) { + di.inject(extensionInjectable, extension).register(); + } + + return instances.map(ext => ({ + isBundled: ext.isBundled, + loaded: (async () => { + try { + await ext.enable(); + } catch (err) { + logger.error(`failed to enable`, { ext, err }); + } + })(), + })); + }; + }, +}); + +export default finalizeExtensionLoadingInjectable; diff --git a/packages/core/src/features/extensions/loader/common/find-instance-by-name.injectable.ts b/packages/core/src/features/extensions/loader/common/find-instance-by-name.injectable.ts new file mode 100644 index 0000000000..06c23cd65a --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/find-instance-by-name.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { iter } from "../../../../common/utils"; +import extensionInstancesInjectable from "../../../../extensions/extension-loader/extension-instances.injectable"; +import type { LensExtension } from "../../../../extensions/lens-extension"; +import extensionsWithoutInstancesByNameInjectable from "./non-instances-by-name.injectable"; + +/** + * Tries to find an extension by its name. If found it will be returned. + * + * If the extension is installed but doesn't provide an instance for this environment then + * `"not-this-environment"` will be returned. If the extension isn't installed then `"not-installed"` + * will be returned + */ +export type FindExtensionInstanceByName = (name: string) => LensExtension | "not-this-environment" | "not-installed"; + +const findExtensionInstanceByNameInjectable = getInjectable({ + id: "find-extension-instance-by-name", + instantiate: (di): FindExtensionInstanceByName => { + const extensionsWithoutInstancesByName = di.inject(extensionsWithoutInstancesByNameInjectable); + const extensionInstances = di.inject(extensionInstancesInjectable); + + return (name) => { + if (extensionsWithoutInstancesByName.has(name)) { + return "not-this-environment"; + } + + const instance = iter.find(extensionInstances.values(), instance => instance.name === name); + + if (instance) { + return instance; + } + + return "not-installed"; + }; + }, +}); + +export default findExtensionInstanceByNameInjectable; diff --git a/packages/core/src/features/extensions/loader/common/import-installed-extension.injectable.ts b/packages/core/src/features/extensions/loader/common/import-installed-extension.injectable.ts new file mode 100644 index 0000000000..cec1483cbd --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/import-installed-extension.injectable.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import getDirnameOfPathInjectable from "../../../../common/path/get-dirname.injectable"; +import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; +import type { InstalledExtension } from "../../../../extensions/common-api"; +import { extensionEntryPointNameInjectionToken } from "../../../../extensions/extension-loader/entry-point-name"; +import type { LensExtensionConstructor } from "../../../../extensions/lens-extension"; +import extensionLoadingLoggerInjectable from "./logger.injectable"; + +export type ImportInstalledExtension = (extension: InstalledExtension) => Promise; + +const importInstalledExtensionInjectable = getInjectable({ + id: "import-installed-extension", + instantiate: (di): ImportInstalledExtension => { + const joinPaths = di.inject(joinPathsInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); + const logger = di.inject(extensionLoadingLoggerInjectable); + const extensionEntryPointName = di.inject(extensionEntryPointNameInjectionToken); + + return async (extension) => { + const extRelativePath = extension.manifest[extensionEntryPointName]; + + if (!extRelativePath) { + return null; + } + + const extAbsolutePath = joinPaths(getDirnameOfPath(extension.manifestPath), extRelativePath); + + try { + const LensExtensionClass = (await import(extAbsolutePath)).default; + + if (typeof LensExtensionClass === "function") { + return LensExtensionClass; + } + + logger.error(`the ${extensionEntryPointName} entry point for "${extension.manifest.name}" is invalid`); + } catch (error) { + const message = (error instanceof Error ? error.stack : undefined) || error; + + logger.error(`can't load ${extensionEntryPointName} for "${extension.manifest.name}": ${message}`, { extension }); + } + + return null; + }; + }, +}); + +export default importInstalledExtensionInjectable; diff --git a/packages/core/src/features/extensions/loader/common/load-bundled-extensions.injectable.ts b/packages/core/src/features/extensions/loader/common/load-bundled-extensions.injectable.ts new file mode 100644 index 0000000000..c9cfee0d6c --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/load-bundled-extensions.injectable.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { bundledExtensionInjectionToken } from "../../../../common/library"; +import { isDefined } from "../../../../common/utils"; +import { extensionEntryPointNameInjectionToken } from "../../../../extensions/extension-loader/entry-point-name"; +import extensionInstancesInjectable from "../../../../extensions/extension-loader/extension-instances.injectable"; +import type { LensExtension } from "../../../../extensions/lens-extension"; +import extensionLoadingLoggerInjectable from "./logger.injectable"; + +export type LoadBundledExtensions = () => Promise; + +const loadBundledExtensionsInjectable = getInjectable({ + id: "load-bundled-extensions", + instantiate: (di): LoadBundledExtensions => { + const bundledExtensions = di.injectMany(bundledExtensionInjectionToken); + const extensionEntryPointName = di.inject(extensionEntryPointNameInjectionToken); + const extensionInstances = di.inject(extensionInstancesInjectable); + const logger = di.inject(extensionLoadingLoggerInjectable); + + return async () => ( + (await Promise.all(bundledExtensions + .map(async extension => { + try { + const LensExtensionClass = await extension[extensionEntryPointName](); + + if (!LensExtensionClass) { + return null; + } + + const instance = new LensExtensionClass({ + id: extension.manifest.name, + isBundled: true, + isCompatible: true, + isEnabled: true, + manifest: extension.manifest, + }); + + extensionInstances.set(extension.manifest.name, instance); + + return instance; + } catch (err) { + logger.error(`error loading extension`, { ext: extension, err }); + + return null; + } + }))) + .filter(isDefined) + ); + }, +}); + +export default loadBundledExtensionsInjectable; diff --git a/packages/core/src/features/extensions/loader/common/load-user-extensions.injectable.ts b/packages/core/src/features/extensions/loader/common/load-user-extensions.injectable.ts new file mode 100644 index 0000000000..b552a0ac47 --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/load-user-extensions.injectable.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { isDefined } from "../../../../common/utils"; +import type { ExternalInstalledExtension } from "../../../../extensions/extension-discovery/extension-discovery"; +import extensionInstancesInjectable from "../../../../extensions/extension-loader/extension-instances.injectable"; +import type { LensExtension, LensExtensionId } from "../../../../extensions/lens-extension"; +import importInstalledExtensionInjectable from "./import-installed-extension.injectable"; +import extensionLoadingLoggerInjectable from "./logger.injectable"; +import extensionsWithoutInstancesByNameInjectable from "./non-instances-by-name.injectable"; +import removeExtensionInstanceInjectable from "./remove-instance.injectable"; + +export type LoadUserExtensions = (installedExtensions: [LensExtensionId, ExternalInstalledExtension][]) => Promise; + +const loadUserExtensionsInjectable = getInjectable({ + id: "load-user-extensions", + instantiate: (di): LoadUserExtensions => { + const importInstalledExtension = di.inject(importInstalledExtensionInjectable); + const removeExtensionInstance = di.inject(removeExtensionInstanceInjectable); + const extensionsWithoutInstancesByName = di.inject(extensionsWithoutInstancesByNameInjectable); + const extensionInstances = di.inject(extensionInstancesInjectable); + const logger = di.inject(extensionLoadingLoggerInjectable); + + return async (installedExtensions) => { + const instances = await Promise.all(( + installedExtensions + .map(async ([extId, extension]) => { + const alreadyInit = extensionInstances.has(extId) || extensionsWithoutInstancesByName.has(extension.manifest.name); + + if (extension.isCompatible && extension.isEnabled && !alreadyInit) { + try { + const LensExtensionClass = await importInstalledExtension(extension); + + if (!LensExtensionClass) { + extensionsWithoutInstancesByName.add(extension.manifest.name); + + return null; + } + + const instance = new LensExtensionClass(extension); + + extensionInstances.set(extId, instance); + + return instance; + } catch (err) { + logger.error(`error loading extension`, { ext: extension, err }); + } + } else if (!extension.isEnabled && alreadyInit) { + removeExtensionInstance(extId); + } + + return null; + }) + )); + + return instances.filter(isDefined); + }; + }, +}); + +export default loadUserExtensionsInjectable; diff --git a/packages/core/src/features/extensions/loader/common/logger.injectable.ts b/packages/core/src/features/extensions/loader/common/logger.injectable.ts new file mode 100644 index 0000000000..595f73cac4 --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/logger.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import prefixedLoggerInjectable from "../../../../common/logger/prefixed-logger.injectable"; + +const extensionLoadingLoggerInjectable = getInjectable({ + id: "extension-loading-logger", + instantiate: (di) => di.inject(prefixedLoggerInjectable, "EXTENSION-LOADER"), +}); + +export default extensionLoadingLoggerInjectable; diff --git a/packages/core/src/features/extensions/loader/common/non-instances-by-name.injectable.ts b/packages/core/src/features/extensions/loader/common/non-instances-by-name.injectable.ts new file mode 100644 index 0000000000..a94fc9325d --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/non-instances-by-name.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; + +const extensionsWithoutInstancesByNameInjectable = getInjectable({ + id: "extensions-without-instances-by-name", + instantiate: () => observable.set(), +}); + +export default extensionsWithoutInstancesByNameInjectable; diff --git a/packages/core/src/features/extensions/loader/common/remove-instance.injectable.ts b/packages/core/src/features/extensions/loader/common/remove-instance.injectable.ts new file mode 100644 index 0000000000..c06b98b508 --- /dev/null +++ b/packages/core/src/features/extensions/loader/common/remove-instance.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { action } from "mobx"; +import extensionInstancesInjectable from "../../../../extensions/extension-loader/extension-instances.injectable"; +import extensionInjectable from "../../../../extensions/extension-loader/extension/extension.injectable"; +import type { LensExtensionId } from "../../../../extensions/lens-extension"; +import extensionLoadingLoggerInjectable from "./logger.injectable"; +import extensionsWithoutInstancesByNameInjectable from "./non-instances-by-name.injectable"; + +export type RemoveExtensionInstance = (id: LensExtensionId) => void; + +const removeExtensionInstanceInjectable = getInjectable({ + id: "remove-extension-instance", + instantiate: (di): RemoveExtensionInstance => { + const logger = di.inject(extensionLoadingLoggerInjectable); + const extensionInstances = di.inject(extensionInstancesInjectable); + const extensionsWithoutInstancesByName = di.inject(extensionsWithoutInstancesByNameInjectable); + + return action((id) => { + logger.info(`deleting extension instance ${id}`); + const instance = extensionInstances.get(id); + + if (!instance) { + return; + } + + try { + instance.disable(); + di.inject(extensionInjectable, instance).deregister(); + extensionInstances.delete(id); + extensionsWithoutInstancesByName.delete(instance.name); + } catch (error) { + logger.error(`deactivation extension error`, { id, error }); + } + }); + }, +}); + +export default removeExtensionInstanceInjectable; diff --git a/packages/core/src/features/extensions/loader/main/handle-loaded-extensions.injectable.ts b/packages/core/src/features/extensions/loader/main/handle-loaded-extensions.injectable.ts new file mode 100644 index 0000000000..ccfcb888de --- /dev/null +++ b/packages/core/src/features/extensions/loader/main/handle-loaded-extensions.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import installedExtensionsInjectable from "../../common/installed-extensions.injectable"; +import { loadedExtensionsChannel } from "../common/channels"; + +const handleLoadedExtensionRequestsInjectable = getRequestChannelListenerInjectable({ + channel: loadedExtensionsChannel, + handler: (di) => { + const installedExtensions = di.inject(installedExtensionsInjectable); + + return () => installedExtensions.toJSON(); + }, +}); + +export default handleLoadedExtensionRequestsInjectable; diff --git a/packages/core/src/features/extensions/loader/main/setup-extensions-broadcasting.injectable.ts b/packages/core/src/features/extensions/loader/main/setup-extensions-broadcasting.injectable.ts new file mode 100644 index 0000000000..1ec8a79ebf --- /dev/null +++ b/packages/core/src/features/extensions/loader/main/setup-extensions-broadcasting.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { autorun } from "mobx"; +import { sendMessageToChannelInjectionToken } from "../../../../common/utils/channel/message-to-channel-injection-token"; +import { onLoadOfApplicationInjectionToken } from "../../../../main/library"; +import installedExtensionsInjectable from "../../common/installed-extensions.injectable"; +import { extensionStateUpdatesChannel } from "../common/channels"; + +const setupInstalledExtensionsBroadcastingInjectable = getInjectable({ + id: "setup-installed-extensions-broadcasting", + instantiate: (di) => ({ + id: "setup-installed-extensions-broadcasting", + run: () => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + const installedExtensions = di.inject(installedExtensionsInjectable); + + autorun(() => sendMessageToChannel(extensionStateUpdatesChannel, installedExtensions.toJSON())); + }, + }), + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupInstalledExtensionsBroadcastingInjectable; diff --git a/packages/core/src/features/extensions/loader/main/sync-enabled-states-with-store.injectable.ts b/packages/core/src/features/extensions/loader/main/sync-enabled-states-with-store.injectable.ts new file mode 100644 index 0000000000..6eac79ff79 --- /dev/null +++ b/packages/core/src/features/extensions/loader/main/sync-enabled-states-with-store.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { autorun } from "mobx"; +import extensionsStoreInjectable from "../../../../extensions/extensions-store/extensions-store.injectable"; +import { onLoadOfApplicationInjectionToken } from "../../../../main/library"; +import installedUserExtensionsInjectable from "../../common/user-extensions.injectable"; + +const syncExtensionEnabledStateWithStoreInjectable = getInjectable({ + id: "sync-extension-enabled-state-with-store", + instantiate: (di) => ({ + id: "sync-extension-enabled-state-with-store", + run: () => { + const extensionsStore = di.inject(extensionsStoreInjectable); + const installedUserExtensions = di.inject(installedUserExtensionsInjectable); + + autorun(() => { + extensionsStore.state.merge(( + [...installedUserExtensions.get().entries()] + .map(([extId, extension]) => [extId, { + enabled: extension.isEnabled, + name: extension.manifest.name, + }] as const) + )); + }); + }, + }), + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default syncExtensionEnabledStateWithStoreInjectable; diff --git a/packages/core/src/features/extensions/loader/renderer/initialize-installed-extensions.injectable.ts b/packages/core/src/features/extensions/loader/renderer/initialize-installed-extensions.injectable.ts new file mode 100644 index 0000000000..5e42051a58 --- /dev/null +++ b/packages/core/src/features/extensions/loader/renderer/initialize-installed-extensions.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { beforeFrameStartsSecondInjectionToken } from "../../../../renderer/before-frame-starts/tokens"; +import installedExtensionsInjectable from "../../common/installed-extensions.injectable"; +import requestLoadedExtensionsInjectable from "./request-loaded-extensions.injectable"; + +const initializeInstalledExtensionsInjectable = getInjectable({ + id: "initialize-installed-extensions", + instantiate: (di) => ({ + id: "initialize-installed-extensions", + run: async () => { + const installedExtensions = di.inject(installedExtensionsInjectable); + const requestLoadedExtensions = di.inject(requestLoadedExtensionsInjectable); + + installedExtensions.replace(await requestLoadedExtensions()); + }, + }), + injectionToken: beforeFrameStartsSecondInjectionToken, +}); + +export default initializeInstalledExtensionsInjectable; diff --git a/packages/core/src/features/extensions/loader/renderer/request-loaded-extensions.injectable.ts b/packages/core/src/features/extensions/loader/renderer/request-loaded-extensions.injectable.ts new file mode 100644 index 0000000000..711b22b2d6 --- /dev/null +++ b/packages/core/src/features/extensions/loader/renderer/request-loaded-extensions.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import { loadedExtensionsChannel } from "../common/channels"; + +const requestLoadedExtensionsInjectable = getInjectable({ + id: "request-loaded-extensions", + instantiate: (di) => { + const requestFromChannel = di.inject(requestFromChannelInjectable); + + return () => requestFromChannel(loadedExtensionsChannel); + }, +}); + +export default requestLoadedExtensionsInjectable; diff --git a/packages/core/src/features/extensions/loader/renderer/setup-extensions-broadcasting.injectable.ts b/packages/core/src/features/extensions/loader/renderer/setup-extensions-broadcasting.injectable.ts new file mode 100644 index 0000000000..5eee2ee3ae --- /dev/null +++ b/packages/core/src/features/extensions/loader/renderer/setup-extensions-broadcasting.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { autorun } from "mobx"; +import { sendMessageToChannelInjectionToken } from "../../../../common/utils/channel/message-to-channel-injection-token"; +import { beforeFrameStartsFirstInjectionToken } from "../../../../renderer/before-frame-starts/tokens"; +import installedExtensionsInjectable from "../../common/installed-extensions.injectable"; +import { extensionStateUpdatesChannel } from "../common/channels"; + +const setupInstalledExtensionsBroadcastingInjectable = getInjectable({ + id: "setup-installed-extensions-broadcasting", + instantiate: (di) => ({ + id: "setup-installed-extensions-broadcasting", + run: () => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + const installedExtensions = di.inject(installedExtensionsInjectable); + + autorun(() => sendMessageToChannel(extensionStateUpdatesChannel, installedExtensions.toJSON())); + }, + }), + injectionToken: beforeFrameStartsFirstInjectionToken, +}); + +export default setupInstalledExtensionsBroadcastingInjectable; diff --git a/packages/core/src/features/extensions/navigate/renderer/listener.injectable.ts b/packages/core/src/features/extensions/navigate/renderer/listener.injectable.ts index fccf0efca6..002cf31366 100644 --- a/packages/core/src/features/extensions/navigate/renderer/listener.injectable.ts +++ b/packages/core/src/features/extensions/navigate/renderer/listener.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; -import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; +import extensionInstancesInjectable from "../../../../extensions/extension-loader/extension-instances.injectable"; import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; import { navigateForExtensionChannel } from "../common/channel"; @@ -11,13 +11,13 @@ const navigateForExtensionListenerInjectable = getMessageChannelListenerInjectab channel: navigateForExtensionChannel, id: "main", handler: (di) => { - const extensionLoader = di.inject(extensionLoaderInjectable); + const extensionInstances = di.inject(extensionInstancesInjectable); return ({ extId, pageId, params }) => { - const extension = extensionLoader.getInstanceById(extId) as LensRendererExtension | undefined; + const extension = extensionInstances.get(extId); if (extension) { - extension.navigate(pageId, params); + (extension as LensRendererExtension).navigate(pageId, params); } }; }, diff --git a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts index bce91119c2..d12626d92f 100644 --- a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts +++ b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts @@ -3,24 +3,23 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } 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"; import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable"; import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; import loggerInjectable from "../../../common/logger.injectable"; +import findExtensionInstanceByNameInjectable from "../../../features/extensions/loader/common/find-instance-by-name.injectable"; const lensProtocolRouterMainInjectable = getInjectable({ id: "lens-protocol-router-main", - instantiate: (di) => - new LensProtocolRouterMain({ - extensionLoader: di.inject(extensionLoaderInjectable), - extensionsStore: di.inject(extensionsStoreInjectable), - showApplicationWindow: di.inject(showApplicationWindowInjectable), - broadcastMessage: di.inject(broadcastMessageInjectable), - logger: di.inject(loggerInjectable), - }), + instantiate: (di) => new LensProtocolRouterMain({ + extensionsStore: di.inject(extensionsStoreInjectable), + showApplicationWindow: di.inject(showApplicationWindowInjectable), + broadcastMessage: di.inject(broadcastMessageInjectable), + logger: di.inject(loggerInjectable), + findExtensionInstanceByName: di.inject(findExtensionInstanceByNameInjectable), + }), }); export default lensProtocolRouterMainInjectable; diff --git a/packages/core/src/main/start-main-application/runnables/initialize-extensions.injectable.ts b/packages/core/src/main/start-main-application/runnables/initialize-extensions.injectable.ts index 8765721d90..cced65b0cb 100644 --- a/packages/core/src/main/start-main-application/runnables/initialize-extensions.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/initialize-extensions.injectable.ts @@ -3,11 +3,10 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery"; -import type { LensExtensionId } from "../../../extensions/lens-extension"; import loggerInjectable from "../../../common/logger.injectable"; import extensionDiscoveryInjectable from "../../../extensions/extension-discovery/extension-discovery.injectable"; -import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; +import autoInitExtensionsInjectable from "../../../features/extensions/loader/common/auto-init-extensions.injectable"; +import removeExtensionInstanceInjectable from "../../../features/extensions/loader/common/remove-instance.injectable"; import showErrorPopupInjectable from "../../electron-app/features/show-error-popup.injectable"; import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; @@ -17,8 +16,9 @@ const initializeExtensionsInjectable = getInjectable({ instantiate: (di) => { const logger = di.inject(loggerInjectable); const extensionDiscovery = di.inject(extensionDiscoveryInjectable); - const extensionLoader = di.inject(extensionLoaderInjectable); const showErrorPopup = di.inject(showErrorPopupInjectable); + const removeExtensionInstance = di.inject(removeExtensionInstanceInjectable); + const autoInitExtensions = di.inject(autoInitExtensionsInjectable); return { id: "initialize-extensions", @@ -27,30 +27,18 @@ const initializeExtensionsInjectable = getInjectable({ await extensionDiscovery.init(); - await extensionLoader.init(); + await autoInitExtensions(); try { - const extensions = await extensionDiscovery.load(); + await extensionDiscovery.load(); + extensionDiscovery.events.on("remove", (ext) => removeExtensionInstance(ext.id)); // Start watching after bundled extensions are loaded extensionDiscovery.watchExtensions(); - - // Subscribe to extensions that are copied or deleted to/from the extensions folder - extensionDiscovery.events - .on("add", (extension: InstalledExtension) => { - extensionLoader.addExtension(extension); - }) - .on("remove", (lensExtensionId: LensExtensionId) => { - extensionLoader.removeExtension(lensExtensionId); - }); - - extensionLoader.initExtensions(extensions); } catch (error: any) { showErrorPopup( "Lens Error", - `Could not load extensions${ - error?.message ? `: ${error.message}` : "" - }`, + `Could not load extensions${error?.message ? `: ${error.message}` : ""}`, ); console.error(error); diff --git a/packages/core/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/packages/core/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 751c1a38cc..c185c5d3fd 100644 --- a/packages/core/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/packages/core/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -6,12 +6,10 @@ import "@testing-library/jest-dom/extend-expect"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import React from "react"; -import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery"; -import type { ExtensionLoader } from "../../../../extensions/extension-loader"; +import type { ExtensionDiscovery, InstalledExtension } from "../../../../extensions/extension-discovery/extension-discovery"; import { ConfirmDialog } from "../../confirm-dialog"; import { Extensions } from "../extensions"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; -import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import type { DiRender } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor"; import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable"; @@ -22,15 +20,18 @@ import type { InstallExtensionFromInput } from "../install-extension-from-input. import installExtensionFromInputInjectable from "../install-extension-from-input.injectable"; 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 type { ObservableMap } from "mobx"; import { observable, when } from "mobx"; import type { RemovePath } from "../../../../common/fs/remove.injectable"; import removePathInjectable from "../../../../common/fs/remove.injectable"; import type { DownloadBinary } from "../../../../common/fetch/download-binary.injectable"; import downloadBinaryInjectable from "../../../../common/fetch/download-binary.injectable"; import currentlyInClusterFrameInjectable from "../../../routes/currently-in-cluster-frame.injectable"; +import type { LensExtensionId } from "../../../../extensions/lens-extension"; +import installedExtensionsInjectable from "../../../../features/extensions/common/installed-extensions.injectable"; describe("Extensions", () => { - let extensionLoader: ExtensionLoader; + let installedExtensions: ObservableMap; let extensionDiscovery: ExtensionDiscovery; let installExtensionFromInput: jest.MockedFunction; let extensionInstallationStateStore: ExtensionInstallationStateStore; @@ -56,11 +57,11 @@ describe("Extensions", () => { downloadBinary = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); }); di.override(downloadBinaryInjectable, () => downloadBinary); - extensionLoader = di.inject(extensionLoaderInjectable); + installedExtensions = di.inject(installedExtensionsInjectable); extensionDiscovery = di.inject(extensionDiscoveryInjectable); extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); - extensionLoader.addExtension({ + installedExtensions.set("extensionId", { id: "extensionId", manifest: { name: "test", diff --git a/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx index 8d5aec850e..4c59387b74 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx @@ -3,154 +3,126 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import uninstallExtensionInjectable from "../uninstall-extension.injectable"; -import type { UnpackExtension } from "./unpack-extension.injectable"; import unpackExtensionInjectable from "./unpack-extension.injectable"; -import type { GetExtensionDestFolder } from "./get-extension-dest-folder.injectable"; import getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable"; -import type { CreateTempFilesAndValidate } from "./create-temp-files-and-validate.injectable"; import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate.injectable"; import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import type { Disposer } from "../../../../common/utils"; import { disposer } from "../../../../common/utils"; -import type { ShowNotification } from "../../notifications"; import { Button } from "../../button"; -import type { ExtensionLoader } from "../../../../extensions/extension-loader"; -import type { LensExtensionId } from "../../../../extensions/lens-extension"; import React from "react"; import { remove as removeDir } from "fs-extra"; import { shell } from "electron"; -import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; import { ExtensionInstallationState } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; import showInfoNotificationInjectable from "../../notifications/show-info-notification.injectable"; +import getInstalledExtensionInjectable from "../../../../features/extensions/common/get-installed-extension.injectable"; export interface InstallRequest { fileName: string; data: Buffer; } -interface Dependencies { - extensionLoader: ExtensionLoader; - uninstallExtension: (id: LensExtensionId) => Promise; - unpackExtension: UnpackExtension; - createTempFilesAndValidate: CreateTempFilesAndValidate; - getExtensionDestFolder: GetExtensionDestFolder; - installStateStore: ExtensionInstallationStateStore; - showErrorNotification: ShowNotification; - showInfoNotification: ShowNotification; -} - export type AttemptInstall = (request: InstallRequest, cleanup?: Disposer) => Promise; -const attemptInstall = ({ - extensionLoader, - uninstallExtension, - unpackExtension, - createTempFilesAndValidate, - getExtensionDestFolder, - installStateStore, - showErrorNotification, - showInfoNotification, -}: Dependencies): AttemptInstall => - async (request, cleanup) => { - const dispose = disposer( - installStateStore.startPreInstall(), - cleanup, - ); - - const validatedRequest = await createTempFilesAndValidate(request); - - if (!validatedRequest) { - return dispose(); - } - - const { name, version, description } = validatedRequest.manifest; - const curState = installStateStore.getInstallationState(validatedRequest.id); - - if (curState !== ExtensionInstallationState.IDLE) { - dispose(); - - return void showErrorNotification( -
- Extension Install Collision: -

- {"The "} - {name} - {` extension is currently ${curState.toLowerCase()}.`} -

-

Will not proceed with this current install request.

-
, - ); - } - - const extensionFolder = getExtensionDestFolder(name); - const installedExtension = extensionLoader.getExtensionById(validatedRequest.id); - - if (installedExtension) { - const { version: oldVersion } = installedExtension.manifest; - - // confirm to uninstall old version before installing new version - const removeNotification = showInfoNotification( -
-
-

- {"Install extension "} - {`${name}@${version}`} - ? -

-

- {"Description: "} - {description} -

-
shell.openPath(extensionFolder)} - > - Warning: - {` ${name}@${oldVersion} will be removed before installation.`} -
-
-
, - { - onClose: dispose, - }, - ); - } else { - // clean up old data if still around - await removeDir(extensionFolder); - - // install extension if not yet exists - await unpackExtension(validatedRequest, dispose); - } - }; - const attemptInstallInjectable = getInjectable({ id: "attempt-install", - instantiate: (di) => attemptInstall({ - extensionLoader: di.inject(extensionLoaderInjectable), - uninstallExtension: di.inject(uninstallExtensionInjectable), - unpackExtension: di.inject(unpackExtensionInjectable), - createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable), - getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable), - installStateStore: di.inject(extensionInstallationStateStoreInjectable), - showErrorNotification: di.inject(showErrorNotificationInjectable), - showInfoNotification: di.inject(showInfoNotificationInjectable), - }), + instantiate: (di): AttemptInstall => { + const uninstallExtension = di.inject(uninstallExtensionInjectable); + const unpackExtension = di.inject(unpackExtensionInjectable); + const createTempFilesAndValidate = di.inject(createTempFilesAndValidateInjectable); + const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable); + const installStateStore = di.inject(extensionInstallationStateStoreInjectable); + const showErrorNotification = di.inject(showErrorNotificationInjectable); + const showInfoNotification = di.inject(showInfoNotificationInjectable); + const getInstalledExtension = di.inject(getInstalledExtensionInjectable); + + return async (request, cleanup) => { + const dispose = disposer( + installStateStore.startPreInstall(), + cleanup, + ); + + const validatedRequest = await createTempFilesAndValidate(request); + + if (!validatedRequest) { + return dispose(); + } + + const { name, version, description } = validatedRequest.manifest; + const curState = installStateStore.getInstallationState(validatedRequest.id); + + if (curState !== ExtensionInstallationState.IDLE) { + dispose(); + + return void showErrorNotification( +
+ Extension Install Collision: +

+ {"The "} + {name} + {` extension is currently ${curState.toLowerCase()}.`} +

+

Will not proceed with this current install request.

+
, + ); + } + + const extensionFolder = getExtensionDestFolder(name); + const installedExtension = getInstalledExtension(validatedRequest.id); + + if (installedExtension) { + const { version: oldVersion } = installedExtension.manifest; + + // confirm to uninstall old version before installing new version + const removeNotification = showInfoNotification( +
+
+

+ {"Install extension "} + {`${name}@${version}`} + ? +

+

+ {"Description: "} + {description} +

+
shell.openPath(extensionFolder)} + > + Warning: + {` ${name}@${oldVersion} will be removed before installation.`} +
+
+
, + { + onClose: dispose, + }, + ); + } else { + // clean up old data if still around + await removeDir(extensionFolder); + + // install extension if not yet exists + await unpackExtension(validatedRequest, dispose); + } + }; + }, }); export default attemptInstallInjectable; diff --git a/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx index 35bb9ec68f..c479da09d4 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable"; import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import type { Disposer } from "../../../../common/utils"; @@ -19,19 +18,22 @@ import extractTarInjectable from "../../../../common/fs/extract-tar.injectable"; import loggerInjectable from "../../../../common/logger.injectable"; import showInfoNotificationInjectable from "../../notifications/show-info-notification.injectable"; import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; +import installedUserExtensionsInjectable from "../../../../features/extensions/common/user-extensions.injectable"; +import enableExtensionInjectable from "../enable-extension.injectable"; export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise; const unpackExtensionInjectable = getInjectable({ id: "unpack-extension", instantiate: (di): UnpackExtension => { - const extensionLoader = di.inject(extensionLoaderInjectable); const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable); const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); const extractTar = di.inject(extractTarInjectable); const logger = di.inject(loggerInjectable); const showInfoNotification = di.inject(showInfoNotificationInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); + const installedUserExtensions = di.inject(installedUserExtensionsInjectable); + const enableExtension = di.inject(enableExtensionInjectable); return async (request, disposeDownloading) => { const { @@ -73,10 +75,10 @@ const unpackExtensionInjectable = getInjectable({ await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true }); // wait for the loader has actually install it - await when(() => extensionLoader.userExtensions.get().has(id)); + await when(() => installedUserExtensions.get().has(id)); // Enable installed extensions by default. - extensionLoader.setIsEnabled(id, true); + enableExtension(id); showInfoNotification((

diff --git a/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts index 4a07270161..d6f295ea1c 100644 --- a/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts +++ b/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts @@ -3,24 +3,31 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; +import { action } from "mobx"; import type { LensExtensionId } from "../../../extensions/lens-extension"; +import getInstalledExtensionInjectable from "../../../features/extensions/common/get-installed-extension.injectable"; -export type DisableExtension = (extId: LensExtensionId) => void; +export type DisableExtension = (id: LensExtensionId) => void; const disableExtensionInjectable = getInjectable({ id: "disable-extension", instantiate: (di): DisableExtension => { - const extensionLoader = di.inject(extensionLoaderInjectable); + const getInstalledExtension = di.inject(getInstalledExtensionInjectable); - return (extId) => { - const ext = extensionLoader.getExtensionById(extId); + return action((id) => { + const extension = getInstalledExtension(id); - if (ext && !ext.isBundled) { - ext.isEnabled = false; + if (!extension) { + throw new Error(`Missing extension with id="${id}"`); } - }; + + if (extension.isBundled) { + throw new Error("Cannot change the enabled state for bundled extensions"); + } + + extension.isEnabled = false; + }); }, }); diff --git a/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts index 4e68573fae..9fd77a112e 100644 --- a/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts +++ b/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts @@ -3,24 +3,31 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; +import { action } from "mobx"; import type { LensExtensionId } from "../../../extensions/lens-extension"; +import getInstalledExtensionInjectable from "../../../features/extensions/common/get-installed-extension.injectable"; -export type EnableExtension = (extId: LensExtensionId) => void; +export type EnableExtension = (id: LensExtensionId) => void; const enableExtensionInjectable = getInjectable({ id: "enable-extension", instantiate: (di): EnableExtension => { - const extensionLoader = di.inject(extensionLoaderInjectable); + const getInstalledExtension = di.inject(getInstalledExtensionInjectable); - return (extId) => { - const ext = extensionLoader.getExtensionById(extId); + return action((id) => { + const extension = getInstalledExtension(id); - if (ext && !ext.isBundled) { - ext.isEnabled = true; + if (!extension) { + throw new Error(`Missing extension with id="${id}"`); } - }; + + if (extension.isBundled) { + throw new Error("Cannot change the enabled state for bundled extensions"); + } + + extension.isEnabled = true; + }); }, }); diff --git a/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx b/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx index 89821f7317..6342a23168 100644 --- a/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import extensionDiscoveryInjectable from "../../../extensions/extension-discovery/extension-discovery.injectable"; import loggerInjectable from "../../../common/logger.injectable"; @@ -14,20 +13,23 @@ import { when } from "mobx"; import { getMessageFromError } from "./get-message-from-error/get-message-from-error"; import showSuccessNotificationInjectable from "../notifications/show-success-notification.injectable"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; +import installedUserExtensionsInjectable from "../../../features/extensions/common/user-extensions.injectable"; +import getInstalledExtensionInjectable from "../../../features/extensions/common/get-installed-extension.injectable"; const uninstallExtensionInjectable = getInjectable({ id: "uninstall-extension", instantiate: (di) => { - const extensionLoader = di.inject(extensionLoaderInjectable); const extensionDiscovery = di.inject(extensionDiscoveryInjectable); const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); const logger = di.inject(loggerInjectable); const showSuccessNotification = di.inject(showSuccessNotificationInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); + const installedUserExtensions = di.inject(installedUserExtensionsInjectable); + const getInstalledExtension = di.inject(getInstalledExtensionInjectable); return async (extensionId: LensExtensionId): Promise => { - const ext = extensionLoader.getExtensionById(extensionId); + const ext = getInstalledExtension(extensionId); if (!ext) { logger.debug(`[EXTENSIONS]: cannot uninstall ${extensionId}, was not installed`); @@ -45,7 +47,7 @@ const uninstallExtensionInjectable = getInjectable({ await extensionDiscovery.uninstallExtension(extensionId); // wait for the ExtensionLoader to actually uninstall the extension - await when(() => !extensionLoader.userExtensions.get().has(extensionId)); + await when(() => !installedUserExtensions.get().has(extensionId)); showSuccessNotification(

diff --git a/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts b/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts index 9c3d8fc3eb..b5c0559f6f 100644 --- a/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts +++ b/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts @@ -4,15 +4,15 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { computed } from "mobx"; -import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; +import installedUserExtensionsInjectable from "../../../../features/extensions/common/user-extensions.injectable"; const userExtensionsInjectable = getInjectable({ id: "user-extensions", instantiate: (di) => { - const extensionLoader = di.inject(extensionLoaderInjectable); + const installedUserExtensions = di.inject(installedUserExtensionsInjectable); - return computed(() => [...extensionLoader.userExtensions.get().values()]); + return computed(() => [...installedUserExtensions.get().values()]); }, }); diff --git a/packages/core/src/renderer/extension-discovery/init.injectable.ts b/packages/core/src/renderer/extension-discovery/init.injectable.ts index fad1a70a79..e903926bf0 100644 --- a/packages/core/src/renderer/extension-discovery/init.injectable.ts +++ b/packages/core/src/renderer/extension-discovery/init.injectable.ts @@ -5,7 +5,6 @@ import { getInjectable } from "@ogre-tools/injectable"; import extensionDiscoveryInjectable from "../../extensions/extension-discovery/extension-discovery.injectable"; import { beforeFrameStartsSecondInjectionToken } from "../before-frame-starts/tokens"; -import initializeExtensionLoaderInjectable from "../extension-loader/init.injectable"; const initializeExtensionDiscoveryInjectable = getInjectable({ id: "initialize-extension-discovery", @@ -16,7 +15,6 @@ const initializeExtensionDiscoveryInjectable = getInjectable({ await extensionDiscovery.init(); }, - runAfter: di.inject(initializeExtensionLoaderInjectable), }), injectionToken: beforeFrameStartsSecondInjectionToken, }); diff --git a/packages/core/src/renderer/extension-loader/init.injectable.ts b/packages/core/src/renderer/extension-loader/init.injectable.ts deleted file mode 100644 index e00e8a090f..0000000000 --- a/packages/core/src/renderer/extension-loader/init.injectable.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../extensions/extension-loader/extension-loader.injectable"; -import { beforeFrameStartsSecondInjectionToken } from "../before-frame-starts/tokens"; - -const initializeExtensionLoaderInjectable = getInjectable({ - id: "initialize-extension-loader", - instantiate: (di) => ({ - id: "initialize-extension-loader", - run: async () => { - const extensionLoader = di.inject(extensionLoaderInjectable); - - await extensionLoader.init(); - }, - }), - injectionToken: beforeFrameStartsSecondInjectionToken, -}); - -export default initializeExtensionLoaderInjectable; diff --git a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts index c640264ee3..5ed56e4cfa 100644 --- a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts +++ b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts @@ -9,9 +9,9 @@ import frameRoutingIdInjectable from "./frame-routing-id/frame-routing-id.inject import hostedClusterInjectable from "../../../cluster-frame-context/hosted-cluster.injectable"; import assert from "assert"; import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable"; -import loadExtensionsInjectable from "../../load-extensions.injectable"; import loggerInjectable from "../../../../common/logger.injectable"; import showErrorNotificationInjectable from "../../../components/notifications/show-error-notification.injectable"; +import autoInitExtensionsInjectable from "../../../../features/extensions/loader/common/auto-init-extensions.injectable"; const initClusterFrameInjectable = getInjectable({ id: "init-cluster-frame", @@ -23,7 +23,7 @@ const initClusterFrameInjectable = getInjectable({ return initClusterFrame({ hostedCluster, - loadExtensions: di.inject(loadExtensionsInjectable), + loadExtensions: di.inject(autoInitExtensionsInjectable), catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable), frameRoutingId: di.inject(frameRoutingIdInjectable), emitAppEvent: di.inject(emitAppEventInjectable), diff --git a/packages/core/src/renderer/frames/load-extensions.injectable.ts b/packages/core/src/renderer/frames/load-extensions.injectable.ts deleted file mode 100644 index b3973dfe41..0000000000 --- a/packages/core/src/renderer/frames/load-extensions.injectable.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../extensions/extension-loader/extension-loader.injectable"; - -const loadExtensionsInjectable = getInjectable({ - id: "load-extensions", - instantiate: (di) => { - const extensionLoader = di.inject(extensionLoaderInjectable); - - return () => extensionLoader.autoInitExtensions(); - }, -}); - -export default loadExtensionsInjectable; diff --git a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts index 52c8bc1ffb..f457c1641a 100644 --- a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts +++ b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts @@ -7,16 +7,16 @@ import bindProtocolAddRouteHandlersInjectable from "../../protocol-handler/bind- import lensProtocolRouterRendererInjectable from "../../protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable"; import registerIpcListenersInjectable from "../../ipc/register-ipc-listeners.injectable"; -import loadExtensionsInjectable from "../load-extensions.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import { delay } from "../../../common/utils"; import { broadcastMessage } from "../../../common/ipc"; import sendBundledExtensionsLoadedInjectable from "../../../features/extensions/loader/renderer/send-bundled-extensions-loaded.injectable"; +import autoInitExtensionsInjectable from "../../../features/extensions/loader/common/auto-init-extensions.injectable"; const initRootFrameInjectable = getInjectable({ id: "init-root-frame", instantiate: (di) => { - const loadExtensions = di.inject(loadExtensionsInjectable); + const autoInitExtensions = di.inject(autoInitExtensionsInjectable); const registerIpcListeners = di.inject(registerIpcListenersInjectable); const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable); const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable); @@ -31,7 +31,7 @@ const initRootFrameInjectable = getInjectable({ // maximum time to let bundled extensions finish loading const timeout = delay(10000); - const loadingExtensions = await loadExtensions(); + const loadingExtensions = await autoInitExtensions(); const loadingBundledExtensions = loadingExtensions .filter((e) => e.isBundled) diff --git a/packages/core/src/renderer/ipc/index.ts b/packages/core/src/renderer/ipc/index.ts index 4bbf473515..08b3f1e92a 100644 --- a/packages/core/src/renderer/ipc/index.ts +++ b/packages/core/src/renderer/ipc/index.ts @@ -6,9 +6,7 @@ import { clusterActivateHandler, clusterDisconnectHandler, clusterSetFrameIdHandler, clusterStates } from "../../common/ipc/cluster"; import type { ClusterId, ClusterState } from "../../common/cluster-types"; import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel, type WindowAction } from "../../common/ipc/window"; -import { extensionDiscoveryStateChannel, extensionLoaderFromMainChannel } from "../../common/ipc/extension-handling"; -import type { InstalledExtension } from "../../extensions/extension-discovery/extension-discovery"; -import type { LensExtensionId } from "../../extensions/lens-extension"; +import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; import { toJS } from "../utils"; import type { Location } from "history"; import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; @@ -61,7 +59,3 @@ export function requestInitialClusterStates(): Promise<{ id: string; state: Clus export function requestInitialExtensionDiscovery(): Promise<{ isLoaded: boolean }> { return requestMain(extensionDiscoveryStateChannel); } - -export function requestExtensionLoaderInitialState(): Promise<[LensExtensionId, InstalledExtension][]> { - return requestMain(extensionLoaderFromMainChannel); -} diff --git a/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts b/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts index 35938b32fd..5126f210c0 100644 --- a/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts +++ b/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts @@ -3,22 +3,22 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } 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"; import loggerInjectable from "../../../common/logger.injectable"; import showErrorNotificationInjectable from "../../components/notifications/show-error-notification.injectable"; import showShortInfoNotificationInjectable from "../../components/notifications/show-short-info.injectable"; +import findExtensionInstanceByNameInjectable from "../../../features/extensions/loader/common/find-instance-by-name.injectable"; const lensProtocolRouterRendererInjectable = getInjectable({ id: "lens-protocol-router-renderer", instantiate: (di) => new LensProtocolRouterRenderer({ - extensionLoader: di.inject(extensionLoaderInjectable), extensionsStore: di.inject(extensionsStoreInjectable), logger: di.inject(loggerInjectable), showErrorNotification: di.inject(showErrorNotificationInjectable), showShortInfoNotification: di.inject(showShortInfoNotificationInjectable), + findExtensionInstanceByName: di.inject(findExtensionInstanceByNameInjectable), }), });