1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into show-extension-preferences-in-separate-page

This commit is contained in:
Alex Andreev 2022-05-11 10:44:57 +03:00
commit 5a86076040
14 changed files with 215 additions and 172 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ locales/**/**.js
lens.log lens.log
static/build static/build
static/types static/types
build/tray/
binaries/client/ binaries/client/
binaries/server/ binaries/server/
src/extensions/*/*.js src/extensions/*/*.js

View File

@ -32,7 +32,7 @@ compile-dev: node_modules
ci-validate-dev: binaries/client build-extensions compile-dev ci-validate-dev: binaries/client build-extensions compile-dev
.PHONY: dev .PHONY: dev
dev: binaries/client build-extensions dev: binaries/client build/tray/trayIconTemplate.png build-extensions
rm -rf static/build/ rm -rf static/build/
yarn dev yarn dev
@ -53,7 +53,7 @@ integration: build
yarn integration yarn integration
.PHONY: build .PHONY: build
build: node_modules binaries/client build: node_modules binaries/client build/tray/trayIconTemplate.png
yarn run npm:fix-build-version yarn run npm:fix-build-version
$(MAKE) build-extensions -B $(MAKE) build-extensions -B
yarn run compile yarn run compile
@ -70,6 +70,9 @@ $(extension_node_modules): node_modules
$(extension_dists): src/extensions/npm/extensions/dist $(extension_node_modules) $(extension_dists): src/extensions/npm/extensions/dist $(extension_node_modules)
cd $(@:/dist=) && ../../node_modules/.bin/npm run build cd $(@:/dist=) && ../../node_modules/.bin/npm run build
build/tray/trayIconTemplate.png: node_modules
yarn ts-node ./build/generate-tray-icons.ts
.PHONY: clean-old-extensions .PHONY: clean-old-extensions
clean-old-extensions: clean-old-extensions:
find ./extensions -mindepth 1 -maxdepth 1 -type d '!' -exec test -e '{}/package.json' \; -exec rm -rf {} \; find ./extensions -mindepth 1 -maxdepth 1 -type d '!' -exec test -e '{}/package.json' \; -exec rm -rf {} \;
@ -126,6 +129,7 @@ clean: clean-npm clean-extensions
rm -rf binaries/client rm -rf binaries/client
rm -rf dist rm -rf dist
rm -rf static/build rm -rf static/build
rm -rf build/tray
rm -rf node_modules rm -rf node_modules
rm -rf site rm -rf site
rm -rf docs/extensions/api rm -rf docs/extensions/api

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { readFileSync } from "fs";
import { ensureDirSync } from "fs-extra";
import { JSDOM } from "jsdom";
import path from "path";
import sharp from "sharp";
const size = Number(process.env.OUTPUT_SIZE || "16");
const outputFolder = process.env.OUTPUT_DIR || "./build/tray";
const inputFile = process.env.INPUT_SVG_PATH || "./src/renderer/components/icon/logo-lens.svg";
const svgData = readFileSync(inputFile, { encoding: "utf-8" });
const svgDom = new JSDOM(`<body>${svgData}</body>`);
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
svgRoot.innerHTML += `<style>* {fill: white !important;}</style>`;
const lightTemplate = svgRoot.outerHTML;
svgRoot.innerHTML += `<style>* {fill: black !important;}</style>`;
const darkTemplate = svgRoot.outerHTML;
console.log("Generating tray icon pngs");
ensureDirSync(outputFolder);
Promise.allSettled([
sharp(Buffer.from(darkTemplate))
.resize({ width: size, height: size })
.png()
.toFile(path.join(outputFolder, "trayIconDarkTemplate.png")),
sharp(Buffer.from(darkTemplate))
.resize({ width: size*2, height: size*2 })
.png()
.toFile(path.join(outputFolder, "trayIconDarkTemplate@2x.png")),
sharp(Buffer.from(lightTemplate))
.resize({ width: size, height: size })
.png()
.toFile(path.join(outputFolder, "trayIconTemplate.png")),
sharp(Buffer.from(lightTemplate))
.resize({ width: size*2, height: size*2 })
.png()
.toFile(path.join(outputFolder, "trayIconTemplate@2x.png")),
])
.then(console.log)
.catch(console.error);

View File

