diff --git a/configuration.go b/configuration.go index eb9e4ac..564ccfd 100644 --- a/configuration.go +++ b/configuration.go @@ -2,6 +2,17 @@ package via import "github.com/alexedwards/scs/v2" +// DatastarConfig configures a custom Datastar.js script. +type DatastarConfig struct { + // Content is the Datastar.js script content. + // If nil, the embedded default is used. + Content []byte + + // Path is the URL path where the script is served. + // Defaults to "/_datastar.js" if empty. + Path string +} + type LogLevel int const ( @@ -37,4 +48,8 @@ type Options struct { // with scs LoadAndSave middleware. Configure the session manager before // passing it (lifetime, cookie settings, store, etc). SessionManager *scs.SessionManager + + // Datastar configures a custom Datastar.js script. + // If nil, Via uses its embedded default. + Datastar *DatastarConfig } diff --git a/h/h.go b/h/h.go index 03c2e62..5aa2d9d 100644 --- a/h/h.go +++ b/h/h.go @@ -79,7 +79,6 @@ func HTML5(p HTML5Props) H { Body: retype(p.Body), HTMLAttrs: retype(p.HTMLAttrs), } - gp.Head = append(gp.Head, Script(Type("module"), Src("/_datastar.js"))) return gc.HTML5(gp) } diff --git a/via.go b/via.go index b8d278b..dbadd1f 100644 --- a/via.go +++ b/via.go @@ -32,14 +32,17 @@ var datastarJS []byte // V is the root application. // It manages page routing, user sessions, and SSE connections for live updates. type V struct { - cfg Options - mux *http.ServeMux - contextRegistry map[string]*Context - contextRegistryMutex sync.RWMutex - documentHeadIncludes []h.H - documentFootIncludes []h.H - devModePageInitFnMap map[string]func(*Context) - sessionManager *scs.SessionManager + cfg Options + mux *http.ServeMux + contextRegistry map[string]*Context + contextRegistryMutex sync.RWMutex + documentHeadIncludes []h.H + documentFootIncludes []h.H + devModePageInitFnMap map[string]func(*Context) + sessionManager *scs.SessionManager + datastarPath string + datastarContent []byte + datastarHandlerRegistered bool } func (v *V) logFatal(format string, a ...any) { @@ -108,6 +111,14 @@ func (v *V) Config(cfg Options) { if cfg.SessionManager != nil { v.sessionManager = cfg.SessionManager } + if cfg.Datastar != nil { + if cfg.Datastar.Content != nil { + v.datastarContent = cfg.Datastar.Content + } + if cfg.Datastar.Path != "" { + v.datastarPath = cfg.Datastar.Path + } + } } // AppendToHead appends the given h.H nodes to the head of the base HTML document. @@ -141,6 +152,7 @@ func (v *V) AppendToFoot(elements ...h.H) { // }) // }) func (v *V) Page(route string, initContextFn func(c *Context)) { + v.ensureDatastarHandler() // check for panics func() { defer func() { @@ -176,7 +188,7 @@ func (v *V) Page(route string, initContextFn func(c *Context)) { if v.cfg.DevMode { 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, h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))), @@ -258,6 +270,17 @@ func (v *V) HTTPServeMux() *http.ServeMux { return v.mux } +func (v *V) ensureDatastarHandler() { + if v.datastarHandlerRegistered { + return + } + v.datastarHandlerRegistered = true + 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) { p := filepath.Join(".via", "devmode", "ctx.json") if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { @@ -378,6 +401,8 @@ func New() *V { contextRegistry: make(map[string]*Context), devModePageInitFnMap: make(map[string]func(*Context)), sessionManager: scs.New(), + datastarPath: "/_datastar.js", + datastarContent: datastarJS, cfg: Options{ DevMode: false, ServerAddress: ":3000", @@ -386,11 +411,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) { isReconnect := false if r.Header.Get("last-event-id") == "via" { diff --git a/via_test.go b/via_test.go index 40da652..213bf9b 100644 --- a/via_test.go +++ b/via_test.go @@ -28,6 +28,10 @@ func TestPageRoute(t *testing.T) { func TestDatastarJS(t *testing.T) { v := New() + 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) @@ -37,6 +41,54 @@ func TestDatastarJS(t *testing.T) { assert.Contains(t, w.Body.String(), "🖕JS_DS🚀") } +func TestCustomDatastarContent(t *testing.T) { + customScript := []byte("// Custom Datastar Script") + v := New() + v.Config(Options{ + Datastar: &DatastarConfig{ + Content: 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{ + Datastar: &DatastarConfig{ + Path: "/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) { var sig *signal v := New()