--
Change 1 of 19 by Filipe Silva <filipematossilva@gmail.com>:

Use native.sh_binary for cross-platform ts_devserver

--
Change 2 of 19 by Filipe Silva <filipematossilva@gmail.com>:

Use runfiles resolution in ts_devserver

--
Change 3 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

Resolve all devserver files using runfiles manifest

--
Change 4 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

Support index.html files in subdirectories on windows

--
Change 5 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

Properly handle directories for symlinked runfiles in devserver

--
Change 6 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

Update gazelle and properly resolve Go runfile library

--
Change 7 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

Add workaround for specifying serving_path on windows.

--
Change 8 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

Do not resolve entry_module as runfile

* As with https://github.com/bazelbuild/rules_typescript/pull/327/commits/b739d771fb6c71f0d2b4c25e04a1aabb6808c27d, the `entry_module` is resolved through `rlocation`. This is wrong because the entry_module is not a real runfile.

--
Change 9 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

Support serving runfiles through absolute manifest path

--
Change 10 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

fixup! Resolve all devserver files using runfiles manifest

Address feedback

--
Change 11 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

fixup! Resolve all devserver files using runfiles manifest

Update rules_go version to avoid incompatible protobuf version

--
Change 12 of 19 by Minko Gechev <mgechev@google.com>:

Fixes for golint and refactoring for g3

--
Change 13 of 19 by Minko Gechev <mgechev@google.com>:

Refactor Runfile invocation and fix golint errors

--
Change 14 of 19 by Minko Gechev <mgechev@google.com>:

Refactor Runfile invocation and fix golint errors

--
Change 15 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

fixup! Refactor Runfile invocation and fix golint errors

Fix accidentally revert

--
Change 16 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

fixup! Refactor Runfile invocation and fix golint errors

Fix wrong package name

--
Change 17 of 19 by Minko Gechev <mgechev@google.com>:

Update method name

--
Change 18 of 19 by Minko Gechev <mgechev@google.com>:

Do not depend on external rules_go package

--
Change 19 of 19 by Paul Gschwendtner <paulgschwendtner@gmail.com>:

Add logic to resolve runfiles within G3 without using external runfile helpers

Closes #327

PiperOrigin-RevId: 229863281
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 298966d..fef6760 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -49,4 +49,6 @@
     - "--action_env=PATH"
     - "--test_env=PATH"
     test_targets:
+    - "--"
     - "..."
+    - "-//devserver/devserver:go_default_test"
diff --git a/WORKSPACE b/WORKSPACE
index 7a18919..a62f07b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -174,3 +174,8 @@
     name = "disable_tsetse_for_external_test",
     path = "internal/e2e/disable_tsetse_for_external",
 )
+
+local_repository(
+    name = "devserver_test_workspace",
+    path = "devserver/devserver/test/test-workspace",
+)
diff --git a/devserver/BUILD.bazel b/devserver/BUILD.bazel
index f70274b..a79f545 100644
--- a/devserver/BUILD.bazel
+++ b/devserver/BUILD.bazel
@@ -5,12 +5,16 @@
 
 go_library(
     name = "go_default_library",
-    srcs = ["main.go"],
+    srcs = [
+        "main.go",
+        "runfile-filesystem.go",
+    ],
     importpath = "github.com/bazelbuild/rules_typescript/devserver",
     visibility = ["//visibility:private"],
     deps = [
         "//devserver/concatjs:go_default_library",
         "//devserver/devserver:go_default_library",
+        "//devserver/runfiles:go_default_library",
     ],
 )
 
diff --git a/devserver/concatjs/BUILD.bazel b/devserver/concatjs/BUILD.bazel
deleted file mode 100644
index 6cc1a74..0000000
--- a/devserver/concatjs/BUILD.bazel
+++ /dev/null
@@ -1,14 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-
-go_library(
-    name = "go_default_library",
-    srcs = ["concatjs.go"],
-    importpath = "github.com/bazelbuild/rules_typescript/devserver/concatjs",
-    visibility = ["//visibility:public"],
-)
-
-go_test(
-    name = "go_default_test",
-    srcs = ["concatjs_test.go"],
-    embed = [":go_default_library"],
-)
diff --git a/devserver/concatjs/concatjs.go b/devserver/concatjs/concatjs.go
index b6eea8a..bde0244 100644
--- a/devserver/concatjs/concatjs.go
+++ b/devserver/concatjs/concatjs.go
@@ -96,14 +96,16 @@
 // FileSystem is the interface to reading files from disk.
 // It's abstracted into an interface to allow tests to replace it.
 type FileSystem interface {
-	statMtime(filename string) (time.Time, error)
-	readFile(filename string) ([]byte, error)
+	StatMtime(filename string) (time.Time, error)
+	ReadFile(filename string) ([]byte, error)
+	ResolvePath(root string, file string) (string, error)
 }
 
-// realFileSystem implements FileSystem by actual disk access.
-type realFileSystem struct{}
+// RealFileSystem implements FileSystem by actual disk access.
+type RealFileSystem struct{}
 
