diff --git a/packages/core/src/common/library.ts b/packages/core/src/common/library.ts index f4aee75513..9dc6450bf0 100644 --- a/packages/core/src/common/library.ts +++ b/packages/core/src/common/library.ts @@ -6,4 +6,6 @@ // @experimental export { applicationInformationToken } from "./vars/application-information-token"; export type { ApplicationInformation } from "./vars/application-information-token"; +export type { BundledExtension } from "../extensions/extension-discovery/bundled-extension-token"; +export type { BundledLensExtensionManifest } from "../extensions/lens-extension"; export { bundledExtensionInjectionToken } from "../extensions/extension-discovery/bundled-extension-token"; diff --git a/packages/core/src/common/protocol-handler/router.ts b/packages/core/src/common/protocol-handler/router.ts index 8c9915b287..3b17b2f600 100644 --- a/packages/core/src/common/protocol-handler/router.ts +++ b/packages/core/src/common/protocol-handler/router.ts @@ -209,7 +209,7 @@ export abstract class LensProtocolRouter { return name; } - if (!this.dependencies.extensionsStore.isEnabled(extension)) { + if (!extension.isBundled && !this.dependencies.extensionsStore.isEnabled(extension.id)) { this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); return name; diff --git a/packages/core/src/extensions/__tests__/extension-loader.test.ts b/packages/core/src/extensions/__tests__/extension-loader.test.ts index 1c010f7640..cd06f80f09 100644 --- a/packages/core/src/extensions/__tests__/extension-loader.test.ts +++ b/packages/core/src/extensions/__tests__/extension-loader.test.ts @@ -114,38 +114,36 @@ describe("ExtensionLoader", () => { }); it("renderer updates extension after ipc broadcast", async () => { - expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`); + expect(extensionLoader.userExtensions.get().size).toBe(0); await extensionLoader.init(); await delay(10); // Assert the extensions after the extension broadcast event - expect(extensionLoader.userExtensions).toMatchInlineSnapshot(` - Map { - "manifest/path" => Object { - "absolutePath": "/test/1", - "id": "manifest/path", - "isBundled": false, - "isEnabled": true, - "manifest": Object { - "name": "TestExtension", - "version": "1.0.0", - }, - "manifestPath": "manifest/path", + 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", }, - "manifest/path3" => Object { - "absolutePath": "/test/3", - "id": "manifest/path3", - "isBundled": false, - "isEnabled": true, - "manifest": Object { - "name": "TestExtension3", - "version": "3.0.0", - }, - "manifestPath": "manifest/path3", + 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 () => { diff --git a/packages/core/src/extensions/extension-discovery/bundled-extension-token.ts b/packages/core/src/extensions/extension-discovery/bundled-extension-token.ts index 1a1a40f9fa..c4415ddc25 100644 --- a/packages/core/src/extensions/extension-discovery/bundled-extension-token.ts +++ b/packages/core/src/extensions/extension-discovery/bundled-extension-token.ts @@ -4,12 +4,12 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { LensExtensionConstructor, LensExtensionManifest } from "../lens-extension"; +import type { BundledLensExtensionManifest, BundledLensExtensionContructor } from "../lens-extension"; export interface BundledExtension { - readonly manifest: LensExtensionManifest; - main: () => LensExtensionConstructor | null; - renderer: () => LensExtensionConstructor | null; + readonly manifest: BundledLensExtensionManifest; + main: () => Promise; + renderer: () => Promise; } export const bundledExtensionInjectionToken = getInjectionToken({ diff --git a/packages/core/src/extensions/extension-discovery/extension-discovery.ts b/packages/core/src/extensions/extension-discovery/extension-discovery.ts index bd9baff2c8..59d778ab7c 100644 --- a/packages/core/src/extensions/extension-discovery/extension-discovery.ts +++ b/packages/core/src/extensions/extension-discovery/extension-discovery.ts @@ -10,7 +10,7 @@ import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc import { isErrnoException, toJS } from "../../common/utils"; import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { ExtensionLoader } from "../extension-loader"; -import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; +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"; import { requestInitialExtensionDiscovery } from "../../renderer/ipc"; @@ -58,22 +58,31 @@ interface Dependencies { getRelativePath: GetRelativePath; } -export interface InstalledExtension { - id: LensExtensionId; - - readonly manifest: LensExtensionManifest; - +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; - readonly isBundled: boolean; // defined in project root's package.json +} + +export interface BundledInstalledExtension extends BaseInstalledExtension { + readonly manifest: BundledLensExtensionManifest; + readonly isBundled: true; + readonly isCompatible: true; + readonly isEnabled: true; +} + +export interface ExternalInstalledExtension extends BaseInstalledExtension { + readonly manifest: LensExtensionManifest; + readonly isBundled: false; readonly isCompatible: boolean; isEnabled: boolean; } +export type InstalledExtension = BundledInstalledExtension | ExternalInstalledExtension; + const logModule = "[EXTENSION-DISCOVERY]"; export const manifestFilename = "package.json"; @@ -88,10 +97,6 @@ interface ExtensionDiscoveryChannelMessage { */ const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink(); -interface LoadFromFolderOptions { - isBundled?: boolean; -} - interface ExtensionDiscoveryEvents { add: (ext: InstalledExtension) => void; remove: (extId: LensExtensionId) => void; @@ -286,7 +291,7 @@ export class ExtensionDiscovery { * @param extensionId The ID of the extension to uninstall. */ async uninstallExtension(extensionId: LensExtensionId): Promise { - const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtension(extensionId); + const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtensionById(extensionId); if (!extension) { return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId }); @@ -345,24 +350,26 @@ export class ExtensionDiscovery { * Returns InstalledExtension from path to package.json file. * Also updates this.packagesJson. */ - protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise { + protected async loadExtensionFromFolder(folderPath: string): Promise { + const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename); + try { const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest; - const id = isBundled ? manifestPath : this.getInstalledManifestPath(manifest.name); - const isEnabled = this.dependencies.extensionsStore.isEnabled({ id, isBundled }); + const id = this.getInstalledManifestPath(manifest.name); + const isEnabled = this.dependencies.extensionsStore.isEnabled(id); const extensionDir = this.dependencies.getDirnameOfPath(manifestPath); const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`); const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage) ? npmPackage : extensionDir; - const isCompatible = isBundled || this.dependencies.isCompatibleExtension(manifest); + const isCompatible = this.dependencies.isCompatibleExtension(manifest); return { id, absolutePath, manifestPath: id, manifest, - isBundled, + isBundled: false, isEnabled, isCompatible, }; @@ -378,14 +385,14 @@ export class ExtensionDiscovery { } } - async ensureExtensions(): Promise> { + async ensureExtensions(): Promise> { const userExtensions = await this.loadFromFolder(this.localFolderPath); return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension])); } - async loadFromFolder(folderPath: string): Promise { - const extensions: InstalledExtension[] = []; + async loadFromFolder(folderPath: string): Promise { + const extensions: ExternalInstalledExtension[] = []; const paths = await this.dependencies.readDirectory(folderPath); for (const fileName of paths) { @@ -418,16 +425,6 @@ export class ExtensionDiscovery { return extensions; } - /** - * Loads extension from absolute path, updates this.packagesJson to include it and returns the extension. - * @param folderPath Folder path to extension - */ - async loadExtensionFromFolder(folderPath: string, { isBundled = false }: LoadFromFolderOptions = {}): Promise { - const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename); - - return this.getByManifest(manifestPath, { isBundled }); - } - toJSON(): ExtensionDiscoveryChannelMessage { return toJS({ isLoaded: this.isLoaded, diff --git a/packages/core/src/extensions/extension-loader/create-extension-instance.token.ts b/packages/core/src/extensions/extension-loader/create-extension-instance.token.ts index d7680b018b..e68e26b63c 100644 --- a/packages/core/src/extensions/extension-loader/create-extension-instance.token.ts +++ b/packages/core/src/extensions/extension-loader/create-extension-instance.token.ts @@ -4,10 +4,13 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { InstalledExtension } from "../extension-discovery/extension-discovery"; -import type { LensExtension, LensExtensionConstructor } from "../lens-extension"; +import type { BundledInstalledExtension, ExternalInstalledExtension } from "../extension-discovery/extension-discovery"; +import type { BundledLensExtensionContructor, LensExtension, LensExtensionConstructor } from "../lens-extension"; -export type CreateExtensionInstance = (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension; +export interface CreateExtensionInstance { + (ExtensionClass: LensExtensionConstructor, extension: ExternalInstalledExtension): LensExtension; + (ExtensionClass: BundledLensExtensionContructor, extension: BundledInstalledExtension): LensExtension; +} export const createExtensionInstanceInjectionToken = getInjectionToken({ id: "create-extension-instance-token", diff --git a/packages/core/src/extensions/extension-loader/extension-loader.ts b/packages/core/src/extensions/extension-loader/extension-loader.ts index 5dce874e54..f65ce58ea0 100644 --- a/packages/core/src/extensions/extension-loader/extension-loader.ts +++ b/packages/core/src/extensions/extension-loader/extension-loader.ts @@ -6,10 +6,10 @@ import { ipcMain, ipcRenderer } from "electron"; import { isEqual } from "lodash"; import type { ObservableMap } from "mobx"; -import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; +import { runInAction, action, computed, observable, reaction, when } from "mobx"; import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc"; -import { isDefined, toJS } from "../../common/utils"; -import type { InstalledExtension } from "../extension-discovery/extension-discovery"; +import { isDefined, iter, toJS } from "../../common/utils"; +import type { 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"; @@ -61,51 +61,21 @@ export class ExtensionLoader { */ protected readonly nonInstancesByName = observable.set(); - /** - * This is updated by the `observe` in the constructor. DO NOT write directly to it - */ - protected readonly instancesByName = observable.map(); + protected readonly instancesByName = computed(() => new Map(( + iter.chain(this.dependencies.extensionInstances.entries()) + .map(([, instance]) => [instance.name, instance]) + ))); private readonly onRemoveExtensionId = new EventEmitter<[string]>(); - @observable isLoaded = false; + readonly isLoaded = observable.box(false); - get whenLoaded() { - return when(() => this.isLoaded); - } + constructor(protected readonly dependencies: Dependencies) {} - constructor(protected readonly dependencies: Dependencies) { - makeObservable(this); - - observe(this.dependencies.extensionInstances, change => { - switch (change.type) { - case "add": - if (this.instancesByName.has(change.newValue.name)) { - throw new TypeError("Extension names must be unique"); - } - - this.instancesByName.set(change.newValue.name, change.newValue); - break; - case "delete": - this.instancesByName.delete(change.oldValue.name); - break; - case "update": - throw new Error("Extension instances shouldn't be updated"); - } - }); - } - - @computed get userExtensions(): Map { - const extensions = this.toJSON(); - - extensions.forEach((ext, extId) => { - if (ext.isBundled) { - extensions.delete(extId); - } - }); - - return extensions; - } + readonly userExtensions = computed(() => new Map(( + this.extensions.toJSON() + .filter(([, extension]) => !extension.isBundled) + ))); /** * Get the extension instance by its manifest name @@ -121,21 +91,20 @@ export class ExtensionLoader { return null; } - return this.instancesByName.get(name); + return this.instancesByName.get().get(name); } - // Transform userExtensions to a state object for storing into ExtensionsStore - @computed get storeState() { - return Object.fromEntries( - Array.from(this.userExtensions) - .map(([extId, extension]) => [extId, { + readonly storeState = computed(() => Object.fromEntries(( + iter.chain(this.userExtensions.get().entries()) + .map(([extId, extension]) => [ + extId, + { enabled: extension.isEnabled, name: extension.manifest.name, - }]), - ); - } + }, + ]) + ))); - @action async init() { if (ipcMain) { await this.initMain(); @@ -143,7 +112,7 @@ export class ExtensionLoader { await this.initRenderer(); } - await Promise.all([this.whenLoaded]); + await when(() => this.isLoaded.get()); // broadcasting extensions between main/renderer processes reaction(() => this.toJSON(), () => this.broadcastExtensions(), { @@ -151,8 +120,7 @@ export class ExtensionLoader { }); reaction( - () => this.storeState, - + () => this.storeState.get(), (state) => { this.dependencies.updateExtensionsState(state); }, @@ -203,17 +171,19 @@ export class ExtensionLoader { 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() { - this.isLoaded = true; + runInAction(() => { + this.isLoaded.set(true); + }); + await this.autoInitExtensions(); - ipcMainHandle(extensionLoaderFromMainChannel, () => { - return Array.from(this.toJSON()); - }); + ipcMainHandle(extensionLoaderFromMainChannel, () => [...this.toJSON()]); ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { this.syncExtensions(extensions); @@ -222,7 +192,9 @@ export class ExtensionLoader { protected async initRenderer() { const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => { - this.isLoaded = true; + runInAction(() => { + this.isLoaded.set(true); + }); this.syncExtensions(extensions); const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId); @@ -258,10 +230,10 @@ export class ExtensionLoader { } protected async loadBundledExtensions() { - return this.dependencies.bundledExtensions - .map(extension => { + const bundledExtensions = await Promise.all((this.dependencies.bundledExtensions + .map(async extension => { try { - const LensExtensionClass = extension[this.dependencies.extensionEntryPointName](); + const LensExtensionClass = await extension[this.dependencies.extensionEntryPointName](); if (!LensExtensionClass) { return null; @@ -294,7 +266,9 @@ export class ExtensionLoader { return null; } }) - .filter(isDefined); + )); + + return bundledExtensions.filter(isDefined); } protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise { @@ -335,6 +309,7 @@ export class ExtensionLoader { // 4. Return ExtensionLoading[] return [...installedExtensions.entries()] + .filter((entry): entry is [string, ExternalInstalledExtension] => !entry[1].isBundled) .map(([extId, extension]) => { const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); @@ -394,7 +369,7 @@ export class ExtensionLoader { return loadedExtensions; } - protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null { + protected requireExtension(extension: ExternalInstalledExtension): LensExtensionConstructor | null { const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName]; if (!extRelativePath) { @@ -414,7 +389,7 @@ export class ExtensionLoader { return null; } - getExtension(extId: LensExtensionId) { + getExtensionById(extId: LensExtensionId) { return this.extensions.get(extId); } diff --git a/packages/core/src/extensions/extensions-store/extensions-store.ts b/packages/core/src/extensions/extensions-store/extensions-store.ts index 3b2dc80eb1..ce23e98e19 100644 --- a/packages/core/src/extensions/extensions-store/extensions-store.ts +++ b/packages/core/src/extensions/extensions-store/extensions-store.ts @@ -18,11 +18,6 @@ export interface LensExtensionState { name: string; } -export interface IsEnabledExtensionDescriptor { - id: string; - isBundled: boolean; -} - export class ExtensionsStore extends BaseStore { constructor(deps: BaseStoreDependencies) { super(deps, { @@ -39,12 +34,12 @@ export class ExtensionsStore extends BaseStore { .map(({ name }) => name); } - protected state = observable.map(); + protected readonly state = observable.map(); - isEnabled({ id, isBundled }: IsEnabledExtensionDescriptor): boolean { + isEnabled(extId: LensExtensionId): boolean { // By default false, so that copied extensions are disabled by default. // If user installs the extension from the UI, the Extensions component will specifically enable it. - return isBundled || Boolean(this.state.get(id)?.enabled); + return this.state.get(extId)?.enabled ?? false; } mergeState = action((extensionsState: Record | [LensExtensionId, LensExtensionState][]) => { diff --git a/packages/core/src/extensions/lens-extension.ts b/packages/core/src/extensions/lens-extension.ts index 53c9343607..f6227a0d2e 100644 --- a/packages/core/src/extensions/lens-extension.ts +++ b/packages/core/src/extensions/lens-extension.ts @@ -3,19 +3,24 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { InstalledExtension } from "./extension-discovery/extension-discovery"; +import type { BundledInstalledExtension, ExternalInstalledExtension, InstalledExtension } from "./extension-discovery/extension-discovery"; import { action, computed, makeObservable, observable } from "mobx"; -import type { PackageJson } from "type-fest"; import { disposer } from "../common/utils"; import type { LensExtensionDependencies } from "./lens-extension-set-dependencies"; import type { ProtocolHandlerRegistration } from "../common/protocol-handler/registration"; +import type { PackageJson } from "type-fest"; export type LensExtensionId = string; // path to manifest (package.json) -export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; +export type LensExtensionConstructor = new (ext: ExternalInstalledExtension) => LensExtension; +export type BundledLensExtensionContructor = new (ext: BundledInstalledExtension) => LensExtension; -export interface LensExtensionManifest extends PackageJson { +export interface BundledLensExtensionManifest extends PackageJson { name: string; version: string; + publishConfig?: Partial>; +} + +export interface LensExtensionManifest extends BundledLensExtensionManifest { main?: string; // path to %ext/dist/main.js renderer?: string; // path to %ext/dist/renderer.js /** @@ -24,8 +29,7 @@ export interface LensExtensionManifest extends PackageJson { */ engines: { lens: string; // "semver"-package format - npm?: string; - node?: string; + [x: string]: string | undefined; }; // Specify extension name used for persisting data. @@ -65,14 +69,12 @@ export class LensExtension< [Disposers] = disposer(); constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { - makeObservable(this); - // id is the name of the manifest this.id = id; - - this.manifest = manifest; + this.manifest = manifest as LensExtensionManifest; this.manifestPath = manifestPath; this.isBundled = !!isBundled; + makeObservable(this); } get name() { diff --git a/packages/core/src/main/extension-loader/create-extension-instance.injectable.ts b/packages/core/src/main/extension-loader/create-extension-instance.injectable.ts index 62417149d4..1f828c892a 100644 --- a/packages/core/src/main/extension-loader/create-extension-instance.injectable.ts +++ b/packages/core/src/main/extension-loader/create-extension-instance.injectable.ts @@ -24,7 +24,7 @@ const createExtensionInstanceInjectable = getInjectable({ }; return (ExtensionClass, extension) => { - const instance = new ExtensionClass(extension) as LensMainExtension; + const instance = new ExtensionClass(extension as any) as LensMainExtension; (instance as Writable)[lensExtensionDependencies] = deps; diff --git a/packages/core/src/main/protocol-handler/__test__/router.test.ts b/packages/core/src/main/protocol-handler/__test__/router.test.ts index a06c3b057b..a5b0b873bc 100644 --- a/packages/core/src/main/protocol-handler/__test__/router.test.ts +++ b/packages/core/src/main/protocol-handler/__test__/router.test.ts @@ -6,23 +6,18 @@ import * as uuid from "uuid"; import { ProtocolHandlerExtension, ProtocolHandlerInternal, ProtocolHandlerInvalid } from "../../../common/protocol-handler"; -import { delay, noop } from "../../../common/utils"; -import type { ExtensionsStore, IsEnabledExtensionDescriptor } from "../../../extensions/extensions-store/extensions-store"; +import { noop } from "../../../common/utils"; import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable"; import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; -import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import { LensExtension } from "../../../extensions/lens-extension"; import type { LensExtensionId } from "../../../extensions/lens-extension"; import type { ObservableMap } from "mobx"; +import { runInAction } from "mobx"; import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; -import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable"; -import pathExistsInjectable from "../../../common/fs/path-exists.injectable"; -import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable"; -import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable"; function throwIfDefined(val: any): void { if (val != null) { @@ -39,20 +34,13 @@ describe("protocol router tests", () => { beforeEach(async () => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - di.override(pathExistsInjectable, () => () => { throw new Error("tried call pathExists without override"); }); - di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); }); - di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); - di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); - enabledExtensions = new Set(); di.override(extensionsStoreInjectable, () => ({ - isEnabled: ({ id, isBundled }: IsEnabledExtensionDescriptor) => isBundled || enabledExtensions.has(id), - } as unknown as ExtensionsStore)); + isEnabled: (id) => enabledExtensions.has(id), + })); - di.permitSideEffects(getConfigurationFileModelInjectable); - - di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); broadcastMessageMock = jest.fn(); di.override(broadcastMessageInjectable, () => broadcastMessageMock); @@ -60,7 +48,9 @@ describe("protocol router tests", () => { extensionInstances = di.inject(extensionInstancesInjectable); lpr = di.inject(lensProtocolRouterMainInjectable); - lpr.rendererLoaded = true; + runInAction(() => { + lpr.rendererLoaded.set(true); + }); }); it("should broadcast invalid protocol on non-lens URLs", async () => { @@ -73,7 +63,19 @@ describe("protocol router tests", () => { expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar"); }); - it("should not throw when has valid host", async () => { + it("should broadcast internal route when called with valid host", async () => { + lpr.addInternalHandler("/", noop); + + try { + expect(await lpr.route("lens://app")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched"); + }); + + it("should broadcast external route when called with valid host", async () => { const extId = uuid.v4(); const ext = new LensExtension({ id: extId, @@ -97,22 +99,12 @@ describe("protocol router tests", () => { extensionInstances.set(extId, ext); enabledExtensions.add(extId); - lpr.addInternalHandler("/", noop); - - try { - expect(await lpr.route("lens://app")).toBeUndefined(); - } catch (error) { - expect(throwIfDefined(error)).not.toThrow(); - } - try { expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined(); } catch (error) { expect(throwIfDefined(error)).not.toThrow(); } - await delay(50); - expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched"); expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched"); }); @@ -183,7 +175,6 @@ describe("protocol router tests", () => { expect(throwIfDefined(error)).not.toThrow(); } - await delay(50); expect(called).toBe("foob"); expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob", "matched"); }); @@ -252,7 +243,6 @@ describe("protocol router tests", () => { expect(throwIfDefined(error)).not.toThrow(); } - await delay(50); expect(called).toBe(1); expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page", "matched"); diff --git a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts index 3beb596e4e..bcf727a6a0 100644 --- a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts +++ b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts @@ -6,7 +6,7 @@ import * as proto from "../../../common/protocol-handler"; import URLParse from "url-parse"; import type { LensExtension } from "../../../extensions/lens-extension"; -import { observable, when, makeObservable } from "mobx"; +import { observable, when } from "mobx"; import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler"; import { ProtocolHandlerInvalid } from "../../../common/protocol-handler"; import { disposer, noop } from "../../../common/utils"; @@ -39,17 +39,15 @@ export interface LensProtocolRouterMainDependencies extends LensProtocolRouterDe } export class LensProtocolRouterMain extends proto.LensProtocolRouter { - private missingExtensionHandlers: FallbackHandler[] = []; + private readonly missingExtensionHandlers: FallbackHandler[] = []; // TODO: This is used to solve out-of-place temporal dependency. Remove, and solve otherwise. - @observable rendererLoaded = false; + readonly rendererLoaded = observable.box(false); - protected disposers = disposer(); + protected readonly disposers = disposer(); constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) { super(dependencies); - - makeObservable(this); } public cleanup() { @@ -118,8 +116,13 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { protected _routeToInternal(url: URLParse>): RouteAttempt { const rawUrl = url.toString(); // for sending to renderer const attempt = super._routeToInternal(url); + const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt); - this.disposers.push(when(() => this.rendererLoaded, () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt))); + if (this.rendererLoaded.get()) { + broadcastToRenderer(); + } else { + this.disposers.push(when(() => this.rendererLoaded.get(), broadcastToRenderer)); + } return attempt; } @@ -135,8 +138,13 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { * argument. */ const attempt = await super._routeToExtension(new URLParse(url.toString(), true)); + const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt); - this.disposers.push(when(() => this.rendererLoaded, () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt))); + if (this.rendererLoaded.get()) { + broadcastToRenderer(); + } else { + this.disposers.push(when(() => this.rendererLoaded.get(), broadcastToRenderer)); + } return attempt; } diff --git a/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts b/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts index 2bb05cfb52..11a1ba73dc 100644 --- a/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts @@ -18,7 +18,7 @@ const flagRendererAsLoadedInjectable = getInjectable({ run: () => { runInAction(() => { // Todo: remove this kludge which enables out-of-place temporal dependency. - lensProtocolRouterMain.rendererLoaded = true; + lensProtocolRouterMain.rendererLoaded.set(true); }); }, }; diff --git a/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts b/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts index d5f7444232..74773fe300 100644 --- a/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts @@ -18,7 +18,7 @@ const flagRendererAsNotLoadedInjectable = getInjectable({ run: () => { runInAction(() => { // Todo: remove this kludge which enables out-of-place temporal dependency. - lensProtocolRouterMain.rendererLoaded = false; + lensProtocolRouterMain.rendererLoaded.set(false); }); return undefined; diff --git a/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx index 47b18a3772..8d5aec850e 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx @@ -86,7 +86,7 @@ const attemptInstall = ({ } const extensionFolder = getExtensionDestFolder(name); - const installedExtension = extensionLoader.getExtension(validatedRequest.id); + const installedExtension = extensionLoader.getExtensionById(validatedRequest.id); if (installedExtension) { const { version: oldVersion } = installedExtension.manifest; diff --git a/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx index c81a69f9b0..35bb9ec68f 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx @@ -73,7 +73,7 @@ const unpackExtensionInjectable = getInjectable({ await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true }); // wait for the loader has actually install it - await when(() => extensionLoader.userExtensions.has(id)); + await when(() => extensionLoader.userExtensions.get().has(id)); // Enable installed extensions by default. extensionLoader.setIsEnabled(id, true); diff --git a/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts new file mode 100644 index 0000000000..4a07270161 --- /dev/null +++ b/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts @@ -0,0 +1,27 @@ +/** + * 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 type { LensExtensionId } from "../../../extensions/lens-extension"; + +export type DisableExtension = (extId: LensExtensionId) => void; + +const disableExtensionInjectable = getInjectable({ + id: "disable-extension", + + instantiate: (di): DisableExtension => { + const extensionLoader = di.inject(extensionLoaderInjectable); + + return (extId) => { + const ext = extensionLoader.getExtensionById(extId); + + if (ext && !ext.isBundled) { + ext.isEnabled = false; + } + }; + }, +}); + +export default disableExtensionInjectable; diff --git a/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.injectable.ts deleted file mode 100644 index 274679c331..0000000000 --- a/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.injectable.ts +++ /dev/null @@ -1,18 +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 { disableExtension } from "./disable-extension"; - -const disableExtensionInjectable = getInjectable({ - id: "disable-extension", - - instantiate: (di) => - disableExtension({ - extensionLoader: di.inject(extensionLoaderInjectable), - }), -}); - -export default disableExtensionInjectable; diff --git a/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.ts b/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.ts deleted file mode 100644 index f7ab4de7fd..0000000000 --- a/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { LensExtensionId } from "../../../../extensions/lens-extension"; -import type { ExtensionLoader } from "../../../../extensions/extension-loader"; - -interface Dependencies { - extensionLoader: ExtensionLoader; -} - -export const disableExtension = - ({ extensionLoader }: Dependencies) => - (id: LensExtensionId) => { - const extension = extensionLoader.getExtension(id); - - if (extension) { - extension.isEnabled = false; - } - }; diff --git a/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts new file mode 100644 index 0000000000..4e68573fae --- /dev/null +++ b/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts @@ -0,0 +1,27 @@ +/** + * 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 type { LensExtensionId } from "../../../extensions/lens-extension"; + +export type EnableExtension = (extId: LensExtensionId) => void; + +const enableExtensionInjectable = getInjectable({ + id: "enable-extension", + + instantiate: (di): EnableExtension => { + const extensionLoader = di.inject(extensionLoaderInjectable); + + return (extId) => { + const ext = extensionLoader.getExtensionById(extId); + + if (ext && !ext.isBundled) { + ext.isEnabled = true; + } + }; + }, +}); + +export default enableExtensionInjectable; diff --git a/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.injectable.ts deleted file mode 100644 index 19e5e83233..0000000000 --- a/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.injectable.ts +++ /dev/null @@ -1,18 +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 { enableExtension } from "./enable-extension"; - -const enableExtensionInjectable = getInjectable({ - id: "enable-extension", - - instantiate: (di) => - enableExtension({ - extensionLoader: di.inject(extensionLoaderInjectable), - }), -}); - -export default enableExtensionInjectable; diff --git a/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.ts b/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.ts deleted file mode 100644 index 573fa9b037..0000000000 --- a/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { LensExtensionId } from "../../../../extensions/lens-extension"; -import type { ExtensionLoader } from "../../../../extensions/extension-loader"; - -interface Dependencies { - extensionLoader: ExtensionLoader; -} - -export const enableExtension = - ({ extensionLoader }: Dependencies) => - (id: LensExtensionId) => { - const extension = extensionLoader.getExtension(id); - - if (extension) { - extension.isEnabled = true; - } - }; diff --git a/packages/core/src/renderer/components/+extensions/extensions.tsx b/packages/core/src/renderer/components/+extensions/extensions.tsx index e039822eb0..d1542a336c 100644 --- a/packages/core/src/renderer/components/+extensions/extensions.tsx +++ b/packages/core/src/renderer/components/+extensions/extensions.tsx @@ -24,8 +24,8 @@ import { docsUrl } from "../../../common/vars"; import { withInjectables } from "@ogre-tools/injectable-react"; import userExtensionsInjectable from "./user-extensions/user-extensions.injectable"; -import enableExtensionInjectable from "./enable-extension/enable-extension.injectable"; -import disableExtensionInjectable from "./disable-extension/disable-extension.injectable"; +import enableExtensionInjectable from "./enable-extension.injectable"; +import disableExtensionInjectable from "./disable-extension.injectable"; import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable"; import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable"; import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable"; diff --git a/packages/core/src/renderer/components/+extensions/installed-extensions.tsx b/packages/core/src/renderer/components/+extensions/installed-extensions.tsx index 60579ca3ec..8da6e14185 100644 --- a/packages/core/src/renderer/components/+extensions/installed-extensions.tsx +++ b/packages/core/src/renderer/components/+extensions/installed-extensions.tsx @@ -13,7 +13,7 @@ import { Icon } from "../icon"; import { List } from "../list/list"; import { MenuActions, MenuItem } from "../menu"; import { Spinner } from "../spinner"; -import { cssNames } from "../../utils"; +import { cssNames, toJS } from "../../utils"; import { observer } from "mobx-react"; import type { Row } from "react-table"; import type { LensExtensionId } from "../../../extensions/lens-extension"; @@ -45,7 +45,14 @@ function getStatus(extension: InstalledExtension) { return extension.isEnabled ? "Enabled" : "Disabled"; } -const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }: Dependencies & InstalledExtensionsProps) => { +const NonInjectedInstalledExtensions = observer(({ + extensionDiscovery, + extensionInstallationStateStore, + extensions, + uninstall, + enable, + disable, +}: Dependencies & InstalledExtensionsProps) => { const columns = useMemo( () => [ { @@ -138,7 +145,7 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension ), }; - }), [extensions, extensionInstallationStateStore.anyUninstalling], + }), [toJS(extensions), extensionInstallationStateStore.anyUninstalling], ); if (!extensionDiscovery.isLoaded) { diff --git a/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx b/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx index fa7cf89fb8..89821f7317 100644 --- a/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx @@ -27,7 +27,7 @@ const uninstallExtensionInjectable = getInjectable({ const showErrorNotification = di.inject(showErrorNotificationInjectable); return async (extensionId: LensExtensionId): Promise => { - const ext = extensionLoader.getExtension(extensionId); + const ext = extensionLoader.getExtensionById(extensionId); if (!ext) { logger.debug(`[EXTENSIONS]: cannot uninstall ${extensionId}, was not installed`); @@ -45,7 +45,7 @@ const uninstallExtensionInjectable = getInjectable({ await extensionDiscovery.uninstallExtension(extensionId); // wait for the ExtensionLoader to actually uninstall the extension - await when(() => !extensionLoader.userExtensions.has(extensionId)); + await when(() => !extensionLoader.userExtensions.get().has(extensionId)); showSuccessNotification(

diff --git a/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts b/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts index 63d5c7a5fc..9c3d8fc3eb 100644 --- a/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts +++ b/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts @@ -12,7 +12,7 @@ const userExtensionsInjectable = getInjectable({ instantiate: (di) => { const extensionLoader = di.inject(extensionLoaderInjectable); - return computed(() => [...extensionLoader.userExtensions.values()]); + return computed(() => [...extensionLoader.userExtensions.get().values()]); }, }); diff --git a/packages/core/src/renderer/extension-loader/create-extension-instance.injectable.ts b/packages/core/src/renderer/extension-loader/create-extension-instance.injectable.ts index 0f92440338..0e53983ec6 100644 --- a/packages/core/src/renderer/extension-loader/create-extension-instance.injectable.ts +++ b/packages/core/src/renderer/extension-loader/create-extension-instance.injectable.ts @@ -30,7 +30,7 @@ const createExtensionInstanceInjectable = getInjectable({ }; return (ExtensionClass, extension) => { - const instance = new ExtensionClass(extension) as LensRendererExtension; + const instance = new ExtensionClass(extension as any) as LensRendererExtension; (instance as Writable)[lensExtensionDependencies] = deps;