diff --git a/go.mod b/go.mod index 09732e3c..8947308b 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,8 @@ require ( github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index d964448e..1f9afc02 100644 --- a/go.sum +++ b/go.sum @@ -251,6 +251,7 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= @@ -260,6 +261,8 @@ github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ= github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-plugin v1.5.1 h1:oGm7cWBaYIp3lJpx1RUEfLWophprE2EV/KUeqBYo+6k= diff --git a/sui/core/build.go b/sui/core/build.go new file mode 100644 index 00000000..f1dd0582 --- /dev/null +++ b/sui/core/build.go @@ -0,0 +1,96 @@ +package core + +import ( + "fmt" + "strings" + + "github.com/PuerkitoBio/goquery" +) + +// Build is the struct for the public +func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error) { + + warnings := []string{} + html, err := page.BuildHTML(option.AssetRoot) + if err != nil { + warnings = append(warnings, err.Error()) + } + + // Add Style & Script & Warning + doc, err := NewDocument([]byte(html)) + if err != nil { + warnings = append(warnings, err.Error()) + } + + // Add Style + style, err := page.BuildStyle(option.AssetRoot) + if err != nil { + warnings = append(warnings, err.Error()) + } + doc.Selection.Find("head").AppendHtml(style) + + // Add Script + script, err := page.BuildScript(option.AssetRoot) + if err != nil { + warnings = append(warnings, err.Error()) + } + doc.Selection.Find("body").AppendHtml(script) + return doc, warnings, nil +} + +// BuildHTML build the html +func (page *Page) BuildHTML(assetRoot string) (string, error) { + + html := string(page.Document) + if page.Codes.HTML.Code != "" { + html = strings.Replace(html, "{{ __page }}", page.Codes.HTML.Code, 1) + } + + code := strings.ReplaceAll(html, "@assets", assetRoot) + res, err := page.CompileHTML([]byte(code), false) + if err != nil { + return "", err + } + + return string(res), nil +} + +// BuildStyle build the style +func (page *Page) BuildStyle(assetRoot string) (string, error) { + if page.Codes.CSS.Code == "" { + return "", nil + } + + code := strings.ReplaceAll(page.Codes.CSS.Code, "@assets", assetRoot) + res, err := page.CompileCSS([]byte(code), false) + if err != nil { + return "", err + } + + return fmt.Sprintf("\n", res), nil +} + +// BuildScript build the script +func (page *Page) BuildScript(assetRoot string) (string, error) { + + if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { + return "", nil + } + + if page.Codes.TS.Code != "" { + res, err := page.CompileTS([]byte(page.Codes.TS.Code), false) + if err != nil { + return "", err + } + + return fmt.Sprintf("\n", res), nil + } + + code := strings.ReplaceAll(page.Codes.JS.Code, "@assets", assetRoot) + res, err := page.CompileJS([]byte(code), false) + if err != nil { + return "", err + } + + return fmt.Sprintf("\n", res), nil +} diff --git a/sui/core/compile.go b/sui/core/compile.go index aff2ea7c..46457075 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -1,9 +1,7 @@ package core import ( - "fmt" "regexp" - "strings" "github.com/evanw/esbuild/pkg/api" "github.com/yaoapp/gou/runtime/transform" @@ -53,60 +51,3 @@ func (page *Page) CompileCSS(source []byte, minify bool) ([]byte, error) { func (page *Page) CompileHTML(source []byte, minify bool) ([]byte, error) { return source, nil } - -// BuildHTML build the html -func (page *Page) BuildHTML(assetRoot string) (string, error) { - - html := string(page.Document) - if page.Codes.HTML.Code != "" { - html = strings.Replace(html, "{{ __page }}", page.Codes.HTML.Code, 1) - } - - code := strings.ReplaceAll(html, "@assets", assetRoot) - res, err := page.CompileHTML([]byte(code), false) - if err != nil { - return "", err - } - - return string(res), nil -} - -// BuildStyle build the style -func (page *Page) BuildStyle(assetRoot string) (string, error) { - if page.Codes.CSS.Code == "" { - return "", nil - } - - code := strings.ReplaceAll(page.Codes.CSS.Code, "@assets", assetRoot) - res, err := page.CompileCSS([]byte(code), false) - if err != nil { - return "", err - } - - return fmt.Sprintf("\n", res), nil -} - -// BuildScript build the script -func (page *Page) BuildScript(assetRoot string) (string, error) { - - if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { - return "", nil - } - - if page.Codes.TS.Code != "" { - res, err := page.CompileTS([]byte(page.Codes.TS.Code), false) - if err != nil { - return "", err - } - - return fmt.Sprintf("\n", res), nil - } - - code := strings.ReplaceAll(page.Codes.JS.Code, "@assets", assetRoot) - res, err := page.CompileJS([]byte(code), false) - if err != nil { - return "", err - } - - return fmt.Sprintf("\n", res), nil -} diff --git a/sui/core/interfaces.go b/sui/core/interfaces.go index 66a7dffd..c23b6d30 100644 --- a/sui/core/interfaces.go +++ b/sui/core/interfaces.go @@ -31,6 +31,9 @@ type ITemplate interface { Themes() []SelectOption Asset(file string) (*Asset, error) + + Build(option *BuildOption) error + SyncAssets(option *BuildOption) error } // IPage is the interface for the page @@ -53,6 +56,8 @@ type IPage interface { AssetScript() (*Asset, error) AssetStyle() (*Asset, error) + + Build(option *BuildOption) error } // IBlock is the interface for the block diff --git a/sui/core/preview.go b/sui/core/preview.go index 1e8a5ff7..7172deaf 100644 --- a/sui/core/preview.go +++ b/sui/core/preview.go @@ -8,41 +8,16 @@ import ( func (page *Page) PreviewRender(request *Request) (string, error) { warnings := []string{} - html, err := page.BuildHTML(request.AssetRoot) - if err != nil { - warnings = append(warnings, err.Error()) - } + doc, warnings, err := page.Build(&BuildOption{ + SSR: true, + AssetRoot: request.AssetRoot, + }) data, _, err := page.Data(request) if err != nil { warnings = append(warnings, err.Error()) } - html, err = page.Render(html, data, warnings) - if err != nil { - warnings = append(warnings, err.Error()) - } - - // Add Style & Script & Warning - doc, err := NewDocument([]byte(html)) - if err != nil { - warnings = append(warnings, err.Error()) - } - - // Add Style - style, err := page.BuildStyle(request.AssetRoot) - if err != nil { - warnings = append(warnings, err.Error()) - } - doc.Selection.Find("head").AppendHtml(style) - - // Add Script - script, err := page.BuildScript(request.AssetRoot) - if err != nil { - warnings = append(warnings, err.Error()) - } - doc.Selection.Find("body").AppendHtml(script) - // Add Frame Height if request.Referer != "" { doc.Selection.Find("body").AppendHtml(` @@ -80,5 +55,15 @@ func (page *Page) PreviewRender(request *Request) (string, error) { doc.Selection.Find("body").AppendHtml(warningHTML) } + html, err := doc.Html() + if err != nil { + return "", err + } + + html, err = page.Render(html, data, warnings) + if err != nil { + warnings = append(warnings, err.Error()) + } + return doc.Html() } diff --git a/sui/core/types.go b/sui/core/types.go index 52ee09a4..85401561 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -76,6 +76,14 @@ type Asset struct { Content []byte `json:"content"` } +// BuildOption is the struct for the option option +type BuildOption struct { + SSR bool `json:"ssr"` + CDN bool `json:"cdn"` + UpdateAll bool `json:"update_all"` + AssetRoot string `json:"asset_root,omitempty"` +} + // Request is the struct for the request type Request struct { Method string `json:"method"` diff --git a/sui/storages/local/build.go b/sui/storages/local/build.go new file mode 100644 index 00000000..750f3b7f --- /dev/null +++ b/sui/storages/local/build.go @@ -0,0 +1,117 @@ +package local + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hashicorp/go-multierror" + "github.com/yaoapp/gou/application" + "github.com/yaoapp/yao/sui/core" +) + +// Build the template +func (tmpl *Template) Build(option *core.BuildOption) error { + var err error + + if option.AssetRoot == "" { + option.AssetRoot = filepath.Join(tmpl.local.DSL.Public.Root, "assets") + } + + // Sync the assets + if err = tmpl.SyncAssets(option); err != nil { + return err + } + + // Build all pages + pages, err := tmpl.Pages() + if err != nil { + return err + } + + for _, page := range pages { + perr := page.Load() + if err != nil { + err = multierror.Append(perr) + continue + } + + perr = page.Build(option) + if perr != nil { + err = multierror.Append(perr) + } + } + + return err +} + +// SyncAssets sync the assets +func (tmpl *Template) SyncAssets(option *core.BuildOption) error { + + // get source abs path + sourceRoot := filepath.Join(tmpl.local.fs.Root(), tmpl.Root, "__assets") + if exist, _ := os.Stat(sourceRoot); exist == nil { + return nil + } + + //get target abs path + root := tmpl.local.DSL.Public.Root + targetRoot := filepath.Join(application.App.Root(), "public", root, "assets") + if exist, _ := os.Stat(targetRoot); exist == nil { + os.MkdirAll(targetRoot, os.ModePerm) + } + os.RemoveAll(targetRoot) + + return copyDirectory(sourceRoot, targetRoot) +} + +// Build is the struct for the public +func (page *Page) Build(option *core.BuildOption) error { + + doc, _, err := page.Page.Build(option) + if err != nil { + return err + } + + html, err := doc.Html() + if err != nil { + return err + } + + // Save the html + err = page.writeHTML([]byte(html)) + if err != nil { + return err + } + + // Save the data + return page.writeData() +} + +func (page *Page) publicFile() string { + root := page.tmpl.local.DSL.Public.Root + return filepath.Join(application.App.Root(), "public", root, page.Route) +} + +// writeHTMLTo write the html to file +func (page *Page) writeHTML(html []byte) error { + htmlFile := fmt.Sprintf("%s.html", page.publicFile()) + dir := filepath.Dir(htmlFile) + if exist, _ := os.Stat(dir); exist == nil { + os.MkdirAll(dir, os.ModePerm) + } + return os.WriteFile(htmlFile, html, 0644) +} + +// writeHTMLTo write the html to file +func (page *Page) writeData() error { + if page.Codes.DATA.Code == "" { + return nil + } + dataFile := fmt.Sprintf("%s.json", page.publicFile()) + dir := filepath.Dir(dataFile) + if exist, _ := os.Stat(dir); exist == nil { + os.MkdirAll(dir, os.ModePerm) + } + return os.WriteFile(dataFile, []byte(page.Codes.DATA.Code), 0644) +} diff --git a/sui/storages/local/build_test.go b/sui/storages/local/build_test.go new file mode 100644 index 00000000..5b3ce45e --- /dev/null +++ b/sui/storages/local/build_test.go @@ -0,0 +1,22 @@ +package local + +import ( + "testing" + + "github.com/yaoapp/yao/sui/core" +) + +func TestTemplateBuild(t *testing.T) { + tests := prepare(t) + defer clean() + + tmpl, err := tests.Demo.GetTemplate("tech-blue") + if err != nil { + t.Fatalf("GetTemplate error: %v", err) + } + + err = tmpl.Build(&core.BuildOption{SSR: true}) + if err != nil { + t.Fatalf("Components error: %v", err) + } +} diff --git a/sui/storages/local/copy.go b/sui/storages/local/copy.go new file mode 100644 index 00000000..3a02da48 --- /dev/null +++ b/sui/storages/local/copy.go @@ -0,0 +1,127 @@ +package local + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "syscall" +) + +// copyDirectory copy the directory +func copyDirectory(scrDir, dest string) error { + + if err := createIfNotexists(dest, 0755); err != nil { + return err + } + + entries, err := os.ReadDir(scrDir) + if err != nil { + return err + } + for _, entry := range entries { + sourcePath := filepath.Join(scrDir, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + + fileInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + + stat, ok := fileInfo.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("failed to get raw syscall.Stat_t data for '%s'", sourcePath) + } + + switch fileInfo.Mode() & os.ModeType { + case os.ModeDir: + if err := createIfNotexists(destPath, 0755); err != nil { + return err + } + if err := copyDirectory(sourcePath, destPath); err != nil { + return err + } + case os.ModeSymlink: + if err := copySymLink(sourcePath, destPath); err != nil { + return err + } + default: + if strings.HasPrefix(entry.Name(), ".") { + continue + } + + if err := copy(sourcePath, destPath); err != nil { + return fmt.Errorf("failed to copy '%s' to '%s': %s", sourcePath, destPath, err.Error()) + } + } + + if err := os.Lchown(destPath, int(stat.Uid), int(stat.Gid)); err != nil { + return err + } + + fInfo, err := entry.Info() + if err != nil { + return err + } + + isSymlink := fInfo.Mode()&os.ModeSymlink != 0 + if !isSymlink { + if err := os.Chmod(destPath, fInfo.Mode()); err != nil { + return err + } + } + } + return nil +} + +func copy(srcFile, dstFile string) error { + out, err := os.Create(dstFile) + if err != nil { + return err + } + + defer out.Close() + + in, err := os.Open(srcFile) + if err != nil { + return err + } + + defer in.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + + return nil +} + +func exists(filePath string) bool { + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return false + } + + return true +} + +func createIfNotexists(dir string, perm os.FileMode) error { + if exists(dir) { + return nil + } + + if err := os.MkdirAll(dir, perm); err != nil { + return fmt.Errorf("failed to create directory: '%s', error: '%s'", dir, err.Error()) + } + + return nil +} + +func copySymLink(source, dest string) error { + link, err := os.Readlink(source) + if err != nil { + return err + } + return os.Symlink(link, dest) +}