mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-05 23:07:44 +00:00
client/web: precompress assets
Precompress webclient assets with precompress util. This cuts our css and js build sizes to about 1/3 of non-compressed size. Similar compression done on tsconnect and adminhttp assets. Updates #10261 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
parent
97f8577ad2
commit
e5e5ebda44
@ -4,6 +4,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -13,10 +14,13 @@
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
prebuilt "github.com/tailscale/web-client-prebuilt"
|
||||
)
|
||||
|
||||
var start = time.Now()
|
||||
|
||||
func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) {
|
||||
if devMode {
|
||||
// When in dev mode, proxy asset requests to the Vite dev server.
|
||||
@ -25,19 +29,46 @@ func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) {
|
||||
}
|
||||
|
||||
fsys := prebuilt.FS()
|
||||
fileserver := http.FileServer(http.FS(fsys))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := fs.Stat(fsys, strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if os.IsNotExist(err) {
|
||||
// rewrite request to just fetch /index.html and let
|
||||
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||
f, err := openPrecompressedFile(w, r, path, fsys)
|
||||
if err != nil {
|
||||
// Rewrite request to just fetch index.html and let
|
||||
// the frontend router handle it.
|
||||
r = r.Clone(r.Context())
|
||||
r.URL.Path = "/"
|
||||
path = "index.html"
|
||||
f, err = openPrecompressedFile(w, r, path, fsys)
|
||||
}
|
||||
fileserver.ServeHTTP(w, r)
|
||||
if f == nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// fs.File does not claim to implement Seeker, but in practice it does.
|
||||
fSeeker, ok := f.(io.ReadSeeker)
|
||||
if !ok {
|
||||
http.Error(w, "Not seekable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Aggressively cache static assets, since we cache-bust our assets with
|
||||
// hashed filenames.
|
||||
w.Header().Set("Cache-Control", "public, max-age=31535996")
|
||||
w.Header().Set("Vary", "Accept-Encoding")
|
||||
|
||||
http.ServeContent(w, r, path, start, fSeeker)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func openPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) {
|
||||
if f, err := fs.Open(path + ".gz"); err == nil {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
return f, nil
|
||||
}
|
||||
return fs.Open(path) // fallback
|
||||
}
|
||||
|
||||
// startDevServer starts the JS dev server that does on-demand rebuilding
|
||||
// and serving of web client JS and CSS resources.
|
||||
func startDevServer() (cleanup func()) {
|
||||
|
@ -6,10 +6,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
|
||||
|
||||
<script type="module" crossorigin src="./assets/index-4d1f45ea.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index-8612dca6.css">
|
||||
<link rel="preload" as="font" href="./assets/Inter.var.latin-39e72c07.woff2" type="font/woff2" crossorigin />
|
||||
<script type="module" crossorigin src="./assets/index-fd4af382.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index-218918fa.css">
|
||||
</head>
|
||||
<body>
|
||||
<body class="px-2">
|
||||
<noscript>
|
||||
<p class="mb-2">You need to enable Javascript to access the Tailscale web client.</p>
|
||||
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a>.</p>
|
||||
|
95
cmd/build-webclient/build-webclient.go
Normal file
95
cmd/build-webclient/build-webclient.go
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The build-webclient tool generates the static resources needed for the
|
||||
// web client (code at client/web).
|
||||
//
|
||||
// # Running
|
||||
//
|
||||
// Meant to be invoked from the tailscale/web-client-prebuilt repo when
|
||||
// updating the production built web client assets. To run it manually,
|
||||
// you can use `./tool/go run ./misc/build-webclient`
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/util/precompress"
|
||||
)
|
||||
|
||||
var (
|
||||
outDir = flag.String("outDir", "build/", "path to output directory")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// The toolDir flag is relative to the current working directory,
|
||||
// so we need to resolve it to an absolute path.
|
||||
toolDir, err := filepath.Abs("./tool")
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot resolve tool-dir: %v", err)
|
||||
}
|
||||
|
||||
if err := build(toolDir, "client/web"); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func build(toolDir, appDir string) error {
|
||||
if err := os.Chdir(appDir); err != nil {
|
||||
return fmt.Errorf("Cannot change cwd: %w", err)
|
||||
}
|
||||
|
||||
if err := yarn(toolDir); err != nil {
|
||||
return fmt.Errorf("install failed: %w", err)
|
||||
}
|
||||
|
||||
if err := yarn(toolDir, "lint"); err != nil {
|
||||
return fmt.Errorf("lint failed: %w", err)
|
||||
}
|
||||
|
||||
if err := yarn(toolDir, "build", "--outDir="+*outDir, "--emptyOutDir"); err != nil {
|
||||
return fmt.Errorf("build failed: %w", err)
|
||||
}
|
||||
|
||||
var compressedFiles []string
|
||||
if err := precompress.PrecompressDir(*outDir, precompress.Options{
|
||||
ProgressFn: func(path string) {
|
||||
log.Printf("Pre-compressing %v\n", path)
|
||||
compressedFiles = append(compressedFiles, path)
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Cannot precompress: %w", err)
|
||||
}
|
||||
|
||||
// Cleanup pre-compressed files.
|
||||
for _, f := range compressedFiles {
|
||||
if err := os.Remove(f); err != nil {
|
||||
log.Printf("Failed to cleanup %q: %v", f, err)
|
||||
}
|
||||
// Removing intermediate ".br" version, we use ".gz" asset.
|
||||
if err := os.Remove(f + ".br"); err != nil {
|
||||
log.Printf("Failed to cleanup %q: %v", f+".gz", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func yarn(toolDir string, args ...string) error {
|
||||
args = append([]string{"--silent", "--non-interactive"}, args...)
|
||||
return run(filepath.Join(toolDir, "yarn"), args...)
|
||||
}
|
||||
|
||||
func run(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user