support extensions in main process

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen committed 2020-10-06 14:37:42 +03:00
1 parent 2b6f283e1b
commit 8a793c01db
24 files changed
+227 -136

No files matched your search

+3
View File
@@ -24,6 +24,7 @@ export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
// Extensions
export const extensionsLibName = `${appName}-extensions.api`
export const extensionsRendererLibName = `${appName}-extensions-renderer.api`
export const extensionsDir = path.join(contextDir, "src/extensions");
// Special runtime paths
@@ -39,8 +40,10 @@ defineGlobal("__static", {
// Special dynamic module aliases
if (isProduction && process.resourcesPath) {
addAlias("@lens/extensions", path.join(process.resourcesPath, "static", `build/${extensionsLibName}.js`))
addAlias("@lens/ui-extensions", path.join(process.resourcesPath, "static", `build/${extensionsRendererLibName}.js`))
} else {
addAlias("@lens/extensions", path.join(contextDir, "static", `build/${extensionsLibName}.js`))
addAlias("@lens/ui-extensions", path.join(contextDir, "static", `build/${extensionsRendererLibName}.js`))
}
// Apis
+2 -11
View File
@@ -1,14 +1,5 @@
// Lens-extensions api developer's kit
export type { LensRuntimeRendererEnv } from "./lens-runtime";
export type { LensExtensionRuntimeEnv } from "./lens-runtime";
// APIs
export * from "./lens-extension"
export { DynamicPageType } from "./register-page";
// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
export { default as React } from "react"
export * from "../renderer/components/icon"
export * from "../renderer/components/tooltip"
export * from "../renderer/components/button"
export * from "../renderer/components/tabs"
export * from "../renderer/components/badge"
export * from "./lens-main-extension"
+40 -9
View File
@@ -1,10 +1,12 @@
import type { ExtensionId, LensExtension, ExtensionManifest, ExtensionModel } from "./lens-extension"
import type { LensRendererExtension } from "./lens-renderer-extension"
import { broadcastIpc } from "../common/ipc"
import type { LensRuntimeRendererEnv } from "./lens-runtime"
import type { LensExtensionRuntimeEnv } from "./lens-runtime"
import path from "path"
import { observable, reaction, toJS, } from "mobx"
import logger from "../main/logger"
import { app, remote, ipcRenderer } from "electron"
import { pageStore } from "./page-store";
export interface InstalledExtension extends ExtensionModel {
manifestPath: string;
@@ -42,8 +44,28 @@ export class ExtensionLoader {
}
}
autoEnableOnLoad(getLensRuntimeEnv: () => LensRuntimeRendererEnv, { delay = 0 } = {}) {
logger.info('[EXTENSIONS-LOADER]: auto-activation loaded extensions: ON');
loadOnClusterRenderer(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer')
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensRendererExtension) => {
instance.registerPages(pageStore)
})
}
loadOnMainRenderer(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) {
logger.info('[EXTENSIONS-LOADER]: load on main renderer')
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensRendererExtension) => {
instance.registerPages(pageStore)
})
}
loadOnMain(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensExtension) => {
// todo
})
}
protected autoloadExtensions(getLensRuntimeEnv: () => LensExtensionRuntimeEnv, callback: (instance: LensExtension) => void) {
return reaction(() => this.extensions.toJS(), installedExtensions => {
installedExtensions.forEach((ext) => {
let instance = this.instances.get(ext.manifestPath)
@@ -55,23 +77,32 @@ export class ExtensionLoader {
}
const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
instance.enable(getLensRuntimeEnv());
instance.enable(getLensRuntimeEnv())
callback(instance)
this.instances.set(ext.id, instance)
}
})
}, {
fireImmediately: true,
delay: delay,
delay: 0,
})
}
protected requireExtension(extension: InstalledExtension) {
let extEntrypoint = ""
return withExtensionPackagesRoot(() => {
try {
const extMain = path.join(path.dirname(extension.manifestPath), extension.manifest.main)
return __non_webpack_require__(extMain)
if (ipcRenderer) {
extEntrypoint = path.join(path.dirname(extension.manifestPath), extension.manifest.renderer)
} else {
extEntrypoint = path.join(path.dirname(extension.manifestPath), extension.manifest.main)
}
if (extEntrypoint !== "") {
return __non_webpack_require__(extEntrypoint)
}
} catch (err) {
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extension.manifestPath}: ${err}`, { extension });
console.trace(err)
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
}
})
}
@@ -84,7 +115,7 @@ export class ExtensionLoader {
const extension = this.getById(id);
if (extension) {
const instance = this.instances.get(extension.id)
if (instance) { await instance.uninstall() }
if (instance) { await instance.disable() }
this.extensions.delete(id);
}
}
+14
View File
@@ -0,0 +1,14 @@
// Lens-extensions api developer's kit
export type { LensExtensionRuntimeEnv, PageStore } from "./lens-runtime";
// APIs
export * from "./lens-renderer-extension"
export { DynamicPageType } from "./page-store";
// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
export { default as React } from "react"
export * from "../renderer/components/icon"
export * from "../renderer/components/tooltip"
export * from "../renderer/components/button"
export * from "../renderer/components/tabs"
export * from "../renderer/components/badge"
+12 -31
View File
@@ -1,5 +1,4 @@
import type { LensRuntimeRendererEnv } from "./lens-runtime";
import type { PageRegistration } from "./register-page";
import type { LensExtensionRuntimeEnv } from "./lens-runtime";
import { readJsonSync } from "fs-extra";
import { action, observable, toJS } from "mobx";
import logger from "../main/logger";
@@ -19,7 +18,8 @@ export interface ExtensionModel {
}
export interface ExtensionManifest extends ExtensionModel {
main: string;
main?: string;
renderer?: string;
description?: string; // todo: add more fields similar to package.json + some extra
}
@@ -34,7 +34,7 @@ export class LensExtension implements ExtensionModel {
@observable manifest: ExtensionManifest;
@observable manifestPath: string;
@observable isEnabled = false;
@observable.ref runtime: LensRuntimeRendererEnv;
@observable.ref runtime: LensExtensionRuntimeEnv;
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
this.importModel(model, manifest);
@@ -52,10 +52,14 @@ export class LensExtension implements ExtensionModel {
}
}
async enable(runtime: LensRuntimeRendererEnv) {
async migrate(appVersion: string) {
// mock
}
async enable(runtime: LensExtensionRuntimeEnv) {
this.isEnabled = true;
this.runtime = runtime;
console.log(`[EXTENSION]: enabled ${this.name}@${this.version}`, this.getMeta());
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`, this.getMeta());
this.onActivate();
}
@@ -65,7 +69,7 @@ export class LensExtension implements ExtensionModel {
this.runtime = null;
this.disposers.forEach(cleanUp => cleanUp());
this.disposers.length = 0;
console.log(`[EXTENSION]: disabled ${this.name}@${this.version}`, this.getMeta());
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`, this.getMeta());
}
// todo: add more hooks
@@ -77,27 +81,12 @@ export class LensExtension implements ExtensionModel {
// mock
}
// 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,
enabled: this.isEnabled
}, {
recurseEverything: true
})
@@ -116,12 +105,4 @@ export class LensExtension implements ExtensionModel {
recurseEverything: true,
})
}
// Runtime helpers
protected registerPage(params: PageRegistration, autoDisable = true) {
const dispose = this.runtime.dynamicPages.register(params);
if (autoDisable) {
this.disposers.push(dispose);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
import { LensExtension } from "./lens-extension"
export class LensMainExtension extends LensExtension {
async registerAppMenus() {
//
}
async registerPrometheusProviders(registry: any) {
//
}
}
+16
View File
@@ -0,0 +1,16 @@
import type { PageStore } from "./lens-runtime"
import type { PageRegistration } from "./page-store"
import { LensExtension } from "./lens-extension"
export class LensRendererExtension extends LensExtension {
registerPages(pageStore: PageStore) {
// mock
}
// Runtime helpers
protected registerPage(pageStore: PageStore, params: PageRegistration) {
const dispose = pageStore.register(params);
this.disposers.push(dispose)
}
}
+9 -17
View File
@@ -1,26 +1,18 @@
// Lens renderer runtime params available to extensions after activation
// Lens extension runtime params available to extensions after activation
import logger from "../main/logger";
import { dynamicPages } from "./register-page";
import { TabLayout } from "../renderer/components/layout/tab-layout";
import { navigate } from "../renderer/navigation";
import { PageRegistration } from "./page-store";
export interface LensRuntimeRendererEnv {
navigate: typeof navigate;
logger: typeof logger;
dynamicPages: typeof dynamicPages
components: {
TabLayout: typeof TabLayout
}
export interface PageStore {
register(params: PageRegistration): () => void
}
export function getLensRuntime(): LensRuntimeRendererEnv {
export interface LensExtensionRuntimeEnv {
logger: typeof logger;
}
export function getLensRuntime(): LensExtensionRuntimeEnv {
return {
logger,
navigate,
dynamicPages,
components: {
TabLayout // fixme: refactor, import as pure component from "@lens/extensions"
}
}
}
@@ -21,7 +21,7 @@ export interface PageComponents {
MenuIcon: React.ComponentType<IconProps>;
}
export class PagesStore {
export class PageStore {
protected pages = observable.array<PageRegistration>([], { deep: false });
@computed get globalPages() {
@@ -43,4 +43,4 @@ export class PagesStore {
}
}
export const dynamicPages = new PagesStore();
export const pageStore = new PageStore();
+14 -1
View File
@@ -22,6 +22,19 @@ const config: RollupOptions = {
],
};
const rendererConfig: RollupOptions = {
input: "src/extensions/extension-renderer-api.ts",
output: [
{ file: "types/extension-renderer-api.d.ts", format: "es", }
],
plugins: [
dts(),
dtsModuleWrap({ name: "@lens/ui-extensions" }),
ignoreImport({ extensions: ['.scss'] }),
json(),
],
};
function dtsModuleWrap({ name }: { name: string }): Plugin {
return {
name,
@@ -56,4 +69,4 @@ function dtsModuleWrap({ name }: { name: string }): Plugin {
}
}
export default config;
export default [config, rendererConfig];
+2
View File
@@ -19,6 +19,7 @@ import { workspaceStore } from "../common/workspace-store";
import { tracker } from "../common/tracker";
import { extensionManager } from "../extensions/extension-manager";
import { extensionLoader } from "../extensions/extension-loader";
import { getLensRuntime } from "../extensions/lens-runtime";
import logger from "./logger"
const workingDir = path.join(app.getPath("appData"), appName);
@@ -78,6 +79,7 @@ async function main() {
// create window manager and open app
windowManager = new WindowManager(proxyPort);
extensionLoader.loadOnMain(getLensRuntime)
extensionLoader.extensions.replace(await extensionManager.load())
}
-3
View File
@@ -4,13 +4,11 @@ import { render, unmountComponentAtNode } from "react-dom";
import { isMac } from "../common/vars";
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
import { extensionLoader } from "../extensions/extension-loader";
import { clusterStore } from "../common/cluster-store";
import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store";
import { App } from "./components/app";
import { LensApp } from "./lens-app";
import { getLensRuntime } from "../extensions/lens-runtime";
type AppComponent = React.ComponentType & {
init?(): void;
@@ -31,7 +29,6 @@ export async function bootstrap(App: AppComponent) {
// Register additional store listeners
clusterStore.registerIpcListener();
extensionLoader.autoEnableOnLoad(getLensRuntime);
// init app's dependencies if any
if (App.init) {
+5 -2
View File
@@ -36,7 +36,9 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store
import logger from "../../main/logger";
import { clusterIpc } from "../../common/cluster-ipc";
import { webFrame } from "electron";
import { dynamicPages } from "../../extensions/register-page";
import { pageStore } from "../../extensions/page-store";
import { extensionLoader } from "../../extensions/extension-loader";
import { getLensRuntime } from "../../extensions/lens-runtime";
@observer
export class App extends React.Component {
@@ -47,6 +49,7 @@ export class App extends React.Component {
await Terminal.preloadFonts()
await clusterIpc.activate.invokeFromRenderer(clusterId, frameId);
await getHostedCluster().whenReady; // cluster.refresh() is done at this point
extensionLoader.loadOnClusterRenderer(getLensRuntime)
}
get startURL() {
@@ -74,7 +77,7 @@ export class App extends React.Component {
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
{dynamicPages.clusterPages.map(({ path, components: { Page } }) => {
{pageStore.clusterPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}
<Redirect exact from="/" to={this.startURL}/>
@@ -14,7 +14,7 @@ import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
import { clusterStore } from "../../../common/cluster-store";
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { dynamicPages } from "../../../extensions/register-page";
import { pageStore } from "../../../extensions/page-store";
@observer
export class ClusterManager extends React.Component {
@@ -63,7 +63,7 @@ export class ClusterManager extends React.Component {
<Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} />
<Route component={ClusterSettings} {...clusterSettingsRoute} />
{dynamicPages.globalPages.map(({ path, components: { Page } }) => {
{pageStore.globalPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}
<Redirect exact to={this.startUrl} />
+6
View File
@@ -11,9 +11,15 @@ import { ErrorBoundary } from "./components/error-boundary";
import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
import { Notifications } from "./components/notifications";
import { ConfirmDialog } from "./components/confirm-dialog";
import { extensionLoader } from "../extensions/extension-loader";
import { getLensRuntime } from "../extensions/lens-runtime";
@observer
export class LensApp extends React.Component {
static async init() {
extensionLoader.loadOnMainRenderer(getLensRuntime)
}
render() {
return (
<I18nProvider i18n={_i18n}>