Merge changes from topics "norootintegrationtest", "profileshelltestdatafile"

* changes:
  Assert correct callsite name using TP.
  Run perfetto_integrationtests from /data/local/tests.
  Do not require root for integrationtest.
diff --git a/docs/analysis/metrics.md b/docs/analysis/metrics.md
index 32d52e9..2286bd1 100644
--- a/docs/analysis/metrics.md
+++ b/docs/analysis/metrics.md
@@ -100,7 +100,7 @@
       the walkthrough. The prerequisites and Step 4 below give instructions on
       how to get trace processor and run the metrics code.
 
-[gist]: https://gist.github.com/tilal6991/c221cf0cae17e298dfa82b118edf9080
+[gist]: https://gist.github.com/LalitMaganti/c221cf0cae17e298dfa82b118edf9080
 
 ### Prerequisites
 
@@ -330,7 +330,7 @@
 
 _Notes:_
 
-- If something doesn't work as intended, check that the workspace looks the same as the contents of this [GitHub gist](https://gist.github.com/tilal6991/c221cf0cae17e298dfa82b118edf9080).
+- If something doesn't work as intended, check that the workspace looks the same as the contents of this [GitHub gist](https://gist.github.com/LalitMaganti/c221cf0cae17e298dfa82b118edf9080).
 - A good example trace for this metric is the Android example trace used by the Perfetto UI found [here](https://storage.googleapis.com/perfetto-misc/example_android_trace_30s_1).
 - stderr is redirected to remove any noise from parsing the trace that trace processor generates.
 
diff --git a/docs/contributing/build-instructions.md b/docs/contributing/build-instructions.md
index 76d53e3..9afa60f 100644
--- a/docs/contributing/build-instructions.md
+++ b/docs/contributing/build-instructions.md
@@ -80,20 +80,26 @@
 Build the UI:
 
 ```bash
-gn args out/default  # The only relevant arg is is_debug=true|false
-
-# This will generate the static content for serving the UI in out/default/ui/.
-tools/ninja -C out/default ui
+# Will build into ./out/ui by default. Can be changed with --out path/
+# The final bundle will be available at ./ui/out/dist/.
+# The build script creates a symlink from ./ui/out to $OUT_PATH/ui/.
+ui/build
 ```
 
 Test your changes on a local server using:
 
 ```bash
-ui/run-dev-server out/default
+# This will automatically build the UI. There is no need to manually run
+# ui/build before running ui/run-dev-server.
+ui/run-dev-server
 ```
 
 Navigate to http://localhost:10000/ to see the changes.
 
+The server supports live reloading of CSS and TS/JS contents. Whenever a ui
+source file is changed it, the script will automatically re-build it and show a
+prompt in the web page.
+
 ## IDE setup
 
 Use a following command in the checkout directory in order to generate the
diff --git a/include/perfetto/tracing/debug_annotation.h b/include/perfetto/tracing/debug_annotation.h
index 38bc653..86494cf 100644
--- a/include/perfetto/tracing/debug_annotation.h
+++ b/include/perfetto/tracing/debug_annotation.h
@@ -18,6 +18,7 @@
 #define INCLUDE_PERFETTO_TRACING_DEBUG_ANNOTATION_H_
 
 #include "perfetto/base/export.h"
+#include "perfetto/tracing/traced_value_forward.h"
 #include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 
 #include <stdint.h>
@@ -54,86 +55,10 @@
 
   // Called to write the contents of the debug annotation into the trace.
   virtual void Add(protos::pbzero::DebugAnnotation*) const = 0;
+
+  void WriteIntoTracedValue(TracedValue context) const;
 };
 
-namespace internal {
-// Overloads for all the supported built in debug annotation types. Numeric
-// types are handled with templates to avoid problems with overloading
-// platform-specific types (e.g., size_t).
-void PERFETTO_EXPORT WriteDebugAnnotation(protos::pbzero::DebugAnnotation*,
-                                          const char*);
-void PERFETTO_EXPORT WriteDebugAnnotation(protos::pbzero::DebugAnnotation*,
-                                          const std::string&);
-void PERFETTO_EXPORT WriteDebugAnnotation(protos::pbzero::DebugAnnotation*,
-                                          const void*);
-void PERFETTO_EXPORT WriteDebugAnnotation(protos::pbzero::DebugAnnotation*,
-                                          const DebugAnnotation&);
-
-template <typename T>
-void WriteDebugAnnotation(
-    protos::pbzero::DebugAnnotation* annotation,
-    T value,
-    typename std::enable_if<std::is_floating_point<T>::value>::type* =
-        nullptr) {
-  annotation->set_double_value(static_cast<double>(value));
-}
-
-template <typename T>
-void WriteDebugAnnotation(
-    protos::pbzero::DebugAnnotation* annotation,
-    T value,
-    typename std::enable_if<std::is_integral<T>::value &&
-                            !std::is_same<T, bool>::value &&
-                            std::is_signed<T>::value>::type* = nullptr) {
-  annotation->set_int_value(static_cast<int64_t>(value));
-}
-
-template <typename T>
-void WriteDebugAnnotation(
-    protos::pbzero::DebugAnnotation* annotation,
-    T value,
-    typename std::enable_if<
-        std::is_enum<T>::value &&
-        std::is_signed<typename safe_underlying_type<T>::type>::value>::type* =
-        nullptr) {
-  annotation->set_int_value(static_cast<int64_t>(value));
-}
-
-template <typename T>
-void WriteDebugAnnotation(
-    protos::pbzero::DebugAnnotation* annotation,
-    T value,
-    typename std::enable_if<std::is_enum<T>::value &&
-                            std::is_unsigned<typename safe_underlying_type<
-                                T>::type>::value>::type* = nullptr) {
-  annotation->set_uint_value(static_cast<uint64_t>(value));
-}
-
-template <typename T>
-void WriteDebugAnnotation(
-    protos::pbzero::DebugAnnotation* annotation,
-    T value,
-    typename std::enable_if<std::is_integral<T>::value &&
-                            !std::is_same<T, bool>::value &&
-                            std::is_unsigned<T>::value>::type* = nullptr) {
-  annotation->set_uint_value(static_cast<uint64_t>(value));
-}
-
-template <typename T>
-void WriteDebugAnnotation(
-    protos::pbzero::DebugAnnotation* annotation,
-    T value,
-    typename std::enable_if<std::is_same<T, bool>::value>::type* = nullptr) {
-  annotation->set_bool_value(static_cast<bool>(value));
-}
-
-template <typename T>
-void WriteDebugAnnotation(protos::pbzero::DebugAnnotation* annotation,
-                          const std::unique_ptr<T>& value) {
-  WriteDebugAnnotation(annotation, *value);
-}
-
-}  // namespace internal
 }  // namespace perfetto
 
 #endif  // INCLUDE_PERFETTO_TRACING_DEBUG_ANNOTATION_H_
diff --git a/include/perfetto/tracing/internal/track_event_internal.h b/include/perfetto/tracing/internal/track_event_internal.h
index 747b2ef..bba08db 100644
--- a/include/perfetto/tracing/internal/track_event_internal.h
+++ b/include/perfetto/tracing/internal/track_event_internal.h
@@ -23,6 +23,7 @@
 #include "perfetto/tracing/data_source.h"
 #include "perfetto/tracing/debug_annotation.h"
 #include "perfetto/tracing/trace_writer_base.h"
+#include "perfetto/tracing/traced_value.h"
 #include "perfetto/tracing/track.h"
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
@@ -145,7 +146,8 @@
                                  const char* name,
                                  T&& value) {
     auto annotation = AddDebugAnnotation(event_ctx, name);
-    WriteDebugAnnotation(annotation, value);
+    WriteIntoTracedValue(internal::CreateTracedValueFromProto(annotation),
+                         std::forward<T>(value));
   }
 
   // If the given track hasn't been seen by the trace writer yet, write a
diff --git a/include/perfetto/tracing/traced_value.h b/include/perfetto/tracing/traced_value.h
index 6029482..a5481b0 100644
--- a/include/perfetto/tracing/traced_value.h
+++ b/include/perfetto/tracing/traced_value.h
@@ -374,7 +374,7 @@
 }  // namespace internal
 
 template <typename T>
-PERFETTO_EXPORT void WriteIntoTracedValue(TracedValue context, T&& value) {
+void WriteIntoTracedValue(TracedValue context, T&& value) {
   // TODO(altimin): Add a URL to documentation and a list of common failure
   // patterns.
   static_assert(
@@ -396,20 +396,18 @@
 // See WriteWithFallback test in traced_value_unittest.cc for a concrete
 // example.
 template <typename T>
-PERFETTO_EXPORT
-    typename std::enable_if<internal::has_traced_value_support<T>::value>::type
-    WriteIntoTracedValueWithFallback(TracedValue context,
-                                     T&& value,
-                                     const std::string&) {
+typename std::enable_if<internal::has_traced_value_support<T>::value>::type
+WriteIntoTracedValueWithFallback(TracedValue context,
+                                 T&& value,
+                                 const std::string&) {
   WriteIntoTracedValue(std::move(context), std::forward<T>(value));
 }
 
 template <typename T>
-PERFETTO_EXPORT
-    typename std::enable_if<!internal::has_traced_value_support<T>::value>::type
-    WriteIntoTracedValueWithFallback(TracedValue context,
-                                     T&&,
-                                     const std::string& fallback) {
+typename std::enable_if<!internal::has_traced_value_support<T>::value>::type
+WriteIntoTracedValueWithFallback(TracedValue context,
+                                 T&&,
+                                 const std::string& fallback) {
   std::move(context).WriteString(fallback);
 }
 
diff --git a/include/perfetto/tracing/track_event_category_registry.h b/include/perfetto/tracing/track_event_category_registry.h
index 5e5304d..e51c4d6 100644
--- a/include/perfetto/tracing/track_event_category_registry.h
+++ b/include/perfetto/tracing/track_event_category_registry.h
@@ -282,7 +282,7 @@
   }
 
   static constexpr bool IsValidCategoryName(const char* name) {
-    return (!name || *name == '\"' || *name == '*')
+    return (!name || *name == '\"' || *name == '*' || *name == ' ')
                ? false
                : *name ? IsValidCategoryName(name + 1) : true;
   }
diff --git a/include/perfetto/tracing/track_event_legacy.h b/include/perfetto/tracing/track_event_legacy.h
index d138223..6bd156a 100644
--- a/include/perfetto/tracing/track_event_legacy.h
+++ b/include/perfetto/tracing/track_event_legacy.h
@@ -146,6 +146,7 @@
 static constexpr uint8_t TRACE_VALUE_TYPE_STRING = 6;
 static constexpr uint8_t TRACE_VALUE_TYPE_COPY_STRING = 7;
 static constexpr uint8_t TRACE_VALUE_TYPE_CONVERTABLE = 8;
+static constexpr uint8_t TRACE_VALUE_TYPE_PROTO = 9;
 
 // Enum reflecting the scope of an INSTANT event. Must fit within
 // TRACE_EVENT_FLAG_SCOPE_MASK.
diff --git a/src/tracing/core/packet_stream_validator.cc b/src/tracing/core/packet_stream_validator.cc
index 3d2d846..72a20f5 100644
--- a/src/tracing/core/packet_stream_validator.cc
+++ b/src/tracing/core/packet_stream_validator.cc
@@ -72,8 +72,11 @@
     varint_ |= static_cast<uint64_t>(octet & 0x7F) << varint_shift_;
     if (octet & 0x80) {
       varint_shift_ += 7;
-      if (varint_shift_ >= 64)
+      if (varint_shift_ >= 64) {
+        // Do not invoke UB on next call.
+        varint_shift_ = 0;
         state_ = kInvalidVarInt;
+      }
       return 0;
     }
     uint64_t varint = varint_;
diff --git a/src/tracing/debug_annotation.cc b/src/tracing/debug_annotation.cc
index 81e889c..8b3b0ff 100644
--- a/src/tracing/debug_annotation.cc
+++ b/src/tracing/debug_annotation.cc
@@ -16,33 +16,20 @@
 
 #include "perfetto/tracing/debug_annotation.h"
 
+#include "perfetto/tracing/traced_value.h"
 #include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 
 namespace perfetto {
 
 DebugAnnotation::~DebugAnnotation() = default;
 
-namespace internal {
-
-void WriteDebugAnnotation(protos::pbzero::DebugAnnotation* annotation,
-                          const char* value) {
-  annotation->set_string_value(value);
+void DebugAnnotation::WriteIntoTracedValue(TracedValue context) const {
+  if (!context.root_context_) {
+    PERFETTO_DFATAL("DebugAnnotation should not be used in non-root contexts.");
+    std::move(context).WriteString("<not supported>");
+    return;
+  }
+  Add(context.root_context_);
 }
 
-void WriteDebugAnnotation(protos::pbzero::DebugAnnotation* annotation,
-                          const std::string& value) {
-  annotation->set_string_value(value);
-}
-
-void WriteDebugAnnotation(protos::pbzero::DebugAnnotation* annotation,
-                          const void* value) {
-  annotation->set_pointer_value(reinterpret_cast<uint64_t>(value));
-}
-
-void WriteDebugAnnotation(protos::pbzero::DebugAnnotation* annotation,
-                          const DebugAnnotation& custom_annotation) {
-  custom_annotation.Add(annotation);
-}
-
-}  // namespace internal
 }  // namespace perfetto
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index acb86a0..08bd8a3 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -98,7 +98,6 @@
     perfetto::Category("cat").SetTags("slow"),
     perfetto::Category("cat.verbose").SetTags("debug"),
     perfetto::Category("cat-with-dashes"),
-    perfetto::Category("cat with spaces"),
     perfetto::Category::Group("foo,bar"),
     perfetto::Category::Group("baz,bar,quux"),
     perfetto::Category::Group("red,green,blue,foo"),
@@ -990,7 +989,7 @@
 
   // Check that the advertised categories match PERFETTO_DEFINE_CATEGORIES (see
   // above).
-  EXPECT_EQ(8, desc.available_categories_size());
+  EXPECT_EQ(7, desc.available_categories_size());
   EXPECT_EQ("test", desc.available_categories()[0].name());
   EXPECT_EQ("This is a test category",
             desc.available_categories()[0].description());
@@ -1002,9 +1001,8 @@
   EXPECT_EQ("cat.verbose", desc.available_categories()[4].name());
   EXPECT_EQ("debug", desc.available_categories()[4].tags()[0]);
   EXPECT_EQ("cat-with-dashes", desc.available_categories()[5].name());
-  EXPECT_EQ("cat with spaces", desc.available_categories()[6].name());
-  EXPECT_EQ("disabled-by-default-cat", desc.available_categories()[7].name());
-  EXPECT_EQ("slow", desc.available_categories()[7].tags()[0]);
+  EXPECT_EQ("disabled-by-default-cat", desc.available_categories()[6].name());
+  EXPECT_EQ("slow", desc.available_categories()[6].tags()[0]);
 }
 
 TEST_P(PerfettoApiTest, TrackEventSharedIncrementalState) {
@@ -1921,6 +1919,10 @@
   TRACE_EVENT_BEGIN("test", "E", "enum_arg", ENUM_BAR);
   TRACE_EVENT_BEGIN("test", "E", "signed_enum_arg", SIGNED_ENUM_FOO);
   TRACE_EVENT_BEGIN("test", "E", "class_enum_arg", MyClassEnum::VALUE);
+  TRACE_EVENT_BEGIN("test", "E", "traced_value",
+                    [&](perfetto::TracedValue context) {
+                      std::move(context).WriteInt64(42);
+                    });
   perfetto::TrackEvent::Flush();
 
   tracing_session->get()->StopBlocking();
@@ -1935,7 +1937,7 @@
           "B:test.E(ptr_arg=(pointer)baadf00d)",
           "B:test.E(size_t_arg=(uint)42)", "B:test.E(ptrdiff_t_arg=(int)-7)",
           "B:test.E(enum_arg=(uint)1)", "B:test.E(signed_enum_arg=(int)-1)",
-          "B:test.E(class_enum_arg=(int)0)"));
+          "B:test.E(class_enum_arg=(int)0)", "B:test.E(traced_value=(int)42)"));
 }
 
 TEST_P(PerfettoApiTest, TrackEventCustomDebugAnnotations) {
@@ -3024,10 +3026,8 @@
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("dynamic"));
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("dynamic_2"));
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED(dynamic));
-  EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("cat with spaces"));
 
-  auto* tracing_session =
-      NewTraceWithCategories({"foo", "dynamic", "cat with spaces"});
+  auto* tracing_session = NewTraceWithCategories({"foo", "dynamic"});
   tracing_session->get()->StartBlocking();
   EXPECT_TRUE(TRACE_EVENT_CATEGORY_ENABLED("foo"));
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("bar"));
@@ -3035,7 +3035,6 @@
   EXPECT_TRUE(TRACE_EVENT_CATEGORY_ENABLED("dynamic"));
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("dynamic_2"));
   EXPECT_TRUE(TRACE_EVENT_CATEGORY_ENABLED(dynamic));
-  EXPECT_TRUE(TRACE_EVENT_CATEGORY_ENABLED("cat with spaces"));
 
   tracing_session->get()->StopBlocking();
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("foo"));
@@ -3044,7 +3043,6 @@
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("dynamic"));
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("dynamic_2"));
   EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED(dynamic));
