summaryrefslogblamecommitdiff
path: root/js/js.go
blob: c5b8818bd0330215ea43656f8bf39ad5679b1dd9 (plain) (tree)




































































































































                                                                                                                                     
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)
}