From 45b4c69b107cb37b093577532c3854b30343ecbc Mon Sep 17 00:00:00 2001
From: Curt Tudor <curt@rentallect.com>
Date: Mon, 5 Feb 2024 10:16:19 -0700
Subject: [PATCH] feat: introduce ZitiDummyWebSocketWrapper (#258)

---
 package.json                             |   4 +-
 rollup.config.js                         |   2 +
 src/http/ziti-dummy-websocket-wrapper.js | 143 +++++++++++++++++++++++
 src/http/ziti-xhr.js                     |  19 +++
 src/runtime.js                           |  89 ++++++++------
 yarn.lock                                |  55 ++++++++-
 6 files changed, 273 insertions(+), 39 deletions(-)
 create mode 100644 src/http/ziti-dummy-websocket-wrapper.js

diff --git a/package.json b/package.json
index 70b5855..1ed3692 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
     "rollup-plugin-babel": "^4.4.0",
     "rollup-plugin-copy": "^3.4.0",
     "rollup-plugin-esformatter": "^2.0.1",
+    "rollup-plugin-polyfill-node": "^0.13.0",
     "rollup-plugin-prettier": "^2.2.2",
     "rollup-plugin-terser": "^7.0.2",
     "tinyify": "^3.0.0"
@@ -94,10 +95,11 @@
     "@auth0/auth0-spa-js": "^2.0.4",
     "@azure/msal-browser": "^2.38.0",
     "@babel/runtime": "^7.17.9",
-    "@openziti/ziti-browzer-core": "^0.36.1",
+    "@openziti/ziti-browzer-core": "^0.37.0",
     "bowser": "^2.11.0",
     "cookie-interceptor": "^1.0.0",
     "core-js": "^3.22.8",
+    "events": "^3.3.0",
     "js-base64": "^3.7.2",
     "jwt-decode": "^3.1.2",
     "localforage": "^1.10.0",
diff --git a/rollup.config.js b/rollup.config.js
index e537f84..cb26a81 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -5,6 +5,7 @@ import json from '@rollup/plugin-json';
 // import commonjs from '@rollup/plugin-commonjs';
 import prettier from 'rollup-plugin-prettier';
 import copy from 'rollup-plugin-copy';
+import nodePolyfills from 'rollup-plugin-polyfill-node';
 
 
 const SRC_DIR   = 'src';
@@ -33,6 +34,7 @@ let plugins = [
       }
     ]
   }),
+  nodePolyfills(),
   nodeResolve(),
 ];
 
