Repository: yaitoo/xun
Pull request #69: fix(viewengine): continue to load other files when one file fails
Author: login: cnlangzi
Created: 2025-04-18T15:04:08Z | Merged: 2025-04-19T14:12:07Z

### Changed
-

### Fixed
- fix(viewengine): continue to load other files even a file is failed to load

### Added
- 

### Tests
Tasks to complete before merging PR:
- [ ]  Ensure unit tests are passing. If not run `make unit-tests` to check for any regressions :clipboard:
- [ ]  Ensure lint tests are passing. if not run `make lint` to check for any issues
- [ ]  Ensure codecov/patch is passing for changes.

## Summary by Sourcery

Modify view engine loading to continue processing files even when individual file loading fails, improving error handling and resilience

Bug Fixes:
- Prevent view engine from stopping template loading when a single file fails to load

Enhancements:
- Modify view engine methods to log errors instead of stopping the entire loading process
- Update error handling to continue processing other files when one file fails


Diff:
diff --git a/app.go b/app.go
index 61f105d..d11223c 100644
--- a/app.go
+++ b/app.go
@@ -79,10 +79,7 @@ func New(opts ...Option) *App {
 
 	if app.fsys != nil {
 		for _, ve := range app.engines {
-			err := ve.Load(app.fsys, app)
-			if err != nil {
-				app.logger.Error("xun: load views", slog.Any("err", err))
-			}
+			ve.Load(app.fsys, app)
 		}
 
 		if app.watch {
diff --git a/app_test.go b/app_test.go
index 280d873..e0fb0f1 100644
--- a/app_test.go
+++ b/app_test.go
@@ -25,14 +25,16 @@ var (
 )
 
 func TestMain(m *testing.M) {
-	tr := http.DefaultTransport.(*http.Transport).Clone()
-	tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
-	tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { // skipcq: RVV-B0012
-		if strings.HasPrefix(addr, "abc.com") {
-			return net.Dial("tcp", strings.TrimPrefix(addr, "abc.com"))
-		}
-		return net.Dial("tcp", addr)
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { // skipcq: RVV-B0012
+			if strings.HasPrefix(addr, "abc.com") {
+				return net.Dial("tcp", net.JoinHostPort("127.0.0.1", strings.TrimPrefix(addr, "abc.com:")))
+			}
+			return net.Dial("tcp", addr)
+		},
 	}
+
 	client = http.Client{
 		Transport: tr,
 	}
diff --git a/app_watch_test.go b/app_watch_test.go
index 87f1c94..a5fe40c 100644
--- a/app_watch_test.go
+++ b/app_watch_test.go
@@ -286,8 +286,8 @@ func TestWatchOnHtml(t *testing.T) {
 type mockViewEngine struct {
 }
 
-func (*mockViewEngine) Load(fsys fs.FS, app *App) error { // skipcq: RVV-B0012
-	return nil
+func (*mockViewEngine) Load(fsys fs.FS, app *App) { // skipcq: RVV-B0012
+
 }
 func (*mockViewEngine) FileChanged(fsys fs.FS, app *App, event fsnotify.Event) error { // skipcq: RVV-B0012
 	return errors.New("err: unhandled error")
diff --git a/viewengine.go b/viewengine.go
index 7863ecf..89e156f 100644
--- a/viewengine.go
+++ b/viewengine.go
@@ -10,6 +10,6 @@ import (
 // an effective view engine, namely methods for loading templates from a file
 // system and reloading templates when the file system changes.
 type ViewEngine interface {
-	Load(fsys fs.FS, app *App) error
+	Load(fsys fs.FS, app *App)
 	FileChanged(fsys fs.FS, app *App, event fsnotify.Event) error
 }
diff --git a/viewengine_html.go b/viewengine_html.go
index 692a035..de70d3a 100644
--- a/viewengine_html.go
+++ b/viewengine_html.go
@@ -1,8 +1,8 @@
 package xun
 
 import (
-	"errors"
 	"io/fs"
+	"log/slog"
 	"path/filepath"
 	"strings"
 
@@ -27,7 +27,7 @@ type HtmlViewEngine struct {
 // Load loads all templates from the given file system.
 //
 // It loads all components, layouts, pages and views from the given file system.
-func (ve *HtmlViewEngine) Load(fsys fs.FS, app *App) error {
+func (ve *HtmlViewEngine) Load(fsys fs.FS, app *App) {
 	if ve.templates == nil {
 		ve.templates = map[string]*HtmlTemplate{}
 	}
@@ -35,23 +35,10 @@ func (ve *HtmlViewEngine) Load(fsys fs.FS, app *App) error {
 	ve.fsys = fsys
 	ve.app = app
 
-	err := ve.loadComponents()
-	if err != nil {
-		return err
-	}
-
-	err = ve.loadLayouts()
-	if err != nil {
-		return err
-	}
-
-	err = ve.loadPages()
-	if err != nil {
-		return err
-	}
-
-	return ve.loadViews()
-
+	ve.loadComponents()
+	ve.loadLayouts()
+	ve.loadPages()
+	ve.loadViews()
 }
 
 // FileChanged is called when a file has been changed.
@@ -88,25 +75,23 @@ func (ve *HtmlViewEngine) FileChanged(fsys fs.FS, app *App, event fsnotify.Event
 
 }
 
-func (ve *HtmlViewEngine) loadComponents() error {
-	err := fs.WalkDir(ve.fsys, "components", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-		if d.IsDir() || !strings.EqualFold(filepath.Ext(path), ".html") {
+func (ve *HtmlViewEngine) loadFiles(dir string, process func(path string) error) {
+	fs.WalkDir(ve.fsys, dir, func(path string, d fs.DirEntry, _ error) error { // nolint: errcheck
+		if !strings.EqualFold(filepath.Ext(path), ".html") {
 			return nil
 		}
+		if err := process(path); err != nil {
+			ve.app.logger.Error("xun: load html", slog.String("path", path), slog.Any("err", err))
+		}
+		return nil
+	})
+}
 
-		_, err = ve.loadTemplate(path)
+func (ve *HtmlViewEngine) loadComponents() {
+	ve.loadFiles("components", func(path string) error {
+		_, err := ve.loadTemplate(path)
 		return err
 	})
-
-	if err != nil && errors.Is(err, fs.ErrNotExist) {
-		return nil
-	}
-
-	return err
-
 }
 
 func (ve *HtmlViewEngine) loadTemplate(path string) (*HtmlTemplate, error) {
@@ -130,49 +115,17 @@ func (ve *HtmlViewEngine) loadTemplate(path string) (*HtmlTemplate, error) {
 	return t, nil
 }
 
-func (ve *HtmlViewEngine) loadLayouts() error {
-	err := fs.WalkDir(ve.fsys, "layouts", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-
-		if !d.IsDir() {
-
-			_, err = ve.loadTemplate(path)
-
-			return err
-
-		}
-
-		return nil
+func (ve *HtmlViewEngine) loadLayouts() {
+	ve.loadFiles("layouts", func(path string) error {
+		_, err := ve.loadTemplate(path)
+		return err
 	})
-
-	if err != nil && errors.Is(err, fs.ErrNotExist) {
-		return nil
-	}
-
-	return err
 }
 
-func (ve *HtmlViewEngine) loadPages() error {
-	err := fs.WalkDir(ve.fsys, "pages", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-
-		if d.IsDir() || !strings.EqualFold(filepath.Ext(path), ".html") {
-			return nil
-		}
-
+func (ve *HtmlViewEngine) loadPages() {
+	ve.loadFiles("pages", func(path string) error { // nolint: errcheck
 		return ve.loadPage(path)
 	})
-
-	if err != nil && errors.Is(err, fs.ErrNotExist) {
-		return nil
-	}
-
-	return err
-
 }
 
 func (ve *HtmlViewEngine) loadPage(path string) error {
@@ -201,25 +154,10 @@ func (ve *HtmlViewEngine) loadPage(path string) error {
 	return nil
 }
 
-func (ve *HtmlViewEngine) loadViews() error {
-	err := fs.WalkDir(ve.fsys, "views", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-
-		if d.IsDir() || !strings.EqualFold(filepath.Ext(path), ".html") {
-			return nil
-		}
-
+func (ve *HtmlViewEngine) loadViews() {
+	ve.loadFiles("views", func(path string) error {
 		return ve.loadView(path)
 	})
-
-	if err != nil && errors.Is(err, fs.ErrNotExist) {
-		return nil
-	}
-
-	return err
-
 }
 
 func (ve *HtmlViewEngine) loadView(path string) error {
diff --git a/viewengine_static.go b/viewengine_static.go
index 3ca73f5..04d8fa7 100644
--- a/viewengine_static.go
+++ b/viewengine_static.go
@@ -1,7 +1,6 @@
 package xun
 
 import (
-	"errors"
 	"io/fs"
 	"reflect"
 	"strings"
@@ -19,7 +18,7 @@ type StaticViewEngine struct {
 // It scans the "public" directory in the given file system and registers each file
 // with the application. It also handles file changes for the "public" directory
 // and updates the application accordingly.
-func (ve *StaticViewEngine) Load(fsys fs.FS, app *App) error {
+func (ve *StaticViewEngine) Load(fsys fs.FS, app *App) {
 	root, err := fsys.Open(".")
 	if err == nil {
 		t := reflect.TypeOf(root)
@@ -28,23 +27,13 @@ func (ve *StaticViewEngine) Load(fsys fs.FS, app *App) error {
 		}
 	}
 
-	err = fs.WalkDir(fsys, "public", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-
-		if !d.IsDir() {
+	fs.WalkDir(fsys, "public", func(path string, d fs.DirEntry, err error) error { // nolint: errcheck
+		if d != nil && !d.IsDir() {
 			ve.handle(fsys, app, path)
 		}
 
 		return nil
 	})
-
-	if err != nil && errors.Is(err, fs.ErrNotExist) {
-		return nil
-	}
-
-	return err
 }
 
 // FileChanged handles file changes for the given file system and updates the
diff --git a/viewengine_text.go b/viewengine_text.go
index 9a9433c..b36bbbc 100644
--- a/viewengine_text.go
+++ b/viewengine_text.go
@@ -1,8 +1,8 @@
 package xun
 
 import (
-	"errors"
 	"io/fs"
+	"log/slog"
 	"strings"
 
 	"github.com/yaitoo/xun/fsnotify"
@@ -18,7 +18,7 @@ type TextViewEngine struct {
 
 // Load walks the file system and loads all text-based templates that match the TextViewEngine's pattern.
 // It calls the handle method for each matching file to add the template to the app's viewers.
-func (ve *TextViewEngine) Load(fsys fs.FS, app *App) error {
+func (ve *TextViewEngine) Load(fsys fs.FS, app *App) {
 	if ve.templates == nil {
 		ve.templates = map[string]*TextTemplate{}
 	}
@@ -26,25 +26,16 @@ func (ve *TextViewEngine) Load(fsys fs.FS, app *App) error {
 	ve.fsys = fsys
 	ve.app = app
 
-	err := fs.WalkDir(fsys, "text", func(path string, d fs.DirEntry, err error) error {
-
-		if err != nil {
-			return err
-		}
-
-		if !d.IsDir() {
-			return ve.loadText(path)
-
+	fs.WalkDir(fsys, "text", func(path string, d fs.DirEntry, err error) error { // nolint: errcheck
+		if d != nil && !d.IsDir() {
+			if err := ve.loadText(path); err != nil {
+				slog.Error("text: load text", slog.String("path", path), slog.Any("err", err))
+			}
+			return nil
 		}
 
 		return nil
 	})
-
-	if err != nil && errors.Is(err, fs.ErrNotExist) {
-		return nil
-	}
-
-	return err
 }
 
 func (ve *TextViewEngine) loadText(path string) error {


Reviews:
user: login: sourcery-ai[bot]
body: Hey @cnlangzi - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- The `Load` methods no longer return errors, so the error variable in the walk functions is now unused and can be dropped.
- Consider adding a recover statement to the goroutines to prevent panics from crashing the application.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T15:04:53Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR modifies the view engine loading mechanism to ensure that a single file loading failure does not prevent the rest of the files from being processed. Key changes include:
- Removing the error propagation from the Load functions in TextViewEngine, StaticViewEngine, and HtmlViewEngine.
- Logging errors for individual file failures using slog instead of stopping the entire loading process.
- Adjusting the ViewEngine interface and corresponding mock implementations in tests to reflect the new Load signature.

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                  | Description                                              |
| --------------------- | -------------------------------------------------------- |
| viewengine_text.go    | Removed error returns from Load; now logs errors per file load. |
| viewengine_static.go  | Updated Load to remove error propagation and log errors. |
| viewengine_html.go    | Changed Load and its helper functions to log errors instead of returning them. |
| viewengine.go         | Updated the ViewEngine interface to use a void Load method. |
| app_watch_test.go     | Adjusted mock implementations to reflect updated Load signature. |
| app.go                | Updated engines loading to call the new void Load method. |
</details>





state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T15:41:40Z
user: login: sourcery-ai[bot]
body: Hey @cnlangzi - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- The nolint comments should have a reason why the error is being ignored.
- Consider adding a method to the ViewEngine interface to return the name of the engine for logging purposes.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T15:42:10Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR enhances the resilience of the view engine by modifying the Load methods to log errors instead of stopping file processing when an individual file fails to load. Key changes include:
- Removing error returns from Load methods in TextViewEngine, StaticViewEngine, and HtmlViewEngine.
- Logging errors for file-load failures within WalkDir callbacks.
- Updating test and interface definitions to align with the new error-handling design.

### Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                    | Description                                                         |
| ----------------------- | ------------------------------------------------------------------- |
| viewengine_text.go      | Updated Load method to log errors instead of returning them.        |
| viewengine_static.go    | Modified Load to ignore errors on file loading in the public folder.  |
| viewengine_html.go      | Adjusted Load and its helper methods to internalize and log errors.   |
| viewengine.go           | Updated the ViewEngine interface to remove the error return from Load.|
| app_watch_test.go       | Updated mock to reflect new Load signature.                         |
| app_test.go             | Tweaked HTTP transport DialContext to target a local address.         |
| app.go                  | Removed error handling from Load call for each view engine.           |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**viewengine_html.go:113**
* Ensure there are unit tests verifying that errors during layout loading are correctly logged since the function no longer returns an error.
```
fs.WalkDir(ve.fsys, "layouts", func(path string, d fs.DirEntry, _ error) error { // nolint: errcheck
```
**viewengine.go:13**
* Add tests to verify that any file load errors are properly handled and logged by each view engine, given that the Load method no longer returns an error.
```
Load(fsys fs.FS, app *App)
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T15:49:04Z
user: login: sourcery-ai[bot]
body: Hey @cnlangzi - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding a comment to the `fs.WalkDir` anonymous functions to explain why the error is being ignored.
- The `FileChanged` method on `mockViewEngine` should probably not return an unhandled error.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T15:49:11Z

Review comments:
diff_hunk: @@ -130,49 +109,32 @@ func (ve *HtmlViewEngine) loadTemplate(path string) (*HtmlTemplate, error) {
 	return t, nil
 }
 
-func (ve *HtmlViewEngine) loadLayouts() error {
-	err := fs.WalkDir(ve.fsys, "layouts", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-
-		if !d.IsDir() {
-
-			_, err = ve.loadTemplate(path)
-
-			return err
+func (ve *HtmlViewEngine) loadLayouts() {
+	fs.WalkDir(ve.fsys, "layouts", func(path string, d fs.DirEntry, _ error) error { // nolint: errcheck
 
+		if d != nil && !d.IsDir() {
+			if _, err := ve.loadTemplate(path); err != nil {
+				ve.app.logger.Error("html: load html layuots", slog.String("path", path), slog.Any("err", err))
path: viewengine_html.go
user: login: sourcery-ai[bot]
body: **nitpick (typo):** Typo in the logging message for layouts.

The log message label "html: load html layuots" likely should be corrected to "html: load html layouts" for clarity and consistency.

```suggestion
				ve.app.logger.Error("html: load html layouts", slog.String("path", path), slog.Any("err", err))
```
created_at: 2025-04-18T15:04:53Z
author_association: NONE
diff_hunk: @@ -88,25 +75,17 @@ func (ve *HtmlViewEngine) FileChanged(fsys fs.FS, app *App, event fsnotify.Event
 
 }
 
-func (ve *HtmlViewEngine) loadComponents() error {
-	err := fs.WalkDir(ve.fsys, "components", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-		if d.IsDir() || !strings.EqualFold(filepath.Ext(path), ".html") {
+func (ve *HtmlViewEngine) loadComponents() {
+	fs.WalkDir(ve.fsys, "components", func(path string, d fs.DirEntry, _ error) error { // nolint: errcheck
path: viewengine_html.go
user: login: Copilot
body: Consider using consistent logging prefixes across all HTML view engine loading functions (e.g. components, layouts, pages, views). Currently, 'xun: load html components' is used in loadComponents while other functions use different prefixes, which may lead to confusion during debugging.
created_at: 2025-04-18T15:41:40Z
author_association: NONE
diff_hunk: @@ -27,31 +27,18 @@
 // Load loads all templates from the given file system.
 //
 // It loads all components, layouts, pages and views from the given file system.
-func (ve *HtmlViewEngine) Load(fsys fs.FS, app *App) error {
+func (ve *HtmlViewEngine) Load(fsys fs.FS, app *App) {
 	if ve.templates == nil {
 		ve.templates = map[string]*HtmlTemplate{}
 	}
 
 	ve.fsys = fsys
 	ve.app = app
 
-	err := ve.loadComponents()
-	if err != nil {
-		return err
-	}
-
-	err = ve.loadLayouts()
-	if err != nil {
-		return err
-	}
-
-	err = ve.loadPages()
-	if err != nil {
-		return err
-	}
-
-	return ve.loadViews()
-
+	ve.loadComponents()
+	ve.loadLayouts()
+	ve.loadPages()
+	ve.loadViews()
path: viewengine_html.go
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the directory walking and error-logging logic into a shared helper function to reduce code duplication in the loader functions and simplify error handling in the anonymous callbacks

Consider extracting the duplicated directory walking and error‐logging logic into a shared helper. This minimizes duplication of the inline error handling in anonymous callbacks. For example, you might define a helper like this:

```go
func (ve *HtmlViewEngine) walkAndLoadDir(dir, logMsg string, loadFunc func(path string) error) {
	fs.WalkDir(ve.fsys, dir, func(path string, d fs.DirEntry, _ error) error { // nolint: errcheck
		if d == nil || d.IsDir() || !strings.EqualFold(filepath.Ext(path), ".html") {
			return nil
		}
		if err := loadFunc(path); err != nil {
			ve.app.logger.Error(logMsg, slog.String("path", path), slog.Any("err", err))
		}
		return nil
	})
}
```

Then, refactor your individual loaders using the helper:

```go
func (ve *HtmlViewEngine) loadComponents() {
	ve.walkAndLoadDir("components", "xun: load html components", ve.loadTemplate)
}

func (ve *HtmlViewEngine) loadLayouts() {
	ve.walkAndLoadDir("layouts", "html: load html layouts", ve.loadTemplate)
}

func (ve *HtmlViewEngine) loadPages() {
	ve.walkAndLoadDir("pages", "xun: load html page", func(path string) error {
		return ve.loadPage(path)
	})
}

func (ve *HtmlViewEngine) loadViews() {
	ve.walkAndLoadDir("views", "xun: load html view", func(path string) error {
		return ve.loadView(path)
	})
}
```

This approach keeps all functionality intact while reducing repetition and complexity.
created_at: 2025-04-18T15:42:10Z
author_association: NONE
diff_hunk: @@ -25,14 +25,16 @@ var (
 )
 
 func TestMain(m *testing.M) {
-	tr := http.DefaultTransport.(*http.Transport).Clone()
-	tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
-	tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { // skipcq: RVV-B0012
-		if strings.HasPrefix(addr, "abc.com") {
-			return net.Dial("tcp", strings.TrimPrefix(addr, "abc.com"))
-		}
-		return net.Dial("tcp", addr)
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { // skipcq: RVV-B0012
+			if strings.HasPrefix(addr, "abc.com") {
+				return net.Dial("tcp", "127.0.0.1"+strings.TrimPrefix(addr, "abc.com"))
path: app_test.go
user: login: Copilot
body: Ensure that concatenating '127.0.0.1' with the trimmed address correctly forms the expected host:port string; consider using net.JoinHostPort for more reliable address formatting.
```suggestion
				return net.Dial("tcp", net.JoinHostPort("127.0.0.1", strings.TrimPrefix(addr, "abc.com")))
```
created_at: 2025-04-18T15:49:03Z
author_association: NONE
diff_hunk: @@ -1,8 +1,8 @@
 package xun
path: viewengine_html.go
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider creating a common helper function to encapsulate the FS walk logic and error logging, reducing code duplication across the load functions for components, layouts, pages, and views, and improving maintainability and readability..

You’re repeating four nearly identical FS walks with custom callbacks. Consolidating them into a common helper would reduce duplication and overall complexity. For example, you can create a helper method to encapsulate the walking logic and error logging, then pass a function to process matching files. For instance:

```go
func (ve *HtmlViewEngine) walkAndProcess(dir string, process func(path string) error) {
    fs.WalkDir(ve.fsys, dir, func(path string, d fs.DirEntry, _ error) error { // nolint: errcheck
        if !strings.EqualFold(filepath.Ext(path), ".html") {
            return nil
        }
        if err := process(path); err != nil {
            ve.app.logger.Error("xun: load html", slog.String("path", path), slog.Any("err", err))
        }
        return nil
    })
}
```

Then update your functions like `loadComponents`, `loadLayouts`, `loadViews`, and even `loadPages` (with slight adjustments if needed) to use this helper:

```go
func (ve *HtmlViewEngine) loadComponents() {
    ve.walkAndProcess("components", func(path string) error {
        _, err := ve.loadTemplate(path)
        return err
    })
}

func (ve *HtmlViewEngine) loadLayouts() {
    ve.walkAndProcess("layouts", func(path string) error {
        _, err := ve.loadTemplate(path)
        return err
    })
}

func (ve *HtmlViewEngine) loadViews() {
    ve.walkAndProcess("views", func(path string) error {
        return ve.loadView(path)
    })
}

// Adjust loadPages accordingly if needed:
func (ve *HtmlViewEngine) loadPages() {
    ve.walkAndProcess("pages", func(path string) error {
        return ve.loadPage(path)
    })
}
```

This refactoring removes repetitive error handling and inline logging while keeping all functionality intact.
created_at: 2025-04-18T15:49:11Z
author_association: NONE

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-04-18T15:04:12Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

The pull request modifies the view engine loading process to continue loading files even if one file fails to load. This is achieved by changing the Load methods in the view engines to not return errors and instead log them. The individual load methods for components, layouts, pages, and views in the HTML view engine were also modified to log errors instead of returning them.

#### Sequence diagram for loading HTML templates with error handling

```mermaid
sequenceDiagram
  participant HtmlViewEngine
  participant fsys as FileSystem
  participant app as App

  HtmlViewEngine->>fsys: WalkDir("components", ...)
  loop For each component file
    fsys->>HtmlViewEngine: Returns path to file
    HtmlViewEngine->>HtmlViewEngine: loadTemplate(path)
    alt Error loading template
      HtmlViewEngine->>app: app.logger.Error("xun: load html", path, err)
    end
  end

  HtmlViewEngine->>fsys: WalkDir("layouts", ...)
  loop For each layout file
    fsys->>HtmlViewEngine: Returns path to file
    HtmlViewEngine->>HtmlViewEngine: loadTemplate(path)
    alt Error loading template
      HtmlViewEngine->>app: app.logger.Error("xun: load html", path, err)
    end
  end

  HtmlViewEngine->>fsys: WalkDir("pages", ...)
  loop For each page file
    fsys->>HtmlViewEngine: Returns path to file
    HtmlViewEngine->>HtmlViewEngine: loadPage(path)
    alt Error loading page
      HtmlViewEngine->>app: app.logger.Error("xun: load html", path, err)
    end
  end

  HtmlViewEngine->>fsys: WalkDir("views", ...)
  loop For each view file
    fsys->>HtmlViewEngine: Returns path to file
    HtmlViewEngine->>HtmlViewEngine: loadView(path)
    alt Error loading view
      HtmlViewEngine->>app: app.logger.Error("xun: load html", path, err)
    end
  end
```

#### Updated class diagram for TextViewEngine

```mermaid
classDiagram
  class TextViewEngine {
    -templates map[string]*TextTemplate
    -fsys fs.FS
    -app *App
    +Load(fsys fs.FS, app *App)
    -loadText(path string) error
  }
  note for TextViewEngine "Load method no longer returns an error. Errors are now logged internally."
```

#### Updated class diagram for StaticViewEngine

```mermaid
classDiagram
  class StaticViewEngine {
    +Load(fsys fs.FS, app *App)
    +FileChanged(fsys fs.FS, app *App, event fsnotify.Event) error
    -handle(fsys fs.FS, app *App, path string)
  }
  note for StaticViewEngine "Load method no longer returns an error."
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Modified view engine loading to continue processing files even when individual file loading fails, improving error handling and resilience. | <ul><li>Updated `HtmlViewEngine.Load` to not return an error.</li><li>Updated `TextViewEngine.Load` to not return an error.</li><li>Updated `StaticViewEngine.Load` to not return an error.</li><li>Updated `mockViewEngine.Load` to not return an error.</li><li>Removed error returns from the `loadComponents`, `loadLayouts`, `loadPages`, and `loadViews` methods in `HtmlViewEngine`.</li><li>Modified the `loadComponents`, `loadLayouts`, `loadPages`, `loadViews` and `loadText` methods to log errors instead of returning them.</li><li>Updated the main application loop to not return an error when loading views.</li></ul> | `viewengine_html.go`<br/>`viewengine_text.go`<br/>`app_test.go`<br/>`viewengine_static.go`<br/>`app.go`<br/>`app_watch_test.go`<br/>`viewengine.go` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: deepsource-io[bot]
created_at: 2025-04-18T15:04:23Z
body: <div><p>Here's the code health analysis summary for commits <code>006d1eb..3029703</code>. <a href="https://app.deepsource.com/gh/yaitoo/xun/run/1d678507-e56b-4bc5-9462-24ca2c32a118/">View details on DeepSource</a>&nbsp;↗.</p><h3>Analysis Summary</h3><table><thead><tr><th>Analyzer</th><th>Status</th><th>Summary</th><th>Link</th></tr></thead><tbody><tr><td><img src="https://static.deepsource.com/analyzer_logos/go.svg" alt="DeepSource Go Logo" width="16px" height="16px"/><strong>Go</strong></td><td><span>✅&nbsp;</span><span>Success</span></td><td></td><td><a href="https://app.deepsource.com/gh/yaitoo/xun/run/1d678507-e56b-4bc5-9462-24ca2c32a118/go/">View Check</a>&nbsp;↗</td></tr></tbody></table><hr/><blockquote><div>💡 If you’re a repository administrator, you can configure the quality gates from the <a href="https://app.deepsource.com/gh/yaitoo/xun/settings/reporting">settings</a>.</div></blockquote></div>

author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-18T15:06:19Z
body: ## [Codecov](https://app.codecov.io/gh/yaitoo/xun/pull/69?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo) Report
Attention: Patch coverage is `82.35294%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 91.28%. Comparing base [(`006d1eb`)](https://app.codecov.io/gh/yaitoo/xun/commit/006d1eb5481ec6c6bb0675c3bb53e6a72bfa54a8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo) to head [(`3029703`)](https://app.codecov.io/gh/yaitoo/xun/commit/3029703e72e6399c90ec94aeb9c46461554228cd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/yaitoo/xun/pull/69?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo) | Patch % | Lines |
|---|---|---|
| [viewengine\_html.go](https://app.codecov.io/gh/yaitoo/xun/pull/69?src=pr&el=tree&filepath=viewengine_html.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo#diff-dmlld2VuZ2luZV9odG1sLmdv) | 86.95% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/yaitoo/xun/pull/69?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo) |
| [viewengine\_text.go](https://app.codecov.io/gh/yaitoo/xun/pull/69?src=pr&el=tree&filepath=viewengine_text.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo#diff-dmlld2VuZ2luZV90ZXh0Lmdv) | 57.14% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/yaitoo/xun/pull/69?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #69      +/-   ##
==========================================
+ Coverage   91.23%   91.28%   +0.04%     
==========================================
  Files          62       62              
  Lines        2545     2490      -55     
==========================================
- Hits         2322     2273      -49     
+ Misses        192      188       -4     
+ Partials       31       29       -2     
```

| [Flag](https://app.codecov.io/gh/yaitoo/xun/pull/69/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo) | Coverage Δ | |
|---|---|---|
| [Unit-Tests](https://app.codecov.io/gh/yaitoo/xun/pull/69/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo) | `91.28% <82.35%> (+0.04%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/yaitoo/xun/pull/69?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yaitoo).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: cnlangzi
created_at: 2025-04-18T15:41:24Z
body: @sourcery-ai review
author_association: MEMBER
user: login: cnlangzi
created_at: 2025-04-18T15:48:21Z
body: @sourcery-ai review
author_association: MEMBER