feat: better CLI, build and serve mode
parent
f5d34a7c51
commit
5f837b5e89
@ -0,0 +1 @@
|
|||||||
|
package main
|
@ -1,25 +1,185 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/repr"
|
||||||
"github.com/aziis98/cabret/config"
|
"github.com/aziis98/cabret/config"
|
||||||
"github.com/aziis98/cabret/runner"
|
"github.com/aziis98/cabret/runner"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const HelpMessage = `usage: cabret <SUBCOMMAND> [OPTIONS...]
|
||||||
|
subcommands:
|
||||||
|
build [OPTIONS...] Builds the current project
|
||||||
|
serve [OPTIONS...] Starts an http server and watches files for changes
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.SetFlags(0)
|
flagSet := pflag.FlagSet{
|
||||||
|
Usage: func() {
|
||||||
|
fmt.Print(HelpMessage)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := flagSet.Parse(os.Args[1:]); err != nil {
|
||||||
|
if err != pflag.ErrHelp {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
optConfig := pflag.StringP("config", "c", "Cabretfile.yaml", `which configuration file to use`)
|
switch flagSet.Arg(0) {
|
||||||
pflag.Parse()
|
case "serve":
|
||||||
|
if err := serveSubcommand(os.Args[2:]); err != nil {
|
||||||
|
if err != pflag.ErrHelp {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cabretfile, err := config.ReadCabretfile(*optConfig)
|
case "build":
|
||||||
if err != nil {
|
if err := buildSubcommand(os.Args[2:]); err != nil {
|
||||||
|
if err != pflag.ErrHelp {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Print(HelpMessage)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildHelpMessage = `usage: cabret build [OPTIONS...]
|
||||||
|
options:
|
||||||
|
|
||||||
|
-c, --config <path> Path to configuration file (default is "Cabretfile.yaml")
|
||||||
|
-v, --verbose Be more verbose
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func buildSubcommand(args []string) error {
|
||||||
|
flagSet := pflag.FlagSet{
|
||||||
|
Usage: func() { fmt.Print(BuildHelpMessage) },
|
||||||
|
}
|
||||||
|
|
||||||
|
var configFile string
|
||||||
|
flagSet.StringVarP(&configFile, "config", "c",
|
||||||
|
"Cabretfile.yaml",
|
||||||
|
`path to configuration file`,
|
||||||
|
)
|
||||||
|
|
||||||
|
var verbose bool
|
||||||
|
flagSet.BoolVarP(&verbose, "verbose", "v",
|
||||||
|
false,
|
||||||
|
`verbose output`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := flagSet.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cabretfile, err := config.ReadCabretfile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := runner.RunConfig(cabretfile); err != nil {
|
if err := runner.RunConfig(cabretfile); err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServeHelpMessage = `usage: cabret serve [OPTIONS...]
|
||||||
|
options:
|
||||||
|
|
||||||
|
-c, --config <path> Path to configuration file (default is "Cabretfile.yaml")
|
||||||
|
-v, --verbose Be more verbose
|
||||||
|
-w, --watch <pattern> List of paths to watch for rebuild
|
||||||
|
-m, --mount <path> Mount a file or folder on the default static file
|
||||||
|
server, this will mount file or folder at "<path>"
|
||||||
|
on route "<path>".
|
||||||
|
-m, --mount <from>:<to> Mount a file or folder on the default static file
|
||||||
|
server, this will mount file or folder at "<from>"
|
||||||
|
on route "<to>".
|
||||||
|
|
||||||
|
By default the mount list is "public/:/", "dist/:/", "out/:/".
|
||||||
|
|
||||||
|
The server will also serve a special js script on "/cabret/live-reload.js" that
|
||||||
|
will add live reload on file change for development, this can be included with
|
||||||
|
the following tag
|
||||||
|
|
||||||
|
<script type="module" src="/__cabret__/live-reload.js" async></script>
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func serveSubcommand(args []string) error {
|
||||||
|
flagSet := pflag.FlagSet{
|
||||||
|
Usage: func() { fmt.Print(ServeHelpMessage) },
|
||||||
|
}
|
||||||
|
|
||||||
|
var configFile string
|
||||||
|
flagSet.StringVarP(&configFile, "config", "c",
|
||||||
|
"Cabretfile.yaml",
|
||||||
|
`path to configuration file`,
|
||||||
|
)
|
||||||
|
|
||||||
|
var verbose bool
|
||||||
|
flagSet.BoolVarP(&verbose, "verbose", "v",
|
||||||
|
false,
|
||||||
|
`verbose output`,
|
||||||
|
)
|
||||||
|
|
||||||
|
var watchedPatterns []string
|
||||||
|
flagSet.StringArrayVarP(&watchedPatterns, "watch", "w",
|
||||||
|
[]string{"src/**/"},
|
||||||
|
`list of paths to watch for rebuild`,
|
||||||
|
)
|
||||||
|
|
||||||
|
var mounts []string
|
||||||
|
flagSet.StringArrayVarP(&mounts, "mount", "m",
|
||||||
|
[]string{"public/:/", "out/:/", "dist/:/"},
|
||||||
|
`list of paths to mount for the static file server`,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := flagSet.Parse(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serveMounts := make([]serveMount, len(mounts))
|
||||||
|
for i, m := range mounts {
|
||||||
|
path, route, found := strings.Cut(m, ":")
|
||||||
|
if !found {
|
||||||
|
if strings.HasPrefix(path, "/") {
|
||||||
|
return fmt.Errorf(`cannot mount an absolute path without a route: "%s"`, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
route = "/" + path
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(route, "/") {
|
||||||
|
return fmt.Errorf(`route must start with a slash: "%s"`, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
serveMounts[i] = serveMount{path, route}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(`configFile = %v`, repr.String(configFile))
|
||||||
|
log.Printf(`verbose = %v`, repr.String(verbose))
|
||||||
|
log.Printf(`watchedPatterns = %v`, repr.String(watchedPatterns))
|
||||||
|
log.Printf(`serveMounts = %v`, repr.String(serveMounts))
|
||||||
|
|
||||||
|
return serve(serveConfig{
|
||||||
|
configFile,
|
||||||
|
verbose,
|
||||||
|
watchedPatterns,
|
||||||
|
serveMounts,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,165 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aziis98/cabret/config"
|
||||||
|
"github.com/aziis98/cabret/runner"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/gobwas/ws"
|
||||||
|
"github.com/gobwas/ws/wsutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serveMount struct {
|
||||||
|
path, route string
|
||||||
|
}
|
||||||
|
|
||||||
|
type serveConfig struct {
|
||||||
|
configFile string
|
||||||
|
verbose bool
|
||||||
|
watchedPatterns []string
|
||||||
|
mounts []serveMount
|
||||||
|
}
|
||||||
|
|
||||||
|
const LiveReloadScript = `
|
||||||
|
const ws = new WebSocket("ws://" + location.host + "/__cabret__/websocket")
|
||||||
|
ws.addEventListener("message", e => {
|
||||||
|
console.log("Server: " + e.data)
|
||||||
|
if (e.data === "reload") {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ws.addEventListener("close", e => {
|
||||||
|
console.log("Connection closed")
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("Live reload enabled")
|
||||||
|
`
|
||||||
|
|
||||||
|
func serve(c serveConfig) error {
|
||||||
|
cabretFile, err := config.ReadCabretfile(c.configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners := map[net.Conn]struct{}{}
|
||||||
|
|
||||||
|
// Create new watcher.
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
// Start listening for events.
|
||||||
|
go func() {
|
||||||
|
lastRebuild := time.Now()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Has(fsnotify.Write) {
|
||||||
|
log.Printf(`[Watcher] event: %s`, event)
|
||||||
|
|
||||||
|
if lastRebuild.Add(50 * time.Millisecond).After(time.Now()) {
|
||||||
|
log.Printf(`[Watcher] too fast, skipping`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this incremental
|
||||||
|
|
||||||
|
// trigger full rebuild
|
||||||
|
err := runner.RunConfig(cabretFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// broadcast refresh
|
||||||
|
for conn := range listeners {
|
||||||
|
err := wsutil.WriteServerText(conn, []byte("reload"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(`[LiveReload] got "%v", removing listener`, err)
|
||||||
|
delete(listeners, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRebuild = time.Now()
|
||||||
|
}
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// register watchers
|
||||||
|
for _, pattern := range c.watchedPatterns {
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range matches {
|
||||||
|
log.Printf(`[Watcher] registering "%s"`, m)
|
||||||
|
|
||||||
|
if err := watcher.Add(m); err != nil {
|
||||||
|
return fmt.Errorf(`cannot register pattern "%s": %w`, pattern, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
r.Use(middleware.NoCache)
|
||||||
|
|
||||||
|
r.Get("/__cabret__/live-reload.js", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeContent(w, r, "live-reload.js", time.Time{}, strings.NewReader(LiveReloadScript))
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Get("/__cabret__/websocket", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, _, _, err := ws.UpgradeHTTP(r, w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[conn] = struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, m := range c.mounts {
|
||||||
|
if strings.HasSuffix(m.route, "/") {
|
||||||
|
log.Printf(`[FileServer] mounting directory "%s" on "%s"`, m.path, m.route)
|
||||||
|
r.Handle(m.route+"*", http.StripPrefix(m.route, http.FileServer(http.Dir(m.path))))
|
||||||
|
} else {
|
||||||
|
log.Printf(`[FileServer] mounting file "%s" on "%s"`, m.path, m.route)
|
||||||
|
r.Get(m.route, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, m.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runner.RunConfig(cabretFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{Addr: ":5000", Handler: r}
|
||||||
|
|
||||||
|
log.Printf(`[FileServer] starting on ":5000"`)
|
||||||
|
return server.ListenAndServe()
|
||||||
|
}
|
Loading…
Reference in New Issue