diff --git a/build/download_kubectl.ts b/build/download_kubectl.ts index f046b23e30..e75ffe1402 100644 --- a/build/download_kubectl.ts +++ b/build/download_kubectl.ts @@ -28,11 +28,7 @@ class KubectlDownloader { resolveWithFullResponse: true }).catch((error) => { console.log(error); }); - if (response.headers["etag"]) { - return response.headers["etag"].replace(/"/g, ""); - } - - return ""; + return response.headers?.["etag"]?.replace(/"/g, "") ?? ""; } public async checkBinary() { @@ -87,7 +83,7 @@ class KubectlDownloader { throw(error); }); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { file.on("close", () => { console.log("kubectl binary download closed"); fs.chmod(this.path, 0o755, (err) => { @@ -116,4 +112,3 @@ downloads.forEach((dlOpts) => { console.log(`Downloading: ${JSON.stringify(dlOpts)}`); downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete"))); }); - diff --git a/package.json b/package.json index 06066d6ade..0c3ed55ab1 100644 --- a/package.json +++ b/package.json @@ -362,12 +362,12 @@ "terser-webpack-plugin": "^3.0.3", "ts-jest": "^26.1.0", "ts-loader": "^7.0.5", - "ts-node": "^8.10.2", + "ts-node": "^9.1.1", "type-fest": "^0.18.0", "typedoc": "0.17.0-3", "typedoc-plugin-markdown": "^2.4.0", "typeface-roboto": "^0.0.75", - "typescript": "4.0.2", + "typescript": "^4.2.3", "url-loader": "^4.1.0", "webpack": "^4.44.2", "webpack-cli": "^3.3.11", diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index d1d2f76603..2143b03f5c 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -88,7 +88,7 @@ describe("empty config", () => { expect(storedCluster.id).toBe("foo"); expect(storedCluster.preferences.terminalCWD).toBe("/tmp"); expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5"); - expect(storedCluster.enabled).toBe(true); + expect(clusterStore.isClusterEnabled(storedCluster)).toBe(true); }); it("adds cluster to default workspace", () => { @@ -265,8 +265,8 @@ describe("config with existing clusters", () => { it("marks owned cluster disabled by default", () => { const storedClusters = clusterStore.clustersList; - expect(storedClusters[0].enabled).toBe(true); - expect(storedClusters[2].enabled).toBe(false); + expect(clusterStore.isClusterEnabled(storedClusters[0])).toBe(true); + expect(clusterStore.isClusterEnabled(storedClusters[2])).toBe(false); }); }); @@ -336,9 +336,9 @@ users: const storedClusters = clusterStore.clustersList; expect(storedClusters.length).toBe(2); - expect(storedClusters[0].enabled).toBeFalsy; + expect(clusterStore.isClusterEnabled(storedClusters[0])).toBeFalsy; expect(storedClusters[1].id).toBe("cluster2"); - expect(storedClusters[1].enabled).toBeTruthy; + expect(clusterStore.isClusterEnabled(storedClusters[1])).toBeTruthy; }); }); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 7ae402eefe..b62ea7feac 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -11,11 +11,12 @@ import { appEventBus } from "./event-bus"; import { dumpConfigYaml } from "./kube-helpers"; import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; -import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; +import { broadcastMessage, handleRequest, InvalidKubeconfigChannel, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; import _ from "lodash"; import move from "array-move"; import type { WorkspaceId } from "./workspace-store"; import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting"; +import { LensExtensionId } from "../extensions/lens-extension"; export interface ClusterIconUpload { clusterId: string; @@ -34,8 +35,9 @@ export type ClusterPrometheusMetadata = { }; export interface ClusterStoreModel { - activeCluster?: ClusterId; // last opened cluster + activeClusterId?: ClusterId; // last opened cluster clusters?: ClusterModel[]; + clusterOwners?: [ClusterId, LensExtensionId][]; } export type ClusterId = string; @@ -59,11 +61,6 @@ export interface ClusterModel { /** Metadata */ metadata?: ClusterMetadata; - /** - * If extension sets ownerRef it has to explicitly mark a cluster as enabled during onActive (or when cluster is saved) - */ - ownerRef?: string; - /** List of accessible namespaces */ accessibleNamespaces?: string[]; @@ -71,6 +68,16 @@ export interface ClusterModel { kubeConfig?: string; // yaml } +export interface ClusterManagementRecord { + ownerId: LensExtensionId; + enabled: boolean; +} + +export interface GetByWorkspaceIdOptions { + includeDisabled?: boolean; // default false + sortByIconOrder?: boolean; // default true +} + export interface ClusterPreferences extends ClusterPrometheusPreferences { terminalCWD?: string; clusterName?: string; @@ -92,6 +99,22 @@ export interface ClusterPrometheusPreferences { }; } +function splitAddClusterArgs(args: ClusterModel[] | [...ClusterModel[], string]): [ClusterModel[]] | [ClusterModel[], string] { + const lastArg = args.pop(); + + if (lastArg) { + return [[]]; + } + + if (typeof lastArg === "string") { + return [args as ClusterModel[], lastArg]; + } + + args.push(lastArg); + + return [args as ClusterModel[]]; +} + export class ClusterStore extends BaseStore { static getCustomKubeConfigPath(clusterId: ClusterId): string { return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId); @@ -106,9 +129,11 @@ export class ClusterStore extends BaseStore { return filePath; } - @observable activeCluster: ClusterId; + @observable activeClusterId: ClusterId; @observable removedClusters = observable.map(); @observable clusters = observable.map(); + @observable clusterManagingInfo = observable.map(); + @observable erroredClusterModels = observable.array(); private static stateRequestChannel = "cluster:states"; @@ -189,28 +214,50 @@ export class ClusterStore extends BaseStore { }); } - get activeClusterId() { - return this.activeCluster; - } - @computed get clustersList(): Cluster[] { return Array.from(this.clusters.values()); } @computed get enabledClustersList(): Cluster[] { - return this.clustersList.filter((c) => c.enabled); + return this.clustersList.filter(c => this.isClusterEnabled(c)); } @computed get active(): Cluster | null { - return this.getById(this.activeCluster); + return this.getById(this.activeClusterId); } @computed get connectedClustersList(): Cluster[] { return this.clustersList.filter((c) => !c.disconnected); } + /** + * + * @param clusterOrId The cluster or its ID to check if it is owned + * @returns true if an extension has claimed to be managing a cluster + */ + isClusterManaged(clusterOrId: Cluster | ClusterId): boolean { + const clusterId = typeof clusterOrId === "string" + ? clusterOrId + : clusterOrId.id; + + return this.clusterManagingInfo.has(clusterId); + } + + /** + * Get the enabled status of a cluster + * @param clusterOrId The cluster or its ID to check if it is owned + * @returns true if not managed, else true if owner has marked as enabled + */ + isClusterEnabled(clusterOrId: Cluster | ClusterId): boolean { + const clusterId = typeof clusterOrId === "string" + ? clusterOrId + : clusterOrId.id; + + return this.clusterManagingInfo.get(clusterId)?.enabled ?? true; + } + isActive(id: ClusterId) { - return this.activeCluster === id; + return this.activeClusterId === id; } isMetricHidden(resource: ResourceType) { @@ -221,7 +268,7 @@ export class ClusterStore extends BaseStore { setActive(id: ClusterId) { const clusterId = this.clusters.has(id) ? id : null; - this.activeCluster = clusterId; + this.activeClusterId = clusterId; workspaceStore.setLastActiveClusterId(clusterId); } @@ -249,37 +296,50 @@ export class ClusterStore extends BaseStore { return this.clusters.get(id); } - getByWorkspaceId(workspaceId: string): Cluster[] { + getByWorkspaceId(workspaceId: string, options?: GetByWorkspaceIdOptions): Cluster[] { + const includeDisabled = options?.includeDisabled ?? false; + const sortByIconOrder = options?.sortByIconOrder ?? true; + const clusters = Array.from(this.clusters.values()) - .filter(cluster => cluster.workspace === workspaceId); + .filter(cluster => ( + cluster.workspace === workspaceId + && ( + includeDisabled + || this.isClusterEnabled(cluster) + ) + )); - return _.sortBy(clusters, cluster => cluster.preferences.iconOrder); - } - - @action - addClusters(...models: ClusterModel[]): Cluster[] { - const clusters: Cluster[] = []; - - models.forEach(model => { - clusters.push(this.addCluster(model)); - }); + if (sortByIconOrder) { + return _.sortBy(clusters, cluster => cluster.preferences.iconOrder); + } return clusters; } @action - addCluster(model: ClusterModel | Cluster): Cluster { + addClusters(...args: ([...MM] | [...MM, LensExtensionId])): Cluster[] { + const [models, ownerId] = splitAddClusterArgs(args); + + return models.map(model => this.addCluster(model, ownerId)); + } + + @action + addCluster(clusterOrModel: ClusterModel | Cluster, ownerId?: LensExtensionId): Cluster { appEventBus.emit({ name: "cluster", action: "add" }); - let cluster = model as Cluster; - if (!(model instanceof Cluster)) { - cluster = new Cluster(model); + const cluster = clusterOrModel instanceof Cluster + ? clusterOrModel + : new Cluster(clusterOrModel); + + if (ownerId) { + if (this.isClusterManaged(cluster)) { + throw new Error("Extension tried to claim an already managed cluster"); + } + + this.clusterManagingInfo.set(cluster.id, { ownerId, enabled: false }); } - if (!cluster.isManaged) { - cluster.enabled = true; - } - this.clusters.set(model.id, cluster); + this.clusters.set(clusterOrModel.id, cluster); return cluster; } @@ -296,7 +356,7 @@ export class ClusterStore extends BaseStore { if (cluster) { this.clusters.delete(clusterId); - if (this.activeCluster === clusterId) { + if (this.activeClusterId === clusterId) { this.setActive(null); } @@ -309,31 +369,43 @@ export class ClusterStore extends BaseStore { @action removeByWorkspaceId(workspaceId: string) { - this.getByWorkspaceId(workspaceId).forEach(cluster => { - this.removeById(cluster.id); + const workspaceClusters = this.getByWorkspaceId(workspaceId, { + includeDisabled: true, + sortByIconOrder: false, }); + + for (const cluster of workspaceClusters) { + this.removeById(cluster.id); + } } @action - protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { + protected fromStore({ activeClusterId, clusters = [], clusterOwners = [] }: ClusterStoreModel = {}) { const currentClusters = this.clusters.toJS(); const newClusters = new Map(); const removedClusters = new Map(); + const erroredClusterModels: ClusterModel[] = []; + const clusterOwnersMap = new Map(clusterOwners); // update new clusters for (const clusterModel of clusters) { - let cluster = currentClusters.get(clusterModel.id); + if (currentClusters.has(clusterModel.id)) { + const cluster = currentClusters.get(clusterModel.id); - if (cluster) { cluster.updateModel(clusterModel); + newClusters.set(clusterModel.id, cluster); } else { - cluster = new Cluster(clusterModel); + try { + newClusters.set(clusterModel.id, new Cluster(clusterModel)); + } catch (error) { + const { preferences, contextName: context, kubeConfigPath: kubeconfig } = clusterModel; + const clusterName = preferences?.clusterName || context; - if (!cluster.isManaged && cluster.apiUrl) { - cluster.enabled = true; + logger.error(`[CLUSTER-STORE]: Failed to load kubeconfig for the cluster '${clusterName}'.`, { error, context, kubeconfig }); + broadcastMessage(InvalidKubeconfigChannel, clusterModel.id); + erroredClusterModels.push(clusterModel); } } - newClusters.set(clusterModel.id, cluster); } // update removed clusters @@ -343,15 +415,17 @@ export class ClusterStore extends BaseStore { } }); - this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null; + this.activeClusterId = clusterOwnersMap.get(activeClusterId) ? null : activeClusterId; this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); + this.erroredClusterModels.replace(erroredClusterModels); + this.clusterManagingInfo.replace(clusterOwnersMap); } toJSON(): ClusterStoreModel { return toJS({ - activeCluster: this.activeCluster, - clusters: this.clustersList.map(cluster => cluster.toJSON()), + activeClusterId: this.activeClusterId, + clusters: this.clustersList.map(cluster => cluster.toJSON()).concat(this.erroredClusterModels), }, { recurseEverything: true }); diff --git a/src/common/utils/split-args.ts b/src/common/utils/split-args.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts index 36a2f0bfb8..e185977d99 100644 --- a/src/extensions/cluster-feature.ts +++ b/src/extensions/cluster-feature.ts @@ -44,7 +44,7 @@ export abstract class ClusterFeature { * * @param cluster the cluster that the feature is to be installed on */ - abstract async install(cluster: Cluster): Promise; + abstract install(cluster: Cluster): Promise; /** * to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation @@ -52,7 +52,7 @@ export abstract class ClusterFeature { * * @param cluster the cluster that the feature is to be upgraded on */ - abstract async upgrade(cluster: Cluster): Promise; + abstract upgrade(cluster: Cluster): Promise; /** * to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation @@ -60,7 +60,7 @@ export abstract class ClusterFeature { * * @param cluster the cluster that the feature is to be uninstalled from */ - abstract async uninstall(cluster: Cluster): Promise; + abstract uninstall(cluster: Cluster): Promise; /** * to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation @@ -72,7 +72,7 @@ export abstract class ClusterFeature { * * @return a promise, resolved with the updated ClusterFeatureStatus */ - abstract async updateStatus(cluster: Cluster): Promise; + abstract updateStatus(cluster: Cluster): Promise; /** * this is a helper method that conveniently applies kubernetes resources to the cluster. diff --git a/src/extensions/stores/cluster-store.ts b/src/extensions/stores/cluster-store.ts index b2aba41c06..e9deb09398 100644 --- a/src/extensions/stores/cluster-store.ts +++ b/src/extensions/stores/cluster-store.ts @@ -18,14 +18,14 @@ export class ClusterStore extends Singleton { * Active cluster id */ get activeClusterId(): string { - return internalClusterStore.activeCluster; + return internalClusterStore.activeClusterId; } /** * Set active cluster id */ set activeClusterId(id : ClusterId) { - internalClusterStore.activeCluster = id; + internalClusterStore.activeClusterId = id; } /** diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 8920568c73..172ec3c406 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -4,7 +4,7 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; -import { broadcastMessage, InvalidKubeconfigChannel } from "../common/ipc"; +import { broadcastMessage } from "../common/ipc"; import { ContextHandler } from "./context-handler"; import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { Kubectl } from "./kubectl"; @@ -38,7 +38,6 @@ export type ClusterRefreshOptions = { export interface ClusterState { initialized: boolean; - enabled: boolean; apiUrl: string; online: boolean; disconnected: boolean; @@ -71,12 +70,6 @@ export class Cluster implements ClusterModel, ClusterState { * @internal */ public contextHandler: ContextHandler; - /** - * Owner reference - * - * If extension sets this it needs to also mark cluster as enabled on activate (or when added to a store) - */ - public ownerRef: string; protected kubeconfigManager: KubeconfigManager; protected eventDisposers: Function[] = []; protected activated = false; @@ -86,7 +79,7 @@ export class Cluster implements ClusterModel, ClusterState { whenReady = when(() => this.ready); /** - * Is cluster object initializinng on-going + * Is cluster object initializing on-going * * @observable */ @@ -129,12 +122,6 @@ export class Cluster implements ClusterModel, ClusterState { * @internal */ @observable kubeProxyUrl: string; // lens-proxy to kube-api url - /** - * Is cluster instance enabled (disabled clusters are currently hidden) - * - * @observable - */ - @observable enabled = false; // only enabled clusters are visible to users /** * Is cluster online * @@ -258,23 +245,20 @@ export class Cluster implements ClusterModel, ClusterState { constructor(model: ClusterModel) { this.updateModel(model); - try { - const kubeconfig = this.getKubeconfig(); + const kubeconfig = this.getKubeconfig(); - validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false}); - this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server; - } catch(err) { - logger.error(err); - logger.error(`[CLUSTER] Failed to load kubeconfig for the cluster '${this.name || this.contextName}' (context: ${this.contextName}, kubeconfig: ${this.kubeConfigPath}).`); - broadcastMessage(InvalidKubeconfigChannel, model.id); - } + validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false}); + this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server; } + kubeConfig?: string; /** * Is cluster managed by an extension + * + * @deprecated use `clusterStore.isClusterManaged(clusterId)` */ get isManaged(): boolean { - return !!this.ownerRef; + return false; } /** @@ -612,7 +596,6 @@ export class Cluster implements ClusterModel, ClusterState { workspace: this.workspace, preferences: this.preferences, metadata: this.metadata, - ownerRef: this.ownerRef, accessibleNamespaces: this.accessibleNamespaces, }; @@ -627,7 +610,6 @@ export class Cluster implements ClusterModel, ClusterState { getState(): ClusterState { const state: ClusterState = { initialized: this.initialized, - enabled: this.enabled, apiUrl: this.apiUrl, online: this.online, ready: this.ready, diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 7e0d6ed5c7..cf075d5bc8 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -273,7 +273,7 @@ export class Kubectl { logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const stream = customRequest({ url: this.url, gzip: true, diff --git a/src/main/lens-binary.ts b/src/main/lens-binary.ts index 3cf5a5fce7..a992d8c460 100644 --- a/src/main/lens-binary.ts +++ b/src/main/lens-binary.ts @@ -183,7 +183,7 @@ export class LensBinary { throw(error); }); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { file.on("close", () => { this.logger.debug(`${this.originalBinaryName} binary download closed`); if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => { diff --git a/src/migrations/cluster-store/4.2.0.ts b/src/migrations/cluster-store/4.2.0.ts new file mode 100644 index 0000000000..9ed50afe56 --- /dev/null +++ b/src/migrations/cluster-store/4.2.0.ts @@ -0,0 +1,12 @@ +import { migration } from "../migration-wrapper"; + +export default migration({ + version: "4.2.0", + run(store) { + const activeCluster = store.get("activeCluster"); + + if (activeCluster) { + store.set("activeClusterId", activeCluster); + } + } +}); diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts index 4a71d4f7ad..e5e2a91f23 100644 --- a/src/migrations/cluster-store/index.ts +++ b/src/migrations/cluster-store/index.ts @@ -7,6 +7,7 @@ import version260Beta3 from "./2.6.0-beta.3"; import version270Beta0 from "./2.7.0-beta.0"; import version270Beta1 from "./2.7.0-beta.1"; import version360Beta1 from "./3.6.0-beta.1"; +import version420 from "./4.2.0"; import snap from "./snap"; export default { @@ -17,5 +18,6 @@ export default { ...version270Beta0, ...version270Beta1, ...version360Beta1, + ...version420, ...snap }; diff --git a/src/renderer/components/+landing-page/workspace-cluster.store.ts b/src/renderer/components/+landing-page/workspace-cluster.store.ts index 24834927b3..94a65655b5 100644 --- a/src/renderer/components/+landing-page/workspace-cluster.store.ts +++ b/src/renderer/components/+landing-page/workspace-cluster.store.ts @@ -56,7 +56,6 @@ export class WorkspaceClusterStore extends ItemStore { () => ( clusterStore .getByWorkspaceId(this.workspaceId) - .filter(cluster => cluster.enabled) .map(cluster => new ClusterItem(cluster)) ) ); diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index dd36e529e1..68c9ac197e 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -108,8 +108,8 @@ export class ClustersMenu extends React.Component { render() { const { className } = this.props; const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId); - const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled); - const activeClusterId = clusterStore.activeCluster; + const clusters = clusterStore.getByWorkspaceId(workspace.id); + const activeClusterId = clusterStore.activeClusterId; return (
@@ -192,7 +192,7 @@ export class ClustersMenu extends React.Component { @observer export class ChooseCluster extends React.Component { @computed get options() { - const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId).filter(cluster => cluster.enabled); + const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId); const options = clusters.map((cluster) => { return { value: cluster.id, label: cluster.name }; }); diff --git a/src/renderer/utils/readableStream.ts b/src/renderer/utils/readableStream.ts index 3b51106427..5481e099cd 100644 --- a/src/renderer/utils/readableStream.ts +++ b/src/renderer/utils/readableStream.ts @@ -17,7 +17,7 @@ export class ReadableWebToNodeStream extends Readable { * Default web API stream reader * https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader */ - private reader: ReadableStreamReader; + private reader: ReadableStreamReader; private pendingRead: Promise; /** diff --git a/yarn.lock b/yarn.lock index 2a39325781..ab5290fb00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4052,6 +4052,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-fetch@^3.0.4: version "3.0.6" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" @@ -13460,12 +13465,13 @@ ts-loader@^7.0.5: micromatch "^4.0.0" semver "^6.0.0" -ts-node@^8.10.2: - version "8.10.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" - integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== +ts-node@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" + integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== dependencies: arg "^4.1.0" + create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" source-map-support "^0.5.17" @@ -13618,16 +13624,16 @@ typeface-roboto@^0.0.75: resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b" integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg== -typescript@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" - integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== - typescript@^4.0.3: version "4.1.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9" integrity sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ== +typescript@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" + integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== + uglify-js@^3.1.4: version "3.9.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.9.4.tgz#867402377e043c1fc7b102253a22b64e5862401b"