3 Commits

Author SHA1 Message Date
Ryan Hamamura
c77ccc0796 chore: rename module to github.com/ryanhamamura/via
Update module path and all internal imports to use the new repository location.
2026-01-14 10:47:11 -10:00
Ryan Hamamura
d4b831492e refactor: simplify Datastar configuration API
Flatten DatastarConfig struct into Options (DatastarContent, DatastarPath)
and replace datastarHandlerRegistered bool with sync.Once for thread safety.
2026-01-14 02:01:18 -10:00
Ryan Hamamura
ea7b9ad4a1 feat: add custom Datastar.js configuration support
Allow users to provide their own Datastar.js script (e.g., Datastar Pro
or custom builds) via Via's Options configuration. Adds DatastarConfig
struct with Content ([]byte) and Path (string) fields.
2026-01-14 01:47:39 -10:00
21 changed files with 117 additions and 54 deletions

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
// actionTrigger represents a trigger to an event handler fn // actionTrigger represents a trigger to an event handler fn

View File

@@ -37,4 +37,12 @@ type Options struct {
// with scs LoadAndSave middleware. Configure the session manager before // with scs LoadAndSave middleware. Configure the session manager before
// passing it (lifetime, cookie settings, store, etc). // passing it (lifetime, cookie settings, store, etc).
SessionManager *scs.SessionManager SessionManager *scs.SessionManager
// DatastarContent is the Datastar.js script content.
// If nil, the embedded default is used.
DatastarContent []byte
// DatastarPath is the URL path where the script is served.
// Defaults to "/_datastar.js" if empty.
DatastarPath string
} }

View File

@@ -11,7 +11,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
// Context is the living bridge between Go and the browser. // Context is the living bridge between Go and the browser.

6
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/go-via/via module github.com/ryanhamamura/via
go 1.25.4 go 1.25.4
@@ -6,6 +6,7 @@ require maragu.dev/gomponents v1.2.0
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/alexedwards/scs/v2 v2.9.0
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.32
github.com/starfederation/datastar-go v1.0.3 github.com/starfederation/datastar-go v1.0.3
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
@@ -13,13 +14,10 @@ require (
require ( require (
github.com/CAFxX/httpcompression v0.0.9 // indirect github.com/CAFxX/httpcompression v0.0.9 // indirect
github.com/alexedwards/scs/v2 v2.9.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

8
go.sum
View File

@@ -17,11 +17,6 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
@@ -45,9 +40,8 @@ github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

1
h/h.go
View File

@@ -79,7 +79,6 @@ func HTML5(p HTML5Props) H {
Body: retype(p.Body), Body: retype(p.Body),
HTMLAttrs: retype(p.HTMLAttrs), HTMLAttrs: retype(p.HTMLAttrs),
} }
gp.Head = append(gp.Head, Script(Type("module"), Src("/_datastar.js")))
return gc.HTML5(gp) return gc.HTML5(gp)
} }

View File

