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)
+}