From e4c8eeb18084ebb71eba229e0f4ce9f48c9f2d27 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 7 Jul 2024 15:29:50 +0800 Subject: [PATCH] optimize the SUI compiler & add http server gzip support --- cmd/sui/build.go | 16 +++++++++-- cmd/sui/sui.go | 4 +++ cmd/sui/watch.go | 2 -- go.mod | 8 +++--- go.sum | 8 ++++++ service/gzip.go | 46 ++++++++++++++++++++++++++++++ service/middleware.go | 37 ++++++++++++++++-------- service/static.go | 2 +- sui/core/build.go | 6 ++-- sui/core/parser.go | 65 ++++++++++++++++++++++++++++++++----------- sui/core/utils.go | 19 +++++++++++-- 11 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 service/gzip.go diff --git a/cmd/sui/build.go b/cmd/sui/build.go index 914d7a98..3ff94266 100644 --- a/cmd/sui/build.go +++ b/cmd/sui/build.go @@ -81,13 +81,25 @@ var BuildCmd = &cobra.Command{ // Timecost start := time.Now() - err = tmpl.Build(&core.BuildOption{SSR: true, AssetRoot: assetRoot, ExecScripts: true}) + minify := true + mode := "production" + if debug { + minify = false + mode = "development" + } + + err = tmpl.Build(&core.BuildOption{SSR: true, AssetRoot: assetRoot, ExecScripts: true, ScriptMinify: minify, StyleMinify: minify}) if err != nil { fmt.Fprintln(os.Stderr, color.RedString(err.Error())) return } end := time.Now() timecost := end.Sub(start).Truncate(time.Millisecond) - fmt.Println(color.GreenString("Build success in %s", timecost)) + if debug { + fmt.Println(color.YellowString("Build succeeded for %s in %s", mode, timecost)) + return + } + + fmt.Println(color.GreenString("Build succeeded for %s in %s", mode, timecost)) }, } diff --git a/cmd/sui/sui.go b/cmd/sui/sui.go index 6f81c0b6..fffe9df6 100644 --- a/cmd/sui/sui.go +++ b/cmd/sui/sui.go @@ -1,6 +1,10 @@ package sui +var data string +var debug bool + func init() { WatchCmd.PersistentFlags().StringVarP(&data, "data", "d", "::{}", L("Session Data")) BuildCmd.PersistentFlags().StringVarP(&data, "data", "d", "::{}", L("Session Data")) + BuildCmd.PersistentFlags().BoolVarP(&debug, "debug", "D", false, L("Debug mode")) } diff --git a/cmd/sui/watch.go b/cmd/sui/watch.go index b3502147..2b738309 100644 --- a/cmd/sui/watch.go +++ b/cmd/sui/watch.go @@ -23,8 +23,6 @@ import ( "github.com/yaoapp/yao/sui/core" ) -var data string - var watched sync.Map // WatchCmd command diff --git a/go.mod b/go.mod index 0d86ec76..b4769621 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/yaoapp/yao go 1.20 require ( - github.com/PuerkitoBio/goquery v1.8.1 + github.com/PuerkitoBio/goquery v1.9.2 github.com/blang/semver v3.5.1+incompatible github.com/caarlos0/env/v6 v6.10.1 github.com/dchest/captcha v1.0.0 @@ -26,8 +26,8 @@ require ( github.com/yaoapp/gou v0.10.3 github.com/yaoapp/kun v0.9.0 github.com/yaoapp/xun v0.9.0 - golang.org/x/crypto v0.23.0 - golang.org/x/net v0.25.0 + golang.org/x/crypto v0.25.0 + golang.org/x/net v0.27.0 golang.org/x/text v0.16.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -113,7 +113,7 @@ require ( golang.org/x/mod v0.17.0 // indirect golang.org/x/oauth2 v0.14.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect diff --git a/go.sum b/go.sum index b03815fa..f1567541 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= +github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= @@ -261,6 +263,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= @@ -284,6 +288,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= @@ -317,6 +323,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/service/gzip.go b/service/gzip.go new file mode 100644 index 00000000..71b20631 --- /dev/null +++ b/service/gzip.go @@ -0,0 +1,46 @@ +package service + +import ( + "compress/gzip" + "net/http" + "strings" +) + +// gzipHandler +func gzipHandler(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + h.ServeHTTP(w, r) + return + } + + w.Header().Set("Content-Encoding", "gzip") + gz := gzip.NewWriter(w) + defer gz.Close() + + gzWriter := gzipResponseWriter{ResponseWriter: w, Writer: gz} + h.ServeHTTP(gzWriter, r) + } +} + +type gzipResponseWriter struct { + http.ResponseWriter + *gzip.Writer +} + +func (w gzipResponseWriter) WriteHeader(code int) { + w.ResponseWriter.Header().Del("Content-Length") + w.ResponseWriter.WriteHeader(code) +} + +func (w gzipResponseWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +func (w gzipResponseWriter) Flush() { + w.Writer.Flush() +} + +func (w gzipResponseWriter) Header() http.Header { + return w.ResponseWriter.Header() +} diff --git a/service/middleware.go b/service/middleware.go index 9a6e45c1..e6766bc1 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -1,6 +1,9 @@ package service import ( + "bytes" + "compress/gzip" + "net/http" "path/filepath" "strings" @@ -42,15 +45,6 @@ func withStaticFileServer(c *gin.Context) { return } - // Yao Builder - // URL /yao/builder - if length >= 12 && c.Request.URL.Path[0:12] == "/yao/builder" { - c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, "/yao/builder") - BuilderFileServer.ServeHTTP(c.Writer, c.Request) - c.Abort() - return - } - // Rewrite for _, rewrite := range rewriteRules { // log.Debug("Rewrite: %s => %s", c.Request.URL.Path, rewrite.Replacement) @@ -67,8 +61,6 @@ func withStaticFileServer(c *gin.Context) { // Sui file server if strings.HasSuffix(c.Request.URL.Path, ".sui") { - log.Debug("Sui File: %s", c.Request.URL.Path) - // Default index.sui if filepath.Base(c.Request.URL.Path) == ".sui" { c.Request.URL.Path = strings.TrimSuffix(c.Request.URL.Path, ".sui") + "index.sui" @@ -96,9 +88,30 @@ func withStaticFileServer(c *gin.Context) { return } + // Gzip Compression + if strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") { + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + if _, err := gz.Write([]byte(html)); err != nil { + log.Error("GZIP Compression Error: %s", err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + if err := gz.Close(); err != nil { + log.Error("GZIP Close Error: %s", err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.Header("Content-Type", "text/html; charset=utf-8") + c.Header("Content-Encoding", "gzip") + c.Data(http.StatusOK, "text/html", buf.Bytes()) + c.Done() + } + c.Header("Content-Type", "text/html; charset=utf-8") c.String(200, html) - c.Done() + c.Next() return } diff --git a/service/static.go b/service/static.go index 8555eee9..00b4a2bf 100644 --- a/service/static.go +++ b/service/static.go @@ -38,7 +38,7 @@ type rewriteRule struct { func SetupStatic() error { setupAdminRoot() setupRewrite() - AppFileServer = http.FileServer(fs.Dir("public")) + AppFileServer = gzipHandler(http.FileServer(fs.Dir("public"))) return nil } diff --git a/sui/core/build.go b/sui/core/build.go index cbe95f6f..433c0c04 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -55,7 +55,7 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume } // Scripts - namespace := Namespace(page.Name, ctx.sequence) + namespace := Namespace(page.Name, ctx.sequence, option.ScriptMinify) scripts, err := page.BuildScripts(ctx, option, "__page", namespace) if err != nil { return nil, ctx.warnings, err @@ -99,8 +99,8 @@ func (page *Page) BuildAsComponent(sel *goquery.Selection, ctx *BuildContext, op return "", fmt.Errorf("The component tag must have an is attribute") } - namespace := Namespace(name, ctx.sequence) - component := ComponentName(name) + namespace := Namespace(name, ctx.sequence, option.ScriptMinify) + component := ComponentName(name, option.ScriptMinify) attrs := []html.Attribute{ {Key: "s:ns", Val: namespace}, {Key: "s:cn", Val: component}, diff --git a/sui/core/parser.go b/sui/core/parser.go index 92eeab01..3ab235bf 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -64,6 +64,13 @@ var keepWords = map[string]bool{ "s:bind": true, } +var keepAttrs = map[string]bool{ + "s:ns": true, + "s:cn": true, + "s:ready": true, + "s:click": true, +} + // Locales the locales var Locales = map[string]map[string]*Locale{} @@ -229,8 +236,6 @@ func (parser *TemplateParser) Render(html string) (string, error) { if parser.option != nil && (parser.option.Request || parser.option.Preview) { // Remove the sui-hide attribute doc.Find("[sui-hide]").Remove() - - // Remove All comments and used nodes parser.tidy(doc.Selection) } @@ -271,6 +276,8 @@ func (parser *TemplateParser) parseNode(node *html.Node) { func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { + node := sel.Get(0) + if _, exist := sel.Attr("s:for"); exist { parser.forStatementNode(sel) } @@ -279,7 +286,7 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { parser.ifStatementNode(sel) } - if _, exist := sel.Attr("s:set"); exist || sel.Get(0).Data == "s:set" { + if _, exist := sel.Attr("s:set"); exist || node.Data == "s:set" { parser.setStatementNode(sel) } @@ -288,6 +295,11 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { parser.parseJitElementNode(sel) } + // Slot Node + if node.Data == "slot" { + parser.slotNode(sel) + } + // Parse the attributes parser.parseElementAttrs(sel) @@ -361,6 +373,16 @@ func (parser *TemplateParser) transElementNode(sel *goquery.Selection) { } } +// Remove the slot tag and replace it with the children +func (parser *TemplateParser) slotNode(sel *goquery.Selection) { + children := sel.Children() + if children.Length() == 0 { + sel.Remove() + return + } + sel.ReplaceWithSelection(children) +} + func (parser *TemplateParser) setStatementNode(sel *goquery.Selection) { sel.SetAttr("parsed", "true") @@ -396,6 +418,12 @@ func (parser *TemplateParser) parseElementAttrs(sel *goquery.Selection) { attrs := sel.Nodes[0].Attr for _, attr := range attrs { + + // Ignore the s: attributes + if strings.HasPrefix(attr.Key, "s:") { + continue + } + parser.sequence = parser.sequence + 1 res, hasStmt := parser.data.Replace(attr.Val) if hasStmt { @@ -672,30 +700,33 @@ func (parser *TemplateParser) show(sel *goquery.Selection) { // sel.SetAttr("style", style) } -func (parser *TemplateParser) tidy(selection *goquery.Selection) { - selection.Contents().Each(func(i int, s *goquery.Selection) { +func (parser *TemplateParser) tidy(s *goquery.Selection) { - if s.Nodes[0].Type == html.CommentNode { - s.Remove() + s.Contents().Each(func(i int, child *goquery.Selection) { + + node := child.Get(0) + if node.Type == html.CommentNode { + child = child.Remove() return } - // Remove the s:key-* attributes - if s.Nodes[0].Type == html.ElementNode { - for _, attr := range s.Nodes[0].Attr { - if attr.Key == "parsed" || keepWords[attr.Key] || strings.HasPrefix(attr.Key, "s:key") || strings.HasPrefix(attr.Key, "s:bind") { - s.RemoveAttr(attr.Key) - } + // Remove the parsed attribute + attrs := []html.Attribute{} + for _, attr := range node.Attr { + if strings.HasPrefix(attr.Key, "s:") && !keepAttrs[attr.Key] { + continue } - if s.Nodes[0].Data == "s:set" { - s.Remove() - return + if attr.Key == "parsed" || attr.Key == "is" || strings.HasPrefix(attr.Key, "...") { + continue } + attrs = append(attrs, attr) } - parser.tidy(s) + node.Attr = attrs + parser.tidy(child) }) + } func (parser *TemplateParser) key(prefix string, sel *goquery.Selection) string { diff --git a/sui/core/utils.go b/sui/core/utils.go index 95d002fc..6353de89 100644 --- a/sui/core/utils.go +++ b/sui/core/utils.go @@ -3,6 +3,7 @@ package core import ( "bytes" "fmt" + "hash/fnv" "strings" "github.com/PuerkitoBio/goquery" @@ -50,17 +51,29 @@ func NewDocumentStringWithWrapper(htmlContent string) (*goquery.Document, error) } // Namespace convert the name to namespace -func Namespace(name string, idx int) string { +func Namespace(name string, idx int, hash ...bool) string { name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, "[", "_") name = strings.ReplaceAll(name, "]", "_") + ns := fmt.Sprintf("__namespace_%s", name) + if len(hash) > 0 && hash[0] { + h := fnv.New64a() + h.Write([]byte(ns)) + return fmt.Sprintf("ns_%x", h.Sum64()) + } return fmt.Sprintf("__page_%s_%d", name, idx) } // ComponentName convert the name to component name -func ComponentName(name string) string { +func ComponentName(name string, hash ...bool) string { name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, "[", "_") name = strings.ReplaceAll(name, "]", "_") - return fmt.Sprintf("__component_%s", name) + cn := fmt.Sprintf("__component_%s", name) + if len(hash) > 0 && hash[0] { + h := fnv.New64a() + h.Write([]byte(cn)) + return fmt.Sprintf("cn_%x", h.Sum64()) + } + return cn }