move files around, build custom watcher / runner
This commit is contained in:
parent
4ba9340317
commit
a38064ed12
32 changed files with 655 additions and 299 deletions
34
cli/debounce.go
Normal file
34
cli/debounce.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Debouncer is a struct that holds the debounce logic
|
||||
type Debouncer struct {
|
||||
delay time.Duration
|
||||
timer *time.Timer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewDebouncer creates a new Debouncer with the specified delay
|
||||
func NewDebouncer(delay time.Duration) *Debouncer {
|
||||
return &Debouncer{
|
||||
delay: delay,
|
||||
}
|
||||
}
|
||||
|
||||
// Do calls the provided function after the delay, resetting the delay if called again
|
||||
func (d *Debouncer) Do(f func()) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// If there's an existing timer, stop it
|
||||
if d.timer != nil {
|
||||
d.timer.Stop()
|
||||
}
|
||||
|
||||
// Create a new timer
|
||||
d.timer = time.AfterFunc(d.delay, f)
|
||||
}
|
||||
13
cli/go.mod
Normal file
13
cli/go.mod
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
module github.com/maddalax/htmgo/cli
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/dave/jennifer v1.7.1
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
golang.org/x/mod v0.21.0
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/tools v0.25.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.25.0 // indirect
|
||||
83
cli/runner.go
Normal file
83
cli/runner.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/maddalax/htmgo/cli/tasks/astgen"
|
||||
"github.com/maddalax/htmgo/cli/tasks/copyassets"
|
||||
"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"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
done := RegisterSignals()
|
||||
|
||||
commandMap := make(map[string]*flag.FlagSet)
|
||||
commands := []string{"template", "run", "watch", "build", "setup", "css"}
|
||||
|
||||
for _, command := range commands {
|
||||
commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError)
|
||||
}
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | ")))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c := commandMap[os.Args[1]]
|
||||
|
||||
if c == nil {
|
||||
fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | ")))
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.Parse(os.Args[2:])
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
taskName := os.Args[1]
|
||||
|
||||
if taskName == "watch" {
|
||||
astgen.GenAst(true)
|
||||
css.GenerateCss(true)
|
||||
go func() {
|
||||
_ = run.Server(true)
|
||||
}()
|
||||
startWatcher(reloader.OnFileChange)
|
||||
} else {
|
||||
if taskName == "setup" {
|
||||
process.RunOrExit("go mod download")
|
||||
process.RunOrExit("go mod tidy")
|
||||
copyassets.CopyAssets()
|
||||
_ = astgen.GenAst(true)
|
||||
_ = css.GenerateCss(true)
|
||||
}
|
||||
if taskName == "css" {
|
||||
_ = css.GenerateCss(true)
|
||||
}
|
||||
if taskName == "ast" {
|
||||
_ = astgen.GenAst(true)
|
||||
}
|
||||
if taskName == "run" {
|
||||
_ = astgen.GenAst(true)
|
||||
_ = css.GenerateCss(true)
|
||||
_ = run.Server(true)
|
||||
}
|
||||
if taskName == "template" {
|
||||
downloadtemplate.DownloadTemplate("./my-app")
|
||||
}
|
||||
}
|
||||
|
||||
<-done
|
||||
fmt.Println("Cleanup complete. Exiting.")
|
||||
}
|
||||
31
cli/signals.go
Normal file
31
cli/signals.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/maddalax/htmgo/cli/tasks/process"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func RegisterSignals() chan bool {
|
||||
// Create a channel to receive OS signals
|
||||
sigs := make(chan os.Signal, 1)
|
||||
// Register the channel to receive interrupt and terminate signals
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
done := make(chan bool, 1)
|
||||
// Run a goroutine to handle signals
|
||||
go func() {
|
||||
// Block until a signal is received
|
||||
sig := <-sigs
|
||||
fmt.Println()
|
||||
fmt.Println("Received signal:", sig)
|
||||
// Perform cleanup
|
||||
process.KillAll()
|
||||
// Signal that cleanup is done
|
||||
done <- true
|
||||
}()
|
||||
|
||||
return done
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package astgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package astgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package astgen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -243,6 +243,10 @@ func writePartialsFile() {
|
|||
return
|
||||
}
|
||||
|
||||
if len(partials) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
builder := NewCodeBuilder(nil)
|
||||
builder.AppendLine(`// Package partials THIS FILE IS GENERATED. DO NOT EDIT.`)
|
||||
builder.AppendLine("package load")
|
||||
|
|
@ -284,6 +288,7 @@ func formatRoute(path string) string {
|
|||
}
|
||||
|
||||
func writePagesFile() {
|
||||
|
||||
builder := NewCodeBuilder(nil)
|
||||
builder.AppendLine(`// Package pages THIS FILE IS GENERATED. DO NOT EDIT.`)
|
||||
builder.AppendLine("package pages")
|
||||
|
|
@ -292,6 +297,10 @@ func writePagesFile() {
|
|||
|
||||
pages, _ := findPublicFuncsReturningHPage("pages")
|
||||
|
||||
if len(pages) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, page := range pages {
|
||||
if page.Import != "" && page.Package != "pages" {
|
||||
builder.AddImport(page.Import)
|
||||
|
|
@ -336,13 +345,20 @@ func GetModuleName() string {
|
|||
goModBytes, err := os.ReadFile(modPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error reading go.mod: %v\n", err)
|
||||
os.Exit(1)
|
||||
return ""
|
||||
}
|
||||
modName := modfile.ModulePath(goModBytes)
|
||||
return modName
|
||||
}
|
||||
|
||||
func main() {
|
||||
func GenAst(exitOnError bool) error {
|
||||
if GetModuleName() == "" {
|
||||
if exitOnError {
|
||||
os.Exit(1)
|
||||
}
|
||||
return fmt.Errorf("error getting module name")
|
||||
}
|
||||
writePartialsFile()
|
||||
writePagesFile()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package astgen
|
||||
|
||||
// OrderedMap is a generic data structure that maintains the order of keys.
|
||||
type OrderedMap[K comparable, V any] struct {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package astgen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package astgen
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package copyassets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -83,7 +83,7 @@ func copyDir(srcDir, dstDir string) error {
|
|||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
func CopyAssets() {
|
||||
modulePath := "github.com/maddalax/htmgo/framework"
|
||||
version, err := getModuleVersion(modulePath)
|
||||
if err != nil {
|
||||
10
cli/tasks/css/css.go
Normal file
10
cli/tasks/css/css.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package css
|
||||
|
||||
import "github.com/maddalax/htmgo/cli/tasks/process"
|
||||
|
||||
func GenerateCss(exitOnError bool) error {
|
||||
return process.RunMany([]string{
|
||||
"chmod +x ./assets/css/tailwindcss",
|
||||
"./assets/css/tailwindcss -i ./assets/css/input.css -o ./assets/dist/main.css -c ./assets/css/tailwind.config.js",
|
||||
}, exitOnError)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package downloadtemplate
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
|
@ -34,25 +34,23 @@ func deleteAllExceptTemplate(outPath string, excludeDir string) {
|
|||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
func DownloadTemplate(outPath string) {
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
outPath := flag.String("out", "", "Specify the output path for the new app")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
*outPath = strings.ReplaceAll(*outPath, "\n", "")
|
||||
*outPath = strings.ReplaceAll(*outPath, " ", "-")
|
||||
*outPath = strings.ToLower(*outPath)
|
||||
outPath = strings.ReplaceAll(outPath, "\n", "")
|
||||
outPath = strings.ReplaceAll(outPath, " ", "-")
|
||||
outPath = strings.ToLower(outPath)
|
||||
|
||||
if *outPath == "" {
|
||||
if outPath == "" {
|
||||
fmt.Println("Please provide a name for your app.")
|
||||
return
|
||||
}
|
||||
|
||||
excludeDir := "starter-template"
|
||||
|
||||
install := exec.Command("git", "clone", "https://github.com/maddalax/htmgo", "--depth=1", *outPath)
|
||||
install := exec.Command("git", "clone", "https://github.com/maddalax/htmgo", "--depth=1", outPath)
|
||||
install.Stdout = os.Stdout
|
||||
install.Stderr = os.Stderr
|
||||
err := install.Run()
|
||||
|
|
@ -62,9 +60,9 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
deleteAllExceptTemplate(*outPath, excludeDir)
|
||||
deleteAllExceptTemplate(outPath, excludeDir)
|
||||
|
||||
newDir := filepath.Join(cwd, *outPath)
|
||||
newDir := filepath.Join(cwd, outPath)
|
||||
|
||||
commands := [][]string{
|
||||
{"cp", "-vaR", fmt.Sprintf("%s/.", excludeDir), "."},
|
||||
|
|
@ -88,8 +86,8 @@ func main() {
|
|||
|
||||
fmt.Println("Template downloaded successfully.")
|
||||
fmt.Println("To start the development server, run the following commands:")
|
||||
fmt.Printf("cd %s && htmgo run\n", *outPath)
|
||||
fmt.Printf("cd %s && htmgo run\n", outPath)
|
||||
|
||||
fmt.Printf("To build the project, run the following command:\n")
|
||||
fmt.Printf("cd %s && htmgo build\n", *outPath)
|
||||
fmt.Printf("cd %s && htmgo build\n", outPath)
|
||||
}
|
||||
145
cli/tasks/process/process.go
Normal file
145
cli/tasks/process/process.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package process
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var commands = make([]*exec.Cmd, 0)
|
||||
|
||||
func AppendRunning(cmd *exec.Cmd) {
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
|
||||
func KillAll() {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
tries++
|
||||
allFinished := true
|
||||
for _, cmd := range commands {
|
||||
if cmd.Process == nil {
|
||||
allFinished = false
|
||||
|
||||
if tries > 50 {
|
||||
args := strings.Join(cmd.Args, " ")
|
||||
log.Printf("process %v is not running after 50 tries, breaking.\n", args)
|
||||
allFinished = true
|
||||
break
|
||||
} else {
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if allFinished {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, command := range commands {
|
||||
pid := command.Process.Pid
|
||||
err := syscall.Kill(-pid, syscall.SIGKILL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
finished := true
|
||||
for _, c := range commands {
|
||||
if c.Process == nil {
|
||||
continue
|
||||
}
|
||||
exists, err := PidExists(int32(c.Process.Pid))
|
||||
if err != nil {
|
||||
finished = false
|
||||
}
|
||||
if exists {
|
||||
syscall.Kill(-c.Process.Pid, syscall.SIGKILL)
|
||||
finished = false
|
||||
}
|
||||
}
|
||||
|
||||
if finished {
|
||||
break
|
||||
} else {
|
||||
fmt.Printf("waiting for all processes to exit\n")
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
}
|
||||
}
|
||||
|
||||
commands = make([]*exec.Cmd, 0)
|
||||
}
|
||||
|
||||
func PidExists(pid int32) (bool, error) {
|
||||
if pid <= 0 {
|
||||
return false, fmt.Errorf("invalid pid %v", pid)
|
||||
}
|
||||
proc, err := os.FindProcess(int(pid))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = proc.Signal(syscall.Signal(0))
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if err.Error() == "os: process already finished" {
|
||||
return false, nil
|
||||
}
|
||||
var errno syscall.Errno
|
||||
ok := errors.As(err, &errno)
|
||||
if !ok {
|
||||
return false, err
|
||||
}
|
||||
switch errno {
|
||||
case syscall.ESRCH:
|
||||
return false, nil
|
||||
case syscall.EPERM:
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func RunOrExit(command string) {
|
||||
_ = Run(command, true)
|
||||
}
|
||||
|
||||
func RunMany(commands []string, exitOnError bool) error {
|
||||
for _, command := range commands {
|
||||
err := Run(command, false)
|
||||
if err != nil {
|
||||
if exitOnError {
|
||||
os.Exit(1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Run(command string, exitOnError bool) error {
|
||||
cmd := exec.Command("bash", "-c", command)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
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
|
||||
}
|
||||
return nil
|
||||
}
|
||||
115
cli/tasks/reloader/reloader.go
Normal file
115
cli/tasks/reloader/reloader.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package reloader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"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/run"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Change struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func NewChange(name string) *Change {
|
||||
return &Change{name: name}
|
||||
}
|
||||
|
||||
func (c *Change) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c *Change) HasAnyPrefix(prefix ...string) bool {
|
||||
for _, s := range prefix {
|
||||
if strings.HasPrefix(c.name, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Change) HasAnySuffix(suffix ...string) bool {
|
||||
for _, s := range suffix {
|
||||
if strings.HasSuffix(c.name, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Change) IsGenerated() bool {
|
||||
return c.HasAnySuffix("generated.go")
|
||||
}
|
||||
|
||||
func (c *Change) IsGo() bool {
|
||||
return c.HasAnySuffix(".go")
|
||||
}
|
||||
|
||||
type Tasks struct {
|
||||
AstGen bool
|
||||
Css bool
|
||||
Run bool
|
||||
}
|
||||
|
||||
func OnFileChange(events []*fsnotify.Event) {
|
||||
tasks := Tasks{}
|
||||
|
||||
for _, event := range events {
|
||||
c := NewChange(event.Name)
|
||||
|
||||
if c.IsGenerated() {
|
||||
continue
|
||||
}
|
||||
|
||||
if c.IsGo() && c.HasAnyPrefix("pages/", "partials/") {
|
||||
tasks.AstGen = true
|
||||
}
|
||||
|
||||
if c.IsGo() {
|
||||
tasks.Css = true
|
||||
tasks.Run = true
|
||||
}
|
||||
|
||||
if c.HasAnySuffix("tailwind.config.js", ".css") {
|
||||
tasks.Css = true
|
||||
}
|
||||
}
|
||||
|
||||
deps := make([]func() any, 0)
|
||||
|
||||
if tasks.AstGen {
|
||||
deps = append(deps, func() any {
|
||||
return astgen.GenAst(false)
|
||||
})
|
||||
}
|
||||
|
||||
if tasks.Css {
|
||||
deps = append(deps, func() any {
|
||||
return css.GenerateCss(false)
|
||||
})
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
for _, dep := range deps {
|
||||
wg.Add(1)
|
||||
go func(dep func() any) {
|
||||
defer wg.Done()
|
||||
err := dep()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}(dep)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if tasks.Run {
|
||||
process.KillAll()
|
||||
_ = run.Server(false)
|
||||
}
|
||||
}
|
||||
7
cli/tasks/run/runserver.go
Normal file
7
cli/tasks/run/runserver.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package run
|
||||
|
||||
import "github.com/maddalax/htmgo/cli/tasks/process"
|
||||
|
||||
func Server(exitOnError bool) error {
|
||||
return process.Run("go run .", exitOnError)
|
||||
}
|
||||
73
cli/watcher.go
Normal file
73
cli/watcher.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func startWatcher(cb func(file []*fsnotify.Event)) {
|
||||
//debouncer := NewDebouncer(time.Millisecond * 100)
|
||||
events := make([]*fsnotify.Event, 0)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("Recovered from fatal error:", r)
|
||||
// You can log the error here or take other corrective actions
|
||||
}
|
||||
}()
|
||||
// Create new watcher.
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
// Start listening for events.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Has(fsnotify.Write) {
|
||||
events = append(events, &event)
|
||||
go cb(events)
|
||||
events = make([]*fsnotify.Event, 0)
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
rootDir := "."
|
||||
// Walk through the root directory and add all subdirectories to the watcher
|
||||
err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Only watch directories
|
||||
if info.IsDir() {
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
log.Println("Error adding directory to watcher:", err)
|
||||
} else {
|
||||
log.Println("Watching directory:", path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
done := RegisterSignals()
|
||||
<-done
|
||||
println("process exited")
|
||||
}
|
||||
15
framework/h/events.go
Normal file
15
framework/h/events.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package h
|
||||
|
||||
type HxEvent = string
|
||||
|
||||
var (
|
||||
HxBeforeRequest HxEvent = "hx-on::before-request"
|
||||
HxAfterRequest HxEvent = "hx-on::after-request"
|
||||
HxOnMutationError HxEvent = "hx-on::mutation-error"
|
||||
HxOnLoad HxEvent = "hx-on::load"
|
||||
HxOnLoadError HxEvent = "hx-on::load-error"
|
||||
HxRequestTimeout HxEvent = "hx-on::request-timeout"
|
||||
HxTrigger HxEvent = "hx-on::trigger"
|
||||
HxRequestStart HxEvent = "hx-on::xhr:loadstart"
|
||||
HxRequestProgress HxEvent = "hx-on::xhr:progress"
|
||||
)
|
||||
|
|
@ -4,58 +4,72 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
var HxBeforeRequest = "hx-on::before-request"
|
||||
var HxAfterRequest = "hx-on::after-request"
|
||||
var HxOnMutationError = "hx-on::mutation-error"
|
||||
|
||||
type LifeCycle struct {
|
||||
beforeRequest []JsCommand
|
||||
afterRequest []JsCommand
|
||||
onMutationError []JsCommand
|
||||
handlers map[HxEvent][]JsCommand
|
||||
}
|
||||
|
||||
func NewLifeCycle() *LifeCycle {
|
||||
return &LifeCycle{
|
||||
beforeRequest: []JsCommand{},
|
||||
afterRequest: []JsCommand{},
|
||||
onMutationError: []JsCommand{},
|
||||
handlers: make(map[HxEvent][]JsCommand),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LifeCycle) BeforeRequest(cmd ...JsCommand) *LifeCycle {
|
||||
l.beforeRequest = append(l.beforeRequest, cmd...)
|
||||
func (l *LifeCycle) OnEvent(event HxEvent, cmd ...JsCommand) *LifeCycle {
|
||||
if l.handlers[event] == nil {
|
||||
l.handlers[event] = []JsCommand{}
|
||||
}
|
||||
l.handlers[event] = append(l.handlers[event], cmd...)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *LifeCycle) BeforeRequest(cmd ...JsCommand) *LifeCycle {
|
||||
l.OnEvent(HxBeforeRequest, cmd...)
|
||||
return l
|
||||
}
|
||||
|
||||
func OnEvent(event HxEvent, cmd ...JsCommand) *LifeCycle {
|
||||
return NewLifeCycle().OnEvent(event, cmd...)
|
||||
}
|
||||
|
||||
func BeforeRequest(cmd ...JsCommand) *LifeCycle {
|
||||
return NewLifeCycle().BeforeRequest(cmd...)
|
||||
}
|
||||
|
||||
func AfterRequest(cmd ...JsCommand) *LifeCycle {
|
||||
return NewLifeCycle().AfterRequest(cmd...)
|
||||
}
|
||||
|
||||
func OnMutationError(cmd ...JsCommand) *LifeCycle {
|
||||
return NewLifeCycle().OnMutationError(cmd...)
|
||||
}
|
||||
|
||||
func (l *LifeCycle) AfterRequest(cmd ...JsCommand) *LifeCycle {
|
||||
l.afterRequest = append(l.afterRequest, cmd...)
|
||||
l.OnEvent(HxAfterRequest, cmd...)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *LifeCycle) OnMutationError(cmd ...JsCommand) *LifeCycle {
|
||||
l.onMutationError = append(l.onMutationError, cmd...)
|
||||
l.OnEvent(HxOnMutationError, cmd...)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *LifeCycle) Render() *Node {
|
||||
beforeRequest := ""
|
||||
afterReqest := ""
|
||||
onMutationError := ""
|
||||
for _, command := range l.beforeRequest {
|
||||
beforeRequest += fmt.Sprintf("%s;", command.Command)
|
||||
}
|
||||
for _, command := range l.afterRequest {
|
||||
afterReqest += fmt.Sprintf("%s;", command.Command)
|
||||
}
|
||||
for _, command := range l.onMutationError {
|
||||
onMutationError += fmt.Sprintf("%s;", command.Command)
|
||||
m := make(map[string]string)
|
||||
|
||||
for event, commands := range l.handlers {
|
||||
m[event] = ""
|
||||
for _, command := range commands {
|
||||
m[event] += fmt.Sprintf("%s;", command.Command)
|
||||
}
|
||||
}
|
||||
|
||||
return Children(
|
||||
If(beforeRequest != "", Attribute(HxBeforeRequest, beforeRequest)),
|
||||
If(afterReqest != "", Attribute(HxAfterRequest, afterReqest)),
|
||||
If(onMutationError != "", Attribute(HxOnMutationError, onMutationError)),
|
||||
).Render()
|
||||
children := make([]Renderable, 0)
|
||||
|
||||
for event, js := range m {
|
||||
children = append(children, Attribute(event, js))
|
||||
}
|
||||
|
||||
return Children(children...).Render()
|
||||
}
|
||||
|
||||
type JsCommand struct {
|
||||
|
|
@ -66,10 +80,30 @@ func SetText(text string) JsCommand {
|
|||
return JsCommand{Command: fmt.Sprintf("this.innerText = '%s'", text)}
|
||||
}
|
||||
|
||||
func Increment(amount int) JsCommand {
|
||||
return JsCommand{Command: fmt.Sprintf("this.innerText = parseInt(this.innerText) + %d", amount)}
|
||||
}
|
||||
|
||||
func SetInnerHtml(r Renderable) JsCommand {
|
||||
return JsCommand{Command: fmt.Sprintf("this.innerHTML = `%s`", Render(r.Render()))}
|
||||
}
|
||||
|
||||
func SetOuterHtml(r Renderable) JsCommand {
|
||||
return JsCommand{Command: fmt.Sprintf("this.outerHTML = `%s`", Render(r.Render()))}
|
||||
}
|
||||
|
||||
func AddAttribute(name, value string) JsCommand {
|
||||
return JsCommand{Command: fmt.Sprintf("this.setAttribute('%s', '%s')", name, value)}
|
||||
}
|
||||
|
||||
func SetDisabled(disabled bool) JsCommand {
|
||||
if disabled {
|
||||
return AddAttribute("disabled", "true")
|
||||
} else {
|
||||
return RemoveAttribute("disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveAttribute(name string) JsCommand {
|
||||
return JsCommand{Command: fmt.Sprintf("this.removeAttribute('%s')", name)}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
// Package pages THIS FILE IS GENERATED. DO NOT EDIT.
|
||||
package pages
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
import "github.com/maddalax/htmgo/framework/h"
|
||||
|
||||
func RegisterPages(f *fiber.App) {
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module github.com/maddalax/htmgo/framework/tooling/astgen
|
||||
|
||||
go 1.23.0
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
module github.com/maddalax/htmgo/framework/tooling/copyassets
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require golang.org/x/mod v0.21.0
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module github.com/maddalax/htmgo/framework/tooling/downloadtemplate
|
||||
|
||||
go 1.23.0
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module github.com/maddalax/htmgo/framework/tooling/htmgo
|
||||
|
||||
go 1.23.0
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed Taskfile.yml
|
||||
var taskFile string
|
||||
|
||||
func main() {
|
||||
commandMap := make(map[string]*flag.FlagSet)
|
||||
commands := []string{"template", "run", "watch", "build", "setup", "css", "css-watch", "ast-watch", "go-watch"}
|
||||
|
||||
for _, command := range commands {
|
||||
commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError)
|
||||
}
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | ")))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c := commandMap[os.Args[1]]
|
||||
|
||||
if c == nil {
|
||||
fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | ")))
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
// Install the latest version of Task
|
||||
install := exec.Command("go", "install", "github.com/go-task/task/v3/cmd/task@latest")
|
||||
|
||||
err = install.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Error installing task: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
temp, err := os.CreateTemp("", "Taskfile.yml")
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating temporary file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
os.WriteFile(temp.Name(), []byte(taskFile), 0644)
|
||||
|
||||
taskName := os.Args[1]
|
||||
|
||||
if taskName == "watch" {
|
||||
tasks := []string{"css-watch", "ast-watch", "go-watch"}
|
||||
wg := sync.WaitGroup{}
|
||||
for _, task := range tasks {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
cmd := exec.Command("task", "-t", temp.Name(), task)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Error running task command: %v\n", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
} else {
|
||||
// Define the command and arguments
|
||||
cmd := exec.Command("task", "-t", temp.Name(), os.Args[1])
|
||||
// Set the standard output and error to be the same as the Go program
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
// Run the command
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Error running task command: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
once := false
|
||||
if len(os.Args) > 1 {
|
||||
once = os.Args[1] == "--once"
|
||||
}
|
||||
|
||||
command := ""
|
||||
for i, arg := range os.Args {
|
||||
if arg == "--command" {
|
||||
command = os.Args[i+1]
|
||||
}
|
||||
}
|
||||
|
||||
if command == "" {
|
||||
panic("command is required")
|
||||
}
|
||||
|
||||
if once {
|
||||
runCommand(command)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("Recovered from fatal error:", r)
|
||||
// You can log the error here or take other corrective actions
|
||||
}
|
||||
}()
|
||||
|
||||
runCommand(command)
|
||||
// Create new watcher.
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
// Start listening for events.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(event.Name, "generated.go") {
|
||||
continue
|
||||
}
|
||||
|
||||
if event.Has(fsnotify.Write) {
|
||||
success := runCommand(command)
|
||||
if success {
|
||||
log.Println(fmt.Sprintf("file changed. code generation successful"))
|
||||
} else {
|
||||
log.Println(fmt.Sprintf("file changed. code generation failed"))
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
pagesDir := filepath.Join(cwd, "pages")
|
||||
partialsDir := filepath.Join(cwd, "partials")
|
||||
|
||||
toWatch := []string{pagesDir, partialsDir}
|
||||
|
||||
for _, watch := range toWatch {
|
||||
err = watcher.Add(watch)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Block main goroutine forever.
|
||||
<-make(chan struct{})
|
||||
}
|
||||
|
||||
func runCommand(command string) bool {
|
||||
// Create a new command
|
||||
cmd := exec.Command("bash", "-c", command)
|
||||
|
||||
// Capture stdout and stderr in buffers
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Run the command
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Println(fmt.Sprintf("error: %v", err))
|
||||
println(stderr.String())
|
||||
return false
|
||||
} else {
|
||||
println(out.String())
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,9 @@ func main() {
|
|||
now := time.Now()
|
||||
err := ctx.Next()
|
||||
duration := time.Since(now)
|
||||
ctx.Set("X-Response-Time", duration.String())
|
||||
ctx.Set("X-Response-Times", duration.String())
|
||||
// Log or print the request method, URL, and duration
|
||||
log.Printf("Request: %s %s took %dms", ctx.Method(), ctx.OriginalURL(), duration.Milliseconds())
|
||||
log.Printf("Requests: %s %s took %dms", ctx.Method(), ctx.OriginalURL(), duration.Milliseconds())
|
||||
return err
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -8,30 +8,38 @@ import (
|
|||
|
||||
func IndexPage(c *fiber.Ctx) *h.Page {
|
||||
return h.NewPage(h.Html(
|
||||
h.Class("bg-slate-400 flex flex-col items-center h-full w-full"),
|
||||
h.Class("bg-blue-400 flex flex-col items-center h-full w-full"),
|
||||
h.Head(
|
||||
h.Link("/public/main.css", "stylesheet"),
|
||||
h.Script("/public/htmgo.js"),
|
||||
),
|
||||
h.Body(
|
||||
h.Class("flex flex-col gap-3"),
|
||||
h.Class("flex flex-col gap-4"),
|
||||
h.Div(
|
||||
h.Class("flex flex-col items-center justify-center gap-6 p-12 text-center"),
|
||||
h.H1(
|
||||
h.Class("text-4xl sm:text-5xl font-bold max-w-3xl"),
|
||||
h.Text("Welcome to htmgo"),
|
||||
h.Text("Welcome to my fast!!"),
|
||||
),
|
||||
h.P(
|
||||
h.Class("text-lg sm:text-xl max-w-2xl"),
|
||||
h.Text("Combine the simplicity of Go with the power of HTMX for dynamic, JavaScript-light web development."),
|
||||
h.Class("text-lg sm:text-xl max-w-1xl"),
|
||||
),
|
||||
h.Div(
|
||||
h.Button(h.Class("btn bg-blue-500 p-4 rounded text-white"),
|
||||
h.Text("Click here to load a partial"),
|
||||
h.GetPartial(partials.SamplePartial),
|
||||
),
|
||||
Button(),
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func Button() h.Renderable {
|
||||
return h.Button(h.Class("btn bg-slate-500 p-4 rounded text-white"),
|
||||
h.Text("Click here use my ytes"),
|
||||
h.AfterRequest(
|
||||
h.SetDisabled(true),
|
||||
h.RemoveClass("bg-red-600"),
|
||||
h.AddClass("bg-gray-500"),
|
||||
),
|
||||
h.GetPartial(partials.SamplePartial),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ import (
|
|||
)
|
||||
|
||||
func SamplePartial(ctx *fiber.Ctx) *h.Partial {
|
||||
return h.NewPartial(h.Div(h.P(h.Text("This is a sample partials."))))
|
||||
return h.NewPartial(h.Div(h.P(h.Text(" asdas"))))
|
||||
}
|
||||
|
||||
func NewPartial(ctx *fiber.Ctx) *h.Partial {
|
||||
return h.NewPartial(h.Div(h.P(h.Text("This is a new pardtiasl."))))
|
||||
return h.NewPartial(h.Div(h.P(h.Text("This sadsl."))))
|
||||
}
|
||||
|
||||
func NewPartial2(ctx *fiber.Ctx) *h.Partial {
|
||||
return h.NewPartial(h.Div(h.P(h.Text("This sasdsadasdwl."))))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial {
|
|||
if path == "NewPartial" || path == "/starter-template/partials.NewPartial" {
|
||||
return partials.NewPartial(ctx)
|
||||
}
|
||||
if path == "NewPartial2" || path == "/starter-template/partials.NewPartial2" {
|
||||
return partials.NewPartial2(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue