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