feat: introduce support for plugins

This commit is contained in:
Joao Goncalves
2025-11-07 02:45:58 -01:00
parent 798f024743
commit a46c06b467
7 changed files with 87 additions and 21 deletions

View File

@@ -11,8 +11,10 @@ const (
LogLevelDebug LogLevelDebug
) )
type Plugin func(v *V)
// Config defines configuration options for the via application // Config defines configuration options for the via application
type Configuration struct { type Options struct {
// Level of the logs to write to stdout. // Level of the logs to write to stdout.
// Options: Error, Warn, Info, Debug. // Options: Error, Warn, Info, Debug.
LogLvl LogLevel LogLvl LogLevel
@@ -27,4 +29,8 @@ type Configuration struct {
// Elements to include in the bottom of the body of the base // Elements to include in the bottom of the body of the base
// HTML document.Useful for including JS scripts or a footer. // HTML document.Useful for including JS scripts or a footer.
DocumentBodyIncludes []h.H DocumentBodyIncludes []h.H
// Plugins to extend the capabilities of the `Via` application.
// Check `https://github.com/go-via/plugins` for a list of available plugins.
Plugins []Plugin
} }

View File

@@ -17,7 +17,7 @@ import (
// It binds user state and actions, manages reactive signals, and defines UI through View. // It binds user state and actions, manages reactive signals, and defines UI through View.
type Context struct { type Context struct {
id string id string
app *via app *V
view func() h.H view func() h.H
componentRegistry map[string]*Context componentRegistry map[string]*Context
parentPageCtx *Context parentPageCtx *Context
@@ -302,7 +302,7 @@ func (c *Context) ExecScript(s string) {
_ = sse.ExecuteScript(s) _ = sse.ExecuteScript(s)
} }
func newContext(id string, a *via) *Context { func newContext(id string, a *V) *Context {
if a == nil { if a == nil {
log.Fatalf("create context failed: app pointer is nil") log.Fatalf("create context failed: app pointer is nil")
} }

View File

@@ -8,7 +8,7 @@ import (
func main() { func main() {
v := via.New() v := via.New()
v.Config(via.Configuration{ v.Config(via.Options{
DocumentTitle: "Via", DocumentTitle: "Via",
DocumentHeadIncludes: []h.H{ DocumentHeadIncludes: []h.H{
h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")), h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")),

View File

@@ -0,0 +1,49 @@
package main
import (
_ "embed"
"net/http"
"github.com/go-via/via"
"github.com/go-via/via/h"
)
// In this example we create a Via application with a plugin that adds PicoCSS. The plugin
// is handed to Via in the app Configuration.
//go:embed pico.yellow.min.css
var picoCSSFile []byte
func main() {
v := via.New()
v.Config(via.Options{
DocumentTitle: "Via With PicoCSS Plugin",
Plugins: []via.Plugin{PicoCSSPlugin},
})
v.Page("/", func(c *via.Context) {
c.View(func() h.H {
return h.Section(h.Class("container"),
h.H1(h.Text("Hello from ⚡Via")),
h.Div(h.Class("grid"),
h.Button(h.Text("Primary")),
h.Button(h.Class("secondary"), h.Text("Secondary")),
),
)
})
})
v.Start(":3000")
}
func PicoCSSPlugin(v *via.V) {
v.HandleFunc("GET /_plugins/picocss/assets/style.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
_, _ = w.Write(picoCSSFile)
})
v.Config(via.Options{
DocumentHeadIncludes: []h.H{
h.Link(h.Rel("stylesheet"), h.Href("/_plugins/picocss/assets/style.css")),
},
})
}

File diff suppressed because one or more lines are too long

View File

@@ -13,7 +13,7 @@ import (
func main() { func main() {
v := via.New() v := via.New()
v.Config(via.Configuration{ v.Config(via.Options{
DocumentTitle: "Via", DocumentTitle: "Via",
DocumentHeadIncludes: []h.H{ DocumentHeadIncludes: []h.H{
h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")), h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")),

39
via.go
View File

@@ -22,17 +22,17 @@ import (
//go:embed datastar.js //go:embed datastar.js
var datastarJS []byte var datastarJS []byte
// via is the root application. // V is the root application.
// It manages page routing, user sessions, and SSE connections for live updates. // It manages page routing, user sessions, and SSE connections for live updates.
type via struct { type V struct {
cfg Configuration cfg Options
mux *http.ServeMux mux *http.ServeMux
contextRegistry map[string]*Context contextRegistry map[string]*Context
contextRegistryMutex sync.RWMutex contextRegistryMutex sync.RWMutex
baseLayout func(h.HTML5Props) h.H baseLayout func(h.HTML5Props) h.H
} }
func (v *via) logErr(c *Context, format string, a ...any) { func (v *V) logErr(c *Context, format string, a ...any) {
cRef := "" cRef := ""
if c != nil && c.id != "" { if c != nil && c.id != "" {
cRef = fmt.Sprintf("via-ctx=%q ", c.id) cRef = fmt.Sprintf("via-ctx=%q ", c.id)
@@ -40,7 +40,7 @@ func (v *via) logErr(c *Context, format string, a ...any) {
log.Printf("[error] %smsg=%q", cRef, fmt.Sprintf(format, a...)) log.Printf("[error] %smsg=%q", cRef, fmt.Sprintf(format, a...))
} }
func (v *via) logWarn(c *Context, format string, a ...any) { func (v *V) logWarn(c *Context, format string, a ...any) {
cRef := "" cRef := ""
if c != nil && c.id != "" { if c != nil && c.id != "" {
cRef = fmt.Sprintf("via-ctx=%q ", c.id) cRef = fmt.Sprintf("via-ctx=%q ", c.id)
@@ -50,7 +50,7 @@ func (v *via) logWarn(c *Context, format string, a ...any) {
} }
} }
func (v *via) logInfo(c *Context, format string, a ...any) { func (v *V) logInfo(c *Context, format string, a ...any) {
cRef := "" cRef := ""
if c != nil && c.id != "" { if c != nil && c.id != "" {
cRef = fmt.Sprintf("via-ctx=%q ", c.id) cRef = fmt.Sprintf("via-ctx=%q ", c.id)
@@ -60,7 +60,7 @@ func (v *via) logInfo(c *Context, format string, a ...any) {
} }
} }
func (v *via) logDebug(c *Context, format string, a ...any) { func (v *V) logDebug(c *Context, format string, a ...any) {
cRef := "" cRef := ""
if c != nil && c.id != "" { if c != nil && c.id != "" {
cRef = fmt.Sprintf("via-ctx=%q ", c.id) cRef = fmt.Sprintf("via-ctx=%q ", c.id)
@@ -71,7 +71,7 @@ func (v *via) logDebug(c *Context, format string, a ...any) {
} }
// Config overrides the default configuration with the given configuration options. // Config overrides the default configuration with the given configuration options.
func (v *via) Config(cfg Configuration) { func (v *V) Config(cfg Options) {
if cfg.LogLvl != v.cfg.LogLvl { if cfg.LogLvl != v.cfg.LogLvl {
v.cfg.LogLvl = cfg.LogLvl v.cfg.LogLvl = cfg.LogLvl
} }
@@ -81,6 +81,13 @@ func (v *via) Config(cfg Configuration) {
if cfg.DocumentBodyIncludes != nil { if cfg.DocumentBodyIncludes != nil {
v.cfg.DocumentBodyIncludes = cfg.DocumentBodyIncludes v.cfg.DocumentBodyIncludes = cfg.DocumentBodyIncludes
} }
if cfg.Plugins != nil {
for _, plugin := range cfg.Plugins {
if plugin != nil {
plugin(v)
}
}
}
} }
// Page registers a route and its associated page handler. // Page registers a route and its associated page handler.
@@ -93,7 +100,7 @@ func (v *via) Config(cfg Configuration) {
// return h.H1(h.Text("Hello, Via!")) // return h.H1(h.Text("Hello, Via!"))
// }) // })
// }) // })
func (v *via) Page(route string, composeContext func(c *Context)) { func (v *V) Page(route string, composeContext func(c *Context)) {
v.mux.HandleFunc("GET "+route, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { v.mux.HandleFunc("GET "+route, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "favicon") { if strings.Contains(r.URL.Path, "favicon") {
return return
@@ -119,7 +126,7 @@ func (v *via) Page(route string, composeContext func(c *Context)) {
})) }))
} }
func (v *via) registerCtx(id string, c *Context) { func (v *V) registerCtx(id string, c *Context) {
v.contextRegistryMutex.Lock() v.contextRegistryMutex.Lock()
defer v.contextRegistryMutex.Unlock() defer v.contextRegistryMutex.Unlock()
v.contextRegistry[id] = c v.contextRegistry[id] = c
@@ -134,7 +141,7 @@ func (v *via) registerCtx(id string, c *Context) {
// } // }
// } // }
func (v *via) getCtx(id string) (*Context, error) { func (v *V) getCtx(id string) (*Context, error) {
if c, ok := v.contextRegistry[id]; ok { if c, ok := v.contextRegistry[id]; ok {
return c, nil return c, nil
} }
@@ -143,23 +150,23 @@ func (v *via) getCtx(id string) (*Context, error) {
// HandleFunc registers the HTTP handler function for a given pattern. The handler function panics if // HandleFunc registers the HTTP handler function for a given pattern. The handler function panics if
// in conflict with another registered handler with the same pattern. // in conflict with another registered handler with the same pattern.
func (v *via) HandleFunc(pattern string, f http.HandlerFunc) { func (v *V) HandleFunc(pattern string, f http.HandlerFunc) {
v.mux.HandleFunc(pattern, f) v.mux.HandleFunc(pattern, f)
} }
// Start starts the Via HTTP server on the given address. // Start starts the Via HTTP server on the given address.
func (v *via) Start(addr string) { func (v *V) Start(addr string) {
v.logInfo(nil, "via started") v.logInfo(nil, "via started")
log.Fatalf("via failed: %v", http.ListenAndServe(addr, v.mux)) log.Fatalf("via failed: %v", http.ListenAndServe(addr, v.mux))
} }
// New creates a new Via application with default configuration. // New creates a new Via application with default configuration.
func New() *via { func New() *V {
mux := http.NewServeMux() mux := http.NewServeMux()
app := &via{ app := &V{
mux: mux, mux: mux,
contextRegistry: make(map[string]*Context), contextRegistry: make(map[string]*Context),
cfg: Configuration{ cfg: Options{
LogLvl: LogLevelDebug, LogLvl: LogLevelDebug,
DocumentTitle: "Via Application", DocumentTitle: "Via Application",
DocumentHeadIncludes: make([]h.H, 0), DocumentHeadIncludes: make([]h.H, 0),