Use Flow to typecheck JavaScript code.

Change-Id: I9c0c9b5aa74d592a04eb6533e64669f1896fb7cd
diff --git a/build.gradle b/build.gradle
index 6274531..773eb8e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -129,8 +129,20 @@
   commandLine "yarn", "run", "snowpack", "--optimize"
 }
 
+task flow(type:Exec) {
+  def resourceDir = "src/main/resources/META-INF/resources"
+
+  onlyIf { !project.hasProperty('skipWeb') }
+
+  dependsOn snowpack
+
+  workingDir resourceDir
+  commandLine "yarn", "run", "flow", "--color=always"
+}
+
 task compileWeb {
   dependsOn snowpack
+  dependsOn flow
 
   doLast {}
 }
diff --git a/src/main/resources/META-INF/resources/.flowconfig b/src/main/resources/META-INF/resources/.flowconfig
new file mode 100644
index 0000000..fe59993
--- /dev/null
+++ b/src/main/resources/META-INF/resources/.flowconfig
@@ -0,0 +1,8 @@
+[libs]
+
+[ignore]
+<PROJECT_ROOT>/node_modules/.*
+
+[options]
+emoji=true
+module.system.node.resolve_dirname=node_modules
diff --git a/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js b/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
index 012a314..41892c1 100644
--- a/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
+++ b/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
@@ -1,3 +1,5 @@
+// @flow
+
 import ProgressSpinner from "../web_modules/elix/define/ProgressSpinner.js";
 
 const template = document.createElement('template');
@@ -33,18 +35,27 @@
   </form>`;
 
 export class MlkBookmarkSubmissionForm extends HTMLElement {
+  /*::
+  descriptionInput: HTMLTextAreaElement;
+  titleInput: HTMLInputElement;
+  uriInput: HTMLInputElement;
+  uriSpinner: ProgressSpinner;
+  */
+
   constructor() {
     super();
 
-    this.onUriBlur = this.onUriBlur.bind(this);
-
     let shadow = this.attachShadow({mode: "open"});
-    this.shadowRoot.appendChild(template.content.cloneNode(true));
+    shadow.appendChild(template.content.cloneNode(true));
 
-    this.descriptionInput = shadow.getElementById('description-input');
-    this.titleInput = shadow.getElementById('title-input');
-    this.uriInput = shadow.getElementById('uri-input');
-    this.uriSpinner = shadow.getElementById('uri-spinner');
+    this.descriptionInput =
+        this.cast(shadow.getElementById('description-input'));
+    this.titleInput =
+        this.cast(shadow.getElementById('title-input'));
+    this.uriInput =
+        this.cast(shadow.getElementById('uri-input'));
+    this.uriSpinner =
+        this.cast(shadow.getElementById('uri-spinner'));
   }
 
   static get observedAttributes() {
@@ -52,21 +63,20 @@
   }
 
   connectedCallback () {
-    this.uriInput.addEventListener('blur', this.onUriBlur);
+    this.uriInput.addEventListener('blur', this.onUriBlur.bind(this));
     this.uriInput.value = this.uri || "";
-    this.titleInput.value = this.title || "";
+    this.titleInput.value = this.titleText || "";
     this.descriptionInput.innerText = this.description || "";
   }
 
-  attributeChangedCallback(name, oldValue, newValue) {
-    this.render();
+  attributeChangedCallback(name /*:string*/, oldValue /*:string*/, newValue /*:string*/) {
   }
 
   get uri() {
     return this.getAttribute("uri");
   }
 
-  get title() {
+  get titleText() {
     return this.getAttribute("title");
   }
 
@@ -93,8 +103,8 @@
     this.uriSpinner.hidden = false;
     this.uriSpinner.playing = true;
     let searchParams = new URLSearchParams({'uri': this.uriInput.value});
-    console.log(`/bookmarks/page-info?${searchParams}`);
-    let fetchUrl = new URL(`/bookmarks/page-info?${searchParams}`, document.URL);
+    console.log(`/bookmarks/page-info?${searchParams.toString()}`);
+    let fetchUrl = new URL(`/bookmarks/page-info?${searchParams.toString()}`, document.URL);
     let r = await fetch(fetchUrl);
     this.uriSpinner.hidden = true;
     this.uriSpinner.playing = false;
@@ -106,6 +116,16 @@
     let pageInfo = await r.json();
     this.titleInput.value = pageInfo.title;
   }
+
+  cast/*:: <T>*/(x /*: ?Object*/) /*: T*/ {
+    if (x === null || x === undefined) {
+      throw "unexpected null or undefined";
+    } else {
+      /*:: (x: T); */
+      return x;
+    }
+  }
+
 }
 
 customElements.define("mlk-bookmark-submission-form", MlkBookmarkSubmissionForm);
diff --git a/src/main/resources/META-INF/resources/package.json b/src/main/resources/META-INF/resources/package.json
index d0657d4..dc8cdeb 100644
--- a/src/main/resources/META-INF/resources/package.json
+++ b/src/main/resources/META-INF/resources/package.json
@@ -13,6 +13,7 @@
     "sanitize.css": "^11.0.0"
   },
   "devDependencies": {
+    "flow-bin": "^0.118.0",
     "snowpack": "^1.1.0"
   },
   "snowpack": {
diff --git a/src/main/resources/META-INF/resources/yarn.lock b/src/main/resources/META-INF/resources/yarn.lock
index 2b69a44..1a2a5a1 100644
--- a/src/main/resources/META-INF/resources/yarn.lock
+++ b/src/main/resources/META-INF/resources/yarn.lock
@@ -1051,6 +1051,11 @@
   resolved "https://registry.yarnpkg.com/find-package-json/-/find-package-json-1.2.0.tgz#4057d1b943f82d8445fe52dc9cf456f6b8b58083"
   integrity sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==
 
+flow-bin@^0.118.0:
+  version "0.118.0"
+  resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.118.0.tgz#fb706364a58c682d67a2ca7df39396467dc397d1"
+  integrity sha512-jlbUu0XkbpXeXhan5xyTqVK1jmEKNxE8hpzznI3TThHTr76GiFwK0iRzhDo4KNy+S9h/KxHaqVhTP86vA6wHCg==
+
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"