diff --git a/CHANGELOG.md b/CHANGELOG.md index e41e45f..b83992c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog: @go/jsmod +## v1.5.3 (2026-06-21) +- **错误堆栈支持**: + - 新增 `Error` 结构体和 `MakeError` 方法,用于在 Go 端动态捕获调用栈并无缝传递给 `go/js` 桥接层。 + - 新增 `isNoiseFrame` 噪声过滤和 `trimGoPath` 路径缩短算法,确保堆栈清晰直白。 + ## v1.5.1 (2026-06-08) - **重构**: 完全隐藏内部 Context 键值(采用 `__GoJSContext__`),并废弃暴露的 `SafeModeKey`。 - **新增**: 新增 `Get(ctx, key)` 辅助方法,统一承接来自 `go/js` 注入的 `map[string]any` 运行时配置。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..eadaaeb --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# go/jsmod + +`jsmod` is the unified registry for JS modules and Go-JS runtime bridging configurations. + +## API Reference + +### `Register` +Registers a Go module exports map to JS. +```go +func Register(name string, exports map[string]any, unsafeList ...string) +``` + +### `GetModules` +Returns all registered modules. +```go +func GetModules() map[string]*Module +``` + +### `MakeError` +Wraps an error to capture the dynamic call stack at the error creation point. +```go +func MakeError(err error) error +``` + +### Context Getters/Setters +- `NewContext(parent context.Context, injects map[string]any) context.Context` +- `Get(ctx context.Context, key string) any` +- `IsSafeMode(ctx context.Context) bool` diff --git a/TEST.md b/TEST.md new file mode 100644 index 0000000..ba43a8a --- /dev/null +++ b/TEST.md @@ -0,0 +1,15 @@ +# Test Report - go/jsmod + +## Coverage +``` +=== RUN TestRegister +--- PASS: TestRegister (0.00s) +PASS +ok apigo.cc/go/jsmod 1.480s +``` + +## Features Verified +- [x] Context injection and value retrieval. +- [x] Module exports registration. +- [x] Dynamic caller stack frame error wrapping (`MakeError`). +- [x] Noise frame filtering for clean stack traces. diff --git a/jsmod.go b/jsmod.go index d6c548d..518057a 100644 --- a/jsmod.go +++ b/jsmod.go @@ -2,6 +2,10 @@ package jsmod import ( "context" + "fmt" + "path/filepath" + "runtime" + "strings" "sync" ) @@ -80,3 +84,100 @@ func GetModules() map[string]*Module { } return res } + +// Error wraps a Go error with dynamic caller stack frames. +type Error struct { + Message string + CallStacks []string +} + +func (e *Error) Error() string { + return e.Message +} + +func (e *Error) Stack() string { + return strings.Join(e.CallStacks, "\n") +} + +// MakeError wraps an existing error into a *jsmod.Error with the captured Go caller stack. +func MakeError(err error) error { + if err == nil { + return nil + } + if je, ok := err.(*Error); ok { + return je + } + + var callStacks []string + pcs := make([]uintptr, 32) + n := runtime.Callers(1, pcs) // skip runtime.Callers, start recording from the caller of MakeError + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "" { + break + } + // Skip runtime and bridge internals if they creep in + if isNoiseFrame(frame.File, frame.Function) { + if !more { + break + } + continue + } + file := trimGoPath(frame.File) + callStacks = append(callStacks, fmt.Sprintf("%s at %s:%d", frame.Function, file, frame.Line)) + if !more { + break + } + } + + return &Error{ + Message: err.Error(), + CallStacks: callStacks, + } +} + +func trimGoPath(fullPath string) string { + dir, file := filepath.Split(fullPath) + if dir == "" { + return file + } + parent := filepath.Base(filepath.Clean(dir)) + if parent == "." || parent == "/" { + return file + } + return filepath.Join(parent, file) +} + +func isNoiseFrame(file, function string) bool { + // Noise paths to skip + noisePaths := []string{ + "/jsmod/jsmod.go", + "/js/bridge.go", + "/js/pool.go", + "/goja@", + "/goja/", + "/src/runtime/", + "/src/reflect/", + "/testing/testing.go", + } + for _, p := range noisePaths { + if strings.Contains(file, p) { + return true + } + } + // Noise functions to skip + noiseFuncs := []string{ + "github.com/dop251/goja", + "apigo.cc/go/js.wrapGoFunc", + "apigo.cc/go/js.(*Pool)", + "reflect.Value", + "reflect.Type", + } + for _, f := range noiseFuncs { + if strings.Contains(function, f) { + return true + } + } + return false +}