diff --git a/sui/api/compile_test.go b/sui/api/compile_test.go index 60f599c49..9a3c844a1 100644 --- a/sui/api/compile_test.go +++ b/sui/api/compile_test.go @@ -13,7 +13,7 @@ func TestCompile(t *testing.T) { loadTestSui(t) page := testPage(t) - html, err := page.Compile(&core.BuildOption{KeepPageTag: false}) + html, err := page.Compile(nil, &core.BuildOption{KeepPageTag: false}) if err != nil { t.Fatalf("Compile error: %v", err) } diff --git a/sui/api/process.go b/sui/api/process.go index bd0e41bc1..56c7639b7 100644 --- a/sui/api/process.go +++ b/sui/api/process.go @@ -1004,7 +1004,7 @@ func BuildPage(process *process.Process) interface{} { } data := process.ArgsMap(5, map[string]interface{}{}) - err = page.Build(&core.BuildOption{SSR: ssr, AssetRoot: assetRoot, Data: data}) + err = page.Build(nil, &core.BuildOption{SSR: ssr, AssetRoot: assetRoot, Data: data}) if err != nil { exception.New(err.Error(), 500).Throw() } diff --git a/sui/core/build.go b/sui/core/build.go index af39383cf..1da9e45ef 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -19,16 +19,6 @@ var cssRe = regexp.MustCompile(`([\.a-z0-9A-Z-:# ]+)\{`) var langFuncRe = regexp.MustCompile(`L\s*\(\s*["'](.*?)["']\s*\)`) var langAttrRe = regexp.MustCompile(`'::(.*?)'`) -// NewBuildContext create a new build context -func NewBuildContext() *BuildContext { - return &BuildContext{ - components: map[string]bool{}, - sequence: 1, - scripts: []string{}, - styles: []string{}, - } -} - // Build is the struct for the public func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Document, []string, error) { @@ -148,6 +138,7 @@ func (page *Page) BuildForImport(ctx *BuildContext, option *BuildOption, slots m func (page *Page) parse(ctx *BuildContext, doc *goquery.Document, option *BuildOption, warnings []string) error { + // Find the import pages pages := doc.Find("*").FilterFunction(func(i int, sel *goquery.Selection) bool { // Get the translation @@ -164,8 +155,12 @@ func (page *Page) parse(ctx *BuildContext, doc *goquery.Document, option *BuildO return false } - _, has := sel.Attr("is") - return has + name, has := sel.Attr("is") + _, jit := sel.Attr("s:jit") // Just in time rendering + if has && jit { + ctx.addJitComponent(name) + } + return has && !jit }) sui := SUIs[page.SuiID] diff --git a/sui/core/compile.go b/sui/core/compile.go index 06483bdad..c8f25d6ed 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -16,9 +16,8 @@ var importAssetsRe = regexp.MustCompile(`import\s*\t*\n*\s*['"]@assets\/([^'"]+) var AssetsRe = regexp.MustCompile(`[` + quoteRe + `]@assets\/([^` + quoteRe + `]+)[` + quoteRe + `]`) // '@assets/foo.js' or "@assets/foo.js" or `@assets/foo` // Compile the page -func (page *Page) Compile(option *BuildOption) (string, error) { +func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, error) { - ctx := NewBuildContext() doc, warnings, err := page.Build(ctx, option) if err != nil { return "", err diff --git a/sui/core/context.go b/sui/core/context.go new file mode 100644 index 000000000..6e75f0d85 --- /dev/null +++ b/sui/core/context.go @@ -0,0 +1,54 @@ +package core + +// NewBuildContext create a new build context +func NewBuildContext(global *GlobalBuildContext) *BuildContext { + return &BuildContext{ + components: map[string]bool{}, + sequence: 1, + scripts: []string{}, + styles: []string{}, + jitComponents: map[string]bool{}, + global: global, + } +} + +// NewGlobalBuildContext create a new global build context +func NewGlobalBuildContext() *GlobalBuildContext { + return &GlobalBuildContext{ + jitComponents: map[string]bool{}, + } +} + +// GetJitComponents get the just in time components +func (ctx *BuildContext) GetJitComponents() []string { + if ctx.jitComponents == nil { + return []string{} + } + jitComponents := []string{} + for name := range ctx.jitComponents { + jitComponents = append(jitComponents, name) + } + return jitComponents +} + +// GetJitComponents get the just in time components +func (globalCtx *GlobalBuildContext) GetJitComponents() []string { + if globalCtx.jitComponents == nil { + return []string{} + } + + jitComponents := []string{} + for name := range globalCtx.jitComponents { + jitComponents = append(jitComponents, name) + } + return jitComponents +} + +func (ctx *BuildContext) addJitComponent(name string) { + name = stmtRe.ReplaceAllString(name, "*") + name = propRe.ReplaceAllString(name, "*") + ctx.jitComponents[name] = true + if ctx.global != nil { + ctx.global.jitComponents[name] = true + } +} diff --git a/sui/core/editor.go b/sui/core/editor.go index a996f9e96..ccf46c3ab 100644 --- a/sui/core/editor.go +++ b/sui/core/editor.go @@ -48,7 +48,7 @@ func (page *Page) EditorRender() (*ResponseEditorRender, error) { // Render tools // res.Scripts = append(res.Scripts, filepath.Join("@assets", "__render.js")) // res.Styles = append(res.Styles, filepath.Join("@assets", "__render.css")) - ctx := NewBuildContext() + ctx := NewBuildContext(nil) doc, warnings, err := page.Build(ctx, &BuildOption{ SSR: true, IgnoreAssetRoot: true, diff --git a/sui/core/interfaces.go b/sui/core/interfaces.go index c5adda28b..795869e37 100644 --- a/sui/core/interfaces.go +++ b/sui/core/interfaces.go @@ -92,7 +92,8 @@ type IPage interface { AssetScript() (*Asset, error) AssetStyle() (*Asset, error) - Build(option *BuildOption) error + Build(globalCtx *GlobalBuildContext, option *BuildOption) error + BuildAsComponent(globalCtx *GlobalBuildContext, option *BuildOption) error } // IBlock is the interface for the block diff --git a/sui/core/preview.go b/sui/core/preview.go index e135aa1aa..635455a04 100644 --- a/sui/core/preview.go +++ b/sui/core/preview.go @@ -17,7 +17,7 @@ func (page *Page) PreviewRender(referer string) (string, error) { } warnings := []string{} - ctx := NewBuildContext() + ctx := NewBuildContext(nil) doc, warnings, err := page.Build(ctx, &BuildOption{ SSR: true, AssetRoot: fmt.Sprintf("/api/__yao/sui/v1/%s/asset/%s/@assets", page.SuiID, page.TemplateID), diff --git a/sui/core/types.go b/sui/core/types.go index 557952257..1eeabeb3e 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -39,16 +39,25 @@ type Page struct { Document []byte `json:"-"` GlobalData []byte `json:"-"` Attrs map[string]string `json:"-"` - Translations []Translation `json:"-"` + Translations []Translation `json:"-"` // will be deprecated } // BuildContext is the struct for the build context type BuildContext struct { - components map[string]bool - sequence int - doc *goquery.Document - scripts []string - styles []string + components map[string]bool + jitComponents map[string]bool + sequence int + doc *goquery.Document + scripts []string + styles []string + global *GlobalBuildContext + translations []Translation +} + +// GlobalBuildContext is the struct for the global build context +type GlobalBuildContext struct { + jitComponents map[string]bool + tmpl ITemplate } // Translation is the struct for the translation diff --git a/sui/storages/local/build.go b/sui/storages/local/build.go index df6f1c988..90d1c54dd 100644 --- a/sui/storages/local/build.go +++ b/sui/storages/local/build.go @@ -52,11 +52,14 @@ func (tmpl *Template) Build(option *core.BuildOption) error { } // Build all pages + ctx := core.NewGlobalBuildContext() pages, err := tmpl.Pages() if err != nil { return err } + // loaed pages + tmpl.loaded = map[string]core.IPage{} for _, page := range pages { perr := page.Load() if err != nil { @@ -64,9 +67,29 @@ func (tmpl *Template) Build(option *core.BuildOption) error { continue } - perr = page.Build(option) + perr = page.Build(ctx, option) if perr != nil { err = multierror.Append(perr) + continue + } + tmpl.loaded[page.Get().Route] = page + } + + // Build jit components for the global -> .sui.lib + jitComponents, err := tmpl.GlobRoutes(ctx.GetJitComponents(), true) + if err != nil { + return err + } + + for _, route := range jitComponents { + page, has := tmpl.loaded[route] + if !has { + err = multierror.Append(fmt.Errorf("The page %s is not loaded", route)) + continue + } + err = page.BuildAsComponent(ctx, option) + if err != nil { + err = multierror.Append(err) } } @@ -145,8 +168,9 @@ func (tmpl *Template) SyncAssets(option *core.BuildOption) error { } // Build is the struct for the public -func (page *Page) Build(option *core.BuildOption) error { +func (page *Page) Build(globalCtx *core.GlobalBuildContext, option *core.BuildOption) error { + ctx := core.NewBuildContext(globalCtx) if option.AssetRoot == "" { root, err := page.tmpl.local.DSL.PublicRoot(option.Data) if err != nil { @@ -157,7 +181,7 @@ func (page *Page) Build(option *core.BuildOption) error { } log.Trace("Build the page %s AssetRoot: %s", page.Route, option.AssetRoot) - html, err := page.Page.Compile(option) + html, err := page.Page.Compile(ctx, option) if err != nil { return err } @@ -169,7 +193,43 @@ func (page *Page) Build(option *core.BuildOption) error { } // Save the locale files - return page.writeLocaleFiles(option.Data) + err = page.writeLocaleFiles(option.Data) + if err != nil { + return err + } + + // Jit Components + if globalCtx == nil { + jitComponents, err := page.tmpl.GlobRoutes(ctx.GetJitComponents(), true) + if err != nil { + return err + } + + for _, route := range jitComponents { + var err error + p := page.tmpl.loaded[route] + if p == nil { + p, err = page.tmpl.Page(route) + if err != nil { + err = multierror.Append(err) + continue + } + } + + err = p.BuildAsComponent(globalCtx, option) + if err != nil { + err = multierror.Append(err) + } + } + } + + return err + +} + +// BuildAsComponent build the page as component +func (page *Page) BuildAsComponent(globalCtx *core.GlobalBuildContext, option *core.BuildOption) error { + return page.writeJitHTML([]byte("
"+page.Route+"
"), option.Data) } func (page *Page) publicFile(data map[string]interface{}) string { @@ -354,3 +414,20 @@ func (page *Page) writeHTML(html []byte, data map[string]interface{}) error { log.Trace("The page %s is removed", htmlFile) return nil } + +// writeHTMLTo write the html to file +func (page *Page) writeJitHTML(html []byte, data map[string]interface{}) error { + htmlFile := fmt.Sprintf("%s.jit", page.publicFile(data)) + htmlFileAbs := filepath.Join(application.App.Root(), htmlFile) + dir := filepath.Dir(htmlFileAbs) + if exist, _ := os.Stat(dir); exist == nil { + os.MkdirAll(dir, os.ModePerm) + } + err := os.WriteFile(htmlFileAbs, html, 0644) + if err != nil { + return err + } + core.RemoveCache(htmlFile) + log.Trace("The page %s is removed", htmlFile) + return nil +} diff --git a/sui/storages/local/template.go b/sui/storages/local/template.go index 69c5c20b4..ca7fb7257 100644 --- a/sui/storages/local/template.go +++ b/sui/storages/local/template.go @@ -26,6 +26,49 @@ func (tmpl *Template) GetRoot() string { return tmpl.Root } +// Glob the files +func (tmpl *Template) Glob(pattern string) ([]string, error) { + path := filepath.Join(tmpl.Root, pattern) + paths, err := tmpl.local.fs.Glob(path) + if err != nil { + return nil, err + } + + routes := []string{} + for _, p := range paths { + routes = append(routes, strings.TrimPrefix(p, tmpl.Root)) + } + return routes, nil +} + +// GlobRoutes the files +func (tmpl *Template) GlobRoutes(patterns []string, unique ...bool) ([]string, error) { + routes := []string{} + for _, pattern := range patterns { + paths, err := tmpl.Glob(pattern) + if err != nil { + return nil, err + } + routes = append(routes, paths...) + } + + // Unique + if len(unique) > 0 && unique[0] { + mapRoutes := map[string]bool{} + for _, route := range routes { + mapRoutes[route] = true + } + + routes = []string{} + for route := range mapRoutes { + routes = append(routes, route) + } + return routes, nil + } + + return routes, nil +} + // Reload the template func (tmpl *Template) Reload() error { newTmpl, err := tmpl.local.getTemplateFrom(tmpl.Root) diff --git a/sui/storages/local/types.go b/sui/storages/local/types.go index a4521860d..4b7300ffd 100644 --- a/sui/storages/local/types.go +++ b/sui/storages/local/types.go @@ -17,6 +17,7 @@ type Template struct { Root string `json:"-"` local *Local locales []core.SelectOption + loaded map[string]core.IPage *core.Template }