diff --git a/cli/logger.go b/cli/logger.go new file mode 100644 index 0000000..e68f076 --- /dev/null +++ b/cli/logger.go @@ -0,0 +1,24 @@ +package main + +import ( + "log/slog" + "os" +) + +func getLogLevel() slog.Level { + // Get the log level from the environment variable + logLevel := os.Getenv("LOG_LEVEL") + switch 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 + } +} diff --git a/cli/runner.go b/cli/runner.go index 351dc6a..3e226b2 100644 --- a/cli/runner.go +++ b/cli/runner.go @@ -7,8 +7,10 @@ import ( "github.com/maddalax/htmgo/cli/tasks/astgen" "github.com/maddalax/htmgo/cli/tasks/css" "github.com/maddalax/htmgo/cli/tasks/downloadtemplate" + "github.com/maddalax/htmgo/cli/tasks/process" "github.com/maddalax/htmgo/cli/tasks/reloader" "github.com/maddalax/htmgo/cli/tasks/run" + "log/slog" "os" "strings" ) @@ -17,7 +19,7 @@ func main() { done := RegisterSignals() commandMap := make(map[string]*flag.FlagSet) - commands := []string{"template", "run", "watch", "build", "setup", "css"} + commands := []string{"template", "run", "watch", "build", "setup", "css", "schema"} for _, command := range commands { commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError) @@ -44,16 +46,35 @@ func main() { return } + slog.SetLogLoggerLevel(getLogLevel()) + taskName := os.Args[1] + slog.Debug("Running task: %s", taskName) + slog.Debug("working dir %s", process.GetWorkingDir()) + if taskName == "watch" { + os.Setenv("ENV", "development") + os.Setenv("WATCH_MODE", "true") astgen.GenAst(true) css.GenerateCss(true) + run.EntGenerate() go func() { _ = run.Server(true) }() startWatcher(reloader.OnFileChange) } else { + if taskName == "schema" { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Enter entity name:") + text, _ := reader.ReadString('\n') + text = strings.TrimSuffix(text, "\n") + run.EntNewSchema(text) + } + if taskName == "generate" { + run.EntGenerate() + astgen.GenAst(true) + } if taskName == "setup" { run.Setup() } else if taskName == "css" { diff --git a/cli/tasks/process/process.go b/cli/tasks/process/process.go index efb4d66..1f2f2f5 100644 --- a/cli/tasks/process/process.go +++ b/cli/tasks/process/process.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "log/slog" "os" "os/exec" "path/filepath" @@ -16,6 +17,7 @@ var workingDir string var commands = make([]*exec.Cmd, 0) func AppendRunning(cmd *exec.Cmd) { + slog.Info("running", slog.String("command", strings.Join(cmd.Args, " "))) commands = append(commands, cmd) } @@ -154,15 +156,18 @@ func Run(command string, exitOnError bool) error { AppendRunning(cmd) err := cmd.Run() - if err != nil { - if strings.Contains(err.Error(), "signal: killed") { - return nil - } - if exitOnError { - log.Println(fmt.Sprintf("error: %v", err)) - os.Exit(1) - } - return err + + if err == nil { + return nil } - return nil + + if exitOnError { + log.Println(fmt.Sprintf("error: %v", err)) + os.Exit(1) + } + + if strings.Contains(err.Error(), "signal: killed") { + return nil + } + return err } diff --git a/cli/tasks/reloader/reloader.go b/cli/tasks/reloader/reloader.go index 8405463..515978c 100644 --- a/cli/tasks/reloader/reloader.go +++ b/cli/tasks/reloader/reloader.go @@ -7,6 +7,7 @@ import ( "github.com/maddalax/htmgo/cli/tasks/css" "github.com/maddalax/htmgo/cli/tasks/process" "github.com/maddalax/htmgo/cli/tasks/run" + "log/slog" "strings" "sync" ) @@ -53,6 +54,7 @@ type Tasks struct { AstGen bool Css bool Run bool + Ent bool } func OnFileChange(events []*fsnotify.Event) { @@ -61,6 +63,8 @@ func OnFileChange(events []*fsnotify.Event) { for _, event := range events { c := NewChange(event.Name) + slog.Debug("file changed", slog.String("file", c.Name())) + if c.IsGenerated() { continue } @@ -77,6 +81,10 @@ func OnFileChange(events []*fsnotify.Event) { if c.HasAnySuffix("tailwind.config.js", ".css") { tasks.Css = true } + + if c.HasAnyPrefix("ent/schema") { + tasks.Ent = true + } } deps := make([]func() any, 0) @@ -93,6 +101,13 @@ func OnFileChange(events []*fsnotify.Event) { }) } + if tasks.Ent { + deps = append(deps, func() any { + run.EntGenerate() + return nil + }) + } + wg := sync.WaitGroup{} for _, dep := range deps { diff --git a/cli/tasks/run/entschema.go b/cli/tasks/run/entschema.go new file mode 100644 index 0000000..f907702 --- /dev/null +++ b/cli/tasks/run/entschema.go @@ -0,0 +1,11 @@ +package run + +import "github.com/maddalax/htmgo/cli/tasks/process" + +func EntNewSchema(name string) { + process.RunOrExit("GOWORK=off go run -mod=mod entgo.io/ent/cmd/ent new " + name) +} + +func EntGenerate() { + process.RunOrExit("GOWORK=off go generate ./ent") +} diff --git a/cli/watcher.go b/cli/watcher.go index 0290d9a..4a1af28 100644 --- a/cli/watcher.go +++ b/cli/watcher.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/fsnotify/fsnotify" "log" + "log/slog" "os" "path/filepath" ) @@ -11,7 +12,6 @@ import ( var ignoredDirs = []string{".git", ".idea", "node_modules", "vendor"} func startWatcher(cb func(file []*fsnotify.Event)) { - //debouncer := NewDebouncer(time.Millisecond * 100) events := make([]*fsnotify.Event, 0) defer func() { @@ -43,7 +43,7 @@ func startWatcher(cb func(file []*fsnotify.Event)) { if !ok { return } - log.Println("error:", err) + slog.Error("error:", err.Error()) } } }() diff --git a/framework-ui/ui/input.go b/framework-ui/ui/input.go index bd4ae9c..a077feb 100644 --- a/framework-ui/ui/input.go +++ b/framework-ui/ui/input.go @@ -1,6 +1,8 @@ package ui -import "github.com/maddalax/htmgo/framework/h" +import ( + "github.com/maddalax/htmgo/framework/h" +) type InputProps struct { Id string @@ -13,7 +15,6 @@ type InputProps struct { } func Input(props InputProps) h.Renderable { - validation := h.If(props.ValidationPath != "", h.Children( h.Post(props.ValidationPath), h.Trigger("change"), diff --git a/framework/h/app.go b/framework/h/app.go index 83ed124..5ecc8fa 100644 --- a/framework/h/app.go +++ b/framework/h/app.go @@ -1,7 +1,11 @@ package h import ( + "fmt" "github.com/gofiber/fiber/v2" + "github.com/maddalax/htmgo/framework/util/process" + "log/slog" + "time" ) type App struct { @@ -31,10 +35,23 @@ func (a App) start(app *fiber.App) { AddLiveReloadHandler("/livereload", a.Fiber) } - err := a.Fiber.Listen(":3000") + port := ":3000" + err := a.Fiber.Listen(port) if err != nil { - panic(err) + // 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") + process.RunOrExit(fmt.Sprintf("kill -9 $(lsof -t -i%s)", port)) + time.Sleep(time.Millisecond * 50) + err = a.Fiber.Listen(port) + if err != nil { + panic(err) + } + } else { + panic(err) + } } } diff --git a/framework/h/env.go b/framework/h/env.go new file mode 100644 index 0000000..53b797c --- /dev/null +++ b/framework/h/env.go @@ -0,0 +1,15 @@ +package h + +import "os" + +func IsWatchMode() bool { + return os.Getenv("WATCH_MODE") == "true" +} + +func IsDevelopment() bool { + return os.Getenv("ENV") == "development" +} + +func IsProduction() bool { + return os.Getenv("ENV") == "production" +} diff --git a/framework/util/process/process.go b/framework/util/process/process.go new file mode 100644 index 0000000..bc9b9a6 --- /dev/null +++ b/framework/util/process/process.go @@ -0,0 +1,18 @@ +package process + +import ( + "log/slog" + "os" + "os/exec" +) + +func RunOrExit(command string) { + cmd := exec.Command("bash", "-c", command) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + slog.Error("Error running command: ", slog.String("command", command)) + os.Exit(1) + } +} diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..e2fe4d9 --- /dev/null +++ b/notes.md @@ -0,0 +1,2 @@ +structured logging - https://go.dev/blog/slog + diff --git a/sandbox/go.sum b/sandbox/go.sum index 7297dd0..3af5118 100644 --- a/sandbox/go.sum +++ b/sandbox/go.sum @@ -1,5 +1,7 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -17,6 +19,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/setup.md b/setup.md index dfb5a85..ba6bf8c 100644 --- a/setup.md +++ b/setup.md @@ -1,2 +1 @@ -go install github.com/maddalax/htmgo/framework/tooling/htmgo@latest -go run htmgo -task template \ No newline at end of file +go run github.com/maddalax/cli/htmgo@latest template \ No newline at end of file diff --git a/starter-template/ent/generate.go b/starter-template/ent/generate.go new file mode 100644 index 0000000..8d3fdfd --- /dev/null +++ b/starter-template/ent/generate.go @@ -0,0 +1,3 @@ +package ent + +//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema diff --git a/starter-template/ent/schema/user.go b/starter-template/ent/schema/user.go new file mode 100644 index 0000000..b0d0903 --- /dev/null +++ b/starter-template/ent/schema/user.go @@ -0,0 +1,42 @@ +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema/field" + "time" +) + +// User holds the schema definition for the User entity. +type User struct { + ent.Schema +} + +// Fields of the User. +func (User) Fields() []ent.Field { + return []ent.Field{ + field.Int("age"). + Positive(), + field.String("name"). + Default("unknown"), + field.String("occupation").Optional(), + field.String("email").Optional(), + field.String("password").Sensitive().Optional(), + field.String("test").Optional(), + field.String("test2").Optional(), + field.String("test4").Optional(), + field.Time("created_at"). + Default(time.Now).Annotations( + entsql.Default("CURRENT_TIMESTAMP"), + ), + field.Time("updated_at"). + Default(time.Now).Annotations( + entsql.Default("CURRENT_TIMESTAMP"), + ), + } +} + +// Edges of the User. +func (User) Edges() []ent.Edge { + return nil +} diff --git a/starter-template/go.mod b/starter-template/go.mod index 4bc3ff8..b23a75a 100644 --- a/starter-template/go.mod +++ b/starter-template/go.mod @@ -3,20 +3,38 @@ module starter-template go 1.23.0 require ( + entgo.io/ent v0.14.1 github.com/gofiber/fiber/v2 v2.52.5 github.com/maddalax/htmgo/framework v0.0.0-20240916224719-9e5d8edada65 ) require ( + ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect + github.com/agext/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/go-openapi/inflect v0.19.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/hcl/v2 v2.13.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/zclconf/go-cty v1.8.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/tools v0.24.0 // indirect ) diff --git a/starter-template/main.go b/starter-template/main.go index c04124d..84971ca 100644 --- a/starter-template/main.go +++ b/starter-template/main.go @@ -1,9 +1,12 @@ package main import ( + "context" "github.com/gofiber/fiber/v2" "github.com/maddalax/htmgo/framework/h" + _ "github.com/mattn/go-sqlite3" "log" + "starter-template/ent" "starter-template/pages" "starter-template/partials/load" "time" @@ -30,6 +33,16 @@ func main() { load.RegisterPartials(f) pages.RegisterPages(f) + client, err := ent.Open("sqlite3", "file:ent.db?cache=shared&_fk=1") + if err != nil { + log.Fatalf("failed opening connection to sqlite: %v", err) + } + defer client.Close() + // Run the auto migration tool. + if err := client.Schema.Create(context.Background()); err != nil { + log.Fatalf("failed schema resources: %v", err) + } + h.Start(f, h.App{ LiveReload: true, })