-  EXPECT_FALSE(TRACE_EVENT_CATEGORY_ENABLED("cat with spaces"));
 }
 
 class TestInterceptor : public perfetto::Interceptor<TestInterceptor> {
diff --git a/test/ci/common.sh b/test/ci/common.sh
index a798448..f4d6d0f 100644
--- a/test/ci/common.sh
+++ b/test/ci/common.sh
@@ -18,13 +18,14 @@
 cd $(dirname ${BASH_SOURCE[0]})/../..
 OUT_PATH="out/dist"
 
-if [[ -e buildtools/clang/bin/llvm-symbolizer ]]; then
-  export ASAN_SYMBOLIZER_PATH="buildtools/clang/bin/llvm-symbolizer"
-  export MSAN_SYMBOLIZER_PATH="buildtools/clang/bin/llvm-symbolizer"
-fi
-
 tools/install-build-deps $INSTALL_BUILD_DEPS_ARGS
 
+# Assumes Linux. Windows should use /win/clang instead.
+if [[ -e buildtools/linux64/clang/bin/llvm-symbolizer ]]; then
+  export ASAN_SYMBOLIZER_PATH="$(readlink -f buildtools/linux64/clang/bin/llvm-symbolizer)"
+  export MSAN_SYMBOLIZER_PATH="$(readlink -f buildtools/linux64/clang/bin/llvm-symbolizer)"
+fi
+
 # Performs checks on generated protos and build files.
 tools/gn gen out/tmp.protoc --args="is_debug=false cc_wrapper=\"ccache\""
 tools/gen_all --check-only out/tmp.protoc
diff --git a/test/ci/ui_tests.sh b/test/ci/ui_tests.sh
index 6b59820..9a1fa3a 100755
--- a/test/ci/ui_tests.sh
+++ b/test/ci/ui_tests.sh
@@ -16,10 +16,8 @@
 INSTALL_BUILD_DEPS_ARGS="--ui"
 source $(dirname ${BASH_SOURCE[0]})/common.sh
 
-tools/gn gen ${OUT_PATH} --args="${PERFETTO_TEST_GN_ARGS}" --check
-tools/ninja -C ${OUT_PATH} ${PERFETTO_TEST_NINJA_ARGS} ui
+tools/node ui/build.js --out ${OUT_PATH}
 
-cp -a ${OUT_PATH}/ui /ci/artifacts/
+cp -a ${OUT_PATH}/ui/dist/ /ci/artifacts/ui
 
-# Run the tests
-${OUT_PATH}/ui_unittests --ci
+tools/node ui/build.js --out ${OUT_PATH} --no-build --run-tests
diff --git a/tools/run_test_like_ci b/tools/run_test_like_ci
index 4f35def..35497c4 100755
--- a/tools/run_test_like_ci
+++ b/tools/run_test_like_ci
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2019 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/ui/.gitignore b/ui/.gitignore
index 4372381..4f807ce 100644
--- a/ui/.gitignore
+++ b/ui/.gitignore
@@ -1,2 +1,2 @@
-/dist
 /node_modules/
+/out
diff --git a/ui/BUILD.gn b/ui/BUILD.gn
index a668a29..98aeb5b 100644
--- a/ui/BUILD.gn
+++ b/ui/BUILD.gn
@@ -12,593 +12,43 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import("../gn/gen_perfetto_version_header.gni")
 import("../gn/perfetto.gni")
-import("../gn/perfetto_check_build_deps.gni")
-import("../gn/wasm.gni")
-import("../protos/perfetto/trace_processor/proto_files.gni")
 
 # Prevent that this file is accidentally included in embedder builds.
 assert(enable_perfetto_ui)
 
-ui_dir = "$root_build_dir/ui"
-chrome_extension_dir = "$root_build_dir/chrome_extension"
-ui_gen_dir = "$target_out_dir/gen"
 nodejs_bin = rebase_path("//tools/node", root_build_dir)
 
-# +----------------------------------------------------------------------------+
-# | The outer "ui" target to just ninja -C out/xxx ui                          |
-# +----------------------------------------------------------------------------+
-
 group("ui") {
   deps = [
-    ":chrome_extension_assets_dist",
-    ":chrome_extension_bundle_dist",
-    ":dist",
-    ":gen_dist_file_map",
-    ":service_worker_bundle_dist",
-    ":test_scripts",
-
-    # IMPORTANT: Only add deps here if they are NOT part of the production UI
-    # (e.g., tests, extensions, ...). Any UI dep should go in the
-    # |ui_dist_targets| list below. The only exception is the service worker
-    # target, that depends on that list.
+    ":ui_build($host_toolchain)",
+    "//src/trace_processor:trace_processor.wasm($wasm_toolchain)",
+    "//tools/trace_to_text:trace_to_text.wasm($wasm_toolchain)",
   ]
 }
 
-# The list of targets that produces dist/ files for the UI. This list is used
-# also by the gen_dist_file_map to generate the map of hashes of all UI files,
-# which is turn used by the service worker code for the offline caching.
-ui_dist_targets = [
-  ":assets_dist",
-  ":catapult_dist",
-  ":controller_bundle_dist",
-  ":engine_bundle_dist",
-  ":frontend_bundle_dist",
-  ":index_dist",
-  ":scss",
-  ":typefaces_dist",
-  ":wasm_dist",
-]
-
-# Builds the ui, but not service worker, tests and extensions.
-group("dist") {
-  deps = ui_dist_targets
-}
-
-# A minimal page to profile the WASM engine without the all UI.
-group("query") {
-  deps = [
-    ":query_bundle_dist",
-    ":query_dist",
-    ":ui",
-  ]
-}
-
-# +----------------------------------------------------------------------------+
-# | Template used to run node binaries using the hermetic node toolchain.      |
-# +----------------------------------------------------------------------------+
-template("node_bin") {
-  action(target_name) {
-    forward_variables_from(invoker,
-                           [
-                             "inputs",
-                             "outputs",
-                             "depfile",
-                           ])
-    deps = [ ":node_modules" ]
-    if (defined(invoker.deps)) {
-      deps += invoker.deps
-    }
-    script = "../gn/standalone/build_tool_wrapper.py"
-    _node_cmd = invoker.node_cmd
-    args = []
-    if (defined(invoker.suppress_stdout) && invoker.suppress_stdout) {
-      args += [ "--suppress_stdout" ]
-    }
-    if (defined(invoker.suppress_stderr) && invoker.suppress_stderr) {
-      args += [ "--suppress_stderr" ]
-    }
-
-    # Some of the node_bin rules *cough*transpile_all_ts*cough* don't
-    # accuratly report the output files. This means if they can run,
-    # change something, and not cause their dependees to rerun causing
-    # bugs. Adding a stamp file which is always rewritten avoids this.
-    # See also b/120010518
-    stamp_path = "$ui_dir/$target_name.node_bin.stamp"
-    outputs += [ stamp_path ]
-    args += [
-              "--stamp=" + rebase_path(stamp_path, root_build_dir),
-              nodejs_bin,
-              rebase_path("node_modules/.bin/$_node_cmd", root_build_dir),
-            ] + invoker.args
-  }
-}
-
-# +----------------------------------------------------------------------------+
-# | Template for "sorcery" the source map resolver.                            |
-# +----------------------------------------------------------------------------+
-template("sorcery") {
-  node_bin(target_name) {
-    assert(defined(invoker.input))
-    assert(defined(invoker.output))
-    forward_variables_from(invoker, [ "deps" ])
-    inputs = [ invoker.input ]
-    outputs = [
-      invoker.output,
-      invoker.output + ".map",
-    ]
-    node_cmd = "sorcery"
-    args = [
-      "-i",
-      rebase_path(invoker.input, root_build_dir),
-      "-o",
-      rebase_path(invoker.output, root_build_dir),
-    ]
-  }
-}
-
-# +----------------------------------------------------------------------------+
-# | Template for bundling js                                                   |
-# +----------------------------------------------------------------------------+
-template("bundle") {
-  node_bin(target_name) {
-    assert(defined(invoker.input))
-    assert(defined(invoker.output))
-    forward_variables_from(invoker, [ "deps" ])
-    inputs = [
-      invoker.input,
-      "rollup.config.js",
-    ]
-    outputs = [
-      invoker.output,
-      invoker.output + ".map",
-    ]
-    node_cmd = "rollup"
-    args = [
-      "-c",
-      rebase_path("rollup.config.js", root_build_dir),
-      rebase_path(invoker.input, root_build_dir),
-      "-o",
-      rebase_path(invoker.output, root_build_dir),
-      "-f",
-      "iife",
-      "-m",
-      "--silent",
-    ]
-  }
-}
-
-# +----------------------------------------------------------------------------+
-# | Bundles all *.js files together resolving CommonJS require() deps.         |
-# +----------------------------------------------------------------------------+
-
-# Bundle together all js sources into a bundle.js file, that will ultimately be
-# included by the .html files.
-
-bundle("frontend_bundle") {
-  deps = [ ":transpile_all_ts" ]
-  input = "$target_out_dir/frontend/index.js"
-  output = "$target_out_dir/frontend_bundle.js"
-}
-
-bundle("chrome_extension_bundle") {
-  deps = [ ":transpile_all_ts" ]
-  input = "$target_out_dir/chrome_extension/index.js"
-  output = "$target_out_dir/chrome_extension_bundle.js"
-}
-
-bundle("controller_bundle") {
-  deps = [ ":transpile_all_ts" ]
-  input = "$target_out_dir/controller/index.js"
-  output = "$target_out_dir/controller_bundle.js"
-}
-
-bundle("engine_bundle") {
-  deps = [ ":transpile_all_ts" ]
-  input = "$target_out_dir/engine/index.js"
-  output = "$target_out_dir/engine_bundle.js"
-}
-
-bundle("service_worker_bundle") {
-  deps = [ ":transpile_service_worker_ts" ]
-  input = "$target_out_dir/service_worker/service_worker.js"
-  output = "$target_out_dir/service_worker.js"
-}
-
-bundle("query_bundle") {
-  deps = [ ":transpile_all_ts" ]
-  input = "$target_out_dir/query/index.js"
-  output = "$target_out_dir/query_bundle.js"
-}
-
-# +----------------------------------------------------------------------------+
-# | Protobuf: gen rules to create .js and .d.ts files from protos.             |
-# +----------------------------------------------------------------------------+
-node_bin("protos_to_js") {
+action("deprecation_warning") {
+  script = "../gn/standalone/build_tool_wrapper.py"
+  outputs = [ "$target_out_dir/never_written_always_execute_rule-2.stamp" ]
   inputs = []
-  foreach(proto, trace_processor_protos) {
-    inputs += [ "../protos/perfetto/trace_processor/$proto.proto" ]
-  }
-  inputs += [
-    "../protos/perfetto/common/trace_stats.proto",
-    "../protos/perfetto/common/tracing_service_capabilities.proto",
-    "../protos/perfetto/config/perfetto_config.proto",
-    "../protos/perfetto/ipc/consumer_port.proto",
-    "../protos/perfetto/ipc/wire_protocol.proto",
-    "../protos/perfetto/metrics/metrics.proto",
-  ]
-  outputs = [ "$ui_gen_dir/protos.js" ]
-  node_cmd = "pbjs"
   args = [
-           "--force-number",
-           "-t",
-           "static-module",
-           "-w",
-           "commonjs",
-           "-p",
-           rebase_path("..", root_build_dir),
-           "-o",
-           rebase_path(outputs[0], root_build_dir),
-         ] + rebase_path(inputs, root_build_dir)
-}
-
-# Protobuf.js requires to first generate .js files from the .proto and then
-# create .ts definitions for them.
-node_bin("protos_to_ts") {
-  deps = [ ":protos_to_js" ]
-  inputs = [ "$ui_gen_dir/protos.js" ]
-  outputs = [ "$ui_gen_dir/protos.d.ts" ]
-  node_cmd = "pbts"
-  args = [
-    "-p",
-    rebase_path("..", root_build_dir),
-    "-o",
-    rebase_path(outputs[0], root_build_dir),
-    rebase_path(inputs[0], root_build_dir),
+    "cat",
+    rebase_path("config/gn_deprecation_banner.txt", root_build_dir),
   ]
 }
 
-# +----------------------------------------------------------------------------+
-# | TypeScript: transpiles all *.ts into .js                                   |
-# +----------------------------------------------------------------------------+
-
-# Builds all .ts sources in the repo under |src|.
-node_bin("transpile_all_ts") {
-  deps = [
-    ":dist_symlink",
-    ":protos_to_ts",
-    ":version_ts_gen",
-    ":wasm_gen",
-  ]
-  inputs = [ "tsconfig.json" ]
-  outputs = [
-    "$target_out_dir/frontend/index.js",
-    "$target_out_dir/engine/index.js",
-    "$target_out_dir/controller/index.js",
-    "$target_out_dir/query/index.js",
-    "$target_out_dir/chrome_extension/index.js",
-  ]
-
-  depfile = root_out_dir + "/tsc.d"
-  exec_script("../gn/standalone/glob.py",
-              [
-                "--root=" + rebase_path(".", root_build_dir),
-                "--filter=*.ts",
-                "--exclude=node_modules",
-                "--exclude=dist",
-                "--exclude=service_worker",
-                "--deps=obj/ui/frontend/index.js",
-                "--output=" + rebase_path(depfile),
-              ],
-              "")
-
-  node_cmd = "tsc"
-  args = [
-    "--incremental",
-    "--project",
-    rebase_path(".", root_build_dir),
-    "--outDir",
-    rebase_path(target_out_dir, root_build_dir),
-  ]
-}
-
-node_bin("transpile_service_worker_ts") {
-  deps = [
-    ":dist_symlink",
-    ":gen_dist_file_map",
-  ]
+action("ui_build") {
+  deps = [ ":deprecation_warning" ]
+  script = "../gn/standalone/build_tool_wrapper.py"
+  outputs = [ "$target_out_dir/never_written_always_execute_rule.stamp" ]
   inputs = [
-    "tsconfig.json",
-    "src/service_worker/service_worker.ts",
+    "//tools/node",
+    "build.js",
   ]
-  outputs = [ "$target_out_dir/service_worker/service_worker.js" ]
-
-  node_cmd = "tsc"
   args = [
-    "--project",
-    rebase_path("src/service_worker", root_build_dir),
-    "--outDir",
-    rebase_path(target_out_dir, root_build_dir),
+    nodejs_bin,
+    rebase_path("build.js", root_build_dir),
+    "--out",
+    ".",
   ]
 }
-
-# +----------------------------------------------------------------------------+
-# | Build css.                                                                 |
-# +----------------------------------------------------------------------------+
-
-scss_root = "src/assets/perfetto.scss"
-scss_srcs = [
-  "src/assets/analyze_page.scss",
-  "src/assets/common.scss",
-  "src/assets/details.scss",
-  "src/assets/metrics_page.scss",
-  "src/assets/modal.scss",
-  "src/assets/record.scss",
-  "src/assets/sidebar.scss",
-  "src/assets/topbar.scss",
-  "src/assets/trace_info_page.scss",
-  "src/assets/typefaces.scss",
-]
-
-# Build css.
-node_bin("scss") {
-  deps = [ ":dist_symlink" ]
-  inputs = [ scss_root ] + scss_srcs
-  outputs = [ "$ui_dir/perfetto.css" ]
-
-  node_cmd = "node-sass"
-  args = [
-    "--quiet",
-    rebase_path(scss_root, root_build_dir),
-    rebase_path(outputs[0], root_build_dir),
-  ]
-}
-
-# +----------------------------------------------------------------------------+
-# | Copy rules: create the final output directory.                             |
-# +----------------------------------------------------------------------------+
-copy("index_dist") {
-  sources = [ "index.html" ]
-  outputs = [ "$ui_dir/index.html" ]
-}
-
-copy("typefaces_dist") {
-  sources = [
-    "../buildtools/typefaces/MaterialIcons.woff2",
-    "../buildtools/typefaces/Raleway-Regular.woff2",
-    "../buildtools/typefaces/Raleway-Thin.woff2",
-    "../buildtools/typefaces/RobotoCondensed-Light.woff2",
-    "../buildtools/typefaces/RobotoCondensed-Regular.woff2",
-    "../buildtools/typefaces/RobotoMono-Regular.woff2",
-  ]
-
-  outputs = [ "$ui_dir/assets/{{source_file_part}}" ]
-}
-
-copy("query_dist") {
-  sources = [ "query.html" ]
-  outputs = [ "$ui_dir/query.html" ]
-}
-
-copy("assets_dist") {
-  sources = [
-              "src/assets/brand.png",
-              "src/assets/favicon.png",
-              "src/assets/logo-3d.png",
-              "src/assets/rec_atrace.png",
-              "src/assets/rec_battery_counters.png",
-              "src/assets/rec_board_voltage.png",
-              "src/assets/rec_cpu_coarse.png",
-              "src/assets/rec_cpu_fine.png",
-              "src/assets/rec_cpu_freq.png",
-              "src/assets/rec_cpu_voltage.png",
-              "src/assets/rec_ftrace.png",
-              "src/assets/rec_gpu_mem_total.png",
-              "src/assets/rec_java_heap_dump.png",
-              "src/assets/rec_lmk.png",
-              "src/assets/rec_logcat.png",
-              "src/assets/rec_long_trace.png",
-              "src/assets/rec_mem_hifreq.png",
-              "src/assets/rec_meminfo.png",
-              "src/assets/rec_native_heap_profiler.png",
-              "src/assets/rec_one_shot.png",
-              "src/assets/rec_ps_stats.png",
-              "src/assets/rec_ring_buf.png",
-              "src/assets/rec_vmstat.png",
-            ] + [ scss_root ] + scss_srcs
-  outputs = [ "$ui_dir/assets/{{source_file_part}}" ]
-}
-copy("chrome_extension_assets_dist") {
-  sources = [
-    "src/assets/logo-128.png",
-    "src/chrome_extension/manifest.json",
-  ]
-  outputs = [ "$chrome_extension_dir/{{source_file_part}}" ]
-}
-
-sorcery("frontend_bundle_dist") {
-  deps = [ ":frontend_bundle" ]
-  input = "$target_out_dir/frontend_bundle.js"
-  output = "$ui_dir/frontend_bundle.js"
-}
-
-sorcery("chrome_extension_bundle_dist") {
-  deps = [ ":chrome_extension_bundle" ]
-  input = "$target_out_dir/chrome_extension_bundle.js"
-  output = "$chrome_extension_dir/chrome_extension_bundle.js"
-}
-
-sorcery("controller_bundle_dist") {
-  deps = [ ":controller_bundle" ]
-  input = "$target_out_dir/controller_bundle.js"
-  output = "$ui_dir/controller_bundle.js"
-}
-
-sorcery("engine_bundle_dist") {
-  deps = [ ":engine_bundle" ]
-  input = "$target_out_dir/engine_bundle.js"
-  output = "$ui_dir/engine_bundle.js"
-}
-
-sorcery("service_worker_bundle_dist") {
-  deps = [ ":service_worker_bundle" ]
-  input = "$target_out_dir/service_worker.js"
-  output = "$ui_dir/service_worker.js"
-}
-
-sorcery("query_bundle_dist") {
-  deps = [ ":query_bundle" ]
-  input = "$target_out_dir/query_bundle.js"
-  output = "$ui_dir/query_bundle.js"
-}
-
-copy("wasm_dist") {
-  deps = [
-    "//src/trace_processor:trace_processor.wasm($wasm_toolchain)",
-    "//tools/trace_to_text:trace_to_text.wasm($wasm_toolchain)",
-  ]
-  sources = [
-    "$root_build_dir/wasm/trace_processor.wasm",
-    "$root_build_dir/wasm/trace_to_text.wasm",
-  ]
-  outputs = [ "$ui_dir/{{source_file_part}}" ]
-}
-
-copy("wasm_gen") {
-  deps = [
-    ":dist_symlink",
-
-    # trace_processor
-    "//src/trace_processor:trace_processor.d.ts($wasm_toolchain)",
-    "//src/trace_processor:trace_processor.js($wasm_toolchain)",
-    "//src/trace_processor:trace_processor.wasm($wasm_toolchain)",
-
-    # trace_to_text
-    "//tools/trace_to_text:trace_to_text.d.ts($wasm_toolchain)",
-    "//tools/trace_to_text:trace_to_text.js($wasm_toolchain)",
-    "//tools/trace_to_text:trace_to_text.wasm($wasm_toolchain)",
-  ]
-  sources = [
-    # trace_processor
-    "$root_build_dir/wasm/trace_processor.d.ts",
-    "$root_build_dir/wasm/trace_processor.js",
-    "$root_build_dir/wasm/trace_processor.wasm",
-
-    # trace_to_text
-    "$root_build_dir/wasm/trace_to_text.d.ts",
-    "$root_build_dir/wasm/trace_to_text.js",
-    "$root_build_dir/wasm/trace_to_text.wasm",
-  ]
-  if (is_debug) {
-    sources += [
-      "$root_build_dir/wasm/trace_processor.wasm.map",
-      "$root_build_dir/wasm/trace_to_text.wasm.map",
-    ]
-  }
-  outputs = [ "$ui_gen_dir/{{source_file_part}}" ]
-}
-
-# Copy over the vulcanized legacy trace viewer.
-copy("catapult_dist") {
-  sources = [
-    "../buildtools/catapult_trace_viewer/catapult_trace_viewer.html",
-    "../buildtools/catapult_trace_viewer/catapult_trace_viewer.js",
-  ]
-  outputs = [ "$ui_dir/assets/{{source_file_part}}" ]
-}
-
-# +----------------------------------------------------------------------------+
-# | Node JS: Creates a symlink in the out directory to node_modules.           |
-# +----------------------------------------------------------------------------+
-
-perfetto_check_build_deps("check_ui_deps") {
-  args = [ "--ui" ]
-  inputs = [ "package-lock.json" ]
-}
-
-# Creates a symlink from out/xxx/ui/node_modules -> ../../../ui/node_modules.
-# This allows to run rollup and other node tools from the out/xxx directory.
-action("node_modules_symlink") {
-  deps = [ ":check_ui_deps" ]
-  script = "../gn/standalone/build_tool_wrapper.py"
-  stamp_file = "$target_out_dir/.$target_name.stamp"
-  args = [
-    "--stamp",
-    rebase_path(stamp_file, root_build_dir),
-    "/bin/ln",
-    "-fns",
-    rebase_path("node_modules", target_out_dir),
-    rebase_path("$target_out_dir/node_modules", root_build_dir),
-  ]
-  outputs = [ stamp_file ]
-}
-
-group("node_modules") {
-  deps = [ ":node_modules_symlink" ]
-}
-
-# Creates a symlink from //ui/dist -> ../../out/xxx/ui. Used only for
-# autocompletion in IDEs. The problem this is solving is that in tsconfig.json
-# we can't possibly know the path to ../../out/xxx for outDir. Instead, we set
-# outDir to "./dist" and create a symlink on the first build.
-action("dist_symlink") {
-  script = "../gn/standalone/build_tool_wrapper.py"
-  stamp_file = "$target_out_dir/.$target_name.stamp"
-  args = [
-    "--stamp",
-    rebase_path(stamp_file, root_build_dir),
-    "/bin/ln",
-    "-fns",
-    rebase_path(target_out_dir, "."),
-    rebase_path("dist", root_build_dir),
-  ]
-  inputs = [ "$root_build_dir" ]
-  outputs = [ stamp_file ]
-}
-
-group("test_scripts") {
-  deps = [
-    ":copy_tests_script",
-    ":copy_unittests_script",
-  ]
-}
-
-copy("copy_unittests_script") {
-  sources = [ "config/ui_unittests_template" ]
-  outputs = [ "$root_build_dir/ui_unittests" ]
-}
-
-copy("copy_tests_script") {
-  sources = [ "config/ui_tests_template" ]
-  outputs = [ "$root_build_dir/ui_tests" ]
-}
-
-# This target generates an map containing all the UI subresources and their
-# hashes. This is used by the service worker code for offline caching.
-# This target needs to be kept at the end of the BUILD.gn file, because of the
-# get_target_outputs() call (fails otherwise due to GN's evaluation order).
-action("gen_dist_file_map") {
-  out_file_path = "$ui_gen_dir/dist_file_map.ts"
-
-  dist_files = []
-  foreach(target, ui_dist_targets) {
-    foreach(dist_file, get_target_outputs(target)) {
-      dist_files += [ rebase_path(dist_file, root_build_dir) ]
-    }
-  }
-  deps = [ ":dist" ]
-  script = "../gn/standalone/write_ui_dist_file_map.py"
-  inputs = []
-  outputs = [ out_file_path ]
-  args = [
-           "--out",
-           rebase_path(out_file_path, root_build_dir),
-           "--strip",
-           rebase_path(ui_dir, root_build_dir),
-         ] + dist_files
-}
-
-gen_perfetto_version_header("version_ts_gen") {
-  ts_out = "$ui_gen_dir/perfetto_version.ts"
-}
diff --git a/ui/OWNERS b/ui/OWNERS
index 1ae6de1..973653f 100644
--- a/ui/OWNERS
+++ b/ui/OWNERS
@@ -1,3 +1,4 @@
 hjd@google.com
-taylori@google.com
+eseckler@google.com
 dproy@google.com
+primiano@google.com
diff --git a/ui/build b/ui/build
new file mode 100755
index 0000000..8045f2b
--- /dev/null
+++ b/ui/build
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+UI_DIR="$(cd -P ${BASH_SOURCE[0]%/*}; pwd)"
+
+$UI_DIR/node $UI_DIR/build.js "$@"
diff --git a/ui/build.js b/ui/build.js
new file mode 100644
index 0000000..c461324
--- /dev/null
+++ b/ui/build.js
@@ -0,0 +1,614 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+// This script takes care of:
+// - The build process for the whole UI and the chrome extension.
+// - The HTTP dev-server with live-reload capabilities.
+// The reason why this is a hand-rolled script rather than a conventional build
+// system is keeping incremental build fast and maintaining the set of
+// dependencies contained.
+// The only way to keep incremental build fast (i.e. O(seconds) for the
+// edit-one-line -> reload html cycles) is to run both the TypeScript compiler
+// and the rollup bundler in --watch mode. Any other attempt, leads to O(10s)
+// incremental-build times.
+// This script allows mixing build tools that support --watch mode (tsc and
+// rollup) and auto-triggering-on-file-change rules via fs.watch().
+// When invoked without any argument (e.g., for production builds), this script
+// just runs all the build tasks serially. It doesn't to do any mtime-based
+// check, it always re-runs all the tasks.
+// When invoked with --watch, it mounts a pipeline of tasks based on fs.watch()
+// and runs them together with tsc --watch and rollup --watch.
+// The output directory structure is carefully crafted so that any change to UI
+// sources causes cascading triggers of the next steps.
+// The overall build graph looks as follows:
+// +----------------+      +-----------------------------+
+// | protos/*.proto |----->| pbjs out/tsc/gen/protos.js  |--+
+// +----------------+      +-----------------------------+  |
+//                         +-----------------------------+  |
+//                         | pbts out/tsc/gen/protos.d.ts|<-+
+//                         +-----------------------------+
+//                             |
+//                             V      +-------------------------+
+// +---------+              +-----+   |  out/tsc/frontend/*.js  |
+// | ui/*.ts |------------->| tsc |-> +-------------------------+   +--------+
+// +---------+              +-----+   | out/tsc/controller/*.js |-->| rollup |
+//                            ^       +-------------------------+   +--------+
+//                +------------+      |   out/tsc/engine/*.js   |       |
+// +-----------+  |*.wasm.js   |      +-------------------------+       |
+// |ninja *.cc |->|*.wasm.d.ts |                                        |
+// +-----------+  |*.wasm      |-----------------+                      |
+//                +------------+                 |                      |
+//                                               V                      V
+// +-----------+  +------+    +------------------------------------------------+
+// | ui/*.scss |->| scss |--->|              Final out/dist/ dir               |
+// +-----------+  +------+    +------------------------------------------------+
+// +----------------------+   | +----------+ +---------+ +--------------------+|
+// | src/assets/*.png     |   | | assets/  | |*.wasm.js| | frontend_bundle.js ||
+// +----------------------+   | |  *.css   | |*.wasm   | +--------------------+|
+// | buildtools/typefaces |-->| |  *.png   | +---------+ |controller_bundle.js||
+// +----------------------+   | |  *.woff2 |             +--------------------+|
+// | buildtools/legacy_tv |   | |  tv.html |             |  engine_bundle.js  ||
+// +----------------------+   | +----------+             +--------------------+|
+//                            +------------------------------------------------+
+
+const argparse = require('argparse');
+const child_process = require('child_process');
+const fs = require('fs');
+const http = require('http');
+const path = require('path');
+const pjoin = path.join;
+
+const ROOT_DIR = path.dirname(__dirname);  // The repo root.
+const VERSION_SCRIPT = pjoin(ROOT_DIR, 'tools/write_version_header.py');
+
+const cfg = {
+  watch: false,
+  verbose: false,
+  debug: false,
+  startHttpServer: false,
+  wasmModules: ['trace_processor', 'trace_to_text'],
+  testConfigs: ['jest.unit.config.js'],
+
+  // The fields below will be changed by main() after cmdline parsing.
+  // Directory structure:
+  // out/xxx/      -> outDir     : Root build dir, for both ninja/wasm and UI.
+  //   ui/         -> outUiDir   : UI dir. All outputs from this script.
+  //    tsc/       -> outTscDir  : Transpiled .ts -> .js.
+  //      gen/     -> outGenDir  : Auto-generated .ts/.js (e.g. protos).
+  //    dist/      -> outDistDir : Final artifacts (JS bundles, assets).
+  //    chrome_extension/        : Chrome extension.
+  outDir: pjoin(ROOT_DIR, 'out/ui'),
+  outUiDir: '',
+  outTscDir: '',
+  outGenDir: '',
+  outDistRootDir: '',
+  outDistDir: '',
+  outExtDir: '',
+};
+
+const RULES = [
+  {r: /ui\/src\/assets\/(index.html)/, f: copyIntoDistRoot},
+  {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets},
+  {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets},
+  {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
+  {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
+  {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
+  {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
+  {r: /.*\/dist\/(?!service_worker).*/, f: genServiceWorkerDistHashes},
+  {r: /.*\/dist\/.*/, f: notifyLiveServer},
+];
+
+let tasks = [];
+let tasksTot = 0, tasksRan = 0;
+let serverStarted = false;
+let httpWatches = [];
+let tStart = Date.now();
+let subprocesses = [];
+
+function main() {
+  const parser = new argparse.ArgumentParser();
+  parser.addArgument('--out', {help: 'Output directory'});
+  parser.addArgument(['--watch', '-w'], {action: 'storeTrue'});
+  parser.addArgument(['--serve', '-s'], {action: 'storeTrue'});
+  parser.addArgument(['--verbose', '-v'], {action: 'storeTrue'});
+  parser.addArgument(['--no-build', '-n'], {action: 'storeTrue'});
+  parser.addArgument(['--no-wasm', '-W'], {action: 'storeTrue'});
+  parser.addArgument(['--run-tests', '-t'], {action: 'storeTrue'});
+  parser.addArgument(['--debug', '-d'], {action: 'storeTrue'});
+
+  const args = parser.parseArgs();
+  const clean = !args.no_build;
+  cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir));
+  cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean);
+  cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension'));
+  cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist'));
+  // TODO(primiano): for now distDir == distRootDir. In next CLs distDir will
+  // become dist/v1.2.3/.
+  cfg.outDistDir = cfg.outDistRootDir;
+  cfg.outTscDir = ensureDir(pjoin(cfg.outUiDir, 'tsc'));
+  cfg.outGenDir = ensureDir(pjoin(cfg.outUiDir, 'tsc/gen'));
+  cfg.watch = !!args.watch;
+  cfg.verbose = !!args.verbose;
+  cfg.debug = !!args.debug;
+  cfg.startHttpServer = args.serve;
+
+  process.on('SIGINT', () => {
+    console.log('\nSIGINT received. Killing all child processes and exiting');
+    for (const proc of subprocesses) {
+      if (proc) proc.kill('SIGINT');
+    }
+    process.exit(130);  // 130 -> Same behavior of bash when killed by SIGINT.
+  });
+
+  // Check that deps are current before starting.
+  const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
+  const depsArgs = ['--check-only', pjoin(cfg.outDir, '.check_deps'), '--ui'];
+  exec(installBuildDeps, depsArgs);
+
+  console.log('Entering', cfg.outDir);
+  process.chdir(cfg.outDir);
+
+  updateSymilnks();  // Links //ui/out -> //out/xxx/ui/
+
+  // Enqueue empty task. This is needed only for --no-build --serve. The HTTP
+  // server is started when the task queue reaches quiescence, but it takes at
+  // least one task for that.
+  addTask(() => {});
+
+  if (!args.no_build) {
+    buildWasm(args.no_wasm);
+    scanDir('ui/src/assets');
+    scanDir('ui/src/chrome_extension');
+    scanDir('buildtools/typefaces');
+    scanDir('buildtools/catapult_trace_viewer');
+    compileProtos();
+    genVersion();
+    transpileTsProject('ui');
+    bundleJs('rollup.config.js');
+
+    // ServiceWorker.
+    genServiceWorkerDistHashes();
+    transpileTsProject('ui/src/service_worker');
+    bundleJs('rollup-serviceworker.config.js');
+
+    // Watches the /dist. When changed:
+    // - Notifies the HTTP live reload clients.
+    // - Regenerates the ServiceWorker file map.
+    scanDir(cfg.outDistRootDir);
+  }
+
+  if (args.run_tests) {
+    runTests();
+  }
+}
+
+// -----------
+// Build rules
+// -----------
+
+function runTests() {
+  const args =
+      ['--rootDir', cfg.outTscDir, '--verbose', '--runInBand', '--forceExit'];
+  for (const cfgFile of cfg.testConfigs) {
+    args.push('--projects', pjoin(ROOT_DIR, 'ui/config', cfgFile));
+  }
+  if (cfg.watch) {
+    args.push('--watchAll');
+    addTask(execNode, ['jest', args, {async: true}]);
+  } else {
+    addTask(execNode, ['jest', args]);
+  }
+}
+
+function copyIntoDistRoot(src, dst) {
+  addTask(cp, [src, pjoin(cfg.outDistRootDir, dst)]);
+}
+
+function copyAssets(src, dst) {
+  addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]);
+}
+
+function compileScss() {
+  const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss');
+  const dst = pjoin(cfg.outDistDir, 'perfetto.css');
+  // In watch mode, don't exit(1) if scss fails. It can easily happen by
+  // having a typo in the css. It will still print an errror.
+  const noErrCheck = !!cfg.watch;
+  addTask(execNode, ['node-sass', ['--quiet', src, dst], {noErrCheck}]);
+}
+
+function compileProtos() {
+  const dstJs = pjoin(cfg.outGenDir, 'protos.js');
+  const dstTs = pjoin(cfg.outGenDir, 'protos.d.ts');
+  const inputs = [
+    'protos/perfetto/trace_processor/trace_processor.proto',
+    'protos/perfetto/common/trace_stats.proto',
+    'protos/perfetto/common/tracing_service_capabilities.proto',
+    'protos/perfetto/config/perfetto_config.proto',
+    'protos/perfetto/ipc/consumer_port.proto',
+    'protos/perfetto/ipc/wire_protocol.proto',
+    'protos/perfetto/metrics/metrics.proto',
+  ];
+  const pbjsArgs = [
+    '--force-number',
+    '-t',
+    'static-module',
+    '-w',
+    'commonjs',
+    '-p',
+    ROOT_DIR,
+    '-o',
+    dstJs
+  ].concat(inputs);
+  addTask(execNode, ['pbjs', pbjsArgs]);
+  const pbtsArgs = ['-p', ROOT_DIR, '-o', dstTs, dstJs];
+  addTask(execNode, ['pbts', pbtsArgs]);
+}
+
+// Generates a .ts source that defines the VERSION and SCM_REVISION constants.
+function genVersion() {
+  const cmd = 'python3';
+  const args =
+      [VERSION_SCRIPT, '--ts_out', pjoin(cfg.outGenDir, 'perfetto_version.ts')]
+  addTask(exec, [cmd, args]);
+}
+
+function updateSymilnks() {
+  mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
+  mklink(
+      pjoin(ROOT_DIR, 'ui/node_modules'), pjoin(cfg.outTscDir, 'node_modules'))
+}
+
+// Invokes ninja for building the {trace_processor, trace_to_text} Wasm modules.
+// It copies the .wasm directly into the out/dist/ dir, and the .js/.ts into
+// out/tsc/, so the typescript compiler and the bundler can pick them up.
+function buildWasm(skipWasmBuild) {
+  if (!skipWasmBuild) {
+    const gnArgs = ['gen', `--args=is_debug=${cfg.debug}`, cfg.outDir];
+    addTask(exec, [pjoin(ROOT_DIR, 'tools/gn'), gnArgs]);
+
+    const ninjaArgs = ['-C', cfg.outDir]
+    ninjaArgs.push(...cfg.wasmModules.map(x => `${x}_wasm`));
+    addTask(exec, [pjoin(ROOT_DIR, 'tools/ninja'), ninjaArgs]);
+  }
+
+  const wasmOutDir = pjoin(cfg.outDir, 'wasm');
+  for (const wasmMod of cfg.wasmModules) {
+    // The .wasm file goes directly into the dist dir (also .map in debug)
+    for (const ext of ['.wasm'].concat(cfg.debug ? ['.wasm.map'] : [])) {
+      const src = `${wasmOutDir}/${wasmMod}${ext}`;
+      addTask(cp, [src, pjoin(cfg.outDistDir, wasmMod + ext)]);
+    }
+    // The .js / .ts go into intermediates, they will be bundled by rollup.
+    for (const ext of ['.js', '.d.ts']) {
+      const fname = `${wasmMod}${ext}`;
+      addTask(cp, [pjoin(wasmOutDir, fname), pjoin(cfg.outGenDir, fname)]);
+    }
+  }
+}
+
+// This transpiles all the sources (frontend, controller, engine, extension) in
+// one go. The only project that has a dedicated invocation is service_worker.
+function transpileTsProject(project) {
+  const args = [
+    '--project',
+    pjoin(ROOT_DIR, project),
+    '--outDir',
+    cfg.outTscDir,
+  ];
+  if (cfg.watch) {
+    args.push('--watch', '--preserveWatchOutput');
+    addTask(execNode, ['tsc', args, {async: true}]);
+  } else {
+    addTask(execNode, ['tsc', args]);
+  }
+}
+
+// Creates the three {frontend, controller, engine}_bundle.js in one invocation.
+function bundleJs(cfgName) {
+  const rcfg = pjoin(ROOT_DIR, 'ui/config', cfgName)
+  const args = ['-c', rcfg, '--no-indent'];
+  args.push(...(cfg.verbose ? [] : ['--silent']));
+  if (cfg.watch) {
+    // --waitForBundleInput is so that we can run tsc --watch and rollup --watch
+    // together, without having to wait that tsc completes the first build.
+    args.push('--watch', '--waitForBundleInput', '--no-watch.clearScreen');
+    addTask(execNode, ['rollup', args, {async: true}]);
+  } else {
+    addTask(execNode, ['rollup', args]);
+  }
+}
+
+// Generates a map of {"dist_file_name" -> "sha256-01234"} used by the SW.
+function genServiceWorkerDistHashes() {
+  function write_ui_dist_file_map() {
+    const distFiles = [];
+    const skipRegex = /(service_worker\.js)|(\.map$)/;
+    walk(cfg.outDistDir, f => distFiles.push(f), skipRegex);
+    const dst = pjoin(cfg.outGenDir, 'dist_file_map.ts');
+    const cmd = 'python3';
+    const args = [
+      pjoin(ROOT_DIR, 'gn/standalone/write_ui_dist_file_map.py'),
+      '--out',
+      dst,
+      '--strip',
+      cfg.outDistDir,
+    ].concat(distFiles);
+    exec(cmd, args);
+  }
+  addTask(write_ui_dist_file_map, []);
+}
+
+function startServer() {
+  const port = 10000;
+  console.log(`Starting HTTP server on http://localhost:${port}`)
+  http.createServer(function(req, res) {
+        console.debug(req.method, req.url);
+        let uri = req.url.split('?', 1)[0];
+        if (uri.endsWith('/')) {
+          uri += 'index.html';
+        }
+
+        if (uri === '/live_reload') {
+          // Implements the Server-Side-Events protocol.
+          const head = {
+            'Content-Type': 'text/event-stream',
+            'Connection': 'keep-alive',
+            'Cache-Control': 'no-cache'
+          };
+          res.writeHead(200, head);
+          const arrayIdx = httpWatches.length;
+          // We never remove from the array, the delete leaves an undefined item
+          // around. It makes keeping track of the index easier at the cost of a
+          // small leak.
+          httpWatches.push(res);
+          req.on('close', () => delete httpWatches[arrayIdx]);
+          return;
+        }
+
+        const absPath = path.normalize(path.join(cfg.outDistRootDir, uri));
+        fs.readFile(absPath, function(err, data) {
+          if (err) {
+            res.writeHead(404);
+            res.end(JSON.stringify(err));
+            return;
+          }
+
+          const mimeMap = {
+            'html': 'text/html',
+            'css': 'text/css',
+            'js': 'application/javascript',
+            'wasm': 'application/wasm',
+          };
+          const ext = uri.split('.').pop();
+          const cType = mimeMap[ext] || 'octect/stream';
+          const head = {
+            'Content-Type': cType,
+            'Content-Length': data.length,
+            'Last-Modified': fs.statSync(absPath).mtime.toUTCString(),
+            'Cache-Control': 'no-cache',
+          };
+          res.writeHead(200, head);
+          res.end(data);
+        });
+      })
+      .listen(port);
+}
+
+// Called whenever a change in the out/dist directory is detected. It sends a
+// Server-Side-Event to the live_reload.ts script.
+function notifyLiveServer(changedFile) {
+  for (const cli of httpWatches) {
+    if (cli === undefined) continue;
+    cli.write(
+        'data: ' + path.relative(cfg.outDistRootDir, changedFile) + '\n\n');
+  }
+}
+
+function copyExtensionAssets() {
+  addTask(cp, [
+    pjoin(ROOT_DIR, 'ui/src/assets/logo-128.png'),
+    pjoin(cfg.outExtDir, 'logo-128.png')
+  ]);
+  addTask(cp, [
+    pjoin(ROOT_DIR, 'ui/src/chrome_extension/manifest.json'),
+    pjoin(cfg.outExtDir, 'manifest.json')
+  ]);
+}
+
+// -----------------------
+// Task chaining functions
+// -----------------------
+
+function addTask(func, args) {
+  const task = new Task(func, args);
+  for (const t of tasks) {
+    if (t.identity === task.identity) {
+      return;
+    }
+  }
+  tasks.push(task);
+  setTimeout(runTasks, 0);
+}
+
+function runTasks() {
+  const snapTasks = tasks.splice(0);  // snap = std::move(tasks).
+  tasksTot += snapTasks.length;
+  for (const task of snapTasks) {
+    const DIM = '\u001b[2m';
+    const BRT = '\u001b[37m';
+    const RST = '\u001b[0m';
+    const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1);
+    const ts = `[${DIM}${ms}${RST}]`;
+    const descr = task.description.substr(0, 80);
+    console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`);
+    task.func.apply(/*this=*/ undefined, task.args);
+  }
+  // Start the web server once reaching quiescence.
+  if (tasks.length === 0 && !serverStarted && cfg.startHttpServer) {
+    serverStarted = true;
+    startServer();
+  }
+}
+
+// Executes all the RULES that match the given |absPath|.
+function scanFile(absPath) {
+  console.assert(fs.existsSync(absPath));
+  console.assert(path.isAbsolute(absPath));
+  const normPath = path.relative(ROOT_DIR, absPath);
+  for (const rule of RULES) {
+    const match = rule.r.exec(normPath);
+    if (!match || match[0] !== normPath) continue;
+    const captureGroup = match.length > 1 ? match[1] : undefined;
+    rule.f(absPath, captureGroup);
+  }
+}
+
+// Walks the passed |dir| recursively and, for each file, invokes the matching
+// RULES. If --watch is used, it also installs a fs.watch() and re-triggers the
+// matching RULES on each file change.
+function scanDir(dir, regex) {
+  const filterFn = regex ? absPath => regex.test(absPath) : () => true;
+  const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir);
+  // Add a fs watch if in watch mode.
+  if (cfg.watch) {
+    fs.watch(absDir, {recursive: true}, (_eventType, fileName) => {
+      const filePath = pjoin(absDir, fileName);
+      if (!filterFn(filePath)) return;
+      if (cfg.verbose) {
+        console.log('File change detected', _eventType, filePath);
+      }
+      if (fs.existsSync(filePath)) {
+        scanFile(filePath, filterFn);
+      }
+    });
+  }
+  walk(absDir, f => {
+    if (filterFn(f)) scanFile(f);
+  });
+}
+
+function exec(cmd, args, opts) {
+  opts = opts || {};
+  opts.stdout = opts.stdout || 'inherit';
+  if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`);
+  const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']};
+  const checkExitCode = (code, signal) => {
+    if (signal === 'SIGINT' || signal === 'SIGTERM') return;
+    if (code !== 0 && !opts.noErrCheck) {
+      console.error(`${cmd} ${args.join(' ')} failed with code ${code}`);
+      process.exit(1);
+    }
+  };
+  if (opts.async) {
+    const proc = child_process.spawn(cmd, args, spwOpts);
+    const procIndex = subprocesses.length;
+    subprocesses.push(proc);
+    return new Promise((resolve, _reject) => {
+      proc.on('exit', (code, signal) => {
+        delete subprocesses[procIndex];
+        checkExitCode(code, signal);
+        resolve();
+      });
+    });
+  } else {
+    const spawnRes = child_process.spawnSync(cmd, args, spwOpts);
+    checkExitCode(spawnRes.status, spawnRes.signal);
+    return spawnRes;
+  }
+}
+
+function execNode(module, args, opts) {
+  const modPath = pjoin(ROOT_DIR, 'ui/node_modules/.bin', module);
+  const nodeBin = pjoin(ROOT_DIR, 'tools/node');
+  args = [modPath].concat(args || []);
+  const argsJson = JSON.stringify(args);
+  return exec(nodeBin, args, opts);
+}
+
+// ------------------------------------------
+// File system & subprocess utility functions
+// ------------------------------------------
+
+class Task {
+  constructor(func, args) {
+    this.func = func;
+    this.args = args || [];
+    // |identity| is used to dedupe identical tasks in the queue.
+    this.identity = JSON.stringify([this.func.name, this.args]);
+  }
+
+  get description() {
+    const ret = this.func.name.startsWith('exec') ? [] : [this.func.name];
+    const flattenedArgs = [].concat.apply([], this.args);
+    for (const arg of flattenedArgs) {
+      const argStr = `${arg}`;
+      if (argStr.startsWith('/')) {
+        ret.push(path.relative(cfg.outDir, arg));
+      } else {
+        ret.push(argStr);
+      }
+    }
+    return ret.join(' ');
+  }
+}
+
+function walk(dir, callback, skipRegex) {
+  for (const child of fs.readdirSync(dir)) {
+    const childPath = pjoin(dir, child);
+    const stat = fs.lstatSync(childPath);
+    if (skipRegex !== undefined && skipRegex.test(child)) continue;
+    if (stat.isDirectory()) {
+      walk(childPath, callback, skipRegex);
+    } else if (!stat.isSymbolicLink()) {
+      callback(childPath);
+    }
+  }
+}
+
+function ensureDir(dirPath, clean) {
+  const exists = fs.existsSync(dirPath);
+  if (exists && clean) {
+    console.log('rm', dirPath);
+    fs.rmSync(dirPath, {recursive: true});
+  }
+  if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true});
+  return dirPath;
+}
+
+function cp(src, dst) {
+  ensureDir(path.dirname(dst));
+  if (cfg.verbose) {
+    console.log(
+        'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst));
+  }
+  fs.copyFileSync(src, dst);
+}
+
+function mklink(src, dst) {
+  // If the symlink already points to the right place don't touch it. This is
+  // to avoid changing the mtime of the ui/ dir when unnecessary.
+  if (fs.existsSync(dst)) {
+    if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) {
+      return;
+    } else {
+      fs.unlinkSync(dst);
+    }
+  }
+  fs.symlinkSync(src, dst);
+}
+
+main();
diff --git a/ui/config/gn_deprecation_banner.txt b/ui/config/gn_deprecation_banner.txt
new file mode 100644
index 0000000..06aed23
--- /dev/null
+++ b/ui/config/gn_deprecation_banner.txt
@@ -0,0 +1,6 @@
+
+-------------------------------------------------------
+WARNING: building the UI through GN+ninja is deprecated.
+Going forward use the //ui/build script
+-------------------------------------------------------
+
\ No newline at end of file
diff --git a/ui/jest.unit.config.js b/ui/config/jest.headless.config.js
similarity index 77%
copy from ui/jest.unit.config.js
copy to ui/config/jest.headless.config.js
index a58dcbf..723ab25 100644
--- a/ui/jest.unit.config.js
+++ b/ui/config/jest.headless.config.js
@@ -13,8 +13,9 @@
 // limitations under the License.
 
 module.exports = {
-    "transform": {},
-    "testRegex": "_unittest.js$",
-    "testEnvironment": "node"
+  transform: {},
+  testRegex: '.*_headlesstest.js$',
+  globalSetup: './headless_setup.js',
+  globalTeardown: './headless_teardown.js',
+  testEnvironment: './headless_environment.js'
 }
