Files
jscriptx/bundle_finalize.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
3.5 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 (
"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
}