6cd89a78d3
Co-authored-by: Kandev Agent <agent@kandev.dev>
159 lines
3.9 KiB
Go
159 lines
3.9 KiB
Go
package webapp
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/kandev/kandev/internal/i18n"
|
|
)
|
|
|
|
// DevHandler fronts a Vite dev server with the same Go boot-payload shell used
|
|
// in production. Vite remains responsible for module transforms and HMR.
|
|
type DevHandler struct {
|
|
viteIndexURL string
|
|
proxy *httputil.ReverseProxy
|
|
client *http.Client
|
|
payloadFor PayloadBuilder
|
|
}
|
|
|
|
func NewDevHandler(viteBaseURL string, opts ...HandlerOption) (*DevHandler, error) {
|
|
target, err := url.Parse(viteBaseURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse Vite dev server URL: %w", err)
|
|
}
|
|
if target.Scheme == "" || target.Host == "" {
|
|
return nil, fmt.Errorf("parse Vite dev server URL: missing scheme or host")
|
|
}
|
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
proxy.FlushInterval = -1
|
|
proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, _ error) {
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
}
|
|
|
|
config := newHandlerDefaults(nil)
|
|
for _, opt := range opts {
|
|
opt(config)
|
|
}
|
|
|
|
indexURL := *target
|
|
indexURL.Path = "/"
|
|
indexURL.RawQuery = ""
|
|
indexURL.Fragment = ""
|
|
|
|
return &DevHandler{
|
|
viteIndexURL: indexURL.String(),
|
|
proxy: proxy,
|
|
client: http.DefaultClient,
|
|
payloadFor: config.payloadFor,
|
|
}, nil
|
|
}
|
|
|
|
func (h *DevHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if h == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
if shouldProxyToVite(r) {
|
|
h.proxy.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
route := ClassifyRoute(r.URL.Path)
|
|
if route.Kind != RouteKindSPA {
|
|
if route.Kind == RouteKindStatic {
|
|
h.proxy.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
if !acceptsHTML(r) {
|
|
h.proxy.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
indexHTML, err := h.fetchViteIndex(r)
|
|
if err != nil {
|
|
http.Error(w, i18n.T(i18n.FromRequest(r), "webapp.devServerUnavailable"), http.StatusBadGateway)
|
|
return
|
|
}
|
|
payload := h.payloadFor(r, route)
|
|
html, err := RenderShellHTML(indexHTML, payload)
|
|
if err != nil {
|
|
http.Error(w, i18n.T(i18n.FromRequest(r), "webapp.shellUnavailable"), http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(html)
|
|
}
|
|
|
|
func (h *DevHandler) fetchViteIndex(r *http.Request) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, h.viteIndexURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "text/html")
|
|
|
|
resp, err := h.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = resp.Body.Close()
|
|
}()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("vite index status %d", resp.StatusCode)
|
|
}
|
|
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
|
|
func shouldProxyToVite(r *http.Request) bool {
|
|
if isWebSocketUpgrade(r) {
|
|
return true
|
|
}
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
return true
|
|
}
|
|
return isViteDevPath(r.URL.Path)
|
|
}
|
|
|
|
func acceptsHTML(r *http.Request) bool {
|
|
accept := r.Header.Get("Accept")
|
|
return accept == "" || strings.Contains(accept, "text/html")
|
|
}
|
|
|
|
func isWebSocketUpgrade(r *http.Request) bool {
|
|
return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") &&
|
|
strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade")
|
|
}
|
|
|
|
func isViteDevPath(requestPath string) bool {
|
|
switch {
|
|
case strings.HasPrefix(requestPath, "/@vite/"),
|
|
strings.HasPrefix(requestPath, "/@id/"),
|
|
strings.HasPrefix(requestPath, "/@react-refresh"),
|
|
strings.HasPrefix(requestPath, "/@fs/"),
|
|
strings.HasPrefix(requestPath, "/__vite"),
|
|
strings.HasPrefix(requestPath, "/src/"),
|
|
strings.HasPrefix(requestPath, "/app/"),
|
|
strings.HasPrefix(requestPath, "/components/"),
|
|
strings.HasPrefix(requestPath, "/hooks/"),
|
|
strings.HasPrefix(requestPath, "/lib/"),
|
|
strings.HasPrefix(requestPath, "/generated/"),
|
|
strings.HasPrefix(requestPath, "/node_modules/"):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|