-
diff --git a/ui/jest.unit.config.js b/ui/config/jest.jsdom.config.js
similarity index 87%
rename from ui/jest.unit.config.js
rename to ui/config/jest.jsdom.config.js
index a58dcbf..dd07d83 100644
--- a/ui/jest.unit.config.js
+++ b/ui/config/jest.jsdom.config.js
@@ -13,8 +13,7 @@
 // limitations under the License.
 
 module.exports = {
-    "transform": {},
-    "testRegex": "_unittest.js$",
-    "testEnvironment": "node"
+  transform: {},
+  testRegex: '_jsdomtest.js$',
+  testEnvironment: 'jsdom'
 }
-
diff --git a/ui/jest.unit.config.js b/ui/config/jest.unit.config.js
similarity index 86%
copy from ui/jest.unit.config.js
copy to ui/config/jest.unit.config.js
index a58dcbf..a2ddb95 100644
--- a/ui/jest.unit.config.js
+++ b/ui/config/jest.unit.config.js
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 module.exports = {
-    "transform": {},
-    "testRegex": "_unittest.js$",
-    "testEnvironment": "node"
+  transform: {},
+  testRegex: '.*_unittest.js$',
+  testEnvironment: 'node',
+  verbose: true,
 }
-
diff --git a/ui/config/rollup-serviceworker.config.js b/ui/config/rollup-serviceworker.config.js
new file mode 100644
index 0000000..b7aaf21
--- /dev/null
+++ b/ui/config/rollup-serviceworker.config.js
@@ -0,0 +1,41 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import commonjs from '@rollup/plugin-commonjs';
+import nodeResolve from '@rollup/plugin-node-resolve';
+import sourcemaps from 'rollup-plugin-sourcemaps';
+
+const path = require('path');
+const ROOT_DIR = path.dirname(path.dirname(__dirname));  // The repo root.
+const OUT_SYMLINK = path.join(ROOT_DIR, 'ui/out');
+
+export default [{
+  input: `${OUT_SYMLINK}/tsc/service_worker/service_worker.js`,
+  output: {
+    name: 'service_worker',
+    format: 'iife',
+    esModule: false,
+    file: `${OUT_SYMLINK}/dist/service_worker.js`,
+    sourcemap: true,
+  },
+  plugins: [
+    nodeResolve({
+      mainFields: ['browser'],
+      browser: true,
+      preferBuiltins: false,
+    }),
+    commonjs(),
+    sourcemaps(),
+  ],
+}]
diff --git a/ui/config/rollup.config.js b/ui/config/rollup.config.js
new file mode 100644
index 0000000..0055444
--- /dev/null
+++ b/ui/config/rollup.config.js
@@ -0,0 +1,68 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import commonjs from '@rollup/plugin-commonjs';
+import nodeResolve from '@rollup/plugin-node-resolve';
+import replace from 'rollup-plugin-re';
+import sourcemaps from 'rollup-plugin-sourcemaps';
+
+const path = require('path');
+const ROOT_DIR = path.dirname(path.dirname(__dirname));  // The repo root.
+const OUT_SYMLINK = path.join(ROOT_DIR, 'ui/out');
+
+function defBundle(bundle, distDir) {
+  return {
+    input: `${OUT_SYMLINK}/tsc/${bundle}/index.js`,
+    output: {
+      name: bundle,
+      format: 'iife',
+      esModule: false,
+      file: `${OUT_SYMLINK}/${distDir}/${bundle}_bundle.js`,
+      sourcemap: true,
+    },
+    plugins: [
+      nodeResolve({
+        mainFields: ['browser'],
+        browser: true,
+        preferBuiltins: false,
+      }),
+      // emscripten conditionally executes require('fs') (likewise for
+      // others), when running under node. Rollup can't find those libraries
+      // so expects these to be present in the global scope, which then fails
+      // at runtime. To avoid this we ignore require('fs') and the like.
+      commonjs({
+        ignore: [
+          'fs',
+          'path',
+          'crypto',
+        ]
+      }),
+      // Protobufjs's inquire() uses eval but that's not really needed in
+      // the browser.
+      // See https://github.com/protobufjs/protobuf.js/issues/593
+      replace({
+        patterns: [{test: /eval\(.*\(moduleName\);/g, replace: 'undefined;'}]
+      }),
+      // Translate source maps to point back to the .ts sources.
+      sourcemaps(),
+    ],
+  };
+}
+
+export default [
+  defBundle('frontend', 'dist'),
+  defBundle('controller', 'dist'),
+  defBundle('engine', 'dist'),
+  defBundle('chrome_extension', 'chrome_extension'),
+]
diff --git a/ui/config/ui_tests_template b/ui/config/ui_tests_template
deleted file mode 100755
index be02a37..0000000
--- a/ui/config/ui_tests_template
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-DIR="$(dirname "${BASH_SOURCE[0]}")"
-
-exec ui/node ui/node_modules/jest/bin/jest.js \
-  --projects=ui/jest.unit.config.js \
-  --projects=ui/jest.jsdom.config.js \
-  --projects=ui/jest.headless.config.js \
-  --roots=../$DIR/obj/ui "${@:1}"
-
diff --git a/ui/config/ui_unittests_template b/ui/config/ui_unittests_template
deleted file mode 100755
index 8375151..0000000
--- a/ui/config/ui_unittests_template
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-DIR="$(dirname "${BASH_SOURCE[0]}")"
-
-exec ui/node ui/node_modules/jest/bin/jest.js \
-  --projects=ui/jest.unit.config.js \
-  --projects=ui/jest.jsdom.config.js \
-  --roots=../$DIR/obj/ui ${@:1}
-
diff --git a/ui/deploy b/ui/deploy
index 8e11128..6eb0740 100755
--- a/ui/deploy
+++ b/ui/deploy
@@ -36,7 +36,7 @@
 CLEAN_OUT_DIR=true
 DEPLOY_PROD=false
 DEPLOY_STAGING=false
-DEBUG_BUILD=false
+DEBUG_ARG=""
 
 while [[ $# -gt 0 ]]; do
   key="$1"
@@ -54,7 +54,7 @@
       shift
       ;;
       --debug)
-      DEBUG_BUILD=true
+      DEBUG_ARG="--debug"
       shift
       ;;
       -h|--help)
@@ -75,13 +75,7 @@
 fi
 echo_and_do mkdir -p "$UI_DIST_DIR"
 
-if [ "$DEBUG_BUILD" = true ]; then
-  echo_and_do "$PROJECT_ROOT/tools/gn" gen "$OUT_DIR" --args="is_debug=true"
-else
-  echo_and_do "$PROJECT_ROOT/tools/gn" gen "$OUT_DIR" --args="is_debug=false"
-fi
-
-echo_and_do "$PROJECT_ROOT/tools/ninja" -C "$OUT_DIR" ui
+echo_and_do "$PROJECT_ROOT/ui/build" --out "$OUT_DIR" $DEBUG_ARG
 
 echo "Writing $UI_DIST_DIR/app.yaml"
 cat<<EOF > "$UI_DIST_DIR/app.yaml"
@@ -110,7 +104,7 @@
   upload: static/(.*)
 EOF
 
-echo_and_do ln -fs ../ui $UI_DIST_DIR/static
+echo_and_do rm -f $UI_DIST_DIR/static; ln -fs ../ui/dist $UI_DIST_DIR/static
 
 (
   echo_and_do cd "$UI_DIST_DIR";
diff --git a/ui/index.html b/ui/index.html
deleted file mode 100644
index ff1ad37..0000000
--- a/ui/index.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-<head>
-  <title>Perfetto UI</title>
-  <!-- See b/149573396 for CSP rationale. -->
-  <!-- TODO(b/121211019): remove script-src-elem rule once fixed. -->
-  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src-elem 'self' https://*.google.com https://*.googleusercontent.com https://www.googletagmanager.com https://www.google-analytics.com 'sha256-eYlPNiizBKy/rhHAaz06RXrXVsKmBN6tTFYwmJTvcwc='; object-src 'none'; connect-src 'self' http://127.0.0.1:9001 https://www.google-analytics.com https://*.googleapis.com blob: data:; img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; navigate-to https://*.perfetto.dev;">
-  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
-  <link href="perfetto.css" rel="stylesheet">
-  <link rel="icon" type="image/png" href="assets/favicon.png">
-  <!-- Global site tag (gtag.js) - Google Analytics -->
-  <script async src="https://www.googletagmanager.com/gtag/js?id=UA-137828855-1"></script>
-</head>
-<body>
-  <main>
-    <div class="full-page-loading-screen"></div>
-  </main>
-  <div id="main-modal" aria-hidden="true" class="modal micromodal-slide"></div>
-</body>
-</html>
-<script src="frontend_bundle.js"></script>
-<script src="https://storage.cloud.google.com/perfetto-ui-internal/is_internal_user.js" async defer></script>
diff --git a/ui/jest.headless.config.js b/ui/jest.headless.config.js
deleted file mode 100644
index e2487b8..0000000
--- a/ui/jest.headless.config.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-module.exports = {
-    "transform": {},
-    "testRegex": ".*_headlesstest.js$",
-    "globalSetup": './config/headless_setup.js',
-    "globalTeardown": './config/headless_teardown.js',
-    "testEnvironment": "./config/headless_environment.js"
-}
-
diff --git a/ui/jest.jsdom.config.js b/ui/jest.jsdom.config.js
deleted file mode 100644
index 51c9ede..0000000
--- a/ui/jest.jsdom.config.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-module.exports = {
-    "transform": {},
-    "testRegex": "_jsdomtest.js$",
-    "testEnvironment": "jsdom"
-}
-
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 5c47b71..9d805e9 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -5,28 +5,28 @@
   "requires": true,
   "dependencies": {
     "@babel/code-frame": {
-      "version": "7.12.11",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
-      "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
+      "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
       "dev": true,
       "requires": {
-        "@babel/highlight": "^7.10.4"
+        "@babel/highlight": "^7.12.13"
       }
     },
     "@babel/core": {
-      "version": "7.12.10",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz",
-      "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz",
+      "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==",
       "dev": true,
       "requires": {
-        "@babel/code-frame": "^7.10.4",
-        "@babel/generator": "^7.12.10",
-        "@babel/helper-module-transforms": "^7.12.1",
-        "@babel/helpers": "^7.12.5",
-        "@babel/parser": "^7.12.10",
-        "@babel/template": "^7.12.7",
-        "@babel/traverse": "^7.12.10",
-        "@babel/types": "^7.12.10",
+        "@babel/code-frame": "^7.12.13",
+        "@babel/generator": "^7.12.13",
+        "@babel/helper-module-transforms": "^7.12.13",
+        "@babel/helpers": "^7.12.13",
+        "@babel/parser": "^7.12.13",
+        "@babel/template": "^7.12.13",
+        "@babel/traverse": "^7.12.13",
+        "@babel/types": "^7.12.13",
         "convert-source-map": "^1.7.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.1",
@@ -36,19 +36,10 @@
         "source-map": "^0.5.0"
       },
       "dependencies": {
-        "debug": {
-          "version": "4.3.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-          "dev": true,
-          "requires": {
-            "ms": "2.1.2"
-          }
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
           "dev": true
         },
         "source-map": {
@@ -60,12 +51,12 @@
       }
     },
     "@babel/generator": {
-      "version": "7.12.11",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
-      "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
+      "version": "7.12.15",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz",
+      "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.11",
+        "@babel/types": "^7.12.13",
         "jsesc": "^2.5.1",
         "source-map": "^0.5.0"
       },
@@ -79,103 +70,103 @@
       }
     },
     "@babel/helper-function-name": {
-      "version": "7.12.11",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz",
-      "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz",
+      "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==",
       "dev": true,
       "requires": {
-        "@babel/helper-get-function-arity": "^7.12.10",
-        "@babel/template": "^7.12.7",
-        "@babel/types": "^7.12.11"
+        "@babel/helper-get-function-arity": "^7.12.13",
+        "@babel/template": "^7.12.13",
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/helper-get-function-arity": {
-      "version": "7.12.10",
-      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz",
-      "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz",
+      "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.10"
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/helper-member-expression-to-functions": {
-      "version": "7.12.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz",
-      "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz",
+      "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.7"
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/helper-module-imports": {
-      "version": "7.12.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz",
-      "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz",
+      "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.5"
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/helper-module-transforms": {
-      "version": "7.12.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz",
-      "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz",
+      "integrity": "sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA==",
       "dev": true,
       "requires": {
-        "@babel/helper-module-imports": "^7.12.1",
-        "@babel/helper-replace-supers": "^7.12.1",
-        "@babel/helper-simple-access": "^7.12.1",
-        "@babel/helper-split-export-declaration": "^7.11.0",
-        "@babel/helper-validator-identifier": "^7.10.4",
-        "@babel/template": "^7.10.4",
-        "@babel/traverse": "^7.12.1",
-        "@babel/types": "^7.12.1",
+        "@babel/helper-module-imports": "^7.12.13",
+        "@babel/helper-replace-supers": "^7.12.13",
+        "@babel/helper-simple-access": "^7.12.13",
+        "@babel/helper-split-export-declaration": "^7.12.13",
+        "@babel/helper-validator-identifier": "^7.12.11",
+        "@babel/template": "^7.12.13",
+        "@babel/traverse": "^7.12.13",
+        "@babel/types": "^7.12.13",
         "lodash": "^4.17.19"
       }
     },
     "@babel/helper-optimise-call-expression": {
-      "version": "7.12.10",
-      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
-      "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz",
+      "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.10"
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/helper-plugin-utils": {
-      "version": "7.10.4",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
-      "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz",
+      "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==",
       "dev": true
     },
     "@babel/helper-replace-supers": {
-      "version": "7.12.11",
-      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
-      "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz",
+      "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==",
       "dev": true,
       "requires": {
-        "@babel/helper-member-expression-to-functions": "^7.12.7",
-        "@babel/helper-optimise-call-expression": "^7.12.10",
-        "@babel/traverse": "^7.12.10",
-        "@babel/types": "^7.12.11"
+        "@babel/helper-member-expression-to-functions": "^7.12.13",
+        "@babel/helper-optimise-call-expression": "^7.12.13",
+        "@babel/traverse": "^7.12.13",
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/helper-simple-access": {
-      "version": "7.12.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz",
-      "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz",
+      "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.1"
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/helper-split-export-declaration": {
-      "version": "7.12.11",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz",
-      "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz",
+      "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.11"
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/helper-validator-identifier": {
@@ -185,23 +176,23 @@
       "dev": true
     },
     "@babel/helpers": {
-      "version": "7.12.5",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz",
-      "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.13.tgz",
+      "integrity": "sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ==",
       "dev": true,
       "requires": {
-        "@babel/template": "^7.10.4",
-        "@babel/traverse": "^7.12.5",
-        "@babel/types": "^7.12.5"
+        "@babel/template": "^7.12.13",
+        "@babel/traverse": "^7.12.13",
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/highlight": {
-      "version": "7.10.4",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
-      "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz",
+      "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==",
       "dev": true,
       "requires": {
-        "@babel/helper-validator-identifier": "^7.10.4",
+        "@babel/helper-validator-identifier": "^7.12.11",
         "chalk": "^2.0.0",
         "js-tokens": "^4.0.0"
       },
@@ -259,9 +250,9 @@
       }
     },
     "@babel/parser": {
-      "version": "7.12.11",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
-      "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
+      "version": "7.12.15",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz",
+      "integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==",
       "dev": true
     },
     "@babel/plugin-syntax-async-generators": {
@@ -283,12 +274,12 @@
       }
     },
     "@babel/plugin-syntax-class-properties": {
-      "version": "7.12.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz",
-      "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+      "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
       "dev": true,
       "requires": {
-        "@babel/helper-plugin-utils": "^7.10.4"
+        "@babel/helper-plugin-utils": "^7.12.13"
       }
     },
     "@babel/plugin-syntax-import-meta": {
@@ -364,54 +355,37 @@
       }
     },
     "@babel/template": {
-      "version": "7.12.7",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz",
-      "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz",
+      "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==",
       "dev": true,
       "requires": {
-        "@babel/code-frame": "^7.10.4",
-        "@babel/parser": "^7.12.7",
-        "@babel/types": "^7.12.7"
+        "@babel/code-frame": "^7.12.13",
+        "@babel/parser": "^7.12.13",
+        "@babel/types": "^7.12.13"
       }
     },
     "@babel/traverse": {
-      "version": "7.12.12",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz",
-      "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz",
+      "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==",
       "dev": true,
       "requires": {
-        "@babel/code-frame": "^7.12.11",
-        "@babel/generator": "^7.12.11",
-        "@babel/helper-function-name": "^7.12.11",
-        "@babel/helper-split-export-declaration": "^7.12.11",
-        "@babel/parser": "^7.12.11",
-        "@babel/types": "^7.12.12",
+        "@babel/code-frame": "^7.12.13",
+        "@babel/generator": "^7.12.13",
+        "@babel/helper-function-name": "^7.12.13",
+        "@babel/helper-split-export-declaration": "^7.12.13",
+        "@babel/parser": "^7.12.13",
+        "@babel/types": "^7.12.13",
         "debug": "^4.1.0",
         "globals": "^11.1.0",
         "lodash": "^4.17.19"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "4.3.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-          "dev": true,
-          "requires": {
-            "ms": "2.1.2"
-          }
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-          "dev": true
-        }
       }
     },
     "@babel/types": {
-      "version": "7.12.12",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz",
-      "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==",
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz",
+      "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==",
       "dev": true,
       "requires": {
         "@babel/helper-validator-identifier": "^7.12.11",
@@ -501,32 +475,6 @@
         "rimraf": "^3.0.0",
         "slash": "^3.0.0",
         "strip-ansi": "^6.0.0"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
-          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
-          "dev": true
-        },
-        "rimraf": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-          "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-          "dev": true,
-          "requires": {
-            "glob": "^7.1.3"
-          }
-        },
-        "strip-ansi": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
-          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^5.0.0"
-          }
-        }
       }
     },
     "@jest/environment": {
@@ -765,9 +713,9 @@
       }
     },
     "@sinonjs/commons": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz",