-func (fs *realFileSystem) statMtime(filename string) (time.Time, error) {
+// StatMtime gets the last modification time of the specified file.
+func (fs *RealFileSystem) StatMtime(filename string) (time.Time, error) {
 	s, err := os.Stat(filename)
 	if err != nil {
 		return time.Time{}, err
@@ -111,10 +113,19 @@
 	return s.ModTime(), nil
 }
 
-func (fs *realFileSystem) readFile(filename string) ([]byte, error) {
+// ReadFile reads the specified file using the real filesystem.
+func (fs *RealFileSystem) ReadFile(filename string) ([]byte, error) {
 	return ioutil.ReadFile(filename)
 }
 
+// ResolvePath resolves the specified path within a given root by joining root and the filepath.
+// This is only works if the specified file is located within the given root in the
+// real filesystem. This does not work in Bazel where requested files aren't always
+// located within the specified root. Files would need to be resolved as runfiles.
+func (fs *RealFileSystem) ResolvePath(root string, file string) (string, error) {
+	return filepath.Join(root, file), nil
+}
+
 // FileCache caches a set of files in memory and provides a single
 // method, WriteFiles(), that streams them out in the concatjs format.
 type FileCache struct {
@@ -129,7 +140,7 @@
 // will use the real file system if nil.
 func NewFileCache(root string, fs FileSystem) *FileCache {
 	if fs == nil {
-		fs = &realFileSystem{}
+		fs = &RealFileSystem{}
 	}
 	return &FileCache{
 		root:    root,
@@ -140,10 +151,11 @@
 
 type cacheEntry struct {
 	// err holds an error encountered while updating the entry; if
-	// it's non-nil, then mtime and contents are invalid.
-	err      error
-	mtime    time.Time
-	contents []byte
+	// it's non-nil, then mtime, contents and the resolved path are invalid.
+	err          error
+	mtime        time.Time
+	contents     []byte
+	resolvedPath string
 }
 
 // manifestFiles parses a manifest, returning a list of the files in the manifest.
@@ -213,8 +225,8 @@
 
 // refresh ensures a single cacheEntry is up to date.  It stat()s and
 // potentially reads the contents of the file it is caching.
-func (e *cacheEntry) refresh(root, path string, fs FileSystem) error {
-	mt, err := fs.statMtime(filepath.Join(root, path))
+func (e *cacheEntry) refresh(fs FileSystem) error {
+	mt, err := fs.StatMtime(e.resolvedPath)
 	if err != nil {
 		return err
 	}
@@ -222,7 +234,7 @@
 		return nil // up to date
 	}
 
-	contents, err := fileContents(root, path, fs)
+	contents, err := fileContents(e.resolvedPath, fs)
 	if err != nil {
 		return err
 	}
@@ -231,6 +243,10 @@
 	return nil
 }
 
+// Convert Windows paths separators. We can use this to create canonical paths that
+// can be also used as browser source urls.
+var pathReplacer = strings.NewReplacer("\\", "/")
+
 // refreshFiles stats the given files and updates the cache for them.
 func (cache *FileCache) refreshFiles(files []string) {
 	// Stating many files asynchronously is faster on network file systems.
@@ -248,7 +264,7 @@
 		// TODO(evanm): benchmark limiting this to fewer goroutines.
 		go func() {
 			w := <-work
-			w.entry.err = w.entry.refresh(cache.root, w.path, cache.fs)
+			w.entry.err = w.entry.refresh(cache.fs)
 			wg.Done()
 		}()
 	}
@@ -256,7 +272,21 @@
 	for _, path := range files {
 		entry := cache.entries[path]
 		if entry == nil {
-			entry = &cacheEntry{}
+			// Resolve path only once for a cache entry. The resolved path will be part of the
+			// cache item.
+			resolvedPath, err := cache.fs.ResolvePath(cache.root, path)
+
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "could not resolve path %s. %v\n", path, err)
+				os.Exit(1)
+			}
+
+			// Create a new cache entry with the corresponding resolved path. Also normalize the path
+			// before storing it persistently in the cache. The normalizing is good to do here because
+			// the path might be used in browser source URLs and should be kept in posix format.
+			entry = &cacheEntry{
+				resolvedPath: pathReplacer.Replace(resolvedPath),
+			}
 			cache.entries[path] = entry
 		}
 		work <- workItem{path, entry}
@@ -275,10 +305,8 @@
 var googModuleRegExp = regexp.MustCompile(`(?m)^\s*goog\.module\s*\(\s*['"]`)
 
 // fileContents returns escaped JS file contents for the given path.
-// The path is resolved relative to root, but the path without root is used as the path
-// in the source map.
-func fileContents(root, path string, fs FileSystem) ([]byte, error) {
-	contents, err := fs.readFile(filepath.Join(root, path))
+func fileContents(path string, fs FileSystem) ([]byte, error) {
+	contents, err := fs.ReadFile(path)
 	if err != nil {
 		return nil, err
 	}
diff --git a/devserver/concatjs/concatjs_test.go b/devserver/concatjs/concatjs_test.go
index 2ee47f1..00793be 100644
--- a/devserver/concatjs/concatjs_test.go
+++ b/devserver/concatjs/concatjs_test.go
@@ -5,6 +5,7 @@
 	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"path/filepath"
 	"reflect"
 	"strings"
 	"testing"
@@ -41,21 +42,26 @@
 }
 
 type fakeFileSystem struct {
-	fakeReadFile  func(filename string) ([]byte, error)
-	fakeStatMtime func(filename string) (time.Time, error)
+	fakeReadFile    func(filename string) ([]byte, error)
+	fakeStatMtime   func(filename string) (time.Time, error)
+	fakeResolvePath func(root string, filename string) (string, error)
 }
 
-func (fs *fakeFileSystem) readFile(filename string) ([]byte, error) {
+func (fs *fakeFileSystem) ReadFile(filename string) ([]byte, error) {
 	return fs.fakeReadFile(filename)
 }
 
-func (fs *fakeFileSystem) statMtime(filename string) (time.Time, error) {
+func (fs *fakeFileSystem) StatMtime(filename string) (time.Time, error) {
 	return fs.fakeStatMtime(filename)
 }
 
+func (fs *fakeFileSystem) ResolvePath(root string,  filename string) (string, error) {
+	return fs.fakeResolvePath(root, filename)
+}
+
 func TestWriteFiles(t *testing.T) {
-	// Convert Windows paths separators for easier matching.
-	var pathReplacer = strings.NewReplacer("\\", "/")
+	var inputFiles = []string{"a", "missing", "module"}
+
 	fs := fakeFileSystem{
 		fakeReadFile: func(filename string) ([]byte, error) {
 			var normalizedFilename = pathReplacer.Replace(filename)
@@ -77,20 +83,23 @@
 				return time.Time{}, fmt.Errorf("unexpected file stat: %s", normalizedFilename)
 			}
 		},
+		fakeResolvePath: func(root string, filename string) (string, error) {
+			return filepath.Join(root, filename), nil
+		},
 	}
 
 	cache := NewFileCache("root", &fs)
 
 	var b bytes.Buffer
-	cache.WriteFiles(&b, []string{"a", "missing", "module"})
+	cache.WriteFiles(&b, inputFiles)
 
 	got := string(b.Bytes())
 	want := `// a
-eval('a content\n\n//# sourceURL=http://concatjs/a\n');
+eval('a content\n\n//# sourceURL=http://concatjs/root/a\n');
 // missing
 throw new Error('loading missing failed: unexpected file stat: root/missing');
 // module
-goog.loadModule('// A module\ngoog.module(\'hello\');\n\n//# sourceURL=http://concatjs/module\n');
+goog.loadModule('// A module\ngoog.module(\'hello\');\n\n//# sourceURL=http://concatjs/root/module\n');
 `
 
 	if got != want {
@@ -109,6 +118,9 @@
 		fakeStatMtime: func(string) (time.Time, error) {
 			return time.Time{}, nil
 		},
+		fakeResolvePath: func(root string, filename string) (string, error) {
+			return filepath.Join(root, filename), nil
+		},
 	}
 
 	var b bytes.Buffer
@@ -141,6 +153,46 @@
 	}
 }
 
+func TestCustomFileResolving(t *testing.T) {
+	fs := fakeFileSystem{
+		fakeReadFile: func(filename string) ([]byte, error) {
+			var normalizedFilename = pathReplacer.Replace(filename)
+			switch normalizedFilename {
+			case "/system_root/bazel-bin/a.txt":
+				return []byte("a content"), nil
+			case "/system_root/bazel-bin/nested/b.js":
+				return []byte("b content"), nil
+			default:
+				return []byte{}, fmt.Errorf("unexpected file read: %s", normalizedFilename)
+			}
+		},
+		fakeStatMtime: func(filename string) (time.Time, error) {
+				return time.Now(), nil
+		},
+		fakeResolvePath: func(root string, filename string) (string, error) {
+			// For this test, we use an absolute root. This is similar to how
+			// Bazel resolves runfiles through the manifest.
+			return filepath.Join("/system_root/bazel-bin/", filename), nil
+		},
+	}
+
+	cache := NewFileCache("", &fs)
+
+	var b bytes.Buffer
+	cache.WriteFiles(&b, []string{"a.txt", "nested/b.js"})
+
+	actual := string(b.Bytes())
+	expected := `// a.txt
+eval('a content\n\n//# sourceURL=http://concatjs//system_root/bazel-bin/a.txt\n');
+// nested/b.js
+eval('b content\n\n//# sourceURL=http://concatjs//system_root/bazel-bin/nested/b.js\n');
+`
+
+	if actual != expected {
+		t.Errorf("Response differs, actual: %s, expected: %s", actual, expected)
+	}
+}
+
 func runOneRequest(b *testing.B, handler http.Handler, gzip bool) {
 	req, err := http.NewRequest("GET", "", nil)
 	if err != nil {
diff --git a/devserver/devserver/BUILD.bazel b/devserver/devserver/BUILD.bazel
index 484068d..a7c2735 100644
--- a/devserver/devserver/BUILD.bazel
+++ b/devserver/devserver/BUILD.bazel
@@ -5,10 +5,20 @@
     srcs = ["devserver.go"],
     importpath = "github.com/bazelbuild/rules_typescript/devserver/devserver",
     visibility = ["//visibility:public"],
+    deps = [
+        "//devserver/runfiles:go_default_library",
+    ],
 )
 
 go_test(
     name = "go_default_test",
     srcs = ["devserver_test.go"],
+    # Required runfiles for the devserver tests.
+    data = [
+        "test/index.html",
+        "test/relative.html",
+        "@devserver_test_workspace//:sources",
+    ],
     embed = [":go_default_library"],
 )
+
diff --git a/devserver/devserver/devserver.go b/devserver/devserver/devserver.go
index 95c12d7..4748908 100644
--- a/devserver/devserver/devserver.go
+++ b/devserver/devserver/devserver.go
@@ -11,6 +11,8 @@
 	"path/filepath"
 	"strings"
 	"time"
+
+	"github.com/bazelbuild/rules_typescript/devserver/runfiles"
 )
 
 // Convert Windows paths separators.
@@ -93,16 +95,15 @@
 
 // CreateFileHandler returns an http handler to locate files on disk
 func CreateFileHandler(servingPath, manifest string, pkgs []string, base string) http.HandlerFunc {
-	pkgPaths := chainedDir{}
+	// We want to add the root runfile path because by default developers should be able to request
+	// runfiles through their absolute manifest path (e.g. "my_workspace_name/src/file.css")
+	// We use the empty string package because of the different algorithm for
+	// file resolution used internally and externally.
+	pkgPaths := dirHTTPFileSystem{[]string{"./"}, base}
 	for _, pkg := range pkgs {
-		path := pathReplacer.Replace(filepath.Join(base, pkg))
-		if _, err := os.Stat(path); err != nil {
-			fmt.Fprintf(os.Stderr, "Cannot read server root package at %s: %v\n", path, err)
-			os.Exit(1)
-		}
-		pkgPaths = append(pkgPaths, http.Dir(path))
+		pkgPaths.files = append(pkgPaths.files, pathReplacer.Replace(pkg))
 	}
-	pkgPaths = append(pkgPaths, http.Dir(base))
+	pkgPaths.files = append(pkgPaths.files, base)
 
 	fileHandler := http.FileServer(pkgPaths).ServeHTTP
 
@@ -122,12 +123,18 @@
 	indexHandler := func(w http.ResponseWriter, r *http.Request) {
 		// search through pkgs for the first index.html file found if any exists
 		for _, pkg := range pkgs {
-			// defaultIndex is not cached, so that a user's edits will be reflected.
-			defaultIndex := pathReplacer.Replace(filepath.Join(base, pkg, "index.html"))
-			if _, err := os.Stat(defaultIndex); err == nil {
-				http.ServeFile(w, r, defaultIndex)
-				return
+			// File path is not cached, so that a user's edits will be reflected.
+			userIndexFile, err := runfiles.Runfile(base, pathReplacer.Replace(filepath.Join(pkg, "index.html")))
+
+			// In case the potential user index file couldn't be found in the runfiles,
+			// just continue searching.
+			if _, statErr := os.Stat(userIndexFile); err != nil || statErr != nil {
+				continue
 			}
+
+			// We can assume that the file is readable if it's listed in the runfiles manifest.
+			http.ServeFile(w, r, userIndexFile)
+			return
 		}
 		content := bytes.NewReader(defaultPage)
 		http.ServeContent(w, r, "index.html", time.Now(), content)
@@ -166,40 +173,49 @@
 	return indexOnNotFoundHandler
 }
 
-// chainedDir implements http.FileSystem by looking in the list of dirs one after each other.
-type chainedDir []http.Dir
+// dirHTTPFileSystem implements http.FileSystem by looking in the list of dirs one after each other.
+type dirHTTPFileSystem struct {
+	files []string
+	base string
+}
 
-func (chain chainedDir) Open(name string) (http.File, error) {
-	for _, dir := range chain {
-		f, err := dir.Open(name)
-		if os.IsNotExist(err) {
-			continue
-		}
-		if err != nil {
-			return nil, err
-		}
+func (fs dirHTTPFileSystem) Open(name string) (http.File, error) {
+	for _, packageName := range fs.files {
+		filePackageName := filepath.Join(packageName, name)
+		realFilePath, err := runfiles.Runfile(fs.base, filePackageName)
+		stat, statErr := os.Stat(realFilePath)
 
-		// Do not return a directory, since FileServer will either:
-		//  1) serve the index.html file -or-
-		//  2) fall back to directory listings
-		// In place of (2), we prefer to fall back to our index.html. We accomplish
-		// this by lying to the FileServer that the directory doesn't exist.
-		stat, err := f.Stat()
-		if err != nil {
-			return nil, err
-		}
-		if stat.IsDir() {
-			// Make sure to close the previous file handle before moving to a different file.
-			f.Close()
-			indexName := pathReplacer.Replace(filepath.Join(name, "index.html"))
-			f, err := dir.Open(indexName)
-			if os.IsNotExist(err) {
+		if err != nil || statErr != nil {
+			// In case the runfile could not be found, we also need to check that the requested
+			// path does not refer to a directory containing an "index.html" file. This can
+			// happen if Bazel runs without runfile symlinks, where only files can be resolved
+			// from the manifest. In that case we dirty check if there is a "index.html" file.
+			realFilePath, err = runfiles.Runfile(fs.base, filepath.Join(filePackageName, "index.html"))
+
+			// Continue searching if the runfile couldn't be found for the request filed.
+			if _, statErr := os.Stat(realFilePath); err != nil || statErr != nil {
 				continue
 			}
-			return f, err
 		}
 
-		return f, nil
+		// In case the resolved file resolves to a directory. This can only happen if
+		// Bazel runs with symlinked runfiles (e.g. on MacOS, linux). In that case, we
+		// just look for a index.html in the directory.
+		if stat.IsDir() {
+			realFilePath, err = runfiles.Runfile(fs.base, filepath.Join(filePackageName, "index.html"))
+
+			// In case the index.html file of the requested directory couldn't be found,
+			// we just continue searching.
+			if err != nil {
+				continue
+			}
+		}
+
+
+		// We can assume that the file is present, if it's listed in the runfile manifest. Though, we
+		// return the error, in case something prevented the read-access.
+		return os.Open(realFilePath)
 	}
+
 	return nil, os.ErrNotExist
 }
diff --git a/devserver/devserver/devserver_test.go b/devserver/devserver/devserver_test.go
index 2fc77c1..0c0ba56 100644
--- a/devserver/devserver/devserver_test.go
+++ b/devserver/devserver/devserver_test.go
@@ -44,63 +44,48 @@
 }
 
 func TestDevserverFileHandling(t *testing.T) {
-	_, del := tmpfile(t, "TestIndexServing/manifest.MF", "file1.js\nfile2.js")
-	defer del()
-	_, delIdx := tmpfile(t, "TestIndexServing/pkg1/index.html", "contents of index.html")
-	defer delIdx()
-	_, del = tmpfile(t, "TestIndexServing/pkg1/foo.html", "contents of foo.html")
-	defer del()
-	_, del = tmpfile(t, "TestIndexServing/pkg2/bar.html", "contents of bar.html")
-	defer del()
-	_, del = tmpfile(t, "TestIndexServing/pkg2/foo.html", "contents of foo.html in pkg2")
-	defer del()
-	_, del = tmpfile(t, "TestIndexServing/pkg2/rpc/items/index.html", "contents of rpc/items/index.html")
-	defer del()
-	_, del = tmpfile(t, "TestIndexServing/pkg3/baz.html", "contents of baz.html in pkg3")
-	defer del()
+	handler := CreateFileHandler("/app.js", "manifest.MF", []string{
+		// This verifies that we can resolve relatively to the current package. Usually the
+		// devserver Bazel rule adds the current package here.
+		"build_bazel_rules_typescript/devserver/devserver",
+		// Verifies that we can specify subfolders of workspaces
+		"build_bazel_rules_typescript/devserver/devserver/test",
+		// Verifies that we can specify external workspaces as root dirs.
+		"devserver_test_workspace",
+		// Verifies that we can specify subfolders from external workspaces.
+		"devserver_test_workspace/pkg2",
+	}, "")
 
-	handler := CreateFileHandler("/app.js", "manifest.MF", []string{"pkg1", "pkg2"},
-		filepath.Join(os.Getenv("TEST_TMPDIR"), "TestIndexServing"))
 	defaultPageContent := `<script src="/app.js">`
 
 	tests := []struct {
 		code    int
 		url     string
 		content string
-		delIdx  bool
 	}{
 		// index file from pkg1.
-		{http.StatusOK, "/", "contents of index.html", false},
+		{http.StatusOK, "/", "contents of index.html"},
 		// index file as a response to not found handler.
-		{http.StatusNotFound, "/no/such/dir", "contents of index.html", false},
+		{http.StatusNotFound, "/no/such/dir", "contents of index.html"},
 		// index file as a response to not found handler.
-		{http.StatusNotFound, "/no/such/dir/", "contents of index.html", false},
+		{http.StatusNotFound, "/no/such/dir/", "contents of index.html"},
 		// index file as a response to a directory that is found.
-		{http.StatusNotFound, "/pkg2/", "contents of index.html", false},
-		// file from the base package.
-		{http.StatusOK, "/foo.html", "contents of foo.html", false},
+		{http.StatusNotFound, "/pkg2/", "contents of index.html"},
+		// file from relative to base package.
+		{http.StatusOK, "/test/relative.html", "contents of relative.html"},
 		// file from the base package with full path.
-		{http.StatusOK, "/pkg1/foo.html", "contents of foo.html", false},
+		{http.StatusOK, "/pkg1/foo.html", "contents of foo.html"},
 		// file from pkg2.
-		{http.StatusOK, "/bar.html", "contents of bar.html", false},
+		{http.StatusOK, "/bar.html", "contents of bar.html"},
 		// file from pkg2 with full path.
-		{http.StatusOK, "/pkg2/bar.html", "contents of bar.html", false},
+		{http.StatusOK, "/pkg2/bar.html", "contents of bar.html"},
 		// index file from disk
-		{http.StatusOK, "/rpc/items", "contents of rpc/items/index.html", false},
+		{http.StatusOK, "/rpc/items", "contents of rpc/items/index.html"},
 		// file from an unrelated package.
-		{http.StatusOK, "/pkg3/baz.html", "contents of baz.html in pkg3", false},
-		// generated index for root.
-		{http.StatusOK, "/", `<script src="/app.js">`, true},
-		// generated index as a response to not found handler.
-		{http.StatusNotFound, "/no/such/dir", defaultPageContent, true},
-		// generated index file as a response to a directory that is found.
-		{http.StatusNotFound, "/pkg2/", defaultPageContent, true},
+		{http.StatusOK, "/pkg3/baz.html", "contents of baz.html in pkg3"},
 	}
 
 	for _, tst := range tests {
-		if tst.delIdx {
-			delIdx() // from here on, use the generated index.
-		}
 		code, body := req(handler, fmt.Sprintf("http://test%s", tst.url))
 		if code != tst.code {
 			t.Errorf("got %d, expected %d", code, tst.code)
@@ -108,8 +93,65 @@
 		if !strings.Contains(body, tst.content) {
 			t.Errorf("expected %q to contain %q, got %q", tst.url, tst.content, body)
 		}
-		if !tst.delIdx && strings.Contains(body, defaultPageContent) {
+		if strings.Contains(body, defaultPageContent) {
 			t.Errorf("got %q, default page shouldn't be part of response", body)
 		}
 	}
 }
+
+func TestDevserverGeneratedIndexFile(t *testing.T) {
+	handler := CreateFileHandler("/app.js", "manifest.MF", []string{
+		"devserver_test_workspace",
+	}, "")
+	defaultPageContent := `<script src="/app.js">`
+
+	tests := []struct {
+		code    int
+		url     string
+		content string
+	}{
+		// Assert generated index for root.
+		{http.StatusOK, "/", defaultPageContent},
+		// Assert generated index as a response to not found handler.
+		{http.StatusNotFound, "/no/such/dir", defaultPageContent},
+		// Assert index file as a response to a directory that is found, but does not
+		// have an index file.
+		{http.StatusNotFound, "/pkg2/", defaultPageContent},
+	}
+
+	for _, tst := range tests {
+		code, body := req(handler, fmt.Sprintf("http://test%s", tst.url))
+		if code != tst.code {
+			t.Errorf("got %d, expected %d", code, tst.code)
+		}
+		if !strings.Contains(body, tst.content) {
+			t.Errorf("expected %q to contain %q, got %q", tst.url, tst.content, body)
+		}
+	}
+}
+
+func TestDevserverAbsoluteRunfileRequest(t *testing.T) {
+	handler := CreateFileHandler("/app.js", "manifest.MF", []string{}, "")
+
+	tests := []struct {
+		code    int
+		url     string
+		content string
+	}{
+		// Assert that it's possible to request a runfile through it's absolute manifest path.
+		{http.StatusOK, "/devserver_test_workspace/pkg2/bar.html", "contents of bar.html"},
+		// Assert that it's possible to request a runfile directory through it's absolute manifest path. This
+		// should resolve to the directories "index.html" file.
+		{http.StatusOK, "/devserver_test_workspace/pkg1", "contents of index.html"},
+	}
+
+	for _, tst := range tests {
+		code, body := req(handler, fmt.Sprintf("http://test%s", tst.url))
+		if code != tst.code {
+			t.Errorf("got %d, expected %d", code, tst.code)
+		}
+		if !strings.Contains(body, tst.content) {
+			t.Errorf("expected %q to contain %q, got %q", tst.url, tst.content, body)
+		}
+	}
+}
diff --git a/devserver/devserver/test/index.html b/devserver/devserver/test/index.html
new file mode 100644
index 0000000..c04b444
--- /dev/null
+++ b/devserver/devserver/test/index.html
@@ -0,0 +1 @@
+contents of index.html
\ No newline at end of file
diff --git a/devserver/devserver/test/relative.html b/devserver/devserver/test/relative.html
new file mode 100644
index 0000000..a0e4874
--- /dev/null
+++ b/devserver/devserver/test/relative.html
@@ -0,0 +1 @@
+contents of relative.html
\ No newline at end of file
diff --git a/devserver/devserver/test/test-workspace/BUILD.bazel b/devserver/devserver/test/test-workspace/BUILD.bazel
new file mode 100644
index 0000000..d2c2e9d
--- /dev/null
+++ b/devserver/devserver/test/test-workspace/BUILD.bazel
@@ -0,0 +1,6 @@
+filegroup(
+    name = "sources",
+    srcs = glob(["**/*"]),
+    visibility = ["//visibility:public"],
+)
+
diff --git a/devserver/devserver/test/test-workspace/WORKSPACE b/devserver/devserver/test/test-workspace/WORKSPACE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/devserver/devserver/test/test-workspace/WORKSPACE
diff --git a/devserver/devserver/test/test-workspace/pkg1/foo.html b/devserver/devserver/test/test-workspace/pkg1/foo.html
new file mode 100644
index 0000000..7923b1b
--- /dev/null
+++ b/devserver/devserver/test/test-workspace/pkg1/foo.html
@@ -0,0 +1 @@
+contents of foo.html
\ No newline at end of file
diff --git a/devserver/devserver/test/test-workspace/pkg1/index.html b/devserver/devserver/test/test-workspace/pkg1/index.html
new file mode 100644
index 0000000..c04b444
--- /dev/null
+++ b/devserver/devserver/test/test-workspace/pkg1/index.html
@@ -0,0 +1 @@
+contents of index.html
\ No newline at end of file
diff --git a/devserver/devserver/test/test-workspace/pkg2/bar.html b/devserver/devserver/test/test-workspace/pkg2/bar.html
new file mode 100644
index 0000000..70646c6
--- /dev/null
+++ b/devserver/devserver/test/test-workspace/pkg2/bar.html
@@ -0,0 +1 @@
+contents of bar.html
\ No newline at end of file
diff --git a/devserver/devserver/test/test-workspace/pkg2/foo.html b/devserver/devserver/test/test-workspace/pkg2/foo.html
new file mode 100644
index 0000000..9c5a98c
--- /dev/null
+++ b/devserver/devserver/test/test-workspace/pkg2/foo.html
@@ -0,0 +1 @@
+contents of foo.html in pkg2
\ No newline at end of file
diff --git a/devserver/devserver/test/test-workspace/pkg2/rpc/items/index.html b/devserver/devserver/test/test-workspace/pkg2/rpc/items/index.html
new file mode 100644
index 0000000..3850cc9
--- /dev/null
+++ b/devserver/devserver/test/test-workspace/pkg2/rpc/items/index.html
@@ -0,0 +1 @@
+contents of rpc/items/index.html
\ No newline at end of file
diff --git a/devserver/devserver/test/test-workspace/pkg3/baz.html b/devserver/devserver/test/test-workspace/pkg3/baz.html
new file mode 100644
index 0000000..f471ef4
--- /dev/null
+++ b/devserver/devserver/test/test-workspace/pkg3/baz.html
@@ -0,0 +1 @@
+contents of baz.html in pkg3
\ No newline at end of file
diff --git a/devserver/runfile-filesystem.go b/devserver/runfile-filesystem.go
new file mode 100644
index 0000000..3f99c2f
--- /dev/null
+++ b/devserver/runfile-filesystem.go
@@ -0,0 +1,35 @@
+// Main package that provides a command line interface for starting a Bazel devserver
+// using Bazel runfile resolution and ConcatJS for in-memory bundling of specified AMD files.
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"time"
+
+	"github.com/bazelbuild/rules_typescript/devserver/runfiles"
+)
+
+// RunfileFileSystem implements FileSystem type from concatjs.
+type RunfileFileSystem struct{}
+
+// StatMtime gets the filestamp for the last file modification.
+func (fs *RunfileFileSystem) StatMtime(filename string) (time.Time, error) {
+	s, err := os.Stat(filename)
+	if err != nil {
+		return time.Time{}, err
+	}
+	return s.ModTime(), nil
+}
+
+// ReadFile reads a file given its file name
+func (fs *RunfileFileSystem) ReadFile(filename string) ([]byte, error) {
+	return ioutil.ReadFile(filename)
+}
+
+// ResolvePath resolves the specified path within a given root using Bazel's runfile resolution.
+// This is necessary because on Windows, runfiles are not symlinked and need to be
+// resolved using the runfile manifest file.
+func (fs *RunfileFileSystem) ResolvePath(root string, file string) (string, error) {
+	return runfiles.Runfile(root, file)
+}
diff --git a/devserver/runfiles/runfiles.go b/devserver/runfiles/runfiles.go
new file mode 100644
index 0000000..1d03448
--- /dev/null
+++ b/devserver/runfiles/runfiles.go
@@ -0,0 +1,10 @@
+// Package runfiles that provides utility helpers for resolving Bazel runfiles within Go.
+package runfiles
+
+import "github.com/bazelbuild/rules_go/go/tools/bazel"
+
+// Runfile returns the base directory to the bazel runfiles
+func Runfile(_ string, path string) (string, error) {
+	return bazel.Runfile(path)
+}
+
diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel
index 7864c07..a5a3072 100644
--- a/examples/BUILD.bazel
+++ b/examples/BUILD.bazel
@@ -17,6 +17,8 @@
 
 package(default_visibility = ["//visibility:public"])
 
+exports_files(["tsconfig.json"])
+
 ts_library(
     name = "types",
     srcs = ["types.d.ts"],
diff --git a/examples/devserver/BUILD.bazel b/examples/devserver/BUILD.bazel
new file mode 100644
index 0000000..f29149e
--- /dev/null
+++ b/examples/devserver/BUILD.bazel
@@ -0,0 +1,43 @@
+load("//:defs.bzl", "ts_devserver", "ts_library")
+
+ts_library(
+    name = "app",
+    srcs = ["app.ts"],
+    tsconfig = "//examples:tsconfig.json",
+    deps = [
+        "@npm//@types/node",
+    ],
+)
+
+ts_devserver(
+    name = "devserver",
+    additional_root_paths = [
+        "npm/node_modules/tslib",
+        "build_bazel_rules_typescript/examples/devserver/",
+    ],
+    port = 80,
+    serving_path = "/bundle.js",
+    static_files = [
+        # Files you want to import from the "additional_root_paths", still need to be explicitly specified
+        # as files that should be served. The root paths just make it more convenient to import those dependencies.
+        "@npm//tslib",
+        ":say-hello",
+        ":print-host",
+        ":index.html",
+    ],
+    # Dependencies that produce JavaScript output will be automatically picked up by ConcatJS and will be
+    # part of the serving_path bundle.
+    deps = [":app"],
+)
+
+genrule(
+    name = "say-hello",
+    outs = ["say-hello.js"],
+    cmd = "echo 'console.log(\"Hello!\")' > $@",
+)
+
+genrule(
+    name = "print-host",
+    outs = ["test/print-host.js"],
+    cmd = "echo 'console.log(location.host)' > $@",
+)
diff --git a/examples/devserver/app.ts b/examples/devserver/app.ts
new file mode 100644
index 0000000..a200b40
--- /dev/null
+++ b/examples/devserver/app.ts
@@ -0,0 +1,7 @@
+const body = document.body;
+const textElement = document.createElement('span');
+
+textElement.innerText = 'Hello from TypeScript';
+
+// Append element to the body.
+body.appendChild(textElement);
\ No newline at end of file
diff --git a/examples/devserver/index.html b/examples/devserver/index.html
new file mode 100644
index 0000000..991a2f1
--- /dev/null
+++ b/examples/devserver/index.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+    <title>Devserver example</title>
+</head>
+<body>
+    <!-- Scripts loaded through the additional_root_paths. -->
+    <script src="/say-hello.js"></script>
+    <script src="/tslib.js"></script>
+
+    <!-- Bundle that comes from concatjs. Specified with serving_path. -->
+    <script src="/bundle.js"></script>
+
+    <!-- Script that is imported relatively to the package directory. -->
+    <script src="/test/print-host.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/examples/protocol_buffers/BUILD.bazel b/examples/protocol_buffers/BUILD.bazel
index 1cb2172..89a2cbb 100644
--- a/examples/protocol_buffers/BUILD.bazel
+++ b/examples/protocol_buffers/BUILD.bazel
@@ -1,4 +1,6 @@
 load("@build_bazel_rules_nodejs//:defs.bzl", "http_server", "rollup_bundle")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
 load(
     "//:defs.bzl",
     "ts_devserver",
@@ -60,7 +62,7 @@
     bootstrap = ["@build_bazel_rules_typescript//:protobufjs_bootstrap_scripts"],
     entry_module = "build_bazel_rules_typescript/examples/protocol_buffers/app",
     port = 8080,
-    deps = [":app"],
+    deps = [":bundle"],
 )
 
 # Test for production mode
@@ -111,3 +113,26 @@
         "@npm//protractor",
     ],
 )
+
+proto_library(
+    name = "rules_typescript_proto",
+    srcs = [
+        "car.proto",
+        "tire.proto",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+    name = "rules_typescript_go_proto",
+    importpath = "github.com/bazelbuild/rules_typescript/examples/protocol_buffers",
+    proto = ":rules_typescript_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    embed = [":rules_typescript_go_proto"],
+    importpath = "github.com/bazelbuild/rules_typescript/examples/protocol_buffers",
+    visibility = ["//visibility:public"],
+)
diff --git a/examples/protocol_buffers/index.html b/examples/protocol_buffers/index.html
index 617b833..6f8690e 100644
--- a/examples/protocol_buffers/index.html
+++ b/examples/protocol_buffers/index.html
@@ -1,10 +1,10 @@
 <html>
-  <head>
+<head>
     <title>protocol_buffers example</title>
-  </head>
-  <body>
-    <script src="/protobuf.min.js"></script>
-    <script src="/long.js"></script>
-    <script src="/bundle.min.js"></script>
-  </body>
+</head>
+<body>
+<script src="/protobuf.min.js"></script>
+<script src="/long.js"></script>
+<script src="/bundle.min.js"></script>
+</body>
 </html>
\ No newline at end of file
diff --git a/package.bzl b/package.bzl
index 1f54dde..5e1e5b0 100644
--- a/package.bzl
+++ b/package.bzl
@@ -56,8 +56,11 @@
     _maybe(
         http_archive,
         name = "io_bazel_rules_go",
-        url = "https://github.com/bazelbuild/rules_go/releases/download/0.16.3/rules_go-0.16.3.tar.gz",
-        # sha256 = "ee5fe78fe417c685ecb77a0a725dc9f6040ae5beb44a0ba4ddb55453aad23a8a",
+        # We need https://github.com/bazelbuild/rules_go/commit/109c520465fcb418f2c4be967f3744d959ad66d3 which
+        # is not part of any 0.16.x release yet. This commit provides runfile resolve support for Windows.
+        urls = ["https://github.com/bazelbuild/rules_go/archive/12a52e9845a5b06a28ffda06d7f2b07ff2320b97.zip"],
+        strip_prefix = "rules_go-12a52e9845a5b06a28ffda06d7f2b07ff2320b97",
+        sha256 = "5c0a059afe51c744c90ae2b33ac70b9b4f4c514715737e2ec0b5fd297400c10d",
     )
 
     # go_repository is defined in bazel_gazelle