Implement Multi-Directory Watching and Enhance Locale File Handling
- Introduced the IWatchDirs interface to support watching multiple directories for changes, allowing templates to specify directories dynamically. - Updated the watch command to handle multiple directories and display watched directories in the output. - Added functionality to write locale files from a page's __locales directory to the public directory, improving localization support. - Enhanced error handling during directory watching and locale file processing for better reliability and logging.
This commit is contained in:
parent
ebf3dcc870
commit
0825e06dbe
4 changed files with 295 additions and 1 deletions
138
cmd/sui/watch.go
138
cmd/sui/watch.go
|
|
@ -99,7 +99,20 @@ var WatchCmd = &cobra.Command{
|
|||
return
|
||||
}
|
||||
|
||||
go watch(root, func(event, name string) {
|
||||
// Get all directories to watch
|
||||
watchDirs := []string{root}
|
||||
if watchDirsProvider, ok := tmpl.(core.IWatchDirs); ok {
|
||||
watchDirs = []string{}
|
||||
watchRoot := cfg.DataRoot
|
||||
if watchDirsProvider.GetWatchRoot() == "app" {
|
||||
watchRoot = cfg.Root
|
||||
}
|
||||
for _, dir := range watchDirsProvider.GetWatchDirs() {
|
||||
watchDirs = append(watchDirs, filepath.Join(watchRoot, dir))
|
||||
}
|
||||
}
|
||||
|
||||
go watchMultiple(watchDirs, func(event, name string) {
|
||||
if event == "WRITE" || event == "CREATE" || event == "RENAME" {
|
||||
// @Todo build single page and sync single asset file to public
|
||||
fmt.Print(color.WhiteString("Building... "))
|
||||
|
|
@ -134,6 +147,15 @@ var WatchCmd = &cobra.Command{
|
|||
fmt.Println(color.WhiteString("Public Root: /public%s", publicRoot))
|
||||
fmt.Println(color.WhiteString(" Template: %s", tmpl.GetRoot()))
|
||||
fmt.Println(color.WhiteString(" Session: %s", strings.TrimLeft(data, "::")))
|
||||
fmt.Println(color.WhiteString("Watch Dirs:"))
|
||||
for _, dir := range watchDirs {
|
||||
// Show path relative to either app root or data root
|
||||
displayDir := strings.TrimPrefix(dir, cfg.Root)
|
||||
if displayDir == dir {
|
||||
displayDir = strings.TrimPrefix(dir, cfg.DataRoot)
|
||||
}
|
||||
fmt.Println(color.WhiteString(" - %s", displayDir))
|
||||
}
|
||||
fmt.Println(color.WhiteString("-----------------------"))
|
||||
fmt.Println(color.GreenString("Watching..."))
|
||||
fmt.Println(color.GreenString("Press Ctrl+C to exit"))
|
||||
|
|
@ -151,6 +173,116 @@ var WatchCmd = &cobra.Command{
|
|||
},
|
||||
}
|
||||
|
||||
func watchMultiple(roots []string, handler func(event string, name string), interrupt chan uint8) error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
shutdown := make(chan bool, 1)
|
||||
|
||||
// Walk all root directories
|
||||
watchedCount := 0
|
||||
for _, root := range roots {
|
||||
// Check if root exists
|
||||
if _, err := os.Stat(root); os.IsNotExist(err) {
|
||||
fmt.Println(color.YellowString("[Watch] Directory not found: %s", root))
|
||||
continue
|
||||
}
|
||||
|
||||
err = filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Warn("[Watch] Error accessing path %s: %v", path, err)
|
||||
return nil // Skip this path and continue walking
|
||||
}
|
||||
if info.IsDir() {
|
||||
if filepath.Base(path) == ".tmp" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
watchedCount++
|
||||
log.Info("[Watch] Watching: %s", path)
|
||||
watched.Store(path, true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(color.YellowString("[Watch] Error walking root %s: %v", root, err))
|
||||
}
|
||||
}
|
||||
fmt.Println(color.GreenString("[Watch] Total directories watched: %d", watchedCount))
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-shutdown:
|
||||
log.Info("[Watch] handler exit")
|
||||
return
|
||||
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
interrupt <- 1
|
||||
return
|
||||
}
|
||||
|
||||
basname := filepath.Base(event.Name)
|
||||
isdir := true
|
||||
if strings.Contains(basname, ".") {
|
||||
isdir = false
|
||||
}
|
||||
|
||||
events := strings.Split(event.Op.String(), "|")
|
||||
for _, eventType := range events {
|
||||
// ADD / REMOVE Watching dir
|
||||
if isdir {
|
||||
switch eventType {
|
||||
case "CREATE":
|
||||
log.Info("[Watch] Watching: %s", event.Name)
|
||||
watcher.Add(event.Name)
|
||||
watched.Store(event.Name, true)
|
||||
break
|
||||
|
||||
case "REMOVE":
|
||||
log.Info("[Watch] Unwatching: %s", event.Name)
|
||||
watcher.Remove(event.Name)
|
||||
watched.Delete(event.Name)
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
handler(eventType, event.Name)
|
||||
log.Info("[Watch] %s %s", eventType, event.Name)
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
interrupt <- 2
|
||||
return
|
||||
}
|
||||
log.Error("[Watch] Error: %s", err.Error())
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case code := <-interrupt:
|
||||
shutdown <- true
|
||||
log.Info("[Watch] Exit(%d)", code)
|
||||
fmt.Println(color.YellowString("[Watch] Exit(%d)", code))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watch(root string, handler func(event string, name string), interrupt chan uint8) error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
|
|
@ -160,6 +292,10 @@ func watch(root string, handler func(event string, name string), interrupt chan
|
|||
shutdown := make(chan bool, 1)
|
||||
|
||||
err = filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Warn("[Watch] Error accessing path %s: %v", path, err)
|
||||
return nil // Skip this path and continue walking
|
||||
}
|
||||
if info.IsDir() {
|
||||
if filepath.Base(path) == ".tmp" {
|
||||
return filepath.SkipDir
|
||||
|
|
|
|||
|
|
@ -118,3 +118,13 @@ type IComponent interface {
|
|||
Load() error
|
||||
Source() string
|
||||
}
|
||||
|
||||
// IWatchDirs is an optional interface for templates that need to watch multiple directories
|
||||
type IWatchDirs interface {
|
||||
// GetWatchDirs returns all directories that should be watched for changes
|
||||
// The returned paths are relative to the application source root (not data root)
|
||||
GetWatchDirs() []string
|
||||
// GetWatchRoot returns the root directory for watch paths
|
||||
// Returns "app" for application source root, "data" for data root
|
||||
GetWatchRoot() string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/sui/core"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Page wraps core.Page with agent-specific functionality
|
||||
|
|
@ -315,6 +316,13 @@ func (page *Page) Build(globalCtx *core.GlobalBuildContext, option *core.BuildOp
|
|||
return warnings, err
|
||||
}
|
||||
|
||||
// Write locale files from page's __locales directory
|
||||
err = page.writeLocaleFiles(option.Data)
|
||||
if err != nil {
|
||||
log.Warn("[Agent] Write locale files error: %s", err.Error())
|
||||
// Don't fail the build for locale errors
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
|
|
@ -465,3 +473,116 @@ func (page *Page) AssetRoot() string {
|
|||
func (page *Page) AssistantID() string {
|
||||
return page.assistantID
|
||||
}
|
||||
|
||||
// writeLocaleFiles writes locale files from page's __locales directory to public
|
||||
func (page *Page) writeLocaleFiles(data map[string]interface{}) error {
|
||||
fs := page.tmpl.agent.fs
|
||||
|
||||
// Check if page has __locales directory
|
||||
localesDir := filepath.Join(page.Path, "__locales")
|
||||
if !fs.IsDir(localesDir) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the public root
|
||||
root, err := page.tmpl.agent.DSL.PublicRoot(data)
|
||||
if err != nil {
|
||||
log.Error("writeLocaleFiles: Get the public root error: %s. use %s", err.Error(), page.tmpl.agent.DSL.Public.Root)
|
||||
root = page.tmpl.agent.DSL.Public.Root
|
||||
}
|
||||
|
||||
// Read all locale files in __locales directory
|
||||
files, err := fs.ReadDir(localesDir, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
// Skip directories
|
||||
if fs.IsDir(file) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only process .yml files
|
||||
if filepath.Ext(file) != ".yml" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get locale name (e.g., "zh-cn" from "zh-cn.yml")
|
||||
localeName := filepath.Base(file)
|
||||
localeName = localeName[:len(localeName)-4] // Remove .yml extension
|
||||
|
||||
// Read the locale file
|
||||
content, err := fs.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Error("[Agent] Read locale file error: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the locale file
|
||||
var localeData map[string]interface{}
|
||||
err = yaml.Unmarshal(content, &localeData)
|
||||
if err != nil {
|
||||
log.Error("[Agent] Parse locale file error: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert to the format expected by core.Locale
|
||||
locale := core.Locale{
|
||||
Name: localeName,
|
||||
Keys: map[string]string{},
|
||||
Messages: map[string]string{},
|
||||
ScriptMessages: map[string]string{},
|
||||
}
|
||||
|
||||
// Extract messages
|
||||
if messages, ok := localeData["messages"].(map[string]interface{}); ok {
|
||||
for k, v := range messages {
|
||||
if strVal, ok := v.(string); ok {
|
||||
locale.Messages[k] = strVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract script_messages
|
||||
if scriptMessages, ok := localeData["script_messages"].(map[string]interface{}); ok {
|
||||
for k, v := range scriptMessages {
|
||||
if strVal, ok := v.(string); ok {
|
||||
locale.ScriptMessages[k] = strVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract timezone and direction
|
||||
if tz, ok := localeData["timezone"].(string); ok {
|
||||
locale.Timezone = tz
|
||||
}
|
||||
if dir, ok := localeData["direction"].(string); ok {
|
||||
locale.Direction = dir
|
||||
}
|
||||
|
||||
// Write to public/.locales/<locale>/<route>.yml
|
||||
// page.Route may contain path like /expense/test, so we need to create nested directories
|
||||
targetFile := filepath.Join(application.App.Root(), "public", root, ".locales", localeName, fmt.Sprintf("%s.yml", page.Route))
|
||||
targetDir := filepath.Dir(targetFile)
|
||||
if exist, _ := os.Stat(targetDir); exist == nil {
|
||||
os.MkdirAll(targetDir, os.ModePerm)
|
||||
}
|
||||
|
||||
localeContent, err := yaml.Marshal(locale)
|
||||
if err != nil {
|
||||
log.Error("[Agent] Marshal locale error: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
err = os.WriteFile(targetFile, localeContent, 0644)
|
||||
if err != nil {
|
||||
log.Error("[Agent] Write locale file error: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("[Agent] Wrote locale file: %s", targetFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,6 +226,33 @@ func (tmpl *Template) GetRoot() string {
|
|||
return tmpl.agent.root
|
||||
}
|
||||
|
||||
// GetWatchDirs returns all directories that should be watched for changes
|
||||
// This implements the core.IWatchDirs interface
|
||||
func (tmpl *Template) GetWatchDirs() []string {
|
||||
dirs := []string{}
|
||||
|
||||
// 1. Add the main agent template directory
|
||||
dirs = append(dirs, tmpl.agent.root)
|
||||
|
||||
// 2. Add each assistant's pages directory
|
||||
assistants, err := tmpl.agent.getAssistants()
|
||||
if err != nil {
|
||||
return dirs
|
||||
}
|
||||
|
||||
for _, assistantID := range assistants {
|
||||
pagesDir := tmpl.agent.getAssistantPagesRoot(assistantID)
|
||||
dirs = append(dirs, pagesDir)
|
||||
}
|
||||
|
||||
return dirs
|
||||
}
|
||||
|
||||
// GetWatchRoot returns "app" to indicate paths are relative to application source root
|
||||
func (tmpl *Template) GetWatchRoot() string {
|
||||
return "app"
|
||||
}
|
||||
|
||||
// Asset get the asset (check agent assets first, then assistant assets)
|
||||
func (tmpl *Template) Asset(file string, width, height uint) (*core.Asset, error) {
|
||||
// First check in agent assets
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue