Skip to content

Commit f8bf3be

Browse files
committed
feat: Extend devtools with error box
1 parent eb29c74 commit f8bf3be

File tree

7 files changed

+443
-1
lines changed

7 files changed

+443
-1
lines changed

docs/articles/devtools.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,39 @@ RFW_DEVTOOLS=1 rfw dev --debug
1010

1111
Avoid enabling it for production builds where the overlay is unnecessary.
1212

13+
## Runtime error overlay
14+
15+
### Why
16+
Surface uncaught panics and JavaScript errors directly in the browser.
17+
18+
### When to use
19+
Available in development when `RFW_DEVTOOLS=1` is set.
20+
21+
### How
22+
1. Trigger an error, e.g. `panic("boom")`.
23+
2. The overlay shows the message and stack trace.
24+
3. Use the arrow buttons to navigate multiple errors or reload the page.
25+
26+
### API
27+
The overlay hooks `window` error events through the [`js` package](../api/js) and requires no application code.
28+
29+
### Example
30+
```go
31+
func broken() {
32+
panic("boom")
33+
}
34+
```
35+
36+
@include:ExampleFrame:{code:"/examples/components/runtime_error_component.go", uri:"/examples/runtime-error"}
37+
38+
### Notes
39+
- Active only in debug builds.
40+
- Closing the overlay clears captured errors.
41+
- Stores at most 15 errors per page; additional ones are ignored.
42+
43+
### Related links
44+
- [js package](../api/js)
45+
1346
## Development Server
1447

1548
The `rfw dev` command launches a file‑watching server that recompiles your project into WebAssembly on every change. It also serves the generated assets over an HTTP server so you can iterate quickly without leaving the terminal. Any files placed in a top-level `static/` directory are available at the root URL during development, and requests to `/static/*` are transparently served as `/*`. When a `host/` directory is present, `rfw dev` builds and runs the host binary from `build/host/host` so host components can be exercised locally.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//go:build js && wasm
2+
3+
package components
4+
5+
import (
6+
_ "embed"
7+
8+
"github.com/rfwlab/rfw/v1/composition"
9+
core "github.com/rfwlab/rfw/v1/core"
10+
"github.com/rfwlab/rfw/v1/js"
11+
)
12+
13+
//go:embed templates/runtime_error_component.rtml
14+
var runtimeErrorComponentTpl []byte
15+
16+
// NewRuntimeErrorComponent demonstrates the runtime error overlay by
17+
// triggering a JavaScript error followed by a Go panic.
18+
func NewRuntimeErrorComponent() *core.HTMLComponent {
19+
cmp := composition.Wrap(core.NewComponent("RuntimeErrorComponent", runtimeErrorComponentTpl, nil))
20+
21+
cmp.SetOnMount(func(*core.HTMLComponent) {
22+
// Schedule a JavaScript error asynchronously so the overlay captures it
23+
// without crashing the Go runtime.
24+
js.Window().Call("setTimeout", "throw new Error('js example error')", 0)
25+
26+
var panicFn js.Func
27+
panicFn = js.FuncOf(func(this js.Value, args []js.Value) any {
28+
panicFn.Release()
29+
panic("example panic")
30+
return nil
31+
})
32+
js.Window().Call("setTimeout", panicFn, 100)
33+
})
34+
35+
return cmp.Unwrap()
36+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="p-4">
2+
<h1>Runtime Error Example</h1>
3+
<p>This page triggers a JavaScript error followed by a panic.</p>
4+
</div>

docs/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ func main() {
214214
Path: "/examples/netcode",
215215
Component: func() core.Component { return excomponents.NewNetcodeComponent() },
216216
})
217+
router.RegisterRoute(router.Route{
218+
Path: "/examples/runtime-error",
219+
Component: func() core.Component { return excomponents.NewRuntimeErrorComponent() },
220+
})
217221

218222
router.InitRouter()
219223
select {}

v1/composition/node.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
package composition
44

5-
import "github.com/rfwlab/rfw/v1/dom"
5+
import (
6+
"fmt"
7+
8+
"github.com/rfwlab/rfw/v1/dom"
9+
)
610

