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

refactoring base lens-extension.ts, added isBundled flag

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-11-02 17:35:17 +02:00
parent 1b3251ae2e
commit 3776aca28b
4 changed files with 77 additions and 118 deletions

View File

@ -1,4 +1,4 @@
import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } from "./lens-extension" import type { LensExtension, LensExtensionConstructor, LensExtensionManifest } 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 path from "path" import path from "path"
@ -6,15 +6,14 @@ import { broadcastIpc } from "../common/ipc"
import { computed, observable, reaction, toJS, } from "mobx" import { computed, observable, reaction, toJS, } 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 { import * as registries from "./registries";
appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry,
kubeObjectDetailRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry
} from "./registries";
import { getBundledExtensions } from "../common/utils"
export interface InstalledExtension extends ExtensionModel { type ExtensionManifestPath = string; // path to package.json
export interface InstalledExtension {
manifestPath: string; manifestPath: string;
manifest: ExtensionManifest; manifest: LensExtensionManifest;
isBundled?: boolean; // defined in package.json
} }
// lazy load so that we get correct userData // lazy load so that we get correct userData
@ -23,14 +22,14 @@ export function extensionPackagesRoot() {
} }
export class ExtensionLoader { export class ExtensionLoader {
@observable extensions = observable.map<ExtensionId, InstalledExtension>([], { deep: false }); @observable extensions = observable.map<ExtensionManifestPath, InstalledExtension>([], { deep: false });
@observable instances = observable.map<ExtensionId, LensExtension>([], { deep: false }) @observable instances = observable.map<ExtensionManifestPath, LensExtension>([], { deep: false })
constructor() { constructor() {
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => { ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
extensions.forEach((ext) => { extensions.forEach((ext) => {
if (!this.getById(ext.manifestPath)) { if (!this.getByManifest(ext.manifestPath)) {
this.extensions.set(ext.manifestPath, ext) this.extensions.set(ext.manifestPath, ext)
} }
}) })
@ -39,57 +38,52 @@ export class ExtensionLoader {
} }
@computed get userExtensions(): LensExtension[] { @computed get userExtensions(): LensExtension[] {
const builtIn = getBundledExtensions().map(ext => `lens-${ext}`) return [...this.instances.values()].filter(ext => !ext.isBundled)
const extensions: LensExtension[] = []
this.instances.forEach(instance => {
if (builtIn.includes(instance.name)) return
extensions.push(instance)
})
return extensions
} }
loadOnMain() { loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main') logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions((extension: LensMainExtension) => { this.autoloadExtensions((extension: LensMainExtension) => {
extension.registerTo(menuRegistry, extension.appMenus) extension.registerTo(registries.menuRegistry, 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.autoloadExtensions((extension: LensRendererExtension) => { this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(globalPageRegistry, extension.globalPages) extension.registerTo(registries.globalPageRegistry, extension.globalPages)
extension.registerTo(appPreferenceRegistry, extension.appPreferences) extension.registerTo(registries.appPreferenceRegistry, extension.appPreferences)
extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures) extension.registerTo(registries.clusterFeatureRegistry, extension.clusterFeatures)
extension.registerTo(statusBarRegistry, extension.statusBarItems) extension.registerTo(registries.statusBarRegistry, extension.statusBarItems)
}) })
} }
loadOnClusterRenderer() { loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)') logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoloadExtensions((extension: LensRendererExtension) => { this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(clusterPageRegistry, extension.clusterPages) extension.registerTo(registries.clusterPageRegistry, extension.clusterPages)
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems) extension.registerTo(registries.kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
extension.registerTo(kubeObjectDetailRegistry, extension.kubeObjectDetailItems) extension.registerTo(registries.kubeObjectDetailRegistry, extension.kubeObjectDetailItems)
}) })
} }
protected autoloadExtensions(callback: (instance: LensExtension) => void) { protected autoloadExtensions(callback: (instance: LensExtension) => void) {
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.id) let instance = this.instances.get(ext.manifestPath)
if (!instance) { if (!instance) {
const extensionModule = this.requireExtension(ext) const extensionModule = this.requireExtension(ext)
if (!extensionModule) { if (!extensionModule) {
continue continue
} }
const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
try { try {
const LensExtensionClass: LensExtensionConstructor = extensionModule.default;
instance = new LensExtensionClass(ext);
instance.enable() instance.enable()
callback(instance) callback(instance)
} finally { this.instances.set(ext.manifestPath, instance)
this.instances.set(ext.id, instance) } catch (err) {
logger.error(`[EXTENSIONS-LOADER]: activating extension error`, { ext, err })
} }
} }
} }
@ -116,19 +110,8 @@ export class ExtensionLoader {
} }
} }
getById(id: ExtensionId): InstalledExtension { getByManifest(manifestPath: ExtensionManifestPath): InstalledExtension {
return this.extensions.get(id); return this.extensions.get(manifestPath);
}
async removeById(id: ExtensionId) {
const extension = this.getById(id);
if (extension) {
const instance = this.instances.get(extension.id)
if (instance) {
await instance.disable()
}
this.extensions.delete(id);
}
} }
broadcastExtensions(frameId?: number) { broadcastExtensions(frameId?: number) {

View File

@ -1,4 +1,4 @@
import type { ExtensionManifest } from "./lens-extension" import type { 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"
@ -51,7 +51,7 @@ export class ExtensionManager {
return path.join(this.extensionPackagesRoot, "package.json") return path.join(this.extensionPackagesRoot, "package.json")
} }
async load() { async load(): Promise<Map<string, 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,8 +71,8 @@ export class ExtensionManager {
return await this.loadExtensions(); return await this.loadExtensions();
} }
async getExtensionByManifest(manifestPath: string): Promise<InstalledExtension> { async getByManifest(manifestPath: string): Promise<InstalledExtension> {
let manifestJson: ExtensionManifest; 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
manifestJson = __non_webpack_require__(manifestPath) manifestJson = __non_webpack_require__(manifestPath)
@ -80,9 +80,6 @@ export class ExtensionManager {
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name) logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
return { return {
id: manifestJson.name,
version: manifestJson.version,
name: manifestJson.name,
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"), manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson manifest: manifestJson
} }
@ -109,10 +106,10 @@ export class ExtensionManager {
async loadExtensions() { async loadExtensions() {
const bundledExtensions = await this.loadBundledExtensions() const bundledExtensions = await this.loadBundledExtensions()
const localExtensions = await this.loadFromFolder(this.localFolderPath) const localExtensions = await this.loadFromFolder(this.localFolderPath)
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), {mode: 0o600}) await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 })
await this.installPackages() await this.installPackages()
const extensions = bundledExtensions.concat(localExtensions) const extensions = bundledExtensions.concat(localExtensions)
return new Map(extensions.map(ext => [ext.id, ext])); return new Map(extensions.map(ext => [ext.manifestPath, ext]));
} }
async loadBundledExtensions() { async loadBundledExtensions() {
@ -126,8 +123,9 @@ export class ExtensionManager {
} }
const absPath = path.resolve(folderPath, fileName); const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json"); const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null) const ext = await this.getByManifest(manifestPath).catch(() => null)
if (ext) { if (ext) {
ext.isBundled = true;
extensions.push(ext) extensions.push(ext)
} }
} }
@ -148,7 +146,7 @@ export class ExtensionManager {
continue; continue;
} }
const manifestPath = path.resolve(absPath, "package.json"); const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null) const ext = await this.getByManifest(manifestPath).catch(() => null)
if (ext) { if (ext) {
extensions.push(ext) extensions.push(ext)
} }

