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:
authored and GitHub committed 2020-09-09 13:00:25 +03:00
1 parent 4250523fe4
commit f1b03990ea
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"
]
}
+22
View File
@@ -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";
+174
View File
@@ -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>()
+89
View File
@@ -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,
}
}
}
+19
View File
@@ -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,
}
}
+13
View File
@@ -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"
]
}