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

Remove request and request-promise-native in favour of got

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-02-15 14:33:46 -05:00
parent d656a9e289
commit aecdb222ac
15 changed files with 175 additions and 198 deletions

View File

@ -4,19 +4,18 @@
*/
import packageInfo from "../package.json";
import fs from "fs";
import request from "request";
import md5File from "md5-file";
import requestPromise from "request-promise-native";
import { ensureDir, pathExists } from "fs-extra";
import path from "path";
import { noop } from "lodash";
import { isLinux, isMac } from "../src/common/vars";
import got from "got";
class KubectlDownloader {
public kubectlVersion: string;
protected url: string;
protected path: string;
protected dirname: string;
public readonly kubectlVersion: string;
protected readonly url: string;
protected readonly path: string;
protected readonly dirname: string;
constructor(clusterVersion: string, platform: string, arch: string, target: string) {
this.kubectlVersion = clusterVersion;
@ -28,14 +27,14 @@ class KubectlDownloader {
}
protected async urlEtag() {
const response = await requestPromise({
method: "HEAD",
uri: this.url,
resolveWithFullResponse: true,
}).catch(console.error);
try {
const response = await got.head(this.url);
if (response.headers["etag"]) {
return response.headers["etag"].replace(/"/g, "");
if (response.headers["etag"]) {
return response.headers["etag"].replace(/"/g, "");
}
} catch (error) {
console.error(error);
}
return "";
@ -71,11 +70,10 @@ class KubectlDownloader {
const file = fs.createWriteStream(this.path);
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
const requestOpts: request.UriOptions & request.CoreOptions = {
uri: this.url,
gzip: true,
};
const stream = request(requestOpts);
const stream = got.stream({
url: this.url,
decompress: true,
});
stream.on("complete", () => {
console.log("kubectl binary download finished");

View File

@ -216,6 +216,7 @@
"got": "^11.8.3",
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.7",
"hpagent": "^0.1.2",
"http-proxy": "^1.18.1",
"immer": "^9.0.6",
"joi": "^17.5.0",
@ -246,8 +247,6 @@
"react-router": "^5.2.0",
"react-virtualized-auto-sizer": "^1.0.6",
"readable-stream": "^3.6.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.9",
"rfc6902": "^4.0.2",
"semver": "^7.3.2",
"shell-env": "^3.0.1",

View File

@ -1,34 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import request from "request";
import requestPromise from "request-promise-native";
import { UserStore } from "./user-store";
// todo: get rid of "request" (deprecated)
// https://github.com/lensapp/lens/issues/459
function getDefaultRequestOpts(): Partial<request.Options> {
const { httpsProxy, allowUntrustedCAs } = UserStore.getInstance();
return {
proxy: httpsProxy || undefined,
rejectUnauthorized: !allowUntrustedCAs,
};
}
/**
* @deprecated
*/
export function customRequest(opts: request.Options) {
return request.defaults(getDefaultRequestOpts())(opts);
}
/**
* @deprecated
*/
export function customRequestPromise(opts: requestPromise.Options) {
return requestPromise.defaults(getDefaultRequestOpts())(opts);
}

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import requestPromise from "request-promise-native";
import got from "got";
import packageInfo from "../../../package.json";
export function getAppVersion(): string {
@ -14,13 +14,14 @@ export function getBundledKubectlVersion(): string {
return packageInfo.config.bundledKubectlVersion;
}
interface AppVersion {
version: string;
}
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
const response = await requestPromise({
method: "GET",
uri: `http://127.0.0.1:${proxyPort}/version`,
resolveWithFullResponse: true,
proxy: undefined,
const { body } = await got<AppVersion>(`http://localhost:${proxyPort}/version`, {
responseType: "json",
});
return JSON.parse(response.body).version;
return body.version;
}

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import request from "request";
import got from "got";
export interface DownloadFileOptions {
url: string;
@ -19,7 +19,11 @@ export interface DownloadFileTicket<T> {
export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket<Buffer> {
const fileChunks: Buffer[] = [];
const req = request(url, { gzip, timeout });
const req = got.stream({
url,
timeout,
decompress: gzip,
});
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
req.on("data", (chunk: Buffer) => {
fileChunks.push(chunk);
@ -36,7 +40,7 @@ export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions)
url,
promise,
cancel() {
req.abort();
req.destroy();
},
};
}

View File

@ -30,7 +30,6 @@ jest.mock("winston", () => ({
jest.mock("../../common/ipc");
jest.mock("request");
jest.mock("request-promise-native");
import { Console } from "console";
import mockFs from "mock-fs";

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestPromiseOptions } from "request-promise-native";
import type { Cluster } from "../../common/cluster/cluster";
import type { OptionsOfJSONResponseBody } from "got";
import { k8sRequest } from "../k8s-request";
export type ClusterDetectionResult = {
@ -12,17 +12,14 @@ export type ClusterDetectionResult = {
accuracy: number
};
export class BaseClusterDetector {
key: string;
export abstract class BaseClusterDetector {
abstract key: string;
constructor(public cluster: Cluster) {
}
constructor(public cluster: Cluster) {}
detect(): Promise<ClusterDetectionResult> {
return null;
}
abstract detect(): Promise<ClusterDetectionResult>;
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
protected async k8sRequest<T = any>(path: string, options: OptionsOfJSONResponseBody = {}): Promise<T> {
return k8sRequest(this.cluster, path, options);
}
}

View File

@ -5,43 +5,51 @@
import { observable } from "mobx";
import type { ClusterMetadata } from "../../common/cluster-types";
import { Singleton } from "../../common/utils";
import { iter, Singleton } from "../../common/utils";
import type { Cluster } from "../../common/cluster/cluster";
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
export class DetectorRegistry extends Singleton {
registry = observable.array<typeof BaseClusterDetector>([], { deep: false });
export type ClusterDetectionConstructor = new (cluster: Cluster) => BaseClusterDetector;
add(detectorClass: typeof BaseClusterDetector): this {
export class DetectorRegistry extends Singleton {
private registry = observable.array<ClusterDetectionConstructor>([], { deep: false });
add(detectorClass: ClusterDetectionConstructor): this {
this.registry.push(detectorClass);
return this;
}
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
const results: { [key: string]: ClusterDetectionResult } = {};
const results = new Map<string, ClusterDetectionResult>();
const detections = this.registry.map(async DetectorClass => {
const detector = new DetectorClass(cluster);
for (const detectorClass of this.registry) {
const detector = new detectorClass(cluster);
return [detector.key, await detector.detect()] as const;
});
for (const detection of detections) {
try {
const data = await detector.detect();
const [key, data] = await detection;
if (!data) continue;
const existingValue = results[detector.key];
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
results[detector.key] = data;
} catch (e) {
// detector raised error, do nothing
if (
data && (
!results.has(key)
|| results.get(key).accuracy <= data.accuracy
)
) {
results.set(key, data);
}
} catch {
// ignore errors
}
}
const metadata: ClusterMetadata = {};
for (const [key, result] of Object.entries(results)) {
metadata[key] = result.value;
}
return metadata;
return Object.fromEntries(
iter.map(
results.entries(),
([key, { value }]) => [key, value],
),
);
}
}

View File

@ -8,10 +8,9 @@ import { BaseEncodingOptions, readFile } from "fs-extra";
import { promiseExecFile } from "../../common/utils/promise-exec";
import { helmCli } from "./helm-cli";
import { Singleton } from "../../common/utils/singleton";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy";
import logger from "../logger";
import type { ExecFileOptions } from "child_process";
import { iter } from "../../common/utils";
export type HelmEnv = Record<string, string> & {
HELM_REPOSITORY_CACHE?: string;
@ -46,47 +45,42 @@ async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFile
}
}
/**
* A Regex for matching strings of the form <key>=<value> where key MUST NOT
* include a '='
*/
const envVarMatcher = /^((?<key>[^=]*)=(?<value>.*))$/;
export class HelmRepoManager extends Singleton {
protected repos: HelmRepo[];
protected helmEnv: HelmEnv;
protected initialized: boolean;
protected helmEnv: HelmEnv | null = null;
public static async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
return orderBy<HelmRepo>(res.body, repo => repo.name);
}
private async init() {
private async ensureInitialized() {
helmCli.setLogger(logger);
await helmCli.ensureBinary();
if (!this.initialized) {
if (!this.helmEnv) {
this.helmEnv = await HelmRepoManager.parseHelmEnv();
await HelmRepoManager.update();
this.initialized = true;
try {
await HelmRepoManager.update();
} catch (error) {
logger.warn(`[HELM-REPO-MANAGER]: failed to update helm repos`, error);
}
}
}
protected static async parseHelmEnv() {
protected static async parseHelmEnv(): Promise<HelmEnv> {
const output = await execHelm(["env"]);
const lines = output.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
return env;
return Object.fromEntries(
iter.map(
iter.filterMap(
output.split(/\r?\n/),
line => line.match(envVarMatcher),
),
({ groups: { key, value }}) => [key, JSON.parse(value)],
),
);
}
public async repo(name: string): Promise<HelmRepo> {
@ -114,9 +108,7 @@ export class HelmRepoManager extends Singleton {
public async repositories(): Promise<HelmRepo[]> {
try {
if (!this.initialized) {
await this.init();
}
await this.ensureInitialized();
const { repositories } = await this.readConfig();
@ -144,18 +136,7 @@ export class HelmRepoManager extends Singleton {
]);
}
public static async addRepo({ name, url }: HelmRepo) {
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
return execHelm([
"repo",
"add",
name,
url,
]);
}
public static async addCustomRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
public static async addRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = [
"repo",

View File

@ -3,32 +3,40 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import request, { RequestPromiseOptions } from "request-promise-native";
import { apiKubePrefix } from "../common/vars";
import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api";
import { LensProxy } from "./lens-proxy";
import type { Cluster } from "../common/cluster/cluster";
import got, { OptionsOfJSONResponseBody } from "got";
export async function k8sRequest<T = any>(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise<T> {
const kubeProxyUrl = `http://localhost:${LensProxy.getInstance().port}${apiKubePrefix}`;
function getKubeProxyUrl() {
return `http://localhost:${LensProxy.getInstance().port}${apiKubePrefix}`;
}
export async function k8sRequest<T = any>(cluster: Cluster, path: string, options: OptionsOfJSONResponseBody = {}): Promise<T> {
const kubeProxyUrl = getKubeProxyUrl();
options.headers ??= {};
options.json ??= true;
options.timeout ??= 30000;
options.headers ??= {};
options.headers.Host = `${cluster.id}.${new URL(kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest()
options.responseType = "json";
return request(kubeProxyUrl + path, options);
const { body } = await got<T>(kubeProxyUrl + path, options);
return body;
}
export async function getMetrics(cluster: Cluster, prometheusPath: string, queryParams: IMetricsReqParams & { query: string }): Promise<any> {
const prometheusPrefix = cluster.preferences.prometheus?.prefix || "";
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
return k8sRequest(cluster, metricsPath, {
timeout: 0,
resolveWithFullResponse: false,
json: true,
method: "POST",
const kubeProxyUrl = getKubeProxyUrl();
const url = `${kubeProxyUrl}/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
const { body } = await got.post<any>(url, {
form: queryParams,
headers: {
Host: `${cluster.id}.${new URL(kubeProxyUrl).host}`,
},
responseType: "json",
});
return body;
}

View File

@ -14,7 +14,8 @@ import { getBundledKubectlVersion } from "../../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../../common/vars";
import { SemVer } from "semver";
import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers";
import got from "got/dist/source";
import got, { Agents } from "got";
import { HttpsProxyAgent } from "hpagent";
import { promisify } from "util";
import stream from "stream";
import { noop } from "lodash/fp";
@ -68,6 +69,8 @@ interface Dependencies {
downloadBinariesPath?: string
downloadKubectlBinaries: boolean
downloadMirror: string
httpsProxy?: string;
allowUntrustedCAs?: boolean;
};
}
@ -297,7 +300,20 @@ export class Kubectl {
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
const downloadStream = got.stream({ url: this.url, decompress: true });
const { httpsProxy, allowUntrustedCAs } = this.dependencies.userStore;
const agent: Agents | false = httpsProxy
? {
https: new HttpsProxyAgent({
proxy: httpsProxy,
}),
}
: false;
const downloadStream = got.stream({
url: this.url,
decompress: true,
rejectUnauthorized: !allowUntrustedCAs,
agent,
});
const fileWriteStream = fs.createWriteStream(this.path, { mode: 0o755 });
const pipeline = promisify(stream.pipeline);

View File

@ -5,18 +5,23 @@
import path from "path";
import fs from "fs";
import request from "request";
import { ensureDir, pathExists } from "fs-extra";
import * as tar from "tar";
import { isWindows } from "../common/vars";
import type winston from "winston";
import got, { Options } from "got";
import type { Merge } from "type-fest";
type GotStreamOptions = Merge<Options, {
isStream?: true;
}>;
export type LensBinaryOpts = {
version: string;
baseDir: string;
originalBinaryName: string;
newBinaryName?: string;
requestOpts?: request.Options;
requestOpts?: GotStreamOptions;
};
export class LensBinary {
@ -31,7 +36,7 @@ export class LensBinary {
protected platformName: string;
protected arch: string;
protected originalBinaryName: string;
protected requestOpts: request.Options;
protected requestOpts: GotStreamOptions;
protected logger: Console | winston.Logger;
constructor(opts: LensBinaryOpts) {
@ -40,7 +45,7 @@ export class LensBinary {
this.originalBinaryName = opts.originalBinaryName;
this.binaryName = opts.newBinaryName || opts.originalBinaryName;
this.binaryVersion = opts.version;
this.requestOpts = opts.requestOpts;
this.requestOpts = opts.requestOpts || {};
this.logger = console;
let arch = null;
@ -160,6 +165,7 @@ export class LensBinary {
}
protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath();
await ensureDir(this.getBinaryDir(), 0o755);
@ -168,12 +174,11 @@ export class LensBinary {
const url = this.getUrl();
this.logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`);
const requestOpts: request.UriOptions & request.CoreOptions = {
uri: url,
gzip: true,
const stream = got.stream(url, {
decompress: true,
...this.requestOpts,
};
const stream = request(requestOpts);
});
stream.on("complete", () => {
this.logger.info(`Download of ${this.originalBinaryName} finished`);

View File

@ -91,7 +91,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
async addCustomRepo() {
try {
await HelmRepoManager.addCustomRepo(this.helmRepo);
await HelmRepoManager.addRepo(this.helmRepo);
Notifications.ok(<>Helm repository <b>{this.helmRepo.name}</b> has added</>);
this.props.onAddRepo();
this.close();

View File

@ -18,6 +18,17 @@ import { observer } from "mobx-react";
import { RemovableItem } from "./removable-item";
import { Notice } from "../+extensions/notice";
import { Spinner } from "../spinner";
import got from "got";
import { orderBy } from "lodash";
async function loadAvailableHelmRepos(): Promise<HelmRepo[]> {
const { body } = await got.get<HelmRepo[]>("https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json", {
timeout: 10_000,
responseType: "json",
});
return orderBy(body, repo => repo.name);
}
@observer
export class HelmCharts extends React.Component {
@ -47,7 +58,7 @@ export class HelmCharts extends React.Component {
try {
if (!this.repos.length) {
this.repos = await HelmRepoManager.loadAvailableRepos();
this.repos = await loadAvailableHelmRepos();
}
const repos = await HelmRepoManager.getInstance().repositories(); // via helm-cli

View File

@ -6664,6 +6664,11 @@ hpack.js@^2.1.6:
readable-stream "^2.0.1"
wbuf "^1.1.0"
hpagent@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9"
integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==
html-encoding-sniffer@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
@ -8753,7 +8758,7 @@ lodash.without@~4.4.0:
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
lodash@4.x, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0:
lodash@4.x, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -11389,22 +11394,6 @@ repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
request-promise-core@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
dependencies:
lodash "^4.17.19"
request-promise-native@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
dependencies:
request-promise-core "1.1.4"
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
request@^2.88.0, request@^2.88.2:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
@ -12320,11 +12309,6 @@ static-extend@^0.1.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
stealthy-require@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
stream-buffers@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521"
@ -12937,14 +12921,6 @@ touch@^3.1.0:
dependencies:
nopt "~1.0.10"
tough-cookie@^2.3.3, tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
@ -12954,6 +12930,14 @@ tough-cookie@^4.0.0:
punycode "^2.1.1"
universalify "^0.1.2"
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tr46@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"