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:
parent
4d9c958fc9
commit
873bf78c22
5 changed files with 320 additions and 6 deletions
1
Makefile
1
Makefile
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
36
plugin.go
36
plugin.go
|
@ -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
|
||||
}
|
||||
|
|
204
plugin_test.go
204
plugin_test.go
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,11 @@ 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
|
||||
|
||||
|
@ -39,6 +44,7 @@ func RunUpdateAssetsCommand(args []string, pluginPath string) error {
|
|||
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
|
||||
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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue