diff --git a/client/web/dev.go b/client/web/assets.go similarity index 63% rename from client/web/dev.go rename to client/web/assets.go index 4d8765305..9e1c7f8a9 100644 --- a/client/web/dev.go +++ b/client/web/assets.go @@ -4,6 +4,8 @@ package web import ( + "embed" + "io/fs" "log" "net/http" "net/http/httputil" @@ -12,11 +14,42 @@ "os/exec" "path/filepath" "strings" + + "tailscale.com/util/must" ) +// This contains all files needed to build the frontend assets. +// Because we assign this to the blank identifier, it does not actually embed the files. +// However, this does cause `go mod vendor` to include the files when vendoring the package. +// External packages that use the web client can `go mod vendor`, run `yarn build` to +// build the assets, then those asset bundles will be embedded. +// +//go:embed yarn.lock index.html *.js *.json src/* +var _ embed.FS + +//go:embed build/* +var embeddedFS embed.FS + +// staticfiles serves static files from the build directory. +var staticfiles http.Handler + +func init() { + buildFiles := must.Get(fs.Sub(embeddedFS, "build")) + staticfiles = http.FileServer(http.FS(buildFiles)) +} + +func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) { + if devMode { + // When in dev mode, proxy asset requests to the Vite dev server. + cleanup := startDevServer() + return devServerProxy(), cleanup + } + return staticfiles, nil +} + // startDevServer starts the JS dev server that does on-demand rebuilding // and serving of web client JS and CSS resources. -func (s *Server) startDevServer() (cleanup func()) { +func startDevServer() (cleanup func()) { root := gitRootDir() webClientPath := filepath.Join(root, "client", "web") @@ -45,10 +78,8 @@ func (s *Server) startDevServer() (cleanup func()) { } } -func (s *Server) addProxyToDevServer() { - if !s.devMode { - return // only using Vite proxy in dev mode - } +// devServerProxy returns a reverse proxy to the vite dev server. +func devServerProxy() *httputil.ReverseProxy { // We use Vite to develop on the web client. // Vite starts up its own local server for development, // which we proxy requests to from Server.ServeHTTP. @@ -62,8 +93,9 @@ func (s *Server) addProxyToDevServer() { w.Write([]byte("\n\nError: " + err.Error())) } viteTarget, _ := url.Parse("http://127.0.0.1:4000") - s.devProxy = httputil.NewSingleHostReverseProxy(viteTarget) - s.devProxy.ErrorHandler = handleErr + devProxy := httputil.NewSingleHostReverseProxy(viteTarget) + devProxy.ErrorHandler = handleErr + return devProxy } func gitRootDir() string { diff --git a/client/web/build/index.html b/client/web/build/index.html index 80838dc16..c0d39ba94 100644 --- a/client/web/build/index.html +++ b/client/web/build/index.html @@ -6,7 +6,7 @@ - + diff --git a/client/web/web.go b/client/web/web.go index b7514d1d5..d65ce09c3 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -7,14 +7,11 @@ import ( "context" "crypto/rand" - "embed" "encoding/json" "fmt" "io" - "io/fs" "log" "net/http" - "net/http/httputil" "net/netip" "os" "path/filepath" @@ -31,35 +28,20 @@ "tailscale.com/net/netutil" "tailscale.com/tailcfg" "tailscale.com/util/httpm" - "tailscale.com/util/must" "tailscale.com/version/distro" ) -// This contains all files needed to build the frontend assets. -// Because we assign this to the blank identifier, it does not actually embed the files. -// However, this does cause `go mod vendor` to include the files when vendoring the package. -// External packages that use the web client can `go mod vendor`, run `yarn build` to -// build the assets, then those asset bundles will be embedded. -// -//go:embed yarn.lock index.html *.js *.json src/* -var _ embed.FS - -//go:embed build/* -var embeddedFS embed.FS - -// staticfiles serves static files from the build directory. -var staticfiles http.Handler - // Server is the backend server for a Tailscale web client. type Server struct { lc *tailscale.LocalClient - devMode bool - devProxy *httputil.ReverseProxy // only filled when devMode is on + devMode bool cgiMode bool pathPrefix string - apiHandler http.Handler // csrf-protected api handler + + assetsHandler http.Handler // serves frontend assets + apiHandler http.Handler // serves api endpoints; csrf-protected } // ServerOpts contains options for constructing a new Server. @@ -89,11 +71,7 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func()) cgiMode: opts.CGIMode, pathPrefix: opts.PathPrefix, } - cleanup = func() {} - if s.devMode { - cleanup = s.startDevServer() - s.addProxyToDevServer() - } + s.assetsHandler, cleanup = assetsHandler(opts.DevMode) // Create handler for "/api" requests with CSRF protection. // We don't require secure cookies, since the web client is regularly used @@ -107,11 +85,6 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func()) return s, cleanup } -func init() { - buildFiles := must.Get(fs.Sub(embeddedFS, "build")) - staticfiles = http.FileServer(http.FS(buildFiles)) -} - // ServeHTTP processes all requests for the Tailscale web client. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { handler := s.serve @@ -151,14 +124,11 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) { // Pass API requests through to the API handler. s.apiHandler.ServeHTTP(w, r) return - case s.devMode: - // When in dev mode, proxy non-api requests to the Vite dev server. - s.devProxy.ServeHTTP(w, r) - return default: - // Otherwise, serve static files from the embedded filesystem. - s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1) - staticfiles.ServeHTTP(w, r) + if !s.devMode { + s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1) + } + s.assetsHandler.ServeHTTP(w, r) return } }