diff --git a/cli/htmgo/go.mod b/cli/htmgo/go.mod index 6bad2bb..ba191b9 100644 --- a/cli/htmgo/go.mod +++ b/cli/htmgo/go.mod @@ -11,3 +11,5 @@ require ( golang.org/x/sys v0.25.0 golang.org/x/tools v0.25.0 ) + +require github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect diff --git a/cli/htmgo/go.sum b/cli/htmgo/go.sum index b8b03a7..87e7902 100644 --- a/cli/htmgo/go.sum +++ b/cli/htmgo/go.sum @@ -1,3 +1,5 @@ +github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= +github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= diff --git a/cli/htmgo/internal/dirutil/dir.go b/cli/htmgo/internal/dirutil/dir.go index 2715277..7063e83 100644 --- a/cli/htmgo/internal/dirutil/dir.go +++ b/cli/htmgo/internal/dirutil/dir.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/maddalax/htmgo/cli/htmgo/tasks/process" + "github.com/maddalax/htmgo/framework/config" "io" "log/slog" "os" @@ -17,6 +18,10 @@ func HasFileFromRoot(file string) bool { return err == nil } +func GetConfig() *config.ProjectConfig { + return config.FromConfigFile(process.GetWorkingDir()) +} + func CreateHtmgoDir() { if !HasFileFromRoot("__htmgo") { CreateDirFromRoot("__htmgo") diff --git a/cli/htmgo/internal/dirutil/glob.go b/cli/htmgo/internal/dirutil/glob.go new file mode 100644 index 0000000..1315c66 --- /dev/null +++ b/cli/htmgo/internal/dirutil/glob.go @@ -0,0 +1,31 @@ +package dirutil + +import ( + "fmt" + "github.com/bmatcuk/doublestar/v4" +) + +func matchesAny(patterns []string, path string) bool { + for _, pattern := range patterns { + matched, err := doublestar.Match(pattern, path) + if err != nil { + fmt.Printf("Error matching pattern: %v\n", err) + return false + } + if matched { + return true + } + } + return false +} + +func IsGlobExclude(path string, excludePatterns []string) bool { + return matchesAny(excludePatterns, path) +} + +func IsGlobMatch(path string, patterns []string, excludePatterns []string) bool { + if matchesAny(excludePatterns, path) { + return false + } + return matchesAny(patterns, path) +} diff --git a/cli/htmgo/tasks/copyassets/bundle.go b/cli/htmgo/tasks/copyassets/bundle.go index a24da76..f0f7699 100644 --- a/cli/htmgo/tasks/copyassets/bundle.go +++ b/cli/htmgo/tasks/copyassets/bundle.go @@ -92,7 +92,7 @@ func CopyAssets() { }) } - if !dirutil.HasFileFromRoot("tailwind.config.js") { + if dirutil.GetConfig().Tailwind && !dirutil.HasFileFromRoot("tailwind.config.js") { err = dirutil.CopyFile( filepath.Join(assetCssDir, "tailwind.config.js"), filepath.Join(process.GetWorkingDir(), "tailwind.config.js"), diff --git a/cli/htmgo/tasks/css/css.go b/cli/htmgo/tasks/css/css.go index 0bdaf58..1c07ec2 100644 --- a/cli/htmgo/tasks/css/css.go +++ b/cli/htmgo/tasks/css/css.go @@ -12,7 +12,7 @@ import ( ) func IsTailwindEnabled() bool { - return dirutil.HasFileFromRoot("tailwind.config.js") + return dirutil.GetConfig().Tailwind && dirutil.HasFileFromRoot("tailwind.config.js") } func Setup() bool { diff --git a/cli/htmgo/watcher.go b/cli/htmgo/watcher.go index 0509b3d..4838d14 100644 --- a/cli/htmgo/watcher.go +++ b/cli/htmgo/watcher.go @@ -4,6 +4,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/google/uuid" "github.com/maddalax/htmgo/cli/htmgo/internal" + "github.com/maddalax/htmgo/cli/htmgo/internal/dirutil" "github.com/maddalax/htmgo/cli/htmgo/tasks/module" "log" "log/slog" @@ -13,11 +14,10 @@ import ( "time" ) -var ignoredDirs = []string{".git", ".idea", "node_modules", "vendor"} - func startWatcher(cb func(version string, file []*fsnotify.Event)) { events := make([]*fsnotify.Event, 0) debouncer := internal.NewDebouncer(500 * time.Millisecond) + config := dirutil.GetConfig() defer func() { if r := recover(); r != nil { @@ -35,23 +35,22 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) { for { select { case event, ok := <-watcher.Events: - slog.Debug("event:", slog.String("name", event.Name), slog.String("op", event.Op.String())) - if !ok { return } if event.Has(fsnotify.Remove) { - info, err := os.Stat(event.Name) - if err != nil { + if dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) { + watcher.Remove(event.Name) continue } - if info.IsDir() { - _ = watcher.Remove(event.Name) - } } if event.Has(fsnotify.Create) { + if dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) { + watcher.Add(event.Name) + continue + } info, err := os.Stat(event.Name) if err != nil { slog.Error("Error getting file info:", slog.String("path", event.Name), slog.String("error", err.Error())) @@ -68,6 +67,9 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) { } if event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) { + if !dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) { + continue + } events = append(events, &event) debouncer.Do(func() { seen := make(map[string]bool) @@ -82,6 +84,7 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) { events = make([]*fsnotify.Event, 0) }) } + case err, ok := <-watcher.Errors: if !ok { return @@ -107,11 +110,10 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) { return err } // Ignore directories in the ignoredDirs list - for _, ignoredDir := range ignoredDirs { - if ignoredDir == info.Name() { - return filepath.SkipDir - } + if dirutil.IsGlobExclude(path, config.WatchIgnore) { + return filepath.SkipDir } + // Only watch directories if info.IsDir() { err = watcher.Add(path) @@ -123,6 +125,7 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) { } return nil }) + if err != nil { log.Fatal(err) } diff --git a/framework/config/project.go b/framework/config/project.go new file mode 100644 index 0000000..72e222d --- /dev/null +++ b/framework/config/project.go @@ -0,0 +1,58 @@ +package config + +import ( + "gopkg.in/yaml.v3" + "log/slog" + "os" + "path" +) + +type ProjectConfig struct { + Tailwind bool `yaml:"tailwind"` + WatchIgnore []string `yaml:"watch_ignore"` + WatchFiles []string `yaml:"watch_files"` +} + +func DefaultProjectConfig() *ProjectConfig { + return &ProjectConfig{ + Tailwind: true, + WatchIgnore: []string{ + "node_modules", ".git", ".idea", "assets/dist", + }, + WatchFiles: []string{ + "**/*.go", "**/*.html", "**/*.css", "**/*.js", "**/*.json", "**/*.yaml", "**/*.yml", "**/*.md", + }, + } +} + +func (cfg *ProjectConfig) EnhanceWithDefaults() *ProjectConfig { + defaultCfg := DefaultProjectConfig() + if len(cfg.WatchFiles) == 0 { + cfg.WatchFiles = defaultCfg.WatchFiles + } + if len(cfg.WatchIgnore) == 0 { + cfg.WatchIgnore = defaultCfg.WatchIgnore + } + return cfg +} + +func FromConfigFile(workingDir string) *ProjectConfig { + defaultCfg := DefaultProjectConfig() + names := []string{"htmgo.yaml", "htmgo.yml", "_htmgo.yaml", "_htmgo.yml"} + for _, name := range names { + filePath := path.Join(workingDir, name) + if _, err := os.Stat(filePath); err == nil { + cfg := &ProjectConfig{} + bytes, err := os.ReadFile(filePath) + if err == nil { + err = yaml.Unmarshal(bytes, cfg) + if err != nil { + slog.Error("Error parsing config file", slog.String("file", filePath), slog.String("error", err.Error())) + os.Exit(1) + } + return cfg.EnhanceWithDefaults() + } + } + } + return defaultCfg +} diff --git a/framework/config/project_test.go b/framework/config/project_test.go new file mode 100644 index 0000000..f015635 --- /dev/null +++ b/framework/config/project_test.go @@ -0,0 +1,50 @@ +package config + +import ( + "github.com/stretchr/testify/assert" + "os" + "path" + "testing" +) + +func TestDefaultProjectConfig(t *testing.T) { + t.Parallel() + cfg := DefaultProjectConfig() + assert.Equal(t, true, cfg.Tailwind) + assert.Equal(t, 4, len(cfg.WatchIgnore)) + assert.Equal(t, 8, len(cfg.WatchFiles)) +} + +func TestNoConfigFileUsesDefault(t *testing.T) { + t.Parallel() + cfg := FromConfigFile("non-existing-dir") + assert.Equal(t, true, cfg.Tailwind) + assert.Equal(t, 4, len(cfg.WatchIgnore)) + assert.Equal(t, 8, len(cfg.WatchFiles)) +} + +func TestPartialConfigMerges(t *testing.T) { + t.Parallel() + dir := writeConfigFile(t, "tailwind: false") + cfg := FromConfigFile(dir) + assert.Equal(t, false, cfg.Tailwind) + assert.Equal(t, 4, len(cfg.WatchIgnore)) + assert.Equal(t, 8, len(cfg.WatchFiles)) +} + +func TestShouldNotSetTailwindTrue(t *testing.T) { + t.Parallel() + dir := writeConfigFile(t, "someValue: true") + cfg := FromConfigFile(dir) + assert.Equal(t, false, cfg.Tailwind) + assert.Equal(t, 4, len(cfg.WatchIgnore)) + assert.Equal(t, 8, len(cfg.WatchFiles)) +} + +func writeConfigFile(t *testing.T, content string) string { + temp := os.TempDir() + os.Mkdir(temp, 0755) + err := os.WriteFile(path.Join(temp, "htmgo.yml"), []byte(content), 0644) + assert.Nil(t, err) + return temp +} diff --git a/htmgo-site/htmgo.yml b/htmgo-site/htmgo.yml new file mode 100644 index 0000000..d60d2ff --- /dev/null +++ b/htmgo-site/htmgo.yml @@ -0,0 +1,10 @@ +# htmgo configuration + +# if tailwindcss is enabled, htmgo will automatically compile your tailwind and output it to assets/dist +tailwind: true + +# which directories to ignore when watching for changes, supports glob patterns through https://github.com/bmatcuk/doublestar +watch_ignore: [".git", "node_modules", "dist/*"] + +# files to watch for changes, supports glob patterns through https://github.com/bmatcuk/doublestar +watch_files: ["**/*.go", "**/*.css", "**/*.md"] diff --git a/htmgo-site/pages/base/root.go b/htmgo-site/pages/base/root.go index c15900b..b110caa 100644 --- a/htmgo-site/pages/base/root.go +++ b/htmgo-site/pages/base/root.go @@ -45,7 +45,8 @@ func PageWithNav(ctx *h.RequestContext, children ...h.Ren) *h.Element { return RootPage(ctx, h.Fragment( partials.NavBar(ctx, partials.NavBarProps{ - Expanded: false, + Expanded: false, + ShowPreRelease: true, }), h.Div( children..., diff --git a/templates/starter/htmgo.yml b/templates/starter/htmgo.yml new file mode 100644 index 0000000..d60d2ff --- /dev/null +++ b/templates/starter/htmgo.yml @@ -0,0 +1,10 @@ +# htmgo configuration + +# if tailwindcss is enabled, htmgo will automatically compile your tailwind and output it to assets/dist +tailwind: true + +# which directories to ignore when watching for changes, supports glob patterns through https://github.com/bmatcuk/doublestar +watch_ignore: [".git", "node_modules", "dist/*"] + +# files to watch for changes, supports glob patterns through https://github.com/bmatcuk/doublestar +watch_files: ["**/*.go", "**/*.css", "**/*.md"]