package main import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/evanw/esbuild/pkg/api" "github.com/goyek/goyek" "github.com/jaschaephraim/lrserver" "github.com/otiai10/copy" "github.com/radovskyb/watcher" ) var triggerReload = make(chan struct{}) type options struct { ESBuild api.BuildOptions Watch struct { Path string Exclude []string } Serve struct { Path string Port int } Copy []struct { Src string Dest string } Replace []struct { Pattern string Search string Replace string } Link struct { From string To string } } func readCfg(cfgPath string) []options { cfgContent, err := os.ReadFile(cfgPath) if err != nil { fmt.Printf("%+v\n", err) os.Exit(1) } optsSetups := []options{} err = json.Unmarshal(cfgContent, &optsSetups) if err != nil { opt := options{} err = json.Unmarshal(cfgContent, &opt) if err != nil { fmt.Printf("%+v\n", err) os.Exit(1) } optsSetups = append(optsSetups, opt) } return optsSetups } func main() { flow := &goyek.Flow{} cfgPathParam := flow.RegisterStringParam(goyek.StringParam{ Name: "c", Usage: "Path to config file config file.", Default: "./.gowebbuild.json", }) prodParam := flow.RegisterBoolParam(goyek.BoolParam{ Name: "p", Usage: "Use production ready build settings", Default: false, }) buildOnly := goyek.Task{ Name: "build", Usage: "", Params: goyek.Params{cfgPathParam, prodParam}, Action: func(tf *goyek.TF) { cfgPath := cfgPathParam.Get(tf) os.Chdir(filepath.Dir(cfgPath)) opts := readCfg(cfgPath) for _, o := range opts { cp(o) if prodParam.Get(tf) { o.ESBuild.MinifyIdentifiers = true o.ESBuild.MinifySyntax = true o.ESBuild.MinifyWhitespace = true o.ESBuild.Sourcemap = api.SourceMapNone } api.Build(o.ESBuild) replace(o) } }, } watch := goyek.Task{ Name: "watch", Usage: "", Params: goyek.Params{cfgPathParam}, Action: func(tf *goyek.TF) { cfgPath := cfgPathParam.Get(tf) os.Chdir(filepath.Dir(cfgPath)) optsSetups := readCfg(cfgPath) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) for i := range optsSetups { opts := optsSetups[i] go func(opts options) { w := watcher.New() w.SetMaxEvents(1) w.FilterOps(watcher.Write, watcher.Rename, watcher.Move, watcher.Create, watcher.Remove) if len(opts.Watch.Exclude) > 0 { w.Ignore(opts.Watch.Exclude...) } if err := w.AddRecursive(opts.Watch.Path); err != nil { fmt.Println(err.Error()) os.Exit(1) } go func() { for { select { case event := <-w.Event: fmt.Printf("File %s changed\n", event.Name()) cp(opts) build(opts) replace(opts) case err := <-w.Error: fmt.Println(err.Error()) case <-w.Closed: return } } }() fmt.Printf("Watching %d elements in %s\n", len(w.WatchedFiles()), opts.Watch.Path) cp(opts) build(opts) replace(opts) if err := w.Start(time.Millisecond * 100); err != nil { fmt.Println(err.Error()) } }(opts) if opts.Serve.Path != "" { go func() { port := 8888 if opts.Serve.Port != 0 { port = opts.Serve.Port } http.Handle("/", http.FileServer(http.Dir(opts.Serve.Path))) fmt.Printf("Serving contents of %s at :%d\n", opts.Serve.Path, port) err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) if err != nil { fmt.Printf("%+v\n", err.Error()) os.Exit(1) } }() } if opts.Link.From != "" { reqBuildCh := link(opts.Link.From, opts.Link.To) go func() { for range reqBuildCh { cp(opts) build(opts) replace(opts) } }() } } go func() { fmt.Println("Starting live reload server") lr := lrserver.New(lrserver.DefaultName, lrserver.DefaultPort) go func() { for { <-triggerReload lr.Reload("") } }() lr.SetStatusLog(nil) err := lr.ListenAndServe() if err != nil { panic(err) } }() <-c fmt.Println("\nExit") os.Exit(0) }, } flow.DefaultTask = flow.Register(watch) flow.Register(buildOnly) flow.Main() } func cp(opts options) { if len(opts.Copy) == 0 { fmt.Println("Nothing to copy") return } for _, op := range opts.Copy { paths, err := filepath.Glob(op.Src) if err != nil { fmt.Printf("Invalid glob pattern: %s\n", op.Src) continue } destIsDir := isDir(op.Dest) for _, p := range paths { d := op.Dest if destIsDir && isFile(p) { d = filepath.Join(d, filepath.Base(p)) } err := copy.Copy(p, d) fmt.Printf("Copying %s to %s\n", p, d) if err != nil { fmt.Printf("Failed to copy %s: %v\n", p, err) continue } } } } func replace(opts options) { if len(opts.Replace) == 0 { fmt.Println("Nothing to replace") return } for _, op := range opts.Replace { paths, err := filepath.Glob(op.Pattern) if err != nil { fmt.Printf("Invalid glob pattern: %s\n", op.Pattern) continue } for _, p := range paths { if !isFile(p) { continue } read, err := ioutil.ReadFile(p) if err != nil { fmt.Printf("%+v\n", err) os.Exit(1) } r := op.Replace if strings.HasPrefix(op.Replace, "$") { r = os.ExpandEnv(op.Replace) } count := strings.Count(string(read), op.Search) if count > 0 { fmt.Printf("Replacing %d occurrences of '%s' with '%s' in %s\n", count, op.Search, r, p) newContents := strings.ReplaceAll(string(read), op.Search, r) err = ioutil.WriteFile(p, []byte(newContents), 0) if err != nil { fmt.Printf("%+v\n", err) os.Exit(1) } } } } } func isFile(path string) bool { stat, err := os.Stat(path) if errors.Is(err, os.ErrNotExist) { return false } return !stat.IsDir() } func isDir(path string) bool { stat, err := os.Stat(path) if errors.Is(err, os.ErrNotExist) { os.MkdirAll(path, 0755) return true } return err == nil && stat.IsDir() } func build(opts options) { result := api.Build(opts.ESBuild) if len(result.Errors) == 0 { triggerReload <- struct{}{} } }