@ -38,13 +38,15 @@ It contains a mix of Node.js fields, including scripts and dependencies, and Len
Some of the most-important fields include: Some of the most-important fields include:
- `name` and `publisher`: Lens uses `@<publisher>/<name>` as a unique ID for the extension. - `name` and `publisher`: Lens uses `@<publisher>/<name>` as a unique ID for the extension.
For example, the Hello World sample has the ID `@lensapp-samples/helloworld-sample`. For example, the Hello World sample has the ID `@lensapp-samples/helloworld-sample`.
Lens uses this ID to uniquely identify your extension. Lens uses this ID to uniquely identify your extension.
- `main`: the extension's entry point run in `main` process. - `main`: the extension's entry point run in `main` process.
- `renderer`: the extension's entry point run in `renderer` process. - `renderer`: the extension's entry point run in `renderer` process.
- `engines.lens`: the minimum version of Lens API that the extension depends upon. - `engines.lens`: the minimum version of Lens API that the extension depends upon.
We only support the `^` range, which is also optional to specify, and only major and minor version numbers.
Meaning that `^5.4` and `5.4` both mean the same thing, and the patch version in `5.4.2` is ignored.
``` javascript ```javascript
{ {
"name": "helloworld-sample", "name": "helloworld-sample",
"publisher": "lens-samples", "publisher": "lens-samples",
@ -53,7 +55,8 @@ Lens uses this ID to uniquely identify your extension.
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/lensapp/lens-extension-samples", "homepage": "https://github.com/lensapp/lens-extension-samples",
"engines": { "engines": {
"lens": "^4.0.0" "node": "^14.18.12",
"lens": "5.4"
}, },
"main": "dist/main.js", "main": "dist/main.js",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
@ -65,17 +68,51 @@ Lens uses this ID to uniquely identify your extension.
"react-open-doodles": "^1.0.5" "react-open-doodles": "^1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@k8slens/extensions": "^4.0.0-alpha.2", "@k8slens/extensions": "^5.4.6",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
"typescript": "^4.0.3", "typescript": "^4.5.5",
"@types/react": "^16.9.35", "@types/react": "^17.0.44",
"@types/node": "^12.0.0", "@types/node": "^14.18.12",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"webpack-cli": "^3.3.11" "webpack-cli": "^3.3.11"
} }
} }
``` ```
## Webpack configuation
The following webpack `externals` are provided by `Lens` and must be used (when available) to make sure that the versions used are in sync.
| Package | webpack external syntax | Lens versions | Available in Main | Available in Renderer |
| ------------------ | --------------------------- | ------------- | ----------------- | --------------------- |
| `mobx` | `var global.Mobx` | `>5.0.0` | ✅ | ✅ |
| `mobx-react` | `var global.MobxReact` | `>5.0.0` | ❌ | ✅ |
| `react` | `var global.React` | `>5.0.0` | ❌ | ✅ |
| `react-router` | `var global.ReactRouter` | `>5.0.0` | ❌ | ✅ |
| `react-router-dom` | `var global.ReactRouterDom` | `>5.0.0` | ❌ | ✅ |
| `react-dom` | `var global.ReactDOM` | `>5.5.0` | ❌ | ✅ |
What is exported is the whole of the packages as a `*` import (within typescript).
For example, the following is how you would specify these within your webpack configuration files.
```json
{
...
"externals": [
...
{
"mobx": "var global.Mobx"
"mobx-react": "var global.MobxReact"
"react": "var global.React"
"react-router": "var global.ReactRouter"
"react-router-dom": "var global.ReactRouterDom"
"react-dom": "var global.ReactDOM"
}
]
}
```
## Extension Entry Files ## Extension Entry Files
Lens extensions can have two separate entry files. Lens extensions can have two separate entry files.
@ -95,20 +132,20 @@ The `Cluster Page` object registers the `/extension-example` path, and this path
It also registers the `MenuItem` component that displays the `ExampleIcon` React component and the "Hello World" text in the left-side menu of the cluster dashboard. It also registers the `MenuItem` component that displays the `ExampleIcon` React component and the "Hello World" text in the left-side menu of the cluster dashboard.
These React components are defined in the additional `./src/page.tsx` file. These React components are defined in the additional `./src/page.tsx` file.
``` typescript ```typescript
import { Renderer } from "@k8slens/extensions"; import { Renderer } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page" import { ExampleIcon, ExamplePage } from "./page";
import React from "react" import React from "react";
export default class ExampleExtension extends Renderer.LensExtension { export default class ExampleExtension extends Renderer.LensExtension {
clusterPages = [ clusterPages = [
{ {
id: "extension-example", id: "extension-example",
components: { components: {
Page: () => <ExamplePage extension={this}/>, Page: () => <ExamplePage extension={this} />,
} },
} },
] ];
} }
``` ```

View File

