1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

refactoring & fixes

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-11-04 16:34:37 +02:00
parent 462386a206
commit 0446fd4e89
7 changed files with 117 additions and 118 deletions

View File

@ -2,7 +2,7 @@ import path from "path"
import Config from "conf" import Config from "conf"
import { Options as ConfOptions } from "conf/dist/source/types" import { Options as ConfOptions } from "conf/dist/source/types"
import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron" import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron"
import { action, observable, reaction, runInAction, toJS, when } from "mobx"; import { action, IReactionOptions, observable, reaction, runInAction, toJS, when } from "mobx";
import Singleton from "./utils/singleton"; import Singleton from "./utils/singleton";
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
import logger from "../main/logger"; import logger from "../main/logger";
@ -12,6 +12,7 @@ import isEqual from "lodash/isEqual";
export interface BaseStoreParams<T = any> extends ConfOptions<T> { export interface BaseStoreParams<T = any> extends ConfOptions<T> {
autoLoad?: boolean; autoLoad?: boolean;
syncEnabled?: boolean; syncEnabled?: boolean;
syncOptions?: IReactionOptions;
} }
export class BaseStore<T = any> extends Singleton { export class BaseStore<T = any> extends Singleton {
@ -20,7 +21,7 @@ export class BaseStore<T = any> extends Singleton {
whenLoaded = when(() => this.isLoaded); whenLoaded = when(() => this.isLoaded);
@observable isLoaded = false; @observable isLoaded = false;
@observable protected data: T; @observable data = {} as T;
protected constructor(protected params: BaseStoreParams) { protected constructor(protected params: BaseStoreParams) {
super(); super();
@ -36,8 +37,12 @@ export class BaseStore<T = any> extends Singleton {
return path.basename(this.storeConfig.path); return path.basename(this.storeConfig.path);
} }
get path() {
return this.storeConfig.path;
}
get syncChannel() { get syncChannel() {
return `store-sync:${this.name}` return `STORE-SYNC:${this.path}`
} }
protected async init() { protected async init() {
@ -56,19 +61,19 @@ export class BaseStore<T = any> extends Singleton {
...confOptions, ...confOptions,
projectName: "lens", projectName: "lens",
projectVersion: getAppVersion(), projectVersion: getAppVersion(),
cwd: this.storePath(), cwd: this.cwd(),
}); });
logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`); logger.info(`[STORE]: LOADED from ${this.path}`);
this.fromStore(this.storeConfig.store); this.fromStore(this.storeConfig.store);
this.isLoaded = true; this.isLoaded = true;
} }
protected storePath() { protected cwd() {
return (app || remote.app).getPath("userData") return (app || remote.app).getPath("userData")
} }
protected async saveToFile(model: T) { protected async saveToFile(model: T) {
logger.info(`[STORE]: SAVING ${this.name}`); logger.info(`[STORE]: SAVING ${this.path}`);
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114 // todo: update when fixed https://github.com/sindresorhus/conf/issues/114
Object.entries(model).forEach(([key, value]) => { Object.entries(model).forEach(([key, value]) => {
this.storeConfig.set(key, value); this.storeConfig.set(key, value);
@ -77,7 +82,7 @@ export class BaseStore<T = any> extends Singleton {
enableSync() { enableSync() {
this.syncDisposers.push( this.syncDisposers.push(
reaction(() => this.toJSON(), model => this.onModelChange(model)), reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions),
); );
if (ipcMain) { if (ipcMain) {
const callback = (event: IpcMainEvent, model: T) => { const callback = (event: IpcMainEvent, model: T) => {
@ -169,6 +174,7 @@ export class BaseStore<T = any> extends Singleton {
@action @action
protected fromStore(data: T) { protected fromStore(data: T) {
if (!data) return;
this.data = data; this.data = data;
} }

View File

@ -1,42 +1,28 @@
import type { LensExtension, LensExtensionConstructor, LensExtensionId, LensExtensionManifest, LensExtensionStoreModel } from "./lens-extension" import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension"
import type { LensMainExtension } from "./lens-main-extension" import type { LensMainExtension } from "./lens-main-extension"
import type { LensRendererExtension } from "./lens-renderer-extension" import type { LensRendererExtension } from "./lens-renderer-extension"
import type { InstalledExtension } from "./extension-manager";
import path from "path" import path from "path"
import { broadcastIpc } from "../common/ipc" import { broadcastIpc } from "../common/ipc"
import { action, autorun, computed, observable, reaction, toJS } from "mobx" import { computed, observable, reaction, when } from "mobx"
import logger from "../main/logger" import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron" import { app, ipcRenderer, remote } from "electron"
import { BaseStore } from "../common/base-store";
import * as registries from "./registries"; import * as registries from "./registries";
export interface ExtensionLoaderStoreModel {
extensions: LensExtensionStoreModel[]
}
export interface InstalledExtension {
manifest: LensExtensionManifest;
manifestPath: string;
isBundled?: boolean; // defined in package.json
}
// lazy load so that we get correct userData // lazy load so that we get correct userData
export function extensionPackagesRoot() { export function extensionPackagesRoot() {
return path.join((app || remote.app).getPath("userData")) return path.join((app || remote.app).getPath("userData"))
} }
export class ExtensionLoader extends BaseStore<ExtensionLoaderStoreModel> { export class ExtensionLoader {
protected disposers = new Map<LensExtensionId, Function[]>(); @observable isLoaded = false;
protected extensions = observable.map<LensExtensionId, InstalledExtension>([], { deep: false });
@observable extensions = observable.map<LensExtensionId, InstalledExtension>([], { deep: false }); protected instances = observable.map<LensExtensionId, LensExtension>([], { deep: false })
@observable instances = observable.map<LensExtensionId, LensExtension>([], { deep: false })
@observable state = observable.map<LensExtensionId, LensExtensionStoreModel>();
constructor() { constructor() {
super({
configName: "lens-extensions",
});
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => { ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
this.isLoaded = true;
extensions.forEach((ext) => { extensions.forEach((ext) => {
if (!this.extensions.has(ext.manifestPath)) { if (!this.extensions.has(ext.manifestPath)) {
this.extensions.set(ext.manifestPath, ext) this.extensions.set(ext.manifestPath, ext)
@ -50,53 +36,41 @@ export class ExtensionLoader extends BaseStore<ExtensionLoaderStoreModel> {
return [...this.instances.values()].filter(ext => !ext.isBundled) return [...this.instances.values()].filter(ext => !ext.isBundled)
} }
async init() {
const { extensionManager } = await import("./extension-manager");
const installedExtensions = await extensionManager.load();
this.extensions.replace(installedExtensions);
this.isLoaded = true;
this.loadOnMain();
}
loadOnMain() { loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main') logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoInitExtensions(); this.autoInitExtensions((extension: LensMainExtension) => [
this.autoEnableExtensions((extension: LensMainExtension) => [
registries.menuRegistry.add(...extension.appMenus) registries.menuRegistry.add(...extension.appMenus)
]) ]);
} }
loadOnClusterManagerRenderer() { loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)') logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoInitExtensions(); this.autoInitExtensions((extension: LensRendererExtension) => [
this.autoEnableExtensions((extension: LensRendererExtension) => [
registries.globalPageRegistry.add(...extension.globalPages), registries.globalPageRegistry.add(...extension.globalPages),
registries.appPreferenceRegistry.add(...extension.appPreferences), registries.appPreferenceRegistry.add(...extension.appPreferences),
registries.clusterFeatureRegistry.add(...extension.clusterFeatures), registries.clusterFeatureRegistry.add(...extension.clusterFeatures),
registries.statusBarRegistry.add(...extension.statusBarItems), registries.statusBarRegistry.add(...extension.statusBarItems),
]) ]);
} }
loadOnClusterRenderer() { loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)') logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoInitExtensions(); this.autoInitExtensions((extension: LensRendererExtension) => [
this.autoEnableExtensions((extension: LensRendererExtension) => [
registries.clusterPageRegistry.add(...extension.clusterPages), registries.clusterPageRegistry.add(...extension.clusterPages),
registries.kubeObjectMenuRegistry.add(...extension.kubeObjectMenuItems), registries.kubeObjectMenuRegistry.add(...extension.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(...extension.kubeObjectDetailItems), registries.kubeObjectDetailRegistry.add(...extension.kubeObjectDetailItems),
]) ]);
} }
protected autoEnableExtensions(register: (ext: LensExtension) => Function[]) { protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
return autorun(() => {
this.instances.forEach(ext => {
const extensionState = this.state.get(ext.id);
const isEnabled = !extensionState /*enabled by default*/ || extensionState.isEnabled;
const isRegistered = this.disposers.has(ext.id);
if (isEnabled && !isRegistered) {
this.disposers.set(ext.id, register(ext))
} else if (!isEnabled && isRegistered) {
this.disposers.get(ext.id).forEach(dispose => dispose())
this.disposers.delete(ext.id)
}
ext.toggle(isEnabled);
})
})
}
protected autoInitExtensions() {
return reaction(() => this.extensions.toJS(), (installedExtensions) => { return reaction(() => this.extensions.toJS(), (installedExtensions) => {
for (const [id, ext] of installedExtensions) { for (const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.manifestPath) let instance = this.instances.get(ext.manifestPath)
@ -108,6 +82,7 @@ export class ExtensionLoader extends BaseStore<ExtensionLoaderStoreModel> {
try { try {
const LensExtensionClass: LensExtensionConstructor = extensionModule.default; const LensExtensionClass: LensExtensionConstructor = extensionModule.default;
instance = new LensExtensionClass(ext); instance = new LensExtensionClass(ext);
instance.whenEnabled(() => register(instance));
this.instances.set(ext.manifestPath, instance); this.instances.set(ext.manifestPath, instance);
} catch (err) { } catch (err) {
logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err }) logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err })
@ -136,7 +111,8 @@ export class ExtensionLoader extends BaseStore<ExtensionLoaderStoreModel> {
} }
} }
broadcastExtensions(frameId?: number) { async broadcastExtensions(frameId?: number) {
await when(() => this.isLoaded);
broadcastIpc({ broadcastIpc({
channel: "extensions:loaded", channel: "extensions:loaded",
frameId: frameId, frameId: frameId,
@ -146,21 +122,6 @@ export class ExtensionLoader extends BaseStore<ExtensionLoaderStoreModel> {
], ],
}) })
} }
@action
protected fromStore({ extensions = [] }: ExtensionLoaderStoreModel) {
extensions.forEach(ext => {
this.state.set(ext.id, ext);
})
}
toJSON(): ExtensionLoaderStoreModel {
return toJS({
extensions: this.userExtensions.map(ext => ext.toJSON())
}, {
recurseEverything: true,
})
}
} }
export const extensionLoader: ExtensionLoader = ExtensionLoader.getInstance(); export const extensionLoader = new ExtensionLoader();

View File

@ -1,12 +1,18 @@
import type { LensExtensionManifest } from "./lens-extension" import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"
import path from "path" import path from "path"
import os from "os" import os from "os"
import fs from "fs-extra" import fs from "fs-extra"
import child_process from "child_process";
import logger from "../main/logger" import logger from "../main/logger"
import { extensionPackagesRoot, InstalledExtension } from "./extension-loader" import { extensionPackagesRoot } from "./extension-loader"
import * as child_process from 'child_process';
import { getBundledExtensions } from "../common/utils/app-version" import { getBundledExtensions } from "../common/utils/app-version"
export interface InstalledExtension {
manifest: LensExtensionManifest;
manifestPath: string;
isBundled?: boolean; // defined in package.json
}
type Dependencies = { type Dependencies = {
[name: string]: string; [name: string]: string;
} }
@ -51,7 +57,7 @@ export class ExtensionManager {
return path.join(this.extensionPackagesRoot, "package.json") return path.join(this.extensionPackagesRoot, "package.json")
} }
async load(): Promise<Map<string, InstalledExtension>> { async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) { if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json")) await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"))
@ -71,7 +77,7 @@ export class ExtensionManager {
return await this.loadExtensions(); return await this.loadExtensions();
} }
async getByManifest(manifestPath: string): Promise<InstalledExtension> { protected async getByManifest(manifestPath: string): Promise<InstalledExtension> {
let manifestJson: LensExtensionManifest; let manifestJson: LensExtensionManifest;
try { try {
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
@ -81,7 +87,7 @@ export class ExtensionManager {
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name) logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
return { return {
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"), manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson manifest: manifestJson,
} }
} catch (err) { } catch (err) {
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson }); logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });

View File

@ -15,7 +15,7 @@ export class ExtensionStore<T = any> extends BaseStore<T> {
await super.load() await super.load()
} }
protected storePath() { protected cwd() {
return path.join(super.storePath(), "extension-store", this.extension.name) return path.join(super.cwd(), "extension-store", this.extension.name)
} }
} }

View File

@ -1,15 +1,10 @@
import { action, observable, toJS } from "mobx"; import type { InstalledExtension } from "./extension-manager";
import { action, reaction } from "mobx";
import logger from "../main/logger"; import logger from "../main/logger";
import type { InstalledExtension } from "./extension-loader"; import { ExtensionStore } from "./extension-store";
export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (init: InstalledExtension) => LensExtension; export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
export interface LensExtensionStoreModel {
id: LensExtensionId;
name: string;
isEnabled?: boolean;
}
export interface LensExtensionManifest { export interface LensExtensionManifest {
name: string; name: string;
@ -19,17 +14,35 @@ export interface LensExtensionManifest {
renderer?: string; // path to %ext/dist/renderer.js renderer?: string; // path to %ext/dist/renderer.js
} }
export class LensExtension { export interface LensExtensionStoreModel {
public manifest: LensExtensionManifest; isEnabled: boolean;
public manifestPath: string; }
public isBundled: boolean;
@observable isEnabled = false; export class LensExtension<S extends ExtensionStore<LensExtensionStoreModel> = any> {
protected store: S;
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean;
constructor({ manifest, manifestPath, isBundled }: InstalledExtension) { constructor({ manifest, manifestPath, isBundled }: InstalledExtension) {
this.manifest = manifest this.manifest = manifest
this.manifestPath = manifestPath this.manifestPath = manifestPath
this.isBundled = !!isBundled this.isBundled = !!isBundled
this.init();
}
protected async init(store: S = createBaseStore().getInstance()) {
this.store = store;
await this.store.loadExtension(this);
reaction(() => this.store.data.isEnabled, (isEnabled = true) => {
this.toggle(isEnabled); // handle activation & deactivation
}, {
fireImmediately: true
});
}
get isEnabled() {
return !!this.store.data.isEnabled;
} }
get id(): LensExtensionId { get id(): LensExtensionId {
@ -51,7 +64,7 @@ export class LensExtension {
@action @action
async enable() { async enable() {
if (this.isEnabled) return; if (this.isEnabled) return;
this.isEnabled = true; this.store.data.isEnabled = true;
this.onActivate(); this.onActivate();
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
} }
@ -59,7 +72,7 @@ export class LensExtension {
@action @action
async disable() { async disable() {
if (!this.isEnabled) return; if (!this.isEnabled) return;
this.isEnabled = false; this.store.data.isEnabled = false;
this.onDeactivate(); this.onDeactivate();
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
} }
@ -72,6 +85,27 @@ export class LensExtension {
} }
} }
async whenEnabled(handlers: () => Function[]) {
const disposers: Function[] = [];
const unregisterHandlers = () => {
disposers.forEach(unregister => unregister())
disposers.length = 0;
}
const cancelReaction = reaction(() => this.isEnabled, isEnabled => {
if (isEnabled) {
disposers.push(...handlers());
} else {
unregisterHandlers();
}
}, {
fireImmediately: true
})
return () => {
unregisterHandlers();
cancelReaction();
}
}
protected onActivate() { protected onActivate() {
// mock // mock
} }
@ -79,14 +113,14 @@ export class LensExtension {
protected onDeactivate() { protected onDeactivate() {
// mock // mock
} }
}
toJSON(): LensExtensionStoreModel { function createBaseStore() {
return toJS({ return class extends ExtensionStore<LensExtensionStoreModel> {
id: this.id, constructor() {
name: this.name, super({
isEnabled: this.isEnabled, configName: "state"
}, { });
recurseEverything: true, }
})
} }
} }

View File

@ -15,13 +15,12 @@ import { shellSync } from "./shell-sync"
import { getFreePort } from "./port" import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env" import { mangleProxyEnv } from "./proxy-env"
import { registerFileProtocol } from "../common/register-protocol"; import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger"
import { clusterStore } from "../common/cluster-store" import { clusterStore } from "../common/cluster-store"
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store"; import { workspaceStore } from "../common/workspace-store";
import { appEventBus } from "../common/event-bus" import { appEventBus } from "../common/event-bus"
import { extensionManager } from "../extensions/extension-manager";
import { extensionLoader } from "../extensions/extension-loader"; import { extensionLoader } from "../extensions/extension-loader";
import logger from "./logger"
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number; let proxyPort: number;
@ -48,12 +47,11 @@ app.on("ready", async () => {
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
// preload isomorphic stores // preload
await Promise.all([ await Promise.all([
userStore.load(), userStore.load(),
clusterStore.load(), clusterStore.load(),
workspaceStore.load(), workspaceStore.load(),
extensionLoader.load(),
]); ]);
// find free port // find free port
@ -77,12 +75,8 @@ app.on("ready", async () => {
app.exit(); app.exit();
} }
windowManager = new WindowManager(proxyPort); LensExtensionsApi.windowManager = windowManager = new WindowManager(proxyPort);
extensionLoader.init(); // call after windowManager to see splash earlier
LensExtensionsApi.windowManager = windowManager; // expose to extensions
extensionLoader.loadOnMain()
extensionLoader.extensions.replace(await extensionManager.load())
extensionLoader.broadcastExtensions()
setTimeout(() => { setTimeout(() => {
appEventBus.emit({ name: "app", action: "start" }) appEventBus.emit({ name: "app", action: "start" })

View File

@ -13,7 +13,6 @@ import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store"; import { themeStore } from "./theme.store";
import { App } from "./components/app"; import { App } from "./components/app";
import { LensApp } from "./lens-app"; import { LensApp } from "./lens-app";
import { extensionLoader } from "../extensions/extension-loader";
type AppComponent = React.ComponentType & { type AppComponent = React.ComponentType & {
init?(): Promise<void>; init?(): Promise<void>;
@ -35,7 +34,6 @@ export async function bootstrap(App: AppComponent) {
userStore.load(), userStore.load(),
workspaceStore.load(), workspaceStore.load(),
clusterStore.load(), clusterStore.load(),
extensionLoader.load(),
i18nStore.init(), i18nStore.init(),
themeStore.init(), themeStore.init(),
]); ]);