1
0
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:
Sebastian Malton 2023-02-16 13:21:15 -05:00
parent 90c37143bd
commit 7f86a89cc2
47 changed files with 867 additions and 956 deletions

View File

@ -4,5 +4,3 @@
*/
export const extensionDiscoveryStateChannel = "extension-discovery:state";
export const extensionLoaderFromMainChannel = "extension-loader:main:state";
export const extensionLoaderFromRendererChannel = "extension-loader:renderer:state";

View File

@ -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;
}

View File

@ -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",
},
}),
);
});
});

View File

@ -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),
}),
});

View File

@ -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);

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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> = {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
};
},

View File

@ -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;

View File

@ -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);

View File

@ -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",

View File

@ -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;

View File

@ -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>

View File

@ -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;
});
},
});

View File

@ -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;
});
},
});

View File

@ -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>

View File

@ -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()]);
},
});

View File

@ -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,
});

View File

@ -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;

View File

@ -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),

View File

@ -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;

View File

@ -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)

View File

@ -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);
}

View File

@ -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),
}),
});