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
|
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" {
|
if event == "WRITE" || event == "CREATE" || event == "RENAME" {
|
||||||
// @Todo build single page and sync single asset file to public
|
// @Todo build single page and sync single asset file to public
|
||||||
fmt.Print(color.WhiteString("Building... "))
|
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("Public Root: /public%s", publicRoot))
|
||||||
fmt.Println(color.WhiteString(" Template: %s", tmpl.GetRoot()))
|
fmt.Println(color.WhiteString(" Template: %s", tmpl.GetRoot()))
|
||||||
fmt.Println(color.WhiteString(" Session: %s", strings.TrimLeft(data, "::")))
|
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.WhiteString("-----------------------"))
|
||||||
fmt.Println(color.GreenString("Watching..."))
|
fmt.Println(color.GreenString("Watching..."))
|
||||||
fmt.Println(color.GreenString("Press Ctrl+C to exit"))
|
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 {
|
func watch(root string, handler func(event string, name string), interrupt chan uint8) error {
|
||||||
watcher, err := fsnotify.NewWatcher()
|
watcher, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -160,6 +292,10 @@ func watch(root string, handler func(event string, name string), interrupt chan
|
||||||
shutdown := make(chan bool, 1)
|
shutdown := make(chan bool, 1)
|
||||||
|
|
||||||
err = filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
|
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 info.IsDir() {
|
||||||
if filepath.Base(path) == ".tmp" {
|
if filepath.Base(path) == ".tmp" {
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
|
|
|
||||||
|
|
@ -118,3 +118,13 @@ type IComponent interface {
|
||||||
Load() error
|
Load() error
|
||||||
Source() string
|
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"
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/sui/core"
|
"github.com/yaoapp/yao/sui/core"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Page wraps core.Page with agent-specific functionality
|
// 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
|
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
|
return warnings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -465,3 +473,116 @@ func (page *Page) AssetRoot() string {
|
||||||
func (page *Page) AssistantID() string {
|
func (page *Page) AssistantID() string {
|
||||||
return page.assistantID
|
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
|
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)
|
// Asset get the asset (check agent assets first, then assistant assets)
|
||||||
func (tmpl *Template) Asset(file string, width, height uint) (*core.Asset, error) {
|
func (tmpl *Template) Asset(file string, width, height uint) (*core.Asset, error) {
|
||||||
// First check in agent assets
|
// First check in agent assets
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue