pw_web_ui: Bundle and launch the React frontend.

Write build rules to bundle and launch an example page that supports
React and can import files from other parts of the project.

Also, update pigweed_presubmit to support multiline comments for
licenses, and add a license check for html files.

Change-Id: I1bed66c171721ddfa8c7e06e8abaf7a1f936eb98
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/12040
Reviewed-by: Alexei Frolov <frolv@google.com>
Commit-Queue: Matthew Soulanille <msoulanille@google.com>
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index 4774f2f..3a36ae8 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -21,7 +21,7 @@
 from pathlib import Path
 import re
 import sys
-from typing import Sequence
+from typing import Sequence, IO, Tuple, Optional
 
 try:
     import pw_presubmit
@@ -157,8 +157,12 @@
         *ctx.paths)
 
 
-COPYRIGHT_FIRST_LINE = re.compile(
-    r'^(#|//| \*|REM|::) Copyright 20\d\d The Pigweed Authors$')
+# The first line must be regex because of the '20\d\d' date
+COPYRIGHT_FIRST_LINE = r'Copyright 20\d\d The Pigweed Authors'
+COPYRIGHT_COMMENTS = r'(#|//| \*|REM|::)'
+COPYRIGHT_BLOCK_COMMENTS = (
+    # HTML comments
+    (r'<!--', r'-->'), )
 
 COPYRIGHT_FIRST_LINE_EXCEPTIONS = (
     '#!',
@@ -170,18 +174,18 @@
 
 COPYRIGHT_LINES = tuple("""\
 
- 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
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
 
-     https://www.apache.org/licenses/LICENSE-2.0
+    https://www.apache.org/licenses/LICENSE-2.0
 
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- License for the specific language governing permissions and limitations under
- the License.
-""".splitlines(True))
+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.
+""".splitlines())
 
 _EXCLUDE_FROM_COPYRIGHT_NOTICE: Sequence[str] = (
     r'^(?:.+/)?\..+$',
@@ -204,39 +208,83 @@
 )
 
 
+def match_block_comment_start(line: str) -> Optional[str]:
+    """Matches the start of a block comment and returns the end."""
+    for block_comment in COPYRIGHT_BLOCK_COMMENTS:
+        if re.match(block_comment[0], line):
+            # Return the end of the block comment
+            return block_comment[1]
+    return None
+
+
+def copyright_read_first_line(
+        file: IO) -> Tuple[Optional[str], Optional[str], Optional[str]]:
+    """Reads the file until it reads a valid first copyright line.
+
+    Returns (comment, block_comment, line). comment and block_comment are
+    mutually exclusive and refer to the comment character sequence and whether
+    they form a block comment or a line comment. line is the first line of
+    the copyright, and is used for error reporting.
+    """
+    line = file.readline()
+    first_line_matcher = re.compile(COPYRIGHT_COMMENTS + ' ' +
+                                    COPYRIGHT_FIRST_LINE)
+    while line:
+        end_block_comment = match_block_comment_start(line)
+        if end_block_comment:
+            next_line = file.readline()
+            copyright_line = re.match(COPYRIGHT_FIRST_LINE, next_line)
+            if not copyright_line:
+                return (None, None, line)
+            return (None, end_block_comment, line)
+
+        first_line = first_line_matcher.match(line)
+        if first_line:
+            return (first_line.group(1), None, line)
+
+        if (line.strip()
+                and not line.startswith(COPYRIGHT_FIRST_LINE_EXCEPTIONS)):
+            return (None, None, line)
+
+        line = file.readline()
+    return (None, None, None)
+
+
 @filter_paths(exclude=_EXCLUDE_FROM_COPYRIGHT_NOTICE)
 def copyright_notice(ctx: PresubmitContext):
     """Checks that the Pigweed copyright notice is present."""
-
     errors = []
 
     for path in ctx.paths:
         _LOG.debug('Checking %s', path)
         with open(path) as file:
-            line = file.readline()
-            first_line = None
-            while line:
-                first_line = COPYRIGHT_FIRST_LINE.match(line)
-                if first_line:
-                    break
+            (comment, end_block_comment,
+             line) = copyright_read_first_line(file)
 
-                if (line.strip() and
-                        not line.startswith(COPYRIGHT_FIRST_LINE_EXCEPTIONS)):
-                    break
+            if not line:
+                _LOG.debug('%s: invalid first line', path)
+                errors.append(path)
+                continue
 
-                line = file.readline()
-
-            if not first_line:
+            if not (comment or end_block_comment):
                 _LOG.debug('%s: invalid first line %r', path, line)
                 errors.append(path)
                 continue
 
-            comment = first_line.group(1)
+            if end_block_comment:
+                expected_lines = COPYRIGHT_LINES + (end_block_comment, )
+            else:
+                expected_lines = COPYRIGHT_LINES
 
-            for expected, actual in zip(COPYRIGHT_LINES, file):
-                if comment + expected != actual:
+            for expected, actual in zip(expected_lines, file):
+                if end_block_comment:
+                    expected_line = expected + '\n'
+                elif comment:
+                    expected_line = (comment + ' ' + expected).rstrip() + '\n'
+
+                if expected_line != actual:
                     _LOG.debug('  bad line: %r', actual)
-                    _LOG.debug('  expected: %r', comment + expected)
+                    _LOG.debug('  expected: %r', expected_line)
                     errors.append(path)
                     break
 
diff --git a/pw_web_ui/src/frontend/BUILD b/pw_web_ui/src/frontend/BUILD
new file mode 100644
index 0000000..fbd4609
--- /dev/null
+++ b/pw_web_ui/src/frontend/BUILD
@@ -0,0 +1,52 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+load("@npm_bazel_typescript//:index.bzl", "ts_library", "ts_devserver")
+load("//pw_web_ui:web_bundle.bzl", "web_bundle")
+
+ts_library(
+    name = "app_lib",
+    srcs = [
+        "app.tsx",
+        "index.tsx",
+    ],
+    deps = [
+        "@npm//@types/react",
+        "@npm//react",
+        "@npm//@types/react-dom",
+        "@npm//react-dom",
+        "@npm//@material-ui/core",
+        "//pw_web_ui/src/transport:web_serial_transport_lib",
+    ],
+)
+
+web_bundle(
+    name = "app_bundle",
+    deps = [
+        ":app_lib",
+    ],
+    entry_point = "index.tsx",
+)
+
+ts_devserver(
+    # TODO(msoulanille): Use the devserver's bundler
+    # instead of serving the production bundle.
+    name = "devserver",
+    static_files = [
+        "index.html",
+        ":app_bundle",
+    ]
+)
diff --git a/pw_web_ui/src/frontend/app.tsx b/pw_web_ui/src/frontend/app.tsx
new file mode 100644
index 0000000..2674fff
--- /dev/null
+++ b/pw_web_ui/src/frontend/app.tsx
@@ -0,0 +1,34 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import Button from '@material-ui/core/Button';
+import * as React from 'react';
+import { WebSerialTransport } from '../transport/web_serial_transport';
+
+
+export function App() {
+    const transport = new WebSerialTransport();
+
+    transport.chunks.subscribe((item) => {
+        console.log(item);
+    })
+
+    return (
+        <div className="app">
+            <h1>Example Page</h1>
+            <Button variant="contained" color="primary" onClick={() => {
+                transport.connect();
+            }}>Connect</Button>
+        </div>);
+}
diff --git a/pw_web_ui/src/frontend/index.html b/pw_web_ui/src/frontend/index.html
new file mode 100644
index 0000000..21ab25d
--- /dev/null
+++ b/pw_web_ui/src/frontend/index.html
@@ -0,0 +1,24 @@
+<!--
+Copyright 2020 The Pigweed Authors
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+-->
+<html>
+  <head>
+    <title>Pigweed Web UI</title>
+  </head>
+  <body>
+    <div id="react-root"></div>
+    <script src="app_bundle.js"></script>
+  </body>
+</html>
diff --git a/pw_web_ui/src/frontend/index.tsx b/pw_web_ui/src/frontend/index.tsx
new file mode 100644
index 0000000..dc53a1b
--- /dev/null
+++ b/pw_web_ui/src/frontend/index.tsx
@@ -0,0 +1,24 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import { App } from './app';
+
+
+// Bootstrap the app and append it to the DOM
+ReactDOM.render(
+    <App />,
+    document.getElementById("react-root")
+)