From aa864fc19915b6a1abd310b79970026582fae998 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 8 Oct 2020 11:52:45 +0300 Subject: [PATCH] Support extensions in main process (#1032) Signed-off-by: Jari Kolehmainen --- .gitignore | 1 + extensions/example-extension/index.tsx | 48 ----------- extensions/example-extension/main.ts | 11 +++ extensions/example-extension/package.json | 3 +- extensions/example-extension/page.tsx | 32 ++++++++ extensions/example-extension/renderer.ts | 24 ++++++ extensions/example-extension/tsconfig.json | 4 +- package.json | 9 ++- src/common/vars.ts | 3 + src/extensions/dynamic-page.tsx | 15 ++++ src/extensions/extension-api.ts | 16 +--- src/extensions/extension-loader.ts | 58 ++++++++++---- src/extensions/extension-manager.ts | 79 ++++++++++--------- src/extensions/extension-renderer-api.ts | 15 ++++ src/extensions/lens-extension.ts | 43 +++------- src/extensions/lens-main-extension.ts | 11 +++ src/extensions/lens-renderer-extension.ts | 15 ++++ src/extensions/lens-renderer-runtime.ts | 16 ++++ src/extensions/lens-runtime.ts | 14 +--- .../{register-page.tsx => page-store.ts} | 19 +---- src/extensions/rollup.config.ts | 15 +++- src/main/index.ts | 3 + src/main/window-manager.ts | 8 +- src/renderer/bootstrap.tsx | 3 - src/renderer/components/app.tsx | 8 +- .../cluster-manager/cluster-manager.tsx | 4 +- .../cluster-manager/clusters-menu.tsx | 4 +- src/renderer/components/layout/sidebar.tsx | 4 +- src/renderer/lens-app.tsx | 6 ++ webpack.extensions.ts | 50 ++++++++++++ webpack.renderer.ts | 19 +---- 31 files changed, 356 insertions(+), 204 deletions(-) delete mode 100644 extensions/example-extension/index.tsx create mode 100644 extensions/example-extension/main.ts create mode 100644 extensions/example-extension/page.tsx create mode 100644 extensions/example-extension/renderer.ts create mode 100644 src/extensions/dynamic-page.tsx create mode 100644 src/extensions/extension-renderer-api.ts create mode 100644 src/extensions/lens-main-extension.ts create mode 100644 src/extensions/lens-renderer-extension.ts create mode 100644 src/extensions/lens-renderer-runtime.ts rename src/extensions/{register-page.tsx => page-store.ts} (72%) create mode 100755 webpack.extensions.ts diff --git a/.gitignore b/.gitignore index 68c773c895..16803f4b39 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ src/extensions/*/*.js src/extensions/*/*.d.ts src/extensions/example-extension/src/** types/extension-api.d.ts +types/extension-renderer-api.d.ts diff --git a/extensions/example-extension/index.tsx b/extensions/example-extension/index.tsx deleted file mode 100644 index 98688ea077..0000000000 --- a/extensions/example-extension/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Button, Icon, IconProps, LensExtension, React, DynamicPageType } from "@lens/extensions"; -import { CoffeeDoodle } from "react-open-doodles"; -import path from "path"; - -export default class ExampleExtension extends LensExtension { - onActivate() { - console.log('EXAMPLE EXTENSION: ACTIVATED', this.getMeta()); - this.registerPage({ - type: DynamicPageType.CLUSTER, - path: "/extension-example", - title: "Example Extension", - components: { - Page: () => , - MenuIcon: ExtensionIcon, - } - }) - } - - onDeactivate() { - console.log('EXAMPLE EXTENSION: DEACTIVATED', this.getMeta()); - } -} - -export function ExtensionIcon(props: IconProps) { - return -} - -export class ExtensionPage extends React.Component<{ extension: ExampleExtension }> { - deactivate = () => { - const { extension } = this.props; - extension.runtime.navigate("/") - extension.disable(); - } - - render() { - const doodleStyle = { - width: "200px" - } - return ( -
-
-

Hello from Example extension!

-

File: {__filename}

-
- ) - } -} diff --git a/extensions/example-extension/main.ts b/extensions/example-extension/main.ts new file mode 100644 index 0000000000..e60540ac37 --- /dev/null +++ b/extensions/example-extension/main.ts @@ -0,0 +1,11 @@ +import { LensMainExtension } from "@lens/extensions"; + +export default class ExampleExtensionMain extends LensMainExtension { + onActivate() { + console.log('EXAMPLE EXTENSION MAIN: ACTIVATED', this.getMeta()); + } + + onDeactivate() { + console.log('EXAMPLE EXTENSION MAIN: DEACTIVATED', this.getMeta()); + } +} diff --git a/extensions/example-extension/package.json b/extensions/example-extension/package.json index 18cafd26fa..795aace9d1 100644 --- a/extensions/example-extension/package.json +++ b/extensions/example-extension/package.json @@ -2,7 +2,8 @@ "name": "extension-example", "version": "1.0.0", "description": "Example extension", - "main": "dist/index.js", + "main": "dist/main.js", + "renderer": "dist/renderer.js", "lens": { "metadata": {}, "styles": [] diff --git a/extensions/example-extension/page.tsx b/extensions/example-extension/page.tsx new file mode 100644 index 0000000000..68e2844a82 --- /dev/null +++ b/extensions/example-extension/page.tsx @@ -0,0 +1,32 @@ +import { Button, Icon, IconProps, LensRendererExtension, React } from "@lens/ui-extensions"; +import { CoffeeDoodle } from "react-open-doodles"; +import path from "path"; + +export function ExtensionIcon(props: IconProps) { + return +} + +export class ExtensionPage extends React.Component<{ extension: LensRendererExtension }> { + deactivate = () => { + const { extension } = this.props; + extension.disable(); + } + + render() { + const doodleStyle = { + width: "200px" + } + return ( +
+
+

Hello from Example extension!

+

File: {__filename}

+
+ ) + } +} + +export function examplePage(ext: LensRendererExtension) { + return () => +} diff --git a/extensions/example-extension/renderer.ts b/extensions/example-extension/renderer.ts new file mode 100644 index 0000000000..478a0ecc17 --- /dev/null +++ b/extensions/example-extension/renderer.ts @@ -0,0 +1,24 @@ +import { DynamicPageType, LensRendererExtension, PageStore } from "@lens/ui-extensions"; +import { examplePage, ExtensionIcon } from "./page" + +export default class ExampleExtension extends LensRendererExtension { + onActivate() { + console.log('EXAMPLE EXTENSION RENDERER: ACTIVATED', this.getMeta()); + } + + registerPages(pageStore: PageStore) { + this.disposers.push(pageStore.register({ + type: DynamicPageType.CLUSTER, + path: "/extension-example", + title: "Example Extension", + components: { + Page: examplePage(this), + MenuIcon: ExtensionIcon, + } + })) + } + + onDeactivate() { + console.log('EXAMPLE EXTENSION RENDERER: DEACTIVATED', this.getMeta()); + } +} diff --git a/extensions/example-extension/tsconfig.json b/extensions/example-extension/tsconfig.json index 3ba216b67b..3ef4abfc2d 100644 --- a/extensions/example-extension/tsconfig.json +++ b/extensions/example-extension/tsconfig.json @@ -12,11 +12,13 @@ "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, "jsx": "react" }, "include": [ "../../types", - "./index.tsx" + "./renderer.ts", + "./main.ts" ], "exclude": [ "node_modules", diff --git a/package.json b/package.json index a4e44bab2b..14d478c7b6 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,18 @@ }, "scripts": { "dev": "concurrently -k \"yarn dev-run -C\" yarn:dev:*", + "dev-build": "concurrently yarn:compile:*", "dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"", "dev:main": "yarn compile:main --watch", "dev:renderer": "yarn compile:renderer --watch", - "dev:extensions": "yarn compile:extensions --watch", + "dev:extension-rollup": "yarn compile:extension-rollup --watch", + "dev:extension-api": "yarn compile:extension-api --watch", "compile": "env NODE_ENV=production concurrently yarn:compile:*", "compile:main": "webpack --config webpack.main.ts", "compile:renderer": "webpack --config webpack.renderer.ts", "compile:i18n": "lingui compile", - "compile:extensions": "rollup --config src/extensions/rollup.config.js", + "compile:extension-rollup": "rollup --config src/extensions/rollup.config.js", + "compile:extension-api": "webpack --config webpack.extensions.ts", "build:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens", "build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens", "build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens", @@ -320,8 +323,8 @@ "progress-bar-webpack-plugin": "^2.1.0", "raw-loader": "^4.0.1", "react": "^16.13.1", - "react-dom": "^16.13.1", "react-beautiful-dnd": "^13.0.0", + "react-dom": "^16.13.1", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-select": "^3.1.0", diff --git a/src/common/vars.ts b/src/common/vars.ts index 0ee9e05eb4..1bbfd87548 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -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 diff --git a/src/extensions/dynamic-page.tsx b/src/extensions/dynamic-page.tsx new file mode 100644 index 0000000000..b92d102f70 --- /dev/null +++ b/src/extensions/dynamic-page.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { cssNames } from "../renderer/utils"; +import { TabLayout } from "../renderer/components/layout/tab-layout"; +import { PageRegistration } from "./page-store" + +export class DynamicPage extends React.Component<{ page: PageRegistration }> { + render() { + const { className, components: { Page }, subPages = [] } = this.props.page; + return ( + + + + ) + } +} diff --git a/src/extensions/extension-api.ts b/src/extensions/extension-api.ts index 1e7f457754..250a271b30 100644 --- a/src/extensions/extension-api.ts +++ b/src/extensions/extension-api.ts @@ -1,15 +1,5 @@ // Lens-extensions api developer's kit -// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps) +export type { LensExtensionRuntimeEnv } from "./lens-runtime"; -// TODO: figure out how to import as normal npm-package -export { default as React } from "react" - -export * from "./lens-extension" -export { LensRuntimeRendererEnv } from "./lens-runtime"; -export { DynamicPageType } from "./register-page"; - -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" +// APIs +export * from "./lens-main-extension" diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index d34ecd4eb6..e7ac28d43b 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -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,36 +44,64 @@ export class ExtensionLoader { } } - autoEnableOnLoad(getLensRuntimeEnv: () => LensRuntimeRendererEnv, { delay = 0 } = {}) { - logger.info('[EXTENSIONS-LOADER]: auto-activation loaded extensions: ON'); - return reaction(() => this.extensions.toJS(), installedExtensions => { - installedExtensions.forEach((ext) => { + 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) => { + for(const [id, ext] of installedExtensions) { let instance = this.instances.get(ext.manifestPath) if (!instance) { const extensionModule = this.requireExtension(ext) if (!extensionModule) { - logger.error("[EXTENSION-LOADER] failed to load extension " + ext.manifestPath) - return + continue } 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 && extension.manifest.renderer) { + extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer)) + } else if (extension.manifest.main) { + extEntrypoint = path.resolve(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.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension }); + console.trace(err) } }) } @@ -84,7 +114,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); } } diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index 2f856ef8cc..8490071b22 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -2,10 +2,23 @@ import type { ExtensionManifest } from "./lens-extension" import path from "path" import fs from "fs-extra" import logger from "../main/logger" -import { withExtensionPackagesRoot, extensionPackagesRoot, InstalledExtension } from "./extension-loader" -import npm from "npm" +import { extensionPackagesRoot, InstalledExtension } from "./extension-loader" +import * as child_process from 'child_process'; + +type Dependencies = { + [name: string]: string; +} + +type PackageJson = { + dependencies: Dependencies; +} export class ExtensionManager { + + protected packagesJson: PackageJson = { + dependencies: {} + } + get extensionPackagesRoot() { return extensionPackagesRoot() } @@ -14,11 +27,13 @@ export class ExtensionManager { return path.resolve(__static, "../extensions"); } + get npmPath() { + return __non_webpack_require__.resolve('npm/bin/npm-cli') + } + async load() { logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) - await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules")) - await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), `{"dependencies": []}`, {mode: 0o600}) return await this.loadExtensions(); } @@ -27,9 +42,7 @@ export class ExtensionManager { let manifestJson: ExtensionManifest; try { manifestJson = __non_webpack_require__(manifestPath) - withExtensionPackagesRoot(() => { - this.installPackageFromPath(path.dirname(manifestPath)) - }) + this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath) logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name) return { @@ -44,31 +57,17 @@ export class ExtensionManager { } } - protected installPackageFromPath(path: string): Promise { - const origLogger = console.log + protected installPackages(): Promise { return new Promise((resolve, reject) => { - npm.load({ - production: true, - global: false, - prefix: this.extensionPackagesRoot, - dev: false, - spin: false, - "ignore-scripts": true, - loglevel: "silent" - }, (err) => { - console.log = function() { - // just to ignore ts empty function error - } - npm.commands.install([ - path - ], (err) => { - console.log = origLogger - if (err) { - reject(err) - } else { - resolve() - } - }) + const child = child_process.fork(this.npmPath, ["install", "--silent"], { + cwd: extensionPackagesRoot(), + silent: true + }) + child.on("close", () => { + resolve() + }) + child.on("error", (err) => { + reject(err) }) }) } @@ -80,15 +79,19 @@ export class ExtensionManager { async loadFromFolder(folderPath: string): Promise { const paths = await fs.readdir(folderPath); - const manifestsLoading = paths.map(fileName => { + const extensions: InstalledExtension[] = [] + for (const fileName of paths) { const absPath = path.resolve(folderPath, fileName); const manifestPath = path.resolve(absPath, "package.json"); - return fs.access(manifestPath, fs.constants.F_OK) - .then(async () => await 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) + await fs.access(manifestPath, fs.constants.F_OK) + const ext = await this.getExtensionByManifest(manifestPath).catch(() => null) + if (ext) { + extensions.push(ext) + } + } + await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600}) + await this.installPackages() + logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); return extensions; } diff --git a/src/extensions/extension-renderer-api.ts b/src/extensions/extension-renderer-api.ts new file mode 100644 index 0000000000..9003e75901 --- /dev/null +++ b/src/extensions/extension-renderer-api.ts @@ -0,0 +1,15 @@ +// Lens-extensions api developer's kit +export type { LensExtensionRuntimeEnv } from "./lens-renderer-runtime" +export type { PageStore } from "./page-store" + +// 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" diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 9fc766f7a9..27cdaad83e 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -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> { - 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); - } - } } diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts new file mode 100644 index 0000000000..05a303ccad --- /dev/null +++ b/src/extensions/lens-main-extension.ts @@ -0,0 +1,11 @@ +import { LensExtension } from "./lens-extension" + +export class LensMainExtension extends LensExtension { + async registerAppMenus() { + // + } + + async registerPrometheusProviders(registry: any) { + // + } +} diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts new file mode 100644 index 0000000000..405f598e56 --- /dev/null +++ b/src/extensions/lens-renderer-extension.ts @@ -0,0 +1,15 @@ +import type { PageStore } from "./extension-renderer-api" +import type { PageRegistration } from "./page-store" +import { LensExtension } from "./lens-extension" + +export abstract class LensRendererExtension extends LensExtension { + registerPages(pageStore: PageStore) { + return + } + + // Runtime helpers + protected registerPage(pageStore: PageStore, params: PageRegistration) { + const dispose = pageStore.register(params); + this.disposers.push(dispose) + } +} diff --git a/src/extensions/lens-renderer-runtime.ts b/src/extensions/lens-renderer-runtime.ts new file mode 100644 index 0000000000..9849a46f4f --- /dev/null +++ b/src/extensions/lens-renderer-runtime.ts @@ -0,0 +1,16 @@ +// Lens extension runtime params available to renderer extensions after activation + +import logger from "../main/logger"; +import { navigate } from "../renderer/navigation"; + +export interface LensExtensionRuntimeEnv { + logger: typeof logger; + navigate: typeof navigate; +} + +export function getLensRuntime(): LensExtensionRuntimeEnv { + return { + logger, + navigate + } +} diff --git a/src/extensions/lens-runtime.ts b/src/extensions/lens-runtime.ts index 6644ad7d76..07e6a5cbaf 100644 --- a/src/extensions/lens-runtime.ts +++ b/src/extensions/lens-runtime.ts @@ -1,19 +1,13 @@ -// Lens renderer runtime apis exposed to extensions once activated +// Lens extension runtime params available to extensions after activation import logger from "../main/logger"; -import { dynamicPages } from "./register-page"; -import { navigate } from "../renderer/navigation"; -export interface LensRuntimeRendererEnv { - navigate: typeof navigate; +export interface LensExtensionRuntimeEnv { logger: typeof logger; - dynamicPages: typeof dynamicPages } -export function getLensRuntime(): LensRuntimeRendererEnv { +export function getLensRuntime(): LensExtensionRuntimeEnv { return { - logger, - navigate, - dynamicPages, + logger } } diff --git a/src/extensions/register-page.tsx b/src/extensions/page-store.ts similarity index 72% rename from src/extensions/register-page.tsx rename to src/extensions/page-store.ts index 2db368c6fe..53509534d8 100644 --- a/src/extensions/register-page.tsx +++ b/src/extensions/page-store.ts @@ -4,8 +4,8 @@ import { computed, observable } from "mobx"; import React from "react"; import { RouteProps } from "react-router"; import { IconProps } from "../renderer/components/icon"; -import { cssNames, IClassName } from "../renderer/utils"; -import { TabLayout, TabRoute } from "../renderer/components/layout/tab-layout"; +import { IClassName } from "../renderer/utils"; +import { TabRoute } from "../renderer/components/layout/tab-layout"; export enum DynamicPageType { GLOBAL = "lens-scope", @@ -27,7 +27,7 @@ export interface PageComponents { MenuIcon: React.ComponentType; } -export class PagesStore { +export class PageStore { protected pages = observable.array([], { deep: false }); @computed get globalPages() { @@ -49,15 +49,4 @@ export class PagesStore { } } -export class DynamicPage extends React.Component<{ page: PageRegistration }> { - render() { - const { className, components: { Page }, subPages = [] } = this.props.page; - return ( - - - - ) - } -} - -export const dynamicPages = new PagesStore(); +export const pageStore = new PageStore(); diff --git a/src/extensions/rollup.config.ts b/src/extensions/rollup.config.ts index 8d802ef495..4c90d1e232 100644 --- a/src/extensions/rollup.config.ts +++ b/src/extensions/rollup.config.ts @@ -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; \ No newline at end of file +export default [config, rendererConfig]; diff --git a/src/main/index.ts b/src/main/index.ts index 973aa27a8e..43177d0673 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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,7 +79,9 @@ async function main() { // create window manager and open app windowManager = new WindowManager(proxyPort); + extensionLoader.loadOnMain(getLensRuntime) extensionLoader.extensions.replace(await extensionManager.load()) + extensionLoader.broadcastExtensions() } app.on("ready", main); diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 348d5bbd3f..2854c11dd3 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -4,6 +4,7 @@ import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" import windowStateKeeper from "electron-window-state" import { observable } from "mobx"; import { initMenu } from "./menu"; +import { extensionLoader } from "../extensions/extension-loader"; export class WindowManager { protected mainView: BrowserWindow; @@ -40,6 +41,9 @@ export class WindowManager { event.preventDefault(); shell.openExternal(url); }); + this.mainView.webContents.on("dom-ready", () => { + extensionLoader.broadcastExtensions() + }) // track visible cluster from ui ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { @@ -72,8 +76,8 @@ export class WindowManager { try { await this.showSplash(); await this.mainView.loadURL(`http://localhost:${this.proxyPort}`) - this.mainView.show(); - this.splashWindow.close(); + this.mainView.show() + this.splashWindow.close() } catch (err) { dialog.showErrorBox("ERROR!", err.toString()) } diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 021aca3208..37fdc35e03 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -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) { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 0b9330efbe..96f4333c71 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -36,7 +36,10 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store import logger from "../../main/logger"; import { clusterIpc } from "../../common/cluster-ipc"; import { webFrame } from "electron"; -import { DynamicPage, dynamicPages } from "../../extensions/register-page"; +import { pageStore } from "../../extensions/page-store"; +import { DynamicPage } from "../../extensions/dynamic-page"; +import { extensionLoader } from "../../extensions/extension-loader"; +import { getLensRuntime } from "../../extensions/lens-runtime"; @observer export class App extends React.Component { @@ -47,6 +50,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 +78,7 @@ export class App extends React.Component { - {dynamicPages.clusterPages.map(page => { + {pageStore.clusterPages.map(page => { return }/> })} diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index cd3af5391c..4e68fa5623 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -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 { - {dynamicPages.globalPages.map(({ path, components: { Page } }) => { + {pageStore.globalPages.map(({ path, components: { Page } }) => { return })} diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 5c9e9a1ae2..f20fddcf80 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -22,7 +22,7 @@ import { ConfirmDialog } from "../confirm-dialog"; import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterViewURL } from "./cluster-view.route"; import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd"; -import { dynamicPages } from "../../../extensions/register-page"; +import { pageStore } from "../../../extensions/page-store"; interface Props { className?: IClassName; @@ -156,7 +156,7 @@ export class ClustersMenu extends React.Component { )}
- {dynamicPages.globalPages.map(({ path, components: { MenuIcon } }) => { + {pageStore.globalPages.map(({ path, components: { MenuIcon } }) => { return navigate(path)}/> })}
diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index a8a20f8ff5..ab934d2170 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -28,7 +28,7 @@ import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resourc import { CustomResources } from "../+custom-resources/custom-resources"; import { navigation } from "../../navigation"; import { isAllowedResource } from "../../../common/rbac" -import { dynamicPages } from "../../../extensions/register-page"; +import { pageStore } from "../../../extensions/page-store"; const SidebarContext = React.createContext({ pinned: false }); type SidebarContextValue = { @@ -184,7 +184,7 @@ export class Sidebar extends React.Component { > {this.renderCustomResources()} - {dynamicPages.clusterPages.map(({ path, title, components: { MenuIcon } }) => { + {pageStore.clusterPages.map(({ path, title, components: { MenuIcon } }) => { return ( diff --git a/webpack.extensions.ts b/webpack.extensions.ts new file mode 100755 index 0000000000..a6e7a6f918 --- /dev/null +++ b/webpack.extensions.ts @@ -0,0 +1,50 @@ +import { extensionsDir, extensionsLibName, extensionsRendererLibName } from "./src/common/vars"; +import path from "path"; +import webpack from "webpack"; +import nodeExternals from "webpack-node-externals"; +import { webpackLensRenderer } from "./webpack.renderer"; +import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"; +import ProgressBarPlugin from "progress-bar-webpack-plugin"; +import MiniCssExtractPlugin from "mini-css-extract-plugin"; + +export default [ + webpackExtensionsApi, + webpackExtensionsRendererApi +] + +// todo: use common chunks/externals for "react", "react-dom", etc. +export function webpackExtensionsApi(): webpack.Configuration { + const config = webpackLensRenderer({ showVars: false }) + config.name = "extensions-api" + config.entry = { + [extensionsLibName]: path.resolve(extensionsDir, "extension-api.ts") + } + config.externals = [ + nodeExternals() + ] + config.plugins = [ + new ProgressBarPlugin(), + new ForkTsCheckerPlugin(), + ] + config.output.libraryTarget = "commonjs2" + config.devtool = "nosources-source-map" + return config +} + +export function webpackExtensionsRendererApi(): webpack.Configuration { + const config = webpackLensRenderer({ showVars: false }) + config.name = "extensions-renderer-api" + config.entry = { + [extensionsRendererLibName]: path.resolve(extensionsDir, "extension-renderer-api.ts") + } + config.plugins = [ + new ProgressBarPlugin(), + new ForkTsCheckerPlugin(), + new MiniCssExtractPlugin({ + filename: "[name].css", + }) + ] + config.output.libraryTarget = "commonjs2" + config.devtool = "nosources-source-map" + return config +} diff --git a/webpack.renderer.ts b/webpack.renderer.ts index 37ed000384..5d94b56365 100755 --- a/webpack.renderer.ts +++ b/webpack.renderer.ts @@ -1,4 +1,4 @@ -import { appName, buildDir, extensionsDir, extensionsLibName, htmlTemplate, isDevelopment, isProduction, publicPath, rendererDir, sassCommonVars } from "./src/common/vars"; +import { appName, buildDir, extensionsDir, extensionsLibName, extensionsRendererLibName, htmlTemplate, isDevelopment, isProduction, publicPath, rendererDir, sassCommonVars } from "./src/common/vars"; import path from "path"; import webpack from "webpack"; import HtmlWebpackPlugin from "html-webpack-plugin"; @@ -8,22 +8,9 @@ import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin" import ProgressBarPlugin from "progress-bar-webpack-plugin"; export default [ - webpackLensRenderer, - webpackExtensionsApi, + webpackLensRenderer ] -// todo: use common chunks/externals for "react", "react-dom", etc. -export function webpackExtensionsApi(): webpack.Configuration { - const config = webpackLensRenderer({ showVars: false }); - config.name = "extensions-api" - config.entry = { - [extensionsLibName]: path.resolve(extensionsDir, "extension-api.ts") - }; - config.output.libraryTarget = "commonjs2" - config.devtool = "nosources-source-map"; - return config; -} - export function webpackLensRenderer({ showVars = true } = {}): webpack.Configuration { if (showVars) { console.info('WEBPACK:renderer', require("./src/common/vars")); @@ -184,4 +171,4 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura }), ], } -} \ No newline at end of file +}