mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 14:05:39 +00:00
client/web: add barebones vite dev setup
Currently just serving a "Hello world" page when running the web cli in --dev mode. Updates tailscale/corp#13775 Co-authored-by: Will Norris <will@tailscale.com> Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
parent
215480a022
commit
16bc9350e3
4
.gitignore
vendored
4
.gitignore
vendored
@ -35,5 +35,9 @@ cmd/tailscaled/tailscaled
|
|||||||
# Ignore direnv nix-shell environment cache
|
# Ignore direnv nix-shell environment cache
|
||||||
.direnv/
|
.direnv/
|
||||||
|
|
||||||
|
# Ignore web client node modules
|
||||||
|
.vite/
|
||||||
|
client/web/node_modules
|
||||||
|
|
||||||
/gocross
|
/gocross
|
||||||
/dist
|
/dist
|
||||||
|
75
client/web/dev.go
Normal file
75
client/web/dev.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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()) {
|
||||||
|
root := gitRootDir()
|
||||||
|
webClientPath := filepath.Join(root, "client", "web")
|
||||||
|
|
||||||
|
yarn := filepath.Join(root, "tool", "yarn")
|
||||||
|
node := filepath.Join(root, "tool", "node")
|
||||||
|
vite := filepath.Join(webClientPath, "node_modules", ".bin", "vite")
|
||||||
|
|
||||||
|
log.Printf("installing JavaScript deps using %s... (might take ~30s)", yarn)
|
||||||
|
out, err := exec.Command(yarn, "--non-interactive", "-s", "--cwd", webClientPath, "install").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error running tailscale web's yarn install: %v, %s", err, out)
|
||||||
|
}
|
||||||
|
log.Printf("starting JavaScript dev server...")
|
||||||
|
cmd := exec.Command(node, vite)
|
||||||
|
cmd.Dir = webClientPath
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Fatalf("Starting JS dev server: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("JavaScript dev server running as pid %d", cmd.Process.Pid)
|
||||||
|
return func() {
|
||||||
|
cmd.Process.Signal(os.Interrupt)
|
||||||
|
err := cmd.Wait()
|
||||||
|
log.Printf("JavaScript dev server exited: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) addProxyToDevServer() {
|
||||||
|
if !s.devMode {
|
||||||
|
return // only using Vite proxy in dev mode
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
// Here we set up the proxy to Vite's server.
|
||||||
|
handleErr := func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
w.Write([]byte("The web client development server isn't running. " +
|
||||||
|
"Run `./tool/yarn --cwd client/web start` from " +
|
||||||
|
"the repo root to start the development server."))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitRootDir() string {
|
||||||
|
top, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to find git top level (not in corp git?): %v", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(top))
|
||||||
|
}
|
3
client/web/index.html
Normal file
3
client/web/index.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<!doctype html>
|
||||||
|
Hello world
|
||||||
|
</html>
|
31
client/web/package.json
Normal file
31
client/web/package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "webui",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": "18.16.1",
|
||||||
|
"yarn": "1.22.19"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "^3.1.6",
|
||||||
|
"typescript": "^4.7.4",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"vite-tsconfig-paths": "^3.5.0",
|
||||||
|
"vite-plugin-svgr": "^3.2.0",
|
||||||
|
"vite-plugin-rewrite-all": "^1.0.1",
|
||||||
|
"vitest": "^0.32.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "vite",
|
||||||
|
"lint": "tsc --noEmit",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"semi": false,
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
||||||
|
}
|
69
client/web/vite.config.ts
Normal file
69
client/web/vite.config.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/// <reference types="vitest" />
|
||||||
|
import { createLogger, defineConfig } from "vite"
|
||||||
|
import rewrite from "vite-plugin-rewrite-all"
|
||||||
|
import svgr from "vite-plugin-svgr"
|
||||||
|
import paths from "vite-tsconfig-paths"
|
||||||
|
|
||||||
|
// Use a custom logger that filters out Vite's logging of server URLs, since
|
||||||
|
// they are an attractive nuisance (we run a proxy in front of Vite, and the
|
||||||
|
// tailscale web client should be accessed through that).
|
||||||
|
// Unfortunately there's no option to disable this logging, so the best we can
|
||||||
|
// do it to ignore calls from a specific function.
|
||||||
|
const filteringLogger = createLogger(undefined, { allowClearScreen: false })
|
||||||
|
const originalInfoLog = filteringLogger.info
|
||||||
|
filteringLogger.info = (...args) => {
|
||||||
|
if (new Error("ignored").stack?.includes("printServerUrls")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
originalInfoLog.apply(filteringLogger, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
base: "/",
|
||||||
|
plugins: [
|
||||||
|
paths(),
|
||||||
|
svgr(),
|
||||||
|
// By default, the Vite dev server doesn't handle dots
|
||||||
|
// in path names and treats them as static files.
|
||||||
|
// This plugin changes Vite's routing logic to fix this.
|
||||||
|
// See: https://github.com/vitejs/vite/issues/2415
|
||||||
|
rewrite(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
outDir: "build",
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
esbuild: {
|
||||||
|
logOverride: {
|
||||||
|
// Silence a warning about `this` being undefined in ESM when at the
|
||||||
|
// top-level. The way JSX is transpiled causes this to happen, but it
|
||||||
|
// isn't a problem.
|
||||||
|
// See: https://github.com/vitejs/vite/issues/8644
|
||||||
|
"this-is-undefined-in-esm": "silent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
// This needs to be 127.0.0.1 instead of localhost, because of how our
|
||||||
|
// Go proxy connects to it.
|
||||||
|
host: "127.0.0.1",
|
||||||
|
// If you change the port, be sure to update the proxy in adminhttp.go too.
|
||||||
|
port: 4000,
|
||||||
|
// Don't proxy the WebSocket connection used for live reloading by running
|
||||||
|
// it on a separate port.
|
||||||
|
hmr: {
|
||||||
|
protocol: "ws",
|
||||||
|
port: 4001,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
exclude: ["**/node_modules/**", "**/dist/**"],
|
||||||
|
testTimeout: 20000,
|
||||||
|
environment: "jsdom",
|
||||||
|
deps: {
|
||||||
|
inline: ["date-fns", /\.wasm\?url$/],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clearScreen: false,
|
||||||
|
customLogger: filteringLogger,
|
||||||
|
})
|
@ -16,6 +16,7 @@
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -46,22 +47,30 @@
|
|||||||
|
|
||||||
// Server is the backend server for a Tailscale web client.
|
// Server is the backend server for a Tailscale web client.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
devMode bool
|
|
||||||
lc *tailscale.LocalClient
|
lc *tailscale.LocalClient
|
||||||
|
|
||||||
|
devMode bool
|
||||||
|
devProxy *httputil.ReverseProxy // only filled when devMode is on
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer constructs a new Tailscale web client server.
|
// NewServer constructs a new Tailscale web client server.
|
||||||
//
|
//
|
||||||
// lc is an optional parameter. When not filled, NewServer
|
// lc is an optional parameter. When not filled, NewServer
|
||||||
// initializes its own tailscale.LocalClient.
|
// initializes its own tailscale.LocalClient.
|
||||||
func NewServer(devMode bool, lc *tailscale.LocalClient) *Server {
|
func NewServer(devMode bool, lc *tailscale.LocalClient) (s *Server, cleanup func()) {
|
||||||
if lc == nil {
|
if lc == nil {
|
||||||
lc = &tailscale.LocalClient{}
|
lc = &tailscale.LocalClient{}
|
||||||
}
|
}
|
||||||
return &Server{
|
s = &Server{
|
||||||
devMode: devMode,
|
devMode: devMode,
|
||||||
lc: lc,
|
lc: lc,
|
||||||
}
|
}
|
||||||
|
cleanup = func() {}
|
||||||
|
if s.devMode {
|
||||||
|
cleanup = s.startDevServer()
|
||||||
|
s.addProxyToDevServer()
|
||||||
|
}
|
||||||
|
return s, cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -284,6 +293,12 @@ func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
|
|
||||||
// ServeHTTP processes all requests for the Tailscale web client.
|
// ServeHTTP processes all requests for the Tailscale web client.
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.devMode {
|
||||||
|
// When in dev mode, proxy to the Vite dev server.
|
||||||
|
s.devProxy.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
if authRedirect(w, r) {
|
if authRedirect(w, r) {
|
||||||
return
|
return
|
||||||
|
1709
client/web/yarn.lock
Normal file
1709
client/web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@
|
|||||||
webf := newFlagSet("web")
|
webf := newFlagSet("web")
|
||||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
||||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||||
webf.BoolVar(&webArgs.dev, "dev", false, "run web client in developer mode")
|
webf.BoolVar(&webArgs.dev, "dev", false, "run web client in developer mode [this flag is in development, use is unsupported]")
|
||||||
return webf
|
return webf
|
||||||
})(),
|
})(),
|
||||||
Exec: runWeb,
|
Exec: runWeb,
|
||||||
@ -78,7 +78,8 @@ func runWeb(ctx context.Context, args []string) error {
|
|||||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
webServer := web.NewServer(webArgs.dev, nil)
|
webServer, cleanup := web.NewServer(webArgs.dev, nil)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
if webArgs.cgi {
|
if webArgs.cgi {
|
||||||
if err := cgi.Serve(webServer); err != nil {
|
if err := cgi.Serve(webServer); err != nil {
|
||||||
|
@ -267,7 +267,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
net/http from expvar+
|
net/http from expvar+
|
||||||
net/http/cgi from tailscale.com/cmd/tailscale/cli
|
net/http/cgi from tailscale.com/cmd/tailscale/cli
|
||||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||||
net/http/httputil from tailscale.com/cmd/tailscale/cli
|
net/http/httputil from tailscale.com/cmd/tailscale/cli+
|
||||||
net/http/internal from net/http+
|
net/http/internal from net/http+
|
||||||
net/netip from net+
|
net/netip from net+
|
||||||
net/textproto from golang.org/x/net/http/httpguts+
|
net/textproto from golang.org/x/net/http/httpguts+
|
||||||
|
@ -35,7 +35,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serve the Tailscale web client.
|
// Serve the Tailscale web client.
|
||||||
ws := web.NewServer(*devMode, lc)
|
ws, cleanup := web.NewServer(*devMode, lc)
|
||||||
|
defer cleanup()
|
||||||
if err := http.Serve(ln, ws); err != nil {
|
if err := http.Serve(ln, ws); err != nil {
|
||||||
if err != http.ErrServerClosed {
|
if err != http.ErrServerClosed {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
Loading…
Reference in New Issue
Block a user