Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

Akshay and committed by
Tangled
76165f95 ab0a5bd7

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