forked from tangled.org/core
this repo has no description

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.

authored by oppi.li and committed by Tangled 76165f95 ab0a5bd7

Changed files
+118 -27
appview
pages
state
+107 -25
appview/pages/pages.go
··· 11 11 "io/fs" 12 12 "log" 13 13 "net/http" 14 + "os" 14 15 "path" 15 16 "path/filepath" 16 17 "slices" ··· 35 36 var Files embed.FS 36 37 37 38 type Pages struct { 38 - t map[string]*template.Template 39 + t map[string]*template.Template 40 + dev bool 41 + embedFS embed.FS 42 + templateDir string // Path to templates on disk for dev mode 39 43 } 40 44 41 - func NewPages() *Pages { 42 - templates := make(map[string]*template.Template) 45 + func NewPages(dev bool) *Pages { 46 + p := &Pages{ 47 + t: make(map[string]*template.Template), 48 + dev: dev, 49 + embedFS: Files, 50 + templateDir: "appview/pages", 51 + } 43 52 53 + // Initial load of all templates 54 + p.loadAllTemplates() 55 + 56 + return p 57 + } 58 + 59 + func (p *Pages) loadAllTemplates() { 60 + templates := make(map[string]*template.Template) 44 61 var fragmentPaths []string 62 + 63 + // Use embedded FS for initial loading 45 64 // First, collect all fragment paths 46 - err := fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { 65 + err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 47 66 if err != nil { 48 67 return err 49 68 } 50 - 51 69 if d.IsDir() { 52 70 return nil 53 71 } 54 - 55 72 if !strings.HasSuffix(path, ".html") { 56 73 return nil 57 74 } 58 - 59 75 if !strings.Contains(path, "fragments/") { 60 76 return nil 61 77 } 62 - 63 78 name := strings.TrimPrefix(path, "templates/") 64 79 name = strings.TrimSuffix(name, ".html") 65 - 66 80 tmpl, err := template.New(name). 67 81 Funcs(funcMap()). 68 - ParseFS(Files, path) 82 + ParseFS(p.embedFS, path) 69 83 if err != nil { 70 84 log.Fatalf("setting up fragment: %v", err) 71 85 } 72 - 73 86 templates[name] = tmpl 74 87 fragmentPaths = append(fragmentPaths, path) 75 88 log.Printf("loaded fragment: %s", name) ··· 80 93 } 81 94 82 95 // Then walk through and setup the rest of the templates 83 - err = fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { 96 + err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 84 97 if err != nil { 85 98 return err 86 99 } 87 - 88 100 if d.IsDir() { 89 101 return nil 90 102 } 91 - 92 103 if !strings.HasSuffix(path, "html") { 93 104 return nil 94 105 } 95 - 96 106 // Skip fragments as they've already been loaded 97 107 if strings.Contains(path, "fragments/") { 98 108 return nil 99 109 } 100 - 101 110 // Skip layouts 102 111 if strings.Contains(path, "layouts/") { 103 112 return nil 104 113 } 105 - 106 114 name := strings.TrimPrefix(path, "templates/") 107 115 name = strings.TrimSuffix(name, ".html") 108 - 109 116 // Add the page template on top of the base 110 117 allPaths := []string{} 111 118 allPaths = append(allPaths, "templates/layouts/*.html") ··· 113 120 allPaths = append(allPaths, path) 114 121 tmpl, err := template.New(name). 115 122 Funcs(funcMap()). 116 - ParseFS(Files, allPaths...) 123 + ParseFS(p.embedFS, allPaths...) 117 124 if err != nil { 118 125 return fmt.Errorf("setting up template: %w", err) 119 126 } 120 - 121 127 templates[name] = tmpl 122 128 log.Printf("loaded template: %s", name) 123 129 return nil ··· 127 133 } 128 134 129 135 log.Printf("total templates loaded: %d", len(templates)) 136 + p.t = templates 137 + } 130 138 131 - return &Pages{ 132 - t: templates, 139 + // loadTemplateFromDisk loads a template from the filesystem in dev mode 140 + func (p *Pages) loadTemplateFromDisk(name string) error { 141 + if !p.dev { 142 + return nil 133 143 } 134 - } 135 144 136 - type LoginParams struct { 145 + log.Printf("reloading template from disk: %s", name) 146 + 147 + // Find all fragments first 148 + var fragmentPaths []string 149 + err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error { 150 + if err != nil { 151 + return err 152 + } 153 + if d.IsDir() { 154 + return nil 155 + } 156 + if !strings.HasSuffix(path, ".html") { 157 + return nil 158 + } 159 + if !strings.Contains(path, "fragments/") { 160 + return nil 161 + } 162 + fragmentPaths = append(fragmentPaths, path) 163 + return nil 164 + }) 165 + if err != nil { 166 + return fmt.Errorf("walking disk template dir for fragments: %w", err) 167 + } 168 + 169 + // Find the template path on disk 170 + templatePath := filepath.Join(p.templateDir, "templates", name+".html") 171 + if _, err := os.Stat(templatePath); os.IsNotExist(err) { 172 + return fmt.Errorf("template not found on disk: %s", name) 173 + } 174 + 175 + // Create a new template 176 + tmpl := template.New(name).Funcs(funcMap()) 177 + 178 + // Parse layouts 179 + layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html") 180 + layouts, err := filepath.Glob(layoutGlob) 181 + if err != nil { 182 + return fmt.Errorf("finding layout templates: %w", err) 183 + } 184 + 185 + // Create paths for parsing 186 + allFiles := append(layouts, fragmentPaths...) 187 + allFiles = append(allFiles, templatePath) 188 + 189 + // Parse all templates 190 + tmpl, err = tmpl.ParseFiles(allFiles...) 191 + if err != nil { 192 + return fmt.Errorf("parsing template files: %w", err) 193 + } 194 + 195 + // Update the template in the map 196 + p.t[name] = tmpl 197 + log.Printf("template reloaded from disk: %s", name) 198 + return nil 137 199 } 138 200 139 201 func (p *Pages) execute(name string, w io.Writer, params any) error { 140 - return p.t[name].ExecuteTemplate(w, "layouts/base", params) 202 + // In dev mode, reload the template from disk before executing 203 + if p.dev { 204 + if err := p.loadTemplateFromDisk(name); err != nil { 205 + log.Printf("warning: failed to reload template %s from disk: %v", name, err) 206 + // Continue with the existing template 207 + } 208 + } 209 + 210 + tmpl, exists := p.t[name] 211 + if !exists { 212 + return fmt.Errorf("template not found: %s", name) 213 + } 214 + 215 + return tmpl.ExecuteTemplate(w, "layouts/base", params) 141 216 } 142 217 143 218 func (p *Pages) executePlain(name string, w io.Writer, params any) error { ··· 146 221 147 222 func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 148 223 return p.t[name].ExecuteTemplate(w, "layouts/repobase", params) 224 + } 225 + 226 + type LoginParams struct { 149 227 } 150 228 151 229 func (p *Pages) Login(w io.Writer, params LoginParams) error { ··· 794 872 } 795 873 796 874 func (p *Pages) Static() http.Handler { 875 + if p.dev { 876 + return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 877 + } 878 + 797 879 sub, err := fs.Sub(Files, "static") 798 880 if err != nil { 799 881 log.Fatalf("no static dir found? that's crazy: %v", err)
+1 -1
appview/state/state.go
··· 55 55 56 56 clock := syntax.NewTIDClock(0) 57 57 58 - pgs := pages.NewPages() 58 + pgs := pages.NewPages(config.Dev) 59 59 60 60 resolver := appview.NewResolver() 61 61
+10 -1
flake.nix
··· 173 173 ${pkgs.air}/bin/air -c /dev/null \ 174 174 -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" \ 175 175 -build.bin "./out/${name}.out" \ 176 - -build.include_ext "go,html,css" 176 + -build.include_ext "go" 177 + ''; 178 + tailwind-watcher = 179 + pkgs.writeShellScriptBin "run" 180 + '' 181 + ${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css 177 182 ''; 178 183 in { 179 184 watch-appview = { ··· 183 188 watch-knotserver = { 184 189 type = "app"; 185 190 program = ''${air-watcher "knotserver"}/bin/run''; 191 + }; 192 + watch-tailwind = { 193 + type = "app"; 194 + program = ''${tailwind-watcher}/bin/run''; 186 195 }; 187 196 }); 188 197