From 9af92308bb20c3f8c12162d05b72b7960998c738 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 1 Oct 2023 17:20:30 +0800 Subject: [PATCH] [add] template & page asset api --- sui/api/api.go | 22 ++++++ sui/api/process.go | 80 +++++++++++++++++++-- sui/api/process_test.go | 82 +++++++++++++++++++++- sui/core/compile.go | 44 ++++++++++++ sui/core/interfaces.go | 6 ++ sui/core/types.go | 7 ++ sui/storages/local/page.go | 86 +++++++++++++++++++++++ sui/storages/local/page_test.go | 103 ++++++++++++++++++++++++++++ sui/storages/local/template.go | 47 +++++++++++++ sui/storages/local/template_test.go | 19 +++++ 10 files changed, 488 insertions(+), 8 deletions(-) diff --git a/sui/api/api.go b/sui/api/api.go index 8256080fe8..4ccd1c3b6a 100644 --- a/sui/api/api.go +++ b/sui/api/api.go @@ -92,6 +92,28 @@ var dsl = []byte(` "process": "sui.Editor.Source", "in": ["$param.id", "$param.template_id", "$param.route", "$param.kind"], "out": { "status": 200, "type": "application/json" } + }, + + { + "path": "/:id/assets/:template_id/@assets/*path", + "method": "GET", + "process": "sui.Template.Asset", + "in": ["$param.id", "$param.template_id", "$param.path"], + "out": { + "status": 200, + "body": "?:content", + "headers": { "Content-Type": "?:type"} + } + },{ + "path": "/:id/assets/:template_id/@pages/*path", + "method": "GET", + "process": "sui.Page.Asset", + "in": ["$param.id", "$param.template_id", "$param.path"], + "out": { + "status": 200, + "body": "?:content", + "headers": { "Content-Type": "?:type"} + } } ], } diff --git a/sui/api/process.go b/sui/api/process.go index 0be993510d..670fa7f8cb 100644 --- a/sui/api/process.go +++ b/sui/api/process.go @@ -1,6 +1,7 @@ package api import ( + "path/filepath" "strings" "github.com/yaoapp/gou/process" @@ -10,8 +11,9 @@ import ( func init() { process.RegisterGroup("sui", map[string]process.Handler{ - "template.get": TemplateGet, - "template.find": TemplateFind, + "template.get": TemplateGet, + "template.find": TemplateFind, + "template.asset": TemplateAsset, "locale.get": LocaleGet, "theme.get": ThemeGet, @@ -22,8 +24,9 @@ func init() { "component.get": ComponentGet, "component.find": ComponentFind, - "page.tree": PageTree, - "page.get": PageGet, + "page.tree": PageTree, + "page.get": PageGet, + "page.asset": PageAsset, "editor.render": EditorRender, "editor.source": EditorSource, @@ -48,12 +51,32 @@ func TemplateFind(process *process.Process) interface{} { process.ValidateArgNums(2) sui := get(process) - template, err := sui.GetTemplate(process.ArgsString(1)) + tmpl, err := sui.GetTemplate(process.ArgsString(1)) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + return tmpl +} + +// TemplateAsset handle the find Template request +func TemplateAsset(process *process.Process) interface{} { + process.ValidateArgNums(3) + sui := get(process) + tmpl, err := sui.GetTemplate(process.ArgsString(1)) if err != nil { exception.New(err.Error(), 500).Throw() } - return template + asset, err := tmpl.Asset(process.ArgsString(2)) + if err != nil { + exception.New(err.Error(), 404).Throw() + } + + return map[string]interface{}{ + "content": asset.Content, + "type": asset.Type, + } } // LocaleGet handle the find Template request @@ -200,6 +223,51 @@ func PageGet(process *process.Process) interface{} { return tree } +// PageAsset handle the find Template request +func PageAsset(process *process.Process) interface{} { + process.ValidateArgNums(3) + + sui := get(process) + templateID := process.ArgsString(1) + + tmpl, err := sui.GetTemplate(templateID) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + file := process.ArgsString(2) + page, err := tmpl.GetPageFromAsset(file) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + var asset *core.Asset + + switch filepath.Ext(file) { + case ".css": + asset, err = page.AssetStyle() + if err != nil { + exception.New(err.Error(), 400).Throw() + } + break + + case ".js", ".ts": + asset, err = page.AssetScript() + if err != nil { + exception.New(err.Error(), 400).Throw() + } + break + + default: + exception.New("does not support the %s file", 400, filepath.Ext(file)).Throw() + } + + return map[string]interface{}{ + "content": asset.Content, + "type": asset.Type, + } +} + // EditorRender handle the render page request func EditorRender(process *process.Process) interface{} { process.ValidateArgNums(3) diff --git a/sui/api/process_test.go b/sui/api/process_test.go index 86dfb3e5ca..5a7cfc5502 100644 --- a/sui/api/process_test.go +++ b/sui/api/process_test.go @@ -48,6 +48,26 @@ func TestTemplateFind(t *testing.T) { assert.Equal(t, "tech-blue", res.(*local.Template).ID) } +func TestTemplateAsset(t *testing.T) { + load(t) + defer clean() + + // test demo + p, err := process.Of("sui.template.asset", "demo", "tech-blue", "/css/tailwind.css") + if err != nil { + t.Fatal(err) + } + + res, err := p.Exec() + if err != nil { + t.Fatal(err) + } + + assert.NotEmpty(t, res) + assert.Equal(t, "text/css; charset=utf-8", res.(map[string]interface{})["type"]) + assert.NotEmpty(t, res.(map[string]interface{})["content"]) +} + func TestTemplateLocaleGet(t *testing.T) { load(t) defer clean() @@ -174,7 +194,7 @@ func TestTemplateComponentFind(t *testing.T) { assert.Contains(t, res.(string), "window.component__Box=") } -func TestTemplatePageTree(t *testing.T) { +func TestPageTree(t *testing.T) { load(t) defer clean() @@ -195,7 +215,7 @@ func TestTemplatePageTree(t *testing.T) { assert.Equal(t, "index", res.([]*core.PageTreeNode)[1].Name) } -func TestTemplatePageGet(t *testing.T) { +func TestPageGet(t *testing.T) { load(t) defer clean() @@ -218,6 +238,64 @@ func TestTemplatePageGet(t *testing.T) { } } +func TestPageAssetJS(t *testing.T) { + + load(t) + defer clean() + + // test demo + p, err := process.Of("sui.page.asset", "demo", "tech-blue", "/page/404/404.js") + if err != nil { + t.Fatal(err) + } + + res, err := p.Exec() + if err != nil { + t.Fatal(err) + } + assert.IsType(t, map[string]interface{}{}, res) + assert.Equal(t, "text/javascript; charset=utf-8", res.(map[string]interface{})["type"]) + assert.NotEmpty(t, res.(map[string]interface{})["content"]) +} + +func TestPageAssetTS(t *testing.T) { + load(t) + defer clean() + + // test demo + p, err := process.Of("sui.page.asset", "demo", "tech-blue", "/page/404/404.ts") + if err != nil { + t.Fatal(err) + } + + res, err := p.Exec() + if err != nil { + t.Fatal(err) + } + assert.IsType(t, map[string]interface{}{}, res) + assert.Equal(t, "text/javascript; charset=utf-8", res.(map[string]interface{})["type"]) + assert.NotEmpty(t, res.(map[string]interface{})["content"]) +} + +func TestPageAssetCSS(t *testing.T) { + load(t) + defer clean() + + // test demo + p, err := process.Of("sui.page.asset", "demo", "tech-blue", "/page/[id]/[id].css") + if err != nil { + t.Fatal(err) + } + + res, err := p.Exec() + if err != nil { + t.Fatal(err) + } + assert.IsType(t, map[string]interface{}{}, res) + assert.Equal(t, "text/css; charset=utf-8", res.(map[string]interface{})["type"]) + assert.NotEmpty(t, res.(map[string]interface{})["content"]) +} + func TestEditorRender(t *testing.T) { load(t) defer clean() diff --git a/sui/core/compile.go b/sui/core/compile.go index 0867402daa..2de47774a5 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -1,4 +1,48 @@ package core +import ( + "regexp" + + "github.com/evanw/esbuild/pkg/api" + "github.com/yaoapp/gou/runtime/transform" +) + // Compile the page func (page *Page) Compile() {} + +// CompileJS compile the javascript +func (page *Page) CompileJS(source []byte, minify bool) ([]byte, error) { + jsCode := regexp.MustCompile(`import\s+.*;`).ReplaceAllString(string(source), "") + if minify { + minified, err := transform.MinifyJS(jsCode) + return []byte(minified), err + } + return []byte(jsCode), nil +} + +// CompileTS compile the typescript +func (page *Page) CompileTS(source []byte, minify bool) ([]byte, error) { + tsCode := regexp.MustCompile(`import\s+.*;`).ReplaceAllString(string(source), "") + if minify { + jsCode, err := transform.TypeScript(string(tsCode), api.TransformOptions{ + Target: api.ESNext, + MinifyWhitespace: true, + MinifyIdentifiers: true, + MinifySyntax: true, + }) + + return []byte(jsCode), err + } + + jsCode, err := transform.TypeScript(string(tsCode), api.TransformOptions{Target: api.ESNext}) + return []byte(jsCode), err +} + +// CompileCSS compile the css +func (page *Page) CompileCSS(source []byte, minify bool) ([]byte, error) { + if minify { + cssCode, err := transform.MinifyCSS(string(source)) + return []byte(cssCode), err + } + return source, nil +} diff --git a/sui/core/interfaces.go b/sui/core/interfaces.go index aa017fff84..816a3c3858 100644 --- a/sui/core/interfaces.go +++ b/sui/core/interfaces.go @@ -15,6 +15,7 @@ type ITemplate interface { Pages() ([]IPage, error) PageTree(route string) ([]*PageTreeNode, error) Page(route string) (IPage, error) + GetPageFromAsset(asset string) (IPage, error) Blocks() ([]IBlock, error) Block(name string) (IBlock, error) @@ -25,6 +26,8 @@ type ITemplate interface { Assets() []string Locales() []SelectOption Themes() []SelectOption + + Asset(file string) (*Asset, error) } // IPage is the interface for the page @@ -39,6 +42,9 @@ type IPage interface { EditorStyleSource() ResponseSource EditorDataSource() ResponseSource + AssetScript() (*Asset, error) + AssetStyle() (*Asset, error) + // Render() // Html() diff --git a/sui/core/types.go b/sui/core/types.go index f7a36b2ef8..963f8211d5 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -66,6 +66,13 @@ type SelectOption struct { Value string `json:"value"` } +// Asset is the struct for the asset +type Asset struct { + file string + Type string `json:"type"` + Content []byte `json:"content"` +} + // Request is the struct for the request type Request struct { Method string `json:"method"` diff --git a/sui/storages/local/page.go b/sui/storages/local/page.go index 102d979e28..b68705f864 100644 --- a/sui/storages/local/page.go +++ b/sui/storages/local/page.go @@ -254,3 +254,89 @@ func (page *Page) Load() error { page.Document = page.tmpl.Document return nil } + +// GetPageFromAsset get the page from the asset +func (tmpl *Template) GetPageFromAsset(file string) (core.IPage, error) { + route := filepath.Dir(file) + name := tmpl.getPageBase(route) + return &Page{ + tmpl: tmpl, + Page: &core.Page{ + Route: route, + Path: filepath.Join(tmpl.Root, route), + Name: name, + Codes: core.SourceCodes{ + CSS: core.Source{File: fmt.Sprintf("%s.css", name)}, + JS: core.Source{File: fmt.Sprintf("%s.js", name)}, + TS: core.Source{File: fmt.Sprintf("%s.ts", name)}, + LESS: core.Source{File: fmt.Sprintf("%s.less", name)}, + }, + }, + }, nil +} + +// AssetScript get the script +func (page *Page) AssetScript() (*core.Asset, error) { + + // Read the Script code + // Type script is the default language + tsFile := filepath.Join(page.Path, page.Codes.TS.File) + if exist, _ := page.tmpl.local.fs.Exists(tsFile); exist { + tsCode, err := page.tmpl.local.fs.ReadFile(tsFile) + if err != nil { + return nil, err + } + + jsCode, err := page.CompileTS(tsCode, false) + if err != nil { + return nil, err + } + + return &core.Asset{ + Type: "text/javascript; charset=utf-8", + Content: []byte(jsCode), + }, nil + } + + jsFile := filepath.Join(page.Path, page.Codes.JS.File) + if exist, _ := page.tmpl.local.fs.Exists(jsFile); exist { + jsCode, err := page.tmpl.local.fs.ReadFile(jsFile) + if err != nil { + return nil, err + } + + jsCode, err = page.CompileJS(jsCode, false) + if err != nil { + return nil, err + } + + return &core.Asset{ + Type: "text/javascript; charset=utf-8", + Content: jsCode, + }, nil + } + + return nil, fmt.Errorf("Page %s script not found", page.Route) +} + +// AssetStyle get the style +func (page *Page) AssetStyle() (*core.Asset, error) { + cssFile := filepath.Join(page.Path, page.Codes.CSS.File) + if exist, _ := page.tmpl.local.fs.Exists(cssFile); exist { + cssCode, err := page.tmpl.local.fs.ReadFile(cssFile) + if err != nil { + return nil, err + } + + cssCode, err = page.CompileCSS(cssCode, false) + if err != nil { + return nil, err + } + + return &core.Asset{ + Type: "text/css; charset=utf-8", + Content: cssCode, + }, nil + } + return nil, fmt.Errorf("Page %s style not found", page.Route) +} diff --git a/sui/storages/local/page_test.go b/sui/storages/local/page_test.go index be9197b4c2..302630c82d 100644 --- a/sui/storages/local/page_test.go +++ b/sui/storages/local/page_test.go @@ -186,3 +186,106 @@ func TestPageRenderEditor(t *testing.T) { assert.Equal(t, "@assets/css/tailwind.css", res.Styles[3]) assert.Equal(t, "@pages/index/index.css", res.Styles[4]) } + +func TestPageGetPageFromAsset(t *testing.T) { + + tests := prepare(t) + defer clean() + + tmpl, err := tests.Demo.GetTemplate("tech-blue") + if err != nil { + t.Fatalf("GetTemplate error: %v", err) + } + + file := "/index/index.css" + page, err := tmpl.GetPageFromAsset(file) + if err != nil { + t.Fatalf("GetPageFromAsset error: %v", err) + } + + assert.Equal(t, "/index", page.Get().Route) + assert.Equal(t, "/templates/tech-blue/index", page.Get().Path) + assert.Equal(t, "index", page.Get().Name) + + file = "/page/404/404.js" + page, err = tmpl.GetPageFromAsset(file) + if err != nil { + t.Fatalf("GetPageFromAsset error: %v", err) + } + + assert.Equal(t, "/page/404", page.Get().Route) + assert.Equal(t, "/templates/tech-blue/page/404", page.Get().Path) + assert.Equal(t, "404", page.Get().Name) +} + +func TestPageAssetScriptJS(t *testing.T) { + tests := prepare(t) + defer clean() + + tmpl, err := tests.Demo.GetTemplate("tech-blue") + if err != nil { + t.Fatalf("GetTemplate error: %v", err) + } + + file := "/page/404/404.js" + page, err := tmpl.GetPageFromAsset(file) + if err != nil { + t.Fatalf("GetPageFromAsset error: %v", err) + } + + asset, err := page.AssetScript() + if err != nil { + t.Fatalf("AssetScript error: %v", err) + } + + assert.NotEmpty(t, asset.Content) + assert.Equal(t, "text/javascript; charset=utf-8", asset.Type) +} + +func TestPageAssetScriptTS(t *testing.T) { + tests := prepare(t) + defer clean() + + tmpl, err := tests.Demo.GetTemplate("tech-blue") + if err != nil { + t.Fatalf("GetTemplate error: %v", err) + } + + file := "/page/[id]/[id].ts" + page, err := tmpl.GetPageFromAsset(file) + if err != nil { + t.Fatalf("GetPageFromAsset error: %v", err) + } + + asset, err := page.AssetScript() + if err != nil { + t.Fatalf("AssetScript error: %v", err) + } + + assert.NotEmpty(t, asset.Content) + assert.Equal(t, "text/javascript; charset=utf-8", asset.Type) +} + +func TestPageAssetStyle(t *testing.T) { + tests := prepare(t) + defer clean() + + tmpl, err := tests.Demo.GetTemplate("tech-blue") + if err != nil { + t.Fatalf("GetTemplate error: %v", err) + } + + file := "/page/[id]/[id].css" + page, err := tmpl.GetPageFromAsset(file) + if err != nil { + t.Fatalf("GetPageFromAsset error: %v", err) + } + + asset, err := page.AssetStyle() + if err != nil { + t.Fatalf("AssetStyle error: %v", err) + } + + assert.NotEmpty(t, asset.Content) + assert.Equal(t, "text/css; charset=utf-8", asset.Type) +} diff --git a/sui/storages/local/template.go b/sui/storages/local/template.go index bf5f5d7a0e..e17a5f9686 100644 --- a/sui/storages/local/template.go +++ b/sui/storages/local/template.go @@ -1,6 +1,7 @@ package local import ( + "fmt" "path/filepath" "github.com/yaoapp/yao/sui/core" @@ -42,3 +43,49 @@ func (tmpl *Template) Locales() []core.SelectOption { func (tmpl *Template) Themes() []core.SelectOption { return tmpl.Template.Themes } + +// Asset get the asset +func (tmpl *Template) Asset(file string) (*core.Asset, error) { + + file = filepath.Join(tmpl.Root, "__assets", file) + if exist, _ := tmpl.local.fs.Exists(file); exist { + + content, err := tmpl.local.fs.ReadFile(file) + if err != nil { + return nil, err + } + + typ := "text/plain" + switch filepath.Ext(file) { + case ".css": + typ = "text/css; charset=utf-8" + break + + case ".js": + typ = "application/javascript; charset=utf-8" + break + + case ".ts": + typ = "application/javascript; charset=utf-8" + break + + case ".json": + typ = "application/json; charset=utf-8" + break + + case ".html": + typ = "text/html; charset=utf-8" + break + + default: + typ, err = tmpl.local.fs.MimeType(file) + if err != nil { + return nil, err + } + } + + return &core.Asset{Type: typ, Content: content}, nil + } + + return nil, fmt.Errorf("Asset %s not found", file) +} diff --git a/sui/storages/local/template_test.go b/sui/storages/local/template_test.go index 0432452e20..c376d8dd56 100644 --- a/sui/storages/local/template_test.go +++ b/sui/storages/local/template_test.go @@ -49,3 +49,22 @@ func TestTemplateLocales(t *testing.T) { assert.Equal(t, "zh-TW", locales[2].Label) assert.Equal(t, "zh-tw", locales[2].Value) } + +func TestTemplateAsset(t *testing.T) { + tests := prepare(t) + defer clean() + + tmpl, err := tests.Demo.GetTemplate("tech-blue") + if err != nil { + t.Fatalf("GetTemplate error: %v", err) + } + + asset, err := tmpl.Asset("/css/tailwind.css") + if err != nil { + t.Fatalf("Asset error: %v", err) + } + + assert.Equal(t, "text/css; charset=utf-8", asset.Type) + assert.NotEmpty(t, asset.Content) + +}