npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

flowlock-plugin-sdk

v0.10.0

Published

Plugin SDK for FlowLock

Readme

flowlock-plugin-sdk

Plugin SDK for extending FlowLock with custom validation checks.

Overview

The plugin SDK allows developers to create custom validation checks that integrate seamlessly with FlowLock's validation system. Plugins can add domain-specific validations, integrate with external systems, or enforce custom business rules.

Features

  • Simple Plugin API: Easy-to-use interface for creating custom checks
  • Full Spec Access: Access to complete UX specification for validation
  • Issue Reporting: Standardized issue format with severity levels
  • Auto-Fix Support: Provide automatic fixes for common issues
  • Async Support: Perform async operations like API calls
  • TypeScript Support: Full type safety with TypeScript

Installation

npm install flowlock-plugin-sdk

Creating a Plugin

Basic Plugin Structure

import { Plugin, PluginContext, Issue } from 'flowlock-plugin-sdk';

export const myPlugin: Plugin = {
  name: 'MY_CUSTOM_CHECK',
  description: 'Validates custom business rules',
  
  async run(context: PluginContext): Promise<Issue[]> {
    const { spec, options } = context;
    const issues: Issue[] = [];
    
    // Perform validation
    spec.entities.forEach(entity => {
      if (!entity.description) {
        issues.push({
          code: 'MISSING_DESCRIPTION',
          severity: 'warning',
          message: `Entity '${entity.id}' is missing a description`,
          path: `entities[${entity.id}]`
        });
      }
    });
    
    return issues;
  }
};

Plugin with Auto-Fix

import { Plugin, PluginContext, Issue } from 'flowlock-plugin-sdk';

export const autoFixPlugin: Plugin = {
  name: 'AUTO_FIX_EXAMPLE',
  description: 'Example plugin with auto-fix capability',
  
  async run(context: PluginContext): Promise<Issue[]> {
    const { spec, options } = context;
    const issues: Issue[] = [];
    
    spec.screens.forEach((screen, index) => {
      if (!screen.id) {
        issues.push({
          code: 'MISSING_SCREEN_ID',
          severity: 'error',
          message: `Screen '${screen.name}' is missing an ID`,
          path: `screens[${index}]`,
          fix: {
            description: 'Generate ID from name',
            apply: (spec) => {
              spec.screens[index].id = screen.name
                .toLowerCase()
                .replace(/\s+/g, '-');
              return spec;
            }
          }
        });
      }
    });
    
    return issues;
  }
};

Async Plugin with External Validation

import { Plugin, PluginContext, Issue } from 'flowlock-plugin-sdk';
import axios from 'axios';

export const apiValidatorPlugin: Plugin = {
  name: 'API_VALIDATOR',
  description: 'Validates API endpoints exist',
  
  async run(context: PluginContext): Promise<Issue[]> {
    const { spec, options } = context;
    const issues: Issue[] = [];
    const baseUrl = options.apiBaseUrl || 'http://localhost:3000';
    
    // Check each screen's route
    for (const screen of spec.screens) {
      if (screen.route) {
        try {
          // Convert route pattern to test URL
          const testUrl = `${baseUrl}${screen.route.replace(/:[^/]+/g, 'test')}`;
          await axios.head(testUrl);
        } catch (error) {
          issues.push({
            code: 'ROUTE_NOT_FOUND',
            severity: 'error',
            message: `Route '${screen.route}' for screen '${screen.id}' is not accessible`,
            path: `screens[${screen.id}].route`
          });
        }
      }
    }
    
    return issues;
  }
};

Plugin API

Plugin Interface

interface Plugin {
  name: string;              // Unique check name (UPPER_SNAKE_CASE)
  description: string;       // Human-readable description
  run: (context: PluginContext) => Promise<Issue[]>;
  enabled?: boolean;         // Default enabled state
  level?: 'basic' | 'enhanced' | 'strict'; // Validation level
}

PluginContext

interface PluginContext {
  spec: UXSpec;              // The UX specification to validate
  options: PluginOptions;    // Plugin-specific options
  helpers: PluginHelpers;    // Utility functions
}

interface PluginOptions {
  fix?: boolean;             // Enable auto-fix
  quiet?: boolean;           // Suppress output
  [key: string]: any;        // Custom options
}

interface PluginHelpers {
  // Get all entity fields including derived
  getEntityFields(entityId: string): Field[];
  
  // Check if field exists in entity
  hasField(entityId: string, fieldId: string): boolean;
  
  // Get all screens for a role
  getScreensForRole(role: string): Screen[];
  
  // Find flows that write to entity
  getFlowsWritingToEntity(entityId: string): Flow[];
  
  // Parse entity.field notation
  parseFieldReference(ref: string): { entity: string; field: string };
}

Issue Interface

