Use npm packages in server.go

server.go is updated to serve npm packages instead of bower components.
The updated server:
* returns files from node_modules directory if appropriate redirect
  exists in redirects.json file
* patches some import statements in .js files
  For example:
  import "module/file" -> import "/node_modules/module/file"
  The patch is required, because browsers doesn't know anything about
  node_modules

Note: The file redirect.ts is not used in this change. It is added to
describe json schema here. In an upcoming change it is used to update
source code.

Change-Id: I2435e93e0faf5984b5ebec6ca18f930da01fa9cc
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 1c685ab..901244a 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -56,7 +56,7 @@
     srcs = ["server.go"],
     data = [
         ":fonts.zip",
-        "//polygerrit-ui/app:test_components.zip",
+        "@ui_npm//:node_modules",
     ],
     deps = [
         "@org_golang_x_tools//godoc/vfs/httpfs:go_default_library",
diff --git a/polygerrit-ui/app/redirects.json b/polygerrit-ui/app/redirects.json
new file mode 100644
index 0000000..a51d361
--- /dev/null
+++ b/polygerrit-ui/app/redirects.json
@@ -0,0 +1,50 @@
+{
+  "description": "See tools/node_tools/polygerrit_app_preprocessor/redirects.ts",
+  "redirects": [
+    {
+      "from": "/bower_components/ba-linkify",
+      "to": {
+        "npm_module": "ba-linkify"
+      }
+    },
+    {
+      "from": "/bower_components/es6-promise",
+      "to": {
+        "npm_module": "es6-promise"
+      }
+    },
+    {
+      "from": "/bower_components/fetch",
+      "to": {
+        "npm_module": "whatwg-fetch",
+        "files": {
+          "fetch.js": "dist/fetch.umd.js"
+        }
+      }
+    },
+    {
+      "from": "/bower_components/moment",
+      "to": {
+        "npm_module": "moment"
+      }
+    },
+    {
+      "from": "/bower_components/webcomponentsjs",
+      "to": {
+        "npm_module": "@webcomponents/webcomponentsjs"
+      }
+    },
+    {
+      "from": "/bower_components/page",
+      "to": {
+        "npm_module": "page"
+      }
+    },
+    {
+      "from": "/bower_components",
+      "to": {
+        "npm_module": "polymer-bridges"
+      }
+    }
+  ]
+}
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 5b48785..72380b4 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -46,6 +46,36 @@
 	bundledPluginsPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_assets/[0-9.]*")
 )
 
+type redirectTarget struct {
+	NpmModule string            `json:"npm_module"`
+	Dir       string            `json:"dir"`
+	Files     map[string]string `json:"files"`
+}
+
+type redirects struct {
+	From string         `json:"from"`
+	To   redirectTarget `json:"to"`
+}
+
+type redirectsJson struct {
+	Redirects []redirects `json:"redirects"`
+}
+
+func readRedirects() []redirects {
+	redirectsFile, err := os.Open("app/redirects.json")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer redirectsFile.Close()
+	redirectsFileContent, err := ioutil.ReadAll(redirectsFile)
+	if err != nil {
+		log.Fatal(err)
+	}
+	var result redirectsJson
+	json.Unmarshal([]byte(redirectsFileContent), &result)
+	return result.Redirects
+}
+
 func main() {
 	flag.Parse()
 
@@ -54,20 +84,14 @@
 		log.Fatal(err)
 	}
 
-	componentsArchive, err := openDataArchive("app/test_components.zip")
-	if err != nil {
-		log.Fatal(err)
-	}
-
 	workspace := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
 	if err := os.Chdir(filepath.Join(workspace, "polygerrit-ui")); err != nil {
 		log.Fatal(err)
 	}
 
-	http.Handle("/", addDevHeadersMiddleware(http.FileServer(http.Dir("app"))))
-	http.Handle("/bower_components/",
-		addDevHeadersMiddleware(
-			http.FileServer(httpfs.New(zipfs.New(componentsArchive, "bower_components")))))
+	redirects := readRedirects()
+	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { handleSrcRequest(redirects, w, req) })
+
 	http.Handle("/fonts/",
 		addDevHeadersMiddleware(http.FileServer(httpfs.New(zipfs.New(fontsArchive, "fonts")))))
 
@@ -95,12 +119,114 @@
 
 func addDevHeadersMiddleware(h http.Handler) http.Handler {
 	return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
-		writer.Header().Set("Access-Control-Allow-Origin", "*")
-		writer.Header().Set("Cache-Control", "public, max-age=10, must-revalidate")
+		addDevHeaders(writer)
 		h.ServeHTTP(writer, req)
 	})
 }
 
