-
Notifications
You must be signed in to change notification settings - Fork 1
✨ Add plugin architecture (VIZ-60) #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Implement extensible plugin system for CLI commands: - Auto-discover plugins from @vizzly-testing/* packages - Support explicit plugin configuration in vizzly.config.js - Plugin deduplication and validation - Shared context (config, logger, services) for plugins - Example plugin with 4 demo commands - Comprehensive documentation and tests Enables independent releases of integrations like @vizzly-testing/storybook while keeping core CLI lean. Plugins can register custom commands with full access to CLI infrastructure.
Code Review: Plugin Architecture ImplementationSummaryThis PR implements a well-designed plugin system that enables extensibility while maintaining security and backward compatibility. The implementation is solid with good test coverage and comprehensive documentation. ✅ StrengthsArchitecture & Design
Security
Testing
Documentation
🔍 Issues & SuggestionsCritical1. Race Condition in CLI Initialization ( Concern: If a plugin needs to modify service container behavior, it cannot do so because the container is already instantiated. Suggestion: Consider lazy-loading the service container or providing a way for plugins to hook into container initialization. High Priority2. Security: Insufficient Path Validation ( // Current check - only validates relative path
if (pluginRelativePath.startsWith('/') || pluginRelativePath.includes('..')) {
logger.warn(...);
continue;
}Issue: A malicious package could use symlinks or other filesystem tricks after this validation passes. Suggestion: Add validation of the final resolved path: let pluginPath = resolve(packageDir, pluginRelativePath);
// Ensure resolved path is still within package directory
if (!pluginPath.startsWith(packageDir)) {
logger.warn(`Plugin path escapes package directory: ${packageName}`);
continue;
}3. Missing Error Context in Plugin Loading ( Line 144-146: throw new Error(
`Failed to load plugin from ${pluginPath}: ${error.message}`
);Issue: If the error occurs during dynamic import execution (e.g., syntax error in plugin code), the stack trace is lost. Suggestion: Preserve the original error: let newError = new Error(`Failed to load plugin from ${pluginPath}: ${error.message}`);
newError.cause = error; // Node 16.9+ supports error.cause
throw newError;Medium Priority4. Plugin Version Conflicts Not Handled ( Issue: Silent version skipping could lead to unexpected behavior. A developer explicitly configuring a plugin might expect that version to be used. Suggestion: Add version conflict detection: if (loadedNames.has(plugin.name)) {
let existing = plugins.find(p => p.name === plugin.name);
logger.warn(
`Plugin ${plugin.name} already loaded (v${existing.version}), ` +
`skipping v${plugin.version} from config`
);
continue;
}5. Missing Async Validation in Register Function ( Suggestion: Add timeout protection: let registerPromise = plugin.register(program, { config, logger, services: container });
let timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Plugin registration timeout')), 5000)
);
await Promise.race([registerPromise, timeoutPromise]);6. Service Container Lifetime Issue ( Example: The Suggestion: Consider creating the service container per-command or making it more dynamic. Low Priority7. Glob Pattern Could Be More Efficient ( Minor optimization: let packageJsonPaths = await glob(
'node_modules/@vizzly-testing/*/package.json',
{
cwd: process.cwd(),
absolute: true,
ignore: ['**/node_modules/**'], // Prevent nested node_modules
}
);8. Package.json Parsing Error Handling ( Suggestion: Log at } catch (error) {
logger.warn(
`Failed to parse package.json at ${pkgPath}: ${error.message}`
);
}9. Test Cleanup Could Be More Robust ( Suggestion: Log cleanup failures in CI: } catch (error) {
if (process.env.CI) {
console.warn(`Failed to cleanup ${testDir}: ${error.message}`);
}
}10. Example Plugin Missing Error Handling ( Suggestion: Add try-catch for the import: try {
let { glob } = await import('glob');
} catch (error) {
logger.error('glob package not found. Run: npm install glob');
process.exit(1);
}🎯 Best PracticesPositive Patterns
Suggested Improvements
📊 Test Coverage AssessmentWell-Covered
Missing Coverage
🚀 Performance ConsiderationsCurrent: All plugins load synchronously during CLI startup. For 10+ plugins, this could add noticeable latency. Future optimization: Consider lazy-loading plugins when their commands are invoked: program
.command('storybook')
.action(async () => {
let plugin = await loadPlugin('@vizzly-testing/storybook');
await plugin.execute();
});📝 Documentation QualityThe documentation is excellent! The 376-line plugin guide covers:
Minor suggestion: Add a "Plugin Development Checklist" section for quick reference. 🎉 Overall AssessmentThis is a high-quality implementation that demonstrates strong software engineering practices:
The issues noted above are mostly preventive suggestions rather than critical bugs. The implementation is production-ready with the high-priority security fix applied. Recommendation: ✅ Approve with Minor ChangesRequired before merge:
Recommended for follow-up PR: Great work on this feature! The plugin system will significantly improve Vizzly's extensibility. 🎊 Generated with Claude Code - Review based on |
bd35d14 to
af0a2a0
Compare
- Add resolved path validation to prevent symlink attacks - Preserve error context using error.cause for better debugging - Add version conflict warnings when duplicate plugins detected - Upgrade package.json parse errors to warn level - Add 5-second timeout protection for plugin registration
4f7a6c5 to
5ec9d5c
Compare
- Add plugin architecture overview - Document plugin development pattern
Implements @vizzly-testing/storybook - a Vizzly CLI plugin that auto-discovers and captures screenshots of Storybook stories using Puppeteer. Core Features: - Auto-discovery from Storybook index.json (v6/v7/v8 support) - Multi-viewport screenshot capture - Functional architecture with pure functions - Global and per-story configuration - Interaction hooks (beforeScreenshot) - Pattern-based story filtering (include/exclude) - Parallel processing with configurable concurrency - Vizzly SDK integration for screenshot upload Package Structure: - Plugin registration via vizzly.plugin field in package.json - Functional modules: config, crawler, browser, screenshot, hooks - 52 passing tests with full coverage - CI/CD workflows for testing and releases - Comprehensive documentation with examples CI Improvements: - Added path-based conditional execution for client tests - Ruby and Storybook clients only test when their files change - Optimized workflow saves GitHub Actions minutes - Smart ci-check handles conditional job results
Summary
Implements a plugin architecture for the Vizzly CLI that enables packages like
@vizzly-testing/storybookto register custom commands while keeping the core CLI lean.Resolves https://github.com/vizzly-testing/vizzly/issues/80
What Changed
Core Implementation
src/plugin-loader.js) - Auto-discovers plugins from@vizzly-testing/*packages and loads explicit plugins from configsrc/cli.js) - Loads and registers plugins before command definitionssrc/utils/config-loader.js) - Addedpluginsfield with ESM default export handlingFeatures
✅ Zero-config auto-discovery - Just
npm install @vizzly-testing/plugin-nameand commands are available✅ Explicit configuration - Support for local/custom plugins via
vizzly.config.js✅ Deduplication - Prevents loading the same plugin twice
✅ Shared context - Plugins get access to config, logger, and services
✅ Security - Scope restriction to
@vizzly-testing/*and path validation✅ Graceful errors - Plugin failures don't crash the CLI
Documentation & Examples
docs/plugins.md) - Comprehensive documentation with examplesexamples/custom-plugin/) - Working demo with 4 commandsvizzly.config.jsTesting
tests/unit/plugin-loader.spec.js) - Coverage for discovery, loading, validationtests/integration/plugin-system.spec.js) - End-to-end plugin system testsUsage Example
Install a plugin:
Use immediately (auto-discovered):
vizzly storybook ./storybook-static vizzly --help # Shows new commands automaticallyOr configure explicitly:
Benefits
Test Plan
@vizzly-testing/*