- 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
171 lines
3.0 KiB
Go
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()
|
|
}
|