diff --git a/extensions/example-extension/index.tsx b/extensions/example-extension/index.tsx deleted file mode 100644 index 1fd4a4c666..0000000000 --- a/extensions/example-extension/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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.registerPage(pageStore, { - 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/main.ts b/extensions/example-extension/main.ts index 28e5feddee..e60540ac37 100644 --- a/extensions/example-extension/main.ts +++ b/extensions/example-extension/main.ts @@ -5,10 +5,6 @@ export default class ExampleExtensionMain extends LensMainExtension { console.log('EXAMPLE EXTENSION MAIN: ACTIVATED', this.getMeta()); } - onEvent(evt: any) { - // - } - onDeactivate() { console.log('EXAMPLE EXTENSION MAIN: DEACTIVATED', this.getMeta()); } diff --git a/extensions/example-extension/page.tsx b/extensions/example-extension/page.tsx index 823888387a..68e2844a82 100644 --- a/extensions/example-extension/page.tsx +++ b/extensions/example-extension/page.tsx @@ -28,5 +28,5 @@ export class ExtensionPage extends React.Component<{ extension: LensRendererExte } export function examplePage(ext: LensRendererExtension) { - return () => + return () => } diff --git a/extensions/example-extension/renderer.ts b/extensions/example-extension/renderer.ts index 1fd4a4c666..478a0ecc17 100644 --- a/extensions/example-extension/renderer.ts +++ b/extensions/example-extension/renderer.ts @@ -7,7 +7,7 @@ export default class ExampleExtension extends LensRendererExtension { } registerPages(pageStore: PageStore) { - this.registerPage(pageStore, { + this.disposers.push(pageStore.register({ type: DynamicPageType.CLUSTER, path: "/extension-example", title: "Example Extension", @@ -15,7 +15,7 @@ export default class ExampleExtension extends LensRendererExtension { Page: examplePage(this), MenuIcon: ExtensionIcon, } - }) + })) } onDeactivate() { diff --git a/extensions/example-extension/tsconfig.json b/extensions/example-extension/tsconfig.json index 5f73ed06f2..3ef4abfc2d 100644 --- a/extensions/example-extension/tsconfig.json +++ b/extensions/example-extension/tsconfig.json @@ -12,6 +12,7 @@ "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, "jsx": "react" }, "include": [ diff --git a/package.json b/package.json index f36fd31e83..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: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:extension-api": "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", diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 83eda276b7..e7ac28d43b 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -66,14 +66,13 @@ export class ExtensionLoader { } protected autoloadExtensions(getLensRuntimeEnv: () => LensExtensionRuntimeEnv, callback: (instance: LensExtension) => void) { - return reaction(() => this.extensions.toJS(), installedExtensions => { - installedExtensions.forEach((ext) => { + 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); @@ -81,7 +80,7 @@ export class ExtensionLoader { callback(instance) this.instances.set(ext.id, instance) } - }) + } }, { fireImmediately: true, delay: 0, @@ -92,17 +91,17 @@ export class ExtensionLoader { let extEntrypoint = "" return withExtensionPackagesRoot(() => { try { - if (ipcRenderer) { - extEntrypoint = path.join(path.dirname(extension.manifestPath), extension.manifest.renderer) - } else { - extEntrypoint = path.join(path.dirname(extension.manifestPath), extension.manifest.main) + 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.trace(err) console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension }); + console.trace(err) } }) } 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 index 8de4873778..9003e75901 100644 --- a/src/extensions/extension-renderer-api.ts +++ b/src/extensions/extension-renderer-api.ts @@ -1,9 +1,10 @@ // Lens-extensions api developer's kit -export type { LensExtensionRuntimeEnv, PageStore } from "./lens-renderer-runtime"; +export type { LensExtensionRuntimeEnv } from "./lens-renderer-runtime" +export type { PageStore } from "./page-store" // APIs export * from "./lens-renderer-extension" -export { DynamicPageType } from "./page-store"; +export { DynamicPageType } from "./page-store" // TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps) export { default as React } from "react" diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 0c48491bd1..405f598e56 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -1,13 +1,12 @@ -import type { PageStore } from "./lens-renderer-runtime" +import type { PageStore } from "./extension-renderer-api" import type { PageRegistration } from "./page-store" import { LensExtension } from "./lens-extension" -export class LensRendererExtension extends LensExtension { +export abstract class LensRendererExtension extends LensExtension { registerPages(pageStore: PageStore) { - // mock + return } - // Runtime helpers protected registerPage(pageStore: PageStore, params: PageRegistration) { const dispose = pageStore.register(params); diff --git a/src/extensions/lens-renderer-runtime.ts b/src/extensions/lens-renderer-runtime.ts index fef56fca07..9849a46f4f 100644 --- a/src/extensions/lens-renderer-runtime.ts +++ b/src/extensions/lens-renderer-runtime.ts @@ -2,11 +2,6 @@ import logger from "../main/logger"; import { navigate } from "../renderer/navigation"; -import { PageRegistration } from "./page-store"; - -export interface PageStore { - register(params: PageRegistration): () => void -} export interface LensExtensionRuntimeEnv { logger: typeof logger; diff --git a/src/main/index.ts b/src/main/index.ts index 80a40ba3b5..43177d0673 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -81,6 +81,7 @@ async function main() { 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/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 3a949ddd20..5d94b56365 100755 --- a/webpack.renderer.ts +++ b/webpack.renderer.ts @@ -8,34 +8,9 @@ import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin" import ProgressBarPlugin from "progress-bar-webpack-plugin"; export default [ - webpackLensRenderer, - webpackExtensionsApi, - webpackExtensionsRendererApi + 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 webpackExtensionsRendererApi(): webpack.Configuration { - const config = webpackLensRenderer({ showVars: false }); - config.name = "extensions-renderer-api" - config.entry = { - [extensionsRendererLibName]: path.resolve(extensionsDir, "extension-renderer-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"));