diff --git a/build.go b/build.go new file mode 100644 index 0000000..fe79491 --- /dev/null +++ b/build.go @@ -0,0 +1,39 @@ +package tailo + +import ( + "fmt" + "os" + "os/exec" +) + +// Build runs the Tailwind CSS CLI binary to build the +// CSS file and generate compiled CSS it expects to find +// the options in the config file. +func Build(options ...option) { + // Applying passed options + for _, option := range options { + option() + } + + if _, err := os.Stat(binaryPath); os.IsNotExist(err) { + err := Setup() + if err != nil { + panic(err) + } + } + + cmd := exec.Command(binaryPath) + cmd.Args = append(cmd.Args, "--config", configPath) + cmd.Args = append(cmd.Args, "--input", inputPath) + cmd.Args = append(cmd.Args, "--output", outputPath) + + cmd.Stdout = writer(0) + cmd.Stderr = writer(0) + + fmt.Println("[tailo] Running:", cmd.String()) + + err := cmd.Run() + if err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2a8ac46 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/paganotoni/tailo + +go 1.20 + +require github.com/fsnotify/fsnotify v1.6.0 + +require golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..60d7d6e --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/options.go b/options.go new file mode 100644 index 0000000..3cf53b9 --- /dev/null +++ b/options.go @@ -0,0 +1,49 @@ +package tailo + +var ( + // configPath is the path to the TailwindCSS configuration file. + configPath = "config/tailwind.config.js" + + // inputPath is the path to the input file, the one with @apply rules. + inputPath = "web/assets/application.css" + + // outputPath is the path to the output file, the one with compiled CSS. + outputPath = "web/public/application.css" + + // binaryPath is the path to the TailwindCSS CLI binary. + binaryPath = "bin/tailwindcss" +) + +type option func() + +// UseConfigPath sets the path to the TailwindCSS configuration file +// otherwise it defaults to "config/tailwind.config.js". +func UseConfigPath(path string) option { + return func() { + configPath = path + } +} + +// UseInputPath sets the path to the input file, the one with @apply rules +// otherwise it defaults to "web/assets/application.css". +func UseInputPath(path string) option { + return func() { + inputPath = path + } +} + +// UseOutputPath sets the path to the output file, the one with compiled CSS +// otherwise it defaults to "web/public/application.css". +func UseOutputPath(path string) option { + return func() { + outputPath = path + } +} + +// UseBinaryPath sets the path to the TailwindCSS CLI binary otherwise it +// defaults to "bin/tailwindcss". +func UseBinaryPath(path string) option { + return func() { + binaryPath = path + } +} diff --git a/setup.go b/setup.go new file mode 100644 index 0000000..6cb15df --- /dev/null +++ b/setup.go @@ -0,0 +1,75 @@ +// tailo is a wrapper for the Tailwind CSS CLI that +// facilitates the download and of the CLI and the +// config file. +package tailo + +import ( + "fmt" + "io" + "net/http" + "os" + "runtime" +) + +var ( + binaries = map[string]string{ + "darwin_amd64": "tailwindcss-macos-x64", + "darwin_arm64": "tailwindcss-macos-arm64", + "linux_amd64": "tailwindcss-linux-x64", + "linux_arm64": "tailwindcss-linux-arm64", + "linux_arm": "tailwindcss-linux-armv7", + "windows_amd64": "tailwindcss-windows-x64", + "windows_arm64": "tailwindcss-windows-arm64", + } +) + +// Setup downloads the Tailwind CSS CLI binary for the +// given operating system and architecture. It makes the +// binary executable and places it in the bin/ directory. +func Setup() error { + if _, err := os.Stat("bin/tailwindcss"); err == nil { + fmt.Println("Tailwind CSS CLI binary already exists.") + + return nil + } + + currentArch := fmt.Sprintf("%v_%v", runtime.GOOS, runtime.GOARCH) + binary, ok := binaries[currentArch] + if !ok { + return fmt.Errorf("Unsupported operating system and architecture: %s", currentArch) + } + + url := fmt.Sprintf("https://github.com/tailwindlabs/tailwindcss/releases/latest/download/%v", binary) + fmt.Println(url) + + resp, err := http.Get(url) + if err != nil { + return err + } + + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("Could not download Tailwind CSS CLI binary: %s", resp.Status) + } + + // Create the file + out, err := os.Create("bin/tailwindcss") + if err != nil { + return err + } + + defer out.Close() + + err = os.MkdirAll("bin", 0755) + if err != nil { + return err + } + + err = os.Chmod("bin/tailwindcss", 0755) + if err != nil { + return err + } + + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/tailo.go b/tailo.go new file mode 100644 index 0000000..61df4ca --- /dev/null +++ b/tailo.go @@ -0,0 +1,88 @@ +// tailo is a wrapper for the Tailwind CSS CLI that +// facilitates the download and of the CLI and the +// config file. +package tailo + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" +) + +type extensions []string + +func (e extensions) Has(ext string) bool { + for _, v := range e { + if v == ext { + return true + } + } + + return false +} + +func Watch() { + Build() + + watcher, err := buildWatcher() + if err != nil { + panic(err) + } + + // Start listening for events. + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + if event.Has(fsnotify.Write) { + Build() + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) + } + } + }() + + <-make(chan struct{}) +} + +// Extensions to watch for changes. +var watchExtensions = extensions{".html", ".css"} + +func buildWatcher() (*fsnotify.Watcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return watcher, fmt.Errorf("error creating watcher: %w", err) + } + + // Add all files that need to be watched to the + // watcher so it notifies the errors that it needs to + // restart. + err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + if !watchExtensions.Has(filepath.Ext(path)) { + return nil + } + + return watcher.Add(path) + }) + + if err != nil { + return watcher, fmt.Errorf("error loading paths: %w", err) + } + + return watcher, err +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..371955e --- /dev/null +++ b/writer.go @@ -0,0 +1,18 @@ +package tailo + +import ( + "fmt" + "strings" +) + +type writer int + +func (w writer) Write(p []byte) (n int, err error) { + content := strings.TrimSpace(string(p)) + if content == "" { + return len(p), nil + } + + fmt.Println("[tailo]", content) + return len(p), nil +}