summaryrefslogtreecommitdiff
path: root/js/js.go
diff options
context:
space:
mode:
Diffstat (limited to 'js/js.go')
-rw-r--r--js/js.go133
1 files changed, 133 insertions, 0 deletions
diff --git a/js/js.go b/js/js.go
new file mode 100644
index 0000000..c5b8818
--- /dev/null
+++ b/js/js.go
@@ -0,0 +1,133 @@
+package js
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "time"
+
+ "flyscrape/js/jsbundle"
+
+ "github.com/evanw/esbuild/pkg/api"
+ v8 "rogchap.com/v8go"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+type RunOptions struct {
+ HTML string
+}
+
+type RunFunc func(RunOptions) any
+
+type Options struct {
+ URL string `json:"url"`
+}
+
+func Compile(file string) (*Options, RunFunc, error) {
+ src, err := build(file)
+ if err != nil {
+ return nil, nil, err
+ }
+ os.WriteFile("out.js", []byte(src), 0o644)
+ return vm(src)
+}
+
+func build(file string) (string, error) {
+ dir, err := os.MkdirTemp("", "flyscrape")
+ if err != nil {
+ return "", err
+ }
+ defer os.RemoveAll(dir)
+
+ tmpfile := filepath.Join(dir, "flyscrape.js")
+ if err := os.WriteFile(tmpfile, jsbundle.Flyscrape, 0o644); err != nil {
+ return "", err
+ }
+
+ resolve := api.Plugin{
+ Name: "flyscrape",
+ Setup: func(build api.PluginBuild) {
+ build.OnResolve(api.OnResolveOptions{
+ Filter: "^flyscrape$",
+ }, func(ora api.OnResolveArgs) (api.OnResolveResult, error) {
+ return api.OnResolveResult{Path: tmpfile}, nil
+ })
+ },
+ }
+
+ res := api.Build(api.BuildOptions{
+ EntryPoints: []string{file},
+ Bundle: true,
+ Platform: api.PlatformNode,
+ Plugins: []api.Plugin{resolve},
+ })
+
+ var errs []error
+ for _, msg := range res.Errors {
+ errs = append(errs, fmt.Errorf("%s", msg.Text))
+ }
+ if len(res.Errors) > 0 {
+ return "", errors.Join(errs...)
+ }
+
+ out := string(res.OutputFiles[0].Contents)
+ return out, nil
+}
+
+func vm(src string) (*Options, RunFunc, error) {
+ os.WriteFile("out.js", []byte(src), 0o644)
+
+ ctx := v8.NewContext()
+ ctx.RunScript("var module = {}", "main.js")
+ if _, err := ctx.RunScript(src, "main.js"); err != nil {
+ return nil, nil, fmt.Errorf("run bundled js: %w", err)
+ }
+
+ val, err := ctx.RunScript("module.exports.options", "main.js")
+ if err != nil {
+ return nil, nil, fmt.Errorf("export options: %w", err)
+ }
+ options, err := val.AsObject()
+ if err != nil {
+ return nil, nil, fmt.Errorf("cast options as object: %w", err)
+ }
+
+ var opts Options
+ url, err := options.Get("url")
+ if err != nil {
+ return nil, nil, fmt.Errorf("getting url from options: %w", err)
+ }
+ opts.URL = url.String()
+
+ run := func(ro RunOptions) any {
+ suffix := randSeq(10)
+ ctx.Global().Set("html_"+suffix, ro.HTML)
+ data, err := ctx.RunScript(fmt.Sprintf(`JSON.stringify(module.exports.default({html: html_%s}))`, suffix), "main.js")
+ if err != nil {
+ return err.Error()
+ }
+
+ var obj any
+ if err := json.Unmarshal([]byte(data.String()), &obj); err != nil {
+ return err.Error()
+ }
+
+ return obj
+ }
+ return &opts, run, nil
+}
+
+func randSeq(n int) string {
+ letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+ b := make([]rune, n)
+ for i := range b {
+ b[i] = letters[rand.Intn(len(letters))]
+ }
+ return string(b)
+}