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/*