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:
Sebastian Malton authored and GitHub committed 2021-04-21 09:59:59 -04:00
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;
+2 -2
View File
@@ -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`);
+2 -2
View File
@@ -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 -2
View File
@@ -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);
}
}
+7 -7
View File
@@ -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 -6
View File
@@ -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();
-2
View File
@@ -53,5 +53,3 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
});
}
}
export const extensionsStore = new ExtensionsStore();
+2 -2
View File
@@ -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() {
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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;
}