From 149b0a19c39adf0d1e169b85245e29fcc6298952 Mon Sep 17 00:00:00 2001 From: Ben Ilegbodu Date: Thu, 1 Mar 2018 10:37:54 -0800 Subject: [PATCH] feat(request): Add support for SDK request method (#8) Adds `sdk.request(url, fetchOptions)` that can be used to make any API request without having a convience method for it. It reads in the `token` & `baseUrl` properties that are passed in while creating the `sdk` object in order to generate the full URL. Sample usage: ```js const eventbrite = require('eventbrite'); // Create configured Eventbrite SDK const sdk = eventbrite({token: 'OATH_TOKEN_HERE'}); // See: https://www.eventbrite.com/developer/v3/endpoints/users/#ebapi-get-users-id sdk.request('/users/me').then(res => { // handle response data }); ``` Also: - Renamed package from `brite-rest` to `eventbrite` - Updated README to reflect sample usage - Moved `index.spec.ts` into `__tests__` folder - Had to disable `camelcase` ESLint rule because Prettier was unquoting non-conforming property names and the `camelcase` rule doesn't have fix functionality for prettier-eslint to fix - Added a bunch of `eslint-plugin-typescript` rules NOTE: Per https://www.eventbrite.com/developer/v3/api_overview/authentication/#ebapi-authenticating-requests we should move the token into an authorization header instead of a query parameter, which is preferred. It'd also allow us to remove the `url-lib` dependency (and the one-off Typescript definition file). Fixes #6 --- README.md | 16 ++- definitions/url-lib.d.ts | 12 ++ package.json | 20 ++- src/.eslintrc.json | 14 +- src/__tests__/__fixtures__/index.ts | 21 +++ src/__tests__/index.spec.ts | 90 ++++++++++++ src/__tests__/request.spec.ts | 216 ++++++++++++++++++++++++++++ src/__tests__/utils.ts | 36 +++++ src/index.spec.ts | 23 --- src/index.ts | 25 +++- src/request.ts | 127 ++++++++++++++++ src/types.ts | 24 ++++ yarn.lock | 16 ++- 13 files changed, 603 insertions(+), 37 deletions(-) create mode 100644 definitions/url-lib.d.ts create mode 100644 src/__tests__/__fixtures__/index.ts create mode 100644 src/__tests__/index.spec.ts create mode 100644 src/__tests__/request.spec.ts create mode 100644 src/__tests__/utils.ts delete mode 100644 src/index.spec.ts create mode 100644 src/request.ts create mode 100644 src/types.ts diff --git a/README.md b/README.md index 5d30ed0..f760749 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,19 @@ Coming soon... ## Usage -Coming soon... +```js +const eventbrite = require('eventbrite'); + +// Create configured Eventbrite SDK +const sdk = eventbrite({token: 'OATH_TOKEN_HERE'}); + +// See: https://www.eventbrite.com/developer/v3/endpoints/users/#ebapi-get-users-id +sdk.request('/users/me').then(res => { + // handle response data +}); +``` + +Read more on [getting a token](https://www.eventbrite.com/developer/v3/api_overview/authentication/#ebapi-getting-a-token). ## Contributing @@ -16,7 +28,7 @@ Coming soon... ## Project philosophy -We take the stability of this SDK **very** seriously. `brite-rest` follows the [SemVer](http://semver.org/) standard for versioning. +We take the stability of this SDK **very** seriously. `eventbrite` follows the [SemVer](http://semver.org/) standard for versioning. ## License diff --git a/definitions/url-lib.d.ts b/definitions/url-lib.d.ts new file mode 100644 index 0000000..a87241f --- /dev/null +++ b/definitions/url-lib.d.ts @@ -0,0 +1,12 @@ +declare module "url-lib" { + export function formatQuery(queryParams: {}): string; + export function formatQuery(queryParamsList: Array<{}>): string; + + export function formatUrl(urlPath: string, queryParams: {}): string; + export function formatUrl( + urlPath: string, + queryParamsList: Array<{}> + ): string; + + export function parseQuery(serializedQuery: string): {}; +} diff --git a/package.json b/package.json index ca8f820..beca79f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,14 @@ "jsnext:main": "lib/esm/index.js", "browser": "dist/brite-rest.js", "types": "lib/cjs/index.d.ts", - "keywords": ["rest", "api", "sdk", "events", "tickets", "eventbrite"], + "keywords": [ + "rest", + "api", + "sdk", + "events", + "tickets", + "eventbrite" + ], "repository": { "type": "git", "url": "https://github.com/eventbrite/eventbrite-sdk-javascript.git" @@ -33,11 +40,15 @@ "validate": "npm-run-all --parallel check:static test:ci" }, "lint-staged": { - "*.{ts,js}": ["yarn format", "git add"] + "*.{ts,js}": [ + "yarn format", + "git add" + ] }, "dependencies": { "isomorphic-fetch": "^2.2.1", - "lodash": "^4.17.5" + "lodash": "^4.17.5", + "url-lib": "^2.0.2" }, "resolutions": { "babel-core": "^7.0.0-bridge.0" @@ -52,11 +63,12 @@ "@types/isomorphic-fetch": "^0.0.34", "@types/jest": "^22.1.3", "@types/lodash": "^4.14.104", + "@types/node": "^9.4.6", "babel-eslint": "^7.0.0", "eslint": "^3.0.0", "eslint-config-eventbrite": "^4.1.0", "eslint-plugin-import": "^2.0.0", - "eslint-plugin-typescript": "^0.8.1", + "eslint-plugin-typescript": "^0.9.0", "husky": "^0.14.3", "jest": "^22.4.0", "lint-staged": "^6.1.0", diff --git a/src/.eslintrc.json b/src/.eslintrc.json index 907f0c3..7ebbce9 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -8,7 +8,19 @@ "parser": "typescript-eslint-parser", "plugins": ["typescript"], "rules": { - "no-undef": "off" + "no-undef": "off", + "camelcase": "off", + "typescript/adjacent-overload-signatures": "error", + "typescript/class-name-casing": "error", + "typescript/interface-name-prefix": "error", + "typescript/member-delimiter-style": "error", + "typescript/no-unused-vars": "error", + "typescript/member-ordering": "error", + "typescript/no-angle-bracket-type-assertion": "error", + "typescript/no-array-constructor": "error", + "typescript/no-empty-interface": "error", + "typescript/no-use-before-define": "error", + "typescript/type-annotation-spacing": "error" }, "settings": { "import/resolver": { diff --git a/src/__tests__/__fixtures__/index.ts b/src/__tests__/__fixtures__/index.ts new file mode 100644 index 0000000..c16640f --- /dev/null +++ b/src/__tests__/__fixtures__/index.ts @@ -0,0 +1,21 @@ +export const MOCK_USERS_ME_RESPONSE_DATA = { + emails: [ + { + email: 'engineer@eventbrite.com', + verified: true, + primary: true, + }, + ], + id: '142429416488', + name: 'Eventbrite Engineer', + first_name: 'Eventbrite', + last_name: 'Engineer', + is_public: false, + image_id: null as string, +}; + +export const MOCK_ERROR_RESPONSE_DATA = { + status_code: 400, + error: 'INVALID_TEST', + error_description: 'This is an invalid test', +}; diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts new file mode 100644 index 0000000..f132c46 --- /dev/null +++ b/src/__tests__/index.spec.ts @@ -0,0 +1,90 @@ +import eventbrite from '../'; +import { + mockFetch, + getMockFetch, + restoreMockFetch, + getMockResponse +} from './utils'; +import {MOCK_USERS_ME_RESPONSE_DATA} from './__fixtures__'; + +describe('configurations', () => { + it('does not error when creating sdk object w/o configuration', () => { + expect(() => eventbrite()).not.toThrow(); + }); +}); + +describe('request', () => { + const MOCK_TOKEN = 'MOCK_TOKEN'; + const MOCK_BASE_URL = '/api/v3'; + + beforeEach(() => { + mockFetch(getMockResponse(MOCK_USERS_ME_RESPONSE_DATA)); + }); + + afterEach(() => { + restoreMockFetch(); + }); + + it('makes request to API base url default w/ no token when no configuration is specified', async () => { + const {request} = eventbrite(); + + await expect(request('/users/me/')).resolves.toEqual( + MOCK_USERS_ME_RESPONSE_DATA + ); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + expect(getMockFetch()).toHaveBeenCalledWith( + 'https://www.eventbriteapi.com/v3/users/me/', + expect.objectContaining({}) + ); + }); + + it('makes request to API base url override w/ specified token', async () => { + const {request} = eventbrite({ + token: MOCK_TOKEN, + baseUrl: MOCK_BASE_URL, + }); + + await expect(request('/users/me/')).resolves.toEqual( + MOCK_USERS_ME_RESPONSE_DATA + ); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + expect(getMockFetch()).toHaveBeenCalledWith( + `${MOCK_BASE_URL}/users/me/?token=${MOCK_TOKEN}`, + expect.objectContaining({}) + ); + }); + + it('properly appends token to API URL when endpoint already contains query parameters', async () => { + const {request} = eventbrite({ + token: MOCK_TOKEN, + }); + + await expect( + request('/users/me/orders/?time_filter=past') + ).resolves.toEqual(MOCK_USERS_ME_RESPONSE_DATA); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + expect(getMockFetch()).toHaveBeenCalledWith( + `https://www.eventbriteapi.com/v3/users/me/orders/?time_filter=past&token=${MOCK_TOKEN}`, + expect.objectContaining({}) + ); + }); + + it('properly passes through request options', async () => { + const {request} = eventbrite(); + const requestOptions = { + method: 'POST', + body: JSON.stringify({plan: 'package2'}), + }; + + await request('/users/:id/assortment/', requestOptions); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + expect(getMockFetch()).toHaveBeenCalledWith( + 'https://www.eventbriteapi.com/v3/users/:id/assortment/', + expect.objectContaining(requestOptions) + ); + }); +}); diff --git a/src/__tests__/request.spec.ts b/src/__tests__/request.spec.ts new file mode 100644 index 0000000..70984c2 --- /dev/null +++ b/src/__tests__/request.spec.ts @@ -0,0 +1,216 @@ +import request, {checkStatus, fetchJSON, catchStatusError} from '../request'; +import { + mockFetch, + getMockFetch, + restoreMockFetch, + getMockResponse +} from './utils'; +import { + MOCK_USERS_ME_RESPONSE_DATA, + MOCK_ERROR_RESPONSE_DATA +} from './__fixtures__'; + +const TEST_URL = 'https://www.eventbriteapi.com/v3/users/me/'; + +const getSuccessfulCodeRes = () => getMockResponse(MOCK_USERS_ME_RESPONSE_DATA); +const getUnsuccessfulCodeRes = () => + getMockResponse(MOCK_ERROR_RESPONSE_DATA, {status: 400}); + +describe('checkStatus', () => { + describe('on receiving an invalid status', () => { + const response = getUnsuccessfulCodeRes(); + + it('returns a rejected promise', async () => { + await expect(checkStatus(response)).rejects.toBe(response); + }); + }); + + describe('on receiving a valid status', () => { + it('returns a fulfilled promise', async () => { + const response = getSuccessfulCodeRes(); + + await expect(checkStatus(response)).resolves.toBe(response); + }); + }); +}); + +describe('fetchJSON', () => { + afterEach(() => { + restoreMockFetch(); + }); + + describe('on receiving successful status code', () => { + beforeEach(() => { + mockFetch(getSuccessfulCodeRes()); + }); + + it('calls fetch with appropriate defaults', async () => { + await fetchJSON(TEST_URL); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + expect(getMockFetch()).toHaveBeenCalledWith( + TEST_URL, + expect.objectContaining({ + credentials: 'same-origin', + }) + ); + }); + + it('adds "application/json" content type when method is not GET', async () => { + await fetchJSON(TEST_URL, {method: 'POST', body: '{}'}); + + expect(getMockFetch()).toHaveBeenCalledWith( + TEST_URL, + expect.objectContaining({ + credentials: 'same-origin', + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + }), + method: 'POST', + body: '{}', + }) + ); + }); + + it('respects overrides in options', async () => { + await fetchJSON(TEST_URL, {credentials: 'omit'}); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + expect(getMockFetch()).toHaveBeenCalledWith( + TEST_URL, + expect.objectContaining({ + credentials: 'omit', + }) + ); + }); + + it('respects overrides in option headers', async () => { + await fetchJSON(TEST_URL, { + headers: { + 'X-TEST': 'testHeader', + 'X-CSRFToken': 'testCSRF', + 'Content-Type': 'application/xml', + }, + }); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + expect(getMockFetch()).toHaveBeenCalledWith( + TEST_URL, + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-CSRFToken': 'testCSRF', + 'X-TEST': 'testHeader', + 'Content-Type': 'application/xml', + }), + }) + ); + }); + + it('merges overrides with defaults in option headers', async () => { + await fetchJSON(TEST_URL, { + headers: { + 'X-TEST': 'testHeader', + 'X-CSRFToken': 'testCSRF', + }, + method: 'POST', + body: '{}', + }); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + expect(getMockFetch()).toHaveBeenCalledWith( + TEST_URL, + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-CSRFToken': 'testCSRF', + 'X-TEST': 'testHeader', + 'Content-Type': 'application/json', + }), + method: 'POST', + body: '{}', + }) + ); + }); + + it('should parse the response JSON', async () => { + await expect(fetchJSON(TEST_URL)).resolves.toEqual( + MOCK_USERS_ME_RESPONSE_DATA + ); + }); + }); + + describe('on receiving an unsuccessful status code', () => { + beforeEach(() => { + mockFetch(getUnsuccessfulCodeRes()); + }); + + it('should throw an error', async () => { + await expect(fetchJSON(TEST_URL)).rejects.toEqual( + expect.objectContaining({status: 400}) + ); + }); + }); +}); + +describe('catchStatusError', () => { + describe('when response is invalid JSON', () => { + it('should reject without parsed errors, only response', async () => { + const response = new Response('{sa;dfsdfi'); + + await expect(catchStatusError(response)).rejects.toEqual({ + response, + }); + }); + }); + + describe('when response is valid JSON', () => { + it('should reject with parsed errors', async () => { + const response = getUnsuccessfulCodeRes(); + + await expect(catchStatusError(response)).rejects.toEqual({ + response, + parsedError: { + error: 'INVALID_TEST', + description: 'This is an invalid test', + }, + }); + }); + }); +}); + +describe('request', () => { + afterEach(() => { + restoreMockFetch(); + }); + + describe('when no status error', () => { + beforeEach(() => { + mockFetch(getSuccessfulCodeRes()); + }); + + it('calls fetch and return JSON data', async () => { + await expect(request(TEST_URL)).resolves.toEqual( + MOCK_USERS_ME_RESPONSE_DATA + ); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + }); + }); + + describe('when there is a status error', () => { + it('calls fetch and rejects with parsed error', async () => { + const response = getUnsuccessfulCodeRes(); + + mockFetch(response); + + await expect(request(TEST_URL)).rejects.toEqual({ + response, + parsedError: { + error: 'INVALID_TEST', + description: 'This is an invalid test', + }, + }); + + expect(getMockFetch()).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/__tests__/utils.ts b/src/__tests__/utils.ts new file mode 100644 index 0000000..68ecbc4 --- /dev/null +++ b/src/__tests__/utils.ts @@ -0,0 +1,36 @@ +// NOTE: Typescript is pretty unhappy with us mocking and reading from `global.fetch` since +// `isomorphic-fetch` doesn't properly describe that `global.fetch` is now a thing. It also +// does understand what `jest.spyOn` is doing to `global.fetch`. Therefore we have some +// Typescript definitions along with helpers to consolidate everything into a single place. + +declare type Fetch = (url: string, options: RequestInit) => Promise; + +// A fetch function that also has a `mockRestore` function property as well on it +interface MockedFetch extends Fetch { + mockRestore: () => void; +} +declare global { + namespace NodeJS { + interface Global { + // eslint-disable-next-line no-unused-vars + fetch: MockedFetch; + } + } +} + +export const getMockResponse = ( + mockResponseData: any = {}, + responseConfig: ResponseInit = {status: 200} +) => new Response(JSON.stringify(mockResponseData), responseConfig); + +export const mockFetch = (response: Response = getMockResponse()) => { + jest + .spyOn(global, 'fetch') + .mockImplementation(() => Promise.resolve(response)); +}; + +export const getMockFetch = () => global.fetch; + +export const restoreMockFetch = () => { + global.fetch.mockRestore(); +}; diff --git a/src/index.spec.ts b/src/index.spec.ts deleted file mode 100644 index 9a16f6a..0000000 --- a/src/index.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import briteRest from './'; - -describe('configurations', () => { - it('does not error when creating sdk object w/o token', () => { - expect(() => briteRest()).not.toThrow(); - }); -}); - -// TODO: remove when build process has stabilized -describe('build functionality', () => { - it('should compile while using async await', async () => { - const result = await new Promise((resolve) => resolve(1)); - - expect(result).toEqual(1); - }); - - it('should work with spread operator', () => { - const parent = {first: 1}; - const child = {...parent, second: 2}; - - expect(child).toEqual({first: 1, second: 2}); - }); -}); diff --git a/src/index.ts b/src/index.ts index 2a077fc..32350ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,24 @@ -export interface Sdk {} +/// -const briteRest = (): Sdk => ({}); +import {formatUrl} from 'url-lib'; +import {Sdk, SdkConfig} from './types'; +import request from './request'; -export default briteRest; +const DEFAULT_API_URL = 'https://www.eventbriteapi.com/v3'; + +const eventbrite = ({ + baseUrl = DEFAULT_API_URL, + token, +}: SdkConfig = {}): Sdk => ({ + request: (endpoint, options?) => { + let url = `${baseUrl}${endpoint}`; + + if (token) { + url = formatUrl(url, {token}); + } + + return request(url, options); + }, +}); + +export default eventbrite; diff --git a/src/request.ts b/src/request.ts new file mode 100644 index 0000000..32532a4 --- /dev/null +++ b/src/request.ts @@ -0,0 +1,127 @@ +import {isPlainObject} from 'lodash'; +import {JSONResponseData, ParsedResponseError} from './types'; +import 'isomorphic-fetch'; + +/** + * Return a promise that is resolved or rejected depending on the response's + * status code. + */ +export const checkStatus = (res: Response): Promise => { + if (res.status >= 300) { + return Promise.reject(res); + } + return Promise.resolve(res); +}; + +/** + * Calls fetch on provided url with default options necessary for interacting + * with our JSON API. Parses the JSON, provides appropriate headers, and asserts + * a valid status from the server. + */ +export const fetchJSON = ( + url: string, + {headers, method, mode, ...options}: RequestInit = {} +): Promise<{}> => { + let fetchHeaders = headers as HeadersInit; + + if (method !== 'GET') { + fetchHeaders = { + 'Content-Type': 'application/json', + ...headers, + }; + } + + const fetchOptions = { + method, + mode, + headers: fetchHeaders, + credentials: mode === 'cors' ? 'include' : 'same-origin', + ...options, + } as RequestInit; + + return fetch(url, fetchOptions) + .then(checkStatus) + .then((res: Response) => { + let resJSON = {}; + + if ('json' in res) { + resJSON = res.json(); + } + + return resJSON; + }); +}; + +const hasArgumentsError = (responseData: JSONResponseData): boolean => + isPlainObject(responseData['error_detail']) && + isPlainObject(responseData['error_detail']['ARGUMENTS_ERROR']); + +/** + * Parse v3 errors into an array of objects representing the errors returned by + * the API. The format of the parsed errors looks like: + * + * { + * status_code: 400, + * error: 'ERROR_CODE', + * description: 'Description of the error + * } + * + * An ARGUMENTS_ERROR looks like: + * + * { + * error: 'ARGUMENTS_ERROR', + * description: 'Some of the fields were invalid or something', + * argumentErrors: { + * attr1: ['INVALID'], + * attr2: ['This field is required'] + * } + * } + * + */ +export const parseError = ( + responseData: JSONResponseData +): ParsedResponseError => { + if (!responseData.error) { + // Weird error format, return null + return null; + } + + let error = { + error: responseData.error, + description: responseData['error_description'], + } as ParsedResponseError; + + if (hasArgumentsError(responseData)) { + error.argumentErrors = responseData['error_detail']['ARGUMENTS_ERROR']; + } + + return error; +}; + +/** + * Designed to work with `checkStatus`, or any function that + * raises an error on an invalid status. The error raised should have a `response` + * property with the original response object. + * + * Example usage: + * + * fetchJSON('/api/v3/test/path', {'body': someData}) + * .catch(catchStatusError) + * .catch(({response, parsedError}) => doSomethingOnError()) + * .then(doSomethingOnSuccess); + */ +export const catchStatusError = (response: Response): Promise => + new Promise((resolve, reject) => { + response + .json() + .then((responseData) => parseError(responseData)) + .then((parsedError) => reject({response, parsedError})) + .catch(() => reject({response})); + }); + +/** + * fetchV3 is a simple wrapper for http/fetchJSON that parses v3 errors received + * by the API. + */ +export default (url: string, options?: RequestInit): Promise<{}> => + fetchJSON(url, options).catch(catchStatusError); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..bc95fdd --- /dev/null +++ b/src/types.ts @@ -0,0 +1,24 @@ +export interface SdkConfig { + token?: string; + baseUrl?: string; +} +export interface Sdk { + request: (apiPath: string, options?: RequestInit) => Promise<{}>; +} + +export interface ArgumentErrors { + [key: string]: [string]; +} +export interface ParsedResponseError { + error: string; + description: string; + argumentErrors: ArgumentErrors; +} +export interface JSONResponseData { + error?: string; + error_description?: string; + error_detail?: { + ARGUMENTS_ERROR?: ArgumentErrors; + [propName: string]: any; + }; +} diff --git a/yarn.lock b/yarn.lock index abbd40d..bd777c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -489,6 +489,10 @@ version "4.14.104" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" +"@types/node@^9.4.6": + version "9.4.6" + resolved "https://registry.npmjs.org/@types/node/-/node-9.4.6.tgz#d8176d864ee48753d053783e4e463aec86b8d82e" + abab@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -1470,9 +1474,9 @@ eslint-plugin-import@^2.0.0: minimatch "^3.0.3" read-pkg-up "^2.0.0" -eslint-plugin-typescript@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-typescript/-/eslint-plugin-typescript-0.8.1.tgz#e5b2d18e744a04528eac58b099fe1032c4d744ff" +eslint-plugin-typescript@^0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/eslint-plugin-typescript/-/eslint-plugin-typescript-0.9.0.tgz#9b7075bc1560e879ae3403030564026106a1184d" dependencies: requireindex "~1.1.0" @@ -3942,7 +3946,7 @@ require-uncached@^1.0.2, require-uncached@^1.0.3: requireindex@~1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162" + resolved "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162" reserved-words@^0.1.2: version "0.1.2" @@ -4512,6 +4516,10 @@ unicode-property-aliases-ecmascript@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.3.tgz#ac3522583b9e630580f916635333e00c5ead690d" +url-lib@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/url-lib/-/url-lib-2.0.2.tgz#26708f42f4c23ec821e3617044fab22e250e7afc" + user-home@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"