mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
install extensions as normal npm packages
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
de3849d22c
commit
0219c62b8a
2
extensions/example-extension/.gitignore
vendored
2
extensions/example-extension/.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
*.js
|
*.js
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
import { Button, DynamicPageType, Icon, IconProps, LensExtension, React } from "@lens/extensions";
|
import { Button, DynamicPageType, Icon, IconProps, LensExtension, React } from "@lens/extensions";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import _ from "lodash"
|
||||||
|
import stripAnsi from "strip-ansi"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensExtension {
|
export default class ExampleExtension extends LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
|
_.has({a: 1}, "a")
|
||||||
|
stripAnsi("asdasdasd")
|
||||||
console.log('EXAMPLE EXTENSION: ACTIVATED', this.getMeta());
|
console.log('EXAMPLE EXTENSION: ACTIVATED', this.getMeta());
|
||||||
this.registerPage({
|
this.registerPage({
|
||||||
type: DynamicPageType.CLUSTER,
|
type: DynamicPageType.CLUSTER,
|
||||||
@ -36,7 +40,7 @@ export class ExtensionPage extends React.Component<{ extension: ExampleExtension
|
|||||||
return (
|
return (
|
||||||
<TabLayout className="ExampleExtension">
|
<TabLayout className="ExampleExtension">
|
||||||
<div className="flex column gaps align-flex-start">
|
<div className="flex column gaps align-flex-start">
|
||||||
<p>Hello from extensions-api!</p>
|
<p>{stripAnsi("Hello from extensions-api!!!!!!!")}</p>
|
||||||
<p>File: <i>{__filename}</i></p>
|
<p>File: <i>{__filename}</i></p>
|
||||||
<Button accent label="Deactivate" onClick={this.deactivate}/>
|
<Button accent label="Deactivate" onClick={this.deactivate}/>
|
||||||
</div>
|
</div>
|
||||||
32
extensions/example-extension/package-lock.json
generated
Normal file
32
extensions/example-extension/package-lock.json
generated
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "extension-example",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||||
|
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||||
|
},
|
||||||
|
"strip-ansi": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||||
|
"requires": {
|
||||||
|
"ansi-regex": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,19 @@
|
|||||||
"name": "extension-example",
|
"name": "extension-example",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Example extension",
|
"description": "Example extension",
|
||||||
"main": "example-extension.js",
|
"main": "dist/index.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"styles": []
|
"styles": []
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.20",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^4.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,25 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": ".",
|
"outDir": "dist",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"declaration": false
|
"declaration": false,
|
||||||
|
"strict": false,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"jsx": "react"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"../../types",
|
"../../types",
|
||||||
"./example-extension.tsx"
|
"./index.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"*.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
37
package.json
37
package.json
@ -34,8 +34,7 @@
|
|||||||
"download-bins": "concurrently yarn:download:*",
|
"download-bins": "concurrently yarn:download:*",
|
||||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||||
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/",
|
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/"
|
||||||
"extensions:example": "tsc --project extensions/example-extension/tsconfig.json --watch"
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"bundledKubectlVersion": "1.17.11",
|
"bundledKubectlVersion": "1.17.11",
|
||||||
@ -201,6 +200,7 @@
|
|||||||
"module-alias": "^2.2.2",
|
"module-alias": "^2.2.2",
|
||||||
"node-machine-id": "^1.1.12",
|
"node-machine-id": "^1.1.12",
|
||||||
"node-pty": "^0.9.0",
|
"node-pty": "^0.9.0",
|
||||||
|
"npm": "^6.14.8",
|
||||||
"openid-client": "^3.15.2",
|
"openid-client": "^3.15.2",
|
||||||
"path-to-regexp": "^6.1.0",
|
"path-to-regexp": "^6.1.0",
|
||||||
"proper-lockfile": "^4.1.1",
|
"proper-lockfile": "^4.1.1",
|
||||||
@ -235,33 +235,33 @@
|
|||||||
"@lingui/react": "^3.0.0-13",
|
"@lingui/react": "^3.0.0-13",
|
||||||
"@material-ui/core": "^4.10.1",
|
"@material-ui/core": "^4.10.1",
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@types/chart.js": "^2.9.21",
|
||||||
|
"@types/circular-dependency-plugin": "^5.0.1",
|
||||||
|
"@types/color": "^3.0.1",
|
||||||
"@types/crypto-js": "^3.1.47",
|
"@types/crypto-js": "^3.1.47",
|
||||||
|
"@types/dompurify": "^2.0.2",
|
||||||
"@types/electron-window-state": "^2.0.34",
|
"@types/electron-window-state": "^2.0.34",
|
||||||
"@types/fs-extra": "^9.0.1",
|
"@types/fs-extra": "^9.0.1",
|
||||||
|
"@types/hapi": "^18.0.3",
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
|
"@types/html-webpack-plugin": "^3.2.3",
|
||||||
"@types/http-proxy": "^1.17.4",
|
"@types/http-proxy": "^1.17.4",
|
||||||
|
"@types/jest": "^25.2.3",
|
||||||
"@types/js-yaml": "^3.12.4",
|
"@types/js-yaml": "^3.12.4",
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
"@types/lodash": "^4.14.155",
|
"@types/lodash": "^4.14.155",
|
||||||
"@types/marked": "^0.7.4",
|
"@types/marked": "^0.7.4",
|
||||||
"@types/mock-fs": "^4.10.0",
|
|
||||||
"@types/module-alias": "^2.0.0",
|
|
||||||
"@types/node": "^12.12.45",
|
|
||||||
"@types/proper-lockfile": "^4.1.1",
|
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
|
||||||
"@types/tar": "^4.0.3",
|
|
||||||
"@types/chart.js": "^2.9.21",
|
|
||||||
"@types/circular-dependency-plugin": "^5.0.1",
|
|
||||||
"@types/color": "^3.0.1",
|
|
||||||
"@types/dompurify": "^2.0.2",
|
|
||||||
"@types/hapi": "^18.0.3",
|
|
||||||
"@types/hoist-non-react-statics": "^3.3.1",
|
|
||||||
"@types/html-webpack-plugin": "^3.2.3",
|
|
||||||
"@types/jest": "^25.2.3",
|
|
||||||
"@types/material-ui": "^0.21.7",
|
"@types/material-ui": "^0.21.7",
|
||||||
"@types/md5-file": "^4.0.2",
|
"@types/md5-file": "^4.0.2",
|
||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||||
|
"@types/mock-fs": "^4.10.0",
|
||||||
|
"@types/module-alias": "^2.0.0",
|
||||||
|
"@types/node": "^12.12.45",
|
||||||
|
"@types/npm": "^2.0.31",
|
||||||
"@types/progress-bar-webpack-plugin": "^2.1.0",
|
"@types/progress-bar-webpack-plugin": "^2.1.0",
|
||||||
|
"@types/proper-lockfile": "^4.1.1",
|
||||||
"@types/react": "^16.9.35",
|
"@types/react": "^16.9.35",
|
||||||
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-router-dom": "^5.1.5",
|
"@types/react-router-dom": "^5.1.5",
|
||||||
"@types/react-select": "^3.0.13",
|
"@types/react-select": "^3.0.13",
|
||||||
"@types/react-window": "^1.8.2",
|
"@types/react-window": "^1.8.2",
|
||||||
@ -270,6 +270,7 @@
|
|||||||
"@types/semver": "^7.2.0",
|
"@types/semver": "^7.2.0",
|
||||||
"@types/shelljs": "^0.8.8",
|
"@types/shelljs": "^0.8.8",
|
||||||
"@types/spdy": "^3.4.4",
|
"@types/spdy": "^3.4.4",
|
||||||
|
"@types/tar": "^4.0.3",
|
||||||
"@types/tcp-port-used": "^1.0.0",
|
"@types/tcp-port-used": "^1.0.0",
|
||||||
"@types/tempy": "^0.3.0",
|
"@types/tempy": "^0.3.0",
|
||||||
"@types/terser-webpack-plugin": "^3.0.0",
|
"@types/terser-webpack-plugin": "^3.0.0",
|
||||||
@ -319,10 +320,10 @@
|
|||||||
"progress-bar-webpack-plugin": "^2.1.0",
|
"progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"raw-loader": "^4.0.1",
|
"raw-loader": "^4.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-beautiful-dnd": "^13.0.0",
|
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
"react-select": "^3.1.0",
|
"react-select": "^3.1.0",
|
||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
"rollup": "^2.28.2",
|
"rollup": "^2.28.2",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { createIpcChannel } from "./ipc";
|
import { createIpcChannel } from "./ipc";
|
||||||
import { ClusterId, clusterStore } from "./cluster-store";
|
import { ClusterId, clusterStore } from "./cluster-store";
|
||||||
|
import { extensionLoader } from "../extensions/extension-loader"
|
||||||
import { tracker } from "./tracker";
|
import { tracker } from "./tracker";
|
||||||
|
|
||||||
export const clusterIpc = {
|
export const clusterIpc = {
|
||||||
@ -8,7 +9,10 @@ export const clusterIpc = {
|
|||||||
handle: (clusterId: ClusterId, frameId?: number) => {
|
handle: (clusterId: ClusterId, frameId?: number) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
if (frameId) {
|
||||||
|
cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||||
|
}
|
||||||
|
extensionLoader.broadcastExtensions(frameId)
|
||||||
return cluster.activate();
|
return cluster.activate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
111
src/extensions/extension-loader.ts
Normal file
111
src/extensions/extension-loader.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import type { ExtensionId, LensExtension, ExtensionManifest } from "./lens-extension"
|
||||||
|
import { broadcastIpc } from "../common/ipc"
|
||||||
|
import type { LensRuntimeRendererEnv } from "./lens-runtime"
|
||||||
|
import { ExtensionModel } from "./extension-store"
|
||||||
|
import path from "path"
|
||||||
|
import { observable, reaction, toJS, } from "mobx"
|
||||||
|
import logger from "../main/logger"
|
||||||
|
import { app, remote, ipcRenderer } from "electron"
|
||||||
|
|
||||||
|
export interface InstalledExtension extends ExtensionModel {
|
||||||
|
manifestPath: string;
|
||||||
|
manifest: ExtensionManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazy load so that we get correct userData
|
||||||
|
export function extensionPackagesRoot() {
|
||||||
|
return path.join((app || remote.app).getPath("userData"))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withExtensionPackagesRoot(callback: Function) {
|
||||||
|
const cwd = process.cwd()
|
||||||
|
try {
|
||||||
|
process.chdir(path.resolve(extensionPackagesRoot()))
|
||||||
|
return callback()
|
||||||
|
} finally {
|
||||||
|
process.chdir(cwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExtensionLoader {
|
||||||
|
@observable extensions = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
|
||||||
|
@observable instances = observable.map<ExtensionId, LensExtension>([], { deep: false })
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (ipcRenderer) {
|
||||||
|
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
|
||||||
|
extensions.forEach((ext) => {
|
||||||
|
if (!this.getById(ext.manifestPath)) {
|
||||||
|
this.extensions.set(ext.manifestPath, ext)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
autoEnableOnLoad(getLensRuntimeEnv: () => LensRuntimeRendererEnv, { delay = 0 } = {}) {
|
||||||
|
logger.info('[EXTENSIONS-MANAGER]: auto-activation loaded extensions: ON');
|
||||||
|
return reaction(() => this.extensions.toJS(), installedExtensions => {
|
||||||
|
installedExtensions.forEach((ext) => {
|
||||||
|
let instance = this.instances.get(ext.manifestPath)
|
||||||
|
if (!instance) {
|
||||||
|
const extensionModule = this.requireExtension(ext)
|
||||||
|
if (!extensionModule) {
|
||||||
|
logger.error("[EXTENSION-MANAGER] failed to load extension " + ext.manifestPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const LensExtensionClass = extensionModule.default;
|
||||||
|
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
|
||||||
|
instance.enable(getLensRuntimeEnv());
|
||||||
|
this.instances.set(ext.id, instance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
fireImmediately: true,
|
||||||
|
delay: delay,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected requireExtension(extension: InstalledExtension) {
|
||||||
|
return withExtensionPackagesRoot(() => {
|
||||||
|
try {
|
||||||
|
const extMain = path.join(path.dirname(extension.manifestPath), extension.manifest.main)
|
||||||
|
return __non_webpack_require__(extMain)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[EXTENSION-MANAGER]: can't load extension main at ${extension.manifestPath}: ${err}`, { extension });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id: ExtensionId): InstalledExtension {
|
||||||
|
return this.extensions.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeById(id: ExtensionId) {
|
||||||
|
const extension = this.getById(id);
|
||||||
|
if (extension) {
|
||||||
|
const instance = this.instances.get(extension.id)
|
||||||
|
if (instance) { await instance.uninstall() }
|
||||||
|
this.extensions.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastExtensions(frameId?: number) {
|
||||||
|
broadcastIpc({
|
||||||
|
channel: "extensions:loaded",
|
||||||
|
frameId: frameId,
|
||||||
|
frameOnly: !!frameId,
|
||||||
|
args: [this.toJSON().extensions],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return toJS({
|
||||||
|
extensions: Array.from(this.extensions).map(([id, instance]) => instance),
|
||||||
|
}, {
|
||||||
|
recurseEverything: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extensionLoader = new ExtensionLoader()
|
||||||
113
src/extensions/extension-manager.ts
Normal file
113
src/extensions/extension-manager.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
export class ExtensionManager {
|
||||||
|
get extensionPackagesRoot() {
|
||||||
|
return extensionPackagesRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
get folderPath(): string {
|
||||||
|
return path.resolve(__static, "../extensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
|
||||||
|
|
||||||
|
fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules"))
|
||||||
|
fs.writeFileSync(path.join(this.extensionPackagesRoot, "package.json"), `{"dependencies": []}`, {mode: 0o600})
|
||||||
|
|
||||||
|
return await this.loadExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExtensionByManifest(manifestPath: string): Promise<InstalledExtension> {
|
||||||
|
let manifestJson: ExtensionManifest;
|
||||||
|
let mainJs: string;
|
||||||
|
try {
|
||||||
|
manifestJson = __non_webpack_require__(manifestPath)
|
||||||
|
withExtensionPackagesRoot(() => {
|
||||||
|
this.installPackageFromPath(path.dirname(manifestPath))
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
|
||||||
|
return {
|
||||||
|
id: manifestJson.name,
|
||||||
|
version: manifestJson.version,
|
||||||
|
name: manifestJson.name,
|
||||||
|
manifestPath: path.join(this.extensionPackagesRoot, "node_modules", manifestJson.name, "package.json"),
|
||||||
|
manifest: manifestJson
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson, mainJs });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected installPackageFromPath(path: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
npm.load({
|
||||||
|
production: true,
|
||||||
|
global: false,
|
||||||
|
prefix: this.extensionPackagesRoot,
|
||||||
|
loglevel: "silent",
|
||||||
|
dev: false
|
||||||
|
}, (err) => {
|
||||||
|
npm.commands.install([
|
||||||
|
path
|
||||||
|
], (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected installPackage(name: string, version: string | number): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
npm.load({
|
||||||
|
production: true,
|
||||||
|
global: false,
|
||||||
|
prefix: this.extensionPackagesRoot
|
||||||
|
}, (err) => {
|
||||||
|
npm.commands.install([
|
||||||
|
`${name}@${version}`
|
||||||
|
], (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadExtensions() {
|
||||||
|
const extensions = await this.loadFromFolder(this.folderPath);
|
||||||
|
return new Map(extensions.map(ext => [ext.id, ext]));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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)
|
||||||
|
console.info(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||||
|
return extensions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extensionManager = new ExtensionManager()
|
||||||
@ -1,13 +1,8 @@
|
|||||||
import type { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./lens-extension";
|
import type { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./lens-extension";
|
||||||
import type { LensRuntimeRendererEnv } from "./lens-runtime";
|
import { observable, toJS, } from "mobx";
|
||||||
import path from "path";
|
|
||||||
import fs from "fs-extra";
|
|
||||||
import { action, observable, reaction, toJS, } from "mobx";
|
|
||||||
import { BaseStore } from "../common/base-store";
|
import { BaseStore } from "../common/base-store";
|
||||||
import logger from "../main/logger";
|
|
||||||
|
|
||||||
export interface ExtensionStoreModel {
|
export interface ExtensionStoreModel {
|
||||||
version: ExtensionVersion;
|
|
||||||
extensions: [ExtensionId, ExtensionModel][]
|
extensions: [ExtensionId, ExtensionModel][]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,76 +32,7 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable version: ExtensionVersion = "0.0.0";
|
|
||||||
@observable extensions = observable.map<ExtensionId, LensExtension>();
|
@observable extensions = observable.map<ExtensionId, LensExtension>();
|
||||||
@observable removed = observable.map<ExtensionId, LensExtension>();
|
|
||||||
@observable installed = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
|
|
||||||
|
|
||||||
get folderPath(): string {
|
|
||||||
return path.resolve(__static, "../extensions");
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
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 {
|
getById(id: ExtensionId): LensExtension {
|
||||||
return this.extensions.get(id);
|
return this.extensions.get(id);
|
||||||
@ -120,44 +46,8 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
protected fromStore({ extensions, version }: ExtensionStoreModel) {
|
|
||||||
if (version) {
|
|
||||||
this.version = version;
|
|
||||||
}
|
|
||||||
if (extensions) {
|
|
||||||
const currentExtensions = new Map(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 {
|
toJSON(): ExtensionStoreModel {
|
||||||
return toJS({
|
return toJS({
|
||||||
version: this.version,
|
|
||||||
extensions: Array.from(this.extensions).map(([id, instance]) => [id, instance.toJSON()]),
|
extensions: Array.from(this.extensions).map(([id, instance]) => [id, instance.toJSON()]),
|
||||||
}, {
|
}, {
|
||||||
recurseEverything: true,
|
recurseEverything: true,
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import { clusterStore } from "../common/cluster-store"
|
|||||||
import { userStore } from "../common/user-store";
|
import { userStore } from "../common/user-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
import { workspaceStore } from "../common/workspace-store";
|
||||||
import { tracker } from "../common/tracker";
|
import { tracker } from "../common/tracker";
|
||||||
|
import { extensionManager } from "../extensions/extension-manager";
|
||||||
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
@ -75,6 +77,8 @@ async function main() {
|
|||||||
|
|
||||||
// create window manager and open app
|
// create window manager and open app
|
||||||
windowManager = new WindowManager(proxyPort);
|
windowManager = new WindowManager(proxyPort);
|
||||||
|
|
||||||
|
extensionLoader.extensions.replace(await extensionManager.load())
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on("ready", main);
|
app.on("ready", main);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { render, unmountComponentAtNode } from "react-dom";
|
|||||||
import { isMac } from "../common/vars";
|
import { isMac } from "../common/vars";
|
||||||
import { userStore } from "../common/user-store";
|
import { userStore } from "../common/user-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
import { workspaceStore } from "../common/workspace-store";
|
||||||
import { extensionStore } from "../extensions/extension-store";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { clusterStore } from "../common/cluster-store";
|
||||||
import { i18nStore } from "./i18n";
|
import { i18nStore } from "./i18n";
|
||||||
import { themeStore } from "./theme.store";
|
import { themeStore } from "./theme.store";
|
||||||
@ -25,18 +25,17 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
userStore.load(),
|
userStore.load(),
|
||||||
workspaceStore.load(),
|
workspaceStore.load(),
|
||||||
clusterStore.load(),
|
clusterStore.load(),
|
||||||
extensionStore.load(),
|
|
||||||
i18nStore.init(),
|
i18nStore.init(),
|
||||||
themeStore.init(),
|
themeStore.init(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Register additional store listeners
|
// Register additional store listeners
|
||||||
clusterStore.registerIpcListener();
|
clusterStore.registerIpcListener();
|
||||||
|
extensionLoader.autoEnableOnLoad(getLensRuntime);
|
||||||
|
|
||||||
// init app's dependencies if any
|
// init app's dependencies if any
|
||||||
if (App.init) {
|
if (App.init) {
|
||||||
await App.init();
|
await App.init();
|
||||||
extensionStore.autoEnableOnLoad(getLensRuntime);
|
|
||||||
}
|
}
|
||||||
window.addEventListener("message", (ev: MessageEvent) => {
|
window.addEventListener("message", (ev: MessageEvent) => {
|
||||||
if (ev.data === "teardown") {
|
if (ev.data === "teardown") {
|
||||||
|
|||||||
@ -18,12 +18,12 @@ export class Extensions extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { installed: installedExtensions } = extensionStore;
|
const { extensions } = extensionStore;
|
||||||
return (
|
return (
|
||||||
<WizardLayout className="Extensions" infoPanel={this.renderInfoPanel()}>
|
<WizardLayout className="Extensions" infoPanel={this.renderInfoPanel()}>
|
||||||
<h2>Extensions</h2>
|
<h2>Extensions</h2>
|
||||||
<pre>
|
<pre>
|
||||||
{JSON.stringify(installedExtensions.toJSON(), null, 2)}
|
{JSON.stringify(extensions.toJSON(), null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</WizardLayout>
|
</WizardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user