dev.go

121 lines
1 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
}