From 76165f9510d27b89f0ac4c6adeb6ac7776faec03 Mon Sep 17 00:00:00 2001 From: Akshay Date: Thu, 24 Apr 2025 14:58:56 +0100 Subject: [PATCH] appview: pages: faster reload on dev in dev mode; all html templates are loaded from disk on access. UI updates take <100ms now: - go files are watched by air/gust - html/css files are watched by `tailwind -w` html/css changes do not cause appview reload; but do trigger reload of static assets and templates; giving us instant live reloads for UI development. --- appview/pages/pages.go | 132 +++++++++++++++++++++++++++++++++-------- appview/state/state.go | 2 +- flake.nix | 11 +++- 3 files changed, 118 insertions(+), 27 deletions(-) diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 14075bd4..97684933 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -11,6 +11,7 @@ import ( "io/fs" "log" "net/http" + "os" "path" "path/filepath" "slices" @@ -35,41 +36,53 @@ import ( var Files embed.FS type Pages struct { - t map[string]*template.Template + t map[string]*template.Template + dev bool + embedFS embed.FS + templateDir string // Path to templates on disk for dev mode +} + +func NewPages(dev bool) *Pages { + p := &Pages{ + t: make(map[string]*template.Template), + dev: dev, + embedFS: Files, + templateDir: "appview/pages", + } + + // Initial load of all templates + p.loadAllTemplates() + + return p } -func NewPages() *Pages { +func (p *Pages) loadAllTemplates() { templates := make(map[string]*template.Template) - var fragmentPaths []string + + // Use embedded FS for initial loading // First, collect all fragment paths - err := fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { + err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } - if d.IsDir() { return nil } - if !strings.HasSuffix(path, ".html") { return nil } - if !strings.Contains(path, "fragments/") { return nil } - name := strings.TrimPrefix(path, "templates/") name = strings.TrimSuffix(name, ".html") - tmpl, err := template.New(name). Funcs(funcMap()). - ParseFS(Files, path) + ParseFS(p.embedFS, path) if err != nil { log.Fatalf("setting up fragment: %v", err) } - templates[name] = tmpl fragmentPaths = append(fragmentPaths, path) log.Printf("loaded fragment: %s", name) @@ -80,32 +93,26 @@ func NewPages() *Pages { } // Then walk through and setup the rest of the templates - err = fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { + err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } - if d.IsDir() { return nil } - if !strings.HasSuffix(path, "html") { return nil } - // Skip fragments as they've already been loaded if strings.Contains(path, "fragments/") { return nil } - // Skip layouts if strings.Contains(path, "layouts/") { return nil } - name := strings.TrimPrefix(path, "templates/") name = strings.TrimSuffix(name, ".html") - // Add the page template on top of the base allPaths := []string{} allPaths = append(allPaths, "templates/layouts/*.html") @@ -113,11 +120,10 @@ func NewPages() *Pages { allPaths = append(allPaths, path) tmpl, err := template.New(name). Funcs(funcMap()). - ParseFS(Files, allPaths...) + ParseFS(p.embedFS, allPaths...) if err != nil { return fmt.Errorf("setting up template: %w", err) } - templates[name] = tmpl log.Printf("loaded template: %s", name) return nil @@ -127,17 +133,86 @@ func NewPages() *Pages { } log.Printf("total templates loaded: %d", len(templates)) + p.t = templates +} - return &Pages{ - t: templates, +// loadTemplateFromDisk loads a template from the filesystem in dev mode +func (p *Pages) loadTemplateFromDisk(name string) error { + if !p.dev { + return nil } -} -type LoginParams struct { + log.Printf("reloading template from disk: %s", name) + + // Find all fragments first + var fragmentPaths []string + err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".html") { + return nil + } + if !strings.Contains(path, "fragments/") { + return nil + } + fragmentPaths = append(fragmentPaths, path) + return nil + }) + if err != nil { + return fmt.Errorf("walking disk template dir for fragments: %w", err) + } + + // Find the template path on disk + templatePath := filepath.Join(p.templateDir, "templates", name+".html") + if _, err := os.Stat(templatePath); os.IsNotExist(err) { + return fmt.Errorf("template not found on disk: %s", name) + } + + // Create a new template + tmpl := template.New(name).Funcs(funcMap()) + + // Parse layouts + layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html") + layouts, err := filepath.Glob(layoutGlob) + if err != nil { + return fmt.Errorf("finding layout templates: %w", err) + } + + // Create paths for parsing + allFiles := append(layouts, fragmentPaths...) + allFiles = append(allFiles, templatePath) + + // Parse all templates + tmpl, err = tmpl.ParseFiles(allFiles...) + if err != nil { + return fmt.Errorf("parsing template files: %w", err) + } + + // Update the template in the map + p.t[name] = tmpl + log.Printf("template reloaded from disk: %s", name) + return nil } func (p *Pages) execute(name string, w io.Writer, params any) error { - return p.t[name].ExecuteTemplate(w, "layouts/base", params) + // In dev mode, reload the template from disk before executing + if p.dev { + if err := p.loadTemplateFromDisk(name); err != nil { + log.Printf("warning: failed to reload template %s from disk: %v", name, err) + // Continue with the existing template + } + } + + tmpl, exists := p.t[name] + if !exists { + return fmt.Errorf("template not found: %s", name) + } + + return tmpl.ExecuteTemplate(w, "layouts/base", params) } func (p *Pages) executePlain(name string, w io.Writer, params any) error { @@ -148,6 +223,9 @@ func (p *Pages) executeRepo(name string, w io.Writer, params any) error { return p.t[name].ExecuteTemplate(w, "layouts/repobase", params) } +type LoginParams struct { +} + func (p *Pages) Login(w io.Writer, params LoginParams) error { return p.executePlain("user/login", w, params) } @@ -794,6 +872,10 @@ func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) } func (p *Pages) Static() http.Handler { + if p.dev { + return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) + } + sub, err := fs.Sub(Files, "static") if err != nil { log.Fatalf("no static dir found? that's crazy: %v", err) diff --git a/appview/state/state.go b/appview/state/state.go index 29563fa3..449cafef 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -55,7 +55,7 @@ func Make(config *appview.Config) (*State, error) { clock := syntax.NewTIDClock(0) - pgs := pages.NewPages() + pgs := pages.NewPages(config.Dev) resolver := appview.NewResolver() diff --git a/flake.nix b/flake.nix index cc7c4890..1fba9ef1 100644 --- a/flake.nix +++ b/flake.nix @@ -173,7 +173,12 @@ ${pkgs.air}/bin/air -c /dev/null \ -build.cmd "${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o ./appview/pages/static/tw.css && ${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ -build.bin "./out/${name}.out" \ - -build.include_ext "go,html,css" + -build.include_ext "go" + ''; + tailwind-watcher = + pkgs.writeShellScriptBin "run" + '' + ${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css ''; in { watch-appview = { @@ -184,6 +189,10 @@ type = "app"; program = ''${air-watcher "knotserver"}/bin/run''; }; + watch-tailwind = { + type = "app"; + program = ''${tailwind-watcher}/bin/run''; + }; }); nixosModules.appview = { -- 2.51.2