feat: better CLI, build and serve mode
parent
f5d34a7c51
commit
5f837b5e89
@ -0,0 +1 @@
|
||||
package main
|
@ -1,25 +1,185 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/repr"
|
||||
"github.com/aziis98/cabret/config"
|
||||
"github.com/aziis98/cabret/runner"
|
||||
"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() {
|
||||
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`)
|
||||
pflag.Parse()
|
||||
switch flagSet.Arg(0) {
|
||||
case "serve":
|
||||
if err := serveSubcommand(os.Args[2:]); err != nil {
|
||||
if err != pflag.ErrHelp {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cabretfile, err := config.ReadCabretfile(*optConfig)
|
||||
if err != nil {
|
||||
case "build":
|
||||
if err := buildSubcommand(os.Args[2:]); err != nil {
|
||||
if err != pflag.ErrHelp {
|
||||
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 {
|
||||
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