-      "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==",
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz",
+      "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==",
       "dev": true,
       "requires": {
         "type-detect": "4.0.8"
@@ -868,11 +816,6 @@
         "@types/node": "*"
       }
     },
-    "@types/gtag.js": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.3.tgz",
-      "integrity": "sha512-iRF/4Q3G1t0OTNMjK52tpGSQPgEsYzWQL1IdVXt9BywK6MUK3ypB7LaoEQa8sW9gPXENoU1xAyCxqJhMkB2rCA=="
-    },
     "@types/istanbul-lib-coverage": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -915,9 +858,9 @@
       "integrity": "sha512-U/wwKZT8hjstY2Q470bLMGVh/fjT2+SgBMzIILn0Z4nmgzzG6j+n18UOAxQ63aI8vXIOkQsbkAdbESt8+jIQdQ=="
     },
     "@types/node": {
-      "version": "14.14.20",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz",
-      "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A=="
+      "version": "14.14.25",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz",
+      "integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ=="
     },
     "@types/normalize-package-data": {
       "version": "2.4.0",
@@ -971,9 +914,9 @@
       "integrity": "sha512-aaOB3EL5WCWBBOYX7W1MKuzspOM9ZJI9s3iziRVypr1N+QyvIgXzCM4lm1iiOQ1VFzZioUPX9bsa23myCbKK4A=="
     },
     "@types/yargs": {
-      "version": "15.0.12",
-      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz",
-      "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==",
+      "version": "15.0.13",
+      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
+      "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
       "dev": true,
       "requires": {
         "@types/yargs-parser": "*"
@@ -1072,9 +1015,9 @@
       }
     },
     "ansi-regex": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+      "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
       "dev": true
     },
     "ansi-styles": {
@@ -1444,14 +1387,6 @@
       "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
       "dev": true
     },
