Refactor extension to webpack

This change is the biggest in the history of the project. The entire
project has been refactored so it is built with webpack.

This involves:
- Creating webpack and npm config files.
- Fixing some bugs in the code due to the fact that webpack uses strict
mode.
- Merging some pieces of code which were shared throughout the codebase
(not exhaustive, more work should be done in this direction).
- Splitting the console_inject.js file into separate files (it had 1000+
lines).
- Adapting all the build-related files (Makefile, bash scripts, etc.)
- Changing the docs to explain the new build process.
- Changing the Zuul playbook/roles to adapt to the new build process.

Change-Id: I16476d47825461c3a318b3f1a1eddb06b2df2e89
diff --git a/.gitignore b/.gitignore
index 18ca0f6..bc4c13e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
-out
-src/manifest.json
+dist/
+node_modules/
+out/
 .DS_Store
diff --git a/Makefile b/Makefile
index c630cf7..0123e00 100644
--- a/Makefile
+++ b/Makefile
@@ -1,18 +1,52 @@
-.PHONY: all chromium-stable chromium-beta chromium-mv3-beta gecko-stable clean
+.PHONY: node_deps clean_dist deps clean_deps serve_chromium serve_chromium_mv3 serve_gecko release release_chromium_stable release_chromium_beta release_gecko_stable build_test_extension clean_releases clean
 
-all: chromium-stable chromium-beta chromium-mv3-beta gecko-stable
+WEBPACK := ./node_modules/webpack-cli/bin/cli.js
+RELEASE_SCRIPT := bash tools/release.bash
 
-chromium-stable:
-	bash release.bash -c stable -b chromium
+node_deps:
+	npm ci --no-save
 
-chromium-beta:
-	bash release.bash -c beta -b chromium
+clean_dist:
+	rm -rf dist
 
-chromium-mv3-beta:
-	bash release.bash -c beta -b chromium_mv3
+deps: node_deps
+	mkdir -p dist
 
-gecko-stable:
-	bash release.bash -c stable -b gecko
+clean_deps:
+	rm -rf node_modules
 
-clean:
+serve_chromium: deps
+	$(WEBPACK) --mode development --env browser_target=chromium --watch
+
+serve_chromium_mv3: deps
+	$(WEBPACK) --mode development --env browser_target=chromium_mv3 --watch
+
+serve_gecko: deps
+	$(WEBPACK) --mode development --env browser_target=gecko --watch
+
+release: release_chromium_stable release_chromium_beta release_gecko_stable
+
+release_chromium_stable: deps
+	$(WEBPACK) --mode production --env browser_target=chromium
+	$(RELEASE_SCRIPT) -c stable -b chromium
+	rm -rf dist/chromium
+
+release_chromium_beta: deps
+	$(WEBPACK) --mode production --env browser_target=chromium
+	$(RELEASE_SCRIPT) -c beta -b chromium
+	rm -rf dist/chromium
+
+release_gecko_stable: deps
+	$(WEBPACK) --mode production --env browser_target=gecko
+	$(RELEASE_SCRIPT) -c stable -b gecko
+	rm -rf dist/gecko
+
+# Target to build the extension for webext lint in the Zuul Check Pipeline.
+build_test_extension: deps
+	$(WEBPACK) --mode production --env browser_target=gecko
+	$(RELEASE_SCRIPT) -c stable -b gecko
+
+clean_releases:
 	rm -rf out
+
+clean: clean_deps clean_dist clean_releases
diff --git a/docs/developers/add_feature.md b/docs/developers/add_feature.md
index 15053e4..12ec328 100644
--- a/docs/developers/add_feature.md
+++ b/docs/developers/add_feature.md
@@ -9,22 +9,21 @@
 
 ### How to add the feature switch option
 1. First of all, think of a short codename for the feature.
-2. Modify the `//src/common/common.js` file by adding an entry in the
-`optionsPrototype` object.
+2. Modify the `//src/common/optionsPrototype.json5` file by adding an entry.
     - All features should have the `false` value set as a default, so existing
     users have to explicitly enable the option after they receive the extension
     update. Otherwise, it might cause confusion, because users wouldn't know if
     the feature was added by the extension or Google.
-3. Now, modify the `//src/options/options.html` file by appending the following
-HTML code in the corresponding section:
+3. Now, modify the `//src/static/options/options.html` file by appending the
+following HTML code in the corresponding section:
     ```
     <div class="option"><input type="checkbox" id="{{codename}}"> <label for="{{codename}}" data-i18n="{{codename}}"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
     ```
     where you should substitute `{{codename}}` with the codename you've chosen.
 The _experimental_ label is optional and should only be used with features which
 are unreliable (or could be at some point in the future) due to their nature.
-4. Finally, add the option string at `//src/_locales/en/manifest.json`, by
-adding the string under the `options_{{codename}}` key.
+4. Finally, add the option string at `//src/static/_locales/en/manifest.json`,
+by adding the string under the `options_{{codename}}` key.
 
 ### How to add additional options for the feature
 Apart from the feature switch option, additional options can be defined in order
@@ -58,30 +57,33 @@
 1. Think of a codename for the additional option. It should be the feature
 codename appended by an underscore and a suffix
 (`{{feature_codename}}_{{suffix}}`).
-2. Modify the `//src/common/common.js` file by doing the following things:
-    1. Add an entry for the option in the `optionsPrototype` object.
-    2. Append the option's codename to the `specialOptions` object. This is so
-    the option can be handled in a specific way when showing/saving it in the
-    options page, or so it is handled outside of the options page.
+2. Modify the following files:
+    1. Add an entry for the option in the `//src/common/optionsPrototype.json5`
+    file.
+    2. Append the option's codename to the `//src/common/specialOptions.json5`
+    file. This is so the option can be handled in a specific way when
+    showing/saving it in the options page, or so it is handled outside of the
+    options page.
 3. If you want to handle the option from the options page, follow these steps:
-    1. Modify the `//src/options/options.html` file to add the appropriate code
-    which implements the option (usually in the same `.option` div as the
-    feature switch).
+    1. Modify the `//src/static/options/options.html` file to add the
+    appropriate code which implements the option (usually in the same `.option`
+    div as the feature switch).
         - Don't include text, only the HTML structure. If you add a `data-i18n`
         attribute to an HTML element, its contents will be replaced with the
         corresponding i18n string (for instance,
         `<span data-i18n="test"></span>` will be rendered as
         `<span data-i18n="test">Test</span>` if `Test` was the string defined
         with the key `options_test`).
-    2. Modify the `//src/options/options.js` file to add the following things:
+    2. Modify the `//src/optionsCommon.js` file to add the following things:
         1. In the switch statement inside the save function, add a case to
         handle saving your additional option.
         2. In the switch statement inside the load event listener, add another
         case so your option is correctly set in the options page with the saved
         value.
-    3. Add the corresponding i18n strings at `//src/_locales/en/manifest.json`.
+    3. Add the corresponding i18n strings at
+    `//src/static/_locales/en/manifest.json`.
 4. If you want to handle the option outside of the options page, include an
-empty case in both switches at `//src/options/options.js`, so the option is not
+empty case in both switches at `//src/optionsCommon.js`, so the option is not
 handled there.
 
 ## Develop the feature
diff --git a/docs/developers/build.md b/docs/developers/build.md
index 76a1175..9fe8e75 100644
--- a/docs/developers/build.md
+++ b/docs/developers/build.md
@@ -1,41 +1,38 @@
 # Build the extension
 A zip file with the contents of the extension, which can be uploaded to the
