embedfs.go

  1package ui
  2
  3import (
  4	"embed"
  5	"encoding/json"
  6	"fmt"
  7	"io/fs"
  8	"net/http"
  9	"os"
 10	"path/filepath"
 11	"time"
 12)
 13
 14// Dist contains the contents of the built UI under dist/.
 15//
 16//go:embed dist/*
 17var Dist embed.FS
 18
 19var assets http.FileSystem
 20
 21func init() {
 22	sub, err := fs.Sub(Dist, "dist")
 23	if err != nil {
 24		// If the build is misconfigured and dist/ is missing, fail fast.
 25		panic(err)
 26	}
 27	assets = http.FS(sub)
 28
 29	// Check if UI sources are stale compared to the embedded build
 30	checkStaleness()
 31}
 32
 33// checkStaleness verifies that the embedded UI build is not stale.
 34// If ui/src exists and has files modified after the build, we exit with an error.
 35func checkStaleness() {
 36	// Read build-info.json from embedded filesystem
 37	buildInfoData, err := fs.ReadFile(Dist, "dist/build-info.json")
 38	if err != nil {
 39		// If build-info.json doesn't exist, the build is old or incomplete.
 40		fmt.Fprintf(os.Stderr, "\nError: UI build is stale!\n")
 41		fmt.Fprintf(os.Stderr, "\nPlease run 'make serve' instead of 'go run ./cmd/shelley serve'\n")
 42		fmt.Fprintf(os.Stderr, "Or rebuild the UI first: cd ui && pnpm run build\n\n")
 43		os.Exit(1)
 44		return
 45	}
 46
 47	var buildInfo struct {
 48		Timestamp int64  `json:"timestamp"`
 49		Date      string `json:"date"`
 50		SrcDir    string `json:"srcDir"`
 51	}
 52	if err := json.Unmarshal(buildInfoData, &buildInfo); err != nil {
 53		fmt.Fprintf(os.Stderr, "Warning: failed to parse build-info.json: %v\n", err)
 54		return
 55	}
 56
 57	buildTime := time.UnixMilli(buildInfo.Timestamp)
 58
 59	// Check if source directory exists (we might be in a deployed binary without source)
 60	srcDir := buildInfo.SrcDir
 61	if srcDir == "" {
 62		// Build info doesn't have srcDir, can't check staleness
 63		return
 64	}
 65	if _, err := os.Stat(srcDir); os.IsNotExist(err) {
 66		// Source directory doesn't exist, assume we're in production/deployed
 67		return
 68	}
 69
 70	// Walk through ui/src and check if any files are newer than the build
 71	var newerFiles []string
 72	err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
 73		if err != nil {
 74			return err
 75		}
 76		if !info.IsDir() && info.ModTime().After(buildTime) {
 77			newerFiles = append(newerFiles, path)
 78		}
 79		return nil
 80	})
 81	if err != nil {
 82		fmt.Fprintf(os.Stderr, "Warning: failed to check source file timestamps: %v\n", err)
 83		return
 84	}
 85
 86	if len(newerFiles) > 0 {
 87		fmt.Fprintf(os.Stderr, "\nError: UI build is stale!\n")
 88		fmt.Fprintf(os.Stderr, "Build timestamp: %s\n", buildInfo.Date)
 89		fmt.Fprintf(os.Stderr, "\nThe following source files are newer than the build:\n")
 90		for _, f := range newerFiles {
 91			fmt.Fprintf(os.Stderr, "  - %s\n", f)
 92		}
 93		fmt.Fprintf(os.Stderr, "\nPlease run 'make serve' instead of 'go run ./cmd/shelley serve'\n")
 94		fmt.Fprintf(os.Stderr, "Or rebuild the UI first: cd ui && pnpm run build\n\n")
 95		os.Exit(1)
 96	}
 97}
 98
 99// Assets returns an http.FileSystem backed by the embedded UI assets.
100func Assets() http.FileSystem {
101	return assets
102}
103
104// Checksums returns the content checksums for static assets.
105// These are computed during build and used for ETag generation.
106func Checksums() map[string]string {
107	data, err := fs.ReadFile(Dist, "dist/checksums.json")
108	if err != nil {
109		return nil
110	}
111	var checksums map[string]string
112	if err := json.Unmarshal(data, &checksums); err != nil {
113		return nil
114	}
115	return checksums
116}