mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Extensions loading (#795)
Signed-off-by: Roman <ixrock@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
31 files changed
+564
-66
No files matched your search
@@ -0,0 +1,3 @@
|
||||
# Lens Example Extension
|
||||
|
||||
*TODO*: add more info
|
||||
@@ -0,0 +1,17 @@
|
||||
import { LensExtension, Icon, LensRuntimeRendererEnv } from "@lens/extensions"; // fixme: map to generated types from "extension-api.d.ts"
|
||||
|
||||
// todo: register custom icon in cluster-menu
|
||||
// todo: register custom view by clicking the item
|
||||
|
||||
export default class ExampleExtension extends LensExtension {
|
||||
async enable(runtime: /*LensRuntimeRendererEnv*/ any): Promise<any> {
|
||||
try {
|
||||
super.enable(runtime);
|
||||
runtime.logger.info('EXAMPLE EXTENSION: ENABLE() override');
|
||||
} catch (err){
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// <Icon material="camera" onClick={() => console.log("done")}/>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "extension-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Example extension",
|
||||
"main": "example-extension.ts",
|
||||
"lens": {
|
||||
"metadata": {}
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": ".",
|
||||
"module": "CommonJS",
|
||||
"sourceMap": false,
|
||||
"declaration": false
|
||||
},
|
||||
"include": [
|
||||
"../../../types",
|
||||
"./example-extension.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Lens-extensions api developer's kit
|
||||
export type { LensRuntimeRendererEnv } from "./lens-runtime";
|
||||
|
||||
// APIs
|
||||
export * from "./extension"
|
||||
|
||||
// Common UI components
|
||||
export * from "../renderer/components/icon"
|
||||
export * from "../renderer/components/badge"
|
||||
export * from "../renderer/components/tooltip"
|
||||
export * from "../renderer/components/button"
|
||||
export * from "../renderer/components/input"
|
||||
export * from "../renderer/components/select"
|
||||
export * from "../renderer/components/checkbox"
|
||||
export * from "../renderer/components/radio"
|
||||
export * from "../renderer/components/slider"
|
||||
export * from "../renderer/components/spinner"
|
||||
export * from "../renderer/components/tabs"
|
||||
export * from "../renderer/components/line-progress"
|
||||
|
||||
// Utils
|
||||
export * from "../main/logger";
|
||||
@@ -0,0 +1,174 @@
|
||||
import type { LensRuntimeRendererEnv } from "./lens-runtime";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import { action, observable, reaction, toJS, } from "mobx";
|
||||
import { BaseStore } from "../common/base-store";
|
||||
import { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./extension";
|
||||
import { isDevelopment, isProduction, isTestEnv } from "../common/vars";
|
||||
import logger from "../main/logger";
|
||||
|
||||
export interface ExtensionStoreModel {
|
||||
version: ExtensionVersion;
|
||||
extensions: Record<ExtensionId, ExtensionModel>
|
||||
}
|
||||
|
||||
export interface ExtensionModel {
|
||||
id?: ExtensionId; // available in lens-extension instance
|
||||
version: ExtensionVersion;
|
||||
name: string;
|
||||
manifestPath: string;
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
updateUrl?: string;
|
||||
}
|
||||
|
||||
export interface InstalledExtension<T extends ExtensionModel = any> {
|
||||
manifestPath: string;
|
||||
manifest: ExtensionManifest;
|
||||
extensionModule: {
|
||||
[name: string]: any;
|
||||
default: new (model: ExtensionModel, manifest?: ExtensionManifest) => LensExtension
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "lens-extension-store",
|
||||
syncEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
@observable version: ExtensionVersion = "0.0.0";
|
||||
@observable extensions = observable.map<ExtensionId, LensExtension>();
|
||||
@observable removed = observable.map<ExtensionId, LensExtension>();
|
||||
@observable installed = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
|
||||
|
||||
get folderPath(): string {
|
||||
if (isDevelopment) {
|
||||
return path.resolve(__static, "../src/extensions");
|
||||
}
|
||||
return path.resolve(__static, "../extensions"); //todo figure out prod
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.loadExtensions();
|
||||
return super.load();
|
||||
}
|
||||
|
||||
autoEnableOnLoad(getLensRuntimeEnv: () => LensRuntimeRendererEnv, { delay = 0 } = {}) {
|
||||
logger.info('[EXTENSIONS-STORE]: auto-activation loaded extensions: ON');
|
||||
return reaction(() => this.installed.toJS(), installedExtensions => {
|
||||
installedExtensions.forEach(({ extensionModule, manifest, manifestPath }) => {
|
||||
let instance = this.getById(manifestPath);
|
||||
if (!instance) {
|
||||
const LensExtensionClass = extensionModule.default;
|
||||
instance = new LensExtensionClass({ ...manifest, manifestPath, id: manifestPath }, manifest);
|
||||
instance.enable(getLensRuntimeEnv());
|
||||
this.extensions.set(manifestPath, instance); // save
|
||||
}
|
||||
})
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
delay: delay,
|
||||
})
|
||||
}
|
||||
|
||||
getExtensionByManifest(manifestPath: string): InstalledExtension {
|
||||
let manifestJson: ExtensionManifest;
|
||||
let mainJs: string;
|
||||
try {
|
||||
manifestJson = __non_webpack_require__(manifestPath); // "__non_webpack_require__" converts to native node's require()-call
|
||||
mainJs = path.resolve(path.dirname(manifestPath), manifestJson.main);
|
||||
mainJs = mainJs.replace(/\.ts$/i, ".js"); // todo: compile *.ts on the fly?
|
||||
const extensionModule = __non_webpack_require__(mainJs);
|
||||
return {
|
||||
manifestPath: manifestPath,
|
||||
manifest: manifestJson,
|
||||
extensionModule: extensionModule,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[EXTENSION-STORE]: can't load extension at ${manifestPath}: ${err}`, { manifestJson, mainJs });
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async loadExtensions() {
|
||||
const extensions = await this.loadFromFolder(this.folderPath);
|
||||
const extManifestMap = new Map(extensions.map(ext => [ext.manifestPath, ext]));
|
||||
this.installed.replace(extManifestMap);
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const paths = await fs.readdir(folderPath);
|
||||
const manifestsLoading = paths.map(fileName => {
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const manifestPath = path.resolve(absPath, "package.json");
|
||||
return fs.access(manifestPath, fs.constants.F_OK)
|
||||
.then(() => this.getExtensionByManifest(manifestPath))
|
||||
.catch(() => null)
|
||||
});
|
||||
let extensions = await Promise.all(manifestsLoading);
|
||||
extensions = extensions.filter(v => !!v); // filter out files and invalid folders (without manifest.json)
|
||||
console.info(`[EXTENSION-STORE]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
return extensions;
|
||||
}
|
||||
|
||||
getById(id: ExtensionId): LensExtension {
|
||||
return this.extensions.get(id);
|
||||
}
|
||||
|
||||
async removeById(id: ExtensionId) {
|
||||
const extension = this.getById(id);
|
||||
if (extension) {
|
||||
await extension.uninstall();
|
||||
this.extensions.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore({ extensions, version }: ExtensionStoreModel) {
|
||||
if (version) {
|
||||
this.version = version;
|
||||
}
|
||||
if (extensions) {
|
||||
const currentExtensions = new Map(Object.entries(extensions));
|
||||
this.extensions.forEach(extension => {
|
||||
if (!currentExtensions.has(extension.id)) {
|
||||
this.removed.set(extension.id, extension);
|
||||
}
|
||||
})
|
||||
currentExtensions.forEach(model => {
|
||||
const extensionId = model.id || model.manifestPath;
|
||||
const manifest = this.installed.get(extensionId);
|
||||
if (!manifest) {
|
||||
console.error(`[EXTENSION-STORE]: can't load extension manifest at ${model.manifestPath}`, { model })
|
||||
return;
|
||||
}
|
||||
const extensionInstance = this.getById(extensionId)
|
||||
if (!extensionInstance) {
|
||||
try {
|
||||
const { manifest: manifestJson, extensionModule } = manifest;
|
||||
const LensExtensionClass = extensionModule.default;
|
||||
this.extensions.set(model.id, new LensExtensionClass(model, manifestJson));
|
||||
} catch (err) {
|
||||
console.error(`[EXTENSION-STORE]: init extension failed: ${err}`, { model, manifest })
|
||||
}
|
||||
} else {
|
||||
extensionInstance.importModel(model);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): ExtensionStoreModel {
|
||||
return toJS({
|
||||
version: this.version,
|
||||
extensions: this.extensions.toJSON(),
|
||||
}, {
|
||||
recurseEverything: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionStore = ExtensionStore.getInstance<ExtensionStore>()
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { ExtensionModel } from "./extension-store";
|
||||
import type { LensRuntimeRendererEnv } from "./lens-runtime";
|
||||
import { readJsonSync } from "fs-extra";
|
||||
import { action, observable, toJS } from "mobx";
|
||||
import extensionManifest from "./example-extension/package.json"
|
||||
import logger from "../main/logger";
|
||||
|
||||
export type ExtensionId = string; // instance-id or abs path to "%lens-extension/manifest.json"
|
||||
export type ExtensionVersion = string | number;
|
||||
export type ExtensionManifest = typeof extensionManifest & ExtensionModel;
|
||||
|
||||
export class LensExtension implements ExtensionModel {
|
||||
public id: ExtensionId;
|
||||
public updateUrl: string;
|
||||
|
||||
@observable name = "";
|
||||
@observable description = "";
|
||||
@observable version: ExtensionVersion = "0.0.0";
|
||||
@observable manifest: ExtensionManifest;
|
||||
@observable manifestPath: string;
|
||||
@observable isEnabled = false;
|
||||
@observable.ref runtime: Partial<LensRuntimeRendererEnv> = {};
|
||||
|
||||
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
|
||||
this.importModel(model, manifest);
|
||||
}
|
||||
|
||||
@action
|
||||
async importModel({ enabled, manifestPath, ...model }: ExtensionModel, manifest?: ExtensionManifest) {
|
||||
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 enable(runtime: LensRuntimeRendererEnv) {
|
||||
this.isEnabled = true;
|
||||
this.runtime = runtime;
|
||||
console.log(`[EXTENSION]: enabled ${this.name}@${this.version}`, this.getMeta());
|
||||
}
|
||||
|
||||
async disable() {
|
||||
this.isEnabled = false;
|
||||
this.runtime = {};
|
||||
console.log(`[EXTENSION]: disabled ${this.name}@${this.version}`, this.getMeta());
|
||||
}
|
||||
|
||||
// todo
|
||||
async install(downloadUrl?: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
// todo
|
||||
async uninstall() {
|
||||
return;
|
||||
}
|
||||
|
||||
async hasNewVersion(): Promise<Partial<ExtensionModel>> {
|
||||
return;
|
||||
}
|
||||
|
||||
getMeta() {
|
||||
return toJS({
|
||||
id: this.id,
|
||||
manifest: this.manifest,
|
||||
manifestPath: this.manifestPath,
|
||||
enabled: this.isEnabled,
|
||||
runtime: this.runtime,
|
||||
}, {
|
||||
recurseEverything: true
|
||||
})
|
||||
}
|
||||
|
||||
toJSON(): ExtensionModel {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
description: this.description,
|
||||
manifestPath: this.manifestPath,
|
||||
enabled: this.isEnabled,
|
||||
updateUrl: this.updateUrl,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Lens runtime for injecting to extension on activation
|
||||
import { apiManager } from "../renderer/api/api-manager";
|
||||
import logger from "../main/logger";
|
||||
import { dynamicPages } from "../renderer/components/cluster-manager/register-page";
|
||||
|
||||
export interface LensRuntimeRendererEnv {
|
||||
apiManager: typeof apiManager;
|
||||
logger: typeof logger;
|
||||
dynamicPages: typeof dynamicPages
|
||||
}
|
||||
|
||||
// todo: expose more public runtime apis: stores, managers, etc.
|
||||
export function getLensRuntime(): LensRuntimeRendererEnv {
|
||||
return {
|
||||
apiManager,
|
||||
logger,
|
||||
dynamicPages,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "AMD",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outFile": "./../../static/types/extension-api.d.ts"
|
||||
},
|
||||
"include": [
|
||||
"../../types",
|
||||
"./extension-api.ts"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user