-    "bufferutil": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz",
-      "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==",
-      "requires": {
-        "node-gyp-build": "^4.2.0"
-      }
-    },
     "builtin-modules": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
@@ -1476,9 +1411,9 @@
       }
     },
     "call-bind": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.1.tgz",
-      "integrity": "sha512-tvAvUwNcRikl3RVF20X9lsYmmepsovzTWeJiXjO0PkJp15uy/6xKFZOQtuiSULwYW+6ToZBprphCgWXC2dSgcQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
       "requires": {
         "function-bind": "^1.1.1",
         "get-intrinsic": "^1.0.2"
@@ -1514,17 +1449,6 @@
         }
       }
     },
-    "canvas": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz",
-      "integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==",
-      "dev": true,
-      "requires": {
-        "nan": "^2.14.0",
-        "node-pre-gyp": "^0.11.0",
-        "simple-get": "^3.0.3"
-      }
-    },
     "capture-exit": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
@@ -1550,12 +1474,6 @@
         "supports-color": "^7.1.0"
       }
     },
-    "chownr": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
-      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
-      "dev": true
-    },
     "ci-info": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
@@ -1594,40 +1512,6 @@
         "string-width": "^4.2.0",
         "strip-ansi": "^6.0.0",
         "wrap-ansi": "^6.2.0"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
