Skip to content
This repository has been archived by the owner on Sep 16, 2021. It is now read-only.

Fix devserver on windows #327

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f253579
Use native.sh_binary for cross-platform ts_devserver
filipesilva Sep 24, 2018
dfea264
Use runfiles resolution in ts_devserver
filipesilva Sep 24, 2018
d47b47d
Resolve all devserver files using runfiles manifest
devversion Nov 24, 2018
a81355e
Support index.html files in subdirectories on windows
devversion Dec 21, 2018
7c51ca3
Properly handle directories for symlinked runfiles in devserver
devversion Nov 26, 2018
dc1eee9
Update gazelle and properly resolve Go runfile library
devversion Nov 26, 2018
d5b6ee3
Add workaround for specifying serving_path on windows.
devversion Nov 27, 2018
6436d0d
Do not resolve entry_module as runfile
devversion Dec 26, 2018
4119d69
Support serving runfiles through absolute manifest path
devversion Dec 28, 2018
2f6f2f1
fixup! Resolve all devserver files using runfiles manifest
devversion Jan 10, 2019
04ad329
fixup! Resolve all devserver files using runfiles manifest
devversion Jan 13, 2019
7ad2cd0
Fixes for golint and refactoring for g3
mgechev Jan 14, 2019
4defde8
Refactor Runfile invocation and fix golint errors
mgechev Jan 14, 2019
31d196f
Refactor Runfile invocation and fix golint errors
mgechev Jan 14, 2019
ff3fd3c
fixup! Refactor Runfile invocation and fix golint errors
devversion Jan 15, 2019
93b834f
fixup! Refactor Runfile invocation and fix golint errors
devversion Jan 15, 2019
cc1a219
Update method name
mgechev Jan 15, 2019
2e761b5
Do not depend on external rules_go package
mgechev Jan 16, 2019
1bf1739
Add logic to resolve runfiles within G3 without using external runfil…
devversion Jan 17, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,8 @@ local_repository(
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",
)
9 changes: 8 additions & 1 deletion devserver/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")

package(default_visibility = ["//visibility:public"])

# Gazelle by default tries to map the import for the Bazel runfile go library to a repository with
# an auto-generated name. This does not work for us because the rules_go repository name is different.
# This gazelle directive ensures that Gazelle resolves the import to the proper Bazel label.
# Read more here: https://github.com/bazelbuild/bazel-gazelle#directives
# gazelle:resolve go github.com/bazelbuild/rules_go/go/tools/bazel @io_bazel_rules_go//go/tools/bazel:go_default_library

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",
"@io_bazel_rules_go//go/tools/bazel:go_default_library",
],
)

Expand Down
60 changes: 41 additions & 19 deletions devserver/concatjs/concatjs.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,25 +96,30 @@ func acceptGzip(h http.Header) bool {
// 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{}
type RealFileSystem struct {}

func (fs *realFileSystem) statMtime(filename string) (time.Time, error) {
func (fs *RealFileSystem) StatMtime(filename string) (time.Time, error) {
devversion marked this conversation as resolved.
Show resolved Hide resolved
s, err := os.Stat(filename)
if err != nil {
return time.Time{}, err
}
return s.ModTime(), nil
}

func (fs *realFileSystem) readFile(filename string) ([]byte, error) {
func (fs *RealFileSystem) ReadFile(filename string) ([]byte, error) {
devversion marked this conversation as resolved.
Show resolved Hide resolved
return ioutil.ReadFile(filename)
}

func (fs *RealFileSystem) ResolvePath(root string, file string) (string, error) {
devversion marked this conversation as resolved.
Show resolved Hide resolved
return filepath.Join(root, file), nil;
devversion marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 {
Expand All @@ -129,7 +134,7 @@ type FileCache struct {
// 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,
Expand All @@ -140,10 +145,11 @@ func NewFileCache(root string, fs FileSystem) *FileCache {

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.
Expand Down Expand Up @@ -213,16 +219,16 @@ func (cache *FileCache) WriteFiles(w io.Writer, files []string) error {

// 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
}
if e.mtime == mt && e.contents != nil {
return nil // up to date
}

contents, err := fileContents(root, path, fs)
contents, err := fileContents(e.resolvedPath, fs)
if err != nil {
return err
}
Expand All @@ -231,6 +237,10 @@ func (e *cacheEntry) refresh(root, path string, fs FileSystem) error {
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.
Expand All @@ -248,15 +258,29 @@ func (cache *FileCache) refreshFiles(files []string) {
// 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()
}()
}

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}
Expand All @@ -275,10 +299,8 @@ const googModuleSearchLimit = 50 * 1000
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
}
Expand Down
70 changes: 61 additions & 9 deletions devserver/concatjs/concatjs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -41,21 +42,26 @@ func TestWriteJSEscaped(t *testing.T) {
}

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)
Expand All @@ -77,20 +83,23 @@ func TestWriteFiles(t *testing.T) {
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 {
Expand All @@ -109,6 +118,9 @@ func TestFileCaching(t *testing.T) {
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
Expand Down Expand Up @@ -141,6 +153,46 @@ func TestAcceptHeader(t *testing.T) {
}
}

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 {
Expand Down
9 changes: 9 additions & 0 deletions devserver/devserver/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["devserver.go"],
deps = [
"@io_bazel_rules_go//go/tools/bazel:go_default_library",
devversion marked this conversation as resolved.
Show resolved Hide resolved
devversion marked this conversation as resolved.
Show resolved Hide resolved
],
importpath = "github.com/bazelbuild/rules_typescript/devserver/devserver",
visibility = ["//visibility:public"],
)
Expand All @@ -11,4 +14,10 @@ go_test(
name = "go_default_test",
srcs = ["devserver_test.go"],
embed = [":go_default_library"],
# Required runfiles for the devserver tests.
data = [
"test/index.html",
"test/relative.html",
"@devserver_test_workspace//:sources",
]
)
Loading