Migrate from rules_nodejs to rules_js

This patch is huge because I can't easily break it into smaller
pieces. This is largely because a few things are changing with the
migration.

Firstly, we're upgrading the angular libraries and our version of
typescript. This is actually not too disruptive. It required some
changes in the top-level `package.json` file.

Secondly, the new rules have this concept of copying everything into
the `bazel-bin` directory and executing out of there. This makes the
various tools like node and angular happy, but means that a few file
paths are changing. For example, the `common.css` file can't have the
same path in the source tree as it does in the `bazel-bin` tree, so I
moved it to a `common` directory inside the `bazel-bin` tree. You can
read more about this here:
https://docs.aspect.build/rules/aspect_rules_js#running-nodejs-programs

Thirdly, I couldn't find a simple way to support Protractor in
rules_js. Protractor is the end-to-end testing framework we use for
the scouting application. Since protractor is getting deprecated and
won't receive any more updates, it's time to move to something else.
We settled on Cypress because it appears to be popular and should make
debugging easier for the students. For example, it takes screenshots
of the browser when an assertion fails. It would have been ideal to
switch to Cypress before this migration, but I couldn't find a simple
way to make that work with rules_nodejs. In other words, this
migration patch is huge in part because we are also switching testing
frameworks at the same time. I wanted to split it out, but it was more
difficult than I would have liked.

Fourthly, I also needed to migrate the flatbuffer rules. This I think
is relatively low impact, but it again means that this patch is bigger
than I wanted it to be.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I6674874f985952f2e3ef40274da0a2fb9e5631a7
diff --git a/.bazelignore b/.bazelignore
index 5446323..784e4c1 100644
--- a/.bazelignore
+++ b/.bazelignore
@@ -1 +1,12 @@
 external
+node_modules
+scouting/www/node_modules
+scouting/www/counter_button/node_modules
+scouting/www/driver_ranking/node_modules
+scouting/www/entry/node_modules
+scouting/www/import_match_list/node_modules
+scouting/www/match_list/node_modules
+scouting/www/notes/node_modules
+scouting/www/rpc/node_modules
+scouting/www/shift_schedule/node_modules
+scouting/www/view/node_modules
diff --git a/.bazelrc b/.bazelrc
index 831889e..69bc54c 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -98,7 +98,7 @@
 build --spawn_strategy=linux-sandbox
 build --sandbox_default_allow_network=false
 
-build --strategy=TypeScriptCompile=worker --strategy=AngularTemplateCompile=worker
+build --strategy=TsProject=linux-sandbox
 
 # Use our hermetic JDK.
 # Note that this doesn't quite work fully, but it should. See
@@ -111,3 +111,6 @@
 
 # Give each build action and test its own /tmp.
 build --sandbox_tmpfs_path=/tmp
+
+# Prevent cypress from using its own binary. We want to use the hermetic one.
+build --action_env=CYPRESS_INSTALL_BINARY=0
diff --git a/.gitignore b/.gitignore
index c055f98..8570036 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,6 @@
 /tools/python/venv/
 /tools/python/wheelhouse/
 /tools/python/wheelhouse_tmp/
+
+node_modules
+/.aspect
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..e158508
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,6 @@
+# Disabling pnpm [hoisting](https://pnpm.io/npmrc#hoist) by setting `hoist=false` is recommended on
+# projects using rules_js so that pnpm outside of Bazel lays out a node_modules tree similar to what
+# rules_js lays out under Bazel (without a hidden node_modules/.pnpm/node_modules). See
+# https://github.com/aspect-build/rules_js/blob/7377f2d0387cc2a9251137929b1c53ccdb3fbcf0/docs/npm_import.md#npm_translate_lock
+# documentation for more information.
+hoist=false
diff --git a/BUILD b/BUILD
index 413620d..2092844 100644
--- a/BUILD
+++ b/BUILD
@@ -1,10 +1,41 @@
 load("@bazel_gazelle//:def.bzl", "gazelle")
+load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("@aspect_rules_js//npm:defs.bzl", "npm_link_package")
+
+# Link npm packages
+npm_link_all_packages(name = "node_modules")
 
 exports_files([
     "tsconfig.json",
+    "tsconfig.node.json",
     "rollup.config.js",
 ])
 
+# The root repo tsconfig
+ts_config(
+    name = "tsconfig",
+    src = "tsconfig.json",
+    visibility = ["//visibility:public"],
+)
+
+ts_config(
+    name = "tsconfig.node",
+    src = "tsconfig.node.json",
+    visibility = ["//visibility:public"],
+    deps = [":tsconfig"],
+)
+
+npm_link_package(
+    name = "node_modules/flatbuffers",
+    src = "@com_github_google_flatbuffers//ts:flatbuffers",
+)
+
+npm_link_package(
+    name = "node_modules/flatbuffers_reflection",
+    src = "@com_github_google_flatbuffers//reflection:flatbuffers_reflection",
+)
+
 # gazelle:prefix github.com/frc971/971-Robot-Code
 # gazelle:build_file_name BUILD
 # gazelle:proto disable
diff --git a/WORKSPACE b/WORKSPACE
index a4d4406..817e5ba 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -32,10 +32,12 @@
     url = "https://github.com/aspect-build/bazel-lib/releases/download/v1.27.1/bazel-lib-v1.27.1.tar.gz",
 )
 
-load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies")
+load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "register_jq_toolchains")
 
 aspect_bazel_lib_dependencies()
 
+register_jq_toolchains()
+
 http_archive(
     name = "rules_python",
     patch_args = ["-p1"],
@@ -915,44 +917,192 @@
 )
 
 http_archive(
-    name = "build_bazel_rules_nodejs",
-    sha256 = "cfc289523cf1594598215901154a6c2515e8bf3671fd708264a6f6aefe02bf39",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/4.4.6/rules_nodejs-4.4.6.tar.gz"],
+    name = "aspect_rules_js",
+    sha256 = "9fadde0ae6e0101755b8aedabf7d80b166491a8de297c60f6a5179cd0d0fea58",
+    strip_prefix = "rules_js-1.20.0",
+    url = "https://github.com/aspect-build/rules_js/releases/download/v1.20.0/rules_js-v1.20.0.tar.gz",
 )
 
-load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
+load("@aspect_rules_js//npm:npm_import.bzl", "npm_translate_lock", "pnpm_repository")
 
-node_repositories()
+pnpm_repository(name = "pnpm")
 
-# Setup Bazel managed npm dependencies with the `yarn_install` rule.
+load("@aspect_rules_js//js:repositories.bzl", "rules_js_dependencies")
 
