Add skeleton for UI (Node + TypeScript + WASM  + build integration)

This CL checks in a basic skeleton to develop the UI, introducing
- NodeJS binary, pulled in via tools/install-build-deps --ui
- WASM toolchain
- TypeScript compiler
- Protobuf.js library dependency
- Basic build file plumbing
- ui/run-dev-server

For the moment, it just introduced a hello world TypeScript file
that loads a hello world WASM library.
All it takes to build and bundle everything is:
$ ninja -C out/xxx ui
$ ui/run-dev-server

Bug: 73626618
Change-Id: I44a628c3dc8b36d1e9267f43f25f256761c7e90f
diff --git a/ui/BUILD.gn b/ui/BUILD.gn
new file mode 100644
index 0000000..b5a3ffa
--- /dev/null
+++ b/ui/BUILD.gn
@@ -0,0 +1,286 @@
+# 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("../gn/perfetto.gni")
+import("../gn/wasm.gni")
+import("../protos/perfetto/trace_processor/proto_files.gni")
+
+ui_dist_dir = "$root_build_dir/ui"
+nodejs_root = "../buildtools/nodejs"
+nodejs_bin = rebase_path("$nodejs_root/bin", root_build_dir)
+
+# +----------------------------------------------------------------------------+
+# | The outer "ui" target to just ninja -C out/xxx ui                          |
+# +----------------------------------------------------------------------------+
+group("ui") {
+  deps = [
+    ":bundle",
+    ":index",
+    ":wasm",
+  ]
+}
+
+# +----------------------------------------------------------------------------+
+# | Template used to run node binaries using the hermetic node toolchain.      |
+# +----------------------------------------------------------------------------+
+template("node_bin") {
+  action(target_name) {
+    forward_variables_from(invoker,
+                           [
+                             "inputs",
+                             "outputs",
+                           ])
+    deps = [
+      ":node_modules",
+    ]
+    if (defined(invoker.deps)) {
+      deps += invoker.deps
+    }
+    script = "../gn/standalone/build_tool_wrapper.py"
+    _node_cmd = invoker.node_cmd
+    args = [
+             "--path=$nodejs_bin",
+             "node",
+             rebase_path("node_modules/.bin/$_node_cmd", root_build_dir),
+           ] + invoker.args
+  }
+}
+
+# +----------------------------------------------------------------------------+
+# | 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.
+node_bin("bundle") {
+  deps = [
+    ":transpile_all_ts",
+  ]
+  node_cmd = "browserify"
+  inputs = [
+    "$ui_dist_dir/main.js",
+  ]
+  outputs = [
+    "$ui_dist_dir/bundle.js",
+  ]
+  args = [
+    rebase_path(inputs[0], root_build_dir),
+    "-d",
+    "-o",
+    rebase_path(outputs[0], root_build_dir),
+  ]
+}
+
+# +----------------------------------------------------------------------------+
+# | Protobuf: gen rules to create .js and .d.ts files from protos.             |
+# +----------------------------------------------------------------------------+
+proto_gen_dir = "$ui_dist_dir/gen"
+
+node_bin("protos_to_js") {
+  inputs = []
+  foreach(proto, trace_processor_protos) {
+    inputs += [ "../protos/perfetto/trace_processor/$proto.proto" ]
+  }
+  outputs = [
+    "$proto_gen_dir/protos.js",
+  ]
+  node_cmd = "pbjs"
+  args = [
+           "-t",
+           "static-module",
+           "-w",
+           "commonjs",
+           "-p",
+           rebase_path("../protos", 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 = [
+    "$proto_gen_dir/protos.js",
+  ]
+  outputs = [
+    "$proto_gen_dir/protos.d.ts",
+  ]
+  node_cmd = "pbts"
+  args = [
+    "-p",
+    rebase_path("../protos", root_build_dir),
+    "-o",
+    rebase_path(outputs[0], root_build_dir),
+    rebase_path(inputs[0], root_build_dir),
+  ]
+}
+
+# +----------------------------------------------------------------------------+
+# | TypeScript: transpiles all *.ts into .js                                   |
+# +----------------------------------------------------------------------------+
+
+# Builds all .ts sources in the repo that are reachable from |sources|.
+node_bin("transpile_all_ts") {
+  sources = [
+    "main.ts",
+  ]
+  deps = [
+    ":dist_symlink",
+    ":protos_to_ts",
+  ]
+  inputs = sources + [ "tsconfig.json" ]
+  outputs = [
+    "$ui_dist_dir/main.js",
+  ]
+
+  # Find all *.ts files and pass them as input triggers. This does NOT affect
+  # which files will get built. This defines only the set of files that, when
+  # changes, will re-trigger this rule in ninja. The files that get transpiled
+  # are the transitive dependencies of |sources|.
+  all_ts_files = exec_script("../gn/standalone/glob.py",
+                             [
+                               "--root=" + rebase_path(".", root_build_dir),
+                               "--filter=*.ts",
+                               "--exclude=node_modules",
+                               "--exclude=dist",
+                             ],
+                             "list lines",
+                             [ "." ])
+  inputs += all_ts_files
+
+  node_cmd = "tsc"
+  args = [
+    "--project",
+    rebase_path(".", root_build_dir),
+    "--outDir",
+    rebase_path(ui_dist_dir, root_build_dir),
+  ]
+}
+
+# +----------------------------------------------------------------------------+
+# | Copy rules: copy assets into the dist directory.                           |
+# +----------------------------------------------------------------------------+
+copy("index") {
+  sources = [
+    "index.html",
+  ]
+  outputs = [
+    "$ui_dist_dir/index.html",
+  ]
+}
+
+copy("wasm") {
+  deps = [
+    "//src/trace_processor:trace_processor.js($wasm_toolchain)",
+    "//src/trace_processor:trace_processor.wasm($wasm_toolchain)",
+  ]
+  sources = [
+    "$root_build_dir/wasm/trace_processor.js",
+    "$root_build_dir/wasm/trace_processor.wasm",
+  ]
+  outputs = [
+    "$ui_dist_dir/wasm/{{source_file_part}}",
+  ]
+}
+
+# +----------------------------------------------------------------------------+
+# | Node JS: run npm install and creates a symlink in the out directory.       |
+# +----------------------------------------------------------------------------+
+
+action("check_node_exists") {
+  script = "../gn/standalone/check_buildtool_exists.py"
+  args = [ nodejs_bin ]
+  inputs = []
+  outputs = [
+    "$target_out_dir/ignored",
+  ]
+}
+
+# Creates a symlink from out/xxx/ui/node_modules -> ../../../ui/node_modules.
+# This allows to run browserify and other node tools from the out/xxx directory.
+action("node_modules_symlink") {
+  deps = [
+    ":check_node_exists",
+  ]
+  inputs = [
+    "node_modules",
+  ]
+  outputs = [
+    "$ui_dist_dir/node_modules",
+  ]
+  script = "../gn/standalone/build_tool_wrapper.py"
+  args = [
+    "/bin/ln",
+    "-fns",
+    rebase_path(inputs[0], ui_dist_dir),
+    rebase_path(outputs[0], root_build_dir),
+  ]
+}
+
+# Runs npm install.
+action("node_modules") {
+  script = "../gn/standalone/build_tool_wrapper.py"
+  stamp_file = "$ui_dist_dir/.$target_name.stamp"
+  args = [
+    "--chdir",
+    rebase_path(".", root_build_dir),
+    "--stamp",
+    rebase_path(stamp_file, root_build_dir),
+  ]
+  args += [
+    "--path=$nodejs_bin",
+    "node",
+    rebase_path("$nodejs_root/bin/npm", "."),
+    "install",
+    "--no-save",
+    "--silent",
+  ]
+  inputs = [
+    "package.json",
+    "package-lock.json",
+  ]
+  outputs = [
+    stamp_file,
+  ]
+  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 = "$ui_dist_dir/.$target_name.stamp"
+  args = [
+    "--stamp",
+    rebase_path(stamp_file, root_build_dir),
+    "/bin/ln",
+    "-fns",
+    rebase_path(ui_dist_dir, "."),
+    rebase_path("dist", root_build_dir),
+  ]
+  inputs = [
+    "$root_build_dir",
+  ]
+  outputs = [
+    stamp_file,
+  ]
+}