cmd/tsconnect: allow building static resources in a different directory
When using tsconnect as a module in another repo, we cannot write to the ./dist directory (modules directories are read-only by default - there is a -modcacherw flag for `go get` but we can't count on it). We add a -distdir flag that is honored by both the build and serve commands for where to place output in. Somewhat tedious because esbuild outputs paths relative to the working directory, so we need to do some extra munging to make them relative to the output directory. Signed-off-by: Mihai Parparita <mihai@tailscale.com>
This commit is contained in:
committed by
Mihai Parparita
parent
de2dcda2e0
commit
b763a12331
+35
-21
@@ -11,30 +11,48 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
//go:embed dist/* index.html
|
||||
//go:embed index.html
|
||||
var embeddedFS embed.FS
|
||||
|
||||
//go:embed dist/*
|
||||
var embeddedDistFS embed.FS
|
||||
|
||||
var serveStartTime = time.Now()
|
||||
|
||||
func runServe() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
indexBytes, err := generateServeIndex()
|
||||
var distFS fs.FS
|
||||
if *distDir == "./dist" {
|
||||
var err error
|
||||
distFS, err = fs.Sub(embeddedDistFS, "dist")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not drop dist/ prefix from embedded FS: %v", err)
|
||||
}
|
||||
} else {
|
||||
distFS = os.DirFS(*distDir)
|
||||
}
|
||||
|
||||
indexBytes, err := generateServeIndex(distFS)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not generate index.html: %v", err)
|
||||
}
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, "index.html", serveStartTime, bytes.NewReader(indexBytes))
|
||||
}))
|
||||
mux.Handle("/dist/", http.HandlerFunc(handleServeDist))
|
||||
mux.Handle("/dist/", http.StripPrefix("/dist/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handleServeDist(w, r, distFS)
|
||||
})))
|
||||
tsweb.Debugger(mux)
|
||||
|
||||
log.Printf("Listening on %s", *addr)
|
||||
@@ -44,14 +62,19 @@ func runServe() {
|
||||
}
|
||||
}
|
||||
|
||||
func generateServeIndex() ([]byte, error) {
|
||||
func generateServeIndex(distFS fs.FS) ([]byte, error) {
|
||||
log.Printf("Generating index.html...\n")
|
||||
rawIndexBytes, err := embeddedFS.ReadFile("index.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read index.html: %w", err)
|
||||
}
|
||||
|
||||
esbuildMetadataBytes, err := embeddedFS.ReadFile("dist/esbuild-metadata.json")
|
||||
esbuildMetadataFile, err := distFS.Open("esbuild-metadata.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
|
||||
}
|
||||
defer esbuildMetadataFile.Close()
|
||||
esbuildMetadataBytes, err := ioutil.ReadAll(esbuildMetadataFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
|
||||
}
|
||||
@@ -62,7 +85,7 @@ func generateServeIndex() ([]byte, error) {
|
||||
entryPointsToHashedDistPaths := make(map[string]string)
|
||||
for outputPath, output := range esbuildMetadata.Outputs {
|
||||
if output.EntryPoint != "" {
|
||||
entryPointsToHashedDistPaths[output.EntryPoint] = outputPath
|
||||
entryPointsToHashedDistPaths[output.EntryPoint] = path.Join("dist", outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,39 +100,30 @@ func generateServeIndex() ([]byte, error) {
|
||||
return indexBytes, nil
|
||||
}
|
||||
|
||||
// EsbuildMetadata is the subset of metadata struct (described by
|
||||
// https://esbuild.github.io/api/#metafile) that we care about for mapping
|
||||
// from entry points to hashed file names.
|
||||
type EsbuildMetadata = struct {
|
||||
Outputs map[string]struct {
|
||||
EntryPoint string `json:"entryPoint,omitempty"`
|
||||
} `json:"outputs,omitempty"`
|
||||
}
|
||||
|
||||
var entryPointsToDefaultDistPaths = map[string]string{
|
||||
"src/index.css": "dist/index.css",
|
||||
"src/index.js": "dist/index.js",
|
||||
}
|
||||
|
||||
func handleServeDist(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Path[1:]
|
||||
func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
|
||||
path := r.URL.Path
|
||||
var f fs.File
|
||||
// Prefer pre-compressed versions generated during the build step.
|
||||
if tsweb.AcceptsEncoding(r, "br") {
|
||||
if brotliFile, err := embeddedFS.Open(p + ".br"); err == nil {
|
||||
if brotliFile, err := distFS.Open(path + ".br"); err == nil {
|
||||
f = brotliFile
|
||||
w.Header().Set("Content-Encoding", "br")
|
||||
}
|
||||
}
|
||||
if f == nil && tsweb.AcceptsEncoding(r, "gzip") {
|
||||
if gzipFile, err := embeddedFS.Open(p + ".gz"); err == nil {
|
||||
if gzipFile, err := distFS.Open(path + ".gz"); err == nil {
|
||||
f = gzipFile
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
if rawFile, err := embeddedFS.Open(r.URL.Path[1:]); err == nil {
|
||||
if rawFile, err := distFS.Open(path); err == nil {
|
||||
f = rawFile
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
@@ -130,5 +144,5 @@ func handleServeDist(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31535996")
|
||||
w.Header().Set("Vary", "Accept-Encoding")
|
||||
|
||||
http.ServeContent(w, r, path.Base(r.URL.Path), serveStartTime, fSeeker)
|
||||
http.ServeContent(w, r, path, serveStartTime, fSeeker)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user