dev.go
121 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package frontend
import (
"log"
"os"
"path/filepath"
"time"
"github.com/fsnotify/fsnotify"
)
// Dev starts the development server with file watching and HMR.
func (f *Frontend) Dev() error {
if !f.DevMode {
return nil
}
// Create watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
// Watch source directory recursively
if err := f.watchDir(watcher, f.SourceDir); err != nil {
// If source doesn't exist, just return without watching
if os.IsNotExist(err) {
log.Printf("[frontend] No source directory at %s, skipping watch", f.SourceDir)
return nil
}
return err
}
log.Printf("[frontend] Watching %s for changes", f.SourceDir)
// Store watcher for cleanup
f.watcher = watcher
// Start watching in background
go f.watchLoop(watcher)
return nil
}
// watchDir adds a directory and all subdirectories to the watcher.
func (f *Frontend) watchDir(watcher *fsnotify.Watcher, dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return watcher.Add(path)
}
return nil
})
}
// watchLoop handles file system events and triggers rebuilds.
func (f *Frontend) watchLoop(watcher *fsnotify.Watcher) {
// Debounce timer to avoid rapid rebuilds
var timer *time.Timer
debounce := 100 * time.Millisecond
rebuild := func() {
log.Printf("[frontend] Rebuilding components...")
start := time.Now()
if err := f.Build(); err != nil {
log.Printf("[frontend] Build error: %v", err)
return
}
log.Printf("[frontend] Build completed in %v", time.Since(start))
f.notifyClients()
}
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// Only rebuild on write/create/remove events
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) {
// Check if it's a relevant file
ext := filepath.Ext(event.Name)
switch ext {
case ".tsx", ".ts", ".jsx", ".js", ".css":
// Debounce rebuild
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(debounce, rebuild)
}
}
// Watch new directories
if event.Has(fsnotify.Create) {
if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
watcher.Add(event.Name)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Printf("[frontend] Watcher error: %v", err)
}
}
}
// Stop stops the development server and file watcher.
func (f *Frontend) Stop() {
if f.watcher != nil {
f.watcher.Close()
f.watcher = nil
}
close(f.hmrDone) // shuts down hub goroutine, closes all client channels
}