diff --git a/go.mod b/go.mod index 324b661..bab243f 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,15 @@ go 1.22.2 require ( github.com/Nigel2392/goldcrest v1.0.4 + github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 + github.com/elliotchance/orderedmap/v2 v2.2.0 github.com/pkg/errors v0.9.1 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/Nigel2392/mux v1.2.4 // indirect - github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 432cd1c..71c0743 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,75 @@ github.com/Nigel2392/goldcrest v1.0.4 h1:Xx+QLht6QjJ3Gg9uksgc6Ye1XjbtzQ1208ClZwoVWsg= github.com/Nigel2392/goldcrest v1.0.4/go.mod h1:UpnPrYJqZY/b7TkoVKdoNNPKTlQtld+fsrZEA98c1c0= -github.com/Nigel2392/mux v1.2.4 h1:nS/Yeo3DQhQLFcLIuObhqCg9Ay1XD6EYJYiAro0AFn0= -github.com/Nigel2392/mux v1.2.4/go.mod h1:Hrj90gq33BCcFy8167rU03oBr2YIQMRvNkMf9JnRcAU= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM= +github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk= github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index d4de7c8..d0a61af 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,12 @@ type Flagger struct { // List the projects available for use ListProjects bool + // List the commands available for all projects + ListCommands bool + + // Save a global command for this user. + SaveCommand string + // Write an example project configuration Example bool @@ -135,6 +141,8 @@ func main() { flagSet.StringVar(&flagger.Use, "use", "", "Use the specified project configuration.") flagSet.BoolVar(&flagger.Example, "example", false, "Print an example project configuration.") flagSet.BoolVar(&flagger.ListProjects, "list", false, "List the projects available for use.") + flagSet.BoolVar(&flagger.ListCommands, "list-commands", false, "List the commands available for all projects.") + flagSet.StringVar(&flagger.SaveCommand, "save-command", "", "Save a global command for this user by providing a path to a JS file.") flagSet.BoolVar(&flagger.Serve, "serve", false, "Serve the project over HTTP.") flagSet.IntVar(&flagger.Lock, "lock", -1, "Lock the project configuration. 1=Lock, 0=Unlock.") flagSet.BoolFunc("v", "Enable verbose logging.", enableVerboseLogging) @@ -404,6 +412,33 @@ func main() { if err != nil { logger.Fatal(1, fmt.Errorf("failed to start server: %w", err)) } + + case flagger.ListCommands: + + var commands, err = qg.ListJSFiles() + if err != nil { + logger.Fatal(1, fmt.Errorf("failed to list commands: %w", err)) + } + + if len(commands) == 0 { + fmt.Println(quickgo.Craft(quickgo.CMD_Yellow, "No commands found.")) + return + } + + fmt.Println(quickgo.Craft(quickgo.CMD_Red, "Commands:")) + for _, cmd := range commands { + fmt.Printf(" - %s\n", quickgo.Craft( + quickgo.CMD_Blue, cmd, + )) + } + + case flagger.SaveCommand != "": + + var err = qg.SaveJS(flagger.SaveCommand) + if err != nil { + logger.Fatal(1, fmt.Errorf("failed to save command: %w", err)) + } + default: // Parse commands for the project itself. var args = flagSet.Args() @@ -412,6 +447,31 @@ func main() { os.Exit(1) } + // Execute app JS files if the command is 'exec'. + if args[0] == "exec" && len(args) > 1 { + var ( + fn = args[1] + ctx = parseCommandlineContext(args[2:], true) + ) + + if err = qg.LoadCurrentProject(flagger.TargetDir); err != nil { + logger.Warnf("failed to read project config: %s", err) + } + + err = qg.ExecJS( + fn, ctx, + ) + + if err != nil { + logger.Fatal(1, err) + } + + return + } else if args[0] == "exec" { + logger.Fatal(1, "no function provided to execute") + } + + // Look for commands for this project specifically var ( cmd *config.ProjectCommand command = args[0] diff --git a/quickgo/config/config.go b/quickgo/config/config.go index 244ec0c..60c4e77 100644 --- a/quickgo/config/config.go +++ b/quickgo/config/config.go @@ -22,6 +22,8 @@ const ( QUICKGO_CONFIG_NAME = "quickgo.yaml" // Config file for QuickGo, resides in the executable directory. PROJECT_CONFIG_NAME = "quickgo.yaml" // Config file for the project, resides in the project (working) directory. PROJECT_ZIP_NAME = "project.zip" // The name of the project zip file. + PROJECTS_DIR = "projects" // The directory for project files, resides in the executable directory. + COMMANDS_DIR = "commands" // The directory for command javscript files, resides in the executable directory. LOCKFILE_NAME = "quickgo.lock" // The lock file name. // Error messages. diff --git a/quickgo/js/globals.go b/quickgo/js/globals.go new file mode 100644 index 0000000..9e883a7 --- /dev/null +++ b/quickgo/js/globals.go @@ -0,0 +1,25 @@ +package js + +import "fmt" + +func logConsole(args ...any) { + fmt.Println(args...) +} + +type JSConsole struct { + Debug func(...any) `json:"debug"` + Log func(...any) `json:"log"` + Info func(...any) `json:"info"` + Warn func(...any) `json:"warn"` + Error func(...any) `json:"error"` +} + +func Console() *JSConsole { + return &JSConsole{ + Debug: logConsole, + Log: logConsole, + Info: logConsole, + Warn: logConsole, + Error: logConsole, + } +} diff --git a/quickgo/js/js.go b/quickgo/js/js.go new file mode 100644 index 0000000..0ebcf11 --- /dev/null +++ b/quickgo/js/js.go @@ -0,0 +1,105 @@ +package js + +import ( + "maps" + + "github.com/dop251/goja" + "github.com/pkg/errors" +) + +type ( + Command struct { + // The name of the main function to run. + Main string + + _Globals map[string]any + _Funcs []VMFunc + } + + OptFunc func(*Command) + VMFunc func(*goja.Runtime) error +) + +var ( + ErrExitCode = errors.New("script returned with non-zero exit code") + ErrMainMissing = errors.New("main function not found") + ErrMainInvalid = errors.New("main function is invalid") +) + +func WithGlobal(key string, value any) OptFunc { + return func(s *Command) { + s._Globals[key] = value + } +} + +func WithGlobals(globals map[string]any) OptFunc { + return func(s *Command) { + maps.Copy(s._Globals, globals) + } +} + +func WithConsole(s *Command) { + s._Globals["console"] = Console() +} + +func NewScript(mainFunc string, options ...OptFunc) (cmd *Command) { + var s = &Command{ + Main: mainFunc, + _Globals: make(map[string]any), + _Funcs: make([]VMFunc, 0), + } + + for _, opt := range options { + opt(s) + } + + return s +} + +func (s *Command) AddFunc(f ...VMFunc) { + s._Funcs = append(s._Funcs, f...) +} + +func (s *Command) Run(scriptSource string) (err error) { + + var vm = goja.New() + + vm.SetFieldNameMapper( + goja.TagFieldNameMapper("json", true), + ) + + for k, v := range s._Globals { + err = vm.Set(k, v) + if err != nil { + return errors.Wrap(err, "could not add global to VM") + } + } + + for _, f := range s._Funcs { + if err = f(vm); err != nil { + return errors.Wrap(err, "could not add function to VM") + } + } + + _, err = vm.RunString(scriptSource) + if err != nil { + return errors.Wrap(err, "error running script") + } + + var mainFunc = vm.Get(s.Main) + if mainFunc == nil { + return ErrMainMissing + } + + var main func() int + if err = vm.ExportTo(mainFunc, &main); err != nil { + return ErrMainInvalid + } + + var exitCode = main() + if exitCode != 0 { + return ErrExitCode + } + + return nil +} diff --git a/quickgo/js/js_test.go b/quickgo/js/js_test.go new file mode 100644 index 0000000..c424e02 --- /dev/null +++ b/quickgo/js/js_test.go @@ -0,0 +1,40 @@ +package js_test + +import ( + "fmt" + "testing" + + "github.com/Nigel2392/quickgo/v2/quickgo/js" +) + +func NewScript(funcName string, retValue any) string { + return fmt.Sprintf(` + function %s() { + return %v; + } + `, funcName, retValue) +} + +func TestRunScriptOK(t *testing.T) { + var ( + funcName = "testMainFunc" + retValue = 0 + script = NewScript(funcName, retValue) + ) + + if err := js.NewScript(funcName).Run(script); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestRunScriptErrorStatusCode(t *testing.T) { + var ( + funcName = "testMainFunc" + retValue = 1 + script = NewScript(funcName, retValue) + ) + + if err := js.NewScript(funcName).Run(script); err == nil { + t.Fatalf("expected error, got nil") + } +} diff --git a/quickgo/quickgo.go b/quickgo/quickgo.go index 14827ee..d81bb04 100644 --- a/quickgo/quickgo.go +++ b/quickgo/quickgo.go @@ -3,6 +3,7 @@ package quickgo import ( "archive/zip" "bytes" + "encoding/base64" "fmt" "io" "io/fs" @@ -17,8 +18,10 @@ import ( "github.com/Nigel2392/goldcrest" "github.com/Nigel2392/quickgo/v2/quickgo/config" + "github.com/Nigel2392/quickgo/v2/quickgo/js" "github.com/Nigel2392/quickgo/v2/quickgo/logger" "github.com/Nigel2392/quickgo/v2/quickgo/quickfs" + "github.com/dop251/goja" "github.com/pkg/errors" ) @@ -102,7 +105,7 @@ func LoadApp() (*App, error) { // Check for the application config directory. var quickGoDir = GetQuickGoPath() - var projectDir = GetQuickGoPath("projects") + var projectDir = GetQuickGoPath(config.PROJECTS_DIR) _, err = os.Stat(projectDir) // Create the application config directory if it does not exist. @@ -669,6 +672,173 @@ func (a *App) ListProjectObjects() ([]*config.Project, error) { return projects, nil } +func (a *App) ListJSFiles() ([]string, error) { + var ( + dirPath = GetQuickGoPath(config.COMMANDS_DIR) + files = make([]string, 0) + ) + + dir, err := os.ReadDir(dirPath) + if err != nil && os.IsNotExist(err) { + return []string{}, nil + } else if err != nil { + return nil, errors.Wrapf(err, "failed to read directory %s", dirPath) + } + + for _, d := range dir { + if d.IsDir() { + continue + } + + if strings.HasSuffix(d.Name(), ".js") { + files = append(files, d.Name()[0:len(d.Name())-3]) + } + } + + return files, nil +} + +func (a *App) SaveJS(path string) error { + var ( + dirPath = GetQuickGoPath(config.COMMANDS_DIR) + filename = filepath.Join(dirPath, filepath.Base(path)) + scriptSrc *os.File + file *os.File + err error + ) + + path = filepath.FromSlash(path) + + logger.Infof("Copying file %s to %s", path, filename) + + if s, err := os.Stat(path); err != nil || s.IsDir() { + return errors.Wrapf(err, "file %s does not exist or is not a valid file", path) + } + + if err = os.MkdirAll(dirPath, os.ModePerm); err != nil { + return errors.Wrapf(err, "failed to create directory %s", dirPath) + } + + file, err = os.Create( + filename, + ) + if err != nil { + return errors.Wrapf(err, "failed to create file %s", path) + } + defer file.Close() + + scriptSrc, err = os.Open(path) + if err != nil { + return errors.Wrapf(err, "failed to open file %s", path) + } + + _, err = io.Copy(file, scriptSrc) + return errors.Wrapf(err, "failed to copy file %s to %s", path, file.Name()) +} + +func (a *App) ExecJS(scriptName string, args map[string]any) (err error) { + var ( + scriptPath = GetQuickGoPath( + config.COMMANDS_DIR, + fmt.Sprintf("%s.js", scriptName), + ) + + script []byte + proj *config.Project + cmd *js.Command + ) + + if proj = a.ProjectConfig; proj != nil { + args["project"] = proj + } + + script, err = os.ReadFile(scriptPath) + if err != nil { + return errors.Wrapf(err, "failed to read script %s", script) + } + + cmd = js.NewScript( + "main", + js.WithGlobal("readFile", func(path string) []byte { + var data, err = os.ReadFile(path) + if err != nil { + logger.Error(err) + } + return data + }), + js.WithGlobals(args), + ) + + cmd.AddFunc(func(vm *goja.Runtime) error { + vm.Set("app", a) + vm.Set("console", &js.JSConsole{ + Debug: logger.Debug, + Log: logger.Info, + Info: logger.Info, + Warn: logger.Warn, + Error: logger.Error, + }) + vm.Set("base64Encode", func(data goja.Value) string { + if vm.InstanceOf(data, vm.Get("Uint8Array").ToObject(vm)) { + var arr = data.Export().([]byte) + return base64.StdEncoding.EncodeToString(arr) + } + + if data, ok := data.Export().(goja.ArrayBuffer); ok { + return base64.StdEncoding.EncodeToString(data.Bytes()) + } + + var s = data.String() + return base64.StdEncoding.EncodeToString([]byte(s)) + }) + vm.Set("base64Decode", func(data string) goja.Value { + var arr, err = base64.StdEncoding.DecodeString(data) + if err != nil { + logger.Error(err) + return goja.Undefined() + } + + var arrBuff = vm.NewArrayBuffer(arr) + return vm.ToValue(arrBuff) + }) + vm.Set("writeFile", func(path string, data goja.Value) error { + var b []byte + if vm.InstanceOf(data, vm.Get("Uint8Array").ToObject(vm)) { + b = data.Export().([]byte) + } else if d, ok := data.Export().(goja.ArrayBuffer); ok { + b = d.Bytes() + } else { + b = []byte(data.String()) + } + + return os.WriteFile(path, b, os.ModePerm) + }) + vm.Set("readFile", func(path string) goja.Value { + var data []byte + var err error + if data, err = os.ReadFile(path); err != nil { + logger.Error(err) + return goja.Undefined() + } + + var arr = vm.NewArrayBuffer(data) + return vm.ToValue(arr) + }) + vm.Set("readTextFile", func(path string) string { + var data, err = os.ReadFile(path) + if err != nil { + logger.Error(err) + return "" + } + return string(data) + }) + + return nil + }) + + return cmd.Run(string(script)) +} + func (a *App) ListProjects() ([]string, error) { var p, err = a.ListProjectObjects() if err != nil { @@ -725,12 +895,12 @@ func GetProjectDirectoryPath(name string, absolute bool) string { var p string if absolute { p = filepath.ToSlash(GetQuickGoPath( - "projects", + config.PROJECTS_DIR, name, )) } else { p = filepath.ToSlash(filepath.Join( - "projects", + config.PROJECTS_DIR, name, )) }