View File

@ -1,58 +1,42 @@
import { readJsonSync } from "fs-extra"; import { observable, toJS } from "mobx";
import { action, observable, toJS } from "mobx";
import logger from "../main/logger"; import logger from "../main/logger";
import { BaseRegistry } from "./registries/base-registry"; import { BaseRegistry } from "./registries/base-registry";
import type { InstalledExtension } from "./extension-loader";
export type ExtensionId = string | ExtensionPackageJsonPath; export type LensExtensionConstructor = new (init: InstalledExtension) => LensExtension;
export type ExtensionPackageJsonPath = string;
export type ExtensionVersion = string | number;
export interface ExtensionModel { export interface LensExtensionManifest {
id: ExtensionId;
version: ExtensionVersion;
name: string; name: string;
manifestPath: string; version: string;
description?: string; description?: string;
enabled?: boolean; main?: string; // path to %ext/dist/main.js
updateUrl?: string; renderer?: string; // path to %ext/dist/renderer.js
} }
export interface ExtensionManifest extends ExtensionModel { export class LensExtension {
main?: string; public manifest: LensExtensionManifest;
renderer?: string; public manifestPath: string;
description?: string; // todo: add more fields similar to package.json + some extra public isBundled: boolean;
}
export class LensExtension implements ExtensionModel {
public id: ExtensionId;
public updateUrl: string;
protected disposers: (() => void)[] = []; protected disposers: (() => void)[] = [];
@observable name = "";
@observable description = "";
@observable version: ExtensionVersion = "0.0.0";
@observable manifest: ExtensionManifest;
@observable manifestPath: string;
@observable isEnabled = false; @observable isEnabled = false;
constructor(model: ExtensionModel, manifest: ExtensionManifest) { constructor({ manifest, manifestPath, isBundled }: InstalledExtension) {
this.importModel(model, manifest); this.manifest = manifest
this.manifestPath = manifestPath
this.isBundled = !!isBundled
} }
@action get name() {
async importModel({ enabled, manifestPath, ...model }: ExtensionModel, manifest?: ExtensionManifest) { return this.manifest.name
try {
this.manifest = manifest || await readJsonSync(manifestPath, { throws: true })
this.manifestPath = manifestPath;
Object.assign(this, model);
} catch (err) {
logger.error(`[EXTENSION]: cannot read manifest at ${manifestPath}`, { ...model, err: String(err) })
this.disable();
}
} }
async migrate(appVersion: string) { get version() {
// mock return this.manifest.version
}
get description() {
return this.manifest.description
} }
async enable() { async enable() {
@ -69,7 +53,6 @@ export class LensExtension implements ExtensionModel {
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
} }
// todo: add more hooks
protected onActivate() { protected onActivate() {
// mock // mock
} }
@ -86,26 +69,13 @@ export class LensExtension implements ExtensionModel {
}; };
} }
getMeta() { toJSON() {
return toJS({ return toJS({
id: this.id,
manifest: this.manifest,
manifestPath: this.manifestPath,
enabled: this.isEnabled
}, {
recurseEverything: true
})
}
toJSON(): ExtensionModel {
return toJS({
id: this.id,
name: this.name, name: this.name,
version: this.version, version: this.version,
description: this.description, description: this.description,
manifestPath: this.manifestPath, manifestPath: this.manifestPath,
enabled: this.isEnabled, isEnabled: this.isEnabled,
updateUrl: this.updateUrl,
}, { }, {
recurseEverything: true, recurseEverything: true,
}) })

View File

@ -1,5 +1,4 @@
import "./extensions.scss"; import "./extensions.scss";
import { shell } from "electron"; import { shell } from "electron";
import React from "react"; import React from "react";
import { computed, observable } from "mobx"; import { computed, observable } from "mobx";
@ -18,8 +17,13 @@ export class Extensions extends React.Component {
@observable search = "" @observable search = ""
@computed get extensions() { @computed get extensions() {
const extensions = extensionLoader.userExtensions const searchText = this.search.toLowerCase();
return extensions.filter(ext => ext.name.includes(this.search)) return extensionLoader.userExtensions.filter(({ name, description }) => {
return [
name.toLowerCase().includes(searchText),
description.toLowerCase().includes(searchText),
].some(v => v)
})
} }
get extensionsPath() { get extensionsPath() {
@ -62,12 +66,16 @@ export class Extensions extends React.Component {
</div> </div>
) )
} }
return extensions.map(({ id, name, description }) => { return extensions.map(({ manifestPath, name, description }) => {
return ( return (
<div key={id} className="extension flex gaps align-center"> <div key={manifestPath} className="extension flex gaps align-center">
<div className="box grow flex column gaps"> <div className="box grow flex column gaps">
<div className="package">Package: <code className="name">{name}</code></div> <div className="package">
<div>Description: <span className="text-secondary">{description}</span></div> Name: <code className="name">{name}</code>
</div>
<div>
Description: <span className="text-secondary">{description}</span>
</div>
</div> </div>
<Button plain active onClick={() => console.log(`//todo: disable ${name}`)}> <Button plain active onClick={() => console.log(`//todo: disable ${name}`)}>
Disable Disable