Merge pull request #669 from trheyi/main

refactor: Update parsing logic for JIT components
This commit is contained in:
Max 2024-07-08 10:32:58 +08:00 committed by GitHub
commit b989194236
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 251 additions and 60 deletions

View file

@ -40,7 +40,7 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume
ctx.sequence++ ctx.sequence++
namespace := Namespace(page.Name, ctx.sequence, option.ScriptMinify) namespace := Namespace(page.Route, ctx.sequence, option.ScriptMinify)
page.namespace = namespace page.namespace = namespace
html, err := page.BuildHTML(option) html, err := page.BuildHTML(option)
@ -367,6 +367,11 @@ func (page *Page) BuildStyles(ctx *BuildContext, option *BuildOption, component
// BuildScripts build the scripts for the page // BuildScripts build the scripts for the page
func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component string, namespace string) ([]ScriptNode, error) { func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component string, namespace string) ([]ScriptNode, error) {
ispage := component == "__page"
if ispage {
component = ComponentName(page.Route, option.ScriptMinify)
}
scripts := []ScriptNode{} scripts := []ScriptNode{}
if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" {
return scripts, nil return scripts, nil
@ -416,7 +421,7 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component
code := string(source) code := string(source)
parent := "body" parent := "body"
if component != "__page" { if !ispage {
parent = "head" parent = "head"
code = fmt.Sprintf("function %s(){\n%s\n}\n", component, addTabToEachLine(code)) code = fmt.Sprintf("function %s(){\n%s\n}\n", component, addTabToEachLine(code))
} }

View file

@ -1,10 +1,12 @@
package core package core
import ( import (
"fmt"
"regexp" "regexp"
"strings" "strings"
"github.com/evanw/esbuild/pkg/api" "github.com/evanw/esbuild/pkg/api"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/runtime/transform" "github.com/yaoapp/gou/runtime/transform"
"github.com/yaoapp/kun/log" "github.com/yaoapp/kun/log"
) )
@ -93,44 +95,6 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, error
return html, nil return html, nil
} }
// HTML return the html of the script
func (script ScriptNode) HTML() string {
attrs := []string{
"s:ns=\"" + script.Namespace + "\"",
"s:cn=\"" + script.Component + "\"",
}
if script.Attrs != nil {
for _, attr := range script.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Script
if script.Source == "" {
return "<script " + strings.Join(attrs, " ") + "></script>"
}
return "<script " + strings.Join(attrs, " ") + ">\n" + script.Source + "\n</script>"
}
// HTML return the html of the style node
func (style StyleNode) HTML() string {
attrs := []string{
"s:ns=\"" + style.Namespace + "\"",
"s:cn=\"" + style.Component + "\"",
}
if style.Attrs != nil {
for _, attr := range style.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Style
if style.Source == "" {
return "<link " + strings.Join(attrs, " ") + "></link>"
}
return "<style " + strings.Join(attrs, " ") + ">\n" + style.Source + "\n</style>"
}
// CompileAsComponent compile the page as component // CompileAsComponent compile the page as component
func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (string, error) { func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (string, error) {
@ -148,8 +112,34 @@ func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (st
} }
} }
page.ReplaceDocument(doc) body := doc.Find("body")
return doc.Find("body").Html() rawScripts, err := jsoniter.MarshalToString(ctx.scripts)
if err != nil {
return "", err
}
rawStyles, err := jsoniter.MarshalToString(ctx.styles)
if err != nil {
return "", err
}
rawOption, err := jsoniter.MarshalToString(option)
if err != nil {
return "", err
}
if body.Children().Length() == 0 {
return "", fmt.Errorf("page %s as component should have one root element", page.Route)
}
if body.Children().Length() > 1 {
return "", fmt.Errorf("page %s as component should have only one root element", page.Route)
}
body.Children().First().AppendHtml(fmt.Sprintf(`<script name="scripts" type="json">%s</script>`+"\n", rawScripts))
body.Children().First().AppendHtml(fmt.Sprintf(`<script name="styles" type="json">%s</script>`+"\n", rawStyles))
body.Children().First().AppendHtml(fmt.Sprintf(`<script name="option" type="json">%s</script>`+"\n", rawOption))
return body.Html()
} }
// CompileJS compile the javascript // CompileJS compile the javascript
@ -212,3 +202,62 @@ func (page *Page) CompileCSS(source []byte, minify bool) ([]byte, error) {
func (page *Page) CompileHTML(source []byte, minify bool) ([]byte, error) { func (page *Page) CompileHTML(source []byte, minify bool) ([]byte, error) {
return source, nil return source, nil
} }
// HTML return the html of the script
func (script ScriptNode) HTML() string {
attrs := []string{
"s:ns=\"" + script.Namespace + "\"",
"s:cn=\"" + script.Component + "\"",
}
if script.Attrs != nil {
for _, attr := range script.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Script
if script.Source == "" {
return "<script " + strings.Join(attrs, " ") + "></script>"
}
return "<script " + strings.Join(attrs, " ") + ">\n" + script.Source + "\n</script>"
}
// ComponentHTML return the html of the script
func (script ScriptNode) ComponentHTML(ns string) string {
attrs := []string{
"s:ns=\"" + ns + "\"",
"s:cn=\"" + script.Component + "\"",
}
if script.Attrs != nil {
for _, attr := range script.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Script
if script.Source == "" {
return "<script " + strings.Join(attrs, " ") + "></script>"
}
source := fmt.Sprintf(`function %s(){%s};`, script.Component, script.Source)
return "<script " + strings.Join(attrs, " ") + ">\n" + source + "\n</script>"
}
// HTML return the html of the style node
func (style StyleNode) HTML() string {
attrs := []string{
"s:ns=\"" + style.Namespace + "\"",
"s:cn=\"" + style.Component + "\"",
}
if style.Attrs != nil {
for _, attr := range style.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Style
if style.Source == "" {
return "<link " + strings.Join(attrs, " ") + "></link>"
}
return "<style " + strings.Join(attrs, " ") + ">\n" + style.Source + "\n</style>"
}

View file

@ -4,18 +4,23 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/application" "github.com/yaoapp/gou/application"
) )
// JitComponent component // JitComponent component
type JitComponent struct { type JitComponent struct {
html string file string
scripts []ScriptNode route string
styles []StyleNode html string
scripts []ScriptNode
styles []StyleNode
buildOption *BuildOption
} }
const ( const (
@ -32,6 +37,9 @@ type componentData struct {
// Components loaded JIT components // Components loaded JIT components
var Components = map[string]*JitComponent{} var Components = map[string]*JitComponent{}
var chComp = make(chan *componentData, 1) var chComp = make(chan *componentData, 1)
var reScripts = regexp.MustCompile(`<script[^>]*name="scripts"[^>]*>(.*?)</script>`)
var reStyles = regexp.MustCompile(`<script[^>]*name="styles"[^>]*>(.*?)</script>`)
var reOption = regexp.MustCompile(`<script[^>]*name="option"[^>]*>(.*?)</script>`)
func init() { func init() {
go componentWriter() go componentWriter()
@ -47,17 +55,18 @@ func (parser *TemplateParser) parseComponent(sel *goquery.Selection) {
return return
} }
html, err := parser.RenderComponent(comp, props, slots) html, _, err := parser.RenderComponent(comp, props, slots)
if err != nil { if err != nil {
parser.errors = append(parser.errors, err) parser.errors = append(parser.errors, err)
setError(sel, err) setError(sel, err)
return return
} }
sel.SetHtml(html) sel.SetHtml(html)
} }
// RenderComponent render the component // RenderComponent render the component
func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[string]interface{}, slots *goquery.Selection) (string, error) { func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[string]interface{}, slots *goquery.Selection) (string, string, error) {
html := comp.html html := comp.html
randvar := fmt.Sprintf("__%s_$props", time.Now().Format("20060102150405")) randvar := fmt.Sprintf("__%s_$props", time.Now().Format("20060102150405"))
html = slotRe.ReplaceAllStringFunc(html, func(exp string) string { html = slotRe.ReplaceAllStringFunc(html, func(exp string) string {
@ -77,7 +86,7 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri
sel, err := NewDocumentString(`<body>` + html + `</body>`) sel, err := NewDocumentString(`<body>` + html + `</body>`)
if err != nil { if err != nil {
return "", err return "", "", err
} }
root := sel.Find("body") root := sel.Find("body")
@ -103,15 +112,65 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri
compParser := NewTemplateParser(data, &option) compParser := NewTemplateParser(data, &option)
// Parse the node // Parse the node
ns := Namespace(comp.route, compParser.sequence, comp.buildOption.ScriptMinify)
cn := ComponentName(comp.route, comp.buildOption.ScriptMinify)
compParser.parseNode(root.Get(0)) compParser.parseNode(root.Get(0))
for sel, nodes := range compParser.replace { for sel, nodes := range compParser.replace {
sel.ReplaceWithNodes(nodes...) sel.ReplaceWithNodes(nodes...)
delete(parser.replace, sel) delete(parser.replace, sel)
} }
if comp.scripts != nil {
for _, script := range comp.scripts {
script.Namespace = ns
parser.scripts = append(parser.scripts, script)
}
}
if comp.styles != nil {
for _, style := range comp.styles {
style.Namespace = ns
parser.styles = append(parser.styles, style)
}
}
// Update sequence // Update sequence
parser.sequence = compParser.sequence + parser.sequence parser.sequence = compParser.sequence + parser.sequence
return root.Html() first := root.Children().First()
first.SetAttr("s:ns", ns)
first.SetAttr("s:cn", cn)
first.SetAttr("s:ready", cn+"()")
html, err = root.Html()
if err != nil {
return "", "", err
}
return html, ns, nil
}
func (parser *TemplateParser) addScripts(sel *goquery.Selection, scripts []ScriptNode) {
if scripts == nil {
return
}
for _, script := range scripts {
query := fmt.Sprintf(`script[s\:cn="%s"]`, script.Component)
if sel.Find(query).Length() > 0 {
continue
}
sel.AppendHtml(script.ComponentHTML(script.Namespace))
}
}
func (parser *TemplateParser) addStyles(sel *goquery.Selection, styles []StyleNode) {
if styles == nil {
return
}
for _, style := range styles {
query := fmt.Sprintf(`style[s\:cn="%s"]`, style.Component)
if sel.Find(query).Length() > 0 {
continue
}
sel.Append(style.HTML())
}
} }
func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponent, map[string]interface{}, *goquery.Selection, error) { func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponent, map[string]interface{}, *goquery.Selection, error) {
@ -123,12 +182,12 @@ func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponen
return nil, nil, slots, err return nil, nil, slots, err
} }
file, err := parser.componentFile(sel, props) file, route, err := parser.componentFile(sel, props)
if err != nil { if err != nil {
return nil, props, slots, err return nil, props, slots, err
} }
comp, err := getComponent(file, parser.disableCache()) comp, err := getComponent(route, file, parser.disableCache())
if err != nil { if err != nil {
return nil, props, slots, err return nil, props, slots, err
} }
@ -225,10 +284,10 @@ func (parser *TemplateParser) parseComponentProps(props map[string]string) (map[
return result, nil return result, nil
} }
func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[string]interface{}) (string, error) { func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[string]interface{}) (string, string, error) {
route := sel.AttrOr("is", "") route := sel.AttrOr("is", "")
if route == "" { if route == "" {
return "", fmt.Errorf("Component route is required") return "", "", fmt.Errorf("Component route is required")
} }
data := Data{"$props": props} data := Data{"$props": props}
@ -236,7 +295,7 @@ func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[st
route, _ = parser.data.Replace(route) route, _ = parser.data.Replace(route)
root := sel.AttrOr("s:root", "/") root := sel.AttrOr("s:root", "/")
file := filepath.Join(string(os.PathSeparator), "public", root, route+".jit") file := filepath.Join(string(os.PathSeparator), "public", root, route+".jit")
return file, nil return file, route, nil
} }
func componentWriter() { func componentWriter() {
@ -253,7 +312,7 @@ func componentWriter() {
} }
} }
func getComponent(file string, disableCache ...bool) (*JitComponent, error) { func getComponent(route string, file string, disableCache ...bool) (*JitComponent, error) {
noCache := false noCache := false
if len(disableCache) > 0 && disableCache[0] { if len(disableCache) > 0 && disableCache[0] {
@ -264,7 +323,7 @@ func getComponent(file string, disableCache ...bool) (*JitComponent, error) {
return comp, nil return comp, nil
} }
comp, err := readComponent(file) comp, err := readComponent(route, file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -275,10 +334,63 @@ func getComponent(file string, disableCache ...bool) (*JitComponent, error) {
} }
// readComponent read the JIT component // readComponent read the JIT component
func readComponent(file string) (*JitComponent, error) { func readComponent(route string, file string) (*JitComponent, error) {
content, err := application.App.Read(file) content, err := application.App.Read(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &JitComponent{html: string(content)}, nil
// Get the scripts
html := string(content)
rawScripts := ""
rawStyles := ""
rawOption := ""
html = reScripts.ReplaceAllStringFunc(html, func(exp string) string {
rawScripts = reScripts.ReplaceAllString(exp, "$1")
return ""
})
html = reStyles.ReplaceAllStringFunc(html, func(exp string) string {
rawStyles = reStyles.ReplaceAllString(exp, "$1")
return ""
})
html = reOption.ReplaceAllStringFunc(html, func(exp string) string {
rawOption = reOption.ReplaceAllString(exp, "$1")
return ""
})
scripts := []ScriptNode{}
styles := []StyleNode{}
buildOption := BuildOption{}
if rawScripts != "" {
err := jsoniter.UnmarshalFromString(rawScripts, &scripts)
if err != nil {
return nil, err
}
}
if rawStyles != "" {
err := jsoniter.UnmarshalFromString(rawStyles, &styles)
if err != nil {
return nil, err
}
}
if rawOption != "" {
err := jsoniter.UnmarshalFromString(rawOption, &buildOption)
if err != nil {
return nil, err
}
}
return &JitComponent{
file: file,
route: route,
html: html,
scripts: scripts,
styles: styles,
buildOption: &buildOption,
}, nil
} }

View file

@ -26,6 +26,8 @@ type TemplateParser struct {
option *ParserOption // parser option option *ParserOption // parser option
locale *Locale // locale locale *Locale // locale
context *ParserContext // parser context context *ParserContext // parser context
scripts []ScriptNode // scripts
styles []StyleNode // styles
} }
// ParserContext parser context for the template // ParserContext parser context for the template
@ -171,6 +173,8 @@ func NewTemplateParser(data Data, option *ParserOption) *TemplateParser {
errors: []error{}, errors: []error{},
replace: map[*goquery.Selection][]*html.Node{}, replace: map[*goquery.Selection][]*html.Node{},
option: option, option: option,
scripts: []ScriptNode{},
styles: []StyleNode{},
} }
} }
@ -202,6 +206,8 @@ func (parser *TemplateParser) Render(html string) (string, error) {
head := doc.Find("head") head := doc.Find("head")
if head.Length() > 0 { if head.Length() > 0 {
head.AppendHtml(headInjectionScript()) head.AppendHtml(headInjectionScript())
parser.addScripts(head, parser.scripts)
parser.addStyles(head, parser.styles)
} }
// Append the data to the body // Append the data to the body

View file

@ -191,8 +191,9 @@ type Template struct {
// TemplateScirpts is the struct for the template scripts // TemplateScirpts is the struct for the template scripts
type TemplateScirpts struct { type TemplateScirpts struct {
BeforeBuild []*TemplateScript `json:"before:build,omitempty"` // Run before build BeforeBuild []*TemplateScript `json:"before:build,omitempty"` // Run before build
AfterBuild []*TemplateScript `json:"after:build,omitempty"` // Run after build AfterBuild []*TemplateScript `json:"after:build,omitempty"` // Run after build
BuildComplete []*TemplateScript `json:"build:complete,omitempty"` // Run build complete
} }
// TemplateScript is the struct for the template script // TemplateScript is the struct for the template script

View file

@ -16,6 +16,11 @@ import (
// Build the template // Build the template
func (tmpl *Template) Build(option *core.BuildOption) error { func (tmpl *Template) Build(option *core.BuildOption) error {
var err error var err error
defer func() {
if option.ExecScripts {
tmpl.ExecBuildCompleteScripts()
}
}()
// Execute the build before hook // Execute the build before hook
if option.ExecScripts { if option.ExecScripts {

View file

@ -79,6 +79,14 @@ func (tmpl *Template) Reload() error {
return nil return nil
} }
// ExecBuildCompleteScripts execute the build complete scripts
func (tmpl *Template) ExecBuildCompleteScripts() []core.TemplateScirptResult {
if tmpl.Scripts == nil || len(tmpl.Scripts.BuildComplete) == 0 {
return nil
}
return tmpl.ExecScripts(tmpl.Scripts.BuildComplete)
}
// ExecBeforeBuildScripts execute the before build scripts // ExecBeforeBuildScripts execute the before build scripts
func (tmpl *Template) ExecBeforeBuildScripts() []core.TemplateScirptResult { func (tmpl *Template) ExecBeforeBuildScripts() []core.TemplateScirptResult {
if tmpl.Scripts == nil || len(tmpl.Scripts.BeforeBuild) == 0 { if tmpl.Scripts == nil || len(tmpl.Scripts.BeforeBuild) == 0 {
@ -97,7 +105,12 @@ func (tmpl *Template) ExecAfterBuildScripts() []core.TemplateScirptResult {
// ExecScripts execute the scripts // ExecScripts execute the scripts
func (tmpl *Template) ExecScripts(scripts []*core.TemplateScript) []core.TemplateScirptResult { func (tmpl *Template) ExecScripts(scripts []*core.TemplateScript) []core.TemplateScirptResult {
results := []core.TemplateScirptResult{} results := []core.TemplateScirptResult{}
if scripts == nil {
return results
}
for _, script := range scripts { for _, script := range scripts {
switch script.Type { switch script.Type {
case "command": case "command":