+func addDevHeaders(writer http.ResponseWriter) {
+	writer.Header().Set("Access-Control-Allow-Origin", "*")
+	writer.Header().Set("Cache-Control", "public, max-age=10, must-revalidate")
+
+}
+
+func getFinalPath(redirects []redirects, originalPath string) string {
+  testComponentsPrefix := "/components/";
+  if strings.HasPrefix(originalPath, testComponentsPrefix) {
+    return "/../node_modules/" + originalPath[len(testComponentsPrefix):]
+  }
+
+	for _, redirect := range redirects {
+		fromDir := redirect.From
+		if !strings.HasSuffix(fromDir, "/") {
+			fromDir = fromDir + "/"
+		}
+		if strings.HasPrefix(originalPath, fromDir) {
+			targetDir := ""
+			if redirect.To.NpmModule != "" {
+				targetDir = "node_modules/" + redirect.To.NpmModule
+			} else {
+				targetDir = redirect.To.Dir
+			}
+			if !strings.HasSuffix(targetDir, "/") {
+				targetDir = targetDir + "/"
+			}
+			if !strings.HasPrefix(targetDir, "/") {
+				targetDir = "/" + targetDir
+			}
+			filename := originalPath[len(fromDir):]
+			if redirect.To.Files != nil {
+				newfilename, found := redirect.To.Files[filename]
+				if found {
+					filename = newfilename
+				}
+			}
+			return targetDir + filename
+		}
+	}
+	return originalPath
+}
+
+func handleSrcRequest(redirects []redirects, writer http.ResponseWriter, originalRequest *http.Request) {
+	parsedUrl, err := url.Parse(originalRequest.RequestURI)
+	if err != nil {
+		writer.WriteHeader(500)
+		return
+	}
+	if parsedUrl.Path == "/bower_components/web-component-tester/browser.js" {
+    http.Redirect(writer, originalRequest, "/bower_components/wct-browser-legacy/browser.js", 301);
+	  return
+  }
+
+	requestPath := getFinalPath(redirects, parsedUrl.Path)
+
+	if !strings.HasPrefix(requestPath, "/") {
+		requestPath = "/" + requestPath
+	}
+
+	data, err := readFile(parsedUrl.Path, requestPath)
+	if err != nil {
+    writer.WriteHeader(404)
+    return
+	}
+	if strings.HasSuffix(requestPath, ".js") {
+		r := regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
+		data = r.ReplaceAll(data, []byte("$1 '/node_modules/$2'"))
+		writer.Header().Set("Content-Type", "application/javascript")
+	} else if strings.HasSuffix(requestPath, ".css") {
+		writer.Header().Set("Content-Type", "text/css")
+	} else if strings.HasSuffix(requestPath, ".html") {
+		writer.Header().Set("Content-Type", "text/html")
+	}
+	writer.WriteHeader(200)
+	addDevHeaders(writer)
+	writer.Write(data)
+}
+
+func readFile(originalPath string, redirectedPath string) ([]byte, error) {
+  pathsToTry := [] string { "app" + redirectedPath }
+  bowerComponentsSuffix := "/bower_components/"
+  nodeModulesPrefix := "/node_modules/"
+
+  if strings.HasPrefix(originalPath, bowerComponentsSuffix) {
+    pathsToTry = append(pathsToTry, "node_modules/wct-browser-legacy/node_modules/" + originalPath[len(bowerComponentsSuffix):])
+    pathsToTry = append(pathsToTry, "node_modules/" + originalPath[len(bowerComponentsSuffix):])
+  }
+
+  if strings.HasPrefix(originalPath, nodeModulesPrefix) {
+    pathsToTry = append(pathsToTry, "node_modules/" + originalPath[len(nodeModulesPrefix):])
+  }
+
+  for _, path := range pathsToTry {
+    data, err := ioutil.ReadFile(path)
+    if err == nil {
+      return data, nil
+    }
+  }
+
+  return nil, errors.New("File not found")
+}
+
 func openDataArchive(path string) (*zip.ReadCloser, error) {
 	absBinPath, err := resourceBasePath()
 	if err != nil {
diff --git a/tools/node_tools/polygerrit_app_preprocessor/redirects.ts b/tools/node_tools/polygerrit_app_preprocessor/redirects.ts
new file mode 100644
index 0000000..0ccd78f
--- /dev/null
+++ b/tools/node_tools/polygerrit_app_preprocessor/redirects.ts
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+
+/** redirects.json schema*/
+export interface JSONRedirects {
+  /** Short text description. Do not used anywhere*/
+  description?: string;
+  /** List of redirects, from the highest to lower priority. */
+  redirects: Redirect[];
+}
+
+/** Redirect - describes one redirect.
+ * Each link in the html file is converted to a path relative to site root
+ * Redirect is applied, if converted link started with 'from'
+ * */
+export interface Redirect {
+  /** from - path prefix. The '/' is added to the end of string if not present */
+  from: string;
+  /** New location - can be either other directory or node module*/
+  to: PathRedirect;
+}
+
+export type PathRedirect = RedirectToDir | RedirectToNodeModule;
+
+/** RedirectToDir - use another dir instead of original one*/
+export interface RedirectToDir {
+  /** New dir (relative to site root)*/
+  dir: string;
+  /** Redirects for files inside directory
+   * Key is the original relative path, value is the new relative path (relative to new dir) */
+  files?: { [name: string]: string }
+}
+
+export interface RedirectToNodeModule {
+  /** Import from this node module instead of directory*/
+  npm_module: string;
+  /** Redirects for files inside node module
+   * Key is the original relative path, value is the new relative path (relative to npm_module) */
+  files?: { [name: string]: string }
+}
+
+export function isRedirectToNodeModule(redirect: PathRedirect): redirect is RedirectToNodeModule {
+  return (redirect as RedirectToNodeModule).npm_module !== undefined;
+}
+
+export function isRedirectToDir(redirect: PathRedirect): redirect is RedirectToDir {
+  return (redirect as RedirectToDir).dir !== undefined;
+}