Browse Source

build(dev): Add frontend-only build for local development (#19236)

Adds support for frontend-only local development against a remote API. `yarn dev-ui` to start-up the local dev server that proxies to `sentry.io`.
Billy Vong 4 years ago
parent
commit
b650b6905c

+ 2 - 2
package.json

@@ -59,7 +59,6 @@
     "clipboard": "^1.7.1",
     "color": "^3.1.0",
     "compression-webpack-plugin": "3.1.0",
-    "copy-webpack-plugin": "^5.0.3",
     "core-js": "^3.2.1",
     "create-react-class": "^15.6.2",
     "crypto-js": "4.0.0",
@@ -153,6 +152,7 @@
     "enzyme-to-json": "3.4.3",
     "eslint": "5.11.1",
     "eslint-config-sentry-app": "1.37.0",
+    "html-webpack-plugin": "^4.3.0",
     "jest": "24.9.0",
     "jest-canvas-mock": "^2.2.0",
     "jest-junit": "^9.0.0",
@@ -189,7 +189,7 @@
     "lint": "yarn eslint tests/js src/sentry/static/sentry/app --ext .js,.jsx,.ts,.tsx",
     "lint:css": "yarn stylelint 'src/sentry/static/sentry/app/**/*.[jt]sx'",
     "dev": "(yarn check --verify-tree || yarn install --check-files) && sentry devserver",
-    "dev-server": "webpack-dev-server",
+    "dev-ui": "SENTRY_UI_DEV_ONLY=1 SENTRY_WEBPACK_PROXY_PORT=7999 yarn webpack-dev-server",
     "dev-acceptance": "NO_DEV_SERVER=1 NODE_ENV=development yarn webpack --watch",
     "storybook": "SENTRY_UI_HOT_RELOAD='' start-storybook -p 9001 -c .storybook",
     "storybook-build": "build-storybook -c .storybook -o .storybook-out && sed -i -e 's/\\/sb_dll/sb_dll/g' .storybook-out/index.html",

+ 1 - 1
src/sentry/runner/commands/devserver.py

@@ -140,7 +140,7 @@ def devserver(
     daemons = []
 
     if experimental_spa:
-        os.environ["SENTRY_EXPERIMENTAL_SPA"] = "1"
+        os.environ["SENTRY_UI_DEV_ONLY"] = "1"
         if not watchers:
             click.secho(
                 "Using experimental SPA mode without watchers enabled has no effect",

+ 16 - 1
src/sentry/static/sentry/app/actionCreators/deployPreview.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import {DEPLOY_PREVIEW_CONFIG} from 'app/constants';
+import {DEPLOY_PREVIEW_CONFIG, EXPERIMENTAL_SPA} from 'app/constants';
 import {t, tct} from 'app/locale';
 import AlertActions from 'app/actions/alertActions';
 import ExternalLink from 'app/components/links/externalLink';
@@ -33,3 +33,18 @@ export function displayDeployPreviewAlert() {
     neverExpire: true,
   });
 }
+
+export function displayExperimentalSpaAlert() {
+  if (!EXPERIMENTAL_SPA) {
+    return;
+  }
+
+  AlertActions.addAlert({
+    id: 'develop-proxy',
+    message: t(
+      'You are developing against production Sentry API. The API is read-only due to CSRF issues, but please be careful.'
+    ),
+    type: 'warning',
+    neverExpire: true,
+  });
+}

+ 8 - 0
src/sentry/static/sentry/app/bootstrap.tsx

@@ -84,6 +84,14 @@ const appRoutes = Router.createRoutes(routes());
 
 Sentry.init({
   ...window.__SENTRY__OPTIONS,
+  /**
+   * For SPA mode, we need a way to overwrite the default DSN from backend
+   * as well as `whitelistUrls`
+   */
+  dsn: process.env.SPA_DSN || window.__SENTRY__OPTIONS.dsn,
+  whitelistUrls: process.env.SPA_DSN
+    ? ['localhost', 'dev.getsentry.net', 'sentry.dev', 'webpack-internal://']
+    : window.__SENTRY__OPTIONS.whitelistUrls,
   integrations: getSentryIntegrations(hasReplays),
   tracesSampleRate,
 });

+ 6 - 1
src/sentry/static/sentry/app/views/app/index.tsx

@@ -10,7 +10,10 @@ import keydown from 'react-keydown';
 import {Client} from 'app/api';
 import {Config} from 'app/types';
 import {DEPLOY_PREVIEW_CONFIG, EXPERIMENTAL_SPA} from 'app/constants';
-import {displayDeployPreviewAlert} from 'app/actionCreators/deployPreview';
+import {
+  displayDeployPreviewAlert,
+  displayExperimentalSpaAlert,
+} from 'app/actionCreators/deployPreview';
 import {fetchGuides} from 'app/actionCreators/guides';
 import {openCommandPalette} from 'app/actionCreators/modal';
 import {setTransactionName} from 'app/utils/apm';
@@ -125,6 +128,8 @@ class App extends React.Component<Props, State> {
 
     if (DEPLOY_PREVIEW_CONFIG) {
       displayDeployPreviewAlert();
+    } else if (EXPERIMENTAL_SPA) {
+      displayExperimentalSpaAlert();
     }
 
     $(document).ajaxError(function(_evt, jqXHR) {

+ 61 - 0
src/sentry/static/sentry/index.ejs

@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8">
+    <meta name="robots" content="NONE,NOARCHIVE" />
+    <% if (htmlWebpackPlugin.options.mobile) { %>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <% } %>
+
+    <% if (htmlWebpackPlugin.files.favicon) { %>
+    <link rel="shortcut icon" href="<%= htmlWebpackPlugin.files.favicon%>">
+    <% } %>
+
+    <% for (var css in htmlWebpackPlugin.files.css) { %>
+    <link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
+    <% } %>
+
+    <title><%= htmlWebpackPlugin.options.title || 'Sentry Dev'%></title>
+  </head>
+
+  <body>
+    <% if (htmlWebpackPlugin.options.unsupportedBrowser) { %>
+    <style>.unsupported-browser { display: none; }</style>
+    <div class="unsupported-browser">
+      Sorry, your browser is not supported.  Please upgrade to
+      the latest version or switch your browser to use this site.
+      See <a href="http://outdatedbrowser.com/">outdatedbrowser.com</a>
+      for options.
+    </div>
+    <% } %>
+
+    <div id="blk_router">
+      <div class="loading triangle">
+        <div class="loading-mask"></div>
+        <div class="loading-indicator"></div>
+        <div class="loading-message">
+          <p>Please wait while we load an obnoxious amount of JavaScript.</p>
+          <p>
+            <small>You may need to disable adblocking extensions to load Sentry.</small>
+          </p>
+        </div>
+      </div>
+    </div>
+
+    <% if (htmlWebpackPlugin.options.window) { %>
+    <script>
+      <% for (var varName in htmlWebpackPlugin.options.window) { %>
+          window['<%=varName%>'] = <%= JSON.stringify(htmlWebpackPlugin.options.window[varName]) %>;
+        <% } %>
+    </script>
+    <% } %>
+
+    <% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
+    <script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
+    <% } %>
+
+    <% if (htmlWebpackPlugin.options.devServer) { %>
+    <script src="<%= htmlWebpackPlugin.options.devServer%>/webpack-dev-server.js"></script>
+    <% } %>
+  </body>
+</html>

+ 75 - 45
webpack.config.js

@@ -9,7 +9,6 @@ const ExtractTextPlugin = require('mini-css-extract-plugin');
 const CompressionPlugin = require('compression-webpack-plugin');
 const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
 const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
-const CopyPlugin = require('copy-webpack-plugin');
 const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
 
 const IntegrationDocsFetchPlugin = require('./build-utils/integration-docs-fetch-plugin');
@@ -28,6 +27,8 @@ const IS_TEST = env.NODE_ENV === 'test' || env.TEST_SUITE;
 const IS_STORYBOOK = env.STORYBOOK_BUILD === '1';
 const IS_CI = !!env.CI || !!env.TRAVIS;
 const IS_PERCY = env.CI && !!env.PERCY_TOKEN && !!env.TRAVIS;
+const IS_DEPLOY_PREVIEW = !!env.NOW_GITHUB_DEPLOYMENT;
+const IS_UI_DEV_ONLY = !!env.SENTRY_UI_DEV_ONLY;
 const DEV_MODE = !(IS_PRODUCTION || IS_CI);
 const WEBPACK_MODE = IS_PRODUCTION ? 'production' : 'development';
 
@@ -52,7 +53,7 @@ const SHOULD_HOT_MODULE_RELOAD = DEV_MODE && !!env.SENTRY_UI_HOT_RELOAD;
 
 // Deploy previews are built using zeit. We can check if we're in zeit's
 // build process by checking the existence of the PULL_REQUEST env var.
-const DEPLOY_PREVIEW_CONFIG = env.NOW_GITHUB_DEPLOYMENT && {
+const DEPLOY_PREVIEW_CONFIG = IS_DEPLOY_PREVIEW && {
   branch: env.NOW_GITHUB_COMMIT_REF,
   commitSha: env.NOW_GITHUB_COMMIT_SHA,
   githubOrg: env.NOW_GITHUB_COMMIT_ORG,
@@ -62,9 +63,8 @@ const DEPLOY_PREVIEW_CONFIG = env.NOW_GITHUB_DEPLOYMENT && {
 // When deploy previews are enabled always enable experimental SPA mode --
 // deploy previews are served standalone. Otherwise fallback to the environment
 // configuration.
-const SENTRY_EXPERIMENTAL_SPA = !DEPLOY_PREVIEW_CONFIG
-  ? env.SENTRY_EXPERIMENTAL_SPA
-  : true;
+const SENTRY_EXPERIMENTAL_SPA =
+  !DEPLOY_PREVIEW_CONFIG && !IS_UI_DEV_ONLY ? env.SENTRY_EXPERIMENTAL_SPA : true;
 
 // this is set by setup.py sdist
 const staticPrefix = path.join(__dirname, 'src/sentry/static/sentry');
@@ -311,6 +311,7 @@ let appConfig = {
         IS_PERCY: JSON.stringify(IS_PERCY),
         DEPLOY_PREVIEW_CONFIG: JSON.stringify(DEPLOY_PREVIEW_CONFIG),
         EXPERIMENTAL_SPA: JSON.stringify(SENTRY_EXPERIMENTAL_SPA),
+        SPA_DSN: JSON.stringify(env.SENTRY_SPA_DSN),
       },
     }),
 
@@ -379,17 +380,6 @@ let appConfig = {
   devtool: IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map',
 };
 
-if (SENTRY_EXPERIMENTAL_SPA) {
-  /**
-   * Generate a index.html file used for running the app in pure client mode.
-   * This is currently used for PR deploy previews, where only the frontend
-   * is deployed.
-   */
-  appConfig.plugins.push(
-    new CopyPlugin([{from: path.join(staticPrefix, 'app', 'index.html')}])
-  );
-}
-
 if (IS_TEST || IS_STORYBOOK) {
   appConfig.resolve.alias['integration-docs-platforms'] = path.join(
     __dirname,
@@ -406,9 +396,11 @@ if (!IS_PRODUCTION) {
 }
 
 // Dev only! Hot module reloading
-if (FORCE_WEBPACK_DEV_SERVER || (HAS_WEBPACK_DEV_SERVER_CONFIG && !NO_DEV_SERVER)) {
-  const backendAddress = `http://localhost:${SENTRY_BACKEND_PORT}/`;
-
+if (
+  FORCE_WEBPACK_DEV_SERVER ||
+  (HAS_WEBPACK_DEV_SERVER_CONFIG && !NO_DEV_SERVER) ||
+  IS_UI_DEV_ONLY
+) {
   if (SHOULD_HOT_MODULE_RELOAD) {
     // Hot reload react components on save
     // We include the library here as to not break docker/google cloud builds
@@ -434,34 +426,72 @@ if (FORCE_WEBPACK_DEV_SERVER || (HAS_WEBPACK_DEV_SERVER_CONFIG && !NO_DEV_SERVER
     watchOptions: {
       ignored: ['node_modules'],
     },
-    publicPath: '/_webpack',
-    proxy: {'!/_webpack': backendAddress},
-    before: app =>
-      app.use((req, _res, next) => {
-        req.url = req.url.replace(/^\/_static\/[^\/]+\/sentry\/dist/, '/_webpack');
-        next();
-      }),
   };
 
-  // XXX(epurkhiser): Sentry (development) can be run in an experimental
-  // pure-SPA mode, where ONLY /api* requests are proxied directly to the API
-  // backend, otherwise ALL requests are rewritten to a development index.html.
-  // Thus completely separating the frontend from serving any pages through the
-  // backend.
-  //
-  // THIS IS EXPERIMENTAL. Various sentry pages still rely on django to serve
-  // html views.
-  appConfig.devServer = !SENTRY_EXPERIMENTAL_SPA
-    ? appConfig.devServer
-    : {
-        ...appConfig.devServer,
-        before: () => undefined,
-        publicPath: '/_assets',
-        proxy: {'/api/': backendAddress},
-        historyApiFallback: {
-          rewrites: [{from: /^\/.*$/, to: '/_assets/index.html'}],
-        },
-      };
+  if (!IS_UI_DEV_ONLY) {
+    // This proxies to local backend server
+    const backendAddress = `http://localhost:${SENTRY_BACKEND_PORT}/`;
+
+    appConfig.devServer = {
+      ...appConfig.devServer,
+      publicPath: '/_webpack',
+      proxy: {'!/_webpack': backendAddress},
+      before: app =>
+        app.use((req, _res, next) => {
+          req.url = req.url.replace(/^\/_static\/[^\/]+\/sentry\/dist/, '/_webpack');
+          next();
+        }),
+    };
+  }
+}
+
+// XXX(epurkhiser): Sentry (development) can be run in an experimental
+// pure-SPA mode, where ONLY /api* requests are proxied directly to the API
+// backend (in this case, sentry.io), otherwise ALL requests are rewritten
+// to a development index.html -- thus, completely separating the frontend
+// from serving any pages through the backend.
+//
+// THIS IS EXPERIMENTAL and has limitations (e.g. CSRF issues will stop you
+// from writing to the API).
+//
+// Various sentry pages still rely on django to serve html views.
+if (IS_UI_DEV_ONLY) {
+  appConfig.output.publicPath = '/_assets/';
+  appConfig.devServer = {
+    ...appConfig.devServer,
+    compress: true,
+    https: true,
+    publicPath: '/_assets/',
+    proxy: [
+      {
+        context: ['/api/', '/avatar/', '/organization-avatar/'],
+        target: 'https://sentry.io',
+        secure: false,
+        changeOrigin: true,
+      },
+    ],
+    historyApiFallback: {
+      rewrites: [{from: /^\/.*$/, to: '/_assets/index.html'}],
+    },
+  };
+}
+
+if (IS_UI_DEV_ONLY || IS_DEPLOY_PREVIEW) {
+  /**
+   * Generate a index.html file used for running the app in pure client mode.
+   * This is currently used for PR deploy previews, where only the frontend
+   * is deployed.
+   */
+  const HtmlWebpackPlugin = require('html-webpack-plugin');
+  appConfig.plugins.push(
+    new HtmlWebpackPlugin({
+      devServer: `https://localhost:${SENTRY_WEBPACK_PROXY_PORT}`,
+      // inject: false,
+      template: path.resolve(staticPrefix, 'index.ejs'),
+      mobile: true,
+      title: 'Sentry',
+    })
+  );
 }
 
 const minificationPlugins = [

+ 35 - 40
yarn.lock

@@ -2764,6 +2764,11 @@
   resolved "https://registry.yarnpkg.com/@types/history/-/history-3.2.4.tgz#0b6c62240d1fac020853aa5608758991d9f6ef3d"
   integrity sha512-q7x8QeCRk2T6DR2UznwYW//mpN5uNlyajkewH2xd1s1ozCS4oHFRg2WMusxwLFlE57EkUYsd/gCapLBYzV3ffg==
 
+"@types/html-minifier-terser@^5.0.0":
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.0.0.tgz#7532440c138605ced1b555935c3115ddd20e8bef"
+  integrity sha512-q95SP4FdkmF0CwO0F2q0H6ZgudsApaY/yCtAQNRn1gduef5fGpyEphzy0YCq/N0UFvDSnLg5V8jFK/YGXlDiCw==
+
 "@types/invariant@^2.2.29":
   version "2.2.31"
   resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.31.tgz#4444c03004f215289dbca3856538434317dd28b2"
@@ -3023,7 +3028,7 @@
   dependencies:
     csstype "^2.6.4"
 
-"@types/tapable@*":
+"@types/tapable@*", "@types/tapable@^1.0.5":
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02"
   integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ==
@@ -3071,6 +3076,18 @@
     "@types/webpack-sources" "*"
     source-map "^0.6.0"
 
+"@types/webpack@^4.41.8":
+  version "4.41.12"
+  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.12.tgz#0386ee2a2814368e2f2397abb036c0bf173ff6c3"
+  integrity sha512-BpCtM4NnBen6W+KEhrL9jKuZCXVtiH6+0b6cxdvNt2EwU949Al334PjQSl2BeAyvAX9mgoNNG21wvjP3xZJJ5w==
+  dependencies:
+    "@types/anymatch" "*"
+    "@types/node" "*"
+    "@types/tapable" "*"
+    "@types/uglify-js" "*"
+    "@types/webpack-sources" "*"
+    source-map "^0.6.0"
+
 "@types/yargs-parser@*":
   version "15.0.0"
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
@@ -4702,7 +4719,7 @@ bytes@3.1.0:
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
-cacache@^12.0.2, cacache@^12.0.3:
+cacache@^12.0.2:
   version "12.0.3"
   resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390"
   integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==
@@ -5439,24 +5456,6 @@ copy-to-clipboard@^3.0.8:
   dependencies:
     toggle-selection "^1.0.6"
 
-copy-webpack-plugin@^5.0.3:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz#5481a03dea1123d88a988c6ff8b78247214f0b88"
-  integrity sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==
-  dependencies:
-    cacache "^12.0.3"
-    find-cache-dir "^2.1.0"
-    glob-parent "^3.1.0"
-    globby "^7.1.1"
-    is-glob "^4.0.1"
-    loader-utils "^1.2.3"
-    minimatch "^3.0.4"
-    normalize-path "^3.0.0"
-    p-limit "^2.2.1"
-    schema-utils "^1.0.0"
-    serialize-javascript "^2.1.2"
-    webpack-log "^2.0.0"
-
 core-js-compat@^3.6.2:
   version "3.6.4"
   resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17"
@@ -6078,13 +6077,6 @@ dir-glob@2.0.0:
     arrify "^1.0.1"
     path-type "^3.0.0"
 
-dir-glob@^2.0.0:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
-  integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==
-  dependencies:
-    path-type "^3.0.0"
-
 dir-glob@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -7820,18 +7812,6 @@ globby@^6.1.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
-globby@^7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680"
-  integrity sha1-+yzP+UAfhgCUXfral0QMypcrhoA=
-  dependencies:
-    array-union "^1.0.1"
-    dir-glob "^2.0.0"
-    glob "^7.1.2"
-    ignore "^3.3.5"
-    pify "^3.0.0"
-    slash "^1.0.0"
-
 globjoin@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
@@ -8198,6 +8178,21 @@ html-webpack-plugin@^4.0.0-beta.2:
     tapable "^1.1.3"
     util.promisify "1.0.0"
 
+html-webpack-plugin@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.3.0.tgz#53bf8f6d696c4637d5b656d3d9863d89ce8174fd"
+  integrity sha512-C0fzKN8yQoVLTelcJxZfJCE+aAvQiY2VUf3UuKrR4a9k5UMWYOtpDLsaXwATbcVCnI05hUS7L9ULQHWLZhyi3w==
+  dependencies:
+    "@types/html-minifier-terser" "^5.0.0"
+    "@types/tapable" "^1.0.5"
+    "@types/webpack" "^4.41.8"
+    html-minifier-terser "^5.0.1"
+    loader-utils "^1.2.3"
+    lodash "^4.17.15"
+    pretty-error "^2.1.1"
+    tapable "^1.1.3"
+    util.promisify "1.0.0"
+
 htmlparser2@^3.10.0, htmlparser2@^3.3.0, htmlparser2@^3.8.3, htmlparser2@^3.9.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
@@ -11195,7 +11190,7 @@ p-limit@^1.1.0:
   dependencies:
     p-try "^1.0.0"
 
-p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.2.1:
+p-limit@^2.0.0, p-limit@^2.2.0:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
   integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==