From fda8e7c6df6c8de66c8323aa2f0022caa0373ff3 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 7 Sep 2021 14:31:21 +0300 Subject: [PATCH] feat(backend): use helper from @eclipse-che/common instead of NodeRequestError --- packages/common/package.json | 1 + .../src/helpers/__tests__/errors.spec.ts | 200 ++++++++++++------ packages/common/src/helpers/errors.ts | 27 ++- .../__tests__/errors.spec.ts | 7 +- .../src/devworkspace-client/errors.ts | 43 ---- .../services/api/che-api.ts | 9 +- .../services/api/template-api.ts | 10 +- .../services/api/workspace-api.ts | 14 +- .../src/services/kubeclient/helpers/index.ts | 7 +- yarn.lock | 52 ++++- 10 files changed, 235 insertions(+), 135 deletions(-) delete mode 100644 packages/dashboard-backend/src/devworkspace-client/errors.ts diff --git a/packages/common/package.json b/packages/common/package.json index e5cbab508..8f853f9e1 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -16,6 +16,7 @@ "test": "jest" }, "devDependencies": { + "@kubernetes/client-node": "^0.15.1", "@types/jest": "^25.2.3", "axios": "^0.21.1", "axios-mock-adapter": "^1.20.0", diff --git a/packages/common/src/helpers/__tests__/errors.spec.ts b/packages/common/src/helpers/__tests__/errors.spec.ts index cc19fc88e..70149c50a 100644 --- a/packages/common/src/helpers/__tests__/errors.spec.ts +++ b/packages/common/src/helpers/__tests__/errors.spec.ts @@ -11,7 +11,9 @@ */ import axios, { AxiosError, AxiosResponse } from 'axios'; +import { HttpError } from '@kubernetes/client-node'; import AxiosMockAdapter from 'axios-mock-adapter'; +import * as http from 'http'; import { getMessage } from '../errors'; let mockAxios = new AxiosMockAdapter(axios); @@ -42,90 +44,160 @@ describe('Errors helper', () => { const expectedMessage = 'Unexpected error. Check DevTools console and network tabs for more information.' expect(getMessage(notError)).toEqual(expectedMessage); - const expectedOutput = ['Unexpected error:', {'alert': 'Beware of bugs!'}]; + const expectedOutput = ['Unexpected error:', { 'alert': 'Beware of bugs!' }]; expect(console.error).toBeCalledWith(...expectedOutput); }) - it('should return error message if server responds with error', async (done) => { - const message = '500 Internal Server Error.'; + describe('Frontend errors', () => { - mockAxios.onGet('/location/not/found').replyOnce(() => { - return [500, {}, {},] + + it('should return error message if server responds with error', async (done) => { + const message = '"500 Internal Server Error" returned by "/location/".'; + + mockAxios.onGet('/location/').replyOnce(() => { + return [500, {}, {},] + }); + + try { + const data = await axios.get('/location/'); + done.fail(); + } catch (e) { + const err = e as AxiosError; + // provide `statusText` to the response because mocking library cannot do that + (err.response as AxiosResponse).statusText = 'Internal Server Error'; + + expect(getMessage(err)).toEqual(message); + done(); + } }); - try { - const data = await axios.get('/location/not/found'); - done.fail(); - } catch (e) { - const err = e as AxiosError; - // provide `statusText` to the response because mocking library cannot do that - (err.response as AxiosResponse).statusText = 'Internal Server Error'; - - expect(getMessage(err)).toEqual(message); - done(); - } - }); + it('should return error message if server responds with error', async (done) => { + const message = 'The server failed to fulfill a request'; - it('should return error message if server responds with error', async (done) => { - const message = 'The server failed to fulfill a request'; + mockAxios.onGet('/location/').replyOnce(() => { + return [500, { message }, {},] + }); - mockAxios.onGet('/location/not/found').replyOnce(() => { - return [500, {message}, {},] + try { + const data = await axios.get('/location/'); + done.fail(); + } catch (e) { + const err = e as AxiosError; + // provide `statusText` to the response because mocking library cannot do that + (err.response as AxiosResponse).statusText = 'Internal Server Error'; + + expect(getMessage(err)).toEqual(message); + done(); + } }); - try { - const data = await axios.get('/location/not/found'); - done.fail(); - } catch (e) { - const err = e as AxiosError; - // provide `statusText` to the response because mocking library cannot do that - (err.response as AxiosResponse).statusText = 'Internal Server Error'; - - expect(getMessage(err)).toEqual(message); - done(); - } - }); + it('should return error message if network error', async (done) => { + const message = 'Network Error'; - it('should return error message if network error', async (done) => { - const message = 'Network Error'; + mockAxios.onGet('/location/').networkErrorOnce(); - mockAxios.onGet('/location/').networkErrorOnce(); + try { + const data = await axios.get('/location/'); + done.fail(); + } catch (e) { + expect(getMessage(e)).toEqual(message); + done(); + } + }); - try { - const data = await axios.get('/location/'); - done.fail(); - } catch (e) { - expect(getMessage(e)).toEqual(message); - done(); - } - }); + it('should return error message if network timeout', async (done) => { + const message = 'timeout of 0ms exceeded'; + + mockAxios.onGet('/location/').timeoutOnce(); - it('should return error message if network timeout', async (done) => { - const message = 'timeout of 0ms exceeded'; + try { + const data = await axios.get('/location/'); + done.fail(); + } catch (e) { + expect(getMessage(e)).toEqual(message); + done(); + } + }); + + it('should return error message if request aborted', async (done) => { + const message = 'Request aborted'; - mockAxios.onGet('/location/').timeoutOnce(); + mockAxios.onGet('/location/').abortRequestOnce(); + + try { + const data = await axios.get('/location/'); + done.fail(); + } catch (e) { + expect(getMessage(e)).toEqual(message); + done(); + } + }); - try { - const data = await axios.get('/location/'); - done.fail(); - } catch (e) { - expect(getMessage(e)).toEqual(message); - done(); - } }); - it('should return error message if request aborted', async (done) => { - const message = 'Request aborted'; + describe('Backend errors', () => { + + it('should return error message if no response available', () => { + const error: HttpError = { + name: 'HttpError', + message: 'No response available.', + response: { + url: '/location/' + } as http.IncomingMessage, + body: 'No response available', + statusCode: -1, + }; + const expectedMessage = 'no response available due to network issue.'; + expect(getMessage(error)).toEqual(expectedMessage); + + delete error.statusCode; + expect(getMessage(error)).toEqual(expectedMessage); + }); + + it('should return error message if message from K8s is present', () => { + const expectedMessage = 'Error message from K8s.' + const error: HttpError = { + name: 'HttpError', + message: expectedMessage, + response: { + url: '/location/' + } as http.IncomingMessage, + body: { + message: expectedMessage, + }, + statusCode: 500, + }; + expect(getMessage(error)).toEqual(expectedMessage); + }); - mockAxios.onGet('/location/').abortRequestOnce(); + it('should return error message if message in response body is present', () => { + const expectedMessage = 'Error message from K8s.' + const error: HttpError = { + name: 'HttpError', + message: expectedMessage, + response: { + url: '/location/' + } as http.IncomingMessage, + body: expectedMessage, + statusCode: 500, + }; + expect(getMessage(error)).toEqual(expectedMessage); + }); + + it('should return error message if `statusCode` is present', () => { + const expectedMessage = '"500" returned by "/location/".' + const error: HttpError = { + name: 'HttpError', + message: expectedMessage, + response: { + url: '/location/' + } as http.IncomingMessage, + body: undefined, + statusCode: 500, + }; + expect(getMessage(error)).toEqual(expectedMessage); + }); - try { - const data = await axios.get('/location/'); - done.fail(); - } catch (e) { - expect(getMessage(e)).toEqual(message); - done(); - } }); }); diff --git a/packages/common/src/helpers/errors.ts b/packages/common/src/helpers/errors.ts index 44e6cbf2c..8c1f64f21 100644 --- a/packages/common/src/helpers/errors.ts +++ b/packages/common/src/helpers/errors.ts @@ -11,6 +11,7 @@ */ import { AxiosError, AxiosResponse } from 'axios'; +import { HttpError } from "@kubernetes/client-node"; /** * This helper function does its best to get an error message from the provided object. @@ -22,12 +23,30 @@ export function getMessage(error: unknown): string { return 'Unexpected error.'; } + if (isKubeClientError(error)) { + let statusCode = error.statusCode || error.response.statusCode; + if (!statusCode || statusCode === -1) { + return 'no response available due to network issue.'; + } + if (error.body?.message) { + // body is from K8s in JSON form with message present + return error.body.message; + } + if (error.body) { + // pure http response body without message available + return error.body; + } + return `"${statusCode}" returned by "${error.response.url}".`; + } + if (isAxiosError(error) && isAxiosResponse(error.response)) { const response = error.response; if (response.data.message) { return response.data.message; + } else if (response.config.url) { + return `"${response.status} ${response.statusText}" returned by "${response.config.url}".`; } else { - return `${response.status} ${response.statusText}.`; + return `"${response.status} ${response.statusText}".`; } } @@ -63,3 +82,9 @@ export function isAxiosError(object: unknown): object is AxiosError { return object !== undefined && (object as AxiosError).isAxiosError === true; } + +export function isKubeClientError(error: unknown): error is HttpError { + return isError(error) + && (error as HttpError).response !== undefined + && 'body' in (error as HttpError); +} diff --git a/packages/dashboard-backend/src/devworkspace-client/__tests__/errors.spec.ts b/packages/dashboard-backend/src/devworkspace-client/__tests__/errors.spec.ts index 8f1a30906..1f391e6a4 100644 --- a/packages/dashboard-backend/src/devworkspace-client/__tests__/errors.spec.ts +++ b/packages/dashboard-backend/src/devworkspace-client/__tests__/errors.spec.ts @@ -15,8 +15,7 @@ import { conditionalTest, isIntegrationTestEnabled } from './utils/suite'; import { createKubeConfig } from './utils/helper'; import { fail } from 'assert'; import * as k8s from '@kubernetes/client-node'; -import { NodeRequestError } from '../errors'; -import { HttpError } from '@kubernetes/client-node'; +import { helpers } from '@eclipse-che/common'; describe('Kubernetes API integration testing against cluster', () => { @@ -33,8 +32,8 @@ describe('Kubernetes API integration testing against cluster', () => { ); fail('request to non-existing Custom API should fail'); } catch (e) { - let error = new NodeRequestError('unable get non-existing', (e as HttpError)); - expect(error.message).toBe('unable get non-existing: 404 page not found\n'); + let errorMessage = 'unable get non-existing: ' + helpers.errors.getMessage(e); + expect(errorMessage).toBe('unable get non-existing: 404 page not found\n'); } done(); }, 1000); diff --git a/packages/dashboard-backend/src/devworkspace-client/errors.ts b/packages/dashboard-backend/src/devworkspace-client/errors.ts deleted file mode 100644 index 4bbd1f5e7..000000000 --- a/packages/dashboard-backend/src/devworkspace-client/errors.ts +++ /dev/null @@ -1,43 +0,0 @@ -/********************************************************************** - * Copyright (c) 2021 Red Hat, Inc. - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - ***********************************************************************/ - -import { HttpError } from "@kubernetes/client-node/dist/gen/api/apis"; - -/** - * Wraps errors got from Kubernetes client and provides the most user-friendly error message it can. - */ -export class NodeRequestError extends Error { - httpError: HttpError; - message: string; - - constructor(prefix: string, error: HttpError) { - super(); - this.httpError = error; - let statusCode = error.statusCode; - let response = error.response; - if ((!statusCode || statusCode === -1)) { - this.message = prefix + ': no response available due network issue.'; - } else if (error.body) { - if (error.body.message) { - // body is from K8s in JSON form with message present - this.message = prefix + ': ' + error.body.message; - } else { - // pure http response body without message available - this.message = prefix + ': ' + error.body; - } - } else { - // try to evaluate status code from response if it's missing on the root level - if (!statusCode && response && response.statusCode) { - statusCode = response.statusCode; - } - this.message = `${prefix}: "${statusCode}" returned by "${error.response.url}"."`; - } - } -} diff --git a/packages/dashboard-backend/src/devworkspace-client/services/api/che-api.ts b/packages/dashboard-backend/src/devworkspace-client/services/api/che-api.ts index dfde5d595..8bf245cf0 100644 --- a/packages/dashboard-backend/src/devworkspace-client/services/api/che-api.ts +++ b/packages/dashboard-backend/src/devworkspace-client/services/api/che-api.ts @@ -15,8 +15,7 @@ import { ICheApi, } from '../../types'; import { projectApiGroup, projectRequestResources, projectResources, } from '../../const'; import { namespaceModel, projectRequestModel } from '../../const/models'; import { findApi } from '../helpers'; -import { NodeRequestError } from '../../errors'; -import { HttpError } from '@kubernetes/client-node'; +import { helpers } from '@eclipse-che/common'; /** * @deprecated Che Server started to provide rest endpoint to get namespace prepared. @@ -48,7 +47,7 @@ export class CheApi implements ICheApi { } } } catch (e) { - return Promise.reject(new NodeRequestError('unable to init project', (e as HttpError))); + throw new Error('unable to init project: ' + helpers.errors.getMessage(e)); } } @@ -92,7 +91,7 @@ export class CheApi implements ICheApi { projectRequestModel(namespace) ); } catch (e) { - return Promise.reject(new NodeRequestError('unable to create project', (e as HttpError))); + throw new Error('unable to create project: ' + helpers.errors.getMessage(e)); } } @@ -102,7 +101,7 @@ export class CheApi implements ICheApi { namespaceModel(namespace) ); } catch (e) { - return Promise.reject(new NodeRequestError('unable to create namespace', (e as HttpError))); + throw new Error('unable to create namespace: ' + helpers.errors.getMessage(e)); } } } diff --git a/packages/dashboard-backend/src/devworkspace-client/services/api/template-api.ts b/packages/dashboard-backend/src/devworkspace-client/services/api/template-api.ts index d319d3d51..63665c811 100644 --- a/packages/dashboard-backend/src/devworkspace-client/services/api/template-api.ts +++ b/packages/dashboard-backend/src/devworkspace-client/services/api/template-api.ts @@ -11,7 +11,7 @@ */ import * as k8s from '@kubernetes/client-node'; -import { NodeRequestError } from '../../errors'; +import { helpers } from '@eclipse-che/common'; import { devWorkspaceApiGroup, devworkspaceTemplateSubresource, devworkspaceVersion } from '../../const'; import { IDevWorkspaceTemplate, IDevWorkspaceTemplateApi, } from '../../types'; import { HttpError } from '@kubernetes/client-node'; @@ -33,7 +33,7 @@ export class DevWorkspaceTemplateApi implements IDevWorkspaceTemplateApi { ); return (resp.body as any).items as IDevWorkspaceTemplate[]; } catch (e) { - return Promise.reject(new NodeRequestError('unable to list devworkspace templates', (e as HttpError))); + throw new Error('unable to list devworkspace templates: ' + helpers.errors.getMessage(e)); } } @@ -48,7 +48,7 @@ export class DevWorkspaceTemplateApi implements IDevWorkspaceTemplateApi { ); return resp.body as IDevWorkspaceTemplate; } catch (e) { - return Promise.reject(new NodeRequestError(`unable to get devworkspace "${namespace}/${name}"`, (e as HttpError))); + throw new Error(`unable to get devworkspace "${namespace}/${name}": ` + helpers.errors.getMessage(e)); } } @@ -69,7 +69,7 @@ export class DevWorkspaceTemplateApi implements IDevWorkspaceTemplateApi { ); return resp.body as IDevWorkspaceTemplate; } catch (e) { - return Promise.reject(new NodeRequestError('unable to create DevWorkspaceTemplate', (e as HttpError))); + throw new Error('unable to create DevWorkspaceTemplate: ' + helpers.errors.getMessage(e)); } } @@ -83,7 +83,7 @@ export class DevWorkspaceTemplateApi implements IDevWorkspaceTemplateApi { name ); } catch (e) { - return Promise.reject(new NodeRequestError('unable to delete devworkspace template', (e as HttpError))); + throw new Error('unable to delete devworkspace template: ' + helpers.errors.getMessage(e)); } } } diff --git a/packages/dashboard-backend/src/devworkspace-client/services/api/workspace-api.ts b/packages/dashboard-backend/src/devworkspace-client/services/api/workspace-api.ts index 51028999d..ef8ff541d 100644 --- a/packages/dashboard-backend/src/devworkspace-client/services/api/workspace-api.ts +++ b/packages/dashboard-backend/src/devworkspace-client/services/api/workspace-api.ts @@ -26,7 +26,7 @@ import { } from '../../const'; import { devfileToDevWorkspace } from '../converters'; -import { NodeRequestError } from '../../errors'; +import { helpers } from '@eclipse-che/common'; import { HttpError } from '@kubernetes/client-node'; export class DevWorkspaceApi implements IDevWorkspaceApi { @@ -48,7 +48,7 @@ export class DevWorkspaceApi implements IDevWorkspaceApi { ); return resp.body as IDevWorkspaceList; } catch (e) { - throw new NodeRequestError('unable to list devworkspaces', (e as HttpError)); + throw new Error('unable to list devworkspaces: ' + helpers.errors.getMessage(e)); } } @@ -66,7 +66,7 @@ export class DevWorkspaceApi implements IDevWorkspaceApi { ); return resp.body as IDevWorkspace; } catch (e) { - throw new NodeRequestError(`unable to get devworkspace ${namespace}/${name}`, (e as HttpError)); + throw new Error(`unable to get devworkspace ${namespace}/${name}: ` + helpers.errors.getMessage(e)); } } @@ -87,7 +87,7 @@ export class DevWorkspaceApi implements IDevWorkspaceApi { ); return resp.body as IDevWorkspace; } catch (e) { - throw new NodeRequestError('unable to create devworkspace', (e as HttpError)); + throw new Error('unable to create devworkspace: ' + helpers.errors.getMessage(e)); } } @@ -117,7 +117,7 @@ export class DevWorkspaceApi implements IDevWorkspaceApi { ); return resp.body as IDevWorkspace; } catch (e) { - throw new NodeRequestError('unable to update devworkspace', (e as HttpError)); + throw new Error('unable to update devworkspace: ' + helpers.errors.getMessage(e)); } } @@ -131,7 +131,7 @@ export class DevWorkspaceApi implements IDevWorkspaceApi { name ); } catch (e) { - throw new NodeRequestError(`unable to delete devworkspace ${namespace}/${name}`, (e as HttpError)); + throw new Error(`unable to delete devworkspace ${namespace}/${name}: ` + helpers.errors.getMessage(e)); } } @@ -166,7 +166,7 @@ export class DevWorkspaceApi implements IDevWorkspaceApi { ); return resp.body as IDevWorkspace; } catch (e) { - throw new NodeRequestError(`unable to patch devworkspace`, (e as HttpError)); + throw new Error(`unable to patch devworkspace: ` + helpers.errors.getMessage(e)); } } diff --git a/packages/dashboard-backend/src/services/kubeclient/helpers/index.ts b/packages/dashboard-backend/src/services/kubeclient/helpers/index.ts index 5dc84883a..07300fa37 100644 --- a/packages/dashboard-backend/src/services/kubeclient/helpers/index.ts +++ b/packages/dashboard-backend/src/services/kubeclient/helpers/index.ts @@ -28,14 +28,13 @@ async function findApi(apisApi: k8s.ApisApi, apiName: string, version?: string): const resp = await apisApi.getAPIVersions(); const groups = resp.body.groups; const filtered = - groups.filter((apiGroup: k8s.V1APIGroup) => { + groups.some((apiGroup: k8s.V1APIGroup) => { if (version) { return apiGroup.name === apiName && apiGroup.versions.filter(versionGroup => versionGroup.version === version).length > 0; } return apiGroup.name === apiName; - }) - .length > 0; - return Promise.resolve(filtered); + }); + return filtered; } catch (e) { return false; } diff --git a/yarn.lock b/yarn.lock index 8c410f4a1..90d9f35bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -646,6 +646,34 @@ underscore "^1.9.1" ws "^7.3.1" +"@kubernetes/client-node@^0.15.1": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@kubernetes/client-node/-/client-node-0.15.1.tgz#617357873d20de348a99227f3b699c2adb765152" + integrity sha512-j3o5K4TWkdrX2yDndbEZcDxhgea6O2JKnesWoYCJ64WtMn2GbQXGBOnkn2i0WT2MugGbLR+qCm8Y3oHWBApaTA== + dependencies: + "@types/js-yaml" "^4.0.1" + "@types/node" "^10.12.0" + "@types/request" "^2.47.1" + "@types/stream-buffers" "^3.0.3" + "@types/tar" "^4.0.3" + "@types/underscore" "^1.8.9" + "@types/ws" "^6.0.1" + byline "^5.0.0" + execa "5.0.0" + isomorphic-ws "^4.0.1" + js-yaml "^4.1.0" + jsonpath-plus "^0.19.0" + openid-client "^4.1.1" + request "^2.88.0" + rfc4648 "^1.3.0" + shelljs "^0.8.4" + stream-buffers "^3.0.2" + tar "^6.0.2" + tmp-promise "^3.0.2" + tslib "^1.9.3" + underscore "^1.9.1" + ws "^7.3.1" + "@lerna/add@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f" @@ -1922,6 +1950,11 @@ resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.7.tgz#330c5d97a3500e9c903210d6e49f02964af04a0e" integrity sha512-S6+8JAYTE1qdsc9HMVsfY7+SgSuUU/Tp6TYTmITW0PZxiyIMvol3Gy//y69Wkhs0ti4py5qgR3uZH6uz/DNzJQ== +"@types/js-yaml@^4.0.1": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.3.tgz#9f33cd6fbf0d5ec575dc8c8fc69c7fec1b4eb200" + integrity sha512-5t9BhoORasuF5uCPr+d5/hdB++zRFUTMIZOzbNkr+jZh3yQht4HYbRDyj9fY8n2TZT30iW9huzav73x4NikqWg== + "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -5565,6 +5598,21 @@ execa@1.0.0, execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376" + integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + execa@^4.0.0, execa@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" @@ -8317,7 +8365,7 @@ js-yaml@^3.13.1, js-yaml@^3.7.0: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.0.0: +js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -12254,7 +12302,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@^0.8.2: +shelljs@^0.8.2, shelljs@^0.8.4: version "0.8.4" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==