-Chrome Web Store and addons.mozilla.org, can be created with any of the
-following procedures (make sure to [install Go](https://golang.org) before
-building the extension, as it is needed during the build):
+Chrome Web Store and addons.mozilla.org, can be created by following these
+instructions. (make sure to [install Go](https://golang.org) before building the
+extension, as it is needed during the build).
 
-## Using the release.bash script
-Run `bash release.bash -h` in order to learn how to use this command. To
-summarize, the command accepts the `--channel` and `--browser` flags (or their
-short versions `-c` and `-b`).
+The last section explains how to build the extension automatically when you
+change the source code.
 
-As an example, if you wanted to create a ZIP file of the beta-branded extension
-targeted for Firefox, you would run `bash release.bash -c beta -b gecko`.
-
-## Using make
-You can also use _make_ to build the extension. This is just a wrapper for the
-`release.bash` command.
-
-Run `make all` to build the extension for all the available channels and
+## Build the extension as a zip file
+Run `make release` to build the extension for all the available channels and
 browsers. You can also run `make {target}` where `{target}` is one of the
-following: `chromium-stable`, `chromium-beta`, `chromium-mv3-beta`,
-`gecko-stable`.
+following: `release_chromium_stable`, `release_chromium_beta`,
+`release_gecko_stable`.
 
-Run `make clean` to clean all the release files (this removes the `out` folder,
-which is where the release files are saved).
+Run `make clean_releases` to clean all the release files (this removes the `out`
+folder, which is where the release files are saved).
 
-## Load the extension "without" building it
+Take a look at `Makefile`, you'll find other targets for _make_ which do other
+interesting things (like `clean`, for instance).
+
+## Build the extension continously with webpack development mode
 If you're developing the extension, you might want to load it into your browser
-without having to constantly build it after each change. In order to do that,
-you'll only have to manually generate the manifest each time you change the
-`template/manifest.gjson` file (or only once if you don't change it, and once
-every time you pull new changes to your git clone).
+without having to constantly build it manually after each change. In order to do
+that, you can run `make serve_chromium`, `make serve_chromium_mv3` or
+`make serve_gecko` depending on the type of continuous build you want.
 
-In order to do that, run `go run generateManifest.go {browser}`, where
-`{browser}` is `CHROMIUM`, `GECKO` or `CHROMIUM_MV3`, and this will generate the
-`manifest.json` file for the specified browser in the `src` directory. Now, you
-can load the `src` folder directly in the browser in order to import the
-extension.
+This will run webpack with watch mode and continuously serve a fresh version of
+the compiled extension at `dist/{BROWSER}`, where `{BROWSER}` depends on the
+target you selected for _make_. You can load this folder in Chrome by going to
+`chrome://extensions` and selecting "Load unpacked".
+
+Keep in mind that while the extension is continuously built, it is not
+automatically being loaded into Chrome. You'll have to reload the extension
+manually in Chrome (but at least you won't have to both build and reload it).
 
 ## About the _Chromium MV3_ target
 Chromium is working in a
diff --git a/docs/developers/set_up.md b/docs/developers/set_up.md
index 650ce31..8106933 100644
--- a/docs/developers/set_up.md
+++ b/docs/developers/set_up.md
@@ -6,7 +6,8 @@
 with [Homebrew](https://brew.sh/) by running `brew install go`.
    - This is because the build script uses a Go program to generate the
    manifest.
-2. That's it! If you're using a Mac, you're out of luck, because you must
+1. [Install NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/).
+1. That's it! If you're using a Mac, you're out of luck, because you must
 perform some more steps.
 
 ## Mac-specific configuration
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..b2b43f4
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3540 @@
+{
+  "name": "twpowertools",
+  "version": "0.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "twpowertools",
+      "version": "0.0.0",
+      "license": "MIT",
+      "devDependencies": {
+        "clean-webpack-plugin": "^4.0.0-alpha.0",
+        "copy-webpack-plugin": "^9.0.1",
+        "json5": "^2.2.0",
+        "path": "^0.12.7",
+        "webpack": "^5.44.0",
+        "webpack-cli": "^4.7.2",
+        "webpack-shell-plugin-next": "^2.2.2"
+      }
+    },
+    "node_modules/@discoveryjs/json-ext": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz",
+      "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@types/eslint": {
+      "version": "7.2.14",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.14.tgz",
+      "integrity": "sha512-pESyhSbUOskqrGcaN+bCXIQDyT5zTaRWfj5ZjjSlMatgGjIn3QQPfocAu4WSabUR7CGyLZ2CQaZyISOEX7/saw==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "*",
+        "@types/json-schema": "*"
+      }
+    },
+    "node_modules/@types/eslint-scope": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz",
+      "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==",
+      "dev": true,
+      "dependencies": {
+        "@types/eslint": "*",
+        "@types/estree": "*"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "0.0.50",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
+      "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
+      "dev": true
+    },
+    "node_modules/@types/glob": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz",
+      "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==",
+      "dev": true,
+      "dependencies": {
+        "@types/minimatch": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.8",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz",
+      "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
+      "dev": true
+    },
+    "node_modules/@types/minimatch": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
+      "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
+      "dev": true
+    },
+    "node_modules/@types/node": {
+      "version": "16.3.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.1.tgz",
+      "integrity": "sha512-N87VuQi7HEeRJkhzovao/JviiqKjDKMVKxKMfUvSKw+MbkbW8R0nA3fi/MQhhlxV2fQ+2ReM+/Nt4efdrJx3zA==",
+      "dev": true
+    },
+    "node_modules/@webassemblyjs/ast": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
+      "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==",
+      "dev": true,
+      "dependencies": {
+        "@webassemblyjs/helper-numbers": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz",
+      "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==",
+      "dev": true
+    },
+    "node_modules/@webassemblyjs/helper-api-error": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz",
+      "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==",
+      "dev": true
+    },
+    "node_modules/@webassemblyjs/helper-buffer": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz",
+      "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==",
+      "dev": true
+    },
+    "node_modules/@webassemblyjs/helper-numbers": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz",
+      "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==",
+      "dev": true,
+      "dependencies": {
+        "@webassemblyjs/floating-point-hex-parser": "1.11.1",
+        "@webassemblyjs/helper-api-error": "1.11.1",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz",
+      "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==",
+      "dev": true
+    },
+    "node_modules/@webassemblyjs/helper-wasm-section": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz",
+      "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==",
+      "dev": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/ieee754": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz",
+      "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==",
+      "dev": true,
+      "dependencies": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "node_modules/@webassemblyjs/leb128": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz",
+      "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==",
+      "dev": true,
+      "dependencies": {
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/utf8": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz",
+      "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==",
+      "dev": true
+    },
+    "node_modules/@webassemblyjs/wasm-edit": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz",
+      "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==",
+      "dev": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/helper-wasm-section": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1",
+        "@webassemblyjs/wasm-opt": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1",
+        "@webassemblyjs/wast-printer": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-gen": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz",
+      "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==",
+      "dev": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/ieee754": "1.11.1",
+        "@webassemblyjs/leb128": "1.11.1",
+        "@webassemblyjs/utf8": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-opt": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
+      "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
+      "dev": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-parser": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz",
+      "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==",
+      "dev": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-api-error": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/ieee754": "1.11.1",
+        "@webassemblyjs/leb128": "1.11.1",
+        "@webassemblyjs/utf8": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wast-printer": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz",
+      "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==",
+      "dev": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webpack-cli/configtest": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.4.tgz",
+      "integrity": "sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ==",
+      "dev": true,
+      "peerDependencies": {
+        "webpack": "4.x.x || 5.x.x",
+        "webpack-cli": "4.x.x"
+      }
+    },
+    "node_modules/@webpack-cli/info": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.3.0.tgz",
+      "integrity": "sha512-ASiVB3t9LOKHs5DyVUcxpraBXDOKubYu/ihHhU+t1UPpxsivg6Od2E2qU4gJCekfEddzRBzHhzA/Acyw/mlK/w==",
+      "dev": true,
+      "dependencies": {
+        "envinfo": "^7.7.3"
+      },
+      "peerDependencies": {
+        "webpack-cli": "4.x.x"
+      }
+    },
+    "node_modules/@webpack-cli/serve": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.1.tgz",
+      "integrity": "sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw==",
+      "dev": true,
+      "peerDependencies": {
+        "webpack-cli": "4.x.x"
+      },
+      "peerDependenciesMeta": {
+        "webpack-dev-server": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@xtuc/ieee754": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+      "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+      "dev": true
+    },
+    "node_modules/@xtuc/long": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+      "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+      "dev": true
+    },
+    "node_modules/acorn": {
+      "version": "8.4.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz",
+      "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==",
+      "dev": true,
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-keywords": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+      "dev": true,
+      "peerDependencies": {
+        "ajv": "^6.9.1"
+      }
+    },
+    "node_modules/array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "dependencies": {
+        "fill-range": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.16.6",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
+      "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
+      "dev": true,
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001219",
+        "colorette": "^1.2.2",
+        "electron-to-chromium": "^1.3.723",
+        "escalade": "^3.1.1",
+        "node-releases": "^1.1.71"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/browserslist"
+      }
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+      "dev": true
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001243",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
+      "integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
+      "dev": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/browserslist"
+      }
+    },
+    "node_modules/chrome-trace-event": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
+      "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
+      "dev": true,
+      "engines": {
+        "node": ">=6.0"
+      }
+    },
+    "node_modules/clean-webpack-plugin": {
+      "version": "4.0.0-alpha.0",
+      "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0-alpha.0.tgz",
+      "integrity": "sha512-+X6mASBbGSVyw8L9/1rhQ+vS4uaQMopf194kX7Aes8qfezgCFL+qv5W0nwP3a0Tud5kUckARk8tFcoyOSKEjhg==",
+      "dev": true,
+      "dependencies": {
+        "del": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "webpack": ">=4.0.0 <6.0.0"
+      }
+    },
+    "node_modules/clone-deep": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+      "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+      "dev": true,
+      "dependencies": {
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.2",
+        "shallow-clone": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/colorette": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
+      "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
+      "dev": true
+    },
+    "node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "dev": true
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "node_modules/copy-webpack-plugin": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.0.1.tgz",
+      "integrity": "sha512-14gHKKdYIxF84jCEgPgYXCPpldbwpxxLbCmA7LReY7gvbaT555DgeBWBgBZM116tv/fO6RRJrsivBqRyRlukhw==",
+      "dev": true,
+      "dependencies": {
+        "fast-glob": "^3.2.5",
+        "glob-parent": "^6.0.0",
+        "globby": "^11.0.3",
+        "normalize-path": "^3.0.0",
+        "p-limit": "^3.1.0",
+        "schema-utils": "^3.0.0",
+        "serialize-javascript": "^6.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.1.0"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dev": true,
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/del": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
+      "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/glob": "^7.1.1",
+        "globby": "^6.1.0",
+        "is-path-cwd": "^2.0.0",
+        "is-path-in-cwd": "^2.0.0",
+        "p-map": "^2.0.0",
+        "pify": "^4.0.1",
+        "rimraf": "^2.6.3"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/del/node_modules/array-union": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "dev": true,
+      "dependencies": {
+        "array-uniq": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/del/node_modules/globby": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+      "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+      "dev": true,
+      "dependencies": {
+        "array-union": "^1.0.1",
+        "glob": "^7.0.3",
+        "object-assign": "^4.0.1",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/del/node_modules/globby/node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.3.772",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.772.tgz",
+      "integrity": "sha512-X/6VRCXWALzdX+RjCtBU6cyg8WZgoxm9YA02COmDOiNJEZ59WkQggDbWZ4t/giHi/3GS+cvdrP6gbLISANAGYA==",
+      "dev": true
+    },
+    "node_modules/enhanced-resolve": {
+      "version": "5.8.2",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
+      "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==",
+      "dev": true,
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/envinfo": {
+      "version": "7.8.1",
+      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz",
+      "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==",
+      "dev": true,
+      "bin": {
+        "envinfo": "dist/cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.7.1.tgz",
+      "integrity": "sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==",
+      "dev": true
+    },
+    "node_modules/escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+      "dev": true,
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esrecurse/node_modules/estraverse": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+      "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true,
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.x"
+      }
+    },
+    "node_modules/execa": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+      "dev": true,
+      "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"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
+    },
+    "node_modules/fast-glob": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
+      "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "node_modules/fastest-levenshtein": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
+      "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==",
+      "dev": true
+    },
+    "node_modules/fastq": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz",
+      "integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==",
+      "dev": true,
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/glob": {
+      "version": "7.1.7",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+      "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.0.tgz",
+      "integrity": "sha512-Hdd4287VEJcZXUwv1l8a+vXC1GjOQqXe+VS30w/ypihpcnu9M1n3xeYeJu5CBpeEQj2nAab2xxz28GuA3vp4Ww==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/glob-to-regexp": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+      "dev": true
+    },
+    "node_modules/globby": {
+      "version": "11.0.4",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
+      "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
+      "dev": true,
+      "dependencies": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.1.1",
+        "ignore": "^5.1.4",
+        "merge2": "^1.3.0",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+      "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
+      "dev": true
+    },
+    "node_modules/has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/human-signals": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.17.0"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.1.8",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
+      "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-local": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz",
+      "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==",
+      "dev": true,
+      "dependencies": {
+        "pkg-dir": "^4.2.0",
+        "resolve-cwd": "^3.0.0"
+      },
+      "bin": {
+        "import-local-fixture": "fixtures/cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+      "dev": true
+    },
+    "node_modules/interpret": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
+      "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+      "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+      "dev": true,
+      "dependencies": {
+        "has": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+      "dev": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-path-cwd": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+      "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/is-path-in-cwd": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
+      "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
+      "dev": true,
+      "dependencies": {
+        "is-path-inside": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
+      "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
+      "dev": true,
+      "dependencies": {
+        "path-is-inside": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "dependencies": {
+        "isobject": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+      "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "node_modules/isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/jest-worker": {
+      "version": "27.0.6",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.6.tgz",
+      "integrity": "sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*",
+        "merge-stream": "^2.0.0",
+        "supports-color": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "node_modules/json5": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
+      "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
+      "dev": true,
+      "dependencies": {
+        "minimist": "^1.2.5"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/loader-runner": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz",
+      "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6.11.5"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+      "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+      "dev": true,
+      "dependencies": {
+        "braces": "^3.0.1",
+        "picomatch": "^2.2.3"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.48.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
+      "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.31",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
+      "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
+      "dev": true,
+      "dependencies": {
+        "mime-db": "1.48.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true
+    },
+    "node_modules/node-releases": {
+      "version": "1.1.73",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
+      "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
+      "dev": true
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/npm-run-path": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+      "dev": true,
+      "dependencies": {
+        "path-key": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "dev": true,
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-locate/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-map": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+      "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path": {
+      "version": "0.12.7",
+      "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+      "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
+      "dev": true,
+      "dependencies": {
+        "process": "^0.11.1",
+        "util": "^0.10.3"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
+      "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "dependencies": {
+        "pinkie": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pkg-dir": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+      "dev": true,
+      "dependencies": {
+        "find-up": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "node_modules/rechoir": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz",
+      "integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==",
+      "dev": true,
+      "dependencies": {
+        "resolve": "^1.9.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.20.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+      "dev": true,
+      "dependencies": {
+        "is-core-module": "^2.2.0",
+        "path-parse": "^1.0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-cwd": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+      "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+      "dev": true,
+      "dependencies": {
+        "resolve-from": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true,
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+      "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/schema-utils": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.0.tgz",
+      "integrity": "sha512-tTEaeYkyIhEZ9uWgAjDerWov3T9MgX8dhhy2r0IGeeX4W8ngtGl1++dUve/RUqzuaASSh7shwCDJjEzthxki8w==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.7",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/serialize-javascript": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+      "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+      "dev": true,
+      "dependencies": {
+        "randombytes": "^2.1.0"
+      }
+    },
+    "node_modules/shallow-clone": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+      "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+      "dev": true,
+      "dependencies": {
+        "kind-of": "^6.0.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+      "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
+      "dev": true
+    },
+    "node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/source-list-map": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+      "dev": true
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.19",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+      "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+      "dev": true,
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/tapable": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
+      "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/terser": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.1.tgz",
+      "integrity": "sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==",
+      "dev": true,
+      "dependencies": {
+        "commander": "^2.20.0",
+        "source-map": "~0.7.2",
+        "source-map-support": "~0.5.19"
+      },
+      "bin": {
+        "terser": "bin/terser"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/terser-webpack-plugin": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz",
+      "integrity": "sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==",
+      "dev": true,
+      "dependencies": {
+        "jest-worker": "^27.0.2",
+        "p-limit": "^3.1.0",
+        "schema-utils": "^3.0.0",
+        "serialize-javascript": "^6.0.0",
+        "source-map": "^0.6.1",
+        "terser": "^5.7.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.1.0"
+      }
+    },
+    "node_modules/terser/node_modules/source-map": {
+      "version": "0.7.3",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+      "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/util": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+      "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "2.0.3"
+      }
+    },
+    "node_modules/v8-compile-cache": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+      "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+      "dev": true
+    },
+    "node_modules/watchpack": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz",
+      "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==",
+      "dev": true,
+      "dependencies": {
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.1.2"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/webpack": {
+      "version": "5.44.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.44.0.tgz",
+      "integrity": "sha512-I1S1w4QLoKmH19pX6YhYN0NiSXaWY8Ou00oA+aMcr9IUGeF5azns+IKBkfoAAG9Bu5zOIzZt/mN35OffBya8AQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/eslint-scope": "^3.7.0",
+        "@types/estree": "^0.0.50",
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/wasm-edit": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1",
+        "acorn": "^8.4.1",
+        "browserslist": "^4.14.5",
+        "chrome-trace-event": "^1.0.2",
+        "enhanced-resolve": "^5.8.0",
+        "es-module-lexer": "^0.7.1",
+        "eslint-scope": "5.1.1",
+        "events": "^3.2.0",
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.2.4",
+        "json-parse-better-errors": "^1.0.2",
+        "loader-runner": "^4.2.0",
+        "mime-types": "^2.1.27",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^3.0.0",
+        "tapable": "^2.1.1",
+        "terser-webpack-plugin": "^5.1.3",
+        "watchpack": "^2.2.0",
+        "webpack-sources": "^2.3.0"
+      },
+      "bin": {
+        "webpack": "bin/webpack.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependenciesMeta": {
+        "webpack-cli": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-cli": {
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.7.2.tgz",
+      "integrity": "sha512-mEoLmnmOIZQNiRl0ebnjzQ74Hk0iKS5SiEEnpq3dRezoyR3yPaeQZCMCe+db4524pj1Pd5ghZXjT41KLzIhSLw==",
+      "dev": true,
+      "dependencies": {
+        "@discoveryjs/json-ext": "^0.5.0",
+        "@webpack-cli/configtest": "^1.0.4",
+        "@webpack-cli/info": "^1.3.0",
+        "@webpack-cli/serve": "^1.5.1",
+        "colorette": "^1.2.1",
+        "commander": "^7.0.0",
+        "execa": "^5.0.0",
+        "fastest-levenshtein": "^1.0.12",
+        "import-local": "^3.0.2",
+        "interpret": "^2.2.0",
+        "rechoir": "^0.7.0",
+        "v8-compile-cache": "^2.2.0",
+        "webpack-merge": "^5.7.3"
+      },
+      "bin": {
+        "webpack-cli": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "peerDependencies": {
+        "webpack": "4.x.x || 5.x.x"
+      },
+      "peerDependenciesMeta": {
+        "@webpack-cli/generators": {
+          "optional": true
+        },
+        "@webpack-cli/migrate": {
+          "optional": true
+        },
+        "webpack-bundle-analyzer": {
+          "optional": true
+        },
+        "webpack-dev-server": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-cli/node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/webpack-merge": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz",
+      "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==",
+      "dev": true,
+      "dependencies": {
+        "clone-deep": "^4.0.1",
+        "wildcard": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/webpack-shell-plugin-next": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/webpack-shell-plugin-next/-/webpack-shell-plugin-next-2.2.2.tgz",
+      "integrity": "sha512-2HeC1e0cCvTgA3SD4XPAD69DnILM92mCschrtKdlLRORHCR+oG7xnibQMkVySxTAYWTQOcBRHlqa88x4syWwhQ==",
+      "dev": true,
+      "peerDependencies": {
+        "webpack": "^5.18.0"
+      }
+    },
+    "node_modules/webpack-sources": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz",
+      "integrity": "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==",
+      "dev": true,
+      "dependencies": {
+        "source-list-map": "^2.0.1",
+        "source-map": "^0.6.1"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wildcard": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
+      "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
+      "dev": true
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  },
+  "dependencies": {
+    "@discoveryjs/json-ext": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz",
+      "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==",
+      "dev": true
+    },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true
+    },
+    "@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      }
+    },
+    "@types/eslint": {
+      "version": "7.2.14",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.14.tgz",
+      "integrity": "sha512-pESyhSbUOskqrGcaN+bCXIQDyT5zTaRWfj5ZjjSlMatgGjIn3QQPfocAu4WSabUR7CGyLZ2CQaZyISOEX7/saw==",
+      "dev": true,
+      "requires": {
+        "@types/estree": "*",
+        "@types/json-schema": "*"
+      }
+    },
+    "@types/eslint-scope": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz",
+      "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==",
+      "dev": true,
+      "requires": {
+        "@types/eslint": "*",
+        "@types/estree": "*"
+      }
+    },
+    "@types/estree": {
+      "version": "0.0.50",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
+      "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
+      "dev": true
+    },
+    "@types/glob": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz",
+      "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==",
+      "dev": true,
+      "requires": {
+        "@types/minimatch": "*",
+        "@types/node": "*"
+      }
+    },
+    "@types/json-schema": {
+      "version": "7.0.8",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz",
+      "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
+      "dev": true
+    },
+    "@types/minimatch": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
+      "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "16.3.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.1.tgz",
+      "integrity": "sha512-N87VuQi7HEeRJkhzovao/JviiqKjDKMVKxKMfUvSKw+MbkbW8R0nA3fi/MQhhlxV2fQ+2ReM+/Nt4efdrJx3zA==",
+      "dev": true
+    },
+    "@webassemblyjs/ast": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
+      "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/helper-numbers": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1"
+      }
+    },
+    "@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz",
+      "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-api-error": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz",
+      "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-buffer": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz",
+      "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-numbers": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz",
+      "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/floating-point-hex-parser": "1.11.1",
+        "@webassemblyjs/helper-api-error": "1.11.1",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz",
+      "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-wasm-section": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz",
+      "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1"
+      }
+    },
+    "@webassemblyjs/ieee754": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz",
+      "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==",
+      "dev": true,
+      "requires": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "@webassemblyjs/leb128": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz",
+      "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==",
+      "dev": true,
+      "requires": {
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "@webassemblyjs/utf8": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz",
+      "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==",
+      "dev": true
+    },
+    "@webassemblyjs/wasm-edit": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz",
+      "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/helper-wasm-section": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1",
+        "@webassemblyjs/wasm-opt": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1",
+        "@webassemblyjs/wast-printer": "1.11.1"
+      }
+    },
+    "@webassemblyjs/wasm-gen": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz",
+      "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/ieee754": "1.11.1",
+        "@webassemblyjs/leb128": "1.11.1",
+        "@webassemblyjs/utf8": "1.11.1"
+      }
+    },
+    "@webassemblyjs/wasm-opt": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
+      "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1"
+      }
+    },
+    "@webassemblyjs/wasm-parser": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz",
+      "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-api-error": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/ieee754": "1.11.1",
+        "@webassemblyjs/leb128": "1.11.1",
+        "@webassemblyjs/utf8": "1.11.1"
+      }
+    },
+    "@webassemblyjs/wast-printer": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz",
+      "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "@webpack-cli/configtest": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.4.tgz",
+      "integrity": "sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ==",
+      "dev": true,
+      "requires": {}
+    },
+    "@webpack-cli/info": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.3.0.tgz",
+      "integrity": "sha512-ASiVB3t9LOKHs5DyVUcxpraBXDOKubYu/ihHhU+t1UPpxsivg6Od2E2qU4gJCekfEddzRBzHhzA/Acyw/mlK/w==",
+      "dev": true,
+      "requires": {
+        "envinfo": "^7.7.3"
+      }
+    },
+    "@webpack-cli/serve": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.1.tgz",
+      "integrity": "sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw==",
+      "dev": true,
+      "requires": {}
+    },
+    "@xtuc/ieee754": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+      "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+      "dev": true
+    },
+    "@xtuc/long": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+      "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+      "dev": true
+    },
+    "acorn": {
+      "version": "8.4.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz",
+      "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==",
+      "dev": true
+    },
+    "ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ajv-keywords": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+      "dev": true,
+      "requires": {}
+    },
+    "array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "browserslist": {
+      "version": "4.16.6",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
+      "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30001219",
+        "colorette": "^1.2.2",
+        "electron-to-chromium": "^1.3.723",
+        "escalade": "^3.1.1",
+        "node-releases": "^1.1.71"
+      }
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+      "dev": true
+    },
+    "caniuse-lite": {
+      "version": "1.0.30001243",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
+      "integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
+      "dev": true
+    },
+    "chrome-trace-event": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
+      "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
+      "dev": true
+    },
+    "clean-webpack-plugin": {
+      "version": "4.0.0-alpha.0",
+      "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0-alpha.0.tgz",
+      "integrity": "sha512-+X6mASBbGSVyw8L9/1rhQ+vS4uaQMopf194kX7Aes8qfezgCFL+qv5W0nwP3a0Tud5kUckARk8tFcoyOSKEjhg==",
+      "dev": true,
+      "requires": {
+        "del": "^4.1.1"
+      }
+    },
+    "clone-deep": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+      "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+      "dev": true,
+      "requires": {
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.2",
+        "shallow-clone": "^3.0.0"
+      }
+    },
+    "colorette": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
+      "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
+      "dev": true
+    },
+    "commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "dev": true
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "copy-webpack-plugin": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.0.1.tgz",
+      "integrity": "sha512-14gHKKdYIxF84jCEgPgYXCPpldbwpxxLbCmA7LReY7gvbaT555DgeBWBgBZM116tv/fO6RRJrsivBqRyRlukhw==",
+      "dev": true,
+      "requires": {
+        "fast-glob": "^3.2.5",
+        "glob-parent": "^6.0.0",
+        "globby": "^11.0.3",
+        "normalize-path": "^3.0.0",
+        "p-limit": "^3.1.0",
+        "schema-utils": "^3.0.0",
+        "serialize-javascript": "^6.0.0"
+      }
+    },
+    "cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dev": true,
+      "requires": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      }
+    },
+    "del": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
+      "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
+      "dev": true,
+      "requires": {
+        "@types/glob": "^7.1.1",
+        "globby": "^6.1.0",
+        "is-path-cwd": "^2.0.0",
+        "is-path-in-cwd": "^2.0.0",
+        "p-map": "^2.0.0",
+        "pify": "^4.0.1",
+        "rimraf": "^2.6.3"
+      },
+      "dependencies": {
+        "array-union": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+          "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+          "dev": true,
+          "requires": {
+            "array-uniq": "^1.0.1"
+          }
+        },
+        "globby": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+          "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+          "dev": true,
+          "requires": {
+            "array-union": "^1.0.1",
+            "glob": "^7.0.3",
+            "object-assign": "^4.0.1",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "2.3.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+              "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+              "dev": true
+            }
+          }
+        }
+      }
+    },
+    "dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "requires": {
+        "path-type": "^4.0.0"
+      }
+    },
+    "electron-to-chromium": {
+      "version": "1.3.772",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.772.tgz",
+      "integrity": "sha512-X/6VRCXWALzdX+RjCtBU6cyg8WZgoxm9YA02COmDOiNJEZ59WkQggDbWZ4t/giHi/3GS+cvdrP6gbLISANAGYA==",
+      "dev": true
+    },
+    "enhanced-resolve": {
+      "version": "5.8.2",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
+      "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.2.0"
+      }
+    },
+    "envinfo": {
+      "version": "7.8.1",
+      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz",
+      "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==",
+      "dev": true
+    },
+    "es-module-lexer": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.7.1.tgz",
+      "integrity": "sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==",
+      "dev": true
+    },
+    "escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "dev": true
+    },
+    "eslint-scope": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.2.0"
+      },
+      "dependencies": {
+        "estraverse": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+          "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+          "dev": true
+        }
+      }
+    },
+    "estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true
+    },
+    "events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "dev": true
+    },
+    "execa": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+      "dev": true,
+      "requires": {
+        "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"
+      }
+    },
+    "fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
+    },
+    "fast-glob": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
+      "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+          "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+          "dev": true,
+          "requires": {
+            "is-glob": "^4.0.1"
+          }
+        }
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "fastest-levenshtein": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
+      "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==",
+      "dev": true
+    },
+    "fastq": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz",
+      "integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==",
+      "dev": true,
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "requires": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "dev": true
+    },
+    "glob": {
+      "version": "7.1.7",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+      "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.0.tgz",
+      "integrity": "sha512-Hdd4287VEJcZXUwv1l8a+vXC1GjOQqXe+VS30w/ypihpcnu9M1n3xeYeJu5CBpeEQj2nAab2xxz28GuA3vp4Ww==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "glob-to-regexp": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+      "dev": true
+    },
+    "globby": {
+      "version": "11.0.4",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
+      "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
+      "dev": true,
+      "requires": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.1.1",
+        "ignore": "^5.1.4",
+        "merge2": "^1.3.0",
+        "slash": "^3.0.0"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+      "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
+      "dev": true
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true
+    },
+    "human-signals": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+      "dev": true
+    },
+    "ignore": {
+      "version": "5.1.8",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
+      "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
+      "dev": true
+    },
+    "import-local": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz",
+      "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==",
+      "dev": true,
+      "requires": {
+        "pkg-dir": "^4.2.0",
+        "resolve-cwd": "^3.0.0"
+      }
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+      "dev": true
+    },
+    "interpret": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
+      "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
+      "dev": true
+    },
+    "is-core-module": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+      "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.3"
+      }
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true
+    },
+    "is-path-cwd": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+      "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
+      "dev": true
+    },
+    "is-path-in-cwd": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
+      "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
+      "dev": true,
+      "requires": {
+        "is-path-inside": "^2.1.0"
+      }
+    },
+    "is-path-inside": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
+      "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
+      "dev": true,
+      "requires": {
+        "path-is-inside": "^1.0.2"
+      }
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "is-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+      "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
+      "dev": true
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true
+    },
+    "jest-worker": {
+      "version": "27.0.6",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.6.tgz",
+      "integrity": "sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*",
+        "merge-stream": "^2.0.0",
+        "supports-color": "^8.0.0"
+      }
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "json5": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
+      "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
+    "kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "dev": true
+    },
+    "loader-runner": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz",
+      "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==",
+      "dev": true
+    },
+    "locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "requires": {
+        "p-locate": "^4.1.0"
+      }
+    },
+    "merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true
+    },
+    "merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+      "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+      "dev": true,
+      "requires": {
+        "braces": "^3.0.1",
+        "picomatch": "^2.2.3"
+      }
+    },
+    "mime-db": {
+      "version": "1.48.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
+      "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==",
+      "dev": true
+    },
+    "mime-types": {
+      "version": "2.1.31",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
+      "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
+      "dev": true,
+      "requires": {
+        "mime-db": "1.48.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
+    },
+    "neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true
+    },
+    "node-releases": {
+      "version": "1.1.73",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
+      "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
+      "dev": true
+    },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true
+    },
+    "npm-run-path": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+      "dev": true,
+      "requires": {
+        "path-key": "^3.0.0"
+      }
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+      "dev": true
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^2.1.0"
+      }
+    },
+    "p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "requires": {
+        "yocto-queue": "^0.1.0"
+      }
+    },
+    "p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "requires": {
+        "p-limit": "^2.2.0"
+      },
+      "dependencies": {
+        "p-limit": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        }
+      }
+    },
+    "p-map": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+      "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+      "dev": true
+    },
+    "p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true
+    },
+    "path": {
+      "version": "0.12.7",
+      "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+      "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
+      "dev": true,
+      "requires": {
+        "process": "^0.11.1",
+        "util": "^0.10.3"
+      }
+    },
+    "path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true
+    },
+    "path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true
+    },
+    "picomatch": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
+      "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+      "dev": true
+    },
+    "pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "dev": true
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "requires": {
+        "pinkie": "^2.0.0"
+      }
+    },
+    "pkg-dir": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+      "dev": true,
+      "requires": {
+        "find-up": "^4.0.0"
+      }
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true
+    },
+    "queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true
+    },
+    "randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "rechoir": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz",
+      "integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==",
+      "dev": true,
+      "requires": {
+        "resolve": "^1.9.0"
+      }
+    },
+    "resolve": {
+      "version": "1.20.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+      "dev": true,
+      "requires": {
+        "is-core-module": "^2.2.0",
+        "path-parse": "^1.0.6"
+      }
+    },
+    "resolve-cwd": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+      "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+      "dev": true,
+      "requires": {
+        "resolve-from": "^5.0.0"
+      }
+    },
+    "resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true
+    },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+      "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "requires": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "dev": true
+    },
+    "schema-utils": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.0.tgz",
+      "integrity": "sha512-tTEaeYkyIhEZ9uWgAjDerWov3T9MgX8dhhy2r0IGeeX4W8ngtGl1++dUve/RUqzuaASSh7shwCDJjEzthxki8w==",
+      "dev": true,
+      "requires": {
+        "@types/json-schema": "^7.0.7",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      }
+    },
+    "serialize-javascript": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+      "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+      "dev": true,
+      "requires": {
+        "randombytes": "^2.1.0"
+      }
+    },
+    "shallow-clone": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+      "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+      "dev": true,
+      "requires": {
+        "kind-of": "^6.0.2"
+      }
+    },
+    "shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^3.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+      "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
+      "dev": true
+    },
+    "slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true
+    },
+    "source-list-map": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+      "dev": true
+    },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true
+    },
+    "source-map-support": {
+      "version": "0.5.19",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+      "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true
+    },
+    "supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^4.0.0"
+      }
+    },
+    "tapable": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
+      "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
+      "dev": true
+    },
+    "terser": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.1.tgz",
+      "integrity": "sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==",
+      "dev": true,
+      "requires": {
+        "commander": "^2.20.0",
+        "source-map": "~0.7.2",
+        "source-map-support": "~0.5.19"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.7.3",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+          "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
+          "dev": true
+        }
+      }
+    },
+    "terser-webpack-plugin": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz",
+      "integrity": "sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==",
+      "dev": true,
+      "requires": {
+        "jest-worker": "^27.0.2",
+        "p-limit": "^3.1.0",
+        "schema-utils": "^3.0.0",
+        "serialize-javascript": "^6.0.0",
+        "source-map": "^0.6.1",
+        "terser": "^5.7.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "util": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+      "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "v8-compile-cache": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+      "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+      "dev": true
+    },
+    "watchpack": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz",
+      "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==",
+      "dev": true,
+      "requires": {
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.1.2"
+      }
+    },
+    "webpack": {
+      "version": "5.44.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.44.0.tgz",
+      "integrity": "sha512-I1S1w4QLoKmH19pX6YhYN0NiSXaWY8Ou00oA+aMcr9IUGeF5azns+IKBkfoAAG9Bu5zOIzZt/mN35OffBya8AQ==",
+      "dev": true,
+      "requires": {
+        "@types/eslint-scope": "^3.7.0",
+        "@types/estree": "^0.0.50",
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/wasm-edit": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1",
+        "acorn": "^8.4.1",
+        "browserslist": "^4.14.5",
+        "chrome-trace-event": "^1.0.2",
+        "enhanced-resolve": "^5.8.0",
+        "es-module-lexer": "^0.7.1",
+        "eslint-scope": "5.1.1",
+        "events": "^3.2.0",
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.2.4",
+        "json-parse-better-errors": "^1.0.2",
+        "loader-runner": "^4.2.0",
+        "mime-types": "^2.1.27",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^3.0.0",
+        "tapable": "^2.1.1",
+        "terser-webpack-plugin": "^5.1.3",
+        "watchpack": "^2.2.0",
+        "webpack-sources": "^2.3.0"
+      }
+    },
+    "webpack-cli": {
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.7.2.tgz",
+      "integrity": "sha512-mEoLmnmOIZQNiRl0ebnjzQ74Hk0iKS5SiEEnpq3dRezoyR3yPaeQZCMCe+db4524pj1Pd5ghZXjT41KLzIhSLw==",
+      "dev": true,
+      "requires": {
+        "@discoveryjs/json-ext": "^0.5.0",
+        "@webpack-cli/configtest": "^1.0.4",
+        "@webpack-cli/info": "^1.3.0",
+        "@webpack-cli/serve": "^1.5.1",
+        "colorette": "^1.2.1",
+        "commander": "^7.0.0",
+        "execa": "^5.0.0",
+        "fastest-levenshtein": "^1.0.12",
+        "import-local": "^3.0.2",
+        "interpret": "^2.2.0",
+        "rechoir": "^0.7.0",
+        "v8-compile-cache": "^2.2.0",
+        "webpack-merge": "^5.7.3"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+          "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+          "dev": true
+        }
+      }
+    },
+    "webpack-merge": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz",
+      "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==",
+      "dev": true,
+      "requires": {
+        "clone-deep": "^4.0.1",
+        "wildcard": "^2.0.0"
+      }
+    },
+    "webpack-shell-plugin-next": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/webpack-shell-plugin-next/-/webpack-shell-plugin-next-2.2.2.tgz",
+      "integrity": "sha512-2HeC1e0cCvTgA3SD4XPAD69DnILM92mCschrtKdlLRORHCR+oG7xnibQMkVySxTAYWTQOcBRHlqa88x4syWwhQ==",
+      "dev": true,
+      "requires": {}
+    },
+    "webpack-sources": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz",
+      "integrity": "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==",
+      "dev": true,
+      "requires": {
+        "source-list-map": "^2.0.1",
+        "source-map": "^0.6.1"
+      }
+    },
+    "which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "wildcard": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
+      "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
+      "dev": true
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0562556
--- /dev/null
+++ b/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "twpowertools",
+  "version": "0.0.0",
+  "description": "An extension which brings several enhancements to the Google Forums and the Community Console.",
+  "directories": {
+    "doc": "docs"
+  },
+  "scripts": {},
+  "repository": {
+    "type": "git",
+    "url": "https://gerrit.avm99963.com/infinitegforums"
+  },
+  "author": "Adrià Vilanova Martínez (avm99963)",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://bugs.avm99963.com/p/twpowertools"
+  },
+  "keywords": [
+    "chrome",
+    "firefox",
+    "extension",
+    "webext",
+    "add-on",
+    "forums",
+    "google"
+  ],
+  "devDependencies": {
+    "clean-webpack-plugin": "^4.0.0-alpha.0",
+    "copy-webpack-plugin": "^9.0.1",
+    "json5": "^2.2.0",
+    "path": "^0.12.7",
+    "webpack": "^5.44.0",
+    "webpack-cli": "^4.7.2",
+    "webpack-shell-plugin-next": "^2.2.2"
+  },
+  "private": true
+}
diff --git a/playbooks/lint/run.yaml b/playbooks/lint/run.yaml
index c4dba19..fd6ff96 100644
--- a/playbooks/lint/run.yaml
+++ b/playbooks/lint/run.yaml
@@ -1,6 +1,7 @@
 - hosts: all
   roles:
