package gojs import ( "apigo.cloud/git/apigo/plugin" "apigo.cloud/git/apigo/qjs" "errors" "fmt" "github.com/ssgo/log" "github.com/ssgo/u" "path" "regexp" "strings" ) var pluginNameMatcher = regexp.MustCompile(`(\w+?)\.`) 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 } type JSRuntime struct { imports map[string]string imported map[string]string freeJsValues []quickjs.Value rt quickjs.Runtime JsCtx *quickjs.Context GoCtx *plugin.Context logger *log.Logger plugins map[string]*plugin.Plugin } func (rt *JSRuntime) Close() { for _, v := range rt.freeJsValues { v.Free() } rt.freeJsValues = make([]quickjs.Value, 0) rt.JsCtx.Close() rt.rt.Close() } var pluginIdFixer = regexp.MustCompile("[^a-zA-Z0-9_]") func fixPluginId(id string) string { return "_" + pluginIdFixer.ReplaceAllString(id, "_") } func (rt *JSRuntime) run(code string) (out interface{}, err error, stack string) { // support import tryPlugins := map[string]bool{} 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 { if strings.Contains(exportStr, "export default") { exportStr = strings.Replace(exportStr, "export default", "return", 1) } exportStr = strings.Replace(exportStr, "export", "return", 1) return exportStr }) 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 { if plg := plugin.Get(m[2]); plg != nil { tryPlugins[m[2]] = true return "let " + m[1] + " = " + fixPluginId(m[2]) } return "" } } else { return "let " + m[1] + " = " + importVar } }) //tryPlugins := map[string]bool{} //for _, m := range pluginNameMatcher.FindAllStringSubmatch(code, 1024) { // tryPlugins[m[1]] = true //} for _, plg := range plugin.List() { if tryPlugins[plg.Id] && rt.plugins[plg.Id] == nil { //if rt.plugins[plg.Id] == nil { rt.plugins[plg.Id] = &plg rt.JsCtx.Globals().Set(fixPluginId(plg.Id), MakeJsValueForPlugin(rt.GoCtx, plg.Objects, plg.Id, false)) if plg.JsCode != "" { if result, err := rt.JsCtx.Eval(plg.JsCode); err != nil { stack := GetJSError(err, plg.JsCode) rt.logger.Error(err.Error(), "stack", stack) } else { result.Free() } } } } if r, err := rt.JsCtx.Eval(code); err == nil { result := MakeFromJsValue(r) r.Free() return result, nil, "" } else { // 检查错误 stack := GetJSError(err, code) rt.logger.Error(err.Error(), "stack", stack) return nil, err, stack } } func (rt *JSRuntime) Exec(code string) (err error, stack string) { _, err, stack = rt.run(code) return err, stack } 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, "" } } func (rt *JSRuntime) Run(code string) (out interface{}, err error, stack string) { return rt.run("(function(){" + code + "})()") } 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, "" } } func SetPluginsConfig(conf map[string]plugin.Config) { for _, plg := range plugin.List() { if plg.Init != nil { plg.Init(conf[plg.Id]) } } } 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 } // 初始化JS虚拟机 jsRt := quickjs.NewRuntime() jsCtx := jsRt.NewContext() goCtx := plugin.NewContext(map[string]interface{}{ "*log.Logger": option.Logger, "*quickjs.Context": jsCtx, }) rt := &JSRuntime{ imports: option.Imports, imported: map[string]string{}, freeJsValues: make([]quickjs.Value, 0), rt: jsRt, JsCtx: jsCtx, GoCtx: goCtx, logger: option.Logger, plugins: map[string]*plugin.Plugin{}, } rt.GoCtx.SetData("_freeJsValues", &rt.freeJsValues) // 全局变量 if option.Globals != nil { for k, obj := range option.Globals { 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{}) { fmt.Println(makeStringArray(args, u.TextNone, u.BgNone)...) }, "info": func(args ...interface{}) { fmt.Println(makeStringArray(args, u.TextCyan, u.BgNone)...) }, "warn": func(args ...interface{}) { fmt.Println(makeStringArray(args, u.TextBlack, u.BgYellow)...) }, "error": func(args ...interface{}) { fmt.Println(makeStringArray(args, u.TextWhite, u.BgRed)...) }, "input": func() string { line := "" _, _ = fmt.Scanln(&line) return line }, }, 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)...) }, }, false)) return rt } 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 } func Run(code string, option *RuntimeOption) (out interface{}, err error, stack string) { rt := New(option) defer func() { if err := recover(); err != nil { rt.logger.Error(u.String(err)) } rt.Close() }() return rt.Run(code) } 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, "" } } 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 "" } }