diff --git a/.eslintrc.js b/.eslintrc.js index 733f644615..daf6f02f2f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,6 +12,7 @@ module.exports = { "**/static/**/*", "**/site/**/*", "extensions/*/*.tgz", + "build/webpack/**/*", ], settings: { react: { diff --git a/.gitignore b/.gitignore index d018f3b251..0a79ea77c1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ types/extension-renderer-api.d.ts extensions/*/dist docs/extensions/api site/ +build/webpack/ diff --git a/Makefile b/Makefile index 81dd3a732e..857f14b2cb 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ tag-release: scripts/tag-release.sh $(CMD_ARGS) .PHONY: test -test: binaries/client +test: node_modules binaries/client yarn run jest $(or $(CMD_ARGS), "src") .PHONY: integration diff --git a/build/download_binaries.ts b/build/download_binaries.ts index 4b8d38904c..61c38eae06 100644 --- a/build/download_binaries.ts +++ b/build/download_binaries.ts @@ -8,15 +8,19 @@ import { open } from "fs/promises"; import type { WriteStream } from "fs-extra"; import { constants, ensureDir, unlink } from "fs-extra"; import path from "path"; -import fetch from "node-fetch"; +import type * as FetchModule from "node-fetch"; import { promisify } from "util"; import { pipeline as _pipeline, Transform, Writable } from "stream"; import type { SingleBar } from "cli-progress"; import { MultiBar } from "cli-progress"; import { extract } from "tar-stream"; import gunzip from "gunzip-maybe"; -import { getBinaryName, normalizedPlatform } from "../src/common/vars"; -import { isErrnoException } from "../src/common/utils"; +import { getBinaryName } from "../src/common/vars"; +import { isErrnoException, setTimeoutFor } from "../src/common/utils"; + +type Response = FetchModule.Response; +type RequestInfo = FetchModule.RequestInfo; +type RequestInit = FetchModule.RequestInit; const pipeline = promisify(_pipeline); @@ -29,6 +33,10 @@ interface BinaryDownloaderArgs { readonly baseDir: string; } +interface BinaryDownloaderDependencies { + fetch: (url: RequestInfo, init?: RequestInit) => Promise; +} + abstract class BinaryDownloader { protected abstract readonly url: string; protected readonly bar: SingleBar; @@ -38,7 +46,7 @@ abstract class BinaryDownloader { return [file]; } - constructor(public readonly args: BinaryDownloaderArgs, multiBar: MultiBar) { + constructor(protected readonly dependencies: BinaryDownloaderDependencies, public readonly args: BinaryDownloaderArgs, multiBar: MultiBar) { this.bar = multiBar.create(1, 0, args); this.target = path.join(args.baseDir, args.platform, args.fileArch, args.binaryName); } @@ -49,8 +57,10 @@ abstract class BinaryDownloader { } const controller = new AbortController(); - const stream = await fetch(this.url, { - timeout: 15 * 60 * 1000, // 15min + + setTimeoutFor(controller, 15 * 60 * 1000); + + const stream = await this.dependencies.fetch(this.url, { signal: controller.signal, }); const total = Number(stream.headers.get("content-length")); @@ -72,6 +82,10 @@ abstract class BinaryDownloader { */ const handle = fileHandle = await open(this.target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL); + if (!stream.body) { + throw new Error("no body on stream"); + } + await pipeline( stream.body, new Transform({ @@ -108,10 +122,10 @@ abstract class BinaryDownloader { class LensK8sProxyDownloader extends BinaryDownloader { protected readonly url: string; - constructor(args: Omit, bar: MultiBar) { + constructor(deps: BinaryDownloaderDependencies, args: Omit, bar: MultiBar) { const binaryName = getBinaryName("lens-k8s-proxy", { forPlatform: args.platform }); - super({ ...args, binaryName }, bar); + super(deps, { ...args, binaryName }, bar); this.url = `https://github.com/lensapp/lens-k8s-proxy/releases/download/v${args.version}/lens-k8s-proxy-${args.platform}-${args.downloadArch}`; } } @@ -119,10 +133,10 @@ class LensK8sProxyDownloader extends BinaryDownloader { class KubectlDownloader extends BinaryDownloader { protected readonly url: string; - constructor(args: Omit, bar: MultiBar) { + constructor(deps: BinaryDownloaderDependencies, args: Omit, bar: MultiBar) { const binaryName = getBinaryName("kubectl", { forPlatform: args.platform }); - super({ ...args, binaryName }, bar); + super(deps, { ...args, binaryName }, bar); this.url = `https://storage.googleapis.com/kubernetes-release/release/v${args.version}/bin/${args.platform}/${args.downloadArch}/${binaryName}`; } } @@ -130,10 +144,10 @@ class KubectlDownloader extends BinaryDownloader { class HelmDownloader extends BinaryDownloader { protected readonly url: string; - constructor(args: Omit, bar: MultiBar) { + constructor(deps: BinaryDownloaderDependencies, args: Omit, bar: MultiBar) { const binaryName = getBinaryName("helm", { forPlatform: args.platform }); - super({ ...args, binaryName }, bar); + super(deps, { ...args, binaryName }, bar); this.url = `https://get.helm.sh/helm-v${args.version}-${args.platform}-${args.downloadArch}.tar.gz`; } @@ -160,7 +174,24 @@ class HelmDownloader extends BinaryDownloader { type SupportedPlatform = "darwin" | "linux" | "windows"; +const importFetchModule = new Function('return import("node-fetch")') as () => Promise; + async function main() { + const deps: BinaryDownloaderDependencies = { + fetch: (await importFetchModule()).default, + }; + const normalizedPlatform = (() => { + switch (process.platform) { + case "darwin": + return "darwin"; + case "linux": + return "linux"; + case "win32": + return "windows"; + default: + throw new Error(`platform=${process.platform} is unsupported`); + } + })(); const multiBar = new MultiBar({ align: "left", clearOnComplete: false, @@ -171,21 +202,21 @@ async function main() { }); const baseDir = path.join(__dirname, "..", "binaries", "client"); const downloaders: BinaryDownloader[] = [ - new LensK8sProxyDownloader({ + new LensK8sProxyDownloader(deps, { version: packageInfo.config.k8sProxyVersion, platform: normalizedPlatform, downloadArch: "amd64", fileArch: "x64", baseDir, }, multiBar), - new KubectlDownloader({ + new KubectlDownloader(deps, { version: packageInfo.config.bundledKubectlVersion, platform: normalizedPlatform, downloadArch: "amd64", fileArch: "x64", baseDir, }, multiBar), - new HelmDownloader({ + new HelmDownloader(deps, { version: packageInfo.config.bundledHelmVersion, platform: normalizedPlatform, downloadArch: "amd64", @@ -196,21 +227,21 @@ async function main() { if (normalizedPlatform !== "windows") { downloaders.push( - new LensK8sProxyDownloader({ + new LensK8sProxyDownloader(deps, { version: packageInfo.config.k8sProxyVersion, platform: normalizedPlatform, downloadArch: "arm64", fileArch: "arm64", baseDir, }, multiBar), - new KubectlDownloader({ + new KubectlDownloader(deps, { version: packageInfo.config.bundledKubectlVersion, platform: normalizedPlatform, downloadArch: "arm64", fileArch: "arm64", baseDir, }, multiBar), - new HelmDownloader({ + new HelmDownloader(deps, { version: packageInfo.config.bundledHelmVersion, platform: normalizedPlatform, downloadArch: "arm64", diff --git a/package.json b/package.json index 531cbbdd97..2e5de0545c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "compile:main": "yarn run webpack --config webpack/main.ts", "compile:renderer": "yarn run webpack --config webpack/renderer.ts", "compile:extension-types": "yarn run webpack --config webpack/extensions.ts", + "compile:node-fetch": "yarn run webpack --config ./webpack/node-fetch.ts", + "postinstall": "yarn run compile:node-fetch", "npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts", "build:linux": "yarn run compile && electron-builder --linux --dir", "build:mac": "yarn run compile && electron-builder --mac --dir", @@ -262,7 +264,7 @@ "moment-timezone": "^0.5.38", "monaco-editor": "^0.29.1", "monaco-editor-webpack-plugin": "^5.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^3.2.10", "node-pty": "0.10.1", "npm": "^8.19.3", "p-limit": "^3.1.0", @@ -332,9 +334,7 @@ "@types/memorystream": "^0.3.0", "@types/mini-css-extract-plugin": "^2.4.0", "@types/mock-fs": "^4.13.1", - "@types/node": "^16.18.3", - "@types/node-fetch": "^2.6.2", - "@types/npm": "^2.0.32", + "@types/node": "^16.18.2", "@types/proper-lockfile": "^4.1.2", "@types/randomcolor": "^0.5.6", "@types/react": "^17.0.45", diff --git a/src/common/fetch/download-json.injectable.ts b/src/common/fetch/download-json.injectable.ts index 503cd373ec..78a7d030d7 100644 --- a/src/common/fetch/download-json.injectable.ts +++ b/src/common/fetch/download-json.injectable.ts @@ -4,7 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { RequestInit, Response } from "node-fetch"; -import type { JsonValue } from "type-fest"; import type { AsyncResult } from "../utils/async-result"; import fetchInjectable from "./fetch.injectable"; @@ -12,7 +11,7 @@ export interface DownloadJsonOptions { signal?: AbortSignal | null | undefined; } -export type DownloadJson = (url: string, opts?: DownloadJsonOptions) => Promise>; +export type DownloadJson = (url: string, opts?: DownloadJsonOptions) => Promise>; const downloadJsonInjectable = getInjectable({ id: "download-json", diff --git a/src/common/fetch/fetch.global-override-for-injectable.ts b/src/common/fetch/fetch.global-override-for-injectable.ts index cd6160641c..1a5f80735c 100644 --- a/src/common/fetch/fetch.global-override-for-injectable.ts +++ b/src/common/fetch/fetch.global-override-for-injectable.ts @@ -3,9 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getGlobalOverride } from "../test-utils/get-global-override"; +import { getGlobalOverrideForFunction } from "../test-utils/get-global-override-for-function"; import fetchInjectable from "./fetch.injectable"; -export default getGlobalOverride(fetchInjectable, () => () => { - throw new Error("tried to fetch a resource without override in test"); -}); +export default getGlobalOverrideForFunction(fetchInjectable); diff --git a/src/common/fetch/fetch.injectable.ts b/src/common/fetch/fetch.injectable.ts index c4c30bc2d8..e320c0128a 100644 --- a/src/common/fetch/fetch.injectable.ts +++ b/src/common/fetch/fetch.injectable.ts @@ -3,8 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import fetch from "node-fetch"; -import type { RequestInit, Response } from "node-fetch"; +import type * as FetchModule from "node-fetch"; + +const { NodeFetch: { default: fetch }} = require("../../../build/webpack/node-fetch.bundle") as { NodeFetch: typeof FetchModule }; + +type Response = FetchModule.Response; +type RequestInit = FetchModule.RequestInit; export type Fetch = (url: string, init?: RequestInit) => Promise; diff --git a/src/common/k8s-api/__tests__/kube-api.test.ts b/src/common/k8s-api/__tests__/kube-api.test.ts index d76518be00..2e681d4ba9 100644 --- a/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/src/common/k8s-api/__tests__/kube-api.test.ts @@ -15,20 +15,76 @@ import type { Fetch } from "../../fetch/fetch.injectable"; import fetchInjectable from "../../fetch/fetch.injectable"; import type { CreateKubeApiForRemoteCluster } from "../create-kube-api-for-remote-cluster.injectable"; import createKubeApiForRemoteClusterInjectable from "../create-kube-api-for-remote-cluster.injectable"; -import { Response } from "node-fetch"; import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; import { flushPromises } from "../../test-utils/flush-promises"; import createKubeJsonApiInjectable from "../create-kube-json-api.injectable"; import type { IKubeWatchEvent } from "../kube-watch-event"; import type { KubeJsonApiDataFor } from "../kube-object"; +import type { Response, Headers as NodeFetchHeaders } from "node-fetch"; import AbortController from "abort-controller"; +const createMockResponseFromString = (url: string, data: string, statusCode = 200) => { + const res: jest.Mocked = { + buffer: jest.fn(async () => { throw new Error("buffer() is not supported"); }), + clone: jest.fn(() => res), + arrayBuffer: jest.fn(async () => { throw new Error("arrayBuffer() is not supported"); }), + blob: jest.fn(async () => { throw new Error("blob() is not supported"); }), + body: new PassThrough(), + bodyUsed: false, + headers: new Headers() as NodeFetchHeaders, + json: jest.fn(async () => JSON.parse(await res.text())), + ok: 200 <= statusCode && statusCode < 300, + redirected: 300 <= statusCode && statusCode < 400, + size: data.length, + status: statusCode, + statusText: "some-text", + text: jest.fn(async () => data), + type: "basic", + url, + formData: jest.fn(async () => { throw new Error("formData() is not supported"); }), + }; + + return res; +}; + +const createMockResponseFromStream = (url: string, stream: NodeJS.ReadableStream, statusCode = 200) => { + const res: jest.Mocked = { + buffer: jest.fn(async () => { throw new Error("buffer() is not supported"); }), + clone: jest.fn(() => res), + arrayBuffer: jest.fn(async () => { throw new Error("arrayBuffer() is not supported"); }), + blob: jest.fn(async () => { throw new Error("blob() is not supported"); }), + body: stream, + bodyUsed: false, + headers: new Headers() as NodeFetchHeaders, + json: jest.fn(async () => JSON.parse(await res.text())), + ok: 200 <= statusCode && statusCode < 300, + redirected: 300 <= statusCode && statusCode < 400, + size: 10, + status: statusCode, + statusText: "some-text", + text: jest.fn(() => { + const chunks: Buffer[] = []; + + return new Promise((resolve, reject) => { + stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))); + stream.on("error", (err) => reject(err)); + stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); + }); + }), + type: "basic", + url, + formData: jest.fn(async () => { throw new Error("formData() is not supported"); }), + }; + + return res; +}; + describe("createKubeApiForRemoteCluster", () => { let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster; let fetchMock: AsyncFnMock; - beforeEach(() => { + beforeEach(async () => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); fetchMock = asyncFn(); @@ -94,7 +150,7 @@ describe("createKubeApiForRemoteCluster", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["https://127.0.0.1:6443/api/v1/pods"], - new Response(JSON.stringify({ + createMockResponseFromString("https://127.0.0.1:6443/api/v1/pods", JSON.stringify({ kind: "PodList", apiVersion: "v1", metadata:{ @@ -118,7 +174,7 @@ describe("KubeApi", () => { let registerApiSpy: jest.SpiedFunction; let fetchMock: AsyncFnMock; - beforeEach(() => { + beforeEach(async () => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); fetchMock = asyncFn(); @@ -172,7 +228,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1", JSON.stringify({ resources: [{ name: "ingresses", }], @@ -196,7 +252,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/networking.k8s.io", JSON.stringify({ preferredVersion: { version: "v1", }, @@ -234,7 +290,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo"], - new Response(JSON.stringify({})), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo", JSON.stringify({})), ); result = await getCall; }); @@ -274,7 +330,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1"], - new Response(JSON.stringify({})), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1", JSON.stringify({})), ); result = await getCall; }); @@ -292,7 +348,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo", JSON.stringify({ apiVersion: "v1", kind: "Ingress", metadata: { @@ -341,7 +397,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1"], - new Response(JSON.stringify({})), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1", JSON.stringify({})), ); result = await getCall; }); @@ -359,7 +415,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1", JSON.stringify({ resources: [], })), ); @@ -381,7 +437,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1", JSON.stringify({ resources: [{ name: "ingresses", }], @@ -405,7 +461,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/extensions"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/extensions", JSON.stringify({ preferredVersion: { version: "v1beta1", }, @@ -443,7 +499,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo"], - new Response(JSON.stringify({})), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo", JSON.stringify({})), ); result = await getCall; }); @@ -483,7 +539,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1"], - new Response(JSON.stringify({})), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1", JSON.stringify({})), ); result = await getCall; }); @@ -501,7 +557,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo", JSON.stringify({ apiVersion: "v1beta1", kind: "Ingress", metadata: { @@ -550,7 +606,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1"], - new Response(JSON.stringify({})), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1", JSON.stringify({})), ); result = await getCall; }); @@ -604,7 +660,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test", JSON.stringify({ apiVersion: "v1", kind: "Deployment", metadata: { @@ -657,7 +713,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test", JSON.stringify({ apiVersion: "v1", kind: "Deployment", metadata: { @@ -710,7 +766,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test", JSON.stringify({ apiVersion: "v1", kind: "Deployment", metadata: { @@ -768,7 +824,7 @@ describe("KubeApi", () => { beforeEach(async () => { fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background"], - new Response("{}"), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background", "{}"), ); }); @@ -804,7 +860,7 @@ describe("KubeApi", () => { beforeEach(async () => { fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background"], - new Response("{}"), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background", "{}"), ); }); @@ -840,7 +896,7 @@ describe("KubeApi", () => { beforeEach(async () => { fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo?propagationPolicy=Background"], - new Response("{}"), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo?propagationPolicy=Background", "{}"), ); }); @@ -886,7 +942,7 @@ describe("KubeApi", () => { beforeEach(async () => { fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/foo?propagationPolicy=Background"], - new Response("{}"), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/foo?propagationPolicy=Background", "{}"), ); }); @@ -922,7 +978,7 @@ describe("KubeApi", () => { beforeEach(async () => { fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/foo?propagationPolicy=Background"], - new Response("{}"), + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/foo?propagationPolicy=Background", "{}"), ); }); @@ -995,7 +1051,7 @@ describe("KubeApi", () => { return isMatch; }, - new Response(stream), + createMockResponseFromStream("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", stream), ); }); @@ -1091,7 +1147,7 @@ describe("KubeApi", () => { return isMatch; }, - new Response(stream), + createMockResponseFromStream("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", stream), ); }); @@ -1186,7 +1242,7 @@ describe("KubeApi", () => { return isMatch; }, - new Response(stream), + createMockResponseFromStream("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=60", stream), ); }); @@ -1350,7 +1406,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods", JSON.stringify({ kind: "Pod", apiVersion: "v1", metadata: { @@ -1462,7 +1518,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foobar"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foobar", JSON.stringify({ kind: "Pod", apiVersion: "v1", metadata: { @@ -1530,7 +1586,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/pods"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/pods", JSON.stringify({ kind: "PodList", apiVersion: "v1", metadata: {}, @@ -1572,7 +1628,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/pods"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/pods", JSON.stringify({ kind: "PodList", apiVersion: "v1", metadata: {}, @@ -1614,7 +1670,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods"], - new Response(JSON.stringify({ + createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods", JSON.stringify({ kind: "PodList", apiVersion: "v1", metadata: {}, diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index 73d6ab1a82..ae06341d1b 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -14,7 +14,7 @@ import byline from "byline"; import type { IKubeWatchEvent } from "./kube-watch-event"; import type { KubeJsonApiData, KubeJsonApi } from "./kube-json-api"; import type { Disposer } from "../utils"; -import { isDefined, noop, WrappedAbortController } from "../utils"; +import { setTimeoutFor, isDefined, noop, WrappedAbortController } from "../utils"; import type { RequestInit, Response } from "node-fetch"; import type { Patch } from "rfc6902"; import assert from "assert"; @@ -643,11 +643,12 @@ export class KubeApi< clearTimeout(timedRetry); }); + setTimeoutFor(abortController, 600 * 1000); + const requestParams = timeout ? { query: { timeoutSeconds: timeout }} : {}; const watchUrl = this.getWatchUrl(namespace); const responsePromise = this.request.getResponse(watchUrl, requestParams, { signal: abortController.signal, - timeout: 600_000, }); logger.info(`[KUBE-API] watch (${watchId}) ${retry === true ? "retried" : "started"} ${watchUrl}`); @@ -686,7 +687,19 @@ export class KubeApi< }, timeout * 1000 * 1.1); } - ["end", "close", "error"].forEach((eventName) => { + if (!response.body) { + logger.error(`[KUBE-API]: watch (${watchId}) did not return a body`); + requestRetried = true; + + clearTimeout(timedRetry); + timedRetry = setTimeout(() => { // we did not get any kubernetes errors so let's retry + this.watch({ ...opts, namespace, callback, watchId, retry: true }); + }, 1000); + + return; + } + + for (const eventName of ["end", "close", "error"]) { response.body.on(eventName, () => { // We only retry if we haven't retried, haven't aborted and haven't received k8s error // kubernetes errors (=errorReceived set) should be handled in a callback @@ -703,7 +716,7 @@ export class KubeApi< this.watch({ ...opts, namespace, callback, watchId, retry: true }); }, 1000); }); - }); + } byline(response.body).on("data", (line) => { try { diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts index 32a762a74c..12456aa20c 100644 --- a/src/common/k8s-api/kube-object.store.ts +++ b/src/common/k8s-api/kube-object.store.ts @@ -473,7 +473,6 @@ export abstract class KubeObjectStore< callback, }); - // TODO: upgrade node-fetch once we are starting to use ES modules const signal = abortController.signal; const callback: KubeApiWatchCallback = (data, error) => { diff --git a/src/common/utils/abort-controller.ts b/src/common/utils/abort-controller.ts index 8172e6a81a..b062fce487 100644 --- a/src/common/utils/abort-controller.ts +++ b/src/common/utils/abort-controller.ts @@ -18,3 +18,9 @@ export class WrappedAbortController extends AbortController { }); } } + +export function setTimeoutFor(controller: AbortController, timeout: number): void { + const handle = setTimeout(() => controller.abort(), timeout); + + controller.signal.addEventListener("abort", () => clearTimeout(handle)); +} diff --git a/src/common/vars/lens-resources-dir.injectable.ts b/src/common/vars/lens-resources-dir.injectable.ts index c454afb005..9aebba19b3 100644 --- a/src/common/vars/lens-resources-dir.injectable.ts +++ b/src/common/vars/lens-resources-dir.injectable.ts @@ -11,9 +11,9 @@ const lensResourcesDirInjectable = getInjectable({ instantiate: (di) => { const isProduction = di.inject(isProductionInjectable); - return !isProduction - ? process.cwd() - : process.resourcesPath; + return isProduction + ? process.resourcesPath + : process.cwd(); }, causesSideEffects: true, diff --git a/src/main/__test__/static-file-route.test.ts b/src/main/__test__/static-file-route.test.ts index 217d1effd2..4880009017 100644 --- a/src/main/__test__/static-file-route.test.ts +++ b/src/main/__test__/static-file-route.test.ts @@ -4,7 +4,7 @@ */ import type { LensApiRequest, Route } from "../router/route"; -import staticFileRouteInjectable from "../routes/static-file-route.injectable"; +import staticFileRouteInjectable from "../routes/files/static-file-route.injectable"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; jest.mock("electron", () => ({ diff --git a/src/main/routes/files/development.injectable.ts b/src/main/routes/files/development.injectable.ts new file mode 100644 index 0000000000..dcc6fce649 --- /dev/null +++ b/src/main/routes/files/development.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import httpProxy from "http-proxy"; +import { webpackDevServerPort } from "../../../../webpack/vars"; +import { publicPath } from "../../../common/vars"; +import appNameInjectable from "../../../common/vars/app-name.injectable"; +import type { LensApiRequest, RouteResponse } from "../../router/route"; + +const devStaticFileRouteHandlerInjectable = getInjectable({ + id: "dev-static-file-route-handler", + instantiate: (di) => { + const proxy = httpProxy.createProxy(); + const appName = di.inject(appNameInjectable); + const proxyTarget = `http://127.0.0.1:${webpackDevServerPort}`; + + return async ({ raw: { req, res }}: LensApiRequest<"/{path*}">): Promise> => { + if (req.url === "/" || !req.url) { + req.url = `${publicPath}/${appName}.html`; + } else if (!req.url.startsWith("/build/")) { + return { statusCode: 404 }; + } + + proxy.web(req, res, { target: proxyTarget }); + + return { proxy }; + }; + }, +}); + +export default devStaticFileRouteHandlerInjectable; diff --git a/src/main/routes/files/production.injectable.ts b/src/main/routes/files/production.injectable.ts new file mode 100644 index 0000000000..b6f9ca5e40 --- /dev/null +++ b/src/main/routes/files/production.injectable.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import readFileBufferInjectable from "../../../common/fs/read-file-buffer.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; +import staticFilesDirectoryInjectable from "../../../common/vars/static-files-directory.injectable"; +import appNameInjectable from "../../../common/vars/app-name.injectable"; +import type { LensApiRequest } from "../../router/route"; +import path from "path"; +import type { SupportedFileExtension } from "../../router/router-content-types"; +import { contentTypes } from "../../router/router-content-types"; +import loggerInjectable from "../../../common/logger.injectable"; +import { publicPath } from "../../../common/vars"; + +const prodStaticFileRouteHandlerInjectable = getInjectable({ + id: "prod-static-file-route-handler", + instantiate: (di) => { + const readFileBuffer = di.inject(readFileBufferInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); + const appName = di.inject(appNameInjectable); + const logger = di.inject(loggerInjectable); + + return async ({ params }: LensApiRequest<"/{path*}">) => { + let filePath = params.path; + + + for (let retryCount = 0; retryCount < 5; retryCount += 1) { + const assetFilePath = joinPaths(staticFilesDirectory, filePath); + + if (!assetFilePath.startsWith(staticFilesDirectory)) { + return { statusCode: 404 }; + } + + try { + const fileExtension = path + .extname(assetFilePath) + .slice(1) as SupportedFileExtension; + + const contentType = contentTypes[fileExtension] || contentTypes.txt; + + return { response: await readFileBuffer(assetFilePath), contentType }; + } catch (err) { + if (retryCount > 5) { + logger.error("handleStaticFile:", String(err)); + + return { statusCode: 404 }; + } + + filePath = `${publicPath}/${appName}.html`; + } + } + + return { statusCode: 404 }; + }; + }, +}); + +export default prodStaticFileRouteHandlerInjectable; diff --git a/src/main/routes/files/static-file-route.injectable.ts b/src/main/routes/files/static-file-route.injectable.ts new file mode 100644 index 0000000000..0268ac2051 --- /dev/null +++ b/src/main/routes/files/static-file-route.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getRouteInjectable } from "../../router/router.injectable"; +import isDevelopmentInjectable from "../../../common/vars/is-development.injectable"; +import { route } from "../../router/route"; +import prodStaticFileRouteHandlerInjectable from "./production.injectable"; +import devStaticFileRouteHandlerInjectable from "./development.injectable"; + +const staticFileRouteInjectable = getRouteInjectable({ + id: "static-file-route", + + instantiate: (di) => { + const isDevelopment = di.inject(isDevelopmentInjectable); + + return route({ + method: "get", + path: `/{path*}`, + })( + isDevelopment + ? di.inject(devStaticFileRouteHandlerInjectable) + : di.inject(prodStaticFileRouteHandlerInjectable), + ); + }, +}); + +export default staticFileRouteInjectable; diff --git a/src/main/routes/static-file-route.injectable.ts b/src/main/routes/static-file-route.injectable.ts deleted file mode 100644 index 90c3d02442..0000000000 --- a/src/main/routes/static-file-route.injectable.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { SupportedFileExtension } from "../router/router-content-types"; -import { contentTypes } from "../router/router-content-types"; -import logger from "../logger"; -import { getRouteInjectable } from "../router/router.injectable"; -import { publicPath } from "../../common/vars"; -import path from "path"; -import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; -import httpProxy from "http-proxy"; -import readFileBufferInjectable from "../../common/fs/read-file-buffer.injectable"; -import type { JoinPaths } from "../../common/path/join-paths.injectable"; -import joinPathsInjectable from "../../common/path/join-paths.injectable"; -import { webpackDevServerPort } from "../../../webpack/vars"; -import type { LensApiRequest, RouteResponse } from "../router/route"; -import { route } from "../router/route"; -import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable"; -import appNameInjectable from "../../common/vars/app-name.injectable"; -import type { GetAbsolutePath } from "../../common/path/get-absolute-path.injectable"; -import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable"; - -interface ProductionDependencies { - readFileBuffer: (path: string) => Promise; - joinPaths: JoinPaths; - getAbsolutePath: GetAbsolutePath; - staticFilesDirectory: string; - appName: string; -} - -const handleStaticFileInProduction = ({ - readFileBuffer, - getAbsolutePath, - joinPaths, - staticFilesDirectory, - appName, -}: ProductionDependencies) => ( - async ({ params }: LensApiRequest<"/{path*}">): Promise> => { - let filePath = params.path; - - for (let retryCount = 0; retryCount < 5; retryCount += 1) { - const asset = joinPaths(staticFilesDirectory, filePath); - const normalizedFilePath = getAbsolutePath(asset); - - if (!normalizedFilePath.startsWith(staticFilesDirectory)) { - return { statusCode: 404 }; - } - - try { - const fileExtension = path - .extname(asset) - .slice(1) as SupportedFileExtension; - - const contentType = contentTypes[fileExtension] || contentTypes.txt; - - return { response: await readFileBuffer(asset), contentType }; - } catch (err) { - if (retryCount > 5) { - logger.error("handleStaticFile:", String(err)); - - return { statusCode: 404 }; - } - - filePath = `${publicPath}/${appName}.html`; - } - } - - return { statusCode: 404 }; - } -); - -interface DevelopmentDependencies { - proxy: httpProxy; - appName: string; -} - -const handleStaticFileInDevelopment = ({ - proxy, - appName, -}: DevelopmentDependencies) => ( - ({ raw: { req, res }}: LensApiRequest<"/{path*}">): RouteResponse => { - if (req.url === "/" || !req.url?.startsWith("/build/")) { - req.url = `${publicPath}/${appName}.html`; - } - - proxy.web(req, res, { - target: `http://127.0.0.1:${webpackDevServerPort}`, - }); - - return { proxy }; - } -); - -const staticFileRouteInjectable = getRouteInjectable({ - id: "static-file-route", - - instantiate: (di) => { - const isDevelopment = di.inject(isDevelopmentInjectable); - const readFileBuffer = di.inject(readFileBufferInjectable); - const joinPaths = di.inject(joinPathsInjectable); - const getAbsolutePath = di.inject(getAbsolutePathInjectable); - const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); - const appName = di.inject(appNameInjectable); - - return route({ - method: "get", - path: `/{path*}`, - })( - isDevelopment - ? handleStaticFileInDevelopment({ - proxy: httpProxy.createProxy(), - appName, - }) - : handleStaticFileInProduction({ - readFileBuffer, - joinPaths, - staticFilesDirectory, - appName, - getAbsolutePath, - }), - ); - }, -}); - -export default staticFileRouteInjectable; diff --git a/webpack/node-fetch.ts b/webpack/node-fetch.ts new file mode 100644 index 0000000000..1db909889d --- /dev/null +++ b/webpack/node-fetch.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import path from "path"; + +export default { + entry: "./node_modules/node-fetch/src/index.js", + output: { + path: path.resolve(__dirname, "..", "build", "webpack"), + filename: "node-fetch.bundle.js", + library: { + name: "NodeFetch", + type: "commonjs", + }, + clean: true, + asyncChunks: false, // This is required so that only one file is created + }, + mode: "production", + target: "electron-renderer", + optimization: { + concatenateModules: true, + minimize: true, + }, + externalsPresets: { + node: true, + }, + resolve: { + extensions: [".js"], + }, +}; diff --git a/yarn.lock b/yarn.lock index 956d0afedd..11944a6d68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2221,31 +2221,16 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node-fetch@^2.6.2": - version "2.6.2" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" - integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== - dependencies: - "@types/node" "*" - form-data "^3.0.0" - "@types/node@*": version "17.0.24" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.24.tgz#20ba1bf69c1b4ab405c7a01e950c4f446b05029f" integrity sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g== -"@types/node@^16.11.26", "@types/node@^16.18.3": +"@types/node@^16.11.26", "@types/node@^16.18.2": version "16.18.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc" integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg== -"@types/npm@^2.0.32": - version "2.0.32" - resolved "https://registry.yarnpkg.com/@types/npm/-/npm-2.0.32.tgz#036682075b9c2116b510fe24b52a5b932e3a99d5" - integrity sha512-9Lg4woNVzJCtac0lET91H65lbO+8YXfk0nmlmoPGhHXMdaVEDloH6zOPIYMy2n39z/aCXXQR0nax66EDekAyIQ== - dependencies: - "@types/node" "*" - "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -4552,6 +4537,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" + integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -5899,6 +5889,14 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -6127,6 +6125,13 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -9104,12 +9109,19 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== -node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^3.2.10: + version "3.2.10" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8" + integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA== dependencies: - whatwg-url "^5.0.0" + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" node-forge@^0.10.0: version "0.10.0" @@ -12069,11 +12081,6 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - traverse-chain@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" @@ -12601,10 +12608,10 @@ wcwidth@^1.0.0: dependencies: defaults "^1.0.3" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== webidl-conversions@^5.0.0: version "5.0.0" @@ -12795,14 +12802,6 @@ whatwg-url@^11.0.0: tr46 "^3.0.0" webidl-conversions "^7.0.0" -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"