Files
what 7e1893b246 refactor: 文件按主类型划分,一个 struct 一个功能
约定:文件名 = 主类型名;同一个类型要拆多个文件时用 类型_子项.go。

  engine.go          355 → 231 行。原来混了三件不相干的事
  engine_globals.go  ← bind / lazyGlobal / freeze / defineReadOnly(96 行)
                       它是「Go 值 → 只读 JS 全局」的转换层,跟脚本缓存毫无关系
  engine_console.go  ← console.go,跟上面是同一主题
  script.go          325 → 100 行,只留公开方法
  script_vm.go       ← VM 的取、还、装载。上个 commit 合一的三条路径现在住一起
  script_static.go   ← static.go
  errors.go          364 → 150 行
  errors_goja.go     ← goja 错误的翻译层
  errors_hints.go    ← missingGlobalHint,一份 JS 运行时知识库,跟错误分类是
                       两回事;拆出来之后 missing_global_test.go 才有对应源文件
  bundle_finalize.go ← esmwrap.go
  bundle.go          收下 validIdent / hashVersion(原来住在 engine.go)

Engine.New 原来排在所有私有函数之后,挪到导出方法那一段。

classify 83 → 54 行:四个 errors.As 分支各手搓一个 8 字段的 &Error{},脚本上下文
那三行重复了 4 遍,抽出 gojaError 构造器。

顺带修四处注释漂移:

  Session/会话   代码里叫 Instance,注释里大面积残留。engine_globals.go 那条
                 「注入会话全局对象失败」还是用户可见文案,而公开 API 里根本
                 没有「会话」这个概念
  Extension      文档示例写 Module() string,接口是 Module() (path, source string)。
                 这是唯一一段教人写扩展的文档,照抄编译不过
  doc.go 的 freeze  说白名单「逐层拷贝成只读对象,不会跨 VM 共享可变的 Go map」,
                 但那只对 map[string]any 成立。结构体指针和 slice 是**共享同一个
                 对象**的——扩展走的正是这条路,不该被当成隔离保证
2026-09-10 15:28:59 +08:00