diff --git a/src/http/ziti-dummy-websocket-wrapper.js b/src/http/ziti-dummy-websocket-wrapper.js
new file mode 100644
index 0000000..436af42
--- /dev/null
+++ b/src/http/ziti-dummy-websocket-wrapper.js
@@ -0,0 +1,143 @@
+/*
+Copyright NetFoundry, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import EventEmitter from 'events';
+import { isEqual, isUndefined } from 'lodash-es';
+
+
+/**
+ * ZitiDummyWebSocketWrapper:
+ * 
+ */
+class ZitiDummyWebSocketWrapper extends EventEmitter {
+
+    /**
+     * Create a new `ZitiDummyWebSocketWrapper`.
+     *
+     * @param {(String|url.URL)} address The URL to which to connect
+     */
+    constructor(address) {
+
+      super();
+
+      this.address = address;
+
+      setTimeout(async function(self) {
+
+        await window.zitiBrowzerRuntime.awaitInitializationComplete();
+
+        let serviceName;
+        var url = new URL( self.address );
+
+        if (isEqual(url.hostname, zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host)) { // if targeting the bootstrapper
+          serviceName = zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.target.service;
+        } else {
+          serviceName = await zitiBrowzerRuntime.zitiContext.shouldRouteOverZiti( self.address );
+        }
+
+        if (isUndefined(serviceName)) { // If we have no serviceConfig associated with the address, do not intercept
+
+          self.innerWebSocket = new window._ziti_realWebSocket(self.address);
+
+        } else {
+
+          let opts = {}
+
+          opts.serviceName = serviceName;
+          opts.configHostAndPort = await zitiBrowzerRuntime.zitiContext.getConfigHostAndPortByServiceName (serviceName);
+          
+          self.innerWebSocket = new zitiBrowzerRuntime.zitiContext.zitiWebSocketWrapper(self.address, undefined, opts);
+
+        }
+
+        self.innerWebSocket.addEventListener('open', (event) => {
+          if (self.onopen) {
+            self.onopen(event);
+          }
+          self.emit(event);
+        });
+        
+        self.innerWebSocket.addEventListener('close', (event) => {
+          if (self.onclose) {
+            self.onclose(event);
+          }
+          self.emit(event);
+        });
+  
+        self.innerWebSocket.addEventListener('error', (event) => {
+          if (self.onerror) {
+            self.onerror(event);
+          }
+          self.emit(event);
+        });
+
+        self.innerWebSocket.addEventListener('message', (event) => {
+          if (self.onmessage) {
+            self.onmessage(event);
+          }
+          self.emit(event);
+        });
+
+      }, 1, this);
+  
+    }
+
+    /**
+     * Remain in lazy-sleepy loop until inner WebSocket is present.
+     * 
+     */
+    awaitInnerWebSocketPresent() {
+      let self = this;
+      return new Promise((resolve) => {
+        (function waitForInnerWebSocketPresent() {
+          if (!self.innerWebSocket) {
+            setTimeout(waitForInnerWebSocketPresent, 5);  
+          } else {
+            if (self.innerWebSocket.READYSTATE !== self.innerWebSocket.OPEN) {
+              setTimeout(waitForInnerWebSocketPresent, 5);  
+            } else {
+              return resolve();
+            }
+          }
+        })();
+      });
+    }
+
+    /**
+     * 
+     * @param {*} code 
+     * @param {*} data 
+     */
+    async close(code, data) {
+      await this.awaitInnerWebSocketPresent();
+      this.innerWebSocket.close(code, data);
+    }
+
+    /**
+     * 
+     * @param {*} data 
+     */
+    async send(data) {
+      await this.awaitInnerWebSocketPresent();
+      this.innerWebSocket.send(data);
+    }
+
+}
+
+
+export {
+  ZitiDummyWebSocketWrapper
+};
diff --git a/src/http/ziti-xhr.js b/src/http/ziti-xhr.js
index f98dff3..e6b9f7b 100644
--- a/src/http/ziti-xhr.js
+++ b/src/http/ziti-xhr.js
@@ -274,6 +274,25 @@ function ZitiXMLHttpRequest () {
 
     settings.headers = headers;
 
+    await window.zitiBrowzerRuntime.awaitInitializationComplete();
+
+    let url;
+    let targetHost;
+    if (!settings.url.startsWith('/')) {
+      url = new URL(settings.url);
+      targetHost = url.hostname;
+    } else {
+      url = new URL(`https://${window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host}${settings.url}`);
+      targetHost = window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host;
+    }
+    if (isEqual(targetHost, window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host)) {
+      let protocol = url.protocol;
+      if (!isEqual(protocol, 'https:')) {
+        url.protocol = 'https:';
+        settings.url = url.toString();
+      }
+    }
+
     response = await fetch(settings.url, settings);
 
     this.status = response.status;
diff --git a/src/runtime.js b/src/runtime.js
index 0aa574f..f10a024 100644
--- a/src/runtime.js
+++ b/src/runtime.js
@@ -31,6 +31,7 @@ import { flatOptions } from './utils/flat-options'
 import { defaultOptions } from './options'
 import { ZBR_CONSTANTS } from './constants';
 import { ZitiXMLHttpRequest } from './http/ziti-xhr';
+import { ZitiDummyWebSocketWrapper } from './http/ziti-dummy-websocket-wrapper';
 import { buildInfo } from './buildInfo'
 import { ZitiBrowzerLocalStorage } from './utils/localstorage';
 import { Auth0Client } from '@auth0/auth0-spa-js';
@@ -237,15 +238,21 @@ class ZitiBrowzerRuntime {
       return cookie;
     });  
 
