约定:文件名 = 主类型名;同一个类型要拆多个文件时用 类型_子项.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 是**共享同一个
对象**的——扩展走的正是这条路,不该被当成隔离保证
109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
package jscriptx
|
||
|
||
import (
|
||
"strings"
|
||
)
|
||
|
||
// esbuild 按 IIFE 格式输出时,会在产物里塞一整套 CommonJS interop helper
|
||
// (__defProp / __export / __copyProps / __toCommonJS…)。那套东西是为了模拟
|
||
// __esModule 语义,本库根本用不上:我们只要拿到导出对象。
|
||
//
|
||
// 但它的代价是实打实的——每建一个 VM 都要重新创建那 7 个函数、再遍历一遍属性装
|
||
// getter。实测建一个实例 22μs / 472 allocs,而不带 helper 的等价产物只要 4μs / 119。
|
||
//
|
||
// 所以改成按 ESM 格式打包(import 照样内联),再自己把末尾那句 export 改写掉:
|
||
//
|
||
// // p.ts (() => {// p.ts
|
||
// var H = class {…}; ───► var H = class {…};
|
||
// export { return H;})()
|
||
// H as default
|
||
// };
|
||
//
|
||
// 改写有两条硬约束:
|
||
//
|
||
// - 开头的 (() => { 必须紧贴原第一行,不能另起一行,否则所有行号下移一位,
|
||
// sourcemap 就对不上了,报错定位不回 .ts 源码
|
||
// - export 语句在产物末尾,把它整段换掉不影响前面任何行
|
||
|
||
// wrapESM 把 esbuild 的 ESM 产物改写成 goja 能直接执行的形式:
|
||
// 一个立即执行函数,完成值就是脚本的导出。
|
||
//
|
||
// 认不出末尾的 export 语句时(脚本压根没有导出,或者 esbuild 换了输出格式),
|
||
// 返回的产物求值为 undefined——交给"脚本没有任何导出"那条报错去解释。
|
||
func wrapESM(code string) string {
|
||
body, entry, ok := splitESMExports(code)
|
||
if !ok {
|
||
// 没有导出:让它求值成 undefined,报错由 lookup 那边给
|
||
return "(() => {" + code + "\nreturn void 0;})()\n"
|
||
}
|
||
return "(() => {" + body + "return " + entry + ";})()\n"
|
||
}
|
||
|
||
// splitESMExports 从产物末尾切下 export 语句,返回前面的代码和入口表达式。
|
||
//
|
||
// esbuild 的 ESM 输出格式很规整,末尾总是这样:
|
||
//
|
||
// export {
|
||
// H as default,
|
||
// extra
|
||
// };
|
||
func splitESMExports(code string) (body, entry string, ok bool) {
|
||
i := strings.LastIndex(code, "\nexport {")
|
||
if i < 0 {
|
||
return "", "", false
|
||
}
|
||
j := strings.Index(code[i:], "\n};")
|
||
if j < 0 {
|
||
return "", "", false
|
||
}
|
||
body = code[:i+1]
|
||
tail := code[i+len("\nexport {") : i+j]
|
||
|
||
names := parseExportNames(tail)
|
||
if len(names) == 0 {
|
||
return "", "", false
|
||
}
|
||
|
||
// 有 default 就用它——这跟"脚本导出 class/函数/实例"的入口约定对得上;
|
||
// 只有命名导出时,把它们拼成一个对象,按方法名调用。
|
||
if local, has := names["default"]; has {
|
||
return body, local, true
|
||
}
|
||
var b strings.Builder
|
||
b.WriteByte('{')
|
||
first := true
|
||
for exported, local := range names {
|
||
if !first {
|
||
b.WriteByte(',')
|
||
}
|
||
first = false
|
||
b.WriteString(exported)
|
||
b.WriteByte(':')
|
||
b.WriteString(local)
|
||
}
|
||
b.WriteByte('}')
|
||
return body, b.String(), true
|
||
}
|
||
|
||
// parseExportNames 解析 export 语句体,返回 导出名 -> 本地名。
|
||
// 每项形如 "H as default" 或 "foo"(导出名跟本地名相同)。
|
||
func parseExportNames(tail string) map[string]string {
|
||
out := map[string]string{}
|
||
for _, item := range strings.Split(tail, ",") {
|
||
item = strings.TrimSpace(item)
|
||
if item == "" {
|
||
continue
|
||
}
|
||
local, exported := item, item
|
||
if k := strings.Index(item, " as "); k >= 0 {
|
||
local = strings.TrimSpace(item[:k])
|
||
exported = strings.TrimSpace(item[k+len(" as "):])
|
||
}
|
||
if !validIdent(local) || !validIdent(exported) {
|
||
return nil // 格式不认识,交给调用方走兜底
|
||
}
|
||
out[exported] = local
|
||
}
|
||
return out
|
||
}
|