-    - role: generate-ext-manifest
-      target_browser: "GECKO"
+    - role: build-extension
+      make_target: build_test_extension
     - role: web-ext-lint
+      source_dir: "dist/gecko"
       extra_lint_flags: "--ignore-files sw.js"
diff --git a/roles/build-extension/defaults/main.yaml b/roles/build-extension/defaults/main.yaml
new file mode 100644
index 0000000..2774de6
--- /dev/null
+++ b/roles/build-extension/defaults/main.yaml
@@ -0,0 +1 @@
+make_target: chromium_stable
diff --git a/roles/build-extension/tasks/main.yaml b/roles/build-extension/tasks/main.yaml
new file mode 100644
index 0000000..7759c47
--- /dev/null
+++ b/roles/build-extension/tasks/main.yaml
@@ -0,0 +1,6 @@
+- name: Build extension
+  make:
+    chdir: "{{ zuul.project.src_dir }}"
+    target: "{{ make_target }}"
+  environment:
+    PATH: /usr/local/go/bin:{{ ansible_env.PATH }}
diff --git a/roles/generate-ext-manifest/defaults/main.yaml b/roles/generate-ext-manifest/defaults/main.yaml
deleted file mode 100644
index 8aa92ec..0000000
--- a/roles/generate-ext-manifest/defaults/main.yaml
+++ /dev/null
@@ -1 +0,0 @@
-target_browser: CHROMIUM
diff --git a/roles/generate-ext-manifest/tasks/main.yaml b/roles/generate-ext-manifest/tasks/main.yaml
deleted file mode 100644
index 28fedba..0000000
--- a/roles/generate-ext-manifest/tasks/main.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-- name: Generate extension manifest
-  include_role:
-    name: go
-  vars:
-    go_command: "run generateManifest.go {{target_browser}}"
-    zuul_work_dir: "{{ zuul.project.src_dir }}"
diff --git a/src/background.js b/src/background.js
index 8d0f411..15f62b9 100644
--- a/src/background.js
+++ b/src/background.js
@@ -1,4 +1,5 @@
 // IMPORTANT: keep this file in sync with sw.js
