1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2020-10-07 20:17:22 +03:00
parent f97fbdf9ed
commit ffa6a982b5
15 changed files with 122 additions and 119 deletions

View File

@ -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());
}
}

View File

@ -5,10 +5,6 @@ export default class ExampleExtensionMain extends LensMainExtension {
console.log('EXAMPLE EXTENSION MAIN: ACTIVATED', this.getMeta()); console.log('EXAMPLE EXTENSION MAIN: ACTIVATED', this.getMeta());
} }
onEvent(evt: any) {
//
}
onDeactivate() { onDeactivate() {
console.log('EXAMPLE EXTENSION MAIN: DEACTIVATED', this.getMeta()); console.log('EXAMPLE EXTENSION MAIN: DEACTIVATED', this.getMeta());
} }

View File

@ -28,5 +28,5 @@ export class ExtensionPage extends React.Component<{ extension: LensRendererExte
} }
export function examplePage(ext: LensRendererExtension) { export function examplePage(ext: LensRendererExtension) {
return () => <ExtensionPage extension={ext}/> return () => <ExtensionPage extension={ext} />
} }

View File

@ -7,7 +7,7 @@ export default class ExampleExtension extends LensRendererExtension {
} }
registerPages(pageStore: PageStore) { registerPages(pageStore: PageStore) {
this.registerPage(pageStore, { this.disposers.push(pageStore.register({
type: DynamicPageType.CLUSTER, type: DynamicPageType.CLUSTER,
path: "/extension-example", path: "/extension-example",
title: "Example Extension", title: "Example Extension",
@ -15,7 +15,7 @@ export default class ExampleExtension extends LensRendererExtension {
Page: examplePage(this), Page: examplePage(this),
MenuIcon: ExtensionIcon, MenuIcon: ExtensionIcon,
} }
}) }))
} }
onDeactivate() { onDeactivate() {

View File

@ -12,6 +12,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react" "jsx": "react"
}, },
"include": [ "include": [

View File

@ -12,15 +12,18 @@
}, },
"scripts": { "scripts": {
"dev": "concurrently -k \"yarn dev-run -C\" yarn:dev:*", "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-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
"dev:main": "yarn compile:main --watch", "dev:main": "yarn compile:main --watch",
"dev:renderer": "yarn compile:renderer --watch", "dev:renderer": "yarn compile:renderer --watch",
"dev:extension-rollup": "yarn compile:extension-rollup --watch",
"dev:extension-api": "yarn compile:extension-api --watch", "dev:extension-api": "yarn compile:extension-api --watch",
"compile": "env NODE_ENV=production concurrently yarn:compile:*", "compile": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "webpack --config webpack.main.ts", "compile:main": "webpack --config webpack.main.ts",
"compile:renderer": "webpack --config webpack.renderer.ts", "compile:renderer": "webpack --config webpack.renderer.ts",
"compile:i18n": "lingui compile", "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:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens",
"build:mac": "yarn compile && electron-builder --mac --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", "build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens",

View File

@ -66,14 +66,13 @@ export class ExtensionLoader {
} }
protected autoloadExtensions(getLensRuntimeEnv: () => LensExtensionRuntimeEnv, callback: (instance: LensExtension) => void) { protected autoloadExtensions(getLensRuntimeEnv: () => LensExtensionRuntimeEnv, callback: (instance: LensExtension) => void) {
return reaction(() => this.extensions.toJS(), installedExtensions => { return reaction(() => this.extensions.toJS(), (installedExtensions) => {
installedExtensions.forEach((ext) => { for(const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.manifestPath) let instance = this.instances.get(ext.manifestPath)
if (!instance) { if (!instance) {
const extensionModule = this.requireExtension(ext) const extensionModule = this.requireExtension(ext)
if (!extensionModule) { if (!extensionModule) {
logger.error("[EXTENSION-LOADER] failed to load extension " + ext.manifestPath) continue
return
} }
const LensExtensionClass = extensionModule.default; const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest); instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
@ -81,7 +80,7 @@ export class ExtensionLoader {
callback(instance) callback(instance)
this.instances.set(ext.id, instance) this.instances.set(ext.id, instance)
} }
}) }
}, { }, {
fireImmediately: true, fireImmediately: true,
delay: 0, delay: 0,
@ -92,17 +91,17 @@ export class ExtensionLoader {
let extEntrypoint = "" let extEntrypoint = ""
return withExtensionPackagesRoot(() => { return withExtensionPackagesRoot(() => {
try { try {
if (ipcRenderer) { if (ipcRenderer && extension.manifest.renderer) {
extEntrypoint = path.join(path.dirname(extension.manifestPath), extension.manifest.renderer) extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer))
} else { } else if (extension.manifest.main) {
extEntrypoint = path.join(path.dirname(extension.manifestPath), extension.manifest.main) extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main))
} }
if (extEntrypoint !== "") { if (extEntrypoint !== "") {
return __non_webpack_require__(extEntrypoint) return __non_webpack_require__(extEntrypoint)
} }
} catch (err) { } catch (err) {
console.trace(err)
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension }); console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
console.trace(err)
} }
}) })
} }

View File

