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 }