151 lines
5.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package jscriptx
import (
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
)
// 哨兵错误,配合 errors.Is 使用。
var (
// ErrScriptNotFound 找不到脚本(Loader 没有这个名字,也没被 Compile 注册过)。
ErrScriptNotFound = errors.New("jscriptx: 脚本不存在")
// ErrFuncNotFound 脚本里找不到要调用的函数,或者那个名字不是函数。
ErrFuncNotFound = errors.New("jscriptx: 脚本里没有这个函数")
// ErrTimeout 脚本执行超过配置时限,已被强制中断。
ErrTimeout = errors.New("jscriptx: 脚本执行超时")
// ErrInterrupted 脚本被中断,但拿不到更具体的原因。
ErrInterrupted = errors.New("jscriptx: 脚本被中断")
// ErrClosed 脚本或引擎已经关闭。
ErrClosed = errors.New("jscriptx: 已关闭")
// ErrValueEscape 脚本试图把只在 VM 内部有效的值(函数/闭包)传到 Go 侧。
ErrValueEscape = errors.New("jscriptx: 该值不能跨出脚本边界")
// ErrBadGlobal 全局白名单配置不合法。
ErrBadGlobal = errors.New("jscriptx: 全局白名单配置不合法")
// ErrPromiseRejected async 方法返回的 Promise 被 reject。
ErrPromiseRejected = errors.New("jscriptx: Promise 被 reject")
// ErrPromisePending async 方法返回的 Promise 一直没完成(goja 没有事件循环)。
ErrPromisePending = errors.New("jscriptx: Promise 未完成")
)
// Kind 是错误分类,用于结构化日志过滤和调用方分流处理。
type Kind string
const (
KindLoad Kind = "load" // 加载脚本源码失败
KindCompile Kind = "compile" // 编译(语法解析)失败
KindBind Kind = "bind" // 注入全局白名单失败
KindNotFound Kind = "not_found" // 脚本或函数不存在
KindRuntime Kind = "runtime" // 脚本运行期抛出异常
KindTimeout Kind = "timeout" // 超时被中断
KindCanceled Kind = "canceled" // 调用方 context 被取消
KindPanic Kind = "panic" // Go 侧 panic,已兜住转成 error
KindType Kind = "type" // 返回值/参数类型不匹配
KindClosed Kind = "closed" // 脚本已关闭
)
// Frame 是脚本调用栈的一帧。从 goja 的栈信息里摘出来重新包装,
// 避免把 goja 类型暴露到本库的公开 API 上。
type Frame struct {
Source string // 脚本名
Func string // 函数名,匿名函数为空
Line int
Column int
}
func (f Frame) String() string {
name := f.Func
if name == "" {
name = "<anonymous>"
}
return fmt.Sprintf("%s (%s:%d:%d)", name, f.Source, f.Line, f.Column)
}
// Error 是本库对外抛出的统一错误类型,带齐排查现场需要的上下文:
// 哪个脚本、哪个函数、脚本里哪一行、当时的调用参数是什么。
// 它实现了 slog.LogValuerslog.Any("err", err) 就能摊平成结构化字段。
type Error struct {
Kind Kind // 错误分类
Script string // 脚本名
Func string // 被调用的函数名,可能为空
Msg string // 人类可读的说明
Args []string // 调用参数摘要(已截断,只用于排查,不保证可反序列化)
Stack []Frame // 脚本侧调用栈,可能为空
Value any // 脚本 throw 出来的原始值(已导出成 Go 值)
GoStack string // Go 侧调用栈,仅 KindPanic 时填充
Cause error // 底层错误,errors.Is/As 沿这条链走
}
func (e *Error) Error() string {
var b strings.Builder
b.WriteString("jscriptx: 脚本 ")
b.WriteString(strconv.Quote(e.Script))
if e.Func != "" {
b.WriteString(" 函数 ")
b.WriteString(strconv.Quote(e.Func))
}
b.WriteString(" [")
b.WriteString(string(e.Kind))
b.WriteString("]")
if e.Msg != "" {
b.WriteString(": ")
b.WriteString(e.Msg)
}
if len(e.Stack) > 0 {
b.WriteString(" at ")
b.WriteString(e.Stack[0].String())
}
if e.Cause != nil && e.Cause.Error() != e.Msg {
b.WriteString(" (")
b.WriteString(e.Cause.Error())
b.WriteString(")")
}
return b.String()
}
func (e *Error) Unwrap() error { return e.Cause }
// LogValue 实现 slog.LogValuer。
func (e *Error) LogValue() slog.Value {
attrs := []slog.Attr{
slog.String("kind", string(e.Kind)),
slog.String("script", e.Script),
}
if e.Func != "" {
attrs = append(attrs, slog.String("func", e.Func))
}
if e.Msg != "" {
attrs = append(attrs, slog.String("msg", e.Msg))
}
if len(e.Stack) > 0 {
attrs = append(attrs,
slog.Int("line", e.Stack[0].Line),
slog.Int("column", e.Stack[0].Column),
slog.String("at", e.Stack[0].String()),
)
}
if len(e.Args) > 0 {
attrs = append(attrs, slog.Any("args", e.Args))
}
if e.GoStack != "" {
attrs = append(attrs, slog.String("go_stack", e.GoStack))
}
if e.Cause != nil {
attrs = append(attrs, slog.String("cause", e.Cause.Error()))
}
return slog.GroupValue(attrs...)
}
// newError 造一个带脚本上下文的错误。
func newError(kind Kind, script, fn string, cause error, format string, a ...any) *Error {
return &Error{
Kind: kind,
Script: script,
Func: fn,
Msg: fmt.Sprintf(format, a...),
Cause: cause,
}
}