interface Issue {
  code: string;              // Error code (UPPER_SNAKE_CASE)
  severity: 'error' | 'warning' | 'info';
  message: string;           // Human-readable message
  path?: string;             // JSON path to issue location
  fix?: {                    // Optional auto-fix
    description: string;     // What the fix does
    apply: (spec: UXSpec) => UXSpec; // Function to apply fix
  };
}

Registering Plugins

In Code

import { registerPlugin } from 'flowlock-plugin-sdk';
import { myPlugin } from './my-plugin';

// Register plugin
registerPlugin(myPlugin);

// Run checks with plugin
import { runChecks } from 'flowlock-checks-core';
const results = await runChecks(spec);

Via Configuration

Create a flowlock.config.json:

{
  "plugins": [
    "./plugins/my-plugin.js",
    "@company/flowlock-plugin-security",
    {
      "path": "./plugins/api-validator.js",
      "options": {
        "apiBaseUrl": "https://api.example.com"
      }
    }
  ]
}

Best Practices

1. Clear Error Messages

// Good
`Entity '${entity.id}' field '${field.id}' uses reserved name`

// Bad
`Invalid field name`

2. Appropriate Severity

  • Error: Blocks deployment, must be fixed
  • Warning: Should be fixed but not blocking
  • Info: Suggestions and best practices

3. Precise Path Information

// Good - specific path
path: `entities[2].fields[0].type`

// Bad - vague path
path: `entities`

4. Provide Fixes When Possible

fix: {
  description: 'Add missing required field',
  apply: (spec) => {
    spec.entities[index].fields.push({
      id: 'id',
      type: 'string',
      required: true
    });
    return spec;
  }
}

5. Handle Errors Gracefully

async run(context: PluginContext): Promise<Issue[]> {
  try {
    // Validation logic
    return issues;
  } catch (error) {
    return [{
      code: 'PLUGIN_ERROR',
      severity: 'error',
      message: `Plugin failed: ${error.message}`
    }];
  }
}

Example Plugins

Business Rule Validator

export const businessRulePlugin: Plugin = {
  name: 'BUSINESS_RULES',
  description: 'Validates company-specific business rules',
  
  async run(context: PluginContext): Promise<Issue[]> {
    const issues: Issue[] = [];
    const { spec } = context;
    
    // All customer-facing screens must have help text
    spec.screens
      .filter(s => s.role === 'customer')
      .forEach(screen => {
        if (!screen.helpText) {
          issues.push({
            code: 'MISSING_HELP_TEXT',
            severity: 'warning',
            message: `Customer screen '${screen.id}' needs help text`,
            path: `screens[${screen.id}].helpText`
          });
        }
      });
    
    // All forms must have validation rules
    spec.screens.forEach(screen => {
      screen.forms?.forEach(form => {
        if (!form.validation) {
          issues.push({
            code: 'MISSING_VALIDATION',
            severity: 'error',
            message: `Form '${form.id}' needs validation rules`,
            path: `screens[${screen.id}].forms[${form.id}].validation`
          });
        }
      });
    });
    
    return issues;
  }
};

Performance Validator

export const performancePlugin: Plugin = {
  name: 'PERFORMANCE',
  description: 'Validates performance best practices',
  
  async run(context: PluginContext): Promise<Issue[]> {
    const issues: Issue[] = [];
    const { spec } = context;
    
    // Warn about lists without pagination
    spec.screens.forEach(screen => {
      screen.lists?.forEach(list => {
        if (!list.paginated) {
          issues.push({
            code: 'UNPAGINATED_LIST',
            severity: 'warning',
            message: `List '${list.id}' should use pagination for performance`,
            path: `screens[${screen.id}].lists[${list.id}].paginated`,
            fix: {
              description: 'Enable pagination with default page size of 20',
              apply: (spec) => {
                const screenIndex = spec.screens.findIndex(s => s.id === screen.id);
                const listIndex = spec.screens[screenIndex].lists.findIndex(l => l.id === list.id);
                spec.screens[screenIndex].lists[listIndex].paginated = true;
                spec.screens[screenIndex].lists[listIndex].pageSize = 20;
                return spec;
              }
            }
          });
        }
      });
    });
    
    return issues;
  }
};

Testing Plugins

import { myPlugin } from './my-plugin';
import { createTestContext } from 'flowlock-plugin-sdk/testing';

describe('My Plugin', () => {
  it('should detect missing descriptions', async () => {
    const spec = {
      entities: [
        { id: 'User', fields: [] }
      ],
      screens: [],
      flows: []
    };
    
    const context = createTestContext(spec);
    const issues = await myPlugin.run(context);
    
    expect(issues).toHaveLength(1);
    expect(issues[0].code).toBe('MISSING_DESCRIPTION');
  });
});

Contributing

See the main repository for contribution guidelines.

License

MIT