From f33c7932ed1a2132a18285670e0ecdddf64c0340 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Wed, 8 Apr 2026 05:52:09 -0700 Subject: [PATCH] Vendor whatwg-fetch@3.6.20 Summary: Vendor `whatwg-fetch` v3.6.20 directly into `Libraries/Network/`, replacing the npm dependency with a local copy. `whatwg-fetch` has not had a release since December 2023 and appears to be [unmaintained](https://github.com/JakeChampion/fetch/issues/1482), by download numbers `react-native` is the dominant consumer (all this is unsurprising, since `fetch` is natively implemented in every browser). It has several shortcomings we (with the OSS community) could improve on - one important one being that `whatwg-fetch`'s `fetch()` does not resolve immediately on receipt of headers, only on completion. This takes the single source file from the `whatwg-fetch` npm package as `Libraries/Network/whatwg-fetch.js`, and its accompanying types, marking as originally vendored from whatwg-fetch v3.6.20 (Copyright (c) 2014-2023 GitHub, Inc., MIT license). Changelog: [Internal] Reviewed By: huntie Differential Revision: D99854694 --- .../react-native/Libraries/Network/fetch.js | 2 +- .../Libraries/Network/whatwg-fetch.js | 651 ++++++++++++++++++ .../Libraries/Network/whatwg-fetch.js.flow | 133 ++++ packages/react-native/package.json | 1 - yarn.lock | 5 - 5 files changed, 785 insertions(+), 7 deletions(-) create mode 100644 packages/react-native/Libraries/Network/whatwg-fetch.js create mode 100644 packages/react-native/Libraries/Network/whatwg-fetch.js.flow diff --git a/packages/react-native/Libraries/Network/fetch.js b/packages/react-native/Libraries/Network/fetch.js index b014ddbc1d46..292e0dddfc76 100644 --- a/packages/react-native/Libraries/Network/fetch.js +++ b/packages/react-native/Libraries/Network/fetch.js @@ -12,7 +12,7 @@ // side-effectful require() to put fetch, // Headers, Request, Response in global scope -require('whatwg-fetch'); +require('./whatwg-fetch'); export const fetch = global.fetch; export const Headers = global.Headers; diff --git a/packages/react-native/Libraries/Network/whatwg-fetch.js b/packages/react-native/Libraries/Network/whatwg-fetch.js new file mode 100644 index 000000000000..1dce11082523 --- /dev/null +++ b/packages/react-native/Libraries/Network/whatwg-fetch.js @@ -0,0 +1,651 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * Originally vendored from whatwg-fetch v3.6.20 + * Copyright (c) 2014-2023 GitHub, Inc. + * Licensed under the MIT license. + * https://github.com/github/fetch + * + * @noflow + * @nolint + */ + +/* eslint-disable */ +var g = globalThis; + +var support = { + searchParams: 'URLSearchParams' in g, + iterable: 'Symbol' in g && 'iterator' in Symbol, + blob: + 'FileReader' in g && + 'Blob' in g && + (function() { + try { + new Blob() + return true + } catch (e) { + return false + } + })(), + formData: 'FormData' in g, + arrayBuffer: 'ArrayBuffer' in g +} + +function isDataView(obj) { + return obj && DataView.prototype.isPrototypeOf(obj) +} + +if (support.arrayBuffer) { + var viewClasses = [ + '[object Int8Array]', + '[object Uint8Array]', + '[object Uint8ClampedArray]', + '[object Int16Array]', + '[object Uint16Array]', + '[object Int32Array]', + '[object Uint32Array]', + '[object Float32Array]', + '[object Float64Array]' + ] + + var isArrayBufferView = + ArrayBuffer.isView || + function(obj) { + return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 + } +} + +function normalizeName(name) { + if (typeof name !== 'string') { + name = String(name) + } + if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { + throw new TypeError('Invalid character in header field name: "' + name + '"') + } + return name.toLowerCase() +} + +function normalizeValue(value) { + if (typeof value !== 'string') { + value = String(value) + } + return value +} + +// Build a destructive iterator for the value list +function iteratorFor(items) { + var iterator = { + next: function() { + var value = items.shift() + return {done: value === undefined, value: value} + } + } + + if (support.iterable) { + iterator[Symbol.iterator] = function() { + return iterator + } + } + + return iterator +} + +export function Headers(headers) { + this.map = {} + + if (headers instanceof Headers) { + headers.forEach(function(value, name) { + this.append(name, value) + }, this) + } else if (Array.isArray(headers)) { + headers.forEach(function(header) { + if (header.length != 2) { + throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length) + } + this.append(header[0], header[1]) + }, this) + } else if (headers) { + Object.getOwnPropertyNames(headers).forEach(function(name) { + this.append(name, headers[name]) + }, this) + } +} + +Headers.prototype.append = function(name, value) { + name = normalizeName(name) + value = normalizeValue(value) + var oldValue = this.map[name] + this.map[name] = oldValue ? oldValue + ', ' + value : value +} + +Headers.prototype['delete'] = function(name) { + delete this.map[normalizeName(name)] +} + +Headers.prototype.get = function(name) { + name = normalizeName(name) + return this.has(name) ? this.map[name] : null +} + +Headers.prototype.has = function(name) { + return this.map.hasOwnProperty(normalizeName(name)) +} + +Headers.prototype.set = function(name, value) { + this.map[normalizeName(name)] = normalizeValue(value) +} + +Headers.prototype.forEach = function(callback, thisArg) { + for (var name in this.map) { + if (this.map.hasOwnProperty(name)) { + callback.call(thisArg, this.map[name], name, this) + } + } +} + +Headers.prototype.keys = function() { + var items = [] + this.forEach(function(value, name) { + items.push(name) + }) + return iteratorFor(items) +} + +Headers.prototype.values = function() { + var items = [] + this.forEach(function(value) { + items.push(value) + }) + return iteratorFor(items) +} + +Headers.prototype.entries = function() { + var items = [] + this.forEach(function(value, name) { + items.push([name, value]) + }) + return iteratorFor(items) +} + +if (support.iterable) { + Headers.prototype[Symbol.iterator] = Headers.prototype.entries +} + +function consumed(body) { + if (body._noBody) return + if (body.bodyUsed) { + return Promise.reject(new TypeError('Already read')) + } + body.bodyUsed = true +} + +function fileReaderReady(reader) { + return new Promise(function(resolve, reject) { + reader.onload = function() { + resolve(reader.result) + } + reader.onerror = function() { + reject(reader.error) + } + }) +} + +function readBlobAsArrayBuffer(blob) { + var reader = new FileReader() + var promise = fileReaderReady(reader) + reader.readAsArrayBuffer(blob) + return promise +} + +function readBlobAsText(blob) { + var reader = new FileReader() + var promise = fileReaderReady(reader) + var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type) + var encoding = match ? match[1] : 'utf-8' + reader.readAsText(blob, encoding) + return promise +} + +function readArrayBufferAsText(buf) { + var view = new Uint8Array(buf) + var chars = new Array(view.length) + + for (var i = 0; i < view.length; i++) { + chars[i] = String.fromCharCode(view[i]) + } + return chars.join('') +} + +function bufferClone(buf) { + if (buf.slice) { + return buf.slice(0) + } else { + var view = new Uint8Array(buf.byteLength) + view.set(new Uint8Array(buf)) + return view.buffer + } +} + +function Body() { + this.bodyUsed = false + + this._initBody = function(body) { + /* + fetch-mock wraps the Response object in an ES6 Proxy to + provide useful test harness features such as flush. However, on + ES5 browsers without fetch or Proxy support pollyfills must be used; + the proxy-pollyfill is unable to proxy an attribute unless it exists + on the object before the Proxy is created. This change ensures + Response.bodyUsed exists on the instance, while maintaining the + semantic of setting Request.bodyUsed in the constructor before + _initBody is called. + */ + this.bodyUsed = this.bodyUsed + this._bodyInit = body + if (!body) { + this._noBody = true; + this._bodyText = '' + } else if (typeof body === 'string') { + this._bodyText = body + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this._bodyText = body.toString() + } else if (support.arrayBuffer && support.blob && isDataView(body)) { + this._bodyArrayBuffer = bufferClone(body.buffer) + // IE 10-11 can't handle a DataView body. + this._bodyInit = new Blob([this._bodyArrayBuffer]) + } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + this._bodyArrayBuffer = bufferClone(body) + } else { + this._bodyText = body = Object.prototype.toString.call(body) + } + + if (!this.headers.get('content-type')) { + if (typeof body === 'string') { + this.headers.set('content-type', 'text/plain;charset=UTF-8') + } else if (this._bodyBlob && this._bodyBlob.type) { + this.headers.set('content-type', this._bodyBlob.type) + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8') + } + } + } + + if (support.blob) { + this.blob = function() { + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(new Blob([this._bodyArrayBuffer])) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } + } + } + + this.arrayBuffer = function() { + if (this._bodyArrayBuffer) { + var isConsumed = consumed(this) + if (isConsumed) { + return isConsumed + } else if (ArrayBuffer.isView(this._bodyArrayBuffer)) { + return Promise.resolve( + this._bodyArrayBuffer.buffer.slice( + this._bodyArrayBuffer.byteOffset, + this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength + ) + ) + } else { + return Promise.resolve(this._bodyArrayBuffer) + } + } else if (support.blob) { + return this.blob().then(readBlobAsArrayBuffer) + } else { + throw new Error('could not read as ArrayBuffer') + } + } + + this.text = function() { + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } + } + + if (support.formData) { + this.formData = function() { + return this.text().then(decode) + } + } + + this.json = function() { + return this.text().then(JSON.parse) + } + + return this +} + +// HTTP methods whose capitalization should be normalized +var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'] + +function normalizeMethod(method) { + var upcased = method.toUpperCase() + return methods.indexOf(upcased) > -1 ? upcased : method +} + +export function Request(input, options) { + if (!(this instanceof Request)) { + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + } + + options = options || {} + var body = options.body + + if (input instanceof Request) { + if (input.bodyUsed) { + throw new TypeError('Already read') + } + this.url = input.url + this.credentials = input.credentials + if (!options.headers) { + this.headers = new Headers(input.headers) + } + this.method = input.method + this.mode = input.mode + this.signal = input.signal + if (!body && input._bodyInit != null) { + body = input._bodyInit + input.bodyUsed = true + } + } else { + this.url = String(input) + } + + this.credentials = options.credentials || this.credentials || 'same-origin' + if (options.headers || !this.headers) { + this.headers = new Headers(options.headers) + } + this.method = normalizeMethod(options.method || this.method || 'GET') + this.mode = options.mode || this.mode || null + this.signal = options.signal || this.signal || (function () { + if ('AbortController' in g) { + var ctrl = new AbortController(); + return ctrl.signal; + } + }()); + this.referrer = null + + if ((this.method === 'GET' || this.method === 'HEAD') && body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(body) + + if (this.method === 'GET' || this.method === 'HEAD') { + if (options.cache === 'no-store' || options.cache === 'no-cache') { + // Search for a '_' parameter in the query string + var reParamSearch = /([?&])_=[^&]*/ + if (reParamSearch.test(this.url)) { + // If it already exists then set the value with the current time + this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime()) + } else { + // Otherwise add a new '_' parameter to the end with the current time + var reQueryString = /\?/ + this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime() + } + } + } +} + +Request.prototype.clone = function() { + return new Request(this, {body: this._bodyInit}) +} + +function decode(body) { + var form = new FormData() + body + .trim() + .split('&') + .forEach(function(bytes) { + if (bytes) { + var split = bytes.split('=') + var name = split.shift().replace(/\+/g, ' ') + var value = split.join('=').replace(/\+/g, ' ') + form.append(decodeURIComponent(name), decodeURIComponent(value)) + } + }) + return form +} + +function parseHeaders(rawHeaders) { + var headers = new Headers() + // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space + // https://tools.ietf.org/html/rfc7230#section-3.2 + var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ') + // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill + // https://github.com/github/fetch/issues/748 + // https://github.com/zloirock/core-js/issues/751 + preProcessedHeaders + .split('\r') + .map(function(header) { + return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header + }) + .forEach(function(line) { + var parts = line.split(':') + var key = parts.shift().trim() + if (key) { + var value = parts.join(':').trim() + try { + headers.append(key, value) + } catch (error) { + console.warn('Response ' + error.message) + } + } + }) + return headers +} + +Body.call(Request.prototype) + +export function Response(bodyInit, options) { + if (!(this instanceof Response)) { + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + } + if (!options) { + options = {} + } + + this.type = 'default' + this.status = options.status === undefined ? 200 : options.status + if (this.status < 200 || this.status > 599) { + throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].") + } + this.ok = this.status >= 200 && this.status < 300 + this.statusText = options.statusText === undefined ? '' : '' + options.statusText + this.headers = new Headers(options.headers) + this.url = options.url || '' + this._initBody(bodyInit) +} + +Body.call(Response.prototype) + +Response.prototype.clone = function() { + return new Response(this._bodyInit, { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers), + url: this.url + }) +} + +Response.error = function() { + var response = new Response(null, {status: 200, statusText: ''}) + response.ok = false + response.status = 0 + response.type = 'error' + return response +} + +var redirectStatuses = [301, 302, 303, 307, 308] + +Response.redirect = function(url, status) { + if (redirectStatuses.indexOf(status) === -1) { + throw new RangeError('Invalid status code') + } + + return new Response(null, {status: status, headers: {location: url}}) +} + +export var DOMException = g.DOMException +try { + new DOMException() +} catch (err) { + DOMException = function(message, name) { + this.message = message + this.name = name + var error = Error(message) + this.stack = error.stack + } + DOMException.prototype = Object.create(Error.prototype) + DOMException.prototype.constructor = DOMException +} + +export function fetch(input, init) { + return new Promise(function(resolve, reject) { + var request = new Request(input, init) + + if (request.signal && request.signal.aborted) { + return reject(new DOMException('Aborted', 'AbortError')) + } + + var xhr = new XMLHttpRequest() + + function abortXhr() { + xhr.abort() + } + + xhr.onload = function() { + var options = { + statusText: xhr.statusText, + headers: parseHeaders(xhr.getAllResponseHeaders() || '') + } + // This check if specifically for when a user fetches a file locally from the file system + // Only if the status is out of a normal range + if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) { + options.status = 200; + } else { + options.status = xhr.status; + } + options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') + var body = 'response' in xhr ? xhr.response : xhr.responseText + setTimeout(function() { + resolve(new Response(body, options)) + }, 0) + } + + xhr.onerror = function() { + setTimeout(function() { + reject(new TypeError('Network request failed')) + }, 0) + } + + xhr.ontimeout = function() { + setTimeout(function() { + reject(new TypeError('Network request timed out')) + }, 0) + } + + xhr.onabort = function() { + setTimeout(function() { + reject(new DOMException('Aborted', 'AbortError')) + }, 0) + } + + function fixUrl(url) { + try { + return url === '' && g.location.href ? g.location.href : url + } catch (e) { + return url + } + } + + xhr.open(request.method, fixUrl(request.url), true) + + if (request.credentials === 'include') { + xhr.withCredentials = true + } else if (request.credentials === 'omit') { + xhr.withCredentials = false + } + + if ('responseType' in xhr) { + if (support.blob) { + xhr.responseType = 'blob' + } else if ( + support.arrayBuffer + ) { + xhr.responseType = 'arraybuffer' + } + } + + if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) { + var names = []; + Object.getOwnPropertyNames(init.headers).forEach(function(name) { + names.push(normalizeName(name)) + xhr.setRequestHeader(name, normalizeValue(init.headers[name])) + }) + request.headers.forEach(function(value, name) { + if (names.indexOf(name) === -1) { + xhr.setRequestHeader(name, value) + } + }) + } else { + request.headers.forEach(function(value, name) { + xhr.setRequestHeader(name, value) + }) + } + + if (request.signal) { + request.signal.addEventListener('abort', abortXhr) + + xhr.onreadystatechange = function() { + // DONE (success or failure) + if (xhr.readyState === 4) { + request.signal.removeEventListener('abort', abortXhr) + } + } + } + + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) + }) +} + +fetch.polyfill = true + +if (!g.fetch) { + g.fetch = fetch + g.Headers = Headers + g.Request = Request + g.Response = Response +} diff --git a/packages/react-native/Libraries/Network/whatwg-fetch.js.flow b/packages/react-native/Libraries/Network/whatwg-fetch.js.flow new file mode 100644 index 000000000000..24939524e3c3 --- /dev/null +++ b/packages/react-native/Libraries/Network/whatwg-fetch.js.flow @@ -0,0 +1,133 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * Originally vendored from whatwg-fetch v3.6.20 + * Copyright (c) 2014-2023 GitHub, Inc. + * Licensed under the MIT license. + * https://github.com/github/fetch + * + * @nolint + */ + +/* @flow strict */ + +type CredentialsType = 'omit' | 'same-origin' | 'include' + +type ResponseType = 'default' | 'error' + +type BodyInit = string | URLSearchParams | FormData | Blob | ArrayBuffer | $ArrayBufferView + +type RequestInfo = Request | URL | string + +type RequestOptions = {| + body?: ?BodyInit; + + credentials?: CredentialsType; + headers?: HeadersInit; + method?: string; + mode?: string; + referrer?: string; + signal?: ?AbortSignal; +|} + +type ResponseOptions = {| + status?: number; + statusText?: string; + headers?: HeadersInit; +|} + +type HeadersInit = Headers | {[string]: string} + +// https://github.com/facebook/flow/blob/f68b89a5012bd995ab3509e7a41b7325045c4045/lib/bom.js#L902-L914 +declare class Headers { + @@iterator(): Iterator<[string, string]>; + constructor(init?: HeadersInit): void; + append(name: string, value: string): void; + delete(name: string): void; + entries(): Iterator<[string, string]>; + forEach((value: string, name: string, headers: Headers) => $FlowFixMe, thisArg?: $FlowFixMe): void; + get(name: string): null | string; + has(name: string): boolean; + keys(): Iterator; + set(name: string, value: string): void; + values(): Iterator; +} + +// https://github.com/facebook/flow/pull/6548 +interface AbortSignal { + aborted: boolean; + addEventListener(type: string, listener: (Event) => unknown, options?: EventListenerOptionsOrUseCapture): void; + removeEventListener(type: string, listener: (Event) => unknown, options?: EventListenerOptionsOrUseCapture): void; +} + +// https://github.com/facebook/flow/blob/f68b89a5012bd995ab3509e7a41b7325045c4045/lib/bom.js#L994-L1018 +// unsupported in polyfill: +// - cache +// - integrity +// - redirect +// - referrerPolicy +declare class Request { + constructor(input: RequestInfo, init?: RequestOptions): void; + clone(): Request; + + url: string; + + credentials: CredentialsType; + headers: Headers; + method: string; + mode: ModeType; + referrer: string; + signal: ?AbortSignal; + + // Body methods and attributes + bodyUsed: boolean; + + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise<$FlowFixMe>; + text(): Promise; +} + +// https://github.com/facebook/flow/blob/f68b89a5012bd995ab3509e7a41b7325045c4045/lib/bom.js#L968-L992 +// unsupported in polyfill: +// - body +// - redirected +// - trailer +declare class Response { + constructor(input?: ?BodyInit, init?: ResponseOptions): void; + clone(): Response; + static error(): Response; + static redirect(url: string, status?: number): Response; + + type: ResponseType; + url: string; + ok: boolean; + status: number; + statusText: string; + headers: Headers; + + // Body methods and attributes + bodyUsed: boolean; + + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise<$FlowFixMe>; + text(): Promise; +} + +declare class DOMException extends Error { + constructor(message?: string, name?: string): void; +} + +declare module.exports: { + fetch(input: RequestInfo, init?: RequestOptions): Promise; + Headers: typeof Headers; + Request: typeof Request; + Response: typeof Response; + DOMException: typeof DOMException; +} diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 288e70661fb4..a90d56aa4b51 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -191,7 +191,6 @@ "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "tinyglobby": "^0.2.15", - "whatwg-fetch": "^3.6.20", "ws": "^7.5.10", "yargs": "^17.6.2" }, diff --git a/yarn.lock b/yarn.lock index 5efce564f083..c8c0684d7341 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9368,11 +9368,6 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -whatwg-fetch@^3.6.20: - version "3.6.20" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" - integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== - whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"