@@ -3,8 +3,8 @@ package main
import ( import (
"math/rand" "math/rand"
"github.com/go-via/via" "github.com/ryanhamamura/via"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
var ( var (

View File

@@ -1,8 +1,8 @@
package main package main
import ( import (
"github.com/go-via/via" "github.com/ryanhamamura/via"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
type Counter struct{ Count int } type Counter struct{ Count int }

View File

@@ -1,8 +1,8 @@
package main package main
import ( import (
"github.com/go-via/via" "github.com/ryanhamamura/via"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
func main() { func main() {

View File

@@ -1,8 +1,8 @@
package main package main
import ( import (
"github.com/go-via/via" "github.com/ryanhamamura/via"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
func main() { func main() {

View File

@@ -1,9 +1,9 @@
package main package main
import ( import (
"github.com/go-via/via" "github.com/ryanhamamura/via"
// "github.com/go-via/via-plugin-picocss/picocss" // "github.com/go-via/via-plugin-picocss/picocss"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
type Counter struct{ Count int } type Counter struct{ Count int }

View File

@@ -3,9 +3,9 @@ package main
import ( import (
"strconv" "strconv"
"github.com/go-via/via" "github.com/ryanhamamura/via"
// "github.com/go-via/via-plugin-picocss/picocss" // "github.com/go-via/via-plugin-picocss/picocss"
. "github.com/go-via/via/h" . "github.com/ryanhamamura/via/h"
) )
func main() { func main() {

View File

@@ -1,9 +1,9 @@
package main package main
import ( import (
"github.com/go-via/via" "github.com/ryanhamamura/via"
// "github.com/go-via/via-plugin-picocss/picocss" // "github.com/go-via/via-plugin-picocss/picocss"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
type Counter struct{ Count int } type Counter struct{ Count int }

View File

@@ -4,8 +4,8 @@ import (
_ "embed" _ "embed"
"net/http" "net/http"
"github.com/go-via/via" "github.com/ryanhamamura/via"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
// Example of a Via application with a plugin that adds PicoCSS. The plugin // Example of a Via application with a plugin that adds PicoCSS. The plugin

View File

@@ -5,9 +5,9 @@ import (
"math/rand" "math/rand"
"time" "time"
"github.com/go-via/via" "github.com/ryanhamamura/via"
// "github.com/go-via/via-plugin-picocss/picocss" // "github.com/go-via/via-plugin-picocss/picocss"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
func main() { func main() {

View File

@@ -1,8 +1,8 @@
package main package main
import ( import (
"github.com/go-via/via" "github.com/ryanhamamura/via"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
func main() { func main() {

View File

@@ -6,8 +6,8 @@ import (
"log" "log"
"time" "time"
"github.com/go-via/via" "github.com/ryanhamamura/via"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )

View File

@@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
) )
// Signal represents a value that is reactive in the browser. Signals // Signal represents a value that is reactive in the browser. Signals

View File

@@ -4,7 +4,7 @@ import (
// "net/http/httptest" // "net/http/httptest"
"testing" "testing"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

30
via.go
View File

@@ -22,7 +22,7 @@ import (
"sync" "sync"
"github.com/alexedwards/scs/v2" "github.com/alexedwards/scs/v2"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
"github.com/starfederation/datastar-go/datastar" "github.com/starfederation/datastar-go/datastar"
) )
@@ -40,6 +40,9 @@ type V struct {
documentFootIncludes []h.H documentFootIncludes []h.H
devModePageInitFnMap map[string]func(*Context) devModePageInitFnMap map[string]func(*Context)
sessionManager *scs.SessionManager sessionManager *scs.SessionManager
datastarPath string
datastarContent []byte
datastarOnce sync.Once
} }
func (v *V) logFatal(format string, a ...any) { func (v *V) logFatal(format string, a ...any) {
@@ -108,6 +111,12 @@ func (v *V) Config(cfg Options) {
if cfg.SessionManager != nil { if cfg.SessionManager != nil {
v.sessionManager = cfg.SessionManager v.sessionManager = cfg.SessionManager
} }
if cfg.DatastarContent != nil {
v.datastarContent = cfg.DatastarContent
}
if cfg.DatastarPath != "" {
v.datastarPath = cfg.DatastarPath
}
} }
// AppendToHead appends the given h.H nodes to the head of the base HTML document. // AppendToHead appends the given h.H nodes to the head of the base HTML document.
@@ -141,6 +150,7 @@ func (v *V) AppendToFoot(elements ...h.H) {
// }) // })
// }) // })
func (v *V) Page(route string, initContextFn func(c *Context)) { func (v *V) Page(route string, initContextFn func(c *Context)) {
v.ensureDatastarHandler()
// check for panics // check for panics
func() { func() {
defer func() { defer func() {
@@ -176,7 +186,7 @@ func (v *V) Page(route string, initContextFn func(c *Context)) {
if v.cfg.DevMode { if v.cfg.DevMode {
v.devModePersist(c) v.devModePersist(c)
} }
headElements := []h.H{} headElements := []h.H{h.Script(h.Type("module"), h.Src(v.datastarPath))}
headElements = append(headElements, v.documentHeadIncludes...) headElements = append(headElements, v.documentHeadIncludes...)
headElements = append(headElements, headElements = append(headElements,
h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))), h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))),
@@ -258,6 +268,15 @@ func (v *V) HTTPServeMux() *http.ServeMux {
return v.mux return v.mux
} }
func (v *V) ensureDatastarHandler() {
v.datastarOnce.Do(func() {
v.mux.HandleFunc("GET "+v.datastarPath, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
_, _ = w.Write(v.datastarContent)
})
})
}
func (v *V) devModePersist(c *Context) { func (v *V) devModePersist(c *Context) {
p := filepath.Join(".via", "devmode", "ctx.json") p := filepath.Join(".via", "devmode", "ctx.json")
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
@@ -378,6 +397,8 @@ func New() *V {
contextRegistry: make(map[string]*Context), contextRegistry: make(map[string]*Context),
devModePageInitFnMap: make(map[string]func(*Context)), devModePageInitFnMap: make(map[string]func(*Context)),
sessionManager: scs.New(), sessionManager: scs.New(),
datastarPath: "/_datastar.js",
datastarContent: datastarJS,
cfg: Options{ cfg: Options{
DevMode: false, DevMode: false,
ServerAddress: ":3000", ServerAddress: ":3000",
@@ -386,11 +407,6 @@ func New() *V {
}, },
} }
v.mux.HandleFunc("GET /_datastar.js", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
_, _ = w.Write(datastarJS)
})
v.mux.HandleFunc("GET /_sse", func(w http.ResponseWriter, r *http.Request) { v.mux.HandleFunc("GET /_sse", func(w http.ResponseWriter, r *http.Request) {
isReconnect := false isReconnect := false
if r.Header.Get("last-event-id") == "via" { if r.Header.Get("last-event-id") == "via" {

View File

@@ -5,7 +5,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/go-via/via/h" "github.com/ryanhamamura/via/h"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -28,6 +28,10 @@ func TestPageRoute(t *testing.T) {
func TestDatastarJS(t *testing.T) { func TestDatastarJS(t *testing.T) {
v := New() v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H { return h.Div() })
})
req := httptest.NewRequest("GET", "/_datastar.js", nil) req := httptest.NewRequest("GET", "/_datastar.js", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
v.mux.ServeHTTP(w, req) v.mux.ServeHTTP(w, req)
@@ -37,6 +41,50 @@ func TestDatastarJS(t *testing.T) {
assert.Contains(t, w.Body.String(), "🖕JS_DS🚀") assert.Contains(t, w.Body.String(), "🖕JS_DS🚀")
} }
func TestCustomDatastarContent(t *testing.T) {
customScript := []byte("// Custom Datastar Script")
v := New()
v.Config(Options{
DatastarContent: customScript,
})
v.Page("/", func(c *Context) {
c.View(func() h.H { return h.Div() })
})
req := httptest.NewRequest("GET", "/_datastar.js", nil)
w := httptest.NewRecorder()
v.mux.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
assert.Contains(t, w.Body.String(), "Custom Datastar Script")
}
func TestCustomDatastarPath(t *testing.T) {
v := New()
v.Config(Options{
DatastarPath: "/assets/datastar.js",
})
v.Page("/test", func(c *Context) {
c.View(func() h.H { return h.Div() })
})
// Custom path should serve the script
req := httptest.NewRequest("GET", "/assets/datastar.js", nil)
w := httptest.NewRecorder()
v.mux.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
assert.Contains(t, w.Body.String(), "🖕JS_DS🚀")
// Page should reference the custom path in script tag
req2 := httptest.NewRequest("GET", "/test", nil)
w2 := httptest.NewRecorder()
v.mux.ServeHTTP(w2, req2)
assert.Contains(t, w2.Body.String(), `src="/assets/datastar.js"`)
}
func TestSignal(t *testing.T) { func TestSignal(t *testing.T) {
var sig *signal var sig *signal
v := New() v := New()