-    // Toast infra
-    this.PolipopCreated = false;
-    setTimeout(this._createPolipop, 1000, this);
+    const loadedViaBootstrapper = document.getElementById('from-ziti-browzer-bootstrapper');
+
+    if (!loadedViaBootstrapper) {
 
-    // HotKey infra
-    setTimeout(this._createHotKey, 5000, this);    
+      // Toast infra
+      this.PolipopCreated = false;
+      setTimeout(this._createPolipop, 1000, this);
 
-    // Click intercept infra
-    setTimeout(this._createClickIntercept, 3000, this);
+      // HotKey infra
+      setTimeout(this._createHotKey, 5000, this);    
+
+      // Click intercept infra
+      setTimeout(this._createClickIntercept, 3000, this);
+    
+    }
 
     this.authClient = null;
     this.idp = null;
@@ -410,7 +417,7 @@ class ZitiBrowzerRuntime {
 
   _createPolipop(self) {
 
-    if (!this.PolipopCreated) {
+    if (!self.PolipopCreated) {
       try {
         if (document.body && (typeof Polipop !== 'undefined')) {
           self.polipop = new Polipop('ziti-browzer-toast', {
@@ -427,17 +434,17 @@ class ZitiBrowzerRuntime {
             life: 3000,
             icons: true,
           });
-          this.PolipopCreated = true;
-          self.logger.debug(`_createPolipop: Polipop bootstrap completed`);
+          self.PolipopCreated = true;
+          self.logger?.debug(`_createPolipop: Polipop bootstrap completed`);
         }
         else {
-          self.logger.debug(`_createPolipop: awaiting Polipop bootstrap`);
-          setTimeout(this._createPolipop, 100, this);
+          self.logger?.debug(`_createPolipop: awaiting Polipop bootstrap`);
+          setTimeout(self._createPolipop, 100, self);
         }
       }
       catch (e) {
-        self.logger.error(`_createPolipop: bootstrap error`, e);
-        setTimeout(this._createPolipop, 1000, this);
+        self.logger?.error(`_createPolipop: bootstrap error`, e);
+        setTimeout(self._createPolipop, 1000, self);
       }
     }
   }
@@ -1609,9 +1616,9 @@ class ZitiBrowzerRuntime {
 
     /**
      *  Logic devoted to acquiring an access_token from the IdP runs _ONLY_
-     *  when this ZBR has been loaded from the BrowZer Gateway (not the ZBSW).
+     *  when this ZBR has been loaded from the BrowZer Bootstrapper (not the ZBSW).
      * 
-     *  If we were loaded via the ZBSW, then the the access_token we
+     *  If we were loaded via the ZBSW, then the access_token we
      *  need should be in a cookie, and we will obtain it from there instead of 
      *  interacting with the IdP because doing so will lead to a never ending loop.
      */
@@ -1758,8 +1765,6 @@ class ZitiBrowzerRuntime {
       });
       this.logger.trace(`ZitiContext created`);
 
-      window.WebSocket = zitiBrowzerRuntime.zitiContext.zitiWebSocketWrapper;
-
       this.zbrSWM = new ZitiBrowzerRuntimeServiceWorkerMock();
 
       navigator.serviceWorker._ziti_realRegister = navigator.serviceWorker.register;
@@ -1913,6 +1918,8 @@ if (isUndefined(window.zitiBrowzerRuntime)) {
 
   window.zitiBrowzerRuntime._serviceWorkerKeepAliveHeartBeat(window.zitiBrowzerRuntime);
 
+  window.zitiBrowzerRuntime.loadedViaBootstrapper = document.getElementById('from-ziti-browzer-bootstrapper');
+
   /**
    * Use an async IIFE to initialize the runtime and register the SW.
    */
@@ -1984,11 +1991,6 @@ if (isUndefined(window.zitiBrowzerRuntime)) {
        */
       await window.zitiBrowzerRuntime.zitiContext.enroll();
 
-      /**
-       * 
-       */
-      window.WebSocket = zitiBrowzerRuntime.zitiContext.zitiWebSocketWrapper;
-
     }
 
     window.addEventListener('online', (e) => {
@@ -2475,6 +2477,7 @@ if (isUndefined(window.zitiBrowzerRuntime)) {
 var regex = new RegExp( `${window.zitiBrowzerRuntime._obtainBootStrapperURL()}`, 'gi' );
 var regexSlash = new RegExp( /^\//, 'g' );
 var regexDotSlash = new RegExp( /^\.\//, 'g' );
+var regexZBR      = new RegExp( /ziti-browzer-runtime-\w{8}\.js/, 'g' );
 var regexZBWASM   = new RegExp( /libcrypto.*.wasm/, 'g' );
 
 
@@ -2489,27 +2492,45 @@ var regexZBWASM   = new RegExp( /libcrypto.*.wasm/, 'g' );
 
 const zitiFetch = async ( urlObj, opts ) => {
 
-  if (!window.zitiBrowzerRuntime.isAuthenticated) {
-    return window._ziti_realFetch(urlObj, opts);
-  }
-
   let url;
-
   if (urlObj instanceof Request) {
     url = urlObj.url;
+  } else {
+    url = urlObj;
+  }
 
-    for (var pair of urlObj.headers.entries()) {
-      console.log(`${pair[0]} : ${pair[1]}`);
-    }
+  if (url.match( regexZBR ) || url.match( regexZBWASM )) { // the request seeks z-b-r/wasm
+    return window._ziti_realFetch(urlObj, opts);
+  }
+
+  if (!window.zitiBrowzerRuntime.loadedViaBootstrapper) {
+    await window.zitiBrowzerRuntime.awaitInitializationComplete();
+  }
 
+  if (!window.zitiBrowzerRuntime.isAuthenticated) {
+    return window._ziti_realFetch(urlObj, opts);
+  }
+
+  let targetHost;
+  if (!url.startsWith('/')) {
+    url = new URL(url);
+    targetHost = url.hostname;
   } else {
-    url = urlObj;
+    url = new URL(`https://${window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host}${url}`);
+    targetHost = window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host;
+  }
+  if (isEqual(targetHost, window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host)) {
+    let protocol = url.protocol;
+    if (!isEqual(protocol, 'https:')) {
+      url.protocol = 'https:';
+      url = url.toString();
+    }
   }
 
   window.zitiBrowzerRuntime.logger.trace( 'zitiFetch: entered for URL: ', url);
 
 //TEMP TEST... always attempt to go thru SW
-  return window._ziti_realFetch(urlObj, opts);
+  return window._ziti_realFetch(url, opts);
 
   if (url.match( regexZBWASM )) { // the request seeks z-b-r/wasm
     window.zitiBrowzerRuntime.logger.trace('zitiFetch: seeking Ziti z-b-r/wasm, bypassing intercept of [%s]', url);
@@ -2729,4 +2750,4 @@ const zitiDocumentDomain = ( arg ) => {
 window.fetch = zitiFetch;
 window.XMLHttpRequest = ZitiXMLHttpRequest;
 window.document.zitidomain = zitiDocumentDomain;
-
+window.WebSocket = ZitiDummyWebSocketWrapper;
diff --git a/yarn.lock b/yarn.lock
index 2471665..03c4056 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1145,6 +1145,11 @@
   resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec"
   integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==
 
+"@jridgewell/sourcemap-codec@^1.4.15":
+  version "1.4.15"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
 "@jridgewell/trace-mapping@^0.3.0":
   version "0.3.4"
   resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3"
@@ -1195,10 +1200,10 @@
     "@types/emscripten" "^1.39.6"
     "@wasmer/wasi" "^1.0.2"
 
-"@openziti/ziti-browzer-core@^0.36.1":
-  version "0.36.1"
-  resolved "https://registry.yarnpkg.com/@openziti/ziti-browzer-core/-/ziti-browzer-core-0.36.1.tgz#fee5c55a47f462d0dab9fbd2c7b19755cdb75109"
-  integrity sha512-b+7jqEhbAvEOYCl+v12+q3CfwAt30mvKpbeNqM6y5c1nHVyDHcnrKKVjK9Xvt4JV9ZobMjIn34XuDuwQDT5Cyw==
+"@openziti/ziti-browzer-core@^0.37.0":
+  version "0.37.0"
+  resolved "https://registry.yarnpkg.com/@openziti/ziti-browzer-core/-/ziti-browzer-core-0.37.0.tgz#49d3984ffcecc5f746cbf3e27d93e7a9cdc208fc"
+  integrity sha512-TL04Jxplv2VusklbbeHh2xiM4XiCbPGrvS/3Cs+sqwdmVyV9+z+GVLYE9Bv4V2ofMeq2V2CFvQbrRL06qC8a/w==
   dependencies:
     "@openziti/libcrypto-js" "^0.19.0"
     "@openziti/ziti-browzer-edge-client" "^0.6.2"
@@ -1246,6 +1251,15 @@
   dependencies:
     superagent "^7.1.3"
 
+"@rollup/plugin-inject@^5.0.4":
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz#616f3a73fe075765f91c5bec90176608bed277a3"
+  integrity sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==
+  dependencies:
+    "@rollup/pluginutils" "^5.0.1"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.3"
+
 "@rollup/plugin-json@^4.1.0":
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3"
@@ -1274,6 +1288,15 @@
     estree-walker "^1.0.1"
     picomatch "^2.2.2"
 
+"@rollup/pluginutils@^5.0.1":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0"
+  integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==
+  dependencies:
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^2.3.1"
+
 "@types/emscripten@^1.39.6":
   version "1.39.6"
   resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.39.6.tgz#698b90fe60d44acf93c31064218fbea93fbfd85a"
@@ -1284,6 +1307,11 @@
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
   integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
 
+"@types/estree@^1.0.0":
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
+  integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
+
 "@types/fs-extra@^8.0.1":
   version "8.1.2"
   resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.2.tgz#7125cc2e4bdd9bd2fc83005ffdb1d0ba00cca61f"
@@ -2862,6 +2890,11 @@ estree-walker@^1.0.1:
   resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
   integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
 
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
 esutils@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@@ -3934,6 +3967,13 @@ magic-string@^0.23.2:
   dependencies:
     sourcemap-codec "^1.4.1"
 
+magic-string@^0.30.3:
+  version "0.30.6"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.6.tgz#996e21b42f944e45591a68f0905d6a740a12506c"
+  integrity sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.4.15"
+
 make-dir@^2.0.0, make-dir@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@@ -4940,6 +4980,13 @@ rollup-plugin-esformatter@^2.0.1:
     lodash.omitby "4.6.0"
     magic-string "0.25.7"
 
+rollup-plugin-polyfill-node@^0.13.0:
+  version "0.13.0"
+  resolved "https://registry.yarnpkg.com/rollup-plugin-polyfill-node/-/rollup-plugin-polyfill-node-0.13.0.tgz#28e5705b59438da894e55133a0fe7a86b57d9b0a"
+  integrity sha512-FYEvpCaD5jGtyBuBFcQImEGmTxDTPbiHjJdrYIp+mFIwgXiXabxvKUK7ZT9P31ozu2Tqm9llYQMRWsfvTMTAOw==
+  dependencies:
+    "@rollup/plugin-inject" "^5.0.4"
+
 rollup-plugin-prettier@^2.2.2:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/rollup-plugin-prettier/-/rollup-plugin-prettier-2.2.2.tgz#733c25a3cea2ce65b14635729bd1eb3a2a147ebc"