Skip to content

Commit c2f3317

Browse files
patrickkdeva-h
andauthored
feat: add --ignore-pattern flag to generate -watch (#1280)
Co-authored-by: Adrian Hesketh <inbox@adrianhesketh.com>
1 parent 028f398 commit c2f3317

File tree

4 files changed

+112
-21
lines changed

4 files changed

+112
-21
lines changed

cmd/templ/generatecmd/cmd.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ func (cmd Generate) handleEvents(ctx context.Context, events chan fsnotify.Event
296296

297297
func (cmd *Generate) walkAndWatch(ctx context.Context, events chan fsnotify.Event, errs chan error) {
298298
cmd.Log.Debug("Walking directory", slog.String("path", cmd.Args.Path), slog.Bool("devMode", cmd.Args.Watch))
299-
if err := watcher.WalkFiles(ctx, cmd.Args.Path, cmd.Args.WatchPattern, events); err != nil {
299+
if err := watcher.WalkFiles(ctx, cmd.Args.Path, cmd.Args.WatchPattern, cmd.Args.IgnorePattern, events); err != nil {
300300
cmd.Log.Error("WalkFiles failed, exiting", slog.Any("error", err))
301301
errs <- FatalError{Err: fmt.Errorf("failed to walk files: %w", err)}
302302
return
@@ -306,7 +306,7 @@ func (cmd *Generate) walkAndWatch(ctx context.Context, events chan fsnotify.Even
306306
return
307307
}
308308
cmd.Log.Info("Watching files")
309-
rw, err := watcher.Recursive(ctx, cmd.Args.WatchPattern, events, errs)
309+
rw, err := watcher.Recursive(ctx, cmd.Args.WatchPattern, cmd.Args.IgnorePattern, events, errs)
310310
if err != nil {
311311
cmd.Log.Error("Recursive watcher setup failed, exiting", slog.Any("error", err))
312312
errs <- FatalError{Err: fmt.Errorf("failed to setup recursive watcher: %w", err)}

cmd/templ/generatecmd/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Args:
3737
Set to true to watch the path for changes and regenerate code.
3838
-watch-pattern <regexp>
3939
Set the regexp pattern of files that will be watched for changes. (default: '(.+\.go$)|(.+\.templ$)|(.+_templ\.txt$)')
40+
-ignore-pattern <regexp>
41+
Set the regexp pattern of files to ignore when watching for changes. (default: '')
4042
-cmd <cmd>
4143
Set the command to run after generating code.
4244
-proxy
@@ -89,6 +91,7 @@ func NewArguments(stdout, stderr io.Writer, args []string) (cmdArgs Arguments, l
8991
cmd.BoolVar(&cmdArgs.IncludeTimestamp, "include-timestamp", false, "")
9092
cmd.BoolVar(&cmdArgs.Watch, "watch", false, "")
9193
watchPatternFlag := cmd.String("watch-pattern", defaultWatchPattern, "")
94+
ignorePatternFlag := cmd.String("ignore-pattern", "", "")
9295
cmd.BoolVar(&cmdArgs.OpenBrowser, "open-browser", true, "")
9396
cmd.StringVar(&cmdArgs.Command, "cmd", "", "")
9497
cmd.StringVar(&cmdArgs.Proxy, "proxy", "", "")
@@ -115,6 +118,12 @@ func NewArguments(stdout, stderr io.Writer, args []string) (cmdArgs Arguments, l
115118
if err != nil {
116119
return cmdArgs, log, *helpFlag, fmt.Errorf("invalid watch pattern %q: %w", *watchPatternFlag, err)
117120
}
121+
if *ignorePatternFlag != "" {
122+
cmdArgs.IgnorePattern, err = regexp.Compile(*ignorePatternFlag)
123+
if err != nil {
124+
return cmdArgs, log, *helpFlag, fmt.Errorf("invalid ignore pattern %q: %w", *ignorePatternFlag, err)
125+
}
126+
}
118127

119128
// Default to writing to files unless the stdout flag is set.
120129
cmdArgs.FileWriter = FileWriter
@@ -134,6 +143,7 @@ type Arguments struct {
134143
Path string
135144
Watch bool
136145
WatchPattern *regexp.Regexp
146+
IgnorePattern *regexp.Regexp
137147
OpenBrowser bool
138148
Command string
139149
ProxyBind string

cmd/templ/generatecmd/watcher/watch.go

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
func Recursive(
1717
ctx context.Context,
1818
watchPattern *regexp.Regexp,
19+
ignorePattern *regexp.Regexp,
1920
out chan fsnotify.Event,
2021
errors chan error,
2122
) (w *RecursiveWatcher, err error) {
@@ -24,13 +25,14 @@ func Recursive(
2425
return nil, err
2526
}
2627
w = &RecursiveWatcher{
27-
ctx: ctx,
28-
w: fsnw,
29-
WatchPattern: watchPattern,
30-
Events: out,
31-
Errors: errors,
32-
timers: make(map[timerKey]*time.Timer),
33-
loopComplete: sync.WaitGroup{},
28+
ctx: ctx,
29+
w: fsnw,
30+
WatchPattern: watchPattern,
31+
IgnorePattern: ignorePattern,
32+
Events: out,
33+
Errors: errors,
34+
timers: make(map[timerKey]*time.Timer),
35+
loopComplete: sync.WaitGroup{},
3436
}
3537
w.loopComplete.Add(1)
3638
go func() {
@@ -42,7 +44,7 @@ func Recursive(
4244

4345
// WalkFiles walks the file tree rooted at path, sending a Create event for each
4446
// file it encounters.
45-
func WalkFiles(ctx context.Context, rootPath string, watchPattern *regexp.Regexp, out chan fsnotify.Event) (err error) {
47+
func WalkFiles(ctx context.Context, rootPath string, watchPattern, ignorePattern *regexp.Regexp, out chan fsnotify.Event) (err error) {
4648
return fs.WalkDir(os.DirFS(rootPath), ".", func(path string, info os.DirEntry, err error) error {
4749
if err != nil {
4850
return nil
@@ -57,6 +59,9 @@ func WalkFiles(ctx context.Context, rootPath string, watchPattern *regexp.Regexp
5759
if !watchPattern.MatchString(absPath) {
5860
return nil
5961
}
62+
if ignorePattern != nil && ignorePattern.MatchString(absPath) {
63+
return nil
64+
}
6065
out <- fsnotify.Event{
6166
Name: absPath,
6267
Op: fsnotify.Create,
@@ -66,14 +71,15 @@ func WalkFiles(ctx context.Context, rootPath string, watchPattern *regexp.Regexp
6671
}
6772

6873
type RecursiveWatcher struct {
69-
ctx context.Context
70-
w *fsnotify.Watcher
71-
WatchPattern *regexp.Regexp
72-
Events chan fsnotify.Event
73-
Errors chan error
74-
timerMu sync.Mutex
75-
timers map[timerKey]*time.Timer
76-
loopComplete sync.WaitGroup
74+
ctx context.Context
75+
w *fsnotify.Watcher
76+
WatchPattern *regexp.Regexp
77+
IgnorePattern *regexp.Regexp
78+
Events chan fsnotify.Event
79+
Errors chan error
80+
timerMu sync.Mutex
81+
timers map[timerKey]*time.Timer
82+
loopComplete sync.WaitGroup
7783
}
7884

7985
type timerKey struct {
@@ -114,6 +120,10 @@ func (w *RecursiveWatcher) loop() {
114120
if !w.WatchPattern.MatchString(event.Name) {
115121
continue
116122
}
123+
// Skip files that match the ignore pattern.
124+
if w.IgnorePattern != nil && w.IgnorePattern.MatchString(event.Name) {
125+
continue
126+
}
117127
tk := timerKeyFromEvent(event)
118128
w.timerMu.Lock()
119129
t, ok := w.timers[tk]

cmd/templ/generatecmd/watcher/watch_test.go

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestWatchDebouncesDuplicates(t *testing.T) {
1818
if err != nil {
1919
t.Fatal(fmt.Errorf("failed to compile watch pattern: %w", err))
2020
}
21-
rw, err := Recursive(ctx, watchPattern, events, errors)
21+
rw, err := Recursive(ctx, watchPattern, nil, events, errors)
2222
if err != nil {
2323
t.Fatal(fmt.Errorf("failed to create recursive watcher: %w", err))
2424
}
@@ -71,7 +71,7 @@ func TestWatchDoesNotDebounceDifferentEvents(t *testing.T) {
7171
if err != nil {
7272
t.Fatal(fmt.Errorf("failed to compile watch pattern: %w", err))
7373
}
74-
rw, err := Recursive(ctx, watchPattern, events, errors)
74+
rw, err := Recursive(ctx, watchPattern, nil, events, errors)
7575
if err != nil {
7676
t.Fatal(fmt.Errorf("failed to create recursive watcher: %w", err))
7777
}
@@ -107,7 +107,7 @@ func TestWatchDoesNotDebounceSeparateEvents(t *testing.T) {
107107
if err != nil {
108108
t.Fatal(fmt.Errorf("failed to compile watch pattern: %w", err))
109109
}
110-
rw, err := Recursive(ctx, watchPattern, events, errors)
110+
rw, err := Recursive(ctx, watchPattern, nil, events, errors)
111111
if err != nil {
112112
t.Fatal(fmt.Errorf("failed to create recursive watcher: %w", err))
113113
}
@@ -134,3 +134,74 @@ func TestWatchDoesNotDebounceSeparateEvents(t *testing.T) {
134134
}
135135
}
136136
}
137+
138+
func TestWatchIgnoresFilesMatchingIgnorePattern(t *testing.T) {
139+
ctx, cancel := context.WithCancel(context.Background())
140+
events := make(chan fsnotify.Event, 2)
141+
errors := make(chan error)
142+
watchPattern, err := regexp.Compile(".*")
143+
if err != nil {
144+
t.Fatal(fmt.Errorf("failed to compile watch pattern: %w", err))
145+
}
146+
ignorePattern, err := regexp.Compile(`ignore\.templ$`)
147+
if err != nil {
148+
t.Fatal(fmt.Errorf("failed to compile ignore pattern: %w", err))
149+
}
150+
151+
rw, err := Recursive(ctx, watchPattern, ignorePattern, events, errors)
152+
if err != nil {
153+
t.Fatal(fmt.Errorf("failed to create recursive watcher: %w", err))
154+
}
155+
156+
go func() {
157+
// This should be ignored
158+
rw.w.Events <- fsnotify.Event{Name: "file.ignore.templ"}
159+
// This should pass
160+
rw.w.Events <- fsnotify.Event{Name: "file.keep.templ"}
161+
}()
162+
163+
count := 0
164+
exp := time.After(300 * time.Millisecond)
165+
for {
166+
select {
167+
case <-rw.Events:
168+
count++
169+
case <-exp:
170+
if count != 1 {
171+
t.Errorf("expected 1 event, got %d", count)
172+
}
173+
cancel()
174+
if err := rw.Close(); err != nil {
175+
t.Errorf("unexpected error closing watcher: %v", err)
176+
}
177+
return
178+
}
179+
}
180+
}
181+
182+
func TestIgnorePatternTakesPrecedenceOverWatchPattern(t *testing.T) {
183+
ctx, cancel := context.WithCancel(context.Background())
184+
defer cancel()
185+
events := make(chan fsnotify.Event, 2)
186+
errors := make(chan error)
187+
watchPattern := regexp.MustCompile(`.*\.templ$`)
188+
ignorePattern := regexp.MustCompile(`.*\.ignore\.templ$`)
189+
190+
rw, err := Recursive(ctx, watchPattern, ignorePattern, events, errors)
191+
if err != nil {
192+
t.Fatal(err)
193+
}
194+
195+
go func() {
196+
rw.w.Events <- fsnotify.Event{Name: "file.ignore.templ"}
197+
}()
198+
199+
exp := time.After(300 * time.Millisecond)
200+
select {
201+
case <-rw.Events:
202+
t.Errorf("expected no events because ignore should win")
203+
case <-exp:
204+
cancel()
205+
_ = rw.Close()
206+
}
207+
}

0 commit comments

Comments
 (0)