diff --git a/cli/daemon/run/embedded_files.go b/cli/daemon/run/embedded_files.go new file mode 100644 index 0000000000..0a26d043ba --- /dev/null +++ b/cli/daemon/run/embedded_files.go @@ -0,0 +1,127 @@ +package run + +import ( + "encr.dev/pkg/watcher" + "fmt" + "os" + "path/filepath" + "strings" +) + +var embeddedFiles = make(map[string][]string) + +// ignoreEventEmbedded checks whether the event is related to an embedded file +func ignoreEventEmbedded(event watcher.Event) (bool, error) { + switch event.EventType { + case watcher.CREATED: + return true, handleCreatedFile(event.Path) + case watcher.DELETED: + return true, handleDeletedFile(event.Path) + case watcher.MODIFIED: + return handleModifiedFile(event.Path) + default: + return true, nil + } +} + +func handleModifiedFile(path string) (bool, error) { + if strings.HasSuffix(path, ".go") { + return true, updateEmbeddedFiles(path) + } + + embedded, err := isFileEmbedded(path) + if err != nil { + return true, err + } + + return !embedded, nil +} + +func initializeEmbeddedFilesTracker(root string) error { + if len(embeddedFiles) > 0 { + return nil + } + + return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || filepath.Ext(path) != ".go" { + return err + } + return updateEmbeddedFiles(path) + }) +} + +func handleCreatedFile(path string) error { + if strings.HasSuffix(path, ".go") { + return updateEmbeddedFiles(path) + } + return nil +} + +func handleDeletedFile(path string) error { + delete(embeddedFiles, path) + return nil +} + +func isFileEmbedded(fpath string) (bool, error) { + for _, files := range embeddedFiles { + for _, file := range files { + if file == fpath { + return true, nil + } + } + } + return false, nil +} + +func updateEmbeddedFiles(path string) error { + embeds, err := parseEmbeddedFiles(path) + if err != nil { + return fmt.Errorf("failed to parse embedded files: %w", err) + } + embeddedFiles[path] = embeds + return nil +} + +// parseEmbeddedFiles returns all the embedded files for a given source file +func parseEmbeddedFiles(sourceFile string) ([]string, error) { + data, err := os.ReadFile(sourceFile) + if err != nil { + return nil, fmt.Errorf("failed to read source file: %w", err) + } + + var embeddedPaths []string + sourceDir := filepath.Dir(sourceFile) + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "//go:embed") { + parts := strings.Fields(line) + if len(parts) > 1 { + dir := parts[1] + filepaths, err := getFilePathsFromDir(filepath.Join(sourceDir, dir)) + if err != nil { + return nil, err + } + embeddedPaths = append(embeddedPaths, filepaths...) + } + } + } + return embeddedPaths, nil +} + +// getFilePathsFromDir retrieves all file paths from a directory recursively +func getFilePathsFromDir(dir string) ([]string, error) { + var filePaths []string + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + filePaths = append(filePaths, path) + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to walk through directory %s: %w", dir, err) + } + return filePaths, nil +} diff --git a/cli/daemon/run/watch.go b/cli/daemon/run/watch.go index 3b453d1194..2077402278 100644 --- a/cli/daemon/run/watch.go +++ b/cli/daemon/run/watch.go @@ -11,6 +11,12 @@ import ( // watch watches the given app for changes, and reports // them on c. func (mgr *Manager) watch(run *Run) error { + + // Initialize embedded files tracker + if err := initializeEmbeddedFilesTracker(run.App.Root()); err != nil { + return err + } + sub, err := run.App.Watch(func(i *apps.Instance, event []watcher.Event) { if IgnoreEvents(event) { return @@ -44,10 +50,22 @@ func (mgr *Manager) watch(run *Run) error { } // IgnoreEvents will return true if _all_ events are on files that should be ignored -// as the do not impact the running app, or are the result of Encore itself generating code. +// as they do not impact the running app, or are the result of Encore itself generating code. func IgnoreEvents(events []watcher.Event) bool { for _, event := range events { - if !ignoreEvent(event) { + filename := filepath.Base(event.Path) + if strings.HasPrefix(strings.ToLower(filename), "encore.gen.") || + strings.HasSuffix(filename, "~") { + // Ignore generated code and temporary files + return true + } + + ignore, err := ignoreEventEmbedded(event) + if err != nil { + return false + } + + if !ignoreEvent(event) || !ignore { return false } } @@ -55,12 +73,6 @@ func IgnoreEvents(events []watcher.Event) bool { } func ignoreEvent(ev watcher.Event) bool { - filename := filepath.Base(ev.Path) - if strings.HasPrefix(strings.ToLower(filename), "encore.gen.") { - // Ignore generated code - return true - } - // Ignore files which wouldn't impact the running app ext := filepath.Ext(ev.Path) switch ext {