mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
cleanup and added a router for installing extensions
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
21917c17e3
commit
cf90044979
@ -33,6 +33,7 @@ module.exports = {
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1,
|
||||
}],
|
||||
"no-invalid-this": "error",
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
@ -95,6 +96,7 @@ module.exports = {
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1,
|
||||
}],
|
||||
"no-invalid-this": "error",
|
||||
"quotes": ["error", "double", {
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true,
|
||||
@ -160,6 +162,7 @@ module.exports = {
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true,
|
||||
}],
|
||||
"no-invalid-this": "error",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": ["error"],
|
||||
"object-shorthand": "error",
|
||||
|
||||
337
package.json
337
package.json
@ -1,79 +1,52 @@
|
||||
{
|
||||
"name": "kontena-lens",
|
||||
"productName": "Lens",
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"version": "4.0.0-rc.3",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2020, Mirantis, Inc.",
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Mirantis, Inc.",
|
||||
"email": "info@k8slens.dev"
|
||||
},
|
||||
"main": "static/build/main.js",
|
||||
"scripts": {
|
||||
"build:linux": "yarn run compile && electron-builder --linux --config electron-builder.yml --dir -c.productName=Lens",
|
||||
"build:mac": "yarn run compile && electron-builder --mac --config electron-builder.yml --dir -c.productName=Lens",
|
||||
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
|
||||
"build:win": "yarn run compile && electron-builder --win --config electron-builder.yml --dir -c.productName=Lens",
|
||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||
"compile:extension-types": "yarn run webpack --config webpack.extensions.ts",
|
||||
"compile:i18n": "yarn run lingui compile",
|
||||
"compile:main": "yarn run webpack --config webpack.main.ts",
|
||||
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
||||
"dev": "concurrently -k \"yarn run dev-run -C\" yarn:dev:*",
|
||||
"dev-build": "concurrently yarn:compile:*",
|
||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
|
||||
"dev:extension-types": "yarn run compile:extension-types --watch",
|
||||
"dev:main": "yarn run compile:main --watch",
|
||||
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
||||
"dev:extension-types": "yarn run compile:extension-types --watch",
|
||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||
"compile:main": "yarn run webpack --config webpack.main.ts",
|
||||
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
||||
"compile:i18n": "yarn run lingui compile",
|
||||
"compile:extension-types": "yarn run webpack --config webpack.extensions.ts",
|
||||
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
|
||||
"build:linux": "yarn run compile && electron-builder --linux --config electron-builder.yml --dir -c.productName=Lens",
|
||||
"build:mac": "yarn run compile && electron-builder --mac --config electron-builder.yml --dir -c.productName=Lens",
|
||||
"build:win": "yarn run compile && electron-builder --win --config electron-builder.yml --dir -c.productName=Lens",
|
||||
"test": "jest --env=jsdom src $@",
|
||||
"integration": "jest --coverage integration $@",
|
||||
"dist": "yarn run compile && electron-builder --publish onTag",
|
||||
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
||||
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
|
||||
"postinstall": "patch-package",
|
||||
"i18n:extract": "yarn run lingui extract",
|
||||
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
||||
"download-bins": "concurrently yarn:download:*",
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"i18n:extract": "yarn run lingui extract",
|
||||
"postinstall": "patch-package",
|
||||
"integration": "jest --coverage integration $@",
|
||||
"lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 .",
|
||||
"lint:fix": "yarn run lint --fix",
|
||||
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
||||
"verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
||||
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts"
|
||||
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
|
||||
"test": "jest --env=jsdom src $@",
|
||||
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts",
|
||||
"verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict"
|
||||
},
|
||||
"config": {
|
||||
"bundledKubectlVersion": "1.17.11",
|
||||
"bundledHelmVersion": "3.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12 <13"
|
||||
},
|
||||
"lingui": {
|
||||
"locales": [
|
||||
"en",
|
||||
"ru",
|
||||
"fi"
|
||||
],
|
||||
"format": "po",
|
||||
"sourceLocale": "en",
|
||||
"fallbackLocale": "en",
|
||||
"compileNamespace": "cjs",
|
||||
"catalogs": [
|
||||
{
|
||||
"path": "./locales/{locale}/messages",
|
||||
"include": "./src/renderer"
|
||||
}
|
||||
]
|
||||
"bundledHelmVersion": "3.3.4",
|
||||
"bundledKubectlVersion": "1.17.11"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverage": false,
|
||||
"verbose": true,
|
||||
"testEnvironment": "node",
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||
"^@lingui/macro$": "<rootDir>/__mocks__/@linguiMacro.ts"
|
||||
@ -84,7 +57,15 @@
|
||||
],
|
||||
"setupFiles": [
|
||||
"<rootDir>/src/jest.setup.ts"
|
||||
]
|
||||
],
|
||||
"testEnvironment": "node",
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"verbose": true
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12 <13"
|
||||
},
|
||||
"build": {
|
||||
"files": [
|
||||
@ -184,6 +165,7 @@
|
||||
"confinement": "classic"
|
||||
}
|
||||
},
|
||||
"copyright": "© 2020, Mirantis, Inc.",
|
||||
"lens": {
|
||||
"extensions": [
|
||||
"telemetry",
|
||||
@ -194,104 +176,46 @@
|
||||
"kube-object-event-status"
|
||||
]
|
||||
},
|
||||
"lingui": {
|
||||
"locales": [
|
||||
"en",
|
||||
"ru",
|
||||
"fi"
|
||||
],
|
||||
"format": "po",
|
||||
"sourceLocale": "en",
|
||||
"fallbackLocale": "en",
|
||||
"compileNamespace": "cjs",
|
||||
"catalogs": [
|
||||
{
|
||||
"path": "./locales/{locale}/messages",
|
||||
"include": "./src/renderer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"productName": "Lens",
|
||||
"dependencies": {
|
||||
"@hapi/call": "^8.0.0",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.12.0",
|
||||
"@lingui/loader": "^3.0.0-13",
|
||||
"@lingui/macro": "^3.0.0-13",
|
||||
"@lingui/react": "^3.0.0-13",
|
||||
"@material-ui/core": "^4.10.1",
|
||||
"@types/chart.js": "^2.9.21",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
"@types/electron-window-state": "^2.0.34",
|
||||
"@types/dompurify": "^2.0.2",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/hapi": "^18.0.3",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@types/js-yaml": "^3.12.4",
|
||||
"@types/jsdom": "^16.2.4",
|
||||
"@types/jsonpath": "^0.2.0",
|
||||
"@types/lodash": "^4.14.155",
|
||||
"@types/marked": "^0.7.4",
|
||||
"@types/mock-fs": "^4.10.0",
|
||||
"@types/node": "^12.12.45",
|
||||
"@types/proper-lockfile": "^4.1.1",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/tar": "^4.0.4",
|
||||
"@types/url-parse": "^1.4.3",
|
||||
"array-move": "^3.0.0",
|
||||
"await-lock": "^2.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"command-exists": "1.2.9",
|
||||
"conf": "^7.0.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-updater": "^4.3.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"filenamify": "^4.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"handlebars": "^4.7.6",
|
||||
"http-proxy": "^1.18.1",
|
||||
"js-yaml": "^3.14.0",
|
||||
"jsdom": "^16.4.0",
|
||||
"jsonpath": "^1.0.2",
|
||||
"lodash": "^4.17.15",
|
||||
"mac-ca": "^1.0.4",
|
||||
"marked": "^1.1.0",
|
||||
"md5-file": "^5.0.0",
|
||||
"mobx": "^5.15.7",
|
||||
"mobx-observable-history": "^1.0.3",
|
||||
"mock-fs": "^4.12.0",
|
||||
"node-pty": "^0.9.0",
|
||||
"npm": "^6.14.8",
|
||||
"openid-client": "^3.15.2",
|
||||
"path-to-regexp": "^6.1.0",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.8",
|
||||
"semver": "^7.3.2",
|
||||
"serializr": "^2.0.3",
|
||||
"shell-env": "^3.0.0",
|
||||
"spdy": "^4.0.2",
|
||||
"tar": "^6.0.5",
|
||||
"tcp-port-used": "^1.0.1",
|
||||
"tempy": "^0.5.0",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^8.1.0",
|
||||
"win-ca": "^3.2.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-transport-browserconsole": "^1.0.5",
|
||||
"ws": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-runtime": "^7.6.2",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@babel/preset-react": "^7.10.1",
|
||||
"@babel/preset-typescript": "^7.10.1",
|
||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||
"@lingui/babel-preset-react": "^2.9.1",
|
||||
"@lingui/cli": "^3.0.0-13",
|
||||
"@lingui/loader": "^3.0.0-13",
|
||||
"@lingui/macro": "^3.0.0-13",
|
||||
"@lingui/react": "^3.0.0-13",
|
||||
"@material-ui/core": "^4.10.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@testing-library/jest-dom": "^5.11.5",
|
||||
"@testing-library/react": "^11.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/dompurify": "^2.0.2",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/electron-window-state": "^2.0.34",
|
||||
"@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/jest": "^25.2.3",
|
||||
"@types/js-yaml": "^3.12.4",
|
||||
"@types/jsonpath": "^0.2.0",
|
||||
"@types/lodash": "^4.14.155",
|
||||
"@types/marked": "^0.7.4",
|
||||
"@types/material-ui": "^0.21.7",
|
||||
"@types/md5-file": "^4.0.2",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
@ -312,58 +236,54 @@
|
||||
"@types/sharp": "^0.26.0",
|
||||
"@types/shelljs": "^0.8.8",
|
||||
"@types/spdy": "^3.4.4",
|
||||
"@types/tar": "^4.0.4",
|
||||
"@types/tcp-port-used": "^1.0.0",
|
||||
"@types/tempy": "^0.3.0",
|
||||
"@types/terser-webpack-plugin": "^3.0.0",
|
||||
"@types/universal-analytics": "^0.4.4",
|
||||
"@types/url-parse": "^1.4.3",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"@types/webdriverio": "^4.13.0",
|
||||
"@types/webpack": "^4.41.17",
|
||||
"@types/webpack-dev-server": "^3.11.1",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@types/webpack-node-externals": "^1.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"ace-builds": "^1.4.11",
|
||||
"ansi_up": "^4.0.4",
|
||||
"babel-core": "^7.0.0-beta.3",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-macros": "^2.8.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"array-move": "^3.0.0",
|
||||
"await-lock": "^2.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"circular-dependency-plugin": "^5.2.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"color": "^3.1.2",
|
||||
"command-exists": "1.2.9",
|
||||
"concurrently": "^5.2.0",
|
||||
"conf": "^7.0.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"css-element-queries": "^1.2.3",
|
||||
"css-loader": "^3.5.3",
|
||||
"dompurify": "^2.0.11",
|
||||
"electron": "^9.3.5",
|
||||
"electron-builder": "^22.7.0",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-unused-imports": "^1.0.1",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-updater": "^4.3.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"file-loader": "^6.0.0",
|
||||
"filenamify": "^4.1.0",
|
||||
"flex.box": "^3.4.4",
|
||||
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"include-media": "^1.4.9",
|
||||
"jest": "^26.0.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-mock-extended": "^1.0.10",
|
||||
"fs-extra": "^9.0.1",
|
||||
"handlebars": "^4.7.6",
|
||||
"http-proxy": "^1.18.1",
|
||||
"js-yaml": "^3.14.0",
|
||||
"jsdom": "^16.4.0",
|
||||
"jsonpath": "^1.0.2",
|
||||
"lodash": "^4.17.15",
|
||||
"mac-ca": "^1.0.4",
|
||||
"make-plural": "^6.2.1",
|
||||
"marked": "^1.1.0",
|
||||
"md5-file": "^5.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mobx": "^5.15.7",
|
||||
"mobx-observable-history": "^1.0.3",
|
||||
"mobx-react": "^6.2.2",
|
||||
"mock-fs": "^4.12.0",
|
||||
"moment": "^2.26.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"nodemon": "^2.0.4",
|
||||
"patch-package": "^6.2.2",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.2.0",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"node-pty": "^0.9.0",
|
||||
"npm": "^6.14.8",
|
||||
"openid-client": "^3.15.2",
|
||||
"path-to-regexp": "^6.1.0",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"raw-loader": "^4.0.1",
|
||||
"react": "^16.14.0",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
@ -373,6 +293,77 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^3.1.0",
|
||||
"react-window": "^1.8.5",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.8",
|
||||
"semver": "^7.3.2",
|
||||
"serializr": "^2.0.3",
|
||||
"shell-env": "^3.0.0",
|
||||
"spdy": "^4.0.2",
|
||||
"tar": "^6.0.5",
|
||||
"tcp-port-used": "^1.0.1",
|
||||
"tempy": "^0.5.0",
|
||||
"url-loader": "^4.1.0",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^8.1.0",
|
||||
"what-input": "^5.2.10",
|
||||
"win-ca": "^3.2.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-transport-browserconsole": "^1.0.5",
|
||||
"ws": "^7.3.0",
|
||||
"xterm": "^4.6.0",
|
||||
"xterm-addon-fit": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-runtime": "^7.6.2",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@babel/preset-react": "^7.10.1",
|
||||
"@babel/preset-typescript": "^7.10.1",
|
||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||
"@lingui/babel-preset-react": "^2.9.1",
|
||||
"@lingui/cli": "^3.0.0-13",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@testing-library/jest-dom": "^5.11.5",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@types/circular-dependency-plugin": "^5.0.1",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/html-webpack-plugin": "^3.2.3",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/terser-webpack-plugin": "^3.0.0",
|
||||
"@types/universal-analytics": "^0.4.4",
|
||||
"@types/webdriverio": "^4.13.0",
|
||||
"@types/webpack": "^4.41.17",
|
||||
"@types/webpack-dev-server": "^3.11.1",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@types/webpack-node-externals": "^1.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"babel-core": "^7.0.0-beta.3",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-macros": "^2.8.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"circular-dependency-plugin": "^5.2.0",
|
||||
"electron": "^9.3.5",
|
||||
"electron-builder": "^22.7.0",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-unused-imports": "^1.0.1",
|
||||
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"include-media": "^1.4.9",
|
||||
"jest": "^26.0.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-mock-extended": "^1.0.10",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"nodemon": "^2.0.4",
|
||||
"patch-package": "^6.2.2",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sharp": "^0.26.1",
|
||||
"spectron": "11.0.0",
|
||||
@ -386,13 +377,9 @@
|
||||
"typedoc-plugin-markdown": "^2.4.0",
|
||||
"typeface-roboto": "^0.0.75",
|
||||
"typescript": "^4.0.2",
|
||||
"url-loader": "^4.1.0",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"what-input": "^5.2.10",
|
||||
"xterm": "^4.6.0",
|
||||
"xterm-addon-fit": "^0.4.0"
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,7 @@ import AwaitLock from "await-lock";
|
||||
import child_process from "child_process";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { autobind } from "../common/utils";
|
||||
import logger from "../main/logger";
|
||||
import { LensProtocolRouter } from "../main/protocol-handler";
|
||||
import { extensionPackagesRoot } from "./extension-loader";
|
||||
|
||||
const logModule = "[EXTENSION-INSTALLER]";
|
||||
@ -22,13 +20,6 @@ export type PackageJson = {
|
||||
* Installs dependencies for extensions
|
||||
*/
|
||||
export class ExtensionInstaller {
|
||||
|
||||
constructor() {
|
||||
const lpr = LensProtocolRouter.getInstance<LensProtocolRouter>();
|
||||
|
||||
lpr.on("/install-extension", this.protocolHandlerInstall);
|
||||
}
|
||||
|
||||
private installLock = new AwaitLock();
|
||||
|
||||
get extensionPackagesRoot() {
|
||||
@ -39,11 +30,6 @@ export class ExtensionInstaller {
|
||||
return __non_webpack_require__.resolve("npm/bin/npm-cli");
|
||||
}
|
||||
|
||||
@autobind()
|
||||
protocolHandlerInstall(): void {
|
||||
console.log("Installing");
|
||||
}
|
||||
|
||||
installDependencies(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`);
|
||||
|
||||
12
src/renderer/api/protocol-endpoints/index.ts
Normal file
12
src/renderer/api/protocol-endpoints/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { LensProtocolRouter } from "../../../main/protocol-handler";
|
||||
import { installExtension } from "./install-extension";
|
||||
|
||||
export function registerHandlers() {
|
||||
const lpr: LensProtocolRouter = LensProtocolRouter.getInstance();
|
||||
|
||||
lpr.on("/install-extension", installExtension);
|
||||
}
|
||||
|
||||
export default {
|
||||
registerHandlers
|
||||
};
|
||||
17
src/renderer/api/protocol-endpoints/install-extension.ts
Normal file
17
src/renderer/api/protocol-endpoints/install-extension.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import logger from "../../../main/logger";
|
||||
import { RouteParams } from "../../../main/protocol-handler";
|
||||
import { installFromNpm } from "../../components/+extensions";
|
||||
|
||||
export async function installExtension(params: RouteParams): Promise<void> {
|
||||
const { extId } = params.search;
|
||||
|
||||
if (!extId) {
|
||||
return void logger.info("installExtension handler: missing 'extId' from search params");
|
||||
}
|
||||
|
||||
try {
|
||||
await installFromNpm(extId);
|
||||
} catch (error) {
|
||||
logger.error("[PH - Install Extension]: failed to install from NPM", error);
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ import { App } from "./components/app";
|
||||
import { i18nStore } from "./i18n";
|
||||
import { LensApp } from "./lens-app";
|
||||
import { themeStore } from "./theme.store";
|
||||
import protocolEndpoints from "./api/protocol-endpoints";
|
||||
|
||||
type AppComponent = React.ComponentType & {
|
||||
init?(): Promise<void>;
|
||||
@ -36,6 +37,7 @@ export async function bootstrap(App: AppComponent) {
|
||||
|
||||
extensionLoader.init();
|
||||
extensionDiscovery.init();
|
||||
protocolEndpoints.registerHandlers();
|
||||
|
||||
// preload common stores
|
||||
await Promise.all([
|
||||
|
||||
@ -10,4 +10,5 @@ interface ExtensionState {
|
||||
@autobind()
|
||||
export class ExtensionStateStore extends Singleton {
|
||||
extensionState = observable.map<string, ExtensionState>();
|
||||
@observable startingInstall = false;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import React from "react";
|
||||
import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils";
|
||||
import { autobind, downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils";
|
||||
import { docsUrl } from "../../../common/vars";
|
||||
import { extensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
|
||||
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||
@ -41,10 +41,349 @@ interface InstallRequestValidated extends InstallRequestPreloaded {
|
||||
tempFile: string; // temp system path to packed extension for unpacking
|
||||
}
|
||||
|
||||
function searchForExtensions(searchText = "") {
|
||||
return Array.from(extensionLoader.userExtensions.values())
|
||||
.filter(({ manifest: { name, description } }) => (
|
||||
name.toLowerCase().includes(searchText)
|
||||
|| description?.toLowerCase().includes(searchText)
|
||||
));
|
||||
}
|
||||
|
||||
async function validatePackage(filePath: string): Promise < LensExtensionManifest > {
|
||||
const tarFiles = await listTarEntries(filePath);
|
||||
|
||||
// tarball from npm contains single root folder "package/*"
|
||||
const firstFile = tarFiles[0];
|
||||
|
||||
if(!firstFile) {
|
||||
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
|
||||
}
|
||||
|
||||
const rootFolder = path.normalize(firstFile).split(path.sep)[0];
|
||||
const packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder));
|
||||
const manifestLocation = packedInRootFolder ? path.join(rootFolder, manifestFilename) : manifestFilename;
|
||||
|
||||
if(!tarFiles.includes(manifestLocation)) {
|
||||
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
|
||||
}
|
||||
|
||||
const manifest = await readFileFromTar<LensExtensionManifest>({
|
||||
tarPath: filePath,
|
||||
filePath: manifestLocation,
|
||||
parseJson: true,
|
||||
});
|
||||
|
||||
if (!manifest.lens && !manifest.renderer) {
|
||||
throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
async function preloadExtensions(requests: InstallRequest[], { showError = true } = {}) {
|
||||
const preloadedRequests = requests.filter(request => request.data);
|
||||
|
||||
await Promise.all(
|
||||
requests
|
||||
.filter(request => !request.data && request.filePath)
|
||||
.map(async request => {
|
||||
try {
|
||||
const data = await fse.readFile(request.filePath);
|
||||
|
||||
request.data = data;
|
||||
preloadedRequests.push(request);
|
||||
|
||||
return request;
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
Notifications.error(`Error while reading "${request.filePath}": ${String(error)}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return preloadedRequests as InstallRequestPreloaded[];
|
||||
}
|
||||
|
||||
async function createTempFilesAndValidate(requests: InstallRequestPreloaded[], { showErrors = true } = {}) {
|
||||
const validatedRequests: InstallRequestValidated[] = [];
|
||||
|
||||
// copy files to temp
|
||||
await fse.ensureDir(getExtensionPackageTemp());
|
||||
|
||||
for (const request of requests) {
|
||||
const tempFile = getExtensionPackageTemp(request.fileName);
|
||||
|
||||
await fse.writeFile(tempFile, request.data);
|
||||
}
|
||||
|
||||
// validate packages
|
||||
await Promise.all(
|
||||
requests.map(async req => {
|
||||
const tempFile = getExtensionPackageTemp(req.fileName);
|
||||
|
||||
try {
|
||||
const manifest = await validatePackage(tempFile);
|
||||
|
||||
validatedRequests.push({
|
||||
...req,
|
||||
manifest,
|
||||
tempFile,
|
||||
});
|
||||
} catch (error) {
|
||||
fse.unlink(tempFile).catch(() => null); // remove invalid temp package
|
||||
|
||||
if (showErrors) {
|
||||
Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<p>Installing <em>{req.fileName}</em> has failed, skipping.</p>
|
||||
<p>Reason: <em>{String(error)}</em></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return validatedRequests;
|
||||
}
|
||||
|
||||
async function requestInstall(init: InstallRequest | InstallRequest[]) {
|
||||
const requests = Array.isArray(init) ? init : [init];
|
||||
const preloadedRequests = await preloadExtensions(requests);
|
||||
const validatedRequests = await createTempFilesAndValidate(preloadedRequests);
|
||||
|
||||
// If there are no requests for installing, reset startingInstall state
|
||||
if (validatedRequests.length === 0) {
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().startingInstall = false;
|
||||
}
|
||||
|
||||
for (const install of validatedRequests) {
|
||||
const { name, version, description } = install.manifest;
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const folderExists = await fse.pathExists(extensionFolder);
|
||||
|
||||
if (!folderExists) {
|
||||
// auto-install extension if not yet exists
|
||||
unpackExtension(install);
|
||||
} else {
|
||||
// If we show the confirmation dialog, we stop the install spinner until user clicks ok
|
||||
// and the install continues
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().startingInstall = false;
|
||||
|
||||
// otherwise confirmation required (re-install / update)
|
||||
const removeNotification = Notifications.info(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
<div className="flex column gaps">
|
||||
<p>Install extension <b>{name}@{version}</b>?</p>
|
||||
<p>Description: <em>{description}</em></p>
|
||||
<div className="remove-folder-warning" onClick={() => shell.openPath(extensionFolder)}>
|
||||
<b>Warning:</b> <code>{extensionFolder}</code> will be removed before installation.
|
||||
</div>
|
||||
</div>
|
||||
<Button autoFocus label="Install" onClick={() => {
|
||||
removeNotification();
|
||||
unpackExtension(install);
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) {
|
||||
const displayName = extensionDisplayName(name, version);
|
||||
const extensionId = path.join(extensionDiscovery.nodeModulesPath, name, "package.json");
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.set(extensionId, {
|
||||
state: "installing",
|
||||
displayName
|
||||
});
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().startingInstall = false;
|
||||
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const unpackingTempFolder = path.join(path.dirname(tempFile), `${path.basename(tempFile)}-unpacked`);
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
try {
|
||||
// extract to temp folder first
|
||||
await fse.remove(unpackingTempFolder).catch(Function);
|
||||
await fse.ensureDir(unpackingTempFolder);
|
||||
await extractTar(tempFile, { cwd: unpackingTempFolder });
|
||||
|
||||
// move contents to extensions folder
|
||||
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
||||
let unpackedRootFolder = unpackingTempFolder;
|
||||
|
||||
if (unpackedFiles.length === 1) {
|
||||
// check if %extension.tgz was packed with single top folder,
|
||||
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
||||
}
|
||||
|
||||
await fse.ensureDir(extensionFolder);
|
||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
} catch (error) {
|
||||
Notifications.error(
|
||||
<p>Installing extension <b>{displayName}</b> has failed: <em>{error}</em></p>
|
||||
);
|
||||
|
||||
// Remove install state on install failure
|
||||
if (ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.get(extensionId)?.state === "installing") {
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.delete(extensionId);
|
||||
}
|
||||
} finally {
|
||||
// clean up
|
||||
fse.remove(unpackingTempFolder).catch(Function);
|
||||
fse.unlink(tempFile).catch(Function);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extensions that were removed from extensions but are still in "uninstalling" state
|
||||
*/
|
||||
function removedUninstalling(searchText = "") {
|
||||
const extensions = searchForExtensions(searchText);
|
||||
|
||||
return Array.from(ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.entries())
|
||||
.filter(([id, extension]) =>
|
||||
extension.state === "uninstalling"
|
||||
&& !extensions.find(extension => extension.id === id)
|
||||
)
|
||||
.map(([id, extension]) => ({ ...extension, id }));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extensions that were added to extensions but are still in "installing" state
|
||||
*/
|
||||
function addedInstalling(searchText = "") {
|
||||
const extensions = searchForExtensions(searchText);
|
||||
|
||||
return Array.from(ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.entries())
|
||||
.filter(([id, extension]) =>
|
||||
extension.state === "installing"
|
||||
&& extensions.find(extension => extension.id === id)
|
||||
)
|
||||
.map(([id, extension]) => ({ ...extension, id }));
|
||||
}
|
||||
|
||||
function getExtensionPackageTemp(fileName = "") {
|
||||
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
||||
}
|
||||
|
||||
function getExtensionDestFolder(name: string) {
|
||||
return path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
}
|
||||
|
||||
async function uninstallExtension(extension: InstalledExtension) {
|
||||
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
|
||||
|
||||
try {
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.set(extension.id, {
|
||||
state: "uninstalling",
|
||||
displayName
|
||||
});
|
||||
|
||||
await extensionDiscovery.uninstallExtension(extension.absolutePath);
|
||||
} catch (error) {
|
||||
Notifications.error(
|
||||
<p>Uninstalling extension <b>{displayName}</b> has failed: <em>{error?.message ?? ""}</em></p>
|
||||
);
|
||||
|
||||
// Remove uninstall state on uninstall failure
|
||||
if (ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.get(extension.id)?.state === "uninstalling") {
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.delete(extension.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function confirmUninstallExtension(extension: InstalledExtension) {
|
||||
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
|
||||
|
||||
ConfirmDialog.open({
|
||||
message: <p>Are you sure you want to uninstall extension <b>{displayName}</b>?</p>,
|
||||
labelOk: <Trans>Yes</Trans>,
|
||||
labelCancel: <Trans>No</Trans>,
|
||||
ok: () => uninstallExtension(extension)
|
||||
});
|
||||
}
|
||||
|
||||
function installOnDrop(files: File[]) {
|
||||
logger.info("Install from D&D");
|
||||
|
||||
return requestInstall(
|
||||
files.map(file => ({
|
||||
fileName: path.basename(file.path),
|
||||
filePath: file.path,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async function installFromSelectFileDialog() {
|
||||
const { dialog, BrowserWindow, app } = remote;
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
||||
defaultPath: app.getPath("downloads"),
|
||||
properties: ["openFile", "multiSelections"],
|
||||
message: _i18n._(t`Select extensions to install (formats: ${SupportedFormats.join(", ")}), `),
|
||||
buttonLabel: _i18n._(t`Use configuration`),
|
||||
filters: [
|
||||
{ name: "tarball", extensions: [...SupportedFormats] }
|
||||
]
|
||||
});
|
||||
|
||||
if (!canceled && filePaths.length) {
|
||||
requestInstall(
|
||||
filePaths.map(filePath => ({
|
||||
fileName: path.basename(filePath),
|
||||
filePath,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start extension install using a package name, which is resolved to a tarball url using the npm registry.
|
||||
* @param packageName e.g. "@publisher/extension-name"
|
||||
*/
|
||||
export async function installFromNpm(packageName: string) {
|
||||
const tarballUrl = await extensionLoader.getNpmPackageTarballUrl(packageName);
|
||||
|
||||
return installFromUrlOrPath(tarballUrl);
|
||||
}
|
||||
|
||||
async function installFromUrlOrPath(installPath: string) {
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().startingInstall = true;
|
||||
const fileName = path.basename(installPath);
|
||||
|
||||
try {
|
||||
// install via url
|
||||
// fixme: improve error messages for non-tar-file URLs
|
||||
if (InputValidators.isUrl.validate(installPath)) {
|
||||
const { promise: filePromise } = downloadFile({ url: installPath, timeout: 60000 /*1m*/ });
|
||||
const data = await filePromise;
|
||||
|
||||
await requestInstall({ fileName, data });
|
||||
}
|
||||
// otherwise installing from system path
|
||||
else if (InputValidators.isPath.validate(installPath)) {
|
||||
await requestInstall({ fileName, filePath: installPath });
|
||||
}
|
||||
} catch (error) {
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().startingInstall = false;
|
||||
Notifications.error(
|
||||
<p>Installation has failed: <b>{String(error)}</b></p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SupportedFormats = Object.freeze(["tar", "tgz"]);
|
||||
|
||||
@observer
|
||||
export class Extensions extends React.Component {
|
||||
private static supportedFormats = ["tar", "tgz"];
|
||||
|
||||
private static installPathValidator: InputValidator = {
|
||||
message: <Trans>Invalid URL or absolute path</Trans>,
|
||||
validate(value: string) {
|
||||
@ -52,10 +391,6 @@ export class Extensions extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
get extensionStateStore() {
|
||||
return ExtensionStateStore.getInstance<ExtensionStateStore>();
|
||||
}
|
||||
|
||||
@observable search = "";
|
||||
@observable installPath = "";
|
||||
|
||||
@ -63,40 +398,26 @@ export class Extensions extends React.Component {
|
||||
@observable startingInstall = false;
|
||||
|
||||
/**
|
||||
* Extensions that were removed from extensions but are still in "uninstalling" state
|
||||
* Start extension install using the current value of this.installPath
|
||||
*/
|
||||
@computed get removedUninstalling() {
|
||||
return Array.from(this.extensionStateStore.extensionState.entries())
|
||||
.filter(([id, extension]) =>
|
||||
extension.state === "uninstalling"
|
||||
&& !this.extensions.find(extension => extension.id === id)
|
||||
)
|
||||
.map(([id, extension]) => ({ ...extension, id }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extensions that were added to extensions but are still in "installing" state
|
||||
*/
|
||||
@computed get addedInstalling() {
|
||||
return Array.from(this.extensionStateStore.extensionState.entries())
|
||||
.filter(([id, extension]) =>
|
||||
extension.state === "installing"
|
||||
&& this.extensions.find(extension => extension.id === id)
|
||||
)
|
||||
.map(([id, extension]) => ({ ...extension, id }));
|
||||
@autobind()
|
||||
async installFromInstallPath() {
|
||||
if (this.installPath) {
|
||||
installFromUrlOrPath(this.installPath);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this,
|
||||
reaction(() => this.extensions, () => {
|
||||
this.removedUninstalling.forEach(({ id, displayName }) => {
|
||||
removedUninstalling(this.search.toLowerCase()).forEach(({ id, displayName }) => {
|
||||
Notifications.ok(
|
||||
<p>Extension <b>{displayName}</b> successfully uninstalled!</p>
|
||||
);
|
||||
this.extensionStateStore.extensionState.delete(id);
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.delete(id);
|
||||
});
|
||||
|
||||
this.addedInstalling.forEach(({ id, displayName }) => {
|
||||
addedInstalling(this.search.toLowerCase()).forEach(({ id, displayName }) => {
|
||||
const extension = this.extensions.find(extension => extension.id === id);
|
||||
|
||||
if (!extension) {
|
||||
@ -106,7 +427,7 @@ export class Extensions extends React.Component {
|
||||
Notifications.ok(
|
||||
<p>Extension <b>{displayName}</b> successfully installed!</p>
|
||||
);
|
||||
this.extensionStateStore.extensionState.delete(id);
|
||||
ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.delete(id);
|
||||
this.installPath = "";
|
||||
|
||||
// Enable installed extensions by default.
|
||||
@ -117,328 +438,7 @@ export class Extensions extends React.Component {
|
||||
}
|
||||
|
||||
@computed get extensions() {
|
||||
const searchText = this.search.toLowerCase();
|
||||
|
||||
return Array.from(extensionLoader.userExtensions.values())
|
||||
.filter(({ manifest: { name, description }}) => (
|
||||
name.toLowerCase().includes(searchText)
|
||||
|| description?.toLowerCase().includes(searchText)
|
||||
));
|
||||
}
|
||||
|
||||
get extensionsPath() {
|
||||
return extensionDiscovery.localFolderPath;
|
||||
}
|
||||
|
||||
getExtensionPackageTemp(fileName = "") {
|
||||
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
||||
}
|
||||
|
||||
getExtensionDestFolder(name: string) {
|
||||
return path.join(this.extensionsPath, sanitizeExtensionName(name));
|
||||
}
|
||||
|
||||
installFromSelectFileDialog = async () => {
|
||||
const { dialog, BrowserWindow, app } = remote;
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
||||
defaultPath: app.getPath("downloads"),
|
||||
properties: ["openFile", "multiSelections"],
|
||||
message: _i18n._(t`Select extensions to install (formats: ${Extensions.supportedFormats.join(", ")}), `),
|
||||
buttonLabel: _i18n._(t`Use configuration`),
|
||||
filters: [
|
||||
{ name: "tarball", extensions: Extensions.supportedFormats }
|
||||
]
|
||||
});
|
||||
|
||||
if (!canceled && filePaths.length) {
|
||||
this.requestInstall(
|
||||
filePaths.map(filePath => ({
|
||||
fileName: path.basename(filePath),
|
||||
filePath,
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start extension install using a package name, which is resolved to a tarball url using the npm registry.
|
||||
* @param packageName e.g. "@publisher/extension-name"
|
||||
*/
|
||||
async installFromNpm(packageName: string) {
|
||||
const tarballUrl = await extensionLoader.getNpmPackageTarballUrl(packageName);
|
||||
|
||||
return this.installFromUrlOrPath(tarballUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start extension install using the current value of this.installPath
|
||||
*/
|
||||
installFromInstallPath = async () => {
|
||||
if (this.installPath) {
|
||||
this.installFromUrlOrPath(this.installPath);
|
||||
}
|
||||
};
|
||||
|
||||
async installFromUrlOrPath(installPath: string) {
|
||||
this.startingInstall = true;
|
||||
const fileName = path.basename(installPath);
|
||||
|
||||
try {
|
||||
// install via url
|
||||
// fixme: improve error messages for non-tar-file URLs
|
||||
if (InputValidators.isUrl.validate(installPath)) {
|
||||
const { promise: filePromise } = downloadFile({ url: installPath, timeout: 60000 /*1m*/ });
|
||||
const data = await filePromise;
|
||||
|
||||
await this.requestInstall({ fileName, data });
|
||||
}
|
||||
// otherwise installing from system path
|
||||
else if (InputValidators.isPath.validate(installPath)) {
|
||||
await this.requestInstall({ fileName, filePath: installPath });
|
||||
}
|
||||
} catch (error) {
|
||||
this.startingInstall = false;
|
||||
Notifications.error(
|
||||
<p>Installation has failed: <b>{String(error)}</b></p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
installOnDrop = (files: File[]) => {
|
||||
logger.info("Install from D&D");
|
||||
|
||||
return this.requestInstall(
|
||||
files.map(file => ({
|
||||
fileName: path.basename(file.path),
|
||||
filePath: file.path,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
async preloadExtensions(requests: InstallRequest[], { showError = true } = {}) {
|
||||
const preloadedRequests = requests.filter(request => request.data);
|
||||
|
||||
await Promise.all(
|
||||
requests
|
||||
.filter(request => !request.data && request.filePath)
|
||||
.map(async request => {
|
||||
try {
|
||||
const data = await fse.readFile(request.filePath);
|
||||
|
||||
request.data = data;
|
||||
preloadedRequests.push(request);
|
||||
|
||||
return request;
|
||||
} catch(error) {
|
||||
if (showError) {
|
||||
Notifications.error(`Error while reading "${request.filePath}": ${String(error)}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return preloadedRequests as InstallRequestPreloaded[];
|
||||
}
|
||||
|
||||
async validatePackage(filePath: string): Promise<LensExtensionManifest> {
|
||||
const tarFiles = await listTarEntries(filePath);
|
||||
|
||||
// tarball from npm contains single root folder "package/*"
|
||||
const firstFile = tarFiles[0];
|
||||
|
||||
if (!firstFile) {
|
||||
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
|
||||
}
|
||||
|
||||
const rootFolder = path.normalize(firstFile).split(path.sep)[0];
|
||||
const packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder));
|
||||
const manifestLocation = packedInRootFolder ? path.join(rootFolder, manifestFilename) : manifestFilename;
|
||||
|
||||
if (!tarFiles.includes(manifestLocation)) {
|
||||
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
|
||||
}
|
||||
|
||||
const manifest = await readFileFromTar<LensExtensionManifest>({
|
||||
tarPath: filePath,
|
||||
filePath: manifestLocation,
|
||||
parseJson: true,
|
||||
});
|
||||
|
||||
if (!manifest.lens && !manifest.renderer) {
|
||||
throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
async createTempFilesAndValidate(requests: InstallRequestPreloaded[], { showErrors = true } = {}) {
|
||||
const validatedRequests: InstallRequestValidated[] = [];
|
||||
|
||||
// copy files to temp
|
||||
await fse.ensureDir(this.getExtensionPackageTemp());
|
||||
|
||||
for (const request of requests) {
|
||||
const tempFile = this.getExtensionPackageTemp(request.fileName);
|
||||
|
||||
await fse.writeFile(tempFile, request.data);
|
||||
}
|
||||
|
||||
// validate packages
|
||||
await Promise.all(
|
||||
requests.map(async req => {
|
||||
const tempFile = this.getExtensionPackageTemp(req.fileName);
|
||||
|
||||
try {
|
||||
const manifest = await this.validatePackage(tempFile);
|
||||
|
||||
validatedRequests.push({
|
||||
...req,
|
||||
manifest,
|
||||
tempFile,
|
||||
});
|
||||
} catch (error) {
|
||||
fse.unlink(tempFile).catch(() => null); // remove invalid temp package
|
||||
|
||||
if (showErrors) {
|
||||
Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<p>Installing <em>{req.fileName}</em> has failed, skipping.</p>
|
||||
<p>Reason: <em>{String(error)}</em></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return validatedRequests;
|
||||
}
|
||||
|
||||
async requestInstall(init: InstallRequest | InstallRequest[]) {
|
||||
const requests = Array.isArray(init) ? init : [init];
|
||||
const preloadedRequests = await this.preloadExtensions(requests);
|
||||
const validatedRequests = await this.createTempFilesAndValidate(preloadedRequests);
|
||||
|
||||
// If there are no requests for installing, reset startingInstall state
|
||||
if (validatedRequests.length === 0) {
|
||||
this.startingInstall = false;
|
||||
}
|
||||
|
||||
for (const install of validatedRequests) {
|
||||
const { name, version, description } = install.manifest;
|
||||
const extensionFolder = this.getExtensionDestFolder(name);
|
||||
const folderExists = await fse.pathExists(extensionFolder);
|
||||
|
||||
if (!folderExists) {
|
||||
// auto-install extension if not yet exists
|
||||
this.unpackExtension(install);
|
||||
} else {
|
||||
// If we show the confirmation dialog, we stop the install spinner until user clicks ok
|
||||
// and the install continues
|
||||
this.startingInstall = false;
|
||||
|
||||
// otherwise confirmation required (re-install / update)
|
||||
const removeNotification = Notifications.info(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
<div className="flex column gaps">
|
||||
<p>Install extension <b>{name}@{version}</b>?</p>
|
||||
<p>Description: <em>{description}</em></p>
|
||||
<div className="remove-folder-warning" onClick={() => shell.openPath(extensionFolder)}>
|
||||
<b>Warning:</b> <code>{extensionFolder}</code> will be removed before installation.
|
||||
</div>
|
||||
</div>
|
||||
<Button autoFocus label="Install" onClick={() => {
|
||||
removeNotification();
|
||||
this.unpackExtension(install);
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) {
|
||||
const displayName = extensionDisplayName(name, version);
|
||||
const extensionId = path.join(extensionDiscovery.nodeModulesPath, name, "package.json");
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
this.extensionStateStore.extensionState.set(extensionId, {
|
||||
state: "installing",
|
||||
displayName
|
||||
});
|
||||
this.startingInstall = false;
|
||||
|
||||
const extensionFolder = this.getExtensionDestFolder(name);
|
||||
const unpackingTempFolder = path.join(path.dirname(tempFile), `${path.basename(tempFile)}-unpacked`);
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
try {
|
||||
// extract to temp folder first
|
||||
await fse.remove(unpackingTempFolder).catch(Function);
|
||||
await fse.ensureDir(unpackingTempFolder);
|
||||
await extractTar(tempFile, { cwd: unpackingTempFolder });
|
||||
|
||||
// move contents to extensions folder
|
||||
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
||||
let unpackedRootFolder = unpackingTempFolder;
|
||||
|
||||
if (unpackedFiles.length === 1) {
|
||||
// check if %extension.tgz was packed with single top folder,
|
||||
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
||||
}
|
||||
|
||||
await fse.ensureDir(extensionFolder);
|
||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
} catch (error) {
|
||||
Notifications.error(
|
||||
<p>Installing extension <b>{displayName}</b> has failed: <em>{error}</em></p>
|
||||
);
|
||||
|
||||
// Remove install state on install failure
|
||||
if (this.extensionStateStore.extensionState.get(extensionId)?.state === "installing") {
|
||||
this.extensionStateStore.extensionState.delete(extensionId);
|
||||
}
|
||||
} finally {
|
||||
// clean up
|
||||
fse.remove(unpackingTempFolder).catch(Function);
|
||||
fse.unlink(tempFile).catch(Function);
|
||||
}
|
||||
}
|
||||
|
||||
confirmUninstallExtension = (extension: InstalledExtension) => {
|
||||
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
|
||||
|
||||
ConfirmDialog.open({
|
||||
message: <p>Are you sure you want to uninstall extension <b>{displayName}</b>?</p>,
|
||||
labelOk: <Trans>Yes</Trans>,
|
||||
labelCancel: <Trans>No</Trans>,
|
||||
ok: () => this.uninstallExtension(extension)
|
||||
});
|
||||
};
|
||||
|
||||
async uninstallExtension(extension: InstalledExtension) {
|
||||
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
|
||||
|
||||
try {
|
||||
this.extensionStateStore.extensionState.set(extension.id, {
|
||||
state: "uninstalling",
|
||||
displayName
|
||||
});
|
||||
|
||||
await extensionDiscovery.uninstallExtension(extension);
|
||||
} catch (error) {
|
||||
Notifications.error(
|
||||
<p>Uninstalling extension <b>{displayName}</b> has failed: <em>{error?.message ?? ""}</em></p>
|
||||
);
|
||||
|
||||
// Remove uninstall state on uninstall failure
|
||||
if (this.extensionStateStore.extensionState.get(extension.id)?.state === "uninstalling") {
|
||||
this.extensionStateStore.extensionState.delete(extension.id);
|
||||
}
|
||||
}
|
||||
return searchForExtensions(this.search.toLowerCase());
|
||||
}
|
||||
|
||||
renderExtensions() {
|
||||
@ -462,7 +462,7 @@ export class Extensions extends React.Component {
|
||||
return extensions.map(extension => {
|
||||
const { id, isEnabled, manifest } = extension;
|
||||
const { name, description } = manifest;
|
||||
const isUninstalling = this.extensionStateStore.extensionState.get(id)?.state === "uninstalling";
|
||||
const isUninstalling = ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.get(id)?.state === "uninstalling";
|
||||
|
||||
return (
|
||||
<div key={id} className="extension flex gaps align-center">
|
||||
@ -475,19 +475,24 @@ export class Extensions extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
{!isEnabled && (
|
||||
<Button plain active disabled={isUninstalling} onClick={() => {
|
||||
extension.isEnabled = true;
|
||||
}}>Enable</Button>
|
||||
)}
|
||||
{isEnabled && (
|
||||
<Button accent disabled={isUninstalling} onClick={() => {
|
||||
extension.isEnabled = false;
|
||||
}}>Disable</Button>
|
||||
)}
|
||||
<Button plain active disabled={isUninstalling} waiting={isUninstalling} onClick={() => {
|
||||
this.confirmUninstallExtension(extension);
|
||||
}}>Uninstall</Button>
|
||||
<Button
|
||||
plain={isEnabled}
|
||||
accent={!isEnabled}
|
||||
active
|
||||
disabled={isUninstalling}
|
||||
onClick={() => extension.isEnabled = !extension.isEnabled}
|
||||
>
|
||||
{isEnabled ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
<Button
|
||||
plain
|
||||
active
|
||||
disabled={isUninstalling}
|
||||
waiting={isUninstalling}
|
||||
onClick={() => confirmUninstallExtension(extension)}
|
||||
>
|
||||
Uninstall
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -498,7 +503,7 @@ export class Extensions extends React.Component {
|
||||
* True if at least one extension is in installing state
|
||||
*/
|
||||
@computed get isInstalling() {
|
||||
return [...this.extensionStateStore.extensionState.values()].some(extension => extension.state === "installing");
|
||||
return [...ExtensionStateStore.getInstance<ExtensionStateStore>().extensionState.values()].some(extension => extension.state === "installing");
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -506,7 +511,7 @@ export class Extensions extends React.Component {
|
||||
const { installPath } = this;
|
||||
|
||||
return (
|
||||
<DropFileInput onDropFiles={this.installOnDrop}>
|
||||
<DropFileInput onDropFiles={installOnDrop}>
|
||||
<PageLayout showOnTop className="Extensions flex column gaps" header={topHeader} contentGaps={false}>
|
||||
<h2>Lens Extensions</h2>
|
||||
<div>
|
||||
@ -521,7 +526,7 @@ export class Extensions extends React.Component {
|
||||
className="box grow"
|
||||
theme="round-black"
|
||||
disabled={this.isInstalling}
|
||||
placeholder={`Path or URL to an extension package (${Extensions.supportedFormats.join(", ")})`}
|
||||
placeholder={`Path or URL to an extension package (${SupportedFormats.join(", ")})`}
|
||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||
validators={installPath ? Extensions.installPathValidator : undefined}
|
||||
value={installPath}
|
||||
@ -532,7 +537,7 @@ export class Extensions extends React.Component {
|
||||
<Icon
|
||||
interactive
|
||||
material="folder"
|
||||
onClick={prevDefault(this.installFromSelectFileDialog)}
|
||||
onClick={prevDefault(installFromSelectFileDialog)}
|
||||
tooltip={<Trans>Browse</Trans>}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -11763,11 +11763,6 @@ prepend-http@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
prettier@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.0.tgz#8a03c7777883b29b37fb2c4348c66a78e980418b"
|
||||
integrity sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw==
|
||||
|
||||
pretty-error@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user