gojs/gojs.go
2024-10-02 00:07:02 +08:00

504 lines
12 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
}
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")
}