Refactor extension to webpack

This change is the biggest in the history of the project. The entire
project has been refactored so it is built with webpack.

This involves:
- Creating webpack and npm config files.
- Fixing some bugs in the code due to the fact that webpack uses strict
mode.
- Merging some pieces of code which were shared throughout the codebase
(not exhaustive, more work should be done in this direction).
- Splitting the console_inject.js file into separate files (it had 1000+
lines).
- Adapting all the build-related files (Makefile, bash scripts, etc.)
- Changing the docs to explain the new build process.
- Changing the Zuul playbook/roles to adapt to the new build process.

Change-Id: I16476d47825461c3a318b3f1a1eddb06b2df2e89
diff --git a/src/common/api.js b/src/common/api.js
index 20a6c7d..065d84c 100644
--- a/src/common/api.js
+++ b/src/common/api.js
@@ -2,7 +2,7 @@
 
 // Function to wrap calls to the Community Console API with intelligent error
 // handling.
-function CCApi(method, data, authenticated, authuser = 0) {
+export function CCApi(method, data, authenticated, authuser = 0) {
   var authuserPart =
       authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser);
 
diff --git a/src/common/commonUtils.js b/src/common/commonUtils.js
new file mode 100644
index 0000000..ee31037
--- /dev/null
+++ b/src/common/commonUtils.js
@@ -0,0 +1,17 @@
+export function parseUrl(url) {
+  var forum_a = url.match(/forum\/([0-9]+)/i);
+  var thread_a = url.match(/thread\/([0-9]+)/i);
+
+  if (forum_a === null || thread_a === null) {
+    return false;
+  }
+
+  return {
+    'forum': forum_a[1],
+    'thread': thread_a[1],
+  };
+}
+
+export function isEmpty(obj) {
+  return Object.keys(obj).length === 0;
+}
diff --git a/src/common/communityConsoleUtils.js b/src/common/communityConsoleUtils.js
new file mode 100644
index 0000000..05a283c
--- /dev/null
+++ b/src/common/communityConsoleUtils.js
@@ -0,0 +1,13 @@
+// Escapes username from HTML generated by the Community Console.
+export function escapeUsername(username) {
+  var quoteRegex = /"/g;
+  var commentRegex = /<!---->/g;
+  return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
+}
+
+// Retrieves authuser from the data-startup attribute.
+export function getAuthUser() {
+  var startup =
+      JSON.parse(document.querySelector('html').getAttribute('data-startup'));
+  return startup?.[2]?.[1] || '0';
+}
diff --git a/src/common/console.css b/src/common/console.css
deleted file mode 100644
index 2eba28f..0000000
--- a/src/common/console.css
+++ /dev/null
@@ -1,159 +0,0 @@
-.TWPT-badge {
-  width: calc(18/13*var(--icon-size, 16px));
-  height: calc(18/13*var(--icon-size, 16px));
-  border-radius: 50%;
-
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  justify-content: center;
-  align-content: center;
-  align-items: center;
-
-  background-color: #009688;
-  color: #fff;
-  box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.22), 0 2px 2px 0 rgba(0, 0, 0, 0.12);
-
-  user-select: none;
-}
-
-.TWPT-badge .material-icon-i {
-  font-size: var(--icon-size, 16px);
-}
-
-.TWPT-btn--with-badge {
-  position: relative;
-  padding: 4px;
-  cursor: pointer;
-}
-
-.TWPT-btn--with-badge .content {
-  padding: 8px;
-}
-
-.TWPT-btn--with-badge .TWPT-badge {
-  --icon-size: 13px;
-  position: absolute;
-  bottom: 6px;
-  right: 5px;
-}
-
-.TWPT-dark-theme {
-  padding: 4px 8px!important;
-}
-
-.TWPT-previous-posts {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-
-.TWPT-previous-posts .TWPT-badge {
-  --icon-size: 18px;
-  margin-right: 8px;
-}
-
-.TWPT-dialog {
-  display: block!important;
-  width: 600px;
-  max-width: 100%;
-  padding: 16px 0;
-  background: white;
-  box-shadow: 0 24px 38px 3px rgba(0,0,0,.14), 0 9px 46px 8px rgba(0,0,0,.12), 0 11px 15px -7px rgba(0,0,0,.2);
-}
-
-.TWPT-dialog-header {
-  padding: 24px 24px 0;
-  width: 100%;
-  box-sizing: border-box;
-}
-
-.TWPT-dialog-header--title {
-  color: #202124;
-  font-family: 'Google Sans', sans-serif;
-  font-size: 22px;
-  font-weight: 400;
-  line-height: 24px;
-  margin-bottom: 4px;
-  text-align: center;
-}
-
-.TWPT-dialog-main {
-  font-size: 13px;
-  font-weight: 400;
-  color: rgba(0, 0, 0, .87);
-  overflow: auto;
-  padding: 0 24px;
-}
-
-.TWPT-dialog-footer {
-  padding: 0 24px;
-}
-
-.TWPT-dialog-footer.is-hidden {
-  display: none;
-}
-
-.TWPT-dialog-footer-btn {
-  display: inline-block;
-  float: right;
-  position: relative;
-  height: 36px;
-  min-width: 64px;
-  margin: 0 4px;
-  cursor: pointer;
-}
-
-.TWPT-dialog-footer-btn:hover::after {
-  content: "";
-  display: block;
-  position: absolute;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 0;
-  background-color: currentColor;
-  outline: 2px solid transparent;
-  opacity: .12;
-  border-radius: inherit;
-  pointer-events: none;
-}
-
-.TWPT-dialog-footer-btn:not(.is-disabled) {
-  color: #1a73e8!important;
-}
-
-.TWPT-dialog-footer-btn.is-disabled {
-  color: #5f6368!important;
-  cursor: not-allowed;
-}
-
-.TWPT-dialog-footer-btn--content {
-  line-height: 36px;
-  text-align: center;
-}
-
-.TWPT-log {
-  max-height: 300px;
-  padding: 0 8px;
-  margin-bottom: 8px;
-  overflow-y: auto;
-  background-color: #e0e0e0;
-}
-
-.TWPT-log-entry {
-  font-family: 'Roboto Mono', 'Courier New', monospace;
-}
-
-.TWPT-log-entry.TWPT-log-entry--error {
-  color: #ff1744;
-}
-
-/*
- * Fix for the headers' right controls so the dark theme switch has space and
- * doesn't overlap the search bar.
- **/
-.material-content > header .right-control {
-  width: auto!important;
-  max-width: 252px!important;
-}
diff --git a/src/common/content_scripts.js b/src/common/contentScriptsUtils.js
similarity index 60%
rename from src/common/content_scripts.js
rename to src/common/contentScriptsUtils.js
index 1bc9a7d..00bc556 100644
--- a/src/common/content_scripts.js
+++ b/src/common/contentScriptsUtils.js
@@ -1,4 +1,4 @@
-function injectStylesheet(stylesheetName, attributes = {}) {
+export function injectStylesheet(stylesheetName, attributes = {}) {
   var link = document.createElement('link');
   link.setAttribute('rel', 'stylesheet');
   link.setAttribute('href', stylesheetName);
@@ -6,18 +6,12 @@
   document.head.appendChild(link);
 }
 
-function injectStyles(css) {
+export function injectStyles(css) {
   injectStylesheet('data:text/css;charset=UTF-8,' + encodeURIComponent(css));
 }
 
-function injectScript(scriptName) {
+export function injectScript(scriptName) {
   var script = document.createElement('script');
   script.src = scriptName;
   document.head.appendChild(script);
 }
-
-function escapeUsername(username) {
-  var quoteRegex = /"/g;
-  var commentRegex = /<!---->/g;
-  return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
-}
diff --git a/src/common/csEventListener.js b/src/common/csEventListener.js
new file mode 100644
index 0000000..14b4295
--- /dev/null
+++ b/src/common/csEventListener.js
@@ -0,0 +1,43 @@
+// In order to pass i18n strings and settings values to the injected scripts,
+// which don't have access to the chrome.* APIs, we use event listeners.
+
+export function setUpListener() {
+  chrome.storage.sync.get(null, function(options) {
+    window.addEventListener('TWPT_sendRequest', evt => {
+      var request = evt.detail;
+      switch (request.data.action) {
+        case 'geti18nMessage':
+          var data = chrome.i18n.getMessage(
+              request.data.msg,
+              (Array.isArray(request.data.placeholders) ?
+                   request.data.placeholders :
+                   []));
+          break;
+
+        case 'getProfileIndicatorOptions':
+          var data = {
+            'indicatorDot': options.profileindicator,
+            'numPosts': options.profileindicatoralt
+          };
+          break;
+
+        case 'getNumPostMonths':
+          var data = options.profileindicatoralt_months;
+          break;
+
+        default:
+          var data = 'unknownAction';
+          console.warn('Unknown action ' + request.data.action + '.');
+          break;
+      }
+
+      var response = {
+        data,
+        requestId: request.id,
+        prefix: (request.prefix || 'TWPT'),
+      };
+
+      window.postMessage(response, '*');
+    });
+  });
+}
diff --git a/src/common/cs_event_listener.js b/src/common/cs_event_listener.js
deleted file mode 100644
index c7de08f..0000000
--- a/src/common/cs_event_listener.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// In order to pass i18n strings and settings values to the injected scripts,
-// which don't have access to the chrome.* APIs, we use event listeners.
-chrome.storage.sync.get(null, function(options) {
-  window.addEventListener('TWPT_sendRequest', evt => {
-    var request = evt.detail;
-    switch (request.data.action) {
-      case 'geti18nMessage':
-        var data = chrome.i18n.getMessage(
-            request.data.msg,
-            (Array.isArray(request.data.placeholders) ?
-                 request.data.placeholders :
-                 []));
-        break;
-
-      case 'getProfileIndicatorOptions':
-        var data = {
-          'indicatorDot': options.profileindicator,
-          'numPosts': options.profileindicatoralt
-        };
-        break;
-
-      case 'getNumPostMonths':
-        var data = options.profileindicatoralt_months;
-        break;
-
-      default:
-        var data = 'unknownAction';
-        console.warn('Unknown action ' + request.data.action + '.');
-        break;
-    }
-
-    var response = {
-      data,
-      requestId: request.id,
-      prefix: (request.prefix || 'TWPT'),
-    };
-
-    window.postMessage(response, '*');
-  });
-});
diff --git a/src/common/extUtils.js b/src/common/extUtils.js
new file mode 100644
index 0000000..a1b4d48
--- /dev/null
+++ b/src/common/extUtils.js
@@ -0,0 +1,13 @@
+// This method is based on the fact that when building the extension for Firefox
+// the browser_specific_settings.gecko entry is included.
+export function isFirefox() {
+  var manifest = chrome.runtime.getManifest();
+  return manifest.browser_specific_settings !== undefined &&
+      manifest.browser_specific_settings.gecko !== undefined;
+}
+
+// Returns whether the extension is a release version.
+export function isReleaseVersion() {
+  var manifest = chrome.runtime.getManifest();
+  return ('version' in manifest) && manifest.version != '0';
+}
diff --git a/src/common/forum.css b/src/common/forum.css
deleted file mode 100644
index 50316d7..0000000
--- a/src/common/forum.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.TWPT-badge {
-  width: calc(18/13*var(--icon-size, 16px));
-  height: calc(18/13*var(--icon-size, 16px));
-  border-radius: 50%;
-  margin: 4px;
-
-  display: inline-flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  justify-content: center;
-  align-content: center;
-  align-items: center;
-
-  background-color: #009688;
-  color: #fff;
-  box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.22), 0 2px 2px 0 rgba(0, 0, 0, 0.12);
-
-  user-select: none;
-}
-
-.TWPT-badge img {
-  height: var(--icon-size, 16px);
-  filter: invert(1);
-}
-
-.TWPT-user-profile__user-links {
-  margin-top: 8px;
-}
-
-.TWPT-user-link > * {
-  vertical-align: middle;
-}
-
-.TWPT-user-link .TWPT-badge {
-  margin-left: 0;
-}
diff --git a/src/common/common.js b/src/common/optionsPrototype.json5
similarity index 61%
rename from src/common/common.js
rename to src/common/optionsPrototype.json5
index 4fcae45..191d0e1 100644
--- a/src/common/common.js
+++ b/src/common/optionsPrototype.json5
@@ -1,4 +1,4 @@
-const optionsPrototype = {
+{
   // Available options:
   'list': {
     defaultValue: true,
@@ -118,53 +118,4 @@
     defaultValue: false,
     context: 'deprecated',
   },
-};
-
-const specialOptions = [
-  'profileindicatoralt_months',
-  'ccdarktheme_mode',
-  'ccdarktheme_switch_enabled',
-  'ccdragndropfix',
-];
-
-function isEmpty(obj) {
-  return Object.keys(obj).length === 0;
-}
-
-// Adds missing options with their default value. If |dryRun| is set to false,
-// they are also saved to the sync storage area.
-function cleanUpOptions(options, dryRun = false) {
-  console.log('[cleanUpOptions] Previous options', JSON.stringify(options));
-
-  if (typeof options !== 'object' || options === null) options = {};
-
-  var ok = true;
-  for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
-    if (!(opt in options)) {
-      ok = false;
-      options[opt] = optMeta['defaultValue'];
-    }
-  }
-
-  console.log('[cleanUpOptions] New options', JSON.stringify(options));
-
-  if (!ok && !dryRun) {
-    chrome.storage.sync.set(options);
-  }
-
-  return options;
-}
-
-// This method is based on the fact that when building the extension for Firefox
-// the browser_specific_settings.gecko entry is included.
-function isFirefox() {
-  var manifest = chrome.runtime.getManifest();
-  return manifest.browser_specific_settings !== undefined &&
-      manifest.browser_specific_settings.gecko !== undefined;
-}
-
-// Returns whether the extension is a release version.
-function isReleaseVersion() {
-  var manifest = chrome.runtime.getManifest();
-  return ('version' in manifest) && manifest.version != '0';
 }
diff --git a/src/common/optionsUtils.js b/src/common/optionsUtils.js
new file mode 100644
index 0000000..0efb6c9
--- /dev/null
+++ b/src/common/optionsUtils.js
@@ -0,0 +1,28 @@
+import optionsPrototype from './optionsPrototype.json5';
+import specialOptions from './specialOptions.json5';
+
+export {optionsPrototype, specialOptions};
+
+// Adds missing options with their default value. If |dryRun| is set to false,
+// they are also saved to the sync storage area.
+export function cleanUpOptions(options, dryRun = false) {
+  console.log('[cleanUpOptions] Previous options', JSON.stringify(options));
+
+  if (typeof options !== 'object' || options === null) options = {};
+
+  var ok = true;
+  for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
+    if (!(opt in options)) {
+      ok = false;
+      options[opt] = optMeta['defaultValue'];
+    }
+  }
+
+  console.log('[cleanUpOptions] New options', JSON.stringify(options));
+
+  if (!ok && !dryRun) {
+    chrome.storage.sync.set(options);
+  }
+
+  return options;
+}
diff --git a/src/common/specialOptions.json5 b/src/common/specialOptions.json5
new file mode 100644
index 0000000..55dde20
--- /dev/null
+++ b/src/common/specialOptions.json5
@@ -0,0 +1,6 @@
+[
+  'profileindicatoralt_months',
+  'ccdarktheme_mode',
+  'ccdarktheme_switch_enabled',
+  'ccdragndropfix',
+]