2024-09-11 00:52:18 +00:00
|
|
|
package h
|
|
|
|
|
|
|
|
|
|
import (
|
2024-09-26 19:15:57 +00:00
|
|
|
"context"
|
2024-09-26 16:40:31 +00:00
|
|
|
"fmt"
|
|
|
|
|
"log/slog"
|
2024-09-26 19:15:57 +00:00
|
|
|
"net/http"
|
2024-09-28 17:51:23 +00:00
|
|
|
"os"
|
2024-09-26 16:40:31 +00:00
|
|
|
"os/exec"
|
|
|
|
|
"runtime"
|
2024-09-28 17:51:23 +00:00
|
|
|
"strings"
|
2024-09-26 16:40:31 +00:00
|
|
|
"time"
|
2024-10-22 13:32:17 +00:00
|
|
|
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
"github.com/maddalax/htmgo/framework/hx"
|
|
|
|
|
"github.com/maddalax/htmgo/framework/service"
|
2024-09-11 00:52:18 +00:00
|
|
|
)
|
|
|
|
|
|
2024-09-18 19:52:57 +00:00
|
|
|
type RequestContext struct {
|
2024-10-01 03:08:52 +00:00
|
|
|
Request *http.Request
|
|
|
|
|
Response http.ResponseWriter
|
2024-09-21 16:52:56 +00:00
|
|
|
locator *service.Locator
|
|
|
|
|
isBoosted bool
|
|
|
|
|
currentBrowserUrl string
|
|
|
|
|
hxPromptResponse string
|
|
|
|
|
isHxRequest bool
|
|
|
|
|
hxTargetId string
|
|
|
|
|
hxTriggerName string
|
|
|
|
|
hxTriggerId string
|
2024-09-26 19:15:57 +00:00
|
|
|
kv map[string]interface{}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 15:28:41 +00:00
|
|
|
func GetRequestContext(r *http.Request) *RequestContext {
|
|
|
|
|
return r.Context().Value(RequestContextKey).(*RequestContext)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 15:21:37 +00:00
|
|
|
func (c *RequestContext) SetCookie(cookie *http.Cookie) {
|
|
|
|
|
http.SetCookie(c.Response, cookie)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) Redirect(path string, code int) {
|
|
|
|
|
if code == 0 {
|
|
|
|
|
code = http.StatusTemporaryRedirect
|
|
|
|
|
}
|
|
|
|
|
if code < 300 || code > 399 {
|
|
|
|
|
code = http.StatusTemporaryRedirect
|
|
|
|
|
}
|
|
|
|
|
c.Response.Header().Set("Location", path)
|
|
|
|
|
c.Response.WriteHeader(code)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 12:48:19 +00:00
|
|
|
func (c *RequestContext) IsHttpPost() bool {
|
|
|
|
|
return c.Request.Method == http.MethodPost
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) IsHttpGet() bool {
|
|
|
|
|
return c.Request.Method == http.MethodGet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) IsHttpPut() bool {
|
|
|
|
|
return c.Request.Method == http.MethodPut
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) IsHttpDelete() bool {
|
|
|
|
|
return c.Request.Method == http.MethodDelete
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-04 16:21:15 +00:00
|
|
|
func (c *RequestContext) FormValue(key string) string {
|
|
|
|
|
return c.Request.FormValue(key)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 15:28:41 +00:00
|
|
|
func (c *RequestContext) Header(key string) string {
|
|
|
|
|
return c.Request.Header.Get(key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) UrlParam(key string) string {
|
|
|
|
|
return chi.URLParam(c.Request, key)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:39:34 +00:00
|
|
|
func (c *RequestContext) QueryParam(key string) string {
|
|
|
|
|
return c.Request.URL.Query().Get(key)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 15:28:41 +00:00
|
|
|
func (c *RequestContext) IsBoosted() bool {
|
|
|
|
|
return c.isBoosted
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) IsHxRequest() bool {
|
|
|
|
|
return c.isHxRequest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) HxPromptResponse() string {
|
|
|
|
|
return c.hxPromptResponse
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) HxTargetId() string {
|
|
|
|
|
return c.hxTargetId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) HxTriggerName() string {
|
|
|
|
|
return c.hxTriggerName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) HxTriggerId() string {
|
|
|
|
|
return c.hxTriggerId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) HxCurrentBrowserUrl() string {
|
2024-10-09 15:31:34 +00:00
|
|
|
return c.currentBrowserUrl
|
2024-10-09 15:28:41 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
func (c *RequestContext) Set(key string, value interface{}) {
|
|
|
|
|
if c.kv == nil {
|
|
|
|
|
c.kv = make(map[string]interface{})
|
|
|
|
|
}
|
|
|
|
|
c.kv[key] = value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *RequestContext) Get(key string) interface{} {
|
|
|
|
|
if c.kv == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return c.kv[key]
|
2024-09-18 19:52:57 +00:00
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// ServiceLocator returns the service locator to register and retrieve services
|
|
|
|
|
// Usage:
|
|
|
|
|
// service.Set[db.Queries](locator, service.Singleton, db.Provide)
|
|
|
|
|
// service.Get[db.Queries](locator)
|
2024-09-18 19:52:57 +00:00
|
|
|
func (c *RequestContext) ServiceLocator() *service.Locator {
|
|
|
|
|
return c.locator
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AppOpts struct {
|
|
|
|
|
LiveReload bool
|
|
|
|
|
ServiceLocator *service.Locator
|
2024-09-26 19:15:57 +00:00
|
|
|
Register func(app *App)
|
2025-01-06 16:24:49 +00:00
|
|
|
Port int
|
2024-09-18 19:52:57 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-11 00:52:18 +00:00
|
|
|
type App struct {
|
2024-09-26 19:15:57 +00:00
|
|
|
Opts AppOpts
|
|
|
|
|
Router *chi.Mux
|
2024-09-11 00:52:18 +00:00
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// Start starts the htmgo server
|
2024-09-18 19:52:57 +00:00
|
|
|
func Start(opts AppOpts) {
|
2024-09-26 19:15:57 +00:00
|
|
|
router := chi.NewRouter()
|
2024-09-18 19:52:57 +00:00
|
|
|
instance := App{
|
2024-09-26 19:15:57 +00:00
|
|
|
Opts: opts,
|
|
|
|
|
Router: router,
|
2024-09-18 19:52:57 +00:00
|
|
|
}
|
|
|
|
|
instance.start()
|
2024-09-11 00:52:18 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
const RequestContextKey = "htmgo.request.context"
|
|
|
|
|
|
2024-09-21 16:52:56 +00:00
|
|
|
func populateHxFields(cc *RequestContext) {
|
2024-09-26 19:15:57 +00:00
|
|
|
cc.isBoosted = cc.Request.Header.Get(hx.BoostedHeader) == "true"
|
|
|
|
|
cc.currentBrowserUrl = cc.Request.Header.Get(hx.CurrentUrlHeader)
|
|
|
|
|
cc.hxPromptResponse = cc.Request.Header.Get(hx.PromptResponseHeader)
|
|
|
|
|
cc.isHxRequest = cc.Request.Header.Get(hx.RequestHeader) == "true"
|
|
|
|
|
cc.hxTargetId = cc.Request.Header.Get(hx.TargetIdHeader)
|
|
|
|
|
cc.hxTriggerName = cc.Request.Header.Get(hx.TriggerNameHeader)
|
|
|
|
|
cc.hxTriggerId = cc.Request.Header.Get(hx.TriggerIdHeader)
|
2024-09-21 16:52:56 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
func (app *App) UseWithContext(h func(w http.ResponseWriter, r *http.Request, context map[string]any)) {
|
|
|
|
|
app.Router.Use(func(handler http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cc := r.Context().Value(RequestContextKey).(*RequestContext)
|
|
|
|
|
h(w, r, cc.kv)
|
|
|
|
|
handler.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-09-11 00:52:18 +00:00
|
|
|
|
2024-11-09 18:05:53 +00:00
|
|
|
func (app *App) Use(h func(ctx *RequestContext)) {
|
|
|
|
|
app.Router.Use(func(handler http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cc := r.Context().Value(RequestContextKey).(*RequestContext)
|
|
|
|
|
h(cc)
|
|
|
|
|
handler.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 17:51:23 +00:00
|
|
|
func GetLogLevel() slog.Level {
|
|
|
|
|
// Get the log level from the environment variable
|
|
|
|
|
logLevel := os.Getenv("LOG_LEVEL")
|
|
|
|
|
switch strings.ToUpper(logLevel) {
|
|
|
|
|
case "DEBUG":
|
|
|
|
|
return slog.LevelDebug
|
|
|
|
|
case "INFO":
|
|
|
|
|
return slog.LevelInfo
|
|
|
|
|
case "WARN":
|
|
|
|
|
return slog.LevelWarn
|
|
|
|
|
case "ERROR":
|
|
|
|
|
return slog.LevelError
|
|
|
|
|
default:
|
|
|
|
|
// Default to INFO if no valid log level is set
|
|
|
|
|
return slog.LevelInfo
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
func (app *App) start() {
|
2024-09-20 18:25:14 +00:00
|
|
|
|
2024-09-28 17:51:23 +00:00
|
|
|
slog.SetLogLoggerLevel(GetLogLevel())
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
app.Router.Use(func(h http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-09-18 19:52:57 +00:00
|
|
|
cc := &RequestContext{
|
2024-10-01 03:08:52 +00:00
|
|
|
locator: app.Opts.ServiceLocator,
|
|
|
|
|
Request: r,
|
|
|
|
|
Response: w,
|
|
|
|
|
kv: make(map[string]interface{}),
|
2024-09-18 19:52:57 +00:00
|
|
|
}
|
2024-09-21 16:52:56 +00:00
|
|
|
populateHxFields(cc)
|
2024-09-26 19:15:57 +00:00
|
|
|
ctx := context.WithValue(r.Context(), RequestContextKey, cc)
|
|
|
|
|
h.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
|
})
|
2024-09-18 19:52:57 +00:00
|
|
|
})
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
if app.Opts.Register != nil {
|
|
|
|
|
app.Opts.Register(app)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if app.Opts.LiveReload && IsDevelopment() {
|
|
|
|
|
app.AddLiveReloadHandler("/dev/livereload")
|
2024-09-11 00:52:18 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-17 15:41:29 +00:00
|
|
|
port := ":3000"
|
2025-01-06 16:24:49 +00:00
|
|
|
|
|
|
|
|
if os.Getenv("PORT") != "" {
|
|
|
|
|
port = fmt.Sprintf(":%s", os.Getenv("PORT"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if app.Opts.Port != 0 {
|
|
|
|
|
port = fmt.Sprintf(":%d", app.Opts.Port)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-21 14:17:18 +00:00
|
|
|
slog.Info(fmt.Sprintf("Server started at localhost%s", port))
|
2024-09-11 00:52:18 +00:00
|
|
|
|
2024-10-22 13:32:17 +00:00
|
|
|
if err := http.ListenAndServe(port, app.Router); err != nil {
|
2024-09-26 16:40:31 +00:00
|
|
|
// If we are in watch mode, just try to kill any processes holding that port
|
|
|
|
|
// and try again
|
|
|
|
|
if IsDevelopment() && IsWatchMode() {
|
|
|
|
|
slog.Info("Port already in use, trying to kill the process and start again")
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
cmd := exec.Command("cmd", "/C", fmt.Sprintf(`for /F "tokens=5" %%i in ('netstat -aon ^| findstr :%s') do taskkill /F /PID %%i`, port))
|
|
|
|
|
cmd.Run()
|
|
|
|
|
} else {
|
|
|
|
|
cmd := exec.Command("bash", "-c", fmt.Sprintf("kill -9 $(lsof -ti%s)", port))
|
|
|
|
|
cmd.Run()
|
|
|
|
|
}
|
2024-10-22 13:32:17 +00:00
|
|
|
|
2024-09-26 16:40:31 +00:00
|
|
|
time.Sleep(time.Millisecond * 50)
|
2024-10-22 13:32:17 +00:00
|
|
|
|
|
|
|
|
// Try to start server again
|
|
|
|
|
if err := http.ListenAndServe(port, app.Router); err != nil {
|
|
|
|
|
slog.Error("Failed to restart server", "error", err)
|
2024-09-26 16:40:31 +00:00
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-22 13:32:17 +00:00
|
|
|
|
2024-09-24 16:08:35 +00:00
|
|
|
panic(err)
|
2024-09-11 00:52:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
func writeHtml(w http.ResponseWriter, element Ren) error {
|
2024-10-20 12:48:19 +00:00
|
|
|
if element == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2024-10-27 02:41:21 +00:00
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
2024-10-23 14:28:19 +00:00
|
|
|
_, err := fmt.Fprint(w, Render(element, WithDocType()))
|
2024-09-26 19:15:57 +00:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func HtmlView(w http.ResponseWriter, page *Page) error {
|
2024-10-20 12:48:19 +00:00
|
|
|
// if the page is nil, do nothing, this can happen if custom response is written, such as a 302 redirect
|
|
|
|
|
if page == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2024-09-26 19:15:57 +00:00
|
|
|
return writeHtml(w, page.Root)
|
2024-09-11 00:52:18 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
func PartialViewWithHeaders(w http.ResponseWriter, headers *Headers, partial *Partial) error {
|
2024-10-20 12:48:19 +00:00
|
|
|
if partial == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-11 18:09:55 +00:00
|
|
|
if partial.Headers != nil {
|
|
|
|
|
for s, a := range *partial.Headers {
|
2024-09-26 19:15:57 +00:00
|
|
|
w.Header().Set(s, a)
|
2024-09-11 18:09:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if headers != nil {
|
|
|
|
|
for s, a := range *headers {
|
2024-09-26 19:15:57 +00:00
|
|
|
w.Header().Set(s, a)
|
2024-09-11 18:09:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
return writeHtml(w, partial.Root)
|
2024-09-11 18:09:55 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
func PartialView(w http.ResponseWriter, partial *Partial) error {
|
2024-10-20 12:48:19 +00:00
|
|
|
if partial == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-11 00:52:18 +00:00
|
|
|
if partial.Headers != nil {
|
|
|
|
|
for s, a := range *partial.Headers {
|
2024-09-26 19:15:57 +00:00
|
|
|
w.Header().Set(s, a)
|
2024-09-11 00:52:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 19:15:57 +00:00
|
|
|
return writeHtml(w, partial.Root)
|
2024-09-11 00:52:18 +00:00
|
|
|
}
|