Building Extensions

December 4, 2025 ยท View on GitHub

Praxis is designed to be extensible. This guide explains how to create plugins, custom generators, and integrations for the Praxis framework.

Extension Types

TypeDescriptionUse Case
PluginsExtend engine behaviorAdd middleware, effects, logging
GeneratorsCustom code generationGenerate for other frameworks
AdaptersExternal integrationsConnect to databases, APIs
TemplatesCustom project scaffoldingOrganization-specific setups
Canvas ExtensionsExtend visual editorCustom components, tools

Plugins

Creating a Plugin

Plugins can hook into the engine lifecycle:

import type { PraxisPlugin, PraxisState, PraxisEvent } from '@plures/praxis';

interface LoggerOptions {
  level: 'debug' | 'info' | 'warn' | 'error';
  prefix?: string;
}

export function createLoggerPlugin(options: LoggerOptions): PraxisPlugin {
  return {
    name: 'logger',

    // Called when engine is created
    onInit(engine) {
      console.log(`${options.prefix || ''} Engine initialized`);
    },

    // Called before each step
    beforeStep(state, events) {
      if (options.level === 'debug') {
        console.log('Events:', events);
      }
      return { state, events }; // Can modify
    },

    // Called after each step
    afterStep(result) {
      if (options.level === 'debug') {
        console.log('Facts:', result.state.facts);
      }
      return result; // Can modify
    },

    // Called on error
    onError(error, state, events) {
      console.error(`${options.prefix || ''} Error:`, error);
    },
  };
}

Using Plugins

import { createPraxisEngine, PraxisRegistry } from '@plures/praxis';
import { createLoggerPlugin } from './plugins/logger';
import { createPersistPlugin } from './plugins/persist';

const engine = createPraxisEngine({
  initialContext: { count: 0 },
  registry: new PraxisRegistry(),
  plugins: [
    createLoggerPlugin({ level: 'debug', prefix: '[MyApp]' }),
    createPersistPlugin({ storage: 'localStorage', key: 'app-state' }),
  ],
});

Plugin API

interface PraxisPlugin<TContext = any> {
  name: string;

  // Lifecycle hooks
  onInit?(engine: PraxisEngine<TContext>): void;
  onDestroy?(): void;

  // Step hooks
  beforeStep?(
    state: PraxisState<TContext>,
    events: PraxisEvent[]
  ): { state: PraxisState<TContext>; events: PraxisEvent[] };

  afterStep?(result: StepResult<TContext>): StepResult<TContext>;

  // Error handling
  onError?(error: Error, state: PraxisState<TContext>, events: PraxisEvent[]): void;

  // Subscription
  onStateChange?(state: PraxisState<TContext>): void;
}

Custom Generators

Creating a Generator

Generators transform PSF schemas into code:

import type { PSFSchema, Generator, GeneratorOutput } from '@plures/praxis/codegen';

interface ReactGeneratorOptions {
  typescript: boolean;
  styling: 'css' | 'styled-components' | 'tailwind';
}

export const reactGenerator: Generator<ReactGeneratorOptions> = {
  name: 'react',
  description: 'Generate React components from PSF schema',

  generate(schema: PSFSchema, options: ReactGeneratorOptions): GeneratorOutput[] {
    const outputs: GeneratorOutput[] = [];

    // Generate components
    for (const component of schema.components) {
      const code = generateReactComponent(component, options);
      outputs.push({
        path: `components/${component.name}.${options.typescript ? 'tsx' : 'jsx'}`,
        content: code,
        type: 'component',
      });
    }

    // Generate types (TypeScript only)
    if (options.typescript) {
      const types = generateTypes(schema);
      outputs.push({
        path: 'types.ts',
        content: types,
        type: 'types',
      });
    }

    return outputs;
  },
};

