forked from Botanical/BotanJS
Added css minify
This commit is contained in:
@@ -8,7 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
"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/minifier/css"
|
||||
"github.com/tgckpg/resolver-go/internal/resolver"
|
||||
)
|
||||
|
||||
@@ -24,7 +26,12 @@ func main() {
|
||||
|
||||
h := handler{
|
||||
r: r,
|
||||
closure: closure.NewCompileCache(2),
|
||||
jsCache: compilecache.New(
|
||||
closure.NewCompiler(), 2, 128,
|
||||
),
|
||||
cssCache: compilecache.New(
|
||||
css.NewEsbuildCompiler(), 2, 128,
|
||||
),
|
||||
}
|
||||
http.HandleFunc("/", h.index)
|
||||
log.Printf("botan-api listening on %s", *addr)
|
||||
@@ -33,7 +40,8 @@ func main() {
|
||||
|
||||
type handler struct {
|
||||
r *resolver.Resolver
|
||||
closure *closure.CompileCache
|
||||
jsCache *compilecache.Cache
|
||||
cssCache *compilecache.Cache
|
||||
}
|
||||
|
||||
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 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("X-Botan-Compiled", "hit")
|
||||
elapsed := time.Since(timerStart)
|
||||
@@ -86,9 +95,10 @@ func (h handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.closure.Enqueue(closure.CompileJob{
|
||||
h.jsCache.Enqueue(compilecache.Job{
|
||||
Hash: res.Hash,
|
||||
Mode: "js",
|
||||
Payload: closure.CompilePayload{
|
||||
ExternSources: jsExterns,
|
||||
JSSources: []closure.SourceInput{
|
||||
{
|
||||
@@ -99,18 +109,29 @@ func (h handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
Defines: map[string]any{
|
||||
"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("X-Botan-Compiled", "miss")
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
module github.com/tgckpg/resolver-go
|
||||
|
||||
go 1.26
|
||||
|
||||
require github.com/evanw/esbuild v0.28.1
|
||||
|
||||
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
package closure
|
||||
|
||||
import "sync"
|
||||
|
||||
type CompileState int
|
||||
|
||||
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 CompilePayload struct {
|
||||
ExternSources []SourceInput
|
||||
JSSources []SourceInput
|
||||
Defines map[string]any
|
||||
}
|
||||
|
||||
type SourceInput struct {
|
||||
@@ -22,22 +11,10 @@ type SourceInput struct {
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type CompileJob struct {
|
||||
Hash string
|
||||
Mode string
|
||||
|
||||
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 CompileRequest struct {
|
||||
ExternSources []SourceInput `json:"externSources,omitempty"`
|
||||
JSSources []SourceInput `json:"jsSources"`
|
||||
Defines map[string]any `json:"defines,omitempty"`
|
||||
}
|
||||
|
||||
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{}
|
||||
Reference in New Issue
Block a user