mirror of
https://github.com/a-h/templ.git
synced 2025-02-06 10:03:16 +00:00
This commit is contained in:
parent
e16b2d88a4
commit
6afd6768ea
@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@ -25,7 +26,9 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
func NewGenerate(log *slog.Logger, args Arguments) (g *Generate) {
|
||||
const defaultWatchPattern = `(.+\.go$)|(.+\.templ$)|(.+_templ\.txt$)`
|
||||
|
||||
func NewGenerate(log *slog.Logger, args Arguments) (g *Generate, err error) {
|
||||
g = &Generate{
|
||||
Log: log,
|
||||
Args: &args,
|
||||
@ -33,12 +36,20 @@ func NewGenerate(log *slog.Logger, args Arguments) (g *Generate) {
|
||||
if g.Args.WorkerCount == 0 {
|
||||
g.Args.WorkerCount = runtime.NumCPU()
|
||||
}
|
||||
return g
|
||||
if g.Args.WatchPattern == "" {
|
||||
g.Args.WatchPattern = defaultWatchPattern
|
||||
}
|
||||
g.WatchPattern, err = regexp.Compile(g.Args.WatchPattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile watch pattern %q: %w", g.Args.WatchPattern, err)
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
type Generate struct {
|
||||
Log *slog.Logger
|
||||
Args *Arguments
|
||||
Log *slog.Logger
|
||||
Args *Arguments
|
||||
WatchPattern *regexp.Regexp
|
||||
}
|
||||
|
||||
type GenerationEvent struct {
|
||||
@ -143,7 +154,7 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
|
||||
slog.String("path", cmd.Args.Path),
|
||||
slog.Bool("devMode", cmd.Args.Watch),
|
||||
)
|
||||
if err := watcher.WalkFiles(ctx, cmd.Args.Path, events); err != nil {
|
||||
if err := watcher.WalkFiles(ctx, cmd.Args.Path, cmd.WatchPattern, events); err != nil {
|
||||
cmd.Log.Error("WalkFiles failed, exiting", slog.Any("error", err))
|
||||
errs <- FatalError{Err: fmt.Errorf("failed to walk files: %w", err)}
|
||||
return
|
||||
@ -153,7 +164,7 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
|
||||
return
|
||||
}
|
||||
cmd.Log.Info("Watching files")
|
||||
rw, err := watcher.Recursive(ctx, cmd.Args.Path, events, errs)
|
||||
rw, err := watcher.Recursive(ctx, cmd.Args.Path, cmd.WatchPattern, events, errs)
|
||||
if err != nil {
|
||||
cmd.Log.Error("Recursive watcher setup failed, exiting", slog.Any("error", err))
|
||||
errs <- FatalError{Err: fmt.Errorf("failed to setup recursive watcher: %w", err)}
|
||||
@ -187,7 +198,7 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
|
||||
cmd.Args.Lazy,
|
||||
)
|
||||
errorCount.Store(0)
|
||||
if err := watcher.WalkFiles(ctx, cmd.Args.Path, events); err != nil {
|
||||
if err := watcher.WalkFiles(ctx, cmd.Args.Path, cmd.WatchPattern, events); err != nil {
|
||||
cmd.Log.Error("Post dev mode WalkFiles failed", slog.Any("error", err))
|
||||
errs <- FatalError{Err: fmt.Errorf("failed to walk files: %w", err)}
|
||||
return
|
||||
|
@ -13,6 +13,7 @@ type Arguments struct {
|
||||
FileWriter FileWriterFunc
|
||||
Path string
|
||||
Watch bool
|
||||
WatchPattern string
|
||||
OpenBrowser bool
|
||||
Command string
|
||||
ProxyBind string
|
||||
@ -30,5 +31,9 @@ type Arguments struct {
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, log *slog.Logger, args Arguments) (err error) {
|
||||
return NewGenerate(log, args).Run(ctx)
|
||||
g, err := NewGenerate(log, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.Run(ctx)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/templ/cmd/templ/testproject"
|
||||
@ -42,3 +43,74 @@ func TestGenerate(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultWatchPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
matches bool
|
||||
}{
|
||||
{
|
||||
name: "empty file names do not match",
|
||||
input: "",
|
||||
matches: false,
|
||||
},
|
||||
{
|
||||
name: "*_templ.txt matches, Windows",
|
||||
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\strings_templ.txt`,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*_templ.txt matches, Unix",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/strings_templ.txt",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.templ files match, Windows",
|
||||
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\templates.templ`,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.templ files match, Unix",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/templates.templ",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*_templ.go files match, Windows",
|
||||
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\templates_templ.go`,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*_templ.go files match, Unix",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/templates_templ.go",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.go files match, Windows",
|
||||
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\templates.go`,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.go files match, Unix",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/templates.go",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.css files do not match",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/templates.css",
|
||||
matches: false,
|
||||
},
|
||||
}
|
||||
wpRegexp, err := regexp.Compile(defaultWatchPattern)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile default watch pattern: %v", err)
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if wpRegexp.MatchString(test.input) != test.matches {
|
||||
t.Fatalf("expected match of %q to be %v", test.input, test.matches)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
func Recursive(
|
||||
ctx context.Context,
|
||||
path string,
|
||||
watchPattern *regexp.Regexp,
|
||||
out chan fsnotify.Event,
|
||||
errors chan error,
|
||||
) (w *RecursiveWatcher, err error) {
|
||||
@ -23,20 +25,25 @@ func Recursive(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w = &RecursiveWatcher{
|
||||
ctx: ctx,
|
||||
w: fsnw,
|
||||
Events: out,
|
||||
Errors: errors,
|
||||
timers: make(map[timerKey]*time.Timer),
|
||||
}
|
||||
w = NewRecursiveWatcher(ctx, fsnw, watchPattern, out, errors)
|
||||
go w.loop()
|
||||
return w, w.Add(path)
|
||||
}
|
||||
|
||||
func NewRecursiveWatcher(ctx context.Context, w *fsnotify.Watcher, watchPattern *regexp.Regexp, events chan fsnotify.Event, errors chan error) *RecursiveWatcher {
|
||||
return &RecursiveWatcher{
|
||||
ctx: ctx,
|
||||
w: w,
|
||||
WatchPattern: watchPattern,
|
||||
Events: events,
|
||||
Errors: errors,
|
||||
timers: make(map[timerKey]*time.Timer),
|
||||
}
|
||||
}
|
||||
|
||||
// WalkFiles walks the file tree rooted at path, sending a Create event for each
|
||||
// file it encounters.
|
||||
func WalkFiles(ctx context.Context, path string, out chan fsnotify.Event) (err error) {
|
||||
func WalkFiles(ctx context.Context, path string, watchPattern *regexp.Regexp, out chan fsnotify.Event) (err error) {
|
||||
rootPath := path
|
||||
fileSystem := os.DirFS(rootPath)
|
||||
return fs.WalkDir(fileSystem, ".", func(path string, info os.DirEntry, err error) error {
|
||||
@ -50,7 +57,7 @@ func WalkFiles(ctx context.Context, path string, out chan fsnotify.Event) (err e
|
||||
if info.IsDir() && shouldSkipDir(absPath) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if !shouldIncludeFile(absPath) {
|
||||
if !watchPattern.MatchString(absPath) {
|
||||
return nil
|
||||
}
|
||||
out <- fsnotify.Event{
|
||||
@ -61,26 +68,14 @@ func WalkFiles(ctx context.Context, path string, out chan fsnotify.Event) (err e
|
||||
})
|
||||
}
|
||||
|
||||
func shouldIncludeFile(name string) bool {
|
||||
if strings.HasSuffix(name, ".templ") {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(name, "_templ.go") {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(name, "_templ.txt") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type RecursiveWatcher struct {
|
||||
ctx context.Context
|
||||
w *fsnotify.Watcher
|
||||
Events chan fsnotify.Event
|
||||
Errors chan error
|
||||
timerMu sync.Mutex
|
||||
timers map[timerKey]*time.Timer
|
||||
ctx context.Context
|
||||
w *fsnotify.Watcher
|
||||
WatchPattern *regexp.Regexp
|
||||
Events chan fsnotify.Event
|
||||
Errors chan error
|
||||
timerMu sync.Mutex
|
||||
timers map[timerKey]*time.Timer
|
||||
}
|
||||
|
||||
type timerKey struct {
|
||||
@ -114,7 +109,7 @@ func (w *RecursiveWatcher) loop() {
|
||||
}
|
||||
}
|
||||
// Only notify on templ related files.
|
||||
if !shouldIncludeFile(event.Name) {
|
||||
if !w.WatchPattern.MatchString(event.Name) {
|
||||
continue
|
||||
}
|
||||
tk := timerKeyFromEvent(event)
|
||||
|
@ -2,6 +2,8 @@ package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -10,14 +12,16 @@ import (
|
||||
|
||||
func TestWatchDebouncesDuplicates(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
rw := &RecursiveWatcher{
|
||||
ctx: ctx,
|
||||
w: &fsnotify.Watcher{
|
||||
Events: make(chan fsnotify.Event),
|
||||
},
|
||||
Events: make(chan fsnotify.Event, 2),
|
||||
timers: make(map[timerKey]*time.Timer),
|
||||
w := &fsnotify.Watcher{
|
||||
Events: make(chan fsnotify.Event),
|
||||
}
|
||||
events := make(chan fsnotify.Event, 2)
|
||||
errors := make(chan error)
|
||||
watchPattern, err := regexp.Compile(".*")
|
||||
if err != nil {
|
||||
t.Fatal(fmt.Errorf("failed to compile watch pattern: %w", err))
|
||||
}
|
||||
rw := NewRecursiveWatcher(ctx, w, watchPattern, events, errors)
|
||||
go func() {
|
||||
rw.w.Events <- fsnotify.Event{Name: "test.templ"}
|
||||
rw.w.Events <- fsnotify.Event{Name: "test.templ"}
|
||||
@ -60,14 +64,16 @@ func TestWatchDoesNotDebounceDifferentEvents(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
rw := &RecursiveWatcher{
|
||||
ctx: ctx,
|
||||
w: &fsnotify.Watcher{
|
||||
Events: make(chan fsnotify.Event),
|
||||
},
|
||||
Events: make(chan fsnotify.Event, 2),
|
||||
timers: make(map[timerKey]*time.Timer),
|
||||
w := &fsnotify.Watcher{
|
||||
Events: make(chan fsnotify.Event),
|
||||
}
|
||||
events := make(chan fsnotify.Event, 2)
|
||||
errors := make(chan error)
|
||||
watchPattern, err := regexp.Compile(".*")
|
||||
if err != nil {
|
||||
t.Fatal(fmt.Errorf("failed to compile watch pattern: %w", err))
|
||||
}
|
||||
rw := NewRecursiveWatcher(ctx, w, watchPattern, events, errors)
|
||||
go func() {
|
||||
rw.w.Events <- test.event1
|
||||
rw.w.Events <- test.event2
|
||||
@ -93,14 +99,16 @@ func TestWatchDoesNotDebounceDifferentEvents(t *testing.T) {
|
||||
|
||||
func TestWatchDoesNotDebounceSeparateEvents(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
rw := &RecursiveWatcher{
|
||||
ctx: ctx,
|
||||
w: &fsnotify.Watcher{
|
||||
Events: make(chan fsnotify.Event),
|
||||
},
|
||||
Events: make(chan fsnotify.Event, 2),
|
||||
timers: make(map[timerKey]*time.Timer),
|
||||
w := &fsnotify.Watcher{
|
||||
Events: make(chan fsnotify.Event),
|
||||
}
|
||||
events := make(chan fsnotify.Event, 2)
|
||||
errors := make(chan error)
|
||||
watchPattern, err := regexp.Compile(".*")
|
||||
if err != nil {
|
||||
t.Fatal(fmt.Errorf("failed to compile watch pattern: %w", err))
|
||||
}
|
||||
rw := NewRecursiveWatcher(ctx, w, watchPattern, events, errors)
|
||||
go func() {
|
||||
rw.w.Events <- fsnotify.Event{Name: "test.templ"}
|
||||
<-time.After(200 * time.Millisecond)
|
||||
|
@ -157,6 +157,8 @@ Args:
|
||||
Set to true to include the current time in the generated code.
|
||||
-watch
|
||||
Set to true to watch the path for changes and regenerate code.
|
||||
-watch-pattern <regexp>
|
||||
Set the regexp pattern of files that will be watched for changes. (default: '(.+\.go$)|(.+\.templ$)|(.+_templ\.txt$)')
|
||||
-cmd <cmd>
|
||||
Set the command to run after generating code.
|
||||
-proxy
|
||||
|
@ -25,5 +25,5 @@ func main() {
|
||||
}
|
||||
|
||||
// The current commit isn't the one we're about to commit.
|
||||
fmt.Printf("0.2.%d", count+1)
|
||||
fmt.Printf("0.3.%d", count+1)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user