-# To run yarn by hand, use:
-#  bazel run @nodejs_linux_amd64//:bin/yarn -- list
-# I'm sure there is a better path, but that works...
-yarn_install(
-    name = "npm",
-    frozen_lockfile = True,
-    package_json = "//:package.json",
-    patch_args = ["-p1"],
-    post_install_patches = [
-        "//third_party:npm/@bazel/protractor/bazel-protractor.patch",
-    ],
-    symlink_node_modules = False,
-    yarn_lock = "//:yarn.lock",
-)
-
-load("@build_bazel_rules_nodejs//toolchains/esbuild:esbuild_repositories.bzl", "esbuild_repositories")
-
-esbuild_repositories(npm_repository = "npm")
+rules_js_dependencies()
 
 http_archive(
-    name = "io_bazel_rules_webtesting",
-    patch_args = ["-p1"],
-    patches = [
-        "@//third_party:rules_webtesting/rules_webtesting.patch",
+    name = "aspect_rules_esbuild",
+    sha256 = "b98cde83e9e6a006d8300e88e2f09da56b5a6c18166465a224cfe36bdcbc03e0",
+    strip_prefix = "aspect-build-rules_esbuild-110b94c",
+    type = "tar.gz",
+    url = "https://github.com/aspect-build/rules_esbuild/tarball/110b94c7f16f328a0eab8aa0b862030055b86564",
+)
+
+load("@aspect_rules_esbuild//esbuild:dependencies.bzl", "rules_esbuild_dependencies")
+
+rules_esbuild_dependencies()
+
+load("@rules_nodejs//nodejs:repositories.bzl", "DEFAULT_NODE_VERSION", "nodejs_register_toolchains")
+
+nodejs_register_toolchains(
+    name = "nodejs",
+    node_version = DEFAULT_NODE_VERSION,
+)
+
+npm_translate_lock(
+    name = "npm",
+    data = [
+        "@//:package.json",
+        "@//:pnpm-workspace.yaml",
+        "@//scouting/www:package.json",
+        "@//scouting/www/counter_button:package.json",
+        "@//scouting/www/driver_ranking:package.json",
+        "@//scouting/www/entry:package.json",
+        "@//scouting/www/import_match_list:package.json",
+        "@//scouting/www/match_list:package.json",
+        "@//scouting/www/notes:package.json",
+        "@//scouting/www/rpc:package.json",
+        "@//scouting/www/shift_schedule:package.json",
+        "@//scouting/www/view:package.json",
     ],
-    sha256 = "e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a",
-    urls = ["https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz"],
+
+    # Running lifecycle hooks on npm package fsevents@2.3.2 fails in a dramatic way:
+    # ```
+    # SyntaxError: Unexpected strict mode reserved word
+    # at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:117:18)
+    # at ESMLoader.moduleProvider (node:internal/modules/esm/loader:337:14)
+    # at async link (node:internal/modules/esm/module_job:70:21)
+    # ```
+    lifecycle_hooks_no_sandbox = False,
+    npmrc = "@//:.npmrc",
+    pnpm_lock = "//:pnpm-lock.yaml",
+    quiet = False,
+    update_pnpm_lock = False,
+    verify_node_modules_ignored = "//:.bazelignore",
+)
+
+load("@aspect_rules_esbuild//esbuild:repositories.bzl", "esbuild_register_toolchains", LATEST_ESBUILD_VERSION = "LATEST_VERSION")
+
+esbuild_register_toolchains(
+    name = "esbuild",
+    esbuild_version = LATEST_ESBUILD_VERSION,
+)
+
+http_archive(
+    name = "aspect_rules_rollup",
+    patch_args = [
+        "-p1",
+    ],
+    patches = [
+        "//third_party:rules_rollup/0001-Fix-resolving-files.patch",
+    ],
+    sha256 = "4c43d20ce377b93cd43a3553e6159a17b85ce80c36a564b55051c2320d32b777",
+    strip_prefix = "rules_rollup-0.13.1",
+    url = "https://github.com/aspect-build/rules_rollup/releases/download/v0.13.1/rules_rollup-v0.13.1.tar.gz",
+)
+
+load("@aspect_rules_rollup//rollup:dependencies.bzl", "rules_rollup_dependencies")
+
+# Fetches the rules_rollup dependencies.
+# If you want to have a different version of some dependency,
+# you should fetch it *before* calling this.
+# Alternatively, you can skip calling this function, so long as you've
+# already fetched all the dependencies.
+rules_rollup_dependencies()
+
+load("@aspect_rules_rollup//rollup:repositories.bzl", "rollup_repositories")
+
+rollup_repositories(name = "rollup")
+
+load("@rollup//:npm_repositories.bzl", rollup_npm_repositories = "npm_repositories")
+
+rollup_npm_repositories()
+
+http_archive(
+    name = "aspect_rules_terser",
+    sha256 = "918e7ac036eca1402cae4d4ddba75ecdcdd886ac35bc0624d9f1ebc7527e369b",
+    strip_prefix = "rules_terser-0.13.0",
+    url = "https://github.com/aspect-build/rules_terser/archive/refs/tags/v0.13.0.tar.gz",
+)
+
+load("@aspect_rules_terser//terser:dependencies.bzl", "rules_terser_dependencies")
+
+rules_terser_dependencies()
+
+# Fetch and register a nodejs interpreter, if you haven't already
+
+nodejs_register_toolchains(
+    name = "node",
+    node_version = DEFAULT_NODE_VERSION,
+)
+
+# Fetch and register the terser tool
+load("@aspect_rules_terser//terser:repositories.bzl", "terser_repositories")
+
+terser_repositories(name = "terser")
+
+load("@terser//:npm_repositories.bzl", terser_npm_repositories = "npm_repositories")
+
+terser_npm_repositories()
+
+http_archive(
+    name = "aspect_rules_ts",
+    sha256 = "db77d904284d21121ae63dbaaadfd8c75ff6d21ad229f92038b415c1ad5019cc",
+    strip_prefix = "rules_ts-1.3.0",
+    url = "https://github.com/aspect-build/rules_ts/releases/download/v1.3.0/rules_ts-v1.3.0.tar.gz",
+)
+
+load("@aspect_rules_ts//ts:repositories.bzl", "rules_ts_dependencies")
+
+rules_ts_dependencies(ts_version_from = "//:package.json")
+
+load("@npm//:repositories.bzl", "npm_repositories")
+
+npm_repositories()
+
+http_archive(
+    name = "aspect_rules_cypress",
+    sha256 = "06d70a2960108607d2e70f9bc6863af6b82317fdfcf7a5a30fd226a5abc46782",
+    strip_prefix = "aspect-build-rules_cypress-3db1b74",
+    type = "tar.gz",
+    urls = [
+        "https://github.com/aspect-build/rules_cypress/tarball/3db1b74818ac4ce1b9d489a6e0065b36c1076761",
+    ],
+)
+
+load("@aspect_rules_cypress//cypress:dependencies.bzl", "rules_cypress_dependencies")
+load("@aspect_rules_cypress//cypress:repositories.bzl", "cypress_register_toolchains")
+
+rules_cypress_dependencies()
+
+cypress_register_toolchains(
+    name = "cypress",
+    cypress_version = "12.3.0",
+)
+
+# Copied from:
+# https://github.com/aspect-build/rules_cypress/blob/3db1b74818ac4ce1b9d489a6e0065b36c1076761/internal_deps.bzl#L47
+#
+# To update CHROME_REVISION, use the below script
+#
+# LASTCHANGE_URL="https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media"
+# CHROME_REVISION=$(curl -s -S $LASTCHANGE_URL)
+# echo "latest CHROME_REVISION_LINUX is $CHROME_REVISION"
+CHROME_REVISION_LINUX = "1072361"
+
+http_archive(
+    name = "chrome_linux",
+    build_file_content = """filegroup(
+name = "all",
+srcs = glob(["**"]),
+visibility = ["//visibility:public"],
+)""",
+    sha256 = "0df22f743facd1e090eff9b7f8d8bdc293fb4dc31ce9156d2ef19b515974a72b",
+    strip_prefix = "chrome-linux",
+    urls = [
+        "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F" + CHROME_REVISION_LINUX + "%2Fchrome-linux.zip?alt=media",
+    ],
 )
 
 http_archive(
@@ -986,14 +1136,6 @@
     version = "1.62.0",
 )
 
-load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories")
-
-web_test_repositories()
-
-load("@io_bazel_rules_webtesting//web/versioned:browsers-0.3.3.bzl", "browser_repositories")
-
-browser_repositories(chromium = True)
-
 # Flatbuffers
 local_repository(
     name = "com_github_google_flatbuffers",
diff --git a/package.json b/package.json
index b716d1a..8456a91 100644
--- a/package.json
+++ b/package.json
@@ -1,39 +1,31 @@
 {
   "name": "971-Robot-Code",
   "license": "MIT",
+  "type": "module",
+  "private": true,
   "devDependencies": {
-    "@angular/animations": "13.2.0",
-    "@angular/common": "13.2.0",
-    "@angular/compiler": "13.2.0",
-    "@angular/compiler-cli": "13.2.0",
-    "@angular/core": "13.2.0",
-    "@angular/forms": "13.2.0",
-    "@angular/platform-browser": "13.2.0",
-    "@angular/cli": "13.2.0",
+    "@angular/animations": "15.1.5",
+    "@angular/common": "15.1.5",
+    "@angular/compiler": "15.1.5",
+    "@angular/compiler-cli": "15.1.5",
+    "@angular/core": "15.1.5",
+    "@angular/forms": "15.1.5",
+    "@angular/platform-browser": "15.1.5",
+    "@angular/cli": "15.1.5",
     "@babel/cli": "^7.6.0",
     "@babel/core": "^7.6.0",
-    "@bazel/concatjs": "4.4.6",
-    "@bazel/protractor": "4.4.6",
-    "@bazel/rollup": "4.4.6",
-    "@bazel/typescript": "4.4.6",
-    "@bazel/terser": "4.4.6",
     "@types/jasmine": "3.10.3",
-    "jasmine": "3.10.0",
-    "karma": "6.3.12",
-    "karma-chrome-launcher": "3.1.0",
-    "karma-firefox-launcher": "2.1.2",
-    "karma-jasmine": "4.0.1",
-    "karma-requirejs": "1.1.0",
-    "karma-sourcemap-loader": "0.3.8",
-    "protractor": "7.0.0",
+    "html-insert-assets": "0.14.3",
+    "cypress": "12.3.0",
     "prettier": "2.6.1",
     "requirejs": "2.3.6",
-    "rollup": "2.66.1",
+    "rollup": "3.17.2",
+    "rxjs": "7.5.7",
     "@rollup/plugin-node-resolve": "13.1.3",
     "@types/flatbuffers": "1.10.0",
     "@types/node": "17.0.21",
-    "typescript": "4.5.5",
-    "terser": "5.10.0",
+    "typescript": "4.8.4",
+    "terser": "5.16.4",
     "zone.js": "^0.11.4"
   }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 65a160f..01c9f01 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,14 +4,14 @@
 
   .:
     specifiers:
-      '@angular/animations': 15.0.1
-      '@angular/cli': 15.0.1
-      '@angular/common': 15.0.1
-      '@angular/compiler': 15.0.1
-      '@angular/compiler-cli': 15.0.1
-      '@angular/core': 15.0.1
-      '@angular/forms': 15.0.1
-      '@angular/platform-browser': 15.0.1
+      '@angular/animations': 15.1.5
+      '@angular/cli': 15.1.5
+      '@angular/common': 15.1.5
+      '@angular/compiler': 15.1.5
+      '@angular/compiler-cli': 15.1.5
+      '@angular/core': 15.1.5
+      '@angular/forms': 15.1.5
+      '@angular/platform-browser': 15.1.5
       '@babel/cli': ^7.6.0
       '@babel/core': ^7.6.0
       '@rollup/plugin-node-resolve': 13.1.3
@@ -20,54 +20,93 @@
       '@types/node': 17.0.21
       cypress: 12.3.0
       html-insert-assets: 0.14.3
-      jasmine: 3.10.0
-      karma: 6.4.1
-      karma-chrome-launcher: 3.1.0
-      karma-firefox-launcher: 2.1.2
-      karma-jasmine: 4.0.1
-      karma-requirejs: 1.1.0
-      karma-sourcemap-loader: 0.3.8
       prettier: 2.6.1
-      protractor: 7.0.0
       requirejs: 2.3.6
-      rollup: 2.66.1
+      rollup: 3.17.2
       rxjs: 7.5.7
-      terser: 5.10.0
+      terser: 5.16.4
       typescript: 4.8.4
       zone.js: ^0.11.4
     devDependencies:
-      '@angular/animations': 15.0.1_@angular+core@15.0.1
-      '@angular/cli': 15.0.1
-      '@angular/common': 15.0.1_gc4fl5hkkh62bvf2jldebe7kfi
-      '@angular/compiler': 15.0.1_@angular+core@15.0.1
-      '@angular/compiler-cli': 15.0.1_cjgqlygpi5ntpb3clzn7pzsmpy
-      '@angular/core': 15.0.1_rxjs@7.5.7+zone.js@0.11.8
-      '@angular/forms': 15.0.1_lshev2zdnaa2h7gkxro3dgbuau
-      '@angular/platform-browser': 15.0.1_elnjnvrny24npcy6jcashrycoy
+      '@angular/animations': 15.1.5_@angular+core@15.1.5
+      '@angular/cli': 15.1.5
+      '@angular/common': 15.1.5_w2a4ar2ssyezibn6c65i4snjzu
+      '@angular/compiler': 15.1.5_@angular+core@15.1.5
+      '@angular/compiler-cli': 15.1.5_4dhd3kzleoe6yecgeixihz776m
+      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/forms': 15.1.5_kkteiffnjutwxhimrtvkwyrg3a
+      '@angular/platform-browser': 15.1.5_s7kwnqxnlkypgp4vtemlnxkbmi
       '@babel/cli': 7.20.7_@babel+core@7.20.12
       '@babel/core': 7.20.12
-      '@rollup/plugin-node-resolve': 13.1.3_rollup@2.66.1
+      '@rollup/plugin-node-resolve': 13.1.3_rollup@3.17.2
       '@types/flatbuffers': 1.10.0
       '@types/jasmine': 3.10.3
       '@types/node': 17.0.21
       cypress: 12.3.0
       html-insert-assets: 0.14.3
-      jasmine: 3.10.0
-      karma: 6.4.1
-      karma-chrome-launcher: 3.1.0
-      karma-firefox-launcher: 2.1.2
-      karma-jasmine: 4.0.1_karma@6.4.1
-      karma-requirejs: 1.1.0_rexopxsgq4andhwzqrad3qy2ka
-      karma-sourcemap-loader: 0.3.8
       prettier: 2.6.1
-      protractor: 7.0.0
       requirejs: 2.3.6
-      rollup: 2.66.1
+      rollup: 3.17.2
       rxjs: 7.5.7
-      terser: 5.10.0
+      terser: 5.16.4
       typescript: 4.8.4
       zone.js: 0.11.8
 
+  scouting/www:
+    specifiers: {}
+
+  scouting/www/counter_button:
+    specifiers: {}
+
+  scouting/www/driver_ranking:
+    specifiers:
+      '@angular/forms': 15.1.5
+    dependencies:
+      '@angular/forms': 15.1.5
+
+  scouting/www/entry:
+    specifiers:
+      '@angular/forms': 15.1.5
+      '@org_frc971/scouting/www/counter_button': workspace:*
+    dependencies:
+      '@angular/forms': 15.1.5
+      '@org_frc971/scouting/www/counter_button': link:../counter_button
+
+  scouting/www/import_match_list:
+    specifiers:
+      '@angular/forms': 15.1.5
+    dependencies:
+      '@angular/forms': 15.1.5
+
+  scouting/www/match_list:
+    specifiers:
+      '@angular/forms': 15.1.5
+      '@org_frc971/scouting/www/rpc': workspace:*
+    dependencies:
+      '@angular/forms': 15.1.5
+      '@org_frc971/scouting/www/rpc': link:../rpc
+
+  scouting/www/notes:
+    specifiers:
+      '@angular/forms': 15.1.5
+    dependencies:
+      '@angular/forms': 15.1.5
+
+  scouting/www/rpc:
+    specifiers: {}
+
+  scouting/www/shift_schedule:
+    specifiers:
+      '@angular/forms': 15.1.5
+    dependencies:
+      '@angular/forms': 15.1.5
+
+  scouting/www/view:
+    specifiers:
+      '@angular/forms': 15.1.5
+    dependencies:
+      '@angular/forms': 15.1.5
+
 packages:
 
   /@ampproject/remapping/2.2.0:
@@ -78,18 +117,18 @@
       '@jridgewell/trace-mapping': 0.3.17
     dev: true
 
-  /@angular-devkit/architect/0.1500.1:
-    resolution: {integrity: sha512-HoGMdUB9z1brPq3f0m3la6N0ODBarH5LjZN+5KyIMdXgJJN5y+gs2H6yCPQfJT56fqtp/cckxOYcLAFTf45Tcg==}
+  /@angular-devkit/architect/0.1501.5:
+    resolution: {integrity: sha512-T4zJMvJvCqZeeENdeHcFtdrISrZSe8MycQOWZwPYU9zBTGMmdYpa4GQKQmFRZGBwX2PKHFlkQ1HLLe366sySAQ==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     dependencies:
-      '@angular-devkit/core': 15.0.1
+      '@angular-devkit/core': 15.1.5
       rxjs: 6.6.7
     transitivePeerDependencies:
       - chokidar
     dev: true
 
-  /@angular-devkit/core/15.0.1:
-    resolution: {integrity: sha512-Q8sF561Wf53ufdrKWvsqebbD5EjJpdHaPjg5nAHYwPtwD1ciG7oL55cQFs0LYqy9Ux6k34NimodhH3QgXYYPFQ==}
+  /@angular-devkit/core/15.1.5:
+    resolution: {integrity: sha512-SkGQFkruTwVM77WEOIQivfFBtnHW41tttsGrT6MTrti98hs8tvOTlzfYD/sDTyh0WKbZGeAtkRXx0raevb63YQ==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     peerDependencies:
       chokidar: ^3.5.2
@@ -97,55 +136,55 @@
       chokidar:
         optional: true
     dependencies:
-      ajv: 8.11.0
+      ajv: 8.12.0
       ajv-formats: 2.1.1
       jsonc-parser: 3.2.0
       rxjs: 6.6.7
       source-map: 0.7.4
     dev: true
 
-  /@angular-devkit/schematics/15.0.1:
-    resolution: {integrity: sha512-DS9t+xl1lOphYkdz17FwRO0LUs5IYBpyqr3O8SqrXESOhVUXlbcEhVtVeQiYxfeQZVRPWVR64Tf6E6ELXcGLYw==}
+  /@angular-devkit/schematics/15.1.5:
+    resolution: {integrity: sha512-9MPuy0BjJAlSJVMqPmt50lDq6nq6AL5XJwv6NVP1fLSLXABlLBZe7jjaHLg8XVHaKbzS7BSPnHaGfHJkUipP+A==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     dependencies:
-      '@angular-devkit/core': 15.0.1
+      '@angular-devkit/core': 15.1.5
       jsonc-parser: 3.2.0
-      magic-string: 0.26.7
+      magic-string: 0.27.0
       ora: 5.4.1
       rxjs: 6.6.7
     transitivePeerDependencies:
       - chokidar
     dev: true
 
-  /@angular/animations/15.0.1_@angular+core@15.0.1:
-    resolution: {integrity: sha512-GfxqpRcoRfQNS1pVA+PadcgCGJSFag07jFJIQUHX3HZkI/4PyXGn/7ptgebN3tBjy+ASk4PBOQP/ntGbrr55zw==}
+  /@angular/animations/15.1.5_@angular+core@15.1.5:
+    resolution: {integrity: sha512-yac9PHy5Y72MtKQhaBSQFOdIxEJIacmJrYNRFoa82z0YCa3VrEYjvuG0x5JewBN4gQGC5IOpj2C7c9zdXZv5HA==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
     peerDependencies:
-      '@angular/core': 15.0.1
+      '@angular/core': 15.1.5
     dependencies:
-      '@angular/core': 15.0.1_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
       tslib: 2.4.1
     dev: true
 
-  /@angular/cli/15.0.1:
-    resolution: {integrity: sha512-ntwJxtzGuHl07eb56x8WM6tQ3YhBKCP61o8WoHBrOBEFNm9rEV9C2webMIWYVFAa0iG1pmDq6U5Qc7WFPM9rtg==}
+  /@angular/cli/15.1.5:
+    resolution: {integrity: sha512-R+mi0+IJyBFobinCI9nu7hdGR5tXW6mBa/nsN3fwoebV0Qc07rSf9qqYkvnPjLWMiJ5eQxdLJhPcmMjB9Xs0aA==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     hasBin: true
     dependencies:
-      '@angular-devkit/architect': 0.1500.1
-      '@angular-devkit/core': 15.0.1
-      '@angular-devkit/schematics': 15.0.1
-      '@schematics/angular': 15.0.1
+      '@angular-devkit/architect': 0.1501.5
+      '@angular-devkit/core': 15.1.5
+      '@angular-devkit/schematics': 15.1.5
+      '@schematics/angular': 15.1.5
       '@yarnpkg/lockfile': 1.1.0
       ansi-colors: 4.1.3
       ini: 3.0.1
       inquirer: 8.2.4
       jsonc-parser: 3.2.0
-      npm-package-arg: 9.1.2
+      npm-package-arg: 10.1.0
       npm-pick-manifest: 8.0.1
       open: 8.4.0
       ora: 5.4.1
-      pacote: 15.0.6
+      pacote: 15.0.8
       resolve: 1.22.1
       semver: 7.3.8
       symbol-observable: 4.0.0
@@ -156,35 +195,35 @@
       - supports-color
     dev: true
 
-  /@angular/common/15.0.1_gc4fl5hkkh62bvf2jldebe7kfi:
-    resolution: {integrity: sha512-XRD1Dj2aINyp5yYueCuwLU1y84z+ZFXeO84oNfwIu0unHszuo02iIzrV+yCm/ATwt6qUkIbe6xhZNjUorZecyA==}
+  /@angular/common/15.1.5_w2a4ar2ssyezibn6c65i4snjzu:
+    resolution: {integrity: sha512-52Ut/IeoM3avzV3Ts/ISkq7cc1FlA6dhLUq+L3ebY+Z8zZskCWjJWu4UgLGyVdtgSuAItyQm9CoZd+DrPLYtDA==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
     peerDependencies:
-      '@angular/core': 15.0.1
+      '@angular/core': 15.1.5
       rxjs: ^6.5.3 || ^7.4.0
     dependencies:
-      '@angular/core': 15.0.1_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
       rxjs: 7.5.7
       tslib: 2.4.1
     dev: true
 
-  /@angular/compiler-cli/15.0.1_cjgqlygpi5ntpb3clzn7pzsmpy:
-    resolution: {integrity: sha512-M2VsKBw8dQMC5p3PmpM+EBZAZ9Qk/rGX+aIHYBGzsgGFqYMEcz6Nxrj4v6I3Hta7tW7QEVXf883rXiWxHlwtbw==}
+  /@angular/compiler-cli/15.1.5_4dhd3kzleoe6yecgeixihz776m:
+    resolution: {integrity: sha512-gWg6MpMJOpfkwf2zxHJDp9EGwORga4MLTkvugL+1KbN+lvx4Ac9Y0GinlJ4+EGpttvQlTYHzn8GabWhcdzzUiQ==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
     hasBin: true
     peerDependencies:
-      '@angular/compiler': 15.0.1
-      typescript: '>=4.8.2 <4.9'
+      '@angular/compiler': 15.1.5
+      typescript: '>=4.8.2 <5.0'
     dependencies:
-      '@angular/compiler': 15.0.1_@angular+core@15.0.1
-      '@babel/core': 7.20.12
+      '@angular/compiler': 15.1.5_@angular+core@15.1.5
+      '@babel/core': 7.19.3
+      '@jridgewell/sourcemap-codec': 1.4.14
       chokidar: 3.5.3
       convert-source-map: 1.9.0
       dependency-graph: 0.11.0
-      magic-string: 0.26.7
+      magic-string: 0.27.0
       reflect-metadata: 0.1.13
       semver: 7.3.8
-      sourcemap-codec: 1.4.8
       tslib: 2.4.1
       typescript: 4.8.4
       yargs: 17.6.2
@@ -192,21 +231,21 @@
       - supports-color
     dev: true
 
-  /@angular/compiler/15.0.1_@angular+core@15.0.1:
-    resolution: {integrity: sha512-4talkxip79XPfoj69qgY8VXV1KIBKOyZCRWHhNVqMdECyw/fceVWN4r8kDL0qOTBh1CKmhoQFXQilr9g7nFatA==}
+  /@angular/compiler/15.1.5_@angular+core@15.1.5:
+    resolution: {integrity: sha512-4Ciswu3HKE+Pk+6Lhi6v3inZ01WkNBi9D33OKGC+7uEAjl8DCNF13rBXLyMF6tIFd+L98KYpzwUyQYk8FI/vgA==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
     peerDependencies:
-      '@angular/core': 15.0.1
+      '@angular/core': 15.1.5
     peerDependenciesMeta:
       '@angular/core':
         optional: true
     dependencies:
-      '@angular/core': 15.0.1_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
       tslib: 2.4.1
     dev: true
 
-  /@angular/core/15.0.1_rxjs@7.5.7+zone.js@0.11.8:
-    resolution: {integrity: sha512-idaKf9hhguyGn/yj5KMHIUEvW4PpeYcwlRUSoEskQC1799BsXwJyV0AwZ67GH1ltnAj34gbhMhDedcCLdhOffA==}
+  /@angular/core/15.1.5_rxjs@7.5.7+zone.js@0.11.8:
+    resolution: {integrity: sha512-JCbhGVaskqrstLB8CJoPtMQKH4gryhuLFUVL5cwbVy3UJGGNmc3Gzvk+9I7zDf/D08vKyXGGmBNBVx2J65SJgw==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
     peerDependencies:
       rxjs: ^6.5.3 || ^7.4.0
@@ -217,48 +256,48 @@
       zone.js: 0.11.8
     dev: true
 
-  /@angular/forms/15.0.1:
-    resolution: {integrity: sha512-gNj/fY7B7swczWI3jpJK4904W0WHCrYviZB8m97P4MkcxdMfQezp4VoRsj+vIkKGtUPUWje3uIjzqodhJlxIJA==}
+  /@angular/forms/15.1.5:
+    resolution: {integrity: sha512-FnuEdyYs1o/DJepLpTsY2/GwKTEXJ7sZlQb+NKkRWOoGpA0E4nSbhn3aCUic++MTgbZyHO0rmFKnD8TI2yyJDA==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
     peerDependencies:
-      '@angular/common': 15.0.1
-      '@angular/core': 15.0.1
-      '@angular/platform-browser': 15.0.1
+      '@angular/common': 15.1.5
+      '@angular/core': 15.1.5
+      '@angular/platform-browser': 15.1.5
       rxjs: ^6.5.3 || ^7.4.0
     dependencies:
       tslib: 2.4.1
     dev: false
 
-  /@angular/forms/15.0.1_lshev2zdnaa2h7gkxro3dgbuau:
-    resolution: {integrity: sha512-gNj/fY7B7swczWI3jpJK4904W0WHCrYviZB8m97P4MkcxdMfQezp4VoRsj+vIkKGtUPUWje3uIjzqodhJlxIJA==}
+  /@angular/forms/15.1.5_kkteiffnjutwxhimrtvkwyrg3a:
+    resolution: {integrity: sha512-FnuEdyYs1o/DJepLpTsY2/GwKTEXJ7sZlQb+NKkRWOoGpA0E4nSbhn3aCUic++MTgbZyHO0rmFKnD8TI2yyJDA==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
     peerDependencies:
-      '@angular/common': 15.0.1
-      '@angular/core': 15.0.1
-      '@angular/platform-browser': 15.0.1
+      '@angular/common': 15.1.5
+      '@angular/core': 15.1.5
+      '@angular/platform-browser': 15.1.5
       rxjs: ^6.5.3 || ^7.4.0
     dependencies:
-      '@angular/common': 15.0.1_gc4fl5hkkh62bvf2jldebe7kfi
-      '@angular/core': 15.0.1_rxjs@7.5.7+zone.js@0.11.8
-      '@angular/platform-browser': 15.0.1_elnjnvrny24npcy6jcashrycoy
+      '@angular/common': 15.1.5_w2a4ar2ssyezibn6c65i4snjzu
+      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/platform-browser': 15.1.5_s7kwnqxnlkypgp4vtemlnxkbmi
       rxjs: 7.5.7
       tslib: 2.4.1
     dev: true
 
-  /@angular/platform-browser/15.0.1_elnjnvrny24npcy6jcashrycoy:
-    resolution: {integrity: sha512-fH0EfRgbQC0ql8V1ZWVfF75H9lSjT2T6uGfR8cBdRAO/RWwWgx/TfFsjdWAZtjuKRZnKY3wRQ/yVYeQarC3n0Q==}
+  /@angular/platform-browser/15.1.5_s7kwnqxnlkypgp4vtemlnxkbmi:
+    resolution: {integrity: sha512-epeESrWEt41W6i2NqIbGKNE0Oa1JfeDtKfMXtcjUNCgT76qS3zmC0G6irO8BOVbrwpA/YI4yYx1B9vTDUXYbEg==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
     peerDependencies:
-      '@angular/animations': 15.0.1
-      '@angular/common': 15.0.1
-      '@angular/core': 15.0.1
+      '@angular/animations': 15.1.5
+      '@angular/common': 15.1.5
+      '@angular/core': 15.1.5
     peerDependenciesMeta:
       '@angular/animations':
         optional: true
     dependencies:
-      '@angular/animations': 15.0.1_@angular+core@15.0.1
-      '@angular/common': 15.0.1_gc4fl5hkkh62bvf2jldebe7kfi
-      '@angular/core': 15.0.1_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/animations': 15.1.5_@angular+core@15.1.5
+      '@angular/common': 15.1.5_w2a4ar2ssyezibn6c65i4snjzu
+      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
       tslib: 2.4.1
     dev: true
 
@@ -294,6 +333,29 @@
     engines: {node: '>=6.9.0'}
     dev: true
 
+  /@babel/core/7.19.3:
+    resolution: {integrity: sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@ampproject/remapping': 2.2.0
+      '@babel/code-frame': 7.18.6
+      '@babel/generator': 7.20.7
+      '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.19.3
+      '@babel/helper-module-transforms': 7.20.11
+      '@babel/helpers': 7.20.7
+      '@babel/parser': 7.20.7
+      '@babel/template': 7.20.7
+      '@babel/traverse': 7.20.12
+      '@babel/types': 7.20.7
+      convert-source-map: 1.9.0
+      debug: 4.3.4
+      gensync: 1.0.0-beta.2
+      json5: 2.2.3
+      semver: 6.3.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@babel/core/7.20.12:
     resolution: {integrity: sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==}
     engines: {node: '>=6.9.0'}
@@ -326,6 +388,20 @@
       jsesc: 2.5.2
     dev: true
 
+  /@babel/helper-compilation-targets/7.20.7_@babel+core@7.19.3:
+    resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+    dependencies:
+      '@babel/compat-data': 7.20.10
+      '@babel/core': 7.19.3
+      '@babel/helper-validator-option': 7.18.6
+      browserslist: 4.21.4
+      lru-cache: 5.1.1
+      semver: 6.3.0
+    dev: true
+
   /@babel/helper-compilation-targets/7.20.7_@babel+core@7.20.12:
     resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==}
     engines: {node: '>=6.9.0'}
@@ -479,7 +555,9 @@
   /@colors/colors/1.5.0:
     resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
     engines: {node: '>=0.1.90'}
+    requiresBuild: true
     dev: true
+    optional: true
 
   /@cypress/request/2.88.11:
     resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
@@ -545,6 +623,13 @@
     engines: {node: '>=6.0.0'}
     dev: true
 
+  /@jridgewell/source-map/0.3.2:
+    resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==}
+    dependencies:
+      '@jridgewell/gen-mapping': 0.3.2
+      '@jridgewell/trace-mapping': 0.3.17
+    dev: true
+
   /@jridgewell/sourcemap-codec/1.4.14:
     resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
     dev: true
@@ -638,22 +723,22 @@
       - supports-color
     dev: true
 
-  /@rollup/plugin-node-resolve/13.1.3_rollup@2.66.1:
+  /@rollup/plugin-node-resolve/13.1.3_rollup@3.17.2:
     resolution: {integrity: sha512-BdxNk+LtmElRo5d06MGY4zoepyrXX1tkzX2hrnPEZ53k78GuOMWLqmJDGIIOPwVRIFZrLQOo+Yr6KtCuLIA0AQ==}
     engines: {node: '>= 10.0.0'}
     peerDependencies:
       rollup: ^2.42.0
     dependencies:
-      '@rollup/pluginutils': 3.1.0_rollup@2.66.1
+      '@rollup/pluginutils': 3.1.0_rollup@3.17.2
       '@types/resolve': 1.17.1
       builtin-modules: 3.3.0
       deepmerge: 4.2.2
       is-module: 1.0.0
       resolve: 1.22.1
-      rollup: 2.66.1
+      rollup: 3.17.2
     dev: true
 
-  /@rollup/pluginutils/3.1.0_rollup@2.66.1:
+  /@rollup/pluginutils/3.1.0_rollup@3.17.2:
     resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
     engines: {node: '>= 8.0.0'}
     peerDependencies:
@@ -662,39 +747,25 @@
       '@types/estree': 0.0.39
       estree-walker: 1.0.1
       picomatch: 2.3.1
-      rollup: 2.66.1
+      rollup: 3.17.2
     dev: true
 
-  /@schematics/angular/15.0.1:
-    resolution: {integrity: sha512-UGiQ4IwdLWdQwlWVgbAM5B6G4VdzVOn0yS1PkOtTt0hvAkszriu7uyaH2Qh8aFSTvNAIg/l7/6grI/UGj8iDaw==}
+  /@schematics/angular/15.1.5:
+    resolution: {integrity: sha512-mw5adVNSLX8h6c8F0tNEe11LVOlj100c1PrPggZNVz9nd2fwb32SVFSx+FmOxLVfE1kfnPgsvLpDH23z8SF6bg==}
     engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     dependencies:
-      '@angular-devkit/core': 15.0.1
-      '@angular-devkit/schematics': 15.0.1
+      '@angular-devkit/core': 15.1.5
+      '@angular-devkit/schematics': 15.1.5
       jsonc-parser: 3.2.0
     transitivePeerDependencies:
       - chokidar
     dev: true
 
-  /@socket.io/component-emitter/3.1.0:
-    resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==}
-    dev: true
-
   /@tootallnate/once/2.0.0:
     resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
     engines: {node: '>= 10'}
     dev: true
 
-  /@types/cookie/0.4.1:
-    resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
-    dev: true
-
-  /@types/cors/2.8.13:
-    resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==}
-    dependencies:
-      '@types/node': 17.0.21
-    dev: true
-
   /@types/estree/0.0.39:
     resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
     dev: true
@@ -715,20 +786,12 @@
     resolution: {integrity: sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==}
     dev: true
 
-  /@types/q/0.0.32:
-    resolution: {integrity: sha512-qYi3YV9inU/REEfxwVcGZzbS3KG/Xs90lv0Pr+lDtuVjBPGd1A+eciXzVSaRvLify132BfcvhvEjeVahrUl0Ug==}
-    dev: true
-
   /@types/resolve/1.17.1:
     resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
     dependencies:
       '@types/node': 17.0.21
     dev: true
 
-  /@types/selenium-webdriver/3.0.20:
-    resolution: {integrity: sha512-6d8Q5fqS9DWOXEhMDiF6/2FjyHdmP/jSTAUyeQR7QwrFeNmYyzmvGxD5aLIHL445HjWgibs0eAig+KPnbaesXA==}
-    dev: true
-
   /@types/sinonjs__fake-timers/8.1.1:
     resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==}
     dev: true
@@ -753,32 +816,12 @@
     resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
     dev: true
 
-  /accepts/1.3.8:
-    resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
-    engines: {node: '>= 0.6'}
-    dependencies:
-      mime-types: 2.1.35
-      negotiator: 0.6.3
-    dev: true
-
   /acorn/8.8.1:
     resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==}
     engines: {node: '>=0.4.0'}
     hasBin: true
     dev: true
 
-  /adm-zip/0.4.16:
-    resolution: {integrity: sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==}
-    engines: {node: '>=0.3.0'}
-    dev: true
-
-  /agent-base/4.3.0:
-    resolution: {integrity: sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==}
-    engines: {node: '>= 4.0.0'}
-    dependencies:
-      es6-promisify: 5.0.0
-    dev: true
-
   /agent-base/6.0.2:
     resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
     engines: {node: '>= 6.0.0'}
@@ -813,20 +856,11 @@
       ajv:
         optional: true
     dependencies:
-      ajv: 8.11.0
+      ajv: 8.12.0
     dev: true
 
-  /ajv/6.12.6:
-    resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
-    dependencies:
-      fast-deep-equal: 3.1.3
-      fast-json-stable-stringify: 2.1.0
-      json-schema-traverse: 0.4.1
-      uri-js: 4.4.1
-    dev: true
-
-  /ajv/8.11.0:
-    resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==}
+  /ajv/8.12.0:
+    resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
     dependencies:
       fast-deep-equal: 3.1.3
       json-schema-traverse: 1.0.0
@@ -846,21 +880,11 @@
       type-fest: 0.21.3
     dev: true
 
-  /ansi-regex/2.1.1:
-    resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
   /ansi-regex/5.0.1:
     resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
     engines: {node: '>=8'}
     dev: true
 
-  /ansi-styles/2.2.1:
-    resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
   /ansi-styles/3.2.1:
     resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
     engines: {node: '>=4'}
@@ -899,23 +923,6 @@
       readable-stream: 3.6.0
     dev: true
 
-  /array-union/1.0.2:
-    resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      array-uniq: 1.0.3
-    dev: true
-
-  /array-uniq/1.0.3:
-    resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
-  /arrify/1.0.1:
-    resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
   /asn1/0.2.6:
     resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
     dependencies:
@@ -961,11 +968,6 @@
     resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
     dev: true
 
-  /base64id/2.0.0:
-    resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
-    engines: {node: ^4.5.0 || >= 5.9}
-    dev: true
-
   /bcrypt-pbkdf/1.0.2:
     resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
     dependencies:
@@ -989,38 +991,10 @@
     resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
     dev: true
 
-  /blocking-proxy/1.0.1:
-    resolution: {integrity: sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==}
-    engines: {node: '>=6.9.x'}
-    hasBin: true
-    dependencies:
-      minimist: 1.2.7
-    dev: true
-
   /bluebird/3.7.2:
     resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
     dev: true
 
-  /body-parser/1.20.1:
-    resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
-    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
-    dependencies:
-      bytes: 3.1.2
-      content-type: 1.0.4
-      debug: 2.6.9
-      depd: 2.0.0
-      destroy: 1.2.0
-      http-errors: 2.0.0
-      iconv-lite: 0.4.24
-      on-finished: 2.4.1
-      qs: 6.11.0
-      raw-body: 2.5.1
-      type-is: 1.6.18
-      unpipe: 1.0.0
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /brace-expansion/1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
     dependencies:
@@ -1052,14 +1026,6 @@
       update-browserslist-db: 1.0.10_browserslist@4.21.4
     dev: true
 
-  /browserstack/1.6.1:
-    resolution: {integrity: sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==}
-    dependencies:
-      https-proxy-agent: 2.2.4
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /buffer-crc32/0.2.13:
     resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
     dev: true
@@ -1086,11 +1052,6 @@
       semver: 7.3.8
     dev: true
 
-  /bytes/3.1.2:
-    resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
-    engines: {node: '>= 0.8'}
-    dev: true
-
   /cacache/16.1.3:
     resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==}
     engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@@ -1150,11 +1111,6 @@
       get-intrinsic: 1.1.3
     dev: true
 
-  /camelcase/5.3.1:
-    resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
-    engines: {node: '>=6'}
-    dev: true
-
   /caniuse-lite/1.0.30001445:
     resolution: {integrity: sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==}
     dev: true
@@ -1163,17 +1119,6 @@
     resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
     dev: true
 
-  /chalk/1.1.3:
-    resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      ansi-styles: 2.2.1
-      escape-string-regexp: 1.0.5
-      has-ansi: 2.0.0
-      strip-ansi: 3.0.1
-      supports-color: 2.0.0
-    dev: true
-
   /chalk/2.4.2:
     resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
     engines: {node: '>=4'}
@@ -1264,22 +1209,6 @@
     engines: {node: '>= 10'}
     dev: true
 
-  /cliui/6.0.0:
-    resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
-    dependencies:
-      string-width: 4.2.3
-      strip-ansi: 6.0.1
-      wrap-ansi: 6.2.0
-    dev: true
-
-  /cliui/7.0.4:
-    resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
-    dependencies:
-      string-width: 4.2.3
-      strip-ansi: 6.0.1
-      wrap-ansi: 7.0.0
-    dev: true
-
   /cliui/8.0.1:
     resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
     engines: {node: '>=12'}
@@ -1354,52 +1283,18 @@
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
     dev: true
 
-  /connect/3.7.0:
-    resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
-    engines: {node: '>= 0.10.0'}
-    dependencies:
-      debug: 2.6.9
-      finalhandler: 1.1.2
-      parseurl: 1.3.3
-      utils-merge: 1.0.1
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /console-control-strings/1.1.0:
     resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
     dev: true
 
-  /content-type/1.0.4:
-    resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
-    engines: {node: '>= 0.6'}
-    dev: true
-
   /convert-source-map/1.9.0:
     resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
     dev: true
 
-  /cookie/0.4.2:
-    resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
-    engines: {node: '>= 0.6'}
-    dev: true
-
   /core-util-is/1.0.2:
     resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
     dev: true
 
-  /core-util-is/1.0.3:
-    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
-    dev: true
-
-  /cors/2.8.5:
-    resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
-    engines: {node: '>= 0.10'}
-    dependencies:
-      object-assign: 4.1.1
-      vary: 1.1.2
-    dev: true
-
   /cross-spawn/7.0.3:
     resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
     engines: {node: '>= 8'}
@@ -1409,10 +1304,6 @@
       which: 2.0.2
     dev: true
 
-  /custom-event/1.0.1:
-    resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==}
-    dev: true
-
   /cypress/12.3.0:
     resolution: {integrity: sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==}
     engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
@@ -1470,37 +1361,10 @@
       assert-plus: 1.0.0
     dev: true
 
-  /date-format/4.0.14:
-    resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
-    engines: {node: '>=4.0'}
-    dev: true
-
   /dayjs/1.11.7:
     resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==}
     dev: true
 
-  /debug/2.6.9:
-    resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-    dependencies:
-      ms: 2.0.0
-    dev: true
-
-  /debug/3.2.7:
-    resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-    dependencies:
-      ms: 2.1.3
-    dev: true
-
   /debug/3.2.7_supports-color@8.1.1:
     resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
     peerDependencies:
@@ -1538,11 +1402,6 @@
       supports-color: 8.1.1
     dev: true
 
-  /decamelize/1.2.0:
-    resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
   /deepmerge/4.2.2:
     resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
     engines: {node: '>=0.10.0'}
@@ -1559,19 +1418,6 @@
     engines: {node: '>=8'}
     dev: true
 
-  /del/2.2.2:
-    resolution: {integrity: sha512-Z4fzpbIRjOu7lO5jCETSWoqUDVe0IPOlfugBsF6suen2LKDlVb4QZpKEM9P+buNJ4KI1eN7I083w/pbKUpsrWQ==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      globby: 5.0.0
-      is-path-cwd: 1.0.0
-      is-path-in-cwd: 1.0.1
-      object-assign: 4.1.1
-      pify: 2.3.0
-      pinkie-promise: 2.0.1
-      rimraf: 2.7.1
-    dev: true
-
   /delayed-stream/1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
@@ -1586,34 +1432,11 @@
     engines: {node: '>= 0.6'}
     dev: true
 
-  /depd/2.0.0:
-    resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
-    engines: {node: '>= 0.8'}
-    dev: true
-
   /dependency-graph/0.11.0:
     resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==}
     engines: {node: '>= 0.6.0'}
     dev: true
 
-  /destroy/1.2.0:
-    resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
-    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
-    dev: true
-
-  /di/0.0.1:
-    resolution: {integrity: sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==}
-    dev: true
-
-  /dom-serialize/2.2.1:
-    resolution: {integrity: sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==}
-    dependencies:
-      custom-event: 1.0.1
-      ent: 2.2.0
-      extend: 3.0.2
-      void-elements: 2.0.1
-    dev: true
-
   /ecc-jsbn/0.1.2:
     resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
     dependencies:
@@ -1621,10 +1444,6 @@
       safer-buffer: 2.1.2
     dev: true
 
-  /ee-first/1.1.1:
-    resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
-    dev: true
-
   /electron-to-chromium/1.4.284:
     resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==}
     dev: true
@@ -1633,11 +1452,6 @@
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
     dev: true
 
-  /encodeurl/1.0.2:
-    resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
-    engines: {node: '>= 0.8'}
-    dev: true
-
   /encoding/0.1.13:
     resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
     requiresBuild: true
@@ -1652,31 +1466,6 @@
       once: 1.4.0
     dev: true
 
-  /engine.io-parser/5.0.6:
-    resolution: {integrity: sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==}
-    engines: {node: '>=10.0.0'}
-    dev: true
-
-  /engine.io/6.2.1:
-    resolution: {integrity: sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==}
-    engines: {node: '>=10.0.0'}
-    dependencies:
-      '@types/cookie': 0.4.1
-      '@types/cors': 2.8.13
-      '@types/node': 17.0.21
-      accepts: 1.3.8
-      base64id: 2.0.0
-      cookie: 0.4.2
-      cors: 2.8.5
-      debug: 4.3.4
-      engine.io-parser: 5.0.6
-      ws: 8.2.3
-    transitivePeerDependencies:
-      - bufferutil
-      - supports-color
-      - utf-8-validate
-    dev: true
-
   /enquirer/2.3.6:
     resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
     engines: {node: '>=8.6'}
@@ -1684,10 +1473,6 @@
       ansi-colors: 4.1.3
     dev: true
 
-  /ent/2.2.0:
-    resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==}
-    dev: true
-
   /env-paths/2.2.1:
     resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
     engines: {node: '>=6'}
@@ -1697,25 +1482,11 @@
     resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
     dev: true
 
-  /es6-promise/4.2.8:
-    resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
-    dev: true
-
-  /es6-promisify/5.0.0:
-    resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==}
-    dependencies:
-      es6-promise: 4.2.8
-    dev: true
-
   /escalade/3.1.1:
     resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
     engines: {node: '>=6'}
     dev: true
 
-  /escape-html/1.0.3:
-    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
-    dev: true
-
   /escape-string-regexp/1.0.5:
     resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
     engines: {node: '>=0.8.0'}
@@ -1729,10 +1500,6 @@
     resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
     dev: true
 
-  /eventemitter3/4.0.7:
-    resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
-    dev: true
-
   /execa/4.1.0:
     resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
     engines: {node: '>=10'}
@@ -1755,11 +1522,6 @@
       pify: 2.3.0
     dev: true
 
-  /exit/0.1.2:
-    resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
-    engines: {node: '>= 0.8.0'}
-    dev: true
-
   /extend/3.0.2:
     resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
     dev: true
@@ -1796,10 +1558,6 @@
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
     dev: true
 
-  /fast-json-stable-stringify/2.1.0:
-    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
-    dev: true
-
   /fd-slicer/1.1.0:
     resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
     dependencies:
@@ -1820,43 +1578,6 @@
       to-regex-range: 5.0.1
     dev: true
 
-  /finalhandler/1.1.2:
-    resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
-    engines: {node: '>= 0.8'}
-    dependencies:
-      debug: 2.6.9
-      encodeurl: 1.0.2
-      escape-html: 1.0.3
-      on-finished: 2.3.0
-      parseurl: 1.3.3
-      statuses: 1.5.0
-      unpipe: 1.0.0
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
-  /find-up/4.1.0:
-    resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
-    engines: {node: '>=8'}
-    dependencies:
-      locate-path: 5.0.0
-      path-exists: 4.0.0
-    dev: true
-
-  /flatted/3.2.7:
-    resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
-    dev: true
-
-  /follow-redirects/1.15.2:
-    resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
-    engines: {node: '>=4.0'}
-    peerDependencies:
-      debug: '*'
-    peerDependenciesMeta:
-      debug:
-        optional: true
-    dev: true
-
   /forever-agent/0.6.1:
     resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
     dev: true
@@ -1870,15 +1591,6 @@
       mime-types: 2.1.35
     dev: true
 
-  /fs-extra/8.1.0:
-    resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
-    engines: {node: '>=6 <7 || >=8'}
-    dependencies:
-      graceful-fs: 4.2.10
-      jsonfile: 4.0.0
-      universalify: 0.1.2
-    dev: true
-
   /fs-extra/9.1.0:
     resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
     engines: {node: '>=10'}
@@ -2015,43 +1727,10 @@
     engines: {node: '>=4'}
     dev: true
 
-  /globby/5.0.0:
-    resolution: {integrity: sha512-HJRTIH2EeH44ka+LWig+EqT2ONSYpVlNfx6pyd592/VF1TbfljJ7elwie7oSwcViLGqOdWocSdu2txwBF9bjmQ==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      array-union: 1.0.2
-      arrify: 1.0.1
-      glob: 7.2.3
-      object-assign: 4.1.1
-      pify: 2.3.0
-      pinkie-promise: 2.0.1
-    dev: true
-
   /graceful-fs/4.2.10:
     resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
     dev: true
 
-  /har-schema/2.0.0:
-    resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
-    engines: {node: '>=4'}
-    dev: true
-
-  /har-validator/5.1.5:
-    resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==}
-    engines: {node: '>=6'}
-    deprecated: this library is no longer supported
-    dependencies:
-      ajv: 6.12.6
-      har-schema: 2.0.0
-    dev: true
-
-  /has-ansi/2.0.0:
-    resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      ansi-regex: 2.1.1
-    dev: true
-
   /has-flag/3.0.0:
     resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
     engines: {node: '>=4'}
@@ -2078,13 +1757,6 @@
       function-bind: 1.1.1
     dev: true
 
-  /hosted-git-info/5.2.1:
-    resolution: {integrity: sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==}
-    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
-    dependencies:
-      lru-cache: 7.14.1
-    dev: true
-
   /hosted-git-info/6.1.1:
     resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -2104,17 +1776,6 @@
     resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==}
     dev: true
 
-  /http-errors/2.0.0:
-    resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
-    engines: {node: '>= 0.8'}
-    dependencies:
-      depd: 2.0.0
-      inherits: 2.0.4
-      setprototypeof: 1.2.0
-      statuses: 2.0.1
-      toidentifier: 1.0.1
-    dev: true
-
   /http-proxy-agent/5.0.0:
     resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
     engines: {node: '>= 6'}
@@ -2126,26 +1787,6 @@
       - supports-color
     dev: true
 
-  /http-proxy/1.18.1:
-    resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
-    engines: {node: '>=8.0.0'}
-    dependencies:
-      eventemitter3: 4.0.7
-      follow-redirects: 1.15.2
-      requires-port: 1.0.0
-    transitivePeerDependencies:
-      - debug
-    dev: true
-
-  /http-signature/1.2.0:
-    resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
-    engines: {node: '>=0.8', npm: '>=1.3.7'}
-    dependencies:
-      assert-plus: 1.0.0
-      jsprim: 1.4.2
-      sshpk: 1.17.0
-    dev: true
-
   /http-signature/1.3.6:
     resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==}
     engines: {node: '>=0.10'}
@@ -2155,16 +1796,6 @@
       sshpk: 1.17.0
     dev: true
 
-  /https-proxy-agent/2.2.4:
-    resolution: {integrity: sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==}
-    engines: {node: '>= 4.5.0'}
-    dependencies:
-      agent-base: 4.3.0
-      debug: 3.2.7
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /https-proxy-agent/5.0.1:
     resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
     engines: {node: '>= 6'}
@@ -2212,10 +1843,6 @@
       minimatch: 5.1.4
     dev: true
 
-  /immediate/3.0.6:
-    resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
-    dev: true
-
   /imurmurhash/0.1.4:
     resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
     engines: {node: '>=0.8.19'}
@@ -2241,10 +1868,6 @@
     resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
     dev: true
 
-  /ini/1.3.8:
-    resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
-    dev: true
-
   /ini/2.0.0:
     resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==}
     engines: {node: '>=10'}
