package watcher import ( "log" "os" "path/filepath" "strings" "sync" "time" "github.com/fsnotify/fsnotify" ) type FileType string const ( FileTypeStrata FileType = ".strata" FileTypeSTS FileType = ".sts" FileTypeSCSS FileType = ".scss" FileTypeTS FileType = ".ts" FileTypeConfig FileType = "strataconfig.ts" ) type ChangeEvent struct { Path string Type FileType Op fsnotify.Op IsConfig bool } type Watcher struct { watcher *fsnotify.Watcher srcDir string onChange func(ChangeEvent) debounce time.Duration pending map[string]ChangeEvent mu sync.Mutex timer *time.Timer stopCh chan struct{} } func New(srcDir string, onChange func(ChangeEvent)) (*Watcher, error) { fsWatcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } w := &Watcher{ watcher: fsWatcher, srcDir: srcDir, onChange: onChange, debounce: 50 * time.Millisecond, pending: make(map[string]ChangeEvent), stopCh: make(chan struct{}), } return w, nil } func (w *Watcher) Start() error { // Add src directory recursively err := filepath.Walk(w.srcDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return w.watcher.Add(path) } return nil }) if err != nil { return err } // Watch strataconfig.ts in root configPath := filepath.Join(filepath.Dir(w.srcDir), "strataconfig.ts") if _, err := os.Stat(configPath); err == nil { w.watcher.Add(filepath.Dir(configPath)) } go w.run() return nil } func (w *Watcher) run() { for { select { case event, ok := <-w.watcher.Events: if !ok { return } w.handleEvent(event) case err, ok := <-w.watcher.Errors: if !ok { return } log.Printf("Watcher error: %v", err) case <-w.stopCh: return } } } func (w *Watcher) handleEvent(event fsnotify.Event) { // Skip non-relevant events if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove) == 0 { return } // Determine file type fileType := w.getFileType(event.Name) if fileType == "" { return } isConfig := strings.HasSuffix(event.Name, "strataconfig.ts") change := ChangeEvent{ Path: event.Name, Type: fileType, Op: event.Op, IsConfig: isConfig, } w.mu.Lock() w.pending[event.Name] = change // Reset debounce timer if w.timer != nil { w.timer.Stop() } w.timer = time.AfterFunc(w.debounce, w.flush) w.mu.Unlock() } func (w *Watcher) flush() { w.mu.Lock() pending := w.pending w.pending = make(map[string]ChangeEvent) w.mu.Unlock() for _, event := range pending { w.onChange(event) } } func (w *Watcher) getFileType(path string) FileType { ext := filepath.Ext(path) switch ext { case ".strata": return FileTypeStrata case ".sts": return FileTypeSTS case ".scss": return FileTypeSCSS case ".ts": if strings.HasSuffix(path, "strataconfig.ts") { return FileTypeConfig } return FileTypeTS } return "" } func (w *Watcher) Stop() { close(w.stopCh) w.watcher.Close() }