--
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