Skip to content

Commit 556efe1

Browse files
committed
feat: add framework detection and UI build improvements
- Add framework detection (React, Vue, Svelte, vanilla) for views/UI bundles - Display detected framework in build summary output - Sort resource sizes to group related resources together (e.g., core, core/ui) - Separate UI count from resources in build summary statistics - Show detailed build output for failed tasks to aid debugging - Add dependency validation before build (esbuild, @swc/core) - Add --force flag to update command to bypass
1 parent f946a19 commit 556efe1

6 files changed

Lines changed: 170 additions & 28 deletions

File tree

internal/builder/builder.go

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"path/filepath"
77
"runtime"
8+
"sort"
89
"strings"
910
"time"
1011

@@ -633,7 +634,47 @@ type ResourceSize struct {
633634
ServerSize int64
634635
ClientSize int64
635636
TotalSize int64
636-
IsViews bool // true if this is a views/UI bundle
637+
IsViews bool // true if this is a views/UI bundle
638+
Framework string // framework used (react, vue, svelte, vanilla) - only for views
639+
}
640+
641+
// detectFramework detects the framework used in a views directory
642+
func detectFramework(viewPath string) string {
643+
// Check for framework-specific files
644+
hasReact := false
645+
hasVue := false
646+
hasSvelte := false
647+
648+
filepath.WalkDir(viewPath, func(path string, d os.DirEntry, err error) error {
649+
if err != nil || d.IsDir() {
650+
if d != nil && d.Name() == "node_modules" {
651+
return filepath.SkipDir
652+
}
653+
return nil
654+
}
655+
ext := filepath.Ext(d.Name())
656+
switch ext {
657+
case ".tsx", ".jsx":
658+
hasReact = true
659+
case ".vue":
660+
hasVue = true
661+
case ".svelte":
662+
hasSvelte = true
663+
}
664+
return nil
665+
})
666+
667+
// Return detected framework (prioritize by specificity)
668+
if hasSvelte {
669+
return "svelte"
670+
}
671+
if hasVue {
672+
return "vue"
673+
}
674+
if hasReact {
675+
return "react"
676+
}
677+
return "vanilla"
637678
}
638679

639680
// formatSize formats bytes into human readable format (KB/MB)
@@ -686,10 +727,13 @@ func (b *Builder) getResourceSizes(results []BuildResult) []ResourceSize {
686727
if r.Task.Type == TypeViews {
687728
totalSize := getDirSize(resourceDir)
688729
if totalSize > 0 {
730+
// Detect framework from source path
731+
framework := detectFramework(r.Task.Path)
689732
sizes = append(sizes, ResourceSize{
690733
Name: resourceName,
691734
TotalSize: totalSize,
692735
IsViews: true,
736+
Framework: framework,
693737
})
694738
}
695739
continue
@@ -719,14 +763,30 @@ func (b *Builder) getResourceSizes(results []BuildResult) []ResourceSize {
719763
}
720764
}
721765

766+
// Sort sizes to group related resources together (core, core/ui, xchat, xchat/ui, etc.)
767+
sort.Slice(sizes, func(i, j int) bool {
768+
// Extract base names (before any /)
769+
baseI := strings.Split(sizes[i].Name, "/")[0]
770+
baseJ := strings.Split(sizes[j].Name, "/")[0]
771+
772+
// If different base names, sort alphabetically by base
773+
if baseI != baseJ {
774+
return baseI < baseJ
775+
}
776+
777+
// Same base: main resource comes before sub-resources (e.g., core before core/ui)
778+
return len(sizes[i].Name) < len(sizes[j].Name)
779+
})
780+
722781
return sizes
723782
}
724783

