package gojs import ( "apigo.cc/apigo/gojs/dop251/goja" "apigo.cc/apigo/gojs/dop251/goja_nodejs/require" "encoding/json" "errors" "fmt" "github.com/ssgo/log" "github.com/ssgo/tool/watcher" "github.com/ssgo/u" "os" "os/signal" "path/filepath" "regexp" "strings" "sync" "syscall" "time" ) type Map = map[string]any type Module struct { Object Map ObjectMaker func(vm *goja.Runtime) Map TsCode string Desc string Example string } var modules = map[string]Module{} var modulesLock = sync.RWMutex{} func Register(name string, mod Module) { modulesLock.Lock() modules[name] = mod modulesLock.Unlock() } func RunFile(file string, args ...any) (any, error) { return Run(u.ReadFileN(file), file, args...) } func Run(code string, refFile string, args ...any) (any, error) { rt := New() var r any err := rt.StartFromCode(code, refFile) if err == nil { r, err = rt.RunMain(args...) } return r, err } var importModMatcher = regexp.MustCompile(`(?im)^\s*import\s+(.+?)\s+from\s+['"](.+?)['"]`) var requireModMatcher = regexp.MustCompile(`(?im)^\s*(const|let|var)\s+(.+?)\s*=\s*require\s*\(\s*['"](.+?)['"]\s*\)`) // var importLibMatcher = regexp.MustCompile(`(?im)^\s*(import)\s+(.+?)\s+from\s+['"][./\\\w:]+lib[/\\](.+?)(\.ts)?['"]`) // var requireLibMatcher = regexp.MustCompile(`(?im)^\s*(const|let|var)\s+(.+?)\s*=\s*require\s*\(\s*['"][./\\\w:]+lib[/\\](.+?)(\.ts)?['"]\s*\)`) var checkMainMatcher = regexp.MustCompile(`(?im)^\s*function\s+main\s*\(`) type Runtime struct { VM *goja.Runtime required map[string]bool file string srcCode string code string moduleLoader func(string) string started bool } func (rt *Runtime) SetModuleLoader(fn func(filename string) string) { rt.moduleLoader = fn } func (rt *Runtime) GetCallStack() []string { callStacks := make([]string, 0) for _, stack := range rt.VM.CaptureCallStack(0, nil) { callStacks = append(callStacks, stack.Position().String()) } return callStacks } func (rt *Runtime) requireMod(name, realModName string) error { if name != "" { if rt.required[name] { return nil } modulesLock.RLock() mod, ok := modules[name] modulesLock.RUnlock() if !ok { return errors.New("module not found: " + name) } var err error if mod.ObjectMaker != nil { err = rt.VM.Set(realModName, mod.ObjectMaker(rt.VM)) } else { err = rt.VM.Set(realModName, mod.Object) } if err != nil { return err } rt.required[name] = true return nil } else { // 使用所有模块 var allModules = map[string]Module{} modulesLock.RLock() for k, v := range modules { allModules[k] = v } modulesLock.RUnlock() for k, v := range allModules { if !rt.required[k] { if err := rt.VM.Set(k, v); err != nil { return err } rt.required[k] = true } } return nil } } func (rt *Runtime) makeImport(code string) (string, int, error) { var modErr error importCount := 0 code = requireModMatcher.ReplaceAllStringFunc(code, func(str string) string { if m := requireModMatcher.FindStringSubmatch(str); m != nil && len(m) > 3 { optName := m[1] varName := m[2] modName := m[3] realModName := modName if strings.ContainsRune(modName, '/') { realModName = strings.ReplaceAll(realModName, "/", "_") } modulesLock.RLock() _, ok := modules[modName] modulesLock.RUnlock() if !ok { return str } importCount++ if modErr == nil { if err := rt.requireMod(modName, realModName); err != nil { modErr = err } } if varName != realModName { return fmt.Sprintf("%s %s = %s", optName, varName, realModName) } else { return "" } } return str }) if !rt.required["setTimeout"] && strings.Contains(code, "setTimeout") { rt.required["setTimeout"] = true err := rt.VM.Set("setTimeout", func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { args := MakeArgs(&argsIn, vm).Check(2) callback := args.Func(0) timeout := time.Duration(args.Int64(1)) * time.Millisecond if callback != nil { go func() { if timeout > 0 { time.Sleep(timeout) } if _, err := callback(args.This, args.Arguments[2:]...); err != nil { //panic(vm.NewGoError(err)) } }() } return nil }) if err != nil { modErr = err } } return code, importCount, modErr } func New() *Runtime { vm := goja.New() vm.GoData = map[string]any{ "logger": log.New(u.ShortUniqueId()), } rt := &Runtime{ VM: vm, required: map[string]bool{}, } // 处理模块引用 require.NewRegistryWithLoader(func(path string) ([]byte, error) { modFile := path if !filepath.IsAbs(modFile) { modFile = filepath.Join(filepath.Dir(rt.file), modFile) } if !strings.HasSuffix(modFile, ".js") && !u.FileExists(modFile) { modFile += ".js" } modCode := "" if rt.moduleLoader != nil { modCode = rt.moduleLoader(modFile) } if modCode == "" { var err error modCode, err = u.ReadFile(modFile) if err != nil { return nil, err } } modCode = importModMatcher.ReplaceAllString(modCode, "let $1 = require('$2')") modCode, _, _ = rt.makeImport(modCode) return []byte(modCode), nil }).Enable(rt.VM) return rt } func (rt *Runtime) StartFromFile(file string) error { return rt.StartFromCode(u.ReadFileN(file), file) } func (rt *Runtime) StartFromCode(code, refFile string) error { if refFile != "" { rt.file = refFile } if rt.file == "" { rt.file = "main.js" } if absFile, err := filepath.Abs(rt.file); err == nil { rt.file = absFile } refPath := filepath.Dir(refFile) rt.VM.GoData["startPath"] = refPath if rt.srcCode == "" { rt.srcCode = code } rt.code = code // 将 import 转换为 require rt.code = importModMatcher.ReplaceAllString(rt.code, "let $1 = require('$2')") // 按需加载引用 var importCount int var modErr error rt.code, importCount, modErr = rt.makeImport(rt.code) // 如果没有import,默认import所有 if modErr == nil && importCount == 0 { modErr = rt.requireMod("", "") } if modErr != nil { return modErr } //fmt.Println(u.BCyan(rt.code)) // 初始化主函数 if !checkMainMatcher.MatchString(rt.code) { rt.code = "function main(...args){" + rt.code + "}" } if _, err := rt.VM.RunScript(rt.file, rt.code); err != nil { return err } else { rt.started = true return nil } } func (rt *Runtime) RunMain(args ...any) (any, error) { if !rt.started { return nil, errors.New("runtime not started") } // 解析参数 for i, arg := range args { if str, ok := arg.(string); ok { var v interface{} if err := json.Unmarshal([]byte(str), &v); err == nil { args[i] = v } } } if err := rt.VM.Set("__args", args); err != nil { return nil, err } jsResult, err := rt.VM.RunScript("main", "main(...__args)") var result any if err == nil { if jsResult != nil && !jsResult.Equals(goja.Undefined()) { result = jsResult.Export() } } return result, err } func (rt *Runtime) SetGoData(name string, value any) { rt.VM.GoData[name] = value } func (rt *Runtime) GetGoData(name string) any { return rt.VM.GoData[name] } func (rt *Runtime) Set(name string, value any) error { return rt.VM.Set(name, value) } func (rt *Runtime) SetGlobal(global Map) error { var err error for k, v := range global { if err = rt.VM.Set(k, v); err != nil { return err } } return nil } func (rt *Runtime) RunCode(code string) (any, error) { //if !rt.started { // return nil, errors.New("runtime not started") //} code = importModMatcher.ReplaceAllString(code, "let $1 = require('$2')") code, _, _ = rt.makeImport(code) jsResult, err := rt.VM.RunScript(rt.file, code) var result any if err == nil { if jsResult != nil && !jsResult.Equals(goja.Undefined()) { result = jsResult.Export() } } return result, err } //func RunFile(file string, args ...any) (any, error) { // return Run(u.ReadFileN(file), file, args...) //} type WatchRunner struct { w *watcher.Watcher } func (wr *WatchRunner) WaitForKill() { exitCh := make(chan os.Signal, 1) closeCh := make(chan bool, 1) signal.Notify(exitCh, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) go func() { <-exitCh closeCh <- true }() <-closeCh wr.w.Stop() } func (wr *WatchRunner) Stop() { wr.w.Stop() } func WatchRun(file string, extDirs, extTypes []string, args ...any) (*WatchRunner, error) { wr := &WatchRunner{} run := func() { rt := New() if wr.w != nil { rt.SetModuleLoader(func(filename string) string { filePath := filepath.Dir(filename) needWatch := true for _, v := range wr.w.WatchList() { if v == filePath { needWatch = false break } } if needWatch { fmt.Println(u.BMagenta("[watching module path]"), filePath) _ = wr.w.Add(filePath) } return u.ReadFileN(filename) }) } err := rt.StartFromFile(file) result, err := rt.RunMain(args...) if err != nil { fmt.Println(u.BRed(err.Error())) fmt.Println(u.Red(" " + strings.Join(rt.GetCallStack(), "\n "))) } else if result != nil { fmt.Println(u.Cyan(u.JsonP(result))) } } var isWaitingRun = false onChange := func(filename string, event string) { if !isWaitingRun { _, _ = os.Stdout.WriteString("\x1b[3;J\x1b[H\x1b[2J") isWaitingRun = true go func() { time.Sleep(time.Millisecond * 10) isWaitingRun = false run() }() } fmt.Println(u.BYellow("[changed]"), filename) } _, _ = os.Stdout.WriteString("\x1b[3;J\x1b[H\x1b[2J") watchStartPath := filepath.Dir(file) fmt.Println(u.BMagenta("[watching root path]"), watchStartPath) watchDirs := []string{watchStartPath} watchTypes := []string{"js", "json", "yml"} if extDirs != nil { for _, v := range extDirs { watchDirs = append(watchDirs, v) } } if extTypes != nil { for _, v := range extTypes { watchTypes = append(watchTypes, v) } } if w, err := watcher.Start(watchDirs, watchTypes, onChange); err == nil { wr.w = w go func() { run() }() return wr, nil } else { return nil, err } } func ExportForDev() string { var allModules = map[string]Module{} modulesLock.RLock() for k, v := range modules { allModules[k] = v } modulesLock.RUnlock() dir, _ := os.Getwd() nodeModulesPath := "node_modules" packageJsonFile := "package.json" for i := 0; i < 20; i++ { nodePath := filepath.Join(dir, "node_modules") if u.FileExists(nodePath) { nodeModulesPath = nodePath packageJsonFile = filepath.Join(dir, "package.json") break } parentDir := filepath.Dir(dir) if parentDir == dir { break } dir = parentDir } oldPackageJson := u.ReadFileN(packageJsonFile) insertModulesPostfix := "" if oldPackageJson == "" { oldPackageJson = "{\n \"devDependencies\": {\n\n }\n}" } else if !strings.Contains(oldPackageJson, "\"devDependencies\": {") { oldPackageJson = strings.Replace(oldPackageJson, "{", "{\n \"devDependencies\": {\n\n },", 1) } else { insertModulesPostfix = "," } insertModules := make([]string, 0) imports := make([]string, len(allModules)) i := 0 for k, v := range allModules { varName := k tsPath := k if strings.ContainsRune(k, '/') { varName = varName[strings.LastIndex(varName, "/")+1:] if os.PathSeparator != '/' { tsPath = strings.ReplaceAll(tsPath, "/", string(os.PathSeparator)) } } _ = u.WriteFile(filepath.Join(nodeModulesPath, tsPath, "index.ts"), v.TsCode) imports[i] = fmt.Sprintf("import %s from '%s'", varName, k) i++ if !strings.Contains(oldPackageJson, "\""+k+"\":") { insertModules = u.AppendUniqueString(insertModules, fmt.Sprint("\"", k, "\": \"v0.0.0\"")) } } if len(insertModules) > 0 { newPackageJson := strings.Replace(oldPackageJson, "\"devDependencies\": {", "\"devDependencies\": {\n "+strings.Join(insertModules, ",\n ")+insertModulesPostfix, 1) _ = u.WriteFile(packageJsonFile, newPackageJson) } return strings.Join(imports, "\n") }