feat(templates): hot reloading templates

this is supposed to be mostly temporaly until a better templates
interface is made
This commit is contained in:
Guz
2025-03-17 15:43:51 -03:00
parent 82f2c7e67a
commit 8273ff6a1d
5 changed files with 81 additions and 30 deletions

View File

@@ -14,6 +14,7 @@ import (
"syscall"
comicverse "forge.capytal.company/capytalcode/project-comicverse"
"forge.capytal.company/capytalcode/project-comicverse/templates"
"forge.capytal.company/loreddev/x/tinyssert"
"github.com/aws/aws-sdk-go-v2/aws"
@@ -112,6 +113,9 @@ func main() {
d := os.DirFS("./assets")
opts = append(opts, comicverse.WithAssets(d))
t := templates.NewHotTemplates(os.DirFS("./templates"))
opts = append(opts, comicverse.WithTemplates(t))
opts = append(opts, comicverse.WithDevelopmentMode())
}

View File

@@ -26,6 +26,7 @@ func New(cfg Config, opts ...Option) (http.Handler, error) {
bucket: cfg.Bucket,
assets: assets.Files(),
templates: templates.Templates(),
developmentMode: false,
ctx: context.Background(),
@@ -50,6 +51,9 @@ func New(cfg Config, opts ...Option) (http.Handler, error) {
if app.assets == nil {
return nil, errors.New("static files must not be a nil interface")
}
if app.templates == nil {
return nil, errors.New("templates must not be a nil interface")
}
if app.ctx == nil {
return nil, errors.New("context must not be a nil interface")
@@ -82,6 +86,10 @@ func WithAssets(f fs.FS) Option {
return func(app *app) { app.assets = joinedfs.Join(f, app.assets) }
}
func WithTemplates(t templates.ITemplate) Option {
return func(app *app) { app.templates = t }
}
func WithAssertions(a tinyssert.Assertions) Option {
return func(app *app) { app.assert = a }
}
@@ -102,6 +110,7 @@ type app struct {
ctx context.Context
assets fs.FS
templates templates.ITemplate
developmentMode bool
handler http.Handler
@@ -147,9 +156,9 @@ func (app *app) setup() error {
app.handler, err = router.New(router.Config{
Service: service,
Templates: templates.Templates(),
Templates: app.templates,
DisableCache: app.developmentMode,
Assets: app.assets,
Assets: app.assets,
Assertions: app.assert,
Logger: app.logger.WithGroup("router"),

View File

@@ -13,7 +13,7 @@ dev/server:
--build.cmd "go build -o tmp/bin/main ./cmd" \
--build.bin "tmp/bin/main" \
--build.exclude_dir "node_modules" \
--build.include_ext "go,html" \
--build.include_ext "go" \
--build.stop_on_error "false" \
--misc.clean_on_exit true \
-- -dev -port $(PORT) -hostname 0.0.0.0

View File

@@ -2,12 +2,12 @@ package router
import (
"errors"
"html/template"
"io/fs"
"log/slog"
"net/http"
"forge.capytal.company/capytalcode/project-comicverse/service"
"forge.capytal.company/capytalcode/project-comicverse/templates"
"forge.capytal.company/loreddev/x/smalltrip"
"forge.capytal.company/loreddev/x/smalltrip/exception"
"forge.capytal.company/loreddev/x/smalltrip/middleware"
@@ -17,7 +17,7 @@ import (
type router struct {
service *service.Service
templates *template.Template
templates templates.ITemplate
assets fs.FS
cache bool
@@ -59,7 +59,7 @@ func New(cfg Config) (http.Handler, error) {
type Config struct {
Service *service.Service
Templates *template.Template
Templates templates.ITemplate
Assets fs.FS
DisableCache bool

View File

@@ -1,39 +1,77 @@
package templates
// INFO: This will probably become a new lib in loreddev/x at some point
import (
"embed"
"errors"
"fmt"
"html/template"
"io"
"io/fs"
)
var (
patterns = []string{"*.html", "layouts/*.html"}
functions = template.FuncMap{
"args": func(pairs ...any) (map[string]any, error) {
if len(pairs)%2 != 0 {
return nil, errors.New("misaligned map in template arguments")
}
m := make(map[string]any, len(pairs)/2)
for i := 0; i < len(pairs); i += 2 {
key, ok := pairs[i].(string)
if !ok {
return nil, fmt.Errorf("cannot use type %T as map key", pairs[i])
}
m[key] = pairs[i+1]
}
return m, nil
},
}
)
//go:embed *.html layouts/*.html
var embedded embed.FS
var temps = template.Must(template.New("templates").Funcs(template.FuncMap{
"args": func(pairs ...any) (map[string]any, error) {
if len(pairs)%2 != 0 {
return nil, errors.New("misaligned map in template arguments")
}
m := make(map[string]any, len(pairs)/2)
for i := 0; i < len(pairs); i += 2 {
key, ok := pairs[i].(string)
if !ok {
return nil, fmt.Errorf("cannot use type %T as map key", pairs[i])
}
m[key] = pairs[i+1]
}
return m, nil
},
}).ParseFS(embedded,
"*.html",
"layouts/*.html",
))
var temps = template.Must(template.New("templates").Funcs(functions).ParseFS(embedded, patterns...))
func Templates() *template.Template {
return temps
return temps // TODO: Support for local templates/hot-reloading without rebuild
}
func NewHotTemplates(fsys fs.FS) *HotTemplate {
return &HotTemplate{
fs: fsys,
}
}
type HotTemplate struct {
fs fs.FS
template *template.Template
}
func (t *HotTemplate) Execute(wr io.Writer, data any) error {
te, err := template.New("hot-templates").Funcs(functions).ParseFS(t.fs, patterns...)
if err != nil {
return err
}
return te.Execute(wr, data)
}
func (t *HotTemplate) ExecuteTemplate(wr io.Writer, name string, data any) error {
te, err := template.New("hot-templates").Funcs(functions).ParseFS(t.fs, patterns...)
if err != nil {
return err
}
return te.ExecuteTemplate(wr, name, data)
}
type ITemplate interface {
Execute(wr io.Writer, data any) error
ExecuteTemplate(wr io.Writer, name string, data any) error
}