725784
// showSummary displays the build summary
726785
func (b *Builder) showSummary(results []BuildResult) {
727-
// Count unique resources and standalones separately
786+
// Count unique resources, standalones, and UIs separately
728787
successResources := make(map[string]struct{})
729788
successStandalones := make(map[string]struct{})
789+
successUIs := make(map[string]struct{})
730790
failedResources := make(map[string]struct{})
731791
failedStandalones := make(map[string]struct{})
732792
totalDuration := time.Duration(0)
@@ -738,7 +798,9 @@ func (b *Builder) showSummary(results []BuildResult) {
738798
if r.Success {
739799
if isStandalone {
740800
successStandalones[baseResource] = struct{}{}
741-
} else if r.Task.Type != TypeViews { // Don't count views separately
801+
} else if r.Task.Type == TypeViews {
802+
successUIs[r.Task.ResourceName] = struct{}{}
803+
} else {
742804
successResources[baseResource] = struct{}{}
743805
}
744806
totalDuration += r.Duration
@@ -753,6 +815,7 @@ func (b *Builder) showSummary(results []BuildResult) {
753815

754816
successResourceCount := len(successResources)
755817
successStandaloneCount := len(successStandalones)
818+
successUICount := len(successUIs)
756819
failResourceCount := len(failedResources)
757820
failStandaloneCount := len(failedStandalones)
758821
failCount := failResourceCount + failStandaloneCount
@@ -771,12 +834,18 @@ func (b *Builder) showSummary(results []BuildResult) {
771834
boxContent.WriteString("Build completed successfully!\n\n")
772835

773836
// Show counts based on what's present
774-
if successResourceCount > 0 && successStandaloneCount > 0 {
775-
boxContent.WriteString(fmt.Sprintf("Resources: %d | Standalones: %d\n", successResourceCount, successStandaloneCount))
776-
} else if successResourceCount > 0 {
777-
boxContent.WriteString(fmt.Sprintf("Resources: %d\n", successResourceCount))
778-
} else if successStandaloneCount > 0 {
779-
boxContent.WriteString(fmt.Sprintf("Standalones: %d\n", successStandaloneCount))
837+
var countParts []string
838+
if successResourceCount > 0 {
839+
countParts = append(countParts, fmt.Sprintf("Resources: %d", successResourceCount))
840+
}
841+
if successUICount > 0 {
842+
countParts = append(countParts, fmt.Sprintf("UIs: %d", successUICount))
843+
}
844+
if successStandaloneCount > 0 {
845+
countParts = append(countParts, fmt.Sprintf("Standalones: %d", successStandaloneCount))
846+
}
847+
if len(countParts) > 0 {
848+
boxContent.WriteString(strings.Join(countParts, " | ") + "\n")
780849
}
781850

782851
boxContent.WriteString(fmt.Sprintf("Time: %s\n", totalDuration.Round(time.Millisecond)))
@@ -794,10 +863,14 @@ func (b *Builder) showSummary(results []BuildResult) {
794863
for _, s := range sizes {
795864
nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
796865
if s.IsViews {
797-
// Views show only total size (includes JS, CSS, HTML, assets)
866+
// Views show only total size (includes JS, CSS, HTML, assets) + framework
798867
totalStr := lipgloss.NewStyle().Foreground(lipgloss.Color("#E879F9")).Render(formatSize(s.TotalSize))
799-
boxContent.WriteString(fmt.Sprintf("%s Total: %s\n",
800-
nameStyle.Render(fmt.Sprintf("%-14s", s.Name)), totalStr))
868+
frameworkStr := ""
869+
if s.Framework != "" {
870+
frameworkStr = lipgloss.NewStyle().Foreground(lipgloss.Color("#6B7280")).Render(fmt.Sprintf(" (%s)", s.Framework))
871+
}
872+
boxContent.WriteString(fmt.Sprintf("%s Total: %s%s\n",
873+
nameStyle.Render(fmt.Sprintf("%-14s", s.Name)), totalStr, frameworkStr))
801874
} else {
802875
serverStr := lipgloss.NewStyle().Foreground(lipgloss.Color("#6B7280")).Render("-")
803876
clientStr := lipgloss.NewStyle().Foreground(lipgloss.Color("#6B7280")).Render("-")
@@ -821,14 +894,18 @@ func (b *Builder) showSummary(results []BuildResult) {
821894
boxContent.WriteString("Build completed with errors\n\n")
822895

823896
// Show success counts
824-
if successResourceCount > 0 || successStandaloneCount > 0 {
825-
if successResourceCount > 0 && successStandaloneCount > 0 {
826-
boxContent.WriteString(fmt.Sprintf("✓ Success: Resources: %d | Standalones: %d\n", successResourceCount, successStandaloneCount))
827-
} else if successResourceCount > 0 {
828-
boxContent.WriteString(fmt.Sprintf("✓ Success: Resources: %d\n", successResourceCount))
829-
} else if successStandaloneCount > 0 {
830-
boxContent.WriteString(fmt.Sprintf("✓ Success: Standalones: %d\n", successStandaloneCount))
897+
if successResourceCount > 0 || successStandaloneCount > 0 || successUICount > 0 {
898+
var successParts []string
899+
if successResourceCount > 0 {
900+
successParts = append(successParts, fmt.Sprintf("Resources: %d", successResourceCount))
831901
}
902+
if successUICount > 0 {
903+
successParts = append(successParts, fmt.Sprintf("UIs: %d", successUICount))
904+
}
905+
if successStandaloneCount > 0 {
906+
successParts = append(successParts, fmt.Sprintf("Standalones: %d", successStandaloneCount))
907+
}
908+
boxContent.WriteString(fmt.Sprintf("✓ Success: %s\n", strings.Join(successParts, " | ")))
832909
}
833910

834911
// Show fail counts
@@ -1030,6 +1107,12 @@ func (m buildModel) renderFinal() string {
10301107
errMsg = fmt.Sprintf(": %v", ts.result.Error)
10311108
}
10321109
b.WriteString(fmt.Sprintf("%s [%s] failed%s\n", ui.Error("✗"), ts.task.ResourceName, errMsg))
1110+
// Show build output for failed tasks (contains detailed error messages)
1111+
if ts.result != nil && ts.result.Output != "" {
1112+
b.WriteString(ui.Muted("Build output:\n"))
1113+
b.WriteString(ts.result.Output)
1114+
b.WriteString("\n")
1115+
}
10331116
default:
10341117
b.WriteString(fmt.Sprintf("○ [%s] skipped\n", ts.task.ResourceName))
10351118
}

internal/builder/embedded/build.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,51 @@ const path = require('path')
22
const { buildCore, buildResource, buildStandalone, copyResource } = require('./build_functions')
33
const { buildViews } = require('./views')
44

5+
/**
6+
* Check if a dependency is installed
7+
*/
8+
function checkDependency(name) {
9+
try {
10+
require.resolve(name)
11+
return true
12+
} catch (e) {
13+
return false
14+
}
15+
}
16+
17+
/**
18+
* Verify required base dependencies are installed
19+
*/
20+
function checkBaseDependencies() {
21+
const required = [
22+
{ name: 'esbuild', install: 'esbuild' },
23+
{ name: '@swc/core', install: '@swc/core' },
24+
]
25+
26+
const missing = required.filter(dep => !checkDependency(dep.name))
27+
28+
if (missing.length > 0) {
29+
const names = missing.map(d => d.name).join(', ')
30+
const installCmd = missing.map(d => d.install).join(' ')
31+
throw new Error(
32+
`\n` +
33+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
34+
` [build] Missing required dependencies\n` +
35+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
36+
`\n` +
37+
` The following dependencies are required but not installed:\n` +
38+
`\n` +
39+
` Missing: ${names}\n` +
40+
`\n` +
41+
` Run this command to install:\n` +
42+
`\n` +
43+
` pnpm add -D ${installCmd}\n` +
44+
`\n` +
45+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`
46+
)
47+
}
48+
}
49+
550
/**
651
* Build a single resource by type (called from Go CLI)
752
*/
@@ -38,6 +83,9 @@ async function main() {
3883
const options = args[4] ? JSON.parse(args[4]) : {}
3984

4085
try {
86+
// Check base dependencies before building
87+
checkBaseDependencies()
88+
4189
await buildSingle(type, resourcePath, outDir, options)
4290
console.log(JSON.stringify({ success: true }))
4391
} catch (error) {

internal/commands/update.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
)
1111

1212
func NewUpdateCommand() *cobra.Command {
13-
return &cobra.Command{
13+
var force bool
14+
15+
cmd := &cobra.Command{
1416
Use: "update",
1517
Short: "Update OpenCore CLI to the latest version",
1618
Run: func(cmd *cobra.Command, args []string) {
@@ -20,9 +22,13 @@ func NewUpdateCommand() *cobra.Command {
2022
version = "0.0.0"
2123
}
2224

23-
fmt.Println(ui.Info("Checking for updates..."))
25+
if force {
26+
fmt.Println(ui.Info("Forcing update check (ignoring cache)..."))
27+
} else {
28+
fmt.Println(ui.Info("Checking for updates..."))
29+
}
2430

25-
info, err := updater.CheckForUpdate(version)
31+
info, err := updater.CheckForUpdate(version, force)
2632
if err != nil {
2733
fmt.Println(ui.Error(fmt.Sprintf("Failed to check for updates: %v", err)))
2834
return
@@ -52,4 +58,8 @@ func NewUpdateCommand() *cobra.Command {
5258
fmt.Println(ui.Success(fmt.Sprintf("Successfully updated to %s!", info.LatestVersion)))
5359
},
5460
}
61+
62+
cmd.Flags().BoolVarP(&force, "force", "f", false, "Force update check, ignoring cache")
63+
64+
return cmd
5565
}

internal/updater/updater.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ type UpdateInfo struct {
3434
}
3535

3636
// CheckForUpdate checks if a new version is available on GitHub
37-
func CheckForUpdate(currentVersion string) (*UpdateInfo, error) {
38-
// 1. Check cache first
37+
// If force is true, the cache will be ignored
38+
func CheckForUpdate(currentVersion string, force bool) (*UpdateInfo, error) {
39+
// 1. Check cache first (unless force is true)
3940
cachePath, _ := getCachePath()
40-
if cachePath != "" {
41+
if !force && cachePath != "" {
4142
if data, err := os.ReadFile(cachePath); err == nil {
4243
var info UpdateInfo
4344
if err := json.Unmarshal(data, &info); err == nil {

main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

1414
var (
15-
version = "0.4.7"
15+
version = "0.4.8"
1616
)
1717

1818
func main() {
@@ -51,7 +51,7 @@ func main() {
5151

5252
// Check for updates in the background after command execution
5353
if len(os.Args) > 1 && os.Args[1] != "update" && os.Args[1] != "--version" && os.Args[1] != "-v" {
54-
if info, err := updater.CheckForUpdate(version); err == nil {
54+
if info, err := updater.CheckForUpdate(version, false); err == nil {
5555
if updater.NeedsUpdate(version, info.LatestVersion) {
5656
fmt.Println()
5757
fmt.Println(ui.Info(fmt.Sprintf("New version available: %s -> %s", version, info.LatestVersion)))

npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@open-core/cli",
3-
"version": "0.4.7",
3+
"version": "0.4.8",
44
"description": "Official CLI tool for OpenCore Framework",
55
"main": "index.js",
66
"types": "index.d.ts",

0 commit comments

Comments
 (0)