Add GSP project manifest and design layout

This commit is contained in:
2026-05-06 19:06:32 +08:00
parent 69003c8152
commit 67a1bf2600
19 changed files with 288 additions and 31 deletions

View File

@@ -28,26 +28,45 @@ func LoadProject(root string) (*Project, error) {
ByID: map[string]*Unit{},
Duplicates: map[string][]*Unit{},
}
manifest, err := loadManifest(absRoot)
if err != nil {
project.LoadIssues = append(project.LoadIssues, Issue{Level: "error", Code: "manifest_error", File: "gsp.manifest", Message: err.Error()})
}
project.Manifest = manifest
var files []string
err = filepath.WalkDir(absRoot, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
project.LoadIssues = append(project.LoadIssues, Issue{Level: "error", Code: "walk_error", File: path, Message: walkErr.Error()})
return nil
for _, scanEntry := range project.Manifest.scanEntries() {
scanPath := filepath.Join(absRoot, filepath.FromSlash(scanEntry))
info, statErr := os.Stat(scanPath)
if statErr != nil {
project.LoadIssues = append(project.LoadIssues, Issue{Level: "warning", Code: "scan_missing", File: scanEntry, Message: "scan path does not exist"})
continue
}
if entry.IsDir() {
if ignoredDirs[entry.Name()] {
return filepath.SkipDir
if !info.IsDir() {
if strings.EqualFold(filepath.Ext(scanPath), ".gsp") {
files = append(files, scanPath)
}
continue
}
walkErr := filepath.WalkDir(scanPath, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
project.LoadIssues = append(project.LoadIssues, Issue{Level: "error", Code: "walk_error", File: path, Message: walkErr.Error()})
return nil
}
if entry.IsDir() {
if ignoredDirs[entry.Name()] {
return filepath.SkipDir
}
return nil
}
if strings.EqualFold(filepath.Ext(entry.Name()), ".gsp") {
files = append(files, path)
}
return nil
})
if walkErr != nil {
return nil, walkErr
}
if strings.EqualFold(filepath.Ext(entry.Name()), ".gsp") {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, err
}
sort.Strings(files)

View File

@@ -0,0 +1,58 @@
package gsp
import (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
type Manifest struct {
GSPVersion string `json:"gspVersion,omitempty" yaml:"gspVersion"`
ToolkitVersion string `json:"toolkitVersion,omitempty" yaml:"toolkitVersion"`
Project string `json:"project,omitempty" yaml:"project"`
Entry []string `json:"entry,omitempty" yaml:"entry"`
Scan []string `json:"scan,omitempty" yaml:"scan"`
StageRules map[string]StageRule `json:"stageRules,omitempty" yaml:"stageRules"`
Types []string `json:"types,omitempty" yaml:"types"`
Output string `json:"output,omitempty" yaml:"output"`
File string `json:"file,omitempty" yaml:"-"`
}
type StageRule struct {
MinResolution string `json:"minResolution,omitempty" yaml:"minResolution"`
}
func loadManifest(root string) (*Manifest, error) {
path := filepath.Join(root, "gsp.manifest")
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var manifest Manifest
if err := yaml.Unmarshal(data, &manifest); err != nil {
return nil, err
}
manifest.File = relPath(root, path)
return &manifest, nil
}
func (m *Manifest) scanEntries() []string {
if m == nil || len(m.Scan) == 0 {
return []string{"design"}
}
return m.Scan
}
func (m *Manifest) minResolution(stage string) (string, bool) {
if m != nil && m.StageRules != nil {
if rule, ok := m.StageRules[stage]; ok && rule.MinResolution != "" {
return rule.MinResolution, true
}
}
value, ok := defaultStageRules[stage]
return value, ok
}

View File

@@ -96,6 +96,7 @@ func (r *Report) addNotice(code, id, file, message string) {
type Project struct {
Root string
Manifest *Manifest
Units []*Unit
ByID map[string]*Unit
Duplicates map[string][]*Unit

View File

@@ -9,8 +9,15 @@ import (
func (p *Project) Validate(stage string) Report {
report := Report{OK: true}
for _, issue := range p.LoadIssues {
report.Errors = append(report.Errors, issue)
report.OK = false
switch issue.Level {
case "error":
report.Errors = append(report.Errors, issue)
report.OK = false
case "warning":
report.Warnings = append(report.Warnings, issue)
default:
report.Notices = append(report.Notices, issue)
}
}
for _, unit := range p.Units {
if unit.ID == "" {
@@ -81,15 +88,17 @@ func (p *Project) Index() []IndexEntry {
return entries
}
var defaultStageRules = map[string]string{
"design": "L0",
"integrate": "L2",
"implement": "L3",
"bind": "L4",
"release": "L5",
}
func (p *Project) StageCheck(stage string) Report {
report := Report{OK: true}
required, ok := map[string]string{
"design": "L0",
"integrate": "L2",
"implement": "L3",
"bind": "L4",
"release": "L5",
}[stage]
required, ok := p.Manifest.minResolution(stage)
if !ok {
report.addError("unknown_stage", "", "", fmt.Sprintf("unknown stage %q", stage))
return report

View File

@@ -8,7 +8,11 @@ import (
func TestLoadValidateAndFlatten(t *testing.T) {
root := t.TempDir()
writeTestFile(t, root, "page.gsp", `id: page.lottery.main
design := filepath.Join(root, "design")
if err := os.MkdirAll(design, 0755); err != nil {
t.Fatal(err)
}
writeTestFile(t, design, "page.gsp", `id: page.lottery.main
type: page
resolution: L3
context: Lottery page.
@@ -18,17 +22,17 @@ with:
- feedback.positive
refines: page.lottery.base
`)
writeTestFile(t, root, "button.gsp", `id: ui.button.primary
writeTestFile(t, design, "button.gsp", `id: ui.button.primary
type: ui
resolution: L3
context: Primary button.
`)
writeTestFile(t, root, "feedback.gsp", `id: feedback.positive
writeTestFile(t, design, "feedback.gsp", `id: feedback.positive
type: feedback
resolution: L2
context: Positive feedback.
`)
writeTestFile(t, root, "base.gsp", `id: page.lottery.base
writeTestFile(t, design, "base.gsp", `id: page.lottery.base
resolution: L2
context: Base lottery page.
`)
@@ -52,7 +56,11 @@ context: Base lottery page.
func TestValidateMissingReference(t *testing.T) {
root := t.TempDir()
writeTestFile(t, root, "page.gsp", `id: page.missing
design := filepath.Join(root, "design")
if err := os.MkdirAll(design, 0755); err != nil {
t.Fatal(err)
}
writeTestFile(t, design, "page.gsp", `id: page.missing
with:
- ui.missing
`)
@@ -77,7 +85,11 @@ with:
func TestStageCheck(t *testing.T) {
root := t.TempDir()
writeTestFile(t, root, "page.gsp", `id: page.low
design := filepath.Join(root, "design")
if err := os.MkdirAll(design, 0755); err != nil {
t.Fatal(err)
}
writeTestFile(t, design, "page.gsp", `id: page.low
resolution: L2
`)
project, err := LoadProject(root)