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/tools/generateManifest.go b/tools/generateManifest.go
new file mode 100644
index 0000000..185f1c3
--- /dev/null
+++ b/tools/generateManifest.go
@@ -0,0 +1,111 @@
+package main
+
+import (
+  "bufio"
+  "flag"
+  "fmt"
+  "io"
+  "log"
+  "os"
+  "regexp"
+  "strings"
+)
+
+// Generates the manifest.json file according to the dependencies passed
+// via CLI arguments
+
+const (
+  manifestTemplate = "templates/manifest.gjson"
+)
+
+var beginningOfIfStatement = regexp.MustCompile(`^\s*#if defined\(([^\(\)]*)\)\s*$`)
+var endOfIfStatement = regexp.MustCompile(`^\s*#endif\s*$`)
+
+var (
+  quietMode = flag.Bool("quiet", false, "Quiet mode")
+  destFile  = flag.String("dest", "", "Destination file")
+)
+
+func FindWithCaseFolding(slice []string, val string) bool {
+  for _, item := range slice {
+    if strings.EqualFold(item, val) {
+      return true
+    }
+  }
+  return false
+}
+
+func WriteManifest(template io.Reader, dest io.Writer, dependencies []string) error {
+  level := 0
+  activeLevel := 0
+  scanner := bufio.NewScanner(template)
+  for scanner.Scan() {
+    line := scanner.Text()
+    considerLine := false
+    if m := beginningOfIfStatement.FindStringSubmatch(line); m != nil {
+      if level == activeLevel {
+        statementDeps := m[1]
+        deps := strings.Split(statementDeps, "||")
+        for _, dep := range deps {
+          dep = strings.TrimSpace(dep)
+          if FindWithCaseFolding(dependencies, dep) {
+            activeLevel++
+            break
+          }
+        }
+      }
+      level++
+    } else if m := endOfIfStatement.MatchString(line); m {
+      if activeLevel == level {
+        activeLevel--
+      }
+      level--
+    } else {
+      considerLine = level == activeLevel
+    }
+
+    if considerLine {
+      _, err := io.WriteString(dest, line + "\n")
+      if err != nil {
+        return fmt.Errorf("Can't write manifest: %v", err)
+      }
+    }
+  }
+
+  return nil
+}
+
+func main() {
+  log.SetPrefix("generateManifest: ")
+  log.SetFlags(0)
+
+  flag.Parse()
+  dependencies := flag.Args()
+
+  if len(dependencies) == 0 {
+    log.Fatalf("Pass the dependencies as arguments (for instance, run `go run generateManifest.go CHROMIUM`).")
+  }
+
+  if *destFile == "" {
+    log.Fatalf("Pass the destination file name via the -dest flag.")
+  }
+
+  template, err := os.Open(manifestTemplate)
+  if err != nil {
+    log.Fatalf("Couldn't open file %v: %v", manifestTemplate, err)
+  }
+  defer template.Close()
+
+  dest, err := os.Create(*destFile)
+  if err != nil {
+    log.Fatalf("Couldn't create file %v: %v", *destFile, err)
+  }
+  defer dest.Close()
+
+  err = WriteManifest(template, dest, dependencies)
+  if err != nil {
+    log.Fatalf("%v", err)
+  } else if !*quietMode {
+    log.Println("Manifest has been generated successfully")
+  }
+}
diff --git a/tools/generateManifest_test.go b/tools/generateManifest_test.go
new file mode 100644
index 0000000..57f2f56
--- /dev/null
+++ b/tools/generateManifest_test.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+  "bytes"
+  "crypto/sha256"
+  "encoding/hex"
+  "io"
+  "os"
+  "testing"
+)
+
+type Manifest struct {
+  name string
+  templateFile string
+  expectedFile string
+  dependencies []string
+}
+
+func TestManifestConversions(t *testing.T) {
+  testManifests := []Manifest{
+    {
+      name: "ManifestSmall",
+      templateFile: "testdata/manifest_small1.gjson",
+      expectedFile: "testdata/manifest_small1_expected.json",
+      dependencies: []string{"AAA", "BBB", "D"},
+    },
+    {
+      name: "ManifestFrozenChromium",
+      templateFile: "testdata/manifest_frozen.gjson",
+      expectedFile: "testdata/manifest_frozen_chromium_expected.json",
+      dependencies: []string{"CHROMIUM"},
+    },
+    {
+      name: "ManifestFrozenGecko",
+      templateFile: "testdata/manifest_frozen.gjson",
+      expectedFile: "testdata/manifest_frozen_gecko_expected.json",
+      dependencies: []string{"GECKO"},
+    },
+  }
+
+  for _, m := range testManifests {
+    t.Run(m.name, func(t *testing.T) {
+      template, err := os.Open(m.templateFile)
+      if err != nil {
+        t.Fatalf("Can't open test file: %v", err)
+      }
+      defer template.Close()
+
+      expected, err := os.Open(m.expectedFile)
+      if err != nil {
+        t.Fatalf("Can't open expected file: %v", err)
+      }
+      defer expected.Close()
+
+      dest := bytes.NewBufferString("")
+
+      WriteManifest(template, dest, m.dependencies)
+      if err != nil {
+        t.Fatal(err)
+      }
+
+      h1 := sha256.New()
+      if _, err := io.Copy(h1, dest); err != nil {
+        t.Fatalf("Can't prepare sha256 sum of the result: %v", err)
+      }
+
+      h2 := sha256.New()
+      if _, err := io.Copy(h2, expected); err != nil {
+        t.Fatalf("Can't prepare sha256 sum of the expected file: %v", err)
+      }
+
+      sum1 := h1.Sum(nil)
+      sum2 := h2.Sum(nil)
+      if bytes.Compare(sum1, sum2) != 0 {
+        t.Fatalf("The expected manifest file (sha256: %v) differs from what we got (sha256: %v).", hex.EncodeToString(sum2), hex.EncodeToString(sum1))
+      }
+    })
+  }
+}
diff --git a/tools/release.bash b/tools/release.bash
new file mode 100644
index 0000000..338d9e1
--- /dev/null
+++ b/tools/release.bash
@@ -0,0 +1,107 @@
+#!/bin/bash
+#
+# Generate release files (ZIP archives of the extension source code).
+#
+# Precondition: webpack has already built the extension.
+
+# Prints help text
+function usage() {
+  cat <<END
+
+  Usage: $progname [--channel CHANNEL --browser BROWSER]
+
+  optional arguments:
+    -h, --help     show this help message and exit
+    -c, --channel  indicates the channel of the release. Can be "beta"
+                   or "stable". Defaults to "stable".
+    -b, --browser  indicates the target browser for the release. Can be
+                   "chromium", "gecko" or "chromium_mv3".
+                   Defaults to "chromium".
+
+END
+}
+
+# Updates manifest.json field
+function set_manifest_field() {
+  sed -i -E "s/\"$1\": \"[^\"]*\"/\"$1\": \"$2\"/" dist/$browser/manifest.json
+}
+
+# Get options
+opts=$(getopt -l "help,channel:,browser:" -o "hc:b:" -n "$progname" -- "$@")
+eval set -- "$opts"
+
+channel=stable
+browser=chromium
+folder=null
+
+while true; do
+  case "$1" in
+    -h | --help)
+      usage
+      exit
+      ;;
+    -c | --channel)
+      channel="$2"
+      shift 2
+      ;;
+    -b | --browser)
+      browser="$2"
+      shift 2
+      ;;
+    *) break ;;
+  esac
+done
+
+if [[ $channel != "stable" && $channel != "beta" ]]; then
+  echo "channel parameter value is incorrect." >&2
+  usage
+  exit
+fi
+
+if [[ $browser != "chromium" && $browser != "gecko" &&
+      $browser != "chromium_mv3" ]]; then
+  echo "browser parameter value is incorrect." >&2
+  usage
+  exit
+fi
+
+echo "Started building release..."
+
+# This is the version name which git gives us
+version=$(git describe --always --tags --dirty)
+
+# If the version name contains a hyphen then it isn't a release
+# version. This is also the case if it doesn't start with a "v".
+if [[ $version == *"-"* || $version != "v"* ]]; then
+  # As it isn't a release version, setting version number to 0 so it
+  # cannot be uploaded to the Chrome Web Store
+  set_manifest_field "version" "0"
+  set_manifest_field "version_name" "$version-$channel"
+else
+  # It is a release version, set the version fields accordingly.
+  set_manifest_field "version" "${version:1}"
+  set_manifest_field "version_name" "${version:1}-$channel"
+fi
+
+if [[ $channel == "beta" ]]; then
+  # Change manifest.json to label the release as beta
+  set_manifest_field "name" "__MSG_appNameBeta__"
+
+  if [[ $browser == "gecko" ]]; then
+    # Change the extension ID
+    set_manifest_field "id" "twpowertools+beta@avm99963.com"
+  fi
+else
+  if [[ $browser == "gecko" ]]; then
+    set_manifest_field "name" "__MSG_appNameGecko__"
+  fi
+fi
+
+# Create ZIP file for upload to the Chrome Web Store
+mkdir -p out
+rm -rf out/twpowertools-$version-$browser-$channel.zip
+(cd dist/$browser &&
+  zip -rq ../../out/twpowertools-$version-$browser-$channel.zip * -x "*/.git*" \
+  -x "*/\.DS_Store" -x "*/OWNERS")
+
+echo "Done!"
diff --git a/tools/testdata/manifest_frozen.gjson b/tools/testdata/manifest_frozen.gjson
new file mode 100644
index 0000000..8d93b2e
--- /dev/null
+++ b/tools/testdata/manifest_frozen.gjson
@@ -0,0 +1,89 @@
+{
+  "manifest_version": 2,
+  "name": "__MSG_appName__",
+  "version": "0",
+#if defined(CHROMIUM)
+  "version_name": "dirty",
+#endif
+  "description": "__MSG_appDescription__",
+  "icons": {
+    "512": "icons/512.png",
+    "128": "icons/128.png"
+  },
+  "content_scripts": [
+    {
+      "matches": ["https://support.google.com/s/community*"],
+      "js": ["common/content_scripts.js", "content_scripts/console_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/s/community*"],
+      "js": ["common/content_scripts.js", "content_scripts/console_inject_start.js"],
+      "css": ["common/console.css"],
+      "run_at": "document_start"
+    },
+    {
+      "matches": ["https://support.google.com/*/threads*"],
+      "js": ["content_scripts/forum_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/*/thread/*"],
+      "exclude_matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/new*"],
+      "js": ["common/content_scripts.js", "content_scripts/thread_inject.js"],
+      "run_at": "document_end"
+    },
+    {
+      "matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/*"],
+      "exclude_matches": ["https://support.google.com/*/thread/new*"],
+      "js": ["common/content_scripts.js", "common/cs_event_listener.js", "content_scripts/profileindicator_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/*/profile/*"],
+      "js": ["common/content_scripts.js", "content_scripts/profile_inject.js"],
+      "css": ["common/forum.css"]
+    }
+  ],
+  "permissions": [
+    "https://support.google.com/s/community*",
+    "https://support.google.com/*/threads*",
+    "https://support.google.com/*/thread/*",
+    "storage"
+  ],
+  "web_accessible_resources": [
+    "injections/profileindicator_inject.js",
+    "injections/profileindicator_inject.css",
+    "injections/ccdarktheme.css",
+    "injections/batchlock_inject.js"
+  ],
+  "browser_action": {},
+#if defined(CHROMIUM)
+  "options_page": "options.html",
+#endif
+  "options_ui": {
+    "page": "options.html",
+#if defined(CHROMIUM)
+    "chrome_style": true,
+#endif
+#if defined(GECKO)
+    "browser_style": true,
+#endif
+    "open_in_tab": false
+  },
+  "background": {
+#if defined(CHROMIUM)
+    "persistent": false,
+#endif
+    "scripts": [
+      "common/common.js",
+      "background.js"
+    ]
+  },
+#if defined(GECKO)
+  "browser_specific_settings": {
+    "gecko": {
+      "id": "twpowertools@avm99963.com",
+      "strict_min_version": "57.0"
+    }
+  },
+#endif
+  "default_locale": "en"
+}
diff --git a/tools/testdata/manifest_frozen_chromium_expected.json b/tools/testdata/manifest_frozen_chromium_expected.json
new file mode 100644
index 0000000..2bd184b
--- /dev/null
+++ b/tools/testdata/manifest_frozen_chromium_expected.json
@@ -0,0 +1,70 @@
+{
+  "manifest_version": 2,
+  "name": "__MSG_appName__",
+  "version": "0",
+  "version_name": "dirty",
+  "description": "__MSG_appDescription__",
+  "icons": {
+    "512": "icons/512.png",
+    "128": "icons/128.png"
+  },
+  "content_scripts": [
+    {
+      "matches": ["https://support.google.com/s/community*"],
+      "js": ["common/content_scripts.js", "content_scripts/console_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/s/community*"],
+      "js": ["common/content_scripts.js", "content_scripts/console_inject_start.js"],
+      "css": ["common/console.css"],
+      "run_at": "document_start"
+    },
+    {
+      "matches": ["https://support.google.com/*/threads*"],
+      "js": ["content_scripts/forum_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/*/thread/*"],
+      "exclude_matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/new*"],
+      "js": ["common/content_scripts.js", "content_scripts/thread_inject.js"],
+      "run_at": "document_end"
+    },
+    {
+      "matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/*"],
+      "exclude_matches": ["https://support.google.com/*/thread/new*"],
+      "js": ["common/content_scripts.js", "common/cs_event_listener.js", "content_scripts/profileindicator_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/*/profile/*"],
+      "js": ["common/content_scripts.js", "content_scripts/profile_inject.js"],
+      "css": ["common/forum.css"]
+    }
+  ],
+  "permissions": [
+    "https://support.google.com/s/community*",
+    "https://support.google.com/*/threads*",
+    "https://support.google.com/*/thread/*",
+    "storage"
+  ],
+  "web_accessible_resources": [
+    "injections/profileindicator_inject.js",
+    "injections/profileindicator_inject.css",
+    "injections/ccdarktheme.css",
+    "injections/batchlock_inject.js"
+  ],
+  "browser_action": {},
+  "options_page": "options.html",
+  "options_ui": {
+    "page": "options.html",
+    "chrome_style": true,
+    "open_in_tab": false
+  },
+  "background": {
+    "persistent": false,
+    "scripts": [
+      "common/common.js",
+      "background.js"
+    ]
+  },
+  "default_locale": "en"
+}
diff --git a/tools/testdata/manifest_frozen_gecko_expected.json b/tools/testdata/manifest_frozen_gecko_expected.json
new file mode 100644
index 0000000..49792d5
--- /dev/null
+++ b/tools/testdata/manifest_frozen_gecko_expected.json
@@ -0,0 +1,73 @@
+{
+  "manifest_version": 2,
+  "name": "__MSG_appName__",
+  "version": "0",
+  "description": "__MSG_appDescription__",
+  "icons": {
+    "512": "icons/512.png",
+    "128": "icons/128.png"
+  },
+  "content_scripts": [
+    {
+      "matches": ["https://support.google.com/s/community*"],
+      "js": ["common/content_scripts.js", "content_scripts/console_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/s/community*"],
+      "js": ["common/content_scripts.js", "content_scripts/console_inject_start.js"],
+      "css": ["common/console.css"],
+      "run_at": "document_start"
+    },
+    {
+      "matches": ["https://support.google.com/*/threads*"],
+      "js": ["content_scripts/forum_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/*/thread/*"],
+      "exclude_matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/new*"],
+      "js": ["common/content_scripts.js", "content_scripts/thread_inject.js"],
+      "run_at": "document_end"
+    },
+    {
+      "matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/*"],
+      "exclude_matches": ["https://support.google.com/*/thread/new*"],
+      "js": ["common/content_scripts.js", "common/cs_event_listener.js", "content_scripts/profileindicator_inject.js"]
+    },
+    {
+      "matches": ["https://support.google.com/*/profile/*"],
+      "js": ["common/content_scripts.js", "content_scripts/profile_inject.js"],
+      "css": ["common/forum.css"]
+    }
+  ],
+  "permissions": [
+    "https://support.google.com/s/community*",
+    "https://support.google.com/*/threads*",
+    "https://support.google.com/*/thread/*",
+    "storage"
+  ],
+  "web_accessible_resources": [
+    "injections/profileindicator_inject.js",
+    "injections/profileindicator_inject.css",
+    "injections/ccdarktheme.css",
+    "injections/batchlock_inject.js"
+  ],
+  "browser_action": {},
+  "options_ui": {
+    "page": "options.html",
+    "browser_style": true,
+    "open_in_tab": false
+  },
+  "background": {
+    "scripts": [
+      "common/common.js",
+      "background.js"
+    ]
+  },
+  "browser_specific_settings": {
+    "gecko": {
+      "id": "twpowertools@avm99963.com",
+      "strict_min_version": "57.0"
+    }
+  },
+  "default_locale": "en"
+}
diff --git a/tools/testdata/manifest_small1.gjson b/tools/testdata/manifest_small1.gjson
new file mode 100644
index 0000000..5027087
--- /dev/null
+++ b/tools/testdata/manifest_small1.gjson
@@ -0,0 +1,26 @@
+{
+  "foo": "option",
+#if defined(AAA)
+  "bar": 1,
+    #endif                   
+#if defined(    BBB )
+  "count": 2,
+  #if defined(B || C)    
+  "mmm": {},
+#endif
+#if defined(C)
+  #if defined(D)  
+  "ok": true,
+#endif
+#endif
+#if defined(D)
+#if defined(C)
+  "ok2": false,
+#endif
+#endif
+#endif
+#if defined(CDE || AAA)
+  "shouldBe": true,
+#endif
+  "so": 3.14
+}
diff --git a/tools/testdata/manifest_small1_expected.json b/tools/testdata/manifest_small1_expected.json
new file mode 100644
index 0000000..0e32581
--- /dev/null
+++ b/tools/testdata/manifest_small1_expected.json
@@ -0,0 +1,7 @@
+{
+  "foo": "option",
+  "bar": 1,
+  "count": 2,
+  "shouldBe": true,
+  "so": 3.14
+}