Files
Carlos Gutierrez 9e451469f5 git commit -m "feat: initial release of Strata framework v0.1.0
- Static compiler with STRC pattern (Static Template Resolution with
   Compartmentalized Layers)
   - Template syntax: { } interpolation, { s-for }, { s-if/s-elif/s-else
    }
   - File types: .strata, .compiler.sts, .service.sts, .api.sts, .sts,
   .scss
   - CLI tools: strata dev, strata build, strata g (generators)
   - create-strata scaffolding CLI with Pokemon API example
   - Dev server with WebSocket HMR (Hot Module Replacement)
   - Documentation: README, ARCHITECTURE, CHANGELOG, CONTRIBUTING,
   LICENSE
2026-01-16 09:01:29 -05:00

171 lines
3.0 KiB
Go

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()
}