Skip to content

Commit

Permalink
added better error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewmueller committed Apr 16, 2023
1 parent ac58f06 commit 88125ec
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 107 deletions.
5 changes: 5 additions & 0 deletions internal/errs/errs.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func Join(errs ...error) error {
return agg
}

// Errors is an optional interface that be used to unwrap multiple errors
type Errors interface {
Errors() []error
}

// Format reverses the error order to make the cause come first
func Format(err error) string {
// Most errors in Bud are joined by a period
Expand Down
43 changes: 33 additions & 10 deletions package/es/es.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package es

import (
"encoding/json"
"fmt"
"strings"

Expand Down Expand Up @@ -60,11 +61,7 @@ func (b *builder) Serve(serve *Serve) (*File, error) {
result := esbuild.Build(b.serveOptions(serve))
// Check if there were errors
if result.Errors != nil {
errors := esbuild.FormatMessages(result.Errors, esbuild.FormatMessagesOptions{
Kind: esbuild.ErrorMessage,
Color: true,
})
return nil, fmt.Errorf("es: %s", strings.Join(errors, "\n"))
return nil, &Error{result.Errors}
} else if len(result.OutputFiles) == 0 {
return nil, fmt.Errorf("es: no output files")
}
Expand Down Expand Up @@ -98,11 +95,7 @@ func (b *builder) Bundle(bundle *Bundle) ([]File, error) {
result := esbuild.Build(b.bundleOptions(bundle))
// Check if there were errors
if result.Errors != nil {
errors := esbuild.FormatMessages(result.Errors, esbuild.FormatMessagesOptions{
Kind: esbuild.ErrorMessage,
Color: true,
})
return nil, fmt.Errorf("es: %s", strings.Join(errors, "\n"))
return nil, &Error{result.Errors}
} else if len(result.OutputFiles) == 0 {
return nil, fmt.Errorf("es: no output files")
}
Expand Down Expand Up @@ -164,3 +157,33 @@ func (b *builder) dom(absDir string, entries []string, plugins []esbuild.Plugin)
func isRelativeEntry(entry string) bool {
return strings.HasPrefix(entry, "./")
}

type Error struct {
messages []esbuild.Message
}

func (e *Error) Error() string {
errors := esbuild.FormatMessages(e.messages, esbuild.FormatMessagesOptions{
Color: true,
})
return strings.Join(errors, "\n\n")
}

func (e *Error) Errors() []error {
errors := make([]error, len(e.messages))
for i, message := range e.messages {
errors[i] = errorMessage(message)
}
return errors
}

type errorMessage esbuild.Message

func (e errorMessage) Error() string {
return e.Text
}

// TODO: wrap esbuild.Message to use lowercase field names
func (e errorMessage) MarshalJSON() ([]byte, error) {
return json.Marshal((esbuild.Message)(e))
}
30 changes: 23 additions & 7 deletions package/viewer/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,17 @@ func find(fsys FS, ignore func(path string) bool, pages map[Key]*Page, inherited
switch extless {
case "layout":
inherited.Layout[ext] = &View{
Path: fpath,
Key: key,
Ext: ext,
Client: viewClient(fpath),
Path: fpath,
Key: key,
Ext: ext,
}
case "frame":
inherited.Frames[ext] = append(inherited.Frames[ext], &View{
inherited.Frames[ext] = append([]*View{&View{
Path: fpath,
Key: key,
Ext: ext,
Client: viewClient(fpath),
})
}}, inherited.Frames[ext]...)
case "error":
inherited.Error[ext] = &View{
Path: fpath,
Expand All @@ -89,8 +88,25 @@ func find(fsys FS, ignore func(path string) bool, pages map[Key]*Page, inherited
}
extless := extless(de.Name())
switch extless {
case "layout", "frame", "error":
case "layout", "frame":
continue
// Errors are treated just like regular pages with frames and layouts
case "error":
key := path.Join(dir, extless)
fpath := path.Join(dir, de.Name())
pages[key] = &Page{
View: &View{
Path: fpath,
Key: key,
Ext: ext,
Client: viewClient(fpath),
},
Layout: inherited.Layout[ext],
Frames: inherited.Frames[ext],
Error: nil, // Error pages can't have their own error page
Route: route(dir, extless),
Client: entryClient(fpath),
}
default:
key := path.Join(dir, extless)
fpath := path.Join(dir, de.Name())
Expand Down
54 changes: 50 additions & 4 deletions package/viewer/find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,64 @@ func TestNested(t *testing.T) {
is.Equal(len(pages), 1)
is.True(pages["posts/index"] != nil)
is.Equal(pages["posts/index"].Path, "posts/index.svelte")
is.Equal(pages["posts/index"].Client, "/view/posts/index.svelte.entry.js")
is.Equal(pages["posts/index"].View.Client, "/view/posts/index.svelte.js")
is.Equal(pages["posts/index"].Route, "/posts")

// Frames
is.Equal(len(pages["posts/index"].Frames), 2)
is.Equal(pages["posts/index"].Frames[0].Key, "frame")
is.Equal(pages["posts/index"].Frames[0].Path, "frame.svelte")
is.Equal(pages["posts/index"].Frames[1].Key, "posts/frame")
is.Equal(pages["posts/index"].Frames[1].Path, "posts/frame.svelte")
is.Equal(pages["posts/index"].Frames[0].Key, "posts/frame")
is.Equal(pages["posts/index"].Frames[0].Path, "posts/frame.svelte")
is.Equal(pages["posts/index"].Frames[0].Client, "/view/posts/frame.svelte.js")
is.Equal(pages["posts/index"].Frames[1].Key, "frame")
is.Equal(pages["posts/index"].Frames[1].Path, "frame.svelte")
is.Equal(pages["posts/index"].Frames[1].Client, "/view/frame.svelte.js")

// Error page
is.Equal(pages["posts/index"].Error, nil)

// Layout
is.True(pages["posts/index"].Layout != nil)
is.Equal(pages["posts/index"].Layout.Key, "layout")
is.Equal(pages["posts/index"].Layout.Path, "layout.svelte")
// Layout is server-side only
is.Equal(pages["posts/index"].Layout.Client, "")
}

func TestError(t *testing.T) {
is := is.New(t)
fsys := fstest.MapFS{
"layout.svelte": &fstest.MapFile{Data: []byte(`<slot />`)},
"frame.svelte": &fstest.MapFile{Data: []byte(`<slot />`)},
"error.svelte": &fstest.MapFile{Data: []byte(`<h1>Oops!</h1>`)},
"posts/frame.svelte": &fstest.MapFile{Data: []byte(`<slot />`)},
"posts/index.svelte": &fstest.MapFile{Data: []byte(`<h1>Hello {planet}!</h1>`)},
}
// Find the pages
pages, err := viewer.Find(fsys)
is.NoErr(err)
is.Equal(len(pages), 2)

is.True(pages["error"] != nil)
is.Equal(pages["error"].Key, "error")
is.Equal(pages["error"].Path, "error.svelte")
is.Equal(pages["error"].Client, "/view/error.svelte.entry.js")
is.Equal(pages["error"].View.Client, "/view/error.svelte.js")
is.Equal(pages["error"].Route, "/error")

// Frames
is.Equal(len(pages["error"].Frames), 1)
is.Equal(pages["error"].Frames[0].Key, "frame")
is.Equal(pages["error"].Frames[0].Path, "frame.svelte")
is.Equal(pages["error"].Frames[0].Client, "/view/frame.svelte.js")

// Error page
is.Equal(pages["error"].Error, nil)

// Layout
is.True(pages["error"].Layout != nil)
is.Equal(pages["error"].Layout.Key, "layout")
is.Equal(pages["error"].Layout.Path, "layout.svelte")
// Layout is server-side only
is.Equal(pages["error"].Layout.Client, "")
}
54 changes: 34 additions & 20 deletions package/viewer/gohtml/gohtml.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,36 +96,50 @@ func (v *Viewer) Render(ctx context.Context, key string, propMap viewer.PropMap)
func (v *Viewer) RenderError(ctx context.Context, key string, propMap viewer.PropMap, originalError error) []byte {
page, ok := v.pages[key]
if !ok {
return []byte(fmt.Sprintf("gohtml: unable to find page from key %q to render error %s", key, originalError))
return []byte(fmt.Sprintf("gohtml: unable to find page from key %q to render error. %s", key, originalError))
}
v.log.Info("gohtml: rendering error", page.Error.Path)
errorEntry, err := v.parseTemplate(page.Error.Path)
if err != nil {
return []byte(fmt.Sprintf("gohtml: unable to read error template %q to render error %s. %s", page.Error.Path, err, originalError))
if page.Error == nil {
return []byte(fmt.Sprintf("gohtml: page %q has no error page to render error. %s", key, originalError))
}
errorPage, ok := v.pages[page.Error.Key]
if !ok {
return []byte(fmt.Sprintf("gohtml: unable to find error page from key %q to render error. %s", page.Error.Key, originalError))
}
// TODO: support frames
layout, err := v.parseTemplate(page.Layout.Path)
v.log.Info("gohtml: rendering error", errorPage.Path)
errorEntry, err := v.parseTemplate(errorPage.Path)
if err != nil {
return []byte(fmt.Sprintf("gohtml: unable to parse layout template %q to render error %s. %s", page.Error.Path, err, originalError))
return []byte(fmt.Sprintf("gohtml: unable to read error template %q to render error %s. %s", errorPage.Path, err, originalError))
}
state := errorState{
Message: originalError.Error(),
frames := make([]*template.Template, len(errorPage.Frames))
for i, frame := range errorPage.Frames {
frameEntry, err := v.parseTemplate(frame.Path)
if err != nil {
return []byte(fmt.Sprintf("gohtml: unable to read frame template %q to render error %s. %s", frame.Path, err, originalError))
}
frames[i] = frameEntry
}
layout, err := v.parseTemplate(errorPage.Layout.Path)
if err != nil {
return []byte(fmt.Sprintf("gohtml: unable to parse layout template %q to render error %s. %s", errorPage.Path, err, originalError))
}
html, err := render(ctx, errorEntry, state)
html, err := render(ctx, errorEntry, viewer.Error(originalError))
if err != nil {
return []byte(fmt.Sprintf("gohtml: unable to render error template %q to render error %s. %s", page.Error.Path, err, originalError))
return []byte(fmt.Sprintf("gohtml: unable to render error template %q to render error %s. %s", errorPage.Path, err, originalError))
}
for i, frame := range errorPage.Frames {
// TODO: support other props
html, err = render(ctx, frames[i], template.HTML(html))
if err != nil {
return []byte(fmt.Sprintf("gohtml: unable to render frame template %q to render error %s. %s", frame.Path, err, originalError))
}
}
html, err = render(ctx, layout, template.HTML(html))
if err != nil {
return []byte(fmt.Sprintf("gohtml: unable to render layout template %q to render error %s. %s", page.Error.Path, err, originalError))
return []byte(fmt.Sprintf("gohtml: unable to render layout template %q to render error %s. %s", errorPage.Path, err, originalError))
}
return html
}

type errorState struct {
Message string
}

func (v *Viewer) Bundle(ctx context.Context, fs virtual.Tree) (err error) {
for _, page := range v.pages {
// Embed the page
Expand Down Expand Up @@ -161,14 +175,14 @@ func (v *Viewer) Bundle(ctx context.Context, fs virtual.Tree) (err error) {

// Embed the error
if page.Error != nil {
if _, ok := fs[page.Error.Path]; ok {
if _, ok := fs[page.Path]; ok {
continue
}
errorEmbed, err := v.embedView(page.Error.Path)
errorEmbed, err := v.embedView(page.Path)
if err != nil {
return err
}
fs[page.Error.Path] = errorEmbed
fs[page.Path] = errorEmbed
}
}
return nil
Expand Down
50 changes: 44 additions & 6 deletions package/viewer/gohtml/gohtml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,57 @@ func TestRenderError(t *testing.T) {
log := testlog.New()
fsys := virtual.Map{
"index.gohtml": "Hello {{ .Planet }}!",
"frame.gohtml": `<main>{{ . }}</main>`,
"layout.gohtml": "<html>{{ . }}</html>",
"error.gohtml": `<div class="error">{{ .Message }}</div>`,
}
pages, err := viewer.Find(fsys)
is.NoErr(err)
viewer := gohtml.New(fsys, log, pages, transpiler.New())
gohtml := gohtml.New(fsys, log, pages, transpiler.New())
ctx := context.Background()
html := viewer.RenderError(ctx, "index", map[string]interface{}{
"index": map[string]interface{}{
"Planet": "Earth",
},
html := gohtml.RenderError(ctx, "index", map[string]interface{}{
"index": map[string]interface{}{"Planet": "Earth"},
}, errors.New("some error"))
is.Equal(string(html), `<html><main><div class="error">some error</div></main></html>`)
}

// TODO: this should have a default page
func TestRenderErrorNoPage(t *testing.T) {
is := is.New(t)
log := testlog.New()
fsys := virtual.Map{
"index.gohtml": "Hello {{ .Planet }}!",
"frame.gohtml": `<main>{{ . }}</main>`,
"layout.gohtml": "<html>{{ . }}</html>",
}
pages, err := viewer.Find(fsys)
is.NoErr(err)
gohtml := gohtml.New(fsys, log, pages, transpiler.New())
ctx := context.Background()
html := gohtml.RenderError(ctx, "index", map[string]interface{}{
"index": map[string]interface{}{"Planet": "Earth"},
}, errors.New("some error"))
is.Equal(string(html), `gohtml: page "index" has no error page to render error. some error`)
}

func TestRenderErrorWithFrames(t *testing.T) {
is := is.New(t)
log := testlog.New()
fsys := virtual.Map{
"posts/index.gohtml": "Hello {{ .Planet }}!",
"posts/frame.gohtml": `<div class="posts">{{ . }}</div>`,
"frame.gohtml": `<main>{{ . }}</main>`,
"layout.gohtml": "<html>{{ . }}</html>",
"error.gohtml": `<div class="error">{{ .Message }}</div>`,
}
pages, err := viewer.Find(fsys)
is.NoErr(err)
gohtml := gohtml.New(fsys, log, pages, transpiler.New())
ctx := context.Background()
html := gohtml.RenderError(ctx, "posts/index", map[string]interface{}{
"posts/index": map[string]interface{}{"Planet": "Earth"},
}, errors.New("some error"))
is.Equal(string(html), `<html><div class="error">some error</div></html>`)
is.Equal(string(html), `<html><main><div class="error">some error</div></main></html>`)
}

func TestBundle(t *testing.T) {
Expand Down
Loading

0 comments on commit 88125ec

Please sign in to comment.