@@ -2349,25 +1972,6 @@
     engines: {node: '>=0.12.0'}
     dev: true
 
-  /is-path-cwd/1.0.0:
-    resolution: {integrity: sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
-  /is-path-in-cwd/1.0.1:
-    resolution: {integrity: sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      is-path-inside: 1.0.1
-    dev: true
-
-  /is-path-inside/1.0.1:
-    resolution: {integrity: sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      path-is-inside: 1.0.2
-    dev: true
-
   /is-path-inside/3.0.3:
     resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
     engines: {node: '>=8'}
@@ -2394,15 +1998,6 @@
       is-docker: 2.2.1
     dev: true
 
-  /isarray/1.0.0:
-    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
-    dev: true
-
-  /isbinaryfile/4.0.10:
-    resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
-    engines: {node: '>= 8.0.0'}
-    dev: true
-
   /isexe/2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
     dev: true
@@ -2411,40 +2006,6 @@
     resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
     dev: true
 
-  /jasmine-core/2.8.0:
-    resolution: {integrity: sha512-SNkOkS+/jMZvLhuSx1fjhcNWUC/KG6oVyFUGkSBEr9n1axSNduWU8GlI7suaHXr4yxjet6KjrUZxUTE5WzzWwQ==}
-    dev: true
-
-  /jasmine-core/3.10.1:
-    resolution: {integrity: sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==}
-    dev: true
-
-  /jasmine-core/3.99.1:
-    resolution: {integrity: sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==}
-    dev: true
-
-  /jasmine/2.8.0:
-    resolution: {integrity: sha512-KbdGQTf5jbZgltoHs31XGiChAPumMSY64OZMWLNYnEnMfG5uwGBhffePwuskexjT+/Jea/gU3qAU8344hNohSw==}
-    hasBin: true
-    dependencies:
-      exit: 0.1.2
-      glob: 7.2.3
-      jasmine-core: 2.8.0
-    dev: true
-
-  /jasmine/3.10.0:
-    resolution: {integrity: sha512-2Y42VsC+3CQCTzTwJezOvji4qLORmKIE0kwowWC+934Krn6ZXNQYljiwK5st9V3PVx96BSiDYXSB60VVah3IlQ==}
-    hasBin: true
-    dependencies:
-      glob: 7.2.3
-      jasmine-core: 3.10.1
-    dev: true
-
-  /jasminewd2/2.2.0:
-    resolution: {integrity: sha512-Rn0nZe4rfDhzA63Al3ZGh0E+JTmM6ESZYXJGKuqKGZObsAB9fwXPD03GjtIEvJBDOhN94T5MzbwZSqzFHSQPzg==}
-    engines: {node: '>= 6.9.x'}
-    dev: true
-
   /js-tokens/4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
     dev: true
@@ -2464,10 +2025,6 @@
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dev: true
 
-  /json-schema-traverse/0.4.1:
-    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
-    dev: true
-
   /json-schema-traverse/1.0.0:
     resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
     dev: true
@@ -2490,12 +2047,6 @@
     resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
     dev: true
 
-  /jsonfile/4.0.0:
-    resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
-    optionalDependencies:
-      graceful-fs: 4.2.10
-    dev: true
-
   /jsonfile/6.1.0:
     resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
     dependencies:
@@ -2509,16 +2060,6 @@
     engines: {'0': node >= 0.2.0}
     dev: true
 
-  /jsprim/1.4.2:
-    resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
-    engines: {node: '>=0.6.0'}
-    dependencies:
-      assert-plus: 1.0.0
-      extsprintf: 1.3.0
-      json-schema: 0.4.0
-      verror: 1.10.0
-    dev: true
-
   /jsprim/2.0.2:
     resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
     engines: {'0': node >=0.6.0}
@@ -2529,101 +2070,11 @@
       verror: 1.10.0
     dev: true
 
-  /jszip/3.10.1:
-    resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
-    dependencies:
-      lie: 3.3.0
-      pako: 1.0.11
-      readable-stream: 2.3.7
-      setimmediate: 1.0.5
-    dev: true
-
-  /karma-chrome-launcher/3.1.0:
-    resolution: {integrity: sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==}
-    dependencies:
-      which: 1.3.1
-    dev: true
-
-  /karma-firefox-launcher/2.1.2:
-    resolution: {integrity: sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==}
-    dependencies:
-      is-wsl: 2.2.0
-      which: 2.0.2
-    dev: true
-
-  /karma-jasmine/4.0.1_karma@6.4.1:
-    resolution: {integrity: sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==}
-    engines: {node: '>= 10'}
-    peerDependencies:
-      karma: '*'
-    dependencies:
-      jasmine-core: 3.99.1
-      karma: 6.4.1
-    dev: true
-
-  /karma-requirejs/1.1.0_rexopxsgq4andhwzqrad3qy2ka:
-    resolution: {integrity: sha512-MHTOYKdwwJBkvYid0TaYvBzOnFH3TDtzo6ie5E4o9SaUSXXsfMRLa/whUz6efVIgTxj1xnKYasNn/XwEgJeB/Q==}
-    peerDependencies:
-      karma: '>=0.9'
-      requirejs: ^2.1.0
-    dependencies:
-      karma: 6.4.1
-      requirejs: 2.3.6
-    dev: true
-
-  /karma-sourcemap-loader/0.3.8:
-    resolution: {integrity: sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==}
-    dependencies:
-      graceful-fs: 4.2.10
-    dev: true
-
-  /karma/6.4.1:
-    resolution: {integrity: sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA==}
-    engines: {node: '>= 10'}
-    hasBin: true
-    dependencies:
-      '@colors/colors': 1.5.0
-      body-parser: 1.20.1
-      braces: 3.0.2
-      chokidar: 3.5.3
-      connect: 3.7.0
-      di: 0.0.1
-      dom-serialize: 2.2.1
-      glob: 7.2.3
-      graceful-fs: 4.2.10
-      http-proxy: 1.18.1
-      isbinaryfile: 4.0.10
-      lodash: 4.17.21
-      log4js: 6.7.1
-      mime: 2.6.0
-      minimatch: 3.1.2
-      mkdirp: 0.5.6
-      qjobs: 1.2.0
-      range-parser: 1.2.1
-      rimraf: 3.0.2
-      socket.io: 4.5.4
-      source-map: 0.6.1
-      tmp: 0.2.1
-      ua-parser-js: 0.7.32
-      yargs: 16.2.0
-    transitivePeerDependencies:
-      - bufferutil
-      - debug
-      - supports-color
-      - utf-8-validate
-    dev: true
-
   /lazy-ass/1.6.0:
     resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==}
     engines: {node: '> 0.8'}
     dev: true
 
-  /lie/3.3.0:
-    resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
-    dependencies:
-      immediate: 3.0.6
-    dev: true
-
   /listr2/3.14.0_enquirer@2.3.6:
     resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==}
     engines: {node: '>=10.0.0'}
@@ -2644,13 +2095,6 @@
       wrap-ansi: 7.0.0
     dev: true
 
-  /locate-path/5.0.0:
-    resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
-    engines: {node: '>=8'}
-    dependencies:
-      p-locate: 4.1.0
-    dev: true
-
   /lodash.once/4.1.1:
     resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
     dev: true
@@ -2677,19 +2121,6 @@
       wrap-ansi: 6.2.0
     dev: true
 
-  /log4js/6.7.1:
-    resolution: {integrity: sha512-lzbd0Eq1HRdWM2abSD7mk6YIVY0AogGJzb/z+lqzRk+8+XJP+M6L1MS5FUSc3jjGru4dbKjEMJmqlsoYYpuivQ==}
-    engines: {node: '>=8.0'}
-    dependencies:
-      date-format: 4.0.14
-      debug: 4.3.4
-      flatted: 3.2.7
-      rfdc: 1.3.0
-      streamroller: 3.1.4
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /lru-cache/5.1.1:
     resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
     dependencies:
@@ -2708,11 +2139,11 @@
     engines: {node: '>=12'}
     dev: true
 
-  /magic-string/0.26.7:
-    resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==}
+  /magic-string/0.27.0:
+    resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
     engines: {node: '>=12'}
     dependencies:
-      sourcemap-codec: 1.4.8
+      '@jridgewell/sourcemap-codec': 1.4.14
     dev: true
 
   /make-dir/2.1.0:
@@ -2773,11 +2204,6 @@
       - supports-color
     dev: true
 
-  /media-typer/0.3.0:
-    resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
-    engines: {node: '>= 0.6'}
-    dev: true
-
   /merge-stream/2.0.0:
     resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
     dev: true
@@ -2794,12 +2220,6 @@
       mime-db: 1.52.0
     dev: true
 
-  /mime/2.6.0:
-    resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
-    engines: {node: '>=4.0.0'}
-    hasBin: true
-    dev: true
-
   /mimic-fn/2.1.0:
     resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
     engines: {node: '>=6'}
@@ -2901,23 +2321,12 @@
       yallist: 4.0.0
     dev: true
 
-  /mkdirp/0.5.6:
-    resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
-    hasBin: true
-    dependencies:
-      minimist: 1.2.7
-    dev: true
-
   /mkdirp/1.0.4:
     resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
     engines: {node: '>=10'}
     hasBin: true
     dev: true
 
-  /ms/2.0.0:
-    resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
-    dev: true
-
   /ms/2.1.2:
     resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
     dev: true
@@ -3011,16 +2420,6 @@
       validate-npm-package-name: 5.0.0
     dev: true
 