@ -3,7 +3,7 @@
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens", "homepage": "https://github.com/lensapp/lens",
"version": "5.5.0-alpha.0", "version": "5.5.0-beta.0",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors", "copyright": "© 2021 OpenLens Authors",
"license": "MIT", "license": "MIT",
@ -124,9 +124,6 @@
"rpm", "rpm",
"AppImage" "AppImage"
], ],
"asarUnpack": [
"**/node_modules/sharp/**"
],
"extraResources": [ "extraResources": [
{ {
"from": "binaries/client/linux/${arch}/kubectl", "from": "binaries/client/linux/${arch}/kubectl",
@ -248,12 +245,12 @@
"monaco-editor-webpack-plugin": "^5.0.0", "monaco-editor-webpack-plugin": "^5.0.0",
"node-fetch": "lensapp/node-fetch#2.x", "node-fetch": "lensapp/node-fetch#2.x",
"node-pty": "^0.10.1", "node-pty": "^0.10.1",
"npm": "^6.14.16", "npm": "^6.14.17",
"p-limit": "^3.1.0", "p-limit": "^3.1.0",
"path-to-regexp": "^6.2.0", "path-to-regexp": "^6.2.0",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"react": "^18.0.0", "react": "^17.0.2",
"react-dom": "^18.0.0", "react-dom": "^17.0.2",
"react-material-ui-carousel": "^2.3.11", "react-material-ui-carousel": "^2.3.11",
"react-router": "^5.2.0", "react-router": "^5.2.0",
"react-virtualized-auto-sizer": "^1.0.6", "react-virtualized-auto-sizer": "^1.0.6",
@ -263,7 +260,6 @@
"rfc6902": "^4.0.2", "rfc6902": "^4.0.2",
"selfsigned": "^2.0.1", "selfsigned": "^2.0.1",
"semver": "^7.3.7", "semver": "^7.3.7",
"sharp": "^0.30.3",
"shell-env": "^3.0.1", "shell-env": "^3.0.1",
"spdy": "^4.0.2", "spdy": "^4.0.2",
"tar": "^6.1.11", "tar": "^6.1.11",
@ -272,7 +268,7 @@
"url-parse": "^1.5.10", "url-parse": "^1.5.10",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"win-ca": "^3.5.0", "win-ca": "^3.5.0",
"winston": "^3.6.0", "winston": "^3.7.2",
"winston-console-format": "^1.0.8", "winston-console-format": "^1.0.8",
"winston-transport-browserconsole": "^1.0.5", "winston-transport-browserconsole": "^1.0.5",
"ws": "^7.5.7" "ws": "^7.5.7"
@ -283,7 +279,7 @@
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@sentry/types": "^6.19.3", "@sentry/types": "^6.19.7",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.4", "@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
@ -325,7 +321,7 @@
"@types/request": "^2.48.7", "@types/request": "^2.48.7",
"@types/request-promise-native": "^1.0.18", "@types/request-promise-native": "^1.0.18",
"@types/semver": "^7.3.9", "@types/semver": "^7.3.9",
"@types/sharp": "^0.30.0", "@types/sharp": "^0.30.2",
"@types/spdy": "^3.4.5", "@types/spdy": "^3.4.5",
"@types/tar": "^4.0.5", "@types/tar": "^4.0.5",
"@types/tar-stream": "^2.2.2", "@types/tar-stream": "^2.2.2",
@ -392,6 +388,7 @@
"react-window": "^1.8.6", "react-window": "^1.8.6",
"sass": "^1.51.0", "sass": "^1.51.0",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sharp": "^0.30.4",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"tailwindcss": "^3.0.23", "tailwindcss": "^3.0.23",
"tar-stream": "^2.2.0", "tar-stream": "^2.2.0",
@ -400,7 +397,7 @@
"ts-node": "^10.7.0", "ts-node": "^10.7.0",
"type-fest": "^2.12.2", "type-fest": "^2.12.2",
"typed-emitter": "^1.4.0", "typed-emitter": "^1.4.0",
"typedoc": "0.22.14", "typedoc": "0.22.15",
"typedoc-plugin-markdown": "^3.11.12", "typedoc-plugin-markdown": "^3.11.12",
"typeface-roboto": "^1.1.13", "typeface-roboto": "^1.1.13",
"typescript": "^4.5.5", "typescript": "^4.5.5",

View File

@ -10,7 +10,7 @@ import { ipcMain, ipcRenderer } from "electron";
import type { IEqualsComparer } from "mobx"; import type { IEqualsComparer } from "mobx";
import { makeObservable, reaction, runInAction } from "mobx"; import { makeObservable, reaction, runInAction } from "mobx";
import type { Disposer } from "./utils"; import type { Disposer } from "./utils";
import { getAppVersion, Singleton, toJS } from "./utils"; import { Singleton, toJS } from "./utils";
import logger from "../main/logger"; import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
@ -19,6 +19,7 @@ import { kebabCase } from "lodash";
import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "./get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "./get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "./get-configuration-file-model/app-version/app-version.injectable";
export interface BaseStoreParams<T> extends ConfOptions<T> { export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: { syncOptions?: {
@ -60,7 +61,7 @@ export abstract class BaseStore<T> extends Singleton {
this.storeConfig = getConfigurationFileModel({ this.storeConfig = getConfigurationFileModel({
...this.params, ...this.params,
projectName: "lens", projectName: "lens",
projectVersion: getAppVersion(), projectVersion: di.inject(appVersionInjectable),
cwd: this.cwd(), cwd: this.cwd(),
}); });

View File

@ -5,21 +5,10 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import Config from "conf"; import Config from "conf";
import type { BaseStoreParams } from "../base-store"; import type { BaseStoreParams } from "../base-store";
import appVersionInjectable from "./app-version/app-version.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
const getConfigurationFileModelInjectable = getInjectable({ const getConfigurationFileModelInjectable = getInjectable({
id: "get-configuration-file-model", id: "get-configuration-file-model",
instantiate: instantiate: () => <ConfigurationContent>(content: BaseStoreParams<ConfigurationContent>) => new Config(content),
(di) =>
<ConfigurationContent>(content: BaseStoreParams<ConfigurationContent>) =>
new Config({
...content,
projectName: "lens",
projectVersion: di.inject(appVersionInjectable),
cwd: di.inject(directoryForUserDataInjectable),
}),
causesSideEffects: true, causesSideEffects: true,
}); });

View File

@ -144,22 +144,32 @@ export class ClusterManager extends Singleton {
} else { } else {
entity.status.phase = (() => { entity.status.phase = (() => {
if (!cluster) { if (!cluster) {
logger.debug(`${logPrefix} setting entity ${entity.getName()} to DISCONNECTED, reason="no cluster"`);
return LensKubernetesClusterStatus.DISCONNECTED; return LensKubernetesClusterStatus.DISCONNECTED;
} }
if (cluster.accessible) { if (cluster.accessible) {
logger.debug(`${logPrefix} setting entity ${entity.getName()} to CONNECTED, reason="cluster is accessible"`);
return LensKubernetesClusterStatus.CONNECTED; return LensKubernetesClusterStatus.CONNECTED;
} }
if (!cluster.disconnected) { if (!cluster.disconnected) {
logger.debug(`${logPrefix} setting entity ${entity.getName()} to CONNECTING, reason="cluster is not disconnected"`);
return LensKubernetesClusterStatus.CONNECTING; return LensKubernetesClusterStatus.CONNECTING;
} }
// Extensions are not allowed to use the Lens specific status phases // Extensions are not allowed to use the Lens specific status phases
if (!lensSpecificClusterStatuses.has(entity?.status?.phase)) { if (!lensSpecificClusterStatuses.has(entity?.status?.phase)) {
logger.debug(`${logPrefix} not clearing entity ${entity.getName()} status, reason="custom string"`);
return entity.status.phase; return entity.status.phase;
} }
logger.debug(`${logPrefix} setting entity ${entity.getName()} to DISCONNECTED, reason="fallthrough"`);
return LensKubernetesClusterStatus.DISCONNECTED; return LensKubernetesClusterStatus.DISCONNECTED;
})(); })();

View File

@ -305,7 +305,7 @@ async function main(di: DiContainer) {
onQuitCleanup.push( onQuitCleanup.push(
initMenu(applicationMenuItems), initMenu(applicationMenuItems),
await initTray(windowManager, trayMenuItems, navigateToPreferences), initTray(windowManager, trayMenuItems, navigateToPreferences),
() => ShellSession.cleanup(), () => ShellSession.cleanup(),
); );

View File

@ -10,10 +10,7 @@ import { broadcastMessage } from "../../common/ipc";
import { openBrowser } from "../../common/utils"; import { openBrowser } from "../../common/utils";
import { showAbout } from "./menu"; import { showAbout } from "./menu";
import windowManagerInjectable from "../window-manager.injectable"; import windowManagerInjectable from "../window-manager.injectable";
import type { import type { MenuItemConstructorOptions } from "electron";
BrowserWindow,
MenuItem,
MenuItemConstructorOptions } from "electron";
import { import {
webContents, webContents,
} from "electron"; } from "electron";
@ -69,8 +66,8 @@ const applicationMenuItemsInjectable = getInjectable({
{ {
label: `About ${productName}`, label: `About ${productName}`,
id: "about", id: "about",
click(menuItem: MenuItem, browserWindow: BrowserWindow) { click() {
showAbout(browserWindow); showAbout();
}, },
}, },
...ignoreIf(autoUpdateDisabled, [ ...ignoreIf(autoUpdateDisabled, [
@ -286,8 +283,8 @@ const applicationMenuItemsInjectable = getInjectable({
{ {
label: `About ${productName}`, label: `About ${productName}`,
id: "about", id: "about",
click(menuItem: MenuItem, browserWindow: BrowserWindow) { click() {
showAbout(browserWindow); showAbout();
}, },
}, },
...ignoreIf(autoUpdateDisabled, [ ...ignoreIf(autoUpdateDisabled, [

View File

@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { BrowserWindow } from "electron";
import { app, dialog, Menu } from "electron"; import { app, dialog, Menu } from "electron";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { autorun } from "mobx"; import { autorun } from "mobx";
@ -20,7 +19,7 @@ export function initMenu(
}); });
} }
export function showAbout(browserWindow: BrowserWindow) { export function showAbout() {
const appInfo = [ const appInfo = [
`${appName}: ${app.getVersion()}`, `${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`, `Electron: ${process.versions.electron}`,
@ -29,7 +28,7 @@ export function showAbout(browserWindow: BrowserWindow) {
packageJson.copyright, packageJson.copyright,
]; ];
dialog.showMessageBoxSync(browserWindow, { dialog.showMessageBoxSync({
title: `${isWindows ? " ".repeat(2) : ""}${appName}`, title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info", type: "info",
buttons: ["Close"], buttons: ["Close"],

View File

@ -4,22 +4,19 @@
*/ */
import packageInfo from "../../../package.json"; import packageInfo from "../../../package.json";
import type { NativeImage } from "electron"; import { Menu, Tray } from "electron";
import { Menu, nativeImage, nativeTheme, Tray } from "electron";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { autorun } from "mobx"; import { autorun } from "mobx";
import { showAbout } from "../menu/menu"; import { showAbout } from "../menu/menu";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater"; import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import type { WindowManager } from "../window-manager"; import type { WindowManager } from "../window-manager";
import logger from "../logger"; import logger from "../logger";
import { isWindows, productName } from "../../common/vars"; import { isDevelopment, isWindows, productName, staticFilesDirectory } from "../../common/vars";
import { exitApp } from "../exit-app"; import { exitApp } from "../exit-app";
import type { Disposer } from "../../common/utils"; import type { Disposer } from "../../common/utils";
import { base64, disposer, getOrInsertWithAsync, toJS } from "../../common/utils"; import { disposer, toJS } from "../../common/utils";
import type { TrayMenuRegistration } from "./tray-menu-registration"; import type { TrayMenuRegistration } from "./tray-menu-registration";
import sharp from "sharp"; import path from "path";
import LogoLens from "../../renderer/components/icon/logo-lens.svg";
import { JSDOM } from "jsdom";
const TRAY_LOG_PREFIX = "[TRAY]"; const TRAY_LOG_PREFIX = "[TRAY]";
@ -27,69 +24,25 @@ const TRAY_LOG_PREFIX = "[TRAY]";
// note: instance of Tray should be saved somewhere, otherwise it disappears // note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray; export let tray: Tray;
interface CreateTrayIconArgs { function getTrayIconPath(): string {
shouldUseDarkColors: boolean; return path.resolve(
size: number; staticFilesDirectory,
sourceSvg: string; isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
"trayIconTemplate.png",
);
} }
const trayIcons = new Map<boolean, NativeImage>(); export function initTray(
async function createTrayIcon({ shouldUseDarkColors, size, sourceSvg }: CreateTrayIconArgs): Promise<NativeImage> {
return getOrInsertWithAsync(trayIcons, shouldUseDarkColors, async () => {
const trayIconColor = shouldUseDarkColors ? "white" : "black"; // Invert to show contrast
const parsedSvg = base64.decode(sourceSvg.split("base64,")[1]);
const svgDom = new JSDOM(`<body>${parsedSvg}</body>`);
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
const iconBuffer = await sharp(Buffer.from(svgRoot.outerHTML))
.resize({ width: size, height: size })
.png()
.toBuffer();
return nativeImage.createFromBuffer(iconBuffer);
});
}
function createCurrentTrayIcon() {
return createTrayIcon({
shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
size: 16,
sourceSvg: LogoLens,
});
}
function watchShouldUseDarkColors(tray: Tray): Disposer {
let prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors;
const onUpdated = () => {
if (prevShouldUseDarkColors !== nativeTheme.shouldUseDarkColors) {
prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors;
createCurrentTrayIcon()
.then(img => tray.setImage(img));
}
};
nativeTheme.on("updated", onUpdated);
return () => nativeTheme.off("updated", onUpdated);
}
export async function initTray(
windowManager: WindowManager, windowManager: WindowManager,
trayMenuItems: IComputedValue<TrayMenuRegistration[]>, trayMenuItems: IComputedValue<TrayMenuRegistration[]>,
navigateToPreferences: () => void, navigateToPreferences: () => void,
): Promise<Disposer> { ): Disposer {
const icon = await createCurrentTrayIcon(); const icon = getTrayIconPath();
const dispose = disposer();
tray = new Tray(icon); tray = new Tray(icon);
tray.setToolTip(packageInfo.description); tray.setToolTip(packageInfo.description);
tray.setIgnoreDoubleClickEvents(true); tray.setIgnoreDoubleClickEvents(true);
dispose.push(watchShouldUseDarkColors(tray));
if (isWindows) { if (isWindows) {
tray.on("click", () => { tray.on("click", () => {
windowManager windowManager
@ -98,7 +51,7 @@ export async function initTray(
}); });
} }
dispose.push( return disposer(
autorun(() => { autorun(() => {
try { try {
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences); const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences);
@ -113,8 +66,6 @@ export async function initTray(
tray = null; tray = null;
}, },
); );
return dispose;
} }
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions { function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions {

View File

@ -6,14 +6,13 @@
import "./components/app.scss"; import "./components/app.scss";
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM, { render, unmountComponentAtNode } from "react-dom";
import * as Mobx from "mobx"; import * as Mobx from "mobx";
import * as MobxReact from "mobx-react"; import * as MobxReact from "mobx-react";
import * as ReactRouter from "react-router"; import * as ReactRouter from "react-router";
import * as ReactRouterDom from "react-router-dom"; import * as ReactRouterDom from "react-router-dom";
import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsRendererApi from "../extensions/renderer-api"; import * as LensExtensionsRendererApi from "../extensions/renderer-api";
import { createRoot } from "react-dom/client";
import { delay } from "../common/utils"; import { delay } from "../common/utils";
import { isMac, isDevelopment } from "../common/vars"; import { isMac, isDevelopment } from "../common/vars";
import { HelmRepoManager } from "../main/helm/helm-repo-manager"; import { HelmRepoManager } from "../main/helm/helm-repo-manager";
@ -70,7 +69,6 @@ export async function bootstrap(di: DiContainer) {
bindEvents(); bindEvents();
const rootElem = document.getElementById("app"); const rootElem = document.getElementById("app");
const rootNode = createRoot(rootElem);
const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`; const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`;
// TODO: Remove temporal dependencies to make timing of initialization not important // TODO: Remove temporal dependencies to make timing of initialization not important
@ -154,7 +152,7 @@ export async function bootstrap(di: DiContainer) {
} }
try { try {
await initializeApp(() => rootNode.unmount()); await initializeApp(() => unmountComponentAtNode(rootElem));
} catch (error) { } catch (error) {
console.error(`[BOOTSTRAP]: view initialization error: ${error}`, { console.error(`[BOOTSTRAP]: view initialization error: ${error}`, {
origin: location.href, origin: location.href,
@ -162,12 +160,13 @@ export async function bootstrap(di: DiContainer) {
}); });
} }
rootNode.render( render(
<DiContextProvider value={{ di }}> <DiContextProvider value={{ di }}>
<Router history={di.inject(historyInjectable)}> <Router history={di.inject(historyInjectable)}>
{DefaultProps(App)} {DefaultProps(App)}
</Router> </Router>
</DiContextProvider>, </DiContextProvider>,
rootElem,
); );
} }

106
yarn.lock
View File

@ -1129,11 +1129,16 @@
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.2.tgz#0219c9da21ed975951108b8541913b1966464435" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.2.tgz#0219c9da21ed975951108b8541913b1966464435"
integrity sha512-XO5qmVBdTs+7PdCz7fAwn1afWxSnRE2KLBFg5/vOdKosPSSHsSHUURSkxiEZc2QsR+JpRB4AeQ26AkIRX38qTg== integrity sha512-XO5qmVBdTs+7PdCz7fAwn1afWxSnRE2KLBFg5/vOdKosPSSHsSHUURSkxiEZc2QsR+JpRB4AeQ26AkIRX38qTg==
"@sentry/types@6.19.3", "@sentry/types@^6.19.3": "@sentry/types@6.19.3":
version "6.19.3" version "6.19.3"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.3.tgz#94b19da68d4d23561efb1014f72968bcea85cd0c" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.3.tgz#94b19da68d4d23561efb1014f72968bcea85cd0c"
integrity sha512-jHhqxp8MIWSfOc3krorirTGKTEaSFO6XrAvi+2AZhr6gvOChwOgzgrN2ZqesJcZmgCsqWV21u3usSwYeRrjOJA== integrity sha512-jHhqxp8MIWSfOc3krorirTGKTEaSFO6XrAvi+2AZhr6gvOChwOgzgrN2ZqesJcZmgCsqWV21u3usSwYeRrjOJA==
"@sentry/types@^6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7"
integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==
"@sentry/utils@6.19.2": "@sentry/utils@6.19.2":
version "6.19.2" version "6.19.2"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.2.tgz#995efb896c5159369509f4896c27a2d2ea9191f2" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.2.tgz#995efb896c5159369509f4896c27a2d2ea9191f2"
@ -1970,10 +1975,10 @@
"@types/mime" "^1" "@types/mime" "^1"
"@types/node" "*" "@types/node" "*"
"@types/sharp@^0.30.0": "@types/sharp@^0.30.2":
version "0.30.0" version "0.30.2"
resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.0.tgz#58cb016c8fdc558b4c5771ad1f3668336685c843" resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.2.tgz#df5ff34140b3bad165482e6f3d26b08e42a0503a"
integrity sha512-bZ0Y/JVlrOyqwlBMJ2taEgnwFavjLnyZmLOLecmOesuG5kR2Lx9b2fM4osgfVjLJi8UlE+t3R1JzRVMxF6MbfA== integrity sha512-uLCBwjDg/BTcQit0dpNGvkIjvH3wsb8zpaJePCjvONBBSfaKHoxXBIuq1MT8DMQEfk2fKYnpC9QExCgFhkGkMQ==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
@ -3835,9 +3840,9 @@ color-string@^1.5.2, color-string@^1.6.0:
simple-swizzle "^0.2.2" simple-swizzle "^0.2.2"
color-string@^1.9.0: color-string@^1.9.0:
version "1.9.0" version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
dependencies: dependencies:
color-name "^1.0.0" color-name "^1.0.0"
simple-swizzle "^0.2.2" simple-swizzle "^0.2.2"
@ -3858,10 +3863,10 @@ color@^3.2.1:
color-convert "^1.9.3" color-convert "^1.9.3"
color-string "^1.6.0" color-string "^1.6.0"
color@^4.2.1: color@^4.2.3:
version "4.2.1" version "4.2.3"
resolved "https://registry.yarnpkg.com/color/-/color-4.2.1.tgz#498aee5fce7fc982606c8875cab080ac0547c884" resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
integrity sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw== integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
dependencies: dependencies:
color-convert "^2.0.1" color-convert "^2.0.1"
color-string "^1.9.0" color-string "^1.9.0"
@ -9372,9 +9377,9 @@ no-case@^3.0.4:
tslib "^2.0.3" tslib "^2.0.3"
node-abi@^3.3.0: node-abi@^3.3.0:
version "3.5.0" version "3.15.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.5.0.tgz#26e8b7b251c3260a5ac5ba5aef3b4345a0229248" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.15.0.tgz#cd9ac8c58328129b49998cc6fa16aa5506152716"
integrity sha512-LtHvNIBgOy5mO8mPEUtkCW/YCRWYEKshIvqhe1GHHyXEHEB5mgICyYnAcl4qan3uFeRROErKGzatFHPf6kDxWw== integrity sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==
dependencies: dependencies:
semver "^7.3.5" semver "^7.3.5"
@ -9692,10 +9697,10 @@ npm-user-validate@^1.0.1:
resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.1.tgz#31428fc5475fe8416023f178c0ab47935ad8c561" resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.1.tgz#31428fc5475fe8416023f178c0ab47935ad8c561"
integrity sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw== integrity sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw==
npm@^6.14.16: npm@^6.14.17:
version "6.14.16" version "6.14.17"
resolved "https://registry.yarnpkg.com/npm/-/npm-6.14.16.tgz#a882d6b0b32d5212461f0c58719152add1a7b99a" resolved "https://registry.yarnpkg.com/npm/-/npm-6.14.17.tgz#932cd2df5f28db0f13cc487873109d5212acaf83"
integrity sha512-LMiLGYsVNJfVPlQg7v2NYjG7iRIapcLv+oMunlq7fkXVx0BATCjRu7XyWl0G+iuZzHy4CjtM32QB8ox8juTgaw== integrity sha512-CxEDn1ydVRPDl4tHrlnq+WevYAhv4GF2AEHzJKQ4prZDZ96IS3Uo6t0Sy6O9kB6XzqkI+J00WfYCqqk0p6IJ1Q==
dependencies: dependencies:
JSONStream "^1.3.5" JSONStream "^1.3.5"
abbrev "~1.1.1" abbrev "~1.1.1"
@ -10644,9 +10649,9 @@ postcss@^8.3.0, postcss@^8.4.12, postcss@^8.4.6, postcss@^8.4.7:
source-map-js "^1.0.2" source-map-js "^1.0.2"
prebuild-install@^7.0.1: prebuild-install@^7.0.1:
version "7.0.1" version "7.1.0"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370"
integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg== integrity sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==
dependencies: dependencies:
detect-libc "^2.0.0" detect-libc "^2.0.0"
expand-template "^2.0.3" expand-template "^2.0.3"
@ -10958,13 +10963,14 @@ react-beautiful-dnd@^13.1.0:
redux "^4.0.4" redux "^4.0.4"
use-memo-one "^1.1.1" use-memo-one "^1.1.1"
react-dom@^18.0.0: react-dom@^17.0.2:
version "18.0.0" version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw== integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.21.0" object-assign "^4.1.1"
scheduler "^0.20.2"
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
@ -11088,12 +11094,13 @@ react-window@^1.8.6:
"@babel/runtime" "^7.0.0" "@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6" memoize-one ">=3.1.1 <6"
react@^18.0.0: react@^17.0.2:
version "18.0.0" version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A== integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1"
read-cmd-shim@^1.0.1, read-cmd-shim@^1.0.5: read-cmd-shim@^1.0.1, read-cmd-shim@^1.0.5:
version "1.0.5" version "1.0.5"
@ -11652,12 +11659,13 @@ saxes@^5.0.1:
dependencies: dependencies:
xmlchars "^2.2.0" xmlchars "^2.2.0"
scheduler@^0.21.0: scheduler@^0.20.2:
version "0.21.0" version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ== integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@2.7.0: schema-utils@2.7.0:
version "2.7.0" version "2.7.0"
@ -11835,16 +11843,16 @@ shallow-clone@^3.0.0:
dependencies: dependencies:
kind-of "^6.0.2" kind-of "^6.0.2"
sharp@^0.30.3: sharp@^0.30.4:
version "0.30.3" version "0.30.4"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.3.tgz#315a1817423a4d1cde5119a21c99c234a7a6fb37" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.4.tgz#73d9daa63bbc20da189c9328d75d5d395fc8fb73"
integrity sha512-rjpfJFK58ZOFSG8sxYSo3/JQb4ej095HjXp9X7gVu7gEn1aqSG8TCW29h/Rr31+PXrFADo1H/vKfw0uhMQWFtg== integrity sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==
dependencies: dependencies:
color "^4.2.1" color "^4.2.3"
detect-libc "^2.0.1" detect-libc "^2.0.1"
node-addon-api "^4.3.0" node-addon-api "^4.3.0"
prebuild-install "^7.0.1" prebuild-install "^7.0.1"
semver "^7.3.5" semver "^7.3.7"
simple-get "^4.0.1" simple-get "^4.0.1"
tar-fs "^2.1.1" tar-fs "^2.1.1"
tunnel-agent "^0.6.0" tunnel-agent "^0.6.0"
@ -13112,10 +13120,10 @@ typedoc-plugin-markdown@^3.11.12:
dependencies: dependencies:
handlebars "^4.7.7" handlebars "^4.7.7"
typedoc@0.22.14: typedoc@0.22.15:
version "0.22.14" version "0.22.15"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.14.tgz#c690677c31bc1dd5618caffc001bfa8554c4c02f" resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.15.tgz#c6ad7ed9d017dc2c3a06c9189cb392bd8e2d8c3f"
integrity sha512-tlf9wIcsrnQSjetStrnRutuy2RjZkG5PK2umwveZLTkuC2K9VywOZTdu2G19BdOPzGrhZjf9WK7pthXqnFQejg== integrity sha512-CMd1lrqQbFvbx6S9G6fL4HKp3GoIuhujJReWqlIvSb2T26vGai+8Os3Mde7Pn832pXYemd9BMuuYWhFpL5st0Q==
dependencies: dependencies:
glob "^7.2.0" glob "^7.2.0"
lunr "^2.3.9" lunr "^2.3.9"
@ -13783,10 +13791,10 @@ winston-transport@^4.3.0, winston-transport@^4.5.0:
readable-stream "^3.6.0" readable-stream "^3.6.0"
triple-beam "^1.3.0" triple-beam "^1.3.0"
winston@^3.2.1, winston@^3.6.0: winston@^3.2.1, winston@^3.7.2:
version "3.6.0" version "3.7.2"
resolved "https://registry.yarnpkg.com/winston/-/winston-3.6.0.tgz#be32587a099a292b88c49fac6fa529d478d93fb6" resolved "https://registry.yarnpkg.com/winston/-/winston-3.7.2.tgz#95b4eeddbec902b3db1424932ac634f887c400b1"
integrity sha512-9j8T75p+bcN6D00sF/zjFVmPp+t8KMPB1MzbbzYjeN9VWxdsYnTB40TkbNUEXAmILEfChMvAMgidlX64OG3p6w== integrity sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng==
dependencies: dependencies:
"@dabh/diagnostics" "^2.0.2" "@dabh/diagnostics" "^2.0.2"
async "^3.2.3" async "^3.2.3"