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;
+}