178 lines
4.4 KiB
Go
178 lines
4.4 KiB
Go
package template
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
)
|
|
|
|
// HotReloadWatcher watches template files for changes
|
|
type HotReloadWatcher struct {
|
|
watcher *fsnotify.Watcher
|
|
loader *TemplateLoader
|
|
configPath string
|
|
logger *slog.Logger
|
|
stopChan chan struct{}
|
|
}
|
|
|
|
// NewHotReloadWatcher creates a new hot reload watcher
|
|
func NewHotReloadWatcher(loader *TemplateLoader, configPath string) (*HotReloadWatcher, error) {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &HotReloadWatcher{
|
|
watcher: watcher,
|
|
loader: loader,
|
|
configPath: configPath,
|
|
logger: slog.With("component", "hot_reload"),
|
|
stopChan: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
// Start starts watching template files for changes
|
|
func (hr *HotReloadWatcher) Start() error {
|
|
// Watch template directories
|
|
directories := []string{
|
|
hr.configPath,
|
|
filepath.Join(hr.configPath, "custom"),
|
|
filepath.Join(hr.configPath, "builtin"),
|
|
filepath.Join(hr.configPath, "custom", "_default"),
|
|
}
|
|
|
|
for _, dir := range directories {
|
|
if err := hr.addDirectory(dir); err != nil {
|
|
hr.logger.Warn("failed to watch directory", "dir", dir, "error", err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
go hr.watch()
|
|
|
|
hr.logger.Info("hot reload watcher started", "directories", directories)
|
|
return nil
|
|
}
|
|
|
|
// addDirectory adds a directory and its subdirectories to the watcher
|
|
func (hr *HotReloadWatcher) addDirectory(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 hr.watcher.Add(path)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// watch watches for file changes and triggers reloads
|
|
func (hr *HotReloadWatcher) watch() {
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
debounce := make(map[string]time.Time)
|
|
|
|
for {
|
|
select {
|
|
case event := <-hr.watcher.Events:
|
|
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
|
// Debounce rapid file changes
|
|
lastChange := debounce[event.Name]
|
|
if time.Since(lastChange) > 100*time.Millisecond {
|
|
debounce[event.Name] = time.Now()
|
|
hr.handleFileChange(event.Name)
|
|
}
|
|
}
|
|
case err := <-hr.watcher.Errors:
|
|
if err != nil {
|
|
hr.logger.Error("watcher error", "error", err)
|
|
}
|
|
case <-hr.stopChan:
|
|
return
|
|
case <-ticker.C:
|
|
// Cleanup old debounce entries
|
|
now := time.Now()
|
|
for file, lastChange := range debounce {
|
|
if now.Sub(lastChange) > 5*time.Second {
|
|
delete(debounce, file)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleFileChange handles a file change event
|
|
func (hr *HotReloadWatcher) handleFileChange(filePath string) {
|
|
ext := filepath.Ext(filePath)
|
|
if ext != ".html" && ext != ".yaml" && ext != ".yml" {
|
|
return
|
|
}
|
|
|
|
hr.logger.Info("detected file change", "file", filePath)
|
|
|
|
// Reload templates
|
|
hr.loader.ClearCache()
|
|
|
|
// Notify clients via WebSocket or SSE
|
|
hr.notifyClients()
|
|
}
|
|
|
|
// notifyClients notifies connected clients about template changes
|
|
func (hr *HotReloadWatcher) notifyClients() {
|
|
// This could be extended to use WebSocket or Server-Sent Events
|
|
// For now, just log the change
|
|
hr.logger.Info("templates reloaded")
|
|
}
|
|
|
|
// Stop stops the hot reload watcher
|
|
func (hr *HotReloadWatcher) Stop() {
|
|
close(hr.stopChan)
|
|
hr.watcher.Close()
|
|
hr.logger.Info("hot reload watcher stopped")
|
|
}
|
|
|
|
// AddTemplateDirectory adds a new template directory to watch
|
|
func (hr *HotReloadWatcher) AddTemplateDirectory(path string) error {
|
|
return hr.addDirectory(path)
|
|
}
|
|
|
|
// RemoveTemplateDirectory removes a template directory from watch
|
|
func (hr *HotReloadWatcher) RemoveTemplateDirectory(path string) error {
|
|
return hr.watcher.Remove(path)
|
|
}
|
|
|
|
// GetWatchedDirectories returns currently watched directories
|
|
func (hr *HotReloadWatcher) GetWatchedDirectories() []string {
|
|
return hr.watcher.WatchList()
|
|
}
|
|
|
|
// IsWatching returns whether the watcher is active
|
|
func (hr *HotReloadWatcher) IsWatching() bool {
|
|
select {
|
|
case <-hr.stopChan:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// ReloadTemplates triggers a manual template reload
|
|
func (hr *HotReloadWatcher) ReloadTemplates() {
|
|
hr.loader.ClearCache()
|
|
hr.logger.Info("templates manually reloaded")
|
|
}
|
|
|
|
// GetReloadStats returns hot reload statistics
|
|
func (hr *HotReloadWatcher) GetReloadStats() map[string]interface{} {
|
|
watched := hr.GetWatchedDirectories()
|
|
return map[string]interface{}{
|
|
"watched_directories": len(watched),
|
|
"watched_paths": watched,
|
|
"is_active": hr.IsWatching(),
|
|
}
|
|
} |