diff --git a/package.json b/package.json index ea0e7a1bd6..0de0c6197c 100644 --- a/package.json +++ b/package.json @@ -86,8 +86,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" @@ -233,7 +232,6 @@ "@sentry/electron": "^3.0.8", "@sentry/integrations": "^6.19.3", "@side/jest-runtime": "^1.0.1", - "@types/circular-dependency-plugin": "5.0.5", "abort-controller": "^3.0.0", "auto-bind": "^4.0.0", "await-lock": "^2.2.2", @@ -267,8 +265,6 @@ "mock-fs": "^5.2.0", "moment": "^2.29.4", "moment-timezone": "^0.5.39", - "monaco-editor": "^0.29.1", - "monaco-editor-webpack-plugin": "^5.0.0", "node-fetch": "^3.3.0", "node-pty": "0.10.1", "npm": "^8.19.3", @@ -404,6 +400,8 @@ "memorystream": "^0.3.1", "mini-css-extract-plugin": "^2.7.2", "mock-http": "^1.1.0", + "monaco-editor": "^0.29.1", + "monaco-editor-webpack-plugin": "^5.0.0", "node-gyp": "^8.3.0", "node-loader": "^2.0.0", "nodemon": "^2.0.20", @@ -495,6 +493,7 @@ "@types/triple-beam": "^1.3.2", "@types/url-parse": "^1.4.8", "@types/uuid": "^8.3.4", + "monaco-editor": "^0.29.1", "react-select": "^5.7.0", "typed-emitter": "^1.4.0", "xterm-addon-fit": "^0.5.0" diff --git a/src/common/library.ts b/src/common/library.ts index 530476703e..7dc28d2a7f 100644 --- a/src/common/library.ts +++ b/src/common/library.ts @@ -4,8 +4,10 @@ */ import applicationInformationToken from "./vars/application-information-token"; import type { ApplicationInformation } from "./vars/application-information-token"; +import { bundledExtensionInjectionToken } from "../extensions/extension-discovery/bundled-extension-injection-token"; export { applicationInformationToken, ApplicationInformation, + bundledExtensionInjectionToken, }; diff --git a/src/extensions/extension-discovery/bundled-extension-injection-token.ts b/src/extensions/extension-discovery/bundled-extension-injection-token.ts new file mode 100644 index 0000000000..4a12ceebb8 --- /dev/null +++ b/src/extensions/extension-discovery/bundled-extension-injection-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-injection-token", +}); diff --git a/src/extensions/extension-discovery/bundled-extensions.injectable.ts b/src/extensions/extension-discovery/bundled-extensions.injectable.ts deleted file mode 100644 index f7ff14eb6b..0000000000 --- a/src/extensions/extension-discovery/bundled-extensions.injectable.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { InstalledExtension } from "./extension-discovery"; - -const bundledExtensionsInjectable = getInjectable({ - id: "bundled-extensions", - instantiate: (): InstalledExtension[] => [], -}); - -export default bundledExtensionsInjectable; diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index 5c1b872e6b..378f519bb7 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -27,9 +27,7 @@ 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 applicationInformationToken from "../../common/vars/application-information-token"; import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable"; -import bundledExtensionsInjectable from "./bundled-extensions.injectable"; const extensionDiscoveryInjectable = getInjectable({ id: "extension-discovery", @@ -59,8 +57,6 @@ const extensionDiscoveryInjectable = getInjectable({ getRelativePath: di.inject(getRelativePathInjectable), joinPaths: di.inject(joinPathsInjectable), homeDirectoryPath: di.inject(homeDirectoryPathInjectable), - applicationInformation: di.inject(applicationInformationToken), - bundledExtensions: di.inject(bundledExtensionsInjectable), }), }); diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index 8794ce4bdc..c9646a1c6c 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -30,20 +30,17 @@ 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-token"; interface Dependencies { readonly extensionLoader: ExtensionLoader; readonly extensionsStore: ExtensionsStore; readonly extensionInstallationStateStore: ExtensionInstallationStateStore; readonly extensionPackageRootDirectory: string; - readonly bundledExtensions: InstalledExtension[]; readonly resourcesDirectory: string; readonly logger: Logger; readonly isProduction: boolean; readonly fileSystemSeparator: string; readonly homeDirectoryPath: string; - readonly applicationInformation: ApplicationInformation; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; installExtension: (name: string) => Promise; readJsonFile: ReadJson; @@ -385,23 +382,16 @@ export class ExtensionDiscovery { } async ensureExtensions(): Promise> { - const bundledExtensions = this.dependencies.bundledExtensions; - 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 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/extension-entry-point-name-injection-token.ts b/src/extensions/extension-loader/extension-entry-point-name-injection-token.ts new file mode 100644 index 0000000000..30302ae56c --- /dev/null +++ b/src/extensions/extension-loader/extension-entry-point-name-injection-token.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-injection-token", +}); diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 48edce4446..b5975310cb 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-injection-token"; +import { extensionEntryPointNameInjectionToken } from "./extension-entry-point-name-injection-token"; 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 b85c63e369..a5b62c9b44 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-injection-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 SemiLoadedExtension { + 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: "irrelavent", + id: extension.manifest.name, + isBundled: true, + isCompatible: true, + isEnabled: true, + manifest: extension.manifest, + manifestPath: "irrelavent", + }; + const instance = this.dependencies.createExtensionInstance( + LensExtensionClass, + installedExtension, + ); + + this.dependencies.extensionInstances.set(extension.manifest.name, instance); + + return { + instance, + installedExtension, + activated: instance.activate(), + } as SemiLoadedExtension; + } catch (err) { + this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err }); + + return null; + } + }) + .filter(isDefined); + } + + protected async loadExtensions(extensions: SemiLoadedExtension[]): 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 SemiLoadedExtension; } 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/extension-entry-point-name.injectable.ts b/src/main/extension-loader/extension-entry-point-name.injectable.ts new file mode 100644 index 0000000000..27a5478131 --- /dev/null +++ b/src/main/extension-loader/extension-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/extension-entry-point-name-injection-token"; + +const extensionEntryPointNameInjectable = getInjectable({ + id: "extension-entry-point-name", + instantiate: () => "main" as const, + injectionToken: extensionEntryPointNameInjectionToken, +}); + +export default extensionEntryPointNameInjectable; diff --git a/src/main/index.ts b/src/main/index.ts index 47b63f5d35..bbf92a6a00 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,7 +15,6 @@ const di = getDi(); try { await startApp({ di, - extensions: [], }); } catch (error) { console.error(error); diff --git a/src/main/start-app.ts b/src/main/start-app.ts index 911e6ce3c9..c7f98ed5da 100644 --- a/src/main/start-app.ts +++ b/src/main/start-app.ts @@ -5,36 +5,13 @@ import type { DiContainer } from "@ogre-tools/injectable"; import startMainApplicationInjectable from "./start-main-application/start-main-application.injectable"; -import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; -import joinPathsInjectable from "../common/path/join-paths.injectable"; -import type { LensExtensionManifest } from "../extensions/lens-extension"; -import bundledExtensionsInjectable from "../extensions/extension-discovery/bundled-extensions.injectable"; interface AppConfig { di: DiContainer; - extensions: { path: string }[]; } export async function startApp(conf: AppConfig) { - const { di, extensions } = conf; - - const bundledExtensions = di.inject(bundledExtensionsInjectable); - const readJson = di.inject(readJsonFileInjectable); - const joinPaths = di.inject(joinPathsInjectable); - - for (const extension of extensions) { - const manifestPath = joinPaths(extension.path, "package.json"); - - bundledExtensions.push({ - id: manifestPath, - manifest: (await readJson(manifestPath)) as unknown as LensExtensionManifest, - manifestPath, - absolutePath: extension.path, - isCompatible: true, - isBundled: true, - isEnabled: true, - }); - } + const { di } = conf; const startMainApplication = di.inject(startMainApplicationInjectable); diff --git a/src/renderer/extension-loader/extension-entry-point-name.injectable.ts b/src/renderer/extension-loader/extension-entry-point-name.injectable.ts new file mode 100644 index 0000000000..5586a47be2 --- /dev/null +++ b/src/renderer/extension-loader/extension-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/extension-entry-point-name-injection-token"; + +const extensionEntryPointNameInjectable = getInjectable({ + id: "extension-entry-point-name", + instantiate: () => "renderer" as const, + injectionToken: extensionEntryPointNameInjectionToken, +}); + +export default extensionEntryPointNameInjectable; diff --git a/src/renderer/vars/application-information.global-override-for-injectable.ts b/src/renderer/vars/application-information.global-override-for-injectable.ts new file mode 100644 index 0000000000..acc47ce83d --- /dev/null +++ b/src/renderer/vars/application-information.global-override-for-injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../common/test-utils/get-global-override"; +import applicationInformationInjectable from "../../common/vars/application-information-injectable"; + +export default getGlobalOverride(applicationInformationInjectable, () => ({ + name: "some-product-name", + productName: "some-product-name", + version: "6.0.0", + build: {} as any, + config: { + k8sProxyVersion: "0.2.1", + bundledKubectlVersion: "1.23.3", + bundledHelmVersion: "3.7.2", + sentryDsn: "", + contentSecurityPolicy: "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:", + welcomeRoute: "/welcome", + }, + copyright: "some-copyright-information", + description: "some-descriptive-text", +}));