711
// Node represents a DOM node that can be appended to other nodes.
812
type Node interface {
@@ -384,3 +388,62 @@ func (b *buttonNode) Group(g *Elements) *buttonNode {
384388
}
385389
return b
386390
}
391+
392+
// headingNode builds an <h1>..<h6> element.
393+
type headingNode struct{ el dom.Element }
394+
395+
// H creates a new heading node builder for level 1..6 (out-of-range coerced to 1..6).
396+
func H(level int) *headingNode {
397+
if level < 1 {
398+
level = 1
399+
} else if level > 6 {
400+
level = 6
401+
}
402+
tag := fmt.Sprintf("h%d", level)
403+
return &headingNode{el: dom.Doc().CreateElement(tag)}
404+
}
405+
406+
// Element returns the underlying DOM element.
407+
func (h *headingNode) Element() dom.Element { return h.el }
408+
409+
// Class adds a class to the element.
410+
func (h *headingNode) Class(name string) *headingNode {
411+
h.el.AddClass(name)
412+
return h
413+
}
414+
415+
// Classes adds multiple classes to the element.
416+
func (h *headingNode) Classes(names ...string) *headingNode {
417+
for _, name := range names {
418+
h.el.AddClass(name)
419+
}
420+
return h
421+
}
422+
423+
// Style sets an inline style property on the element.
424+
func (h *headingNode) Style(prop, value string) *headingNode {
425+
h.el.SetStyle(prop, value)
426+
return h
427+
}
428+
429+
// Styles adds multiple inline style properties to the element.
430+
func (h *headingNode) Styles(props ...string) *headingNode {
431+
for i := 0; i < len(props); i += 2 {
432+
h.el.SetStyle(props[i], props[i+1])
433+
}
434+
return h
435+
}
436+
437+
// Text sets the text content of the element.
438+
func (h *headingNode) Text(t string) *headingNode {
439+
h.el.SetText(t)
440+
return h
441+
}
442+
443+
// Group adds the node to the provided group.
444+
func (h *headingNode) Group(g *Elements) *headingNode {
445+
if g != nil {
446+
g.add(h)
447+
}
448+
return h
449+
}

v1/devtools/errorbox.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package devtools
2+
3+
// runtimeError represents a captured runtime error.
4+
type runtimeError struct {
5+
Message string
6+
Stack string
7+
Path string
8+
}
9+
10+
var (
11+
errList []runtimeError
12+
errIdx int = -1
13+
)
14+
15+
const (
16+
maxRuntimeErrors = 15
17+
)
18+
19+
// addRuntimeError appends a new runtime error and sets it as current.
20+
func addRuntimeError(e runtimeError) {
21+
if len(errList) >= maxRuntimeErrors {
22+
return
23+
}
24+
errList = append(errList, e)
25+
errIdx = len(errList) - 1
26+
}
27+
28+
// currentRuntimeError returns the active error.
29+
func currentRuntimeError() (runtimeError, bool) {
30+
if errIdx < 0 || errIdx >= len(errList) {
31+
return runtimeError{}, false
32+
}
33+
return errList[errIdx], true
34+
}
35+
36+
// prevRuntimeError moves to the previous error if available.
37+
func prevRuntimeError() (runtimeError, bool) {
38+
if errIdx > 0 {
39+
errIdx--
40+
}
41+
return currentRuntimeError()
42+
}
43+
44+
// nextRuntimeError moves to the next error if available.
45+
func nextRuntimeError() (runtimeError, bool) {
46+
if errIdx < len(errList)-1 {
47+
errIdx++
48+
}
49+
return currentRuntimeError()
50+
}
51+
52+
// resetRuntimeErrors clears all tracked errors.
53+
func resetRuntimeErrors() {
54+
errList = nil
55+
errIdx = -1
56+
}
57+
58+
// runtimeErrorCount returns the number of stored errors.
59+
func runtimeErrorCount() int { return len(errList) }
60+
61+
// runtimeErrorIndex returns the current error index.
62+
func runtimeErrorIndex() int { return errIdx }

0 commit comments

Comments
 (0)