Volt LogoVolt
Extension Development

Best Practices

Tips and guidelines for creating great Volt plugins

Plugin Best Practices

Build high-quality plugins

Follow these best practices to create performant, reliable, and user-friendly Volt plugins.

Performance

Implement Efficient canHandle()

The canHandle() method is called for every query. Keep it fast:

Avoid complex regex
canHandle(context: PluginContext): boolean {
  return /^(search|find|lookup)\s+.+$/i.test(context.query);
}
Use simple checks
canHandle(context: PluginContext): boolean {
  return context.query.startsWith('search ');
}

Use Debouncing

For plugins that make API calls, debounce requests to avoid overwhelming the API:

Debounced API calls
import { debounce } from "lodash";

class MyPlugin implements Plugin {
  private debouncedMatch = debounce(this.match.bind(this), 300);

  async match(context: PluginContext): Promise<PluginResult[]> {
    // Your implementation
    return await this.fetchResults(context.query);
  }
}

300ms is a good balance between responsiveness and API efficiency.

Implement Caching

Cache results to improve performance and reduce API calls:

Result caching
class MyPlugin implements Plugin {
  private cache = new Map<string, PluginResult[]>();
  private cacheTimeout = 5000; // 5 seconds

  async match(context: PluginContext): Promise<PluginResult[]> {
    const cached = this.cache.get(context.query);
    if (cached) return cached;

    const results = await this.fetchResults(context.query);
    this.cache.set(context.query, results);

    setTimeout(() => this.cache.delete(context.query), this.cacheTimeout);

    return results;
  }
}

Respect Timeouts

Frontend plugins have a 500ms timeout. Ensure your plugin responds quickly:

Timeout handling
async match(context: PluginContext): Promise<PluginResult[]> {
  const timeout = new Promise<PluginResult[]>((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 450)
  );

  const results = this.fetchResults(context.query);

  return Promise.race([results, timeout]).catch(() => []);
}

Always handle timeout errors gracefully to prevent crashes.

Error Handling

User Experience

Provide Clear Titles and Subtitles

Make results easy to understand at a glance:

Unclear result
{
  title: 'Result',
  subtitle: 'Click to open',
}
Clear and descriptive
{
  title: 'Search "React hooks" on Google',
  subtitle: 'Opens google.com in your browser',
}

Use Appropriate Icons

Choose icons that clearly represent your results:

Icon examples
// Use emojis for simplicity
icon: '🔍'  // For search
icon: '📊'  // For data/analytics
icon: '⚙️'  // For settings
icon: '📁'  // For files
icon: '🚀'  // For launch/execute

// Or custom icon URLs
icon: 'https://example.com/icon.png'

Emojis work great for quick prototyping. Use custom icons for professional plugins.

Provide Keyboard Shortcuts

Add keyboard shortcuts for common actions:

Keyboard shortcuts
{
  id: '1',
  title: 'Search on Google',
  subtitle: 'Press Enter to open',
  shortcut: 'Enter',
  action: () => window.open(url),
}

Show Loading States

Indicate when results are loading to provide feedback:

Loading indicator
async match(context: PluginContext): Promise<PluginResult[]> {
  // Return loading state immediately
  const loadingResult = {
    id: 'loading',
    title: 'Loading...',
    icon: '⏳',
    type: PluginResultType.Info,
  };

  // Fetch actual results
  const results = await this.fetchResults(context.query);
  return results.length > 0 ? results : [loadingResult];
}

Security

Security is critical

Always follow security best practices to protect users and their data.

Code Quality

Use TypeScript

Always use TypeScript for type safety and better IDE support:

Type-safe plugin
interface MyPluginConfig {
  apiKey: string;
  endpoint: string;
  timeout: number;
}

class MyPlugin implements Plugin {
  constructor(private config: MyPluginConfig) {}
}

TypeScript helps catch errors early and provides better autocomplete.

Follow Naming Conventions

Use consistent naming throughout your plugin:

Naming conventions
// Plugin ID: kebab-case
id = 'web-search';

// Class names: PascalCase
class WebSearchPlugin implements Plugin {}

// Methods: camelCase
async match(context: PluginContext) {}

// Constants: UPPER_SNAKE_CASE
const DEFAULT_TIMEOUT = 5000;

Document Your Code

Add JSDoc comments for better maintainability:

JSDoc comments
/**
 * Searches the web using multiple search engines
 * @param query - The search query
 * @returns Array of search results
 */
async search(query: string): Promise<SearchResult[]> {
  // Implementation
}

Keep Functions Small

Break down complex logic into smaller, focused functions:

Monolithic function
async match(context: PluginContext): Promise<PluginResult[]> {
  // 100 lines of code doing everything
}
Modular approach
async match(context: PluginContext): Promise<PluginResult[]> {
  const query = this.parseQuery(context.query);
  const results = await this.fetchResults(query);
  return this.formatResults(results);
}

Testing

Test your plugins

Comprehensive testing ensures reliability and prevents regressions.

Test Edge Cases

Test with unusual and boundary inputs:

Edge case testing
describe("CalculatorPlugin", () => {
  it("handles empty input", () => {
    const result = plugin.canHandle({ query: "" });
    expect(result).toBe(false);
  });

  it("handles invalid expressions", () => {
    const result = plugin.canHandle({ query: "1 / 0" });
    expect(result).toBe(true);
  });

  it("handles very long input", () => {
    const longQuery = "1+".repeat(1000) + "1";
    const result = plugin.canHandle({ query: longQuery });
    expect(result).toBeDefined();
  });
});

Mock External Dependencies

Use mocks for API calls to ensure consistent tests:

Mocking dependencies
import { vi } from "vitest";

vi.mock("@tauri-apps/api/core", () => ({
  invoke: vi.fn().mockResolvedValue({ data: [] }),
}));

// Test with mocked data
it("handles API responses", async () => {
  const results = await plugin.match({ query: "test" });
  expect(results).toHaveLength(0);
});

Test Performance

Ensure your plugin meets the 500ms timeout requirement:

Performance testing
it("responds within timeout", async () => {
  const start = Date.now();
  await plugin.match({ query: "test" });
  const duration = Date.now() - start;

  expect(duration).toBeLessThan(500);
});

Plugins exceeding 500ms will be terminated automatically.

Configuration

Accessibility

Make plugins accessible to everyone

Accessibility ensures all users can effectively use your plugin.

Documentation

Good documentation is essential

Well-documented plugins are easier to use, maintain, and contribute to.

Community

📬 Be Responsive

Respond to issues and pull requests promptly. Good communication builds trust.

🤝 Welcome Contributions

Make it easy for others to contribute with clear guidelines and a welcoming attitude.

💜 Follow Code of Conduct

Be respectful, inclusive, and professional in all interactions.

Contributing Example

CONTRIBUTING.md
## Contributing

We welcome contributions! Please:

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing`)
5. Open a Pull Request

Ready to build amazing plugins?

Following these best practices will help you create plugins that are fast, reliable, secure, and user-friendly. Happy coding! 🚀

On this page