-  /npm-package-arg/9.1.2:
-    resolution: {integrity: sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg==}
-    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
-    dependencies:
-      hosted-git-info: 5.2.1
-      proc-log: 2.0.1
-      semver: 7.3.8
-      validate-npm-package-name: 4.0.0
-    dev: true
-
   /npm-packlist/7.0.4:
     resolution: {integrity: sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -3071,33 +2470,10 @@
       set-blocking: 2.0.0
     dev: true
 
-  /oauth-sign/0.9.0:
-    resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
-    dev: true
-
-  /object-assign/4.1.1:
-    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
   /object-inspect/1.12.3:
     resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
     dev: true
 
-  /on-finished/2.3.0:
-    resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
-    engines: {node: '>= 0.8'}
-    dependencies:
-      ee-first: 1.1.1
-    dev: true
-
-  /on-finished/2.4.1:
-    resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
-    engines: {node: '>= 0.8'}
-    dependencies:
-      ee-first: 1.1.1
-    dev: true
-
   /once/1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
     dependencies:
@@ -3144,20 +2520,6 @@
     resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
     dev: true
 
-  /p-limit/2.3.0:
-    resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
-    engines: {node: '>=6'}
-    dependencies:
-      p-try: 2.2.0
-    dev: true
-
-  /p-locate/4.1.0:
-    resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
-    engines: {node: '>=8'}
-    dependencies:
-      p-limit: 2.3.0
-    dev: true
-
   /p-map/4.0.0:
     resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
     engines: {node: '>=10'}
@@ -3165,13 +2527,8 @@
       aggregate-error: 3.1.0
     dev: true
 
-  /p-try/2.2.0:
-    resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
-    engines: {node: '>=6'}
-    dev: true
-
-  /pacote/15.0.6:
-    resolution: {integrity: sha512-dQwcz/sME7QIL+cdrw/jftQfMMXxSo17i2kJ/gnhBhUvvBAsxoBu1lw9B5IzCH/Ce8CvEkG/QYZ6txzKfn0bTw==}
+  /pacote/15.0.8:
+    resolution: {integrity: sha512-UlcumB/XS6xyyIMwg/WwMAyUmga+RivB5KgkRwA1hZNtrx+0Bt41KxHCvg1kr0pZ/ZeD8qjhW4fph6VaYRCbLw==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     hasBin: true
     dependencies:
@@ -3180,8 +2537,8 @@
       '@npmcli/promise-spawn': 6.0.2
       '@npmcli/run-script': 6.0.0
       cacache: 17.0.4
-      fs-minipass: 2.1.0
-      minipass: 3.3.6
+      fs-minipass: 3.0.0
+      minipass: 4.0.0
       npm-package-arg: 10.1.0
       npm-packlist: 7.0.4
       npm-pick-manifest: 8.0.1
@@ -3197,33 +2554,15 @@
       - supports-color
     dev: true
 
-  /pako/1.0.11:
-    resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
-    dev: true
-
   /parse5/6.0.1:
     resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
     dev: true
 
-  /parseurl/1.3.3:
-    resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
-    engines: {node: '>= 0.8'}
-    dev: true
-
-  /path-exists/4.0.0:
-    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
-    engines: {node: '>=8'}
-    dev: true
-
   /path-is-absolute/1.0.1:
     resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /path-is-inside/1.0.2:
-    resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
-    dev: true
-
   /path-key/3.1.1:
     resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
     engines: {node: '>=8'}
@@ -3260,18 +2599,6 @@
     engines: {node: '>=6'}
     dev: true
 
-  /pinkie-promise/2.0.1:
-    resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      pinkie: 2.0.4
-    dev: true
-
-  /pinkie/2.0.4:
-    resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
   /prettier/2.6.1:
     resolution: {integrity: sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==}
     engines: {node: '>=10.13.0'}
@@ -3283,20 +2610,11 @@
     engines: {node: '>=6'}
     dev: true
 
-  /proc-log/2.0.1:
-    resolution: {integrity: sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==}
-    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
-    dev: true
-
   /proc-log/3.0.0:
     resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dev: true
 
-  /process-nextick-args/2.0.1:
-    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
-    dev: true
-
   /promise-inflight/1.0.1:
     resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
     peerDependencies:
@@ -3314,31 +2632,6 @@
       retry: 0.12.0
     dev: true
 
-  /protractor/7.0.0:
-    resolution: {integrity: sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==}
-    engines: {node: '>=10.13.x'}
-    deprecated: We have news to share - Protractor is deprecated and will reach end-of-life by Summer 2023. To learn more and find out about other options please refer to this post on the Angular blog. Thank you for using and contributing to Protractor. https://goo.gle/state-of-e2e-in-angular
-    hasBin: true
-    dependencies:
-      '@types/q': 0.0.32
-      '@types/selenium-webdriver': 3.0.20
-      blocking-proxy: 1.0.1
-      browserstack: 1.6.1
-      chalk: 1.1.3
-      glob: 7.2.3
-      jasmine: 2.8.0
-      jasminewd2: 2.2.0
-      q: 1.4.1
-      saucelabs: 1.5.0
-      selenium-webdriver: 3.6.0
-      source-map-support: 0.4.18
-      webdriver-js-extender: 2.1.0
-      webdriver-manager: 12.1.8
-      yargs: 15.4.1
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /proxy-from-env/1.0.0:
     resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
     dev: true
@@ -3359,16 +2652,6 @@
     engines: {node: '>=6'}
     dev: true
 
-  /q/1.4.1:
-    resolution: {integrity: sha512-/CdEdaw49VZVmyIDGUQKDDT53c7qBkO6g5CefWz91Ae+l4+cRtcDYwMTXh6me4O8TMldeGHG3N2Bl84V78Ywbg==}
-    engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
-    dev: true
-
-  /qjobs/1.2.0:
-    resolution: {integrity: sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==}
-    engines: {node: '>=0.9'}
-    dev: true
-
   /qs/6.10.4:
     resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==}
     engines: {node: '>=0.6'}
@@ -3376,33 +2659,6 @@
       side-channel: 1.0.4
     dev: true
 
-  /qs/6.11.0:
-    resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
-    engines: {node: '>=0.6'}
-    dependencies:
-      side-channel: 1.0.4
-    dev: true
-
-  /qs/6.5.3:
-    resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
-    engines: {node: '>=0.6'}
-    dev: true
-
-  /range-parser/1.2.1:
-    resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
-    engines: {node: '>= 0.6'}
-    dev: true
-
-  /raw-body/2.5.1:
-    resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
-    engines: {node: '>= 0.8'}
-    dependencies:
-      bytes: 3.1.2
-      http-errors: 2.0.0
-      iconv-lite: 0.4.24
-      unpipe: 1.0.0
-    dev: true
-
   /read-package-json-fast/3.0.2:
     resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -3421,18 +2677,6 @@
       npm-normalize-package-bin: 3.0.0
     dev: true
 
-  /readable-stream/2.3.7:
-    resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
-    dependencies:
-      core-util-is: 1.0.3
-      inherits: 2.0.4
-      isarray: 1.0.0
-      process-nextick-args: 2.0.1
-      safe-buffer: 5.1.2
-      string_decoder: 1.1.1
-      util-deprecate: 1.0.2
-    dev: true
-
   /readable-stream/3.6.0:
     resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
     engines: {node: '>= 6'}
@@ -3459,33 +2703,6 @@
       throttleit: 1.0.0
     dev: true
 
-  /request/2.88.2:
-    resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
-    engines: {node: '>= 6'}
-    deprecated: request has been deprecated, see https://github.com/request/request/issues/3142
-    dependencies:
-      aws-sign2: 0.7.0
-      aws4: 1.12.0
-      caseless: 0.12.0
-      combined-stream: 1.0.8
-      extend: 3.0.2
-      forever-agent: 0.6.1
-      form-data: 2.3.3
-      har-validator: 5.1.5
-      http-signature: 1.2.0
-      is-typedarray: 1.0.0
-      isstream: 0.1.2
-      json-stringify-safe: 5.0.1
-      mime-types: 2.1.35
-      oauth-sign: 0.9.0
-      performance-now: 2.1.0
-      qs: 6.5.3
-      safe-buffer: 5.2.1
-      tough-cookie: 2.5.0
-      tunnel-agent: 0.6.0
-      uuid: 3.4.0
-    dev: true
-
   /require-directory/2.1.1:
     resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
     engines: {node: '>=0.10.0'}
@@ -3496,20 +2713,12 @@
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /require-main-filename/2.0.0:
-    resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
-    dev: true
-
   /requirejs/2.3.6:
     resolution: {integrity: sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==}
     engines: {node: '>=0.4.0'}
     hasBin: true
     dev: true
 
-  /requires-port/1.0.0:
-    resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
-    dev: true
-
   /resolve/1.22.1:
     resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
     hasBin: true
@@ -3536,13 +2745,6 @@
     resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
     dev: true
 
-  /rimraf/2.7.1:
-    resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
-    hasBin: true
-    dependencies:
-      glob: 7.2.3
-    dev: true
-
   /rimraf/3.0.2:
     resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
     hasBin: true
@@ -3550,9 +2752,9 @@
       glob: 7.2.3
     dev: true
 
-  /rollup/2.66.1:
-    resolution: {integrity: sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==}
-    engines: {node: '>=10.0.0'}
+  /rollup/3.17.2:
+    resolution: {integrity: sha512-qMNZdlQPCkWodrAZ3qnJtvCAl4vpQ8q77uEujVCCbC/6CLB7Lcmvjq7HyiOSnf4fxTT9XgsE36oLHJBH49xjqA==}
+    engines: {node: '>=14.18.0', npm: '>=8.0.0'}
     hasBin: true
     optionalDependencies:
       fsevents: 2.3.2
@@ -3576,10 +2778,6 @@
       tslib: 2.4.1
     dev: true
 
-  /safe-buffer/5.1.2:
-    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
-    dev: true
-
   /safe-buffer/5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
     dev: true
@@ -3588,28 +2786,6 @@
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
     dev: true
 
-  /saucelabs/1.5.0:
-    resolution: {integrity: sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==}
-    dependencies:
-      https-proxy-agent: 2.2.4
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
-  /sax/1.2.4:
-    resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
-    dev: true
-
-  /selenium-webdriver/3.6.0:
-    resolution: {integrity: sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==}
-    engines: {node: '>= 6.9.0'}
-    dependencies:
-      jszip: 3.10.1
-      rimraf: 2.7.1
-      tmp: 0.0.30
-      xml2js: 0.4.23
-    dev: true
-
   /semver/5.7.1:
     resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
     hasBin: true
@@ -3632,14 +2808,6 @@
     resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
     dev: true
 
-  /setimmediate/1.0.5:
-    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
-    dev: true
-
-  /setprototypeof/1.2.0:
-    resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
-    dev: true
-
   /shebang-command/2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -3692,36 +2860,6 @@
     engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
     dev: true
 
-  /socket.io-adapter/2.4.0:
-    resolution: {integrity: sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==}
-    dev: true
-
-  /socket.io-parser/4.2.1:
-    resolution: {integrity: sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==}
-    engines: {node: '>=10.0.0'}
-    dependencies:
-      '@socket.io/component-emitter': 3.1.0
-      debug: 4.3.4
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
-  /socket.io/4.5.4:
-    resolution: {integrity: sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==}
-    engines: {node: '>=10.0.0'}
-    dependencies:
-      accepts: 1.3.8
-      base64id: 2.0.0
-      debug: 4.3.4
-      engine.io: 6.2.1
-      socket.io-adapter: 2.4.0
-      socket.io-parser: 4.2.1
-    transitivePeerDependencies:
-      - bufferutil
-      - supports-color
-      - utf-8-validate
-    dev: true
-
   /socks-proxy-agent/7.0.0:
     resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==}
     engines: {node: '>= 10'}
@@ -3741,12 +2879,6 @@
       smart-buffer: 4.2.0
     dev: true
 
-  /source-map-support/0.4.18:
-    resolution: {integrity: sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==}
-    dependencies:
-      source-map: 0.5.7
-    dev: true
-
   /source-map-support/0.5.21:
     resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
     dependencies:
@@ -3754,11 +2886,6 @@
       source-map: 0.6.1
     dev: true
 
-  /source-map/0.5.7:
-    resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
   /source-map/0.6.1:
     resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
     engines: {node: '>=0.10.0'}
@@ -3769,11 +2896,6 @@
     engines: {node: '>= 8'}
     dev: true
 
-  /sourcemap-codec/1.4.8:
-    resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
-    deprecated: Please use @jridgewell/sourcemap-codec instead
-    dev: true
-
   /spdx-correct/3.1.1:
     resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
     dependencies:
@@ -3826,27 +2948,6 @@
       minipass: 3.3.6
     dev: true
 
-  /statuses/1.5.0:
-    resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
-    engines: {node: '>= 0.6'}
-    dev: true
-
-  /statuses/2.0.1:
-    resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
-    engines: {node: '>= 0.8'}
-    dev: true
-
-  /streamroller/3.1.4:
-    resolution: {integrity: sha512-Ha1Ccw2/N5C/IF8Do6zgNe8F3jQo8MPBnMBGvX0QjNv/I97BcNRzK6/mzOpZHHK7DjMLTI3c7Xw7Y1KvdChkvw==}
-    engines: {node: '>=8.0'}
-    dependencies:
-      date-format: 4.0.14
-      debug: 4.3.4
-      fs-extra: 8.1.0
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /string-width/4.2.3:
     resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
     engines: {node: '>=8'}
@@ -3856,25 +2957,12 @@
       strip-ansi: 6.0.1
     dev: true
 
-  /string_decoder/1.1.1:
-    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
-    dependencies:
-      safe-buffer: 5.1.2
-    dev: true
-
   /string_decoder/1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
     dependencies:
       safe-buffer: 5.2.1
     dev: true
 
-  /strip-ansi/3.0.1:
-    resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
-    engines: {node: '>=0.10.0'}
-    dependencies:
-      ansi-regex: 2.1.1
-    dev: true
-
   /strip-ansi/6.0.1:
     resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
     engines: {node: '>=8'}
@@ -3887,11 +2975,6 @@
     engines: {node: '>=6'}
     dev: true
 
-  /supports-color/2.0.0:
-    resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
-    engines: {node: '>=0.8.0'}
-    dev: true
-
   /supports-color/5.5.0:
     resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
     engines: {node: '>=4'}
@@ -3935,17 +3018,14 @@
       yallist: 4.0.0
     dev: true
 
-  /terser/5.10.0:
-    resolution: {integrity: sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==}
+  /terser/5.16.4:
+    resolution: {integrity: sha512-5yEGuZ3DZradbogeYQ1NaGz7rXVBDWujWlx1PT8efXO6Txn+eWbfKqB2bTDVmFXmePFkoLU6XI8UektMIEA0ug==}
     engines: {node: '>=10'}
     hasBin: true
-    peerDependenciesMeta:
-      acorn:
-        optional: true
     dependencies:
+      '@jridgewell/source-map': 0.3.2
       acorn: 8.8.1
       commander: 2.20.3
-      source-map: 0.7.4
       source-map-support: 0.5.21
     dev: true
 
@@ -3957,13 +3037,6 @@
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
     dev: true
 
-  /tmp/0.0.30:
-    resolution: {integrity: sha512-HXdTB7lvMwcb55XFfrTM8CPr/IYREk4hVBFaQ4b/6nInrluSL86hfHm7vu0luYKCfyBZp2trCjpc8caC3vVM3w==}
-    engines: {node: '>=0.4.0'}
-    dependencies:
-      os-tmpdir: 1.0.2
-    dev: true
-
   /tmp/0.0.33:
     resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
     engines: {node: '>=0.6.0'}
@@ -3990,11 +3063,6 @@
       is-number: 7.0.0
     dev: true
 
-  /toidentifier/1.0.1:
-    resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
-    engines: {node: '>=0.6'}
-    dev: true
-
   /tough-cookie/2.5.0:
     resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
     engines: {node: '>=0.8'}
@@ -4025,24 +3093,12 @@
     engines: {node: '>=10'}
     dev: true
 
-  /type-is/1.6.18:
-    resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
-    engines: {node: '>= 0.6'}
-    dependencies:
-      media-typer: 0.3.0
-      mime-types: 2.1.35
-    dev: true
-
   /typescript/4.8.4:
     resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==}
     engines: {node: '>=4.2.0'}
     hasBin: true
     dev: true
 
-  /ua-parser-js/0.7.32:
-    resolution: {integrity: sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==}
-    dev: true
-
   /unique-filename/2.0.1:
     resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==}
     engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@@ -4071,21 +3127,11 @@
       imurmurhash: 0.1.4
     dev: true
 
-  /universalify/0.1.2:
-    resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
-    engines: {node: '>= 4.0.0'}
-    dev: true
-
   /universalify/2.0.0:
     resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
     engines: {node: '>= 10.0.0'}
     dev: true
 
-  /unpipe/1.0.0:
-    resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
-    engines: {node: '>= 0.8'}
-    dev: true
-
   /untildify/4.0.0:
     resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
     engines: {node: '>=8'}
@@ -4112,17 +3158,6 @@
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
     dev: true
 
-  /utils-merge/1.0.1:
-    resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
-    engines: {node: '>= 0.4.0'}
-    dev: true
-
-  /uuid/3.4.0:
-    resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
-    deprecated: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
-    hasBin: true
-    dev: true
-
   /uuid/8.3.2:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
@@ -4135,13 +3170,6 @@
       spdx-expression-parse: 3.0.1
     dev: true
 
-  /validate-npm-package-name/4.0.0:
-    resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==}
-    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
-    dependencies:
-      builtins: 5.0.1
-    dev: true
-
   /validate-npm-package-name/5.0.0:
     resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -4149,11 +3177,6 @@
       builtins: 5.0.1
     dev: true
 
-  /vary/1.1.2:
-    resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
-    engines: {node: '>= 0.8'}
-    dev: true
-
   /verror/1.10.0:
     resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
     engines: {'0': node >=0.6.0}
@@ -4163,54 +3186,12 @@
       extsprintf: 1.3.0
     dev: true
 
-  /void-elements/2.0.1:
-    resolution: {integrity: sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==}
-    engines: {node: '>=0.10.0'}
-    dev: true
-
   /wcwidth/1.0.1:
     resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
     dependencies:
       defaults: 1.0.4
     dev: true
 
-  /webdriver-js-extender/2.1.0:
-    resolution: {integrity: sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==}
-    engines: {node: '>=6.9.x'}
-    dependencies:
-      '@types/selenium-webdriver': 3.0.20
-      selenium-webdriver: 3.6.0
-    dev: true
-
-  /webdriver-manager/12.1.8:
-    resolution: {integrity: sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==}
-    engines: {node: '>=6.9.x'}
-    hasBin: true
-    dependencies:
-      adm-zip: 0.4.16
-      chalk: 1.1.3
-      del: 2.2.2
-      glob: 7.2.3
-      ini: 1.3.8
-      minimist: 1.2.7
-      q: 1.4.1
-      request: 2.88.2
-      rimraf: 2.7.1
-      semver: 5.7.1
-      xml2js: 0.4.23
-    dev: true
-
-  /which-module/2.0.0:
-    resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==}
-    dev: true
-
-  /which/1.3.1:
-    resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
-    hasBin: true
-    dependencies:
-      isexe: 2.0.0
-    dev: true
-
   /which/2.0.2:
     resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
     engines: {node: '>= 8'}
@@ -4255,36 +3236,6 @@
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
     dev: true
 
-  /ws/8.2.3:
-    resolution: {integrity: sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==}
-    engines: {node: '>=10.0.0'}
-    peerDependencies:
-      bufferutil: ^4.0.1
-      utf-8-validate: ^5.0.2
-    peerDependenciesMeta:
-      bufferutil:
-        optional: true
-      utf-8-validate:
-        optional: true
-    dev: true
-
-  /xml2js/0.4.23:
-    resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
-    engines: {node: '>=4.0.0'}
-    dependencies:
-      sax: 1.2.4
-      xmlbuilder: 11.0.1
-    dev: true
-
-  /xmlbuilder/11.0.1:
-    resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
-    engines: {node: '>=4.0'}
-    dev: true
-
-  /y18n/4.0.3:
-    resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
-    dev: true
-
   /y18n/5.0.8:
     resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
     engines: {node: '>=10'}
@@ -4298,54 +3249,11 @@
     resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
     dev: true
 
-  /yargs-parser/18.1.3:
-    resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
-    engines: {node: '>=6'}
-    dependencies:
-      camelcase: 5.3.1
-      decamelize: 1.2.0
-    dev: true
-
-  /yargs-parser/20.2.9:
-    resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
-    engines: {node: '>=10'}
-    dev: true
-
   /yargs-parser/21.1.1:
     resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
     engines: {node: '>=12'}
     dev: true
 
-  /yargs/15.4.1:
-    resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
-    engines: {node: '>=8'}
-    dependencies:
-      cliui: 6.0.0
-      decamelize: 1.2.0
-      find-up: 4.1.0
-      get-caller-file: 2.0.5
-      require-directory: 2.1.1
-      require-main-filename: 2.0.0
-      set-blocking: 2.0.0
-      string-width: 4.2.3
-      which-module: 2.0.0
-      y18n: 4.0.3
-      yargs-parser: 18.1.3
-    dev: true
-
-  /yargs/16.2.0:
-    resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
-    engines: {node: '>=10'}
-    dependencies:
-      cliui: 7.0.4
-      escalade: 3.1.1
-      get-caller-file: 2.0.5
-      require-directory: 2.1.1
-      string-width: 4.2.3
-      y18n: 5.0.8
-      yargs-parser: 20.2.9
-    dev: true
-
   /yargs/17.6.2:
     resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==}
     engines: {node: '>=12'}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..36eb141
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,3 @@
+packages:
+  - 'scouting/www'
+  - 'scouting/www/*'
diff --git a/scouting/BUILD b/scouting/BUILD
index ae121e6..1d6ac5d 100644
--- a/scouting/BUILD
+++ b/scouting/BUILD
@@ -1,5 +1,5 @@
+load("@aspect_rules_cypress//cypress:defs.bzl", "cypress_module_test")
 load("//tools/build_rules:apache.bzl", "apache_wrapper")
-load("//tools/build_rules:js.bzl", "protractor_ts_test")
 
 sh_binary(
     name = "scouting",
@@ -16,13 +16,23 @@
     ],
 )
 
