mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fixing Singleton typing to correctly return child class (#1914)
- Add distinction between `getInstance` and `getInstanceOrCreate` since it is not always possible to create an instance (since you might not know the correct arguments) - Remove all the `export const *Store = *Store.getInstance<*Store>();` calls as it defeats the purpose of `Singleton`. Plus with the typing changes the appropriate `*Store.getInstance()` is "short enough". - Special case the two extension export facades to not need to use `getInstanceOrCreate`. Plus since they are just facades it is always possible to create them. - Move some other types to be also `Singleton`'s: ExtensionLoader, ExtensionDiscovery, ThemeStore, LocalizationStore, ... - Fixed dev-run always using the same port with electron inspect - Update Store documentation with new recommendations about creating instances of singletons - Fix all unit tests to create their dependent singletons Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
b2a570ce28
commit
9563ead2e6
114 files changed
+924
-736
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { watch } from "chokidar";
|
||||
import { join, normalize } from "path";
|
||||
import { ExtensionDiscovery, InstalledExtension } from "../extension-discovery";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
|
||||
jest.mock("../../common/ipc");
|
||||
jest.mock("fs-extra");
|
||||
@@ -17,6 +18,12 @@ jest.mock("../extension-installer", () => ({
|
||||
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||
|
||||
describe("ExtensionDiscovery", () => {
|
||||
beforeEach(() => {
|
||||
ExtensionDiscovery.resetInstance();
|
||||
ExtensionsStore.resetInstance();
|
||||
ExtensionsStore.getInstanceOrCreate();
|
||||
});
|
||||
|
||||
it("emits add for added extension", async done => {
|
||||
globalThis.__non_webpack_require__.mockImplementation(() => ({
|
||||
name: "my-extension"
|
||||
@@ -36,7 +43,7 @@ describe("ExtensionDiscovery", () => {
|
||||
mockedWatch.mockImplementationOnce(() =>
|
||||
(mockWatchInstance) as any
|
||||
);
|
||||
const extensionDiscovery = new ExtensionDiscovery();
|
||||
const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate();
|
||||
|
||||
// Need to force isLoaded to be true so that the file watching is started
|
||||
extensionDiscovery.isLoaded = true;
|
||||
@@ -76,7 +83,7 @@ describe("ExtensionDiscovery", () => {
|
||||
mockedWatch.mockImplementationOnce(() =>
|
||||
(mockWatchInstance) as any
|
||||
);
|
||||
const extensionDiscovery = new ExtensionDiscovery();
|
||||
const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate();
|
||||
|
||||
// Need to force isLoaded to be true so that the file watching is started
|
||||
extensionDiscovery.isLoaded = true;
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { ExtensionLoader } from "../extension-loader";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { extensionsStore } from "../extensions-store";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
const manifestPath = "manifest/path";
|
||||
const manifestPath2 = "manifest/path2";
|
||||
const manifestPath3 = "manifest/path3";
|
||||
|
||||
jest.mock("../extensions-store", () => ({
|
||||
extensionsStore: {
|
||||
whenLoaded: Promise.resolve(true),
|
||||
mergeState: jest.fn()
|
||||
ExtensionsStore: {
|
||||
getInstance: () => ({
|
||||
whenLoaded: Promise.resolve(true),
|
||||
mergeState: jest.fn()
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -99,8 +105,12 @@ jest.mock(
|
||||
);
|
||||
|
||||
describe("ExtensionLoader", () => {
|
||||
it("renderer updates extension after ipc broadcast", async (done) => {
|
||||
const extensionLoader = new ExtensionLoader();
|
||||
beforeEach(() => {
|
||||
ExtensionLoader.resetInstance();
|
||||
});
|
||||
|
||||
it.only("renderer updates extension after ipc broadcast", async (done) => {
|
||||
const extensionLoader = ExtensionLoader.getInstanceOrCreate();
|
||||
|
||||
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
|
||||
|
||||
@@ -140,20 +150,20 @@ describe("ExtensionLoader", () => {
|
||||
});
|
||||
|
||||
it("updates ExtensionsStore after isEnabled is changed", async () => {
|
||||
(extensionsStore.mergeState as any).mockClear();
|
||||
(ExtensionsStore.getInstance().mergeState as any).mockClear();
|
||||
|
||||
// Disable sending events in this test
|
||||
(ipcRenderer.on as any).mockImplementation();
|
||||
|
||||
const extensionLoader = new ExtensionLoader();
|
||||
const extensionLoader = ExtensionLoader.getInstanceOrCreate();
|
||||
|
||||
await extensionLoader.init();
|
||||
|
||||
expect(extensionsStore.mergeState).not.toHaveBeenCalled();
|
||||
expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled();
|
||||
|
||||
Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false;
|
||||
|
||||
expect(extensionsStore.mergeState).toHaveBeenCalledWith({
|
||||
expect(ExtensionsStore.getInstance().mergeState).toHaveBeenCalledWith({
|
||||
"manifest/path": {
|
||||
enabled: false,
|
||||
name: "TestExtension"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
let ext: LensExtension = null;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import logger from "../main/logger";
|
||||
import { app } from "electron";
|
||||
import { requestMain } from "../common/ipc";
|
||||
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
|
||||
export interface ClusterFeatureStatus {
|
||||
/** feature's current version, as set by the implementation */
|
||||
@@ -86,7 +86,7 @@ export abstract class ClusterFeature {
|
||||
protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) {
|
||||
let resources: string[];
|
||||
|
||||
const clusterModel = clusterStore.getById(cluster.metadata.uid);
|
||||
const clusterModel = ClusterStore.getInstance().getById(cluster.metadata.uid);
|
||||
|
||||
if (!clusterModel) {
|
||||
throw new Error(`cluster not found`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getAppVersion } from "../../common/utils";
|
||||
import { extensionsStore } from "../extensions-store";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
|
||||
export const version = getAppVersion();
|
||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||
|
||||
export function getEnabledExtensions(): string[] {
|
||||
return extensionsStore.enabledExtensions;
|
||||
return ExtensionsStore.getInstance().enabledExtensions;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import { computed } from "mobx";
|
||||
import { CatalogEntity } from "../../common/catalog-entity";
|
||||
import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry";
|
||||
|
||||
@@ -7,7 +6,7 @@ export { catalogCategoryRegistry as catalogCategories } from "../../common/catal
|
||||
export * from "../../common/catalog-entities";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
@computed getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
return registry.getItemsForApiKind<T>(apiVersion, kind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { observable, reaction, toJS, when } from "mobx";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||
import { Singleton } from "../common/utils";
|
||||
import logger from "../main/logger";
|
||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
||||
import { extensionsStore } from "./extensions-store";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||
|
||||
export interface InstalledExtension {
|
||||
@@ -50,7 +51,7 @@ const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymb
|
||||
* - "add": When extension is added. The event is of type InstalledExtension
|
||||
* - "remove": When extension is removed. The event is of type LensExtensionId
|
||||
*/
|
||||
export class ExtensionDiscovery {
|
||||
export class ExtensionDiscovery extends Singleton {
|
||||
protected bundledFolderPath: string;
|
||||
|
||||
private loadStarted = false;
|
||||
@@ -66,6 +67,7 @@ export class ExtensionDiscovery {
|
||||
public events: EventEmitter;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.events = new EventEmitter();
|
||||
}
|
||||
|
||||
@@ -135,7 +137,7 @@ export class ExtensionDiscovery {
|
||||
depth: 1,
|
||||
ignoreInitial: true,
|
||||
// Try to wait until the file has been completely copied.
|
||||
// The OS might emit an event for added file even it's not completely written to the filesysten.
|
||||
// The OS might emit an event for added file even it's not completely written to the file-system.
|
||||
awaitWriteFinish: {
|
||||
// Wait 300ms until the file size doesn't change to consider the file written.
|
||||
// For a small file like package.json this should be plenty of time.
|
||||
@@ -235,7 +237,7 @@ export class ExtensionDiscovery {
|
||||
/**
|
||||
* Uninstalls extension.
|
||||
* The application will detect the folder unlink and remove the extension from the UI automatically.
|
||||
* @param extension Extension to unistall.
|
||||
* @param extension Extension to uninstall.
|
||||
*/
|
||||
async uninstallExtension({ absolutePath, manifest }: InstalledExtension) {
|
||||
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
||||
@@ -325,7 +327,7 @@ export class ExtensionDiscovery {
|
||||
manifestJson = __non_webpack_require__(manifestPath);
|
||||
const installedManifestPath = this.getInstalledManifestPath(manifestJson.name);
|
||||
|
||||
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
||||
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
||||
|
||||
return {
|
||||
id: installedManifestPath,
|
||||
@@ -455,5 +457,3 @@ export class ExtensionDiscovery {
|
||||
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionDiscovery = new ExtensionDiscovery();
|
||||
@@ -5,9 +5,10 @@ import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import path from "path";
|
||||
import { getHostedCluster } from "../common/cluster-store";
|
||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||
import { Singleton } from "../common/utils";
|
||||
import logger from "../main/logger";
|
||||
import type { InstalledExtension } from "./extension-discovery";
|
||||
import { extensionsStore } from "./extensions-store";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
|
||||
import type { LensMainExtension } from "./lens-main-extension";
|
||||
import type { LensRendererExtension } from "./lens-renderer-extension";
|
||||
@@ -24,7 +25,7 @@ const logModule = "[EXTENSIONS-LOADER]";
|
||||
/**
|
||||
* Loads installed extensions to the Lens application
|
||||
*/
|
||||
export class ExtensionLoader {
|
||||
export class ExtensionLoader extends Singleton {
|
||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||
|
||||
@@ -95,11 +96,11 @@ export class ExtensionLoader {
|
||||
await this.initMain();
|
||||
}
|
||||
|
||||
await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]);
|
||||
await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]);
|
||||
|
||||
// save state on change `extension.isEnabled`
|
||||
reaction(() => this.storeState, extensionsState => {
|
||||
extensionsStore.mergeState(extensionsState);
|
||||
ExtensionsStore.getInstance().mergeState(extensionsState);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,5 +330,3 @@ export class ExtensionLoader {
|
||||
broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionLoader = new ExtensionLoader();
|
||||
@@ -53,5 +53,3 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionsStore = new ExtensionsStore();
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InstalledExtension } from "./extension-discovery";
|
||||
import { action, observable, reaction } from "mobx";
|
||||
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import logger from "../main/logger";
|
||||
import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry";
|
||||
|
||||
@@ -49,7 +49,7 @@ export class LensExtension {
|
||||
* folder name.
|
||||
*/
|
||||
async getExtensionFileFolder(): Promise<string> {
|
||||
return filesystemProvisionerStore.requestDirectory(this.id);
|
||||
return FilesystemProvisionerStore.getInstance().requestDirectory(this.id);
|
||||
}
|
||||
|
||||
get description() {
|
||||
|
||||
@@ -10,7 +10,7 @@ export class LensMainExtension extends LensExtension {
|
||||
appMenus: MenuRegistration[] = [];
|
||||
|
||||
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
|
||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||
const windowManager = WindowManager.getInstance();
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: this.name,
|
||||
pageId,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry";
|
||||
import { LensExtension } from "../../lens-extension";
|
||||
import React from "react";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
let ext: LensExtension = null;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { themeStore } from "../../renderer/theme.store";
|
||||
import { ThemeStore } from "../../renderer/theme.store";
|
||||
|
||||
export function getActiveTheme() {
|
||||
return themeStore.activeTheme;
|
||||
return ThemeStore.getInstance().activeTheme;
|
||||
}
|
||||
Reference in new issue
Block a user