package gojs import ( "apigo.cloud/git/apigo/plugin" "apigo.cloud/git/apigo/qjs" "errors" "fmt" "github.com/ssgo/log" "github.com/ssgo/u" "os" "path" "path/filepath" "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./\\\- ]+)['"]`) var flowMethodTypeMatcher = regexp.MustCompile(`\):\s*([\w<>\[\]]+)\s*{`) var functionArgsForFlowMatcher = regexp.MustCompile(`\([\w<>\[\]:,\s]+`) var flowVarTypeMatcher = regexp.MustCompile(`(\w+)\s*:\s*([\w<>\[\]]+)\s*(=|,|\)|$)`) type RuntimeOption struct { Globals map[string]interface{} Imports map[string]string Logger *log.Logger DevMode bool } //type watchInfo struct { // mtime int64 // code string // codePath string //} 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 rootPath string currentPath string devMode bool codeLines map[string][]string realCodeLines map[string][]string anonymousIndex uint //watchList map[string]watchInfo // watch file changed, for dev mode } type JSError struct { error stack string } 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, isClosure bool, setToVar string, fromFilename string) (out interface{}, jsErr *JSError) { // support import tryPlugins := map[string]bool{} fixedCode := "" if isClosure { if setToVar == "" { fixedCode = "(function(){" + code + "})()" } else { fixedCode = "let " + setToVar + " = (function(){" + code + "})()" } } else { fixedCode = code } fixedCode = importMatcher.ReplaceAllStringFunc(fixedCode, 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 { if plugin.Get(m[2]) != nil { isTS = true } else { jsFile += ".js" } } if isTS { if plg := plugin.Get(m[2]); plg != nil { tryPlugins[m[2]] = true return "let " + m[1] + " = " + fixPluginId(m[2]) } else { rt.logger.Error("unknown plugin: " + m[2]) return "" } } else { if varName, searchList, err := rt.Import(m[2]); err == nil { return "let " + m[1] + " = " + varName } else { return "throw new Error('import file not found: " + jsFile + " in [" + strings.Join(searchList, ", ") + "]')" } } } else { return "let " + m[1] + " = " + importVar } }) fixedCode = flowMethodTypeMatcher.ReplaceAllString(fixedCode, ") {") fixedCode = functionArgsForFlowMatcher.ReplaceAllStringFunc(fixedCode, func(str string) string { //if flowVarTypeMatcher.MatchString(str) { // fmt.Println(">>>>>>>>", str, u.BCyan(flowVarTypeMatcher.ReplaceAllString(str, "$1 $3"))) //} return flowVarTypeMatcher.ReplaceAllString(str, "$1 $3") }) //tryPlugins := map[string]bool{} //for _, m := range pluginNameMatcher.FindAllStringSubmatch(fixedCode, 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 != "" { rt.codeLines[plg.Id+".js"] = strings.Split(plg.JsCode, "\n") rt.realCodeLines[plg.Id+".js"] = strings.Split(plg.JsCode, "\n") if result, err := rt.JsCtx.EvalFile(plg.JsCode, plg.Id+".js"); err != nil { stack := rt.getJSError(err) rt.logger.Error(err.Error(), "stack", stack) } else { result.Free() } } } } rt.codeLines[fromFilename] = strings.Split(code, "\n") rt.realCodeLines[fromFilename] = strings.Split(fixedCode, "\n") if r, err := rt.JsCtx.EvalFile(fixedCode, fromFilename); err == nil { result := MakeFromJsValue(r) r.Free() return result, nil } else { // 检查错误 stack := rt.getJSError(err) //stack2 := getJSError(err, fixedCode) rt.logger.Error(err.Error(), "stack", stack) //, "stack2", stack2) return nil, &JSError{error: err} } } func (rt *JSRuntime) Exec(code string) (jsErr *JSError) { return rt.ExecAt(code, "") } func (rt *JSRuntime) ExecAt(code string, dir string) (jsErr *JSError) { rt.anonymousIndex++ filename := filepath.Join(dir, fmt.Sprintf("anonymous%d.js", rt.anonymousIndex)) return rt.execAtFile(code, filename) } func (rt *JSRuntime) ExecFile(filename string) (jsErr *JSError) { if code, err := u.ReadFile(filename); err == nil { return rt.execAtFile(code, filename) } else { rt.logger.Error(err.Error()) return &JSError{error: err} } } func (rt *JSRuntime) execAtFile(code string, filename string) (jsErr *JSError) { if filename == "" { rt.currentPath = rt.rootPath } else { if !filepath.IsAbs(filename) { filename, _ = filepath.Abs(filename) } rt.currentPath = filepath.Dir(filename) } _, jsErr = rt.run(code, false, "", filename) return jsErr } func (rt *JSRuntime) Run(code string) (out interface{}, jsErr *JSError) { return rt.RunAt(code, "") } func (rt *JSRuntime) RunAt(code string, dir string) (out interface{}, jsErr *JSError) { rt.anonymousIndex++ filename := filepath.Join(dir, fmt.Sprintf("anonymous%d.js", rt.anonymousIndex)) return rt.runAtFile(code, filename) } func (rt *JSRuntime) RunFile(filename string) (out interface{}, jsErr *JSError) { rt.currentPath = filepath.Dir(filename) if code, err := u.ReadFile(filename); err == nil { return rt.runAtFile(code, filename) } else { rt.logger.Error(err.Error()) return nil, &JSError{error: err} } } func (rt *JSRuntime) runAtFile(code string, filename string) (out interface{}, jsErr *JSError) { if filename == "" { rt.currentPath = rt.rootPath } else { if !filepath.IsAbs(filename) { filename, _ = filepath.Abs(filename) } rt.currentPath = filepath.Dir(filename) } return rt.run(code, true, "", filename) } func appendSearchFiles(list *[]string, dir, moduleName string, hasJsExt bool) string { filename := moduleName if dir != "" { filename = filepath.Join(dir, moduleName) } if hasJsExt { if u.FileExists(filename) { return filename } *list = append(*list, filename) } else { tryFilename := filename + ".js" if u.FileExists(tryFilename) { return tryFilename } *list = append(*list, tryFilename) tryFilename = filepath.Join(filename, "index.js") if u.FileExists(tryFilename) { return tryFilename } *list = append(*list, tryFilename) } return "" } func (rt *JSRuntime) Import(moduleName string) (varName string, searchList []string, jsErr *JSError) { searchList = make([]string, 0) // if imported importVar := rt.imported[moduleName] if importVar != "" { return importVar, searchList, nil } // check imports set importCode := rt.imports[moduleName] importFile := "" // search file if importCode == "" { hasJsExt := strings.HasSuffix(moduleName, ".js") importFile = appendSearchFiles(&searchList, "", moduleName, hasJsExt) if importFile == "" && !filepath.IsAbs(moduleName) { importFile = appendSearchFiles(&searchList, rt.currentPath, moduleName, hasJsExt) if importFile == "" && rt.rootPath != rt.currentPath { importFile = appendSearchFiles(&searchList, rt.rootPath, moduleName, hasJsExt) } usedNodeModuleDir := "" if importFile == "" { parentPath := rt.currentPath for i := 0; i < 100; i++ { nodeModuleDir := filepath.Join(parentPath, "node_modules") if u.FileExists(nodeModuleDir) { importFile = appendSearchFiles(&searchList, nodeModuleDir, moduleName, hasJsExt) break } parentPath = filepath.Dir(parentPath) if parentPath == "" || parentPath == "." { break } } } if importFile == "" && rt.rootPath != rt.currentPath { parentPath := rt.rootPath for i := 0; i < 100; i++ { nodeModuleDir := filepath.Join(parentPath, "node_modules") if u.FileExists(nodeModuleDir) { if nodeModuleDir != usedNodeModuleDir { importFile = appendSearchFiles(&searchList, nodeModuleDir, moduleName, hasJsExt) } break } parentPath = filepath.Dir(parentPath) if parentPath == "" || parentPath == "." { break } } } } if importFile != "" { if !filepath.IsAbs(importFile) { importFile, _ = filepath.Abs(importFile) } importVar = rt.imported[importFile] if importVar != "" { return importVar, searchList, nil } if code, err := u.ReadFile(importFile); err == nil { importCode = code } else { rt.logger.Error(err.Error()) } } } if importCode != "" { importVar = fmt.Sprintf("_import_%d_%s", len(rt.imported), u.UniqueId()) rt.imported[importFile] = importVar importCode = 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 }) if _, err := rt.run(importCode, true, importVar, importFile); err == nil { return importVar, searchList, nil } else { return "", searchList, err } } return "", searchList, nil } 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, nil, nil, false} } 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{}, codeLines: map[string][]string{}, realCodeLines: map[string][]string{}, } if rt.imports["console"] == "" { rt.imports["console"] = "return _console" } if rt.imports["logger"] == "" { rt.imports["logger"] = "return _logger" } rt.GoCtx.SetData("_freeJsValues", &rt.freeJsValues) rt.rootPath, _ = os.Getwd() rt.currentPath = rt.rootPath //goCtx.SetData("_rootPath", rt.rootPath) //goCtx.SetData("_currentPath", rt.rootPath) //rt.JsCtx.Globals().Set("_rootPath", jsCtx.String(rt.rootPath)) //rt.JsCtx.Globals().Set("_currentPath", jsCtx.String(rt.rootPath)) // 全局变量 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{}{ "print": func(args ...interface{}) { }, "println": func(args ...interface{}) { fmt.Println(makeStringArray(args, u.TextNone, u.BgNone)...) }, "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(prompt *string) string { if prompt != nil { fmt.Print(*prompt) } 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{}, jsErr *JSError) { rt := New(option) defer func() { if err := recover(); err != nil { rt.logger.Error(u.String(err)) } rt.Close() }() return rt.Run(code) } func RunAt(code, dir string, option *RuntimeOption) (out interface{}, jsErr *JSError) { rt := New(option) defer func() { if err := recover(); err != nil { rt.logger.Error(u.String(err)) } rt.Close() }() return rt.RunAt(code, dir) } func RunFile(filename string, option *RuntimeOption) (out interface{}, jsErr *JSError) { rt := New(option) defer func() { if err := recover(); err != nil { rt.logger.Error(u.String(err)) } rt.Close() }() return rt.RunFile(filename) } var jsErrorCodeMatcher = regexp.MustCompile(`([\w./\\\-]+):(\d+)`) func (rt *JSRuntime) getJSError(err error) string { if err != nil { var jsErr *quickjs.Error if errors.As(err, &jsErr) { // 在错误信息中加入代码 return jsErrorCodeMatcher.ReplaceAllStringFunc(jsErr.Stack, func(s2 string) string { m := jsErrorCodeMatcher.FindStringSubmatch(s2) filename := m[1] errorLineNumber := u.Int(m[2]) errorLine := "" codeLines := rt.codeLines[filename] realCodeLines := rt.realCodeLines[filename] realCodeLineStr := "" if codeLines != nil && len(codeLines) >= errorLineNumber { errorLine = strings.TrimSpace(codeLines[errorLineNumber-1]) realErrorLine := strings.TrimSpace(realCodeLines[errorLineNumber-1]) if errorLine != realErrorLine { realCodeLineStr = " > ```" + realErrorLine + "```" } } return s2 + " ```" + errorLine + "```" + realCodeLineStr }) } else { return err.Error() } } else { return "" } }