gojs/gojs.go
Star f37b64c2df many update
support pool
2024-10-07 23:02:11 +08:00

684 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
type Program struct {
prg *goja.Program
startFile string
imports map[string]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) {
if code, err := u.ReadFile(file); err == nil {
return Run(code, file, args...)
} else {
return nil, err
}
}
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
}
func RunProgram(plg *Program, args ...any) (any, error) {
rt := New()
var r any
err := rt.StartFromProgram(plg)
if err == nil {
r, err = rt.RunMain(args...)
}
return r, err
}
func CompileFile(file string) (*Program, error) {
if code, err := u.ReadFile(file); err == nil {
return CompileMain(code, file)
} else {
return nil, err
}
}
func CompileMain(code string, refFile string) (*Program, error) {
imports := makeImports(&code)
if !checkMainMatcher.MatchString(code) {
code = "function main(...Args){" + code + "}"
}
p, err := goja.Compile(refFile, code, false)
return &Program{prg: p, imports: imports, startFile: refFile}, err
}
func CompileCode(code string, refFile string) (*Program, error) {
imports := makeImports(&code)
p, err := goja.Compile(refFile, code, false)
return &Program{prg: p, imports: imports, startFile: refFile}, 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
dataLock sync.RWMutex
}
func (rt *Runtime) lock() {
rt.vm.Locker.Lock()
}
func (rt *Runtime) unlock() {
rt.vm.Locker.Unlock()
}
//func (rt *Runtime) Free() {
// rt.vm.GoData = nil
// rt.vm = nil
//}
func (rt *Runtime) SetModuleLoader(fn func(filename string) string) {
rt.moduleLoader = fn
}
func (rt *Runtime) GetCallStack() []string {
callStacks := make([]string, 0)
rt.lock()
stacks := rt.vm.CaptureCallStack(0, nil)
rt.unlock()
for _, stack := range stacks {
callStacks = append(callStacks, stack.Position().String())
}
return callStacks
}
func (rt *Runtime) requireSpecialMod(name string) error {
if rt.required[name] {
return nil
}
if name == "setTimeout" {
rt.required["setTimeout"] = true
fn := func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := MakeArgs(&argsIn, vm).Check(1)
callback := args.Func(0)
timeout := time.Duration(args.Int64(1)) * time.Millisecond
//locked := vm.Locker.TryLock()
//vm.Locker.Unlock()
//locked2 := vm.CallbackLocker.TryLock()
//vm.CallbackLocker.Unlock()
if callback != nil {
go func() {
if timeout > 0 {
time.Sleep(timeout)
}
//if !locked {
// vm.Locker.Lock()
//}
//if !locked2 {
// vm.CallbackLocker.Lock()
//}
if _, err := callback(args.This, args.Arguments[2:]...); err != nil {
//panic(vm.NewGoError(err))
}
}()
}
return nil
}
rt.lock()
err := rt.vm.Set("setTimeout", fn)
rt.unlock()
return err
}
return errors.New("module not found: " + name)
}
func (rt *Runtime) requireMod(modName, realModName string) error {
if rt.required[modName] {
return nil
}
modulesLock.RLock()
mod, ok := modules[modName]
modulesLock.RUnlock()
if !ok {
// 不在注册的模块中,尝试引入特殊模块
return rt.requireSpecialMod(modName)
}
var err error
rt.lock()
if mod.ObjectMaker != nil {
err = rt.vm.Set(realModName, mod.ObjectMaker(rt.vm))
} else {
err = rt.vm.Set(realModName, mod.Object)
}
rt.unlock()
if err != nil {
return err
}
rt.required[modName] = true
return nil
}
func (rt *Runtime) requireAllMod() error {
var allModules = map[string]Module{}
modulesLock.RLock()
for k, v := range modules {
allModules[k] = v
}
modulesLock.RUnlock()
for modName, mod := range allModules {
if !rt.required[modName] {
var err error
realModName := modName
if strings.ContainsRune(modName, '/') {
realModName = strings.ReplaceAll(realModName, "/", "_")
}
rt.lock()
if mod.ObjectMaker != nil {
err = rt.vm.Set(realModName, mod.ObjectMaker(rt.vm))
} else {
err = rt.vm.Set(realModName, mod.Object)
}
rt.unlock()
if err != nil {
return err
}
rt.required[modName] = true
}
}
if err := rt.requireSpecialMod("setTimeout"); err != nil {
return err
}
return nil
}
func makeImports(code *string) map[string]string {
importModules := map[string]string{}
*code = importModMatcher.ReplaceAllString(*code, "let $1 = require('$2')")
*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
}
importModules[modName] = realModName
if varName != realModName {
return fmt.Sprintf("%s %s = %s", optName, varName, realModName)
} else {
return ""
}
}
return str
})
if strings.Contains(*code, "setTimeout") {
importModules["setTimeout"] = "setTimeout"
}
return importModules
}
func (rt *Runtime) setImports(importModules map[string]string) error {
var modErr error
for k, v := range importModules {
if modErr == nil {
if err := rt.requireMod(k, v); err != nil {
modErr = err
}
}
}
return modErr
}
func New() *Runtime {
vm := goja.New()
rt := &Runtime{
vm: vm,
required: map[string]bool{},
}
vm.GoData = map[string]any{
"logger": log.New(u.ShortUniqueId()),
}
// 处理模块引用
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
}
}
imports := makeImports(&modCode)
if err := rt.setImports(imports); err != nil {
return nil, err
}
return []byte(modCode), nil
}).Enable(rt.vm)
return rt
}
func (rt *Runtime) StartFromFile(file string) error {
if code, err := u.ReadFile(file); err == nil {
return rt.StartFromCode(code, file)
} else {
return err
}
}
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.SetGoData("startFile", refFile)
rt.SetGoData("startPath", refPath)
if rt.srcCode == "" {
rt.srcCode = code
}
rt.code = code
// 按需加载引用
//var importCount int
var modErr error
//rt.code, importCount, modErr = rt.makeImport(rt.code)
imports := makeImports(&rt.code)
if len(imports) == 0 {
modErr = rt.requireAllMod()
} else {
modErr = rt.setImports(imports)
}
//// 如果没有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 + "}"
}
rt.lock()
_, err := rt.vm.RunScript(rt.file, rt.code)
rt.unlock()
if err != nil {
return err
} else {
rt.started = true
return nil
}
}
func (rt *Runtime) StartFromProgram(plg *Program) error {
var modErr error
if len(plg.imports) == 0 {
modErr = rt.requireAllMod()
} else {
modErr = rt.setImports(plg.imports)
}
if modErr != nil {
return modErr
}
rt.SetGoData("startFile", plg.startFile)
rt.SetGoData("startPath", filepath.Dir(plg.startFile))
rt.lock()
_, err := rt.vm.RunProgram(plg.prg)
rt.unlock()
if 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
}
}
}
rt.lock()
err := rt.vm.Set("__args", args)
rt.unlock()
if err != nil {
return nil, err
}
rt.lock()
r, err := rt.vm.RunScript("main", "main(...__args)")
rt.unlock()
return makeResult(r, err)
}
func (rt *Runtime) RunVM(callback func(vm *goja.Runtime) (any, error)) (any, error) {
rt.lock()
r, err := callback(rt.vm)
rt.unlock()
return r, err
}
func (rt *Runtime) SetGoData(name string, value any) {
rt.dataLock.Lock()
rt.vm.GoData[name] = value
rt.dataLock.Unlock()
}
func (rt *Runtime) GetGoData(name string) any {
rt.dataLock.RLock()
defer rt.dataLock.RUnlock()
return rt.vm.GoData[name]
}
func (rt *Runtime) Set(name string, value any) error {
rt.lock()
defer rt.unlock()
return rt.vm.Set(name, value)
}
func (rt *Runtime) SetGlobal(global Map) error {
var err error
rt.lock()
defer rt.unlock()
for k, v := range global {
if err = rt.vm.Set(k, v); err != nil {
return err
}
}
return nil
}
func (rt *Runtime) RunFile(file string) (any, error) {
if code, err := u.ReadFile(file); err == nil {
imports := makeImports(&code)
if err := rt.setImports(imports); err != nil {
return nil, err
}
rt.lock()
r, err := rt.vm.RunScript(file, code)
rt.unlock()
return makeResult(r, err)
} else {
return nil, err
}
}
func (rt *Runtime) RunCode(code string) (any, error) {
imports := makeImports(&code)
if err := rt.setImports(imports); err != nil {
return nil, err
}
rt.lock()
r, err := rt.vm.RunScript("anonymous", code)
rt.unlock()
return makeResult(r, err)
}
func (rt *Runtime) RunProgram(prg *Program) (any, error) {
var r goja.Value
err := rt.setImports(prg.imports)
if err == nil {
rt.lock()
r, err = rt.vm.RunProgram(prg.prg)
rt.unlock()
}
return makeResult(r, err)
}
func makeResult(r goja.Value, err error) (any, error) {
var result any
if err == nil {
if r != nil && !r.Equals(goja.Undefined()) {
result = r.Export()
}
}
return result, err
}
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")
}