refactor(web): move embed package to web/ so go:embed reaches build/
go:embed paths are relative to the source file and cannot reach parent directories, so internal/web/embed.go could not see web/build/ without duplicating the placeholder. Relocating to web/embed.go lets the directive resolve to the real SvelteKit build output with no copy step.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// Package web embeds the SvelteKit SPA build output and serves it over HTTP.
|
||||
// Requests for real files under build/ are served verbatim; anything else —
|
||||
// deep links like /artists/abc, missing asset paths — returns build/index.html
|
||||
// so the SPA can resolve the route client-side.
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed all:build
|
||||
var buildFS embed.FS
|
||||
|
||||
// Handler returns an http.Handler that serves the embedded SPA.
|
||||
//
|
||||
// Behavior:
|
||||
// - GET / -> build/index.html
|
||||
// - GET /<path that exists in build/> -> that file, via http.FileServer
|
||||
// - GET /<anything else> -> build/index.html (SPA fallback)
|
||||
//
|
||||
// Callers are expected to register this as the router's NotFound/catch-all so
|
||||
// explicitly-routed paths (like /api/* and /rest/*) take precedence.
|
||||
func Handler() http.Handler {
|
||||
sub, err := fs.Sub(buildFS, "build")
|
||||
if err != nil {
|
||||
// fs.Sub on a valid embed path can only fail if the embed directive
|
||||
// is malformed, which the compile would have caught.
|
||||
panic("web: fs.Sub(build) failed: " + err.Error())
|
||||
}
|
||||
|
||||
index, err := fs.ReadFile(sub, "index.html")
|
||||
if err != nil {
|
||||
panic("web: embedded build/index.html missing: " + err.Error())
|
||||
}
|
||||
|
||||
fileServer := http.FileServer(http.FS(sub))
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clean := path.Clean(r.URL.Path)
|
||||
if clean == "/" || clean == "." {
|
||||
serveIndex(w, index)
|
||||
return
|
||||
}
|
||||
name := strings.TrimPrefix(clean, "/")
|
||||
if info, err := fs.Stat(sub, name); err == nil && !info.IsDir() {
|
||||
fileServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
serveIndex(w, index)
|
||||
})
|
||||
}
|
||||
|
||||
func serveIndex(w http.ResponseWriter, index []byte) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
_, _ = w.Write(index)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandler_ServesIndexAtRoot(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
Handler().ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||
t.Errorf("Content-Type = %q, want text/html*", ct)
|
||||
}
|
||||
body, _ := io.ReadAll(rec.Body)
|
||||
if !strings.Contains(strings.ToLower(string(body)), "<html") {
|
||||
t.Errorf("body missing <html tag; got: %q", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_UnknownPathFallsBackToIndex(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/artists/some-uuid", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
Handler().ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200 (SPA fallback)", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||
t.Errorf("Content-Type = %q, want text/html* for SPA fallback", ct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_MissingAssetFallsBackToIndex(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/_app/immutable/chunks/does-not-exist.js", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
Handler().ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200 (fallback to index.html)", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||
t.Errorf("Content-Type = %q, want text/html* for missing asset", ct)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user