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
|
# Check changes target
|
||||||
.PHONY: check-changes
|
.PHONY: check-changes
|
||||||
check-changes: lint test ## Check changes (lint, test)
|
check-changes: lint test ## Check changes (lint, test)
|
||||||
|
@echo "All checks passed!"
|
||||||
|
|
||||||
# CI target
|
# CI target
|
||||||
.PHONY: ci
|
.PHONY: ci
|
||||||
|
|
|
@ -125,6 +125,9 @@ func TestPrintPluginInfo(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Initialize logger for testing
|
||||||
|
InitLogger()
|
||||||
|
|
||||||
// Capture stdout
|
// Capture stdout
|
||||||
oldStdout := os.Stdout
|
oldStdout := os.Stdout
|
||||||
r, w, _ := os.Pipe()
|
r, w, _ := os.Pipe()
|
||||||
|
|
36
plugin.go
36
plugin.go
|
@ -11,6 +11,11 @@ import (
|
||||||
|
|
||||||
const PluginManifestName = "plugin.json"
|
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.
|
// LoadPluginManifest loads and parses the plugin.json file from the current directory.
|
||||||
func LoadPluginManifest() (*model.Manifest, error) {
|
func LoadPluginManifest() (*model.Manifest, error) {
|
||||||
return LoadPluginManifestFromPath(".")
|
return LoadPluginManifestFromPath(".")
|
||||||
|
@ -78,3 +83,34 @@ func GetEffectivePluginPath(flagPath string) string {
|
||||||
|
|
||||||
return cwd
|
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,13 +32,19 @@ func RunUpdateAssetsCommand(args []string, pluginPath string) error {
|
||||||
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
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)
|
hasWebapp := HasWebappCode(manifest)
|
||||||
updatedCount := 0
|
updatedCount := 0
|
||||||
|
|
||||||
config := AssetProcessorConfig{
|
config := AssetProcessorConfig{
|
||||||
pluginPath: pluginPath,
|
pluginPath: pluginPath,
|
||||||
hasWebapp: hasWebapp,
|
hasWebapp: hasWebapp,
|
||||||
updatedCount: &updatedCount,
|
updatedCount: &updatedCount,
|
||||||
|
pluginCtlConfig: pluginCtlConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = fs.WalkDir(assetsFS, "assets", func(path string, d fs.DirEntry, err error) error {
|
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
|
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 {
|
type AssetProcessorConfig struct {
|
||||||
pluginPath string
|
pluginPath string
|
||||||
hasWebapp bool
|
hasWebapp bool
|
||||||
updatedCount *int
|
updatedCount *int
|
||||||
|
pluginCtlConfig *PluginCtlConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProcessorConfig) error {
|
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
|
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)
|
targetPath := filepath.Join(config.pluginPath, relativePath)
|
||||||
|
|
||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue