diff --git a/package.json b/package.json index b0b341bd64..8eeef8787d 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,7 @@ "bundledHelmVersion": "3.7.2", "sentryDsn": "", "contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:", - "welcomeRoute": "/welcome", - "extensions": [] + "welcomeRoute": "/welcome" }, "engines": { "node": ">=16 <17" diff --git a/src/extensions/extension-discovery/bundled-extension-token.ts b/src/extensions/extension-discovery/bundled-extension-token.ts new file mode 100644 index 0000000000..1a1a40f9fa --- /dev/null +++ b/src/extensions/extension-discovery/bundled-extension-token.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { LensExtensionConstructor, LensExtensionManifest } from "../lens-extension"; + +export interface BundledExtension { + readonly manifest: LensExtensionManifest; + main: () => LensExtensionConstructor | null; + renderer: () => LensExtensionConstructor | null; +} + +export const bundledExtensionInjectionToken = getInjectionToken({ + id: "bundled-extension-path", +}); diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index 971850c585..378f519bb7 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -27,7 +27,6 @@ import getRelativePathInjectable from "../../common/path/get-relative-path.injec 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 applicationInformationInjectable from "../../common/vars/application-information.injectable"; import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable"; const extensionDiscoveryInjectable = getInjectable({ @@ -58,7 +57,6 @@ const extensionDiscoveryInjectable = getInjectable({ getRelativePath: di.inject(getRelativePathInjectable), joinPaths: di.inject(joinPathsInjectable), homeDirectoryPath: di.inject(homeDirectoryPathInjectable), - applicationInformation: di.inject(applicationInformationInjectable), }), }); diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index 1d14c427d0..c9646a1c6c 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -30,7 +30,6 @@ import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable" import type { GetRelativePath } from "../../common/path/get-relative-path.injectable"; import type { RemovePath } from "../../common/fs/remove.injectable"; import type TypedEventEmitter from "typed-emitter"; -import type { ApplicationInformation } from "../../common/vars/application-information.injectable"; interface Dependencies { readonly extensionLoader: ExtensionLoader; @@ -42,7 +41,6 @@ interface Dependencies { readonly isProduction: boolean; readonly fileSystemSeparator: string; readonly homeDirectoryPath: string; - readonly applicationInformation: ApplicationInformation; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; installExtension: (name: string) => Promise; readJsonFile: ReadJson; @@ -384,42 +382,16 @@ export class ExtensionDiscovery { } async ensureExtensions(): Promise> { - const bundledExtensions = await this.loadBundledExtensions(); - const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name)); - const extensions = bundledExtensions.concat(userExtensions); + const userExtensions = await this.loadFromFolder(this.localFolderPath); - return this.extensions = new Map(extensions.map(extension => [extension.id, extension])); + return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension])); } - async loadBundledExtensions(): Promise { - const extensions: InstalledExtension[] = []; - const extensionNames = this.dependencies.applicationInformation.config.extensions || []; - - for (const dirName of extensionNames) { - const absPath = this.dependencies.joinPaths(__dirname, "..", "..", "node_modules", dirName); - const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true }); - - if (!extension) { - throw new Error(`Couldn't load bundled extension: ${dirName}`); - } - - extensions.push(extension); - } - this.dependencies.logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { extensions }); - - return extensions; - } - - async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise { + async loadFromFolder(folderPath: string): Promise { const extensions: InstalledExtension[] = []; const paths = await this.dependencies.readDirectory(folderPath); for (const fileName of paths) { - // do not allow to override bundled extensions - if (bundledExtensions.includes(fileName)) { - continue; - } - const absPath = this.dependencies.joinPaths(folderPath, fileName); try { diff --git a/src/extensions/extension-loader/entry-point-name.ts b/src/extensions/extension-loader/entry-point-name.ts new file mode 100644 index 0000000000..390da4a34a --- /dev/null +++ b/src/extensions/extension-loader/entry-point-name.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; + +export const extensionEntryPointNameInjectionToken = getInjectionToken<"main" | "renderer">({ + id: "extension-entry-point-name-token", +}); diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 48edce4446..67f8434043 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -12,6 +12,8 @@ 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", @@ -21,6 +23,8 @@ const extensionLoaderInjectable = getInjectable({ createExtensionInstance: di.inject(createExtensionInstanceInjectionToken), 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), diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index c00414e88d..2c186ff38f 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -21,12 +21,15 @@ import type { Extension } from "./extension/extension.injectable"; import type { Logger } from "../../common/logger"; import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; +import type { BundledExtension } from "../extension-discovery/bundled-extension-token"; const logModule = "[EXTENSIONS-LOADER]"; interface Dependencies { readonly extensionInstances: ObservableMap; + readonly bundledExtensions: BundledExtension[]; readonly logger: Logger; + readonly extensionEntryPointName: "main" | "renderer"; updateExtensionsState: (extensionsState: Record) => void; createExtensionInstance: CreateExtensionInstance; getExtension: (instance: LensExtension) => Extension; @@ -34,6 +37,12 @@ interface Dependencies { getDirnameOfPath: GetDirnameOfPath; } +interface ExtensionBeingActivated { + instance: LensExtension; + installedExtension: InstalledExtension; + activated: Promise; +} + export interface ExtensionLoading { isBundled: boolean; loaded: Promise; @@ -248,14 +257,84 @@ export class ExtensionLoader { }); } - protected async loadExtensions(installedExtensions: Map) { + protected async loadBundledExtensions() { + return this.dependencies.bundledExtensions + .map(extension => { + try { + const LensExtensionClass = extension[this.dependencies.extensionEntryPointName](); + + if (!LensExtensionClass) { + return null; + } + + const installedExtension: InstalledExtension = { + absolutePath: "irrelevant", + id: extension.manifest.name, + isBundled: true, + isCompatible: true, + isEnabled: true, + manifest: extension.manifest, + manifestPath: "irrelevant", + }; + const instance = this.dependencies.createExtensionInstance( + 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; + } + }) + .filter(isDefined); + } + + protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise { + // We first need to wait until each extension's `onActivate` is resolved or rejected, + // as this might register new catalog categories. Afterwards we can safely .enable the extension. + await Promise.all( + extensions.map(extension => + // If extension activation fails, log error + extension.activated.catch((error) => { + this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error }); + }), + ), + ); + + extensions.forEach(({ instance }) => { + const extension = this.dependencies.getExtension(instance); + + extension.register(); + }); + + return extensions.map(extension => { + const loaded = extension.instance.enable().catch((err) => { + this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err }); + }); + + return { + isBundled: extension.installedExtension.isBundled, + loaded, + }; + }); + } + + protected async loadUserExtensions(installedExtensions: Map) { // Steps of the function: // 1. require and call .activate for each Extension // 2. Wait until every extension's onActivate has been resolved // 3. Call .enable for each extension // 4. Return ExtensionLoading[] - const extensions = [...installedExtensions.entries()] + return [...installedExtensions.entries()] .map(([extId, extension]) => { const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); @@ -280,7 +359,7 @@ export class ExtensionLoader { instance, installedExtension: extension, activated: instance.activate(), - }; + } as ExtensionBeingActivated; } catch (err) { this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err }); } @@ -290,52 +369,33 @@ export class ExtensionLoader { return null; }) - // Remove null values .filter(isDefined); - - // 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 ExtensionLoading[] - 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, - }; - }); } - autoInitExtensions() { + async autoInitExtensions() { this.dependencies.logger.info(`${logModule}: auto initializing extensions`); - // Setup reaction to load extensions on JSON changes - reaction(() => this.toJSON(), installedExtensions => this.loadExtensions(installedExtensions)); + const bundledExtensions = await this.loadBundledExtensions(); + const userExtensions = await this.loadUserExtensions(this.toJSON()); + const loadedExtensions = await this.loadExtensions([ + ...bundledExtensions, + ...userExtensions, + ]); - // Load initial extensions - return this.loadExtensions(this.toJSON()); + // 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: InstalledExtension): LensExtensionConstructor | null { - const entryPointName = ipcRenderer ? "renderer" : "main"; - const extRelativePath = extension.manifest[entryPointName]; + const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName]; if (!extRelativePath) { return null; @@ -348,7 +408,7 @@ export class ExtensionLoader { } catch (error) { const message = (error instanceof Error ? error.stack : undefined) || error; - this.dependencies.logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension }); + this.dependencies.logger.error(`${logModule}: can't load ${this.dependencies.extensionEntryPointName} for "${extension.manifest.name}": ${message}`, { extension }); } return null; diff --git a/src/main/extension-loader/entry-point-name.injectable.ts b/src/main/extension-loader/entry-point-name.injectable.ts new file mode 100644 index 0000000000..0d209211f2 --- /dev/null +++ b/src/main/extension-loader/entry-point-name.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 { extensionEntryPointNameInjectionToken } from "../../extensions/extension-loader/entry-point-name"; + +const extensionEntryPointNameInjectable = getInjectable({ + id: "extension-entry-point-name", + instantiate: () => "main" as const, + injectionToken: extensionEntryPointNameInjectionToken, +}); + +export default extensionEntryPointNameInjectable; diff --git a/src/renderer/extension-loader/entry-point-name.injectable.ts b/src/renderer/extension-loader/entry-point-name.injectable.ts new file mode 100644 index 0000000000..55315d6d73 --- /dev/null +++ b/src/renderer/extension-loader/entry-point-name.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 { extensionEntryPointNameInjectionToken } from "../../extensions/extension-loader/entry-point-name"; + +const extensionEntryPointNameInjectable = getInjectable({ + id: "extension-entry-point-name", + instantiate: () => "renderer" as const, + injectionToken: extensionEntryPointNameInjectionToken, +}); + +export default extensionEntryPointNameInjectable;