@ -2,10 +2,23 @@ import type { ExtensionManifest } from "./lens-extension"
import path from "path" import path from "path"
import fs from "fs-extra" import fs from "fs-extra"
import logger from "../main/logger" import logger from "../main/logger"
import { withExtensionPackagesRoot, extensionPackagesRoot, InstalledExtension } from "./extension-loader" import { extensionPackagesRoot, InstalledExtension } from "./extension-loader"
import npm from "npm" import * as child_process from 'child_process';
type Dependencies = {
[name: string]: string;
}
type PackageJson = {
dependencies: Dependencies;
}
export class ExtensionManager { export class ExtensionManager {
protected packagesJson: PackageJson = {
dependencies: {}
}
get extensionPackagesRoot() { get extensionPackagesRoot() {
return extensionPackagesRoot() return extensionPackagesRoot()
} }
@ -14,11 +27,13 @@ export class ExtensionManager {
return path.resolve(__static, "../extensions"); return path.resolve(__static, "../extensions");
} }
get npmPath() {
return __non_webpack_require__.resolve('npm/bin/npm-cli')
}
async load() { async load() {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules")) 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(); return await this.loadExtensions();
} }
@ -27,9 +42,7 @@ export class ExtensionManager {
let manifestJson: ExtensionManifest; let manifestJson: ExtensionManifest;
try { try {
manifestJson = __non_webpack_require__(manifestPath) manifestJson = __non_webpack_require__(manifestPath)
withExtensionPackagesRoot(() => { this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath)
this.installPackageFromPath(path.dirname(manifestPath))
})
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name) logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
return { return {
@ -44,31 +57,17 @@ export class ExtensionManager {
} }
} }
protected installPackageFromPath(path: string): Promise<void> { protected installPackages(): Promise<void> {
const origLogger = console.log
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
npm.load({ const child = child_process.fork(this.npmPath, ["install", "--silent"], {
production: true, cwd: extensionPackagesRoot(),
global: false, silent: true
prefix: this.extensionPackagesRoot, })
dev: false, child.on("close", () => {
spin: false, resolve()
"ignore-scripts": true, })
loglevel: "silent" child.on("error", (err) => {
}, (err) => { reject(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()
}
})
}) })
}) })
} }
@ -80,15 +79,19 @@ export class ExtensionManager {
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> { async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const paths = await fs.readdir(folderPath); 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 absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json"); const manifestPath = path.resolve(absPath, "package.json");
return fs.access(manifestPath, fs.constants.F_OK) await fs.access(manifestPath, fs.constants.F_OK)
.then(async () => await this.getExtensionByManifest(manifestPath)) const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
.catch(() => null) if (ext) {
}); extensions.push(ext)
let extensions = await Promise.all(manifestsLoading); }
extensions = extensions.filter(v => !!v); // filter out files and invalid folders (without manifest.json) }
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 }); logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions; return extensions;
} }

View File

@ -1,9 +1,10 @@
// Lens-extensions api developer's kit // 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 // APIs
export * from "./lens-renderer-extension" 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) // TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
export { default as React } from "react" export { default as React } from "react"

View File

@ -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 type { PageRegistration } from "./page-store"
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension"
export class LensRendererExtension extends LensExtension { export abstract class LensRendererExtension extends LensExtension {
registerPages(pageStore: PageStore) { registerPages(pageStore: PageStore) {
// mock return
} }
// Runtime helpers // Runtime helpers
protected registerPage(pageStore: PageStore, params: PageRegistration) { protected registerPage(pageStore: PageStore, params: PageRegistration) {
const dispose = pageStore.register(params); const dispose = pageStore.register(params);

View File

@ -2,11 +2,6 @@
import logger from "../main/logger"; import logger from "../main/logger";
import { navigate } from "../renderer/navigation"; import { navigate } from "../renderer/navigation";
import { PageRegistration } from "./page-store";
export interface PageStore {
register(params: PageRegistration): () => void
}
export interface LensExtensionRuntimeEnv { export interface LensExtensionRuntimeEnv {
logger: typeof logger; logger: typeof logger;

View File

@ -81,6 +81,7 @@ async function main() {
extensionLoader.loadOnMain(getLensRuntime) extensionLoader.loadOnMain(getLensRuntime)
extensionLoader.extensions.replace(await extensionManager.load()) extensionLoader.extensions.replace(await extensionManager.load())
extensionLoader.broadcastExtensions()
} }
app.on("ready", main); app.on("ready", main);

View File

@ -4,6 +4,7 @@ import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
import windowStateKeeper from "electron-window-state" import windowStateKeeper from "electron-window-state"
import { observable } from "mobx"; import { observable } from "mobx";
import { initMenu } from "./menu"; import { initMenu } from "./menu";
import { extensionLoader } from "../extensions/extension-loader";
export class WindowManager { export class WindowManager {
protected mainView: BrowserWindow; protected mainView: BrowserWindow;
@ -40,6 +41,9 @@ export class WindowManager {
event.preventDefault(); event.preventDefault();
shell.openExternal(url); shell.openExternal(url);
}); });
this.mainView.webContents.on("dom-ready", () => {
extensionLoader.broadcastExtensions()
})
// track visible cluster from ui // track visible cluster from ui
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
@ -72,8 +76,8 @@ export class WindowManager {
try { try {
await this.showSplash(); await this.showSplash();
await this.mainView.loadURL(`http://localhost:${this.proxyPort}`) await this.mainView.loadURL(`http://localhost:${this.proxyPort}`)
this.mainView.show(); this.mainView.show()
this.splashWindow.close(); this.splashWindow.close()
} catch (err) { } catch (err) {
dialog.showErrorBox("ERROR!", err.toString()) dialog.showErrorBox("ERROR!", err.toString())
} }

50
webpack.extensions.ts Executable file
View File

@ -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
}

View File

@ -8,34 +8,9 @@ import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"
import ProgressBarPlugin from "progress-bar-webpack-plugin"; import ProgressBarPlugin from "progress-bar-webpack-plugin";
export default [ export default [
webpackLensRenderer, webpackLensRenderer
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.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 { export function webpackLensRenderer({ showVars = true } = {}): webpack.Configuration {
if (showVars) { if (showVars) {
console.info('WEBPACK:renderer', require("./src/common/vars")); console.info('WEBPACK:renderer', require("./src/common/vars"));