Add custom parser for props["pluginctl"] with ignore assets support

- Add PluginCtlConfig struct with IgnoreAssets field for glob patterns
- Add ParsePluginCtlConfig function to parse manifest props["pluginctl"]
- Update updateassets command to respect ignore patterns with glob matching
- Add comprehensive logging when files are skipped due to ignore patterns
- Support patterns like *.test.js, build/, node_modules for flexible exclusion
- Add extensive tests for config parsing and path matching functionality
- Maintain backward compatibility with existing manifests
- Fix Makefile check-changes target and add logger init to tests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Felipe M 2025-07-14 17:38:38 +02:00
parent 4d9c958fc9
commit 873bf78c22
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
5 changed files with 320 additions and 6 deletions

View file

@ -141,6 +141,7 @@ dev: fmt lint snapshot ## Quick development build (fmt, lint, build)
# Check changes target
.PHONY: check-changes
check-changes: lint test ## Check changes (lint, test)
@echo "All checks passed!"
# CI target
.PHONY: ci

View file

@ -125,6 +125,9 @@ func TestPrintPluginInfo(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize logger for testing
InitLogger()
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()

View file

@ -11,6 +11,11 @@ import (
const PluginManifestName = "plugin.json"
// PluginCtlConfig represents the configuration for pluginctl stored in the manifest props.
type PluginCtlConfig struct {
IgnoreAssets []string `json:"ignore_assets,omitempty"`
}
// LoadPluginManifest loads and parses the plugin.json file from the current directory.
func LoadPluginManifest() (*model.Manifest, error) {
return LoadPluginManifestFromPath(".")
@ -78,3 +83,34 @@ func GetEffectivePluginPath(flagPath string) string {
return cwd
}
// ParsePluginCtlConfig extracts and parses the pluginctl configuration from the manifest props.
func ParsePluginCtlConfig(manifest *model.Manifest) (*PluginCtlConfig, error) {
// Default configuration
config := &PluginCtlConfig{
IgnoreAssets: []string{},
}
// Check if props exist
if manifest.Props == nil {
return config, nil
}
// Check if pluginctl config exists in props
pluginctlData, exists := manifest.Props["pluginctl"]
if !exists {
return config, nil
}
// Convert to JSON and parse
jsonData, err := json.Marshal(pluginctlData)
if err != nil {
return nil, fmt.Errorf("failed to marshal pluginctl config: %w", err)
}
if err := json.Unmarshal(jsonData, config); err != nil {
return nil, fmt.Errorf("failed to parse pluginctl config: %w", err)
}
return config, nil
}

View file

@ -402,3 +402,207 @@ func TestHasServerCodeAndWebappCode(t *testing.T) {
})
}
}
func TestParsePluginCtlConfig(t *testing.T) {
tests := []struct {
name string
manifest *model.Manifest
expectedConfig *PluginCtlConfig
expectError bool
errorContains string
}{
{
name: "Manifest with valid pluginctl config",
manifest: &model.Manifest{
Props: map[string]interface{}{
"pluginctl": map[string]interface{}{
"ignore_assets": []string{"*.test.js", "build/", "temp/**"},
},
},
},
expectedConfig: &PluginCtlConfig{
IgnoreAssets: []string{"*.test.js", "build/", "temp/**"},
},
expectError: false,
},
{
name: "Manifest with no props",
manifest: &model.Manifest{
Props: nil,
},
expectedConfig: &PluginCtlConfig{
IgnoreAssets: []string{},
},
expectError: false,
},
{
name: "Manifest with empty props",
manifest: &model.Manifest{
Props: map[string]interface{}{},
},
expectedConfig: &PluginCtlConfig{
IgnoreAssets: []string{},
},
expectError: false,
},
{
name: "Manifest with no pluginctl config",
manifest: &model.Manifest{
Props: map[string]interface{}{
"other": "value",
},
},
expectedConfig: &PluginCtlConfig{
IgnoreAssets: []string{},
},
expectError: false,
},
{
name: "Manifest with empty pluginctl config",
manifest: &model.Manifest{
Props: map[string]interface{}{
"pluginctl": map[string]interface{}{},
},
},
expectedConfig: &PluginCtlConfig{
IgnoreAssets: []string{},
},
expectError: false,
},
{
name: "Manifest with invalid pluginctl config",
manifest: &model.Manifest{
Props: map[string]interface{}{
"pluginctl": "invalid",
},
},
expectedConfig: nil,
expectError: true,
errorContains: "failed to parse pluginctl config",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := ParsePluginCtlConfig(tt.manifest)
if tt.expectError {
if err == nil {
t.Error("Expected error but got nil")
} else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
t.Errorf("Expected error to contain %q but got: %v", tt.errorContains, err)
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if config == nil {
t.Error("Expected config but got nil")
return
}
if len(config.IgnoreAssets) != len(tt.expectedConfig.IgnoreAssets) {
t.Errorf("Expected %d ignore assets but got %d", len(tt.expectedConfig.IgnoreAssets), len(config.IgnoreAssets))
return
}
for i, expected := range tt.expectedConfig.IgnoreAssets {
if config.IgnoreAssets[i] != expected {
t.Errorf("Expected ignore asset %d to be %q but got %q", i, expected, config.IgnoreAssets[i])
}
}
})
}
}
func TestIsPathIgnored(t *testing.T) {
tests := []struct {
name string
relativePath string
ignorePatterns []string
expectedIgnore bool
expectedPattern string
}{
{
name: "No ignore patterns",
relativePath: "webapp/dist/main.js",
ignorePatterns: []string{},
expectedIgnore: false,
expectedPattern: "",
},
{
name: "Direct file match",
relativePath: "test.js",
ignorePatterns: []string{"*.js"},
expectedIgnore: true,
expectedPattern: "*.js",
},
{
name: "Directory pattern with slash",
relativePath: "build/output.js",
ignorePatterns: []string{"build/"},
expectedIgnore: true,
expectedPattern: "build/",
},
{
name: "Directory pattern without slash",
relativePath: "build/output.js",
ignorePatterns: []string{"build"},
expectedIgnore: true,
expectedPattern: "build",
},
{
name: "Nested directory match",
relativePath: "webapp/dist/main.js",
ignorePatterns: []string{"dist"},
expectedIgnore: true,
expectedPattern: "dist",
},
{
name: "Multiple patterns - first match",
relativePath: "test.js",
ignorePatterns: []string{"*.js", "*.css"},
expectedIgnore: true,
expectedPattern: "*.js",
},
{
name: "Multiple patterns - second match",
relativePath: "style.css",
ignorePatterns: []string{"*.js", "*.css"},
expectedIgnore: true,
expectedPattern: "*.css",
},
{
name: "No match",
relativePath: "README.md",
ignorePatterns: []string{"*.js", "*.css"},
expectedIgnore: false,
expectedPattern: "",
},
{
name: "Complex path with match",
relativePath: "webapp/node_modules/package/file.js",
ignorePatterns: []string{"node_modules"},
expectedIgnore: true,
expectedPattern: "node_modules",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ignored, pattern := isPathIgnored(tt.relativePath, tt.ignorePatterns)
if ignored != tt.expectedIgnore {
t.Errorf("Expected ignore result %v but got %v", tt.expectedIgnore, ignored)
}
if pattern != tt.expectedPattern {
t.Errorf("Expected pattern %q but got %q", tt.expectedPattern, pattern)
}
})
}
}

