From f0c1818b9148e56f8e1c0c3fd8a6c4070d9a80d3 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Tue, 5 Dec 2023 18:50:09 -0800 Subject: echo: add CSS and JS minifiers --- echo/controller/css_preprocessor.go | 91 +++++++++++++++++++++++++---- echo/controller/js_preprocessor.go | 110 ++++++++++++++++++++++++++++++++++++ echo/go.mod | 7 ++- echo/go.sum | 16 ++++-- 4 files changed, 208 insertions(+), 16 deletions(-) create mode 100644 echo/controller/js_preprocessor.go (limited to 'echo') diff --git a/echo/controller/css_preprocessor.go b/echo/controller/css_preprocessor.go index a00beb8..b174198 100644 --- a/echo/controller/css_preprocessor.go +++ b/echo/controller/css_preprocessor.go @@ -2,40 +2,111 @@ package controller import ( "bytes" + "errors" + "io" "io/fs" "net/http" + "path/filepath" + "strings" + lru "github.com/hashicorp/golang-lru/v2" "github.com/labstack/echo/v4" "code.crute.us/mcrute/golib/web/css" ) +const defaultCssCacheSize = 100 + type CSSMinifierController struct { Debug bool + Skip bool Name string Files fs.FS - cache []byte + cache *lru.Cache[string, []byte] +} + +type CSSMinifierConfig struct { + Debug bool + Skip bool + Name string + CacheSize int + Files fs.FS } -func (m *CSSMinifierController) Load() error { - if !m.Debug { - c := &bytes.Buffer{} - err := css.ParseWriteSheet(m.Files, m.Name, c) - m.cache = c.Bytes() +func NewCSSMinifierController(cfg CSSMinifierConfig) *CSSMinifierController { + s := cfg.CacheSize + if s == 0 { + s = defaultCssCacheSize + } + + c, err := lru.New[string, []byte](s) + if err != nil { // Should only happen if size == 0 + panic(err) + } + + return &CSSMinifierController{ + Debug: cfg.Debug, + Skip: cfg.Skip, + Name: cfg.Name, + Files: cfg.Files, + cache: c, + } +} + +func (m *CSSMinifierController) minify(name string, w io.Writer) error { + fd, err := m.mayOpenFile(name) + if err != nil { return err } - return nil + defer fd.Close() + return css.ParseWriteSheet(m.Files, name, w) +} + +// mayOpenFile tries to open a file and translates fs.ErrNotExist into +// echo.ErrNotFound +func (m *CSSMinifierController) mayOpenFile(path string) (io.ReadCloser, error) { + fd, err := m.Files.Open(path) + if err != nil && errors.Is(err, fs.ErrNotExist) { + return nil, echo.ErrNotFound + } + return fd, err } func (m *CSSMinifierController) Handle(c echo.Context) error { r := c.Response() r.Header().Set("Content-Type", "text/css") - r.WriteHeader(http.StatusOK) + + path := strings.TrimPrefix(filepath.Clean(c.Request().URL.Path), "/") + if filepath.Ext(path) != ".css" { + return c.NoContent(http.StatusBadRequest) + } if m.Debug { - return css.ParseWriteSheet(m.Files, m.Name, r) + if m.Skip { + fd, err := m.mayOpenFile(path) + if err != nil { + return err + } + defer fd.Close() + + r.WriteHeader(http.StatusOK) + _, err = io.Copy(r, fd) + return err + } + return m.minify(path, r) } else { - _, err := r.Write(m.cache) + val, ok := m.cache.Get(path) + if !ok { + buf := &bytes.Buffer{} + if err := m.minify(path, buf); err != nil { + return err + } + m.cache.Add(path, buf.Bytes()) + val = buf.Bytes() + } + + r.WriteHeader(http.StatusOK) + _, err := r.Write(val) return err } } diff --git a/echo/controller/js_preprocessor.go b/echo/controller/js_preprocessor.go new file mode 100644 index 0000000..377f270 --- /dev/null +++ b/echo/controller/js_preprocessor.go @@ -0,0 +1,110 @@ +package controller + +import ( + "bytes" + "errors" + "io" + "io/fs" + "net/http" + "path/filepath" + "strings" + + lru "github.com/hashicorp/golang-lru/v2" + "github.com/labstack/echo/v4" + "github.com/tdewolff/minify/v2/js" +) + +const defaultJsCacheSize = 100 + +type JSMinifierController struct { + Debug bool + Skip bool + Files fs.FS + cache *lru.Cache[string, []byte] + min *js.Minifier +} + +type JSMinifierConfig struct { + Debug bool + Skip bool + CacheSize int + Files fs.FS +} + +func NewJSMinifierController(cfg JSMinifierConfig) *JSMinifierController { + s := cfg.CacheSize + if s == 0 { + s = defaultJsCacheSize + } + + c, err := lru.New[string, []byte](s) + if err != nil { // Should only happen if size == 0 + panic(err) + } + + return &JSMinifierController{ + Debug: cfg.Debug, + Skip: cfg.Skip, + Files: cfg.Files, + cache: c, + min: &js.Minifier{}, + } +} + +func (m *JSMinifierController) minify(name string, w io.Writer) error { + fd, err := m.mayOpenFile(name) + if err != nil { + return err + } + defer fd.Close() + return m.min.Minify(nil, w, fd, nil) +} + +// mayOpenFile tries to open a file and translates fs.ErrNotExist into +// echo.ErrNotFound +func (m *JSMinifierController) mayOpenFile(path string) (io.ReadCloser, error) { + fd, err := m.Files.Open(path) + if err != nil && errors.Is(err, fs.ErrNotExist) { + return nil, echo.ErrNotFound + } + return fd, err +} + +func (m *JSMinifierController) Handle(c echo.Context) error { + r := c.Response() + r.Header().Set("Content-Type", "text/javascript") + + path := strings.TrimPrefix(filepath.Clean(c.Request().URL.Path), "/") + if filepath.Ext(path) != ".js" { + return c.NoContent(http.StatusBadRequest) + } + + if m.Debug { + if m.Skip { + fd, err := m.mayOpenFile(path) + if err != nil { + return err + } + defer fd.Close() + + r.WriteHeader(http.StatusOK) + _, err = io.Copy(r, fd) + return err + } + return m.minify(path, r) + } else { + val, ok := m.cache.Get(path) + if !ok { + buf := &bytes.Buffer{} + if err := m.minify(path, buf); err != nil { + return err + } + m.cache.Add(path, buf.Bytes()) + val = buf.Bytes() + } + + r.WriteHeader(http.StatusOK) + _, err := r.Write(val) + return err + } +} diff --git a/echo/go.mod b/echo/go.mod index 23a62be..64877fc 100644 --- a/echo/go.mod +++ b/echo/go.mod @@ -7,13 +7,15 @@ require ( code.crute.us/mcrute/golib/clients/netbox/v4 v4.1.0 code.crute.us/mcrute/golib/secrets v0.4.0 code.crute.us/mcrute/golib/vault v0.2.6 - code.crute.us/mcrute/golib/web/css v0.1.0 + code.crute.us/mcrute/golib/web/css v0.1.1 github.com/elnormous/contenttype v1.0.3 + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/labstack/echo/v4 v4.6.1 github.com/labstack/gommon v0.3.1 github.com/prometheus/client_golang v1.11.0 github.com/quic-go/quic-go v0.39.0 github.com/stretchr/testify v1.8.1 + github.com/tdewolff/minify/v2 v2.20.9 gopkg.in/square/go-jose.v2 v2.5.1 ) @@ -70,6 +72,7 @@ require ( github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.3.4 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/tdewolff/parse/v2 v2.7.6 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect go.uber.org/atomic v1.9.0 // indirect @@ -78,7 +81,7 @@ require ( golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.10.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect golang.org/x/tools v0.9.1 // indirect diff --git a/echo/go.sum b/echo/go.sum index 462635f..05485aa 100644 --- a/echo/go.sum +++ b/echo/go.sum @@ -38,8 +38,8 @@ code.crute.us/mcrute/golib/secrets v0.4.0 h1:tZzQEOnJshDGuzvvr0n0BMWZbu3ZMB5QRqI code.crute.us/mcrute/golib/secrets v0.4.0/go.mod h1:c40ezKg/NXe5NE3PaCRIUJC6D6XCoPSu9+duZSdKsNY= code.crute.us/mcrute/golib/vault v0.2.6 h1:X+TlEGFPj6pj3OqmrJprv+wJYdo8QTR2IpP3EfVniHU= code.crute.us/mcrute/golib/vault v0.2.6/go.mod h1:QBgcKiG94tPHAcxeRyNHrfiLGSKtojlRDLGRX5I6LgE= -code.crute.us/mcrute/golib/web/css v0.1.0 h1:VdP0i2Q+JC+TxiyWTAdqksDcKU4GKwL2Ly02ZRkyDFw= -code.crute.us/mcrute/golib/web/css v0.1.0/go.mod h1:USqoGbYKNDhEVZITLxSxd/vFXBihL8/N3Gg/v01hNWo= +code.crute.us/mcrute/golib/web/css v0.1.1 h1:cJ9/fPMPPLzwkMG6Z99SbpAxyknXXEu2ADR5fRG/yTo= +code.crute.us/mcrute/golib/web/css v0.1.1/go.mod h1:USqoGbYKNDhEVZITLxSxd/vFXBihL8/N3Gg/v01hNWo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -228,6 +228,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/vault/api v1.8.0 h1:7765sW1XBt+qf4XKIYE4ebY9qc/yi9V2/egzGSUNMZU= @@ -373,6 +375,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tdewolff/minify/v2 v2.20.9 h1:0RGsL+jBpm77obkuNCjNZ2eiN81CZzTnjeVmTqxCmYk= +github.com/tdewolff/minify/v2 v2.20.9/go.mod h1:hZnNtFqXVQ5QIAR05tdgvS7h6E80jyRwHSGVmM4jbzQ= +github.com/tdewolff/parse/v2 v2.7.6 h1:PGZH2b/itDSye9RatReRn4GBhsT+KFEMtAMjHRuY1h8= +github.com/tdewolff/parse/v2 v2.7.6/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52 h1:gAQliwn+zJrkjAHVcBEYW/RFvd2St4yYimisvozAYlA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -530,8 +538,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -- cgit v1.2.3