-protractor_ts_test(
+cypress_module_test(
     name = "scouting_test",
-    srcs = [
-        ":scouting_test.ts",
+    args = [
+        "run",
+        "--config-file=cypress.config.js",
+        "--browser=../../chrome_linux/chrome",
     ],
-    on_prepare = ":scouting_test.protractor.on-prepare.js",
-    server = "//scouting/testing:scouting_test_servers",
+    browsers = ["@chrome_linux//:all"],
+    copy_data_to_bin = False,
+    cypress = "//:node_modules/cypress",
+    data = [
+        "cypress.config.js",
+        "scouting_test.cy.js",
+        "//scouting/testing:scouting_test_servers",
+        "@xvfb_amd64//:wrapped_bin/Xvfb",
+    ],
+    runner = "cypress_runner.js",
 )
 
 apache_wrapper(
diff --git a/scouting/cypress.config.js b/scouting/cypress.config.js
new file mode 100644
index 0000000..4eb1a82
--- /dev/null
+++ b/scouting/cypress.config.js
@@ -0,0 +1,22 @@
+const {defineConfig} = require('cypress');
+
+module.exports = defineConfig({
+  e2e: {
+    specPattern: ['*.cy.js'],
+    supportFile: false,
+    setupNodeEvents(on, config) {
+      on('before:browser:launch', (browser = {}, launchOptions) => {
+        launchOptions.args.push('--disable-gpu-shader-disk-cache');
+      });
+
+      // Lets users print to the console:
+      //    cy.task('log', 'message here');
+      on('task', {
+        log(message) {
+          console.log(message);
+          return null;
+        },
+      });
+    },
+  },
+});
diff --git a/scouting/cypress_runner.js b/scouting/cypress_runner.js
new file mode 100644
index 0000000..6c63adb
--- /dev/null
+++ b/scouting/cypress_runner.js
@@ -0,0 +1,86 @@
+const child_process = require('child_process');
+const process = require('process');
+
+const cypress = require('cypress');
+
+// Set up the xvfb binary.
+process.env[
+  'PATH'
+] = `${process.env.RUNFILES_DIR}/xvfb_amd64/wrapped_bin:${process.env.PATH}`;
+
+// Start the web server, database, and fake TBA server.
+// We use file descriptor 3 ('pipe') for the test server to let us know when
+// everything has started up.
+console.log('Starting server.');
+let servers = child_process.spawn(
+  'testing/scouting_test_servers',
+  ['--port=8000', '--notify_fd=3'],
+  {
+    stdio: ['inherit', 'inherit', 'inherit', 'pipe'],
+  }
+);
+
+// Wait for the server to finish starting up.
+const serverStartup = new Promise((resolve, reject) => {
+  let cumulativeData = '';
+  servers.stdio[3].on('data', async (data) => {
+    console.log('Got data: ' + data);
+    cumulativeData += data;
+    if (cumulativeData.includes('READY')) {
+      console.log('Everything is ready!');
+      resolve();
+    }
+  });
+
+  servers.on('error', (err) => {
+    console.log(`Failed to start scouting_test_servers: ${err}`);
+    reject();
+  });
+
+  servers.on('close', (code, signal) => {
+    console.log(`scouting_test_servers closed: ${code} (${signal})`);
+    reject();
+  });
+
+  servers.on('exit', (code, signal) => {
+    console.log(`scouting_test_servers exited: ${code} (${signal})`);
+    reject();
+  });
+});
+
+// Wait for the server to shut down.
+const serverShutdown = new Promise((resolve) => {
+  servers.on('exit', () => {
+    resolve();
+  });
+});
+
+// Wait for the server to be ready, run the tests, then shut down the server.
+(async () => {
+  await serverStartup;
+  const result = await cypress.run({
+    headless: true,
+    config: {
+      baseUrl: 'http://localhost:8000',
+      screenshotsFolder:
+        process.env.TEST_UNDECLARED_OUTPUTS_DIR + '/screenshots',
+      video: false,
+      videosFolder: process.env.TEST_UNDECLARED_OUTPUTS_DIR + '/videos',
+    },
+  });
+  await servers.kill();
+  await serverShutdown;
+
+  exitCode = 0;
+  if (result.status == 'failed') {
+    exitCode = 1;
+    console.log('-'.repeat(50));
+    console.log('Test FAILED: ' + result.message);
+    console.log('-'.repeat(50));
+  } else if (result.totalFailed > 0) {
+    // When the "before" hook fails, we don't get a "failed" mesage for some
+    // reason. In that case, we just have to exit with an error.
+    exitCode = 1;
+  }
+  process.exit(exitCode);
+})();
diff --git a/scouting/scouting.sh b/scouting/scouting.sh
index 30e2989..f1d7123 100755
--- a/scouting/scouting.sh
+++ b/scouting/scouting.sh
@@ -17,5 +17,5 @@
 
 exec \
     "${RUNFILES_DIR}"/org_frc971/scouting/webserver/webserver_/webserver \
-    -directory "${RUNFILES_DIR}"/org_frc971/scouting/www/ \
+    -directory "${RUNFILES_DIR}"/org_frc971/scouting/www/static_files/ \
     "$@"
diff --git a/scouting/scouting_test.cy.js b/scouting/scouting_test.cy.js
new file mode 100644
index 0000000..e27fb54
--- /dev/null
+++ b/scouting/scouting_test.cy.js
@@ -0,0 +1,346 @@
+/// <reference types="cypress" />
+
+function disableAlerts() {
+  cy.get('#block_alerts').check({force: true}).should('be.checked');
+}
+
+function switchToTab(tabName) {
+  cy.contains('.nav-link', tabName).click();
+}
+
+function headerShouldBe(text) {
+  cy.get('.header').should('have.text', text);
+}
+
+function clickButton(buttonName) {
+  cy.contains('button', buttonName).click();
+}
+
+function setInputTo(fieldSelector, value) {
+  cy.get(fieldSelector).type('{selectAll}' + value);
+}
+
+// Moves the nth slider left or right. A positive "adjustBy" value moves the
+// slider to the right. A negative value moves the slider to the left.
+//
+//   negative/left <--- 0 ---> positive/right
+function adjustNthSliderBy(n, adjustBy) {
+  let element = cy.get('input[type=range]').eq(n);
+  element.scrollIntoView();
+  element.invoke('val').then((currentValue) => {
+    // We need to query for the slider here again because `invoke('val')` above
+    // somehow invalidates further calls to `val`.
+    cy.get('input[type=range]')
+      .eq(n)
+      .invoke('val', currentValue + adjustBy)
+      .trigger('change');
+  });
+}
+
+// Asserts that the field on the "Submit and Review" screen has a specific
+// value.
+function expectReviewFieldToBe(fieldName, expectedValue) {
+  expectNthReviewFieldToBe(fieldName, 0, expectedValue);
+}
+
+// Asserts that the n'th instance of a field on the "Submit and Review"
+// screen has a specific value.
+function expectNthReviewFieldToBe(fieldName, n, expectedValue) {
+  getNthReviewField(fieldName, n).should(
+    'have.text',
+    `${fieldName}: ${expectedValue}`
+  );
+}
+
+function getNthReviewField(fieldName, n) {
+  let element = cy.get('li').filter(`:contains("${fieldName}: ")`).eq(n);
+  element.scrollIntoView();
+  return element;
+}
+
+before(() => {
+  cy.visit('/');
+  disableAlerts();
+  cy.title().should('eq', 'FRC971 Scouting Application');
+
+  // Import the match list before running any tests. Ideally this should be
+  // run in beforeEach(), but it's not worth doing that at this time. Our
+  // tests are basic enough not to require this.
+  switchToTab('Import Match List');
+  headerShouldBe('Import Match List');
+  setInputTo('#year', '2016');
+  setInputTo('#event_code', 'nytr');
+  clickButton('Import');
+
+  cy.get('.progress_message').contains('Successfully imported match list.');
+});
+
+beforeEach(() => {
+  cy.visit('/');
+  disableAlerts();
+});
+
+describe('Scouting app tests', () => {
+  it('should: show matches in chronological order.', () => {
+    headerShouldBe('Matches');
+    cy.get('.badge').eq(0).contains('Quals Match 1');
+    cy.get('.badge').eq(1).contains('Quals Match 2');
+    cy.get('.badge').eq(2).contains('Quals Match 3');
+    cy.get('.badge').eq(9).contains('Quals Match 10');
+    cy.get('.badge').eq(72).contains('Quarter Final 1 Match 1');
+    cy.get('.badge').eq(73).contains('Quarter Final 2 Match 1');
+    cy.get('.badge').eq(74).contains('Quarter Final 3 Match 1');
+    cy.get('.badge').eq(75).contains('Quarter Final 4 Match 1');
+    cy.get('.badge').eq(76).contains('Quarter Final 1 Match 2');
+    cy.get('.badge').eq(82).contains('Semi Final 1 Match 1');
+    cy.get('.badge').eq(83).contains('Semi Final 2 Match 1');
+    cy.get('.badge').eq(84).contains('Semi Final 1 Match 2');
+    cy.get('.badge').eq(85).contains('Semi Final 2 Match 2');
+    cy.get('.badge').eq(89).contains('Final 1 Match 3');
+  });
+
+  it('should: prefill the match information.', () => {
+    headerShouldBe('Matches');
+
+    // On the 87th row of matches (index 86) click on the second team
+    // (index 1) which resolves to team 5254 in semi final 2 match 3.
+    cy.get('button.match-item')
+      .eq(86 * 6 + 1)
+      .click();
+
+    headerShouldBe('Team Selection');
+    cy.get('#match_number').should('have.value', '3');
+    cy.get('#team_number').should('have.value', '5254');
+    cy.get('#set_number').should('have.value', '2');
+    cy.get('#comp_level').should('have.value', '3: sf');
+  });
+
+  it('should: error on unknown match.', () => {
+    switchToTab('Data Entry');
+    headerShouldBe('Team Selection');
+
+    // Pick a match that doesn't exist in the 2016nytr match list.
+    setInputTo('#match_number', '3');
+    setInputTo('#team_number', '971');
+
+    // Click Next until we get to the submit screen.
+    for (let i = 0; i < 5; i++) {
+      clickButton('Next');
+    }
+    headerShouldBe('Review and Submit');
+
+    // Attempt to submit and validate the error.
+    clickButton('Submit');
+    cy.get('.error_message').contains(
+      'Failed to find team 971 in match 3 in the schedule.'
+    );
+  });
+
+  // Make sure that each page on the Entry tab has both "Next" and "Back"
+  // buttons. The only screens exempted from this are the first page and the
+  // last page.
+  it('should: have forwards and backwards buttons.', () => {
+    switchToTab('Data Entry');
+
+    const expectedOrder = [
+      'Team Selection',
+      'Auto',
+      'TeleOp',
+      'Climb',
+      'Other',
+      'Review and Submit',
+    ];
+
+    // Go forward through the screens.
+    for (let i = 0; i < expectedOrder.length; i++) {
+      headerShouldBe(expectedOrder[i]);
+      if (i != expectedOrder.length - 1) {
+        clickButton('Next');
+      }
+    }
+
+    // Go backwards through the screens.
+    for (let i = 0; i < expectedOrder.length; i++) {
+      headerShouldBe(expectedOrder[expectedOrder.length - i - 1]);
+      if (i != expectedOrder.length - 1) {
+        clickButton('Back');
+      }
+    }
+  });
+
+  it('should: review and submit correct data.', () => {
+    switchToTab('Data Entry');
+
+    // Submit scouting data for a random team that attended 2016nytr.
+    headerShouldBe('Team Selection');
+    setInputTo('#match_number', '2');
+    setInputTo('#team_number', '5254');
+    setInputTo('#set_number', '42');
+    cy.get('#comp_level').select('Semi Finals');
+    clickButton('Next');
+
+    headerShouldBe('Auto');
+    cy.get('#quadrant3').check();
+    clickButton('Next');
+
+    headerShouldBe('TeleOp');
+    clickButton('Next');
+
+    headerShouldBe('Climb');
+    cy.get('#high').check();
+    clickButton('Next');
+
+    headerShouldBe('Other');
+    adjustNthSliderBy(0, 3);
+    adjustNthSliderBy(1, 1);
+    cy.get('#no_show').check();
+    cy.get('#mechanically_broke').check();
+    setInputTo('#comment', 'A very useful comment here.');
+    clickButton('Next');
+
+    headerShouldBe('Review and Submit');
+    cy.get('.error_message').should('have.text', '');
+
+    // Validate Team Selection.
+    expectReviewFieldToBe('Match number', '2');
+    expectReviewFieldToBe('Team number', '5254');
+    expectReviewFieldToBe('SetNumber', '42');
+    expectReviewFieldToBe('Comp Level', 'Semi Finals');
+
+    // Validate Auto.
+    expectNthReviewFieldToBe('Upper Shots Made', 0, '0');
+    expectNthReviewFieldToBe('Lower Shots Made', 0, '0');
+    expectNthReviewFieldToBe('Missed Shots', 0, '0');
+    expectReviewFieldToBe('Quadrant', '3');
+
+    // Validate TeleOp.
+    expectNthReviewFieldToBe('Upper Shots Made', 1, '0');
+    expectNthReviewFieldToBe('Lower Shots Made', 1, '0');
+    expectNthReviewFieldToBe('Missed Shots', 1, '0');
+
+    // Validate Climb.
+    expectReviewFieldToBe('Climb Level', 'High');
+
+    // Validate Other.
+    expectReviewFieldToBe('Defense Played On Rating', '3');
+    expectReviewFieldToBe('Defense Played Rating', '1');
+    expectReviewFieldToBe('No show', 'true');
+    expectReviewFieldToBe('Never moved', 'false');
+    expectReviewFieldToBe('Battery died', 'false');
+    expectReviewFieldToBe('Broke (mechanically)', 'true');
+    expectReviewFieldToBe('Comments', 'A very useful comment here.');
+
+    clickButton('Submit');
+    headerShouldBe('Success');
+  });
+
+  it('should: load all images successfully.', () => {
+    switchToTab('Data Entry');
+
+    // Get to the Auto display with the field pictures.
+    headerShouldBe('Team Selection');
+    clickButton('Next');
+    headerShouldBe('Auto');
+
+    // We expect 2 fully loaded images for each of the orientations.
+    // 2 images for the original orientation and 2 images for the flipped orientation.
+    for (let i = 0; i < 2; i++) {
+      cy.get('img').should(($imgs) => {
+        for (const $img of $imgs) {
+          expect($img.naturalWidth).to.be.greaterThan(0);
+        }
+      });
+      clickButton('Flip');
+    }
+  });
+
+  it('should: submit note scouting for multiple teams', () => {
+    // Navigate to Notes Page.
+    switchToTab('Notes');
+    headerShouldBe('Notes');
+
+    // Add first team.
+    setInputTo('#team_number_notes', '1234');
+    clickButton('Select');
+
+    // Add note and select keyword for first team.
+    cy.get('#team-key-1').should('have.text', '1234');
+    setInputTo('#text-input-1', 'Good Driving');
+    cy.get('#good_driving_0').click();
+
+    // Navigate to add team selection and add another team.
+    clickButton('Add team');
+    setInputTo('#team_number_notes', '1235');
+    clickButton('Select');
+
+    // Add note and select keyword for second team.
+    cy.get('#team-key-2').should('have.text', '1235');
+    setInputTo('#text-input-2', 'Bad Driving');
+    cy.get('#bad_driving_1').click();
+
+    // Submit Notes.
+    clickButton('Submit');
+    cy.get('#team_number_label').should('have.text', ' Team Number ');
+  });
+
+  it('should: switch note text boxes with keyboard shortcuts', () => {
+    // Navigate to Notes Page.
+    switchToTab('Notes');
+    headerShouldBe('Notes');
+
+    // Add first team.
+    setInputTo('#team_number_notes', '1234');
+    clickButton('Select');
+
+    // Add second team.
+    clickButton('Add team');
+    setInputTo('#team_number_notes', '1235');
+    clickButton('Select');
+
+    // Add third team.
+    clickButton('Add team');
+    setInputTo('#team_number_notes', '1236');
+    clickButton('Select');
+
+    for (let i = 1; i <= 3; i++) {
+      // Press Control + i
+      cy.get('body').type(`{ctrl}${i}`);
+
+      // Expect text input to be focused.
+      cy.focused().then(($element) => {
+        expect($element).to.have.id(`text-input-${i}`);
+      });
+    }
+  });
+
+  it('should: submit driver ranking', () => {
+    // Navigate to Driver Ranking Page.
+    switchToTab('Driver Ranking');
+    headerShouldBe('Driver Ranking');
+
+    // Input match and team numbers.
+    setInputTo('#match_number_selection', '11');
+    setInputTo('#team_input_0', '123');
+    setInputTo('#team_input_1', '456');
+    setInputTo('#team_input_2', '789');
+    clickButton('Select');
+
+    // Verify match and team key input.
+    cy.get('#match_number_heading').should('have.text', 'Match #11');
+    cy.get('#team_key_label_0').should('have.text', ' 123 ');
+    cy.get('#team_key_label_1').should('have.text', ' 456 ');
+    cy.get('#team_key_label_2').should('have.text', ' 789 ');
+
+    // Rank teams.
+    cy.get('#up_button_2').click();
+    cy.get('#down_button_0').click();
+
+    // Verify ranking change.
+    cy.get('#team_key_label_0').should('have.text', ' 789 ');
+    cy.get('#team_key_label_1').should('have.text', ' 123 ');
+    cy.get('#team_key_label_2').should('have.text', ' 456 ');
+
+    // Submit.
+    clickButton('Submit');
+  });
+});
diff --git a/scouting/scouting_test.protractor.on-prepare.js b/scouting/scouting_test.protractor.on-prepare.js
deleted file mode 100644
index a1f7267..0000000
--- a/scouting/scouting_test.protractor.on-prepare.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// The function exported from this file is used by the protractor_web_test_suite.
-// It is passed to the `onPrepare` configuration setting in protractor and executed
-// before running tests.
-//
-// If the function returns a promise, as it does here, protractor will wait
-// for the promise to resolve before running tests.
-
-const protractorUtils = require('@bazel/protractor/protractor-utils');
-const protractor = require('protractor');
-
-module.exports = function (config) {
-  // In this example, `@bazel/protractor/protractor-utils` is used to run
-  // the server. protractorUtils.runServer() runs the server on a randomly
-  // selected port (given a port flag to pass to the server as an argument).
-  // The port used is returned in serverSpec and the protractor serverUrl
-  // is the configured.
-  return protractorUtils
-    .runServer(config.workspace, config.server, '--port', [])
-    .then((serverSpec) => {
-      protractor.browser.baseUrl = `http://localhost:${serverSpec.port}`;
-    });
-};
diff --git a/scouting/scouting_test.ts b/scouting/scouting_test.ts
deleted file mode 100644
index cbeffc1..0000000
--- a/scouting/scouting_test.ts
+++ /dev/null
@@ -1,427 +0,0 @@
-import {browser, by, element, protractor} from 'protractor';
-
-const EC = protractor.ExpectedConditions;
-
-// Loads the page (or reloads it) and deals with the "Are you sure you want to
-// leave this page" popup.
-async function loadPage() {
-  await disableAlerts();
-  await browser.navigate().refresh();
-  expect(await browser.getTitle()).toEqual('FRC971 Scouting Application');
-  await disableAlerts();
-}
-
-// Disables alert popups. They are extremely tedious to deal with in
-// Protractor since they're not angular elements. We achieve this by checking
-// an invisible checkbox that's off-screen.
-async function disableAlerts() {
-  await browser.executeAsyncScript(function (callback) {
-    let block_alerts = document.getElementById(
-      'block_alerts'
-    ) as HTMLInputElement;
-    block_alerts.checked = true;
-    callback();
-  });
-}
-// Returns the contents of the header that displays the "Auto", "TeleOp", and
-// "Climb" labels etc.
-function getHeadingText() {
-  return element(by.css('.header')).getText();
-}
-
-// Returns the currently displayed progress message on the screen. This only
-// exists on screens where the web page interacts with the web server.
-function getProgressMessage() {
-  return element(by.css('.progress_message')).getText();
-}
-
-// Returns the currently displayed error message on the screen. This only
-// exists on screens where the web page interacts with the web server.
-function getErrorMessage() {
-  return element(by.css('.error_message')).getText();
-}
-
-// Returns the currently displayed error message on the screen. This only
-// exists on screens where the web page interacts with the web server.
-function getValueOfInputById(id: string) {
-  return element(by.id(id)).getAttribute('value');
-}
-
-// Asserts that the field on the "Submit and Review" screen has a specific
-// value.
-function expectReviewFieldToBe(fieldName: string, expectedValue: string) {
-  return expectNthReviewFieldToBe(fieldName, 0, expectedValue);
-}
-
-// Asserts that the n'th instance of a field on the "Submit and Review"
-// screen has a specific value.
-async function expectNthReviewFieldToBe(
-  fieldName: string,
-  n: number,
-  expectedValue: string
-) {
-  expect(
-    await element
-      .all(by.cssContainingText('li', `${fieldName}:`))
-      .get(n)
-      .getText()
-  ).toEqual(`${fieldName}: ${expectedValue}`);
-}
-
-// Sets a text field to the specified value.
-function setTextboxByIdTo(id: string, value: string) {
-  // Just sending "value" to the input fields is insufficient. We need to
-  // overwrite the text that is there. If we didn't hit CTRL-A to select all
-  // the text, we'd be appending to whatever is there already.
-  return element(by.id(id)).sendKeys(
-    protractor.Key.CONTROL,
-    'a',
-    protractor.Key.NULL,
-    value
-  );
-}
-
-// Moves the nth slider left or right. A positive "adjustBy" value moves the
-// slider to the right. A negative value moves the slider to the left.
-//
-//   negative/left <--- 0 ---> positive/right
-async function adjustNthSliderBy(n: number, adjustBy: number) {
-  const slider = element.all(by.css('input[type=range]')).get(n);
-  const key =
-    adjustBy > 0 ? protractor.Key.ARROW_RIGHT : protractor.Key.ARROW_LEFT;
-  for (let i = 0; i < Math.abs(adjustBy); i++) {
-    await slider.sendKeys(key);
-  }
-}
-
-function getNthMatchLabel(n: number) {
-  return element.all(by.css('.badge')).get(n).getText();
-}
-
-describe('The scouting web page', () => {
-  beforeAll(async () => {
-    await browser.get(browser.baseUrl);
-    expect(await browser.getTitle()).toEqual('FRC971 Scouting Application');
-    await disableAlerts();
-
-    // Import the match list before running any tests. Ideally this should be
-    // run in beforeEach(), but it's not worth doing that at this time. Our
-    // tests are basic enough not to require this.
-    await element(
-      by.cssContainingText('.nav-link', 'Import Match List')
-    ).click();
-    expect(await getHeadingText()).toEqual('Import Match List');
-    await setTextboxByIdTo('year', '2016');
-    await setTextboxByIdTo('event_code', 'nytr');
-    await element(by.buttonText('Import')).click();
-
-    await browser.wait(
-      EC.textToBePresentInElement(
-        element(by.css('.progress_message')),
-        'Successfully imported match list.'
-      )
-    );
-  });
-
-  it('should: show matches in chronological order.', async () => {
-    await loadPage();
-
-    expect(await getNthMatchLabel(0)).toEqual('Quals Match 1');
-    expect(await getNthMatchLabel(1)).toEqual('Quals Match 2');
-    expect(await getNthMatchLabel(2)).toEqual('Quals Match 3');
-    expect(await getNthMatchLabel(9)).toEqual('Quals Match 10');
-    expect(await getNthMatchLabel(72)).toEqual('Quarter Final 1 Match 1');
-    expect(await getNthMatchLabel(73)).toEqual('Quarter Final 2 Match 1');
-    expect(await getNthMatchLabel(74)).toEqual('Quarter Final 3 Match 1');
-    expect(await getNthMatchLabel(75)).toEqual('Quarter Final 4 Match 1');
-    expect(await getNthMatchLabel(76)).toEqual('Quarter Final 1 Match 2');
-    expect(await getNthMatchLabel(82)).toEqual('Semi Final 1 Match 1');
-    expect(await getNthMatchLabel(83)).toEqual('Semi Final 2 Match 1');
-    expect(await getNthMatchLabel(84)).toEqual('Semi Final 1 Match 2');
-    expect(await getNthMatchLabel(85)).toEqual('Semi Final 2 Match 2');
-    expect(await getNthMatchLabel(89)).toEqual('Final 1 Match 3');
-  });
-
-  it('should: prefill the match information.', async () => {
-    await loadPage();
-
-    expect(await getHeadingText()).toEqual('Matches');
-
-    // On the 87th row of matches (index 86) click on the second team
-    // (index 1) which resolves to team 5254 in semi final 2 match 3.
-    await element
-      .all(by.css('button.match-item'))
-      .get(86 * 6 + 1)
-      .click();
-
-    expect(await getHeadingText()).toEqual('Team Selection');
-    expect(await getValueOfInputById('match_number')).toEqual('3');
-    expect(await getValueOfInputById('team_number')).toEqual('5254');
-    expect(await getValueOfInputById('set_number')).toEqual('2');
-    expect(await getValueOfInputById('comp_level')).toEqual('3: sf');
-  });
-
-  it('should: error on unknown match.', async () => {
-    await loadPage();
-
-    await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
-
-    // Pick a match that doesn't exist in the 2016nytr match list.
-    await setTextboxByIdTo('match_number', '3');
-    await setTextboxByIdTo('team_number', '971');
-
-    // Click Next until we get to the submit screen.
-    for (let i = 0; i < 5; i++) {
-      await element(by.buttonText('Next')).click();
-    }
-    expect(await getHeadingText()).toEqual('Review and Submit');
-
-    // Attempt to submit and validate the error.
-    await element(by.buttonText('Submit')).click();
-    expect(await getErrorMessage()).toContain(
-      'Failed to find team 971 in match 3 in the schedule.'
-    );
-  });
-
-  // Make sure that each page on the Entry tab has both "Next" and "Back"
-  // buttons. The only screens exempted from this are the first page and the
-  // last page.
-  it('should: have forwards and backwards buttons.', async () => {
-    await loadPage();
-
-    await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
-
-    const expectedOrder = [
-      'Team Selection',
-      'Auto',
-      'TeleOp',
-      'Climb',
-      'Other',
-      'Review and Submit',
-    ];
-
-    // Go forward through the screens.
-    for (let i = 0; i < expectedOrder.length; i++) {
-      expect(await getHeadingText()).toEqual(expectedOrder[i]);
-      if (i != expectedOrder.length - 1) {
-        await element(by.buttonText('Next')).click();
-      }
-    }
-
-    // Go backwards through the screens.
-    for (let i = 0; i < expectedOrder.length; i++) {
-      expect(await getHeadingText()).toEqual(
-        expectedOrder[expectedOrder.length - i - 1]
-      );
-      if (i != expectedOrder.length - 1) {
-        await element(by.buttonText('Back')).click();
-      }
-    }
-  });
-
-  it('should: review and submit correct data.', async () => {
-    await loadPage();
-
-    await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
-
-    // Submit scouting data for a random team that attended 2016nytr.
-    expect(await getHeadingText()).toEqual('Team Selection');
-    await setTextboxByIdTo('match_number', '2');
-    await setTextboxByIdTo('team_number', '5254');
-    await setTextboxByIdTo('set_number', '42');
-    await element(by.cssContainingText('option', 'Semi Finals')).click();
-    await element(by.buttonText('Next')).click();
-
-    expect(await getHeadingText()).toEqual('Auto');
-    await element(by.id('quadrant3')).click();
-    await element(by.buttonText('Next')).click();
-
-    expect(await getHeadingText()).toEqual('TeleOp');
-    await element(by.buttonText('Next')).click();
-
-    expect(await getHeadingText()).toEqual('Climb');
-    await element(by.id('high')).click();
-    await element(by.buttonText('Next')).click();
-
-    expect(await getHeadingText()).toEqual('Other');
-    await adjustNthSliderBy(0, 3);
-    await adjustNthSliderBy(1, 1);
-    await element(by.id('no_show')).click();
-    await element(by.id('mechanically_broke')).click();
-    await setTextboxByIdTo('comment', 'A very useful comment here.');
-    await element(by.buttonText('Next')).click();
-
-    expect(await getHeadingText()).toEqual('Review and Submit');
-    expect(await getErrorMessage()).toEqual('');
-
-    // Validate Team Selection.
-    await expectReviewFieldToBe('Match number', '2');
-    await expectReviewFieldToBe('Team number', '5254');
-    await expectReviewFieldToBe('SetNumber', '42');
-    await expectReviewFieldToBe('Comp Level', 'Semi Finals');
-
-    // Validate Auto.
-    await expectNthReviewFieldToBe('Upper Shots Made', 0, '0');
-    await expectNthReviewFieldToBe('Lower Shots Made', 0, '0');
-    await expectNthReviewFieldToBe('Missed Shots', 0, '0');
-    await expectReviewFieldToBe('Quadrant', '3');
-
-    // Validate TeleOp.
-    await expectNthReviewFieldToBe('Upper Shots Made', 1, '0');
-    await expectNthReviewFieldToBe('Lower Shots Made', 1, '0');
-    await expectNthReviewFieldToBe('Missed Shots', 1, '0');
-
-    // Validate Climb.
-    await expectReviewFieldToBe('Climb Level', 'High');
-
-    // Validate Other.
-    await expectReviewFieldToBe('Defense Played On Rating', '3');
-    await expectReviewFieldToBe('Defense Played Rating', '1');
-    await expectReviewFieldToBe('No show', 'true');
-    await expectReviewFieldToBe('Never moved', 'false');
-    await expectReviewFieldToBe('Battery died', 'false');
-    await expectReviewFieldToBe('Broke (mechanically)', 'true');
-    await expectReviewFieldToBe('Comments', 'A very useful comment here.');
-
-    await element(by.buttonText('Submit')).click();
-    await browser.wait(
-      EC.textToBePresentInElement(element(by.css('.header')), 'Success')
-    );
-
-    // TODO(phil): Make sure the data made its way to the database correctly.
-  });
-
-  it('should: load all images successfully.', async () => {
-    await loadPage();
-
-    await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
-
-    // Get to the Auto display with the field pictures.
-    expect(await getHeadingText()).toEqual('Team Selection');
-    await element(by.buttonText('Next')).click();
-    expect(await getHeadingText()).toEqual('Auto');
-
-    // We expect 2 fully loaded images for each of the orientations.
-    // 2 images for the original orientation and 2 images for the flipped orientation.
-    for (let i = 0; i < 2; i++) {
-      browser
-        .executeAsyncScript(function (callback) {
-          let images = document.getElementsByTagName('img');
-          let numLoaded = 0;
-          for (let i = 0; i < images.length; i += 1) {
-            if (images[i].naturalWidth > 0) {
-              numLoaded += 1;
-            }
-          }
-          callback(numLoaded);
-        })
-        .then(function (numLoaded) {
-          expect(numLoaded).toBe(2);
-        });
-
-      await element(by.buttonText('Flip')).click();
-    }
-  });
-
-  it('should: submit note scouting for multiple teams', async () => {
-    // Navigate to Notes Page.
-    await loadPage();
-    await element(by.cssContainingText('.nav-link', 'Notes')).click();
-    expect(await getHeadingText()).toEqual('Notes');
-
-    // Add first team.
-    await setTextboxByIdTo('team_number_notes', '1234');
-    await element(by.buttonText('Select')).click();
-
-    // Add note and select keyword for first team.
-    expect(await element(by.id('team-key-1')).getText()).toEqual('1234');
-    await element(by.id('text-input-1')).sendKeys('Good Driving');
-    await element(by.id('good_driving_0')).click();
-
-    // Navigate to add team selection and add another team.
-    await element(by.id('add-team-button')).click();
-    await setTextboxByIdTo('team_number_notes', '1235');
-    await element(by.buttonText('Select')).click();
-
-    // Add note and select keyword for second team.
-    expect(await element(by.id('team-key-2')).getText()).toEqual('1235');
-    await element(by.id('text-input-2')).sendKeys('Bad Driving');
-    await element(by.id('bad_driving_1')).click();
-
-    // Submit Notes.
-    await element(by.buttonText('Submit')).click();
-    expect(await element(by.id('team_number_label')).getText()).toEqual(
-      'Team Number'
-    );
-  });
-
-  it('should: switch note text boxes with keyboard shortcuts', async () => {
-    // Navigate to Notes Page.
-    await loadPage();
-    await element(by.cssContainingText('.nav-link', 'Notes')).click();
-    expect(await getHeadingText()).toEqual('Notes');
-
-    // Add first team.
-    await setTextboxByIdTo('team_number_notes', '1234');
-    await element(by.buttonText('Select')).click();
-
-    // Add second team.
-    await element(by.id('add-team-button')).click();
-    await setTextboxByIdTo('team_number_notes', '1235');
-    await element(by.buttonText('Select')).click();
-
-    // Add third team.
-    await element(by.id('add-team-button')).click();
-    await setTextboxByIdTo('team_number_notes', '1236');
-    await element(by.buttonText('Select')).click();
-
-    for (let i = 1; i <= 3; i++) {
-      // Press Control + i
-      // Keyup Control for future actions.
-      browser
-        .actions()
-        .keyDown(protractor.Key.CONTROL)
-        .sendKeys(i.toString())
-        .keyUp(protractor.Key.CONTROL)
-        .perform();
-
-      // Expect text input to be focused.
-      expect(
-        await browser.driver.switchTo().activeElement().getAttribute('id')
-      ).toEqual('text-input-' + i);
-    }
-  });
-  it('should: submit driver ranking', async () => {
-    // Navigate to Driver Ranking Page.
-    await loadPage();
-    await element(by.cssContainingText('.nav-link', 'Driver Ranking')).click();
-    expect(await getHeadingText()).toEqual('Driver Ranking');
-
-    // Input match and team numbers.
-    await setTextboxByIdTo('match_number_selection', '11');
-    await setTextboxByIdTo('team_input_0', '123');
-    await setTextboxByIdTo('team_input_1', '456');
-    await setTextboxByIdTo('team_input_2', '789');
-    await element(by.id('select_button')).click();
-
-    // Verify match and team key input.
-    expect(await element(by.id('match_number_heading')).getText()).toEqual(
-      'Match #11'
-    );
-    expect(await element(by.id('team_key_label_0')).getText()).toEqual('123');
-    expect(await element(by.id('team_key_label_1')).getText()).toEqual('456');
-    expect(await element(by.id('team_key_label_2')).getText()).toEqual('789');
-
-    // Rank teams.
-    await element(by.id('up_button_2')).click();
-    await element(by.id('down_button_0')).click();
-
-    // Verify ranking change.
-    expect(await element(by.id('team_key_label_0')).getText()).toEqual('789');
-    expect(await element(by.id('team_key_label_1')).getText()).toEqual('123');
-    expect(await element(by.id('team_key_label_2')).getText()).toEqual('456');
-
-    // Submit.
-    await element(by.id('submit_button')).click();
-  });
-});
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index ee0659b..cbcecc3 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -1,134 +1,47 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-load("//tools/build_rules:js.bzl", "rollup_bundle")
-load("@npm//@babel/cli:index.bzl", "babel")
+load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_application")
+load(":defs.bzl", "assemble_static_files")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_application(
     name = "app",
-    srcs = glob([
-        "*.ts",
-    ]),
-    angular_assets = glob([
-        "*.ng.html",
-        "*.css",
-    ]),
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
+    extra_srcs = [
+        "app/common.css",
+    ],
+    html_assets = [
+        "favicon.ico",
+    ],
     deps = [
-        "//scouting/www/driver_ranking",
-        "//scouting/www/entry",
-        "//scouting/www/import_match_list",
-        "//scouting/www/match_list",
-        "//scouting/www/notes",
-        "//scouting/www/shift_schedule",
-        "//scouting/www/view",
-        "@npm//@angular/animations",
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-        "@npm//@angular/platform-browser",
+        "//:node_modules/@angular/animations",
+        "//scouting/www/driver_ranking:_lib",
+        "//scouting/www/entry:_lib",
+        "//scouting/www/import_match_list:_lib",
+        "//scouting/www/match_list:_lib",
+        "//scouting/www/notes:_lib",
+        "//scouting/www/shift_schedule:_lib",
+        "//scouting/www/view:_lib",
     ],
 )
 
-rollup_bundle(
-    name = "main_bundle",
-    entry_point = "main.ts",
-    deps = [
-        "app",
-    ],
-)
-
-babel(
-    name = "main_bundle_compiled",
-    args = [
-        "$(execpath :main_bundle.min.js)",
-        "--no-babelrc",
-        "--source-maps",
-        "--minified",
-        "--no-comments",
-        "--plugins=@angular/compiler-cli/linker/babel",
-        "--out-dir",
-        "$(@D)",
-    ],
-    data = [
-        ":main_bundle.min.js",
-        "@npm//@angular/compiler-cli",
-    ],
-    output_dir = True,
-)
-
-# The babel() rule above puts everything into a directory without telling bazel
-# what's in the directory. That makes it annoying to work with from other
-# rules. This genrule() here copies the one file in the directory we care about
-# so that other rules have an easier time using the file.
-genrule(
-    name = "main_bundle_file",
-    srcs = [":main_bundle_compiled"],
-    outs = ["main_bundle_file.js"],
-    cmd = "cp $(location :main_bundle_compiled)/main_bundle.min.js $(OUTS)",
-)
-
-py_binary(
-    name = "index_html_generator",
-    srcs = ["index_html_generator.py"],
-)
-
-genrule(
-    name = "generate_index_html",
-    srcs = [
-        "index.template.html",
-        "main_bundle_file.js",
-    ],
-    outs = ["index.html"],
-    cmd = " ".join([
-        "$(location :index_html_generator)",
-        "--template $(location index.template.html)",
-        "--bundle $(location main_bundle_file.js)",
-        "--output $(location index.html)",
-    ]),
-    tools = [
-        ":index_html_generator",
-    ],
-)
-
-# Create a copy of zone.js here so that we can have a predictable path to
-# source it from on the webserver.
-genrule(
-    name = "zonejs_copy",
-    srcs = [
-        "@npm//:node_modules/zone.js/dist/zone.min.js",
-    ],
-    outs = [
-        "npm/node_modules/zone.js/dist/zone.min.js",
-    ],
-    cmd = "cp $(SRCS) $(OUTS)",
-)
-
-genrule(
-    name = "field_pictures_copy",
-    srcs = ["//third_party/y2022/field:pictures"],
-    outs = [
-        "pictures/field/balls.jpeg",
-        "pictures/field/quadrants.jpeg",
-        "pictures/field/reversed_quadrants.jpeg",
-        "pictures/field/reversed_balls.jpeg",
-    ],
-    cmd = "cp $(SRCS) $(@D)/pictures/field/",
-)
-
-filegroup(
+assemble_static_files(
     name = "static_files",
-    srcs = [
-        "index.html",
-        ":field_pictures_copy",
-        ":main_bundle_file.js",
-        ":zonejs_copy",
+    app_files = ":app",
+    pictures = [
+        "//third_party/y2022/field:pictures",
     ],
+    replace_prefixes = {
+        "prod": "",
+        "dev": "",
+        "third_party/y2022": "pictures",
+    },
     visibility = ["//visibility:public"],
 )
 
-filegroup(
-    name = "common_css",
-    srcs = ["common.css"],
+copy_file(
+    name = "app_common_css",
+    src = "common.css",
+    out = "app/common.css",
     visibility = ["//scouting/www:__subpackages__"],
 )
diff --git a/scouting/www/app/app.module.ts b/scouting/www/app/app.module.ts
new file mode 100644
index 0000000..ead8b37
--- /dev/null
+++ b/scouting/www/app/app.module.ts
@@ -0,0 +1,30 @@
+import {NgModule} from '@angular/core';
+import {BrowserModule} from '@angular/platform-browser';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+import {App} from './app';
+import {EntryModule} from '../entry';
+import {ImportMatchListModule} from '../import_match_list';
+import {MatchListModule} from '../match_list';
+import {NotesModule} from '../notes';
+import {ShiftScheduleModule} from '../shift_schedule';
+import {ViewModule} from '../view';
+import {DriverRankingModule} from '../driver_ranking';
+
+@NgModule({
+  declarations: [App],
+  imports: [
+    BrowserModule,
+    BrowserAnimationsModule,
+    EntryModule,
+    NotesModule,
+    ImportMatchListModule,
+    MatchListModule,
+    ShiftScheduleModule,
+    DriverRankingModule,
+    ViewModule,
+  ],
+  exports: [App],
+  bootstrap: [App],
+})
+export class AppModule {}
diff --git a/scouting/www/app.ng.html b/scouting/www/app/app.ng.html
similarity index 100%
rename from scouting/www/app.ng.html
rename to scouting/www/app/app.ng.html
diff --git a/scouting/www/app.ts b/scouting/www/app/app.ts
similarity index 97%
rename from scouting/www/app.ts
rename to scouting/www/app/app.ts
index b26f815..7e81d84 100644
--- a/scouting/www/app.ts
+++ b/scouting/www/app/app.ts
@@ -22,7 +22,7 @@
 @Component({
   selector: 'my-app',
   templateUrl: './app.ng.html',
-  styleUrls: ['./common.css'],
+  styleUrls: ['../app/common.css'],
 })
 export class App {
   selectedTeamInMatch: TeamInMatch = {
diff --git a/scouting/www/app_module.ts b/scouting/www/app_module.ts
deleted file mode 100644
index 04d72b3..0000000
--- a/scouting/www/app_module.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import {NgModule} from '@angular/core';
-import {BrowserModule} from '@angular/platform-browser';
-import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
-
-import {App} from './app';
-import {EntryModule} from './entry/entry.module';
-import {ImportMatchListModule} from './import_match_list/import_match_list.module';
-import {MatchListModule} from './match_list/match_list.module';
-import {NotesModule} from './notes/notes.module';
-import {ShiftScheduleModule} from './shift_schedule/shift_schedule.module';
-import {ViewModule} from './view/view.module';
-import {DriverRankingModule} from './driver_ranking/driver_ranking.module';
-
-@NgModule({
-  declarations: [App],
-  imports: [
-    BrowserModule,
-    BrowserAnimationsModule,
-    EntryModule,
-    NotesModule,
-    ImportMatchListModule,
-    MatchListModule,
-    ShiftScheduleModule,
-    DriverRankingModule,
-    ViewModule,
-  ],
-  exports: [App],
-  bootstrap: [App],
-})
-export class AppModule {}
diff --git a/scouting/www/counter_button/BUILD b/scouting/www/counter_button/BUILD
index 1dbcdfc..d081f9d 100644
--- a/scouting/www/counter_button/BUILD
+++ b/scouting/www/counter_button/BUILD
@@ -1,21 +1,8 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "counter_button",
-    srcs = [
-        "counter_button.component.ts",
-        "counter_button.module.ts",
-    ],
-    angular_assets = [
-        "counter_button.component.css",
-        "counter_button.ng.html",
-    ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
-    deps = [
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-    ],
 )
diff --git a/scouting/www/defs.bzl b/scouting/www/defs.bzl
new file mode 100644
index 0000000..828f30a
--- /dev/null
+++ b/scouting/www/defs.bzl
@@ -0,0 +1,36 @@
+load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory_bin_action")
+
+def _assemble_static_files_impl(ctx):
+    out_dir = ctx.actions.declare_directory(ctx.label.name)
+
+    copy_to_directory_bin = ctx.toolchains["@aspect_bazel_lib//lib:copy_to_directory_toolchain_type"].copy_to_directory_info.bin
+
+    copy_to_directory_bin_action(
+        ctx,
+        dst = out_dir,
+        name = ctx.label.name,
+        copy_to_directory_bin = copy_to_directory_bin,
+        files = ctx.files.pictures + ctx.attr.app_files.files.to_list(),
+        replace_prefixes = ctx.attr.replace_prefixes,
+    )
+
+    return [DefaultInfo(
+        files = depset([out_dir]),
+        runfiles = ctx.runfiles([out_dir]),
+    )]
+
+assemble_static_files = rule(
+    implementation = _assemble_static_files_impl,
+    attrs = {
+        "app_files": attr.label(
+            mandatory = True,
+        ),
+        "pictures": attr.label_list(
+            mandatory = True,
+        ),
+        "replace_prefixes": attr.string_dict(
+            mandatory = True,
+        ),
+    },
+    toolchains = ["@aspect_bazel_lib//lib:copy_to_directory_toolchain_type"],
+)
diff --git a/scouting/www/driver_ranking/BUILD b/scouting/www/driver_ranking/BUILD
index 10b6f99..a934ffe 100644
--- a/scouting/www/driver_ranking/BUILD
+++ b/scouting/www/driver_ranking/BUILD
@@ -1,26 +1,17 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "driver_ranking",
-    srcs = [
-        "driver_ranking.component.ts",
-        "driver_ranking.module.ts",
+    extra_srcs = [
+        "//scouting/www:app_common_css",
     ],
-    angular_assets = [
-        "driver_ranking.component.css",
-        "driver_ranking.ng.html",
-        "//scouting/www:common_css",
-    ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
     deps = [
+        ":node_modules/@angular/forms",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_ts_fbs",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-        "@npm//@angular/forms",
     ],
 )
diff --git a/scouting/www/driver_ranking/driver_ranking.component.ts b/scouting/www/driver_ranking/driver_ranking.component.ts
index b251938..f61d901 100644
--- a/scouting/www/driver_ranking/driver_ranking.component.ts
+++ b/scouting/www/driver_ranking/driver_ranking.component.ts
@@ -12,7 +12,7 @@
 @Component({
   selector: 'app-driver-ranking',
   templateUrl: './driver_ranking.ng.html',
-  styleUrls: ['../common.css', './driver_ranking.component.css'],
+  styleUrls: ['../app/common.css', './driver_ranking.component.css'],
 })
 export class DriverRankingComponent {
   section: Section = 'TeamSelection';
diff --git a/scouting/www/driver_ranking/package.json b/scouting/www/driver_ranking/package.json
index 06472cf..83dacf4 100644
--- a/scouting/www/driver_ranking/package.json
+++ b/scouting/www/driver_ranking/package.json
@@ -2,6 +2,6 @@
     "name": "@org_frc971/scouting/www/driver_ranking",
     "private": true,
     "dependencies": {
-        "@angular/forms": "15.0.1"
+        "@angular/forms": "15.1.5"
     }
 }
diff --git a/scouting/www/entry/BUILD b/scouting/www/entry/BUILD
index 17eed23..37b7fe6 100644
--- a/scouting/www/entry/BUILD
+++ b/scouting/www/entry/BUILD
@@ -1,28 +1,19 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "entry",
-    srcs = [
-        "entry.component.ts",
-        "entry.module.ts",
+    extra_srcs = [
+        "//scouting/www:app_common_css",
     ],
-    angular_assets = [
-        "entry.component.css",
-        "entry.ng.html",
-        "//scouting/www:common_css",
-    ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
     deps = [
+        ":node_modules/@angular/forms",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_ts_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_ts_fbs",
-        "//scouting/www/counter_button",
+        "//scouting/www/counter_button:_lib",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-        "@npm//@angular/forms",
     ],
 )
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 01c9fff..6209669 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -58,7 +58,7 @@
 @Component({
   selector: 'app-entry',
   templateUrl: './entry.ng.html',
-  styleUrls: ['../common.css', './entry.component.css'],
+  styleUrls: ['../app/common.css', './entry.component.css'],
 })
 export class EntryComponent {
   // Re-export the type here so that we can use it in the `[value]` attribute
diff --git a/scouting/www/entry/entry.module.ts b/scouting/www/entry/entry.module.ts
index 3322c01..a2aa7bb 100644
--- a/scouting/www/entry/entry.module.ts
+++ b/scouting/www/entry/entry.module.ts
@@ -2,7 +2,7 @@
 import {CommonModule} from '@angular/common';
 import {FormsModule} from '@angular/forms';
 
-import {CounterButtonModule} from '../counter_button/counter_button.module';
+import {CounterButtonModule} from '@org_frc971/scouting/www/counter_button';
 import {EntryComponent} from './entry.component';
 
 import {ClimbLevel} from '../../webserver/requests/messages/submit_data_scouting_generated';
diff --git a/scouting/www/entry/package.json b/scouting/www/entry/package.json
index 5ecf57a..4c05778 100644
--- a/scouting/www/entry/package.json
+++ b/scouting/www/entry/package.json
@@ -3,6 +3,6 @@
     "private": true,
     "dependencies": {
         "@org_frc971/scouting/www/counter_button": "workspace:*",
-        "@angular/forms": "15.0.1"
+        "@angular/forms": "15.1.5"
     }
 }
diff --git a/scouting/www/favicon.ico b/scouting/www/favicon.ico
new file mode 100644
index 0000000..2bf69ea
--- /dev/null
+++ b/scouting/www/favicon.ico
Binary files differ
diff --git a/scouting/www/import_match_list/BUILD b/scouting/www/import_match_list/BUILD
index 9e40794..bc1d5d5 100644
--- a/scouting/www/import_match_list/BUILD
+++ b/scouting/www/import_match_list/BUILD
@@ -1,27 +1,18 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "import_match_list",
-    srcs = [
-        "import_match_list.component.ts",
-        "import_match_list.module.ts",
+    extra_srcs = [
+        "//scouting/www:app_common_css",
     ],
-    angular_assets = [
-        "import_match_list.component.css",
-        "import_match_list.ng.html",
-        "//scouting/www:common_css",
-    ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
     deps = [
+        ":node_modules/@angular/forms",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:refresh_match_list_response_ts_fbs",
         "//scouting/webserver/requests/messages:refresh_match_list_ts_fbs",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-        "@npm//@angular/forms",
     ],
 )
diff --git a/scouting/www/import_match_list/import_match_list.component.ts b/scouting/www/import_match_list/import_match_list.component.ts
index 0e292ae..526a636 100644
--- a/scouting/www/import_match_list/import_match_list.component.ts
+++ b/scouting/www/import_match_list/import_match_list.component.ts
@@ -8,7 +8,7 @@
 @Component({
   selector: 'app-import-match-list',
   templateUrl: './import_match_list.ng.html',
-  styleUrls: ['../common.css', './import_match_list.component.css'],
+  styleUrls: ['../app/common.css', './import_match_list.component.css'],
 })
 export class ImportMatchListComponent {
   year: number = new Date().getFullYear();
diff --git a/scouting/www/import_match_list/package.json b/scouting/www/import_match_list/package.json
index d80b0dc..05aa790 100644
--- a/scouting/www/import_match_list/package.json
+++ b/scouting/www/import_match_list/package.json
@@ -2,6 +2,6 @@
     "name": "@org_frc971/scouting/www/import_match_list",
     "private": true,
     "dependencies": {
-        "@angular/forms": "15.0.1"
+        "@angular/forms": "15.1.5"
     }
 }
diff --git a/scouting/www/index.template.html b/scouting/www/index.html
similarity index 74%
rename from scouting/www/index.template.html
rename to scouting/www/index.html
index 8fd0d7c..208141a 100644
--- a/scouting/www/index.template.html
+++ b/scouting/www/index.html
@@ -1,10 +1,9 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
   <head>
+    <meta charset="utf-8" />
     <title>FRC971 Scouting Application</title>
     <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <base href="/" />
-    <script src="./npm/node_modules/zone.js/dist/zone.min.js"></script>
     <link
       href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
       rel="stylesheet"
@@ -19,7 +18,5 @@
   </head>
   <body>
     <my-app></my-app>
-    <!-- The path here is auto-generated to be /sha256/<checksum>/main_bundle_file.js. -->
-    <script src="{MAIN_BUNDLE_FILE}"></script>
   </body>
 </html>
diff --git a/scouting/www/index_html_generator.py b/scouting/www/index_html_generator.py
deleted file mode 100644
index bc0e63d..0000000
--- a/scouting/www/index_html_generator.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Generates index.html with the right checksum for main_bundle_file.js filled in."""
-
-import argparse
-import hashlib
-import sys
-from pathlib import Path
-
-
-def compute_sha256(filepath):
-    return hashlib.sha256(filepath.read_bytes()).hexdigest()
-
-
-def main(argv):
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--template", type=str)
-    parser.add_argument("--bundle", type=str)
-    parser.add_argument("--output", type=str)
-    args = parser.parse_args(argv[1:])
-
-    template = Path(args.template).read_text()
-    bundle_path = Path(args.bundle)
-    bundle_sha256 = compute_sha256(bundle_path)
-
-    output = template.format(
-        MAIN_BUNDLE_FILE=f"/sha256/{bundle_sha256}/{bundle_path.name}", )
-    Path(args.output).write_text(output)
-
-
-if __name__ == "__main__":
-    sys.exit(main(sys.argv))
diff --git a/scouting/www/main.ts b/scouting/www/main.ts
index e1a4ab5..1f8eb00 100644
--- a/scouting/www/main.ts
+++ b/scouting/www/main.ts
@@ -1,4 +1,4 @@
 import {platformBrowser} from '@angular/platform-browser';
-import {AppModule} from './app_module';
+import {AppModule} from './app/app.module';
 
 platformBrowser().bootstrapModule(AppModule);
diff --git a/scouting/www/match_list/BUILD b/scouting/www/match_list/BUILD
index 10c0a22..c713dda 100644
--- a/scouting/www/match_list/BUILD
+++ b/scouting/www/match_list/BUILD
@@ -1,28 +1,19 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "match_list",
-    srcs = [
-        "match_list.component.ts",
-        "match_list.module.ts",
+    extra_srcs = [
+        "//scouting/www:app_common_css",
     ],
-    angular_assets = [
-        "match_list.component.css",
-        "match_list.ng.html",
-        "//scouting/www:common_css",
-    ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
     deps = [
+        ":node_modules/@angular/forms",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
-        "//scouting/www/rpc",
+        "//scouting/www/rpc:_lib",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-        "@npm//@angular/forms",
     ],
 )
diff --git a/scouting/www/match_list/match_list.component.ts b/scouting/www/match_list/match_list.component.ts
index c50cffd..eb5284f 100644
--- a/scouting/www/match_list/match_list.component.ts
+++ b/scouting/www/match_list/match_list.component.ts
@@ -7,7 +7,7 @@
   RequestAllMatchesResponse,
 } from '../../webserver/requests/messages/request_all_matches_response_generated';
 
-import {MatchListRequestor} from '../rpc/match_list_requestor';
+import {MatchListRequestor} from '@org_frc971/scouting/www/rpc';
 
 type TeamInMatch = {
   teamNumber: number;
@@ -19,7 +19,7 @@
 @Component({
   selector: 'app-match-list',
   templateUrl: './match_list.ng.html',
-  styleUrls: ['../common.css', './match_list.component.css'],
+  styleUrls: ['../app/common.css', './match_list.component.css'],
 })
 export class MatchListComponent implements OnInit {
   @Output() selectedTeamEvent = new EventEmitter<TeamInMatch>();
diff --git a/scouting/www/match_list/package.json b/scouting/www/match_list/package.json
index 3a02094..284e77b 100644
--- a/scouting/www/match_list/package.json
+++ b/scouting/www/match_list/package.json
@@ -3,6 +3,6 @@
     "private": true,
     "dependencies": {
         "@org_frc971/scouting/www/rpc": "workspace:*",
-        "@angular/forms": "15.0.1"
+        "@angular/forms": "15.1.5"
     }
 }
diff --git a/scouting/www/notes/BUILD b/scouting/www/notes/BUILD
index 39a97ef..64e4ae7 100644
--- a/scouting/www/notes/BUILD
+++ b/scouting/www/notes/BUILD
@@ -1,29 +1,20 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "notes",
-    srcs = [
-        "notes.component.ts",
-        "notes.module.ts",
+    extra_srcs = [
+        "//scouting/www:app_common_css",
     ],
-    angular_assets = [
-        "notes.component.css",
-        "notes.ng.html",
-        "//scouting/www:common_css",
-    ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
     deps = [
+        ":node_modules/@angular/forms",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_notes_for_team_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_notes_for_team_ts_fbs",
         "//scouting/webserver/requests/messages:submit_notes_response_ts_fbs",
         "//scouting/webserver/requests/messages:submit_notes_ts_fbs",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-        "@npm//@angular/forms",
     ],
 )
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 177ad44..62b4990 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -64,7 +64,7 @@
 @Component({
   selector: 'frc971-notes',
   templateUrl: './notes.ng.html',
-  styleUrls: ['../common.css', './notes.component.css'],
+  styleUrls: ['../app/common.css', './notes.component.css'],
 })
 export class Notes {
   // Re-export KEYWORD_CHECKBOX_LABELS so that we can
diff --git a/scouting/www/notes/package.json b/scouting/www/notes/package.json
index 8cbeb94..c5c6afe 100644
--- a/scouting/www/notes/package.json
+++ b/scouting/www/notes/package.json
@@ -2,6 +2,6 @@
     "name": "@org_frc971/scouting/www/notes",
     "private": true,
     "dependencies": {
-        "@angular/forms": "15.0.1"
+        "@angular/forms": "15.1.5"
     }
 }
diff --git a/scouting/www/polyfills.ts b/scouting/www/polyfills.ts
new file mode 100644
index 0000000..e4555ed
--- /dev/null
+++ b/scouting/www/polyfills.ts
@@ -0,0 +1,52 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ *      file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes recent versions of Safari, Chrome (including
+ * Opera), Edge on the desktop, and iOS and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ * because those flags need to be set before `zone.js` being loaded, and webpack
+ * will put import in the top of bundle, so user need to create a separate file
+ * in this directory (for example: zone-flags.ts), and put the following flags
+ * into that file, and then add the following code before importing zone.js.
+ * import './zone-flags';
+ *
+ * The flags allowed in zone-flags.ts are listed here.
+ *
+ * The following flags will work for all browsers.
+ *
+ * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+ *
+ *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ *  with the following flag, it will bypass `zone.js` patch for IE/Edge
+ *
+ *  (window as any).__Zone_enable_cross_context_check = true;
+ *
+ */
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js'; // Included with Angular CLI.
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/scouting/www/rpc/BUILD b/scouting/www/rpc/BUILD
index 954b3fb..427b493 100644
--- a/scouting/www/rpc/BUILD
+++ b/scouting/www/rpc/BUILD
@@ -1,15 +1,14 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "rpc",
-    srcs = [
-        "match_list_requestor.ts",
-        "view_data_requestor.ts",
+    extra_srcs = [
+        "public-api.ts",
     ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
+    generate_public_api = False,
     deps = [
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_ts_fbs",
@@ -21,6 +20,5 @@
         "//scouting/webserver/requests/messages:request_data_scouting_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_data_scouting_ts_fbs",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-        "@npm//@angular/core",
     ],
 )
diff --git a/scouting/www/rpc/public-api.ts b/scouting/www/rpc/public-api.ts
new file mode 100644
index 0000000..fe61453
--- /dev/null
+++ b/scouting/www/rpc/public-api.ts
@@ -0,0 +1,2 @@
+export * from './match_list_requestor';
+export * from './view_data_requestor';
diff --git a/scouting/www/shift_schedule/BUILD b/scouting/www/shift_schedule/BUILD
index 8fe99e4..3afb557 100644
--- a/scouting/www/shift_schedule/BUILD
+++ b/scouting/www/shift_schedule/BUILD
@@ -1,27 +1,18 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "shift_schedule",
-    srcs = [
-        "shift_schedule.component.ts",
-        "shift_schedule.module.ts",
+    extra_srcs = [
+        "//scouting/www:app_common_css",
     ],
-    angular_assets = [
-        "shift_schedule.component.css",
-        "shift_schedule.ng.html",
-        "//scouting/www:common_css",
-    ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
     deps = [
+        ":node_modules/@angular/forms",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-        "@npm//@angular/forms",
     ],
 )
diff --git a/scouting/www/shift_schedule/package.json b/scouting/www/shift_schedule/package.json
index 1d71623..1270235 100644
--- a/scouting/www/shift_schedule/package.json
+++ b/scouting/www/shift_schedule/package.json
@@ -2,6 +2,6 @@
     "name": "@org_frc971/scouting/www/shift_schedule",
     "private": true,
     "dependencies": {
-        "@angular/forms": "15.0.1"
+        "@angular/forms": "15.1.5"
     }
 }
diff --git a/scouting/www/shift_schedule/shift_schedule.component.ts b/scouting/www/shift_schedule/shift_schedule.component.ts
index 074eb97..de0b2e1 100644
--- a/scouting/www/shift_schedule/shift_schedule.component.ts
+++ b/scouting/www/shift_schedule/shift_schedule.component.ts
@@ -5,7 +5,7 @@
 @Component({
   selector: 'shift-schedule',
   templateUrl: './shift_schedule.ng.html',
-  styleUrls: ['../common.css', './shift_schedule.component.css'],
+  styleUrls: ['../app/common.css', './shift_schedule.component.css'],
 })
 export class ShiftsComponent {
   progressMessage: string = '';
diff --git a/scouting/www/view/BUILD b/scouting/www/view/BUILD
index b9b6030..168feb6 100644
--- a/scouting/www/view/BUILD
+++ b/scouting/www/view/BUILD
@@ -1,21 +1,15 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
 
-ts_library(
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
     name = "view",
-    srcs = [
-        "view.component.ts",
-        "view.module.ts",
+    extra_srcs = [
+        "//scouting/www:app_common_css",
     ],
-    angular_assets = [
-        "view.component.css",
-        "view.ng.html",
-        "//scouting/www:common_css",
-    ],
-    compiler = "//tools:tsc_wrapped_with_angular",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    use_angular_plugin = True,
-    visibility = ["//visibility:public"],
     deps = [
+        ":node_modules/@angular/forms",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_ts_fbs",
@@ -23,10 +17,7 @@
         "//scouting/webserver/requests/messages:request_all_notes_ts_fbs",
         "//scouting/webserver/requests/messages:request_data_scouting_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_data_scouting_ts_fbs",
-        "//scouting/www/rpc",
+        "//scouting/www/rpc:_lib",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-        "@npm//@angular/common",
-        "@npm//@angular/core",
-        "@npm//@angular/forms",
     ],
 )
diff --git a/scouting/www/view/package.json b/scouting/www/view/package.json
new file mode 100644
index 0000000..ef94a11
--- /dev/null
+++ b/scouting/www/view/package.json
@@ -0,0 +1,7 @@
+{
+    "name": "@org_frc971/scouting/www/view",
+    "private": true,
+    "dependencies": {
+      "@angular/forms": "15.1.5"
+    }
+}
diff --git a/scouting/www/view/view.component.ts b/scouting/www/view/view.component.ts
index 593b0f4..c877faf 100644
--- a/scouting/www/view/view.component.ts
+++ b/scouting/www/view/view.component.ts
@@ -12,7 +12,7 @@
   RequestAllNotesResponse,
 } from '../../webserver/requests/messages/request_all_notes_response_generated';
 
-import {ViewDataRequestor} from '../rpc/view_data_requestor';
+import {ViewDataRequestor} from '../rpc';
 
 type Source = 'Notes' | 'Stats' | 'DriverRanking';
 
@@ -28,7 +28,7 @@
 @Component({
   selector: 'app-view',
   templateUrl: './view.ng.html',
-  styleUrls: ['../common.css', './view.component.css'],
+  styleUrls: ['../app/common.css', './view.component.css'],
 })
 export class ViewComponent {
   constructor(private readonly viewDataRequestor: ViewDataRequestor) {}
diff --git a/third_party/flatbuffers/BUILD.bazel b/third_party/flatbuffers/BUILD.bazel
index 6f92eec..657ec86 100644
--- a/third_party/flatbuffers/BUILD.bazel
+++ b/third_party/flatbuffers/BUILD.bazel
@@ -1,4 +1,5 @@
 load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
+load("@aspect_rules_js//npm:defs.bzl", "npm_link_package")
 
 licenses(["notice"])
 
@@ -6,6 +7,16 @@
     default_visibility = ["//visibility:public"],
 )
 
+npm_link_package(
+    name = "node_modules/flatbuffers",
+    src = "@com_github_google_flatbuffers//ts:flatbuffers",
+)
+
+npm_link_package(
+    name = "node_modules/flatbuffers_reflection",
+    src = "@com_github_google_flatbuffers//reflection:flatbuffers_reflection",
+)
+
 exports_files([
     "LICENSE",
     "tsconfig.json",
diff --git a/third_party/flatbuffers/build_defs.bzl b/third_party/flatbuffers/build_defs.bzl
index 90eb77c..c1e568f 100644
--- a/third_party/flatbuffers/build_defs.bzl
+++ b/third_party/flatbuffers/build_defs.bzl
@@ -8,8 +8,7 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library")
 load("@rules_rust//rust:defs.bzl", "rust_library")
 load("@rules_rust//rust:rust_common.bzl", "CrateInfo")
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@npm//@bazel/typescript:index.bzl", "ts_project")
+load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
 load("@rules_cc//cc:defs.bzl", "cc_library")
 
 flatc_path = "@com_github_google_flatbuffers//:flatc"
diff --git a/third_party/flatbuffers/reflection/BUILD.bazel b/third_party/flatbuffers/reflection/BUILD.bazel
index 6e50dc4..1d58bb3 100644
--- a/third_party/flatbuffers/reflection/BUILD.bazel
+++ b/third_party/flatbuffers/reflection/BUILD.bazel
@@ -1,3 +1,4 @@
+load("@aspect_rules_js//npm:defs.bzl", "npm_package")
 load("//:build_defs.bzl", "flatbuffer_rust_library")
 load("//:typescript.bzl", "flatbuffer_ts_library")
 
@@ -9,12 +10,19 @@
 
 flatbuffer_ts_library(
     name = "reflection_ts_fbs",
-    package_name = "flatbuffers_reflection",
     srcs = ["reflection.fbs"],
     include_reflection = False,
     visibility = ["//visibility:public"],
 )
 
+npm_package(
+    name = "flatbuffers_reflection",
+    srcs = [":reflection_ts_fbs_ts"],
+    include_external_repositories = ["*"],
+    package = "flatbuffers_reflection",
+    visibility = ["//visibility:public"],
+)
+
 flatbuffer_rust_library(
     name = "reflection_rust_fbs",
     srcs = ["reflection.fbs"],
diff --git a/third_party/flatbuffers/ts/BUILD.bazel b/third_party/flatbuffers/ts/BUILD.bazel
index 6e8a10b..5f78892 100644
--- a/third_party/flatbuffers/ts/BUILD.bazel
+++ b/third_party/flatbuffers/ts/BUILD.bazel
@@ -1,5 +1,6 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_project")
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
+load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
+load("@aspect_rules_js//js:defs.bzl", "js_library")
+load("@aspect_rules_js//npm:defs.bzl", "npm_package")
 
 ts_project(
     name = "flatbuffers_ts",
@@ -28,12 +29,13 @@
         },
     },
     visibility = ["//visibility:public"],
-    deps = ["@npm//@types/node"],
+    deps = ["@//:node_modules/@types/node"],
 )
 
-js_library(
+npm_package(
     name = "flatbuffers",
-    package_name = "flatbuffers",
+    srcs = [":flatbuffers_ts"],
+    include_external_repositories = ["*"],
+    package = "flatbuffers",
     visibility = ["//visibility:public"],
-    deps = [":flatbuffers_ts"],
 )
diff --git a/third_party/flatbuffers/typescript.bzl b/third_party/flatbuffers/typescript.bzl
index 39c1e04..f00c550 100644
--- a/third_party/flatbuffers/typescript.bzl
+++ b/third_party/flatbuffers/typescript.bzl
@@ -2,8 +2,8 @@
 Rules for building typescript flatbuffers with Bazel.
 """
 
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@npm//@bazel/typescript:index.bzl", "ts_project")
+load("@aspect_rules_js//js:defs.bzl", "js_library")
+load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
 load(":build_defs.bzl", "DEFAULT_INCLUDE_PATHS", "flatbuffer_library_public")
 
 DEFAULT_FLATC_TS_ARGS = [
@@ -93,6 +93,7 @@
         compatible_with = compatible_with,
         restricted_to = restricted_to,
         target_compatible_with = target_compatible_with,
+        supports_workers = False,
         tsconfig = {
             "compilerOptions": {
                 "declaration": True,
@@ -107,7 +108,12 @@
                 "types": ["node"],
             },
         },
-        deps = deps + ["@com_github_google_flatbuffers//ts:flatbuffers"] + (["@com_github_google_flatbuffers//reflection:reflection_ts_fbs"] if include_reflection else []),
+        deps = deps + [
+            "@//:node_modules/flatbuffers",
+            # TODO(phil): Figure out why @types/node isn't being picked up as a
+            # transitivie dependencies.
+            "@//:node_modules/@types/node",
+        ] + (["@//:node_modules/flatbuffers_reflection"] if include_reflection else []),
     )
     js_library(
         name = name,
@@ -115,8 +121,7 @@
         compatible_with = compatible_with,
         restricted_to = restricted_to,
         target_compatible_with = target_compatible_with,
-        deps = [name + "_ts"],
-        package_name = package_name,
+        srcs = [name + "_ts"],
     )
     native.filegroup(
         name = "%s_includes" % (name),
diff --git a/third_party/rules_rollup/0001-Fix-resolving-files.patch b/third_party/rules_rollup/0001-Fix-resolving-files.patch
new file mode 100644
index 0000000..a81a656
--- /dev/null
+++ b/third_party/rules_rollup/0001-Fix-resolving-files.patch
@@ -0,0 +1,24 @@
+From fd6dd080ea58fd71c70ce2303873feab1abda760 Mon Sep 17 00:00:00 2001
+From: Philipp Schrader <philipp.schrader@gmail.com>
+Date: Sun, 19 Feb 2023 14:18:11 -0800
+Subject: [PATCH] Fix resolving files
+
+I don't really know what the underlying problem is, but returning a
+File instead of a path is causing us grief.
+---
+ rollup/private/rollup_bundle.bzl | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/rollup/private/rollup_bundle.bzl b/rollup/private/rollup_bundle.bzl
+index 32aaad4..a2061dd 100644
+--- a/rollup/private/rollup_bundle.bzl
++++ b/rollup/private/rollup_bundle.bzl
+@@ -186,7 +186,7 @@ def _resolve_js_input(f, inputs):
+     for i in inputs:
+         if i.extension == "js" or i.extension == "mjs":
+             if _no_ext(i) == no_ext:
+-                return i
++                return i.short_path
+     fail("Could not find corresponding javascript entry point for %s. Add the %s.js to your deps." % (f.path, no_ext))
+ 
+ def _rollup_outs(sourcemap, name, entry_point, entry_points, output_dir):
diff --git a/third_party/rules_webtesting/rules_webtesting.patch b/third_party/rules_webtesting/rules_webtesting.patch
deleted file mode 100644
index 90b7716..0000000
--- a/third_party/rules_webtesting/rules_webtesting.patch
+++ /dev/null
@@ -1,56 +0,0 @@
-diff --git a/BUILD.bazel b/BUILD.bazel
-index ab52bbb..5f9f4c3 100644
---- a/BUILD.bazel
-+++ b/BUILD.bazel
-@@ -24,3 +24,17 @@ gazelle(
-     name = "gazelle",
-     prefix = "github.com/bazelbuild/rules_webtesting",
- )
-+
-+genrule(
-+    name = "generate_error_bin",
-+    outs = ["error_bin.sh"],
-+    cmd = "echo 'exit 1' > $(OUTS)",
-+    executable = True,
-+)
-+
-+sh_binary(
-+    name = "error_bin",
-+    srcs = ["error_bin.sh"],
-+    visibility = ["//visibility:public"],
-+    target_compatible_with = ["@platforms//:incompatible"],
-+)
-diff --git a/third_party/chromedriver/BUILD.bazel b/third_party/chromedriver/BUILD.bazel
-index 3d794d4..3644474 100644
---- a/third_party/chromedriver/BUILD.bazel
-+++ b/third_party/chromedriver/BUILD.bazel
-@@ -30,6 +30,7 @@ alias(
-         "//common/conditions:macos_x64": "@org_chromium_chromedriver_macos_x64//:metadata",
-         "//common/conditions:macos_arm64": "@org_chromium_chromedriver_macos_arm64//:metadata",
-         "//common/conditions:windows_x64": "@org_chromium_chromedriver_windows_x64//:metadata",
-+        "//conditions:default": "//:error_bin",
-     }),
-     visibility = ["//browsers:__subpackages__"],
- )
-diff --git a/third_party/chromium/BUILD.bazel b/third_party/chromium/BUILD.bazel
-index 6d8c6e0..7702fb9 100644
---- a/third_party/chromium/BUILD.bazel
-+++ b/third_party/chromium/BUILD.bazel
-@@ -26,6 +26,7 @@ alias(
-         "//common/conditions:macos_x64": "@org_chromium_chromium_macos_x64//:metadata",
-         "//common/conditions:macos_arm64": "@org_chromium_chromium_macos_arm64//:metadata",
-         "//common/conditions:windows_x64": "@org_chromium_chromium_windows_x64//:metadata",
-+        "//conditions:default": "//:error_bin",
-     }),
-     visibility = ["//browsers:__subpackages__"],
- )
-diff --git a/web/internal/executable_name.bzl b/web/internal/executable_name.bzl
-index b103868..12df0cc 100644
---- a/web/internal/executable_name.bzl
-+++ b/web/internal/executable_name.bzl
-@@ -26,4 +26,5 @@ def get_platform_executable_name():
-         "//common/conditions:macos_x64": "main_darwin_x64",
-         "//common/conditions:macos_arm64": "main_darwin_arm64",
-         "//common/conditions:windows_x64": "main_windows_x64.exe",
-+        "//conditions:default": "//:error_bin",
-     })
diff --git a/tools/BUILD b/tools/BUILD
index 5f26d54..7ccf8d6 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -1,4 +1,4 @@
-load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
+#load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -102,13 +102,3 @@
         "@io_bazel_rules_go//go/config:msan": "false",
     },
 )
-
-nodejs_binary(
-    name = "tsc_wrapped_with_angular",
-    data = [
-        "@npm//@angular/compiler-cli",
-        "@npm//@bazel/typescript",
-    ],
-    entry_point = "@npm//:node_modules/@bazel/typescript/internal/tsc_wrapped/tsc_wrapped.js",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-)
diff --git a/tools/build_rules/js.bzl b/tools/build_rules/js.bzl
index d334764..f66d9c4 100644
--- a/tools/build_rules/js.bzl
+++ b/tools/build_rules/js.bzl
@@ -1,55 +1,343 @@
-load("@build_bazel_rules_nodejs//:providers.bzl", "JSModuleInfo")
-load("@npm//@bazel/rollup:index.bzl", upstream_rollup_bundle = "rollup_bundle")
-load("@npm//@bazel/terser:index.bzl", "terser_minified")
-load("@bazel_skylib//lib:paths.bzl", "paths")
-load("@npm//@bazel/protractor:index.bzl", "protractor_web_test_suite")
-load("@npm//@bazel/typescript:index.bzl", upstream_ts_library = "ts_library", upstream_ts_project = "ts_project")
+load("@aspect_rules_js//js:providers.bzl", "JsInfo")
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+load("@aspect_rules_js//js:defs.bzl", "js_library")
+load("@aspect_rules_js//npm:defs.bzl", "npm_package")
+load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
+load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
+load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
 
-def ts_project(**kwargs):
-    """A trivial wrapper to prepare for the rules_js migration.
+#load("@npm//:history-server/package_json.bzl", history_server_bin = "bin")
+load("@npm//:html-insert-assets/package_json.bzl", html_insert_assets_bin = "bin")
+load("//tools/build_rules/js:ng.bzl", "ng_esbuild", "ng_project")
+load("//tools/build_rules/js:ts.bzl", _ts_project = "ts_project")
+load("@aspect_rules_rollup//rollup:defs.bzl", upstream_rollup_bundle = "rollup_bundle")
+load("@aspect_rules_terser//terser:defs.bzl", "terser_minified")
 
-    The intent is to change his macro to wrap the new rules_js ts_project
-    implementation.
+ts_project = _ts_project
+
+# Common dependencies of Angular applications
+APPLICATION_DEPS = [
+    "//:node_modules/@angular/common",
+    "//:node_modules/@angular/core",
+    #"//:node_modules/@angular/router",
+    "//:node_modules/@angular/platform-browser",
+    "//:node_modules/@types/node",
+    "//:node_modules/rxjs",
+    #"//:node_modules/tslib",
+]
+
+APPLICATION_HTML_ASSETS = ["styles.css", "favicon.ico"]
+
+# Common dependencies of Angular packages
+PACKAGE_DEPS = [
+    "//:node_modules/@angular/common",
+    "//:node_modules/@angular/core",
+    #"//:node_modules/@angular/router",
+    "//:node_modules/@types/node",
+    "//:node_modules/rxjs",
+    #"//:node_modules/tslib",
+]
+
+TEST_DEPS = APPLICATION_DEPS + [
+    "//:node_modules/@angular/compiler",
+    "//:node_modules/@types/jasmine",
+    "//:node_modules/jasmine-core",
+    "//:node_modules/@angular/platform-browser-dynamic",
+]
+
+NG_DEV_DEFINE = {
+    "process.env.NODE_ENV": "'development'",
+    "ngJitMode": "false",
+}
+NG_PROD_DEFINE = {
+    "process.env.NODE_ENV": "'production'",
+    "ngDevMode": "false",
+    "ngJitMode": "false",
+}
+
+def ng_application(
+        name,
+        deps = [],
+        test_deps = [],
+        extra_srcs = [],
+        assets = None,
+        html_assets = APPLICATION_HTML_ASSETS,
+        visibility = ["//visibility:public"],
+        **kwargs):
     """
-    upstream_ts_library(**kwargs)
+    Bazel macro for compiling an Angular application. Creates {name}, test, serve targets.
 
-def rollup_bundle(name, deps, visibility = None, **kwargs):
+    Projects structure:
+      main.ts
+      index.html
+      polyfills.ts
+      styles.css, favicon.ico (defaults, can be overriden)
+      app/
+        **/*.{ts,css,html}
+
+    Tests:
+      app/
+        **/*.spec.ts
+
+    Args:
+      name: the rule name
+      deps: direct dependencies of the application
+      test_deps: additional dependencies for tests
+      html_assets: assets to insert into the index.html, [styles.css, favicon.ico] by default
+      assets: assets to include in the file bundle
+      visibility: visibility of the primary targets ({name}, 'test', 'serve')
+      **kwargs: extra args passed to main Angular CLI rules
+    """
+    assets = assets if assets else native.glob(["assets/**/*"])
+    html_assets = html_assets if html_assets else []
+
+    test_spec_srcs = native.glob(["app/**/*.spec.ts"])
+
+    srcs = native.glob(
+        ["main.ts", "app/**/*", "package.json"],
+        exclude = test_spec_srcs,
+    ) + extra_srcs
+
+    # Primary app source
+    ng_project(
+        name = "_app",
+        srcs = srcs,
+        deps = deps + APPLICATION_DEPS,
+        visibility = ["//visibility:private"],
+    )
+
+    # App polyfills source + bundle.
+    ng_project(
+        name = "_polyfills",
+        srcs = ["polyfills.ts"],
+        deps = ["//:node_modules/zone.js"],
+        visibility = ["//visibility:private"],
+    )
+    esbuild(
+        name = "polyfills-bundle",
+        entry_point = "polyfills.js",
+        srcs = [":_polyfills"],
+        define = {"process.env.NODE_ENV": "'production'"},
+        config = {
+            "resolveExtensions": [".mjs", ".js"],
+        },
+        metafile = False,
+        format = "esm",
+        minify = True,
+        visibility = ["//visibility:private"],
+    )
+
+    _pkg_web(
+        name = "prod",
+        entry_point = "main.js",
+        entry_deps = [":_app"],
+        html_assets = html_assets,
+        assets = assets,
+        production = True,
+        visibility = ["//visibility:private"],
+    )
+
+    _pkg_web(
+        name = "dev",
+        entry_point = "main.js",
+        entry_deps = [":_app"],
+        html_assets = html_assets,
+        assets = assets,
+        production = False,
+        visibility = ["//visibility:private"],
+    )
+
+    # The default target: the prod package
+    native.alias(
+        name = name,
+        actual = "prod",
+        visibility = visibility,
+    )
+
+def _pkg_web(name, entry_point, entry_deps, html_assets, assets, production, visibility):
+    """ Bundle and create runnable web package.
+
+      For a given application entry_point, assets and defined constants... generate
+      a bundle using that entry and constants, an index.html referencing the bundle and
+      providated assets, package all content into a resulting directory of the given name.
+    """
+
+    bundle = "bundle-%s" % name
+
+    ng_esbuild(
+        name = bundle,
+        entry_points = [entry_point],
+        srcs = entry_deps,
+        define = NG_PROD_DEFINE if production else NG_DEV_DEFINE,
+        format = "esm",
+        output_dir = True,
+        splitting = True,
+        metafile = False,
+        minify = production,
+        visibility = ["//visibility:private"],
+    )
+
+    html_out = "_%s_html" % name
+
+    html_insert_assets_bin.html_insert_assets(
+        name = html_out,
+        outs = ["%s/index.html" % html_out],
+        args = [
+                   # Template HTML file.
+                   "--html",
+                   "$(location :index.html)",
+                   # Output HTML file.
+                   "--out",
+                   "%s/%s/index.html" % (native.package_name(), html_out),
+                   # Root directory prefixes to strip from asset paths.
+                   "--roots",
+                   native.package_name(),
+                   "%s/%s" % (native.package_name(), html_out),
+               ] +
+               # Generic Assets
+               ["--assets"] + ["$(execpath %s)" % s for s in html_assets] +
+               ["--scripts", "--module", "polyfills-bundle.js"] +
+               # Main bundle to bootstrap the app last
+               ["--scripts", "--module", "%s/main.js" % bundle],
+        # The input HTML template, all assets for potential access for stamping
+        srcs = [":index.html", ":%s" % bundle, ":polyfills-bundle"] + html_assets,
+        visibility = ["//visibility:private"],
+    )
+
+    copy_to_directory(
+        name = name,
+        srcs = [":%s" % bundle, ":polyfills-bundle", ":%s" % html_out] + html_assets + assets,
+        root_paths = [".", "%s/%s" % (native.package_name(), html_out)],
+        visibility = visibility,
+    )
+
+    # http server serving the bundle
+    # TODO(phil): Get this working.
+    #history_server_bin.history_server_binary(
+    #    name = "serve" + ("-prod" if production else ""),
+    #    args = ["$(location :%s)" % name],
+    #    data = [":%s" % name],
+    #    visibility = visibility,
+    #)
+
+def ng_pkg(name, generate_public_api = True, extra_srcs = [], deps = [], test_deps = [], visibility = ["//visibility:public"], **kwargs):
+    """
+    Bazel macro for compiling an npm-like Angular package project. Creates '{name}' and 'test' targets.
+
+    Projects structure:
+      src/
+        public-api.ts
+        **/*.{ts,css,html}
+
+    Tests:
+      src/
+        **/*.spec.ts
+
+    Args:
+      name: the rule name
+      deps: package dependencies
+      test_deps: additional dependencies for tests
+      visibility: visibility of the primary targets ('{name}', 'test')
+    """
+
+    test_spec_srcs = native.glob(["**/*.spec.ts"])
+
+    srcs = native.glob(
+        ["**/*.ts", "**/*.css", "**/*.html"],
+        exclude = test_spec_srcs + [
+            "public-api.ts",
+        ],
+    ) + extra_srcs
+
+    # An index file to allow direct imports of the directory similar to a package.json "main"
+    write_file(
+        name = "_index",
+        out = "index.ts",
+        content = ["export * from \"./public-api\";"],
+        visibility = ["//visibility:private"],
+    )
+
+    if generate_public_api:
+        write_file(
+            name = "_public_api",
+            out = "public-api.ts",
+            content = [
+                "export * from './%s.component';" % name,
+                "export * from './%s.module';" % name,
+            ],
+            visibility = ["//visibility:private"],
+        )
+        srcs.append(":_public_api")
+
+    ng_project(
+        name = "_lib",
+        srcs = srcs + [":_index"],
+        deps = deps + PACKAGE_DEPS,
+        #visibility = ["//visibility:private"],
+        visibility = ["//visibility:public"],
+        **kwargs
+    )
+
+    js_library(
+        name = name + "_js",
+        srcs = [":_lib"],
+        visibility = ["//visibility:public"],
+    )
+
+    npm_package(
+        name = name,
+        srcs = ["package.json", ":_lib"],
+        # This is a perf improvement; the default will be flipped to False in rules_js 2.0
+        include_runfiles = False,
+        visibility = ["//visibility:public"],
+    )
+
+def rollup_bundle(name, entry_point, deps = [], visibility = None, **kwargs):
     """Calls the upstream rollup_bundle() and exposes a .min.js file.
 
     Legacy version of rollup_bundle() used to provide the .min.js file. This
     wrapper provides the same interface by explicitly exposing a .min.js file.
     """
+    copy_file(
+        name = name + "__rollup_config",
+        src = "//:rollup.config.js",
+        out = name + "__rollup_config.js",
+    )
+
     upstream_rollup_bundle(
         name = name,
         visibility = visibility,
         deps = deps + [
-            "@npm//@rollup/plugin-node-resolve",
+            "//:node_modules/@rollup/plugin-node-resolve",
         ],
-        config_file = "//:rollup.config.js",
-        link_workspace_root = True,
+        sourcemap = "false",
+        config_file = ":%s__rollup_config.js" % name,
+        entry_point = entry_point,
         **kwargs
     )
 
     terser_minified(
         name = name + "__min",
-        src = name + ".js",
+        srcs = [name + ".js"],
+        sourcemap = False,
     )
 
     # Copy the __min.js file (a declared output inside the rule) so that it's a
     # pre-declared output and publicly visible. I.e. via attr.output() below.
-    _expose_minified_js(
+    _expose_file_with_suffix(
         name = name + "__min_exposed",
         src = ":%s__min" % name,
         out = name + ".min.js",
+        suffix = "__min.js",
         visibility = visibility,
     )
 
-def _expose_minified_js_impl(ctx):
+def _expose_file_with_suffix_impl(ctx):
     """Copies the .min.js file in order to make it publicly accessible."""
-    sources = ctx.attr.src[JSModuleInfo].sources.to_list()
+    sources = ctx.attr.src[JsInfo].sources.to_list()
     min_js = None
     for src in sources:
-        if src.basename.endswith("__min.js"):
+        if src.basename.endswith(ctx.attr.suffix):
             min_js = src
             break
 
@@ -63,48 +351,11 @@
         arguments = [min_js.path, ctx.outputs.out.path],
     )
 
-_expose_minified_js = rule(
-    implementation = _expose_minified_js_impl,
+_expose_file_with_suffix = rule(
+    implementation = _expose_file_with_suffix_impl,
     attrs = {
-        "src": attr.label(providers = [JSModuleInfo]),
+        "src": attr.label(providers = [JsInfo]),
         "out": attr.output(mandatory = True),
+        "suffix": attr.string(mandatory = True),
     },
 )
-
-def protractor_ts_test(name, srcs, deps = None, data = None, **kwargs):
-    """Wraps upstream protractor_web_test_suite() to reduce boilerplate.
-
-    This is largely based on the upstream protractor example:
-    https://github.com/bazelbuild/rules_nodejs/blob/stable/examples/angular/e2e/BUILD.bazel
-
-    See the documentation for more information:
-    https://bazelbuild.github.io/rules_nodejs/Protractor.html#protractor_web_test_suite
-    """
-    upstream_ts_project(
-        name = name + "__lib",
-        srcs = srcs,
-        testonly = 1,
-        deps = (deps or []) + [
-            # Implicit deps that are necessary to get tests of this kind to
-            # work.
-            "@npm//@types/jasmine",
-            "@npm//jasmine",
-            "@npm//protractor",
-            "@npm//@types/node",
-        ],
-        tsconfig = {},
-        declaration = True,
-        declaration_map = True,
-    )
-
-    data = (data or []) + [
-        "//tools/build_rules/js/waitpid_module",
-    ]
-
-    protractor_web_test_suite(
-        name = name,
-        srcs = [paths.replace_extension(src, ".js") for src in srcs],
-        deps = [":%s__lib" % name],
-        data = data,
-        **kwargs
-    )
diff --git a/tools/build_rules/js/BUILD b/tools/build_rules/js/BUILD
new file mode 100644
index 0000000..2381fcb
--- /dev/null
+++ b/tools/build_rules/js/BUILD
@@ -0,0 +1,22 @@
+load("@npm//:@angular/compiler-cli/package_json.bzl", angular_compiler_cli = "bin")
+load(":ts.bzl", "ts_project")
+
+# Define the @angular/compiler-cli ngc bin binary as a target
+angular_compiler_cli.ngc_binary(
+    name = "ngc",
+    visibility = ["//visibility:public"],
+)
+
+# ESBuild plugin to run the Angular linker
+ts_project(
+    name = "ngc.esbuild",
+    srcs = ["ngc.esbuild.ts"],
+    tsconfig = "//:tsconfig.node",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//:node_modules/@angular/compiler-cli",
+        "//:node_modules/@babel/core",
+        #"//:node_modules/@types/babel__core",
+        "//:node_modules/@types/node",
+    ],
+)
diff --git a/tools/build_rules/js/ng.bzl b/tools/build_rules/js/ng.bzl
new file mode 100644
index 0000000..0a0f1e1
--- /dev/null
+++ b/tools/build_rules/js/ng.bzl
@@ -0,0 +1,25 @@
+load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
+load(":ts.bzl", "ts_project")
+
+def ng_project(name, **kwargs):
+    """The rules_js ts_project() configured with the Angular ngc compiler.
+    """
+    ts_project(
+        name = name,
+
+        # Compiler
+        tsc = "//tools/build_rules/js:ngc",
+
+        # Any other ts_project() or generic args
+        **kwargs
+    )
+
+def ng_esbuild(name, **kwargs):
+    """The rules_esbuild esbuild() configured with the Angular linker configuration
+    """
+
+    esbuild(
+        name = name,
+        config = "//tools/build_rules/js:ngc.esbuild.js",
+        **kwargs
+    )
diff --git a/tools/build_rules/js/ngc.esbuild.ts b/tools/build_rules/js/ngc.esbuild.ts
new file mode 100644
index 0000000..b10fd25
--- /dev/null
+++ b/tools/build_rules/js/ngc.esbuild.ts
@@ -0,0 +1,60 @@
+/**
+ * ESBuild plugin to run the angular linker via babel plugin while bundling.
+ *
+ * Inspired by:
+ *
+ *  Internal angular/dev-infra-private-builds esbuild plugin
+ *  - https://github.com/angular/dev-infra-private-builds/blob/afcc2494c45a63660cb560ee96179969610435db/shared-scripts/angular-linker/esbuild-plugin.mjs
+ *  Rollup
+ *   - https://github.com/angular/angular/blob/14.0.5/integration/ng_elements/rollup.config.mjs
+ *  Webpack
+ *  - https://github.com/angular/angular-cli/blob/14.0.5/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts#L97-L114
+ */
+
+import { transformFileAsync } from '@babel/core';
+import {
+  ConsoleLogger,
+  NodeJSFileSystem,
+  LogLevel,
+} from '@angular/compiler-cli';
+import { createEs2015LinkerPlugin } from '@angular/compiler-cli/linker/babel';
+
+const linkerBabelPlugin = createEs2015LinkerPlugin({
+  fileSystem: new NodeJSFileSystem(),
+  logger: new ConsoleLogger(LogLevel.warn),
+  unknownDeclarationVersionHandling: 'error',
+  // Must enable JIT for unit tests
+  // TODO: would be ideal to only set this for tests
+  linkerJitMode: true,
+  // Workaround for https://github.com/angular/angular/issues/42769 and https://github.com/angular/angular-cli/issues/22647.
+  sourceMapping: false,
+});
+
+const ngLinkerPlugin = {
+  name: 'ng-linker-esbuild',
+  setup(build: any) {
+    build.onLoad({ filter: /node_modules/ }, async (args: any) => {
+      const filePath = args.path;
+      const transformResult = await transformFileAsync(filePath, {
+        filename: filePath,
+        filenameRelative: filePath,
+        plugins: [linkerBabelPlugin],
+        sourceMaps: 'inline',
+        compact: false,
+      });
+
+      if (!transformResult) {
+        throw new Error('Babel NG Linker error');
+      }
+
+      return { contents: transformResult.code };
+    });
+  },
+};
+
+export default {
+  // Ensure only [m]js is consumed. Any typescript should be precompiled
+  // and not consumed by esbuild.
+  resolveExtensions: ['.mjs', '.js'],
+  plugins: [ngLinkerPlugin],
+};
diff --git a/tools/build_rules/js/ts.bzl b/tools/build_rules/js/ts.bzl
new file mode 100644
index 0000000..a5b0770
--- /dev/null
+++ b/tools/build_rules/js/ts.bzl
@@ -0,0 +1,20 @@
+load("@aspect_rules_ts//ts:defs.bzl", _ts_project = "ts_project")
+
+def ts_project(name, **kwargs):
+    """ts_project() macro with default tsconfig and aligning params.
+    """
+
+    _ts_project(
+        name = name,
+
+        # Default tsconfig and aligning attributes
+        tsconfig = kwargs.pop("tsconfig", "//:tsconfig"),
+        declaration = kwargs.pop("declaration", True),
+        declaration_map = kwargs.pop("declaration_map", True),
+        source_map = kwargs.pop("source_map", True),
+
+        # TODO(phil): Is this a good idea? I don't _actually_ know what this
+        # does.
+        supports_workers = False,
+        **kwargs
+    )
diff --git a/tools/dependency_rewrite b/tools/dependency_rewrite
index 27e4d79..23e5d14 100644
--- a/tools/dependency_rewrite
+++ b/tools/dependency_rewrite
@@ -13,8 +13,11 @@
 rewrite www.openssl.org/(.*) software.frc971.org/Build-Dependencies/www.openssl.org/$1
 rewrite zlib.net/(.*) software.frc971.org/Build-Dependencies/zlib.net/$1
 rewrite downloads.sourceforge.net/(.*) software.frc971.org/Build-Dependencies/downloads.sourceforge.net/$1
+rewrite cdn.cypress.io/(.*) software.frc971.org/Build-Dependencies/cdn.cypress.io/$1
+rewrite www.googleapis.com/(.*) software.frc971.org/Build-Dependencies/www.googleapis.com/$1
 allow crates.io
 allow golang.org
+allow registry.npmjs.org
 
 allow software.frc971.org
 allow www.frc971.org
diff --git a/tools/lint/BUILD b/tools/lint/BUILD
index 58ef879..23ba4d6 100644
--- a/tools/lint/BUILD
+++ b/tools/lint/BUILD
@@ -1,5 +1,10 @@
 load("@ci_configure//:ci.bzl", "RUNNING_IN_CI")
 load("@pip_deps//:requirements.bzl", "entry_point")
+load("@npm//:prettier/package_json.bzl", prettier_bin = "bin")
+
+prettier_bin.prettier_binary(
+    name = "prettier_binary",
+)
 
 sh_binary(
     name = "gofmt",
@@ -50,7 +55,7 @@
     name = "prettier",
     srcs = ["prettier.sh"],
     data = [
-        "@npm//prettier/bin:prettier",
+        ":prettier_binary",
     ],
     target_compatible_with = ["@platforms//cpu:x86_64"],
     deps = [
diff --git a/tools/lint/prettier.sh b/tools/lint/prettier.sh
index adea3f2..0198a71 100755
--- a/tools/lint/prettier.sh
+++ b/tools/lint/prettier.sh
@@ -15,7 +15,7 @@
 runfiles_export_envvars
 export RUNFILES="${RUNFILES_DIR}"
 
-PRETTIER="$(rlocation npm/prettier/bin/prettier.sh)"
+PRETTIER="$(rlocation org_frc971/tools/lint/prettier_binary.sh)"
 PRETTIER="$(readlink -f "${PRETTIER}")"
 readonly PRETTIER
 
diff --git a/tools/ts/BUILD b/tools/ts/BUILD
index 697e376..9017223 100644
--- a/tools/ts/BUILD
+++ b/tools/ts/BUILD
@@ -1,5 +1,5 @@
 load("@bazel_skylib//rules:write_file.bzl", "write_file")
-load("@build_bazel_rules_nodejs//toolchains/node:node_toolchain.bzl", "node_toolchain")
+load("@rules_nodejs//nodejs:toolchain.bzl", "node_toolchain")
 
 write_file(
     name = "noop_error_exit",
@@ -28,5 +28,5 @@
         "//tools/platforms/nodejs:lacks_support",
     ],
     toolchain = ":noop_node_toolchain_impl",
-    toolchain_type = "@build_bazel_rules_nodejs//toolchains/node:toolchain_type",
+    toolchain_type = "@rules_nodejs//nodejs:toolchain_type",
 )
diff --git a/tsconfig.json b/tsconfig.json
index 16fe795..bd23965 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,11 +1,19 @@
 {
+  "compileOnSave": false,
+  "exclude": ["bazel-*"],
   "compilerOptions": {
     "experimentalDecorators": true,
+    "sourceMap": true,
+    "declaration": true,
+    "declarationMap": true,
     "strict": false,
     "noImplicitAny": false,
     "target": "es2021",
     "lib": ["es2021", "dom", "dom.iterable"],
-    "moduleResolution": "node"
+    "moduleResolution": "node",
+    "paths": {
+      "@org_frc971/*": ["./*"]
+    }
   },
   "bazelOptions": {
     "workspaceName": "971-Robot-Code"
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..148c41b
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,7 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "allowSyntheticDefaultImports": true,
+    "lib": ["es2021"]
+  }
+}