diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..8b364d2 --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +echo "=== Building frontend ===" +npm run build + +echo "=== Copying dist to server/ ===" +rm -rf server/dist +cp -r dist server/dist + +echo "=== Building Go binary ===" +cd server +go mod init protoc-frontend-server 2>/dev/null || true +GOOS=windows GOARCH=amd64 go build -o ../protoc-frontend.exe . + +echo "" +echo "=== Done! ===" +echo "Binary: protoc-frontend.exe" +echo "Usage: protoc-frontend.exe [-port 1111] [-api http://localhost:8000]" diff --git a/protoc-frontend.exe b/protoc-frontend.exe new file mode 100755 index 0000000..40f2e22 Binary files /dev/null and b/protoc-frontend.exe differ diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..5da3862 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,3 @@ +module protoc-frontend-server + +go 1.25.5 diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..eb569e2 --- /dev/null +++ b/server/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "embed" + "flag" + "fmt" + "io/fs" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" +) + +//go:embed dist/* +var distFS embed.FS + +func main() { + port := flag.Int("port", 1112, "port to listen on") + apiTarget := flag.String("api", "http://localhost:8000", "backend API URL to proxy /api requests") + flag.Parse() + + // Proxy for /api -> backend + backendURL, err := url.Parse(*apiTarget) + if err != nil { + log.Fatalf("invalid api url: %v", err) + } + + proxy := httputil.NewSingleHostReverseProxy(backendURL) + originalDirector := proxy.Director + proxy.Director = func(r *http.Request) { + originalDirector(r) + // Rewrite /api/... -> /... + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") + if r.URL.Path == "" { + r.URL.Path = "/" + } + r.Host = backendURL.Host + } + + // Static files from embedded dist/ + distSub, err := fs.Sub(distFS, "dist") + if err != nil { + log.Fatalf("failed to open embedded dist: %v", err) + } + fileServer := http.FileServer(http.FS(distSub)) + + mux := http.NewServeMux() + + // API proxy + mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) { + proxy.ServeHTTP(w, r) + }) + + // SPA: serve static files, fallback to index.html + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Try to serve the file directly + path := strings.TrimPrefix(r.URL.Path, "/") + if path == "" { + path = "index.html" + } + + // Check if file exists in embedded FS + if f, err := distSub.Open(path); err == nil { + f.Close() + fileServer.ServeHTTP(w, r) + return + } + + // Fallback to index.html for SPA routing + indexFile, err := fs.ReadFile(distSub, "index.html") + if err != nil { + http.Error(w, "index.html not found", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(indexFile) + }) + + addr := fmt.Sprintf(":%d", *port) + log.Printf("Serving on http://localhost%s", addr) + log.Printf("API proxy -> %s", *apiTarget) + + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatal(err) + os.Exit(1) + } +}