229 lines
5.5 KiB
Go
229 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/a-h/templ"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/charmbracelet/log"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
"github.com/tdewolff/minify/v2"
|
|
"github.com/tdewolff/minify/v2/css"
|
|
"github.com/tdewolff/minify/v2/html"
|
|
)
|
|
|
|
var (
|
|
ctx = context.Background()
|
|
previewPort = ":8080"
|
|
logger = log.New(os.Stderr)
|
|
minifier = minify.New()
|
|
)
|
|
|
|
func closeFile(file *os.File) {
|
|
if err := file.Close(); err != nil {
|
|
logger.Fatal("failed to close file", "err", err)
|
|
}
|
|
}
|
|
|
|
// minifyInPlace replaces the file content with the minified version
|
|
func minifyInPlace(mediatype, path string) error {
|
|
tempPath := path + ".bak"
|
|
err := os.Rename(path, tempPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
inputFile, err := os.Open(tempPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Open the file again for writing
|
|
outputFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
closeFile(inputFile)
|
|
closeFile(outputFile)
|
|
|
|
if err := os.Remove(tempPath); err != nil {
|
|
logger.Fatal("failed to delete temporary input file", "file", path)
|
|
}
|
|
}()
|
|
|
|
return minifier.Minify(mediatype, outputFile, inputFile)
|
|
}
|
|
|
|
func genIndexFile(path string, c templ.Component) error {
|
|
indexFile, err := os.Create(filepath.Join(path, "index.html"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeFile(indexFile)
|
|
|
|
buf := new(bytes.Buffer)
|
|
if err := c.Render(ctx, buf); err != nil {
|
|
return err
|
|
}
|
|
|
|
return minifier.Minify("text/html", indexFile, buf)
|
|
}
|
|
|
|
func traverse(prefix, path string, d fs.DirEntry) error {
|
|
// Generate home page for the root directory.
|
|
if prefix == path {
|
|
logger.Info("create index.html for root directory")
|
|
return genIndexFile(path, indexPage())
|
|
}
|
|
|
|
// Otherwise generate a listing page
|
|
if d.IsDir() {
|
|
trimedPath := strings.TrimPrefix(strings.TrimPrefix(path, prefix), "/")
|
|
logger.Info("create index.html", "dir", trimedPath)
|
|
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return genIndexFile(path, listingPage(trimedPath, entries))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// preview Previews the website at specified root directory on localhost:<port>
|
|
func preview(root, port string) error {
|
|
e := echo.New()
|
|
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
|
LogURI: true,
|
|
LogError: true,
|
|
LogStatus: true,
|
|
LogMethod: true,
|
|
LogLatency: true,
|
|
LogValuesFunc: func(_ echo.Context, v middleware.RequestLoggerValues) error {
|
|
logger.
|
|
With("Method", v.Method).
|
|
With("URI", v.URI).
|
|
With("Status", v.Status).
|
|
With("Latency", v.Latency).
|
|
Info("request")
|
|
|
|
if v.Error != nil {
|
|
logger.Error("request", "err", v.Error)
|
|
}
|
|
return nil
|
|
},
|
|
}))
|
|
e.Static("/", root)
|
|
return e.Start(port)
|
|
}
|
|
|
|
func main() {
|
|
genFlag := flag.Bool("gen", false, "Generate index files")
|
|
previewFlag := flag.Bool("preview", false, "Preview the built website")
|
|
minifyFlag := flag.Bool("minify", false, "Minify existing CSS files")
|
|
flag.Usage = func() {
|
|
fmt.Printf(`Usage: go run . [OPTIONS] DIR
|
|
|
|
Arguments:
|
|
DIR
|
|
Path to the directory containing the source files
|
|
|
|
Options:
|
|
`)
|
|
flag.PrintDefaults()
|
|
}
|
|
flag.Parse()
|
|
|
|
// Being fancy ^-^
|
|
logger.SetStyles(&log.Styles{
|
|
Separator: lipgloss.NewStyle().Foreground(lipgloss.Color("#7b88a1")).Bold(true),
|
|
Message: lipgloss.NewStyle().Foreground(lipgloss.Color("#7b88a1")),
|
|
Key: lipgloss.NewStyle().Foreground(lipgloss.Color("4")).Bold(true),
|
|
Levels: map[log.Level]lipgloss.Style{
|
|
log.FatalLevel: lipgloss.NewStyle().
|
|
Bold(true).
|
|
Background(lipgloss.Color("5")).
|
|
Foreground(lipgloss.Color("0")).
|
|
Padding(0, 1, 0, 1).
|
|
SetString("FATAL"),
|
|
log.ErrorLevel: lipgloss.NewStyle().
|
|
Bold(true).
|
|
Background(lipgloss.Color("1")).
|
|
Foreground(lipgloss.Color("0")).
|
|
Padding(0, 1, 0, 1).
|
|
SetString("ERROR"),
|
|
log.InfoLevel: lipgloss.NewStyle().
|
|
Bold(true).
|
|
Background(lipgloss.Color("6")).
|
|
Foreground(lipgloss.Color("0")).
|
|
Padding(0, 1, 0, 1).
|
|
SetString("INFO"),
|
|
},
|
|
Keys: map[string]lipgloss.Style{
|
|
"err": lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("1")),
|
|
},
|
|
})
|
|
|
|
// Load minifiers
|
|
minifier.AddFunc("text/html", html.Minify)
|
|
minifier.AddFunc("text/css", css.Minify)
|
|
|
|
// The root directory argument is required
|
|
if len(flag.Args()) != 1 {
|
|
logger.Fatal("failed to parse arguments", "err", "exactly 1 argument DIR is required")
|
|
}
|
|
|
|
rootDir, err := filepath.Abs(flag.Arg(0))
|
|
if err != nil {
|
|
logger.Fatal("unable to get root directory", "err", err)
|
|
}
|
|
|
|
// Run minification first, to ensure file size is calculated correctly later
|
|
if *minifyFlag {
|
|
err = filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.Type().IsRegular() && filepath.Ext(path) == ".css" {
|
|
logger.Info("minify CSS", "file", strings.TrimPrefix(path, rootDir+"/"))
|
|
return minifyInPlace("text/css", path)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
logger.Fatal("failed minifying CSS files", "err", err)
|
|
}
|
|
}
|
|
|
|
if *genFlag {
|
|
err = filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return traverse(rootDir, path, d)
|
|
})
|
|
if err != nil {
|
|
logger.Fatal("failed to traverse root directory", "err", err)
|
|
}
|
|
}
|
|
|
|
// Only preview after things are generated
|
|
if *previewFlag {
|
|
logger.Fatal("failed to preview website", "err", preview(rootDir, previewPort))
|
|
}
|
|
}
|