View file

@ -32,13 +32,19 @@ func RunUpdateAssetsCommand(args []string, pluginPath string) error {
return fmt.Errorf("failed to load plugin manifest: %w", err)
}
pluginCtlConfig, err := ParsePluginCtlConfig(manifest)
if err != nil {
return fmt.Errorf("failed to parse pluginctl config: %w", err)
}
hasWebapp := HasWebappCode(manifest)
updatedCount := 0
config := AssetProcessorConfig{
pluginPath: pluginPath,
hasWebapp: hasWebapp,
updatedCount: &updatedCount,
pluginPath: pluginPath,
hasWebapp: hasWebapp,
updatedCount: &updatedCount,
pluginCtlConfig: pluginCtlConfig,
}
err = fs.WalkDir(assetsFS, "assets", func(path string, d fs.DirEntry, err error) error {
@ -54,10 +60,67 @@ func RunUpdateAssetsCommand(args []string, pluginPath string) error {
return nil
}
// isPathIgnored checks if a path matches any of the ignore patterns.
func isPathIgnored(relativePath string, ignorePatterns []string) (ignored bool, matchedPattern string) {
for _, pattern := range ignorePatterns {
// Direct file or path match
if matched, err := filepath.Match(pattern, relativePath); err == nil && matched {
return true, pattern
}
// Check if the path starts with the pattern (for directory patterns)
if strings.HasSuffix(pattern, "/") && strings.HasPrefix(relativePath, pattern) {
return true, pattern
}
// Check if any parent directory matches the pattern
if matchesParentDirectory(relativePath, pattern) {
return true, pattern
}
// Check if any directory component matches the pattern
if matchesDirectoryComponent(relativePath, pattern) {
return true, pattern
}
}
return false, ""
}
// matchesParentDirectory checks if any parent directory matches the pattern.
func matchesParentDirectory(relativePath, pattern string) bool {
dir := filepath.Dir(relativePath)
for dir != "." && dir != "/" {
if matched, err := filepath.Match(pattern, dir); err == nil && matched {
return true
}
// Also check direct string match for directory names
if filepath.Base(dir) == pattern {
return true
}
dir = filepath.Dir(dir)
}
return false
}
// matchesDirectoryComponent checks if any directory component matches the pattern.
func matchesDirectoryComponent(relativePath, pattern string) bool {
parts := strings.Split(relativePath, "/")
for _, part := range parts {
if matched, err := filepath.Match(pattern, part); err == nil && matched {
return true
}
}
return false
}
type AssetProcessorConfig struct {
pluginPath string
hasWebapp bool
updatedCount *int
pluginPath string
hasWebapp bool
updatedCount *int
pluginCtlConfig *PluginCtlConfig
}
func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProcessorConfig) error {
@ -75,6 +138,13 @@ func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProces
return nil
}
// Check if path is ignored by pluginctl config
if ignored, pattern := isPathIgnored(relativePath, config.pluginCtlConfig.IgnoreAssets); ignored {
Logger.Info("Skipping asset due to ignore pattern", "path", relativePath, "pattern", pattern)
return nil
}
targetPath := filepath.Join(config.pluginPath, relativePath)
if d.IsDir() {