diff --git a/packages/sdk/jest.config.integration.ts b/packages/sdk/jest.config.integration.ts index 64f8b965f3..773aea3853 100644 --- a/packages/sdk/jest.config.integration.ts +++ b/packages/sdk/jest.config.integration.ts @@ -2,7 +2,8 @@ import { baseConfig } from "@vue-storefront/jest-config"; export default { ...baseConfig, - // globalSetup: "./__tests__/integration/__config__/jest.setup.global.ts", - // globalTeardown: "./__tests__/integration/__config__/jest.teardown.global.ts", + globalSetup: "./src/__tests__/integration/__config__/jest.setup.global.ts", + globalTeardown: + "./src/__tests__/integration/__config__/jest.teardown.global.ts", // setupFilesAfterEnv: ["./__tests__/integration/__config__/jest.setup.ts"], }; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2ec3dc5382..714e04afdb 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -26,7 +26,10 @@ "babel-preset-node": "^5.1.1", "nodemon": "^2.0.20", "ts-jest": "^29.0.2", - "ts-node-dev": "^2.0.0" + "ts-node-dev": "^2.0.0", + "@vue-storefront/middleware": "*", + "isomorphic-fetch": "^3.0.0", + "axios": "^1.6.7" }, "engines": { "npm": ">=7.0.0", diff --git a/packages/sdk/src/__tests__/__mocks__/apiClient/server.js b/packages/sdk/src/__tests__/__mocks__/apiClient/server.js new file mode 100644 index 0000000000..aae75f0d0a --- /dev/null +++ b/packages/sdk/src/__tests__/__mocks__/apiClient/server.js @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +const { apiClientFactory } = require("@vue-storefront/middleware"); + +const onCreate = (settings) => { + return { + config: settings, + client: null, + }; +}; + +const { createApiClient } = apiClientFactory({ + onCreate, + api: { + getProduct: async (_context, params) => { + return { id: params.id, name: "Test Product" }; + }, + getProducts: async (_context, _params) => { + return [{ id: 1, name: "Test Product" }]; + }, + }, +}); + +exports.createApiClient = createApiClient; diff --git a/packages/sdk/src/__tests__/__mocks__/apiClient/types.ts b/packages/sdk/src/__tests__/__mocks__/apiClient/types.ts new file mode 100644 index 0000000000..5800e254e8 --- /dev/null +++ b/packages/sdk/src/__tests__/__mocks__/apiClient/types.ts @@ -0,0 +1,12 @@ +export type Endpoints = { + /** + * Get the product by id. + */ + getProduct: (params: { id: number }) => Promise<{ id: number; name: string }>; + /** + * Get the list of products. + */ + getProducts: (params: { + limit: number; + }) => Promise<{ id: number; name: string }[]>; +}; diff --git a/packages/sdk/src/__tests__/integration/__config__/jest.setup.global.ts b/packages/sdk/src/__tests__/integration/__config__/jest.setup.global.ts new file mode 100644 index 0000000000..4e7960608f --- /dev/null +++ b/packages/sdk/src/__tests__/integration/__config__/jest.setup.global.ts @@ -0,0 +1,22 @@ +import { createServer } from "@vue-storefront/middleware"; + +async function runMiddleware(app) { + return new Promise((resolve) => { + const server = app.listen(8181, async () => { + resolve(server); + }); + }); +} + +export default async () => { + const app = await createServer({ + integrations: { + commerce: { + location: "./src/__tests__/__mocks__/apiClient/server", + configuration: {}, + }, + }, + }); + const server = await runMiddleware(app); + globalThis.__MIDDLEWARE__ = server; +}; diff --git a/packages/sdk/src/__tests__/integration/__config__/jest.teardown.global.ts b/packages/sdk/src/__tests__/integration/__config__/jest.teardown.global.ts new file mode 100644 index 0000000000..770b59ad56 --- /dev/null +++ b/packages/sdk/src/__tests__/integration/__config__/jest.teardown.global.ts @@ -0,0 +1,3 @@ +export default () => { + globalThis.__MIDDLEWARE__.close(); +}; diff --git a/packages/sdk/src/__tests__/integration/modules/moduleFromEndpoints.spec.ts b/packages/sdk/src/__tests__/integration/modules/moduleFromEndpoints.spec.ts index 587887147a..b2276565cf 100644 --- a/packages/sdk/src/__tests__/integration/modules/moduleFromEndpoints.spec.ts +++ b/packages/sdk/src/__tests__/integration/modules/moduleFromEndpoints.spec.ts @@ -1,15 +1,19 @@ +import "isomorphic-fetch"; import { initSDK, buildModule } from "../../../index"; -import { moduleFromEndpoints } from "../../../modules/moduleFromEndpoints"; +import { + moduleFromEndpoints, + prepareConfig, +} from "../../../modules/moduleFromEndpoints"; +import { Endpoints } from "../../__mocks__/apiClient/types"; -type Endpoints = { - getProduct: (params: { id: string }) => Promise; - getProducts: (params: { limit: number }) => Promise; -}; +const axios = require("axios/dist/node/axios.cjs"); describe("moduleFromEndpoints", () => { it("should be able to be used as standard SDK module", async () => { const sdkConfig = { - commerce: buildModule(moduleFromEndpoints), + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + }), }; const sdk = initSDK(sdkConfig); @@ -19,7 +23,9 @@ describe("moduleFromEndpoints", () => { it("should use generic types to define the endpoints", async () => { const sdkConfig = { - commerce: buildModule(moduleFromEndpoints), + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + }), }; const sdk = initSDK(sdkConfig); @@ -27,4 +33,274 @@ describe("moduleFromEndpoints", () => { expect(sdk.commerce.getProduct).toBeInstanceOf(Function); expect(sdk.commerce.getProducts).toBeInstanceOf(Function); }); + + it("should allow to override the default HTTP Client", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProduct({ id: 1 }); + + expect(customHttpClient).toHaveBeenCalled(); + }); + + it("should send a POST request to / by default", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProduct({ id: 1 }); + + expect(customHttpClient).toHaveBeenCalledWith( + "http://localhost:8181/commerce/getProduct", + [{ id: 1 }], + expect.objectContaining({ + method: "POST", + }) + ); + }); + + it("should use default HTTP Client if it's not provided", async () => { + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + }), + }; + const sdk = initSDK(sdkConfig); + + const response = await sdk.commerce.getProduct({ id: 1 }); + + // To avoid mocking fetch, we're calling the real middleware and verifying the response. + expect(response).toEqual({ id: 1, name: "Test Product" }); + }); + + it("should allow to use GET request with query parameters", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProducts( + { limit: 1 }, + prepareConfig({ method: "GET" }) + ); + + expect(customHttpClient).toHaveBeenCalledWith( + `http://localhost:8181/commerce/getProducts?body=${encodeURIComponent( + JSON.stringify([{ limit: 1 }]) + )}`, + [], + expect.objectContaining({ + method: "GET", + }) + ); + }); + + it("should allow to use GET request when apiUrl is a path", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "/api/commerce", + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProducts( + { limit: 1 }, + prepareConfig({ method: "GET" }) + ); + + expect(customHttpClient).toHaveBeenCalledWith( + `/api/commerce/getProducts?body=${encodeURIComponent( + JSON.stringify([{ limit: 1 }]) + )}`, + [], + expect.objectContaining({ + method: "GET", + }) + ); + }); + + it("should normalize the url", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce///", // Extra slashes + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProduct({ id: 1 }); + + expect(customHttpClient).toHaveBeenCalledWith( + "http://localhost:8181/commerce/getProduct", + expect.any(Array), + expect.any(Object) + ); + }); + + it("should allow to use custom headers", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProduct( + { id: 1 }, + prepareConfig({ + method: "POST", + headers: { + "X-Test": "x-test-header", + }, + }) + ); + + expect(customHttpClient).toHaveBeenCalledWith( + "http://localhost:8181/commerce/getProduct", + expect.any(Array), + expect.objectContaining({ + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "X-Test": "x-test-header", + }, + }) + ); + }); + + it("should allow to define default headers", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + httpClient: customHttpClient, + defaultRequestConfig: { + headers: { + "X-Test": "x-test-header", + }, + }, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProduct({ id: 1 }); + + expect(customHttpClient).toHaveBeenCalledWith( + "http://localhost:8181/commerce/getProduct", + expect.any(Array), + expect.objectContaining({ + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "X-Test": "x-test-header", + }, + }) + ); + }); + + it("should use different base URL during SSR if defined", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "/api/commerce", + ssrApiUrl: "http://localhost:8181/commerce", + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProduct({ id: 1 }); + + expect(customHttpClient).toHaveBeenCalledWith( + "http://localhost:8181/commerce/getProduct", + expect.any(Array), + expect.any(Object) + ); + }); + + it("should be able to use axios as a custom HTTP client", async () => { + expect.assertions(2); + + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + httpClient: async (url, params, config) => { + const { data } = await axios(url, { + ...config, + data: params, + }); + return data; + }, + }), + }; + const sdk = initSDK(sdkConfig); + + const postResponse = await sdk.commerce.getProduct({ id: 1 }); + const getResponse = await sdk.commerce.getProduct( + { id: 2 }, + prepareConfig({ method: "GET" }) + ); + + expect(postResponse).toEqual({ id: 1, name: "Test Product" }); + expect(getResponse).toEqual({ id: 2, name: "Test Product" }); + }); + + it("should accept headers as Record", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(moduleFromEndpoints, { + apiUrl: "http://localhost:8181/commerce", + httpClient: customHttpClient, + defaultRequestConfig: { + headers: { + "X-Test-Default": ["x-test-header", "x-test-header-2"], + }, + }, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.getProduct( + { id: 1 }, + prepareConfig({ + method: "POST", + headers: { + "X-Test": ["x-test-header", "x-test-header-2"], + }, + }) + ); + + expect(customHttpClient).toHaveBeenCalledWith( + "http://localhost:8181/commerce/getProduct", + expect.any(Array), + expect.objectContaining({ + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "X-Test-Default": "x-test-header,x-test-header-2", + "X-Test": "x-test-header,x-test-header-2", + }, + }) + ); + }); }); diff --git a/packages/sdk/src/modules/moduleFromEndpoints/connector.ts b/packages/sdk/src/modules/moduleFromEndpoints/connector.ts index 019eb1271b..b588c395d3 100644 --- a/packages/sdk/src/modules/moduleFromEndpoints/connector.ts +++ b/packages/sdk/src/modules/moduleFromEndpoints/connector.ts @@ -1,13 +1,31 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { Connector } from "../../types"; -import { EndpointsConstraint } from "./types"; - -export const connector = () => { - const target = {} as Endpoints; - return new Proxy(target, { - get: (_, endpoint) => { - return async (params: unknown[]) => { - return {}; +import { getHTTPClient } from "./utils/getHttpClient"; +import { EndpointsConstraint, Options, Methods, IncomingConfig } from "./types"; +import { isConfig } from "./consts"; + +export const connector = ( + options: Options +) => { + const httpClient = getHTTPClient(options); + const target = {} as Methods; + return new Proxy>(target, { + get: (_, methodName) => { + if (typeof methodName !== "string") { + throw new Error("Method must be a string"); + } + + return async (...params: any[]) => { + let config: IncomingConfig | undefined; + + // If last parameter contains the `isRequestConfig` symbol, it's a request config + if (params.at(-1)?.[isConfig]) { + // Remove the `isRequestConfig` symbol from the request config + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [isConfig]: omit, ...rest } = params.pop(); + config = rest; + } + + return httpClient(methodName, params, config); }; }, }) satisfies Connector; diff --git a/packages/sdk/src/modules/moduleFromEndpoints/consts.ts b/packages/sdk/src/modules/moduleFromEndpoints/consts.ts new file mode 100644 index 0000000000..311bdf574b --- /dev/null +++ b/packages/sdk/src/modules/moduleFromEndpoints/consts.ts @@ -0,0 +1,4 @@ +/** + * Symbol to mark a method config object. + */ +export const isConfig = Symbol("config"); diff --git a/packages/sdk/src/modules/moduleFromEndpoints/index.ts b/packages/sdk/src/modules/moduleFromEndpoints/index.ts index eeac135c9c..7c3958f882 100644 --- a/packages/sdk/src/modules/moduleFromEndpoints/index.ts +++ b/packages/sdk/src/modules/moduleFromEndpoints/index.ts @@ -1,2 +1,3 @@ export { moduleFromEndpoints } from "./module"; +export { getHTTPClient, prepareConfig } from "./utils"; export * from "./types"; diff --git a/packages/sdk/src/modules/moduleFromEndpoints/module.ts b/packages/sdk/src/modules/moduleFromEndpoints/module.ts index 7eee75bf2f..5b0ddbd791 100644 --- a/packages/sdk/src/modules/moduleFromEndpoints/module.ts +++ b/packages/sdk/src/modules/moduleFromEndpoints/module.ts @@ -1,8 +1,10 @@ import { Module } from "../../types"; import { connector } from "./connector"; -import { EndpointsConstraint } from "./types"; +import { EndpointsConstraint, Options } from "./types"; -export const moduleFromEndpoints = () => +export const moduleFromEndpoints = ( + options: Options +) => ({ - connector: connector(), + connector: connector(options), } satisfies Module); diff --git a/packages/sdk/src/modules/moduleFromEndpoints/types.ts b/packages/sdk/src/modules/moduleFromEndpoints/types.ts index 0f909c2d0f..7c0bdbdf2b 100644 --- a/packages/sdk/src/modules/moduleFromEndpoints/types.ts +++ b/packages/sdk/src/modules/moduleFromEndpoints/types.ts @@ -1,4 +1,5 @@ import { AnyFunction } from "../../types"; +import { isConfig } from "./consts"; /** * Constraint for the endpoints. @@ -15,12 +16,136 @@ export type EndpointsConstraint = { [key: string]: AnyFunction; }; +/** + * Defines the basic configuration for an HTTP request. + * Specifies the HTTP method to be used. + */ +export type BaseConfig = { + /** + * The HTTP method for the request. Optional. Can be "GET" or "POST". + * @default "POST" + */ + method?: "GET" | "POST"; +}; + +/** + * User-defined configuration for HTTP requests, extending `BaseConfig`. + * Allows custom headers, supporting both strings and arrays of strings for header values. + */ +export type IncomingConfig = BaseConfig & { + /** + * Optional custom headers. Keys are header names, values can be a string or an array of strings. + */ + headers?: Record; +}; + +/** + * Computed configuration for HTTP requests, derived from `IncomingConfig`. + * Normalizes header values to strings for consistent request formatting. + */ +export type ComputedConfig = BaseConfig & { + /** + * Normalized headers for the HTTP request, ensuring all values are strings. + */ + headers?: Record; +}; + +/** + * Configuration specific to a method, merging `IncomingConfig` with an internal flag. + * Indicates that the configuration is ready for making a request. + */ +export type MethodConfig = IncomingConfig & { + /** + * Internal flag to mark the configuration as specific to a request. + */ + [isConfig]: boolean; +}; + +/** + * HTTP Client abstraction. + */ +export type HTTPClient = ( + /** + * URL for the request. + * @remarks + * It's the full URL for the request, including the base URL, endpoint and query parameters. + */ + url: string, + /** + * Parameters for the POST request. + */ + params: any[], + /** + * Config for the request. + */ + config?: ComputedConfig +) => Promise; + /** * Options for the `moduleFromEndpoints`. */ -export interface Options { +export type Options = { /** * Base URL for the API. */ apiUrl: string; -} + + /** + * Base URL for the API in the server side rendering. + * It's optional and it will use the `apiUrl` if it's not provided. + * + * @remarks + * This may be useful during implementation of a multi-store feature based on domains. + * + * `apiUrl` could be set to `/api` and on the client side, the HTTP Client would use the current domain. + * + * However, on the server side, the HTTP Client is not aware of the current domain, so it would use just the `/api` path and it would fail. + * + * To solve this issue, the `ssrApiUrl` could be set to the pod name with port (e.g. `https://localhost:8181`) and the HTTP Client would use it on the server side. + */ + ssrApiUrl?: string; + + /** + * Custom HTTP Client. + * + * It's optional and it will use the default HTTP Client if it's not provided. + * + * @example + * Using `axios` as the HTTP Client + * ```ts + * import axios from "axios"; + * + * const options: Options = { + * apiUrl: "https://api.example.com", + * httpClient: async (url, params, config) => { + * const { data } = await axios(url, { + * ...config, + * data: params, + * }); + * + * return data; + * }, + * }; + * ``` + */ + httpClient?: HTTPClient; + + /** + * Default request config for each request. + */ + defaultRequestConfig?: IncomingConfig; +}; + +/** + * Final type for the SDK methods. + * + * It requires the `Endpoints` interface to be provided. + * Based on this interface it will generate the methods with the correct parameters and return types. + * + * To each endpoint, it will add the `config` parameter with the `MethodConfig` type. + */ +export type Methods = { + [Key in keyof Endpoints]: ( + ...params: [...Parameters, config?: MethodConfig] + ) => ReturnType; +}; diff --git a/packages/sdk/src/modules/moduleFromEndpoints/utils/getHttpClient.ts b/packages/sdk/src/modules/moduleFromEndpoints/utils/getHttpClient.ts new file mode 100644 index 0000000000..debfb065bc --- /dev/null +++ b/packages/sdk/src/modules/moduleFromEndpoints/utils/getHttpClient.ts @@ -0,0 +1,88 @@ +import { + Options, + IncomingConfig, + BaseConfig, + ComputedConfig, + HTTPClient, +} from "../types"; + +export const getHTTPClient = (options: Options) => { + const { apiUrl, ssrApiUrl, defaultRequestConfig = {} } = options; + + const getUrl = ( + path: string, + method: BaseConfig["method"], + params: any[] + ): string => { + // Determine the base URL based on the environment + const baseUrl = + typeof window === "undefined" ? ssrApiUrl || apiUrl : apiUrl; + + // Ensure the base URL ends with a slash. + // TODO: Update eslint rule to warn on prefer-template instead of error. + // eslint-disable-next-line prefer-template + const normalizedBaseUrl = baseUrl.replace(/\/+$/, "") + "/"; + const url = `${normalizedBaseUrl}${path}`; + + // If there are no query params, return the URL as is + if (method !== "GET") { + return url; + } + + // If there are query params, append them to the URL as `?body=[]` + const serializedParams = encodeURIComponent(JSON.stringify(params)); + + return `${url}?body=${serializedParams}`; + }; + + const getConfig = (config: IncomingConfig): ComputedConfig => { + const { method, headers } = config; + const defaultHeaders = { + "Content-Type": "application/json", + Accept: "application/json", + ...defaultRequestConfig.headers, + }; + const mergedHeaders = { + ...defaultHeaders, + ...headers, + }; + + const computedHeaders: ComputedConfig["headers"] = {}; + Object.entries(mergedHeaders).forEach(([key, value]) => { + computedHeaders[key] = Array.isArray(value) ? value.join(",") : value; + }); + + return { + ...config, + method, + headers: { + ...computedHeaders, + }, + }; + }; + + const defaultHTTPClient: HTTPClient = async ( + url: string, + params: any[], + config?: ComputedConfig + ) => { + const response = await fetch(url, { + ...config, + body: JSON.stringify(params), + }); + + return response.json(); + }; + + return async (methodName: string, params: any[], config?: IncomingConfig) => { + const { httpClient = defaultHTTPClient } = options; + const { method = "POST", headers = {}, ...restConfig } = config ?? {}; + const computedParams = method === "GET" ? [] : params; + + return httpClient( + getUrl(methodName, method, params), + computedParams, + getConfig({ method, headers, ...restConfig }) + ); + }; +}; diff --git a/packages/sdk/src/modules/moduleFromEndpoints/utils/index.ts b/packages/sdk/src/modules/moduleFromEndpoints/utils/index.ts new file mode 100644 index 0000000000..3527cb37fe --- /dev/null +++ b/packages/sdk/src/modules/moduleFromEndpoints/utils/index.ts @@ -0,0 +1,2 @@ +export { getHTTPClient } from "./getHttpClient"; +export { prepareConfig } from "./prepareConfig"; diff --git a/packages/sdk/src/modules/moduleFromEndpoints/utils/prepareConfig.ts b/packages/sdk/src/modules/moduleFromEndpoints/utils/prepareConfig.ts new file mode 100644 index 0000000000..a666113f63 --- /dev/null +++ b/packages/sdk/src/modules/moduleFromEndpoints/utils/prepareConfig.ts @@ -0,0 +1,25 @@ +import { isConfig } from "../consts"; +import { IncomingConfig, MethodConfig } from "../types"; + +/** + * Prepare the config for the request. + * It's used to differentiate the method config from the params. + * + * @example + * Usage + * ```ts + * import { prepareConfig } from "@vue-storefront/sdk"; + * + * const products = sdk.commerce.getProducts(params, prepareConfig({ method: "GET" })); + * ``` + */ +export const prepareConfig = < + CustomConfig extends IncomingConfig = IncomingConfig +>( + requestConfig: CustomConfig +): MethodConfig => { + return { + ...requestConfig, + [isConfig]: true, + }; +}; diff --git a/yarn.lock b/yarn.lock index 737c1b3910..96b8f0c819 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3380,6 +3380,15 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-cli@^6.26.0: version "6.26.0" resolved "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1" @@ -6352,6 +6361,11 @@ follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -7914,6 +7928,14 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + isomorphic-git@^1.21.0: version "1.25.1" resolved "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.1.tgz#120dba31fec0fd45eeb7a82ac6d007581e6d6eb1" @@ -10051,6 +10073,13 @@ node-fetch@2.6.7, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@^4.3.0: version "4.6.1" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" @@ -13742,6 +13771,11 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-fetch@^3.4.1: + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"