+import {cleanUpOptions} from './common/optionsUtils.js'
 
 // When the extension gets updated, set new options to their default value.
 chrome.runtime.onInstalled.addListener(function(details) {
diff --git a/src/common/api.js b/src/common/api.js
index 20a6c7d..065d84c 100644
--- a/src/common/api.js
+++ b/src/common/api.js
@@ -2,7 +2,7 @@
 
 // Function to wrap calls to the Community Console API with intelligent error
 // handling.
-function CCApi(method, data, authenticated, authuser = 0) {
+export function CCApi(method, data, authenticated, authuser = 0) {
   var authuserPart =
       authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser);
 
diff --git a/src/common/commonUtils.js b/src/common/commonUtils.js
new file mode 100644
index 0000000..ee31037
--- /dev/null
+++ b/src/common/commonUtils.js
@@ -0,0 +1,17 @@
+export function parseUrl(url) {
+  var forum_a = url.match(/forum\/([0-9]+)/i);
+  var thread_a = url.match(/thread\/([0-9]+)/i);
+
+  if (forum_a === null || thread_a === null) {
+    return false;
+  }
+
+  return {
+    'forum': forum_a[1],
+    'thread': thread_a[1],
+  };
+}
+
+export function isEmpty(obj) {
+  return Object.keys(obj).length === 0;
+}
diff --git a/src/common/communityConsoleUtils.js b/src/common/communityConsoleUtils.js
new file mode 100644
index 0000000..05a283c
--- /dev/null
+++ b/src/common/communityConsoleUtils.js
@@ -0,0 +1,13 @@
+// Escapes username from HTML generated by the Community Console.
+export function escapeUsername(username) {
+  var quoteRegex = /"/g;
+  var commentRegex = /<!---->/g;
+  return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
+}
+
+// Retrieves authuser from the data-startup attribute.
+export function getAuthUser() {
+  var startup =
+      JSON.parse(document.querySelector('html').getAttribute('data-startup'));
+  return startup?.[2]?.[1] || '0';
+}
diff --git a/src/common/content_scripts.js b/src/common/contentScriptsUtils.js
similarity index 60%
rename from src/common/content_scripts.js
rename to src/common/contentScriptsUtils.js
index 1bc9a7d..00bc556 100644
--- a/src/common/content_scripts.js
+++ b/src/common/contentScriptsUtils.js
@@ -1,4 +1,4 @@
-function injectStylesheet(stylesheetName, attributes = {}) {
+export function injectStylesheet(stylesheetName, attributes = {}) {
   var link = document.createElement('link');
   link.setAttribute('rel', 'stylesheet');
   link.setAttribute('href', stylesheetName);
@@ -6,18 +6,12 @@
   document.head.appendChild(link);
 }
 
-function injectStyles(css) {
+export function injectStyles(css) {
   injectStylesheet('data:text/css;charset=UTF-8,' + encodeURIComponent(css));
 }
 
-function injectScript(scriptName) {
+export function injectScript(scriptName) {
   var script = document.createElement('script');
   script.src = scriptName;
   document.head.appendChild(script);
 }
-
-function escapeUsername(username) {
-  var quoteRegex = /"/g;
-  var commentRegex = /<!---->/g;
-  return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
-}
diff --git a/src/common/csEventListener.js b/src/common/csEventListener.js
new file mode 100644
index 0000000..14b4295
--- /dev/null
+++ b/src/common/csEventListener.js
@@ -0,0 +1,43 @@
+// In order to pass i18n strings and settings values to the injected scripts,
+// which don't have access to the chrome.* APIs, we use event listeners.
+
+export function setUpListener() {
+  chrome.storage.sync.get(null, function(options) {
+    window.addEventListener('TWPT_sendRequest', evt => {
+      var request = evt.detail;
+      switch (request.data.action) {
+        case 'geti18nMessage':
+          var data = chrome.i18n.getMessage(
+              request.data.msg,
+              (Array.isArray(request.data.placeholders) ?
+                   request.data.placeholders :
+                   []));
+          break;
+
+        case 'getProfileIndicatorOptions':
+          var data = {
+            'indicatorDot': options.profileindicator,
+            'numPosts': options.profileindicatoralt
+          };
+          break;
+
+        case 'getNumPostMonths':
+          var data = options.profileindicatoralt_months;
+          break;
+
+        default:
+          var data = 'unknownAction';
+          console.warn('Unknown action ' + request.data.action + '.');
+          break;
+      }
+
+      var response = {
+        data,
+        requestId: request.id,
+        prefix: (request.prefix || 'TWPT'),
+      };
+
+      window.postMessage(response, '*');
+    });
+  });
+}
diff --git a/src/common/cs_event_listener.js b/src/common/cs_event_listener.js
deleted file mode 100644
index c7de08f..0000000
--- a/src/common/cs_event_listener.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// In order to pass i18n strings and settings values to the injected scripts,
-// which don't have access to the chrome.* APIs, we use event listeners.
-chrome.storage.sync.get(null, function(options) {
-  window.addEventListener('TWPT_sendRequest', evt => {
-    var request = evt.detail;
-    switch (request.data.action) {
-      case 'geti18nMessage':
-        var data = chrome.i18n.getMessage(
-            request.data.msg,
-            (Array.isArray(request.data.placeholders) ?
-                 request.data.placeholders :
-                 []));
-        break;
-
-      case 'getProfileIndicatorOptions':
-        var data = {
-          'indicatorDot': options.profileindicator,
-          'numPosts': options.profileindicatoralt
-        };
-        break;
-
-      case 'getNumPostMonths':
-        var data = options.profileindicatoralt_months;
-        break;
-
-      default:
-        var data = 'unknownAction';
-        console.warn('Unknown action ' + request.data.action + '.');
-        break;
-    }
-
-    var response = {
-      data,
-      requestId: request.id,
-      prefix: (request.prefix || 'TWPT'),
-    };
-
-    window.postMessage(response, '*');
-  });
-});
diff --git a/src/common/extUtils.js b/src/common/extUtils.js
new file mode 100644
index 0000000..a1b4d48
--- /dev/null
+++ b/src/common/extUtils.js
@@ -0,0 +1,13 @@
+// This method is based on the fact that when building the extension for Firefox
+// the browser_specific_settings.gecko entry is included.
+export function isFirefox() {
+  var manifest = chrome.runtime.getManifest();
+  return manifest.browser_specific_settings !== undefined &&
+      manifest.browser_specific_settings.gecko !== undefined;
+}
+
+// Returns whether the extension is a release version.
+export function isReleaseVersion() {
+  var manifest = chrome.runtime.getManifest();
+  return ('version' in manifest) && manifest.version != '0';
+}
diff --git a/src/common/common.js b/src/common/optionsPrototype.json5
similarity index 61%
rename from src/common/common.js
rename to src/common/optionsPrototype.json5
index 4fcae45..191d0e1 100644
--- a/src/common/common.js
+++ b/src/common/optionsPrototype.json5
@@ -1,4 +1,4 @@
-const optionsPrototype = {
+{
   // Available options:
   'list': {
     defaultValue: true,
@@ -118,53 +118,4 @@
     defaultValue: false,
     context: 'deprecated',
   },
-};
-
-const specialOptions = [
-  'profileindicatoralt_months',
-  'ccdarktheme_mode',
-  'ccdarktheme_switch_enabled',
-  'ccdragndropfix',
-];
-
-function isEmpty(obj) {
-  return Object.keys(obj).length === 0;
-}
-
-// Adds missing options with their default value. If |dryRun| is set to false,
-// they are also saved to the sync storage area.
-function cleanUpOptions(options, dryRun = false) {
-  console.log('[cleanUpOptions] Previous options', JSON.stringify(options));
-
-  if (typeof options !== 'object' || options === null) options = {};
-
-  var ok = true;
-  for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
-    if (!(opt in options)) {
-      ok = false;
-      options[opt] = optMeta['defaultValue'];
-    }
-  }
-
-  console.log('[cleanUpOptions] New options', JSON.stringify(options));
-
-  if (!ok && !dryRun) {
-    chrome.storage.sync.set(options);
-  }
-
-  return options;
-}
-
-// This method is based on the fact that when building the extension for Firefox
-// the browser_specific_settings.gecko entry is included.
-function isFirefox() {
-  var manifest = chrome.runtime.getManifest();
-  return manifest.browser_specific_settings !== undefined &&
-      manifest.browser_specific_settings.gecko !== undefined;
-}
-
-// Returns whether the extension is a release version.
-function isReleaseVersion() {
-  var manifest = chrome.runtime.getManifest();
-  return ('version' in manifest) && manifest.version != '0';
 }
diff --git a/src/common/optionsUtils.js b/src/common/optionsUtils.js
new file mode 100644
index 0000000..0efb6c9
--- /dev/null
+++ b/src/common/optionsUtils.js
@@ -0,0 +1,28 @@
+import optionsPrototype from './optionsPrototype.json5';
+import specialOptions from './specialOptions.json5';
+
+export {optionsPrototype, specialOptions};
+
+// Adds missing options with their default value. If |dryRun| is set to false,
+// they are also saved to the sync storage area.
+export function cleanUpOptions(options, dryRun = false) {
+  console.log('[cleanUpOptions] Previous options', JSON.stringify(options));
+
+  if (typeof options !== 'object' || options === null) options = {};
+
+  var ok = true;
+  for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
+    if (!(opt in options)) {
+      ok = false;
+      options[opt] = optMeta['defaultValue'];
+    }
+  }
+
+  console.log('[cleanUpOptions] New options', JSON.stringify(options));
+
+  if (!ok && !dryRun) {
+    chrome.storage.sync.set(options);
+  }
+
+  return options;
+}
diff --git a/src/common/specialOptions.json5 b/src/common/specialOptions.json5
new file mode 100644
index 0000000..55dde20
--- /dev/null
+++ b/src/common/specialOptions.json5
@@ -0,0 +1,6 @@
+[
+  'profileindicatoralt_months',
+  'ccdarktheme_mode',
+  'ccdarktheme_switch_enabled',
+  'ccdragndropfix',
+]
diff --git a/src/contentScripts/communityConsole/autoRefresh.js b/src/contentScripts/communityConsole/autoRefresh.js
new file mode 100644
index 0000000..c39970f
--- /dev/null
+++ b/src/contentScripts/communityConsole/autoRefresh.js
@@ -0,0 +1,250 @@
+import {createExtBadge} from './utils.js';
+import {getAuthUser} from '../../common/communityConsoleUtils.js';
+
+var authuser = getAuthUser();
+
+export var autoRefresh = {
+  isLookingForUpdates: false,
+  isUpdatePromptShown: false,
+  lastTimestamp: null,
+  filter: null,
+  path: null,
+  snackbar: null,
+  interval: null,
+  firstCallTimeout: null,
+  intervalMs: 3 * 60 * 1000,   // 3 minutes
+  firstCallDelayMs: 3 * 1000,  // 3 seconds
+  getStartupData() {
+    return JSON.parse(
+        document.querySelector('html').getAttribute('data-startup'));
+  },
+  isOrderedByTimestampDescending() {
+    var startup = this.getStartupData();
+    // Returns orderOptions.by == TIMESTAMP && orderOptions.desc == true
+    return (
+        startup?.[1]?.[1]?.[3]?.[14]?.[1] == 1 &&
+        startup?.[1]?.[1]?.[3]?.[14]?.[2] == true);
+  },
+  getCustomFilter(path) {
+    var searchRegex = /^\/s\/community\/search\/([^\/]*)/;
+    var matches = path.match(searchRegex);
+    if (matches !== null && matches.length > 1) {
+      var search = decodeURIComponent(matches[1]);
+      var params = new URLSearchParams(search);
+      return params.get('query') || '';
+    }
+
+    return '';
+  },
+  filterHasOverride(filter, override) {
+    var escapedOverride = override.replace(/([^\w\d\s])/gi, '\\$1');
+    var regex = new RegExp('[^a-zA-Z0-9]?' + escapedOverride + ':');
+    return regex.test(filter);
+  },
+  getFilter(path) {
+    var query = this.getCustomFilter(path);
+
+    // Note: This logic has been copied and adapted from the
+    // _buildQuery$1$threadId function in the Community Console
+    var conditions = '';
+    var startup = this.getStartupData();
+
+    // TODO(avm99963): if the selected forums are changed without reloading the
+    // page, this will get the old selected forums. Fix this.
+    var forums = startup?.[1]?.[1]?.[3]?.[8] ?? [];
+    if (!this.filterHasOverride(query, 'forum') && forums !== null &&
+        forums.length > 0)
+      conditions += ' forum:(' + forums.join(' | ') + ')';
+
+    var langs = startup?.[1]?.[1]?.[3]?.[5] ?? [];
+    if (!this.filterHasOverride(query, 'lang') && langs !== null &&
+        langs.length > 0)
+      conditions += ' lang:(' + langs.map(l => '"' + l + '"').join(' | ') + ')';
+
+    if (query.length !== 0 && conditions.length !== 0)
+      return '(' + query + ')' + conditions;
+    return query + conditions;
+  },
+  getLastTimestamp() {
+    var APIRequestUrl = 'https://support.google.com/s/community/api/ViewForum' +
+        (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
+
+    return fetch(APIRequestUrl, {
+             'headers': {
+               'content-type': 'text/plain; charset=utf-8',
+             },
+             'body': JSON.stringify({
+               1: '0',  // TODO: Change, when only a forum is selected, it
+                        // should be set here
+               2: {
+                 1: {
+                   2: 2,
+                 },
+                 2: {
+                   1: 1,
+                   2: true,
+                 },
+                 12: this.filter,
+               },
+             }),
+             'method': 'POST',
+             'mode': 'cors',
+             'credentials': 'include',
+           })
+        .then(res => {
+          if (res.status == 200 || res.status == 400) {
+            return res.json().then(data => ({
+                                     status: res.status,
+                                     body: data,
+                                   }));
+          } else {
+            throw new Error('Status code ' + res.status + ' was not expected.');
+          }
+        })
+        .then(res => {
+          if (res.status == 400) {
+            throw new Error(
+                res.body[4] ||
+                ('Response status: 400. Error code: ' + res.body[2]));
+          }
+
+          return res.body;
+        })
+        .then(body => {
+          var timestamp = body?.[1]?.[2]?.[0]?.[2]?.[17];
+          if (timestamp === undefined)
+            throw new Error(
+                'Unexpected body of response (' +
+                (body?.[1]?.[2]?.[0] === undefined ?
+                     'no threads were returned' :
+                     'the timestamp value is not present in the first thread') +
+                ').');
+
+          return timestamp;
+        });
+    // TODO(avm99963): Add retry mechanism (sometimes thread lists are empty,
+    // but when loading the next page the thread appears).
+    //
+    // NOTE(avm99963): It seems like loading the first 2 threads instead of only
+    // the first one fixes this (empty lists are now rarely returned).
+  },
+  unregister() {
+    console.debug('autorefresh_list: unregistering');
+
+    if (!this.isLookingForUpdates) return;
+
+    window.clearTimeout(this.firstCallTimeout);
+    window.clearInterval(this.interval);
+    this.isUpdatePromptShown = false;
+    this.isLookingForUpdates = false;
+  },
+  showUpdatePrompt() {
+    this.snackbar.classList.remove('TWPT-hidden');
+    document.title = '[!!!] ' + document.title.replace('[!!!] ', '');
+    this.isUpdatePromptShown = true;
+  },
+  hideUpdatePrompt() {
+    this.snackbar.classList.add('TWPT-hidden');
+    document.title = document.title.replace('[!!!] ', '');
+    this.isUpdatePromptShown = false;
+  },
+  injectUpdatePrompt() {
+    var pane = document.createElement('div');
+    pane.classList.add('TWPT-pane-for-snackbar');
+
+    var snackbar = document.createElement('material-snackbar-panel');
+    snackbar.classList.add('TWPT-snackbar');
+    snackbar.classList.add('TWPT-hidden');
+
+    var ac = document.createElement('div');
+    ac.classList.add('TWPT-animation-container');
+
+    var nb = document.createElement('div');
+    nb.classList.add('TWPT-notification-bar');
+
+    var ft = document.createElement('focus-trap');
+
+    var content = document.createElement('div');
+    content.classList.add('TWPT-focus-content-wrapper');
+
+    var badge = createExtBadge();
+
+    var message = document.createElement('div');
+    message.classList.add('TWPT-message');
+    message.textContent =
+        chrome.i18n.getMessage('inject_autorefresh_list_snackbar_message');
+
+    var action = document.createElement('div');
+    action.classList.add('TWPT-action');
+    action.textContent =
+        chrome.i18n.getMessage('inject_autorefresh_list_snackbar_action');
+
+    action.addEventListener('click', e => {
+      this.hideUpdatePrompt();
+      document.querySelector('.app-title-button').click();
+    });
+
+    content.append(badge, message, action);
+    ft.append(content);
+    nb.append(ft);
+    ac.append(nb);
+    snackbar.append(ac);
+    pane.append(snackbar);
+    document.getElementById('default-acx-overlay-container').append(pane);
+    this.snackbar = snackbar;
+  },
+  checkUpdate() {
+    if (location.pathname != this.path) {
+      this.unregister();
+      return;
+    }
+
+    if (this.isUpdatePromptShown) return;
+
+    console.debug('Checking for update at: ', new Date());
+
+    this.getLastTimestamp()
+        .then(timestamp => {
+          if (timestamp != this.lastTimestamp) this.showUpdatePrompt();
+        })
+        .catch(
+            err => console.error(
+                'Coudln\'t get last timestamp (while updating): ', err));
+  },
+  firstCall() {
+    console.debug(
+        'autorefresh_list: now performing first call to finish setup (filter: [' +
+        this.filter + '])');
+
+    if (location.pathname != this.path) {
+      this.unregister();
+      return;
+    }
+
+    this.getLastTimestamp()
+        .then(timestamp => {
+          this.lastTimestamp = timestamp;
+          var checkUpdateCallback = this.checkUpdate.bind(this);
+          this.interval =
+              window.setInterval(checkUpdateCallback, this.intervalMs);
+        })
+        .catch(
+            err => console.error(
+                'Couldn\'t get last timestamp (while setting up): ', err));
+  },
+  setUp() {
+    if (!this.isOrderedByTimestampDescending()) return;
+
+    this.unregister();
+
+    console.debug('autorefresh_list: starting set up...');
+
+    if (this.snackbar === null) this.injectUpdatePrompt();
+    this.isLookingForUpdates = true;
+    this.path = location.pathname;
+    this.filter = this.getFilter(this.path);
+
+    var firstCall = this.firstCall.bind(this);
+    this.firstCallTimeout = window.setTimeout(firstCall, this.firstCallDelayMs);
+  },
+};
diff --git a/src/contentScripts/communityConsole/avatars.js b/src/contentScripts/communityConsole/avatars.js
new file mode 100644
index 0000000..253fe8f
--- /dev/null
+++ b/src/contentScripts/communityConsole/avatars.js
@@ -0,0 +1,178 @@
+import {CCApi} from '../../common/api.js';
+import {parseUrl} from '../../common/commonUtils.js';
+
+export var avatars = {
+  isFilterSetUp: false,
+  privateForums: [],
+
+  // Gets a list of private forums. If it is already cached, the cached list is
+  // returned; otherwise it is also computed and cached.
+  getPrivateForums() {
+    return new Promise((resolve, reject) => {
+      if (this.isFilterSetUp) return resolve(this.privateForums);
+
+      if (!document.documentElement.hasAttribute('data-startup'))
+        return reject('[threadListAvatars] Couldn\'t get startup data.');
+
+      var startupData =
+          JSON.parse(document.documentElement.getAttribute('data-startup'));
+      var forums = startupData?.['1']?.['2'];
+      if (forums === undefined)
+        return reject(
+            '[threadListAvatars] Couldn\'t retrieve forums from startup data.');
+
+      for (var f of forums) {
+        var forumId = f?.['2']?.['1']?.['1'];
+        var forumVisibility = f?.['2']?.['18'];
+        if (forumId === undefined || forumVisibility === undefined) {
+          console.warn(
+              '[threadListAvatars] Coudln\'t retrieve forum id and/or forum visibility for the following forum:',
+              f);
+          continue;
+        }
+
+        // forumVisibility's value 1 means "PUBLIC".
+        if (forumVisibility != 1) this.privateForums.push(forumId);
+      }
+
+      // Forum 51488989 is marked as public but it is in fact private.
+      this.privateForums.push('51488989');
+
+      this.isFilterSetUp = true;
+      return resolve(this.privateForums);
+    });
+  },
+
+  // Some threads belong to private forums, and this feature will not be able to
+  // get its avatars since it makes an anonymomus call to get the contents of
+  // the thread.
+  //
+  // This function returns whether avatars should be retrieved depending on if
+  // the thread belongs to a known private forum.
+  shouldRetrieveAvatars(thread) {
+    return this.getPrivateForums().then(privateForums => {
+      return !privateForums.includes(thread.forum);
+    });
+  },
+
+  // Get an object with the author of the thread and an array of the first |num|
+  // replies from the thread |thread|.
+  getFirstMessages(thread, num = 15) {
+    return CCApi(
+               'ViewThread', {
+                 1: thread.forum,
+                 2: thread.thread,
+                 // options
+                 3: {
+                   // pagination
+                   1: {
+                     2: num,  // maxNum
+                   },
+                   3: true,    // withMessages
+                   5: true,    // withUserProfile
+                   10: false,  // withPromotedMessages
+                   16: false,  // withThreadNotes
+                   18: true,   // sendNewThreadIfMoved
+                 }
+               },
+               // |authentication| is false because otherwise this would mark
+               // the thread as read as a side effect, and that would mark all
+               // threads in the list as read.
+               //
+               // Due to the fact that we have to call this endpoint
+               // anonymously, this means we can't retrieve information about
+               // threads in private forums.
+               /* authentication = */ false)
+        .then(data => {
+          var numMessages = data?.['1']?.['8'];
+          if (numMessages === undefined)
+            throw new Error(
+                'Request to view thread doesn\'t include the number of messages');
+
+          var messages = numMessages == 0 ? [] : data?.['1']['3'];
+          if (messages === undefined)
+            throw new Error(
+                'numMessages was ' + numMessages +
+                ' but the response didn\'t include any message.');
+
+          var author = data?.['1']?.['4'];
+          if (author === undefined)
+            throw new Error(
+                'Author isn\'t included in the ViewThread response.');
+
+          return {
+            messages,
+            author,
+          };
+        });
+  },
+
+  // Get a list of at most |num| avatars for thread |thread|
+  getVisibleAvatars(thread, num = 3) {
+    return this.shouldRetrieveAvatars(thread).then(shouldRetrieve => {
+      if (!shouldRetrieve) {
+        console.debug('[threadListAvatars] Skipping thread', thread);
+        return [];
+      }
+
+      return this.getFirstMessages(thread).then(result => {
+        var messages = result.messages;
+        var author = result.author;
+
+        var avatarUrls = [];
+
+        var authorUrl = author?.['1']?.['2'];
+        if (authorUrl !== undefined) avatarUrls.push(authorUrl);
+
+        for (var m of messages) {
+          var url = m?.['3']?.['1']?.['2'];
+
+          if (url === undefined) continue;
+          if (!avatarUrls.includes(url)) avatarUrls.push(url);
+          if (avatarUrls.length == 3) break;
+        }
+
+        return avatarUrls;
+      });
+    });
+  },
+
+  // Inject avatars for thread summary (thread item) |node| in a thread list.
+  inject(node) {
+    var header = node.querySelector(
+        'ec-thread-summary .main-header .panel-description a.header');
+    if (header === null) {
+      console.error(
+          '[threadListAvatars] Header is not present in the thread item\'s DOM.');
+      return;
+    }
+
+    var thread = parseUrl(header.href);
+    if (thread === false) {
+      console.error('[threadListAvatars] Thread\'s link cannot be parsed.');
+      return;
+    }
+
+    this.getVisibleAvatars(thread)
+        .then(avatarUrls => {
+          var avatarsContainer = document.createElement('div');
+          avatarsContainer.classList.add('TWPT-avatars');
+
+          var count = Math.floor(Math.random() * 4);
+
+          for (var i = 0; i < avatarUrls.length; ++i) {
+            var avatar = document.createElement('div');
+            avatar.classList.add('TWPT-avatar');
+            avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')';
+            avatarsContainer.appendChild(avatar);
+          }
+
+          header.appendChild(avatarsContainer);
+        })
+        .catch(err => {
+          console.error(
+              '[threadListAvatars] Could not retrieve avatars for thread',
+              thread, err);
+        });
+  },
+};
diff --git a/src/contentScripts/communityConsole/batchLock.js b/src/contentScripts/communityConsole/batchLock.js
new file mode 100644
index 0000000..5bc0361
--- /dev/null
+++ b/src/contentScripts/communityConsole/batchLock.js
@@ -0,0 +1,133 @@
+import {removeChildNodes, createExtBadge} from './utils.js';
+
+export function nodeIsReadToggleBtn(node) {
+  return ('tagName' in node) && node.tagName == 'MATERIAL-BUTTON' &&
+      node.getAttribute('debugid') !== null &&
+      (node.getAttribute('debugid') == 'mark-read-button' ||
+       node.getAttribute('debugid') == 'mark-unread-button') &&
+      ('parentNode' in node) && node.parentNode !== null &&
+      ('parentNode' in node.parentNode) &&
+      node.parentNode.querySelector('[debugid="batchlock"]') === null &&
+      node.parentNode.parentNode !== null &&
+      ('tagName' in node.parentNode.parentNode) &&
+      node.parentNode.parentNode.tagName == 'EC-BULK-ACTIONS';
+}
+
+export function addBatchLockBtn(readToggle) {
+  var clone = readToggle.cloneNode(true);
+  clone.setAttribute('debugid', 'batchlock');
+  clone.classList.add('TWPT-btn--with-badge');
+  clone.setAttribute('title', chrome.i18n.getMessage('inject_lockbtn'));
+  clone.querySelector('material-icon').setAttribute('icon', 'lock');
+  clone.querySelector('i.material-icon-i').textContent = 'lock';
+
+  var badge = createExtBadge();
+  clone.append(badge);
+
+  clone.addEventListener('click', function() {
+    var modal = document.querySelector('.pane[pane-id="default-1"]');
+
+    var dialog = document.createElement('material-dialog');
+    dialog.setAttribute('role', 'dialog');
+    dialog.setAttribute('aria-modal', 'true');
+    dialog.classList.add('TWPT-dialog');
+
+    var header = document.createElement('header');
+    header.setAttribute('role', 'presentation');
+    header.classList.add('TWPT-dialog-header');
+
+    var title = document.createElement('div');
+    title.classList.add('TWPT-dialog-header--title', 'title');
+    title.textContent = chrome.i18n.getMessage('inject_lockbtn');
+
+    header.append(title);
+
+    var main = document.createElement('main');
+    main.setAttribute('role', 'presentation');
+    main.classList.add('TWPT-dialog-main');
+
+    var p = document.createElement('p');
+    p.textContent = chrome.i18n.getMessage('inject_lockdialog_desc');
+
+    main.append(p);
+
+    dialog.append(header, main);
+
+    var footers = [['lock', 'unlock', 'cancel'], ['reload', 'close']];
+
+    for (var i = 0; i < footers.length; ++i) {
+      var footer = document.createElement('footer');
+      footer.setAttribute('role', 'presentation');
+      footer.classList.add('TWPT-dialog-footer');
+      footer.setAttribute('data-footer-id', i);
+
+      if (i > 0) footer.classList.add('is-hidden');
+
+      footers[i].forEach(action => {
+        var btn = document.createElement('material-button');
+        btn.setAttribute('role', 'button');
+        btn.classList.add('TWPT-dialog-footer-btn');
+        if (i == 1) btn.classList.add('is-disabled');
+
+        switch (action) {
+          case 'lock':
+          case 'unlock':
+            btn.addEventListener('click', _ => {
+              if (btn.classList.contains('is-disabled')) return;
+              var message = {
+                action,
+                prefix: 'TWPT-batchlock',
+              };
+              window.postMessage(message, '*');
+            });
+            break;
+
+          case 'cancel':
+          case 'close':
+            btn.addEventListener('click', _ => {
+              if (btn.classList.contains('is-disabled')) return;
+              modal.classList.remove('visible');
+              modal.style.display = 'none';
+              removeChildNodes(modal);
+            });
+            break;
+
+          case 'reload':
+            btn.addEventListener('click', _ => {
+              if (btn.classList.contains('is-disabled')) return;
+              window.location.reload()
+            });
+            break;
+        }
+
+        var content = document.createElement('div');
+        content.classList.add('content', 'TWPT-dialog-footer-btn--content');
+        content.textContent =
+            chrome.i18n.getMessage('inject_lockdialog_btn_' + action);
+
+        btn.append(content);
+        footer.append(btn);
+      });
+
+      var clear = document.createElement('div');
+      clear.style.clear = 'both';
+
+      footer.append(clear);
+      dialog.append(footer);
+    }
+
+    removeChildNodes(modal);
+    modal.append(dialog);
+    modal.classList.add('visible', 'modal');
+    modal.style.display = 'flex';
+  });
+
+  var duplicateBtn =
+      readToggle.parentNode.querySelector('[debugid="mark-duplicate-button"]');
+  if (duplicateBtn)
+    duplicateBtn.parentNode.insertBefore(
+        clone, (duplicateBtn.nextSibling || duplicateBtn));
+  else
+    readToggle.parentNode.insertBefore(
+        clone, (readToggle.nextSibling || readToggle));
+}
diff --git a/src/contentScripts/communityConsole/darkMode.js b/src/contentScripts/communityConsole/darkMode.js
new file mode 100644
index 0000000..d10a77c
--- /dev/null
+++ b/src/contentScripts/communityConsole/darkMode.js
@@ -0,0 +1,49 @@
+import {createExtBadge} from './utils.js';
+
+export function injectDarkModeButton(rightControl, previousDarkModeOption) {
+  var darkThemeSwitch = document.createElement('material-button');
+  darkThemeSwitch.classList.add('TWPT-dark-theme', 'TWPT-btn--with-badge');
+  darkThemeSwitch.setAttribute('button', '');
+  darkThemeSwitch.setAttribute(
+      'title', chrome.i18n.getMessage('inject_ccdarktheme_helper'));
+
+  darkThemeSwitch.addEventListener('click', e => {
+    chrome.storage.sync.get(null, currentOptions => {
+      currentOptions.ccdarktheme_switch_status = !previousDarkModeOption;
+      chrome.storage.sync.set(currentOptions, _ => {
+        location.reload();
+      });
+    });
+  });
+
+  var switchContent = document.createElement('div');
+  switchContent.classList.add('content');
+
+  var icon = document.createElement('material-icon');
+
+  var i = document.createElement('i');
+  i.classList.add('material-icon-i', 'material-icons-extended');
+  i.textContent = 'brightness_4';
+
+  icon.appendChild(i);
+  switchContent.appendChild(icon);
+  darkThemeSwitch.appendChild(switchContent);
+
+  var badgeContent = createExtBadge();
+
+  darkThemeSwitch.appendChild(badgeContent);
+
+  rightControl.style.width =
+      (parseInt(window.getComputedStyle(rightControl).width) + 58) + 'px';
+  rightControl.insertAdjacentElement('afterbegin', darkThemeSwitch);
+}
+
+export function isDarkThemeOn(options) {
+  if (!options.ccdarktheme) return false;
+
+  if (options.ccdarktheme_mode == 'switch')
+    return options.ccdarktheme_switch_status;
+
+  return window.matchMedia &&
+      window.matchMedia('(prefers-color-scheme: dark)').matches;
+}
diff --git a/src/contentScripts/communityConsole/dragAndDropFix.js b/src/contentScripts/communityConsole/dragAndDropFix.js
new file mode 100644
index 0000000..1f293f6
--- /dev/null
+++ b/src/contentScripts/communityConsole/dragAndDropFix.js
@@ -0,0 +1,9 @@
+export function applyDragAndDropFix(node) {
+  console.debug('Adding link drag&drop fix to ', node);
+  node.addEventListener('drop', e => {
+    if (e.dataTransfer.types.includes('text/uri-list')) {
+      e.stopImmediatePropagation();
+      console.debug('Stopping link drop event propagation.');
+    }
+  }, true);
+}
diff --git a/src/contentScripts/communityConsole/forceMarkAsRead.js b/src/contentScripts/communityConsole/forceMarkAsRead.js
new file mode 100644
index 0000000..c573788
--- /dev/null
+++ b/src/contentScripts/communityConsole/forceMarkAsRead.js
@@ -0,0 +1,81 @@
+import {CCApi} from '../../common/api.js';
+import {getAuthUser} from '../../common/communityConsoleUtils.js';
+
+var authuser = getAuthUser();
+
+// Send a request to mark the current thread as read
+export function markCurrentThreadAsRead() {
+  console.debug(
+      '[forceMarkAsRead] %cTrying to mark a thread as read.',
+      'color: #1a73e8;');
+
+  var threadRegex =
+      /\/s\/community\/?.*\/forum\/([0-9]+)\/?.*\/thread\/([0-9]+)/;
+
+  var url = location.href;
+  var matches = url.match(threadRegex);
+  if (matches !== null && matches.length > 2) {
+    var forumId = matches[1];
+    var threadId = matches[2];
+
+    console.debug('[forceMarkAsRead] Thread details:', {forumId, threadId});
+
+    return CCApi(
+               'ViewThread', {
+                 1: forumId,
+                 2: threadId,
+                 // options
+                 3: {
+                   // pagination
+                   1: {
+                     2: 0,  // maxNum
+                   },
+                   3: false,   // withMessages
+                   5: false,   // withUserProfile
+                   6: true,    // withUserReadState
+                   9: false,   // withRequestorProfile
+                   10: false,  // withPromotedMessages
+                   11: false,  // withExpertResponder
+                 },
+               },
+               true, authuser)
+        .then(thread => {
+          if (thread?.[1]?.[6] === true) {
+            console.debug(
+                '[forceMarkAsRead] This thread is already marked as read, but marking it as read anyways.');
+          }
+
+          var lastMessageId = thread?.[1]?.[2]?.[10];
+
+          console.debug('[forceMarkAsRead] lastMessageId is:', lastMessageId);
+
+          if (lastMessageId === undefined)
+            throw new Error(
+                'Couldn\'t find lastMessageId in the ViewThread response.');
+
+          return CCApi(
+              'SetUserReadStateBulk', {
+                1: [{
+                  1: forumId,
+                  2: threadId,
+                  3: lastMessageId,
+                }],
+              },
+              true, authuser);
+        })
+        .then(_ => {
+          console.debug(
+              '[forceMarkAsRead] %cSuccessfully set as read!',
+              'color: #1e8e3e;');
+        })
+        .catch(err => {
+          console.error(
+              '[forceMarkAsRead] Error while marking current thread as read: ',
+              err);
+        });
+  } else {
+    console.error(
+        '[forceMarkAsRead] Couldn\'t retrieve forumId and threadId from the current URL.',
+        url);
+  }
+}
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
new file mode 100644
index 0000000..dd11851
--- /dev/null
+++ b/src/contentScripts/communityConsole/main.js
@@ -0,0 +1,235 @@
+import {injectScript, injectStyles, injectStylesheet} from '../../common/contentScriptsUtils.js';
+
+import {autoRefresh} from './autoRefresh.js';
+import {avatars} from './avatars.js';
+import {addBatchLockBtn, nodeIsReadToggleBtn} from './batchLock.js';
+import {injectDarkModeButton, isDarkThemeOn} from './darkMode.js';
+import {applyDragAndDropFix} from './dragAndDropFix.js';
+import {markCurrentThreadAsRead} from './forceMarkAsRead.js';
+import {injectPreviousPostsLinks} from './profileHistoryLink.js';
+import {unifiedProfilesFix} from './unifiedProfiles.js';
+
+var mutationObserver, intersectionObserver, intersectionOptions, options;
+
+const watchedNodesSelectors = [
+  // App container (used to set up the intersection observer and inject the dark
+  // mode button)
+  'ec-app',
+
+  // Load more bar (for the "load more"/"load all" buttons)
+  '.load-more-bar',
+
+  // Username span/editor inside ec-user (user profile view)
+  'ec-user .main-card .header > .name > span',
+  'ec-user .main-card .header > .name > ec-display-name-editor',
+
+  // Rich text editor
+  'ec-movable-dialog',
+  'ec-rich-text-editor',
+
+  // Read/unread bulk action in the list of thread, for the batch lock feature
+  'ec-bulk-actions material-button[debugid="mark-read-button"]',
+  'ec-bulk-actions material-button[debugid="mark-unread-button"]',
+
+  // Thread list items (used to inject the avatars)
+  'li',
+
+  // Thread list (used for the autorefresh feature)
+  'ec-thread-list',
+
+  // Unified profile iframe
+  'iframe',
+
+  // Thread component
+  'ec-thread',
+];
+
+function handleCandidateNode(node) {
+  if (typeof node.classList !== 'undefined') {
+    if (('tagName' in node) && node.tagName == 'EC-APP') {
+      // Set up the intersectionObserver
+      if (typeof intersectionObserver === 'undefined') {
+        var scrollableContent = node.querySelector('.scrollable-content');
+        if (scrollableContent !== null) {
+          intersectionOptions = {
+            root: scrollableContent,
+            rootMargin: '0px',
+            threshold: 1.0,
+          };
+
+          intersectionObserver = new IntersectionObserver(
+              intersectionCallback, intersectionOptions);
+        }
+      }
+
+      // Inject the dark mode button
+      if (options.ccdarktheme && options.ccdarktheme_mode == 'switch') {
+        var rightControl = node.querySelector('header .right-control');
+        if (rightControl !== null)
+          injectDarkModeButton(rightControl, options.ccdarktheme_switch_status);
+      }
+    }
+
+    // Start the intersectionObserver for the "load more"/"load all" buttons
+    // inside a thread
+    if ((options.thread || options.threadall) &&
+        node.classList.contains('load-more-bar')) {
+      if (typeof intersectionObserver !== 'undefined') {
+        if (options.thread)
+          intersectionObserver.observe(node.querySelector('.load-more-button'));
+        if (options.threadall)
+          intersectionObserver.observe(node.querySelector('.load-all-button'));
+      } else {
+        console.warn(
+            '[infinitescroll] ' +
+            'The intersectionObserver is not ready yet.');
+      }
+    }
+
+    // Show the "previous posts" links
+    //   Here we're selecting the 'ec-user > div' element (unique child)
+    if (options.history &&
+        (node.matches('ec-user .main-card .header > .name > span') ||
+         node.matches(
+             'ec-user .main-card .header > .name > ec-display-name-editor'))) {
+      injectPreviousPostsLinks(node);
+    }
+
+    // Fix the drag&drop issue with the rich text editor
+    //
+    //   We target both tags because in different contexts different
+    //   elements containing the text editor get added to the DOM structure.
+    //   Sometimes it's a EC-MOVABLE-DIALOG which already contains the
+    //   EC-RICH-TEXT-EDITOR, and sometimes it's the EC-RICH-TEXT-EDITOR
+    //   directly.
+    if (options.ccdragndropfix && ('tagName' in node) &&
+        (node.tagName == 'EC-MOVABLE-DIALOG' ||
+         node.tagName == 'EC-RICH-TEXT-EDITOR')) {
+      applyDragAndDropFix(node);
+    }
+
+    // Inject the batch lock button in the thread list
+    if (options.batchlock && nodeIsReadToggleBtn(node)) {
+      addBatchLockBtn(node);
+    }
+
+    // Inject avatar links to threads in the thread list
+    if (options.threadlistavatars && ('tagName' in node) &&
+        (node.tagName == 'LI') &&
+        node.querySelector('ec-thread-summary') !== null) {
+      avatars.inject(node);
+    }
+
+    // Set up the autorefresh list feature
+    if (options.autorefreshlist && ('tagName' in node) &&
+        node.tagName == 'EC-THREAD-LIST') {
+      autoRefresh.setUp();
+    }
+
+    // Redirect unified profile iframe to dark version if applicable
+    if (node.tagName == 'IFRAME' && isDarkThemeOn(options) &&
+        unifiedProfilesFix.checkIframe(node)) {
+      unifiedProfilesFix.fixIframe(node);
+    }
+
+    // Force mark thread as read
+    if (options.forcemarkasread && node.tagName == 'EC-THREAD') {
+      markCurrentThreadAsRead();
+    }
+  }
+}
+
+function handleRemovedNode(node) {
+  // Remove snackbar when exiting thread list view
+  if (options.autorefreshlist && 'tagName' in node &&
+      node.tagName == 'EC-THREAD-LIST') {
+    autoRefresh.hideUpdatePrompt();
+  }
+}
+
+function mutationCallback(mutationList, observer) {
+  mutationList.forEach((mutation) => {
+    if (mutation.type == 'childList') {
+      mutation.addedNodes.forEach(function(node) {
+        handleCandidateNode(node);
+      });
+
+      mutation.removedNodes.forEach(function(node) {
+        handleRemovedNode(node);
+      });
+    }
+  });
+}
+
+function intersectionCallback(entries, observer) {
+  entries.forEach(entry => {
+    if (entry.isIntersecting) {
+      entry.target.click();
+    }
+  });
+};
+
+var observerOptions = {
+  childList: true,
+  subtree: true,
+};
+
+chrome.storage.sync.get(null, function(items) {
+  options = items;
+
+  // Before starting the mutation Observer, check whether we missed any
+  // mutations by manually checking whether some watched nodes already
+  // exist.
+  var cssSelectors = watchedNodesSelectors.join(',');
+  document.querySelectorAll(cssSelectors)
+      .forEach(node => handleCandidateNode(node));
+
+  mutationObserver = new MutationObserver(mutationCallback);
+  mutationObserver.observe(document.body, observerOptions);
+
+  if (options.fixedtoolbar) {
+    injectStyles(
+        'ec-bulk-actions{position: sticky; top: 0; background: var(--TWPT-primary-background, #fff); z-index: 96;}');
+  }
+
+  if (options.increasecontrast) {
+    injectStyles(
+        '.thread-summary.read:not(.checked){background: var(--TWPT-thread-read-background, #ecedee)!important;}');
+  }
+
+  if (options.stickysidebarheaders) {
+    injectStyles(
+        'material-drawer .main-header{background: var(--TWPT-drawer-background, #fff)!important; position: sticky; top: 0; z-index: 1;}');
+  }
+
+  if (options.enhancedannouncementsdot) {
+    injectStylesheet(
+        chrome.runtime.getURL('css/enhanced_announcements_dot.css'));
+  }
+
+  if (options.repositionexpandthread) {
+    injectStylesheet(
+        chrome.runtime.getURL('css/reposition_expand_thread.css'));
+  }
+
+  if (options.ccforcehidedrawer) {
+    var drawer = document.querySelector('material-drawer');
+    if (drawer !== null && drawer.classList.contains('mat-drawer-expanded')) {
+      document.querySelector('.material-drawer-button').click();
+    }
+  }
+
+  if (options.batchlock) {
+    injectScript(chrome.runtime.getURL('batchLockInject.bundle.js'));
+    injectStylesheet(chrome.runtime.getURL('css/batchlock_inject.css'));
+  }
+
+  if (options.threadlistavatars) {
+    injectStylesheet(
+        chrome.runtime.getURL('css/thread_list_avatars.css'));
+  }
+
+  if (options.autorefreshlist) {
+    injectStylesheet(chrome.runtime.getURL('css/autorefresh_list.css'));
+  }
+});
diff --git a/src/contentScripts/communityConsole/profileHistoryLink.js b/src/contentScripts/communityConsole/profileHistoryLink.js
new file mode 100644
index 0000000..d53f9ee
--- /dev/null
+++ b/src/contentScripts/communityConsole/profileHistoryLink.js
@@ -0,0 +1,59 @@
+import {getNParent, createExtBadge} from './utils.js';
+import {escapeUsername, getAuthUser} from '../../common/communityConsoleUtils.js';
+
+var authuser = getAuthUser();
+
+function addProfileHistoryLink(node, type, query) {
+  var urlpart = encodeURIComponent('query=' + query);
+  var authuserpart =
+      (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
+  var container = document.createElement('div');
+  container.style.margin = '3px 0';
+
+  var link = document.createElement('a');
+  link.setAttribute(
+      'href',
+      'https://support.google.com/s/community/search/' + urlpart +
+          authuserpart);
+  link.innerText = chrome.i18n.getMessage('inject_previousposts_' + type);
+
+  container.appendChild(link);
+  node.appendChild(container);
+}
+
+export function injectPreviousPostsLinks(nameElement) {
+  var mainCardContent = getNParent(nameElement, 3);
+  if (mainCardContent === null) {
+    console.error(
+        '[previousposts] Couldn\'t find |.main-card-content| element.');
+    return;
+  }
+
+  var forumId = location.href.split('/forum/')[1].split('/')[0] || '0';
+
+  var nameTag =
+      (nameElement.tagName == 'EC-DISPLAY-NAME-EDITOR' ?
+           nameElement.querySelector('.top-section > span') ?? nameElement :
+           nameElement);
+  var name = escapeUsername(nameTag.textContent);
+  var query1 = encodeURIComponent(
+      '(creator:"' + name + '" | replier:"' + name + '") forum:' + forumId);
+  var query2 = encodeURIComponent(
+      '(creator:"' + name + '" | replier:"' + name + '") forum:any');
+
+  var container = document.createElement('div');
+  container.classList.add('TWPT-previous-posts');
+
+  var badge = createExtBadge();
+  container.appendChild(badge);
+
+  var linkContainer = document.createElement('div');
+  linkContainer.classList.add('TWPT-previous-posts--links');
+
+  addProfileHistoryLink(linkContainer, 'forum', query1);
+  addProfileHistoryLink(linkContainer, 'all', query2);
+
+  container.appendChild(linkContainer);
+
+  mainCardContent.appendChild(container);
+}
diff --git a/src/content_scripts/console_inject_start.js b/src/contentScripts/communityConsole/start.js
similarity index 83%
rename from src/content_scripts/console_inject_start.js
rename to src/contentScripts/communityConsole/start.js
index 32f3159..318e466 100644
--- a/src/content_scripts/console_inject_start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -1,3 +1,5 @@
+import {injectStylesheet} from '../../common/contentScriptsUtils.js';
+
 const SMEI_SORT_DIRECTION = 8;
 const SMEI_UNIFIED_PROFILES = 9;
 
@@ -30,11 +32,11 @@
     switch (items.ccdarktheme_mode) {
       case 'switch':
         if (items.ccdarktheme_switch_status == true)
-          injectStylesheet(chrome.runtime.getURL('injections/ccdarktheme.css'));
+          injectStylesheet(chrome.runtime.getURL('css/ccdarktheme.css'));
         break;
 
       case 'system':
-        injectStylesheet(chrome.runtime.getURL('injections/ccdarktheme.css'), {
+        injectStylesheet(chrome.runtime.getURL('css/ccdarktheme.css'), {
           'media': '(prefers-color-scheme: dark)',
         });
         break;
diff --git a/src/contentScripts/communityConsole/unifiedProfiles.js b/src/contentScripts/communityConsole/unifiedProfiles.js
new file mode 100644
index 0000000..e37fbbf
--- /dev/null
+++ b/src/contentScripts/communityConsole/unifiedProfiles.js
@@ -0,0 +1,12 @@
+export var unifiedProfilesFix = {
+  checkIframe(iframe) {
+    var srcRegex = /support.*\.google\.com\/profile\//;
+    return srcRegex.test(iframe.src ?? '');
+  },
+  fixIframe(iframe) {
+    console.info('[unifiedProfilesFix] Fixing unified profiles iframe');
+    var url = new URL(iframe.src);
+    url.searchParams.set('dark', 1);
+    iframe.src = url.href;
+  },
+};
diff --git a/src/contentScripts/communityConsole/utils.js b/src/contentScripts/communityConsole/utils.js
new file mode 100644
index 0000000..ca452b3
--- /dev/null
+++ b/src/contentScripts/communityConsole/utils.js
@@ -0,0 +1,27 @@
+export function removeChildNodes(node) {
+  while (node.firstChild) {
+    node.removeChild(node.firstChild);
+  }
+}
+
+export function getNParent(node, n) {
+  if (n <= 0) return node;
+  if (!('parentNode' in node)) return null;
+  return getNParent(node.parentNode, n - 1);
+}
+
+export function createExtBadge() {
+  var badge = document.createElement('div');
+  badge.classList.add('TWPT-badge');
+  badge.setAttribute(
+      'title', chrome.i18n.getMessage('inject_extension_badge_helper', [
+        chrome.i18n.getMessage('appName')
+      ]));
+
+  var badgeI = document.createElement('i');
+  badgeI.classList.add('material-icon-i', 'material-icons-extended');
+  badgeI.textContent = 'repeat';
+
+  badge.append(badgeI);
+  return badge;
+}
diff --git a/src/content_scripts/profile_inject.js b/src/contentScripts/profile.js
similarity index 96%
rename from src/content_scripts/profile_inject.js
rename to src/contentScripts/profile.js
index d28d3ec..38894d6 100644
--- a/src/content_scripts/profile_inject.js
+++ b/src/contentScripts/profile.js
@@ -1,10 +1,12 @@
+import {escapeUsername} from '../common/communityConsoleUtils.js';
+
 var authuser = (new URL(location.href)).searchParams.get('authuser') || '0';
 
 function getSearchUrl(query) {
   var urlpart = encodeURIComponent('query=' + encodeURIComponent(query));
   var authuserpart =
       (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
-  return url = 'https://support.google.com/s/community/search/' + urlpart +
+  return 'https://support.google.com/s/community/search/' + urlpart +
       authuserpart;
 }
 
diff --git a/src/contentScripts/profileIndicator.js b/src/contentScripts/profileIndicator.js
new file mode 100644
index 0000000..5edc932
--- /dev/null
+++ b/src/contentScripts/profileIndicator.js
@@ -0,0 +1,13 @@
+import {injectScript, injectStylesheet} from '../common/contentScriptsUtils.js';
+import {setUpListener} from '../common/csEventListener.js';
+
+setUpListener();
+
+chrome.storage.sync.get(null, function(options) {
+  if (options.profileindicator || options.profileindicatoralt) {
+    injectScript(
+        chrome.runtime.getURL('profileIndicatorInject.bundle.js'));
+    injectStylesheet(
+        chrome.runtime.getURL('css/profileindicator_inject.css'));
+  }
+});
diff --git a/src/content_scripts/forum_inject.js b/src/contentScripts/publicForum.js
similarity index 100%
rename from src/content_scripts/forum_inject.js
rename to src/contentScripts/publicForum.js
diff --git a/src/content_scripts/thread_inject.js b/src/contentScripts/publicThread.js
similarity index 100%
rename from src/content_scripts/thread_inject.js
rename to src/contentScripts/publicThread.js
diff --git a/src/content_scripts/console_inject.js b/src/content_scripts/console_inject.js
deleted file mode 100644
index 7503df5..0000000
--- a/src/content_scripts/console_inject.js
+++ /dev/null
@@ -1,1028 +0,0 @@
-var mutationObserver, intersectionObserver, intersectionOptions, options,
-    authuser;
-
-function removeChildNodes(node) {
-  while (node.firstChild) {
-    node.removeChild(node.firstChild);
-  }
-}
-
-function getNParent(node, n) {
-  if (n <= 0) return node;
-  if (!('parentNode' in node)) return null;
-  return getNParent(node.parentNode, n - 1);
-}
-
-function parseUrl(url) {
-  var forum_a = url.match(/forum\/([0-9]+)/i);
-  var thread_a = url.match(/thread\/([0-9]+)/i);
-
-  if (forum_a === null || thread_a === null) {
-    return false;
-  }
-
-  return {
-    'forum': forum_a[1],
-    'thread': thread_a[1],
-  };
-}
-
-function createExtBadge() {
-  var badge = document.createElement('div');
-  badge.classList.add('TWPT-badge');
-  badge.setAttribute(
-      'title', chrome.i18n.getMessage('inject_extension_badge_helper', [
-        chrome.i18n.getMessage('appName')
-      ]));
-
-  var badgeI = document.createElement('i');
-  badgeI.classList.add('material-icon-i', 'material-icons-extended');
-  badgeI.textContent = 'repeat';
-
-  badge.append(badgeI);
-  return badge;
-}
-
-function addProfileHistoryLink(node, type, query) {
-  var urlpart = encodeURIComponent('query=' + query);
-  var authuserpart =
-      (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
-  var container = document.createElement('div');
-  container.style.margin = '3px 0';
-
-  var link = document.createElement('a');
-  link.setAttribute(
-      'href',
-      'https://support.google.com/s/community/search/' + urlpart +
-          authuserpart);
-  link.innerText = chrome.i18n.getMessage('inject_previousposts_' + type);
-
-  container.appendChild(link);
-  node.appendChild(container);
-}
-
-function applyDragAndDropFix(node) {
-  console.debug('Adding link drag&drop fix to ', node);
-  node.addEventListener('drop', e => {
-    if (e.dataTransfer.types.includes('text/uri-list')) {
-      e.stopImmediatePropagation();
-      console.debug('Stopping link drop event propagation.');
-    }
-  }, true);
-}
-
-function nodeIsReadToggleBtn(node) {
-  return ('tagName' in node) && node.tagName == 'MATERIAL-BUTTON' &&
-      node.getAttribute('debugid') !== null &&
-      (node.getAttribute('debugid') == 'mark-read-button' ||
-       node.getAttribute('debugid') == 'mark-unread-button') &&
-      ('parentNode' in node) && node.parentNode !== null &&
-      ('parentNode' in node.parentNode) &&
-      node.parentNode.querySelector('[debugid="batchlock"]') === null &&
-      node.parentNode.parentNode !== null &&
-      ('tagName' in node.parentNode.parentNode) &&
-      node.parentNode.parentNode.tagName == 'EC-BULK-ACTIONS';
-}
-
-function injectDarkModeButton(rightControl) {
-  var darkThemeSwitch = document.createElement('material-button');
-  darkThemeSwitch.classList.add('TWPT-dark-theme', 'TWPT-btn--with-badge');
-  darkThemeSwitch.setAttribute('button', '');
-  darkThemeSwitch.setAttribute(
-      'title', chrome.i18n.getMessage('inject_ccdarktheme_helper'));
-
-  darkThemeSwitch.addEventListener('click', e => {
-    chrome.storage.sync.get(null, currentOptions => {
-      currentOptions.ccdarktheme_switch_status =
-          !options.ccdarktheme_switch_status;
-      chrome.storage.sync.set(currentOptions, _ => {
-        location.reload();
-      });
-    });
-  });
-
-  var switchContent = document.createElement('div');
-  switchContent.classList.add('content');
-
-  var icon = document.createElement('material-icon');
-
-  var i = document.createElement('i');
-  i.classList.add('material-icon-i', 'material-icons-extended');
-  i.textContent = 'brightness_4';
-
-  icon.appendChild(i);
-  switchContent.appendChild(icon);
-  darkThemeSwitch.appendChild(switchContent);
-
-  var badgeContent = createExtBadge();
-
-  darkThemeSwitch.appendChild(badgeContent);
-
-  rightControl.style.width =
-      (parseInt(window.getComputedStyle(rightControl).width) + 58) + 'px';
-  rightControl.insertAdjacentElement('afterbegin', darkThemeSwitch);
-}
-
-function addBatchLockBtn(readToggle) {
-  var clone = readToggle.cloneNode(true);
-  clone.setAttribute('debugid', 'batchlock');
-  clone.classList.add('TWPT-btn--with-badge');
-  clone.setAttribute('title', chrome.i18n.getMessage('inject_lockbtn'));
-  clone.querySelector('material-icon').setAttribute('icon', 'lock');
-  clone.querySelector('i.material-icon-i').textContent = 'lock';
-
-  var badge = createExtBadge();
-  clone.append(badge);
-
-  clone.addEventListener('click', function() {
-    var modal = document.querySelector('.pane[pane-id="default-1"]');
-
-    var dialog = document.createElement('material-dialog');
-    dialog.setAttribute('role', 'dialog');
-    dialog.setAttribute('aria-modal', 'true');
-    dialog.classList.add('TWPT-dialog');
-
-    var header = document.createElement('header');
-    header.setAttribute('role', 'presentation');
-    header.classList.add('TWPT-dialog-header');
-
-    var title = document.createElement('div');
-    title.classList.add('TWPT-dialog-header--title', 'title');
-    title.textContent = chrome.i18n.getMessage('inject_lockbtn');
-
-    header.append(title);
-
-    var main = document.createElement('main');
-    main.setAttribute('role', 'presentation');
-    main.classList.add('TWPT-dialog-main');
-
-    var p = document.createElement('p');
-    p.textContent = chrome.i18n.getMessage('inject_lockdialog_desc');
-
-    main.append(p);
-
-    dialog.append(header, main);
-
-    var footers = [['lock', 'unlock', 'cancel'], ['reload', 'close']];
-
-    for (var i = 0; i < footers.length; ++i) {
-      var footer = document.createElement('footer');
-      footer.setAttribute('role', 'presentation');
-      footer.classList.add('TWPT-dialog-footer');
-      footer.setAttribute('data-footer-id', i);
-
-      if (i > 0) footer.classList.add('is-hidden');
-
-      footers[i].forEach(action => {
-        var btn = document.createElement('material-button');
-        btn.setAttribute('role', 'button');
-        btn.classList.add('TWPT-dialog-footer-btn');
-        if (i == 1) btn.classList.add('is-disabled');
-
-        switch (action) {
-          case 'lock':
-          case 'unlock':
-            btn.addEventListener('click', _ => {
-              if (btn.classList.contains('is-disabled')) return;
-              var message = {
-                action,
-                prefix: 'TWPT-batchlock',
-              };
-              window.postMessage(message, '*');
-            });
-            break;
-
-          case 'cancel':
-          case 'close':
-            btn.addEventListener('click', _ => {
-              if (btn.classList.contains('is-disabled')) return;
-              modal.classList.remove('visible');
-              modal.style.display = 'none';
-              removeChildNodes(modal);
-            });
-            break;
-
-          case 'reload':
-            btn.addEventListener('click', _ => {
-              if (btn.classList.contains('is-disabled')) return;
-              window.location.reload()
-            });
-            break;
-        }
-
-        var content = document.createElement('div');
-        content.classList.add('content', 'TWPT-dialog-footer-btn--content');
-        content.textContent =
-            chrome.i18n.getMessage('inject_lockdialog_btn_' + action);
-
-        btn.append(content);
-        footer.append(btn);
-      });
-
-      var clear = document.createElement('div');
-      clear.style.clear = 'both';
-
-      footer.append(clear);
-      dialog.append(footer);
-    }
-
-    removeChildNodes(modal);
-    modal.append(dialog);
-    modal.classList.add('visible', 'modal');
-    modal.style.display = 'flex';
-  });
-
-  var duplicateBtn =
-      readToggle.parentNode.querySelector('[debugid="mark-duplicate-button"]');
-  if (duplicateBtn)
-    duplicateBtn.parentNode.insertBefore(
-        clone, (duplicateBtn.nextSibling || duplicateBtn));
-  else
-    readToggle.parentNode.insertBefore(
-        clone, (readToggle.nextSibling || readToggle));
-}
-
-var avatars = {
-  isFilterSetUp: false,
-  privateForums: [],
-
-  // Gets a list of private forums. If it is already cached, the cached list is
-  // returned; otherwise it is also computed and cached.
-  getPrivateForums() {
-    return new Promise((resolve, reject) => {
-      if (this.isFilterSetUp) return resolve(this.privateForums);
-
-      if (!document.documentElement.hasAttribute('data-startup'))
-        return reject('[threadListAvatars] Couldn\'t get startup data.');
-
-      var startupData =
-          JSON.parse(document.documentElement.getAttribute('data-startup'));
-      var forums = startupData?.['1']?.['2'];
-      if (forums === undefined)
-        return reject(
-            '[threadListAvatars] Couldn\'t retrieve forums from startup data.');
-
-      for (var f of forums) {
-        var forumId = f?.['2']?.['1']?.['1'];
-        var forumVisibility = f?.['2']?.['18'];
-        if (forumId === undefined || forumVisibility === undefined) {
-          console.warn(
-              '[threadListAvatars] Coudln\'t retrieve forum id and/or forum visibility for the following forum:',
-              f);
-          continue;
-        }
-
-        // forumVisibility's value 1 means "PUBLIC".
-        if (forumVisibility != 1) this.privateForums.push(forumId);
-      }
-
-      // Forum 51488989 is marked as public but it is in fact private.
-      this.privateForums.push('51488989');
-
-      this.isFilterSetUp = true;
-      return resolve(this.privateForums);
-    });
-  },
-
-  // Some threads belong to private forums, and this feature will not be able to
-  // get its avatars since it makes an anonymomus call to get the contents of
-  // the thread.
-  //
-  // This function returns whether avatars should be retrieved depending on if
-  // the thread belongs to a known private forum.
-  shouldRetrieveAvatars(thread) {
-    return this.getPrivateForums().then(privateForums => {
-      return !privateForums.includes(thread.forum);
-    });
-  },
-
-  // Get an object with the author of the thread and an array of the first |num|
-  // replies from the thread |thread|.
-  getFirstMessages(thread, num = 15) {
-    return CCApi(
-               'ViewThread', {
-                 1: thread.forum,
-                 2: thread.thread,
-                 // options
-                 3: {
-                   // pagination
-                   1: {
-                     2: num,  // maxNum
-                   },
-                   3: true,    // withMessages
-                   5: true,    // withUserProfile
-                   10: false,  // withPromotedMessages
-                   16: false,  // withThreadNotes
-                   18: true,   // sendNewThreadIfMoved
-                 }
-               },
-               // |authentication| is false because otherwise this would mark
-               // the thread as read as a side effect, and that would mark all
-               // threads in the list as read.
-               //
-               // Due to the fact that we have to call this endpoint
-               // anonymously, this means we can't retrieve information about
-               // threads in private forums.
-               /* authentication = */ false)
-        .then(data => {
-          var numMessages = data?.['1']?.['8'];
-          if (numMessages === undefined)
-            throw new Error(
-                'Request to view thread doesn\'t include the number of messages');
-
-          var messages = numMessages == 0 ? [] : data?.['1']['3'];
-          if (messages === undefined)
-            throw new Error(
-                'numMessages was ' + numMessages +
-                ' but the response didn\'t include any message.');
-
-          var author = data?.['1']?.['4'];
-          if (author === undefined)
-            throw new Error(
-                'Author isn\'t included in the ViewThread response.');
-
-          return {
-            messages,
-            author,
-          };
-        });
-  },
-
-  // Get a list of at most |num| avatars for thread |thread|
-  getVisibleAvatars(thread, num = 3) {
-    return this.shouldRetrieveAvatars(thread).then(shouldRetrieve => {
-      if (!shouldRetrieve) {
-        console.debug('[threadListAvatars] Skipping thread', thread);
-        return [];
-      }
-
-      return this.getFirstMessages(thread).then(result => {
-        var messages = result.messages;
-        var author = result.author;
-
-        var avatarUrls = [];
-
-        var authorUrl = author?.['1']?.['2'];
-        if (authorUrl !== undefined) avatarUrls.push(authorUrl);
-
-        for (var m of messages) {
-          var url = m?.['3']?.['1']?.['2'];
-
-          if (url === undefined) continue;
-          if (!avatarUrls.includes(url)) avatarUrls.push(url);
-          if (avatarUrls.length == 3) break;
-        }
-
-        return avatarUrls;
-      });
-    });
-  },
-
-  // Inject avatars for thread summary (thread item) |node| in a thread list.
-  inject(node) {
-    var header = node.querySelector(
-        'ec-thread-summary .main-header .panel-description a.header');
-    if (header === null) {
-      console.error(
-          '[threadListAvatars] Header is not present in the thread item\'s DOM.');
-      return;
-    }
-
-    var thread = parseUrl(header.href);
-    if (thread === false) {
-      console.error('[threadListAvatars] Thread\'s link cannot be parsed.');
-      return;
-    }
-
-    this.getVisibleAvatars(thread)
-        .then(avatarUrls => {
-          var avatarsContainer = document.createElement('div');
-          avatarsContainer.classList.add('TWPT-avatars');
-
-          var count = Math.floor(Math.random() * 4);
-
-          for (var i = 0; i < avatarUrls.length; ++i) {
-            var avatar = document.createElement('div');
-            avatar.classList.add('TWPT-avatar');
-            avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')';
-            avatarsContainer.appendChild(avatar);
-          }
-
-          header.appendChild(avatarsContainer);
-        })
-        .catch(err => {
-          console.error(
-              '[threadListAvatars] Could not retrieve avatars for thread',
-              thread, err);
-        });
-  },
-};
-
-var autoRefresh = {
-  isLookingForUpdates: false,
-  isUpdatePromptShown: false,
-  lastTimestamp: null,
-  filter: null,
-  path: null,
-  snackbar: null,
-  interval: null,
-  firstCallTimeout: null,
-  intervalMs: 3 * 60 * 1000,   // 3 minutes
-  firstCallDelayMs: 3 * 1000,  // 3 seconds
-  getStartupData() {
-    return JSON.parse(
-        document.querySelector('html').getAttribute('data-startup'));
-  },
-  isOrderedByTimestampDescending() {
-    var startup = this.getStartupData();
-    // Returns orderOptions.by == TIMESTAMP && orderOptions.desc == true
-    return (
-        startup?.[1]?.[1]?.[3]?.[14]?.[1] == 1 &&
-        startup?.[1]?.[1]?.[3]?.[14]?.[2] == true);
-  },
-  getCustomFilter(path) {
-    var searchRegex = /^\/s\/community\/search\/([^\/]*)/;
-    var matches = path.match(searchRegex);
-    if (matches !== null && matches.length > 1) {
-      var search = decodeURIComponent(matches[1]);
-      var params = new URLSearchParams(search);
-      return params.get('query') || '';
-    }
-
-    return '';
-  },
-  filterHasOverride(filter, override) {
-    var escapedOverride = override.replace(/([^\w\d\s])/gi, '\\$1');
-    var regex = new RegExp('[^a-zA-Z0-9]?' + escapedOverride + ':');
-    return regex.test(filter);
-  },
-  getFilter(path) {
-    var query = this.getCustomFilter(path);
-
-    // Note: This logic has been copied and adapted from the
-    // _buildQuery$1$threadId function in the Community Console
-    var conditions = '';
-    var startup = this.getStartupData();
-
-    // TODO(avm99963): if the selected forums are changed without reloading the
-    // page, this will get the old selected forums. Fix this.
-    var forums = startup?.[1]?.[1]?.[3]?.[8] ?? [];
-    if (!this.filterHasOverride(query, 'forum') && forums !== null &&
-        forums.length > 0)
-      conditions += ' forum:(' + forums.join(' | ') + ')';
-
-    var langs = startup?.[1]?.[1]?.[3]?.[5] ?? [];
-    if (!this.filterHasOverride(query, 'lang') && langs !== null &&
-        langs.length > 0)
-      conditions += ' lang:(' + langs.map(l => '"' + l + '"').join(' | ') + ')';
-
-    if (query.length !== 0 && conditions.length !== 0)
-      return '(' + query + ')' + conditions;
-    return query + conditions;
-  },
-  getLastTimestamp() {
-    var APIRequestUrl = 'https://support.google.com/s/community/api/ViewForum' +
-        (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
-
-    return fetch(APIRequestUrl, {
-             'headers': {
-               'content-type': 'text/plain; charset=utf-8',
-             },
-             'body': JSON.stringify({
-               1: '0',  // TODO: Change, when only a forum is selected, it
-                        // should be set here
-               2: {
-                 1: {
-                   2: 2,
-                 },
-                 2: {
-                   1: 1,
-                   2: true,
-                 },
-                 12: this.filter,
-               },
-             }),
-             'method': 'POST',
-             'mode': 'cors',
-             'credentials': 'include',
-           })
-        .then(res => {
-          if (res.status == 200 || res.status == 400) {
-            return res.json().then(data => ({
-                                     status: res.status,
-                                     body: data,
-                                   }));
-          } else {
-            throw new Error('Status code ' + res.status + ' was not expected.');
-          }
-        })
-        .then(res => {
-          if (res.status == 400) {
-            throw new Error(
-                res.body[4] ||
-                ('Response status: 400. Error code: ' + res.body[2]));
-          }
-
-          return res.body;
-        })
-        .then(body => {
-          var timestamp = body?.[1]?.[2]?.[0]?.[2]?.[17];
-          if (timestamp === undefined)
-            throw new Error(
-                'Unexpected body of response (' +
-                (body?.[1]?.[2]?.[0] === undefined ?
-                     'no threads were returned' :
-                     'the timestamp value is not present in the first thread') +
-                ').');
-
-          return timestamp;
-        });
-    // TODO(avm99963): Add retry mechanism (sometimes thread lists are empty,
-    // but when loading the next page the thread appears).
-    //
-    // NOTE(avm99963): It seems like loading the first 2 threads instead of only
-    // the first one fixes this (empty lists are now rarely returned).
-  },
-  unregister() {
-    console.debug('autorefresh_list: unregistering');
-
-    if (!this.isLookingForUpdates) return;
-
-    window.clearTimeout(this.firstCallTimeout);
-    window.clearInterval(this.interval);
-    this.isUpdatePromptShown = false;
-    this.isLookingForUpdates = false;
-  },
-  showUpdatePrompt() {
-    this.snackbar.classList.remove('TWPT-hidden');
-    document.title = '[!!!] ' + document.title.replace('[!!!] ', '');
-    this.isUpdatePromptShown = true;
-  },
-  hideUpdatePrompt() {
-    this.snackbar.classList.add('TWPT-hidden');
-    document.title = document.title.replace('[!!!] ', '');
-    this.isUpdatePromptShown = false;
-  },
-  injectUpdatePrompt() {
-    var pane = document.createElement('div');
-    pane.classList.add('TWPT-pane-for-snackbar');
-
-    var snackbar = document.createElement('material-snackbar-panel');
-    snackbar.classList.add('TWPT-snackbar');
-    snackbar.classList.add('TWPT-hidden');
-
-    var ac = document.createElement('div');
-    ac.classList.add('TWPT-animation-container');
-
-    var nb = document.createElement('div');
-    nb.classList.add('TWPT-notification-bar');
-
-    var ft = document.createElement('focus-trap');
-
-    var content = document.createElement('div');
-    content.classList.add('TWPT-focus-content-wrapper');
-
-    var badge = createExtBadge();
-
-    var message = document.createElement('div');
-    message.classList.add('TWPT-message');
-    message.textContent =
-        chrome.i18n.getMessage('inject_autorefresh_list_snackbar_message');
-
-    var action = document.createElement('div');
-    action.classList.add('TWPT-action');
-    action.textContent =
-        chrome.i18n.getMessage('inject_autorefresh_list_snackbar_action');
-
-    action.addEventListener('click', e => {
-      this.hideUpdatePrompt();
-      document.querySelector('.app-title-button').click();
-    });
-
-    content.append(badge, message, action);
-    ft.append(content);
-    nb.append(ft);
-    ac.append(nb);
-    snackbar.append(ac);
-    pane.append(snackbar);
-    document.getElementById('default-acx-overlay-container').append(pane);
-    this.snackbar = snackbar;
-  },
-  checkUpdate() {
-    if (location.pathname != this.path) {
-      this.unregister();
-      return;
-    }
-
-    if (this.isUpdatePromptShown) return;
-
-    console.debug('Checking for update at: ', new Date());
-
-    this.getLastTimestamp()
-        .then(timestamp => {
-          if (timestamp != this.lastTimestamp) this.showUpdatePrompt();
-        })
-        .catch(
-            err => console.error(
-                'Coudln\'t get last timestamp (while updating): ', err));
-  },
-  firstCall() {
-    console.debug(
-        'autorefresh_list: now performing first call to finish setup (filter: [' +
-        this.filter + '])');
-
-    if (location.pathname != this.path) {
-      this.unregister();
-      return;
-    }
-
-    this.getLastTimestamp()
-        .then(timestamp => {
-          this.lastTimestamp = timestamp;
-          var checkUpdateCallback = this.checkUpdate.bind(this);
-          this.interval =
-              window.setInterval(checkUpdateCallback, this.intervalMs);
-        })
-        .catch(
-            err => console.error(
-                'Couldn\'t get last timestamp (while setting up): ', err));
-  },
-  setUp() {
-    if (!this.isOrderedByTimestampDescending()) return;
-
-    this.unregister();
-
-    console.debug('autorefresh_list: starting set up...');
-
-    if (this.snackbar === null) this.injectUpdatePrompt();
-    this.isLookingForUpdates = true;
-    this.path = location.pathname;
-    this.filter = this.getFilter(this.path);
-
-    var firstCall = this.firstCall.bind(this);
-    this.firstCallTimeout = window.setTimeout(firstCall, this.firstCallDelayMs);
-  },
-};
-
-function isDarkThemeOn() {
-  if (!options.ccdarktheme) return false;
-
-  if (options.ccdarktheme_mode == 'switch')
-    return options.ccdarktheme_switch_status;
-
-  return window.matchMedia &&
-      window.matchMedia('(prefers-color-scheme: dark)').matches;
-}
-
-var unifiedProfilesFix = {
-  checkIframe(iframe) {
-    var srcRegex = /support.*\.google\.com\/profile\//;
-    return srcRegex.test(iframe.src ?? '');
-  },
-  fixIframe(iframe) {
-    console.info('[unifiedProfilesFix] Fixing unified profiles iframe');
-    var url = new URL(iframe.src);
-    url.searchParams.set('dark', 1);
-    iframe.src = url.href;
-  },
-};
-
-function injectPreviousPostsLinks(nameElement) {
-  var mainCardContent = getNParent(nameElement, 3);
-  if (mainCardContent === null) {
-    console.error(
-        '[previousposts] Couldn\'t find |.main-card-content| element.');
-    return;
-  }
-
-  var forumId = location.href.split('/forum/')[1].split('/')[0] || '0';
-
-  var nameTag =
-      (nameElement.tagName == 'EC-DISPLAY-NAME-EDITOR' ?
-           nameElement.querySelector('.top-section > span') ?? nameElement :
-           nameElement);
-  var name = escapeUsername(nameTag.textContent);
-  var query1 = encodeURIComponent(
-      '(creator:"' + name + '" | replier:"' + name + '") forum:' + forumId);
-  var query2 = encodeURIComponent(
-      '(creator:"' + name + '" | replier:"' + name + '") forum:any');
-
-  var container = document.createElement('div');
-  container.classList.add('TWPT-previous-posts');
-
-  var badge = createExtBadge();
-  container.appendChild(badge);
-
-  var linkContainer = document.createElement('div');
-  linkContainer.classList.add('TWPT-previous-posts--links');
-
-  addProfileHistoryLink(linkContainer, 'forum', query1);
-  addProfileHistoryLink(linkContainer, 'all', query2);
-
-  container.appendChild(linkContainer);
-
-  mainCardContent.appendChild(container);
-}
-
-// Send a request to mark the current thread as read
-function markCurrentThreadAsRead() {
-  console.debug(
-      '[forceMarkAsRead] %cTrying to mark a thread as read.',
-      'color: #1a73e8;');
-
-  var threadRegex =
-      /\/s\/community\/?.*\/forum\/([0-9]+)\/?.*\/thread\/([0-9]+)/;
-
-  var url = location.href;
-  var matches = url.match(threadRegex);
-  if (matches !== null && matches.length > 2) {
-    var forumId = matches[1];
-    var threadId = matches[2];
-
-    console.debug('[forceMarkAsRead] Thread details:', {forumId, threadId});
-
-    return CCApi(
-               'ViewThread', {
-                 1: forumId,
-                 2: threadId,
-                 // options
-                 3: {
-                   // pagination
-                   1: {
-                     2: 0,  // maxNum
-                   },
-                   3: false,   // withMessages
-                   5: false,   // withUserProfile
-                   6: true,    // withUserReadState
-                   9: false,   // withRequestorProfile
-                   10: false,  // withPromotedMessages
-                   11: false,  // withExpertResponder
-                 },
-               },
-               true, authuser)
-        .then(thread => {
-          if (thread?.[1]?.[6] === true) {
-            console.debug(
-                '[forceMarkAsRead] This thread is already marked as read, but marking it as read anyways.');
-          }
-
-          var lastMessageId = thread?.[1]?.[2]?.[10];
-
-          console.debug('[forceMarkAsRead] lastMessageId is:', lastMessageId);
-
-          if (lastMessageId === undefined)
-            throw new Error(
-                'Couldn\'t find lastMessageId in the ViewThread response.');
-
-          return CCApi(
-              'SetUserReadStateBulk', {
-                1: [{
-                  1: forumId,
-                  2: threadId,
-                  3: lastMessageId,
-                }],
-              },
-              true, authuser);
-        })
-        .then(_ => {
-          console.debug(
-              '[forceMarkAsRead] %cSuccessfully set as read!',
-              'color: #1e8e3e;');
-        })
-        .catch(err => {
-          console.error(
-              '[forceMarkAsRead] Error while marking current thread as read: ',
-              err);
-        });
-  } else {
-    console.error(
-        '[forceMarkAsRead] Couldn\'t retrieve forumId and threadId from the current URL.',
-        url);
-  }
-}
-
-const watchedNodesSelectors = [
-  // App container (used to set up the intersection observer and inject the dark
-  // mode button)
-  'ec-app',
-
-  // Load more bar (for the "load more"/"load all" buttons)
-  '.load-more-bar',
-
-  // Username span/editor inside ec-user (user profile view)
-  'ec-user .main-card .header > .name > span',
-  'ec-user .main-card .header > .name > ec-display-name-editor',
-
-  // Rich text editor
-  'ec-movable-dialog',
-  'ec-rich-text-editor',
-
-  // Read/unread bulk action in the list of thread, for the batch lock feature
-  'ec-bulk-actions material-button[debugid="mark-read-button"]',
-  'ec-bulk-actions material-button[debugid="mark-unread-button"]',
-
-  // Thread list items (used to inject the avatars)
-  'li',
-
-  // Thread list (used for the autorefresh feature)
-  'ec-thread-list',
-
-  // Unified profile iframe
-  'iframe',
-
-  // Thread component
-  'ec-thread',
-];
-
-function handleCandidateNode(node) {
-  if (typeof node.classList !== 'undefined') {
-    if (('tagName' in node) && node.tagName == 'EC-APP') {
-      // Set up the intersectionObserver
-      if (typeof intersectionObserver === 'undefined') {
-        var scrollableContent = node.querySelector('.scrollable-content');
-        if (scrollableContent !== null) {
-          intersectionOptions = {
-            root: scrollableContent,
-            rootMargin: '0px',
-            threshold: 1.0,
-          };
-
-          intersectionObserver = new IntersectionObserver(
-              intersectionCallback, intersectionOptions);
-        }
-      }
-
-      // Inject the dark mode button
-      if (options.ccdarktheme && options.ccdarktheme_mode == 'switch') {
-        var rightControl = node.querySelector('header .right-control');
-        if (rightControl !== null) injectDarkModeButton(rightControl);
-      }
-    }
-
-    // Start the intersectionObserver for the "load more"/"load all" buttons
-    // inside a thread
-    if ((options.thread || options.threadall) &&
-        node.classList.contains('load-more-bar')) {
-      if (typeof intersectionObserver !== 'undefined') {
-        if (options.thread)
-          intersectionObserver.observe(node.querySelector('.load-more-button'));
-        if (options.threadall)
-          intersectionObserver.observe(node.querySelector('.load-all-button'));
-      } else {
-        console.warn(
-            '[infinitescroll] ' +
-            'The intersectionObserver is not ready yet.');
-      }
-    }
-
-    // Show the "previous posts" links
-    //   Here we're selecting the 'ec-user > div' element (unique child)
-    if (options.history &&
-        (node.matches('ec-user .main-card .header > .name > span') ||
-         node.matches(
-             'ec-user .main-card .header > .name > ec-display-name-editor'))) {
-      injectPreviousPostsLinks(node);
-    }
-
-    // Fix the drag&drop issue with the rich text editor
-    //
-    //   We target both tags because in different contexts different
-    //   elements containing the text editor get added to the DOM structure.
-    //   Sometimes it's a EC-MOVABLE-DIALOG which already contains the
-    //   EC-RICH-TEXT-EDITOR, and sometimes it's the EC-RICH-TEXT-EDITOR
-    //   directly.
-    if (options.ccdragndropfix && ('tagName' in node) &&
-        (node.tagName == 'EC-MOVABLE-DIALOG' ||
-         node.tagName == 'EC-RICH-TEXT-EDITOR')) {
-      applyDragAndDropFix(node);
-    }
-
-    // Inject the batch lock button in the thread list
-    if (options.batchlock && nodeIsReadToggleBtn(node)) {
-      addBatchLockBtn(node);
-    }
-
-    // Inject avatar links to threads in the thread list
-    if (options.threadlistavatars && ('tagName' in node) &&
-        (node.tagName == 'LI') &&
-        node.querySelector('ec-thread-summary') !== null) {
-      avatars.inject(node);
-    }
-
-    // Set up the autorefresh list feature
-    if (options.autorefreshlist && ('tagName' in node) &&
-        node.tagName == 'EC-THREAD-LIST') {
-      autoRefresh.setUp();
-    }
-
-    // Redirect unified profile iframe to dark version if applicable
-    if (node.tagName == 'IFRAME' && isDarkThemeOn() &&
-        unifiedProfilesFix.checkIframe(node)) {
-      unifiedProfilesFix.fixIframe(node);
-    }
-
-    // Force mark thread as read
-    if (options.forcemarkasread && node.tagName == 'EC-THREAD') {
-      markCurrentThreadAsRead();
-    }
-  }
-}
-
-function handleRemovedNode(node) {
-  // Remove snackbar when exiting thread list view
-  if (options.autorefreshlist && 'tagName' in node &&
-      node.tagName == 'EC-THREAD-LIST') {
-    autoRefresh.hideUpdatePrompt();
-  }
-}
-
-function mutationCallback(mutationList, observer) {
-  mutationList.forEach((mutation) => {
-    if (mutation.type == 'childList') {
-      mutation.addedNodes.forEach(function(node) {
-        handleCandidateNode(node);
-      });
-
-      mutation.removedNodes.forEach(function(node) {
-        handleRemovedNode(node);
-      });
-    }
-  });
-}
-
-function intersectionCallback(entries, observer) {
-  entries.forEach(entry => {
-    if (entry.isIntersecting) {
-      entry.target.click();
-    }
-  });
-};
-
-var observerOptions = {
-  childList: true,
-  subtree: true,
-};
-
-chrome.storage.sync.get(null, function(items) {
-  options = items;
-
-  var startup =
-      JSON.parse(document.querySelector('html').getAttribute('data-startup'));
-  authuser = startup[2][1] || '0';
-
-  // Before starting the mutation Observer, check whether we missed any
-  // mutations by manually checking whether some watched nodes already
-  // exist.
-  var cssSelectors = watchedNodesSelectors.join(',');
-  document.querySelectorAll(cssSelectors)
-      .forEach(node => handleCandidateNode(node));
-
-  mutationObserver = new MutationObserver(mutationCallback);
-  mutationObserver.observe(document.body, observerOptions);
-
-  if (options.fixedtoolbar) {
-    injectStyles(
-        'ec-bulk-actions{position: sticky; top: 0; background: var(--TWPT-primary-background, #fff); z-index: 96;}');
-  }
-
-  if (options.increasecontrast) {
-    injectStyles(
-        '.thread-summary.read:not(.checked){background: var(--TWPT-thread-read-background, #ecedee)!important;}');
-  }
-
-  if (options.stickysidebarheaders) {
-    injectStyles(
-        'material-drawer .main-header{background: var(--TWPT-drawer-background, #fff)!important; position: sticky; top: 0; z-index: 1;}');
-  }
-
-  if (options.enhancedannouncementsdot) {
-    injectStylesheet(
-        chrome.runtime.getURL('injections/enhanced_announcements_dot.css'));
-  }
-
-  if (options.repositionexpandthread) {
-    injectStylesheet(
-        chrome.runtime.getURL('injections/reposition_expand_thread.css'));
-  }
-
-  if (options.ccforcehidedrawer) {
-    var drawer = document.querySelector('material-drawer');
-    if (drawer !== null && drawer.classList.contains('mat-drawer-expanded')) {
-      document.querySelector('.material-drawer-button').click();
-    }
-  }
-
-  if (options.batchlock) {
-    injectScript(chrome.runtime.getURL('injections/batchlock_inject.js'));
-    injectStylesheet(chrome.runtime.getURL('injections/batchlock_inject.css'));
-  }
-
-  if (options.threadlistavatars) {
-    injectStylesheet(
-        chrome.runtime.getURL('injections/thread_list_avatars.css'));
-  }
-
-  if (options.autorefreshlist) {
-    injectStylesheet(chrome.runtime.getURL('injections/autorefresh_list.css'));
-  }
-});
diff --git a/src/content_scripts/profileindicator_inject.js b/src/content_scripts/profileindicator_inject.js
deleted file mode 100644
index 8cd1cfc..0000000
--- a/src/content_scripts/profileindicator_inject.js
+++ /dev/null
@@ -1,8 +0,0 @@
-chrome.storage.sync.get(null, function(options) {
-  if (options.profileindicator || options.profileindicatoralt) {
-    injectScript(
-        chrome.runtime.getURL('injections/profileindicator_inject.js'));
-    injectStylesheet(
-        chrome.runtime.getURL('injections/profileindicator_inject.css'));
-  }
-});
diff --git a/src/injections/batchlock_inject.js b/src/injections/batchLock.js
similarity index 91%
rename from src/injections/batchlock_inject.js
rename to src/injections/batchLock.js
index 3ffd298..9a91203 100644
--- a/src/injections/batchlock_inject.js
+++ b/src/injections/batchLock.js
@@ -1,16 +1,5 @@
-function parseUrl(url) {
-  var forum_a = url.match(/forum\/([0-9]+)/i);
-  var thread_a = url.match(/thread\/([0-9]+)/i);
-
-  if (forum_a === null || thread_a === null) {
-    return false;
-  }
-
-  return {
-    'forum': forum_a[1],
-    'thread': thread_a[1],
-  };
-}
+import {parseUrl} from '../common/commonUtils.js';
+import {getAuthUser} from '../common/communityConsoleUtils.js';
 
 function recursiveParentElement(el, tag) {
   while (el !== document.documentElement) {
@@ -20,12 +9,6 @@
   return undefined;
 }
 
-function returnAuthUser() {
-  var startup =
-      JSON.parse(document.querySelector('html').getAttribute('data-startup'));
-  return startup[2][1] || '0';
-}
-
 // Source:
 // https://stackoverflow.com/questions/33063774/communication-from-an-injected-script-to-the-content-script-with-a-response
 var contentScriptRequest = (function() {
@@ -122,7 +105,7 @@
   modal.querySelector('main').textContent = '';
   modal.querySelector('main').append(p, log);
 
-  var authuser = returnAuthUser();
+  var authuser = getAuthUser();
   var APIRequestUrl =
       'https://support.google.com/s/community/api/SetThreadAttribute' +
       (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
diff --git a/src/injections/profileindicator_inject.js b/src/injections/profileIndicator.js
similarity index 97%
rename from src/injections/profileindicator_inject.js
rename to src/injections/profileIndicator.js
index b3bbdc6..ee897fe 100644
--- a/src/injections/profileindicator_inject.js
+++ b/src/injections/profileIndicator.js
@@ -1,3 +1,5 @@
+import {escapeUsername} from '../common/communityConsoleUtils.js';
+
 var CCProfileRegex =
     /^(?:https:\/\/support\.google\.com)?\/s\/community(?:\/forum\/[0-9]*)?\/user\/(?:[0-9]+)$/;
 var CCRegex = /^https:\/\/support\.google\.com\/s\/community/;
@@ -39,12 +41,6 @@
   return false;
 }
 
-function escapeUsername(username) {
-  var quoteRegex = /"/g;
-  var commentRegex = /<!---->/g;
-  return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
-}
-
 function APIRequest(action, body) {
   var authuserPart =
       (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
@@ -381,7 +377,7 @@
     getOptionsAndHandleIndicators(node, true);
   }
 
-  mutationObserver = new MutationObserver(mutationCallback);
+  var mutationObserver = new MutationObserver(mutationCallback);
   mutationObserver.observe(document.body, observerOptions);
 } else {
   // We are in TW
diff --git a/src/options/options_common.js b/src/optionsCommon.js
similarity index 94%
rename from src/options/options_common.js
rename to src/optionsCommon.js
index 098a632..afb6bde 100644
--- a/src/options/options_common.js
+++ b/src/optionsCommon.js
@@ -1,3 +1,6 @@
+import {cleanUpOptions, optionsPrototype, specialOptions} from './common/optionsUtils.js';
+import {isFirefox, isReleaseVersion} from './common/extUtils.js';
+
 var savedSuccessfullyTimeout = null;
 
 const exclusiveOptions = [['thread', 'threadall']];
@@ -88,7 +91,10 @@
   chrome.storage.sync.get(null, function(items) {
     items = cleanUpOptions(items, false);
 
-    for ([opt, optMeta] of Object.entries(optionsPrototype)) {
+    for (var entry of Object.entries(optionsPrototype)) {
+      var opt = entry[0];
+      var optMeta = entry[1];
+
       if (!isOptionShown(opt)) continue;
 
       if (specialOptions.includes(opt)) {
diff --git a/src/LICENSE b/src/static/LICENSE
similarity index 100%
rename from src/LICENSE
rename to src/static/LICENSE
diff --git a/src/_locales/ca/messages.json b/src/static/_locales/ca/messages.json
similarity index 100%
rename from src/_locales/ca/messages.json
rename to src/static/_locales/ca/messages.json
diff --git a/src/_locales/en/messages.json b/src/static/_locales/en/messages.json
similarity index 100%
rename from src/_locales/en/messages.json
rename to src/static/_locales/en/messages.json
diff --git a/src/_locales/es/messages.json b/src/static/_locales/es/messages.json
similarity index 100%
rename from src/_locales/es/messages.json
rename to src/static/_locales/es/messages.json
diff --git a/src/_locales/ru/OWNERS b/src/static/_locales/ru/OWNERS
similarity index 100%
rename from src/_locales/ru/OWNERS
rename to src/static/_locales/ru/OWNERS
diff --git a/src/_locales/ru/messages.json b/src/static/_locales/ru/messages.json
similarity index 100%
rename from src/_locales/ru/messages.json
rename to src/static/_locales/ru/messages.json
diff --git a/src/injections/autorefresh_list.css b/src/static/css/autorefresh_list.css
similarity index 100%
rename from src/injections/autorefresh_list.css
rename to src/static/css/autorefresh_list.css
diff --git a/src/injections/batchlock_inject.css b/src/static/css/batchlock_inject.css
similarity index 100%
rename from src/injections/batchlock_inject.css
rename to src/static/css/batchlock_inject.css
diff --git a/src/injections/ccdarktheme.css b/src/static/css/ccdarktheme.css
similarity index 100%
rename from src/injections/ccdarktheme.css
rename to src/static/css/ccdarktheme.css
diff --git a/src/common/console.css b/src/static/css/common/console.css
similarity index 100%
rename from src/common/console.css
rename to src/static/css/common/console.css
diff --git a/src/common/forum.css b/src/static/css/common/forum.css
similarity index 100%
rename from src/common/forum.css
rename to src/static/css/common/forum.css
diff --git a/src/injections/enhanced_announcements_dot.css b/src/static/css/enhanced_announcements_dot.css
similarity index 100%
rename from src/injections/enhanced_announcements_dot.css
rename to src/static/css/enhanced_announcements_dot.css
diff --git a/src/injections/profileindicator_inject.css b/src/static/css/profileindicator_inject.css
similarity index 100%
rename from src/injections/profileindicator_inject.css
rename to src/static/css/profileindicator_inject.css
diff --git a/src/injections/reposition_expand_thread.css b/src/static/css/reposition_expand_thread.css
similarity index 100%
rename from src/injections/reposition_expand_thread.css
rename to src/static/css/reposition_expand_thread.css
diff --git a/src/injections/thread_list_avatars.css b/src/static/css/thread_list_avatars.css
similarity index 100%
rename from src/injections/thread_list_avatars.css
rename to src/static/css/thread_list_avatars.css
diff --git a/src/icons/128.png b/src/static/icons/128.png
similarity index 100%
rename from src/icons/128.png
rename to src/static/icons/128.png
Binary files differ
diff --git a/src/icons/512.png b/src/static/icons/512.png
similarity index 100%
rename from src/icons/512.png
rename to src/static/icons/512.png
Binary files differ
diff --git a/src/options/chrome_style/chrome_style.css b/src/static/options/chrome_style/chrome_style.css
similarity index 100%
rename from src/options/chrome_style/chrome_style.css
rename to src/static/options/chrome_style/chrome_style.css
diff --git a/src/options/experiments.html b/src/static/options/experiments.html
similarity index 90%
rename from src/options/experiments.html
rename to src/static/options/experiments.html
index d132305..81c8a10 100644
--- a/src/options/experiments.html
+++ b/src/static/options/experiments.html
@@ -18,8 +18,7 @@
       </form>
       <div id="save-indicator"></div>
     </main>
-    <script src="../common/common.js"></script>
     <script src="experiments_bit.js"></script>
-    <script src="options_common.js"></script>
+    <script src="../../optionsCommon.bundle.js"></script>
   </body>
 </html>
diff --git a/src/options/experiments_bit.js b/src/static/options/experiments_bit.js
similarity index 100%
rename from src/options/experiments_bit.js
rename to src/static/options/experiments_bit.js
diff --git a/src/options/options.css b/src/static/options/options.css
similarity index 100%
rename from src/options/options.css
rename to src/static/options/options.css
diff --git a/src/options/options.html b/src/static/options/options.html
similarity index 98%
rename from src/options/options.html
rename to src/static/options/options.html
index 8a09d04..09ce1bb 100644
--- a/src/options/options.html
+++ b/src/static/options/options.html
@@ -54,8 +54,7 @@
       </form>
       <div id="save-indicator"></div>
     </main>
-    <script src="../common/common.js"></script>
     <script src="options_bit.js"></script>
-    <script src="options_common.js"></script>
+    <script src="../../optionsCommon.bundle.js"></script>
   </body>
 </html>
diff --git a/src/options/options_bit.js b/src/static/options/options_bit.js
similarity index 100%
rename from src/options/options_bit.js
rename to src/static/options/options_bit.js
diff --git a/src/sw.js b/src/sw.js
index 2f181cb..b8dccf2 100644
--- a/src/sw.js
+++ b/src/sw.js
@@ -1,6 +1,5 @@
 // IMPORTANT: keep this file in sync with background.js
-
-importScripts('common/common.js')
+import {cleanUpOptions} from './common/optionsUtils.js'
 
 // When the extension gets updated, set new options to their default value.
 chrome.runtime.onInstalled.addListener(details => {
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index 67eeab6..e3efb13 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -18,34 +18,34 @@
   "content_scripts": [
     {
       "matches": ["https://support.google.com/s/community*"],
-      "js": ["common/api.js", "common/content_scripts.js", "content_scripts/console_inject.js"]
+      "js": ["communityConsoleMain.bundle.js"]
     },
     {
       "matches": ["https://support.google.com/s/community*"],
-      "js": ["common/content_scripts.js", "content_scripts/console_inject_start.js"],
-      "css": ["common/console.css"],
+      "js": ["communityConsoleStart.bundle.js"],
+      "css": ["css/common/console.css"],
       "run_at": "document_start"
     },
     {
       "matches": ["https://support.google.com/*/threads*"],
-      "js": ["content_scripts/forum_inject.js"]
+      "js": ["publicForum.bundle.js"]
     },
     {
       "matches": ["https://support.google.com/*/thread/*"],
       "exclude_matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/new*"],
-      "js": ["common/content_scripts.js", "content_scripts/thread_inject.js"],
+      "js": ["publicThread.bundle.js"],
       "run_at": "document_end"
     },
     {
       "matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/*"],
       "exclude_matches": ["https://support.google.com/*/thread/new*"],
-      "js": ["common/content_scripts.js", "common/cs_event_listener.js", "content_scripts/profileindicator_inject.js"]
+      "js": ["profileIndicator.bundle.js"]
     },
     {
       "matches": ["https://support.google.com/*/profile/*", "https://support.google.com/profile/*"],
       "all_frames": true,
-      "js": ["common/content_scripts.js", "content_scripts/profile_inject.js"],
-      "css": ["common/forum.css"]
+      "js": ["profile.bundle.js"],
+      "css": ["css/common/forum.css"]
     }
   ],
   "permissions": [
@@ -64,15 +64,16 @@
     {
       "resources": [
 #endif
-        "injections/profileindicator_inject.js",
-        "injections/profileindicator_inject.css",
-        "injections/ccdarktheme.css",
-        "injections/batchlock_inject.js",
-        "injections/batchlock_inject.css",
-        "injections/enhanced_announcements_dot.css",
-        "injections/reposition_expand_thread.css",
-        "injections/thread_list_avatars.css",
-        "injections/autorefresh_list.css"
+        "profileIndicatorInject.bundle.js",
+        "batchLockInject.bundle.js",
+
+        "css/profileindicator_inject.css",
+        "css/ccdarktheme.css",
+        "css/batchlock_inject.css",
+        "css/enhanced_announcements_dot.css",
+        "css/reposition_expand_thread.css",
+        "css/thread_list_avatars.css",
+        "css/autorefresh_list.css"
 #if defined(CHROMIUM_MV3)
       ],
       "matches": [
@@ -99,13 +100,10 @@
 #if defined(CHROMIUM)
     "persistent": false,
 #endif
-    "scripts": [
-      "common/common.js",
-      "background.js"
-    ]
+    "scripts": ["background.bundle.js"]
 #endif
 #if defined(CHROMIUM_MV3)
-    "service_worker": "sw.js"
+    "service_worker": "sw.bundle.js"
 #endif
   },
 #if defined(GECKO)
diff --git a/generateManifest.go b/tools/generateManifest.go
similarity index 89%
rename from generateManifest.go
rename to tools/generateManifest.go
index 1c2e4f0..185f1c3 100644
--- a/generateManifest.go
+++ b/tools/generateManifest.go
@@ -16,7 +16,6 @@
 
 const (
   manifestTemplate = "templates/manifest.gjson"
-  manifestSrc = "src/manifest.json"
 )
 
 var beginningOfIfStatement = regexp.MustCompile(`^\s*#if defined\(([^\(\)]*)\)\s*$`)
@@ -24,6 +23,7 @@
 
 var (
   quietMode = flag.Bool("quiet", false, "Quiet mode")
+  destFile  = flag.String("dest", "", "Destination file")
 )
 
 func FindWithCaseFolding(slice []string, val string) bool {
@@ -86,15 +86,19 @@
     log.Fatalf("Pass the dependencies as arguments (for instance, run `go run generateManifest.go CHROMIUM`).")
   }
 
+  if *destFile == "" {
+    log.Fatalf("Pass the destination file name via the -dest flag.")
+  }
+
   template, err := os.Open(manifestTemplate)
   if err != nil {
     log.Fatalf("Couldn't open file %v: %v", manifestTemplate, err)
   }
   defer template.Close()
 
-  dest, err := os.Create(manifestSrc)
+  dest, err := os.Create(*destFile)
   if err != nil {
-    log.Fatalf("Couldn't create file %v: %v", manifestSrc, err)
+    log.Fatalf("Couldn't create file %v: %v", *destFile, err)
   }
   defer dest.Close()
 
diff --git a/generateManifest_test.go b/tools/generateManifest_test.go
similarity index 100%
rename from generateManifest_test.go
rename to tools/generateManifest_test.go
diff --git a/release.bash b/tools/release.bash
similarity index 86%
rename from release.bash
rename to tools/release.bash
index 852e4f0..338d9e1 100644
--- a/release.bash
+++ b/tools/release.bash
@@ -1,6 +1,8 @@
 #!/bin/bash
 #
 # Generate release files (ZIP archives of the extension source code).
+#
+# Precondition: webpack has already built the extension.
 
 # Prints help text
 function usage() {
@@ -21,7 +23,7 @@
 
 # Updates manifest.json field
 function set_manifest_field() {
-  sed -i -E "s/\"$1\": \"[^\"]*\"/\"$1\": \"$2\"/" src/manifest.json
+  sed -i -E "s/\"$1\": \"[^\"]*\"/\"$1\": \"$2\"/" dist/$browser/manifest.json
 }
 
 # Get options
@@ -30,6 +32,7 @@
 
 channel=stable
 browser=chromium
+folder=null
 
 while true; do
   case "$1" in
@@ -64,12 +67,6 @@
 
 echo "Started building release..."
 
-# First of all, generate the appropriate manifest.json file for the
-# target browser
-dependencies=(${browser})
-
-go run generateManifest.go "${dependencies[@]}"
-
 # This is the version name which git gives us
 version=$(git describe --always --tags --dirty)
 
@@ -103,11 +100,8 @@
 # Create ZIP file for upload to the Chrome Web Store
 mkdir -p out
 rm -rf out/twpowertools-$version-$browser-$channel.zip
-(cd src &&
-  zip -rq ../out/twpowertools-$version-$browser-$channel.zip * -x "*/.git*" \
+(cd dist/$browser &&
+  zip -rq ../../out/twpowertools-$version-$browser-$channel.zip * -x "*/.git*" \
   -x "*/\.DS_Store" -x "*/OWNERS")
 
-# Clean generated manifest.json file
-rm -f src/manifest.json
-
 echo "Done!"
diff --git a/testdata/manifest_frozen.gjson b/tools/testdata/manifest_frozen.gjson
similarity index 100%
rename from testdata/manifest_frozen.gjson
rename to tools/testdata/manifest_frozen.gjson
diff --git a/testdata/manifest_frozen_chromium_expected.json b/tools/testdata/manifest_frozen_chromium_expected.json
similarity index 100%
rename from testdata/manifest_frozen_chromium_expected.json
rename to tools/testdata/manifest_frozen_chromium_expected.json
diff --git a/testdata/manifest_frozen_gecko_expected.json b/tools/testdata/manifest_frozen_gecko_expected.json
similarity index 100%
rename from testdata/manifest_frozen_gecko_expected.json
rename to tools/testdata/manifest_frozen_gecko_expected.json
diff --git a/testdata/manifest_small1.gjson b/tools/testdata/manifest_small1.gjson
similarity index 100%
rename from testdata/manifest_small1.gjson
rename to tools/testdata/manifest_small1.gjson
diff --git a/testdata/manifest_small1_expected.json b/tools/testdata/manifest_small1_expected.json
similarity index 100%
rename from testdata/manifest_small1_expected.json
rename to tools/testdata/manifest_small1_expected.json
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..6843f0d
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,83 @@
+const {CleanWebpackPlugin} = require('clean-webpack-plugin');
+const path = require('path');
+const json5 = require('json5');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const WebpackShellPluginNext = require('webpack-shell-plugin-next');
+
+module.exports = (env, args) => {
+  let entry = {
+    // Content scripts
+    communityConsoleMain: './src/contentScripts/communityConsole/main.js',
+    communityConsoleStart: './src/contentScripts/communityConsole/start.js',
+    publicForum: './src/contentScripts/publicForum.js',
+    publicThread: './src/contentScripts/publicThread.js',
+    profile: './src/contentScripts/profile.js',
+    profileIndicator: './src/contentScripts/profileIndicator.js',
+
+    // Injected JS
+    profileIndicatorInject: './src/injections/profileIndicator.js',
+    batchLockInject: './src/injections/batchLock.js',
+
+    // Options page
+    optionsCommon: './src/optionsCommon.js',
+  };
+
+  // Background script (or service worker for MV3)
+  if (env.browser_target == 'chromium_mv3')
+    entry.sw = './src/sw.js';
+  else
+    entry.background = './src/background.js';
+
+  let outputPath = path.join(__dirname, 'dist', env.browser_target);
+
+  return {
+    entry,
+    output: {
+      filename: '[name].bundle.js',
+      path: outputPath,
+      clean: {
+        keep(asset) {
+          return asset.includes('static/') || asset.includes('manifest.json') ||
+              asset.includes('LICENSE') || asset.includes('_locales');
+        },
+      },
+    },
+    plugins: [
+      new CleanWebpackPlugin(),
+      new WebpackShellPluginNext({
+        onBuildEnd: {
+          scripts:
+              ['go run tools/generateManifest.go -dest=' +
+               path.join(outputPath, 'manifest.json') + ' ' +
+               env.browser_target]
+        }
+      }),
+      new CopyWebpackPlugin({
+        patterns: [
+          {
+            from: path.join(__dirname, 'src/static'),
+            to: outputPath,
+            globOptions: {
+              ignore: ['**/OWNERS'],
+            }
+          },
+        ]
+      }),
+    ],
+    // NOTE: Change to
+    //   (args.mode == 'production' ? 'source-map' : 'inline-source-map')
+    // once https://crbug.com/212374 is fixed.
+    devtool: 'inline-source-map',
+    module: {
+      rules: [
+        {
+          test: /\.json5$/,
+          type: 'json',
+          parser: {
+            parse: json5.parse,
+          },
+        },
+      ]
+    },
+  };
+};