Added css minify

This commit is contained in:
2026-06-13 04:00:52 +08:00
parent 55541a5930
commit 803bd80557
10 changed files with 259 additions and 142 deletions
+34 -13
View File
@@ -8,7 +8,9 @@ import (
"time" "time"
"github.com/tgckpg/resolver-go/internal/closure" "github.com/tgckpg/resolver-go/internal/closure"
"github.com/tgckpg/resolver-go/internal/compilecache"
"github.com/tgckpg/resolver-go/internal/generated" "github.com/tgckpg/resolver-go/internal/generated"
"github.com/tgckpg/resolver-go/internal/minifier/css"
"github.com/tgckpg/resolver-go/internal/resolver" "github.com/tgckpg/resolver-go/internal/resolver"
) )
@@ -24,7 +26,12 @@ func main() {
h := handler{ h := handler{
r: r, r: r,
closure: closure.NewCompileCache(2), jsCache: compilecache.New(
closure.NewCompiler(), 2, 128,
),
cssCache: compilecache.New(
css.NewEsbuildCompiler(), 2, 128,
),
} }
http.HandleFunc("/", h.index) http.HandleFunc("/", h.index)
log.Printf("botan-api listening on %s", *addr) log.Printf("botan-api listening on %s", *addr)
@@ -33,7 +40,8 @@ func main() {
type handler struct { type handler struct {
r *resolver.Resolver r *resolver.Resolver
closure *closure.CompileCache jsCache *compilecache.Cache
cssCache *compilecache.Cache
} }
func (h handler) index(w http.ResponseWriter, req *http.Request) { func (h handler) index(w http.ResponseWriter, req *http.Request) {
@@ -71,7 +79,8 @@ func (h handler) index(w http.ResponseWriter, req *http.Request) {
} }
if outMode == resolver.ModeJS { if outMode == resolver.ModeJS {
if compiled, ok := h.closure.Get(res.Hash); ok { state, compiled, err := h.jsCache.Get(res.Hash)
if state == compilecache.Ready {
w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Content-Type", "application/javascript")
w.Header().Set("X-Botan-Compiled", "hit") w.Header().Set("X-Botan-Compiled", "hit")
elapsed := time.Since(timerStart) elapsed := time.Since(timerStart)
@@ -86,9 +95,10 @@ func (h handler) index(w http.ResponseWriter, req *http.Request) {
return return
} }
h.closure.Enqueue(closure.CompileJob{ h.jsCache.Enqueue(compilecache.Job{
Hash: res.Hash, Hash: res.Hash,
Mode: "js", Mode: "js",
Payload: closure.CompilePayload{
ExternSources: jsExterns, ExternSources: jsExterns,
JSSources: []closure.SourceInput{ JSSources: []closure.SourceInput{
{ {
@@ -99,18 +109,29 @@ func (h handler) index(w http.ResponseWriter, req *http.Request) {
Defines: map[string]any{ Defines: map[string]any{
"DEBUG": false, "DEBUG": false,
}, },
},
})
} else if outMode == resolver.ModeCSS {
state, compiled, _ := h.cssCache.Get(res.Hash)
if state == compilecache.Ready {
w.Header().Set("Content-Type", "text/css")
w.Header().Set("X-Botan-Compiled", "hit")
elapsed := time.Since(timerStart)
w.Header().Set("X-Botan-Resolve-Time", elapsed.String())
w.Write(compiled)
return
}
h.cssCache.Enqueue(compilecache.Job{
Hash: res.Hash,
Mode: "css",
Payload: css.CompilePayload{
Name: "style.css",
Source: string(res.Content),
},
}) })
} }
// Compatibility flags:
// rjs/rcss/ojs/ocss => content
// hjs/hcss => hash filename only
// js/css => currently content; cluster compiler can switch this later.
if strings.HasPrefix(modeText, "h") {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte(res.Hash))
return
}
w.Header().Set("Content-Type", res.ContentType) w.Header().Set("Content-Type", res.ContentType)
w.Header().Set("X-Botan-Compiled", "miss") w.Header().Set("X-Botan-Compiled", "miss")
+4
View File
@@ -1,3 +1,7 @@
module github.com/tgckpg/resolver-go module github.com/tgckpg/resolver-go
go 1.26 go 1.26
require github.com/evanw/esbuild v0.28.1
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
+4
View File
@@ -0,0 +1,4 @@
github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc=
github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-87
View File
@@ -1,87 +0,0 @@
package closure
import (
"context"
"errors"
"log"
"time"
)
func NewCompileCache(workers int) *CompileCache {
c := &CompileCache{
client: NewClientFromEnv(),
states: make(map[string]CompileState),
results: make(map[string][]byte),
errors: make(map[string]error),
jobs: make(chan CompileJob, 128),
}
for i := 0; i < workers; i++ {
go c.worker()
}
return c
}
func (c *CompileCache) Get(hash string) ([]byte, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.states[hash] != CompileReady {
return nil, false
}
return c.results[hash], true
}
func (c *CompileCache) Enqueue(job CompileJob) {
c.mu.Lock()
switch c.states[job.Hash] {
case CompilePending, CompileReady:
c.mu.Unlock()
return
}
c.states[job.Hash] = CompilePending
c.mu.Unlock()
select {
case c.jobs <- job:
default:
// Queue full. Don't block request path.
c.mu.Lock()
c.states[job.Hash] = CompileMissing
c.errors[job.Hash] = errors.New("compile queue full")
c.mu.Unlock()
}
}
func (c *CompileCache) worker() {
for job := range c.jobs {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
req := CompileRequest{
ExternSources: job.ExternSources,
JSSources: job.JSSources,
Defines: job.Defines,
}
c.client.DebugPrintCurl(ctx, req)
out, err := c.client.Compile(ctx, req)
cancel()
c.mu.Lock()
if err == nil {
c.states[job.Hash] = CompileReady
c.results[job.Hash] = out
delete(c.errors, job.Hash)
} else {
log.Printf("compile failed: %v", err)
c.states[job.Hash] = CompileFailed
c.errors[job.Hash] = err
}
c.mu.Unlock()
}
}
+31
View File
@@ -0,0 +1,31 @@
package closure
import (
"context"
"fmt"
"github.com/tgckpg/resolver-go/internal/compilecache"
)
type Compiler struct {
client *Client
}
func NewCompiler() *Compiler {
return &Compiler{client: NewClientFromEnv()}
}
func (c *Compiler) Compile(ctx context.Context, job compilecache.Job) ([]byte, error) {
payload, ok := job.Payload.(CompilePayload)
if !ok {
return nil, fmt.Errorf("closure compiler got invalid payload type %T", job.Payload)
}
req := CompileRequest{
ExternSources: payload.ExternSources,
JSSources: payload.JSSources,
Defines: payload.Defines,
}
return c.client.Compile(ctx, req)
}
+8 -31
View File
@@ -1,20 +1,9 @@
package closure package closure
import "sync" type CompilePayload struct {
ExternSources []SourceInput
type CompileState int JSSources []SourceInput
Defines map[string]any
const (
CompileMissing CompileState = iota
CompilePending
CompileReady
CompileFailed
)
type CompileRequest struct {
ExternSources []SourceInput `json:"externSources,omitempty"`
JSSources []SourceInput `json:"jsSources"`
Defines map[string]any `json:"defines,omitempty"`
} }
type SourceInput struct { type SourceInput struct {
@@ -22,22 +11,10 @@ type SourceInput struct {
Source string `json:"source"` Source string `json:"source"`
} }
type CompileJob struct { type CompileRequest struct {
Hash string ExternSources []SourceInput `json:"externSources,omitempty"`
Mode string JSSources []SourceInput `json:"jsSources"`
Defines map[string]any `json:"defines,omitempty"`
ExternSources []SourceInput
JSSources []SourceInput
Defines map[string]any
}
type CompileCache struct {
client *Client
mu sync.Mutex
states map[string]CompileState
results map[string][]byte
errors map[string]error
jobs chan CompileJob
} }
type CompileResponse struct { type CompileResponse struct {
@@ -0,0 +1,89 @@
package compilecache
import (
"context"
)
func New(compiler Compiler, workers int, queueSize int) *Cache {
if workers <= 0 {
workers = 1
}
if queueSize <= 0 {
queueSize = 128
}
c := &Cache{
compiler: compiler,
states: make(map[string]State),
results: make(map[string][]byte),
errors: make(map[string]error),
jobs: make(chan Job, queueSize),
}
for i := 0; i < workers; i++ {
go c.worker()
}
return c
}
func (c *Cache) Get(hash string) (State, []byte, error) {
c.mu.Lock()
defer c.mu.Unlock()
state, ok := c.states[hash]
if !ok {
return Missing, nil, nil
}
switch state {
case Ready:
return Ready, c.results[hash], nil
case Failed:
return Failed, nil, c.errors[hash]
default:
return state, nil, nil
}
}
func (c *Cache) Enqueue(job Job) State {
c.mu.Lock()
state, ok := c.states[job.Hash]
if ok {
c.mu.Unlock()
return state
}
c.states[job.Hash] = Pending
c.mu.Unlock()
// Do not hold the lock while sending.
c.jobs <- job
return Pending
}
func (c *Cache) worker() {
for job := range c.jobs {
c.run(job)
}
}
func (c *Cache) run(job Job) {
result, err := c.compiler.Compile(context.Background(), job)
c.mu.Lock()
defer c.mu.Unlock()
if err != nil {
c.states[job.Hash] = Failed
c.errors[job.Hash] = err
delete(c.results, job.Hash)
return
}
c.states[job.Hash] = Ready
c.results[job.Hash] = result
delete(c.errors, job.Hash)
}
@@ -0,0 +1,36 @@
package compilecache
import (
"context"
"sync"
)
type State int
const (
Missing State = iota
Pending
Ready
Failed
)
type Job struct {
Hash string
Mode string
Payload any
}
type Compiler interface {
Compile(ctx context.Context, job Job) ([]byte, error)
}
type Cache struct {
compiler Compiler
mu sync.Mutex
states map[string]State
results map[string][]byte
errors map[string]error
jobs chan Job
}
@@ -0,0 +1,34 @@
package css
import (
"context"
"fmt"
"github.com/evanw/esbuild/pkg/api"
"github.com/tgckpg/resolver-go/internal/compilecache"
)
func NewEsbuildCompiler() *EsbuildCompiler {
return &EsbuildCompiler{}
}
func (c *EsbuildCompiler) Compile(ctx context.Context, job compilecache.Job) ([]byte, error) {
payload, ok := job.Payload.(CompilePayload)
if !ok {
return nil, fmt.Errorf("esbuild css compiler got invalid payload type %T", job.Payload)
}
result := api.Transform(payload.Source, api.TransformOptions{
Loader: api.LoaderCSS,
Sourcefile: payload.Name,
MinifyWhitespace: true,
MinifyIdentifiers: true,
MinifySyntax: true,
})
if len(result.Errors) > 0 {
return nil, fmt.Errorf("css compile failed: %s", result.Errors[0].Text)
}
return result.Code, nil
}
@@ -0,0 +1,8 @@
package css
type CompilePayload struct {
Name string
Source string
}
type EsbuildCompiler struct{}