1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-12-12 05:24:27 +02:00
pocketbase/plugins/jsvm/hooks.go

166 lines
3.5 KiB
Go

package jsvm
import (
"path/filepath"
"runtime"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/process"
"github.com/dop251/goja_nodejs/require"
"github.com/fatih/color"
"github.com/fsnotify/fsnotify"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
// HooksConfig defines the config options of the JS app hooks plugin.
type HooksConfig struct {
// Dir specifies the directory with the JS app hooks.
//
// If not set it fallbacks to a relative "pb_data/../pb_hooks" directory.
Dir string
// Watch enables auto app restarts when a JS app hook file changes.
//
// Note that currently the application cannot be automatically restarted on Windows
// because the restart process relies on execve.
Watch bool
}
// MustRegisterHooks registers the JS hooks plugin to
// the provided app instance and panics if it fails.
//
// Example usage:
//
// jsvm.MustRegisterHooks(app, jsvm.HooksConfig{})
func MustRegisterHooks(app core.App, config HooksConfig) {
if err := RegisterHooks(app, config); err != nil {
panic(err)
}
}
// RegisterHooks registers the JS hooks plugin to the provided app instance.
func RegisterHooks(app core.App, config HooksConfig) error {
p := &hooks{app: app, config: config}
if p.config.Dir == "" {
p.config.Dir = filepath.Join(app.DataDir(), "../pb_hooks")
}
// fetch all js hooks sorted by their filename
files, err := filesContent(p.config.Dir, `^.*\.pb\.js$`)
if err != nil {
return err
}
dbx.HashExp{}.Build(app.DB(), nil)
registry := new(require.Registry) // this can be shared by multiple runtimes
loop := eventloop.NewEventLoop()
loop.Run(func(vm *goja.Runtime) {
registry.Enable(vm)
console.Enable(vm)
process.Enable(vm)
baseBinds(vm)
dbxBinds(vm)
filesystemBinds(vm)
tokensBinds(vm)
securityBinds(vm)
formsBinds(vm)
apisBinds(vm)
vm.Set("$app", app)
for file, content := range files {
_, err := vm.RunString(string(content))
if err != nil {
if p.config.Watch {
color.Red("Failed to execute %s: %v", file, err)
} else {
// return err
}
}
}
})
loop.Start()
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
loop.StopNoWait()
return nil
})
if p.config.Watch {
return p.watchFiles()
}
return nil
}
type hooks struct {
app core.App
config HooksConfig
}
func (h *hooks) watchFiles() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
h.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
watcher.Close()
return nil
})
var debounceTimer *time.Timer
// start listening for events.
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceTimer = time.AfterFunc(100*time.Millisecond, func() {
// app restart is currently not supported on Windows
if runtime.GOOS == "windows" {
color.Yellow("File %s changed, please restart the app", event.Name)
} else {
color.Yellow("File %s changed, restarting...", event.Name)
if err := h.app.Restart(); err != nil {
color.Red("Failed to restart the app:", err)
}
}
})
case err, ok := <-watcher.Errors:
if !ok {
return
}
color.Red("Watch error:", err)
}
}
}()
// add the directory to watch
err = watcher.Add(h.config.Dir)
if err != nil {
watcher.Close()
return err
}
return nil
}