-          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
-          "dev": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-          "dev": true
-        },
-        "string-width": {
-          "version": "4.2.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
-          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
-          "dev": true,
-          "requires": {
-            "emoji-regex": "^8.0.0",
-            "is-fullwidth-code-point": "^3.0.0",
-            "strip-ansi": "^6.0.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
-          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^5.0.0"
-          }
-        }
       }
     },
     "co": {
@@ -1764,6 +1648,12 @@
         "which": "^1.2.9"
       },
       "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        },
         "which": {
           "version": "1.3.1",
           "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -1831,12 +1721,12 @@
       }
     },
     "debug": {
-      "version": "3.2.7",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
-      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+      "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
       "dev": true,
       "requires": {
-        "ms": "^2.1.1"
+        "ms": "2.1.2"
       }
     },
     "decamelize": {
@@ -1851,21 +1741,6 @@
       "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
       "dev": true
     },
-    "decompress-response": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
-      "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
-      "dev": true,
-      "requires": {
-        "mimic-response": "^2.0.0"
-      }
-    },
-    "deep-extend": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
-      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
-      "dev": true
-    },
     "deep-freeze": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz",
@@ -1945,12 +1820,6 @@
       "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
       "dev": true
     },
-    "detect-libc": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
-      "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
-      "dev": true
-    },
     "detect-newline": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -2024,22 +1893,24 @@
       }
     },
     "es-abstract": {
-      "version": "1.18.0-next.1",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
-      "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+      "version": "1.18.0-next.2",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
+      "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
       "requires": {
+        "call-bind": "^1.0.2",
         "es-to-primitive": "^1.2.1",
         "function-bind": "^1.1.1",
+        "get-intrinsic": "^1.0.2",
         "has": "^1.0.3",
         "has-symbols": "^1.0.1",
         "is-callable": "^1.2.2",
-        "is-negative-zero": "^2.0.0",
+        "is-negative-zero": "^2.0.1",
         "is-regex": "^1.1.1",
-        "object-inspect": "^1.8.0",
+        "object-inspect": "^1.9.0",
         "object-keys": "^1.1.1",
-        "object.assign": "^4.1.1",
-        "string.prototype.trimend": "^1.0.1",
-        "string.prototype.trimstart": "^1.0.1"
+        "object.assign": "^4.1.2",
+        "string.prototype.trimend": "^1.0.3",
+        "string.prototype.trimstart": "^1.0.3"
       }
     },
     "es-to-primitive": {
@@ -2425,15 +2296,6 @@
         "map-cache": "^0.2.2"
       }
     },
-    "fs-minipass": {
-      "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
-      "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
-      "dev": true,
-      "requires": {
-        "minipass": "^2.6.0"
-      }
-    },
     "fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2441,9 +2303,9 @@
       "dev": true
     },
     "fsevents": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz",
-      "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
       "dev": true,
       "optional": true
     },
@@ -2457,6 +2319,17 @@
         "inherits": "~2.0.0",
         "mkdirp": ">=0.5 0",
         "rimraf": "2"
+      },
+      "dependencies": {
+        "rimraf": {
+          "version": "2.7.1",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+          "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+          "dev": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        }
       }
     },
     "function-bind": {
@@ -2478,6 +2351,43 @@
         "string-width": "^1.0.1",
         "strip-ansi": "^3.0.1",
         "wide-align": "^1.1.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
       }
     },
     "gaze": {
@@ -2502,9 +2412,9 @@
       "dev": true
     },
     "get-intrinsic": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz",
-      "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+      "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
       "requires": {
         "function-bind": "^1.1.1",
         "has": "^1.0.3",
@@ -2579,9 +2489,9 @@
       }
     },
     "graceful-fs": {
-      "version": "4.2.4",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
-      "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz",
+      "integrity": "sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw==",
       "dev": true
     },
     "growly": {
@@ -2622,6 +2532,14 @@
       "dev": true,
       "requires": {
         "ansi-regex": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        }
       }
     },
     "has-flag": {
@@ -2733,6 +2651,17 @@
       "requires": {
         "agent-base": "^4.3.0",
         "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.7",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+          "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
       }
     },
     "human-signals": {
@@ -2750,15 +2679,6 @@
         "safer-buffer": ">= 2.1.2 < 3"
       }
     },
-    "ignore-walk": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
-      "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
-      "dev": true,
-      "requires": {
-        "minimatch": "^3.0.4"
-      }
-    },
     "immer": {
       "version": "1.12.1",
       "resolved": "https://registry.npmjs.org/immer/-/immer-1.12.1.tgz",
@@ -2810,12 +2730,6 @@
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
-    "ini": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
-      "dev": true
-    },
     "ip-regex": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
@@ -2863,9 +2777,9 @@
       "dev": true
     },
     "is-callable": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
-      "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
+      "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
     },
     "is-ci": {
       "version": "2.0.0",
@@ -2949,13 +2863,10 @@
       "dev": true
     },
     "is-fullwidth-code-point": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
-      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
-      "dev": true,
-      "requires": {
-        "number-is-nan": "^1.0.0"
-      }
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true
     },
     "is-generator-fn": {
       "version": "2.1.0",
@@ -3004,10 +2915,11 @@
       }
     },
     "is-regex": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
-      "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
+      "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
       "requires": {
+        "call-bind": "^1.0.2",
         "has-symbols": "^1.0.1"
       }
     },
@@ -3105,14 +3017,6 @@
         "@istanbuljs/schema": "^0.1.2",
         "istanbul-lib-coverage": "^3.0.0",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
-        }
       }
     },
     "istanbul-lib-report": {
@@ -3135,23 +3039,6 @@
         "debug": "^4.1.1",
         "istanbul-lib-coverage": "^3.0.0",
         "source-map": "^0.6.1"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "4.3.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-          "dev": true,
-          "requires": {
-            "ms": "2.1.2"
-          }
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-          "dev": true
-        }
       }
     },
     "istanbul-reports": {
@@ -3379,14 +3266,6 @@
         "jest-mock": "^25.5.0",
         "jest-util": "^25.5.0",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
-        }
       }
     },
     "jest-get-type": {
@@ -3619,14 +3498,6 @@
         "natural-compare": "^1.4.0",
         "pretty-format": "^25.5.0",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
-        }
       }
     },
     "jest-util": {
@@ -3773,9 +3644,9 @@
       "dev": true
     },
     "json5": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
-      "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
+      "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
       "dev": true,
       "requires": {
         "minimist": "^1.2.5"
@@ -3913,14 +3784,6 @@
       "requires": {
         "pseudomap": "^1.0.2",
         "yallist": "^2.1.2"
-      },
-      "dependencies": {
-        "yallist": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
-          "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
-          "dev": true
-        }
       }
     },
     "magic-string": {
@@ -3939,14 +3802,6 @@
       "dev": true,
       "requires": {
         "semver": "^6.0.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
-        }
       }
     },
     "makeerror": {
@@ -4061,9 +3916,9 @@
       "integrity": "sha512-2VDso2a22jWPpqwuWT/4RomVpoU3Bl9qF9D01xzwlNp5UVsImeA0gY4nSpF44vqcQtQOtkiMUV9EZkAJSRxBsg=="
     },
     "mime": {
-      "version": "2.4.7",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz",
-      "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==",
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz",
+      "integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==",
       "dev": true
     },
     "mime-db": {
@@ -4087,12 +3942,6 @@
       "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
       "dev": true
     },
-    "mimic-response": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
-      "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
-      "dev": true
-    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -4108,25 +3957,6 @@
       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
       "dev": true
     },
-    "minipass": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
-      "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
-      "dev": true,
-      "requires": {
-        "safe-buffer": "^5.1.2",
-        "yallist": "^3.0.0"
-      }
-    },
-    "minizlib": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
-      "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
-      "dev": true,
-      "requires": {
-        "minipass": "^2.9.0"
-      }
-    },
     "mithril": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/mithril/-/mithril-2.0.4.tgz",
@@ -4163,9 +3993,9 @@
       }
     },
     "ms": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
       "dev": true
     },
     "nan": {
@@ -4199,17 +4029,6 @@
       "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
       "dev": true
     },
-    "needle": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz",
-      "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==",
-      "dev": true,
-      "requires": {
-        "debug": "^3.2.6",
-        "iconv-lite": "^0.4.4",
-        "sax": "^1.2.4"
-      }
-    },
     "nice-try": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -4236,13 +4055,13 @@
         "which": "1"
       },
       "dependencies": {
-        "nopt": {
-          "version": "3.0.6",
-          "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
-          "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+        "rimraf": {
+          "version": "2.7.1",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+          "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
           "dev": true,
           "requires": {
-            "abbrev": "1"
+            "glob": "^7.1.3"
           }
         },
         "semver": {
@@ -4251,17 +4070,6 @@
           "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
           "dev": true
         },
-        "tar": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
-          "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
-          "dev": true,
-          "requires": {
-            "block-stream": "*",
-            "fstream": "^1.0.12",
-            "inherits": "2"
-          }
-        },
         "which": {
           "version": "1.3.1",
           "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -4273,11 +4081,6 @@
         }
       }
     },
-    "node-gyp-build": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
-      "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
-    },
     "node-int64": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -4304,13 +4107,6 @@
         "which": "^1.3.1"
       },
       "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true,
-          "optional": true
-        },
         "which": {
           "version": "1.3.1",
           "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -4323,24 +4119,6 @@
         }
       }
     },
-    "node-pre-gyp": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
-      "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
-      "dev": true,
-      "requires": {
-        "detect-libc": "^1.0.2",
-        "mkdirp": "^0.5.1",
-        "needle": "^2.2.1",
-        "nopt": "^4.0.1",
-        "npm-packlist": "^1.1.6",
-        "npmlog": "^4.0.2",
-        "rc": "^1.2.7",
-        "rimraf": "^2.6.1",
-        "semver": "^5.3.0",
-        "tar": "^4"
-      }
-    },
     "node-sass": {
       "version": "4.14.1",
       "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz",
@@ -4366,6 +4144,12 @@
         "true-case-path": "^1.0.2"
       },
       "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
         "ansi-styles": {
           "version": "2.2.1",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
@@ -4395,6 +4179,15 @@
             "which": "^1.2.9"
           }
         },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
         "supports-color": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
@@ -4418,13 +4211,12 @@
       "integrity": "sha512-Wm+otW+drKzdqlSPoSwj34tUEq/Xj1gX6Cr2avrykvTW4IY7d3ngLmP+PErALzS0s9nYRokXvYDM54sbFvLlDA=="
     },
     "nopt": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
-      "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
       "dev": true,
       "requires": {
-        "abbrev": "1",
-        "osenv": "^0.1.4"
+        "abbrev": "1"
       }
     },
     "normalize-package-data": {
@@ -4437,6 +4229,14 @@
         "resolve": "^1.10.0",
         "semver": "2 || 3 || 4 || 5",
         "validate-npm-package-license": "^3.0.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        }
       }
     },
     "normalize-path": {
@@ -4445,32 +4245,6 @@
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
       "dev": true
     },
-    "npm-bundled": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
-      "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
-      "dev": true,
-      "requires": {
-        "npm-normalize-package-bin": "^1.0.1"
-      }
-    },
-    "npm-normalize-package-bin": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
-      "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
-      "dev": true
-    },
-    "npm-packlist": {
-      "version": "1.4.8",
-      "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
-      "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
-      "dev": true,
-      "requires": {
-        "ignore-walk": "^3.0.1",
-        "npm-bundled": "^1.0.1",
-        "npm-normalize-package-bin": "^1.0.1"
-      }
-    },
     "npm-run-path": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
@@ -4682,9 +4456,9 @@
       "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
     },
     "parse-json": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
-      "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
@@ -4878,9 +4652,9 @@
       },
       "dependencies": {
         "@types/node": {
-          "version": "13.13.39",
-          "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.39.tgz",
-          "integrity": "sha512-wct+WgRTTkBm2R3vbrFOqyZM5w0g+D8KnhstG9463CJBVC3UVZHMToge7iMBR1vDl/I+NWFHUeK9X+JcF0rWKw=="
+          "version": "13.13.41",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.41.tgz",
+          "integrity": "sha512-qLT9IvHiXJfdrje9VmsLzun7cQ65obsBTmtU3EOnCSLFOoSHx1hpiRHoBnpdbyFqnzqdUUIv81JcEJQCB8un9g=="
         }
       }
     },
@@ -4934,21 +4708,15 @@
         "ws": "^6.1.0"
       },
       "dependencies": {
-        "debug": {
-          "version": "4.3.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+        "rimraf": {
+          "version": "2.7.1",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+          "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
           "dev": true,
           "requires": {
-            "ms": "2.1.2"
+            "glob": "^7.1.3"
           }
         },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-          "dev": true
-        },
         "ws": {
           "version": "6.2.1",
           "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
@@ -4966,18 +4734,6 @@
       "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
       "dev": true
     },
-    "rc": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
-      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
-      "dev": true,
-      "requires": {
-        "deep-extend": "^0.6.0",
-        "ini": "~1.3.0",
-        "minimist": "^1.2.0",
-        "strip-json-comments": "~2.0.1"
-      }
-    },
     "react-is": {
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -5213,29 +4969,80 @@
       "dev": true
     },
     "rimraf": {
-      "version": "2.7.1",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
-      "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
       "dev": true,
       "requires": {
         "glob": "^7.1.3"
       }
     },
     "rollup": {
-      "version": "2.36.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.36.1.tgz",
-      "integrity": "sha512-eAfqho8dyzuVvrGqpR0ITgEdq0zG2QJeWYh+HeuTbpcaXk8vNFc48B7bJa1xYosTCKx0CuW+447oQOW8HgBIZQ==",
+      "version": "2.38.5",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.5.tgz",
+      "integrity": "sha512-VoWt8DysFGDVRGWuHTqZzT02J0ASgjVq/hPs9QcBOGMd7B+jfTr/iqMVEyOi901rE3xq+Deq66GzIT1yt7sGwQ==",
       "dev": true,
       "requires": {
-        "fsevents": "~2.1.2"
+        "fsevents": "~2.3.1"
+      }
+    },
+    "rollup-plugin-re": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/rollup-plugin-re/-/rollup-plugin-re-1.0.7.tgz",
+      "integrity": "sha1-/hdHBO1ZzahMrwK9ATtYLm/apPY=",
+      "dev": true,
+      "requires": {
+        "magic-string": "^0.16.0",
+        "rollup-pluginutils": "^2.0.1"
       },
       "dependencies": {
-        "fsevents": {
-          "version": "2.1.3",
-          "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
-          "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+        "magic-string": {
+          "version": "0.16.0",
+          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.16.0.tgz",
+          "integrity": "sha1-lw67DacZMwEoX7GqZQ85vdgetFo=",
           "dev": true,
-          "optional": true
+          "requires": {
+            "vlq": "^0.2.1"
+          }
+        }
+      }
+    },
+    "rollup-plugin-sourcemaps": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz",
+      "integrity": "sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==",
+      "dev": true,
+      "requires": {
+        "@rollup/pluginutils": "^3.0.9",
+        "source-map-resolve": "^0.6.0"
+      },
+      "dependencies": {
+        "source-map-resolve": {
+          "version": "0.6.0",
+          "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
+          "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
+          "dev": true,
+          "requires": {
+            "atob": "^2.1.2",
+            "decode-uri-component": "^0.2.0"
+          }
+        }
+      }
+    },
+    "rollup-pluginutils": {
+      "version": "2.8.2",
+      "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
+      "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
+      "dev": true,
+      "requires": {
+        "estree-walker": "^0.6.1"
+      },
+      "dependencies": {
+        "estree-walker": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+          "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+          "dev": true
         }
       }
     },
