cleaning up logging, speed up live reload using tailwind watch

This commit is contained in:
maddalax 2024-09-18 22:32:09 -05:00
parent f0a38379c3
commit b610aefa36
20 changed files with 158 additions and 95 deletions

View file

@ -3,12 +3,13 @@ package main
import ( import (
"log/slog" "log/slog"
"os" "os"
"strings"
) )
func getLogLevel() slog.Level { func getLogLevel() slog.Level {
// Get the log level from the environment variable // Get the log level from the environment variable
logLevel := os.Getenv("LOG_LEVEL") logLevel := os.Getenv("LOG_LEVEL")
switch logLevel { switch strings.ToUpper(logLevel) {
case "DEBUG": case "DEBUG":
return slog.LevelDebug return slog.LevelDebug
case "INFO": case "INFO":

View file

@ -51,18 +51,21 @@ func main() {
taskName := os.Args[1] taskName := os.Args[1]
slog.Debug("Running task: %s", taskName) slog.Debug("Running task:", slog.String("task", taskName))
slog.Debug("working dir %s", process.GetWorkingDir()) slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir()))
if taskName == "watch" { if taskName == "watch" {
os.Setenv("ENV", "development") os.Setenv("ENV", "development")
os.Setenv("WATCH_MODE", "true") os.Setenv("WATCH_MODE", "true")
copyassets.CopyAssets() copyassets.CopyAssets()
astgen.GenAst(true) astgen.GenAst(process.ExitOnError)
css.GenerateCss(true) css.GenerateCss(process.ExitOnError, process.Silent)
run.EntGenerate() run.EntGenerate()
go func() { go func() {
_ = run.Server(true) css.GenerateCssWatch(process.ExitOnError)
}()
go func() {
_ = run.Server(process.ExitOnError)
}() }()
startWatcher(reloader.OnFileChange) startWatcher(reloader.OnFileChange)
} else { } else {
@ -75,18 +78,18 @@ func main() {
} }
if taskName == "generate" { if taskName == "generate" {
run.EntGenerate() run.EntGenerate()
astgen.GenAst(true) astgen.GenAst(process.ExitOnError)
} }
if taskName == "setup" { if taskName == "setup" {
run.Setup() run.Setup()
} else if taskName == "css" { } else if taskName == "css" {
_ = css.GenerateCss(true) _ = css.GenerateCss(process.ExitOnError)
} else if taskName == "ast" { } else if taskName == "ast" {
_ = astgen.GenAst(true) _ = astgen.GenAst(process.ExitOnError)
} else if taskName == "run" { } else if taskName == "run" {
_ = astgen.GenAst(true) _ = astgen.GenAst(process.ExitOnError)
_ = css.GenerateCss(true) _ = css.GenerateCss(process.ExitOnError)
_ = run.Server(true) _ = run.Server(process.ExitOnError)
} else if taskName == "template" { } else if taskName == "template" {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
fmt.Print("What would you like to call your new app?: ") fmt.Print("What would you like to call your new app?: ")

View file

@ -9,6 +9,7 @@ import (
"golang.org/x/mod/modfile" "golang.org/x/mod/modfile"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
) )
@ -354,9 +355,9 @@ func GetModuleName() string {
return modName return modName
} }
func GenAst(exitOnError bool) error { func GenAst(flags ...process.RunFlag) error {
if GetModuleName() == "" { if GetModuleName() == "" {
if exitOnError { if slices.Contains(flags, process.ExitOnError) {
os.Exit(1) os.Exit(1)
} }
return fmt.Errorf("error getting module name") return fmt.Errorf("error getting module name")

View file

@ -7,6 +7,7 @@ import (
"golang.org/x/mod/modfile" "golang.org/x/mod/modfile"
"io" "io"
"log" "log"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -122,5 +123,5 @@ func CopyAssets() {
log.Fatalf("Error: %v", err) log.Fatalf("Error: %v", err)
} }
fmt.Printf("successfully copied assets to %s\n", destDir) slog.Debug("successfully copied assets to %s\n", destDir)
} }

View file

@ -2,9 +2,15 @@ package css
import "github.com/maddalax/htmgo/cli/tasks/process" import "github.com/maddalax/htmgo/cli/tasks/process"
func GenerateCss(exitOnError bool) error { func GenerateCss(flags ...process.RunFlag) error {
return process.RunMany([]string{ return process.RunMany([]string{
"chmod +x ./assets/css/tailwindcss", "chmod +x ./assets/css/tailwindcss",
"./assets/css/tailwindcss -i ./assets/css/input.css -o ./assets/dist/main.css -c ./assets/css/tailwind.config.js", "./assets/css/tailwindcss -i ./assets/css/input.css -o ./assets/dist/main.css -c ./assets/css/tailwind.config.js",
}, exitOnError) }, append(flags, process.Silent)...)
}
func GenerateCssWatch(flags ...process.RunFlag) error {
return process.RunMany([]string{
"./assets/css/tailwindcss -i ./assets/css/input.css -o ./assets/dist/main.css -c ./assets/css/tailwind.config.js --watch=always",
}, append(flags, process.DontTrack, process.Silent)...)
} }

View file

@ -3,11 +3,11 @@ package process
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@ -17,7 +17,7 @@ var workingDir string
var commands = make([]*exec.Cmd, 0) var commands = make([]*exec.Cmd, 0)
func AppendRunning(cmd *exec.Cmd) { func AppendRunning(cmd *exec.Cmd) {
slog.Info("running", slog.String("command", strings.Join(cmd.Args, " "))) slog.Debug("running", slog.String("command", strings.Join(cmd.Args, " ")))
commands = append(commands, cmd) commands = append(commands, cmd)
} }
@ -49,7 +49,7 @@ func KillAll() {
if tries > 50 { if tries > 50 {
args := strings.Join(cmd.Args, " ") args := strings.Join(cmd.Args, " ")
log.Printf("process %v is not running after 50 tries, breaking.\n", args) slog.Debug("process %v is not running after 50 tries, breaking.\n", args)
allFinished = true allFinished = true
break break
} else { } else {
@ -90,13 +90,13 @@ func KillAll() {
if finished { if finished {
break break
} else { } else {
fmt.Printf("waiting for all processes to exit\n") slog.Debug("waiting for all processes to exit\n")
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 5)
} }
} }
commands = make([]*exec.Cmd, 0) commands = make([]*exec.Cmd, 0)
fmt.Printf("all processes killed\n") slog.Debug("all processes killed\n")
} }
func PidExists(pid int32) (bool, error) { func PidExists(pid int32) (bool, error) {
@ -129,14 +129,22 @@ func PidExists(pid int32) (bool, error) {
} }
func RunOrExit(command string) { func RunOrExit(command string) {
_ = Run(command, true) _ = Run(command, ExitOnError)
} }
func RunMany(commands []string, exitOnError bool) error { type RunFlag int
const (
ExitOnError RunFlag = iota
Silent
DontTrack
)
func RunMany(commands []string, flags ...RunFlag) error {
for _, command := range commands { for _, command := range commands {
err := Run(command, false) err := Run(command, flags...)
if err != nil { if err != nil {
if exitOnError { if slices.Contains(flags, ExitOnError) {
os.Exit(1) os.Exit(1)
} }
return err return err
@ -145,17 +153,26 @@ func RunMany(commands []string, exitOnError bool) error {
return nil return nil
} }
func Run(command string, exitOnError bool) error { func Run(command string, flags ...RunFlag) error {
cmd := exec.Command("bash", "-c", command) parts := strings.Fields(command)
cmd := exec.Command(parts[0], parts[1:]...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr if slices.Contains(flags, Silent) {
cmd.Stdout = nil
cmd.Stderr = nil
} else {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
if workingDir != "" { if workingDir != "" {
cmd.Dir = workingDir cmd.Dir = workingDir
} }
AppendRunning(cmd) if !slices.Contains(flags, DontTrack) {
AppendRunning(cmd)
}
err := cmd.Run() err := cmd.Run()
if err == nil { if err == nil {
@ -166,8 +183,10 @@ func Run(command string, exitOnError bool) error {
return nil return nil
} }
if exitOnError { if slices.Contains(flags, ExitOnError) {
log.Println(fmt.Sprintf("error: %v", err)) slog.Error("Error running command: ",
slog.String("error", err.Error()),
slog.String("command", command))
os.Exit(1) os.Exit(1)
} }

View file

@ -4,20 +4,22 @@ import (
"fmt" "fmt"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/maddalax/htmgo/cli/tasks/astgen" "github.com/maddalax/htmgo/cli/tasks/astgen"
"github.com/maddalax/htmgo/cli/tasks/css"
"github.com/maddalax/htmgo/cli/tasks/process" "github.com/maddalax/htmgo/cli/tasks/process"
"github.com/maddalax/htmgo/cli/tasks/run" "github.com/maddalax/htmgo/cli/tasks/run"
"github.com/maddalax/htmgo/cli/tasks/util"
"log/slog" "log/slog"
"strings" "strings"
"sync" "sync"
"time"
) )
type Change struct { type Change struct {
name string name string
op fsnotify.Op
} }
func NewChange(name string) *Change { func NewChange(event *fsnotify.Event) *Change {
return &Change{name: name} return &Change{name: event.Name, op: event.Op}
} }
func (c *Change) Name() string { func (c *Change) Name() string {
@ -46,60 +48,70 @@ func (c *Change) IsGenerated() bool {
return c.HasAnySuffix("generated.go") return c.HasAnySuffix("generated.go")
} }
func (c *Change) IsWrite() bool {
return c.op == fsnotify.Write
}
func (c *Change) IsGo() bool { func (c *Change) IsGo() bool {
return c.HasAnySuffix(".go") return c.HasAnySuffix(".go")
} }
type Tasks struct { type Tasks struct {
AstGen bool AstGen bool
Css bool
Run bool Run bool
Ent bool Ent bool
} }
func OnFileChange(events []*fsnotify.Event) { func OnFileChange(events []*fsnotify.Event) {
now := time.Now()
tasks := Tasks{} tasks := Tasks{}
for _, event := range events { for _, event := range events {
c := NewChange(event.Name) c := NewChange(event)
slog.Debug("file changed", slog.String("file", c.Name()))
if c.IsGenerated() { if c.IsGenerated() {
continue continue
} }
slog.Debug("file changed", slog.String("file", c.Name()))
if c.IsGo() && c.HasAnyPrefix("pages/", "partials/") { if c.IsGo() && c.HasAnyPrefix("pages/", "partials/") {
tasks.AstGen = true tasks.AstGen = true
} }
if c.IsGo() { if c.IsGo() {
tasks.Css = true
tasks.Run = true tasks.Run = true
} }
if c.HasAnySuffix("tailwind.config.js", ".css") { if c.HasAnySuffix("tailwind.config.js", ".css") {
tasks.Css = true
tasks.Run = true tasks.Run = true
} }
if c.HasAnyPrefix("ent/schema") { if c.HasAnyPrefix("ent/schema") {
tasks.Ent = true tasks.Ent = true
} }
slog.Info("file changed", slog.String("file", c.Name()))
} }
deps := make([]func() any, 0) deps := make([]func() any, 0)
if tasks.AstGen { if tasks.AstGen {
deps = append(deps, func() any { deps = append(deps, func() any {
return astgen.GenAst(false) return util.Trace("generate ast", func() any {
astgen.GenAst()
return nil
})
}) })
} }
if tasks.Ent { if tasks.Ent {
deps = append(deps, func() any { deps = append(deps, func() any {
run.EntGenerate() return util.Trace("generate ent", func() any {
return nil run.EntGenerate()
return nil
})
}) })
} }
@ -119,14 +131,16 @@ func OnFileChange(events []*fsnotify.Event) {
wg.Wait() wg.Wait()
if tasks.Run { if tasks.Run {
process.KillAll() util.Trace("kill all processes", func() any {
} process.KillAll()
return nil
if tasks.Css { })
go css.GenerateCss(false)
} }
if tasks.Run { if tasks.Run {
_ = run.Server(false) go run.Server()
} }
slog.Info("reloaded in", slog.Duration("duration", time.Since(now)))
} }

View file

@ -7,8 +7,8 @@ import (
) )
func Build() { func Build() {
astgen.GenAst(true) astgen.GenAst(process.ExitOnError)
css.GenerateCss(true) css.GenerateCss(process.ExitOnError)
process.RunOrExit("rm -rf ./dist") process.RunOrExit("rm -rf ./dist")
process.RunOrExit("mkdir -p ./dist/assets/dist") process.RunOrExit("mkdir -p ./dist/assets/dist")
process.RunOrExit("cp -r ./assets/dist/* ./dist/assets/dist/") process.RunOrExit("cp -r ./assets/dist/* ./dist/assets/dist/")

View file

@ -3,9 +3,9 @@ package run
import "github.com/maddalax/htmgo/cli/tasks/process" import "github.com/maddalax/htmgo/cli/tasks/process"
func EntNewSchema(name string) { func EntNewSchema(name string) {
process.RunOrExit("GOWORK=off go run -mod=mod entgo.io/ent/cmd/ent new " + name) process.RunOrExit("bash -c GOWORK=off go run -mod=mod entgo.io/ent/cmd/ent new " + name)
} }
func EntGenerate() { func EntGenerate() {
process.RunOrExit("GOWORK=off go generate ./ent") process.RunOrExit("bash -c GOWORK=off go generate ./ent")
} }

View file

@ -2,6 +2,6 @@ package run
import "github.com/maddalax/htmgo/cli/tasks/process" import "github.com/maddalax/htmgo/cli/tasks/process"
func Server(exitOnError bool) error { func Server(flags ...process.RunFlag) error {
return process.Run("go run .", exitOnError) return process.Run("go run .", flags...)
} }

View file

@ -11,7 +11,7 @@ func Setup() {
process.RunOrExit("go mod download") process.RunOrExit("go mod download")
process.RunOrExit("go mod tidy") process.RunOrExit("go mod tidy")
copyassets.CopyAssets() copyassets.CopyAssets()
_ = astgen.GenAst(true) _ = astgen.GenAst(process.ExitOnError)
_ = css.GenerateCss(true) _ = css.GenerateCss(process.ExitOnError)
EntGenerate() EntGenerate()
} }

13
cli/tasks/util/trace.go Normal file
View file

@ -0,0 +1,13 @@
package util
import (
"log/slog"
"time"
)
func Trace(name string, cb func() any) any {
now := time.Now()
result := cb()
slog.Debug("trace", slog.String("name", name), slog.Duration("duration", time.Since(now)))
return result
}

View file

@ -1,23 +1,23 @@
package main package main
import ( import (
"fmt"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"log" "log"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
"time"
) )
var ignoredDirs = []string{".git", ".idea", "node_modules", "vendor"} var ignoredDirs = []string{".git", ".idea", "node_modules", "vendor"}
func startWatcher(cb func(file []*fsnotify.Event)) { func startWatcher(cb func(file []*fsnotify.Event)) {
events := make([]*fsnotify.Event, 0) events := make([]*fsnotify.Event, 0)
debouncer := NewDebouncer(100 * time.Millisecond)
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Println("Recovered from fatal error:", r) slog.Debug("Recovered from fatal error:", slog.String("error", r.(error).Error()))
// You can log the error here or take other corrective actions
} }
}() }()
// Create new watcher. // Create new watcher.
@ -36,8 +36,10 @@ func startWatcher(cb func(file []*fsnotify.Event)) {
} }
if event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) { if event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
events = append(events, &event) events = append(events, &event)
go cb(events) debouncer.Do(func() {
events = make([]*fsnotify.Event, 0) cb(events)
events = make([]*fsnotify.Event, 0)
})
} }
case err, ok := <-watcher.Errors: case err, ok := <-watcher.Errors:
if !ok { if !ok {
@ -64,9 +66,9 @@ func startWatcher(cb func(file []*fsnotify.Event)) {
if info.IsDir() { if info.IsDir() {
err = watcher.Add(path) err = watcher.Add(path)
if err != nil { if err != nil {
log.Println("Error adding directory to watcher:", err) slog.Error("Error adding directory to watcher:", err)
} else { } else {
log.Println("Watching directory:", path) slog.Debug("Watching directory:", slog.String("path", path))
} }
} }
return nil return nil

View file

@ -11,7 +11,7 @@ type InputProps struct {
Type string Type string
DefaultValue string DefaultValue string
ValidationPath string ValidationPath string
Childen []h.Renderable Children []h.Renderable
} }
func Input(props InputProps) h.Renderable { func Input(props InputProps) h.Renderable {
@ -27,7 +27,7 @@ func Input(props InputProps) h.Renderable {
h.Class("border p-2 rounded"), h.Class("border p-2 rounded"),
h.If(props.Id != "", h.Id(props.Id)), h.If(props.Id != "", h.Id(props.Id)),
h.If(props.Name != "", h.Name(props.Name)), h.If(props.Name != "", h.Name(props.Name)),
h.If(props.Childen != nil, h.Children(props.Childen...)), h.If(props.Children != nil, h.Children(props.Children...)),
h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)), h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)),
validation, validation,
) )

File diff suppressed because one or more lines are too long

View file

@ -33,14 +33,15 @@ htmx.defineExtension("livereload", {
}); });
function reload() { function reload() {
fetch(window.location.href).then(response => { window.location.reload()
return response.text(); // fetch(window.location.href).then(response => {
}).then(html => { // return response.text();
document.open(); // }).then(html => {
document.write(html); // document.open();
document.close(); // document.write(html);
}).catch(err => { // document.close();
console.log('failed to fetch live reload', err) // }).catch(err => {
setTimeout(reload, 100) // console.log('failed to fetch live reload', err)
}) // setTimeout(reload, 100)
// })
} }

View file

@ -10,16 +10,8 @@ type WsOpts = {
export function createWebSocketClient(opts: WsOpts) { export function createWebSocketClient(opts: WsOpts) {
let socket: WebSocket | null = null; let socket: WebSocket | null = null;
const connect = (tries: number) => { const connect = (tries: number) => {
console.log('connecting to ws', opts.url, 'attempt', tries)
if(tries > 50) {
console.error('failed to connect to websocket after 50 tries, please reload the page');
return;
}
socket = new WebSocket(opts.url); socket = new WebSocket(opts.url);
// Handle connection open
socket.onopen = () => {
};
// Handle incoming messages // Handle incoming messages
socket.onmessage = (event) => { socket.onmessage = (event) => {
opts.onMessage(event.data) opts.onMessage(event.data)
@ -31,12 +23,14 @@ export function createWebSocketClient(opts: WsOpts) {
} catch(ex) { } catch(ex) {
// noop // noop
} }
let interval = tries * (opts.reconnectInterval || 100); socket = null
let interval = tries * (opts.reconnectInterval || 1000);
setTimeout(() => connect(tries + 1), interval); setTimeout(() => connect(tries + 1), interval);
}; };
// Handle connection close and attempt reconnection // Handle connection close and attempt reconnection
socket.onclose = () => { socket.onclose = () => {
let interval = tries * (opts.reconnectInterval || 100); socket = null;
let interval = tries * (opts.reconnectInterval || 1000);
setTimeout(() => connect(tries + 1), interval); setTimeout(() => connect(tries + 1), interval);
}; };
}; };

View file

@ -318,6 +318,10 @@ func NewHeaders(headers ...string) *Headers {
return &m return &m
} }
func Checkbox(children ...Renderable) Renderable {
return Input("checkbox", children...)
}
func Input(inputType string, children ...Renderable) Renderable { func Input(inputType string, children ...Renderable) Renderable {
return &Node{ return &Node{
tag: "input", tag: "input",

View file

@ -6,6 +6,7 @@ import "github.com/maddalax/htmgo/framework/h"
func RegisterPages(f *echo.Echo) { func RegisterPages(f *echo.Echo) {
f.GET("/", func(ctx echo.Context) error { f.GET("/", func(ctx echo.Context) error {
return h.HtmlView(ctx, IndexPage(ctx)) cc := ctx.(*h.RequestContext)
return h.HtmlView(ctx, IndexPage(cc))
}) })
} }

View file

@ -6,15 +6,18 @@ import "github.com/labstack/echo/v4"
import "starter-template/partials" import "starter-template/partials"
func GetPartialFromContext(ctx echo.Context) *h.Partial { func GetPartialFromContext(ctx echo.Context) *h.Partial {
path := ctx.Path() path := ctx.Request().URL.Path
if path == "SamplePartial" || path == "/starter-template/partials.SamplePartial" { if path == "SamplePartial" || path == "/starter-template/partials.SamplePartial" {
return partials.SamplePartial(ctx) cc := ctx.(*h.RequestContext)
return partials.SamplePartial(cc)
} }
if path == "NewPartial" || path == "/starter-template/partials.NewPartial" { if path == "NewPartial" || path == "/starter-template/partials.NewPartial" {
return partials.NewPartial(ctx) cc := ctx.(*h.RequestContext)
return partials.NewPartial(cc)
} }
if path == "NewPartial2" || path == "/starter-template/partials.NewPartial2" { if path == "NewPartial2" || path == "/starter-template/partials.NewPartial2" {
return partials.NewPartial2(ctx) cc := ctx.(*h.RequestContext)
return partials.NewPartial2(cc)
} }
return nil return nil
} }