mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fully convert ExtensionLoader to be injectable
- To fix unit tests Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
90c37143bd
commit
7f86a89cc2
@ -4,5 +4,3 @@
|
||||
*/
|
||||
|
||||
export const extensionDiscoveryStateChannel = "extension-discovery:state";
|
||||
export const extensionLoaderFromMainChannel = "extension-loader:main:state";
|
||||
export const extensionLoaderFromRendererChannel = "extension-loader:renderer:state";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -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<LensExtensionId, InstalledExtension>;
|
||||
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
|
||||
installExtension: (name: string) => Promise<void>;
|
||||
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<string, InstalledExtension> = 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<void> {
|
||||
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<void> {
|
||||
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<Map<LensExtensionId, InstalledExtension>> {
|
||||
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<Map<LensExtensionId, ExternalInstalledExtension>> {
|
||||
const userExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||
|
||||
return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension]));
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<ExternalInstalledExtension[]> {
|
||||
const extensions: ExternalInstalledExtension[] = [];
|
||||
const paths = await this.dependencies.readDirectory(folderPath);
|
||||
|
||||
@ -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;
|
||||
@ -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<LensExtensionId, LensExtension>;
|
||||
readonly bundledExtensions: BundledExtension[];
|
||||
readonly logger: Logger;
|
||||
readonly extensionEntryPointName: "main" | "renderer";
|
||||
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void;
|
||||
getExtension: (instance: LensExtension) => Extension;
|
||||
joinPaths: JoinPaths;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
}
|
||||
|
||||
interface ExtensionBeingActivated {
|
||||
instance: LensExtension;
|
||||
installedExtension: InstalledExtension;
|
||||
activated: Promise<void>;
|
||||
}
|
||||
|
||||
export interface ExtensionLoading {
|
||||
isBundled: boolean;
|
||||
loaded: Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads installed extensions to the Lens application
|
||||
*/
|
||||
export class ExtensionLoader {
|
||||
protected readonly extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||
|
||||
/**
|
||||
* 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<string>();
|
||||
|
||||
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<LensExtensionId, InstalledExtension>) {
|
||||
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<ExtensionLoading[]> {
|
||||
// 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<string, InstalledExtension>) {
|
||||
// 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<LensExtensionId, InstalledExtension> {
|
||||
return toJS(this.extensions);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -34,7 +34,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
.map(({ name }) => name);
|
||||
}
|
||||
|
||||
protected readonly state = observable.map<LensExtensionId, LensExtensionState>();
|
||||
readonly state = observable.map<LensExtensionId, LensExtensionState>();
|
||||
|
||||
isEnabled(extId: LensExtensionId): boolean {
|
||||
// By default false, so that copied extensions are disabled by default.
|
||||
@ -42,10 +42,6 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
return this.state.get(extId)?.enabled ?? false;
|
||||
}
|
||||
|
||||
mergeState = action((extensionsState: Record<LensExtensionId, LensExtensionState> | [LensExtensionId, LensExtensionState][]) => {
|
||||
this.state.merge(extensionsState);
|
||||
});
|
||||
|
||||
@action
|
||||
protected fromStore({ extensions }: LensExtensionsStoreModel) {
|
||||
this.state.merge(extensions);
|
||||
|
||||
@ -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<Record<string, string>>;
|
||||
|
||||
// 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();
|
||||
|
||||
@ -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;
|
||||
@ -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<LensExtensionId, InstalledExtension>(),
|
||||
});
|
||||
|
||||
export default installedExtensionsInjectable;
|
||||
@ -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;
|
||||
@ -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<ExtensionLoading[]>;
|
||||
|
||||
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;
|
||||
@ -12,12 +12,8 @@ export const loadedExtensionsChannel: RequestChannel<void, [LensExtensionId, Ins
|
||||
id: "loaded-extensions",
|
||||
};
|
||||
|
||||
export const extensionStateUpdateChannel: MessageChannel<[LensExtensionId, InstalledExtension]> = {
|
||||
id: "add-extension",
|
||||
};
|
||||
|
||||
export const removeExtensionChannel: MessageChannel<LensExtensionId> = {
|
||||
id: "remove-extension",
|
||||
export const extensionStateUpdatesChannel: MessageChannel<[LensExtensionId, InstalledExtension][]> = {
|
||||
id: "extensions-updated",
|
||||
};
|
||||
|
||||
export const bundledExtensionsLoadedChannel: MessageChannel<void> = {
|
||||
|
||||
@ -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;
|
||||
@ -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<void>;
|
||||
}
|
||||
|
||||
export type FinalizeExtensionLoading = (instances: LensExtension[]) => Promise<ExtensionLoading[]>;
|
||||
|
||||
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;
|
||||
@ -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;
|
||||
@ -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<LensExtensionConstructor | null>;
|
||||
|
||||
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;
|
||||
@ -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<LensExtension[]>;
|
||||
|
||||
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;
|
||||
@ -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<LensExtension[]>;
|
||||
|
||||
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;
|
||||
@ -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;
|
||||
@ -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<string>(),
|
||||
});
|
||||
|
||||
export default extensionsWithoutInstancesByNameInjectable;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<LensExtensionId, InstalledExtension>;
|
||||
let extensionDiscovery: ExtensionDiscovery;
|
||||
let installExtensionFromInput: jest.MockedFunction<InstallExtensionFromInput>;
|
||||
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",
|
||||
|
||||
@ -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<boolean>;
|
||||
unpackExtension: UnpackExtension;
|
||||
createTempFilesAndValidate: CreateTempFilesAndValidate;
|
||||
getExtensionDestFolder: GetExtensionDestFolder;
|
||||
installStateStore: ExtensionInstallationStateStore;
|
||||
showErrorNotification: ShowNotification;
|
||||
showInfoNotification: ShowNotification;
|
||||
}
|
||||
|
||||
export type AttemptInstall = (request: InstallRequest, cleanup?: Disposer) => Promise<void>;
|
||||
|
||||
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(
|
||||
<div className="flex column gaps">
|
||||
<b>Extension Install Collision:</b>
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{` extension is currently ${curState.toLowerCase()}.`}
|
||||
</p>
|
||||
<p>Will not proceed with this current install request.</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Install extension "}
|
||||
<b>{`${name}@${version}`}</b>
|
||||
?
|
||||
</p>
|
||||
<p>
|
||||
{"Description: "}
|
||||
<em>{description}</em>
|
||||
</p>
|
||||
<div
|
||||
className="remove-folder-warning"
|
||||
onClick={() => shell.openPath(extensionFolder)}
|
||||
>
|
||||
<b>Warning:</b>
|
||||
{` ${name}@${oldVersion} will be removed before installation.`}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
autoFocus
|
||||
label="Install"
|
||||
onClick={async () => {
|
||||
removeNotification();
|
||||
|
||||
if (await uninstallExtension(validatedRequest.id)) {
|
||||
await unpackExtension(validatedRequest, dispose);
|
||||
} else {
|
||||
dispose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
{
|
||||
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(
|
||||
<div className="flex column gaps">
|
||||
<b>Extension Install Collision:</b>
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{` extension is currently ${curState.toLowerCase()}.`}
|
||||
</p>
|
||||
<p>Will not proceed with this current install request.</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Install extension "}
|
||||
<b>{`${name}@${version}`}</b>
|
||||
?
|
||||
</p>
|
||||
<p>
|
||||
{"Description: "}
|
||||
<em>{description}</em>
|
||||
</p>
|
||||
<div
|
||||
className="remove-folder-warning"
|
||||
onClick={() => shell.openPath(extensionFolder)}
|
||||
>
|
||||
<b>Warning:</b>
|
||||
{` ${name}@${oldVersion} will be removed before installation.`}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
autoFocus
|
||||
label="Install"
|
||||
onClick={async () => {
|
||||
removeNotification();
|
||||
|
||||
if (await uninstallExtension(validatedRequest.id)) {
|
||||
await unpackExtension(validatedRequest, dispose);
|
||||
} else {
|
||||
dispose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
{
|
||||
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;
|
||||
|
||||
@ -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<void>;
|
||||
|
||||
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((
|
||||
<p>
|
||||
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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<boolean> => {
|
||||
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(
|
||||
<p>
|
||||
|
||||
@ -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()]);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user