@@ -5265,26 +5072,6 @@
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "dev": true
     },
-    "sander": {
-      "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
-      "integrity": "sha1-dB4kXiMfB8r7b98PEzrfohalAq0=",
-      "dev": true,
-      "requires": {
-        "es6-promise": "^3.1.2",
-        "graceful-fs": "^4.1.3",
-        "mkdirp": "^0.5.1",
-        "rimraf": "^2.5.2"
-      },
-      "dependencies": {
-        "es6-promise": {
-          "version": "3.3.1",
-          "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
-          "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=",
-          "dev": true
-        }
-      }
-    },
     "sane": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz",
@@ -5438,12 +5225,6 @@
         "yargs": "^13.3.2"
       },
       "dependencies": {
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
-          "dev": true
-        },
         "ansi-styles": {
           "version": "3.2.1",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -5586,12 +5367,6 @@
         }
       }
     },
-    "sax": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
-      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
-      "dev": true
-    },
     "saxes": {
       "version": "3.1.11",
       "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
@@ -5623,9 +5398,9 @@
       }
     },
     "semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
       "dev": true
     },
     "set-blocking": {
@@ -5685,23 +5460,6 @@
       "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
       "dev": true
     },
-    "simple-concat": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
-      "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
-      "dev": true
-    },
-    "simple-get": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
-      "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
-      "dev": true,
-      "requires": {
-        "decompress-response": "^4.2.0",
-        "once": "^1.3.1",
-        "simple-concat": "^1.0.0"
-      }
-    },
     "sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -5842,18 +5600,6 @@
         }
       }
     },
-    "sorcery": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
-      "integrity": "sha1-iukK19fLBfxZ8asMY3hF1cFaUrc=",
-      "dev": true,
-      "requires": {
-        "buffer-crc32": "^0.2.5",
-        "minimist": "^1.2.0",
-        "sander": "^0.5.0",
-        "sourcemap-codec": "^1.3.0"
-      }
-    },
     "source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -5884,9 +5630,9 @@
       }
     },
     "source-map-url": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
-      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
+      "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
       "dev": true
     },
     "sourcemap-codec": {
@@ -6022,12 +5768,6 @@
         "strip-ansi": "^5.2.0"
       },
       "dependencies": {
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
-          "dev": true
-        },
         "strip-ansi": {
           "version": "5.2.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
@@ -6040,14 +5780,14 @@
       }
     },
     "string-width": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
-      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+      "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
       "dev": true,
       "requires": {
-        "code-point-at": "^1.0.0",
-        "is-fullwidth-code-point": "^1.0.0",
-        "strip-ansi": "^3.0.0"
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.0"
       }
     },
     "string.prototype.trimend": {
@@ -6086,12 +5826,20 @@
       }
     },
     "strip-ansi": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
-      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+      "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
       "dev": true,
       "requires": {
-        "ansi-regex": "^2.0.0"
+        "ansi-regex": "^5.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        }
       }
     },
     "strip-bom": {
@@ -6121,12 +5869,6 @@
         "get-stdin": "^4.0.1"
       }
     },
-    "strip-json-comments": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
-      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
-      "dev": true
-    },
     "supports-color": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -6153,18 +5895,14 @@
       "dev": true
     },
     "tar": {
-      "version": "4.4.13",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
-      "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
+      "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
       "dev": true,
       "requires": {
-        "chownr": "^1.1.1",
-        "fs-minipass": "^1.2.5",
-        "minipass": "^2.8.6",
-        "minizlib": "^1.2.1",
-        "mkdirp": "^0.5.0",
-        "safe-buffer": "^5.1.2",
-        "yallist": "^3.0.3"
+        "block-stream": "*",
+        "fstream": "^1.0.12",
+        "inherits": "2"
       }
     },
     "terminal-link": {
@@ -6356,6 +6094,12 @@
           "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
           "dev": true
         },
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        },
         "supports-color": {
           "version": "5.5.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -6506,14 +6250,6 @@
       "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
       "dev": true
     },
-    "utf-8-validate": {
-      "version": "5.0.4",
-      "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.4.tgz",
-      "integrity": "sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==",
-      "requires": {
-        "node-gyp-build": "^4.2.0"
-      }
-    },
     "util": {
       "version": "0.12.3",
       "resolved": "https://registry.npmjs.org/util/-/util-0.12.3.tgz",
@@ -6578,6 +6314,12 @@
         "extsprintf": "^1.2.0"
       }
     },
+    "vlq": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz",
+      "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==",
+      "dev": true
+    },
     "w3c-hr-time": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
@@ -6675,6 +6417,39 @@
       "dev": true,
       "requires": {
         "string-width": "^1.0.2 || 2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
       }
     },
     "word-wrap": {
@@ -6692,40 +6467,6 @@
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
         "strip-ansi": "^6.0.0"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
-          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
-          "dev": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-          "dev": true
-        },
-        "string-width": {
-          "version": "4.2.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
-          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
-          "dev": true,
-          "requires": {
-            "emoji-regex": "^8.0.0",
-            "is-fullwidth-code-point": "^3.0.0",
-            "strip-ansi": "^6.0.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
-          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^5.0.0"
-          }
-        }
       }
     },
     "wrappy": {
@@ -6747,9 +6488,9 @@
       }
     },
     "ws": {
-      "version": "7.4.2",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
-      "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==",
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
+      "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
       "dev": true
     },
     "xml-name-validator": {
@@ -6771,9 +6512,9 @@
       "dev": true
     },
     "yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
       "dev": true
     },
     "yargs": {
@@ -6793,40 +6534,6 @@
         "which-module": "^2.0.0",
         "y18n": "^4.0.0",
         "yargs-parser": "^18.1.2"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
-          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
-          "dev": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-          "dev": true
-        },
-        "string-width": {
-          "version": "4.2.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
-          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
-          "dev": true,
-          "requires": {
-            "emoji-regex": "^8.0.0",
-            "is-fullwidth-code-point": "^3.0.0",
-            "strip-ansi": "^6.0.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
-          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^5.0.0"
-          }
-        }
       }
     },
     "yargs-parser": {
diff --git a/ui/package.json b/ui/package.json
index 955a33c..0013cae 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -10,13 +10,11 @@
     "@tsundoku/micromodal_types": "^0.0.1",
     "@types/chrome": "0.0.86",
     "@types/color-convert": "^1.9.0",
-    "@types/gtag.js": "0.0.3",
     "@types/mithril": "^1.1.17",
     "@types/node": "^14.0.10",
     "@types/pako": "^1.0.1",
     "@types/uuid": "^3.4.9",
     "@types/w3c-web-usb": "^1.0.4",
-    "bufferutil": "^4.0.1",
     "color-convert": "^2.0.1",
     "custom_utils": "file:src/base/utils",
     "devtools-protocol": "0.0.681549",
@@ -27,7 +25,6 @@
     "noice-json-rpc": "^1.2.0",
     "pako": "^1.0.11",
     "protobufjs": "^6.9.0",
-    "utf-8-validate": "^5.0.2",
     "util": "^0.12.3",
     "uuid": "^3.4.0"
   },
@@ -40,11 +37,15 @@
     "jest": "^25.5.4",
     "node-sass": "^4.14.1",
     "puppeteer": "^1.20.0",
-    "rollup": "^2.3.4",
-    "sorcery": "^0.10.0",
+    "rollup": "^2.38.5",
+    "rollup-plugin-re": "^1.0.7",
+    "rollup-plugin-sourcemaps": "^0.6.3",
     "tslib": "^1.13.0",
     "tslint": "^5.20.1",
-    "typescript": "^3.9.3",
-    "canvas": "^2.6.1"
+    "typescript": "^3.9.3"
+  },
+  "scripts": {
+    "build": "node build.js",
+    "test": "node build.js --run-tests"
   }
 }
diff --git a/ui/query.html b/ui/query.html
deleted file mode 100644
index bd45758..0000000
--- a/ui/query.html
+++ /dev/null
@@ -1,84 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-<head>
-  <title>Perfetto - Query</title>
-  <link rel="icon" type="image/png" href="assets/favicon.png">
-  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport"/>
-  <style>
-    * {
-      box-sizing: border-box;
-    }
-
-    #root {
-      margin: 0 auto;
-      max-width: 1200px;
-    }
-
-    input[type=text] {
-      font-size: 150%;
-      width: 100%;
-    }
-
-    .query-list {
-    }
-
-    .query {
-      margin: 2rem 0;
-      border-left: 10px grey;
-      display: grid;
-      grid-template: "text    time"
-                     "content content";
-    }
-
-    .query-content {
-      grid-area: content;
-    }
-
-    .query-content:empty {
-      background: grey;
-    }
-
-    .query-text {
-      grid-area: text;
-      font-family: monospace;
-    }
-
-    .query-time{
-      grid-area: time;
-    }
-
-    table {
-      width: 100%;
-      margin-left: -1rem;
-    }
-
-    tr {
-    }
-
-    tr:hover {
-      background-color: #0000000a;
-    }
-
-    th {
-      font-weight: normal;
-      font-style: italic;
-    }
-
-    td {
-      padding: 0.25rem 0.25rem;
-    }
-
-    tr > td:first-child {
-      padding-left: 1rem;
-    }
-
-    tr > td:last-child {
-      padding-right: 1rem;
-    }
-  </style>
-</head>
-<body>
-  <div id="root"></div>
-  <script src="/query_bundle.js"></script>
-</body>
-</html>
diff --git a/ui/rollup.config.js b/ui/rollup.config.js
deleted file mode 100644
index 421e494..0000000
--- a/ui/rollup.config.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import commonjs from '@rollup/plugin-commonjs';
-import nodeResolve from '@rollup/plugin-node-resolve';
-
-export default {
-  output: {name: 'perfetto'},
-  plugins:
-      [
-        nodeResolve({
-          mainFields: ['browser'],
-          browser: true,
-          preferBuiltins: false,
-        }),
-
-        // emscripten conditionally executes require('fs') (likewise for
-        // others), when running under node. Rollup can't find those libraries
-        // so expects these to be present in the global scope, which then fails
-        // at runtime. To avoid this we ignore require('fs') and the like.
-        commonjs({
-          ignore: [
-            'fs',
-            'path',
-            'crypto',
-          ]
-        }),
-      ],
-}
diff --git a/ui/run-dev-server b/ui/run-dev-server
index 9faf567..6de8927 100755
--- a/ui/run-dev-server
+++ b/ui/run-dev-server
@@ -14,34 +14,4 @@
 # limitations under the License.
 
 UI_DIR="$(cd -P ${BASH_SOURCE[0]%/*}; pwd)"
-ROOT_DIR=$(dirname "$UI_DIR")
-
-if [ -z "$1" ]; then
-  echo "ERROR: no output directory specified."
-  echo "Usage: $0 out/mac_debug"
-  exit 127
-fi
-OUT_DIR="$1"
-
-echo 'Initial build:'
-$ROOT_DIR/tools/ninja -C $OUT_DIR ui
-
-UI_OUT_DIR="$OUT_DIR/ui"
-if [ ! -d $UI_OUT_DIR ]; then
-  echo "ERROR: cannot find the UI output directory (\"$UI_OUT_DIR\")."
-  echo "Did you run ninja ui?"
-  exit 127
-fi
-
-$ROOT_DIR/tools/dev_server \
-  -p 10000 \
-  -i $ROOT_DIR/.git \
-  -i $ROOT_DIR/src/traced \
-  -i $ROOT_DIR/buildtools \
-  -i $ROOT_DIR/out \
-  -i $ROOT_DIR/infra \
-  -i $ROOT_DIR/ui/node_modules \
-  -s $UI_OUT_DIR \
-  "$ROOT_DIR/tools/ninja -C $OUT_DIR ui"
-
-
+$UI_DIR/node $UI_DIR/build.js --serve --watch
diff --git a/ui/run-tests b/ui/run-tests
new file mode 100755
index 0000000..d06f7d2
--- /dev/null
+++ b/ui/run-tests
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+UI_DIR="$(cd -P ${BASH_SOURCE[0]%/*}; pwd)"
+
+$UI_DIR/node $UI_DIR/build.js --run-tests "$@"
diff --git a/ui/src/assets/index.html b/ui/src/assets/index.html
new file mode 100644
index 0000000..3237423
--- /dev/null
+++ b/ui/src/assets/index.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html lang="en-us">
+<head>
+  <title>Perfetto UI</title>
+  <!-- See b/149573396 for CSP rationale. -->
+  <!-- TODO(b/121211019): remove script-src-elem rule once fixed. -->
+  <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'sha256-LirUKeorCU4uRNtNzr8tlB11uy8rzrdmqHCX38JSwHY='; script-src 'self' https://*.google.com https://*.googleusercontent.com https://www.googletagmanager.com https://www.google-analytics.com; object-src 'none'; connect-src 'self' http://127.0.0.1:9001 https://www.google-analytics.com https://*.googleapis.com blob: data:; img-src 'self' data: blob: https://www.google-analytics.com https://www.googletagmanager.com; navigate-to https://*.perfetto.dev;">
+
+  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
+  <link href="perfetto.css" rel="stylesheet">
+  <link rel="icon" type="image/png" href="assets/favicon.png">
+</head>
+<body>
+  <main>
+    <div class="full-page-loading-screen"></div>
+  </main>
+  <div id="main-modal" aria-hidden="true" class="modal micromodal-slide"></div>
+</body>
+</html>
+<script src="frontend_bundle.js"></script>
diff --git a/ui/src/frontend/analytics.ts b/ui/src/frontend/analytics.ts
index dc6f051..5abf7ed 100644
--- a/ui/src/frontend/analytics.ts
+++ b/ui/src/frontend/analytics.ts
@@ -19,20 +19,37 @@
 
 export function initAnalytics() {
   // Only initialize logging on prod or staging
-  if (window.location.origin.endsWith('.perfetto.dev') ||
+  if (window.location.origin.startsWith('http://localhost:') ||
+      window.location.origin.endsWith('.perfetto.dev') ||
       window.location.origin.endsWith('staging-dot-perfetto-ui.appspot.com')) {
-    return new Analytics();
+    return new AnalyticsImpl();
   }
   return new NullAnalytics();
 }
 
-export class NullAnalytics {
+const gtagGlobals = window as {} as {
+  // tslint:disable-next-line no-any
+  dataLayer: any[];
+  gtag: (command: string, event: string|Date, args?: {}) => void;
+};
+
+export interface Analytics {
+  initialize(): void;
+  updatePath(_: string): void;
+  logEvent(_x: TraceCategories|null, _y: string): void;
+  logError(_x: string, _y?: boolean): void;
+}
+
+export class NullAnalytics implements Analytics {
+  initialize() {}
   updatePath(_: string) {}
   logEvent(_x: TraceCategories|null, _y: string) {}
   logError(_x: string) {}
 }
 
-export class Analytics {
+class AnalyticsImpl implements Analytics {
+  private initialized_ = false;
+
   constructor() {
     // The code below is taken from the official Google Analytics docs [1] and
     // adapted to TypeScript. We have it here rather than as an inline script
@@ -40,31 +57,54 @@
     // play nicely with the CSP policy, at least in Firefox (Firefox doesn't
     // support all CSP 3 features we use).
     // [1] https://developers.google.com/analytics/devguides/collection/gtagjs .
-    const gtagGlobals = window as {} as {
-      dataLayer: IArguments[],
-      gtag: () => void,
-    };
     gtagGlobals.dataLayer = gtagGlobals.dataLayer || [];
-    if (gtagGlobals.gtag === undefined) {
-      gtagGlobals.gtag = () => gtagGlobals.dataLayer.push(arguments);
+
+    // tslint:disable-next-line no-any
+    function gtagFunction(..._: any[]) {
+      // This needs to be a function and not a lambda. |arguments| behaves
+      // slightly differently in a lambda and breaks GA.
+      gtagGlobals.dataLayer.push(arguments);
     }
-    gtag('js', new Date());
+    gtagGlobals.gtag = gtagFunction;
+    gtagGlobals.gtag('js', new Date());
+  }
+
+  // This is callled only after the script that sets isInternalUser loads.
+  // It is fine to call updatePath() and log*() functions before initialize().
+  // The gtag() function internally enqueues all requests into |dataLayer|.
+  initialize() {
+    if (this.initialized_) return;
+    this.initialized_ = true;
+    const script = document.createElement('script');
+    script.src = 'https://www.googletagmanager.com/gtag/js?id=' + ANALYTICS_ID;
+    script.defer = true;
+    document.head.appendChild(script);
+    const route = globals.state.route || '/';
+    console.log(
+        `GA initialized. route=${route}`,
+        `isInternalUser=${globals.isInternalUser}`);
+    // GA's reccomendation for SPAs is to disable automatic page views and
+    // manually send page_view events. See:
+    // https://developers.google.com/analytics/devguides/collection/gtagjs/pages#manual_pageviews
+    gtagGlobals.gtag('config', ANALYTICS_ID, {
+      anonymize_ip: true,
+      page_path: route,
+      referrer: document.referrer.split('?')[0],
+      send_page_view: false,
+      dimension1: globals.isInternalUser ? '1' : '0',
+    });
+    this.updatePath(route);
   }
 
   updatePath(path: string) {
-    gtag('config', ANALYTICS_ID, {
-      'anonymize_ip': true,
-      'page_path': path,
-      'referrer': document.referrer.split('?')[0],
-      'dimension1': globals.isInternalUser,
-    });
+    gtagGlobals.gtag('event', 'page_view', {page_path: path});
   }
 
   logEvent(category: TraceCategories|null, event: string) {
-    gtag('event', event, {'event_category': category});
+    gtagGlobals.gtag('event', event, {event_category: category});
   }
 
   logError(description: string, fatal = true) {
-    gtag('event', 'exception', {description, fatal});
+    gtagGlobals.gtag('event', 'exception', {description, fatal});
   }
 }
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 2962bf5..38bd7a9 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -136,6 +136,7 @@
   private _rafScheduler?: RafScheduler = undefined;
   private _serviceWorkerController?: ServiceWorkerController = undefined;
   private _logging?: Analytics = undefined;
+  private _isInternalUser: boolean|undefined = undefined;
 
   // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
   private _trackDataStore?: TrackDataStore = undefined;
@@ -172,11 +173,6 @@
     count: new Uint8Array(0),
   };
 
-  // This variable is set by the is_internal_user.js script if the user is a
-  // googler. This is used to avoid exposing features that are not ready yet
-  // for public consumption. The gated features themselves are not secret.
-  isInternalUser = false;
-
   initialize(dispatch: Dispatch, controllerWorker: Worker) {
     this._dispatch = dispatch;
     this._controllerWorker = controllerWorker;
@@ -430,6 +426,24 @@
     };
   }
 
+  // This variable is set by the is_internal_user.js script if the user is a
+  // googler. This is used to avoid exposing features that are not ready yet
+  // for public consumption. The gated features themselves are not secret.
+  // If a user has been detected as a Googler once, make that sticky in
+  // localStorage, so that we keep treating them as such when they connect over
+  // public networks.
+  get isInternalUser() {
+    if (this._isInternalUser === undefined) {
+      this._isInternalUser = localStorage.getItem('isInternalUser') === '1';
+    }
+    return this._isInternalUser;
+  }
+
+  set isInternalUser(value: boolean) {
+    localStorage.setItem('isInternalUser', value ? '1' : '0');
+    this._isInternalUser = value;
+  }
+
   // Used when switching to the legacy TraceViewer UI.
   // Most resources are cleaned up by replacing the current |window| object,
   // however pending RAFs and workers seem to outlive the |window| and need to
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index d4cb2da..a2ba0ac 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -47,6 +47,7 @@
 } from './globals';
 import {HomePage} from './home_page';
 import {openBufferWithLegacyTraceViewer} from './legacy_trace_viewer';
+import {initLiveReloadIfLocalhost} from './live_reload';
 import {MetricsPage} from './metrics_page';
 import {postMessageHandler} from './post_message_handler';
 import {RecordPage, updateAvailableAdbDevices} from './record_page';
@@ -398,10 +399,22 @@
 
   MicroModal.init();
 
+  // Load the script to detect if this is a Googler (see comments on globals.ts)
+  // and initialize GA after that (or after a timeout if something goes wrong).
+  const script = document.createElement('script');
+  script.src =
+      'https://storage.cloud.google.com/perfetto-ui-internal/is_internal_user.js';
+  script.async = true;
+  script.onerror = () => globals.logging.initialize();
+  script.onload = () => globals.logging.initialize();
+  setTimeout(() => globals.logging.initialize(), 5000);
+  document.head.appendChild(script);
+
   // Will update the chip on the sidebar footer that notifies that the RPC is
   // connected. Has no effect on the controller (which will repeat this check
   // before creating a new engine).
   CheckHttpRpcConnection();
+  initLiveReloadIfLocalhost();
 }
 
 main();