function generateReactComponent(component: ComponentDef, options: ReactGeneratorOptions): string {
  const { name, type, model, props } = component;

  return `
import React from 'react';
${options.typescript ? `import type { ${model} } from '../types';` : ''}

${options.typescript ? `interface ${name}Props {` : ''}
${props?.map((p) => `  ${p.name}${p.required ? '' : '?'}: ${p.type};`).join('\n')}
${options.typescript ? '}' : ''}

export function ${name}(${options.typescript ? `props: ${name}Props` : 'props'}) {
  return (
    <div className="${name.toLowerCase()}">
      {/* Generated component */}
    </div>
  );
}
`.trim();
}

Registering a Generator

import { registerGenerator } from '@plures/praxis/codegen';
import { reactGenerator } from './generators/react';

registerGenerator(reactGenerator);

// Use via CLI
// praxis generate --schema ./schema.psf.json --generator react

Generator Output Structure

interface GeneratorOutput {
  path: string; // Output file path
  content: string; // Generated code
  type: OutputType; // 'component' | 'types' | 'docs' | 'config'
  overwrite?: boolean; // Allow overwriting existing files
  format?: boolean; // Apply code formatting
}

Adapters

Creating a Database Adapter

import type { DatabaseAdapter, Collection, Query } from '@plures/praxis';

interface PostgresOptions {
  connectionString: string;
  ssl?: boolean;
}

export function createPostgresAdapter(options: PostgresOptions): DatabaseAdapter {
  let pool: Pool;

  return {
    name: 'postgres',

    async connect() {
      pool = new Pool({ connectionString: options.connectionString });
      await pool.connect();
    },

    async disconnect() {
      await pool.end();
    },

    async createCollection(name: string, schema: CollectionSchema) {
      const columns = schemaToColumns(schema);
      await pool.query(`CREATE TABLE IF NOT EXISTS ${name} (${columns})`);
    },

    async insert(collection: string, document: any) {
      const { columns, values, placeholders } = prepareInsert(document);
      const result = await pool.query(
        `INSERT INTO ${collection} (${columns}) VALUES (${placeholders}) RETURNING *`,
        values
      );
      return result.rows[0];
    },

    async find(collection: string, query: Query) {
      const { where, params } = queryToSQL(query);
      const result = await pool.query(`SELECT * FROM ${collection} ${where}`, params);
      return result.rows;
    },

    // ... other CRUD operations
  };
}

Using Adapters

import { createPluresDB } from '@plures/praxis';
import { createPostgresAdapter } from './adapters/postgres';

const db = createPluresDB({
  adapter: createPostgresAdapter({
    connectionString: process.env.DATABASE_URL,
  }),
  collections: ['users', 'posts'],
});

Templates

Creating a Project Template

Templates are directories with files that get copied and transformed:

templates/
  my-template/
    template.json           # Template configuration
    package.json.template   # Files with .template get variable substitution
    src/
      schema.psf.json.template
      index.ts
    README.md.template

template.json:

{
  "name": "my-template",
  "description": "My custom Praxis template",
  "version": "1.0.0",
  "variables": [
    {
      "name": "appName",
      "prompt": "Application name",
      "default": "my-app"
    },
    {
      "name": "author",
      "prompt": "Author name"
    }
  ],
  "postInstall": ["npm install", "npx praxis generate --schema ./src/schema.psf.json"]
}

package.json.template:

{
  "name": "{{appName}}",
  "version": "0.1.0",
  "author": "{{author}}",
  "dependencies": {
    "@plures/praxis": "^0.2.1"
  }
}

Registering Templates

# Global templates directory
~/.praxis/templates/my-template/

# Or in a package
npm install @myorg/praxis-template-enterprise

Using Custom Templates

praxis create app my-app --template @myorg/praxis-template-enterprise

Canvas Extensions

Creating a Canvas Extension

import type { CanvasExtension, CanvasContext } from '@plures/praxis/canvas';

export const customPaletteExtension: CanvasExtension = {
  name: 'custom-palette',

  // Add items to the component palette
  palette: {
    category: 'Custom',
    items: [
      {
        id: 'custom-chart',
        name: 'Chart Component',
        icon: '๐Ÿ“Š',
        createComponent: () => ({
          id: `comp_${Date.now()}`,
          name: 'Chart',
          type: 'display',
          description: 'A chart component',
        }),
      },
    ],
  },

  // Add toolbar buttons
  toolbar: [
    {
      id: 'export-figma',
      label: 'Export to Figma',
      icon: 'figma',
      action: async (context: CanvasContext) => {
        const schema = context.getSchema();
        await exportToFigma(schema);
      },
    },
  ],

  // Add context menu items
  contextMenu: [
    {
      id: 'duplicate-with-variants',
      label: 'Duplicate with Variants',
      applies: (selection) => selection.type === 'component',
      action: (context, selection) => {
        duplicateWithVariants(context, selection);
      },
    },
  ],
};

Registering Canvas Extensions

canvas.config.ts:

import { customPaletteExtension } from './extensions/custom-palette';

export const config: CanvasConfig = {
  extensions: [
    customPaletteExtension,
    '@plures/canvas-figma', // npm package
    './extensions/my-extension.ts', // local file
  ],
};

Publishing Extensions

Package Structure

my-praxis-extension/
  package.json
  src/
    index.ts      # Main entry point
    plugin.ts     # Plugin code
    generator.ts  # Generator code (if applicable)
  dist/           # Compiled output
  README.md

package.json:

{
  "name": "@myorg/praxis-extension-foo",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "keywords": ["praxis", "praxis-extension", "praxis-plugin"],
  "peerDependencies": {
    "@plures/praxis": "^0.2.0"
  },
  "praxis": {
    "type": "extension",
    "provides": ["plugin", "generator"]
  }
}

Publishing

npm publish --access public

Discovery

Extensions with praxis in keywords are discoverable:

npm search praxis-extension
praxis extensions list
praxis extensions install @myorg/praxis-extension-foo

Best Practices

1. Type Safety

Use TypeScript and export proper types:

export interface MyPluginOptions {
  // Document all options
  enabled: boolean;
  level?: 'basic' | 'advanced';
}

export function createMyPlugin(options: MyPluginOptions): PraxisPlugin {
  // Implementation
}

2. Error Handling

Handle errors gracefully:

export const myPlugin: PraxisPlugin = {
  name: 'my-plugin',

  beforeStep(state, events) {
    try {
      // Plugin logic
    } catch (error) {
      console.error('[my-plugin] Error:', error);
      // Don't break the engine
      return { state, events };
    }
  },
};

3. Documentation

Include comprehensive README:

# My Praxis Extension

## Installation

\`\`\`bash
npm install @myorg/praxis-extension-foo
\`\`\`

## Usage

\`\`\`typescript
import { createFooPlugin } from '@myorg/praxis-extension-foo';

const engine = createPraxisEngine({
plugins: [createFooPlugin({ option: 'value' })],
});
\`\`\`

## Options

| Option | Type   | Default   | Description |
| ------ | ------ | --------- | ----------- |
| option | string | 'default' | Description |

4. Testing

Test your extensions:

import { describe, it, expect } from 'vitest';
import { createMyPlugin } from './my-plugin';
import { createPraxisEngine, PraxisRegistry } from '@plures/praxis';

describe('MyPlugin', () => {
  it('should log on state change', () => {
    const logs: string[] = [];
    const plugin = createMyPlugin({
      onLog: (msg) => logs.push(msg),
    });

    const engine = createPraxisEngine({
      initialContext: {},
      registry: new PraxisRegistry(),
      plugins: [plugin],
    });

    engine.dispatch([{ tag: 'TEST', payload: {} }]);

    expect(logs).toContain('Step executed');
  });
});

Next: Tutorials