2024-02-17 12:55:08 +08:00
|
|
|
package gojs
|
|
|
|
|
|
|
|
import (
|
|
|
|
"apigo.cloud/git/apigo/plugin"
|
|
|
|
"apigo.cloud/git/apigo/qjs"
|
|
|
|
"errors"
|
2024-02-18 13:20:58 +08:00
|
|
|
"fmt"
|
2024-02-17 12:55:08 +08:00
|
|
|
"github.com/ssgo/log"
|
|
|
|
"github.com/ssgo/u"
|
2024-03-15 13:40:20 +08:00
|
|
|
"path"
|
2024-02-17 12:55:08 +08:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
var pluginNameMatcher = regexp.MustCompile(`(\w+?)\.`)
|
2024-03-15 13:40:20 +08:00
|
|
|
var exportMatcher = regexp.MustCompile(`export\s+([\w{}, ]+)\s*;?`)
|
|
|
|
var importMatcher = regexp.MustCompile(`import\s+([\w{}, ]+)\s+from\s+['"]([\w./\- ]+)['"]`)
|
|
|
|
|
|
|
|
type RuntimeOption struct {
|
|
|
|
Globals map[string]interface{}
|
|
|
|
Imports map[string]string
|
|
|
|
Logger *log.Logger
|
|
|
|
}
|
2024-02-17 12:55:08 +08:00
|
|
|
|
2024-02-18 13:20:58 +08:00
|
|
|
type JSRuntime struct {
|
2024-03-15 13:40:20 +08:00
|
|
|
imports map[string]string
|
|
|
|
imported map[string]string
|
2024-02-18 13:20:58 +08:00
|
|
|
freeJsValues []quickjs.Value
|
|
|
|
rt quickjs.Runtime
|
|
|
|
JsCtx *quickjs.Context
|
|
|
|
GoCtx *plugin.Context
|
|
|
|
logger *log.Logger
|
|
|
|
plugins map[string]*plugin.Plugin
|
|
|
|
}
|
2024-02-17 12:55:08 +08:00
|
|
|
|
2024-02-18 13:20:58 +08:00
|
|
|
func (rt *JSRuntime) Close() {
|
|
|
|
for _, v := range rt.freeJsValues {
|
|
|
|
v.Free()
|
|
|
|
}
|
|
|
|
rt.freeJsValues = make([]quickjs.Value, 0)
|
|
|
|
rt.JsCtx.Close()
|
|
|
|
rt.rt.Close()
|
|
|
|
}
|
|
|
|
|
2024-03-16 22:50:05 +08:00
|
|
|
var pluginIdFixer = regexp.MustCompile("[^a-zA-Z0-9_]")
|
|
|
|
|
|
|
|
func fixPluginId(id string) string {
|
|
|
|
return "_" + pluginIdFixer.ReplaceAllString(id, "_")
|
|
|
|
}
|
|
|
|
|
2024-03-15 13:40:20 +08:00
|
|
|
func (rt *JSRuntime) run(code string) (out interface{}, err error, stack string) {
|
|
|
|
// support import
|
2024-03-16 22:50:05 +08:00
|
|
|
tryPlugins := map[string]bool{}
|
2024-03-15 13:40:20 +08:00
|
|
|
code = importMatcher.ReplaceAllStringFunc(code, func(importStr string) string {
|
|
|
|
m := importMatcher.FindStringSubmatch(importStr)
|
|
|
|
importVar := rt.imported[m[2]]
|
|
|
|
if importVar == "" {
|
|
|
|
baseName := path.Base(m[2])
|
|
|
|
jsFile := m[2]
|
|
|
|
isTS := false
|
|
|
|
if strings.HasSuffix(baseName, ".ts") {
|
|
|
|
isTS = true
|
|
|
|
baseName = baseName[0 : len(baseName)-3]
|
|
|
|
}
|
|
|
|
if strings.HasSuffix(baseName, ".js") {
|
|
|
|
baseName = baseName[0 : len(baseName)-3]
|
|
|
|
} else {
|
|
|
|
jsFile += ".js"
|
|
|
|
}
|
|
|
|
if !isTS && (rt.imports[m[2]] != "" || u.FileExists(jsFile)) {
|
|
|
|
importCode := rt.imports[m[2]]
|
|
|
|
if importCode == "" {
|
|
|
|
importCode, _ = u.ReadFile(jsFile)
|
|
|
|
}
|
|
|
|
if importCode != "" {
|
|
|
|
importVar = "import_" + u.UniqueId()
|
|
|
|
rt.imported[m[2]] = importVar
|
|
|
|
importedCode := exportMatcher.ReplaceAllStringFunc(importCode, func(exportStr string) string {
|
2024-03-16 22:50:05 +08:00
|
|
|
if strings.Contains(exportStr, "export default") {
|
|
|
|
exportStr = strings.Replace(exportStr, "export default", "return", 1)
|
|
|
|
}
|
|
|
|
exportStr = strings.Replace(exportStr, "export", "return", 1)
|
|
|
|
return exportStr
|
2024-03-15 13:40:20 +08:00
|
|
|
})
|
|
|
|
err, stack := rt.Exec("let " + importVar + " = (function(){" + importedCode + "})()")
|
|
|
|
if err != nil {
|
|
|
|
rt.logger.Error(err.Error(), "stack", stack)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
importVar = "{}"
|
|
|
|
}
|
|
|
|
return "let " + m[1] + " = " + importVar
|
|
|
|
} else {
|
2024-03-16 22:50:05 +08:00
|
|
|
if plg := plugin.Get(m[2]); plg != nil {
|
|
|
|
tryPlugins[m[2]] = true
|
|
|
|
return "let " + m[1] + " = " + fixPluginId(m[2])
|
|
|
|
}
|
2024-03-15 13:40:20 +08:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return "let " + m[1] + " = " + importVar
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2024-03-16 22:50:05 +08:00
|
|
|
//tryPlugins := map[string]bool{}
|
|
|
|
//for _, m := range pluginNameMatcher.FindAllStringSubmatch(code, 1024) {
|
|
|
|
// tryPlugins[m[1]] = true
|
|
|
|
//}
|
2024-02-17 12:55:08 +08:00
|
|
|
|
|
|
|
for _, plg := range plugin.List() {
|
2024-02-18 13:20:58 +08:00
|
|
|
if tryPlugins[plg.Id] && rt.plugins[plg.Id] == nil {
|
2024-03-16 22:50:05 +08:00
|
|
|
//if rt.plugins[plg.Id] == nil {
|
2024-02-18 13:20:58 +08:00
|
|
|
rt.plugins[plg.Id] = &plg
|
2024-03-16 22:50:05 +08:00
|
|
|
rt.JsCtx.Globals().Set(fixPluginId(plg.Id), MakeJsValueForPlugin(rt.GoCtx, plg.Objects, plg.Id, false))
|
2024-02-17 12:55:08 +08:00
|
|
|
if plg.JsCode != "" {
|
2024-02-18 13:20:58 +08:00
|
|
|
if result, err := rt.JsCtx.Eval(plg.JsCode); err != nil {
|
2024-02-17 12:55:08 +08:00
|
|
|
stack := GetJSError(err, plg.JsCode)
|
2024-02-18 13:20:58 +08:00
|
|
|
rt.logger.Error(err.Error(), "stack", stack)
|
2024-02-17 12:55:08 +08:00
|
|
|
} else {
|
|
|
|
result.Free()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-18 13:20:58 +08:00
|
|
|
if r, err := rt.JsCtx.Eval(code); err == nil {
|
2024-02-17 12:55:08 +08:00
|
|
|
result := MakeFromJsValue(r)
|
|
|
|
r.Free()
|
2024-02-18 13:20:58 +08:00
|
|
|
return result, nil, ""
|
2024-02-17 12:55:08 +08:00
|
|
|
} else {
|
|
|
|
// 检查错误
|
|
|
|
stack := GetJSError(err, code)
|
2024-02-18 13:20:58 +08:00
|
|
|
rt.logger.Error(err.Error(), "stack", stack)
|
|
|
|
return nil, err, stack
|
2024-02-17 12:55:08 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-18 13:20:58 +08:00
|
|
|
func (rt *JSRuntime) Exec(code string) (err error, stack string) {
|
|
|
|
_, err, stack = rt.run(code)
|
|
|
|
return err, stack
|
|
|
|
}
|
|
|
|
|
2024-03-15 13:40:20 +08:00
|
|
|
func (rt *JSRuntime) ExecFile(filename string) (err error, stack string) {
|
|
|
|
if code, err := u.ReadFile(filename); err == nil {
|
|
|
|
return rt.Exec(code)
|
|
|
|
} else {
|
|
|
|
return err, ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-18 13:20:58 +08:00
|
|
|
func (rt *JSRuntime) Run(code string) (out interface{}, err error, stack string) {
|
|
|
|
return rt.run("(function(){" + code + "})()")
|
|
|
|
}
|
|
|
|
|
2024-03-15 13:40:20 +08:00
|
|
|
func (rt *JSRuntime) RunFile(filename string) (out interface{}, err error, stack string) {
|
|
|
|
if code, err := u.ReadFile(filename); err == nil {
|
|
|
|
return rt.Run(code)
|
|
|
|
} else {
|
|
|
|
return nil, err, ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-18 13:20:58 +08:00
|
|
|
func SetPluginsConfig(conf map[string]plugin.Config) {
|
|
|
|
for _, plg := range plugin.List() {
|
|
|
|
if plg.Init != nil {
|
|
|
|
plg.Init(conf[plg.Id])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-15 13:40:20 +08:00
|
|
|
func New(option *RuntimeOption) *JSRuntime {
|
|
|
|
if option == nil {
|
|
|
|
option = &RuntimeOption{nil, map[string]string{}, log.DefaultLogger}
|
|
|
|
}
|
|
|
|
|
|
|
|
if option.Imports == nil {
|
|
|
|
option.Imports = map[string]string{}
|
|
|
|
}
|
|
|
|
if option.Logger == nil {
|
|
|
|
option.Logger = log.DefaultLogger
|
2024-02-18 13:20:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 初始化JS虚拟机
|
|
|
|
jsRt := quickjs.NewRuntime()
|
|
|
|
jsCtx := jsRt.NewContext()
|
|
|
|
goCtx := plugin.NewContext(map[string]interface{}{
|
2024-03-15 13:40:20 +08:00
|
|
|
"*log.Logger": option.Logger,
|
2024-02-18 13:20:58 +08:00
|
|
|
"*quickjs.Context": jsCtx,
|
|
|
|
})
|
|
|
|
|
|
|
|
rt := &JSRuntime{
|
2024-03-15 13:40:20 +08:00
|
|
|
imports: option.Imports,
|
|
|
|
imported: map[string]string{},
|
2024-02-18 13:20:58 +08:00
|
|
|
freeJsValues: make([]quickjs.Value, 0),
|
|
|
|
rt: jsRt,
|
|
|
|
JsCtx: jsCtx,
|
|
|
|
GoCtx: goCtx,
|
2024-03-15 13:40:20 +08:00
|
|
|
logger: option.Logger,
|
2024-02-18 13:20:58 +08:00
|
|
|
plugins: map[string]*plugin.Plugin{},
|
|
|
|
}
|
|
|
|
|
|
|
|
rt.GoCtx.SetData("_freeJsValues", &rt.freeJsValues)
|
|
|
|
|
|
|
|
// 全局变量
|
2024-03-15 13:40:20 +08:00
|
|
|
if option.Globals != nil {
|
|
|
|
for k, obj := range option.Globals {
|
2024-02-18 13:20:58 +08:00
|
|
|
rt.JsCtx.Globals().Set(k, MakeJsValue(rt.GoCtx, obj, false))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 注入 console
|
|
|
|
rt.JsCtx.Globals().Set("console", MakeJsValue(rt.GoCtx, map[string]interface{}{
|
|
|
|
"log": func(args ...interface{}) {
|
2024-03-08 11:45:13 +08:00
|
|
|
fmt.Println(makeStringArray(args, u.TextNone, u.BgNone)...)
|
2024-02-18 13:20:58 +08:00
|
|
|
},
|
|
|
|
"info": func(args ...interface{}) {
|
2024-03-08 11:45:13 +08:00
|
|
|
fmt.Println(makeStringArray(args, u.TextCyan, u.BgNone)...)
|
2024-02-18 13:20:58 +08:00
|
|
|
},
|
|
|
|
"warn": func(args ...interface{}) {
|
2024-03-08 11:45:13 +08:00
|
|
|
fmt.Println(makeStringArray(args, u.TextBlack, u.BgYellow)...)
|
2024-02-18 13:20:58 +08:00
|
|
|
},
|
|
|
|
"error": func(args ...interface{}) {
|
2024-03-08 11:45:13 +08:00
|
|
|
fmt.Println(makeStringArray(args, u.TextWhite, u.BgRed)...)
|
|
|
|
},
|
2024-03-16 22:50:05 +08:00
|
|
|
"input": func() string {
|
|
|
|
line := ""
|
|
|
|
_, _ = fmt.Scanln(&line)
|
|
|
|
return line
|
|
|
|
},
|
2024-03-08 11:45:13 +08:00
|
|
|
}, false))
|
|
|
|
|
|
|
|
// 注入 logger
|
|
|
|
rt.JsCtx.Globals().Set("logger", MakeJsValue(rt.GoCtx, map[string]interface{}{
|
|
|
|
"debug": func(message string, args *map[string]interface{}) {
|
|
|
|
rt.logger.Debug(message, makeMapToArray(args)...)
|
|
|
|
},
|
|
|
|
"info": func(message string, args *map[string]interface{}) {
|
|
|
|
rt.logger.Info(message, makeMapToArray(args)...)
|
|
|
|
},
|
|
|
|
"warn": func(message string, args *map[string]interface{}) {
|
|
|
|
rt.logger.Warning(message, makeMapToArray(args)...)
|
|
|
|
},
|
|
|
|
"error": func(message string, args *map[string]interface{}) {
|
|
|
|
rt.logger.Error(message, makeMapToArray(args)...)
|
2024-02-18 13:20:58 +08:00
|
|
|
},
|
|
|
|
}, false))
|
|
|
|
|
|
|
|
return rt
|
|
|
|
}
|
|
|
|
|
2024-03-08 11:45:13 +08:00
|
|
|
func makeMapToArray(args *map[string]interface{}) []interface{} {
|
|
|
|
outArgs := make([]interface{}, 0)
|
|
|
|
if args != nil {
|
|
|
|
for k, v := range *args {
|
|
|
|
outArgs = append(outArgs, k, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return outArgs
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeStringArray(args []interface{}, color u.TextColor, bg u.BgColor) []interface{} {
|
|
|
|
stringArgs := make([]interface{}, len(args))
|
|
|
|
for i, v := range args {
|
|
|
|
if color != u.TextNone || bg != u.BgNone {
|
|
|
|
stringArgs[i] = u.Color(u.StringP(v), color, bg)
|
|
|
|
} else {
|
|
|
|
stringArgs[i] = u.StringP(v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return stringArgs
|
|
|
|
}
|
|
|
|
|
2024-03-15 13:40:20 +08:00
|
|
|
func Run(code string, option *RuntimeOption) (out interface{}, err error, stack string) {
|
|
|
|
rt := New(option)
|
2024-02-18 13:20:58 +08:00
|
|
|
defer func() {
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
rt.logger.Error(u.String(err))
|
|
|
|
}
|
|
|
|
rt.Close()
|
|
|
|
}()
|
|
|
|
return rt.Run(code)
|
|
|
|
}
|
|
|
|
|
2024-03-15 13:40:20 +08:00
|
|
|
func RunFile(filename string, option *RuntimeOption) (out interface{}, err error, stack string) {
|
|
|
|
if code, err := u.ReadFile(filename); err == nil {
|
|
|
|
return Run(code, option)
|
|
|
|
} else {
|
|
|
|
return nil, err, ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-17 12:55:08 +08:00
|
|
|
var jsErrorCodeMatcher = regexp.MustCompile(`code:(\d+)`)
|
|
|
|
|
|
|
|
func GetJSError(err error, code string) string {
|
|
|
|
if err != nil {
|
|
|
|
var jsErr *quickjs.Error
|
|
|
|
if errors.As(err, &jsErr) {
|
|
|
|
// 在错误信息中加入代码
|
|
|
|
codeLines := strings.Split(code, "\n")
|
|
|
|
return jsErrorCodeMatcher.ReplaceAllStringFunc(jsErr.Stack, func(s2 string) string {
|
|
|
|
errorLineNumber := u.Int(jsErrorCodeMatcher.FindStringSubmatch(s2)[1])
|
|
|
|
errorLine := ""
|
|
|
|
if len(codeLines) >= errorLineNumber {
|
|
|
|
errorLine = codeLines[errorLineNumber-1]
|
|
|
|
}
|
|
|
|
return s2 + " ```" + errorLine + "```"
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return err.Error()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|