diff --git a/ui/src/frontend/live_reload.ts b/ui/src/frontend/live_reload.ts
new file mode 100644
index 0000000..c30b6e0
--- /dev/null
+++ b/ui/src/frontend/live_reload.ts
@@ -0,0 +1,63 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+let lastReloadDialogTime = 0;
+const kMinTimeBetweenDialogsMs = 10000;
+const changedPaths = new Set<string>();
+
+export function initLiveReloadIfLocalhost() {
+  if (!location.origin.startsWith('http://localhost:')) return;
+
+  const monitor = new EventSource('/live_reload');
+  monitor.onmessage = msg => {
+    const change = msg.data;
+    console.log('Live reload:', change);
+    changedPaths.add(change);
+    if (change.endsWith('.css')) {
+      reloadCSS();
+    } else if (change.endsWith('.html') || change.endsWith('.js')) {
+      reloadDelayed();
+    }
+  };
+  monitor.onerror = (err) => {
+    // In most cases the error is fired on reload, when the socket disconnects.
+    // Delay the error and the reconnection, so in the case of a reload we don't
+    // see any midleading message.
+    setTimeout(() => console.error('LiveReload SSE error', err), 1000);
+  };
+}
+
+function reloadCSS() {
+  const css = document.querySelector('link[rel=stylesheet]') as HTMLLinkElement;
+  if (!css) return;
+  const parent = css.parentElement!;
+  parent.removeChild(css);
+  parent.appendChild(css);
+}
+
+function reloadDelayed() {
+  setTimeout(() => {
+    let pathsStr = '';
+    for (const path of changedPaths) {
+      pathsStr += path + '\n';
+    }
+    changedPaths.clear();
+    if (Date.now() - lastReloadDialogTime < kMinTimeBetweenDialogsMs) return;
+    const reload = confirm(`${pathsStr}changed, click to reload`);
+    lastReloadDialogTime = Date.now();
+    if (reload) {
+      window.location.reload();
+    }
+  }, 1000);
+}
diff --git a/ui/src/frontend/post_message_handler.ts b/ui/src/frontend/post_message_handler.ts
index 03efb23..f4ff214 100644
--- a/ui/src/frontend/post_message_handler.ts
+++ b/ui/src/frontend/post_message_handler.ts
@@ -43,17 +43,19 @@
 // ready, so the message handler always replies to a 'PING' message with 'PONG',
 // which indicates it is ready to receive a trace.
 export function postMessageHandler(messageEvent: MessageEvent) {
+  if (messageEvent.origin === 'https://tagassistant.google.com') {
+    // The GA debugger, does a window.open() and sends messages to the GA
+    // script. Ignore them.
+    return;
+  }
+
   if (document.readyState !== 'complete') {
     console.error('Ignoring message - document not ready yet.');
     return;
   }
 
-  if (messageEvent.source === null) {
-    throw new Error('Incoming message has no source');
-  }
-
-  // This can happen if an extension tries to postMessage.
-  if (messageEvent.source !== window.opener) {
+  if (messageEvent.source === null || messageEvent.source !== window.opener) {
+    // This can happen if an extension tries to postMessage.
     return;
   }
 
@@ -77,7 +79,12 @@
   } else if (messageEvent.data instanceof ArrayBuffer) {
     postedTrace = {title: 'External trace', buffer: messageEvent.data};
   } else {
-    throw new Error('Incoming message data is not in a usable format');
+    console.warn(
+        'Unknown postMessage() event received. If you are trying to open a ' +
+        'trace via postMessage(), this is a bug in your code. If not, this ' +
+        'could be due to some Chrome extension.');
+    console.log('origin:', messageEvent.origin, 'data:', messageEvent.data);
+    return;
   }
 
   if (postedTrace.buffer.byteLength === 0) {
diff --git a/ui/src/gen b/ui/src/gen
index 71d53d9..652818c 120000
--- a/ui/src/gen
+++ b/ui/src/gen
@@ -1 +1 @@
-../dist/gen
\ No newline at end of file
+../out/tsc/gen
\ No newline at end of file
diff --git a/ui/src/query/index.ts b/ui/src/query/index.ts
deleted file mode 100644
index bbb1f2c..0000000
--- a/ui/src/query/index.ts
+++ /dev/null
@@ -1,317 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import * as m from 'mithril';
-
-import {Engine} from '../common/engine';
-import {
-  RawQueryResult,
-  rawQueryResultColumns,
-  rawQueryResultIter
-} from '../common/protos';
-import {
-  createWasmEngine,
-  destroyWasmEngine,
-  warmupWasmEngine,
-  WasmEngineProxy
-} from '../common/wasm_engine_proxy';
-
-const kEngineId = 'engine';
-const kSliceSize = 1024 * 1024;
-
-
-interface OnReadSlice {
-  (blob: Blob, end: number, slice: ArrayBuffer): void;
-}
-
-function readSlice(
-    blob: Blob, start: number, end: number, callback: OnReadSlice) {
-  const slice = blob.slice(start, end);
-  const reader = new FileReader();
-  reader.onerror = e => {
-    console.error(e);
-  };
-  reader.onloadend = _ => {
-    callback(blob, end, reader.result as ArrayBuffer);
-  };
-  reader.readAsArrayBuffer(slice);
-}
-
-
-// Represents an in flight or resolved query.
-type QueryState = QueryPendingState|QueryResultState|QueryErrorState;
-
-interface QueryResultState {
-  kind: 'QueryResultState';
-  id: number;
-  query: string;
-  result: RawQueryResult;
-  executionTimeNs: number;
-}
-
-interface QueryErrorState {
-  kind: 'QueryErrorState';
-  id: number;
-  query: string;
-  error: string;
-}
-
-interface QueryPendingState {
-  kind: 'QueryPendingState';
-  id: number;
-  query: string;
-}
-
-function isPending(q: QueryState): q is QueryPendingState {
-  return q.kind === 'QueryPendingState';
-}
-
-function isError(q: QueryState): q is QueryErrorState {
-  return q.kind === 'QueryErrorState';
-}
-
-function isResult(q: QueryState): q is QueryResultState {
-  return q.kind === 'QueryResultState';
-}
-
-
-// Helpers for accessing a query result
-function columns(result: RawQueryResult): string[] {
-  return [...rawQueryResultColumns(result)];
-}
-
-function rows(result: RawQueryResult, offset: number, count: number):
-    Array<Array<number|string>> {
-  const rows: Array<Array<number|string>> = [];
-
-  let i = 0;
-  for (const value of rawQueryResultIter(result)) {
-    if (i < offset) continue;
-    if (i > offset + count) break;
-    rows.push(Object.values(value));
-    i++;
-  }
-  return rows;
-}
-
-
-// State machine controller for the UI.
-type Input = NewFile|NewQuery|MoreData|QuerySuccess|QueryFailure;
-
-interface NewFile {
-  kind: 'NewFile';
-  file: File;
-}
-
-interface MoreData {
-  kind: 'MoreData';
-  end: number;
-  source: Blob;
-  buffer: ArrayBuffer;
-}
-
-interface NewQuery {
-  kind: 'NewQuery';
-  query: string;
-}
-
-interface QuerySuccess {
-  kind: 'QuerySuccess';
-  id: number;
-  result: RawQueryResult;
-}
-
-interface QueryFailure {
-  kind: 'QueryFailure';
-  id: number;
-  error: string;
-}
-
-class QueryController {
-  engine: Engine|undefined;
-  file: File|undefined;
-  state: 'initial'|'loading'|'ready';
-  render: (state: QueryController) => void;
-  nextQueryId: number;
-  queries: Map<number, QueryState>;
-
-  constructor(render: (state: QueryController) => void) {
-    this.render = render;
-    this.state = 'initial';
-    this.nextQueryId = 0;
-    this.queries = new Map();
-    this.render(this);
-  }
-
-  onInput(input: Input) {
-    // tslint:disable-next-line no-any
-    const f = (this as any)[`${this.state}On${input.kind}`];
-    if (f === undefined) {
-      throw new Error(`No edge for input '${input.kind}' in '${this.state}'`);
-    }
-    f.call(this, input);
-    this.render(this);
-  }
-
-  initialOnNewFile(input: NewFile) {
-    this.state = 'loading';
-    if (this.engine) {
-      destroyWasmEngine(kEngineId);
-    }
-    this.engine = new WasmEngineProxy('engine', createWasmEngine(kEngineId));
-
-    this.file = input.file;
-    this.readNextSlice(0);
-  }
-
-  loadingOnMoreData(input: MoreData) {
-    if (input.source !== this.file) return;
-    this.engine!.parse(new Uint8Array(input.buffer));
-    if (input.end === this.file.size) {
-      this.engine!.notifyEof();
-      this.state = 'ready';
-    } else {
-      this.readNextSlice(input.end);
-    }
-  }
-
-  readyOnNewQuery(input: NewQuery) {
-    const id = this.nextQueryId++;
-    this.queries.set(id, {
-      kind: 'QueryPendingState',
-      id,
-      query: input.query,
-    });
-
-    this.engine!.query(input.query)
-        .then(result => {
-          if (result.error) {
-            this.onInput({
-              kind: 'QueryFailure',
-              id,
-              error: result.error,
-            });
-          } else {
-            this.onInput({
-              kind: 'QuerySuccess',
-              id,
-              result,
-            });
-          }
-        })
-        .catch(error => {
-          this.onInput({
-            kind: 'QueryFailure',
-            id,
-            error,
-          });
-        });
-  }
-
-  readyOnQuerySuccess(input: QuerySuccess) {
-    const oldQueryState = this.queries.get(input.id);
-    console.log('sucess', input);
-    if (!oldQueryState) return;
-    this.queries.set(input.id, {
-      kind: 'QueryResultState',
-      id: oldQueryState.id,
-      query: oldQueryState.query,
-      result: input.result,
-      executionTimeNs: +input.result.executionTimeNs,
-    });
-  }
-
-  readyOnQueryFailure(input: QueryFailure) {
-    const oldQueryState = this.queries.get(input.id);
-    console.log('failure', input);
-    if (!oldQueryState) return;
-    this.queries.set(input.id, {
-      kind: 'QueryErrorState',
-      id: oldQueryState.id,
-      query: oldQueryState.query,
-      error: input.error,
-    });
-  }
-
-  readNextSlice(start: number) {
-    const end = Math.min(this.file!.size, start + kSliceSize);
-    readSlice(this.file!, start, end, (source, end, buffer) => {
-      this.onInput({
-        kind: 'MoreData',
-        end,
-        source,
-        buffer,
-      });
-    });
-  }
-}
-
-function render(root: Element, controller: QueryController) {
-  const queries = [...controller.queries.values()].sort((a, b) => b.id - a.id);
-  m.render(root, [
-    m('h1', controller.state),
-    m('input[type=file]', {
-      onchange: (e: Event) => {
-        if (!(e.target instanceof HTMLInputElement)) return;
-        if (!e.target.files) return;
-        if (!e.target.files[0]) return;
-        const file = e.target.files[0];
-        controller.onInput({
-          kind: 'NewFile',
-          file,
-        });
-      },
-    }),
-    m('input[type=text]', {
-      disabled: controller.state !== 'ready',
-      onchange: (e: Event) => {
-        controller.onInput({
-          kind: 'NewQuery',
-          query: (e.target as HTMLInputElement).value,
-        });
-      }
-    }),
-    m('.query-list',
-      queries.map(
-          q =>
-              m('.query',
-                {
-                  key: q.id,
-                },
-                m('.query-text', q.query),
-                m('.query-time',
-                  isResult(q) ? `${q.executionTimeNs / 1000000}ms` : ''),
-                isResult(q) ? m('.query-content', renderTable(q.result)) : null,
-                isError(q) ? m('.query-content', q.error) : null,
-                isPending(q) ? m('.query-content') : null))),
-  ]);
-}
-
-function renderTable(result: RawQueryResult) {
-  return m(
-      'table',
-      m('tr', columns(result).map(c => m('th', c))),
-      rows(result, 0, 1000).map(r => {
-        return m('tr', Object.values(r).map(d => m('td', d)));
-      }));
-}
-
-function main() {
-  warmupWasmEngine();
-  const root = document.querySelector('#root');
-  if (!root) throw new Error('Could not find root element');
-  new QueryController(ctrl => render(root, ctrl));
-}
-
-main();
diff --git a/ui/src/service_worker/service_worker.ts b/ui/src/service_worker/service_worker.ts
index 3289352..668f840 100644
--- a/ui/src/service_worker/service_worker.ts
+++ b/ui/src/service_worker/service_worker.ts
@@ -61,6 +61,7 @@
   }
 
   const url = new URL(req.url);
+  if (url.pathname === '/live_reload') return false;
   return req.method === 'GET' && url.origin === self.location.origin;
 }
 
diff --git a/ui/tsconfig.base.json b/ui/tsconfig.base.json
index e240301..f8cdfc4 100644
--- a/ui/tsconfig.base.json
+++ b/ui/tsconfig.base.json
@@ -8,7 +8,7 @@
     "allowJs": true,
     "declaration": false,                  // Generates corresponding '.d.ts' file.
     "sourceMap": true,                     // Generates corresponding '.map' file.
-    "outDir": "./dist",                    // Redirect output structure to the directory.
+    "outDir": "./out/tsc",                 // Redirect output structure to the directory.
     "removeComments": false,               // Do not emit comments to output.
     "importHelpers": true,                 // Import emit helpers from 'tslib'.
     "downlevelIteration": true,            // Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
index 8f2c266..ada3c9b 100644
--- a/ui/tsconfig.json
+++ b/ui/tsconfig.json
@@ -4,7 +4,8 @@
   "exclude": [
     "./node_modules/",
     "./src/service_worker/",
-    "./src/gen/"
+    "./src/gen/",
+    "./out"
   ],
   "compilerOptions": {
     "lib": [