Files
jscriptx/engine_globals.go
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

109 lines
4.4 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 "github.com/dop251/goja"
// Go 值 → 只读 JS 全局的转换层。
//
// 脚本能看见的全局只有这里放行的:WithGlobals 配的白名单,加上作用域带来的扩展。
// 别的一概没有——没有 require、没有 fs、没有 processgoja 本身也不提供。
//
// 注入只在**建 VM 时**发生一次,对每次调用零开销;而且是惰性的(见 lazyGlobal),
// 脚本没读到的全局根本不会被转换。
// bind 把白名单全局对象注入到一个新建的 VM 里。
// extra 是这个 VM 专属的额外全局(作用域带来的扩展),可以为 nil;
// 它跟白名单同样按只读注入,同名时以 extra 为准。
//
// 注入是**惰性**的:这里只装一个 getter,脚本第一次读到那个名字才把值转成
// JS 对象。一个脚本通常只用得上少数几个扩展,而 freeze 要把每个方法都包装成
// JS 函数——用不到的那些不该在每次建 VM 时都付一遍这个成本。见 lazyGlobal。
func (e *Engine) bind(rt *goja.Runtime, script string, extra map[string]any) error {
global := rt.GlobalObject()
for name, val := range e.globals {
if _, overridden := extra[name]; overridden {
continue
}
if err := e.lazyGlobal(rt, global, name, val); err != nil {
return newError(KindBind, script, "", err, "注入全局对象 %q 失败", name)
}
}
for name, val := range extra {
if err := e.lazyGlobal(rt, global, name, val); err != nil {
return newError(KindBind, script, "", err, "注入作用域全局对象 %q 失败", name)
}
}
if e.logger != nil {
_, taken := e.globals["console"]
if _, t2 := extra["console"]; t2 {
taken = true
}
if !taken {
if err := e.lazyGlobal(rt, global, "console", newConsole(e.logger, script)); err != nil {
return newError(KindBind, script, "", err, "注入 console 失败")
}
}
}
return nil
}
// lazyGlobal 装一个惰性只读全局:脚本第一次读它才 freeze,之后复用。
//
// 为什么惰性:freeze 要把 map 里每个方法都包装成 JS 函数对象,而一个脚本通常只用
// 得上少数几个扩展。急切注入的话,每建一个 VM 都要为**所有**扩展付这份成本——
// 实测这是脚本层剩余开销里最大的一块。
//
// 缓存放在闭包里,不加锁:getter 只在脚本执行期间被调用,而那时这个 VM 是被独占的
// (实例持着自己的锁,池化的 VM 同时只有一个借用者)。goja.Value 也跨不了 Runtime
// 所以这份缓存天然是每 VM 一份。
//
// 只给 getter 不给 setter,效果等同原来的 writable=false:脚本赋值时没有 setter 可调。
// 产物跑在非严格模式下(见 compileLocked 那里的说明),所以赋值是**静默失败**——
// 不抛错,值也不变。configurable 同样保持 false,删不掉也重定义不了。
//
// 代价是 freeze 的错误从"建 VM 时返回 Go 错误"变成"脚本读它时抛 JS 异常"。
// freeze 只在属性名不合法时才会失败,而 New() 里的 validIdent 已经挡过一道,
// 实际碰不到。
func (e *Engine) lazyGlobal(rt *goja.Runtime, global *goja.Object, name string, val any) error {
var (
cached goja.Value
failed error
)
getter := rt.ToValue(func(goja.FunctionCall) goja.Value {
if cached == nil && failed == nil {
cached, failed = e.freeze(rt, val)
}
if failed != nil {
panic(rt.NewGoError(failed))
}
return cached
})
return global.DefineAccessorProperty(name, getter, nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
}
// freeze 把 map[string]any 递归转成只读的 JS 对象,其他值原样交给 goja 包装。
//
// 直接 rt.Set(name, someMap) 会把同一个 Go map 暴露给每个 VM:脚本一句
// db.C = null 既能污染别的 VM,又是实打实的数据竞争。这里每个 VM 都拿到
// 自己的一份不可写、不可重定义的对象。
func (e *Engine) freeze(rt *goja.Runtime, val any) (goja.Value, error) {
m, ok := val.(map[string]any)
if !ok {
return rt.ToValue(val), nil
}
obj := rt.NewObject()
for k, v := range m {
child, err := e.freeze(rt, v)
if err != nil {
return nil, err
}
if err := defineReadOnly(obj, k, child); err != nil {
return nil, err
}
}
return obj, nil
}
func defineReadOnly(obj *goja.Object, name string, v goja.Value) error {
return obj.DefineDataProperty(name, v, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE)
}